mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-01-02 13:23:10 +00:00
Compare commits
1286 Commits
demo-versi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d09afd9e0 | ||
|
|
2d1ee4b279 | ||
|
|
f2235548da | ||
|
|
48d1d0e858 | ||
|
|
bece9a9108 | ||
|
|
f4a8429461 | ||
|
|
9d37a88069 | ||
|
|
de751952af | ||
|
|
e81d7f099d | ||
|
|
5d30de695f | ||
|
|
cecf39cecd | ||
|
|
28b64e87f5 | ||
|
|
2e82d2ddb8 | ||
|
|
e6a73a02b5 | ||
|
|
c6531aaab4 | ||
|
|
c25e93edd5 | ||
|
|
fcdd6e96a5 | ||
|
|
c5e640d56c | ||
|
|
56151e9d3d | ||
|
|
8289c07b28 | ||
|
|
a84057561e | ||
|
|
072cf5cd0f | ||
|
|
5baa759283 | ||
|
|
ca7cc10f7c | ||
|
|
c49a3afec7 | ||
|
|
fe3c11fd3e | ||
|
|
639c282c61 | ||
|
|
05d34bf9ec | ||
|
|
32de4e2990 | ||
|
|
0a1bbb9d21 | ||
|
|
22c60cb97f | ||
|
|
22f6d92ca6 | ||
|
|
c1a999c02e | ||
|
|
4f15237446 | ||
|
|
e351279278 | ||
|
|
c95c4b2f16 | ||
|
|
f6c787e15f | ||
|
|
c30ba4be1a | ||
|
|
9601e0053e | ||
|
|
9adc88f8c8 | ||
|
|
5a03ad431d | ||
|
|
3581c97424 | ||
|
|
9b37a2a8b7 | ||
|
|
d427d49c65 | ||
|
|
1b0dea11f6 | ||
|
|
a280551ee3 | ||
|
|
10b6cfcf93 | ||
|
|
6997a8da54 | ||
|
|
621b7c0bfa | ||
|
|
45670902d1 | ||
|
|
56f8df1d2b | ||
|
|
6075e03d06 | ||
|
|
172b13d03a | ||
|
|
9b729d6c3e | ||
|
|
d6d722c016 | ||
|
|
0ac7b5c702 | ||
|
|
dc38b8ea2d | ||
|
|
d425d70d4c | ||
|
|
ddf6e707ec | ||
|
|
d126dac303 | ||
|
|
081a366c16 | ||
|
|
87f45b8215 | ||
|
|
90a1ee994f | ||
|
|
44f6f44fa2 | ||
|
|
e73e4b0558 | ||
|
|
144c037114 | ||
|
|
7d12502542 | ||
|
|
a5496eb5d5 | ||
|
|
2b15bed3fa | ||
|
|
f729072fae | ||
|
|
ef97fade99 | ||
|
|
2975d9d878 | ||
|
|
c1283d4a0c | ||
|
|
cde11e75d6 | ||
|
|
8dc709a6e0 | ||
|
|
92b5dffec2 | ||
|
|
b3dca76b67 | ||
|
|
cd99318bf0 | ||
|
|
bbc02f0cf3 | ||
|
|
d4a471e948 | ||
|
|
e52e7d9ee9 | ||
|
|
c5cca4376e | ||
|
|
c3cd110688 | ||
|
|
d78d97c815 | ||
|
|
2419d4a112 | ||
|
|
8dd1c2d846 | ||
|
|
4389364b55 | ||
|
|
bb6cf322a0 | ||
|
|
93b616a434 | ||
|
|
1599bd41ce | ||
|
|
de1bf770f0 | ||
|
|
165e331aea | ||
|
|
5fe29016b7 | ||
|
|
3acc51ac87 | ||
|
|
e76800e283 | ||
|
|
aa81a311ac | ||
|
|
213e8dfb82 | ||
|
|
ae50ff65fa | ||
|
|
fac1dc19e2 | ||
|
|
30b4c8036e | ||
|
|
a360df29bf | ||
|
|
40bb718527 | ||
|
|
63f102643b | ||
|
|
77fd189010 | ||
|
|
89c1a97798 | ||
|
|
7453734dbe | ||
|
|
3e3ee9ba36 | ||
|
|
31405af143 | ||
|
|
5212d456ab | ||
|
|
9f443b92a8 | ||
|
|
bb58f7dd0a | ||
|
|
8c92a58bbc | ||
|
|
d019c26578 | ||
|
|
2d8722b7d0 | ||
|
|
0be88d4343 | ||
|
|
f77481f3b5 | ||
|
|
abb1fc5e45 | ||
|
|
7e8b8c7624 | ||
|
|
1b2962fa03 | ||
|
|
1b7afbecdc | ||
|
|
33ee1fc72e | ||
|
|
d7f0346671 | ||
|
|
5276cc8f07 | ||
|
|
e9c9058827 | ||
|
|
c6f0782d26 | ||
|
|
340c9c4ce6 | ||
|
|
9637b8b666 | ||
|
|
cd2430c2ee | ||
|
|
0ac5cbb35d | ||
|
|
45e3223d51 | ||
|
|
81316f6766 | ||
|
|
81393925d0 | ||
|
|
dc277b7e71 | ||
|
|
8e2b1c5993 | ||
|
|
1309185ecc | ||
|
|
f54cdf4a4c | ||
|
|
22440cf95d | ||
|
|
2e37e20014 | ||
|
|
70c228dbec | ||
|
|
471611f848 | ||
|
|
8c493a0155 | ||
|
|
cf4d7ba80b | ||
|
|
17f77f9ae7 | ||
|
|
1ff5de1443 | ||
|
|
79b4dc0333 | ||
|
|
ab6ea1f4c0 | ||
|
|
8cfb586bee | ||
|
|
31e7016948 | ||
|
|
4574acfc49 | ||
|
|
f6e2bcaad7 | ||
|
|
2518199be2 | ||
|
|
e5cbd82343 | ||
|
|
46ac451284 | ||
|
|
e0c6baf1ff | ||
|
|
e1eff07478 | ||
|
|
eed485bd2c | ||
|
|
47ff7f0b64 | ||
|
|
26fb822da1 | ||
|
|
d0064a4b0b | ||
|
|
5169e8fda4 | ||
|
|
6bc983d50b | ||
|
|
80a188e6a3 | ||
|
|
65681a5b74 | ||
|
|
ea19ad0c1f | ||
|
|
ddeeab7d80 | ||
|
|
f1760b67ee | ||
|
|
01f7acb4d6 | ||
|
|
40991cf6d1 | ||
|
|
beab5f9e96 | ||
|
|
ad7f773632 | ||
|
|
d77185ded4 | ||
|
|
4bcfbb9bee | ||
|
|
5a1d37d607 | ||
|
|
c6e68ff9b1 | ||
|
|
8506f5948f | ||
|
|
a11155e441 | ||
|
|
3a3c6f5507 | ||
|
|
106b33a57f | ||
|
|
ea27eeb929 | ||
|
|
1ae10f553b | ||
|
|
c6cde35203 | ||
|
|
7825f69e06 | ||
|
|
cc08e0a614 | ||
|
|
3e303f2c95 | ||
|
|
7e971a6c4d | ||
|
|
09116830c9 | ||
|
|
80d9ce8553 | ||
|
|
d2dc255bc5 | ||
|
|
f0066bc2aa | ||
|
|
29c3737704 | ||
|
|
97f2e93f8a | ||
|
|
b1ad51f5b9 | ||
|
|
7236a7074c | ||
|
|
925ae8d0c1 | ||
|
|
a84b18f22c | ||
|
|
b5589d53bb | ||
|
|
cf9c567e29 | ||
|
|
f1aed0b632 | ||
|
|
35bf943244 | ||
|
|
926a292c9c | ||
|
|
068bfa0ec5 | ||
|
|
686eb787c9 | ||
|
|
95ddea9fb0 | ||
|
|
fe83a20c4d | ||
|
|
2453ae095f | ||
|
|
1412ad4da4 | ||
|
|
03e911ecd5 | ||
|
|
6b26811229 | ||
|
|
4ed86ce182 | ||
|
|
3393c55768 | ||
|
|
ed949c07b1 | ||
|
|
44b4c53d04 | ||
|
|
91fe8ced6e | ||
|
|
8a269858c5 | ||
|
|
d677db7f4e | ||
|
|
91c898f19c | ||
|
|
282b932a8e | ||
|
|
78ce57e19b | ||
|
|
b81aafbc23 | ||
|
|
33264ba59a | ||
|
|
5affa4f9fd | ||
|
|
90d6580377 | ||
|
|
c0c1228e10 | ||
|
|
d16908d463 | ||
|
|
c360db9698 | ||
|
|
407c3f3c95 | ||
|
|
3e9f0f9384 | ||
|
|
863ed888ad | ||
|
|
93ee75a339 | ||
|
|
352b97910a | ||
|
|
0a645281c6 | ||
|
|
9da684b7c6 | ||
|
|
5547f0635c | ||
|
|
2166bbfe03 | ||
|
|
853d4d6b59 | ||
|
|
a426839b3b | ||
|
|
10d42f352b | ||
|
|
e8f1bca17e | ||
|
|
2d34925725 | ||
|
|
dcef017f9b | ||
|
|
468b39bc32 | ||
|
|
ce9cadc46d | ||
|
|
fdc53927ca | ||
|
|
3d529d19fa | ||
|
|
2fd4a37ee4 | ||
|
|
4fb6ba9530 | ||
|
|
04ab7444a4 | ||
|
|
df88d8bad6 | ||
|
|
4afbd65e3b | ||
|
|
0c44785a07 | ||
|
|
55fc4e9777 | ||
|
|
df64f8864f | ||
|
|
aee6f45a8b | ||
|
|
500e0862f3 | ||
|
|
ed38be57bb | ||
|
|
c937cb591e | ||
|
|
b1b7bc4463 | ||
|
|
38acb89b93 | ||
|
|
28c20f4d21 | ||
|
|
fa73a0a704 | ||
|
|
0f617a8601 | ||
|
|
5801073f84 | ||
|
|
2cf60fff10 | ||
|
|
a8abec196b | ||
|
|
777486ce2c | ||
|
|
41a833c258 | ||
|
|
c94d353b54 | ||
|
|
7ab44507f5 | ||
|
|
640ed18f90 | ||
|
|
bfbd50e8cb | ||
|
|
c7bcd20a38 | ||
|
|
ed00876800 | ||
|
|
dd5db5b32c | ||
|
|
1989fd25a1 | ||
|
|
c7ce31d00c | ||
|
|
46515ce695 | ||
|
|
64e2fb73a8 | ||
|
|
e61a971790 | ||
|
|
449ba3e3a9 | ||
|
|
3fbf1e1fec | ||
|
|
702882c6ee | ||
|
|
d82f06593d | ||
|
|
a3e07347a4 | ||
|
|
409ec19959 | ||
|
|
f46d7ee426 | ||
|
|
aba1d844f8 | ||
|
|
103332f6cd | ||
|
|
577fad6d5f | ||
|
|
6a507fdad1 | ||
|
|
055c5388e0 | ||
|
|
6c409b6fb1 | ||
|
|
a99aec05eb | ||
|
|
b26b972d3d | ||
|
|
00386eba24 | ||
|
|
bce0ac79cb | ||
|
|
72d389a7c3 | ||
|
|
d7331455fc | ||
|
|
d1d2292028 | ||
|
|
1df24eb11f | ||
|
|
bc24f006ef | ||
|
|
12e21c935c | ||
|
|
97209caa9c | ||
|
|
d78c83b7d3 | ||
|
|
8568302f38 | ||
|
|
c9b911836b | ||
|
|
30f19b245d | ||
|
|
77570c48e9 | ||
|
|
e9d7e224b0 | ||
|
|
fc531021fb | ||
|
|
0d11c3688e | ||
|
|
463942df80 | ||
|
|
4d1d765cbe | ||
|
|
a714e3c563 | ||
|
|
72d20a3546 | ||
|
|
1910d68430 | ||
|
|
819f96da6c | ||
|
|
ea9c659fb1 | ||
|
|
a3061afebe | ||
|
|
ba556bee0b | ||
|
|
58933a8270 | ||
|
|
3f636465f6 | ||
|
|
8e1c53bd4e | ||
|
|
425f0ccc1b | ||
|
|
3779372674 | ||
|
|
a0e1bdb963 | ||
|
|
893011e7bf | ||
|
|
4fc01afbaa | ||
|
|
46fad3c5e7 | ||
|
|
adcf70da76 | ||
|
|
8af6365c5d | ||
|
|
8c9b46b0df | ||
|
|
0054ce5847 | ||
|
|
d73fcbd2b3 | ||
|
|
47a7656ec4 | ||
|
|
386d958c88 | ||
|
|
bfe38b012e | ||
|
|
b59cd0da92 | ||
|
|
3355d23218 | ||
|
|
dc57f6b027 | ||
|
|
05e01f3a76 | ||
|
|
effbfac7d9 | ||
|
|
8839edc377 | ||
|
|
e3ce522588 | ||
|
|
110589d332 | ||
|
|
95ec42bba7 | ||
|
|
d15d208ad3 | ||
|
|
c1fabb2181 | ||
|
|
6280110c9f | ||
|
|
b90837edb6 | ||
|
|
1214bf6e7e | ||
|
|
e3d095464c | ||
|
|
4847c84347 | ||
|
|
899b1b79a5 | ||
|
|
2f07853975 | ||
|
|
e44c0a8250 | ||
|
|
7ace27c4f9 | ||
|
|
30bcd20ac5 | ||
|
|
ef73336aa5 | ||
|
|
4f650e939f | ||
|
|
38490a6163 | ||
|
|
fd4ebde1fb | ||
|
|
10731e326d | ||
|
|
20a31bd364 | ||
|
|
0ad6d290ae | ||
|
|
f2fb98608a | ||
|
|
521ba5adbd | ||
|
|
ee47d98300 | ||
|
|
e707b1b74f | ||
|
|
0ea46f5048 | ||
|
|
c7b415b2f4 | ||
|
|
a94440fa1f | ||
|
|
2e582e7874 | ||
|
|
e92ad2132f | ||
|
|
6cbc5028cf | ||
|
|
fcb3993a47 | ||
|
|
a8462ab5a3 | ||
|
|
ec149d3227 | ||
|
|
20c276e63e | ||
|
|
ef7a5b6610 | ||
|
|
ffe2ae4e0b | ||
|
|
d69e8a292e | ||
|
|
a048fad7cd | ||
|
|
d9a130cc55 | ||
|
|
dfd45c8678 | ||
|
|
4a430622f4 | ||
|
|
af1e129b6b | ||
|
|
9788f189b1 | ||
|
|
5e22357f9a | ||
|
|
51c1b7f31f | ||
|
|
883fdc583a | ||
|
|
343f8c4864 | ||
|
|
2920953184 | ||
|
|
f593e6be94 | ||
|
|
9d31b143ed | ||
|
|
4be1977925 | ||
|
|
aa36cc09fa | ||
|
|
f635f07ebd | ||
|
|
18dca9407c | ||
|
|
e40d387475 | ||
|
|
12974f6f6b | ||
|
|
228f7591d5 | ||
|
|
0fb72e452f | ||
|
|
2c21abc2d9 | ||
|
|
c2b8459645 | ||
|
|
48fc643952 | ||
|
|
3a27719392 | ||
|
|
813868f16f | ||
|
|
a903c221db | ||
|
|
6d9d6b3d28 | ||
|
|
e96bbb84a4 | ||
|
|
719a8dcafe | ||
|
|
ad9ad8d1d3 | ||
|
|
43cf732ad4 | ||
|
|
97eb110f0e | ||
|
|
4a23accbde | ||
|
|
e41ffeac68 | ||
|
|
785ed5f169 | ||
|
|
39f48cd74e | ||
|
|
5840f9b779 | ||
|
|
b3e4b9a8ca | ||
|
|
5b3732d9f2 | ||
|
|
27bb5bbb0f | ||
|
|
60b7c58af8 | ||
|
|
bf25c92b30 | ||
|
|
6b75609012 | ||
|
|
1ad93c2977 | ||
|
|
62668161b2 | ||
|
|
a14b15d123 | ||
|
|
7f075fcdd3 | ||
|
|
4648f46f51 | ||
|
|
c47693b9b0 | ||
|
|
66ee0c5449 | ||
|
|
0384efc38f | ||
|
|
a12d3cc354 | ||
|
|
13232200c0 | ||
|
|
5c6c7d1c3e | ||
|
|
4e36ae4679 | ||
|
|
f96f4d9117 | ||
|
|
d7089eac96 | ||
|
|
4ecd0430b8 | ||
|
|
31783d9b78 | ||
|
|
8ced04ae97 | ||
|
|
32b7780ff3 | ||
|
|
d99fbde101 | ||
|
|
37360e6d8c | ||
|
|
941738c26f | ||
|
|
a01bfe3688 | ||
|
|
3b3493df94 | ||
|
|
bdb0c576a3 | ||
|
|
04f7e67384 | ||
|
|
5b7e162e4d | ||
|
|
bbab0399cd | ||
|
|
1e3f7038ee | ||
|
|
2da861c2d2 | ||
|
|
17e87a5f53 | ||
|
|
e3a498faf0 | ||
|
|
86bbbcdda8 | ||
|
|
fd7907f893 | ||
|
|
204e5444c8 | ||
|
|
2c138cb027 | ||
|
|
348156043a | ||
|
|
79606f81ed | ||
|
|
1ded99c7de | ||
|
|
28c1fd39a4 | ||
|
|
ff448cbb3c | ||
|
|
833d6bf932 | ||
|
|
e5a6acf28d | ||
|
|
7724b7a2db | ||
|
|
e1085e1907 | ||
|
|
6447f8fbf7 | ||
|
|
fd0e783176 | ||
|
|
93fd485622 | ||
|
|
7d85424948 | ||
|
|
b93c0aa390 | ||
|
|
217551f960 | ||
|
|
9e03507dd4 | ||
|
|
b3822c4b75 | ||
|
|
c3ca1c4804 | ||
|
|
f669f10d9e | ||
|
|
983865d31f | ||
|
|
e0fd94152a | ||
|
|
0b44afa4f6 | ||
|
|
323f58d566 | ||
|
|
25ecfa733f | ||
|
|
6b18168c3c | ||
|
|
6157e3023b | ||
|
|
19215ee009 | ||
|
|
f3fbae66b5 | ||
|
|
ba20728f40 | ||
|
|
9614c3143b | ||
|
|
942e90ae2a | ||
|
|
bfde33d78d | ||
|
|
da28f3317b | ||
|
|
28fe9e2113 | ||
|
|
5f00c89c0a | ||
|
|
5010c6c370 | ||
|
|
8e3fb2f67b | ||
|
|
1428dc4a90 | ||
|
|
88446b17f9 | ||
|
|
54c54199d7 | ||
|
|
c39c9ad4ca | ||
|
|
4b7319afe9 | ||
|
|
d05093f84f | ||
|
|
d556c9b699 | ||
|
|
d4a8cbd8d8 | ||
|
|
9559fb4bf0 | ||
|
|
fb4bb02f1a | ||
|
|
a2b9aef152 | ||
|
|
c772f1b0fe | ||
|
|
6274addad9 | ||
|
|
cf9d296d08 | ||
|
|
ba35fafad4 | ||
|
|
898c5e9bcf | ||
|
|
577171bc5c | ||
|
|
a205cc1505 | ||
|
|
35806f1036 | ||
|
|
d7240e073a | ||
|
|
766d72bd75 | ||
|
|
f200f39779 | ||
|
|
69b610269b | ||
|
|
7213da587c | ||
|
|
0635bd201d | ||
|
|
359e545b69 | ||
|
|
e8f660c2c7 | ||
|
|
4c45fa7768 | ||
|
|
566df336f5 | ||
|
|
a071340564 | ||
|
|
667ad5a532 | ||
|
|
1645dacd1f | ||
|
|
ed9682bf58 | ||
|
|
e101a9311c | ||
|
|
e041b57807 | ||
|
|
4bb8bc94ff | ||
|
|
cbbc3cfb9e | ||
|
|
33e3db9f19 | ||
|
|
bc01fe0dc7 | ||
|
|
468b669c14 | ||
|
|
b2e51990c9 | ||
|
|
4e51a9c778 | ||
|
|
63436955c6 | ||
|
|
8b70da517c | ||
|
|
f8153c6fb9 | ||
|
|
313425f6f8 | ||
|
|
8f874a6b4f | ||
|
|
b6e7019588 | ||
|
|
46f899ac95 | ||
|
|
9141fbf06c | ||
|
|
d54ea96bba | ||
|
|
e69fb8b1b8 | ||
|
|
ddc127f591 | ||
|
|
aa8b138945 | ||
|
|
86f61e5ac9 | ||
|
|
a00773d5aa | ||
|
|
6070213232 | ||
|
|
4bd5494370 | ||
|
|
64048ccf1d | ||
|
|
dfab1d6fa8 | ||
|
|
23afa1e05a | ||
|
|
f707d3893d | ||
|
|
f49d79d0e4 | ||
|
|
fea132ef24 | ||
|
|
36beca9c7c | ||
|
|
01638ad7be | ||
|
|
c3ae7ab0a2 | ||
|
|
8d9e7764aa | ||
|
|
3b99bf60c7 | ||
|
|
cc927d2e43 | ||
|
|
bedcccb633 | ||
|
|
4a755a0c23 | ||
|
|
c58ef27224 | ||
|
|
1cdec1e05f | ||
|
|
6f2a193a50 | ||
|
|
49b2069f0f | ||
|
|
2ce773705d | ||
|
|
0e5087ca8f | ||
|
|
ae0c589d1e | ||
|
|
21e68b6b4a | ||
|
|
a7c60a56b7 | ||
|
|
d7c577061e | ||
|
|
bc5b9478df | ||
|
|
476d5a9b14 | ||
|
|
0d68bec358 | ||
|
|
3d704bd321 | ||
|
|
e0f5ac9e7c | ||
|
|
d3742867ed | ||
|
|
6fda3d84dd | ||
|
|
f5f5ab4ef1 | ||
|
|
2de7e49eeb | ||
|
|
fcebd5f726 | ||
|
|
c204915e1d | ||
|
|
6114ff0d3c | ||
|
|
aded05f493 | ||
|
|
76d374a453 | ||
|
|
4f67ff0c35 | ||
|
|
cca835e016 | ||
|
|
ec55e00049 | ||
|
|
b7c2e8a3ec | ||
|
|
0e720dd9d3 | ||
|
|
f1de182ec6 | ||
|
|
2178a3a408 | ||
|
|
0e81035039 | ||
|
|
f75fab89b0 | ||
|
|
9336a6c130 | ||
|
|
85a16a2f04 | ||
|
|
931b8c7176 | ||
|
|
f83fca56d0 | ||
|
|
9aab707ec7 | ||
|
|
473a5fd98b | ||
|
|
5979d8d0cf | ||
|
|
24f3952c49 | ||
|
|
afc977e044 | ||
|
|
856114019e | ||
|
|
b9e0ff230f | ||
|
|
29914b3220 | ||
|
|
37b4d0d6e2 | ||
|
|
bfbb5e2870 | ||
|
|
32910e76e3 | ||
|
|
c3b2f4691b | ||
|
|
3d240c72f8 | ||
|
|
80505f0440 | ||
|
|
cee882502c | ||
|
|
bf95a4112e | ||
|
|
08526d47bf | ||
|
|
75b7fdd069 | ||
|
|
ae70baf5c4 | ||
|
|
37b5aec264 | ||
|
|
2d22b170d4 | ||
|
|
d796d7baf1 | ||
|
|
d07b813739 | ||
|
|
93fcbb8f61 | ||
|
|
a11f372091 | ||
|
|
eb9c45bbb7 | ||
|
|
d91b07a785 | ||
|
|
2c2c4fed76 | ||
|
|
f31bbe4876 | ||
|
|
d81fb2665f | ||
|
|
e12fe4492b | ||
|
|
e8ace6838f | ||
|
|
854d96af72 | ||
|
|
3a9d9af815 | ||
|
|
65dfbd8bbb | ||
|
|
4a3af34c6a | ||
|
|
6552e6d015 | ||
|
|
d63cde85b9 | ||
|
|
ef32f7179e | ||
|
|
54590f8157 | ||
|
|
125fac770a | ||
|
|
95032e8e06 | ||
|
|
d24969387c | ||
|
|
23e79d3e17 | ||
|
|
4837413989 | ||
|
|
a5123992c9 | ||
|
|
33783e06d8 | ||
|
|
0e1905ad22 | ||
|
|
5747070e39 | ||
|
|
8ceb27421e | ||
|
|
e2d596be20 | ||
|
|
a432019b23 | ||
|
|
df2026f107 | ||
|
|
c055941738 | ||
|
|
4a0a99e264 | ||
|
|
4ae2af21a5 | ||
|
|
49019eaee2 | ||
|
|
41f7ea654e | ||
|
|
5dea03027a | ||
|
|
26acdbed61 | ||
|
|
1e7c3e1555 | ||
|
|
73c11600ba | ||
|
|
6e7520bfea | ||
|
|
45b31d304c | ||
|
|
5eeb925384 | ||
|
|
4e122abbce | ||
|
|
324f477b63 | ||
|
|
e2494467ea | ||
|
|
4a44b12384 | ||
|
|
ebe616247f | ||
|
|
cab8153201 | ||
|
|
b1d3b71df7 | ||
|
|
57045fa8e1 | ||
|
|
00c76df04b | ||
|
|
d402cfca52 | ||
|
|
762a0d760e | ||
|
|
7052bccfbc | ||
|
|
ee91d7d79c | ||
|
|
1d627ce5b9 | ||
|
|
efb7108c58 | ||
|
|
63517b6b1c | ||
|
|
83542b310c | ||
|
|
9b4506de3b | ||
|
|
e07733e5c3 | ||
|
|
82f4df638e | ||
|
|
55d207ac9a | ||
|
|
b4f21b2f09 | ||
|
|
4f9ee9f19f | ||
|
|
82070f9918 | ||
|
|
f08ed17509 | ||
|
|
a1e4c06bc7 | ||
|
|
f3d63806c3 | ||
|
|
5190b486c4 | ||
|
|
ea426d6650 | ||
|
|
c297186aee | ||
|
|
570a516cbf | ||
|
|
4ac2196ddf | ||
|
|
348ec65059 | ||
|
|
2bcee59d39 | ||
|
|
6efac43210 | ||
|
|
211b6f9115 | ||
|
|
fbcc9cf3e2 | ||
|
|
6967c0c22f | ||
|
|
4363a16a62 | ||
|
|
7c42b3de10 | ||
|
|
5de28e999e | ||
|
|
9993590d45 | ||
|
|
786d0b2899 | ||
|
|
5c3418ca8e | ||
|
|
22cdce5102 | ||
|
|
ca131b2c26 | ||
|
|
512d96d384 | ||
|
|
a36592485c | ||
|
|
87577726ec | ||
|
|
5bd45b685f | ||
|
|
8dbb636a14 | ||
|
|
0a3f093d60 | ||
|
|
66b5efaacb | ||
|
|
fd74216249 | ||
|
|
e508547914 | ||
|
|
f47692773c | ||
|
|
562480225c | ||
|
|
ab35d37766 | ||
|
|
d623812c24 | ||
|
|
20897596b0 | ||
|
|
9ff6a5bc85 | ||
|
|
2e371300d3 | ||
|
|
f2a7c574e5 | ||
|
|
96ca181f2d | ||
|
|
d00c551027 | ||
|
|
75211903d7 | ||
|
|
f7e1b85091 | ||
|
|
fedf63b759 | ||
|
|
1d2b0bbed7 | ||
|
|
41af90e2eb | ||
|
|
1ca3d68d7e | ||
|
|
02ad6129d6 | ||
|
|
79727f0195 | ||
|
|
d53edf7b61 | ||
|
|
48a70cb6e5 | ||
|
|
9b7c2587fe | ||
|
|
b6cabe8fb8 | ||
|
|
a5dc01d85e | ||
|
|
8a0e2d780a | ||
|
|
914cbfb9dc | ||
|
|
6d56ee51db | ||
|
|
0554500f2f | ||
|
|
e9fbce2106 | ||
|
|
096a404859 | ||
|
|
44a4f2f9f3 | ||
|
|
b3f6b2756e | ||
|
|
499b23c335 | ||
|
|
c41eb6b517 | ||
|
|
612a67f5ed | ||
|
|
6547d0efb4 | ||
|
|
0c7456d7a0 | ||
|
|
0abb23e26a | ||
|
|
15e2c131b2 | ||
|
|
ceba630cff | ||
|
|
8239855e88 | ||
|
|
538bb72556 | ||
|
|
769e372e8f | ||
|
|
80620b6aac | ||
|
|
d2ab8dd4d4 | ||
|
|
f905e79f4c | ||
|
|
562fe2e5e6 | ||
|
|
330d79379f | ||
|
|
a185e52203 | ||
|
|
1e1ab787bc | ||
|
|
e617cac69c | ||
|
|
2813d536bb | ||
|
|
acbde736f0 | ||
|
|
b20a97e5a1 | ||
|
|
4268830d18 | ||
|
|
1cdf058938 | ||
|
|
35b636a27d | ||
|
|
97f69f9498 | ||
|
|
1a10dade25 | ||
|
|
507988832f | ||
|
|
a694e705ea | ||
|
|
d1ebb831ef | ||
|
|
c5a4e83e3e | ||
|
|
035f950229 | ||
|
|
35ffb65df0 | ||
|
|
74f0c983d3 | ||
|
|
fe06a22589 | ||
|
|
6cada5bd09 | ||
|
|
e589ddae5a | ||
|
|
9d8c74ec1e | ||
|
|
d7acffd8da | ||
|
|
a9d81b19ed | ||
|
|
494cc7fb1e | ||
|
|
fed2d50a9a | ||
|
|
939553cc85 | ||
|
|
490ac908b8 | ||
|
|
df78d5f162 | ||
|
|
b22de50107 | ||
|
|
63cca0f30f | ||
|
|
4bdd2c6d9f | ||
|
|
2c0c417c22 | ||
|
|
504eb00819 | ||
|
|
acaf62f31f | ||
|
|
24a3165634 | ||
|
|
d0a1cd5fa0 | ||
|
|
d3827fc823 | ||
|
|
2d9896f6ed | ||
|
|
0dcdb2188a | ||
|
|
b2f79e2965 | ||
|
|
0b2650127a | ||
|
|
2884fdd00a | ||
|
|
82818db873 | ||
|
|
066aa8f3f7 | ||
|
|
927254f6ce | ||
|
|
e8e565fe8c | ||
|
|
82907f5d8f | ||
|
|
bab1112251 | ||
|
|
6bced51764 | ||
|
|
a5d0c27d6b | ||
|
|
5bc1c9c688 | ||
|
|
703a127f0e | ||
|
|
54dd9aa814 | ||
|
|
f32d7a7daf | ||
|
|
96b256bd4c | ||
|
|
4581dcc1d1 | ||
|
|
a3444f0c5c | ||
|
|
b5011d6b1d | ||
|
|
8727dabe27 | ||
|
|
56ac4ab982 | ||
|
|
c3b5d62ea6 | ||
|
|
43058193df | ||
|
|
9d19183786 | ||
|
|
102d2e13f9 | ||
|
|
48fe6f2740 | ||
|
|
15ca5ad4ec | ||
|
|
dacf880b88 | ||
|
|
f86719f54c | ||
|
|
ad47f48a79 | ||
|
|
3594c846da | ||
|
|
a3b64e4021 | ||
|
|
ecdb4ba130 | ||
|
|
bfdf039ef3 | ||
|
|
4f7bde100c | ||
|
|
6030e54cd7 | ||
|
|
51d8ac47cf | ||
|
|
55e241dc97 | ||
|
|
04f6474799 | ||
|
|
7d23983309 | ||
|
|
00773d7457 | ||
|
|
fcb90f6f11 | ||
|
|
3188f98117 | ||
|
|
20a7dad9a0 | ||
|
|
13cc82e3ed | ||
|
|
22c16c1ec9 | ||
|
|
002ad048b0 | ||
|
|
3dfcb47534 | ||
|
|
dbb276e470 | ||
|
|
ef6af9a967 | ||
|
|
c0318de646 | ||
|
|
99640bd70c | ||
|
|
ec909a1625 | ||
|
|
ae9963aec3 | ||
|
|
17b6851d00 | ||
|
|
aba8f3549f | ||
|
|
a06af6da0a | ||
|
|
c5097ab879 | ||
|
|
7be870369c | ||
|
|
4f95cef08f | ||
|
|
6565e0af18 | ||
|
|
ebec84b3c0 | ||
|
|
2657e64df8 | ||
|
|
418ce51805 | ||
|
|
c17160602c | ||
|
|
5d42e53b18 | ||
|
|
7f97340667 | ||
|
|
f5d9990cc8 | ||
|
|
7a6a958179 | ||
|
|
aa142eed07 | ||
|
|
6df4a04f84 | ||
|
|
28317b6227 | ||
|
|
18489be4d1 | ||
|
|
c6fb383adf | ||
|
|
e9b11af986 | ||
|
|
6a38c2eaa2 | ||
|
|
a1d53ee6f0 | ||
|
|
6ed4369647 | ||
|
|
4300e87485 | ||
|
|
76a023c33a | ||
|
|
966a5c9a04 | ||
|
|
72b5035733 | ||
|
|
e72df20b60 | ||
|
|
5d7d6278ee | ||
|
|
f21c0df652 | ||
|
|
484c22b649 | ||
|
|
70b31856e9 | ||
|
|
d181ce73e6 | ||
|
|
c546af7865 | ||
|
|
6d947710d1 | ||
|
|
e0b1ec165c | ||
|
|
7695e8e46e | ||
|
|
3de86d975e | ||
|
|
d232cecbb0 | ||
|
|
00f1490eff | ||
|
|
494ff2ac71 | ||
|
|
41b65521f9 | ||
|
|
eaaf485325 | ||
|
|
8a31522444 | ||
|
|
6ede7eac16 | ||
|
|
95fec47897 | ||
|
|
88872f31dd | ||
|
|
ef0b659ad1 | ||
|
|
af6608585c | ||
|
|
262531799b | ||
|
|
645429d8d8 | ||
|
|
872986926e | ||
|
|
d9ced25e98 | ||
|
|
128b58127b | ||
|
|
dfb28abbe7 | ||
|
|
8604a672bd | ||
|
|
0f82c0ec05 | ||
|
|
5b2af29efd | ||
|
|
bf139aa634 | ||
|
|
db5815ec09 | ||
|
|
693aa0c9f7 | ||
|
|
3e54156abf | ||
|
|
5dd7d25762 | ||
|
|
98631721fa | ||
|
|
c9574b0d09 | ||
|
|
1afb77e63e | ||
|
|
13536e69af | ||
|
|
9d85ea234e | ||
|
|
9f8724ee4c | ||
|
|
cab71a0e00 | ||
|
|
d1cd653ed5 | ||
|
|
f3aaacb6ca | ||
|
|
3eee533a17 | ||
|
|
5de28c062e | ||
|
|
71fdce8050 | ||
|
|
361e3b6f76 | ||
|
|
b3323c78cf | ||
|
|
a6cb55221b | ||
|
|
39b4c866a1 | ||
|
|
4580812b64 | ||
|
|
5e3476673e | ||
|
|
ca3a63a885 | ||
|
|
59501f2ca5 | ||
|
|
9646152550 | ||
|
|
7d11c95be8 | ||
|
|
140a051fc9 | ||
|
|
3bc2f6aba7 | ||
|
|
eb16534ad9 | ||
|
|
b9d3f921e9 | ||
|
|
1c4ec47f1a | ||
|
|
6d5e2a1a9f | ||
|
|
5b49156a5f | ||
|
|
a4deb42d3d | ||
|
|
103804683d | ||
|
|
50fed7ae42 | ||
|
|
da7285856c | ||
|
|
de8462803d | ||
|
|
9097762a53 | ||
|
|
9954f8e0da | ||
|
|
085d7b4c95 | ||
|
|
d1a7b03b55 | ||
|
|
14da838637 | ||
|
|
0211ff78e1 | ||
|
|
8b6769d7e8 | ||
|
|
00532b564b | ||
|
|
19de364958 | ||
|
|
91ca3af477 | ||
|
|
079b343981 | ||
|
|
c60c7235c1 | ||
|
|
e79c30f765 | ||
|
|
b6ea00daa4 | ||
|
|
ab06501e04 | ||
|
|
8b1d3b478c | ||
|
|
b1724ec235 | ||
|
|
1ef7be0af6 | ||
|
|
e7b727c0ba | ||
|
|
6e48bcfd9e | ||
|
|
f02c97e622 | ||
|
|
a174eb4b85 | ||
|
|
f0c8e19e64 | ||
|
|
14b35a93c0 | ||
|
|
38b57df883 | ||
|
|
eee7cc1397 | ||
|
|
90205c6330 | ||
|
|
2078867d7b | ||
|
|
a24a40f4bc | ||
|
|
5f4d6c0f8d | ||
|
|
a43a32fc55 | ||
|
|
d76358c37f | ||
|
|
5e9c6a0677 | ||
|
|
b115313955 | ||
|
|
b49f38ca4b | ||
|
|
e79261a566 | ||
|
|
1d4548fb3b | ||
|
|
e286108bb5 | ||
|
|
81b6d34693 | ||
|
|
b16575b407 | ||
|
|
f01be72d8f | ||
|
|
e1e018fcfc | ||
|
|
16e3a682fe | ||
|
|
789dec673e | ||
|
|
54284be74b | ||
|
|
8e987847ee | ||
|
|
28005ae633 | ||
|
|
b31f753c11 | ||
|
|
a5c8e5125c | ||
|
|
553e37b701 | ||
|
|
90231f76cc | ||
|
|
d9696e77eb | ||
|
|
d0c5040fe9 | ||
|
|
eb14d632d9 | ||
|
|
2fadf0c82f | ||
|
|
c1aa0d0004 | ||
|
|
5d5dde33b7 | ||
|
|
5172a95f53 | ||
|
|
352c97f25c | ||
|
|
3f9d671fb1 | ||
|
|
ac697c0bd8 | ||
|
|
48a43eb268 | ||
|
|
a870650faf | ||
|
|
7fd44d0247 | ||
|
|
f9eee73ef4 | ||
|
|
f491266105 | ||
|
|
cdd1bdbfbd | ||
|
|
411d8e9034 | ||
|
|
e826fc4a31 | ||
|
|
5e3fd4bbf8 | ||
|
|
adb802f28e | ||
|
|
31722b2f72 | ||
|
|
a65dcd518d | ||
|
|
b62821cb74 | ||
|
|
62f06582ef | ||
|
|
222b0136bf | ||
|
|
720263ae2d | ||
|
|
b77289937f | ||
|
|
bf4097aa60 | ||
|
|
d755a529ef | ||
|
|
79d8bfb864 | ||
|
|
b57d6f5d21 | ||
|
|
baf43f9780 | ||
|
|
4d7cda0001 | ||
|
|
0c9bbb5dde | ||
|
|
9cd81c62eb | ||
|
|
9609595525 | ||
|
|
6a351419d6 | ||
|
|
a9f8dff7e9 | ||
|
|
9b8ddb0580 | ||
|
|
87e6363686 | ||
|
|
88da732d5c | ||
|
|
13d8c72a82 | ||
|
|
fe79365c63 | ||
|
|
5e9499873d | ||
|
|
8cbb8089fb | ||
|
|
5df8763166 | ||
|
|
6a5ec2d953 | ||
|
|
7e47ecd91e | ||
|
|
894ba19345 | ||
|
|
9d3bda9bce | ||
|
|
014cbc3c4d | ||
|
|
66def746e6 | ||
|
|
0d577da288 | ||
|
|
9ad013253a | ||
|
|
81ae388ac4 | ||
|
|
880c0e6802 | ||
|
|
8d3ee36392 | ||
|
|
a642946665 | ||
|
|
895c6490bb | ||
|
|
0b1a1c03dc | ||
|
|
b69969942a | ||
|
|
c71e1b9f8b | ||
|
|
611436a943 | ||
|
|
ee26147dde | ||
|
|
4350b69a18 | ||
|
|
caaa6108d8 | ||
|
|
a53e8f708c | ||
|
|
d9e82f4ffc | ||
|
|
b89dcb7fca | ||
|
|
e94173aa12 | ||
|
|
958e1dcb9c | ||
|
|
9bf0ec9de7 | ||
|
|
cb22a95df2 | ||
|
|
00297db3ab | ||
|
|
00fe02c1d1 | ||
|
|
77062e2e4e | ||
|
|
f33c2f14e5 | ||
|
|
e6184e4bd8 | ||
|
|
f8c925c265 | ||
|
|
da5a0c8749 | ||
|
|
ecaa6874ce | ||
|
|
67422a5cd8 | ||
|
|
81f0cf72ae | ||
|
|
ef5d026074 | ||
|
|
b6700f8adb | ||
|
|
efcd797648 | ||
|
|
f4b9aaffef | ||
|
|
6aa6c9d34f | ||
|
|
1490b51f71 | ||
|
|
4524177931 | ||
|
|
f1643492bc | ||
|
|
b565c65628 | ||
|
|
c6a4a00fda | ||
|
|
4ba4c12a87 | ||
|
|
f50ef5be9a | ||
|
|
b4c0fb9351 | ||
|
|
2346f06d23 | ||
|
|
cdb8e3a08a | ||
|
|
9df2b51813 | ||
|
|
0daffaf4ad | ||
|
|
96dc9bd840 | ||
|
|
cde9f0a5c8 | ||
|
|
25ef949a97 | ||
|
|
7b2c22f0ba | ||
|
|
74d8fd54d9 | ||
|
|
7572188ab0 | ||
|
|
7a117150ef | ||
|
|
837cc31a93 | ||
|
|
aa88d1c8f4 | ||
|
|
456016959a | ||
|
|
825e80a5a0 | ||
|
|
7760b95a41 | ||
|
|
73d0f75844 | ||
|
|
a9b7f061fe | ||
|
|
72bf31a828 | ||
|
|
a959ff78c9 | ||
|
|
257044b210 | ||
|
|
92b9fe252a | ||
|
|
9f5ac1af94 | ||
|
|
b85633fa6e | ||
|
|
35cc38a947 | ||
|
|
bcef0650dc | ||
|
|
1a33d464f0 | ||
|
|
56bb3dba6a | ||
|
|
4f86b3cd31 | ||
|
|
c27be69d92 | ||
|
|
d48924ed01 | ||
|
|
3921841630 | ||
|
|
c617fcc6e4 | ||
|
|
93ef263c5e | ||
|
|
e09735a3c2 | ||
|
|
c6b784266b | ||
|
|
7336ccb660 | ||
|
|
373e3b953d | ||
|
|
e430c02c7f | ||
|
|
8e4956891c | ||
|
|
46361868ed | ||
|
|
80192ea42a | ||
|
|
7b82c5b00c | ||
|
|
e75db3b73d | ||
|
|
b3d96d8b53 | ||
|
|
0db4a60399 | ||
|
|
bc99026521 | ||
|
|
2779e2e18c | ||
|
|
0b37cb6634 | ||
|
|
cce349ff15 | ||
|
|
9f5276a497 | ||
|
|
e9a2cf361d | ||
|
|
8a4606f5d3 | ||
|
|
e7249002e5 | ||
|
|
7a57b4f51c | ||
|
|
10e779c1e9 | ||
|
|
3d56c52291 | ||
|
|
def10dea18 | ||
|
|
8b811ae65f | ||
|
|
94493d1051 | ||
|
|
593235a09f | ||
|
|
6447545ec4 | ||
|
|
d6b6d92122 | ||
|
|
ded84933b2 | ||
|
|
aa93d26b80 | ||
|
|
31655f2c35 | ||
|
|
97aeb48109 | ||
|
|
bbb84cd849 | ||
|
|
0113925b92 | ||
|
|
250dec1ae6 | ||
|
|
8fc244b271 | ||
|
|
bfe39185fa | ||
|
|
45555782c6 | ||
|
|
a893cce2f7 | ||
|
|
df57293ebb | ||
|
|
33934f1e05 | ||
|
|
a9bd1a1a5c | ||
|
|
6066554a7a | ||
|
|
59e6ee8240 | ||
|
|
2f3a6ab921 | ||
|
|
bd35f5e025 | ||
|
|
a1ba0894d9 | ||
|
|
4b0666c617 | ||
|
|
ef3125b48e | ||
|
|
691bc1d4e6 | ||
|
|
88c30645e7 | ||
|
|
0b1df5b3d5 | ||
|
|
4ff5fe895b | ||
|
|
ae5645251d | ||
|
|
6b302a051f | ||
|
|
2aec310c1e | ||
|
|
dc1f975f13 | ||
|
|
5b2098c9f7 | ||
|
|
4193094232 | ||
|
|
d84a134904 | ||
|
|
a05cde7256 | ||
|
|
ea20f0be48 | ||
|
|
dd3bcc8f73 | ||
|
|
dadf6ffb1d | ||
|
|
7083e77d9c | ||
|
|
dd976dee00 | ||
|
|
305e8d7296 | ||
|
|
437a3a5700 | ||
|
|
2c7326d3fc | ||
|
|
7f512474dc | ||
|
|
90c8b50689 | ||
|
|
e808422320 | ||
|
|
45cb6b21a2 | ||
|
|
5708735ca8 | ||
|
|
bcbe8a73b8 | ||
|
|
c5a9c81642 | ||
|
|
75e5652bf6 | ||
|
|
af458635e2 | ||
|
|
847e77ff72 | ||
|
|
f4aa3e7c18 | ||
|
|
37ce873db8 | ||
|
|
ace1021959 | ||
|
|
470d1ae3ec | ||
|
|
c3b5921d14 | ||
|
|
9300b786f8 | ||
|
|
443ee9bb4e | ||
|
|
620aefec23 | ||
|
|
b925b3e639 | ||
|
|
09835b0462 | ||
|
|
b88358949c | ||
|
|
9c5b4fb3d4 | ||
|
|
e8c14f9185 | ||
|
|
a0580238a6 | ||
|
|
01b370d9b6 | ||
|
|
8815eb836c | ||
|
|
f472c25349 | ||
|
|
809cc090e5 | ||
|
|
d34d87c274 | ||
|
|
c4bff550bb | ||
|
|
818f58e2f9 | ||
|
|
bfb79e38d6 | ||
|
|
a52bf50a0f | ||
|
|
49750b600f | ||
|
|
ffad7c48d2 | ||
|
|
43bd2a8314 | ||
|
|
e96d640082 | ||
|
|
df9d90d017 | ||
|
|
bbad30b86d | ||
|
|
86c12d13a7 | ||
|
|
bd8b4b8fa8 | ||
|
|
4ecb87658b | ||
|
|
c1e8153871 | ||
|
|
d54b0ca556 | ||
|
|
c67969efa2 | ||
|
|
2f9ce0c5cf | ||
|
|
ec702ddc05 | ||
|
|
98ba83e0a4 | ||
|
|
37938a022c | ||
|
|
7ee5f0d448 | ||
|
|
b98de3d992 | ||
|
|
2670fb1b4f | ||
|
|
360981b77d | ||
|
|
c693ad434a | ||
|
|
67bb04c6c2 | ||
|
|
764669398c | ||
|
|
cc3db7d323 | ||
|
|
bd7809bc17 | ||
|
|
0e30e64534 | ||
|
|
01d51c6e45 | ||
|
|
8d63de52f9 | ||
|
|
f2a781545b | ||
|
|
671a1f2923 | ||
|
|
ef1d95ea2d | ||
|
|
182adad9d2 | ||
|
|
91de70b7dc | ||
|
|
aa74dae70f | ||
|
|
4f61d792bc | ||
|
|
603461f19a | ||
|
|
4a535085c5 |
36
.dockerignore
Normal file
36
.dockerignore
Normal file
@ -0,0 +1,36 @@
|
||||
# Build artifacts
|
||||
target/
|
||||
**/target/
|
||||
|
||||
# RocksDB data
|
||||
rocksdb/
|
||||
**/rocksdb/
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
ci_scripts/
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Configs (copy selectively if needed)
|
||||
configs/
|
||||
|
||||
# License
|
||||
LICENSE
|
||||
10
.github/actions/install-risc0/action.yml
vendored
Normal file
10
.github/actions/install-risc0/action.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: Install risc0
|
||||
description: Installs risc0 in the environment
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install risc0
|
||||
run: |
|
||||
curl -L https://risczero.com/install | bash
|
||||
/home/runner/.risc0/bin/rzup install
|
||||
shell: bash
|
||||
10
.github/actions/install-system-deps/action.yml
vendored
Normal file
10
.github/actions/install-system-deps/action.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: Install system dependencies
|
||||
description: Installs system dependencies in the environment
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential clang libclang-dev libssl-dev pkg-config
|
||||
shell: bash
|
||||
43
.github/pull_request_template.md
vendored
Normal file
43
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
## 🎯 Purpose
|
||||
|
||||
*What problem does this PR solve or what feature does it add? Mention issues related to it*
|
||||
|
||||
TO COMPLETE
|
||||
|
||||
## ⚙️ Approach
|
||||
|
||||
*Describe the core changes introduced by this PR.*
|
||||
|
||||
TO COMPLETE
|
||||
|
||||
- [ ] Change ...
|
||||
- [ ] Add ...
|
||||
- [ ] Fix ...
|
||||
- [ ] ...
|
||||
|
||||
## 🧪 How to Test
|
||||
|
||||
*How to verify that this PR works as intended.*
|
||||
|
||||
TO COMPLETE
|
||||
|
||||
## 🔗 Dependencies
|
||||
|
||||
*Link PRs that must be merged before this one.*
|
||||
|
||||
TO COMPLETE IF APPLICABLE
|
||||
|
||||
## 🔜 Future Work
|
||||
|
||||
*List any work intentionally left out of this PR, whether due to scope, prioritization, or pending decisions.*
|
||||
|
||||
TO COMPLETE IF APPLICABLE
|
||||
|
||||
## 📋 PR Completion Checklist
|
||||
|
||||
*Mark only completed items. A complete PR should have all boxes ticked.*
|
||||
|
||||
- [ ] Complete PR description
|
||||
- [ ] Implement the core functionality
|
||||
- [ ] Add/update tests
|
||||
- [ ] Add/update documentation and inline comments
|
||||
196
.github/workflows/ci.yml
vendored
196
.github/workflows/ci.yml
vendored
@ -1,7 +1,7 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "!.github/workflows/*.yml"
|
||||
@ -14,64 +14,164 @@ on:
|
||||
name: General
|
||||
|
||||
jobs:
|
||||
build-ubuntu-latest:
|
||||
fmt-rs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Install nightly toolchain for rustfmt
|
||||
run: rustup install nightly --profile minimal --component rustfmt
|
||||
|
||||
- name: Check Rust files are formatted
|
||||
run: cargo +nightly fmt --check
|
||||
|
||||
fmt-toml:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Install taplo-cli
|
||||
run: cargo install --locked taplo-cli
|
||||
|
||||
- name: Check TOML files are formatted
|
||||
run: taplo fmt --check .
|
||||
|
||||
machete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Install cargo-machete
|
||||
run: cargo install cargo-machete
|
||||
|
||||
- name: Check for unused dependencies
|
||||
run: cargo machete
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
name: build - ubuntu-latest
|
||||
name: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- name: build - ubuntu-latest
|
||||
if: success() || failure()
|
||||
run: chmod 777 ./ci_scripts/build-ubuntu.sh && ./ci_scripts/build-ubuntu.sh
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
lint:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ ubuntu-latest ]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Lint workspace
|
||||
env:
|
||||
RISC0_SKIP_BUILD: "1"
|
||||
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Lint programs
|
||||
env:
|
||||
RISC0_SKIP_BUILD: "1"
|
||||
run: cargo clippy -p "*programs" -- -D warnings
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo install cargo-nextest
|
||||
|
||||
- name: Run unit tests
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
run: cargo nextest run --no-fail-fast
|
||||
|
||||
valid-proof-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Test valid proof
|
||||
env:
|
||||
NSSA_WALLET_HOME_DIR: ./integration_tests/configs/debug/wallet
|
||||
RUST_LOG: "info"
|
||||
run: cargo run --bin integration_tests -- ./integration_tests/configs/debug/ test_success_private_transfer_to_another_owned_account
|
||||
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Run integration tests
|
||||
env:
|
||||
NSSA_WALLET_HOME_DIR: ./integration_tests/configs/debug/wallet
|
||||
RUST_LOG: "info"
|
||||
RISC0_DEV_MODE: "1"
|
||||
run: cargo run --bin integration_tests -- ./integration_tests/configs/debug/ all
|
||||
|
||||
artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
name: lint - ${{ matrix.crate }} - ${{ matrix.platform }}
|
||||
name: artifacts
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: lint - ubuntu-latest
|
||||
if: success() || failure()
|
||||
run: chmod 777 ./ci_scripts/lint-ubuntu.sh && ./ci_scripts/lint-ubuntu.sh
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ ubuntu-latest ]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
timeout-minutes: 60
|
||||
- name: Install just
|
||||
run: cargo install just
|
||||
|
||||
name: test - ${{ matrix.crate }} - ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- name: test ubuntu-latest
|
||||
if: success() || failure()
|
||||
run: chmod 777 ./ci_scripts/test-ubuntu.sh && ./ci_scripts/test-ubuntu.sh
|
||||
- name: Build artifacts
|
||||
run: just build-artifacts
|
||||
|
||||
- name: Check if artifacts match repository
|
||||
run: |
|
||||
if ! git diff --exit-code artifacts/; then
|
||||
echo "❌ Artifacts in the repository are out of date!"
|
||||
echo "Please run 'just build-artifacts' and commit the changes."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Artifacts are up to date"
|
||||
|
||||
23
.github/workflows/deploy.yml
vendored
Normal file
23
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: Deploy Sequencer
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@v1.2.4
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_SSH_HOST }}
|
||||
username: ${{ secrets.DEPLOY_SSH_USERNAME }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
envs: GITHUB_ACTOR
|
||||
script_path: ci_scripts/deploy.sh
|
||||
44
.github/workflows/publish_image.yml
vendored
Normal file
44
.github/workflows/publish_image.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Publish Sequencer Runner Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_REGISTRY }}/${{ github.repository }}/sequencer_runner
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix={{branch}}-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./sequencer_runner/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ data/
|
||||
.idea/
|
||||
.vscode/
|
||||
rocksdb
|
||||
sequencer_runner/data/
|
||||
storage.json
|
||||
4052
Cargo.lock
generated
4052
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
126
Cargo.toml
126
Cargo.toml
@ -1,87 +1,97 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
resolver = "3"
|
||||
members = [
|
||||
"node_runner",
|
||||
"integration_tests",
|
||||
"sequencer_runner",
|
||||
"storage",
|
||||
"accounts",
|
||||
"utxo",
|
||||
"vm",
|
||||
"networking",
|
||||
"consensus",
|
||||
"node_rpc",
|
||||
"key_protocol",
|
||||
"sequencer_rpc",
|
||||
"mempool",
|
||||
"zkvm",
|
||||
"node_core",
|
||||
"wallet",
|
||||
"sequencer_core",
|
||||
"rpc_primitives",
|
||||
"common",
|
||||
"nssa",
|
||||
"nssa/core",
|
||||
"program_methods",
|
||||
"program_methods/guest",
|
||||
"test_program_methods",
|
||||
"test_program_methods/guest",
|
||||
"integration_tests/proc_macro_test_attribute",
|
||||
"examples/program_deployment",
|
||||
"examples/program_deployment/methods",
|
||||
"examples/program_deployment/methods/guest",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
nssa = { path = "nssa" }
|
||||
nssa_core = { path = "nssa/core" }
|
||||
common = { path = "common" }
|
||||
mempool = { path = "mempool" }
|
||||
storage = { path = "storage" }
|
||||
key_protocol = { path = "key_protocol" }
|
||||
sequencer_core = { path = "sequencer_core" }
|
||||
sequencer_rpc = { path = "sequencer_rpc" }
|
||||
sequencer_runner = { path = "sequencer_runner" }
|
||||
wallet = { path = "wallet" }
|
||||
test_program_methods = { path = "test_program_methods" }
|
||||
|
||||
tokio = { version = "1.28.2", features = [
|
||||
"net",
|
||||
"rt-multi-thread",
|
||||
"sync",
|
||||
"fs",
|
||||
] }
|
||||
risc0-zkvm = { version = "3.0.3", features = ['std'] }
|
||||
risc0-build = "3.0.3"
|
||||
anyhow = "1.0.98"
|
||||
num_cpus = "1.13.1"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
openssl-probe = { version = "0.1.2" }
|
||||
serde = { version = "1.0.60", default-features = false, features = ["derive"] }
|
||||
serde_json = "1.0.81"
|
||||
actix = "0.13.0"
|
||||
actix-cors = "0.6.1"
|
||||
futures = "0.3"
|
||||
actix-rt = "*"
|
||||
|
||||
lazy_static = "1.5.0"
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
log = "0.4.28"
|
||||
lru = "0.7.8"
|
||||
thiserror = "1.0"
|
||||
rs_merkle = "1.4"
|
||||
thiserror = "2.0.12"
|
||||
sha2 = "0.10.8"
|
||||
monotree = "0.1.5"
|
||||
hex = "0.4.3"
|
||||
bytemuck = "1.24.0"
|
||||
aes-gcm = "0.10.3"
|
||||
toml = "0.7.4"
|
||||
secp256k1-zkp = "0.11.0"
|
||||
bincode = "1.3.3"
|
||||
tempfile = "3.14.0"
|
||||
light-poseidon = "0.3.0"
|
||||
ark-bn254 = "0.5.0"
|
||||
ark-ff = "0.5.0"
|
||||
tiny-keccak = { version = "2.0.2", features = ["keccak"] }
|
||||
base64 = "0.22.1"
|
||||
bip39 = "2.2.0"
|
||||
hmac-sha512 = "1.1.7"
|
||||
chrono = "0.4.41"
|
||||
borsh = "1.5.7"
|
||||
base58 = "0.2.0"
|
||||
itertools = "0.14.0"
|
||||
|
||||
rocksdb = { version = "0.21.0", default-features = false, features = [
|
||||
rocksdb = { version = "0.24.0", default-features = false, features = [
|
||||
"snappy",
|
||||
"bindgen-runtime",
|
||||
] }
|
||||
|
||||
[workspace.dependencies.rand]
|
||||
features = ["std", "std_rng", "getrandom"]
|
||||
version = "0.8.5"
|
||||
|
||||
[workspace.dependencies.k256]
|
||||
features = ["ecdsa-core", "arithmetic", "expose-field"]
|
||||
version = "0.13.4"
|
||||
|
||||
[workspace.dependencies.elliptic-curve]
|
||||
features = ["arithmetic"]
|
||||
version = "0.13.8"
|
||||
|
||||
[workspace.dependencies.serde]
|
||||
features = ["derive"]
|
||||
version = "1.0.60"
|
||||
|
||||
[workspace.dependencies.actix-web]
|
||||
default-features = false
|
||||
version = "=4.1.0"
|
||||
|
||||
[workspace.dependencies.clap]
|
||||
features = ["derive", "env"]
|
||||
version = "3.1.6"
|
||||
|
||||
[workspace.dependencies.tokio-retry]
|
||||
version = "0.3.0"
|
||||
|
||||
[workspace.dependencies.reqwest]
|
||||
features = ["json"]
|
||||
version = "0.11.16"
|
||||
|
||||
[workspace.dependencies.tokio]
|
||||
features = ["net", "rt-multi-thread", "sync", "fs"]
|
||||
version = "1.28.2"
|
||||
|
||||
[workspace.dependencies.tracing]
|
||||
features = ["std"]
|
||||
version = "0.1.13"
|
||||
rand = { version = "0.8.5", features = ["std", "std_rng", "getrandom"] }
|
||||
k256 = { version = "0.13.3", features = [
|
||||
"ecdsa-core",
|
||||
"arithmetic",
|
||||
"expose-field",
|
||||
"serde",
|
||||
"pem",
|
||||
] }
|
||||
elliptic-curve = { version = "0.13.8", features = ["arithmetic"] }
|
||||
actix-web = { version = "=4.1.0", default-features = false, features = [
|
||||
"macros",
|
||||
] }
|
||||
clap = { version = "4.5.42", features = ["derive", "env"] }
|
||||
reqwest = { version = "0.11.16", features = ["json"] }
|
||||
|
||||
19
Justfile
Normal file
19
Justfile
Normal file
@ -0,0 +1,19 @@
|
||||
set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
|
||||
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# ---- Configuration ----
|
||||
METHODS_PATH := "program_methods"
|
||||
TEST_METHODS_PATH := "test_program_methods"
|
||||
ARTIFACTS := "artifacts"
|
||||
|
||||
# ---- Artifacts build ----
|
||||
build-artifacts:
|
||||
@echo "🔨 Building artifacts"
|
||||
@for methods_path in {{METHODS_PATH}} {{TEST_METHODS_PATH}}; do \
|
||||
echo "Building artifacts for $methods_path"; \
|
||||
CARGO_TARGET_DIR=target/$methods_path cargo risczero build --manifest-path $methods_path/guest/Cargo.toml; \
|
||||
mkdir -p {{ARTIFACTS}}/$methods_path; \
|
||||
cp target/$methods_path/riscv32im-risc0-zkvm-elf/docker/*.bin {{ARTIFACTS}}/$methods_path; \
|
||||
done
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Vac
|
||||
|
||||
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.
|
||||
753
README.md
753
README.md
@ -1,2 +1,751 @@
|
||||
# nescience-testnet
|
||||
This repo serves for Nescience Node testnet
|
||||
# Nescience
|
||||
|
||||
Nescience State Separation Architecture (NSSA) is a programmable blockchain system that introduces a clean separation between public and private states, while keeping them fully interoperable. It lets developers build apps that can operate across both transparent and privacy-preserving accounts. Privacy is handled automatically by the protocol through zero-knowledge proofs (ZKPs). The result is a programmable blockchain where privacy comes built-in.
|
||||
|
||||
## Background
|
||||
|
||||
Typically, public blockchains maintain a fully transparent state, where the mapping from account IDs to account values is entirely visible. In NSSA, we introduce a parallel *private state*, a new layer of accounts that coexists with the public one. The public and private states can be viewed as a partition of the account ID space: accounts with public IDs are openly visible, while private accounts are accessible only to holders of the corresponding viewing keys. Consistency across both states is enforced through zero-knowledge proofs (ZKPs).
|
||||
|
||||
Public accounts are represented on-chain as a visible map from IDs to account states and are modified in-place when their values change. Private accounts, by contrast, are never stored in raw form on-chain. Each update creates a new commitment, which cryptographically binds the current value of the account while preserving privacy. Commitments of previous valid versions remain on-chain, but a nullifier set is maintained to mark old versions as spent, ensuring that only the most up-to-date version of each private account can be used in any execution.
|
||||
|
||||
### Programmability and selective privacy
|
||||
|
||||
Our goal is to enable full programmability within this hybrid model, matching the flexibility and composability of public blockchains. Developers write and deploy programs in NSSA just as they would on any other blockchain. Privacy, along with the ability to execute programs involving any combination of public and private accounts, is handled entirely at the protocol level and available out of the box for all programs. From the program’s perspective, all accounts are indistinguishable. This abstraction allows developers to focus purely on business logic, while the system transparently enforces privacy and consistency guarantees.
|
||||
|
||||
To the best of our knowledge, this approach is unique to Nescience. Other programmable blockchains with a focus on privacy typically adopt a developer-driven model for private execution, meaning that dApp logic must explicitly handle private inputs correctly. In contrast, Nescience handles privacy at the protocol level, so developers do not need to modify their programs—private and public accounts are treated uniformly, and privacy-preserving execution is available out of the box.
|
||||
|
||||
### Example: creating and transferring tokens across states
|
||||
|
||||
1. Token creation (public execution):
|
||||
- Alice submits a transaction to execute the token program `New` function on-chain.
|
||||
- A new public token account is created, representing the token.
|
||||
- The minted tokens are recorded on-chain and fully visible on Alice's public account.
|
||||
2. Transfer from public to private (local / privacy-preserving execution)
|
||||
- Alice executes the token program `Transfer` function locally, specifying a Bob’s private account as recipient.
|
||||
- A ZKP of correct execution is generated.
|
||||
- The proof is submitted to the blockchain, and validator nodes verify it.
|
||||
- Alice's public account balance is modified accordingly.
|
||||
- Bob’s private account and balance remain hidden, while the transfer is provably valid.
|
||||
3. Transferring private to public (local / privacy-preserving execution)
|
||||
- Bob executes the token program `Transfer` function locally, specifying a Charlie’s public account as recipient.
|
||||
- A ZKP of correct execution is generated.
|
||||
- Bob’s private account and balance still remain hidden.
|
||||
- Charlie's public account is modified with the new tokens added.
|
||||
4. Transferring public to public (public execution):
|
||||
- Alice submits a transaction to execute the token program `Transfer` function on-chain, specifying Charlie's public account as recipient.
|
||||
- The execution is handled on-chain without ZKPs involved.
|
||||
- Alice's and Charlie's accounts are modified according to the transaction.
|
||||
|
||||
#### Key points:
|
||||
- The same token program is used in all executions.
|
||||
- The difference lies in execution mode: public executions update visible accounts on-chain, while private executions rely on ZKPs.
|
||||
- Validators only need to verify proofs for privacy-preserving transactions, keeping processing efficient.
|
||||
|
||||
### The account’s model
|
||||
|
||||
To achieve both state separation and full programmability, NSSA adopts a stateless program model. Programs do not hold internal state. Instead, all persistent data resides in accounts explicitly passed to the program during execution. This design enables fine-grained control over access and visibility while maintaining composability across public and private states.
|
||||
|
||||
### Execution types
|
||||
|
||||
Execution is divided into two fundamentally distinct types based on how they are processed: public execution, which is executed transparently on-chain, and private execution, which occurs off-chain. For private execution, the blockchain relies on ZKPs to verify the correctness of execution and ensure that all system invariants are preserved.
|
||||
|
||||
Both public and private executions of the same program are enforced to use the same Risc0 VM bytecode. For public transactions, programs are executed directly on-chain like any standard RISC-V VM execution, without generating or verifying proofs. For privacy-preserving transactions, users generate Risc0 ZKPs of correct execution, and validator nodes only verify these proofs rather than re-executing the program. This design ensures that from a validator’s perspective, public transactions are processed as quickly as any RISC-V–based VM, while verification of ZKPs keeps privacy-preserving transactions efficient as well. Additionally, the system naturally supports parallel execution similar to Solana, further increasing throughput. The main computational bottleneck for privacy-preserving transactions lies on the user side, in generating zk proofs.
|
||||
|
||||
### Resources
|
||||
- [IFT Research call](https://forum.vac.dev/t/ift-research-call-september-10th-2025-updates-on-the-development-of-nescience/566)
|
||||
- [NSSA v0.2 specs](https://www.notion.so/NSSA-v0-2-specifications-2848f96fb65c800c9818e6f66d9be8f2)
|
||||
- [Choice of VM/zkVM](https://www.notion.so/Conclusion-on-the-chosen-VM-and-zkVM-for-NSSA-2318f96fb65c806a810ed1300f56992d)
|
||||
- [NSSA vs other privacy projects](https://www.notion.so/Privacy-projects-comparison-2688f96fb65c8096b694ecf7e4deca30)
|
||||
- [NSSA state model](https://www.notion.so/Public-state-model-decision-2388f96fb65c80758b20c76de07b1fcc)
|
||||
- [NSSA sequencer specs](https://www.notion.so/Sequencer-specs-2428f96fb65c802da2bfea7b0b214ecb)
|
||||
- [NSSA sequencer code](https://www.notion.so/NSSA-sequencer-pseudocode-2508f96fb65c805e8859e047dffd6785)
|
||||
- [NSSA Token program desing](https://www.notion.so/Token-program-design-2538f96fb65c80a1b4bdc4fd9dd162d7)
|
||||
- [NSSA cross program calls](https://www.notion.so/NSSA-cross-program-calls-Tail-call-model-proposal-extended-version-2838f96fb65c8096b3a2d390444193b6)
|
||||
|
||||
|
||||
# Install dependencies
|
||||
Install build dependencies
|
||||
|
||||
- On Linux
|
||||
Ubuntu / Debian
|
||||
```sh
|
||||
apt install build-essential clang libclang-dev libssl-dev pkg-config
|
||||
```
|
||||
|
||||
Fedora
|
||||
```sh
|
||||
sudo dnf install clang clang-devel openssl-devel pkgconf
|
||||
```
|
||||
|
||||
- On Mac
|
||||
```sh
|
||||
xcode-select --install
|
||||
brew install pkg-config openssl
|
||||
```
|
||||
|
||||
Install Rust
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
Install Risc0
|
||||
|
||||
```sh
|
||||
curl -L https://risczero.com/install | bash
|
||||
```
|
||||
|
||||
Then restart your shell and run
|
||||
```sh
|
||||
rzup install
|
||||
```
|
||||
|
||||
# Run tests
|
||||
|
||||
The NSSA repository includes both unit and integration test suites.
|
||||
|
||||
### Unit tests
|
||||
|
||||
```bash
|
||||
# RISC0_DEV_MODE=1 is used to skip proof generation and reduce test runtime overhead
|
||||
RISC0_DEV_MODE=1 cargo test --release
|
||||
```
|
||||
|
||||
### Integration tests
|
||||
|
||||
```bash
|
||||
export NSSA_WALLET_HOME_DIR=$(pwd)/integration_tests/configs/debug/wallet/
|
||||
cd integration_tests
|
||||
# RISC0_DEV_MODE=1 skips proof generation; RUST_LOG=info enables runtime logs
|
||||
RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all
|
||||
```
|
||||
|
||||
# Run the sequencer
|
||||
|
||||
The sequencer can be run locally:
|
||||
|
||||
```bash
|
||||
cd sequencer_runner
|
||||
RUST_LOG=info cargo run --release configs/debug
|
||||
```
|
||||
|
||||
If everything went well you should see an output similar to this:
|
||||
```bash
|
||||
[2025-11-13T19:50:29Z INFO sequencer_runner] Sequencer core set up
|
||||
[2025-11-13T19:50:29Z INFO network] Starting http server at 0.0.0.0:3040
|
||||
[2025-11-13T19:50:29Z INFO actix_server::builder] starting 8 workers
|
||||
[2025-11-13T19:50:29Z INFO sequencer_runner] HTTP server started
|
||||
[2025-11-13T19:50:29Z INFO sequencer_runner] Starting main sequencer loop
|
||||
[2025-11-13T19:50:29Z INFO actix_server::server] Tokio runtime found; starting in existing Tokio runtime
|
||||
[2025-11-13T19:50:29Z INFO actix_server::server] starting service: "actix-web-service-0.0.0.0:3040", workers: 8, listening on: 0.0.0.0:3040
|
||||
[2025-11-13T19:50:39Z INFO sequencer_runner] Collecting transactions from mempool, block creation
|
||||
[2025-11-13T19:50:39Z INFO sequencer_core] Created block with 0 transactions in 0 seconds
|
||||
[2025-11-13T19:50:39Z INFO sequencer_runner] Block with id 2 created
|
||||
[2025-11-13T19:50:39Z INFO sequencer_runner] Waiting for new transactions
|
||||
```
|
||||
|
||||
# Try the Wallet CLI
|
||||
|
||||
## Install
|
||||
|
||||
This repository includes a CLI for interacting with the Nescience sequencer. To install it, run the following command from the root of the repository:
|
||||
|
||||
```bash
|
||||
cargo install --path wallet --force
|
||||
```
|
||||
|
||||
Run `wallet help` to check everything went well.
|
||||
|
||||
## Tutorial
|
||||
|
||||
This tutorial walks you through creating accounts and executing NSSA programs in both public and private contexts.
|
||||
|
||||
> [!NOTE]
|
||||
> The NSSA state is split into two separate but interconnected components: the public state and the private state.
|
||||
> The public state is an on-chain, publicly visible record of accounts indexed by their Account IDs
|
||||
> The private state mirrors this, but the actual account values are stored locally by each account owner. On-chain, only a hidden commitment to each private account state is recorded. This allows the chain to enforce freshness (i.e., prevent the reuse of stale private states) while preserving privacy and unlinkability across executions and private accounts.
|
||||
>
|
||||
> Every piece of state in NSSA is stored in an account (public or private). Accounts are either uninitialized or are owned by a program, and programs can only modify the accounts they own.
|
||||
>
|
||||
> In NSSA, accounts can only be modified through program execution. A program is the sole mechanism that can change an account’s value.
|
||||
> Programs run publicly when all involved accounts are public, and privately when at least one private account participates.
|
||||
|
||||
### Health-check
|
||||
|
||||
Verify that the node is running and that the wallet can connect to it:
|
||||
|
||||
```bash
|
||||
wallet check-health
|
||||
```
|
||||
|
||||
You should see `✅ All looks good!`.
|
||||
|
||||
### The commands
|
||||
|
||||
The wallet provides several commands to interact with the node and query state. To see the full list, run `wallet help`:
|
||||
|
||||
```bash
|
||||
Commands:
|
||||
auth-transfer Authenticated transfer subcommand
|
||||
chain-info Generic chain info subcommand
|
||||
account Account view and sync subcommand
|
||||
pinata Pinata program interaction subcommand
|
||||
token Token program interaction subcommand
|
||||
amm AMM program interaction subcommand
|
||||
check-health Check the wallet can connect to the node and builtin local programs match the remote versions
|
||||
```
|
||||
|
||||
### Accounts
|
||||
|
||||
> [!NOTE]
|
||||
> Accounts are the basic unit of state in NSSA. They essentially hold native tokens and arbitrary data managed by some program.
|
||||
|
||||
The CLI provides commands to manage accounts. Run `wallet account` to see the options available:
|
||||
```bash
|
||||
Commands:
|
||||
get Get account data
|
||||
new Produce new public or private account
|
||||
sync-private Sync private accounts
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
```
|
||||
|
||||
#### Create a new public account
|
||||
|
||||
You can create both public and private accounts through the CLI. For example:
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
```
|
||||
|
||||
This id is required when executing any program that interacts with the account.
|
||||
|
||||
> [!NOTE]
|
||||
> Public accounts live on-chain and are identified by a 32-byte Account ID.
|
||||
> Running `wallet account new public` generates a fresh keypair for the signature scheme used in NSSA.
|
||||
> The account ID is derived from the public key. The private key is used to sign transactions and to authorize the account in program executions.
|
||||
|
||||
#### Account initialization
|
||||
|
||||
To query the account’s current status, run:
|
||||
|
||||
```bash
|
||||
# Replace the id with yours
|
||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account is Uninitialized
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> New accounts begin in an uninitialized state, meaning they are not yet owned by any program. A program may claim an uninitialized account; once claimed, the account becomes owned by that program.
|
||||
> Owned accounts can only be modified through executions of the owning program. The only exception is native-token credits: any program may credit native tokens to any account.
|
||||
> However, debiting native tokens from an account must always be performed by its owning program.
|
||||
|
||||
In this example, we will initialize the account for the Authenticated transfer program, which securely manages native token transfers by requiring authentication for debits.
|
||||
|
||||
Initialize the account by running:
|
||||
|
||||
```bash
|
||||
# This command submits a public transaction executing the `init` function of the
|
||||
# Authenticated-transfer program. The wallet polls the sequencer until the
|
||||
# transaction is included in a block, which may take several seconds.
|
||||
wallet auth-transfer init --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
```
|
||||
|
||||
After it completes, check the updated account status:
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":0}
|
||||
```
|
||||
|
||||
### Funding the account: executing the Piñata program
|
||||
|
||||
Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program.
|
||||
|
||||
```bash
|
||||
# Complete with your id
|
||||
wallet pinata claim --to Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
```
|
||||
|
||||
After the claim succeeds, the account will be funded with some tokens:
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":150}
|
||||
```
|
||||
|
||||
### Native token transfers: executing the Authenticated transfers program
|
||||
|
||||
NSSA comes with a program for managing and transferring native tokens. Run `wallet auth-transfer` to see the options available:
|
||||
```bash
|
||||
Commands:
|
||||
init Initialize account under authenticated transfer program
|
||||
send Send native tokens from one account to another with variable privacy
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
```
|
||||
|
||||
We have already used the `init` command. The `send` command is used to execute the `Transfer` function of the authenticated program.
|
||||
Let's try it. For that we need to create another account for the recipient of the transfer.
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
```
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
> The new account is uninitialized. The authenticated transfers program will claim any uninitialized account used in a transfer. So we don't need to manually initialize the recipient account.
|
||||
|
||||
Let's send 37 tokens to the new account.
|
||||
|
||||
```bash
|
||||
wallet auth-transfer send \
|
||||
--from Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ \
|
||||
--to Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
||||
--amount 37
|
||||
```
|
||||
|
||||
Once that succeeds we can check the states.
|
||||
|
||||
```bash
|
||||
# Sender account
|
||||
wallet account get --account-id Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":113}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Recipient account
|
||||
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":37}
|
||||
```
|
||||
|
||||
#### Create a new private account
|
||||
|
||||
> [!NOTE]
|
||||
> Private accounts are structurally identical to public accounts; they differ only in how their state is stored off-chain and represented on-chain.
|
||||
> The raw values of a private account are never stored on-chain. Instead, the chain only holds a 32-byte commitment (a hash-like binding to the actual values). Transactions include encrypted versions of the private values so that users can recover them from the blockchain. The decryption keys are known only to the user and are never shared.
|
||||
> Private accounts are not managed through the usual signature mechanism used for public accounts. Instead, each private account is associated with two keypairs:
|
||||
> - *Nullifier keys*, for using the corresponding private account in privacy preserving executions.
|
||||
> - *Viewing keys*, used for encrypting and decrypting the values included in transactions.
|
||||
>
|
||||
> Private accounts also have a 32-byte identifier, derived from the nullifier public key.
|
||||
>
|
||||
> Just like public accounts, private accounts can only be initialized once. Any user can initialize them without knowing the owner's secret keys. However, modifying an initialized private account through an off-chain program execution requires knowledge of the owner’s secret keys.
|
||||
>
|
||||
> Transactions that modify the values of a private account include a commitment to the new values, which will be added to the on-chain commitment set. They also include a nullifier that marks the previous version as old.
|
||||
> The nullifier is constructed so that it cannot be linked to any prior commitment, ensuring that updates to the same private account cannot be correlated.
|
||||
|
||||
Now let’s switch to the private state and create a private account.
|
||||
|
||||
```bash
|
||||
wallet account new private
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951
|
||||
With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17
|
||||
```
|
||||
|
||||
For now, focus only on the account id. Ignore the `npk` and `ipk` values. These are the Nullifier public key and the Viewing public key. They are stored locally in the wallet and are used internally to build privacy-preserving transactions.
|
||||
Also, the account id for private accounts is derived from the `npk` value. But we won't need them now.
|
||||
|
||||
Just like public accounts, new private accounts start out uninitialized:
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
|
||||
# Output:
|
||||
Account is Uninitialized
|
||||
```
|
||||
Unlike public accounts, private accounts are never visible to the network. They exist only in your local wallet storage.
|
||||
|
||||
#### Sending tokens from the public account to the private account
|
||||
|
||||
Sending tokens to an uninitialized private account causes the Authenticated-Transfers program to claim it. Just like with public accounts.
|
||||
This happens because program execution logic does not depend on whether the involved accounts are public or private.
|
||||
|
||||
Let’s send 17 tokens to the new private account.
|
||||
|
||||
The syntax is identical to the public-to-public transfer; just set the private ID as the recipient.
|
||||
|
||||
This command will run the Authenticated-Transfer program locally, generate a proof, and submit it to the sequencer. Depending on your machine, this can take from 30 seconds to 4 minutes.
|
||||
|
||||
```bash
|
||||
wallet auth-transfer send \
|
||||
--from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
||||
--to Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL \
|
||||
--amount 17
|
||||
```
|
||||
|
||||
After it succeeds, check both accounts:
|
||||
|
||||
```bash
|
||||
# Public sender account
|
||||
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":20}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Private recipient account
|
||||
wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":17}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The last command does not query the network.
|
||||
> It works even offline because private account data lives only in your wallet storage. Other users cannot read your private balances.
|
||||
|
||||
#### Digression: modifying private accounts
|
||||
|
||||
As a general rule, private accounts can only be modified through a program execution performed by their owner. That is, the person who holds the private key for that account. There is one exception: an uninitialized private account may be initialized by any user, without requiring the private key. After initialization, only the owner can modify it.
|
||||
|
||||
This mechanism enables a common use case: transferring funds from any account (public or private) to a private account owned by someone else. For such transfers, the recipient’s private account must be uninitialized.
|
||||
|
||||
|
||||
#### Sending tokens from the public account to a private account owned by someone else
|
||||
|
||||
For this tutorial, we’ll simulate that scenario by creating a new private account that we own, but we’ll treat it as if it belonged to someone else.
|
||||
|
||||
Let's create a new (uninitialized) private account like before:
|
||||
|
||||
```bash
|
||||
wallet account new private
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5
|
||||
With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e
|
||||
With ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72
|
||||
```
|
||||
|
||||
Now we'll ignore the private account ID and focus on the `npk` and `ipk` values. We'll need this to send tokens to a foreign private account. Syntax is very similar.
|
||||
|
||||
```bash
|
||||
wallet auth-transfer send \
|
||||
--from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
||||
--to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \
|
||||
--to-ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \
|
||||
--amount 3
|
||||
```
|
||||
|
||||
The command above produces a privacy-preserving transaction, which may take a few minutes to complete. The updated values of the private account are encrypted and included in the transaction.
|
||||
|
||||
Once the transaction is accepted, the recipient must run `wallet account sync-private`. This command scans the chain for encrypted values that belong to their private accounts and updates the local versions accordingly.
|
||||
|
||||
|
||||
#### Transfers in other combinations of public and private accounts
|
||||
|
||||
We’ve shown how to use the authenticated-transfers program for transfers between two public accounts, and for transfers from a public sender to a private recipient. Sending tokens from a private account (whether to a public account or to another private account) works in essentially the same way.
|
||||
|
||||
### The token program
|
||||
|
||||
So far, we’ve made transfers using the authenticated-transfers program, which handles native token transfers. The Token program, on the other hand, is used for creating and managing custom tokens.
|
||||
|
||||
> [!NOTE]
|
||||
> The token program is a single program responsible for creating and managing all tokens. There is no need to deploy new programs to introduce new tokens. All token-related operations are performed by invoking the appropriate functions of the token program.
|
||||
|
||||
The CLI provides commands to execute the token program. To see the options available run `wallet token`:
|
||||
|
||||
```bash
|
||||
Commands:
|
||||
new Produce a new token
|
||||
send Send tokens from one account to another with variable privacy
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
```
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
> The Token program manages its accounts in two categories. Meaning, all accounts owned by the Token program fall into one of these types.
|
||||
> - Token definition accounts: these accounts store metadata about a token, such as its name, total supply, and other identifying properties. They act as the token’s unique identifier.
|
||||
> - Token holding accounts: these accounts hold actual token balances. In addition to the balance, they also record which token definition they belong to.
|
||||
|
||||
#### Creating a new token
|
||||
|
||||
To create a new token, simply run `wallet token new`. This will create a transaction to execute the `New` function of the token program.
|
||||
The command expects a name, the desired total supply, and two uninitialized accounts:
|
||||
- One that will be initialized as the token definition account for the new token.
|
||||
- Another that will be initialized as a token holding account and receive the token’s entire initial supply.
|
||||
|
||||
|
||||
##### New token with both definition and supply accounts set as public
|
||||
|
||||
For example, let's create two new (uninitialized) public accounts and then use them to create a new token.
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
```
|
||||
|
||||
Now we use them to create a new token. Let's call it the "Token A"
|
||||
|
||||
```bash
|
||||
wallet token new \
|
||||
--name TOKENA \
|
||||
--total-supply 1337 \
|
||||
--definition-account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 \
|
||||
--supply-account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
```
|
||||
|
||||
After it succeeds, we can inspect the two accounts to see how they were initialized.
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
|
||||
# Output:
|
||||
Definition account owned by token program
|
||||
{"account_type":"Token definition","name":"TOKENA","total_supply":1337}
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
{"account_type":"Token holding","definition_id":"4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7","balance":1337}
|
||||
```
|
||||
|
||||
##### New token with public account definition but private holding account for initial supply
|
||||
|
||||
Let’s create a new token, but this time using a public definition account and a private holding account to store the entire supply.
|
||||
|
||||
Since we can’t reuse the accounts from the previous example, we need to create fresh ones for this case.
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account new private
|
||||
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
With npk 6a2dfe433cf28e525aa0196d719be3c16146f7ee358ca39595323f94fde38f93
|
||||
With ipk 03d59abf4bee974cc12ddb44641c19f0b5441fef39191f047c988c29a77252a577
|
||||
```
|
||||
|
||||
And we use them to create the token.
|
||||
|
||||
Now we use them to create a new token. Let's call it "Token B".
|
||||
|
||||
```bash
|
||||
wallet token new \
|
||||
--name TOKENB \
|
||||
--total-supply 7331 \
|
||||
--definition-account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii \
|
||||
--supply-account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
```
|
||||
|
||||
After it succeeds, we can check their values
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
||||
|
||||
# Output:
|
||||
Definition account owned by token program
|
||||
{"account_type":"Token definition","name":"TOKENB","total_supply":7331}
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":7331}
|
||||
```
|
||||
|
||||
Like any other private account owned by us, it cannot be seen by other users.
|
||||
|
||||
#### Custom token transfers
|
||||
|
||||
The Token program has a function to move funds from one token holding account to another one. If executed with an uninitialized account as the recipient, this will be automatically claimed by the token program.
|
||||
|
||||
The transfer function can be executed with the `wallet token send` command.
|
||||
|
||||
Let's create a new public account for the recipient.
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
||||
```
|
||||
|
||||
Let's send 1000 B tokens to this new account. We'll debit this from the supply account used in the creation of the token.
|
||||
|
||||
```bash
|
||||
wallet token send \
|
||||
--from Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF \
|
||||
--to Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
||||
--amount 1000
|
||||
```
|
||||
|
||||
Let's inspect the public account:
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":1000}
|
||||
```
|
||||
|
||||
### Chain information
|
||||
|
||||
The wallet provides some commands to query information about the chain. These are under the `wallet chain-info` command.
|
||||
|
||||
```bash
|
||||
Commands:
|
||||
current-block-id Get current block id from sequencer
|
||||
block Get block at id from sequencer
|
||||
transaction Get transaction at hash from sequencer
|
||||
```
|
||||
|
||||
For example, run this to find the current block id.
|
||||
|
||||
```bash
|
||||
wallet chain-info current-block-id
|
||||
|
||||
# Output:
|
||||
Last block id is 65537
|
||||
```
|
||||
|
||||
|
||||
### Automated Market Maker (AMM)
|
||||
|
||||
NSSA includes an AMM program that manages liquidity pools and enables swaps between custom tokens. To test this functionality, we first need to create a liquidity pool.
|
||||
|
||||
#### Creating a liquidity pool for a token pair
|
||||
|
||||
We start by creating a new pool for the tokens previously created. In return for providing liquidity, we will receive liquidity provider (LP) tokens, which represent our share of the pool and are required to withdraw liquidity later.
|
||||
|
||||
>[!NOTE]
|
||||
> The AMM program does not currently charge swap fees or distribute rewards to liquidity providers. LP tokens therefore only represent a proportional share of the pool reserves and do not provide additional value from swap activity. Fee support for liquidity providers will be added in future versions of the AMM program.
|
||||
|
||||
To hold these LP tokens, we first create a new account:
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf
|
||||
```
|
||||
|
||||
Next, we initialize the liquidity pool by depositing tokens A and B and specifying the account that will receive the LP tokens:
|
||||
|
||||
```bash
|
||||
wallet amm new \
|
||||
--user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \
|
||||
--user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
||||
--user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \
|
||||
--balance-a 100 \
|
||||
--balance-b 200
|
||||
```
|
||||
|
||||
The newly created account is owned by the token program, meaning that LP tokens are managed by the same token infrastructure as regular tokens.
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
{"account_type":"Token holding","definition_id":"7BeDS3e28MA5Err7gBswmR1fUKdHXqmUpTefNPu3pJ9i","balance":100}
|
||||
```
|
||||
|
||||
If you inspect the `user-holding-a` and `user-holding-b` accounts passed to the `wallet amm new` command, you will see that 100 and 200 tokens were deducted, respectively. These tokens now reside in the liquidity pool and are available for swaps by any user.
|
||||
|
||||
|
||||
#### Swaping
|
||||
|
||||
Token swaps can be performed using the wallet amm swap command:
|
||||
|
||||
```bash
|
||||
wallet amm swap \
|
||||
--user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \
|
||||
--user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
||||
# The amount of tokens to swap
|
||||
--amount-in 5 \
|
||||
# The minimum number of tokens expected in return
|
||||
--min-amount-out 8 \
|
||||
# The definition ID of the token being provided to the swap
|
||||
# In this case, we are swapping from TOKENA to TOKENB, and so this is the definition ID of TOKENA
|
||||
--token-definition 4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
```
|
||||
|
||||
Once executed, 5 tokens are deducted from the Token A holding account and the corresponding amount (determined by the pool’s pricing function) is credited to the Token B holding account.
|
||||
|
||||
|
||||
#### Withdrawing liquidity from the pool
|
||||
|
||||
Liquidity providers can withdraw assets from the pool by redeeming (burning) LP tokens. The amount of tokens received is proportional to the share of LP tokens being redeemed relative to the total LP supply.
|
||||
|
||||
This operation is performed using the `wallet amm remove-liquidity` command:
|
||||
|
||||
```bash
|
||||
wallet amm remove-liquidity \
|
||||
--user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \
|
||||
--user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
||||
--user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \
|
||||
--balance-lp 20 \
|
||||
--min-amount-a 1 \
|
||||
--min-amount-b 1
|
||||
```
|
||||
|
||||
This instruction burns `balance-lp` LP tokens from the user’s LP holding account. In exchange, the AMM transfers tokens A and B from the pool’s vault accounts to the user’s holding accounts, according to the current pool reserves.
|
||||
|
||||
The `min-amount-a` and `min-amount-b` parameters specify the minimum acceptable amounts of tokens A and B to be received. If the computed outputs fall below either threshold, the instruction fails, protecting the user against unfavorable pool state changes.
|
||||
|
||||
#### Adding liquidity to the pool
|
||||
|
||||
Additional liquidity can be added to an existing pool by depositing tokens A and B in the ratio implied by the current pool reserves. In return, new LP tokens are minted to represent the user’s proportional share of the pool.
|
||||
|
||||
This is done using the `wallet amm add-liquidity` command:
|
||||
|
||||
```bash
|
||||
wallet amm add-liquidity \
|
||||
--user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \
|
||||
--user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
||||
--user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \
|
||||
--min-amount-lp 1 \
|
||||
--max-amount-a 10 \
|
||||
--max-amount-b 10
|
||||
```
|
||||
|
||||
In this instruction, `max-amount-a` and `max-amount-b` define upper bounds on the number of tokens A and B that may be withdrawn from the user’s accounts. The AMM computes the actual required amounts based on the pool’s reserve ratio.
|
||||
|
||||
The `min-amount-lp` parameter specifies the minimum number of LP tokens that must be minted for the transaction to succeed. If the resulting LP token amount is below this threshold, the instruction fails.
|
||||
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "accounts"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde_json.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
k256.workspace = true
|
||||
sha2.workspace = true
|
||||
rand.workspace = true
|
||||
elliptic-curve.workspace = true
|
||||
hex.workspace = true
|
||||
aes-gcm.workspace = true
|
||||
|
||||
[dependencies.storage]
|
||||
path = "../storage"
|
||||
|
||||
[dependencies.utxo]
|
||||
path = "../utxo"
|
||||
@ -1,205 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use k256::AffinePoint;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use storage::{merkle_tree_public::TreeHashType, nullifier::UTXONullifier};
|
||||
use utxo::{
|
||||
utxo_core::{UTXOPayload, UTXO},
|
||||
utxo_tree::UTXOSparseMerkleTree,
|
||||
};
|
||||
|
||||
use crate::key_management::{
|
||||
constants_types::{CipherText, Nonce},
|
||||
ephemeral_key_holder::EphemeralKeyHolder,
|
||||
AddressKeyHolder,
|
||||
};
|
||||
|
||||
pub type PublicKey = AffinePoint;
|
||||
pub type AccountAddress = TreeHashType;
|
||||
|
||||
pub struct Account {
|
||||
pub key_holder: AddressKeyHolder,
|
||||
pub address: AccountAddress,
|
||||
pub balance: u64,
|
||||
pub utxo_tree: UTXOSparseMerkleTree,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new() -> Self {
|
||||
let key_holder = AddressKeyHolder::new_os_random();
|
||||
let address = key_holder.address;
|
||||
let balance = 0;
|
||||
let utxo_tree = UTXOSparseMerkleTree::new();
|
||||
|
||||
Self {
|
||||
key_holder,
|
||||
address,
|
||||
balance,
|
||||
utxo_tree,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_balance(balance: u64) -> Self {
|
||||
let key_holder = AddressKeyHolder::new_os_random();
|
||||
let address = key_holder.address;
|
||||
let utxo_tree = UTXOSparseMerkleTree::new();
|
||||
|
||||
Self {
|
||||
key_holder,
|
||||
address,
|
||||
balance,
|
||||
utxo_tree,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn produce_ephemeral_key_holder(&self) -> EphemeralKeyHolder {
|
||||
self.key_holder.produce_ephemeral_key_holder()
|
||||
}
|
||||
|
||||
pub fn encrypt_data(
|
||||
ephemeral_key_holder: &EphemeralKeyHolder,
|
||||
viewing_public_key_receiver: AffinePoint,
|
||||
data: &[u8],
|
||||
) -> (CipherText, Nonce) {
|
||||
ephemeral_key_holder.encrypt_data(viewing_public_key_receiver, data)
|
||||
}
|
||||
|
||||
pub fn decrypt_data(
|
||||
&self,
|
||||
ephemeral_public_key_sender: AffinePoint,
|
||||
ciphertext: CipherText,
|
||||
nonce: Nonce,
|
||||
) -> Result<Vec<u8>, aes_gcm::Error> {
|
||||
self.key_holder
|
||||
.decrypt_data(ephemeral_public_key_sender, ciphertext, nonce)
|
||||
}
|
||||
|
||||
pub fn mark_spent_utxo(
|
||||
&mut self,
|
||||
utxo_nullifier_map: HashMap<TreeHashType, UTXONullifier>,
|
||||
) -> Result<()> {
|
||||
for (hash, nullifier) in utxo_nullifier_map {
|
||||
if let Some(utxo_entry) = self.utxo_tree.store.get_mut(&hash) {
|
||||
utxo_entry.consume_utxo(nullifier)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_new_utxo_outputs(&mut self, utxos: Vec<UTXO>) -> Result<()> {
|
||||
Ok(self.utxo_tree.insert_items(utxos)?)
|
||||
}
|
||||
|
||||
pub fn update_public_balance(&mut self, new_balance: u64) {
|
||||
self.balance = new_balance;
|
||||
}
|
||||
|
||||
pub fn add_asset<Asset: Serialize>(
|
||||
&mut self,
|
||||
asset: Asset,
|
||||
amount: u128,
|
||||
privacy_flag: bool,
|
||||
) -> Result<()> {
|
||||
let payload_with_asset = UTXOPayload {
|
||||
owner: self.address,
|
||||
asset: serde_json::to_vec(&asset)?,
|
||||
amount,
|
||||
privacy_flag,
|
||||
};
|
||||
|
||||
let asset_utxo = UTXO::create_utxo_from_payload(payload_with_asset);
|
||||
|
||||
self.utxo_tree.insert_item(asset_utxo)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn log(&self) {
|
||||
info!("Keys generated");
|
||||
info!("Account address is {:?}", hex::encode(self.address));
|
||||
info!("Account balance is {:?}", self.balance);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Account {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn generate_dummy_utxo_nullifier() -> UTXONullifier {
|
||||
UTXONullifier::default()
|
||||
}
|
||||
|
||||
fn generate_dummy_utxo(address: TreeHashType, amount: u128) -> UTXO {
|
||||
let payload = UTXOPayload {
|
||||
owner: address,
|
||||
asset: vec![],
|
||||
amount,
|
||||
privacy_flag: false,
|
||||
};
|
||||
UTXO::create_utxo_from_payload(payload)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_account() {
|
||||
let account = Account::new();
|
||||
|
||||
assert_eq!(account.balance, 0);
|
||||
assert!(account.key_holder.address != [0u8; 32]); // Check if the address is not empty
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_spent_utxo() {
|
||||
let mut account = Account::new();
|
||||
let utxo = generate_dummy_utxo(account.address, 100);
|
||||
account.add_new_utxo_outputs(vec![utxo]).unwrap();
|
||||
|
||||
let mut utxo_nullifier_map = HashMap::new();
|
||||
utxo_nullifier_map.insert(account.address, generate_dummy_utxo_nullifier());
|
||||
|
||||
let result = account.mark_spent_utxo(utxo_nullifier_map);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(account.utxo_tree.store.get(&account.address).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_new_utxo_outputs() {
|
||||
let mut account = Account::new();
|
||||
let utxo1 = generate_dummy_utxo(account.address, 100);
|
||||
let utxo2 = generate_dummy_utxo(account.address, 200);
|
||||
|
||||
let result = account.add_new_utxo_outputs(vec![utxo1.clone(), utxo2.clone()]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(account.utxo_tree.store.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_public_balance() {
|
||||
let mut account = Account::new();
|
||||
account.update_public_balance(500);
|
||||
|
||||
assert_eq!(account.balance, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_asset() {
|
||||
let mut account = Account::new();
|
||||
let asset = "dummy_asset";
|
||||
let amount = 1000u128;
|
||||
|
||||
let result = account.add_asset(asset, amount, false);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(account.utxo_tree.store.len(), 1);
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
use elliptic_curve::{
|
||||
consts::{B0, B1},
|
||||
generic_array::GenericArray,
|
||||
};
|
||||
use sha2::digest::typenum::{UInt, UTerm};
|
||||
|
||||
pub const NULLIFIER_SECRET_CONST: [u8; 32] = [
|
||||
38, 29, 97, 210, 148, 172, 75, 220, 36, 249, 27, 111, 73, 14, 250, 38, 55, 87, 164, 169, 95,
|
||||
101, 135, 28, 212, 241, 107, 46, 162, 60, 59, 93,
|
||||
];
|
||||
pub const VIEVING_SECRET_CONST: [u8; 32] = [
|
||||
97, 23, 175, 117, 11, 48, 215, 162, 150, 103, 46, 195, 179, 178, 93, 52, 137, 190, 202, 60,
|
||||
254, 87, 112, 250, 57, 242, 117, 206, 195, 149, 213, 206,
|
||||
];
|
||||
|
||||
pub type CipherText = Vec<u8>;
|
||||
pub type Nonce = GenericArray<u8, UInt<UInt<UInt<UInt<UTerm, B1>, B1>, B0>, B0>>;
|
||||
@ -1,62 +0,0 @@
|
||||
use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, Key, KeyInit};
|
||||
use elliptic_curve::group::GroupEncoding;
|
||||
use elliptic_curve::PrimeField;
|
||||
use k256::{AffinePoint, FieldBytes, Scalar};
|
||||
use log::info;
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
|
||||
use super::constants_types::{CipherText, Nonce};
|
||||
|
||||
#[derive(Debug)]
|
||||
///Ephemeral secret key holder. Non-clonable as intended for one-time use. Produces ephemeral public keys. Can produce shared secret for sender.
|
||||
pub struct EphemeralKeyHolder {
|
||||
ephemeral_secret_key: Scalar,
|
||||
}
|
||||
|
||||
impl EphemeralKeyHolder {
|
||||
pub fn new_os_random() -> Self {
|
||||
let mut bytes = FieldBytes::default();
|
||||
|
||||
OsRng.fill_bytes(&mut bytes);
|
||||
|
||||
Self {
|
||||
ephemeral_secret_key: Scalar::from_repr(bytes).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_ephemeral_public_key(&self) -> AffinePoint {
|
||||
(AffinePoint::GENERATOR * self.ephemeral_secret_key).into()
|
||||
}
|
||||
|
||||
pub fn calculate_shared_secret_sender(
|
||||
&self,
|
||||
viewing_public_key_receiver: AffinePoint,
|
||||
) -> AffinePoint {
|
||||
(viewing_public_key_receiver * self.ephemeral_secret_key).into()
|
||||
}
|
||||
|
||||
pub fn encrypt_data(
|
||||
&self,
|
||||
viewing_public_key_receiver: AffinePoint,
|
||||
data: &[u8],
|
||||
) -> (CipherText, Nonce) {
|
||||
let key_point = self.calculate_shared_secret_sender(viewing_public_key_receiver);
|
||||
let binding = key_point.to_bytes();
|
||||
let key_raw = &binding.as_slice()[..32];
|
||||
let key_raw_adjust: [u8; 32] = key_raw.try_into().unwrap();
|
||||
|
||||
let key: Key<Aes256Gcm> = key_raw_adjust.into();
|
||||
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
|
||||
(cipher.encrypt(&nonce, data).unwrap(), nonce)
|
||||
}
|
||||
|
||||
pub fn log(&self) {
|
||||
info!(
|
||||
"Ephemeral private key is {:?}",
|
||||
hex::encode(self.ephemeral_secret_key.to_bytes())
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,383 +0,0 @@
|
||||
use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit};
|
||||
use constants_types::{CipherText, Nonce};
|
||||
use elliptic_curve::group::GroupEncoding;
|
||||
use ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use k256::AffinePoint;
|
||||
use log::info;
|
||||
use secret_holders::{SeedHolder, TopSecretKeyHolder, UTXOSecretKeyHolder};
|
||||
use storage::merkle_tree_public::TreeHashType;
|
||||
|
||||
use crate::account_core::PublicKey;
|
||||
|
||||
pub mod constants_types;
|
||||
pub mod ephemeral_key_holder;
|
||||
pub mod secret_holders;
|
||||
|
||||
#[derive(Clone)]
|
||||
///Entrypoint to key management
|
||||
pub struct AddressKeyHolder {
|
||||
//Will be useful in future
|
||||
#[allow(dead_code)]
|
||||
top_secret_key_holder: TopSecretKeyHolder,
|
||||
pub utxo_secret_key_holder: UTXOSecretKeyHolder,
|
||||
pub address: TreeHashType,
|
||||
pub nullifer_public_key: PublicKey,
|
||||
pub viewing_public_key: PublicKey,
|
||||
}
|
||||
|
||||
impl AddressKeyHolder {
|
||||
pub fn new_os_random() -> Self {
|
||||
//Currently dropping SeedHolder at the end of initialization.
|
||||
//Now entirely sure if we need it in the future.
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
let top_secret_key_holder = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let utxo_secret_key_holder = top_secret_key_holder.produce_utxo_secret_holder();
|
||||
|
||||
let address = utxo_secret_key_holder.generate_address();
|
||||
let nullifer_public_key = utxo_secret_key_holder.generate_nullifier_public_key();
|
||||
let viewing_public_key = utxo_secret_key_holder.generate_viewing_public_key();
|
||||
|
||||
Self {
|
||||
top_secret_key_holder,
|
||||
utxo_secret_key_holder,
|
||||
address,
|
||||
nullifer_public_key,
|
||||
viewing_public_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_shared_secret_receiver(
|
||||
&self,
|
||||
ephemeral_public_key_sender: AffinePoint,
|
||||
) -> AffinePoint {
|
||||
(ephemeral_public_key_sender * self.utxo_secret_key_holder.viewing_secret_key).into()
|
||||
}
|
||||
|
||||
pub fn produce_ephemeral_key_holder(&self) -> EphemeralKeyHolder {
|
||||
EphemeralKeyHolder::new_os_random()
|
||||
}
|
||||
|
||||
pub fn decrypt_data(
|
||||
&self,
|
||||
ephemeral_public_key_sender: AffinePoint,
|
||||
ciphertext: CipherText,
|
||||
nonce: Nonce,
|
||||
) -> Result<Vec<u8>, aes_gcm::Error> {
|
||||
let key_point = self.calculate_shared_secret_receiver(ephemeral_public_key_sender);
|
||||
let binding = key_point.to_bytes();
|
||||
let key_raw = &binding.as_slice()[..32];
|
||||
let key_raw_adjust: [u8; 32] = key_raw.try_into().unwrap();
|
||||
|
||||
let key: Key<Aes256Gcm> = key_raw_adjust.into();
|
||||
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
|
||||
cipher.decrypt(&nonce, ciphertext.as_slice())
|
||||
}
|
||||
|
||||
pub fn log(&self) {
|
||||
info!(
|
||||
"Secret spending key is {:?}",
|
||||
hex::encode(self.top_secret_key_holder.secret_spending_key.to_bytes()),
|
||||
);
|
||||
info!(
|
||||
"Nulifier secret key is {:?}",
|
||||
hex::encode(self.utxo_secret_key_holder.nullifier_secret_key.to_bytes()),
|
||||
);
|
||||
info!(
|
||||
"Viewing secret key is {:?}",
|
||||
hex::encode(self.utxo_secret_key_holder.viewing_secret_key.to_bytes()),
|
||||
);
|
||||
info!(
|
||||
"Nullifier public key is {:?}",
|
||||
hex::encode(self.nullifer_public_key.to_bytes()),
|
||||
);
|
||||
info!(
|
||||
"Viewing public key is {:?}",
|
||||
hex::encode(self.viewing_public_key.to_bytes()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use aes_gcm::{
|
||||
aead::{Aead, KeyInit, OsRng},
|
||||
Aes256Gcm,
|
||||
};
|
||||
use constants_types::{CipherText, Nonce};
|
||||
use constants_types::{NULLIFIER_SECRET_CONST, VIEVING_SECRET_CONST};
|
||||
use elliptic_curve::ff::Field;
|
||||
use elliptic_curve::group::prime::PrimeCurveAffine;
|
||||
use elliptic_curve::group::GroupEncoding;
|
||||
use k256::{AffinePoint, ProjectivePoint, Scalar};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_os_random() {
|
||||
// Ensure that a new AddressKeyHolder instance can be created without errors.
|
||||
let address_key_holder = AddressKeyHolder::new_os_random();
|
||||
|
||||
// Check that key holder fields are initialized with expected types
|
||||
assert!(!Into::<bool>::into(
|
||||
address_key_holder.nullifer_public_key.is_identity()
|
||||
));
|
||||
assert!(!Into::<bool>::into(
|
||||
address_key_holder.viewing_public_key.is_identity()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_shared_secret_receiver() {
|
||||
let address_key_holder = AddressKeyHolder::new_os_random();
|
||||
|
||||
// Generate a random ephemeral public key sender
|
||||
let scalar = Scalar::random(&mut OsRng);
|
||||
let ephemeral_public_key_sender = (ProjectivePoint::generator() * scalar).to_affine();
|
||||
|
||||
// Calculate shared secret
|
||||
let shared_secret =
|
||||
address_key_holder.calculate_shared_secret_receiver(ephemeral_public_key_sender);
|
||||
|
||||
// Ensure the shared secret is not an identity point (suggesting non-zero output)
|
||||
assert!(!Into::<bool>::into(shared_secret.is_identity()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_data() {
|
||||
let address_key_holder = AddressKeyHolder::new_os_random();
|
||||
|
||||
// Generate an ephemeral key and shared secret
|
||||
let scalar = Scalar::random(OsRng);
|
||||
let ephemeral_public_key_sender = address_key_holder
|
||||
.produce_ephemeral_key_holder()
|
||||
.generate_ephemeral_public_key();
|
||||
let shared_secret =
|
||||
address_key_holder.calculate_shared_secret_receiver(ephemeral_public_key_sender);
|
||||
|
||||
// Prepare the encryption key from shared secret
|
||||
let key_raw = shared_secret.to_bytes();
|
||||
let key_raw_adjust_pre = &key_raw.as_slice()[..32];
|
||||
let key_raw_adjust: [u8; 32] = key_raw_adjust_pre.try_into().unwrap();
|
||||
let key: Key<Aes256Gcm> = key_raw_adjust.into();
|
||||
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
|
||||
// Encrypt sample data
|
||||
let nonce = Nonce::from_slice(b"unique nonce");
|
||||
let plaintext = b"Sensitive data";
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, plaintext.as_ref())
|
||||
.expect("encryption failure");
|
||||
|
||||
// Attempt decryption
|
||||
let decrypted_data: Vec<u8> = address_key_holder
|
||||
.decrypt_data(
|
||||
ephemeral_public_key_sender,
|
||||
CipherText::from(ciphertext),
|
||||
nonce.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Verify decryption is successful and matches original plaintext
|
||||
assert_eq!(decrypted_data, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_os_random_initialization() {
|
||||
// Ensure that AddressKeyHolder is initialized correctly
|
||||
let address_key_holder = AddressKeyHolder::new_os_random();
|
||||
|
||||
// Check that key holder fields are initialized with expected types and values
|
||||
assert!(!Into::<bool>::into(
|
||||
address_key_holder.nullifer_public_key.is_identity()
|
||||
));
|
||||
assert!(!Into::<bool>::into(
|
||||
address_key_holder.viewing_public_key.is_identity()
|
||||
));
|
||||
assert!(address_key_holder.address.as_slice().len() > 0); // Assume TreeHashType has non-zero length for a valid address
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_shared_secret_with_identity_point() {
|
||||
let address_key_holder = AddressKeyHolder::new_os_random();
|
||||
|
||||
// Use identity point as ephemeral public key
|
||||
let identity_point = AffinePoint::identity();
|
||||
|
||||
// Calculate shared secret
|
||||
let shared_secret = address_key_holder.calculate_shared_secret_receiver(identity_point);
|
||||
|
||||
// The shared secret with the identity point should also result in the identity point
|
||||
assert!(Into::<bool>::into(shared_secret.is_identity()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_decrypt_data_with_incorrect_nonce() {
|
||||
let address_key_holder = AddressKeyHolder::new_os_random();
|
||||
|
||||
// Generate ephemeral public key and shared secret
|
||||
let scalar = Scalar::random(OsRng);
|
||||
let ephemeral_public_key_sender = (ProjectivePoint::generator() * scalar).to_affine();
|
||||
let shared_secret =
|
||||
address_key_holder.calculate_shared_secret_receiver(ephemeral_public_key_sender);
|
||||
|
||||
// Prepare the encryption key from shared secret
|
||||
let key_raw = shared_secret.to_bytes();
|
||||
let key_raw_adjust_pre = &key_raw.as_slice()[..32];
|
||||
let key_raw_adjust: [u8; 32] = key_raw_adjust_pre.try_into().unwrap();
|
||||
let key: Key<Aes256Gcm> = key_raw_adjust.into();
|
||||
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
|
||||
// Encrypt sample data with a specific nonce
|
||||
let nonce = Nonce::from_slice(b"unique nonce");
|
||||
let plaintext = b"Sensitive data";
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, plaintext.as_ref())
|
||||
.expect("encryption failure");
|
||||
|
||||
// Attempt decryption with an incorrect nonce
|
||||
let incorrect_nonce = Nonce::from_slice(b"wrong nonce");
|
||||
let decrypted_data = address_key_holder
|
||||
.decrypt_data(
|
||||
ephemeral_public_key_sender,
|
||||
CipherText::from(ciphertext.clone()),
|
||||
incorrect_nonce.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The decryption should fail or produce incorrect output due to nonce mismatch
|
||||
assert_ne!(decrypted_data, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_decrypt_data_with_incorrect_ciphertext() {
|
||||
let address_key_holder = AddressKeyHolder::new_os_random();
|
||||
|
||||
// Generate ephemeral public key and shared secret
|
||||
let scalar = Scalar::random(OsRng);
|
||||
let ephemeral_public_key_sender = (ProjectivePoint::generator() * scalar).to_affine();
|
||||
let shared_secret =
|
||||
address_key_holder.calculate_shared_secret_receiver(ephemeral_public_key_sender);
|
||||
|
||||
// Prepare the encryption key from shared secret
|
||||
let key_raw = shared_secret.to_bytes();
|
||||
let key_raw_adjust_pre = &key_raw.as_slice()[..32];
|
||||
let key_raw_adjust: [u8; 32] = key_raw_adjust_pre.try_into().unwrap();
|
||||
let key: Key<Aes256Gcm> = key_raw_adjust.into();
|
||||
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
|
||||
// Encrypt sample data
|
||||
let nonce = Nonce::from_slice(b"unique nonce");
|
||||
let plaintext = b"Sensitive data";
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, plaintext.as_ref())
|
||||
.expect("encryption failure");
|
||||
|
||||
// Tamper with the ciphertext to simulate corruption
|
||||
let mut corrupted_ciphertext = ciphertext.clone();
|
||||
corrupted_ciphertext[0] ^= 1; // Flip a bit in the ciphertext
|
||||
|
||||
// Attempt decryption
|
||||
let result = address_key_holder
|
||||
.decrypt_data(
|
||||
ephemeral_public_key_sender,
|
||||
CipherText::from(corrupted_ciphertext),
|
||||
nonce.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The decryption should fail or produce incorrect output due to tampered ciphertext
|
||||
assert_ne!(result, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_decryption_round_trip() {
|
||||
let address_key_holder = AddressKeyHolder::new_os_random();
|
||||
|
||||
// Generate ephemeral key and shared secret
|
||||
let scalar = Scalar::random(OsRng);
|
||||
let ephemeral_public_key_sender = (ProjectivePoint::generator() * scalar).to_affine();
|
||||
|
||||
// Encrypt sample data
|
||||
let plaintext = b"Round-trip test data";
|
||||
let nonce = Nonce::from_slice(b"unique nonce");
|
||||
|
||||
let shared_secret =
|
||||
address_key_holder.calculate_shared_secret_receiver(ephemeral_public_key_sender);
|
||||
// Prepare the encryption key from shared secret
|
||||
let key_raw = shared_secret.to_bytes();
|
||||
let key_raw_adjust_pre = &key_raw.as_slice()[..32];
|
||||
let key_raw_adjust: [u8; 32] = key_raw_adjust_pre.try_into().unwrap();
|
||||
let key: Key<Aes256Gcm> = key_raw_adjust.into();
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, plaintext.as_ref())
|
||||
.expect("encryption failure");
|
||||
|
||||
// Decrypt the data using the `AddressKeyHolder` instance
|
||||
let decrypted_data = address_key_holder
|
||||
.decrypt_data(
|
||||
ephemeral_public_key_sender,
|
||||
CipherText::from(ciphertext),
|
||||
nonce.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Verify the decrypted data matches the original plaintext
|
||||
assert_eq!(decrypted_data, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_generation_test() {
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
let top_secret_key_holder = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let utxo_secret_key_holder = top_secret_key_holder.produce_utxo_secret_holder();
|
||||
|
||||
let address = utxo_secret_key_holder.generate_address();
|
||||
let nullifer_public_key = utxo_secret_key_holder.generate_nullifier_public_key();
|
||||
let viewing_public_key = utxo_secret_key_holder.generate_viewing_public_key();
|
||||
|
||||
println!("======Prerequisites======");
|
||||
println!();
|
||||
|
||||
println!(
|
||||
"Group generator {:?}",
|
||||
hex::encode(AffinePoint::GENERATOR.to_bytes())
|
||||
);
|
||||
println!(
|
||||
"Nullifier constant {:?}",
|
||||
hex::encode(NULLIFIER_SECRET_CONST)
|
||||
);
|
||||
println!("Viewing constatnt {:?}", hex::encode(VIEVING_SECRET_CONST));
|
||||
println!();
|
||||
|
||||
println!("======Holders======");
|
||||
println!();
|
||||
|
||||
println!("{seed_holder:?}");
|
||||
println!("{top_secret_key_holder:?}");
|
||||
println!("{utxo_secret_key_holder:?}");
|
||||
println!();
|
||||
|
||||
println!("======Public data======");
|
||||
println!();
|
||||
println!("Address{:?}", hex::encode(address));
|
||||
println!(
|
||||
"Nulifier public key {:?}",
|
||||
hex::encode(nullifer_public_key.to_bytes())
|
||||
);
|
||||
println!(
|
||||
"Viewing public key {:?}",
|
||||
hex::encode(viewing_public_key.to_bytes())
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
use elliptic_curve::group::GroupEncoding;
|
||||
use elliptic_curve::PrimeField;
|
||||
use k256::{AffinePoint, FieldBytes, Scalar};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use sha2::{digest::FixedOutput, Digest};
|
||||
use storage::merkle_tree_public::TreeHashType;
|
||||
|
||||
use super::constants_types::{NULLIFIER_SECRET_CONST, VIEVING_SECRET_CONST};
|
||||
|
||||
#[derive(Debug)]
|
||||
///Seed holder. Non-clonable to ensure that different holders use different seeds.
|
||||
/// Produces `TopSecretKeyHolder` objects.
|
||||
pub struct SeedHolder {
|
||||
seed: Scalar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
///Secret spending key holder. Produces `UTXOSecretKeyHolder` objects.
|
||||
pub struct TopSecretKeyHolder {
|
||||
pub secret_spending_key: Scalar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
///Nullifier secret key and viewing secret key holder. Produces public keys. Can produce address. Can produce shared secret for recepient.
|
||||
pub struct UTXOSecretKeyHolder {
|
||||
pub nullifier_secret_key: Scalar,
|
||||
pub viewing_secret_key: Scalar,
|
||||
}
|
||||
|
||||
impl SeedHolder {
|
||||
pub fn new_os_random() -> Self {
|
||||
let mut bytes = FieldBytes::default();
|
||||
|
||||
OsRng.fill_bytes(&mut bytes);
|
||||
|
||||
Self {
|
||||
seed: Scalar::from_repr(bytes).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_secret_spending_key_hash(&self) -> TreeHashType {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
|
||||
hasher.update(self.seed.to_bytes());
|
||||
|
||||
<TreeHashType>::from(hasher.finalize_fixed())
|
||||
}
|
||||
|
||||
pub fn generate_secret_spending_key_scalar(&self) -> Scalar {
|
||||
let hash = self.generate_secret_spending_key_hash();
|
||||
|
||||
Scalar::from_repr(hash.into()).unwrap()
|
||||
}
|
||||
|
||||
pub fn produce_top_secret_key_holder(&self) -> TopSecretKeyHolder {
|
||||
TopSecretKeyHolder {
|
||||
secret_spending_key: self.generate_secret_spending_key_scalar(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TopSecretKeyHolder {
|
||||
pub fn generate_nullifier_secret_key(&self) -> Scalar {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
|
||||
hasher.update(self.secret_spending_key.to_bytes());
|
||||
hasher.update(NULLIFIER_SECRET_CONST);
|
||||
|
||||
let hash = <TreeHashType>::from(hasher.finalize_fixed());
|
||||
|
||||
Scalar::from_repr(hash.into()).unwrap()
|
||||
}
|
||||
|
||||
pub fn generate_viewing_secret_key(&self) -> Scalar {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
|
||||
hasher.update(self.secret_spending_key.to_bytes());
|
||||
hasher.update(VIEVING_SECRET_CONST);
|
||||
|
||||
let hash = <TreeHashType>::from(hasher.finalize_fixed());
|
||||
|
||||
Scalar::from_repr(hash.into()).unwrap()
|
||||
}
|
||||
|
||||
pub fn produce_utxo_secret_holder(&self) -> UTXOSecretKeyHolder {
|
||||
UTXOSecretKeyHolder {
|
||||
nullifier_secret_key: self.generate_nullifier_secret_key(),
|
||||
viewing_secret_key: self.generate_viewing_secret_key(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UTXOSecretKeyHolder {
|
||||
pub fn generate_nullifier_public_key(&self) -> AffinePoint {
|
||||
(AffinePoint::GENERATOR * self.nullifier_secret_key).into()
|
||||
}
|
||||
|
||||
pub fn generate_viewing_public_key(&self) -> AffinePoint {
|
||||
(AffinePoint::GENERATOR * self.viewing_secret_key).into()
|
||||
}
|
||||
|
||||
pub fn generate_address(&self) -> TreeHashType {
|
||||
let npk = self.generate_nullifier_public_key();
|
||||
let vpk = self.generate_viewing_public_key();
|
||||
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
|
||||
hasher.update(npk.to_bytes());
|
||||
hasher.update(vpk.to_bytes());
|
||||
|
||||
<TreeHashType>::from(hasher.finalize_fixed())
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
pub mod account_core;
|
||||
pub mod key_management;
|
||||
BIN
artifacts/program_methods/amm.bin
Normal file
BIN
artifacts/program_methods/amm.bin
Normal file
Binary file not shown.
BIN
artifacts/program_methods/authenticated_transfer.bin
Normal file
BIN
artifacts/program_methods/authenticated_transfer.bin
Normal file
Binary file not shown.
BIN
artifacts/program_methods/pinata.bin
Normal file
BIN
artifacts/program_methods/pinata.bin
Normal file
Binary file not shown.
BIN
artifacts/program_methods/pinata_token.bin
Normal file
BIN
artifacts/program_methods/pinata_token.bin
Normal file
Binary file not shown.
BIN
artifacts/program_methods/privacy_preserving_circuit.bin
Normal file
BIN
artifacts/program_methods/privacy_preserving_circuit.bin
Normal file
Binary file not shown.
BIN
artifacts/program_methods/token.bin
Normal file
BIN
artifacts/program_methods/token.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/burner.bin
Normal file
BIN
artifacts/test_program_methods/burner.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/chain_caller.bin
Normal file
BIN
artifacts/test_program_methods/chain_caller.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/claimer.bin
Normal file
BIN
artifacts/test_program_methods/claimer.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/data_changer.bin
Normal file
BIN
artifacts/test_program_methods/data_changer.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/extra_output.bin
Normal file
BIN
artifacts/test_program_methods/extra_output.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/minter.bin
Normal file
BIN
artifacts/test_program_methods/minter.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/missing_output.bin
Normal file
BIN
artifacts/test_program_methods/missing_output.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/modified_transfer.bin
Normal file
BIN
artifacts/test_program_methods/modified_transfer.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/nonce_changer.bin
Normal file
BIN
artifacts/test_program_methods/nonce_changer.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/noop.bin
Normal file
BIN
artifacts/test_program_methods/noop.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/program_owner_changer.bin
Normal file
BIN
artifacts/test_program_methods/program_owner_changer.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/simple_balance_transfer.bin
Normal file
BIN
artifacts/test_program_methods/simple_balance_transfer.bin
Normal file
Binary file not shown.
@ -1,4 +0,0 @@
|
||||
set -e
|
||||
curl -L https://risczero.com/install | bash
|
||||
/Users/runner/.risc0/bin/rzup install
|
||||
cargo build
|
||||
@ -1,4 +0,0 @@
|
||||
set -e
|
||||
curl -L https://risczero.com/install | bash
|
||||
/home/runner/.risc0/bin/rzup install
|
||||
cargo build
|
||||
84
ci_scripts/deploy.sh
Normal file
84
ci_scripts/deploy.sh
Normal file
@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Base directory for deployment
|
||||
LSSA_DIR="/home/arjentix/test_deploy/lssa"
|
||||
|
||||
# Expect GITHUB_ACTOR to be passed as first argument or environment variable
|
||||
GITHUB_ACTOR="${1:-${GITHUB_ACTOR:-unknown}}"
|
||||
|
||||
# Function to log messages with timestamp
|
||||
log_deploy() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] $1" >> "${LSSA_DIR}/deploy.log"
|
||||
}
|
||||
|
||||
# Error handler
|
||||
handle_error() {
|
||||
echo "✗ Deployment failed by: ${GITHUB_ACTOR}"
|
||||
log_deploy "Deployment failed by: ${GITHUB_ACTOR}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
find_sequencer_runner_pids() {
|
||||
pgrep -f "sequencer_runner" | grep -v $$
|
||||
}
|
||||
|
||||
# Set trap to catch any errors
|
||||
trap 'handle_error' ERR
|
||||
|
||||
# Log deployment info
|
||||
log_deploy "Deployment initiated by: ${GITHUB_ACTOR}"
|
||||
|
||||
# Navigate to code directory
|
||||
if [ ! -d "${LSSA_DIR}/code" ]; then
|
||||
mkdir -p "${LSSA_DIR}/code"
|
||||
fi
|
||||
cd "${LSSA_DIR}/code"
|
||||
|
||||
# Stop current sequencer if running
|
||||
if find_sequencer_runner_pids > /dev/null; then
|
||||
echo "Stopping current sequencer..."
|
||||
find_sequencer_runner_pids | xargs -r kill -SIGINT || true
|
||||
sleep 2
|
||||
# Force kill if still running
|
||||
find_sequencer_runner_pids | grep -v $$ | xargs -r kill -9 || true
|
||||
fi
|
||||
|
||||
# Clone or update repository
|
||||
if [ -d ".git" ]; then
|
||||
echo "Updating existing repository..."
|
||||
git fetch origin
|
||||
git checkout main
|
||||
git reset --hard origin/main
|
||||
else
|
||||
echo "Cloning repository..."
|
||||
git clone https://github.com/vacp2p/nescience-testnet.git .
|
||||
git checkout main
|
||||
fi
|
||||
|
||||
# Build sequencer_runner and wallet in release mode
|
||||
echo "Building sequencer_runner"
|
||||
# That could be just `cargo build --release --bin sequencer_runner --bin wallet`
|
||||
# but we have `no_docker` feature bug, see issue #179
|
||||
cd sequencer_runner
|
||||
cargo build --release
|
||||
cd ../wallet
|
||||
cargo build --release
|
||||
cd ..
|
||||
|
||||
# Run sequencer_runner with config
|
||||
echo "Starting sequencer_runner..."
|
||||
export RUST_LOG=info
|
||||
nohup ./target/release/sequencer_runner "${LSSA_DIR}/configs/sequencer" > "${LSSA_DIR}/sequencer.log" 2>&1 &
|
||||
|
||||
# Wait 5 seconds and check health using wallet
|
||||
sleep 5
|
||||
if ./target/release/wallet check-health; then
|
||||
echo "✓ Sequencer started successfully and is healthy"
|
||||
log_deploy "Deployment completed successfully by: ${GITHUB_ACTOR}"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Sequencer failed health check"
|
||||
tail -n 50 "${LSSA_DIR}/sequencer.log"
|
||||
handle_error
|
||||
fi
|
||||
@ -1,8 +0,0 @@
|
||||
set -e
|
||||
|
||||
curl -L https://risczero.com/install | bash
|
||||
/home/runner/.risc0/bin/rzup install
|
||||
cargo install taplo-cli --locked
|
||||
|
||||
cargo fmt -- --check
|
||||
taplo fmt --check
|
||||
@ -1,6 +0,0 @@
|
||||
set -e
|
||||
|
||||
curl -L https://risczero.com/install | bash
|
||||
/home/runner/.risc0/bin/rzup install
|
||||
|
||||
cargo test --release
|
||||
@ -1,17 +1,19 @@
|
||||
[package]
|
||||
name = "utxo"
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde_json.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
monotree.workspace = true
|
||||
sha2.workspace = true
|
||||
hex.workspace = true
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
|
||||
[dependencies.storage]
|
||||
path = "../storage"
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
reqwest.workspace = true
|
||||
sha2.workspace = true
|
||||
log.workspace = true
|
||||
hex.workspace = true
|
||||
borsh.workspace = true
|
||||
base64.workspace = true
|
||||
98
common/src/block.rs
Normal file
98
common/src/block.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use sha2::{Digest, Sha256, digest::FixedOutput};
|
||||
|
||||
use crate::transaction::EncodedTransaction;
|
||||
|
||||
pub type HashType = [u8; 32];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Our own hasher.
|
||||
/// Currently it is SHA256 hasher wrapper. May change in a future.
|
||||
pub struct OwnHasher {}
|
||||
|
||||
impl OwnHasher {
|
||||
fn hash(data: &[u8]) -> HashType {
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(data);
|
||||
<HashType>::from(hasher.finalize_fixed())
|
||||
}
|
||||
}
|
||||
|
||||
pub type BlockHash = [u8; 32];
|
||||
pub type BlockId = u64;
|
||||
pub type TimeStamp = u64;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockHeader {
|
||||
pub block_id: BlockId,
|
||||
pub prev_block_hash: BlockHash,
|
||||
pub hash: BlockHash,
|
||||
pub timestamp: TimeStamp,
|
||||
pub signature: nssa::Signature,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockBody {
|
||||
pub transactions: Vec<EncodedTransaction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Block {
|
||||
pub header: BlockHeader,
|
||||
pub body: BlockBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct HashableBlockData {
|
||||
pub block_id: BlockId,
|
||||
pub prev_block_hash: BlockHash,
|
||||
pub timestamp: TimeStamp,
|
||||
pub transactions: Vec<EncodedTransaction>,
|
||||
}
|
||||
|
||||
impl HashableBlockData {
|
||||
pub fn into_block(self, signing_key: &nssa::PrivateKey) -> Block {
|
||||
let data_bytes = borsh::to_vec(&self).unwrap();
|
||||
let signature = nssa::Signature::new(signing_key, &data_bytes);
|
||||
let hash = OwnHasher::hash(&data_bytes);
|
||||
Block {
|
||||
header: BlockHeader {
|
||||
block_id: self.block_id,
|
||||
prev_block_hash: self.prev_block_hash,
|
||||
hash,
|
||||
timestamp: self.timestamp,
|
||||
signature,
|
||||
},
|
||||
body: BlockBody {
|
||||
transactions: self.transactions,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Block> for HashableBlockData {
|
||||
fn from(value: Block) -> Self {
|
||||
Self {
|
||||
block_id: value.header.block_id,
|
||||
prev_block_hash: value.header.prev_block_hash,
|
||||
timestamp: value.header.timestamp,
|
||||
transactions: value.body.transactions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{block::HashableBlockData, test_utils};
|
||||
|
||||
#[test]
|
||||
fn test_encoding_roundtrip() {
|
||||
let transactions = vec![test_utils::produce_dummy_empty_transaction()];
|
||||
let block = test_utils::produce_dummy_block(1, Some([1; 32]), transactions);
|
||||
let hashable = HashableBlockData::from(block);
|
||||
let bytes = borsh::to_vec(&hashable).unwrap();
|
||||
let block_from_bytes = borsh::from_slice::<HashableBlockData>(&bytes).unwrap();
|
||||
assert_eq!(hashable, block_from_bytes);
|
||||
}
|
||||
}
|
||||
55
common/src/error.rs
Normal file
55
common/src/error.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use nssa::AccountId;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::rpc_primitives::errors::RpcError;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SequencerRpcError {
|
||||
pub jsonrpc: String,
|
||||
pub error: RpcError,
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SequencerClientError {
|
||||
#[error("HTTP error")]
|
||||
HTTPError(reqwest::Error),
|
||||
#[error("Serde error")]
|
||||
SerdeError(serde_json::Error),
|
||||
#[error("Internal error")]
|
||||
InternalError(SequencerRpcError),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for SequencerClientError {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
SequencerClientError::HTTPError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for SequencerClientError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
SequencerClientError::SerdeError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SequencerRpcError> for SequencerClientError {
|
||||
fn from(value: SequencerRpcError) -> Self {
|
||||
SequencerClientError::InternalError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ExecutionFailureKind {
|
||||
#[error("Failed to get account data from sequencer")]
|
||||
SequencerError,
|
||||
#[error("Inputs amounts does not match outputs")]
|
||||
AmountMismatchError,
|
||||
#[error("Accounts key not found")]
|
||||
KeyNotFoundError,
|
||||
#[error("Sequencer client error: {0:?}")]
|
||||
SequencerClientError(#[from] SequencerClientError),
|
||||
#[error("Can not pay for operation")]
|
||||
InsufficientFundsError,
|
||||
#[error("Account {0} data is invalid")]
|
||||
AccountDataError(AccountId),
|
||||
}
|
||||
12
common/src/lib.rs
Normal file
12
common/src/lib.rs
Normal file
@ -0,0 +1,12 @@
|
||||
pub mod block;
|
||||
pub mod error;
|
||||
pub mod rpc_primitives;
|
||||
pub mod sequencer_client;
|
||||
pub mod transaction;
|
||||
|
||||
// Module for tests utility functions
|
||||
// TODO: Compile only for tests
|
||||
pub mod test_utils;
|
||||
pub type HashType = [u8; 32];
|
||||
|
||||
pub const PINATA_BASE58: &str = "EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7";
|
||||
@ -1,6 +1,7 @@
|
||||
use serde_json::{to_value, Value};
|
||||
use std::fmt;
|
||||
|
||||
use serde_json::{Value, to_value};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct RpcParseError(pub String);
|
||||
|
||||
@ -65,7 +66,7 @@ impl RpcError {
|
||||
return Self::server_error(Some(format!(
|
||||
"Failed to serialize invalid parameters error: {:?}",
|
||||
err.to_string()
|
||||
)))
|
||||
)));
|
||||
}
|
||||
};
|
||||
RpcError::new(-32_602, "Invalid params".to_owned(), Some(value))
|
||||
@ -178,7 +179,7 @@ impl From<ServerError> for RpcError {
|
||||
let error_data = match to_value(&e) {
|
||||
Ok(value) => value,
|
||||
Err(_err) => {
|
||||
return RpcError::new_internal_error(None, "Failed to serialize ServerError")
|
||||
return RpcError::new_internal_error(None, "Failed to serialize ServerError");
|
||||
}
|
||||
};
|
||||
RpcError::new_internal_error(Some(error_data), e.to_string().as_str())
|
||||
@ -9,11 +9,14 @@
|
||||
//!
|
||||
//! The main entrypoint here is the [Message](enum.Message.html). The others are just building
|
||||
//! blocks and you should generally work with `Message` instead.
|
||||
use serde::de::{Deserializer, Error, Unexpected, Visitor};
|
||||
use serde::ser::{SerializeStruct, Serializer};
|
||||
use serde_json::{Result as JsonResult, Value};
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
use serde::{
|
||||
de::{Deserializer, Error, Unexpected, Visitor},
|
||||
ser::{SerializeStruct, Serializer},
|
||||
};
|
||||
use serde_json::{Result as JsonResult, Value};
|
||||
|
||||
use super::errors::RpcError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@ -59,6 +62,16 @@ pub struct Request {
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn from_payload_version_2_0(method: String, payload: serde_json::Value) -> Self {
|
||||
Self {
|
||||
jsonrpc: Version,
|
||||
method,
|
||||
params: payload,
|
||||
// ToDo: Correct checking of id
|
||||
id: 1.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Answer the request with a (positive) reply.
|
||||
///
|
||||
/// The ID is taken from the request.
|
||||
@ -69,6 +82,7 @@ impl Request {
|
||||
id: self.id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Answer the request with an error.
|
||||
pub fn error(&self, error: RpcError) -> Message {
|
||||
Message::Response(Response {
|
||||
@ -207,6 +221,7 @@ impl Message {
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a top-level error (without an ID).
|
||||
pub fn error(error: RpcError) -> Self {
|
||||
Message::Response(Response {
|
||||
@ -215,6 +230,7 @@ impl Message {
|
||||
id: Value::Null,
|
||||
})
|
||||
}
|
||||
|
||||
/// A constructor for a notification.
|
||||
pub fn notification(method: String, params: Value) -> Self {
|
||||
Message::Notification(Notification {
|
||||
@ -223,6 +239,7 @@ impl Message {
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/// A constructor for a response.
|
||||
pub fn response(id: Value, result: Result<Value, RpcError>) -> Self {
|
||||
Message::Response(Response {
|
||||
@ -231,6 +248,7 @@ impl Message {
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns id or Null if there is no id.
|
||||
pub fn id(&self) -> Value {
|
||||
match self {
|
||||
@ -315,10 +333,7 @@ impl From<Message> for Vec<u8> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::de::from_slice;
|
||||
use serde_json::json;
|
||||
use serde_json::ser::to_vec;
|
||||
use serde_json::Value;
|
||||
use serde_json::{Value, de::from_slice, json, ser::to_vec};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -447,7 +462,7 @@ mod tests {
|
||||
/// A helper for the `broken` test.
|
||||
///
|
||||
/// Check that the given JSON string parses, but is not recognized as a valid RPC message.
|
||||
|
||||
///
|
||||
/// Test things that are almost but not entirely JSONRPC are rejected
|
||||
///
|
||||
/// The reject is done by returning it as Unmatched.
|
||||
@ -1,25 +1,9 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod errors;
|
||||
pub mod message;
|
||||
pub mod parser;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub struct RpcPollingConfig {
|
||||
pub polling_interval: Duration,
|
||||
pub polling_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for RpcPollingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
polling_interval: Duration::from_millis(500),
|
||||
polling_timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod requests;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct RpcLimitsConfig {
|
||||
@ -39,7 +23,6 @@ impl Default for RpcLimitsConfig {
|
||||
pub struct RpcConfig {
|
||||
pub addr: String,
|
||||
pub cors_allowed_origins: Vec<String>,
|
||||
pub polling_config: RpcPollingConfig,
|
||||
#[serde(default)]
|
||||
pub limits_config: RpcLimitsConfig,
|
||||
}
|
||||
@ -49,7 +32,6 @@ impl Default for RpcConfig {
|
||||
RpcConfig {
|
||||
addr: "0.0.0.0:3040".to_owned(),
|
||||
cors_allowed_origins: vec!["*".to_owned()],
|
||||
polling_config: RpcPollingConfig::default(),
|
||||
limits_config: RpcLimitsConfig::default(),
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::errors::RpcParseError;
|
||||
use super::errors::RpcParseError;
|
||||
|
||||
pub trait RpcRequest: Sized {
|
||||
fn parse(value: Option<Value>) -> Result<Self, RpcParseError>;
|
||||
218
common/src/rpc_primitives/requests.rs
Normal file
218
common/src/rpc_primitives/requests.rs
Normal file
@ -0,0 +1,218 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use nssa_core::program::ProgramId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
errors::RpcParseError,
|
||||
parser::{RpcRequest, parse_params},
|
||||
};
|
||||
use crate::parse_request;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct HelloRequest {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct RegisterAccountRequest {
|
||||
pub account_id: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SendTxRequest {
|
||||
#[serde(with = "base64_deser")]
|
||||
pub transaction: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetBlockDataRequest {
|
||||
pub block_id: u64,
|
||||
}
|
||||
|
||||
/// Get a range of blocks from `start_block_id` to `end_block_id` (inclusive)
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetBlockRangeDataRequest {
|
||||
pub start_block_id: u64,
|
||||
pub end_block_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetGenesisIdRequest {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetLastBlockRequest {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetInitialTestnetAccountsRequest {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetAccountBalanceRequest {
|
||||
pub account_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetTransactionByHashRequest {
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetAccountsNoncesRequest {
|
||||
pub account_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetAccountRequest {
|
||||
pub account_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetProofForCommitmentRequest {
|
||||
pub commitment: nssa_core::Commitment,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetProgramIdsRequest {}
|
||||
|
||||
parse_request!(HelloRequest);
|
||||
parse_request!(RegisterAccountRequest);
|
||||
parse_request!(SendTxRequest);
|
||||
parse_request!(GetBlockDataRequest);
|
||||
parse_request!(GetBlockRangeDataRequest);
|
||||
parse_request!(GetGenesisIdRequest);
|
||||
parse_request!(GetLastBlockRequest);
|
||||
parse_request!(GetInitialTestnetAccountsRequest);
|
||||
parse_request!(GetAccountBalanceRequest);
|
||||
parse_request!(GetTransactionByHashRequest);
|
||||
parse_request!(GetAccountsNoncesRequest);
|
||||
parse_request!(GetProofForCommitmentRequest);
|
||||
parse_request!(GetAccountRequest);
|
||||
parse_request!(GetProgramIdsRequest);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct HelloResponse {
|
||||
pub greeting: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct RegisterAccountResponse {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SendTxResponse {
|
||||
pub status: String,
|
||||
pub tx_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetBlockDataResponse {
|
||||
#[serde(with = "base64_deser")]
|
||||
pub block: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetBlockRangeDataResponse {
|
||||
#[serde(with = "base64_deser::vec")]
|
||||
pub blocks: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
mod base64_deser {
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use serde::{self, Deserialize, Deserializer, Serializer, ser::SerializeSeq as _};
|
||||
|
||||
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let base64_string = general_purpose::STANDARD.encode(bytes);
|
||||
serializer.serialize_str(&base64_string)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let base64_string: String = Deserialize::deserialize(deserializer)?;
|
||||
general_purpose::STANDARD
|
||||
.decode(&base64_string)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
pub mod vec {
|
||||
use super::*;
|
||||
|
||||
pub fn serialize<S>(bytes_vec: &[Vec<u8>], serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(bytes_vec.len()))?;
|
||||
for bytes in bytes_vec {
|
||||
let s = general_purpose::STANDARD.encode(bytes);
|
||||
seq.serialize_element(&s)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let base64_strings: Vec<String> = Deserialize::deserialize(deserializer)?;
|
||||
base64_strings
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
general_purpose::STANDARD
|
||||
.decode(&s)
|
||||
.map_err(serde::de::Error::custom)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetGenesisIdResponse {
|
||||
pub genesis_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetLastBlockResponse {
|
||||
pub last_block: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetAccountBalanceResponse {
|
||||
pub balance: u128,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetAccountsNoncesResponse {
|
||||
pub nonces: Vec<u128>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetTransactionByHashResponse {
|
||||
pub transaction: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetAccountResponse {
|
||||
pub account: nssa::Account,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetProofForCommitmentResponse {
|
||||
pub membership_proof: Option<nssa_core::MembershipProof>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetProgramIdsResponse {
|
||||
pub program_ids: HashMap<String, ProgramId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GetInitialTestnetAccountsResponse {
|
||||
/// Hex encoded account id
|
||||
pub account_id: String,
|
||||
pub balance: u64,
|
||||
}
|
||||
344
common/src/sequencer_client.rs
Normal file
344
common/src/sequencer_client.rs
Normal file
@ -0,0 +1,344 @@
|
||||
use std::{collections::HashMap, ops::RangeInclusive};
|
||||
|
||||
use anyhow::Result;
|
||||
use nssa_core::program::ProgramId;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::rpc_primitives::requests::{
|
||||
GetAccountBalanceRequest, GetAccountBalanceResponse, GetBlockDataRequest, GetBlockDataResponse,
|
||||
GetGenesisIdRequest, GetGenesisIdResponse, GetInitialTestnetAccountsRequest,
|
||||
};
|
||||
use crate::{
|
||||
error::{SequencerClientError, SequencerRpcError},
|
||||
rpc_primitives::{
|
||||
self,
|
||||
requests::{
|
||||
GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest,
|
||||
GetAccountsNoncesResponse, GetBlockRangeDataRequest, GetBlockRangeDataResponse,
|
||||
GetInitialTestnetAccountsResponse, GetLastBlockRequest, GetLastBlockResponse,
|
||||
GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest,
|
||||
GetProofForCommitmentResponse, GetTransactionByHashRequest,
|
||||
GetTransactionByHashResponse, SendTxRequest, SendTxResponse,
|
||||
},
|
||||
},
|
||||
transaction::{EncodedTransaction, NSSATransaction},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SequencerClient {
|
||||
pub client: reqwest::Client,
|
||||
pub sequencer_addr: String,
|
||||
pub basic_auth: Option<(String, Option<String>)>,
|
||||
}
|
||||
|
||||
impl SequencerClient {
|
||||
pub fn new(sequencer_addr: String) -> Result<Self> {
|
||||
Self::new_with_auth(sequencer_addr, None)
|
||||
}
|
||||
|
||||
pub fn new_with_auth(
|
||||
sequencer_addr: String,
|
||||
basic_auth: Option<(String, Option<String>)>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
client: Client::builder()
|
||||
//Add more fiedls if needed
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.build()?,
|
||||
sequencer_addr,
|
||||
basic_auth,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn call_method_with_payload(
|
||||
&self,
|
||||
method: &str,
|
||||
payload: Value,
|
||||
) -> Result<Value, SequencerClientError> {
|
||||
let request =
|
||||
rpc_primitives::message::Request::from_payload_version_2_0(method.to_string(), payload);
|
||||
|
||||
let mut call_builder = self.client.post(&self.sequencer_addr);
|
||||
|
||||
if let Some((username, password)) = &self.basic_auth {
|
||||
call_builder = call_builder.basic_auth(username, password.as_deref());
|
||||
}
|
||||
|
||||
let call_res = call_builder.json(&request).send().await?;
|
||||
|
||||
let response_vall = call_res.json::<Value>().await?;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SequencerRpcResponse {
|
||||
pub jsonrpc: String,
|
||||
pub result: serde_json::Value,
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
if let Ok(response) = serde_json::from_value::<SequencerRpcResponse>(response_vall.clone())
|
||||
{
|
||||
Ok(response.result)
|
||||
} else {
|
||||
let err_resp = serde_json::from_value::<SequencerRpcError>(response_vall)?;
|
||||
|
||||
Err(err_resp.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get block data at `block_id` from sequencer
|
||||
pub async fn get_block(
|
||||
&self,
|
||||
block_id: u64,
|
||||
) -> Result<GetBlockDataResponse, SequencerClientError> {
|
||||
let block_req = GetBlockDataRequest { block_id };
|
||||
|
||||
let req = serde_json::to_value(block_req)?;
|
||||
|
||||
let resp = self.call_method_with_payload("get_block", req).await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
pub async fn get_block_range(
|
||||
&self,
|
||||
range: RangeInclusive<u64>,
|
||||
) -> Result<GetBlockRangeDataResponse, SequencerClientError> {
|
||||
let block_req = GetBlockRangeDataRequest {
|
||||
start_block_id: *range.start(),
|
||||
end_block_id: *range.end(),
|
||||
};
|
||||
|
||||
let req = serde_json::to_value(block_req)?;
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_block_range", req)
|
||||
.await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get last known `blokc_id` from sequencer
|
||||
pub async fn get_last_block(&self) -> Result<GetLastBlockResponse, SequencerClientError> {
|
||||
let block_req = GetLastBlockRequest {};
|
||||
|
||||
let req = serde_json::to_value(block_req)?;
|
||||
|
||||
let resp = self.call_method_with_payload("get_last_block", req).await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get account public balance for `account_id`. `account_id` must be a valid hex-string for 32
|
||||
/// bytes.
|
||||
pub async fn get_account_balance(
|
||||
&self,
|
||||
account_id: String,
|
||||
) -> Result<GetAccountBalanceResponse, SequencerClientError> {
|
||||
let block_req = GetAccountBalanceRequest { account_id };
|
||||
|
||||
let req = serde_json::to_value(block_req)?;
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_account_balance", req)
|
||||
.await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get accounts nonces for `account_ids`. `account_ids` must be a list of valid hex-strings for
|
||||
/// 32 bytes.
|
||||
pub async fn get_accounts_nonces(
|
||||
&self,
|
||||
account_ids: Vec<String>,
|
||||
) -> Result<GetAccountsNoncesResponse, SequencerClientError> {
|
||||
let block_req = GetAccountsNoncesRequest { account_ids };
|
||||
|
||||
let req = serde_json::to_value(block_req)?;
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_accounts_nonces", req)
|
||||
.await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
pub async fn get_account(
|
||||
&self,
|
||||
account_id: String,
|
||||
) -> Result<GetAccountResponse, SequencerClientError> {
|
||||
let block_req = GetAccountRequest { account_id };
|
||||
|
||||
let req = serde_json::to_value(block_req)?;
|
||||
|
||||
let resp = self.call_method_with_payload("get_account", req).await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get transaction details for `hash`.
|
||||
pub async fn get_transaction_by_hash(
|
||||
&self,
|
||||
hash: String,
|
||||
) -> Result<GetTransactionByHashResponse, SequencerClientError> {
|
||||
let block_req = GetTransactionByHashRequest { hash };
|
||||
|
||||
let req = serde_json::to_value(block_req)?;
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_transaction_by_hash", req)
|
||||
.await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Send transaction to sequencer
|
||||
pub async fn send_tx_public(
|
||||
&self,
|
||||
transaction: nssa::PublicTransaction,
|
||||
) -> Result<SendTxResponse, SequencerClientError> {
|
||||
let transaction = EncodedTransaction::from(NSSATransaction::Public(transaction));
|
||||
|
||||
let tx_req = SendTxRequest {
|
||||
transaction: borsh::to_vec(&transaction).unwrap(),
|
||||
};
|
||||
|
||||
let req = serde_json::to_value(tx_req)?;
|
||||
|
||||
let resp = self.call_method_with_payload("send_tx", req).await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Send transaction to sequencer
|
||||
pub async fn send_tx_private(
|
||||
&self,
|
||||
transaction: nssa::PrivacyPreservingTransaction,
|
||||
) -> Result<SendTxResponse, SequencerClientError> {
|
||||
let transaction = EncodedTransaction::from(NSSATransaction::PrivacyPreserving(transaction));
|
||||
|
||||
let tx_req = SendTxRequest {
|
||||
transaction: borsh::to_vec(&transaction).unwrap(),
|
||||
};
|
||||
|
||||
let req = serde_json::to_value(tx_req)?;
|
||||
|
||||
let resp = self.call_method_with_payload("send_tx", req).await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get genesis id from sequencer
|
||||
pub async fn get_genesis_id(&self) -> Result<GetGenesisIdResponse, SequencerClientError> {
|
||||
let genesis_req = GetGenesisIdRequest {};
|
||||
|
||||
let req = serde_json::to_value(genesis_req).unwrap();
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_genesis", req)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resp_deser = serde_json::from_value(resp).unwrap();
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get initial testnet accounts from sequencer
|
||||
pub async fn get_initial_testnet_accounts(
|
||||
&self,
|
||||
) -> Result<Vec<GetInitialTestnetAccountsResponse>, SequencerClientError> {
|
||||
let acc_req = GetInitialTestnetAccountsRequest {};
|
||||
|
||||
let req = serde_json::to_value(acc_req).unwrap();
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_initial_testnet_accounts", req)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resp_deser = serde_json::from_value(resp).unwrap();
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get proof for commitment
|
||||
pub async fn get_proof_for_commitment(
|
||||
&self,
|
||||
commitment: nssa_core::Commitment,
|
||||
) -> Result<Option<nssa_core::MembershipProof>, SequencerClientError> {
|
||||
let acc_req = GetProofForCommitmentRequest { commitment };
|
||||
|
||||
let req = serde_json::to_value(acc_req).unwrap();
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_proof_for_commitment", req)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resp_deser = serde_json::from_value::<GetProofForCommitmentResponse>(resp)
|
||||
.unwrap()
|
||||
.membership_proof;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
pub async fn send_tx_program(
|
||||
&self,
|
||||
transaction: nssa::ProgramDeploymentTransaction,
|
||||
) -> Result<SendTxResponse, SequencerClientError> {
|
||||
let transaction = EncodedTransaction::from(NSSATransaction::ProgramDeployment(transaction));
|
||||
|
||||
let tx_req = SendTxRequest {
|
||||
transaction: borsh::to_vec(&transaction).unwrap(),
|
||||
};
|
||||
|
||||
let req = serde_json::to_value(tx_req)?;
|
||||
|
||||
let resp = self.call_method_with_payload("send_tx", req).await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get Ids of the programs used by the node
|
||||
pub async fn get_program_ids(
|
||||
&self,
|
||||
) -> Result<HashMap<String, ProgramId>, SequencerClientError> {
|
||||
let acc_req = GetProgramIdsRequest {};
|
||||
|
||||
let req = serde_json::to_value(acc_req).unwrap();
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_program_ids", req)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resp_deser = serde_json::from_value::<GetProgramIdsResponse>(resp)
|
||||
.unwrap()
|
||||
.program_ids;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
}
|
||||
78
common/src/test_utils.rs
Normal file
78
common/src/test_utils.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use crate::{
|
||||
block::{Block, HashableBlockData},
|
||||
transaction::{EncodedTransaction, NSSATransaction},
|
||||
};
|
||||
|
||||
// Helpers
|
||||
|
||||
pub fn sequencer_sign_key_for_testing() -> nssa::PrivateKey {
|
||||
nssa::PrivateKey::try_new([37; 32]).unwrap()
|
||||
}
|
||||
|
||||
// Dummy producers
|
||||
|
||||
/// Produce dummy block with
|
||||
///
|
||||
/// `id` - block id, provide zero for genesis
|
||||
///
|
||||
/// `prev_hash` - hash of previous block, provide None for genesis
|
||||
///
|
||||
/// `transactions` - vector of `EncodedTransaction` objects
|
||||
pub fn produce_dummy_block(
|
||||
id: u64,
|
||||
prev_hash: Option<[u8; 32]>,
|
||||
transactions: Vec<EncodedTransaction>,
|
||||
) -> Block {
|
||||
let block_data = HashableBlockData {
|
||||
block_id: id,
|
||||
prev_block_hash: prev_hash.unwrap_or_default(),
|
||||
timestamp: id * 100,
|
||||
transactions,
|
||||
};
|
||||
|
||||
block_data.into_block(&sequencer_sign_key_for_testing())
|
||||
}
|
||||
|
||||
pub fn produce_dummy_empty_transaction() -> EncodedTransaction {
|
||||
let program_id = nssa::program::Program::authenticated_transfer_program().id();
|
||||
let account_ids = vec![];
|
||||
let nonces = vec![];
|
||||
let instruction_data: u128 = 0;
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
program_id,
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction_data,
|
||||
)
|
||||
.unwrap();
|
||||
let private_key = nssa::PrivateKey::try_new([1; 32]).unwrap();
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[&private_key]);
|
||||
|
||||
let nssa_tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
EncodedTransaction::from(NSSATransaction::Public(nssa_tx))
|
||||
}
|
||||
|
||||
pub fn create_transaction_native_token_transfer(
|
||||
from: [u8; 32],
|
||||
nonce: u128,
|
||||
to: [u8; 32],
|
||||
balance_to_move: u128,
|
||||
signing_key: nssa::PrivateKey,
|
||||
) -> EncodedTransaction {
|
||||
let account_ids = vec![nssa::AccountId::new(from), nssa::AccountId::new(to)];
|
||||
let nonces = vec![nonce];
|
||||
let program_id = nssa::program::Program::authenticated_transfer_program().id();
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
program_id,
|
||||
account_ids,
|
||||
nonces,
|
||||
balance_to_move,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[&signing_key]);
|
||||
|
||||
let nssa_tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
EncodedTransaction::from(NSSATransaction::Public(nssa_tx))
|
||||
}
|
||||
143
common/src/transaction.rs
Normal file
143
common/src/transaction.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, digest::FixedOutput};
|
||||
|
||||
pub type HashType = [u8; 32];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum NSSATransaction {
|
||||
Public(nssa::PublicTransaction),
|
||||
PrivacyPreserving(nssa::PrivacyPreservingTransaction),
|
||||
ProgramDeployment(nssa::ProgramDeploymentTransaction),
|
||||
}
|
||||
|
||||
impl From<nssa::PublicTransaction> for NSSATransaction {
|
||||
fn from(value: nssa::PublicTransaction) -> Self {
|
||||
Self::Public(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nssa::PrivacyPreservingTransaction> for NSSATransaction {
|
||||
fn from(value: nssa::PrivacyPreservingTransaction) -> Self {
|
||||
Self::PrivacyPreserving(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nssa::ProgramDeploymentTransaction> for NSSATransaction {
|
||||
fn from(value: nssa::ProgramDeploymentTransaction) -> Self {
|
||||
Self::ProgramDeployment(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize,
|
||||
)]
|
||||
pub enum TxKind {
|
||||
Public,
|
||||
PrivacyPreserving,
|
||||
ProgramDeployment,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
/// General transaction object
|
||||
pub struct EncodedTransaction {
|
||||
pub tx_kind: TxKind,
|
||||
/// Encoded blobs of data
|
||||
pub encoded_transaction_data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<NSSATransaction> for EncodedTransaction {
|
||||
fn from(value: NSSATransaction) -> Self {
|
||||
match value {
|
||||
NSSATransaction::Public(tx) => Self {
|
||||
tx_kind: TxKind::Public,
|
||||
encoded_transaction_data: tx.to_bytes(),
|
||||
},
|
||||
NSSATransaction::PrivacyPreserving(tx) => Self {
|
||||
tx_kind: TxKind::PrivacyPreserving,
|
||||
encoded_transaction_data: tx.to_bytes(),
|
||||
},
|
||||
NSSATransaction::ProgramDeployment(tx) => Self {
|
||||
tx_kind: TxKind::ProgramDeployment,
|
||||
encoded_transaction_data: tx.to_bytes(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&EncodedTransaction> for NSSATransaction {
|
||||
type Error = nssa::error::NssaError;
|
||||
|
||||
fn try_from(value: &EncodedTransaction) -> Result<Self, Self::Error> {
|
||||
match value.tx_kind {
|
||||
TxKind::Public => nssa::PublicTransaction::from_bytes(&value.encoded_transaction_data)
|
||||
.map(|tx| tx.into()),
|
||||
TxKind::PrivacyPreserving => {
|
||||
nssa::PrivacyPreservingTransaction::from_bytes(&value.encoded_transaction_data)
|
||||
.map(|tx| tx.into())
|
||||
}
|
||||
TxKind::ProgramDeployment => {
|
||||
nssa::ProgramDeploymentTransaction::from_bytes(&value.encoded_transaction_data)
|
||||
.map(|tx| tx.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncodedTransaction {
|
||||
/// Computes and returns the SHA-256 hash of the JSON-serialized representation of `self`.
|
||||
pub fn hash(&self) -> HashType {
|
||||
let bytes_to_hash = borsh::to_vec(&self).unwrap();
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(&bytes_to_hash);
|
||||
HashType::from(hasher.finalize_fixed())
|
||||
}
|
||||
|
||||
pub fn log(&self) {
|
||||
info!("Transaction hash is {:?}", hex::encode(self.hash()));
|
||||
info!("Transaction tx_kind is {:?}", self.tx_kind);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use sha2::{Digest, digest::FixedOutput};
|
||||
|
||||
use crate::{
|
||||
HashType,
|
||||
transaction::{EncodedTransaction, TxKind},
|
||||
};
|
||||
|
||||
fn test_transaction_body() -> EncodedTransaction {
|
||||
EncodedTransaction {
|
||||
tx_kind: TxKind::Public,
|
||||
encoded_transaction_data: vec![1, 2, 3, 4],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transaction_hash_is_sha256_of_json_bytes() {
|
||||
let body = test_transaction_body();
|
||||
let expected_hash = {
|
||||
let data = borsh::to_vec(&body).unwrap();
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(&data);
|
||||
HashType::from(hasher.finalize_fixed())
|
||||
};
|
||||
|
||||
let hash = body.hash();
|
||||
|
||||
assert_eq!(expected_hash, hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_bytes_from_bytes() {
|
||||
let body = test_transaction_body();
|
||||
|
||||
let body_bytes = borsh::to_vec(&body).unwrap();
|
||||
let body_new = borsh::from_slice::<EncodedTransaction>(&body_bytes).unwrap();
|
||||
|
||||
assert_eq!(body, body_new);
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "consensus"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde_json.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[dependencies.networking]
|
||||
path = "../networking"
|
||||
@ -1,22 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use networking::peer_manager::PeerManager;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug)]
|
||||
///Entrypoint to consensus.
|
||||
/// Manages consensus protocol.
|
||||
pub struct ConsensusManager {
|
||||
pub peer_manager: Arc<Mutex<PeerManager>>,
|
||||
}
|
||||
|
||||
impl ConsensusManager {
|
||||
pub fn new(peer_manager: Arc<Mutex<PeerManager>>) -> Self {
|
||||
Self { peer_manager }
|
||||
}
|
||||
|
||||
//ToDo: change block from generic value into struct, when data block will be defined
|
||||
pub fn vote(&self, _block: serde_json::Value) -> bool {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
12
examples/program_deployment/Cargo.toml
Normal file
12
examples/program_deployment/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "program_deployment"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
wallet.workspace = true
|
||||
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
clap.workspace = true
|
||||
661
examples/program_deployment/README.md
Normal file
661
examples/program_deployment/README.md
Normal file
@ -0,0 +1,661 @@
|
||||
# Program deployment tutorial
|
||||
|
||||
This guide walks you through running the sequencer, compiling example programs, deploying a Hello World program, and interacting with accounts.
|
||||
|
||||
You'll find:
|
||||
- Programs: example NSSA programs under `methods/guest/src/bin`.
|
||||
- Runners: scripts to create and submit transactions to invoke these programs publicly and privately under `src/bin`.
|
||||
|
||||
# 0. Install the wallet
|
||||
From the project’s root directory:
|
||||
```bash
|
||||
cargo install --path wallet --force
|
||||
```
|
||||
|
||||
# 1. Run the sequencer
|
||||
From the project’s root directory, start the sequencer:
|
||||
```bash
|
||||
cd sequencer_runner
|
||||
RUST_LOG=info cargo run $(pwd)/configs/debug
|
||||
```
|
||||
Keep this terminal open. We’ll use it only to observe the node logs.
|
||||
|
||||
> [!NOTE]
|
||||
> If you have already ran this before you'll see a `rocksdb` directory with stored blocks. Be sure to remove that directory to follow this tutorial.
|
||||
|
||||
|
||||
## Checking and setting up the wallet
|
||||
For sanity let's check that the wallet can connect to it.
|
||||
|
||||
```bash
|
||||
wallet check-health
|
||||
```
|
||||
|
||||
If this is your first time, the wallet will ask for a password. This is used as seed to deterministically generate all account keys (public and private).
|
||||
For this tutorial, use: `program-tutorial`
|
||||
|
||||
You should see `✅All looks good!` if everything went well.
|
||||
|
||||
# 2. Compile the example programs
|
||||
In a second terminal, from the `lssa` root directory, compile the example Risc0 programs:
|
||||
```bash
|
||||
cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml
|
||||
```
|
||||
The compiled `.bin` files will appear under:
|
||||
```
|
||||
examples/program_deployment/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/
|
||||
```
|
||||
For convenience, export this path:
|
||||
```bash
|
||||
export EXAMPLE_PROGRAMS_BUILD_DIR=$(pwd)/examples/program_deployment/methods/guest/target/riscv32im-risc0-zkvm-elf/docker
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **All remaining commands must be run from the `examples/program_deployment` directory.**
|
||||
|
||||
# 3. Hello world example
|
||||
|
||||
The Hello world program reads an arbitrary sequence of bytes from its instruction and appends them to the data field of the input account.
|
||||
Execution succeeds only if the account is:
|
||||
|
||||
- Uninitialized, or
|
||||
- Already owned by this program
|
||||
|
||||
If uninitialized, the program will claim the account and emit the updated state.
|
||||
|
||||
## Navigate to the example directory
|
||||
All remaining commands must be run from:
|
||||
```bash
|
||||
cd examples/program_deployment
|
||||
```
|
||||
|
||||
## Deploy the Program
|
||||
|
||||
Use the wallet’s built-in program deployment command:
|
||||
```bash
|
||||
wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin
|
||||
```
|
||||
|
||||
# 4. Public execution of the Hello world example
|
||||
|
||||
## Create a Public Account
|
||||
|
||||
Generate a new public account:
|
||||
```bash
|
||||
wallet account new public
|
||||
```
|
||||
|
||||
You'll see an output similar to:
|
||||
```bash
|
||||
Generated new account with account_id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 at path /0
|
||||
```
|
||||
The relevant part is the account id `BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9`
|
||||
|
||||
## Check the account state
|
||||
New accounts are always Uninitialized. Verify:
|
||||
```bash
|
||||
wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9
|
||||
```
|
||||
Expected output:
|
||||
```
|
||||
Account is Uninitialized
|
||||
```
|
||||
The `Public/` prefix tells the wallet to query the public state.
|
||||
|
||||
## Execute the Hello world program
|
||||
Run the example:
|
||||
```bash
|
||||
cargo run --bin run_hello_world \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin \
|
||||
BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9
|
||||
```
|
||||
> [!NOTE]
|
||||
> - Passing the `.bin` lets the script compute the program ID and build the transaction.
|
||||
> - Because this program executes publicly, the node performs the execution.
|
||||
> - The program will claim the account and write data into it.
|
||||
|
||||
Monitor the sequencer terminal to confirm execution.
|
||||
|
||||
## Inspect the updated account
|
||||
After the transaction is processed, check the new state:
|
||||
```bash
|
||||
wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9
|
||||
```
|
||||
Example output:
|
||||
```json
|
||||
{
|
||||
"balance": 0,
|
||||
"program_owner_b64": "o6C6/bbjDmN9VUC51McBpPrta8lxrx2X0iHExhX0yNU=",
|
||||
"data_b64": "SG9sYSBtdW5kbyE=",
|
||||
"nonce": 0
|
||||
}
|
||||
```
|
||||
The `data_b64` field contains de data in Base64.
|
||||
Decode it:
|
||||
```bash
|
||||
echo -n SG9sYSBtdW5kbyE= | base64 -d
|
||||
```
|
||||
You should see `Hola mundo!`.
|
||||
|
||||
# 5. Understanding the code in `hello_world.rs`.
|
||||
The Hello world example demonstrates the minimal structure of an NSSA program.
|
||||
Its purpose is very simple: append the instruction bytes to the data field of a single account.
|
||||
|
||||
### What this program does in a nutshell
|
||||
1. Reads the program inputs
|
||||
- The list of pre-state accounts (`pre_states`)
|
||||
- The instruction bytes (`instruction`)
|
||||
- The raw instruction data (used again when writing outputs)
|
||||
2. Checks that there is exactly one input account: this example operates on a single account, so it expects `pre_states` to contain exactly one entry.
|
||||
3. Builds the post-state: It clones the input account and appends the instruction bytes to its data field.
|
||||
4. Handles account claiming logic: If the account is uninitialized (i.e. not yet claimed by any program), its program_owner will equal `DEFAULT_PROGRAM_ID`. In that case, the program issues a claim request, meaning: "This program now owns this account."
|
||||
5. Outputs the proposed state transition: `write_nssa_outputs` emits:
|
||||
- The original instruction data
|
||||
- The original pre-states
|
||||
- The new post-states
|
||||
|
||||
## Code walkthrough
|
||||
1. Reading inputs:
|
||||
```rust
|
||||
let (ProgramInput { pre_states, instruction: greeting }, instruction_data)
|
||||
= read_nssa_inputs::<Instruction>();
|
||||
```
|
||||
2. Extracting the single account:
|
||||
```rust
|
||||
let [pre_state] = pre_states
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("Input pre states should consist of a single account"));
|
||||
```
|
||||
3. Constructing the updated account post state
|
||||
```rust
|
||||
let mut this = pre_state.account.clone();
|
||||
let mut bytes = this.data.into_inner();
|
||||
bytes.extend_from_slice(&greeting);
|
||||
this.data = bytes.try_into().expect("Data should fit within the allowed limits");
|
||||
```
|
||||
4. Instantiating the `AccountPostState` with a claiming request only if the account pre state is uninitialized:
|
||||
```rust
|
||||
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID {
|
||||
AccountPostState::new_claimed(post_account)
|
||||
} else {
|
||||
AccountPostState::new(post_account)
|
||||
};
|
||||
```
|
||||
5. Emmiting the output
|
||||
```rust
|
||||
write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]);
|
||||
```
|
||||
|
||||
# 6. Understanding the runner script `run_hello_world.rs`
|
||||
The `run_hello_world.rs` example demonstrates how to construct and submit a public transaction that executes the `hello_world` program. Below is a breakdown of what the file does and how the pieces fit together.
|
||||
|
||||
### 1. Wallet initialization
|
||||
```rust
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
```
|
||||
The example loads the wallet configuration and initializes `WalletCore`.
|
||||
This gives access to:
|
||||
- the sequencer client,
|
||||
- the wallet’s account storage.
|
||||
|
||||
### 2. Parsing inputs
|
||||
```rust
|
||||
let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap();
|
||||
let account_id: AccountId = std::env::args_os().nth(2).unwrap().into_string().unwrap().parse().unwrap();
|
||||
```
|
||||
The program expects two arguments:
|
||||
- Path to the guest binary
|
||||
- AccountId of the public account to operate on
|
||||
|
||||
This is the account that the program will claim and write data into.
|
||||
|
||||
### 3. Loading the program bytecode
|
||||
```rust
|
||||
let bytecode: Vec<u8> = std::fs::read(program_path).unwrap();
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
```
|
||||
The Risc0 ELF is read from disk and wrapped in a Program object, which can be used to compute the program ID. The ID is used by the node to identify which program is invoked by the transaction.
|
||||
|
||||
|
||||
### 4. Preparing the instruction data
|
||||
```rust
|
||||
let greeting: Vec<u8> = vec![72,111,108,97,32,109,117,110,100,111,33];
|
||||
```
|
||||
The example hardcodes the ASCII bytes for `Hola mundo!`. These bytes are passed to the program as its “instruction,” which the Hello World program simply appends to the account’s data field.
|
||||
|
||||
### 5. Creating the public transaction
|
||||
|
||||
```rust
|
||||
let nonces = vec![];
|
||||
let signing_keys = [];
|
||||
let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, &signing_keys);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
```
|
||||
|
||||
A public transaction consists of:
|
||||
- a `Message`
|
||||
- a corresponding `WitnessSet`
|
||||
|
||||
For this simple example, no signing or nonces are required. The transaction includes only the program ID, the target account, and the instruction bytes. The Hello World program allows this because it does not explicitly require authorization. In the next example, we’ll see how authorization requirements are enforced and how to construct a transaction that includes signatures and nonces.
|
||||
|
||||
### 6. Submitting the transaction
|
||||
```rust
|
||||
let response = wallet_core.sequencer_client.send_tx_public(tx).await.unwrap();
|
||||
```
|
||||
The transaction is sent to the sequencer, which processes it and updates the public state accordingly.
|
||||
|
||||
Once executed, you’ll be able to query the updated account to see the newly written "Hola mundo!" data.
|
||||
|
||||
# 7. Private execution of the Hello world example
|
||||
|
||||
This section is very similar to the previous case:
|
||||
|
||||
## Create a private account
|
||||
|
||||
Generate a new private account:
|
||||
```bash
|
||||
wallet account new private
|
||||
```
|
||||
|
||||
You'll see an output similar to:
|
||||
```bash
|
||||
Generated new account with account_id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr at path /0
|
||||
```
|
||||
The relevant part for this tutorial is the account id `7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr`
|
||||
|
||||
You can check it's uninitialized with
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr
|
||||
```
|
||||
|
||||
## Privately executing the Hello world program
|
||||
|
||||
### Execute the Hello world program
|
||||
Run the example:
|
||||
```bash
|
||||
cargo run --bin run_hello_world_private \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin \
|
||||
7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr
|
||||
```
|
||||
> [!NOTE]
|
||||
> - This command may take a few minutes to complete. A ZK proof of the Hello world program execution and the privacy preserving circuit are being generated. Depending on the machine this can take from 30 seconds to 4 minutes.
|
||||
> - We are passing the same `hello_world.bin` binary as in the previous case with public executions. This is because the program is the same, it is the privacy context of the input account that's different.
|
||||
> - Because this program executes privately, the local machine runs the program and generate the proof of execution.
|
||||
> - The program will claim the private account and write data into it.
|
||||
|
||||
### Syncing the new private account values
|
||||
The `run_hello_world` script submitted a transaction and it was (hopefully) accepted by the node. On chain there is now a commitment to the new private account values, and the account data is stored encrypted. However, the local client hasn’t updated its private state yet. That’s why, if you try to get the private account values now, it still reads the old values from local storage instead.
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr
|
||||
```
|
||||
|
||||
This will still show `Account is Uninitialized`. To see the new values locally, you need to run the wallet sync command. Once the client syncs, the local store will reflect the updated account data.
|
||||
|
||||
To sync private accounts run:
|
||||
```bash
|
||||
wallet account sync-private
|
||||
```
|
||||
> [!NOTE]
|
||||
> - This queries the node for transactions and goes throught the encrypted accounts. Whenever a new value is found for one of the owned private accounts, the local storage is updated.
|
||||
|
||||
After this completes, running
|
||||
```bash
|
||||
wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr
|
||||
```
|
||||
should show something similar to
|
||||
```json
|
||||
{
|
||||
"balance":0,
|
||||
"program_owner_b64":"dWgtNRixwjC0C8aA0NL0Iuss3Q26Dw6ECk7bzExW4bI=",
|
||||
"data_b64":"SG9sYSBtdW5kbyE=",
|
||||
"nonce":236788677072686551559312843688143377080
|
||||
}
|
||||
```
|
||||
|
||||
## The `run_hello_world_private.rs` runner
|
||||
This example extends the public `run_hello_world.rs` flow by constructing a privacy-preserving transaction instead of a public one.
|
||||
Both runners load a guest program, prepare a transaction, and submit it. But the private version handles encrypted account data, nullifiers, ephemeral keys, and zk proofs.
|
||||
|
||||
Unlike the public version, `run_hello_world_private.rs` must:
|
||||
- prepare the private account pre-state (nullifier keys, membership proof, encrypted values)
|
||||
- derive a shared secret to encrypt the post-state
|
||||
- compute the correct visibility mask (initialized vs. uninitialized private account)
|
||||
- execute the guest program inside the zkVM and produce a proof
|
||||
- build a PrivacyPreservingTransaction composed of:
|
||||
- a Message encoding commitments + encrypted post-state
|
||||
- a WitnessSet embedding the zk proof
|
||||
|
||||
Luckily all that complexity is hidden behind the `wallet_core.send_privacy_preserving_tx` function:
|
||||
```rust
|
||||
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
|
||||
|
||||
// Construct and submit the privacy-preserving transaction
|
||||
wallet_core
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
&Program::serialize_instruction(greeting).unwrap(),
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
```
|
||||
Check the `run_hello_world_private.rs` file to see how it is used.
|
||||
|
||||
# 8. Account authorization mechanism
|
||||
The Hello world example does not enforce any authorization on the input account. This means any user can execute it on any account, regardless of ownership.
|
||||
NSSA provides a mechanism for programs to enforce proper authorization before an execution can succeed. The meaning of authorization differs between public and private accounts:
|
||||
- Public accounts: authorization requires that the transaction is signed with the account’s signing key.
|
||||
- Private accounts: authorization requires that the circuit verifies knowledge of the account’s nullifier secret key.
|
||||
|
||||
From the program development perspective it is very simple: input accounts come with a flag indicating whether they has been properly authorized. And so, the only difference between the program `hello_world.rs` and `hello_world_with_authorization.rs` is in the lines
|
||||
|
||||
```rust
|
||||
// #### Difference with `hello_world` example here:
|
||||
// Fail if the input account is not authorized
|
||||
// The `is_authorized` field will be correctly populated or verified by the system if
|
||||
// authorization is provided.
|
||||
if !pre_state.is_authorized {
|
||||
panic!("Missing required authorization");
|
||||
}
|
||||
// ####
|
||||
```
|
||||
|
||||
Which just checks the `is_authorized` flag and fails if it is set to false.
|
||||
|
||||
# 9. Public execution of the Hello world with authorization example
|
||||
The workflow to execute it publicly is very similar:
|
||||
|
||||
### Deploy the program
|
||||
```bash
|
||||
wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin
|
||||
```
|
||||
|
||||
### Create a new public account
|
||||
Our previous public account is already claimed by the simple Hello world program. So we need a new one to work with this other version of the hello program
|
||||
```bash
|
||||
wallet account new public
|
||||
```
|
||||
|
||||
Outupt:
|
||||
```
|
||||
Generated new account with account_id Public/9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE at path /1
|
||||
```
|
||||
|
||||
### Run the program
|
||||
|
||||
```bash
|
||||
cargo run --bin run_hello_world_with_authorization \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin \
|
||||
9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE
|
||||
```
|
||||
|
||||
# 10. Understanding `run_hello_world_with_authorization.rs`
|
||||
From the runner script perspective, the only difference is that the signing keys are passed to the `WitnessSet` constructor for it to sign it. You can see this in the following parts of the code:
|
||||
|
||||
1. Loading the sigining keys from the wallet storage
|
||||
```rust
|
||||
// Load signing keys to provide authorization
|
||||
let signing_key = wallet_core
|
||||
.storage
|
||||
.user_data
|
||||
.get_pub_account_signing_key(&account_id)
|
||||
.expect("Input account should be a self owned public account");
|
||||
```
|
||||
2. Fetching the current public nonce.
|
||||
```rust
|
||||
// Construct the public transaction
|
||||
// Query the current nonce from the node
|
||||
let nonces = wallet_core
|
||||
.get_accounts_nonces(vec![account_id])
|
||||
.await
|
||||
.expect("Node should be reachable to query account data");
|
||||
```
|
||||
2. Instantiate the witness set using the signing keys
|
||||
```rust
|
||||
let signing_keys = [signing_key];
|
||||
let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap();
|
||||
// Pass the signing key to sign the message. This will be used by the node
|
||||
// to flag the pre_state as `is_authorized` when executing the program
|
||||
let witness_set = WitnessSet::for_message(&message, &signing_keys);
|
||||
```
|
||||
|
||||
## Seeing the mechanism in action
|
||||
If everything went well you won't notice any difference with the first Hello world, because the runner takes care of signing the transaction to provide authorization and the program just succeeds.
|
||||
Try using the `run_hello_world.rs` runner with the `hello_world_with_authorization.bin` program. This will fail because the runner will submit the transaction without the corresponding signature.
|
||||
```bash
|
||||
cargo run --bin run_hello_world \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin \
|
||||
9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE
|
||||
```
|
||||
|
||||
You should see something like the following **on the node logs**.
|
||||
```bash
|
||||
[2025-12-11T13:43:22Z WARN sequencer_core] Error at transition ProgramExecutionFailed(
|
||||
"Guest panicked: Missing required authorization",
|
||||
)
|
||||
```
|
||||
|
||||
# 11. Public and private account interaction example
|
||||
Previous examples only operated on public or private accounts independently. Those minimal programs were useful to introduce basic concepts, but they couldn't demonstrate how different types of accounts interact within a single program invocation.
|
||||
The "Hello world with move function" introduces two operations that require one or two input accounts:
|
||||
- `write`: appends arbitrary bytes to a single account. This is what we already had.
|
||||
- `move_data`: reads all bytes from one account, clears it, and appends those bytes to another account.
|
||||
Because these operations may involve multiple accounts, we'll see how public and private accounts can participate together in one execution. It highlights how ownership checks work, when an account needs to be claimed, and how multiple post-states are emitted when several accounts are modified.
|
||||
|
||||
> [!NOTE]
|
||||
> The program logic is completely agnostic to whether input accounts are public or private. It always executes the same way.
|
||||
> See `methods/guest/src/bin/hello_world_with_move_function.rs`. The program just reads the instruction bytes and updates the accounts state.
|
||||
> All privacy handling happens on the runner side. When constructing the transaction, the runner decides which accounts are public or private and prepares the appropriate proofs. The program itself can't differentiate between privacy modes.
|
||||
|
||||
Let's start by deploying the program
|
||||
```bash
|
||||
wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin
|
||||
```
|
||||
|
||||
Let's also create a new public account
|
||||
```bash
|
||||
wallet account new public
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Generated new account with account_id Public/95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs at path /0/0
|
||||
```
|
||||
|
||||
Let's execute the write function
|
||||
|
||||
```bash
|
||||
cargo run --bin run_hello_world_with_move_function \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \
|
||||
write-public 95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs mundo!
|
||||
```
|
||||
|
||||
Let's crate a new private account.
|
||||
|
||||
```bash
|
||||
wallet account new private
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Generated new account with account_id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU at path /1
|
||||
```
|
||||
|
||||
Let's execute the write function
|
||||
|
||||
```bash
|
||||
cargo run --bin run_hello_world_with_move_function \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \
|
||||
write-private 8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU Hola
|
||||
```
|
||||
|
||||
To check the values of the accounts are as expected run:
|
||||
```bash
|
||||
wallet account get --account-id Public/95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs
|
||||
```
|
||||
and
|
||||
|
||||
```bash
|
||||
wallet account sync-private
|
||||
wallet account get --account-id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU
|
||||
```
|
||||
|
||||
and check the (base64 encoded) data values are `mundo!` and `Hola` respectively.
|
||||
|
||||
Now we can execute the move function to clear the data on the public account and move it to the private account.
|
||||
|
||||
```bash
|
||||
cargo run --bin run_hello_world_with_move_function \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \
|
||||
move-data-public-to-private 95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs 8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU
|
||||
```
|
||||
|
||||
After succeeding, re run the get and sync commands and check that the public account has empty data and the private account data is `Holamundo!`.
|
||||
|
||||
# 12. Program composition: tail calls
|
||||
Programs can chain calls to other programs when they return. This is the tail call or chained call mechanism. It is used by programs that depend on other programs.
|
||||
|
||||
The examples include a `guest/src/bin/simple_tail_call.rs` program that shows how to trigger this mechanism. It internally calls the first Hello World program with a fixed greeting: `Hello from tail call`.
|
||||
|
||||
> [!NOTE]
|
||||
> This program hardcodes the ID of the Hello World program. If something fails, check that this ID matches the one produced when building the Hello World program. You can see it in the output of `cargo risczero build` from the earlier sections of this tutorial. If it differs, update the ID in `simple_tail_call.rs` and build again.
|
||||
|
||||
As before, let's start by deploying the program
|
||||
|
||||
```bash
|
||||
wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/simple_tail_call.bin
|
||||
```
|
||||
|
||||
We'll use the first public account of this tutorial. The one with account id `BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9`. This account is already owned by the Hello world program and its data reads `Hola mundo!`.
|
||||
|
||||
Let's run the tail call program
|
||||
|
||||
```bash
|
||||
cargo run --bin run_hello_world_through_tail_call \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/simple_tail_call.bin \
|
||||
BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9
|
||||
```
|
||||
|
||||
Once the transaction is processed, query the account values with:
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9
|
||||
```
|
||||
|
||||
You should se an output similar to
|
||||
|
||||
```json
|
||||
{
|
||||
"balance":0,
|
||||
"program_owner_b64":"fpnW4tFY9N6llZcBHaXRwu7xe+7WZnZX9RWzhwNbk1o=",
|
||||
"data_b64":"SG9sYSBtdW5kbyFIZWxsbyBmcm9tIHRhaWwgY2FsbA==",
|
||||
"nonce":0
|
||||
}
|
||||
```
|
||||
|
||||
Decoding the (base64 encoded) data
|
||||
```bash
|
||||
echo -n SG9sYSBtdW5kbyFIZWxsbyBmcm9tIHRhaWwgY2FsbA== | base64 -d
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Hola mundo!Hello from tail call
|
||||
```
|
||||
## Private tail-calls
|
||||
There's support for tail calls in privacy preserving executions too. The `run_hello_world_through_tail_call_private.rs` runner walks you through the process of invoking such an execution.
|
||||
The only difference is that, since the execution is local, the runner will need both programs: the `simple_tail_call` and it's dependency `hello_world`.
|
||||
|
||||
Let's use our existing private account with id `8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU`. This one is already owned by the `hello_world` program.
|
||||
|
||||
You can test the privacy tail calls with
|
||||
```bash
|
||||
cargo run --bin run_hello_world_through_tail_call_private \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/simple_tail_call.bin \
|
||||
$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin \
|
||||
8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU
|
||||
```
|
||||
|
||||
>[!NOTE]
|
||||
> The above command may take longer than the previous privacy executions because needs to generate proofs of execution of both the `simple_tail_call` and the `hello_world` programs.
|
||||
|
||||
Once finished run the following to see the changes
|
||||
```bash
|
||||
wallet account sync-private
|
||||
wallet account get --account-id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU
|
||||
```
|
||||
|
||||
# 13. Program derived accounts: authorizing accounts through tail calls
|
||||
|
||||
## Digression: account authority vs account program ownership
|
||||
|
||||
In NSSA there are two distinct concepts that control who can modify an account:
|
||||
**Program Ownership:** Each account has a field: `program_owner: ProgramId`.
|
||||
This indicates which program is allowed to update the account’s state during execution.
|
||||
- If a program is the program_owner of an account, it can freely mutate its fields.
|
||||
- If the account is uninitialized (`program_owner = DEFAULT_PROGRAM_ID`), a program may claim it and become its owner.
|
||||
- If a program is not the owner and the account is not claimable, any attempt to modify it will cause the transition to fail.
|
||||
Program ownership is about mutation rights during program execution.
|
||||
|
||||
**Account authority**: Independent from program ownership, each account also has an authority. The entity that is allowed to set: `is_authorized = true`. This flag indicates that the account has been authorized for use in a transaction.
|
||||
Who can act as authority?
|
||||
- User-defined accounts: The user is the authority. They can mark an account as authorized by:
|
||||
- Signing the transaction (public accounts)
|
||||
- Providing a valid nullifiers secret key ownership proof (private accounts)
|
||||
- Program derived accounts: Programs are automatically the authority of a dedicated namespace of public accounts.
|
||||
|
||||
Each program owns a non-overlapping space of 2^256 **public** account IDs. They do not overlap with:
|
||||
- User accounts (public or private)
|
||||
- Other program’s PDAs
|
||||
|
||||
> [!NOTE]
|
||||
> Currently PDAs are restricted to the public state.
|
||||
|
||||
A program can be the authority of an account owned by another program, which is the most common case.
|
||||
During a chained call, a program can mark its PDA accounts as `is_authorized=true` without requiring any user signatures or nullifier secret keys. This enables programs to safely authorize accounts during program composition. Importantly, these flags can only be set to true for PDA accounts through an execution of the program that is their authority. No user and no other program can execute any transition that requires authorization of PDA accounts belonging to a different program.
|
||||
|
||||
## Running the example
|
||||
This tutorial includes an example of PDA usage in `methods/guest/src/bin/tail_call_with_pda.rs.`. That program’s sole purpose is to forward one of its own PDA accounts, an account for which it is the authority, to the "Hello World with authorization" program via a chained call. The Hello World program will then claim the account and become its program owner, but the `tail_call_with_pda` program remains the authority. This means it is still the only entity capable of marking that account as `is_authorized=true`.
|
||||
|
||||
Deploy the program:
|
||||
```bash
|
||||
wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/tail_call_with_pda.bin
|
||||
```
|
||||
|
||||
There is no need to create a new account for this example, because we simply use one of the PDA accounts belonging to the `tail_call_with_pda` program.
|
||||
|
||||
Execute the program
|
||||
```bash
|
||||
cargo run --bin run_hello_world_with_authorization_through_tail_call_with_pda $EXAMPLE_PROGRAMS_BUILD_DIR/tail_call_with_pda.bin
|
||||
```
|
||||
|
||||
You'll see an output like the following:
|
||||
|
||||
```bash
|
||||
The program derived account ID is: 3tfTPPuxj3eSE1cLVuNBEk8eSHzpnYS1oqEdeH3Nfsks
|
||||
```
|
||||
|
||||
Then check the status of that account
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/3tfTPPuxj3eSE1cLVuNBEk8eSHzpnYS1oqEdeH3Nfsks
|
||||
```
|
||||
|
||||
Output:
|
||||
```bash
|
||||
{
|
||||
"balance":0,
|
||||
"program_owner_b64":"HZXHYRaKf6YusVo8x00/B15uyY5sGsJb1bzH4KlCY5g=",
|
||||
"data_b64": "SGVsbG8gZnJvbSB0YWlsIGNhbGwgd2l0aCBQcm9ncmFtIERlcml2ZWQgQWNjb3VudCBJRA==",
|
||||
"nonce":0"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
10
examples/program_deployment/methods/Cargo.toml
Normal file
10
examples/program_deployment/methods/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "example_program_deployment_methods"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[build-dependencies]
|
||||
risc0-build.workspace = true
|
||||
|
||||
[package.metadata.risc0]
|
||||
methods = ["guest"]
|
||||
11
examples/program_deployment/methods/guest/Cargo.toml
Normal file
11
examples/program_deployment/methods/guest/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "example_program_deployment_programs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
|
||||
hex.workspace = true
|
||||
bytemuck.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
@ -0,0 +1,60 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
|
||||
};
|
||||
|
||||
// Hello-world example program.
|
||||
//
|
||||
// This program reads an arbitrary sequence of bytes as its instruction
|
||||
// and appends those bytes to the `data` field of the single input account.
|
||||
//
|
||||
// Execution succeeds only if the input account is either:
|
||||
// - uninitialized, or
|
||||
// - already owned by this program.
|
||||
//
|
||||
// In case the input account is uninitialized, the program claims it.
|
||||
//
|
||||
// The updated account is emitted as the sole post-state.
|
||||
|
||||
type Instruction = Vec<u8>;
|
||||
|
||||
fn main() {
|
||||
// Read inputs
|
||||
let (
|
||||
ProgramInput {
|
||||
pre_states,
|
||||
instruction: greeting,
|
||||
},
|
||||
instruction_data,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
// Unpack the input account pre state
|
||||
let [pre_state] = pre_states
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("Input pre states should consist of a single account"));
|
||||
|
||||
// Construct the post state account values
|
||||
let post_account = {
|
||||
let mut this = pre_state.account.clone();
|
||||
let mut bytes = this.data.into_inner();
|
||||
bytes.extend_from_slice(&greeting);
|
||||
this.data = bytes
|
||||
.try_into()
|
||||
.expect("Data should fit within the allowed limits");
|
||||
this
|
||||
};
|
||||
|
||||
// Wrap the post state account values inside a `AccountPostState` instance.
|
||||
// This is used to forward the account claiming request if any
|
||||
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID {
|
||||
// This produces a claim request
|
||||
AccountPostState::new_claimed(post_account)
|
||||
} else {
|
||||
// This doesn't produce a claim request
|
||||
AccountPostState::new(post_account)
|
||||
};
|
||||
|
||||
// The output is a proposed state difference. It will only succeed if the pre states coincide
|
||||
// with the previous values of the accounts, and the transition to the post states conforms
|
||||
// with the NSSA program rules.
|
||||
write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]);
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
|
||||
};
|
||||
|
||||
// Hello-world with authorization example program.
|
||||
//
|
||||
// This program reads an arbitrary sequence of bytes as its instruction
|
||||
// and appends those bytes to the `data` field of the single input account.
|
||||
//
|
||||
// Execution succeeds only if the input account **is authorized** and is either:
|
||||
// - uninitialized, or
|
||||
// - already owned by this program.
|
||||
//
|
||||
// In case the input account is uninitialized, the program claims it.
|
||||
//
|
||||
// The updated account is emitted as the sole post-state.
|
||||
|
||||
type Instruction = Vec<u8>;
|
||||
|
||||
fn main() {
|
||||
// Read inputs
|
||||
let (
|
||||
ProgramInput {
|
||||
pre_states,
|
||||
instruction: greeting,
|
||||
},
|
||||
instruction_data,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
// Unpack the input account pre state
|
||||
let [pre_state] = pre_states
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("Input pre states should consist of a single account"));
|
||||
|
||||
// #### Difference with `hello_world` example here:
|
||||
// Fail if the input account is not authorized
|
||||
// The `is_authorized` field will be correctly populated or verified by the system if
|
||||
// authorization is provided.
|
||||
if !pre_state.is_authorized {
|
||||
panic!("Missing required authorization");
|
||||
}
|
||||
// ####
|
||||
|
||||
// Construct the post state account values
|
||||
let post_account = {
|
||||
let mut this = pre_state.account.clone();
|
||||
let mut bytes = this.data.into_inner();
|
||||
bytes.extend_from_slice(&greeting);
|
||||
this.data = bytes
|
||||
.try_into()
|
||||
.expect("Data should fit within the allowed limits");
|
||||
this
|
||||
};
|
||||
|
||||
// Wrap the post state account values inside a `AccountPostState` instance.
|
||||
// This is used to forward the account claiming request if any
|
||||
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID {
|
||||
// This produces a claim request
|
||||
AccountPostState::new_claimed(post_account)
|
||||
} else {
|
||||
// This doesn't produce a claim request
|
||||
AccountPostState::new(post_account)
|
||||
};
|
||||
|
||||
// The output is a proposed state difference. It will only succeed if the pre states coincide
|
||||
// with the previous values of the accounts, and the transition to the post states conforms
|
||||
// with the NSSA program rules.
|
||||
write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]);
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata},
|
||||
program::{
|
||||
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
|
||||
},
|
||||
};
|
||||
|
||||
// Hello-world with write + move_data example program.
|
||||
//
|
||||
// This program reads an instruction of the form `(function_id, data)` and
|
||||
// dispatches to either:
|
||||
//
|
||||
// - `write`: appends `data` to the `data` field of a single input account.
|
||||
// - `move_data`: moves all bytes from one account to another. The source account is cleared and the
|
||||
// destination account receives the appended bytes.
|
||||
//
|
||||
// Execution succeeds only if:
|
||||
// - the accounts involved are either uninitialized, or
|
||||
// - already owned by this program.
|
||||
//
|
||||
// In case an input account is uninitialized, the program will claim it when
|
||||
// producing the post-state.
|
||||
|
||||
type Instruction = (u8, Vec<u8>);
|
||||
const WRITE_FUNCTION_ID: u8 = 0;
|
||||
const MOVE_DATA_FUNCTION_ID: u8 = 1;
|
||||
|
||||
fn build_post_state(post_account: Account) -> AccountPostState {
|
||||
if post_account.program_owner == DEFAULT_PROGRAM_ID {
|
||||
// This produces a claim request
|
||||
AccountPostState::new_claimed(post_account)
|
||||
} else {
|
||||
// This doesn't produce a claim request
|
||||
AccountPostState::new(post_account)
|
||||
}
|
||||
}
|
||||
|
||||
fn write(pre_state: AccountWithMetadata, greeting: Vec<u8>) -> AccountPostState {
|
||||
// Construct the post state account values
|
||||
let post_account = {
|
||||
let mut this = pre_state.account.clone();
|
||||
let mut bytes = this.data.into_inner();
|
||||
bytes.extend_from_slice(&greeting);
|
||||
this.data = bytes
|
||||
.try_into()
|
||||
.expect("Data should fit within the allowed limits");
|
||||
this
|
||||
};
|
||||
|
||||
build_post_state(post_account)
|
||||
}
|
||||
|
||||
fn move_data(
|
||||
from_pre: &AccountWithMetadata,
|
||||
to_pre: &AccountWithMetadata,
|
||||
) -> Vec<AccountPostState> {
|
||||
// Construct the post state account values
|
||||
let from_data: Vec<u8> = from_pre.account.data.clone().into();
|
||||
|
||||
let from_post = {
|
||||
let mut this = from_pre.account.clone();
|
||||
this.data = Default::default();
|
||||
build_post_state(this)
|
||||
};
|
||||
|
||||
let to_post = {
|
||||
let mut this = to_pre.account.clone();
|
||||
let mut bytes = this.data.into_inner();
|
||||
bytes.extend_from_slice(&from_data);
|
||||
this.data = bytes
|
||||
.try_into()
|
||||
.expect("Data should fit within the allowed limits");
|
||||
build_post_state(this)
|
||||
};
|
||||
|
||||
vec![from_post, to_post]
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Read input accounts.
|
||||
let (
|
||||
ProgramInput {
|
||||
pre_states,
|
||||
instruction: (function_id, data),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let post_states = match (pre_states.as_slice(), function_id, data.len()) {
|
||||
([account_pre], WRITE_FUNCTION_ID, _) => {
|
||||
let post = write(account_pre.clone(), data);
|
||||
vec![post]
|
||||
}
|
||||
([account_from_pre, account_to_pre], MOVE_DATA_FUNCTION_ID, 0) => {
|
||||
move_data(account_from_pre, account_to_pre)
|
||||
}
|
||||
_ => panic!("invalid params"),
|
||||
};
|
||||
|
||||
write_nssa_outputs(instruction_words, pre_states, post_states);
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs,
|
||||
write_nssa_outputs_with_chained_call,
|
||||
};
|
||||
|
||||
// Tail Call example program.
|
||||
//
|
||||
// This program shows how to chain execution to another program using `ChainedCall`.
|
||||
// It reads a single account, emits it unchanged, and then triggers a tail call
|
||||
// to the Hello World program with a fixed greeting.
|
||||
|
||||
/// This needs to be set to the ID of the Hello world program.
|
||||
/// To get the ID run **from the root directoy of the repository**:
|
||||
/// `cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml`
|
||||
/// This compiles the programs and outputs the IDs in hex that can be used to copy here.
|
||||
const HELLO_WORLD_PROGRAM_ID_HEX: &str =
|
||||
"e9dfc5a5d03c9afa732adae6e0edfce4bbb44c7a2afb9f148f4309917eb2de6f";
|
||||
|
||||
fn hello_world_program_id() -> ProgramId {
|
||||
let hello_world_program_id_bytes: [u8; 32] = hex::decode(HELLO_WORLD_PROGRAM_ID_HEX)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
bytemuck::cast(hello_world_program_id_bytes)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Read inputs
|
||||
let (
|
||||
ProgramInput {
|
||||
pre_states,
|
||||
instruction: _,
|
||||
},
|
||||
instruction_data,
|
||||
) = read_nssa_inputs::<()>();
|
||||
|
||||
// Unpack the input account pre state
|
||||
let [pre_state] = pre_states
|
||||
.clone()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("Input pre states should consist of a single account"));
|
||||
|
||||
// Create the (unchanged) post state
|
||||
let post_state = AccountPostState::new(pre_state.account.clone());
|
||||
|
||||
// Create the chained call
|
||||
let chained_call_greeting: Vec<u8> = b"Hello from tail call".to_vec();
|
||||
let chained_call_instruction_data = risc0_zkvm::serde::to_vec(&chained_call_greeting).unwrap();
|
||||
let chained_call = ChainedCall {
|
||||
program_id: hello_world_program_id(),
|
||||
instruction_data: chained_call_instruction_data,
|
||||
pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
// Write the outputs
|
||||
write_nssa_outputs_with_chained_call(
|
||||
instruction_data,
|
||||
vec![pre_state],
|
||||
vec![post_state],
|
||||
vec![chained_call],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs,
|
||||
write_nssa_outputs_with_chained_call,
|
||||
};
|
||||
|
||||
// Tail Call with PDA example program.
|
||||
//
|
||||
// Demonstrates how to chain execution to another program using `ChainedCall`
|
||||
// while authorizing program-derived accounts.
|
||||
//
|
||||
// Expects a single input account whose Account ID is derived from this
|
||||
// program’s ID and the fixed PDA seed below (as defined by the
|
||||
// `<AccountId as From<(&ProgramId, &PdaSeed)>>` implementation).
|
||||
//
|
||||
// Emits this account unchanged, then performs a tail call to the
|
||||
// Hello-World-with-Authorization program with a fixed greeting. The same
|
||||
// account is passed along but marked with `is_authorized = true`.
|
||||
|
||||
const HELLO_WORLD_WITH_AUTHORIZATION_PROGRAM_ID_HEX: &str =
|
||||
"1d95c761168a7fa62eb15a3cc74d3f075e6ec98e6c1ac25bd5bcc7e0a9426398";
|
||||
const PDA_SEED: PdaSeed = PdaSeed::new([37; 32]);
|
||||
|
||||
fn hello_world_program_id() -> ProgramId {
|
||||
let hello_world_program_id_bytes: [u8; 32] =
|
||||
hex::decode(HELLO_WORLD_WITH_AUTHORIZATION_PROGRAM_ID_HEX)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
bytemuck::cast(hello_world_program_id_bytes)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Read inputs
|
||||
let (
|
||||
ProgramInput {
|
||||
pre_states,
|
||||
instruction: _,
|
||||
},
|
||||
instruction_data,
|
||||
) = read_nssa_inputs::<()>();
|
||||
|
||||
// Unpack the input account pre state
|
||||
let [pre_state] = pre_states
|
||||
.clone()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("Input pre states should consist of a single account"));
|
||||
|
||||
// Create the (unchanged) post state
|
||||
let post_state = AccountPostState::new(pre_state.account.clone());
|
||||
|
||||
// Create the chained call
|
||||
let chained_call_greeting: Vec<u8> =
|
||||
b"Hello from tail call with Program Derived Account ID".to_vec();
|
||||
let chained_call_instruction_data = risc0_zkvm::serde::to_vec(&chained_call_greeting).unwrap();
|
||||
|
||||
// Flip the `is_authorized` flag to true
|
||||
let pre_state_for_chained_call = {
|
||||
let mut this = pre_state.clone();
|
||||
this.is_authorized = true;
|
||||
this
|
||||
};
|
||||
let chained_call = ChainedCall {
|
||||
program_id: hello_world_program_id(),
|
||||
instruction_data: chained_call_instruction_data,
|
||||
pre_states: vec![pre_state_for_chained_call],
|
||||
pda_seeds: vec![PDA_SEED],
|
||||
};
|
||||
|
||||
// Write the outputs
|
||||
write_nssa_outputs_with_chained_call(
|
||||
instruction_data,
|
||||
vec![pre_state],
|
||||
vec![post_state],
|
||||
vec![chained_call],
|
||||
);
|
||||
}
|
||||
67
examples/program_deployment/src/bin/run_hello_world.rs
Normal file
67
examples/program_deployment/src/bin/run_hello_world.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use nssa::{
|
||||
AccountId, PublicTransaction,
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use wallet::{WalletCore, helperfunctions::fetch_config};
|
||||
|
||||
// Before running this example, compile the `hello_world.rs` guest program with:
|
||||
//
|
||||
// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml
|
||||
//
|
||||
// Note: you must run the above command from the root of the `lssa` repository.
|
||||
// Note: The compiled binary file is stored in
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// cargo run --bin run_hello_world /path/to/guest/binary <account_id>
|
||||
//
|
||||
// Example:
|
||||
// cargo run --bin run_hello_world \
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin \
|
||||
// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load wallet config and storage
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap();
|
||||
// Second argument is the account_id
|
||||
let account_id: AccountId = std::env::args_os()
|
||||
.nth(2)
|
||||
.unwrap()
|
||||
.into_string()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
// Load the program
|
||||
let bytecode: Vec<u8> = std::fs::read(program_path).unwrap();
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
|
||||
// Define the desired greeting in ASCII
|
||||
let greeting: Vec<u8> = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33];
|
||||
|
||||
// Construct the public transaction
|
||||
// No nonces nor signing keys are needed for this example. Check out the
|
||||
// `run_hello_world_with_authorization` on how to use them.
|
||||
let nonces = vec![];
|
||||
let signing_keys = [];
|
||||
let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, &signing_keys);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.send_tx_public(tx)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
use nssa::{AccountId, program::Program};
|
||||
use wallet::{PrivacyPreservingAccount, WalletCore, helperfunctions::fetch_config};
|
||||
|
||||
// Before running this example, compile the `hello_world.rs` guest program with:
|
||||
//
|
||||
// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml
|
||||
//
|
||||
// Note: you must run the above command from the root of the `lssa` repository.
|
||||
// Note: The compiled binary file is stored in
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// cargo run --bin run_hello_world_private /path/to/guest/binary <account_id>
|
||||
//
|
||||
// Note: the provided account_id needs to be of a private self owned account
|
||||
//
|
||||
// Example:
|
||||
// cargo run --bin run_hello_world_private \
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin \
|
||||
// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load wallet config and storage
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap();
|
||||
// Second argument is the account_id
|
||||
let account_id: AccountId = std::env::args_os()
|
||||
.nth(2)
|
||||
.unwrap()
|
||||
.into_string()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
// Load the program
|
||||
let bytecode: Vec<u8> = std::fs::read(program_path).unwrap();
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
|
||||
// Define the desired greeting in ASCII
|
||||
let greeting: Vec<u8> = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33];
|
||||
|
||||
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
|
||||
|
||||
// Construct and submit the privacy-preserving transaction
|
||||
wallet_core
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
&Program::serialize_instruction(greeting).unwrap(),
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
use nssa::{
|
||||
AccountId, PublicTransaction,
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use wallet::{WalletCore, helperfunctions::fetch_config};
|
||||
|
||||
// Before running this example, compile the `simple_tail_call.rs` guest program with:
|
||||
//
|
||||
// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml
|
||||
//
|
||||
// Note: you must run the above command from the root of the `lssa` repository.
|
||||
// Note: The compiled binary file is stored in
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// cargo run --bin run_hello_world_through_tail_call /path/to/guest/binary <account_id>
|
||||
//
|
||||
// Example:
|
||||
// cargo run --bin run_hello_world_through_tail_call \
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin \
|
||||
// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load wallet config and storage
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap();
|
||||
// Second argument is the account_id
|
||||
let account_id: AccountId = std::env::args_os()
|
||||
.nth(2)
|
||||
.unwrap()
|
||||
.into_string()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
// Load the program
|
||||
let bytecode: Vec<u8> = std::fs::read(program_path).unwrap();
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
|
||||
let instruction_data = ();
|
||||
let nonces = vec![];
|
||||
let signing_keys = [];
|
||||
let message =
|
||||
Message::try_new(program.id(), vec![account_id], nonces, instruction_data).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, &signing_keys);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.send_tx_public(tx)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use nssa::{
|
||||
AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies,
|
||||
program::Program,
|
||||
};
|
||||
use wallet::{PrivacyPreservingAccount, WalletCore, helperfunctions::fetch_config};
|
||||
|
||||
// Before running this example, compile the `simple_tail_call.rs` guest program with:
|
||||
//
|
||||
// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml
|
||||
//
|
||||
// Note: you must run the above command from the root of the `lssa` repository.
|
||||
// Note: The compiled binary file is stored in
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// cargo run --bin run_hello_world_through_tail_call_private /path/to/guest/binary <account_id>
|
||||
//
|
||||
// Example:
|
||||
// cargo run --bin run_hello_world_through_tail_call \
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin \
|
||||
// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load wallet config and storage
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the simple_tail_call program binary
|
||||
let simple_tail_call_path = std::env::args_os().nth(1).unwrap().into_string().unwrap();
|
||||
// Second argument is the path to the hello_world program binary
|
||||
let hello_world_path = std::env::args_os().nth(2).unwrap().into_string().unwrap();
|
||||
// Third argument is the account_id
|
||||
let account_id: AccountId = std::env::args_os()
|
||||
.nth(3)
|
||||
.unwrap()
|
||||
.into_string()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
// Load the program and its dependencies (the hellow world program)
|
||||
let simple_tail_call_bytecode: Vec<u8> = std::fs::read(simple_tail_call_path).unwrap();
|
||||
let simple_tail_call = Program::new(simple_tail_call_bytecode).unwrap();
|
||||
let hello_world_bytecode: Vec<u8> = std::fs::read(hello_world_path).unwrap();
|
||||
let hello_world = Program::new(hello_world_bytecode).unwrap();
|
||||
let dependencies: HashMap<ProgramId, Program> =
|
||||
[(hello_world.id(), hello_world)].into_iter().collect();
|
||||
let program_with_dependencies = ProgramWithDependencies::new(simple_tail_call, dependencies);
|
||||
|
||||
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
|
||||
|
||||
// Construct and submit the privacy-preserving transaction
|
||||
let instruction = ();
|
||||
wallet_core
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
&Program::serialize_instruction(instruction).unwrap(),
|
||||
&program_with_dependencies,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
use nssa::{
|
||||
AccountId, PublicTransaction,
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use wallet::{WalletCore, helperfunctions::fetch_config};
|
||||
|
||||
// Before running this example, compile the `hello_world_with_authorization.rs` guest program with:
|
||||
//
|
||||
// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml
|
||||
//
|
||||
// Note: you must run the above command from the root of the `lssa` repository.
|
||||
// Note: The compiled binary file is stored in
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_authorization.bin
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// ./run_hello_world_with_authorization /path/to/guest/binary <account_id>
|
||||
//
|
||||
// Note: the provided account_id needs to be of a public self owned account
|
||||
//
|
||||
// Example:
|
||||
// cargo run --bin run_hello_world_with_authorization \
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_authorization.bin \
|
||||
// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load wallet config and storage
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap();
|
||||
// Second argument is the account_id
|
||||
let account_id: AccountId = std::env::args_os()
|
||||
.nth(2)
|
||||
.unwrap()
|
||||
.into_string()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
// Load the program
|
||||
let bytecode: Vec<u8> = std::fs::read(program_path).unwrap();
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
|
||||
// Load signing keys to provide authorization
|
||||
let signing_key = wallet_core
|
||||
.storage
|
||||
.user_data
|
||||
.get_pub_account_signing_key(&account_id)
|
||||
.expect("Input account should be a self owned public account");
|
||||
|
||||
// Define the desired greeting in ASCII
|
||||
let greeting: Vec<u8> = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33];
|
||||
|
||||
// Construct the public transaction
|
||||
// Query the current nonce from the node
|
||||
let nonces = wallet_core
|
||||
.get_accounts_nonces(vec![account_id])
|
||||
.await
|
||||
.expect("Node should be reachable to query account data");
|
||||
let signing_keys = [signing_key];
|
||||
let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap();
|
||||
// Pass the signing key to sign the message. This will be used by the node
|
||||
// to flag the pre_state as `is_authorized` when executing the program
|
||||
let witness_set = WitnessSet::for_message(&message, &signing_keys);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.send_tx_public(tx)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
use nssa::{
|
||||
AccountId, PublicTransaction,
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use nssa_core::program::PdaSeed;
|
||||
use wallet::{WalletCore, helperfunctions::fetch_config};
|
||||
|
||||
// Before running this example, compile the `simple_tail_call.rs` guest program with:
|
||||
//
|
||||
// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml
|
||||
//
|
||||
// Note: you must run the above command from the root of the `lssa` repository.
|
||||
// Note: The compiled binary file is stored in
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// cargo run --bin run_hello_world_with_authorization_through_tail_call_with_pda
|
||||
// /path/to/guest/binary <account_id>
|
||||
//
|
||||
// Example:
|
||||
// cargo run --bin run_hello_world_with_authorization_through_tail_call_with_pda \
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/tail_call_with_pda.bin
|
||||
|
||||
const PDA_SEED: PdaSeed = PdaSeed::new([37; 32]);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load wallet config and storage
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap();
|
||||
|
||||
// Load the program
|
||||
let bytecode: Vec<u8> = std::fs::read(program_path).unwrap();
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
|
||||
// Compute the PDA to pass it as input account to the public execution
|
||||
let pda = AccountId::from((&program.id(), &PDA_SEED));
|
||||
let account_ids = vec![pda];
|
||||
let instruction_data = ();
|
||||
let nonces = vec![];
|
||||
let signing_keys = [];
|
||||
let message = Message::try_new(program.id(), account_ids, nonces, instruction_data).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, &signing_keys);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.send_tx_public(tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("The program derived account id is: {pda}");
|
||||
}
|
||||
@ -0,0 +1,155 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use nssa::{PublicTransaction, program::Program, public_transaction};
|
||||
use wallet::{PrivacyPreservingAccount, WalletCore, helperfunctions::fetch_config};
|
||||
|
||||
// Before running this example, compile the `hello_world_with_move_function.rs` guest program with:
|
||||
//
|
||||
// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml
|
||||
//
|
||||
// Note: you must run the above command from the root of the `lssa` repository.
|
||||
// Note: The compiled binary file is stored in
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_move_function.bin
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// cargo run --bin run_hello_world_with_move_function /path/to/guest/binary <function> <params>
|
||||
//
|
||||
// Example:
|
||||
// cargo run --bin run_hello_world_with_move_function \
|
||||
// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_move_function.bin \
|
||||
// write-public Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE Hola
|
||||
|
||||
type Instruction = (u8, Vec<u8>);
|
||||
const WRITE_FUNCTION_ID: u8 = 0;
|
||||
const MOVE_DATA_FUNCTION_ID: u8 = 1;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
/// Path to program binary
|
||||
program_path: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Write instruction into one account
|
||||
WritePublic {
|
||||
account_id: String,
|
||||
greeting: String,
|
||||
},
|
||||
WritePrivate {
|
||||
account_id: String,
|
||||
greeting: String,
|
||||
},
|
||||
/// Move data between two accounts
|
||||
MoveDataPublicToPublic {
|
||||
from: String,
|
||||
to: String,
|
||||
},
|
||||
MoveDataPublicToPrivate {
|
||||
from: String,
|
||||
to: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Load the program
|
||||
let bytecode: Vec<u8> = std::fs::read(cli.program_path).unwrap();
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
|
||||
// Load wallet config and storage
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_core = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match cli.command {
|
||||
Command::WritePublic {
|
||||
account_id,
|
||||
greeting,
|
||||
} => {
|
||||
let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes());
|
||||
let account_id = account_id.parse().unwrap();
|
||||
let nonces = vec![];
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![account_id],
|
||||
nonces,
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.send_tx_public(tx)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Command::WritePrivate {
|
||||
account_id,
|
||||
greeting,
|
||||
} => {
|
||||
let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes());
|
||||
let account_id = account_id.parse().unwrap();
|
||||
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
|
||||
|
||||
wallet_core
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
&Program::serialize_instruction(instruction).unwrap(),
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Command::MoveDataPublicToPublic { from, to } => {
|
||||
let instruction: Instruction = (MOVE_DATA_FUNCTION_ID, vec![]);
|
||||
let from = from.parse().unwrap();
|
||||
let to = to.parse().unwrap();
|
||||
let nonces = vec![];
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![from, to],
|
||||
nonces,
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.send_tx_public(tx)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Command::MoveDataPublicToPrivate { from, to } => {
|
||||
let instruction: Instruction = (MOVE_DATA_FUNCTION_ID, vec![]);
|
||||
let from = from.parse().unwrap();
|
||||
let to = to.parse().unwrap();
|
||||
|
||||
let accounts = vec![
|
||||
PrivacyPreservingAccount::Public(from),
|
||||
PrivacyPreservingAccount::PrivateOwned(to),
|
||||
];
|
||||
|
||||
wallet_core
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
&Program::serialize_instruction(instruction).unwrap(),
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
26
integration_tests/Cargo.toml
Normal file
26
integration_tests/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "integration_tests"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
nssa_core = { workspace = true, features = ["host"] }
|
||||
nssa.workspace = true
|
||||
sequencer_core = { workspace = true, features = ["testnet"] }
|
||||
sequencer_runner.workspace = true
|
||||
wallet.workspace = true
|
||||
common.workspace = true
|
||||
key_protocol.workspace = true
|
||||
proc_macro_test_attribute = { path = "./proc_macro_test_attribute" }
|
||||
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
anyhow.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
actix.workspace = true
|
||||
actix-web.workspace = true
|
||||
base64.workspace = true
|
||||
tokio.workspace = true
|
||||
hex.workspace = true
|
||||
tempfile.workspace = true
|
||||
borsh.workspace = true
|
||||
158
integration_tests/configs/debug/sequencer/sequencer_config.json
Normal file
158
integration_tests/configs/debug/sequencer/sequencer_config.json
Normal file
@ -0,0 +1,158 @@
|
||||
{
|
||||
"home": "./sequencer",
|
||||
"override_rust_log": null,
|
||||
"genesis_id": 1,
|
||||
"is_genesis_random": true,
|
||||
"max_num_tx_in_block": 20,
|
||||
"mempool_max_size": 10000,
|
||||
"block_create_timeout_millis": 10000,
|
||||
"port": 3040,
|
||||
"initial_accounts": [
|
||||
{
|
||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
||||
"balance": 10000
|
||||
},
|
||||
{
|
||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
||||
"balance": 20000
|
||||
}
|
||||
],
|
||||
"initial_commitments": [
|
||||
{
|
||||
"npk": [
|
||||
63,
|
||||
202,
|
||||
178,
|
||||
231,
|
||||
183,
|
||||
82,
|
||||
237,
|
||||
212,
|
||||
216,
|
||||
221,
|
||||
215,
|
||||
255,
|
||||
153,
|
||||
101,
|
||||
177,
|
||||
161,
|
||||
254,
|
||||
210,
|
||||
128,
|
||||
122,
|
||||
54,
|
||||
190,
|
||||
230,
|
||||
151,
|
||||
183,
|
||||
64,
|
||||
225,
|
||||
229,
|
||||
113,
|
||||
1,
|
||||
228,
|
||||
97
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 10000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"npk": [
|
||||
192,
|
||||
251,
|
||||
166,
|
||||
243,
|
||||
167,
|
||||
236,
|
||||
84,
|
||||
249,
|
||||
35,
|
||||
136,
|
||||
130,
|
||||
172,
|
||||
219,
|
||||
225,
|
||||
161,
|
||||
139,
|
||||
229,
|
||||
89,
|
||||
243,
|
||||
125,
|
||||
194,
|
||||
213,
|
||||
209,
|
||||
30,
|
||||
23,
|
||||
174,
|
||||
100,
|
||||
244,
|
||||
124,
|
||||
74,
|
||||
140,
|
||||
47
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 20000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"signing_key": [
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37
|
||||
]
|
||||
}
|
||||
547
integration_tests/configs/debug/wallet/wallet_config.json
Normal file
547
integration_tests/configs/debug/wallet/wallet_config.json
Normal file
@ -0,0 +1,547 @@
|
||||
{
|
||||
"override_rust_log": null,
|
||||
"sequencer_addr": "http://127.0.0.1:3040",
|
||||
"seq_poll_timeout_millis": 12000,
|
||||
"seq_tx_poll_max_blocks": 5,
|
||||
"seq_poll_max_retries": 5,
|
||||
"seq_block_poll_max_amount": 100,
|
||||
"initial_accounts": [
|
||||
{
|
||||
"Public": {
|
||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
||||
"pub_sign_key": [
|
||||
16,
|
||||
162,
|
||||
106,
|
||||
154,
|
||||
236,
|
||||
125,
|
||||
52,
|
||||
184,
|
||||
35,
|
||||
100,
|
||||
238,
|
||||
174,
|
||||
69,
|
||||
197,
|
||||
41,
|
||||
77,
|
||||
187,
|
||||
10,
|
||||
118,
|
||||
75,
|
||||
0,
|
||||
11,
|
||||
148,
|
||||
238,
|
||||
185,
|
||||
181,
|
||||
133,
|
||||
17,
|
||||
220,
|
||||
72,
|
||||
124,
|
||||
77
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Public": {
|
||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
||||
"pub_sign_key": [
|
||||
113,
|
||||
121,
|
||||
64,
|
||||
177,
|
||||
204,
|
||||
85,
|
||||
229,
|
||||
214,
|
||||
178,
|
||||
6,
|
||||
109,
|
||||
191,
|
||||
29,
|
||||
154,
|
||||
63,
|
||||
38,
|
||||
242,
|
||||
18,
|
||||
244,
|
||||
219,
|
||||
8,
|
||||
208,
|
||||
35,
|
||||
136,
|
||||
23,
|
||||
127,
|
||||
207,
|
||||
237,
|
||||
216,
|
||||
169,
|
||||
190,
|
||||
27
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Private": {
|
||||
"account_id": "3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw",
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 10000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
},
|
||||
"key_chain": {
|
||||
"secret_spending_key": [
|
||||
251,
|
||||
82,
|
||||
235,
|
||||
1,
|
||||
146,
|
||||
96,
|
||||
30,
|
||||
81,
|
||||
162,
|
||||
234,
|
||||
33,
|
||||
15,
|
||||
123,
|
||||
129,
|
||||
116,
|
||||
0,
|
||||
84,
|
||||
136,
|
||||
176,
|
||||
70,
|
||||
190,
|
||||
224,
|
||||
161,
|
||||
54,
|
||||
134,
|
||||
142,
|
||||
154,
|
||||
1,
|
||||
18,
|
||||
251,
|
||||
242,
|
||||
189
|
||||
],
|
||||
"private_key_holder": {
|
||||
"nullifier_secret_key": [
|
||||
29,
|
||||
250,
|
||||
10,
|
||||
187,
|
||||
35,
|
||||
123,
|
||||
180,
|
||||
250,
|
||||
246,
|
||||
97,
|
||||
216,
|
||||
153,
|
||||
44,
|
||||
156,
|
||||
16,
|
||||
93,
|
||||
241,
|
||||
26,
|
||||
174,
|
||||
219,
|
||||
72,
|
||||
84,
|
||||
34,
|
||||
247,
|
||||
112,
|
||||
101,
|
||||
217,
|
||||
243,
|
||||
189,
|
||||
173,
|
||||
75,
|
||||
20
|
||||
],
|
||||
"incoming_viewing_secret_key": [
|
||||
251,
|
||||
201,
|
||||
22,
|
||||
154,
|
||||
100,
|
||||
165,
|
||||
218,
|
||||
108,
|
||||
163,
|
||||
190,
|
||||
135,
|
||||
91,
|
||||
145,
|
||||
84,
|
||||
69,
|
||||
241,
|
||||
46,
|
||||
117,
|
||||
217,
|
||||
110,
|
||||
197,
|
||||
248,
|
||||
91,
|
||||
193,
|
||||
14,
|
||||
104,
|
||||
88,
|
||||
103,
|
||||
67,
|
||||
153,
|
||||
182,
|
||||
158
|
||||
],
|
||||
"outgoing_viewing_secret_key": [
|
||||
25,
|
||||
67,
|
||||
121,
|
||||
76,
|
||||
175,
|
||||
100,
|
||||
30,
|
||||
198,
|
||||
105,
|
||||
123,
|
||||
49,
|
||||
169,
|
||||
75,
|
||||
178,
|
||||
75,
|
||||
210,
|
||||
100,
|
||||
143,
|
||||
210,
|
||||
243,
|
||||
228,
|
||||
243,
|
||||
21,
|
||||
18,
|
||||
36,
|
||||
84,
|
||||
164,
|
||||
186,
|
||||
139,
|
||||
113,
|
||||
214,
|
||||
12
|
||||
]
|
||||
},
|
||||
"nullifer_public_key": [
|
||||
63,
|
||||
202,
|
||||
178,
|
||||
231,
|
||||
183,
|
||||
82,
|
||||
237,
|
||||
212,
|
||||
216,
|
||||
221,
|
||||
215,
|
||||
255,
|
||||
153,
|
||||
101,
|
||||
177,
|
||||
161,
|
||||
254,
|
||||
210,
|
||||
128,
|
||||
122,
|
||||
54,
|
||||
190,
|
||||
230,
|
||||
151,
|
||||
183,
|
||||
64,
|
||||
225,
|
||||
229,
|
||||
113,
|
||||
1,
|
||||
228,
|
||||
97
|
||||
],
|
||||
"incoming_viewing_public_key": [
|
||||
3,
|
||||
235,
|
||||
139,
|
||||
131,
|
||||
237,
|
||||
177,
|
||||
122,
|
||||
189,
|
||||
6,
|
||||
177,
|
||||
167,
|
||||
178,
|
||||
202,
|
||||
117,
|
||||
246,
|
||||
58,
|
||||
28,
|
||||
65,
|
||||
132,
|
||||
79,
|
||||
220,
|
||||
139,
|
||||
119,
|
||||
243,
|
||||
187,
|
||||
160,
|
||||
212,
|
||||
121,
|
||||
61,
|
||||
247,
|
||||
116,
|
||||
72,
|
||||
205
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Private": {
|
||||
"account_id": "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX",
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 20000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
},
|
||||
"key_chain": {
|
||||
"secret_spending_key": [
|
||||
238,
|
||||
171,
|
||||
241,
|
||||
69,
|
||||
111,
|
||||
217,
|
||||
85,
|
||||
64,
|
||||
19,
|
||||
82,
|
||||
18,
|
||||
189,
|
||||
32,
|
||||
91,
|
||||
78,
|
||||
175,
|
||||
107,
|
||||
7,
|
||||
109,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
243,
|
||||
230,
|
||||
72,
|
||||
244,
|
||||
192,
|
||||
92,
|
||||
137,
|
||||
33,
|
||||
118,
|
||||
254
|
||||
],
|
||||
"private_key_holder": {
|
||||
"nullifier_secret_key": [
|
||||
25,
|
||||
211,
|
||||
215,
|
||||
119,
|
||||
57,
|
||||
223,
|
||||
247,
|
||||
37,
|
||||
245,
|
||||
144,
|
||||
122,
|
||||
29,
|
||||
118,
|
||||
245,
|
||||
83,
|
||||
228,
|
||||
23,
|
||||
9,
|
||||
101,
|
||||
120,
|
||||
88,
|
||||
33,
|
||||
238,
|
||||
207,
|
||||
128,
|
||||
61,
|
||||
110,
|
||||
2,
|
||||
89,
|
||||
62,
|
||||
164,
|
||||
13
|
||||
],
|
||||
"incoming_viewing_secret_key": [
|
||||
193,
|
||||
181,
|
||||
14,
|
||||
196,
|
||||
142,
|
||||
84,
|
||||
15,
|
||||
65,
|
||||
128,
|
||||
101,
|
||||
70,
|
||||
196,
|
||||
241,
|
||||
47,
|
||||
130,
|
||||
221,
|
||||
23,
|
||||
146,
|
||||
161,
|
||||
237,
|
||||
221,
|
||||
40,
|
||||
19,
|
||||
126,
|
||||
59,
|
||||
15,
|
||||
169,
|
||||
236,
|
||||
25,
|
||||
105,
|
||||
104,
|
||||
231
|
||||
],
|
||||
"outgoing_viewing_secret_key": [
|
||||
20,
|
||||
170,
|
||||
220,
|
||||
108,
|
||||
41,
|
||||
23,
|
||||
155,
|
||||
217,
|
||||
247,
|
||||
190,
|
||||
175,
|
||||
168,
|
||||
247,
|
||||
34,
|
||||
105,
|
||||
134,
|
||||
114,
|
||||
74,
|
||||
104,
|
||||
91,
|
||||
211,
|
||||
62,
|
||||
126,
|
||||
13,
|
||||
130,
|
||||
100,
|
||||
241,
|
||||
214,
|
||||
250,
|
||||
236,
|
||||
38,
|
||||
150
|
||||
]
|
||||
},
|
||||
"nullifer_public_key": [
|
||||
192,
|
||||
251,
|
||||
166,
|
||||
243,
|
||||
167,
|
||||
236,
|
||||
84,
|
||||
249,
|
||||
35,
|
||||
136,
|
||||
130,
|
||||
172,
|
||||
219,
|
||||
225,
|
||||
161,
|
||||
139,
|
||||
229,
|
||||
89,
|
||||
243,
|
||||
125,
|
||||
194,
|
||||
213,
|
||||
209,
|
||||
30,
|
||||
23,
|
||||
174,
|
||||
100,
|
||||
244,
|
||||
124,
|
||||
74,
|
||||
140,
|
||||
47
|
||||
],
|
||||
"incoming_viewing_public_key": [
|
||||
2,
|
||||
181,
|
||||
98,
|
||||
93,
|
||||
216,
|
||||
241,
|
||||
241,
|
||||
110,
|
||||
58,
|
||||
198,
|
||||
119,
|
||||
174,
|
||||
250,
|
||||
184,
|
||||
1,
|
||||
204,
|
||||
200,
|
||||
173,
|
||||
44,
|
||||
238,
|
||||
37,
|
||||
247,
|
||||
170,
|
||||
156,
|
||||
100,
|
||||
254,
|
||||
116,
|
||||
242,
|
||||
28,
|
||||
183,
|
||||
187,
|
||||
77,
|
||||
255
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"basic_auth": null
|
||||
}
|
||||
9
integration_tests/proc_macro_test_attribute/Cargo.toml
Normal file
9
integration_tests/proc_macro_test_attribute/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "proc_macro_test_attribute"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
49
integration_tests/proc_macro_test_attribute/src/lib.rs
Normal file
49
integration_tests/proc_macro_test_attribute/src/lib.rs
Normal file
@ -0,0 +1,49 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
use proc_macro::*;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn nssa_integration_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input = item.to_string();
|
||||
|
||||
let fn_keyword = "fn ";
|
||||
let fn_keyword_alternative = "fn\n";
|
||||
|
||||
let mut start_opt = None;
|
||||
let mut fn_name = String::new();
|
||||
|
||||
if let Some(start) = input.find(fn_keyword) {
|
||||
start_opt = Some(start);
|
||||
} else if let Some(start) = input.find(fn_keyword_alternative) {
|
||||
start_opt = Some(start);
|
||||
}
|
||||
|
||||
if let Some(start) = start_opt {
|
||||
let rest = &input[start + fn_keyword.len()..];
|
||||
if let Some(end) = rest.find(|c: char| c == '(' || c.is_whitespace()) {
|
||||
let name = &rest[..end];
|
||||
fn_name = name.to_string();
|
||||
}
|
||||
} else {
|
||||
println!("ERROR: keyword fn not found");
|
||||
}
|
||||
|
||||
let extension = format!(
|
||||
r#"
|
||||
{input}
|
||||
|
||||
function_map.insert("{fn_name}".to_string(), |home_dir: PathBuf| Box::pin(async {{
|
||||
let res = pre_test(home_dir).await.unwrap();
|
||||
|
||||
info!("Waiting for first block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
{fn_name}().await;
|
||||
|
||||
post_test(res).await;
|
||||
}}));
|
||||
"#
|
||||
);
|
||||
|
||||
extension.parse().unwrap()
|
||||
}
|
||||
189
integration_tests/src/lib.rs
Normal file
189
integration_tests/src/lib.rs
Normal file
@ -0,0 +1,189 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix_web::dev::ServerHandle;
|
||||
use anyhow::Result;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||
use clap::Parser;
|
||||
use common::{
|
||||
sequencer_client::SequencerClient,
|
||||
transaction::{EncodedTransaction, NSSATransaction},
|
||||
};
|
||||
use log::{info, warn};
|
||||
use nssa::PrivacyPreservingTransaction;
|
||||
use nssa_core::Commitment;
|
||||
use sequencer_core::config::SequencerConfig;
|
||||
use sequencer_runner::startup_sequencer;
|
||||
use tempfile::TempDir;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::test_suite_map::{prepare_function_map, tps_test};
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_test_attribute;
|
||||
|
||||
pub mod test_suite_map;
|
||||
|
||||
mod tps_test_utils;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version)]
|
||||
struct Args {
|
||||
/// Path to configs
|
||||
home_dir: PathBuf,
|
||||
/// Test name
|
||||
test_name: String,
|
||||
}
|
||||
|
||||
pub const ACC_SENDER: &str = "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy";
|
||||
pub const ACC_RECEIVER: &str = "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw";
|
||||
|
||||
pub const ACC_SENDER_PRIVATE: &str = "3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw";
|
||||
pub const ACC_RECEIVER_PRIVATE: &str = "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX";
|
||||
|
||||
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12;
|
||||
|
||||
pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin";
|
||||
|
||||
fn make_public_account_input_from_str(account_id: &str) -> String {
|
||||
format!("Public/{account_id}")
|
||||
}
|
||||
|
||||
fn make_private_account_input_from_str(account_id: &str) -> String {
|
||||
format!("Private/{account_id}")
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub async fn pre_test(
|
||||
home_dir: PathBuf,
|
||||
) -> Result<(ServerHandle, JoinHandle<Result<()>>, TempDir)> {
|
||||
wallet::cli::execute_setup("test_pass".to_owned()).await?;
|
||||
|
||||
let home_dir_sequencer = home_dir.join("sequencer");
|
||||
|
||||
let mut sequencer_config =
|
||||
sequencer_runner::config::from_file(home_dir_sequencer.join("sequencer_config.json"))
|
||||
.unwrap();
|
||||
|
||||
let temp_dir_sequencer = replace_home_dir_with_temp_dir_in_configs(&mut sequencer_config);
|
||||
|
||||
let (seq_http_server_handle, sequencer_loop_handle) =
|
||||
startup_sequencer(sequencer_config).await?;
|
||||
|
||||
Ok((
|
||||
seq_http_server_handle,
|
||||
sequencer_loop_handle,
|
||||
temp_dir_sequencer,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn replace_home_dir_with_temp_dir_in_configs(
|
||||
sequencer_config: &mut SequencerConfig,
|
||||
) -> TempDir {
|
||||
let temp_dir_sequencer = tempfile::tempdir().unwrap();
|
||||
|
||||
sequencer_config.home = temp_dir_sequencer.path().to_path_buf();
|
||||
|
||||
temp_dir_sequencer
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub async fn post_test(residual: (ServerHandle, JoinHandle<Result<()>>, TempDir)) {
|
||||
let (seq_http_server_handle, sequencer_loop_handle, _) = residual;
|
||||
|
||||
info!("Cleanup");
|
||||
|
||||
sequencer_loop_handle.abort();
|
||||
seq_http_server_handle.stop(true).await;
|
||||
|
||||
let wallet_home = wallet::helperfunctions::get_home().unwrap();
|
||||
let persistent_data_home = wallet_home.join("storage.json");
|
||||
|
||||
// Removing persistent accounts after run to not affect other executions
|
||||
// Not necessary an error, if fails as there is tests for failure scenario
|
||||
let _ = std::fs::remove_file(persistent_data_home)
|
||||
.inspect_err(|err| warn!("Failed to remove persistent data with err {err:#?}"));
|
||||
|
||||
// At this point all of the references to sequencer_core must be lost.
|
||||
// So they are dropped and tempdirs will be dropped too,
|
||||
}
|
||||
|
||||
pub async fn main_tests_runner() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let args = Args::parse();
|
||||
let Args {
|
||||
home_dir,
|
||||
test_name,
|
||||
} = args;
|
||||
|
||||
let function_map = prepare_function_map();
|
||||
|
||||
match test_name.as_str() {
|
||||
"all" => {
|
||||
// Tests that use default config
|
||||
for (_, fn_pointer) in function_map {
|
||||
fn_pointer(home_dir.clone()).await;
|
||||
}
|
||||
// Run TPS test with its own specific config
|
||||
tps_test().await;
|
||||
}
|
||||
_ => {
|
||||
let fn_pointer = function_map.get(&test_name).expect("Unknown test name");
|
||||
|
||||
fn_pointer(home_dir.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_privacy_preserving_tx(
|
||||
seq_client: &SequencerClient,
|
||||
tx_hash: String,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let transaction_encoded = seq_client
|
||||
.get_transaction_by_hash(tx_hash.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.transaction
|
||||
.unwrap();
|
||||
|
||||
let tx_base64_decode = BASE64.decode(transaction_encoded).unwrap();
|
||||
match NSSATransaction::try_from(
|
||||
&borsh::from_slice::<EncodedTransaction>(&tx_base64_decode).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
{
|
||||
NSSATransaction::PrivacyPreserving(privacy_preserving_transaction) => {
|
||||
privacy_preserving_transaction
|
||||
}
|
||||
_ => panic!("Invalid tx type"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_commitment_is_in_state(
|
||||
commitment: Commitment,
|
||||
seq_client: &SequencerClient,
|
||||
) -> bool {
|
||||
matches!(
|
||||
seq_client.get_proof_for_commitment(commitment).await,
|
||||
Ok(Some(_))
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{make_private_account_input_from_str, make_public_account_input_from_str};
|
||||
|
||||
#[test]
|
||||
fn correct_account_id_from_prefix() {
|
||||
let account_id1 = "cafecafe";
|
||||
let account_id2 = "deadbeaf";
|
||||
|
||||
let account_id1_pub = make_public_account_input_from_str(account_id1);
|
||||
let account_id2_priv = make_private_account_input_from_str(account_id2);
|
||||
|
||||
assert_eq!(account_id1_pub, "Public/cafecafe".to_string());
|
||||
assert_eq!(account_id2_priv, "Private/deadbeaf".to_string());
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use integration_tests::main_tests_runner;
|
||||
|
||||
use node_runner::main_runner;
|
||||
|
||||
pub const NUM_THREADS: usize = 4;
|
||||
pub const NUM_THREADS: usize = 8;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
actix::System::with_tokio_rt(|| {
|
||||
@ -12,5 +11,5 @@ fn main() -> Result<()> {
|
||||
.build()
|
||||
.unwrap()
|
||||
})
|
||||
.block_on(main_runner())
|
||||
.block_on(main_tests_runner())
|
||||
}
|
||||
3154
integration_tests/src/test_suite_map.rs
Normal file
3154
integration_tests/src/test_suite_map.rs
Normal file
File diff suppressed because it is too large
Load Diff
187
integration_tests/src/tps_test_utils.rs
Normal file
187
integration_tests/src/tps_test_utils.rs
Normal file
@ -0,0 +1,187 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use nssa::{
|
||||
Account, AccountId, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction,
|
||||
privacy_preserving_transaction::{self as pptx, circuit},
|
||||
program::Program,
|
||||
public_transaction as putx,
|
||||
};
|
||||
use nssa_core::{
|
||||
MembershipProof, NullifierPublicKey,
|
||||
account::{AccountWithMetadata, data::Data},
|
||||
encryption::IncomingViewingPublicKey,
|
||||
};
|
||||
use sequencer_core::config::{AccountInitialData, CommitmentsInitialData, SequencerConfig};
|
||||
|
||||
pub(crate) struct TpsTestManager {
|
||||
public_keypairs: Vec<(PrivateKey, AccountId)>,
|
||||
target_tps: u64,
|
||||
}
|
||||
|
||||
impl TpsTestManager {
|
||||
/// Generates public account keypairs. These are used to populate the config and to generate
|
||||
/// valid public transactions for the tps test.
|
||||
pub(crate) fn new(target_tps: u64, number_transactions: usize) -> Self {
|
||||
let public_keypairs = (1..(number_transactions + 2))
|
||||
.map(|i| {
|
||||
let mut private_key_bytes = [0u8; 32];
|
||||
private_key_bytes[..8].copy_from_slice(&i.to_le_bytes());
|
||||
let private_key = PrivateKey::try_new(private_key_bytes).unwrap();
|
||||
let public_key = PublicKey::new_from_private_key(&private_key);
|
||||
let account_id = AccountId::from(&public_key);
|
||||
(private_key, account_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Self {
|
||||
public_keypairs,
|
||||
target_tps,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn target_time(&self) -> Duration {
|
||||
let number_transactions = (self.public_keypairs.len() - 1) as u64;
|
||||
Duration::from_secs_f64(number_transactions as f64 / self.target_tps as f64)
|
||||
}
|
||||
|
||||
/// Build a batch of public transactions to submit to the node.
|
||||
pub fn build_public_txs(&self) -> Vec<PublicTransaction> {
|
||||
// Create valid public transactions
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let public_txs: Vec<PublicTransaction> = self
|
||||
.public_keypairs
|
||||
.windows(2)
|
||||
.map(|pair| {
|
||||
let amount: u128 = 1;
|
||||
let message = putx::Message::try_new(
|
||||
program.id(),
|
||||
[pair[0].1, pair[1].1].to_vec(),
|
||||
[0u128].to_vec(),
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set =
|
||||
nssa::public_transaction::WitnessSet::for_message(&message, &[&pair[0].0]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
})
|
||||
.collect();
|
||||
|
||||
public_txs
|
||||
}
|
||||
|
||||
/// Generates a sequencer configuration with initial balance in a number of public accounts.
|
||||
/// The transactions generated with the function `build_public_txs` will be valid in a node
|
||||
/// started with the config from this method.
|
||||
pub(crate) fn generate_tps_test_config(&self) -> SequencerConfig {
|
||||
// Create public public keypairs
|
||||
let initial_public_accounts = self
|
||||
.public_keypairs
|
||||
.iter()
|
||||
.map(|(_, account_id)| AccountInitialData {
|
||||
account_id: account_id.to_string(),
|
||||
balance: 10,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Generate an initial commitment to be used with the privacy preserving transaction
|
||||
// created with the `build_privacy_transaction` function.
|
||||
let sender_nsk = [1; 32];
|
||||
let sender_npk = NullifierPublicKey::from(&sender_nsk);
|
||||
let account = Account {
|
||||
balance: 100,
|
||||
nonce: 0xdeadbeef,
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
data: Data::default(),
|
||||
};
|
||||
let initial_commitment = CommitmentsInitialData {
|
||||
npk: sender_npk,
|
||||
account,
|
||||
};
|
||||
|
||||
SequencerConfig {
|
||||
home: ".".into(),
|
||||
override_rust_log: None,
|
||||
genesis_id: 1,
|
||||
is_genesis_random: true,
|
||||
max_num_tx_in_block: 300,
|
||||
mempool_max_size: 10000,
|
||||
block_create_timeout_millis: 12000,
|
||||
port: 3040,
|
||||
initial_accounts: initial_public_accounts,
|
||||
initial_commitments: vec![initial_commitment],
|
||||
signing_key: [37; 32],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a single privacy transaction to use in stress tests. This involves generating a proof so
|
||||
/// it may take a while to run. In normal execution of the node this transaction will be accepted
|
||||
/// only once. Disabling the node's nullifier uniqueness check allows to submit this transaction
|
||||
/// multiple times with the purpose of testing the node's processing performance.
|
||||
#[allow(unused)]
|
||||
fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let sender_nsk = [1; 32];
|
||||
let sender_isk = [99; 32];
|
||||
let sender_ipk = IncomingViewingPublicKey::from_scalar(sender_isk);
|
||||
let sender_npk = NullifierPublicKey::from(&sender_nsk);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 100,
|
||||
nonce: 0xdeadbeef,
|
||||
program_owner: program.id(),
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::from(&sender_npk),
|
||||
);
|
||||
let recipient_nsk = [2; 32];
|
||||
let recipient_isk = [99; 32];
|
||||
let recipient_ipk = IncomingViewingPublicKey::from_scalar(recipient_isk);
|
||||
let recipient_npk = NullifierPublicKey::from(&recipient_nsk);
|
||||
let recipient_pre =
|
||||
AccountWithMetadata::new(Account::default(), false, AccountId::from(&recipient_npk));
|
||||
|
||||
let eph_holder_from = EphemeralKeyHolder::new(&sender_npk);
|
||||
let sender_ss = eph_holder_from.calculate_shared_secret_sender(&sender_ipk);
|
||||
let sender_epk = eph_holder_from.generate_ephemeral_public_key();
|
||||
|
||||
let eph_holder_to = EphemeralKeyHolder::new(&recipient_npk);
|
||||
let recipient_ss = eph_holder_to.calculate_shared_secret_sender(&recipient_ipk);
|
||||
let recipient_epk = eph_holder_from.generate_ephemeral_public_key();
|
||||
|
||||
let balance_to_move: u128 = 1;
|
||||
let proof: MembershipProof = (
|
||||
1,
|
||||
vec![[
|
||||
170, 10, 217, 228, 20, 35, 189, 177, 238, 235, 97, 129, 132, 89, 96, 247, 86, 91, 222,
|
||||
214, 38, 194, 216, 67, 56, 251, 208, 226, 0, 117, 149, 39,
|
||||
]],
|
||||
);
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre, recipient_pre],
|
||||
&Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
&[1, 2],
|
||||
&[0xdeadbeef1, 0xdeadbeef2],
|
||||
&[
|
||||
(sender_npk.clone(), sender_ss),
|
||||
(recipient_npk.clone(), recipient_ss),
|
||||
],
|
||||
&[sender_nsk],
|
||||
&[Some(proof)],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = pptx::message::Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(sender_npk, sender_ipk, sender_epk),
|
||||
(recipient_npk, recipient_ipk, recipient_epk),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]);
|
||||
pptx::PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
22
key_protocol/Cargo.toml
Normal file
22
key_protocol/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "key_protocol"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
common.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
k256.workspace = true
|
||||
sha2.workspace = true
|
||||
rand.workspace = true
|
||||
base58.workspace = true
|
||||
hex.workspace = true
|
||||
aes-gcm.workspace = true
|
||||
bip39.workspace = true
|
||||
hmac-sha512.workspace = true
|
||||
thiserror.workspace = true
|
||||
itertools.workspace = true
|
||||
52
key_protocol/src/key_management/ephemeral_key_holder.rs
Normal file
52
key_protocol/src/key_management/ephemeral_key_holder.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use nssa_core::{
|
||||
NullifierPublicKey, SharedSecretKey,
|
||||
encryption::{EphemeralPublicKey, EphemeralSecretKey, IncomingViewingPublicKey},
|
||||
};
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use sha2::Digest;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Ephemeral secret key holder. Non-clonable as intended for one-time use. Produces ephemeral
|
||||
/// public keys. Can produce shared secret for sender.
|
||||
pub struct EphemeralKeyHolder {
|
||||
ephemeral_secret_key: EphemeralSecretKey,
|
||||
}
|
||||
|
||||
pub fn produce_one_sided_shared_secret_receiver(
|
||||
ipk: &IncomingViewingPublicKey,
|
||||
) -> (SharedSecretKey, EphemeralPublicKey) {
|
||||
let mut esk = [0; 32];
|
||||
OsRng.fill_bytes(&mut esk);
|
||||
(
|
||||
SharedSecretKey::new(&esk, ipk),
|
||||
EphemeralPublicKey::from_scalar(esk),
|
||||
)
|
||||
}
|
||||
|
||||
impl EphemeralKeyHolder {
|
||||
pub fn new(receiver_nullifier_public_key: &NullifierPublicKey) -> Self {
|
||||
let mut nonce_bytes = [0; 16];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(receiver_nullifier_public_key);
|
||||
hasher.update(nonce_bytes);
|
||||
|
||||
Self {
|
||||
ephemeral_secret_key: hasher.finalize().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_ephemeral_public_key(&self) -> EphemeralPublicKey {
|
||||
EphemeralPublicKey::from_scalar(self.ephemeral_secret_key)
|
||||
}
|
||||
|
||||
pub fn calculate_shared_secret_sender(
|
||||
&self,
|
||||
receiver_incoming_viewing_public_key: &IncomingViewingPublicKey,
|
||||
) -> SharedSecretKey {
|
||||
SharedSecretKey::new(
|
||||
&self.ephemeral_secret_key,
|
||||
receiver_incoming_viewing_public_key,
|
||||
)
|
||||
}
|
||||
}
|
||||
298
key_protocol/src/key_management/key_tree/chain_index.rs
Normal file
298
key_protocol/src/key_management/key_tree/chain_index.rs
Normal file
@ -0,0 +1,298 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize, Hash)]
|
||||
pub struct ChainIndex(Vec<u32>);
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ChainIndexError {
|
||||
#[error("No root found")]
|
||||
NoRootFound,
|
||||
#[error("Failed to parse segment into a number")]
|
||||
ParseIntError(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl FromStr for ChainIndex {
|
||||
type Err = ChainIndexError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if !s.starts_with('/') {
|
||||
return Err(ChainIndexError::NoRootFound);
|
||||
}
|
||||
|
||||
if s == "/" {
|
||||
return Ok(ChainIndex(vec![]));
|
||||
}
|
||||
|
||||
let uprooted_substring = s.strip_prefix("/").unwrap();
|
||||
|
||||
let splitted_chain: Vec<&str> = uprooted_substring.split("/").collect();
|
||||
let mut res = vec![];
|
||||
|
||||
for split_ch in splitted_chain {
|
||||
let cci = split_ch.parse()?;
|
||||
res.push(cci);
|
||||
}
|
||||
|
||||
Ok(Self(res))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ChainIndex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "/")?;
|
||||
for cci in &self.0[..(self.0.len().saturating_sub(1))] {
|
||||
write!(f, "{cci}/")?;
|
||||
}
|
||||
if let Some(last) = self.0.last() {
|
||||
write!(f, "{}", last)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChainIndex {
|
||||
fn default() -> Self {
|
||||
ChainIndex::from_str("/").expect("Root parsing failure")
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainIndex {
|
||||
pub fn root() -> Self {
|
||||
ChainIndex::default()
|
||||
}
|
||||
|
||||
pub fn chain(&self) -> &[u32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn next_in_line(&self) -> ChainIndex {
|
||||
let mut chain = self.0.clone();
|
||||
// ToDo: Add overflow check
|
||||
if let Some(last_p) = chain.last_mut() {
|
||||
*last_p += 1
|
||||
}
|
||||
|
||||
ChainIndex(chain)
|
||||
}
|
||||
|
||||
pub fn previous_in_line(&self) -> Option<ChainIndex> {
|
||||
let mut chain = self.0.clone();
|
||||
if let Some(last_p) = chain.last_mut() {
|
||||
*last_p = last_p.checked_sub(1)?;
|
||||
}
|
||||
|
||||
Some(ChainIndex(chain))
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<ChainIndex> {
|
||||
if self.0.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ChainIndex(self.0[..(self.0.len() - 1)].to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nth_child(&self, child_id: u32) -> ChainIndex {
|
||||
let mut chain = self.0.clone();
|
||||
chain.push(child_id);
|
||||
|
||||
ChainIndex(chain)
|
||||
}
|
||||
|
||||
pub fn depth(&self) -> u32 {
|
||||
self.0.iter().map(|cci| cci + 1).sum()
|
||||
}
|
||||
|
||||
fn collapse_back(&self) -> Option<Self> {
|
||||
let mut res = self.parent()?;
|
||||
|
||||
let last_mut = res.0.last_mut()?;
|
||||
*last_mut += *(self.0.last()?) + 1;
|
||||
|
||||
Some(res)
|
||||
}
|
||||
|
||||
fn shuffle_iter(&self) -> impl Iterator<Item = ChainIndex> {
|
||||
self.0
|
||||
.iter()
|
||||
.permutations(self.0.len())
|
||||
.unique()
|
||||
.map(|item| ChainIndex(item.into_iter().cloned().collect()))
|
||||
}
|
||||
|
||||
pub fn chain_ids_at_depth(depth: usize) -> impl Iterator<Item = ChainIndex> {
|
||||
let mut stack = vec![ChainIndex(vec![0; depth])];
|
||||
let mut cumulative_stack = vec![ChainIndex(vec![0; depth])];
|
||||
|
||||
while let Some(id) = stack.pop() {
|
||||
if let Some(collapsed_id) = id.collapse_back() {
|
||||
for id in collapsed_id.shuffle_iter() {
|
||||
stack.push(id.clone());
|
||||
cumulative_stack.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cumulative_stack.into_iter().unique()
|
||||
}
|
||||
|
||||
pub fn chain_ids_at_depth_rev(depth: usize) -> impl Iterator<Item = ChainIndex> {
|
||||
let mut stack = vec![ChainIndex(vec![0; depth])];
|
||||
let mut cumulative_stack = vec![ChainIndex(vec![0; depth])];
|
||||
|
||||
while let Some(id) = stack.pop() {
|
||||
if let Some(collapsed_id) = id.collapse_back() {
|
||||
for id in collapsed_id.shuffle_iter() {
|
||||
stack.push(id.clone());
|
||||
cumulative_stack.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cumulative_stack.into_iter().rev().unique()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_root_correct() {
|
||||
let chain_id = ChainIndex::root();
|
||||
let chain_id_2 = ChainIndex::from_str("/").unwrap();
|
||||
|
||||
assert_eq!(chain_id, chain_id_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_deser_correct() {
|
||||
let chain_id = ChainIndex::from_str("/257").unwrap();
|
||||
|
||||
assert_eq!(chain_id.chain(), &[257]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_deser_failure_no_root() {
|
||||
let chain_index_error = ChainIndex::from_str("257").err().unwrap();
|
||||
|
||||
assert!(matches!(chain_index_error, ChainIndexError::NoRootFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_deser_failure_int_parsing_failure() {
|
||||
let chain_index_error = ChainIndex::from_str("/hello").err().unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
chain_index_error,
|
||||
ChainIndexError::ParseIntError(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_next_in_line_correct() {
|
||||
let chain_id = ChainIndex::from_str("/257").unwrap();
|
||||
let next_in_line = chain_id.next_in_line();
|
||||
|
||||
assert_eq!(next_in_line, ChainIndex::from_str("/258").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_child_correct() {
|
||||
let chain_id = ChainIndex::from_str("/257").unwrap();
|
||||
let child = chain_id.nth_child(3);
|
||||
|
||||
assert_eq!(child, ChainIndex::from_str("/257/3").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_correct_display() {
|
||||
let chainid = ChainIndex(vec![5, 7, 8]);
|
||||
|
||||
let string_index = format!("{chainid}");
|
||||
|
||||
assert_eq!(string_index, "/5/7/8".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prev_in_line() {
|
||||
let chain_id = ChainIndex(vec![1, 7, 3]);
|
||||
|
||||
let prev_chain_id = chain_id.previous_in_line().unwrap();
|
||||
|
||||
assert_eq!(prev_chain_id, ChainIndex(vec![1, 7, 2]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prev_in_line_no_prev() {
|
||||
let chain_id = ChainIndex(vec![1, 7, 0]);
|
||||
|
||||
let prev_chain_id = chain_id.previous_in_line();
|
||||
|
||||
assert_eq!(prev_chain_id, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parent() {
|
||||
let chain_id = ChainIndex(vec![1, 7, 3]);
|
||||
|
||||
let parent_chain_id = chain_id.parent().unwrap();
|
||||
|
||||
assert_eq!(parent_chain_id, ChainIndex(vec![1, 7]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parent_no_parent() {
|
||||
let chain_id = ChainIndex(vec![]);
|
||||
|
||||
let parent_chain_id = chain_id.parent();
|
||||
|
||||
assert_eq!(parent_chain_id, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parent_root() {
|
||||
let chain_id = ChainIndex(vec![1]);
|
||||
|
||||
let parent_chain_id = chain_id.parent().unwrap();
|
||||
|
||||
assert_eq!(parent_chain_id, ChainIndex::root())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collapse_back() {
|
||||
let chain_id = ChainIndex(vec![1, 1]);
|
||||
|
||||
let collapsed = chain_id.collapse_back().unwrap();
|
||||
|
||||
assert_eq!(collapsed, ChainIndex(vec![3]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collapse_back_one() {
|
||||
let chain_id = ChainIndex(vec![1]);
|
||||
|
||||
let collapsed = chain_id.collapse_back();
|
||||
|
||||
assert_eq!(collapsed, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collapse_back_root() {
|
||||
let chain_id = ChainIndex(vec![]);
|
||||
|
||||
let collapsed = chain_id.collapse_back();
|
||||
|
||||
assert_eq!(collapsed, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shuffle() {
|
||||
for id in ChainIndex::chain_ids_at_depth(5) {
|
||||
println!("{id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
263
key_protocol/src/key_management/key_tree/keys_private.rs
Normal file
263
key_protocol/src/key_management/key_tree/keys_private.rs
Normal file
@ -0,0 +1,263 @@
|
||||
use k256::{Scalar, elliptic_curve::PrimeField};
|
||||
use nssa_core::encryption::IncomingViewingPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::{
|
||||
KeyChain,
|
||||
key_tree::traits::KeyNode,
|
||||
secret_holders::{PrivateKeyHolder, SecretSpendingKey},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ChildKeysPrivate {
|
||||
pub value: (KeyChain, nssa::Account),
|
||||
pub ccc: [u8; 32],
|
||||
/// Can be [`None`] if root
|
||||
pub cci: Option<u32>,
|
||||
}
|
||||
|
||||
impl KeyNode for ChildKeysPrivate {
|
||||
fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_priv");
|
||||
|
||||
let ssk = SecretSpendingKey(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
);
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
|
||||
let nsk = ssk.generate_nullifier_secret_key();
|
||||
let isk = ssk.generate_incoming_viewing_secret_key();
|
||||
let ovk = ssk.generate_outgoing_viewing_secret_key();
|
||||
|
||||
let npk = (&nsk).into();
|
||||
let ipk = IncomingViewingPublicKey::from_scalar(isk);
|
||||
|
||||
Self {
|
||||
value: (
|
||||
KeyChain {
|
||||
secret_spending_key: ssk,
|
||||
nullifer_public_key: npk,
|
||||
incoming_viewing_public_key: ipk,
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: nsk,
|
||||
incoming_viewing_secret_key: isk,
|
||||
outgoing_viewing_secret_key: ovk,
|
||||
},
|
||||
},
|
||||
nssa::Account::default(),
|
||||
),
|
||||
ccc,
|
||||
cci: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn nth_child(&self, cci: u32) -> Self {
|
||||
let parent_pt = Scalar::from_repr(
|
||||
self.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.outgoing_viewing_secret_key
|
||||
.into(),
|
||||
)
|
||||
.expect("Key generated as scalar, must be valid representation")
|
||||
+ Scalar::from_repr(self.value.0.private_key_holder.nullifier_secret_key.into())
|
||||
.expect("Key generated as scalar, must be valid representation")
|
||||
* Scalar::from_repr(
|
||||
self.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.incoming_viewing_secret_key
|
||||
.into(),
|
||||
)
|
||||
.expect("Key generated as scalar, must be valid representation");
|
||||
let mut input = vec![];
|
||||
|
||||
input.extend_from_slice(b"NSSA_seed_priv");
|
||||
input.extend_from_slice(&parent_pt.to_bytes());
|
||||
input.extend_from_slice(&cci.to_le_bytes());
|
||||
|
||||
let hash_value = hmac_sha512::HMAC::mac(input, self.ccc);
|
||||
|
||||
let ssk = SecretSpendingKey(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
);
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
|
||||
let nsk = ssk.generate_nullifier_secret_key();
|
||||
let isk = ssk.generate_incoming_viewing_secret_key();
|
||||
let ovk = ssk.generate_outgoing_viewing_secret_key();
|
||||
|
||||
let npk = (&nsk).into();
|
||||
let ipk = IncomingViewingPublicKey::from_scalar(isk);
|
||||
|
||||
Self {
|
||||
value: (
|
||||
KeyChain {
|
||||
secret_spending_key: ssk,
|
||||
nullifer_public_key: npk,
|
||||
incoming_viewing_public_key: ipk,
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: nsk,
|
||||
incoming_viewing_secret_key: isk,
|
||||
outgoing_viewing_secret_key: ovk,
|
||||
},
|
||||
},
|
||||
nssa::Account::default(),
|
||||
),
|
||||
ccc,
|
||||
cci: Some(cci),
|
||||
}
|
||||
}
|
||||
|
||||
fn chain_code(&self) -> &[u8; 32] {
|
||||
&self.ccc
|
||||
}
|
||||
|
||||
fn child_index(&self) -> Option<u32> {
|
||||
self.cci
|
||||
}
|
||||
|
||||
fn account_id(&self) -> nssa::AccountId {
|
||||
nssa::AccountId::from(&self.value.0.nullifer_public_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ChildKeysPrivate> for &'a (KeyChain, nssa::Account) {
|
||||
fn from(value: &'a ChildKeysPrivate) -> Self {
|
||||
&value.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) {
|
||||
fn from(value: &'a mut ChildKeysPrivate) -> Self {
|
||||
&mut value.value
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_keys_deterministic_generation() {
|
||||
let root_keys = ChildKeysPrivate::root([42; 64]);
|
||||
let child_keys = root_keys.nth_child(5);
|
||||
|
||||
assert_eq!(root_keys.cci, None);
|
||||
assert_eq!(child_keys.cci, Some(5));
|
||||
|
||||
assert_eq!(
|
||||
root_keys.value.0.secret_spending_key.0,
|
||||
[
|
||||
249, 83, 253, 32, 174, 204, 185, 44, 253, 167, 61, 92, 128, 5, 152, 4, 220, 21, 88,
|
||||
84, 167, 180, 154, 249, 44, 77, 33, 136, 59, 131, 203, 152
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.value.0.secret_spending_key.0,
|
||||
[
|
||||
16, 242, 229, 242, 252, 158, 153, 210, 234, 120, 70, 85, 83, 196, 5, 53, 28, 26,
|
||||
187, 230, 22, 193, 146, 232, 237, 3, 166, 184, 122, 1, 233, 93
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.value.0.private_key_holder.nullifier_secret_key,
|
||||
[
|
||||
38, 195, 52, 182, 16, 66, 167, 156, 9, 14, 65, 100, 17, 93, 166, 71, 27, 148, 93,
|
||||
85, 116, 109, 130, 8, 195, 222, 159, 214, 141, 41, 124, 57
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.value.0.private_key_holder.nullifier_secret_key,
|
||||
[
|
||||
215, 46, 2, 151, 174, 60, 86, 154, 5, 3, 175, 245, 12, 176, 220, 58, 250, 118, 236,
|
||||
49, 254, 221, 229, 58, 40, 1, 170, 145, 175, 108, 23, 170
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys
|
||||
.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.incoming_viewing_secret_key,
|
||||
[
|
||||
153, 161, 15, 34, 96, 184, 165, 165, 27, 244, 155, 40, 70, 5, 241, 133, 78, 40, 61,
|
||||
118, 48, 148, 226, 5, 97, 18, 201, 128, 82, 248, 163, 72
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys
|
||||
.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.incoming_viewing_secret_key,
|
||||
[
|
||||
192, 155, 55, 43, 164, 115, 71, 145, 227, 225, 21, 57, 55, 12, 226, 44, 10, 103,
|
||||
39, 73, 230, 173, 60, 69, 69, 122, 110, 241, 164, 3, 192, 57
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys
|
||||
.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.outgoing_viewing_secret_key,
|
||||
[
|
||||
205, 87, 71, 129, 90, 242, 217, 200, 140, 252, 124, 46, 207, 7, 33, 156, 83, 166,
|
||||
150, 81, 98, 131, 182, 156, 110, 92, 78, 140, 125, 218, 152, 154
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys
|
||||
.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.outgoing_viewing_secret_key,
|
||||
[
|
||||
131, 202, 219, 172, 219, 29, 48, 120, 226, 209, 209, 10, 216, 173, 48, 167, 233,
|
||||
17, 35, 155, 30, 217, 176, 120, 72, 146, 250, 226, 165, 178, 255, 90
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.value.0.nullifer_public_key.0,
|
||||
[
|
||||
65, 176, 149, 243, 192, 45, 216, 177, 169, 56, 229, 7, 28, 66, 204, 87, 109, 83,
|
||||
152, 64, 14, 188, 179, 210, 147, 60, 22, 251, 203, 70, 89, 215
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.value.0.nullifer_public_key.0,
|
||||
[
|
||||
69, 104, 130, 115, 48, 134, 19, 188, 67, 148, 163, 54, 155, 237, 57, 27, 136, 228,
|
||||
111, 233, 205, 158, 149, 31, 84, 11, 241, 176, 243, 12, 138, 249
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.value.0.incoming_viewing_public_key.0,
|
||||
&[
|
||||
3, 174, 56, 136, 244, 179, 18, 122, 38, 220, 36, 50, 200, 41, 104, 167, 70, 18, 60,
|
||||
202, 93, 193, 29, 16, 125, 252, 96, 51, 199, 152, 47, 233, 178
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.value.0.incoming_viewing_public_key.0,
|
||||
&[
|
||||
3, 18, 202, 246, 79, 141, 169, 51, 55, 202, 120, 169, 244, 201, 156, 162, 216, 115,
|
||||
126, 53, 46, 94, 235, 125, 114, 178, 215, 81, 171, 93, 93, 88, 117
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
132
key_protocol/src/key_management/key_tree/keys_public.rs
Normal file
132
key_protocol/src/key_management/key_tree/keys_public.rs
Normal file
@ -0,0 +1,132 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::key_tree::traits::KeyNode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ChildKeysPublic {
|
||||
pub csk: nssa::PrivateKey,
|
||||
pub cpk: nssa::PublicKey,
|
||||
pub ccc: [u8; 32],
|
||||
/// Can be [`None`] if root
|
||||
pub cci: Option<u32>,
|
||||
}
|
||||
|
||||
impl KeyNode for ChildKeysPublic {
|
||||
fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_pub");
|
||||
|
||||
let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap();
|
||||
let ccc = *hash_value.last_chunk::<32>().unwrap();
|
||||
let cpk = nssa::PublicKey::new_from_private_key(&csk);
|
||||
|
||||
Self {
|
||||
csk,
|
||||
cpk,
|
||||
ccc,
|
||||
cci: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn nth_child(&self, cci: u32) -> Self {
|
||||
let mut hash_input = vec![];
|
||||
hash_input.extend_from_slice(self.csk.value());
|
||||
hash_input.extend_from_slice(&cci.to_le_bytes());
|
||||
|
||||
let hash_value = hmac_sha512::HMAC::mac(&hash_input, self.ccc);
|
||||
|
||||
let csk = nssa::PrivateKey::try_new(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
)
|
||||
.unwrap();
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
let cpk = nssa::PublicKey::new_from_private_key(&csk);
|
||||
|
||||
Self {
|
||||
csk,
|
||||
cpk,
|
||||
ccc,
|
||||
cci: Some(cci),
|
||||
}
|
||||
}
|
||||
|
||||
fn chain_code(&self) -> &[u8; 32] {
|
||||
&self.ccc
|
||||
}
|
||||
|
||||
fn child_index(&self) -> Option<u32> {
|
||||
self.cci
|
||||
}
|
||||
|
||||
fn account_id(&self) -> nssa::AccountId {
|
||||
nssa::AccountId::from(&self.cpk)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ChildKeysPublic> for &'a nssa::PrivateKey {
|
||||
fn from(value: &'a ChildKeysPublic) -> Self {
|
||||
&value.csk
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_keys_deterministic_generation() {
|
||||
let root_keys = ChildKeysPublic::root([42; 64]);
|
||||
let child_keys = root_keys.nth_child(5);
|
||||
|
||||
assert_eq!(root_keys.cci, None);
|
||||
assert_eq!(child_keys.cci, Some(5));
|
||||
|
||||
assert_eq!(
|
||||
root_keys.ccc,
|
||||
[
|
||||
61, 30, 91, 26, 133, 91, 236, 192, 231, 53, 186, 139, 11, 221, 202, 11, 178, 215,
|
||||
254, 103, 191, 60, 117, 112, 1, 226, 31, 156, 83, 104, 150, 224
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.ccc,
|
||||
[
|
||||
67, 26, 102, 68, 189, 155, 102, 80, 199, 188, 112, 142, 207, 157, 36, 210, 48, 224,
|
||||
35, 6, 112, 180, 11, 190, 135, 218, 9, 14, 84, 231, 58, 98
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.csk.value(),
|
||||
&[
|
||||
241, 82, 246, 237, 62, 130, 116, 47, 189, 112, 99, 67, 178, 40, 115, 245, 141, 193,
|
||||
77, 164, 243, 76, 222, 64, 50, 146, 23, 145, 91, 164, 92, 116
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.csk.value(),
|
||||
&[
|
||||
11, 151, 27, 212, 167, 26, 77, 234, 103, 145, 53, 191, 184, 25, 240, 191, 156, 25,
|
||||
60, 144, 65, 22, 193, 163, 246, 227, 212, 81, 49, 170, 33, 158
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.cpk.value(),
|
||||
&[
|
||||
220, 170, 95, 177, 121, 37, 86, 166, 56, 238, 232, 72, 21, 106, 107, 217, 158, 74,
|
||||
133, 91, 143, 244, 155, 15, 2, 230, 223, 169, 13, 20, 163, 138
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.cpk.value(),
|
||||
&[
|
||||
152, 249, 236, 111, 132, 96, 184, 122, 21, 179, 240, 15, 234, 155, 164, 144, 108,
|
||||
110, 120, 74, 176, 147, 196, 168, 243, 186, 203, 79, 97, 17, 194, 52
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
606
key_protocol/src/key_management/key_tree/mod.rs
Normal file
606
key_protocol/src/key_management/key_tree/mod.rs
Normal file
@ -0,0 +1,606 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use common::sequencer_client::SequencerClient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::{
|
||||
key_tree::{
|
||||
chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
|
||||
traits::KeyNode,
|
||||
},
|
||||
secret_holders::SeedHolder,
|
||||
};
|
||||
|
||||
pub mod chain_index;
|
||||
pub mod keys_private;
|
||||
pub mod keys_public;
|
||||
pub mod traits;
|
||||
|
||||
pub const DEPTH_SOFT_CAP: u32 = 20;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct KeyTree<N: KeyNode> {
|
||||
pub key_map: BTreeMap<ChainIndex, N>,
|
||||
pub account_id_map: HashMap<nssa::AccountId, ChainIndex>,
|
||||
}
|
||||
|
||||
pub type KeyTreePublic = KeyTree<ChildKeysPublic>;
|
||||
pub type KeyTreePrivate = KeyTree<ChildKeysPrivate>;
|
||||
|
||||
impl<N: KeyNode> KeyTree<N> {
|
||||
pub fn new(seed: &SeedHolder) -> Self {
|
||||
let seed_fit: [u8; 64] = seed
|
||||
.seed
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("SeedHolder seed is 64 bytes long");
|
||||
|
||||
let root_keys = N::root(seed_fit);
|
||||
let account_id = root_keys.account_id();
|
||||
|
||||
let key_map = BTreeMap::from_iter([(ChainIndex::root(), root_keys)]);
|
||||
let account_id_map = HashMap::from_iter([(account_id, ChainIndex::root())]);
|
||||
|
||||
Self {
|
||||
key_map,
|
||||
account_id_map,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_root(root: N) -> Self {
|
||||
let account_id_map = HashMap::from_iter([(root.account_id(), ChainIndex::root())]);
|
||||
let key_map = BTreeMap::from_iter([(ChainIndex::root(), root)]);
|
||||
|
||||
Self {
|
||||
key_map,
|
||||
account_id_map,
|
||||
}
|
||||
}
|
||||
|
||||
// ToDo: Add function to create a tree from list of nodes with consistency check.
|
||||
|
||||
pub fn find_next_last_child_of_id(&self, parent_id: &ChainIndex) -> Option<u32> {
|
||||
if !self.key_map.contains_key(parent_id) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let leftmost_child = parent_id.nth_child(u32::MIN);
|
||||
|
||||
if !self.key_map.contains_key(&leftmost_child) {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let mut right = u32::MAX - 1;
|
||||
let mut left_border = u32::MIN;
|
||||
let mut right_border = u32::MAX;
|
||||
|
||||
loop {
|
||||
let rightmost_child = parent_id.nth_child(right);
|
||||
|
||||
let rightmost_ref = self.key_map.get(&rightmost_child);
|
||||
let rightmost_ref_next = self.key_map.get(&rightmost_child.next_in_line());
|
||||
|
||||
match (&rightmost_ref, &rightmost_ref_next) {
|
||||
(Some(_), Some(_)) => {
|
||||
left_border = right;
|
||||
right = (right + right_border) / 2;
|
||||
}
|
||||
(Some(_), None) => {
|
||||
break Some(right + 1);
|
||||
}
|
||||
(None, None) => {
|
||||
right_border = right;
|
||||
right = (left_border + right) / 2;
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_new_node(
|
||||
&mut self,
|
||||
parent_cci: &ChainIndex,
|
||||
) -> Option<(nssa::AccountId, ChainIndex)> {
|
||||
let parent_keys = self.key_map.get(parent_cci)?;
|
||||
let next_child_id = self
|
||||
.find_next_last_child_of_id(parent_cci)
|
||||
.expect("Can be None only if parent is not present");
|
||||
let next_cci = parent_cci.nth_child(next_child_id);
|
||||
|
||||
let child_keys = parent_keys.nth_child(next_child_id);
|
||||
let account_id = child_keys.account_id();
|
||||
|
||||
self.key_map.insert(next_cci.clone(), child_keys);
|
||||
self.account_id_map.insert(account_id, next_cci.clone());
|
||||
|
||||
Some((account_id, next_cci))
|
||||
}
|
||||
|
||||
fn find_next_slot_layered(&self) -> ChainIndex {
|
||||
let mut depth = 1;
|
||||
|
||||
'outer: loop {
|
||||
for chain_id in ChainIndex::chain_ids_at_depth_rev(depth) {
|
||||
if !self.key_map.contains_key(&chain_id) {
|
||||
break 'outer chain_id;
|
||||
}
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option<(nssa::AccountId, ChainIndex)> {
|
||||
let parent_keys = self.key_map.get(&chain_index.parent()?)?;
|
||||
let child_id = *chain_index.chain().last()?;
|
||||
|
||||
let child_keys = parent_keys.nth_child(child_id);
|
||||
let account_id = child_keys.account_id();
|
||||
|
||||
self.key_map.insert(chain_index.clone(), child_keys);
|
||||
self.account_id_map.insert(account_id, chain_index.clone());
|
||||
|
||||
Some((account_id, chain_index.clone()))
|
||||
}
|
||||
|
||||
pub fn generate_new_node_layered(&mut self) -> Option<(nssa::AccountId, ChainIndex)> {
|
||||
self.fill_node(&self.find_next_slot_layered())
|
||||
}
|
||||
|
||||
pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> {
|
||||
self.account_id_map
|
||||
.get(&account_id)
|
||||
.and_then(|chain_id| self.key_map.get(chain_id))
|
||||
}
|
||||
|
||||
pub fn get_node_mut(&mut self, account_id: nssa::AccountId) -> Option<&mut N> {
|
||||
self.account_id_map
|
||||
.get(&account_id)
|
||||
.and_then(|chain_id| self.key_map.get_mut(chain_id))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, account_id: nssa::AccountId, chain_index: ChainIndex, node: N) {
|
||||
self.account_id_map.insert(account_id, chain_index.clone());
|
||||
self.key_map.insert(chain_index, node);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, addr: nssa::AccountId) -> Option<N> {
|
||||
let chain_index = self.account_id_map.remove(&addr).unwrap();
|
||||
self.key_map.remove(&chain_index)
|
||||
}
|
||||
|
||||
/// Populates tree with children.
|
||||
///
|
||||
/// For given `depth` adds children to a tree such that their `ChainIndex::depth(&self) <
|
||||
/// depth`.
|
||||
///
|
||||
/// Tree must be empty before start
|
||||
pub fn generate_tree_for_depth(&mut self, depth: u32) {
|
||||
let mut id_stack = vec![ChainIndex::root()];
|
||||
|
||||
while let Some(curr_id) = id_stack.pop() {
|
||||
let mut next_id = curr_id.nth_child(0);
|
||||
|
||||
while (next_id.depth()) < depth {
|
||||
self.generate_new_node(&curr_id);
|
||||
id_stack.push(next_id.clone());
|
||||
next_id = next_id.next_in_line();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyTree<ChildKeysPrivate> {
|
||||
/// Cleanup of all non-initialized accounts in a private tree
|
||||
///
|
||||
/// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) <
|
||||
/// depth`.
|
||||
///
|
||||
/// If account is default, removes them.
|
||||
///
|
||||
/// Chain must be parsed for accounts beforehand
|
||||
///
|
||||
/// Fast, leaves gaps between accounts
|
||||
pub fn cleanup_tree_remove_uninit_for_depth(&mut self, depth: u32) {
|
||||
let mut id_stack = vec![ChainIndex::root()];
|
||||
|
||||
while let Some(curr_id) = id_stack.pop() {
|
||||
if let Some(node) = self.key_map.get(&curr_id)
|
||||
&& node.value.1 == nssa::Account::default()
|
||||
&& curr_id != ChainIndex::root()
|
||||
{
|
||||
let addr = node.account_id();
|
||||
self.remove(addr);
|
||||
}
|
||||
|
||||
let mut next_id = curr_id.nth_child(0);
|
||||
|
||||
while (next_id.depth()) < depth {
|
||||
id_stack.push(next_id.clone());
|
||||
next_id = next_id.next_in_line();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleanup of non-initialized accounts in a private tree
|
||||
///
|
||||
/// If account is default, removes them, stops at first non-default account.
|
||||
///
|
||||
/// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()`
|
||||
///
|
||||
/// Chain must be parsed for accounts beforehand
|
||||
///
|
||||
/// Slow, maintains tree consistency.
|
||||
pub fn cleanup_tree_remove_uninit_layered(&mut self, depth: u32) {
|
||||
'outer: for i in (1..(depth as usize)).rev() {
|
||||
println!("Cleanup of tree at depth {i}");
|
||||
for id in ChainIndex::chain_ids_at_depth(i) {
|
||||
if let Some(node) = self.key_map.get(&id) {
|
||||
if node.value.1 == nssa::Account::default() {
|
||||
let addr = node.account_id();
|
||||
self.remove(addr);
|
||||
} else {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyTree<ChildKeysPublic> {
|
||||
/// Cleanup of all non-initialized accounts in a public tree
|
||||
///
|
||||
/// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) <
|
||||
/// depth`.
|
||||
///
|
||||
/// If account is default, removes them.
|
||||
///
|
||||
/// Fast, leaves gaps between accounts
|
||||
pub async fn cleanup_tree_remove_ininit_for_depth(
|
||||
&mut self,
|
||||
depth: u32,
|
||||
client: Arc<SequencerClient>,
|
||||
) -> Result<()> {
|
||||
let mut id_stack = vec![ChainIndex::root()];
|
||||
|
||||
while let Some(curr_id) = id_stack.pop() {
|
||||
if let Some(node) = self.key_map.get(&curr_id) {
|
||||
let address = node.account_id();
|
||||
let node_acc = client.get_account(address.to_string()).await?.account;
|
||||
|
||||
if node_acc == nssa::Account::default() && curr_id != ChainIndex::root() {
|
||||
self.remove(address);
|
||||
}
|
||||
}
|
||||
|
||||
let mut next_id = curr_id.nth_child(0);
|
||||
|
||||
while (next_id.depth()) < depth {
|
||||
id_stack.push(next_id.clone());
|
||||
next_id = next_id.next_in_line();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cleanup of non-initialized accounts in a public tree
|
||||
///
|
||||
/// If account is default, removes them, stops at first non-default account.
|
||||
///
|
||||
/// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()`
|
||||
///
|
||||
/// Slow, maintains tree consistency.
|
||||
pub async fn cleanup_tree_remove_uninit_layered(
|
||||
&mut self,
|
||||
depth: u32,
|
||||
client: Arc<SequencerClient>,
|
||||
) -> Result<()> {
|
||||
'outer: for i in (1..(depth as usize)).rev() {
|
||||
println!("Cleanup of tree at depth {i}");
|
||||
for id in ChainIndex::chain_ids_at_depth(i) {
|
||||
if let Some(node) = self.key_map.get(&id) {
|
||||
let address = node.account_id();
|
||||
let node_acc = client.get_account(address.to_string()).await?.account;
|
||||
|
||||
if node_acc == nssa::Account::default() {
|
||||
let addr = node.account_id();
|
||||
self.remove(addr);
|
||||
} else {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use nssa::AccountId;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn seed_holder_for_tests() -> SeedHolder {
|
||||
SeedHolder {
|
||||
seed: [42; 64].to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_key_tree() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let tree = KeyTreePublic::new(&seed_holder);
|
||||
|
||||
assert!(tree.key_map.contains_key(&ChainIndex::root()));
|
||||
assert!(tree.account_id_map.contains_key(&AccountId::new([
|
||||
46, 223, 229, 177, 59, 18, 189, 219, 153, 31, 249, 90, 112, 230, 180, 164, 80, 25, 106,
|
||||
159, 14, 238, 1, 192, 91, 8, 210, 165, 199, 41, 60, 104,
|
||||
])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_small_key_tree() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let mut tree = KeyTreePrivate::new(&seed_holder);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 0);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_tree_can_not_make_child_keys() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let mut tree = KeyTreePrivate::new(&seed_holder);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 0);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
|
||||
let key_opt = tree.generate_new_node(&ChainIndex::from_str("/3").unwrap());
|
||||
|
||||
assert_eq!(key_opt, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_tree_complex_structure() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let mut tree = KeyTreePublic::new(&seed_holder);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 0);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::root()).unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/1").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 2);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0/0").unwrap())
|
||||
);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 2);
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0/1").unwrap())
|
||||
);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 3);
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0/2").unwrap())
|
||||
);
|
||||
|
||||
tree.generate_new_node(&ChainIndex::from_str("/0/1").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0/1/0").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::from_str("/0/1").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tree_balancing_automatic() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let mut tree = KeyTreePublic::new(&seed_holder);
|
||||
|
||||
for _ in 0..100 {
|
||||
tree.generate_new_node_layered().unwrap();
|
||||
}
|
||||
|
||||
let next_slot = tree.find_next_slot_layered();
|
||||
|
||||
assert_eq!(next_slot, ChainIndex::from_str("/0/0/2/1").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let mut tree = KeyTreePrivate::new(&seed_holder);
|
||||
tree.generate_tree_for_depth(10);
|
||||
|
||||
let acc = tree
|
||||
.key_map
|
||||
.get_mut(&ChainIndex::from_str("/1").unwrap())
|
||||
.unwrap();
|
||||
acc.value.1.balance = 2;
|
||||
|
||||
let acc = tree
|
||||
.key_map
|
||||
.get_mut(&ChainIndex::from_str("/2").unwrap())
|
||||
.unwrap();
|
||||
acc.value.1.balance = 3;
|
||||
|
||||
let acc = tree
|
||||
.key_map
|
||||
.get_mut(&ChainIndex::from_str("/0/1").unwrap())
|
||||
.unwrap();
|
||||
acc.value.1.balance = 5;
|
||||
|
||||
let acc = tree
|
||||
.key_map
|
||||
.get_mut(&ChainIndex::from_str("/1/0").unwrap())
|
||||
.unwrap();
|
||||
acc.value.1.balance = 6;
|
||||
|
||||
tree.cleanup_tree_remove_uninit_layered(10);
|
||||
|
||||
let mut key_set_res = HashSet::new();
|
||||
key_set_res.insert("/0".to_string());
|
||||
key_set_res.insert("/1".to_string());
|
||||
key_set_res.insert("/2".to_string());
|
||||
key_set_res.insert("/".to_string());
|
||||
key_set_res.insert("/0/0".to_string());
|
||||
key_set_res.insert("/0/1".to_string());
|
||||
key_set_res.insert("/1/0".to_string());
|
||||
|
||||
let mut key_set = HashSet::new();
|
||||
|
||||
for key in tree.key_map.keys() {
|
||||
key_set.insert(key.to_string());
|
||||
}
|
||||
|
||||
assert_eq!(key_set, key_set_res);
|
||||
|
||||
let acc = tree
|
||||
.key_map
|
||||
.get(&ChainIndex::from_str("/1").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(acc.value.1.balance, 2);
|
||||
|
||||
let acc = tree
|
||||
.key_map
|
||||
.get(&ChainIndex::from_str("/2").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(acc.value.1.balance, 3);
|
||||
|
||||
let acc = tree
|
||||
.key_map
|
||||
.get(&ChainIndex::from_str("/0/1").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(acc.value.1.balance, 5);
|
||||
|
||||
let acc = tree
|
||||
.key_map
|
||||
.get(&ChainIndex::from_str("/1/0").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(acc.value.1.balance, 6);
|
||||
}
|
||||
}
|
||||
14
key_protocol/src/key_management/key_tree/traits.rs
Normal file
14
key_protocol/src/key_management/key_tree/traits.rs
Normal file
@ -0,0 +1,14 @@
|
||||
/// Trait, that reperesents a Node in hierarchical key tree
|
||||
pub trait KeyNode {
|
||||
/// Tree root node
|
||||
fn root(seed: [u8; 64]) -> Self;
|
||||
|
||||
/// `cci`'s child of node
|
||||
fn nth_child(&self, cci: u32) -> Self;
|
||||
|
||||
fn chain_code(&self) -> &[u8; 32];
|
||||
|
||||
fn child_index(&self) -> Option<u32>;
|
||||
|
||||
fn account_id(&self) -> nssa::AccountId;
|
||||
}
|
||||
155
key_protocol/src/key_management/mod.rs
Normal file
155
key_protocol/src/key_management/mod.rs
Normal file
@ -0,0 +1,155 @@
|
||||
use nssa_core::{
|
||||
NullifierPublicKey, SharedSecretKey,
|
||||
encryption::{EphemeralPublicKey, IncomingViewingPublicKey},
|
||||
};
|
||||
use secret_holders::{PrivateKeyHolder, SecretSpendingKey, SeedHolder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type PublicAccountSigningKey = [u8; 32];
|
||||
|
||||
pub mod ephemeral_key_holder;
|
||||
pub mod key_tree;
|
||||
pub mod secret_holders;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
/// Entrypoint to key management
|
||||
pub struct KeyChain {
|
||||
pub secret_spending_key: SecretSpendingKey,
|
||||
pub private_key_holder: PrivateKeyHolder,
|
||||
pub nullifer_public_key: NullifierPublicKey,
|
||||
pub incoming_viewing_public_key: IncomingViewingPublicKey,
|
||||
}
|
||||
|
||||
impl KeyChain {
|
||||
pub fn new_os_random() -> Self {
|
||||
// Currently dropping SeedHolder at the end of initialization.
|
||||
// Now entirely sure if we need it in the future.
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let private_key_holder = secret_spending_key.produce_private_key_holder();
|
||||
|
||||
let nullifer_public_key = private_key_holder.generate_nullifier_public_key();
|
||||
let incoming_viewing_public_key = private_key_holder.generate_incoming_viewing_public_key();
|
||||
|
||||
Self {
|
||||
secret_spending_key,
|
||||
private_key_holder,
|
||||
nullifer_public_key,
|
||||
incoming_viewing_public_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
||||
// Currently dropping SeedHolder at the end of initialization.
|
||||
// Not entirely sure if we need it in the future.
|
||||
let seed_holder = SeedHolder::new_mnemonic(passphrase);
|
||||
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let private_key_holder = secret_spending_key.produce_private_key_holder();
|
||||
|
||||
let nullifer_public_key = private_key_holder.generate_nullifier_public_key();
|
||||
let incoming_viewing_public_key = private_key_holder.generate_incoming_viewing_public_key();
|
||||
|
||||
Self {
|
||||
secret_spending_key,
|
||||
private_key_holder,
|
||||
nullifer_public_key,
|
||||
incoming_viewing_public_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_shared_secret_receiver(
|
||||
&self,
|
||||
ephemeral_public_key_sender: EphemeralPublicKey,
|
||||
) -> SharedSecretKey {
|
||||
SharedSecretKey::new(
|
||||
&self
|
||||
.secret_spending_key
|
||||
.generate_incoming_viewing_secret_key(),
|
||||
&ephemeral_public_key_sender,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use aes_gcm::aead::OsRng;
|
||||
use base58::ToBase58;
|
||||
use k256::{AffinePoint, elliptic_curve::group::GroupEncoding};
|
||||
use rand::RngCore;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_os_random() {
|
||||
// Ensure that a new KeyChain instance can be created without errors.
|
||||
let account_id_key_holder = KeyChain::new_os_random();
|
||||
|
||||
// Check that key holder fields are initialized with expected types
|
||||
assert_ne!(
|
||||
account_id_key_holder.nullifer_public_key.as_ref(),
|
||||
&[0u8; 32]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_shared_secret_receiver() {
|
||||
let account_id_key_holder = KeyChain::new_os_random();
|
||||
|
||||
// Generate a random ephemeral public key sender
|
||||
let mut scalar = [0; 32];
|
||||
OsRng.fill_bytes(&mut scalar);
|
||||
let ephemeral_public_key_sender = EphemeralPublicKey::from_scalar(scalar);
|
||||
|
||||
// Calculate shared secret
|
||||
let _shared_secret =
|
||||
account_id_key_holder.calculate_shared_secret_receiver(ephemeral_public_key_sender);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_generation_test() {
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
let top_secret_key_holder = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let utxo_secret_key_holder = top_secret_key_holder.produce_private_key_holder();
|
||||
|
||||
let nullifer_public_key = utxo_secret_key_holder.generate_nullifier_public_key();
|
||||
let viewing_public_key = utxo_secret_key_holder.generate_incoming_viewing_public_key();
|
||||
|
||||
let pub_account_signing_key = nssa::PrivateKey::new_os_random();
|
||||
|
||||
let public_key = nssa::PublicKey::new_from_private_key(&pub_account_signing_key);
|
||||
|
||||
let account = nssa::AccountId::from(&public_key);
|
||||
|
||||
println!("======Prerequisites======");
|
||||
println!();
|
||||
|
||||
println!(
|
||||
"Group generator {:?}",
|
||||
hex::encode(AffinePoint::GENERATOR.to_bytes())
|
||||
);
|
||||
println!();
|
||||
|
||||
println!("======Holders======");
|
||||
println!();
|
||||
|
||||
println!("{seed_holder:?}");
|
||||
println!("{top_secret_key_holder:?}");
|
||||
println!("{utxo_secret_key_holder:?}");
|
||||
println!();
|
||||
|
||||
println!("======Public data======");
|
||||
println!();
|
||||
println!("Account {:?}", account.value().to_base58());
|
||||
println!(
|
||||
"Nulifier public key {:?}",
|
||||
hex::encode(nullifer_public_key.to_byte_array())
|
||||
);
|
||||
println!(
|
||||
"Viewing public key {:?}",
|
||||
hex::encode(viewing_public_key.to_bytes())
|
||||
);
|
||||
}
|
||||
}
|
||||
181
key_protocol/src/key_management/secret_holders.rs
Normal file
181
key_protocol/src/key_management/secret_holders.rs
Normal file
@ -0,0 +1,181 @@
|
||||
use bip39::Mnemonic;
|
||||
use common::HashType;
|
||||
use nssa_core::{
|
||||
NullifierPublicKey, NullifierSecretKey,
|
||||
encryption::{IncomingViewingPublicKey, Scalar},
|
||||
};
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, digest::FixedOutput};
|
||||
|
||||
const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32];
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Seed holder. Non-clonable to ensure that different holders use different seeds.
|
||||
/// Produces `TopSecretKeyHolder` objects.
|
||||
pub struct SeedHolder {
|
||||
// ToDo: Needs to be vec as serde derives is not implemented for [u8; 64]
|
||||
pub(crate) seed: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
/// Secret spending key object. Can produce `PrivateKeyHolder` objects.
|
||||
pub struct SecretSpendingKey(pub(crate) [u8; 32]);
|
||||
|
||||
pub type IncomingViewingSecretKey = Scalar;
|
||||
pub type OutgoingViewingSecretKey = Scalar;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
/// Private key holder. Produces public keys. Can produce account_id. Can produce shared secret for
|
||||
/// recepient.
|
||||
pub struct PrivateKeyHolder {
|
||||
pub nullifier_secret_key: NullifierSecretKey,
|
||||
pub(crate) incoming_viewing_secret_key: IncomingViewingSecretKey,
|
||||
pub outgoing_viewing_secret_key: OutgoingViewingSecretKey,
|
||||
}
|
||||
|
||||
impl SeedHolder {
|
||||
pub fn new_os_random() -> Self {
|
||||
let mut enthopy_bytes: [u8; 32] = [0; 32];
|
||||
OsRng.fill_bytes(&mut enthopy_bytes);
|
||||
|
||||
let mnemonic = Mnemonic::from_entropy(&enthopy_bytes)
|
||||
.expect("Enthropy must be a multiple of 32 bytes");
|
||||
let seed_wide = mnemonic.to_seed("mnemonic");
|
||||
|
||||
Self {
|
||||
seed: seed_wide.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
||||
let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES)
|
||||
.expect("Enthropy must be a multiple of 32 bytes");
|
||||
let seed_wide = mnemonic.to_seed(passphrase);
|
||||
|
||||
Self {
|
||||
seed: seed_wide.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_secret_spending_key_hash(&self) -> HashType {
|
||||
let mut hash = hmac_sha512::HMAC::mac(&self.seed, "NSSA_seed");
|
||||
|
||||
for _ in 1..2048 {
|
||||
hash = hmac_sha512::HMAC::mac(hash, "NSSA_seed");
|
||||
}
|
||||
|
||||
// Safe unwrap
|
||||
*hash.first_chunk::<32>().unwrap()
|
||||
}
|
||||
|
||||
pub fn produce_top_secret_key_holder(&self) -> SecretSpendingKey {
|
||||
SecretSpendingKey(self.generate_secret_spending_key_hash())
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretSpendingKey {
|
||||
pub fn generate_nullifier_secret_key(&self) -> NullifierSecretKey {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
|
||||
hasher.update("NSSA_keys");
|
||||
hasher.update(self.0);
|
||||
hasher.update([1u8]);
|
||||
hasher.update([0u8; 22]);
|
||||
|
||||
<NullifierSecretKey>::from(hasher.finalize_fixed())
|
||||
}
|
||||
|
||||
pub fn generate_incoming_viewing_secret_key(&self) -> IncomingViewingSecretKey {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
|
||||
hasher.update("NSSA_keys");
|
||||
hasher.update(self.0);
|
||||
hasher.update([2u8]);
|
||||
hasher.update([0u8; 22]);
|
||||
|
||||
<HashType>::from(hasher.finalize_fixed())
|
||||
}
|
||||
|
||||
pub fn generate_outgoing_viewing_secret_key(&self) -> OutgoingViewingSecretKey {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
|
||||
hasher.update("NSSA_keys");
|
||||
hasher.update(self.0);
|
||||
hasher.update([3u8]);
|
||||
hasher.update([0u8; 22]);
|
||||
|
||||
<HashType>::from(hasher.finalize_fixed())
|
||||
}
|
||||
|
||||
pub fn produce_private_key_holder(&self) -> PrivateKeyHolder {
|
||||
PrivateKeyHolder {
|
||||
nullifier_secret_key: self.generate_nullifier_secret_key(),
|
||||
incoming_viewing_secret_key: self.generate_incoming_viewing_secret_key(),
|
||||
outgoing_viewing_secret_key: self.generate_outgoing_viewing_secret_key(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKeyHolder {
|
||||
pub fn generate_nullifier_public_key(&self) -> NullifierPublicKey {
|
||||
(&self.nullifier_secret_key).into()
|
||||
}
|
||||
|
||||
pub fn generate_incoming_viewing_public_key(&self) -> IncomingViewingPublicKey {
|
||||
IncomingViewingPublicKey::from_scalar(self.incoming_viewing_secret_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn seed_generation_test() {
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
|
||||
assert_eq!(seed_holder.seed.len(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssk_generation_test() {
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
|
||||
assert_eq!(seed_holder.seed.len(), 64);
|
||||
|
||||
let _ = seed_holder.generate_secret_spending_key_hash();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ivs_generation_test() {
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
|
||||
assert_eq!(seed_holder.seed.len(), 64);
|
||||
|
||||
let top_secret_key_holder = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let _ = top_secret_key_holder.generate_incoming_viewing_secret_key();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ovs_generation_test() {
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
|
||||
assert_eq!(seed_holder.seed.len(), 64);
|
||||
|
||||
let top_secret_key_holder = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let _ = top_secret_key_holder.generate_outgoing_viewing_secret_key();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_seeds_generated_same_from_same_mnemonic() {
|
||||
let mnemonic = "test_pass";
|
||||
|
||||
let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_string());
|
||||
let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_string());
|
||||
|
||||
assert_eq!(seed_holder1.seed, seed_holder2.seed);
|
||||
}
|
||||
}
|
||||
205
key_protocol/src/key_protocol_core/mod.rs
Normal file
205
key_protocol/src/key_protocol_core/mod.rs
Normal file
@ -0,0 +1,205 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use k256::AffinePoint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::{
|
||||
KeyChain,
|
||||
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
|
||||
secret_holders::SeedHolder,
|
||||
};
|
||||
|
||||
pub type PublicKey = AffinePoint;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct NSSAUserData {
|
||||
/// Default public accounts
|
||||
pub default_pub_account_signing_keys: HashMap<nssa::AccountId, nssa::PrivateKey>,
|
||||
/// Default private accounts
|
||||
pub default_user_private_accounts:
|
||||
HashMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
|
||||
/// Tree of public keys
|
||||
pub public_key_tree: KeyTreePublic,
|
||||
/// Tree of private keys
|
||||
pub private_key_tree: KeyTreePrivate,
|
||||
}
|
||||
|
||||
impl NSSAUserData {
|
||||
fn valid_public_key_transaction_pairing_check(
|
||||
accounts_keys_map: &HashMap<nssa::AccountId, nssa::PrivateKey>,
|
||||
) -> bool {
|
||||
let mut check_res = true;
|
||||
for (account_id, key) in accounts_keys_map {
|
||||
let expected_account_id =
|
||||
nssa::AccountId::from(&nssa::PublicKey::new_from_private_key(key));
|
||||
if &expected_account_id != account_id {
|
||||
println!("{}, {}", expected_account_id, account_id);
|
||||
check_res = false;
|
||||
}
|
||||
}
|
||||
check_res
|
||||
}
|
||||
|
||||
fn valid_private_key_transaction_pairing_check(
|
||||
accounts_keys_map: &HashMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
|
||||
) -> bool {
|
||||
let mut check_res = true;
|
||||
for (account_id, (key, _)) in accounts_keys_map {
|
||||
let expected_account_id = nssa::AccountId::from(&key.nullifer_public_key);
|
||||
if expected_account_id != *account_id {
|
||||
println!("{}, {}", expected_account_id, account_id);
|
||||
check_res = false;
|
||||
}
|
||||
}
|
||||
check_res
|
||||
}
|
||||
|
||||
pub fn new_with_accounts(
|
||||
default_accounts_keys: HashMap<nssa::AccountId, nssa::PrivateKey>,
|
||||
default_accounts_key_chains: HashMap<
|
||||
nssa::AccountId,
|
||||
(KeyChain, nssa_core::account::Account),
|
||||
>,
|
||||
public_key_tree: KeyTreePublic,
|
||||
private_key_tree: KeyTreePrivate,
|
||||
) -> Result<Self> {
|
||||
if !Self::valid_public_key_transaction_pairing_check(&default_accounts_keys) {
|
||||
anyhow::bail!(
|
||||
"Key transaction pairing check not satisfied, there is account_ids, which is not derived from keys"
|
||||
);
|
||||
}
|
||||
|
||||
if !Self::valid_private_key_transaction_pairing_check(&default_accounts_key_chains) {
|
||||
anyhow::bail!(
|
||||
"Key transaction pairing check not satisfied, there is account_ids, which is not derived from keys"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
default_pub_account_signing_keys: default_accounts_keys,
|
||||
default_user_private_accounts: default_accounts_key_chains,
|
||||
public_key_tree,
|
||||
private_key_tree,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generated new private key for public transaction signatures
|
||||
///
|
||||
/// Returns the account_id of new account
|
||||
pub fn generate_new_public_transaction_private_key(
|
||||
&mut self,
|
||||
parent_cci: Option<ChainIndex>,
|
||||
) -> (nssa::AccountId, ChainIndex) {
|
||||
match parent_cci {
|
||||
Some(parent_cci) => self
|
||||
.public_key_tree
|
||||
.generate_new_node(&parent_cci)
|
||||
.expect("Parent must be present in a tree"),
|
||||
None => self
|
||||
.public_key_tree
|
||||
.generate_new_node_layered()
|
||||
.expect("Search for new node slot failed"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the signing key for public transaction signatures
|
||||
pub fn get_pub_account_signing_key(
|
||||
&self,
|
||||
account_id: &nssa::AccountId,
|
||||
) -> Option<&nssa::PrivateKey> {
|
||||
// First seek in defaults
|
||||
if let Some(key) = self.default_pub_account_signing_keys.get(account_id) {
|
||||
Some(key)
|
||||
// Then seek in tree
|
||||
} else {
|
||||
self.public_key_tree.get_node(*account_id).map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generated new private key for privacy preserving transactions
|
||||
///
|
||||
/// Returns the account_id of new account
|
||||
pub fn generate_new_privacy_preserving_transaction_key_chain(
|
||||
&mut self,
|
||||
parent_cci: Option<ChainIndex>,
|
||||
) -> (nssa::AccountId, ChainIndex) {
|
||||
match parent_cci {
|
||||
Some(parent_cci) => self
|
||||
.private_key_tree
|
||||
.generate_new_node(&parent_cci)
|
||||
.expect("Parent must be present in a tree"),
|
||||
None => self
|
||||
.private_key_tree
|
||||
.generate_new_node_layered()
|
||||
.expect("Search for new node slot failed"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the signing key for public transaction signatures
|
||||
pub fn get_private_account(
|
||||
&self,
|
||||
account_id: &nssa::AccountId,
|
||||
) -> Option<&(KeyChain, nssa_core::account::Account)> {
|
||||
// First seek in defaults
|
||||
if let Some(key) = self.default_user_private_accounts.get(account_id) {
|
||||
Some(key)
|
||||
// Then seek in tree
|
||||
} else {
|
||||
self.private_key_tree.get_node(*account_id).map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the signing key for public transaction signatures
|
||||
pub fn get_private_account_mut(
|
||||
&mut self,
|
||||
account_id: &nssa::AccountId,
|
||||
) -> Option<&mut (KeyChain, nssa_core::account::Account)> {
|
||||
// First seek in defaults
|
||||
if let Some(key) = self.default_user_private_accounts.get_mut(account_id) {
|
||||
Some(key)
|
||||
// Then seek in tree
|
||||
} else {
|
||||
self.private_key_tree
|
||||
.get_node_mut(*account_id)
|
||||
.map(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NSSAUserData {
|
||||
fn default() -> Self {
|
||||
Self::new_with_accounts(
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_string())),
|
||||
KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_string())),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_account() {
|
||||
let mut user_data = NSSAUserData::default();
|
||||
|
||||
let (account_id_private, _) = user_data
|
||||
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
|
||||
|
||||
let is_key_chain_generated = user_data.get_private_account(&account_id_private).is_some();
|
||||
|
||||
assert!(is_key_chain_generated);
|
||||
|
||||
let account_id_private_str = account_id_private.to_string();
|
||||
println!("{account_id_private_str:#?}");
|
||||
let key_chain = &user_data
|
||||
.get_private_account(&account_id_private)
|
||||
.unwrap()
|
||||
.0;
|
||||
println!("{key_chain:#?}");
|
||||
}
|
||||
}
|
||||
2
key_protocol/src/lib.rs
Normal file
2
key_protocol/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod key_management;
|
||||
pub mod key_protocol_core;
|
||||
@ -1,11 +1,10 @@
|
||||
[package]
|
||||
name = "mempool"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde_json.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
@ -1,243 +1,99 @@
|
||||
use std::collections::VecDeque;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
use mempoolitem::MemPoolItem;
|
||||
|
||||
pub mod mempoolitem;
|
||||
|
||||
pub struct MemPool<Item: MemPoolItem> {
|
||||
items: VecDeque<Item>,
|
||||
pub struct MemPool<T> {
|
||||
receiver: Receiver<T>,
|
||||
}
|
||||
|
||||
impl<Item: MemPoolItem> MemPool<Item> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items: VecDeque::new(),
|
||||
}
|
||||
impl<T> MemPool<T> {
|
||||
pub fn new(max_size: usize) -> (Self, MemPoolHandle<T>) {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(max_size);
|
||||
|
||||
let mem_pool = Self { receiver };
|
||||
let sender = MemPoolHandle::new(sender);
|
||||
(mem_pool, sender)
|
||||
}
|
||||
|
||||
pub fn pop_last(&mut self) -> Option<Item> {
|
||||
self.items.pop_front()
|
||||
}
|
||||
pub fn pop(&mut self) -> Option<T> {
|
||||
use tokio::sync::mpsc::error::TryRecvError;
|
||||
|
||||
pub fn peek_last(&self) -> Option<&Item> {
|
||||
self.items.front()
|
||||
}
|
||||
|
||||
pub fn push_item(&mut self, item: Item) {
|
||||
self.items.push_back(item);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn pop_size(&mut self, size: usize) -> Vec<Item> {
|
||||
let mut ret_vec = vec![];
|
||||
|
||||
for _ in 0..size {
|
||||
let item = self.pop_last();
|
||||
|
||||
match item {
|
||||
Some(item) => ret_vec.push(item),
|
||||
None => break,
|
||||
match self.receiver.try_recv() {
|
||||
Ok(item) => Some(item),
|
||||
Err(TryRecvError::Empty) => None,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
panic!("Mempool senders disconnected, cannot receive items, this is a bug")
|
||||
}
|
||||
}
|
||||
|
||||
ret_vec
|
||||
}
|
||||
|
||||
pub fn drain_size(&mut self, remainder: usize) -> Vec<Item> {
|
||||
self.pop_size(self.len().saturating_sub(remainder))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item: MemPoolItem> Default for MemPool<Item> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
pub struct MemPoolHandle<T> {
|
||||
sender: Sender<T>,
|
||||
}
|
||||
|
||||
impl<T> MemPoolHandle<T> {
|
||||
fn new(sender: Sender<T>) -> Self {
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
/// Send an item to the mempool blocking if max size is reached
|
||||
pub async fn push(&self, item: T) -> Result<(), tokio::sync::mpsc::error::SendError<T>> {
|
||||
self.sender.send(item).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::vec;
|
||||
use tokio::test;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub type ItemId = u64;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct TestItem {
|
||||
id: ItemId,
|
||||
}
|
||||
|
||||
impl MemPoolItem for TestItem {
|
||||
type Identifier = ItemId;
|
||||
|
||||
fn identifier(&self) -> Self::Identifier {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
fn test_item_with_id(id: u64) -> TestItem {
|
||||
TestItem { id }
|
||||
#[test]
|
||||
async fn test_mempool_new() {
|
||||
let (mut pool, _handle): (MemPool<u64>, _) = MemPool::new(10);
|
||||
assert_eq!(pool.pop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_empty_mempool() {
|
||||
let _: MemPool<TestItem> = MemPool::new();
|
||||
async fn test_push_and_pop() {
|
||||
let (mut pool, handle) = MemPool::new(10);
|
||||
|
||||
handle.push(1).await.unwrap();
|
||||
|
||||
let item = pool.pop();
|
||||
assert_eq!(item, Some(1));
|
||||
assert_eq!(pool.pop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mempool_new() {
|
||||
let pool: MemPool<TestItem> = MemPool::new();
|
||||
assert!(pool.is_empty());
|
||||
assert_eq!(pool.len(), 0);
|
||||
async fn test_multiple_push_pop() {
|
||||
let (mut pool, handle) = MemPool::new(10);
|
||||
|
||||
handle.push(1).await.unwrap();
|
||||
handle.push(2).await.unwrap();
|
||||
handle.push(3).await.unwrap();
|
||||
|
||||
assert_eq!(pool.pop(), Some(1));
|
||||
assert_eq!(pool.pop(), Some(2));
|
||||
assert_eq!(pool.pop(), Some(3));
|
||||
assert_eq!(pool.pop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_item() {
|
||||
let mut pool = MemPool::new();
|
||||
pool.push_item(test_item_with_id(1));
|
||||
assert!(!pool.is_empty());
|
||||
assert_eq!(pool.len(), 1);
|
||||
async fn test_pop_empty() {
|
||||
let (mut pool, _handle): (MemPool<u64>, _) = MemPool::new(10);
|
||||
assert_eq!(pool.pop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pop_last() {
|
||||
let mut pool = MemPool::new();
|
||||
pool.push_item(test_item_with_id(1));
|
||||
pool.push_item(test_item_with_id(2));
|
||||
let item = pool.pop_last();
|
||||
assert_eq!(item, Some(test_item_with_id(1)));
|
||||
assert_eq!(pool.len(), 1);
|
||||
}
|
||||
async fn test_max_size() {
|
||||
let (mut pool, handle) = MemPool::new(2);
|
||||
|
||||
#[test]
|
||||
fn test_peek_last() {
|
||||
let mut pool = MemPool::new();
|
||||
pool.push_item(test_item_with_id(1));
|
||||
pool.push_item(test_item_with_id(2));
|
||||
let item = pool.peek_last();
|
||||
assert_eq!(item, Some(&test_item_with_id(1)));
|
||||
}
|
||||
handle.push(1).await.unwrap();
|
||||
handle.push(2).await.unwrap();
|
||||
|
||||
#[test]
|
||||
fn test_pop_size() {
|
||||
let mut pool = MemPool::new();
|
||||
pool.push_item(test_item_with_id(1));
|
||||
pool.push_item(test_item_with_id(2));
|
||||
pool.push_item(test_item_with_id(3));
|
||||
|
||||
let items = pool.pop_size(2);
|
||||
assert_eq!(items, vec![test_item_with_id(1), test_item_with_id(2)]);
|
||||
assert_eq!(pool.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drain_size() {
|
||||
let mut pool = MemPool::new();
|
||||
pool.push_item(test_item_with_id(1));
|
||||
pool.push_item(test_item_with_id(2));
|
||||
pool.push_item(test_item_with_id(3));
|
||||
pool.push_item(test_item_with_id(4));
|
||||
|
||||
let items = pool.drain_size(2);
|
||||
assert_eq!(items, vec![test_item_with_id(1), test_item_with_id(2)]);
|
||||
assert_eq!(pool.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
let pool: MemPool<TestItem> = MemPool::default();
|
||||
assert!(pool.is_empty());
|
||||
assert_eq!(pool.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_empty() {
|
||||
let mut pool = MemPool::new();
|
||||
assert!(pool.is_empty());
|
||||
pool.push_item(test_item_with_id(1));
|
||||
assert!(!pool.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_pop() {
|
||||
let mut mempool: MemPool<TestItem> = MemPool::new();
|
||||
|
||||
let items = vec![
|
||||
test_item_with_id(1),
|
||||
test_item_with_id(2),
|
||||
test_item_with_id(3),
|
||||
];
|
||||
|
||||
for item in items {
|
||||
mempool.push_item(item);
|
||||
}
|
||||
assert_eq!(mempool.len(), 3);
|
||||
|
||||
let item = mempool.pop_last();
|
||||
|
||||
assert_eq!(item, Some(TestItem { id: 1 }));
|
||||
assert_eq!(mempool.len(), 2);
|
||||
|
||||
let item = mempool.pop_last();
|
||||
|
||||
assert_eq!(item, Some(TestItem { id: 2 }));
|
||||
assert_eq!(mempool.len(), 1);
|
||||
|
||||
let item = mempool.pop_last();
|
||||
|
||||
assert_eq!(item, Some(TestItem { id: 3 }));
|
||||
assert_eq!(mempool.len(), 0);
|
||||
|
||||
let item = mempool.pop_last();
|
||||
|
||||
assert_eq!(item, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pop_many() {
|
||||
let mut mempool: MemPool<TestItem> = MemPool::new();
|
||||
|
||||
let mut items = vec![];
|
||||
|
||||
for i in 1..11 {
|
||||
items.push(test_item_with_id(i));
|
||||
}
|
||||
|
||||
for item in items {
|
||||
mempool.push_item(item);
|
||||
}
|
||||
|
||||
assert_eq!(mempool.len(), 10);
|
||||
|
||||
let items1 = mempool.pop_size(4);
|
||||
assert_eq!(
|
||||
items1,
|
||||
vec![
|
||||
test_item_with_id(1),
|
||||
test_item_with_id(2),
|
||||
test_item_with_id(3),
|
||||
test_item_with_id(4)
|
||||
]
|
||||
);
|
||||
assert_eq!(mempool.len(), 6);
|
||||
|
||||
let items2 = mempool.drain_size(2);
|
||||
assert_eq!(
|
||||
items2,
|
||||
vec![
|
||||
test_item_with_id(5),
|
||||
test_item_with_id(6),
|
||||
test_item_with_id(7),
|
||||
test_item_with_id(8)
|
||||
]
|
||||
);
|
||||
assert_eq!(mempool.len(), 2);
|
||||
// This should block if buffer is full, but we'll use try_send in a real scenario
|
||||
// For now, just verify we can pop items
|
||||
assert_eq!(pool.pop(), Some(1));
|
||||
assert_eq!(pool.pop(), Some(2));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
pub trait MemPoolItem {
|
||||
type Identifier;
|
||||
fn identifier(&self) -> Self::Identifier;
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "networking"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde_json.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
@ -1,5 +0,0 @@
|
||||
pub mod network_protocol;
|
||||
pub mod peer;
|
||||
pub mod peer_manager;
|
||||
pub mod rate_limiter;
|
||||
pub mod tcp;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user