Compare commits
880 Commits
revert-213
...
v1.8.14
Author | SHA1 | Date | |
---|---|---|---|
|
9c01d90c70 | ||
|
edf1954817 | ||
|
59eee75d65 | ||
|
c2dd9a006d | ||
|
8eb0a1091a | ||
|
cb5106a15b | ||
|
2d5957a4b4 | ||
|
99846eea12 | ||
|
78b82dedb1 | ||
|
db942269f0 | ||
|
eede487797 | ||
|
42c3fbc198 | ||
|
4706aa4900 | ||
|
b357eda15b | ||
|
fc8f61368d | ||
|
aabcdcf8fb | ||
|
81e65eae0b | ||
|
38e72982bc | ||
|
d52dd97ad1 | ||
|
61f88e04d2 | ||
|
3e131a5324 | ||
|
5655ea0061 | ||
|
40ef11ec86 | ||
|
debac00724 | ||
|
304afd42c6 | ||
|
5d3f3bc9b1 | ||
|
42531a11a5 | ||
|
10c40c9c2d | ||
|
1ef48f369e | ||
|
05e75ae937 | ||
|
a7aa5202ab | ||
|
9aa7821277 | ||
|
fee86726f2 | ||
|
8a470d3ae8 | ||
|
8c6df1f234 | ||
|
7cbfab0958 | ||
|
6130466a21 | ||
|
299a59f458 | ||
|
b30c726d22 | ||
|
069bb5e32f | ||
|
cad0e7f04f | ||
|
c90fa6643e | ||
|
54db774203 | ||
|
57b5ce5731 | ||
|
e0d933f940 | ||
|
e2559f20df | ||
|
7b9ca3e9d9 | ||
|
f0de9b43be | ||
|
6d7b64b140 | ||
|
f506851ca1 | ||
|
b70113e201 | ||
|
6093c7a218 | ||
|
b9777e10ee | ||
|
d840d56565 | ||
|
afb8df0c62 | ||
|
84d535b89e | ||
|
de1a9f84ac | ||
|
22bbd91843 | ||
|
1babd07faf | ||
|
0592b5568f | ||
|
ce98feac4e | ||
|
a53dd611c0 | ||
|
ec970f9d69 | ||
|
7793a11b65 | ||
|
4deac1daa4 | ||
|
4c36a93665 | ||
|
052309227f | ||
|
3b895104f3 | ||
|
24b0fc8927 | ||
|
271ae3c2fc | ||
|
a305fa0472 | ||
|
262b157d21 | ||
|
bfb02029bf | ||
|
039244417e | ||
|
1e1f383970 | ||
|
891b0a5152 | ||
|
685e40cbf2 | ||
|
17d698d20a | ||
|
cf34ae7d6f | ||
|
acd03fc29b | ||
|
ddef156305 | ||
|
33cd3a161e | ||
|
31a0906410 | ||
|
a0669af872 | ||
|
0f6f0545d1 | ||
|
89524d7b61 | ||
|
4a66832fb0 | ||
|
df40ede6ea | ||
|
93a8fd6a2b | ||
|
985fae2dcf | ||
|
90730899f1 | ||
|
b7cf6e7d9a | ||
|
fb59f2ad59 | ||
|
0e9fd84228 | ||
|
5e9d20378d | ||
|
01e932c6a4 | ||
|
e3e9d32f33 | ||
|
a523d09902 | ||
|
3ed9a47082 | ||
|
9985f5fa5c | ||
|
0958760592 | ||
|
9f53f3455a | ||
|
9fff4aa8b8 | ||
|
704d05f52d | ||
|
50d62bdd13 | ||
|
33c28da055 | ||
|
dd8e7f650a | ||
|
7621fa3c25 | ||
|
58e46e107c | ||
|
3c5ac9ab27 | ||
|
cc931ff47f | ||
|
37727fbbc3 | ||
|
c39d9eab04 | ||
|
a70e4c05d4 | ||
|
961509abbd | ||
|
20681ea2ce | ||
|
32bcfb757b | ||
|
eefb9875bf | ||
|
957914b3b5 | ||
|
f4113d24b4 | ||
|
2a43a89eca | ||
|
4a93be9f77 | ||
|
f03b6d701d | ||
|
83676aed53 | ||
|
a931b3f868 | ||
|
feef0ea478 | ||
|
c3c1b820fc | ||
|
548c0afac2 | ||
|
6bc52d4d47 | ||
|
423a4d6546 | ||
|
16f6bdf0b8 | ||
|
63629351f8 | ||
|
2d93db9dec | ||
|
558a52d5cb | ||
|
b00087e1ca | ||
|
e2850c84d2 | ||
|
f9b7e24846 | ||
|
843f26c2de | ||
|
a09e8397fb | ||
|
ef626e144d | ||
|
4cae58bfe9 | ||
|
f97d28caf4 | ||
|
16abcac802 | ||
|
a5e6a188c1 | ||
|
9f44d60a39 | ||
|
9284c6fffb | ||
|
2c49ab1ab4 | ||
|
9832ac54a4 | ||
|
fbf93f89a9 | ||
|
7210a883eb | ||
|
03db11eb3a | ||
|
3d526883e3 | ||
|
36838427d4 | ||
|
dfa1bc1bbd | ||
|
312f2fc6f6 | ||
|
24ace5cfaa | ||
|
23be4d89ef | ||
|
fa83e05d2c | ||
|
589eeb2432 | ||
|
a7eb10a787 | ||
|
336c39446b | ||
|
afa87c6a0b | ||
|
49e9adbba4 | ||
|
fbc519523f | ||
|
480895cc25 | ||
|
451ea7c9b2 | ||
|
bcacaf78e9 | ||
|
c60314c4a1 | ||
|
bdaac86f84 | ||
|
dc633e3385 | ||
|
8af4716710 | ||
|
afd17ec5d9 | ||
|
ef760fba32 | ||
|
fdc05ecd5a | ||
|
eae3166bdc | ||
|
d7377d4794 | ||
|
d1e0941289 | ||
|
77a3adb5a6 | ||
|
4f28ed1044 | ||
|
c2891c392c | ||
|
05a43e071c | ||
|
6aaff6183d | ||
|
de8dc27ecf | ||
|
cc75f576e2 | ||
|
4a186c5834 | ||
|
7e2e7dffa6 | ||
|
c6141925a9 | ||
|
e68cd335d5 | ||
|
3369826bd1 | ||
|
c901227d0f | ||
|
7bc9da3a97 | ||
|
64c5e7d9a9 | ||
|
156caeb710 | ||
|
ae11cc3297 | ||
|
a0b73d5658 | ||
|
fe5363ec6a | ||
|
402d72bc48 | ||
|
c0794d6fbb | ||
|
6deb0a9f5d | ||
|
be3209712d | ||
|
8028f218a4 | ||
|
3b5bafe510 | ||
|
d3e92f4250 | ||
|
400418fe2f | ||
|
b73d23d50a | ||
|
13d40d6a66 | ||
|
09c68ce696 | ||
|
801dc58b78 | ||
|
51cab5d16d | ||
|
39e27b130f | ||
|
e9d8a7a641 | ||
|
73acae9b5d | ||
|
87c71647e8 | ||
|
2b2536ac42 | ||
|
43e7368f3a | ||
|
87471ed7fb | ||
|
7ac43b16d2 | ||
|
69027e3f7e | ||
|
32f507dc51 | ||
|
387d5af52e | ||
|
eeb2bef63e | ||
|
83e01442a7 | ||
|
922c74caea | ||
|
1ab8c01ab7 | ||
|
9959ede9ce | ||
|
6d1e1287bc | ||
|
b8306a99d8 | ||
|
6777ca244f | ||
|
f04e06e0c2 | ||
|
e52b3fd1d8 | ||
|
06a3e9b178 | ||
|
9d66458a40 | ||
|
765fbc9a8c | ||
|
231a3bda8e | ||
|
8d6b54837c | ||
|
0cefd46d9d | ||
|
585695445f | ||
|
b6d040b0d7 | ||
|
4d3352e0e0 | ||
|
139d15cd84 | ||
|
5a7b487e3d | ||
|
416fccfc01 | ||
|
fe923bc56c | ||
|
b3904d80e6 | ||
|
e368de5f9c | ||
|
008139f506 | ||
|
e5aa5efbac | ||
|
a54fa45d5a | ||
|
713b61677e | ||
|
b1bf420524 | ||
|
6293b9d218 | ||
|
90f791a6a9 | ||
|
484856e4d4 | ||
|
26f32a3288 | ||
|
6e656deb59 | ||
|
b030d4be7c | ||
|
002693ab7d | ||
|
73d469991f | ||
|
d4192e3ac4 | ||
|
04cc50126b | ||
|
27b2561650 | ||
|
85302d6d17 | ||
|
42a67d30fc | ||
|
f3ea9bc995 | ||
|
b63617a3e1 | ||
|
d1ca16e9f8 | ||
|
0565fe3320 | ||
|
336ee01aae | ||
|
76c5c94a8a | ||
|
c3b1906f1d | ||
|
318d26f0ff | ||
|
c53174dc56 | ||
|
baa4c6eaf2 | ||
|
064cce41f7 | ||
|
d291bcf26e | ||
|
40c86a0605 | ||
|
80c3591391 | ||
|
741f9ea57f | ||
|
0e6b476cbf | ||
|
8bfba571f2 | ||
|
1a0eabe340 | ||
|
a05c08e711 | ||
|
6a6dd86262 | ||
|
5d6b52f9af | ||
|
8072635967 | ||
|
a86fdebb3b | ||
|
31b3ada6d9 | ||
|
2992a7154a | ||
|
21cd423e67 | ||
|
09ef4d12f7 | ||
|
de94c4e867 | ||
|
74684a107c | ||
|
19b3ba0442 | ||
|
784c745efa | ||
|
a7b3436b1e | ||
|
89b2a3d0ae | ||
|
56fc58a2b5 | ||
|
53e0c9710e | ||
|
c14864a608 | ||
|
780302af56 | ||
|
90557564c3 | ||
|
6d2fd078be | ||
|
4a874e9ba1 | ||
|
22510678b4 | ||
|
69b973a3a6 | ||
|
cc7ed71cb7 | ||
|
b6fe051d24 | ||
|
506d39ea82 | ||
|
a595e06b48 | ||
|
da08f3dc2b | ||
|
8dd3c1ece1 | ||
|
42a2c29234 | ||
|
a1f1264962 | ||
|
66caead016 | ||
|
de1f60fb2d | ||
|
7528016e2d | ||
|
adc57899fe | ||
|
afe229a89e | ||
|
5dd00e9230 | ||
|
0a698fc48f | ||
|
1666fc5483 | ||
|
467abd1f5b | ||
|
19432f2e5f | ||
|
7e7f8ef5f0 | ||
|
9e81798d6d | ||
|
8986bd301c | ||
|
6baad8e239 | ||
|
782d143489 | ||
|
b15e87631c | ||
|
d18f553e2d | ||
|
e84c57b659 | ||
|
66630804de | ||
|
72158e3bf9 | ||
|
df6063a622 | ||
|
55a1f03eee | ||
|
d20cccc26b | ||
|
6c4a8b2d72 | ||
|
307cda52ac | ||
|
026385effd | ||
|
0363d8d373 | ||
|
5c3f15e9c5 | ||
|
47e80be023 | ||
|
460dcad578 | ||
|
257d19ca48 | ||
|
de2aa898a7 | ||
|
23b6ce7980 | ||
|
8cba6cca76 | ||
|
85048c667c | ||
|
440ccd189e | ||
|
d5fc81e12a | ||
|
53f4bde471 | ||
|
232731e869 | ||
|
63835ec214 | ||
|
6de9ef62e8 | ||
|
0759b666ce | ||
|
c7e3d4cf79 | ||
|
63e37b2b20 | ||
|
436ec212f4 | ||
|
564cc95b00 | ||
|
28eb6ff796 | ||
|
de32ab4d57 | ||
|
cabe2d5d04 | ||
|
ece4ecb792 | ||
|
ba366f49ad | ||
|
8e666f47e0 | ||
|
0619705ce5 | ||
|
188089389f | ||
|
0a6bb84aec | ||
|
c8f6a0817b | ||
|
5350250a06 | ||
|
f8fccc7e91 | ||
|
eaa6d1a4b5 | ||
|
b66c9539c2 | ||
|
bdea60cc19 | ||
|
63ac5e4561 | ||
|
88e6f41bec | ||
|
e0280a68ba | ||
|
aa8d04d44b | ||
|
778f37b12d | ||
|
ebe77a0985 | ||
|
400a88786a | ||
|
29eae21057 | ||
|
0d1dbb6160 | ||
|
927d3b5e0d | ||
|
df929bda38 | ||
|
200c5c9fd6 | ||
|
9acf708344 | ||
|
af4c1785b6 | ||
|
b8f68860a4 | ||
|
547f33d1d1 | ||
|
67738a229c | ||
|
50803c3f58 | ||
|
50cb612ae1 | ||
|
e5dc8d731b | ||
|
f9dcb8228f | ||
|
68e8a05848 | ||
|
bfc5f9fb6c | ||
|
c3cc7d52fe | ||
|
4268cf1d8b | ||
|
c693ecc4c8 | ||
|
afe866ad02 | ||
|
6a73bf767b | ||
|
7d0494fcaa | ||
|
33d8e242c5 | ||
|
ef55045724 | ||
|
81d2c3261c | ||
|
348ba57b12 | ||
|
4a8ff62ad3 | ||
|
db85d659b9 | ||
|
a4df784e82 | ||
|
414674eba1 | ||
|
d922971ec6 | ||
|
95ac00d30a | ||
|
1ca4f7d110 | ||
|
8999f07ed2 | ||
|
9f4f8fc9e9 | ||
|
00b03897e1 | ||
|
6181df68cf | ||
|
1588b00f2c | ||
|
ef306aa7cb | ||
|
e718f4b04a | ||
|
51593a882b | ||
|
1c15cc6e9a | ||
|
734b380cdb | ||
|
9cc26b3b00 | ||
|
ef5a0e842c | ||
|
5bdb824267 | ||
|
474f2bcdf4 | ||
|
2302211963 | ||
|
8178db52a5 | ||
|
5d8429d953 | ||
|
fec15f69f4 | ||
|
257ddbeee1 | ||
|
47c1730808 | ||
|
a005a6b816 | ||
|
2f2948f998 | ||
|
55ccff7604 | ||
|
1bf88556ee | ||
|
4c4f183515 | ||
|
282322cbe8 | ||
|
2dc00d0e13 | ||
|
a90c338982 | ||
|
36c283026f | ||
|
a1a0c63862 | ||
|
e20fdde0a4 | ||
|
5b52ac8990 | ||
|
502ae8b319 | ||
|
30f0b3cf53 | ||
|
2975dc5c1a | ||
|
d68377e927 | ||
|
cc1a3d6645 | ||
|
e9a993fb59 | ||
|
88177d33fd | ||
|
0ec301f1c3 | ||
|
34665571fa | ||
|
5dd1c2191e | ||
|
aacb5e58ad | ||
|
8d2dce6f6b | ||
|
e50a26a493 | ||
|
302887da67 | ||
|
597c504c27 | ||
|
b7af118091 | ||
|
9e392687eb | ||
|
bee923ca6c | ||
|
879df38059 | ||
|
3e776267b5 | ||
|
0bfb466184 | ||
|
71bb3bf6aa | ||
|
99c74c8902 | ||
|
085f5f945d | ||
|
e757e51ddc | ||
|
458ccecb5d | ||
|
7f21a55a32 | ||
|
b112e4a8aa | ||
|
63b24d9577 | ||
|
49a6acffca | ||
|
7b4638aa0b | ||
|
f51ee23837 | ||
|
55f27d5c26 | ||
|
f33c651114 | ||
|
a71ebcc9f3 | ||
|
cd575945b6 | ||
|
6b24dd1c6a | ||
|
03da3eaa81 | ||
|
07b71329a7 | ||
|
53b387f113 | ||
|
d0143dad8f | ||
|
3e870a40f2 | ||
|
707302d9f2 | ||
|
b8ab6a46a8 | ||
|
9ad801a52c | ||
|
c85816c44e | ||
|
70d556782b | ||
|
ca83167cfc | ||
|
54ad080bf2 | ||
|
80e239550b | ||
|
992b313941 | ||
|
97bd521725 | ||
|
3ae2917603 | ||
|
e51c2d1a84 | ||
|
a24b0dc81c | ||
|
6e856cd468 | ||
|
0dde54b95b | ||
|
1fa863e4b2 | ||
|
73e97aab5b | ||
|
8f082a239a | ||
|
7e5026bde2 | ||
|
7ac0ea0885 | ||
|
dc06d3dee5 | ||
|
0b1aadf446 | ||
|
b9a0156a93 | ||
|
f786d1d0f3 | ||
|
b360c90d21 | ||
|
da801b753b | ||
|
8f168a610e | ||
|
d7e6ab58c4 | ||
|
a0e6a7c73b | ||
|
ed4e7c0c87 | ||
|
584f8deae3 | ||
|
e9c3f11d24 | ||
|
57e87a09c0 | ||
|
a2b435916d | ||
|
ad66189463 | ||
|
9a280426da | ||
|
864006a78a | ||
|
e37e46cfc2 | ||
|
376b21c6e7 | ||
|
fcda5d4a7d | ||
|
2e4a2c15be | ||
|
a8ec380c56 | ||
|
afb87a386a | ||
|
2d060fd2d9 | ||
|
c180f4c84e | ||
|
105a89175c | ||
|
959334d2e3 | ||
|
aa2098d115 | ||
|
37ee47c3e6 | ||
|
4fb43bbd90 | ||
|
5fbcb10e6f | ||
|
6f86abf551 | ||
|
744a69f818 | ||
|
1c6cac2054 | ||
|
0c64bd0938 | ||
|
f73a61d2ec | ||
|
bdb77b0c98 | ||
|
58bef3a94b | ||
|
52dfb4a09c | ||
|
c734db59cb | ||
|
3b36e8e285 | ||
|
cdbc77bf97 | ||
|
8ab358ce78 | ||
|
5193ba2062 | ||
|
7e5767f926 | ||
|
e5e6829d20 | ||
|
3976816b79 | ||
|
9732157f0c | ||
|
ed18d6d38d | ||
|
ef336a44a1 | ||
|
de5f503a76 | ||
|
b5b1ed2a55 | ||
|
caea9c99cd | ||
|
1495b94f7a | ||
|
f55bb78307 | ||
|
fa5a71dbf0 | ||
|
80a6479c47 | ||
|
a492de1bea | ||
|
43b414b0df | ||
|
6f31882260 | ||
|
5042808ecf | ||
|
1c9d0521ca | ||
|
ddda94e486 | ||
|
73ed9f56d7 | ||
|
ab5d032634 | ||
|
03b930515b | ||
|
c9f763ea6e | ||
|
422044f375 | ||
|
7584262f47 | ||
|
894f121d0e | ||
|
4b133509d9 | ||
|
e8040a828d | ||
|
78086329be | ||
|
6deeedd886 | ||
|
1a0146f21d | ||
|
7d0a9e0381 | ||
|
0f7b84197f | ||
|
bb06502d24 | ||
|
b7f1f19d8e | ||
|
0707290bbf | ||
|
8934a3961f | ||
|
b142ef5f8b | ||
|
f2dc9dd96e | ||
|
20ad3005b5 | ||
|
eacc69efba | ||
|
3c200ae45a | ||
|
74e7c7cbd0 | ||
|
6bd6d6212c | ||
|
4d2e66cf9c | ||
|
65fe00b7ad | ||
|
02c509390a | ||
|
49b0d1792b | ||
|
9511031490 | ||
|
04ee86e93c | ||
|
82f25f982e | ||
|
1cc32c9cda | ||
|
aedcab846c | ||
|
548ddff7ed | ||
|
7aced9e772 | ||
|
1cc8de0fed | ||
|
f08c7b2294 | ||
|
cc58d36de6 | ||
|
128393da54 | ||
|
e7964a0b89 | ||
|
77dc3746a3 | ||
|
e88f4d689c | ||
|
f1858c74a4 | ||
|
7427dafc36 | ||
|
1cdcabf7cf | ||
|
ea192b3c83 | ||
|
927057df26 | ||
|
19049ca91b | ||
|
3542348c1e | ||
|
df9061b933 | ||
|
98658ebed5 | ||
|
2a93147b1b | ||
|
4145c629c0 | ||
|
551dc0a74c | ||
|
85bbcdad9a | ||
|
336c1c1d37 | ||
|
1570afe493 | ||
|
c7c650fccc | ||
|
9b7fba69f4 | ||
|
0bd355c166 | ||
|
0d8c8d013d | ||
|
7a57f7b8cc | ||
|
940c4731c4 | ||
|
4332f0ca05 | ||
|
e0e6e20e02 | ||
|
0fdaa1438e | ||
|
8a111229f7 | ||
|
e3eb9c195a | ||
|
8dd5ec6fbd | ||
|
e5d60bc56d | ||
|
cba97d576a | ||
|
0670213365 | ||
|
ed5c11b3aa | ||
|
4b8c4704b0 | ||
|
c5374095e6 | ||
|
6abd5fbc3e | ||
|
a6a302f41f | ||
|
4c764829da | ||
|
9900c1ad8a | ||
|
3fabbab417 | ||
|
6c99c1ae13 | ||
|
c2320fceab | ||
|
8b87d86358 | ||
|
5d0e1d62d5 | ||
|
7e613c7a78 | ||
|
2a30436e45 | ||
|
5315a89e7d | ||
|
8c328316ae | ||
|
030a97d098 | ||
|
c40e71dc03 | ||
|
2f633cdfb7 | ||
|
edd6ae588a | ||
|
2a73ba2fbb | ||
|
5eadb86500 | ||
|
c98ab6c6dc | ||
|
5321463892 | ||
|
d668a7694f | ||
|
9bb482e46f | ||
|
c534c928a7 | ||
|
0d0478c4a4 | ||
|
cda681a2f0 | ||
|
72ed4f28b1 | ||
|
6afeaac7a5 | ||
|
75a2b66206 | ||
|
5966053a6d | ||
|
e500b79858 | ||
|
428c20c79f | ||
|
bf84bc17ea | ||
|
30fa9cbee7 | ||
|
eefca613ad | ||
|
f6d943aec7 | ||
|
88462e67b5 | ||
|
eb683dd402 | ||
|
c7d0aea5f4 | ||
|
8f08953100 | ||
|
09b009abd9 | ||
|
c37e481a43 | ||
|
72cf55b8c3 | ||
|
6cb24ae7b6 | ||
|
eeaf0234f0 | ||
|
0200740d70 | ||
|
1268eef3b2 | ||
|
b433048003 | ||
|
daf2c3c155 | ||
|
03d213d764 | ||
|
99f9481b5d | ||
|
10bd14bca6 | ||
|
d26533e370 | ||
|
820abacf49 | ||
|
4466aa39c4 | ||
|
f4e43731ef | ||
|
884ef211f7 | ||
|
896ef5a15f | ||
|
4ff482cd47 | ||
|
6e27360fbc | ||
|
a10a0824eb | ||
|
2fdda2ec1b | ||
|
57f76a2111 | ||
|
287daa9248 | ||
|
7e40a051a4 | ||
|
bd0a7730b6 | ||
|
0d1c87e650 | ||
|
56cd963fd7 | ||
|
d1b26cb267 | ||
|
6a0a03415c | ||
|
41d50adbf3 | ||
|
328b52c4d6 | ||
|
b7d04cf7b9 | ||
|
7ed2cf30a5 | ||
|
78fe5576a9 | ||
|
41179b1282 | ||
|
f82d99b4c2 | ||
|
967f0d07f2 | ||
|
940dbe99e9 | ||
|
98e1c68a70 | ||
|
d8e250e9b0 | ||
|
c8be8510ba | ||
|
c99460ba15 | ||
|
769fcb7f50 | ||
|
60812790e1 | ||
|
a01e44f3b9 | ||
|
0917370bd5 | ||
|
09b612b130 | ||
|
26fdf3fb07 | ||
|
f41f3f6b51 | ||
|
677664e71d | ||
|
a8071f1039 | ||
|
bc08351a0a | ||
|
088b3893c3 | ||
|
d9d6dd9ba6 | ||
|
aca66674d3 | ||
|
597429ab3e | ||
|
715360c1e7 | ||
|
fcabaa7eff | ||
|
b14af989b8 | ||
|
1a919e0c3e | ||
|
9c1a6bed7b | ||
|
c48ec02f42 | ||
|
6f376489a5 | ||
|
0cbf7bef1e | ||
|
0f87e598f6 | ||
|
363b75619f | ||
|
090c801cc6 | ||
|
b711778d4a | ||
|
d52569d66f | ||
|
c676b7a473 | ||
|
71c49bc8cd | ||
|
97a7f747fb | ||
|
cef9e0de0c | ||
|
5637acb799 | ||
|
3d3bdcb966 | ||
|
c9f02ae020 | ||
|
0e7512a225 | ||
|
c330016109 | ||
|
cb13cdec85 | ||
|
c65c580b20 | ||
|
d159ae9342 | ||
|
a540af1ca7 | ||
|
e9c234d89f | ||
|
b472dac6b3 | ||
|
523dac1be3 | ||
|
962a2126b5 | ||
|
5c495ad1b0 | ||
|
f633f34e43 | ||
|
bacf1b9acc | ||
|
4df9da5c48 | ||
|
30bbc1350d | ||
|
2f0f1fd5f5 | ||
|
c28e6ebc4c | ||
|
6479c11e9a | ||
|
4ed0fcdde6 | ||
|
296a8ade63 | ||
|
a84953ccfd | ||
|
8492031fd0 | ||
|
bff7259111 | ||
|
67e1814581 | ||
|
12e92dd59d | ||
|
4ee366edfa | ||
|
61573756f8 | ||
|
fe5fed1182 | ||
|
0c90307677 | ||
|
cdd2a51f1f | ||
|
0dbe3434f0 | ||
|
ef205593c5 | ||
|
8b5ba771ad | ||
|
991f99b239 | ||
|
d6f17517cb | ||
|
15b2f280e3 | ||
|
60b43a1ddf | ||
|
d6b83e3b0a | ||
|
0446f89d22 | ||
|
54bc3e606e | ||
|
fed90cfbe8 | ||
|
e2e41a29eb | ||
|
3b813db42f | ||
|
16b1a4d003 | ||
|
b51ea3ca0c | ||
|
dc76675644 | ||
|
274a238a00 | ||
|
10507f0ade | ||
|
af2a6106da | ||
|
120a7e433f | ||
|
98e34f07df | ||
|
738df79394 | ||
|
98e9b6e70b | ||
|
c9318f8dc2 | ||
|
78f3606e30 | ||
|
97f4d098e1 | ||
|
144a13b096 | ||
|
af0869c66c | ||
|
48e565038a | ||
|
cd6e1d921c | ||
|
fb767f4612 | ||
|
9f35db28e5 | ||
|
ab19543dff | ||
|
3d5f333a3b | ||
|
d06ca605cf | ||
|
334e11e4b9 | ||
|
fd68b4e7a8 | ||
|
e5ea16fad8 | ||
|
6d5a4b5cce | ||
|
ffb6b5a23b | ||
|
e247625025 | ||
|
67c07aa5d3 | ||
|
0de1ce0c2c | ||
|
59f4fba05c | ||
|
c0c764377c | ||
|
c9bc059637 | ||
|
78147d48e4 | ||
|
5e7db52087 | ||
|
ae42413d57 | ||
|
d433bd3d84 | ||
|
58dd6dc227 | ||
|
893df9b277 | ||
|
17dc13760b | ||
|
498bf911eb | ||
|
96de58b3a4 | ||
|
9b61fa24c7 | ||
|
e9be3cf6bc | ||
|
ea44a71914 | ||
|
a00fbbf5ca | ||
|
eb1a04af65 | ||
|
d1fbf77f3f | ||
|
04fbf73a29 | ||
|
db70eb3160 | ||
|
cd974c26b6 | ||
|
53e0f5d61c | ||
|
e864bf4898 | ||
|
81d12b7644 | ||
|
309fcd6270 | ||
|
938112e449 | ||
|
3e012ea69e | ||
|
975c942ea7 | ||
|
2798271da0 | ||
|
dc258cebab | ||
|
4b8c5194c7 | ||
|
3c7c6dacfb | ||
|
e36337a764 | ||
|
a49856b898 | ||
|
8ca2f52041 | ||
|
2f7f243022 | ||
|
7e443770d7 | ||
|
8ec09884b8 | ||
|
88c7e636d6 | ||
|
add3fd479d | ||
|
70410536b9 | ||
|
6f3f9b485c | ||
|
bd9ce3590d |
28
.github/workflows/explorer.yml
vendored
28
.github/workflows/explorer.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Explorer_build&test_on_PR
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'explorer/**'
|
||||
jobs:
|
||||
check-explorer:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: explorer
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: explorer/package-lock.json
|
||||
- run: npm i -g npm@7
|
||||
- run: npm ci
|
||||
- run: npm run format
|
||||
- run: npm run build
|
||||
- run: npm run test
|
22
.github/workflows/explorer_preview.yml
vendored
22
.github/workflows/explorer_preview.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name : explorer_preview
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Explorer_build&test_on_PR"]
|
||||
# types:
|
||||
# - completed
|
||||
jobs:
|
||||
explorer_preview:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: amondnet/vercel-action@v20
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }} #Optional
|
||||
vercel-org-id: ${{ secrets.ORG_ID}} #Required
|
||||
vercel-project-id: ${{ secrets.PROJECT_ID}} #Required
|
||||
working-directory: ./explorer
|
||||
scope: ${{ secrets.TEAM_ID }}
|
46
.github/workflows/explorer_production.yml
vendored
46
.github/workflows/explorer_production.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Explorer_production_build&test
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'explorer/**'
|
||||
jobs:
|
||||
Explorer_production_build_test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: explorer
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: explorer/package-lock.json
|
||||
- run: npm i -g npm@7
|
||||
- run: npm ci
|
||||
- run: npm run format
|
||||
- run: npm run build
|
||||
- run: npm run test
|
||||
|
||||
Explorer_production_deploy:
|
||||
needs: Explorer_production_build_test
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: explorer
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: amondnet/vercel-action@v20
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }} #Optional
|
||||
vercel-args: '--prod' #for production
|
||||
vercel-org-id: ${{ secrets.ORG_ID}} #Required
|
||||
vercel-project-id: ${{ secrets.PROJECT_ID}} #Required
|
||||
working-directory: ./explorer
|
||||
scope: ${{ secrets.TEAM_ID }}
|
208
.github/workflows/solana-action.yml.txt
vendored
208
.github/workflows/solana-action.yml.txt
vendored
@@ -1,208 +0,0 @@
|
||||
name : minimal
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
Export_Github_Repositories:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERCEL_TOKEN: ${{secrets.VERCEL_TOKEN}}
|
||||
GITHUB_TOKEN: ${{secrets.PAT_ANM}}
|
||||
COMMIT_RANGE: ${{ github.event.before}}...${{ github.event.after}}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- run: echo "COMMIT_DIFF_RANGE=$(echo $COMMIT_RANGE)" >> $GITHUB_ENV
|
||||
# - run: echo "$COMMIT_DIFF_RANGE"
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
GITHUB_TOKEN: ${{secrets.PAT_ANM}}
|
||||
if: ${{ github.event_name == 'push' && 'cron'&& github.ref == 'refs/heads/master'}}
|
||||
|
||||
- name: cmd
|
||||
run : |
|
||||
.travis/export-github-repo.sh web3.js/ solana-web3.js
|
||||
|
||||
macos-artifacts:
|
||||
needs: [Export_Github_Repositories]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: macos-latest
|
||||
if : ${{ github.event_name == 'api' && 'cron' || 'push' || startsWith(github.ref, 'refs/tags/v')}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup | Rust
|
||||
uses: ATiltedTree/setup-rust@v1
|
||||
with:
|
||||
rust-version: stable
|
||||
- name: release artifact
|
||||
run: |
|
||||
source ci/rust-version.sh
|
||||
brew install coreutils
|
||||
export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
|
||||
greadlink -f .
|
||||
source ci/env.sh
|
||||
rustup set profile default
|
||||
ci/publish-tarball.sh
|
||||
shell: bash
|
||||
|
||||
- name: Cache modules
|
||||
uses: actions/cache@master
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
|
||||
# - To stop from uploading on the production
|
||||
# - uses: ochanje210/simple-s3-upload-action@master
|
||||
# with:
|
||||
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
# AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
# SOURCE_DIR: 'travis-s3-upload1'
|
||||
# DEST_DIR: 'giitsol'
|
||||
|
||||
# - uses: ochanje210/simple-s3-upload-action@master
|
||||
# with:
|
||||
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
# AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
# SOURCE_DIR: './docs/'
|
||||
# DEST_DIR: 'giitsol'
|
||||
|
||||
|
||||
windows-artifact:
|
||||
needs: [Export_Github_Repositories]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: windows-latest
|
||||
if : ${{ github.event_name == 'api' && 'cron' || 'push' || startsWith(github.ref, 'refs/tags/v')}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup | Rust
|
||||
uses: ATiltedTree/setup-rust@v1
|
||||
with:
|
||||
rust-version: stable
|
||||
release-artifact:
|
||||
needs: windows-artifact
|
||||
runs-on: windows-latest
|
||||
if : ${{ github.event_name == 'api' && 'cron' || github.ref == 'refs/heads/master'}}
|
||||
steps:
|
||||
- name: release artifact
|
||||
run: |
|
||||
git clone git://git.openssl.org/openssl.git
|
||||
cd openssl
|
||||
make
|
||||
make test
|
||||
make install
|
||||
openssl version
|
||||
# choco install openssl
|
||||
# vcpkg integrate install
|
||||
# refreshenv
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
- run: choco install msys2
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
openssl version
|
||||
bash ci/rust-version.sh
|
||||
readlink -f .
|
||||
bash ci/env.sh
|
||||
rustup set profile default
|
||||
bash ci/publish-tarball.sh
|
||||
shell: bash
|
||||
|
||||
- name: Cache modules
|
||||
uses: actions/cache@v1
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
# - To stop from uploading on the production
|
||||
# - name: Config. aws cred
|
||||
# uses: aws-actions/configure-aws-credentials@v1
|
||||
# with:
|
||||
# aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# aws-region: us-east-2
|
||||
# - name: Deploy
|
||||
# uses: shallwefootball/s3-upload-action@master
|
||||
# with:
|
||||
# folder: build
|
||||
# aws_bucket: ${{ secrets.AWS_S3_BUCKET }}
|
||||
# aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# destination_dir: /
|
||||
# bucket-region: us-east-2
|
||||
# delete-removed: true
|
||||
# no-cache: true
|
||||
# private: true
|
||||
|
||||
# Docs:
|
||||
# needs: [windows-artifact,release-artifact]
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{secrets.PAT_NEW}}
|
||||
# GITHUB_EVENT_BEFORE: ${{ github.event.before }}
|
||||
# GITHUB_EVENT_AFTER: ${{ github.event.after }}
|
||||
# COMMIT_RANGE: ${{ github.event.before}}...${{ github.event.after}}
|
||||
# steps:
|
||||
# - name: Checkout repo
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 2
|
||||
# - name: docs
|
||||
# if: ${{github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/v')}}
|
||||
# run: |
|
||||
# touch .env
|
||||
# echo "COMMIT_RANGE=($COMMIT_RANGE)" > .env
|
||||
# source ci/env.sh
|
||||
# .travis/channel_restriction.sh edge beta || exit 0
|
||||
# .travis/affects.sh docs/ .travis || exit 0
|
||||
# cd docs/
|
||||
# source .travis/before_install.sh
|
||||
# source .travis/script.sh
|
||||
# - name: setup-node
|
||||
# uses: actions/checkout@v2
|
||||
# - name: setup-node
|
||||
# uses: actions/setup-node@v2
|
||||
# with:
|
||||
# node-version: 'lts/*'
|
||||
# - name: Cache
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.OS }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.OS }}-npm-cache-2
|
||||
|
||||
# auto_bump:
|
||||
# needs: [windows-artifact,release-artifact,Docs]
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name : checkout repo
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: '0'
|
||||
# - name: Bump version and push tag
|
||||
# uses: anothrNick/github-tag-action@1.26.0
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# WITH_V: true
|
||||
# DEFAULT_BUMP: patch
|
84
.github/workflows/web3.yml
vendored
84
.github/workflows/web3.yml
vendored
@@ -1,84 +0,0 @@
|
||||
name: Web3
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- "web3.js/**"
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- "web3.js/**"
|
||||
|
||||
jobs:
|
||||
# needed for grouping check-web3 strategies into one check for mergify
|
||||
all-web3-checks:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-web3
|
||||
steps:
|
||||
- run: echo "Done"
|
||||
|
||||
web3-commit-lint:
|
||||
runs-on: ubuntu-latest
|
||||
# Set to true in order to avoid cancelling other workflow jobs.
|
||||
# Mergify will still require web3-commit-lint for automerge
|
||||
continue-on-error: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web3.js
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
# maybe needed for base sha below
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web3.js/package-lock.json
|
||||
- run: npm ci
|
||||
- name: commit-lint
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: bash commitlint.sh
|
||||
env:
|
||||
COMMIT_RANGE: ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}
|
||||
|
||||
check-web3:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web3.js
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node: [ '12', '14', '16' ]
|
||||
|
||||
name: Node ${{ matrix.node }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web3.js/package-lock.json
|
||||
- run: npm i -g npm@7
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: |
|
||||
npm run build
|
||||
ls -l lib
|
||||
test -r lib/index.iife.js
|
||||
test -r lib/index.cjs.js
|
||||
test -r lib/index.esm.js
|
||||
- run: npm run doc
|
||||
- run: npm run codecov
|
||||
- run: |
|
||||
sh -c "$(curl -sSfL https://release.solana.com/edge/install)"
|
||||
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||
PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"
|
||||
solana --version
|
||||
- run: npm run test:live-with-test-validator
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,5 +27,3 @@ log-*/
|
||||
/spl_*.so
|
||||
|
||||
.DS_Store
|
||||
# scripts that may be generated by cargo *-bpf commands
|
||||
**/cargo-*-bpf-child-script-*.sh
|
||||
|
95
.mergify.yml
95
.mergify.yml
@@ -4,71 +4,24 @@
|
||||
#
|
||||
# https://doc.mergify.io/
|
||||
pull_request_rules:
|
||||
- name: label changes from community
|
||||
conditions:
|
||||
- author≠@core-contributors
|
||||
- author≠mergify[bot]
|
||||
- author≠dependabot[bot]
|
||||
actions:
|
||||
label:
|
||||
add:
|
||||
- community
|
||||
- name: request review for community changes
|
||||
conditions:
|
||||
- author≠@core-contributors
|
||||
- author≠mergify[bot]
|
||||
- author≠dependabot[bot]
|
||||
# Only request reviews from the pr subscribers group if no one
|
||||
# has reviewed the community PR yet. These checks only match
|
||||
# reviewers with admin, write or maintain permission on the repository.
|
||||
- "#approved-reviews-by=0"
|
||||
- "#commented-reviews-by=0"
|
||||
- "#changes-requested-reviews-by=0"
|
||||
actions:
|
||||
request_reviews:
|
||||
teams:
|
||||
- "@solana-labs/community-pr-subscribers"
|
||||
- name: automatic merge (squash) on CI success
|
||||
conditions:
|
||||
- and:
|
||||
- status-success=buildkite/solana
|
||||
- status-success=ci-gate
|
||||
- label=automerge
|
||||
- author≠@dont-squash-my-commits
|
||||
- or:
|
||||
# only require travis success if docs files changed
|
||||
- status-success=Travis CI - Pull Request
|
||||
- -files~=^docs/
|
||||
- or:
|
||||
# only require explorer checks if explorer files changed
|
||||
- status-success=check-explorer
|
||||
- -files~=^explorer/
|
||||
- or:
|
||||
- and:
|
||||
- status-success=all-web3-checks
|
||||
- status-success=web3-commit-lint
|
||||
# only require web3 checks if web3.js files changed
|
||||
- -files~=^web3.js/
|
||||
- status-success=buildkite/solana
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=ci-gate
|
||||
- label=automerge
|
||||
- author≠@dont-squash-my-commits
|
||||
actions:
|
||||
merge:
|
||||
method: squash
|
||||
# Join the dont-squash-my-commits group if you won't like your commits squashed
|
||||
- name: automatic merge (rebase) on CI success
|
||||
conditions:
|
||||
- and:
|
||||
- status-success=buildkite/solana
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=ci-gate
|
||||
- label=automerge
|
||||
- author=@dont-squash-my-commits
|
||||
- or:
|
||||
# only require explorer checks if explorer files changed
|
||||
- status-success=check-explorer
|
||||
- -files~=^explorer/
|
||||
- or:
|
||||
# only require web3 checks if web3.js files changed
|
||||
- status-success=all-web3-checks
|
||||
- -files~=^web3.js/
|
||||
- status-success=buildkite/solana
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=ci-gate
|
||||
- label=automerge
|
||||
- author=@dont-squash-my-commits
|
||||
actions:
|
||||
merge:
|
||||
method: rebase
|
||||
@@ -97,19 +50,35 @@ pull_request_rules:
|
||||
label:
|
||||
add:
|
||||
- automerge
|
||||
- name: v1.8 backport
|
||||
- name: v1.4 backport
|
||||
conditions:
|
||||
- label=v1.8
|
||||
- label=v1.4
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
branches:
|
||||
- v1.8
|
||||
- name: v1.9 backport
|
||||
- v1.4
|
||||
- name: v1.5 backport
|
||||
conditions:
|
||||
- label=v1.9
|
||||
- label=v1.5
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
branches:
|
||||
- v1.9
|
||||
- v1.5
|
||||
- name: v1.6 backport
|
||||
conditions:
|
||||
- label=v1.6
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
branches:
|
||||
- v1.6
|
||||
- name: v1.7 backport
|
||||
conditions:
|
||||
- label=v1.7
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
branches:
|
||||
- v1.7
|
||||
|
45
.travis.yml
45
.travis.yml
@@ -23,6 +23,7 @@ jobs:
|
||||
depth: false
|
||||
script:
|
||||
- .travis/export-github-repo.sh web3.js/ solana-web3.js
|
||||
- .travis/export-github-repo.sh explorer/ explorer
|
||||
|
||||
- &release-artifacts
|
||||
if: type IN (api, cron) OR tag IS present
|
||||
@@ -77,6 +78,50 @@ jobs:
|
||||
# before_install:
|
||||
# - sudo apt-get install libssl-dev libudev-dev
|
||||
|
||||
# explorer pull request
|
||||
- name: "explorer"
|
||||
if: type = pull_request AND branch = master
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "lts/*"
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- ~/.npm
|
||||
|
||||
before_install:
|
||||
- .travis/affects.sh explorer/ .travis || travis_terminate 0
|
||||
- cd explorer
|
||||
|
||||
script:
|
||||
- npm run build
|
||||
- npm run format
|
||||
|
||||
# web3.js pull request
|
||||
- name: "web3.js"
|
||||
if: type = pull_request AND branch = master
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "lts/*"
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- ~/.npm
|
||||
|
||||
before_install:
|
||||
- .travis/affects.sh web3.js/ .travis || travis_terminate 0
|
||||
- cd web3.js/
|
||||
- source .travis/before_install.sh
|
||||
|
||||
script:
|
||||
- ../.travis/commitlint.sh
|
||||
- source .travis/script.sh
|
||||
|
||||
# docs pull request
|
||||
- name: "docs"
|
||||
if: type IN (push, pull_request) OR tag IS present
|
||||
|
@@ -20,13 +20,13 @@ if [[ ! -f "$basedir"/commitlint.config.js ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $COMMIT_RANGE ]]; then
|
||||
echo "Error: COMMIT_RANGE not defined"
|
||||
if [[ -z $TRAVIS_COMMIT_RANGE ]]; then
|
||||
echo "Error: TRAVIS_COMMIT_RANGE not defined"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$basedir"
|
||||
echo "Checking commits in COMMIT_RANGE: $COMMIT_RANGE"
|
||||
echo "Checking commits in TRAVIS_COMMIT_RANGE: $TRAVIS_COMMIT_RANGE"
|
||||
while IFS= read -r line; do
|
||||
echo "$line" | npx commitlint
|
||||
done < <(git log "$COMMIT_RANGE" --format=%s -- .)
|
||||
done < <(git log "$TRAVIS_COMMIT_RANGE" --format=%s -- .)
|
3573
Cargo.lock
generated
3573
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -4,6 +4,7 @@ members = [
|
||||
"accountsdb-plugin-manager",
|
||||
"accountsdb-plugin-postgres",
|
||||
"accounts-cluster-bench",
|
||||
"bench-exchange",
|
||||
"bench-streamer",
|
||||
"bench-tps",
|
||||
"accounts-bench",
|
||||
@@ -11,7 +12,7 @@ members = [
|
||||
"banks-client",
|
||||
"banks-interface",
|
||||
"banks-server",
|
||||
"bucket_map",
|
||||
"bloom",
|
||||
"clap-utils",
|
||||
"cli-config",
|
||||
"cli-output",
|
||||
@@ -19,7 +20,6 @@ members = [
|
||||
"core",
|
||||
"dos",
|
||||
"download-utils",
|
||||
"entry",
|
||||
"faucet",
|
||||
"frozen-abi",
|
||||
"perf",
|
||||
@@ -46,14 +46,14 @@ members = [
|
||||
"poh",
|
||||
"poh-bench",
|
||||
"program-test",
|
||||
"programs/address-lookup-table",
|
||||
"programs/address-lookup-table-tests",
|
||||
"programs/bpf_loader",
|
||||
"programs/compute-budget",
|
||||
"programs/config",
|
||||
"programs/exchange",
|
||||
"programs/ed25519",
|
||||
"programs/secp256k1",
|
||||
"programs/stake",
|
||||
"programs/vote",
|
||||
"rbpf-cli",
|
||||
"remote-wallet",
|
||||
"rpc",
|
||||
"runtime",
|
||||
@@ -61,11 +61,10 @@ members = [
|
||||
"sdk",
|
||||
"sdk/cargo-build-bpf",
|
||||
"sdk/cargo-test-bpf",
|
||||
"send-transaction-service",
|
||||
"scripts",
|
||||
"stake-accounts",
|
||||
"sys-tuner",
|
||||
"tokens",
|
||||
"transaction-dos",
|
||||
"transaction-status",
|
||||
"account-decoder",
|
||||
"upload-perf",
|
||||
@@ -74,11 +73,6 @@ members = [
|
||||
"cli",
|
||||
"rayon-threadlimit",
|
||||
"watchtower",
|
||||
"replica-node",
|
||||
"replica-lib",
|
||||
"test-validator",
|
||||
"rpc-test",
|
||||
"client-test",
|
||||
]
|
||||
|
||||
exclude = [
|
||||
@@ -89,3 +83,6 @@ exclude = [
|
||||
# dependency is supported on Apple M1. v2 of the feature resolver is needed to
|
||||
# specify arch-specific features.
|
||||
resolver = "2"
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
14
README.md
14
README.md
@@ -19,18 +19,12 @@ $ source $HOME/.cargo/env
|
||||
$ rustup component add rustfmt
|
||||
```
|
||||
|
||||
When building the master branch, please make sure you are using the latest stable rust version by running:
|
||||
Please make sure you are always using the latest stable rust version by running:
|
||||
|
||||
```bash
|
||||
$ rustup update
|
||||
```
|
||||
|
||||
When building a specific release branch, you should check the rust version in `ci/rust-version.sh` and if necessary, install that version by running:
|
||||
```bash
|
||||
$ rustup install VERSION
|
||||
```
|
||||
Note that if this is not the latest rust version on your machine, cargo commands may require an [override](https://rust-lang.github.io/rustup/overrides.html) in order to use the correct version.
|
||||
|
||||
On Linux systems you may need to install libssl-dev, pkg-config, zlib1g-dev, etc. On Ubuntu:
|
||||
|
||||
```bash
|
||||
@@ -116,13 +110,13 @@ send us that patch!
|
||||
|
||||
All claims, content, designs, algorithms, estimates, roadmaps,
|
||||
specifications, and performance measurements described in this project
|
||||
are done with the Solana Foundation's ("SF") good faith efforts. It is up to
|
||||
are done with the Solana Foundation's ("SF") best efforts. It is up to
|
||||
the reader to check and validate their accuracy and truthfulness.
|
||||
Furthermore, nothing in this project constitutes a solicitation for
|
||||
Furthermore nothing in this project constitutes a solicitation for
|
||||
investment.
|
||||
|
||||
Any content produced by SF or developer resources that SF provides, are
|
||||
for educational and inspirational purposes only. SF does not encourage,
|
||||
for educational and inspiration purposes only. SF does not encourage,
|
||||
induce or sanction the deployment, integration or use of any such
|
||||
applications (including the code comprising the Solana blockchain
|
||||
protocol) in violation of applicable laws or regulations and hereby
|
||||
|
@@ -18,24 +18,24 @@ Expect a response as fast as possible, within one business day at the latest.
|
||||
We offer bounties for critical security issues. Please see below for more details.
|
||||
|
||||
Loss of Funds:
|
||||
$2,000,000 USD in locked SOL tokens (locked for 12 months)
|
||||
$500,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* Theft of funds without users signature from any account
|
||||
* Theft of funds without users interaction in system, token, stake, vote programs
|
||||
* Theft of funds that requires users signature - creating a vote program that drains the delegated stakes.
|
||||
|
||||
Consensus/Safety Violations:
|
||||
$1,000,000 USD in locked SOL tokens (locked for 12 months)
|
||||
$250,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* Consensus safety violation
|
||||
* Tricking a validator to accept an optimistic confirmation or rooted slot without a double vote, etc..
|
||||
|
||||
Other Attacks:
|
||||
$400,000 USD in locked SOL tokens (locked for 12 months)
|
||||
$100,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* Protocol liveness attacks,
|
||||
* Eclipse attacks,
|
||||
* Remote attacks that partition the network,
|
||||
|
||||
DoS Attacks:
|
||||
$100,000 USD in locked SOL tokens (locked for 12 months)
|
||||
$25,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* Remote resource exaustion via Non-RPC protocols
|
||||
|
||||
RPC DoS/Crashes:
|
||||
|
@@ -1,30 +1,30 @@
|
||||
[package]
|
||||
name = "solana-account-decoder"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
description = "Solana account decoder"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-account-decoder"
|
||||
license = "Apache-2.0"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.12.3"
|
||||
bincode = "1.3.3"
|
||||
bs58 = "0.4.0"
|
||||
bincode = "1.3.1"
|
||||
bs58 = "0.3.1"
|
||||
bv = "0.11.1"
|
||||
Inflector = "0.11.4"
|
||||
lazy_static = "1.4.0"
|
||||
serde = "1.0.131"
|
||||
serde = "1.0.122"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.72"
|
||||
solana-config-program = { path = "../programs/config", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "=1.10.0" }
|
||||
serde_json = "1.0.56"
|
||||
solana-config-program = { path = "../programs/config", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "=1.8.14" }
|
||||
spl-token = { version = "=3.2.0", features = ["no-entrypoint"] }
|
||||
thiserror = "1.0"
|
||||
zstd = "0.9.0"
|
||||
zstd = "0.5.1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -1,5 +1,3 @@
|
||||
#[allow(deprecated)]
|
||||
use solana_sdk::sysvar::{fees::Fees, recent_blockhashes::RecentBlockhashes};
|
||||
use {
|
||||
crate::{
|
||||
parse_account_data::{ParsableAccount, ParseAccountError},
|
||||
@@ -15,12 +13,11 @@ use {
|
||||
slot_hashes::SlotHashes,
|
||||
slot_history::{self, SlotHistory},
|
||||
stake_history::{StakeHistory, StakeHistoryEntry},
|
||||
sysvar::{self, rewards::Rewards},
|
||||
sysvar::{self, fees::Fees, recent_blockhashes::RecentBlockhashes, rewards::Rewards},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn parse_sysvar(data: &[u8], pubkey: &Pubkey) -> Result<SysvarAccountType, ParseAccountError> {
|
||||
#[allow(deprecated)]
|
||||
let parsed_account = {
|
||||
if pubkey == &sysvar::clock::id() {
|
||||
deserialize::<Clock>(data)
|
||||
@@ -96,9 +93,7 @@ pub fn parse_sysvar(data: &[u8], pubkey: &Pubkey) -> Result<SysvarAccountType, P
|
||||
pub enum SysvarAccountType {
|
||||
Clock(UiClock),
|
||||
EpochSchedule(EpochSchedule),
|
||||
#[allow(deprecated)]
|
||||
Fees(UiFees),
|
||||
#[allow(deprecated)]
|
||||
RecentBlockhashes(Vec<UiRecentBlockhashesEntry>),
|
||||
Rent(UiRent),
|
||||
Rewards(UiRewards),
|
||||
@@ -134,7 +129,6 @@ impl From<Clock> for UiClock {
|
||||
pub struct UiFees {
|
||||
pub fee_calculator: UiFeeCalculator,
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
impl From<Fees> for UiFees {
|
||||
fn from(fees: Fees) -> Self {
|
||||
Self {
|
||||
@@ -220,17 +214,16 @@ pub struct UiStakeHistoryEntry {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[allow(deprecated)]
|
||||
use solana_sdk::sysvar::recent_blockhashes::IterItem;
|
||||
use {
|
||||
super::*,
|
||||
solana_sdk::{account::create_account_for_test, fee_calculator::FeeCalculator, hash::Hash},
|
||||
solana_sdk::{
|
||||
account::create_account_for_test, fee_calculator::FeeCalculator, hash::Hash,
|
||||
sysvar::recent_blockhashes::IterItem,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_sysvars() {
|
||||
let hash = Hash::new(&[1; 32]);
|
||||
|
||||
let clock_sysvar = create_account_for_test(&Clock::default());
|
||||
assert_eq!(
|
||||
parse_sysvar(&clock_sysvar.data, &sysvar::clock::id()).unwrap(),
|
||||
@@ -250,29 +243,31 @@ mod test {
|
||||
SysvarAccountType::EpochSchedule(epoch_schedule),
|
||||
);
|
||||
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
let fees_sysvar = create_account_for_test(&Fees::default());
|
||||
assert_eq!(
|
||||
parse_sysvar(&fees_sysvar.data, &sysvar::fees::id()).unwrap(),
|
||||
SysvarAccountType::Fees(UiFees::default()),
|
||||
);
|
||||
let fees_sysvar = create_account_for_test(&Fees::default());
|
||||
assert_eq!(
|
||||
parse_sysvar(&fees_sysvar.data, &sysvar::fees::id()).unwrap(),
|
||||
SysvarAccountType::Fees(UiFees::default()),
|
||||
);
|
||||
|
||||
let recent_blockhashes: RecentBlockhashes =
|
||||
vec![IterItem(0, &hash, 10)].into_iter().collect();
|
||||
let recent_blockhashes_sysvar = create_account_for_test(&recent_blockhashes);
|
||||
assert_eq!(
|
||||
parse_sysvar(
|
||||
&recent_blockhashes_sysvar.data,
|
||||
&sysvar::recent_blockhashes::id()
|
||||
)
|
||||
.unwrap(),
|
||||
SysvarAccountType::RecentBlockhashes(vec![UiRecentBlockhashesEntry {
|
||||
blockhash: hash.to_string(),
|
||||
fee_calculator: FeeCalculator::new(10).into(),
|
||||
}]),
|
||||
);
|
||||
}
|
||||
let hash = Hash::new(&[1; 32]);
|
||||
let fee_calculator = FeeCalculator {
|
||||
lamports_per_signature: 10,
|
||||
};
|
||||
let recent_blockhashes: RecentBlockhashes = vec![IterItem(0, &hash, &fee_calculator)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let recent_blockhashes_sysvar = create_account_for_test(&recent_blockhashes);
|
||||
assert_eq!(
|
||||
parse_sysvar(
|
||||
&recent_blockhashes_sysvar.data,
|
||||
&sysvar::recent_blockhashes::id()
|
||||
)
|
||||
.unwrap(),
|
||||
SysvarAccountType::RecentBlockhashes(vec![UiRecentBlockhashesEntry {
|
||||
blockhash: hash.to_string(),
|
||||
fee_calculator: fee_calculator.into(),
|
||||
}]),
|
||||
);
|
||||
|
||||
let rent = Rent {
|
||||
lamports_per_byte_year: 10,
|
||||
|
@@ -118,7 +118,6 @@ pub fn parse_token(
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TokenAccountType {
|
||||
Account(UiTokenAccount),
|
||||
Mint(UiMint),
|
||||
|
@@ -1,22 +1,24 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-accounts-bench"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.14"
|
||||
rayon = "1.5.1"
|
||||
solana-logger = { path = "../logger", version = "=1.10.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.10.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-version = { path = "../version", version = "=1.10.0" }
|
||||
log = "0.4.11"
|
||||
rayon = "1.5.0"
|
||||
solana-logger = { path = "../logger", version = "=1.8.14" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.8.14" }
|
||||
solana-measure = { path = "../measure", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
solana-version = { path = "../version", version = "=1.8.14" }
|
||||
rand = "0.7.0"
|
||||
clap = "2.33.1"
|
||||
crossbeam-channel = "0.4"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -62,12 +62,13 @@ fn main() {
|
||||
if fs::remove_dir_all(path.clone()).is_err() {
|
||||
println!("Warning: Couldn't remove {:?}", path);
|
||||
}
|
||||
let accounts = Accounts::new_with_config_for_benches(
|
||||
let accounts = Accounts::new_with_config(
|
||||
vec![path],
|
||||
&ClusterType::Testnet,
|
||||
AccountSecondaryIndexes::default(),
|
||||
false,
|
||||
AccountShrinkThreshold::default(),
|
||||
None,
|
||||
);
|
||||
println!("Creating {} accounts", num_accounts);
|
||||
let mut create_time = Measure::start("create accounts");
|
||||
@@ -104,7 +105,7 @@ fn main() {
|
||||
for x in 0..iterations {
|
||||
if clean {
|
||||
let mut time = Measure::start("clean");
|
||||
accounts.accounts_db.clean_accounts(None, false, None);
|
||||
accounts.accounts_db.clean_accounts(None, false);
|
||||
time.stop();
|
||||
println!("{}", time);
|
||||
for slot in 0..num_slots {
|
||||
@@ -125,7 +126,6 @@ fn main() {
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
time_store.stop();
|
||||
if results != results_store {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-accounts-cluster-bench"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -10,28 +10,27 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.1"
|
||||
log = "0.4.14"
|
||||
log = "0.4.11"
|
||||
rand = "0.7.0"
|
||||
rayon = "1.5.1"
|
||||
solana-account-decoder = { path = "../account-decoder", version = "=1.10.0" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.10.0" }
|
||||
solana-client = { path = "../client", version = "=1.10.0" }
|
||||
solana-core = { path = "../core", version = "=1.10.0" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.10.0" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.10.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.10.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.10.0" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.10.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.10.0" }
|
||||
solana-test-validator = { path = "../test-validator", version = "=1.10.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.10.0" }
|
||||
solana-version = { path = "../version", version = "=1.10.0" }
|
||||
rayon = "1.4.1"
|
||||
solana-account-decoder = { path = "../account-decoder", version = "=1.8.14" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.14" }
|
||||
solana-client = { path = "../client", version = "=1.8.14" }
|
||||
solana-core = { path = "../core", version = "=1.8.14" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.8.14" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.8.14" }
|
||||
solana-logger = { path = "../logger", version = "=1.8.14" }
|
||||
solana-measure = { path = "../measure", version = "=1.8.14" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.8.14" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.8.14" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.8.14" }
|
||||
solana-version = { path = "../version", version = "=1.8.14" }
|
||||
spl-token = { version = "=3.2.0", features = ["no-entrypoint"] }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-local-cluster = { path = "../local-cluster", version = "=1.10.0" }
|
||||
solana-local-cluster = { path = "../local-cluster", version = "=1.8.14" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -6,35 +6,39 @@ use {
|
||||
rayon::prelude::*,
|
||||
solana_account_decoder::parse_token::spl_token_pubkey,
|
||||
solana_clap_utils::input_parsers::pubkey_of,
|
||||
solana_client::{rpc_client::RpcClient, transaction_executor::TransactionExecutor},
|
||||
solana_client::rpc_client::RpcClient,
|
||||
solana_faucet::faucet::{request_airdrop_transaction, FAUCET_PORT},
|
||||
solana_gossip::gossip_service::discover,
|
||||
solana_measure::measure::Measure,
|
||||
solana_runtime::inline_spl_token,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
rpc_port::DEFAULT_RPC_PORT,
|
||||
signature::{read_keypair_file, Keypair, Signer},
|
||||
signature::{read_keypair_file, Keypair, Signature, Signer},
|
||||
system_instruction, system_program,
|
||||
timing::timestamp,
|
||||
transaction::Transaction,
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
solana_transaction_status::parse_token::spl_token_instruction,
|
||||
std::{
|
||||
cmp::min,
|
||||
net::SocketAddr,
|
||||
process::exit,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread::sleep,
|
||||
thread::{sleep, Builder, JoinHandle},
|
||||
time::{Duration, Instant},
|
||||
},
|
||||
};
|
||||
|
||||
// Create and close messages both require 2 signatures; if transaction construction changes, update
|
||||
// this magic number
|
||||
const NUM_SIGNATURES: u64 = 2;
|
||||
|
||||
pub fn airdrop_lamports(
|
||||
client: &RpcClient,
|
||||
faucet_addr: &SocketAddr,
|
||||
@@ -53,7 +57,7 @@ pub fn airdrop_lamports(
|
||||
id.pubkey(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_latest_blockhash().unwrap();
|
||||
let (blockhash, _fee_calculator) = client.get_recent_blockhash().unwrap();
|
||||
match request_airdrop_transaction(faucet_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
||||
Ok(transaction) => {
|
||||
let mut tries = 0;
|
||||
@@ -98,6 +102,160 @@ pub fn airdrop_lamports(
|
||||
true
|
||||
}
|
||||
|
||||
// signature, timestamp, id
|
||||
type PendingQueue = Vec<(Signature, u64, u64)>;
|
||||
|
||||
struct TransactionExecutor {
|
||||
sig_clear_t: JoinHandle<()>,
|
||||
sigs: Arc<RwLock<PendingQueue>>,
|
||||
cleared: Arc<RwLock<Vec<u64>>>,
|
||||
exit: Arc<AtomicBool>,
|
||||
counter: AtomicU64,
|
||||
client: RpcClient,
|
||||
}
|
||||
|
||||
impl TransactionExecutor {
|
||||
fn new(entrypoint_addr: SocketAddr) -> Self {
|
||||
let sigs = Arc::new(RwLock::new(Vec::new()));
|
||||
let cleared = Arc::new(RwLock::new(Vec::new()));
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
let sig_clear_t = Self::start_sig_clear_thread(&exit, &sigs, &cleared, entrypoint_addr);
|
||||
let client =
|
||||
RpcClient::new_socket_with_commitment(entrypoint_addr, CommitmentConfig::confirmed());
|
||||
Self {
|
||||
sigs,
|
||||
cleared,
|
||||
sig_clear_t,
|
||||
exit,
|
||||
counter: AtomicU64::new(0),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
fn num_outstanding(&self) -> usize {
|
||||
self.sigs.read().unwrap().len()
|
||||
}
|
||||
|
||||
fn push_transactions(&self, txs: Vec<Transaction>) -> Vec<u64> {
|
||||
let mut ids = vec![];
|
||||
let new_sigs = txs.into_iter().filter_map(|tx| {
|
||||
let id = self.counter.fetch_add(1, Ordering::Relaxed);
|
||||
ids.push(id);
|
||||
match self.client.send_transaction(&tx) {
|
||||
Ok(sig) => {
|
||||
return Some((sig, timestamp(), id));
|
||||
}
|
||||
Err(e) => {
|
||||
info!("error: {:#?}", e);
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
let mut sigs_w = self.sigs.write().unwrap();
|
||||
sigs_w.extend(new_sigs);
|
||||
ids
|
||||
}
|
||||
|
||||
fn drain_cleared(&self) -> Vec<u64> {
|
||||
std::mem::take(&mut *self.cleared.write().unwrap())
|
||||
}
|
||||
|
||||
fn close(self) {
|
||||
self.exit.store(true, Ordering::Relaxed);
|
||||
self.sig_clear_t.join().unwrap();
|
||||
}
|
||||
|
||||
fn start_sig_clear_thread(
|
||||
exit: &Arc<AtomicBool>,
|
||||
sigs: &Arc<RwLock<PendingQueue>>,
|
||||
cleared: &Arc<RwLock<Vec<u64>>>,
|
||||
entrypoint_addr: SocketAddr,
|
||||
) -> JoinHandle<()> {
|
||||
let sigs = sigs.clone();
|
||||
let exit = exit.clone();
|
||||
let cleared = cleared.clone();
|
||||
Builder::new()
|
||||
.name("sig_clear".to_string())
|
||||
.spawn(move || {
|
||||
let client = RpcClient::new_socket_with_commitment(
|
||||
entrypoint_addr,
|
||||
CommitmentConfig::confirmed(),
|
||||
);
|
||||
let mut success = 0;
|
||||
let mut error_count = 0;
|
||||
let mut timed_out = 0;
|
||||
let mut last_log = Instant::now();
|
||||
while !exit.load(Ordering::Relaxed) {
|
||||
let sigs_len = sigs.read().unwrap().len();
|
||||
if sigs_len > 0 {
|
||||
let mut sigs_w = sigs.write().unwrap();
|
||||
let mut start = Measure::start("sig_status");
|
||||
let statuses: Vec<_> = sigs_w
|
||||
.chunks(200)
|
||||
.flat_map(|sig_chunk| {
|
||||
let only_sigs: Vec<_> = sig_chunk.iter().map(|s| s.0).collect();
|
||||
client
|
||||
.get_signature_statuses(&only_sigs)
|
||||
.expect("status fail")
|
||||
.value
|
||||
})
|
||||
.collect();
|
||||
let mut num_cleared = 0;
|
||||
let start_len = sigs_w.len();
|
||||
let now = timestamp();
|
||||
let mut new_ids = vec![];
|
||||
let mut i = 0;
|
||||
let mut j = 0;
|
||||
while i != sigs_w.len() {
|
||||
let mut retain = true;
|
||||
let sent_ts = sigs_w[i].1;
|
||||
if let Some(e) = &statuses[j] {
|
||||
debug!("error: {:?}", e);
|
||||
if e.status.is_ok() {
|
||||
success += 1;
|
||||
} else {
|
||||
error_count += 1;
|
||||
}
|
||||
num_cleared += 1;
|
||||
retain = false;
|
||||
} else if now - sent_ts > 30_000 {
|
||||
retain = false;
|
||||
timed_out += 1;
|
||||
}
|
||||
if !retain {
|
||||
new_ids.push(sigs_w.remove(i).2);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
let final_sigs_len = sigs_w.len();
|
||||
drop(sigs_w);
|
||||
cleared.write().unwrap().extend(new_ids);
|
||||
start.stop();
|
||||
debug!(
|
||||
"sigs len: {:?} success: {} took: {}ms cleared: {}/{}",
|
||||
final_sigs_len,
|
||||
success,
|
||||
start.as_ms(),
|
||||
num_cleared,
|
||||
start_len,
|
||||
);
|
||||
if last_log.elapsed().as_millis() > 5000 {
|
||||
info!(
|
||||
"success: {} error: {} timed_out: {}",
|
||||
success, error_count, timed_out,
|
||||
);
|
||||
last_log = Instant::now();
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(200));
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
struct SeedTracker {
|
||||
max_created: Arc<AtomicU64>,
|
||||
max_closed: Arc<AtomicU64>,
|
||||
@@ -148,8 +306,8 @@ fn make_create_message(
|
||||
|
||||
instructions
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
let instructions: Vec<_> = instructions.into_iter().flatten().collect();
|
||||
|
||||
Message::new(&instructions, Some(&keypair.pubkey()))
|
||||
}
|
||||
@@ -157,30 +315,24 @@ fn make_create_message(
|
||||
fn make_close_message(
|
||||
keypair: &Keypair,
|
||||
base_keypair: &Keypair,
|
||||
max_created: Arc<AtomicU64>,
|
||||
max_closed: Arc<AtomicU64>,
|
||||
max_closed_seed: Arc<AtomicU64>,
|
||||
num_instructions: usize,
|
||||
balance: u64,
|
||||
spl_token: bool,
|
||||
) -> Message {
|
||||
let instructions: Vec<_> = (0..num_instructions)
|
||||
.into_iter()
|
||||
.filter_map(|_| {
|
||||
.map(|_| {
|
||||
let program_id = if spl_token {
|
||||
inline_spl_token::id()
|
||||
} else {
|
||||
system_program::id()
|
||||
};
|
||||
let max_created_seed = max_created.load(Ordering::Relaxed);
|
||||
let max_closed_seed = max_closed.load(Ordering::Relaxed);
|
||||
if max_closed_seed >= max_created_seed {
|
||||
return None;
|
||||
}
|
||||
let seed = max_closed.fetch_add(1, Ordering::Relaxed).to_string();
|
||||
let seed = max_closed_seed.fetch_add(1, Ordering::Relaxed).to_string();
|
||||
let address =
|
||||
Pubkey::create_with_seed(&base_keypair.pubkey(), &seed, &program_id).unwrap();
|
||||
if spl_token {
|
||||
Some(spl_token_instruction(
|
||||
spl_token_instruction(
|
||||
spl_token::instruction::close_account(
|
||||
&spl_token::id(),
|
||||
&spl_token_pubkey(&address),
|
||||
@@ -189,16 +341,16 @@ fn make_close_message(
|
||||
&[],
|
||||
)
|
||||
.unwrap(),
|
||||
))
|
||||
)
|
||||
} else {
|
||||
Some(system_instruction::transfer_with_seed(
|
||||
system_instruction::transfer_with_seed(
|
||||
&address,
|
||||
&base_keypair.pubkey(),
|
||||
seed,
|
||||
&program_id,
|
||||
&keypair.pubkey(),
|
||||
balance,
|
||||
))
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -218,7 +370,6 @@ fn run_accounts_bench(
|
||||
maybe_lamports: Option<u64>,
|
||||
num_instructions: usize,
|
||||
mint: Option<Pubkey>,
|
||||
reclaim_accounts: bool,
|
||||
) {
|
||||
assert!(num_instructions > 0);
|
||||
let client =
|
||||
@@ -226,10 +377,10 @@ fn run_accounts_bench(
|
||||
|
||||
info!("Targeting {}", entrypoint_addr);
|
||||
|
||||
let mut latest_blockhash = Instant::now();
|
||||
let mut last_blockhash = Instant::now();
|
||||
let mut last_log = Instant::now();
|
||||
let mut count = 0;
|
||||
let mut blockhash = client.get_latest_blockhash().expect("blockhash");
|
||||
let mut recent_blockhash = client.get_recent_blockhash().expect("blockhash");
|
||||
let mut tx_sent_count = 0;
|
||||
let mut total_accounts_created = 0;
|
||||
let mut total_accounts_closed = 0;
|
||||
@@ -257,33 +408,16 @@ fn run_accounts_bench(
|
||||
|
||||
let executor = TransactionExecutor::new(entrypoint_addr);
|
||||
|
||||
// Create and close messages both require 2 signatures, fake a 2 signature message to calculate fees
|
||||
let mut message = Message::new(
|
||||
&[
|
||||
Instruction::new_with_bytes(
|
||||
Pubkey::new_unique(),
|
||||
&[],
|
||||
vec![AccountMeta::new(Pubkey::new_unique(), true)],
|
||||
),
|
||||
Instruction::new_with_bytes(
|
||||
Pubkey::new_unique(),
|
||||
&[],
|
||||
vec![AccountMeta::new(Pubkey::new_unique(), true)],
|
||||
),
|
||||
],
|
||||
None,
|
||||
);
|
||||
|
||||
loop {
|
||||
if latest_blockhash.elapsed().as_millis() > 10_000 {
|
||||
blockhash = client.get_latest_blockhash().expect("blockhash");
|
||||
latest_blockhash = Instant::now();
|
||||
if last_blockhash.elapsed().as_millis() > 10_000 {
|
||||
recent_blockhash = client.get_recent_blockhash().expect("blockhash");
|
||||
last_blockhash = Instant::now();
|
||||
}
|
||||
|
||||
message.recent_blockhash = blockhash;
|
||||
let fee = client
|
||||
.get_fee_for_message(&message)
|
||||
.expect("get_fee_for_message");
|
||||
let fee = recent_blockhash
|
||||
.1
|
||||
.lamports_per_signature
|
||||
.saturating_mul(NUM_SIGNATURES);
|
||||
let lamports = min_balance + fee;
|
||||
|
||||
for (i, balance) in balances.iter_mut().enumerate() {
|
||||
@@ -332,7 +466,7 @@ fn run_accounts_bench(
|
||||
mint,
|
||||
);
|
||||
let signers: Vec<&Keypair> = vec![keypair, &base_keypair];
|
||||
Transaction::new(&signers, message, blockhash)
|
||||
Transaction::new(&signers, message, recent_blockhash.0)
|
||||
})
|
||||
.collect();
|
||||
balances[i] = balances[i].saturating_sub(lamports * txs.len() as u64);
|
||||
@@ -358,14 +492,13 @@ fn run_accounts_bench(
|
||||
let message = make_close_message(
|
||||
payer_keypairs[0],
|
||||
&base_keypair,
|
||||
seed_tracker.max_created.clone(),
|
||||
seed_tracker.max_closed.clone(),
|
||||
1,
|
||||
min_balance,
|
||||
mint.is_some(),
|
||||
);
|
||||
let signers: Vec<&Keypair> = vec![payer_keypairs[0], &base_keypair];
|
||||
Transaction::new(&signers, message, blockhash)
|
||||
Transaction::new(&signers, message, recent_blockhash.0)
|
||||
})
|
||||
.collect();
|
||||
balances[0] = balances[0].saturating_sub(fee * txs.len() as u64);
|
||||
@@ -381,7 +514,7 @@ fn run_accounts_bench(
|
||||
}
|
||||
|
||||
count += 1;
|
||||
if last_log.elapsed().as_millis() > 3000 || count >= iterations {
|
||||
if last_log.elapsed().as_millis() > 3000 {
|
||||
info!(
|
||||
"total_accounts_created: {} total_accounts_closed: {} tx_sent_count: {} loop_count: {} balance(s): {:?}",
|
||||
total_accounts_created, total_accounts_closed, tx_sent_count, count, balances
|
||||
@@ -396,83 +529,6 @@ fn run_accounts_bench(
|
||||
}
|
||||
}
|
||||
executor.close();
|
||||
|
||||
if reclaim_accounts {
|
||||
let executor = TransactionExecutor::new(entrypoint_addr);
|
||||
loop {
|
||||
let max_closed_seed = seed_tracker.max_closed.load(Ordering::Relaxed);
|
||||
let max_created_seed = seed_tracker.max_created.load(Ordering::Relaxed);
|
||||
|
||||
if latest_blockhash.elapsed().as_millis() > 10_000 {
|
||||
blockhash = client.get_latest_blockhash().expect("blockhash");
|
||||
latest_blockhash = Instant::now();
|
||||
}
|
||||
message.recent_blockhash = blockhash;
|
||||
let fee = client
|
||||
.get_fee_for_message(&message)
|
||||
.expect("get_fee_for_message");
|
||||
|
||||
let sigs_len = executor.num_outstanding();
|
||||
if sigs_len < batch_size && max_closed_seed < max_created_seed {
|
||||
let num_to_close = min(
|
||||
batch_size - sigs_len,
|
||||
(max_created_seed - max_closed_seed) as usize,
|
||||
);
|
||||
if num_to_close >= payer_keypairs.len() {
|
||||
info!("closing {} accounts", num_to_close);
|
||||
let chunk_size = num_to_close / payer_keypairs.len();
|
||||
info!("{:?} chunk_size", chunk_size);
|
||||
if chunk_size > 0 {
|
||||
for (i, keypair) in payer_keypairs.iter().enumerate() {
|
||||
let txs: Vec<_> = (0..chunk_size)
|
||||
.into_par_iter()
|
||||
.filter_map(|_| {
|
||||
let message = make_close_message(
|
||||
keypair,
|
||||
&base_keypair,
|
||||
seed_tracker.max_created.clone(),
|
||||
seed_tracker.max_closed.clone(),
|
||||
num_instructions,
|
||||
min_balance,
|
||||
mint.is_some(),
|
||||
);
|
||||
if message.instructions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let signers: Vec<&Keypair> = vec![keypair, &base_keypair];
|
||||
Some(Transaction::new(&signers, message, blockhash))
|
||||
})
|
||||
.collect();
|
||||
balances[i] = balances[i].saturating_sub(fee * txs.len() as u64);
|
||||
info!("close txs: {}", txs.len());
|
||||
let new_ids = executor.push_transactions(txs);
|
||||
info!("close ids: {}", new_ids.len());
|
||||
tx_sent_count += new_ids.len();
|
||||
total_accounts_closed += (num_instructions * new_ids.len()) as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = executor.drain_cleared();
|
||||
}
|
||||
count += 1;
|
||||
if last_log.elapsed().as_millis() > 3000 || max_closed_seed >= max_created_seed {
|
||||
info!(
|
||||
"total_accounts_closed: {} tx_sent_count: {} loop_count: {} balance(s): {:?}",
|
||||
total_accounts_closed, tx_sent_count, count, balances
|
||||
);
|
||||
last_log = Instant::now();
|
||||
}
|
||||
|
||||
if max_closed_seed >= max_created_seed {
|
||||
break;
|
||||
}
|
||||
if executor.num_outstanding() >= batch_size {
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
executor.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -548,7 +604,7 @@ fn main() {
|
||||
.long("iterations")
|
||||
.takes_value(true)
|
||||
.value_name("NUM")
|
||||
.help("Number of iterations to make. 0 = unlimited iterations."),
|
||||
.help("Number of iterations to make"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("check_gossip")
|
||||
@@ -561,12 +617,6 @@ fn main() {
|
||||
.takes_value(true)
|
||||
.help("Mint address to initialize account"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("reclaim_accounts")
|
||||
.long("reclaim-accounts")
|
||||
.takes_value(false)
|
||||
.help("Reclaim accounts after session ends; incompatible with --iterations 0"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let skip_gossip = !matches.is_present("check_gossip");
|
||||
@@ -648,7 +698,6 @@ fn main() {
|
||||
lamports,
|
||||
num_instructions,
|
||||
mint,
|
||||
matches.is_present("reclaim_accounts"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -657,18 +706,11 @@ pub mod test {
|
||||
use {
|
||||
super::*,
|
||||
solana_core::validator::ValidatorConfig,
|
||||
solana_faucet::faucet::run_local_faucet,
|
||||
solana_local_cluster::{
|
||||
local_cluster::{ClusterConfig, LocalCluster},
|
||||
validator_configs::make_identical_validator_configs,
|
||||
},
|
||||
solana_measure::measure::Measure,
|
||||
solana_sdk::{native_token::sol_to_lamports, poh_config::PohConfig},
|
||||
solana_test_validator::TestValidator,
|
||||
spl_token::{
|
||||
solana_program::program_pack::Pack,
|
||||
state::{Account, Mint},
|
||||
},
|
||||
solana_sdk::poh_config::PohConfig,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -704,108 +746,6 @@ pub mod test {
|
||||
maybe_lamports,
|
||||
num_instructions,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
start.stop();
|
||||
info!("{}", start);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_then_reclaim_spl_token_accounts() {
|
||||
solana_logger::setup();
|
||||
let mint_keypair = Keypair::new();
|
||||
let mint_pubkey = mint_keypair.pubkey();
|
||||
let faucet_addr = run_local_faucet(mint_keypair, None);
|
||||
let test_validator = TestValidator::with_custom_fees(
|
||||
mint_pubkey,
|
||||
1,
|
||||
Some(faucet_addr),
|
||||
SocketAddrSpace::Unspecified,
|
||||
);
|
||||
let rpc_client =
|
||||
RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed());
|
||||
|
||||
// Created funder
|
||||
let funder = Keypair::new();
|
||||
let latest_blockhash = rpc_client.get_latest_blockhash().unwrap();
|
||||
let signature = rpc_client
|
||||
.request_airdrop_with_blockhash(
|
||||
&funder.pubkey(),
|
||||
sol_to_lamports(1.0),
|
||||
&latest_blockhash,
|
||||
)
|
||||
.unwrap();
|
||||
rpc_client
|
||||
.confirm_transaction_with_spinner(
|
||||
&signature,
|
||||
&latest_blockhash,
|
||||
CommitmentConfig::confirmed(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create Mint
|
||||
let spl_mint_keypair = Keypair::new();
|
||||
let spl_mint_len = Mint::get_packed_len();
|
||||
let spl_mint_rent = rpc_client
|
||||
.get_minimum_balance_for_rent_exemption(spl_mint_len)
|
||||
.unwrap();
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[
|
||||
system_instruction::create_account(
|
||||
&funder.pubkey(),
|
||||
&spl_mint_keypair.pubkey(),
|
||||
spl_mint_rent,
|
||||
spl_mint_len as u64,
|
||||
&inline_spl_token::id(),
|
||||
),
|
||||
spl_token_instruction(
|
||||
spl_token::instruction::initialize_mint(
|
||||
&spl_token::id(),
|
||||
&spl_token_pubkey(&spl_mint_keypair.pubkey()),
|
||||
&spl_token_pubkey(&spl_mint_keypair.pubkey()),
|
||||
None,
|
||||
2,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
],
|
||||
Some(&funder.pubkey()),
|
||||
&[&funder, &spl_mint_keypair],
|
||||
latest_blockhash,
|
||||
);
|
||||
let _sig = rpc_client
|
||||
.send_and_confirm_transaction(&transaction)
|
||||
.unwrap();
|
||||
|
||||
let account_len = Account::get_packed_len();
|
||||
let minimum_balance = rpc_client
|
||||
.get_minimum_balance_for_rent_exemption(account_len)
|
||||
.unwrap();
|
||||
|
||||
let iterations = 5;
|
||||
let batch_size = 100;
|
||||
let close_nth_batch = 0;
|
||||
let num_instructions = 4;
|
||||
let mut start = Measure::start("total accounts run");
|
||||
let keypair0 = Keypair::new();
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
run_accounts_bench(
|
||||
test_validator
|
||||
.rpc_url()
|
||||
.replace("http://", "")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
faucet_addr,
|
||||
&[&keypair0, &keypair1, &keypair2],
|
||||
iterations,
|
||||
Some(account_len as u64),
|
||||
batch_size,
|
||||
close_nth_batch,
|
||||
Some(minimum_balance),
|
||||
num_instructions,
|
||||
Some(spl_mint_keypair.pubkey()),
|
||||
true,
|
||||
);
|
||||
start.stop();
|
||||
info!("{}", start);
|
||||
|
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-accountsdb-plugin-interface"
|
||||
description = "The Solana AccountsDb plugin interface."
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -11,9 +11,7 @@ documentation = "https://docs.rs/solana-accountsdb-plugin-interface"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.11"
|
||||
thiserror = "1.0.30"
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.10.0" }
|
||||
thiserror = "1.0.29"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -3,8 +3,6 @@
|
||||
/// In addition, the dynamic library must export a "C" function _create_plugin which
|
||||
/// creates the implementation of the plugin.
|
||||
use {
|
||||
solana_sdk::{signature::Signature, transaction::SanitizedTransaction},
|
||||
solana_transaction_status::TransactionStatusMeta,
|
||||
std::{any::Any, error, io},
|
||||
thiserror::Error,
|
||||
};
|
||||
@@ -48,18 +46,6 @@ pub enum ReplicaAccountInfoVersions<'a> {
|
||||
V0_0_1(&'a ReplicaAccountInfo<'a>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReplicaTransactionInfo<'a> {
|
||||
pub signature: &'a Signature,
|
||||
pub is_vote: bool,
|
||||
pub transaction: &'a SanitizedTransaction,
|
||||
pub transaction_status_meta: &'a TransactionStatusMeta,
|
||||
}
|
||||
|
||||
pub enum ReplicaTransactionInfoVersions<'a> {
|
||||
V0_0_1(&'a ReplicaTransactionInfo<'a>),
|
||||
}
|
||||
|
||||
/// Errors returned by plugin calls
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AccountsDbPluginError {
|
||||
@@ -137,53 +123,21 @@ pub trait AccountsDbPlugin: Any + Send + Sync + std::fmt::Debug {
|
||||
/// When `is_startup` is true, it indicates the account is loaded from
|
||||
/// snapshots when the validator starts up. When `is_startup` is false,
|
||||
/// the account is updated during transaction processing.
|
||||
#[allow(unused_variables)]
|
||||
fn update_account(
|
||||
&mut self,
|
||||
account: ReplicaAccountInfoVersions,
|
||||
slot: u64,
|
||||
is_startup: bool,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
) -> Result<()>;
|
||||
|
||||
/// Called when all accounts are notified of during startup.
|
||||
fn notify_end_of_startup(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn notify_end_of_startup(&mut self) -> Result<()>;
|
||||
|
||||
/// Called when a slot status is updated
|
||||
#[allow(unused_variables)]
|
||||
fn update_slot_status(
|
||||
&mut self,
|
||||
slot: u64,
|
||||
parent: Option<u64>,
|
||||
status: SlotStatus,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called when a transaction is updated at a slot.
|
||||
#[allow(unused_variables)]
|
||||
fn notify_transaction(
|
||||
&mut self,
|
||||
transaction: ReplicaTransactionInfoVersions,
|
||||
slot: u64,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the plugin is interested in account data
|
||||
/// Default is true -- if the plugin is not interested in
|
||||
/// account data, please return false.
|
||||
fn account_data_notifications_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Check if the plugin is interested in transaction data
|
||||
/// Default is false -- if the plugin is not interested in
|
||||
/// transaction data, please return false.
|
||||
fn transaction_notifications_enabled(&self) -> bool {
|
||||
false
|
||||
}
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-accountsdb-plugin-manager"
|
||||
description = "The Solana AccountsDb plugin manager."
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -11,21 +11,20 @@ documentation = "https://docs.rs/solana-validator"
|
||||
|
||||
[dependencies]
|
||||
bs58 = "0.4.0"
|
||||
crossbeam-channel = "0.5"
|
||||
libloading = "0.7.2"
|
||||
crossbeam-channel = "0.4"
|
||||
libloading = "0.7.0"
|
||||
log = "0.4.11"
|
||||
serde = "1.0.131"
|
||||
serde = "1.0.130"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.72"
|
||||
solana-accountsdb-plugin-interface = { path = "../accountsdb-plugin-interface", version = "=1.10.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.10.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.10.0" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.10.0" }
|
||||
solana-rpc = { path = "../rpc", version = "=1.10.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.10.0" }
|
||||
thiserror = "1.0.30"
|
||||
serde_json = "1.0.67"
|
||||
solana-accountsdb-plugin-interface = { path = "../accountsdb-plugin-interface", version = "=1.8.14" }
|
||||
solana-logger = { path = "../logger", version = "=1.8.14" }
|
||||
solana-measure = { path = "../measure", version = "=1.8.14" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.8.14" }
|
||||
solana-rpc = { path = "../rpc", version = "=1.8.14" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
thiserror = "1.0.21"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -3,7 +3,7 @@ use {
|
||||
crate::accountsdb_plugin_manager::AccountsDbPluginManager,
|
||||
log::*,
|
||||
solana_accountsdb_plugin_interface::accountsdb_plugin_interface::{
|
||||
ReplicaAccountInfo, ReplicaAccountInfoVersions,
|
||||
ReplicaAccountInfo, ReplicaAccountInfoVersions, SlotStatus,
|
||||
},
|
||||
solana_measure::measure::Measure,
|
||||
solana_metrics::*,
|
||||
@@ -86,6 +86,18 @@ impl AccountsUpdateNotifierInterface for AccountsUpdateNotifierImpl {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_slot_confirmed(&self, slot: Slot, parent: Option<Slot>) {
|
||||
self.notify_slot_status(slot, parent, SlotStatus::Confirmed);
|
||||
}
|
||||
|
||||
fn notify_slot_processed(&self, slot: Slot, parent: Option<Slot>) {
|
||||
self.notify_slot_status(slot, parent, SlotStatus::Processed);
|
||||
}
|
||||
|
||||
fn notify_slot_rooted(&self, slot: Slot, parent: Option<Slot>) {
|
||||
self.notify_slot_status(slot, parent, SlotStatus::Rooted);
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountsUpdateNotifierImpl {
|
||||
@@ -177,4 +189,39 @@ impl AccountsUpdateNotifierImpl {
|
||||
100000
|
||||
);
|
||||
}
|
||||
|
||||
pub fn notify_slot_status(&self, slot: Slot, parent: Option<Slot>, slot_status: SlotStatus) {
|
||||
let mut plugin_manager = self.plugin_manager.write().unwrap();
|
||||
if plugin_manager.plugins.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for plugin in plugin_manager.plugins.iter_mut() {
|
||||
let mut measure = Measure::start("accountsdb-plugin-update-slot");
|
||||
match plugin.update_slot_status(slot, parent, slot_status.clone()) {
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to update slot status at slot {}, error: {} to plugin {}",
|
||||
slot,
|
||||
err,
|
||||
plugin.name()
|
||||
)
|
||||
}
|
||||
Ok(_) => {
|
||||
trace!(
|
||||
"Successfully updated slot status at slot {} to plugin {}",
|
||||
slot,
|
||||
plugin.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
measure.stop();
|
||||
inc_new_counter_debug!(
|
||||
"accountsdb-plugin-update-slot-us",
|
||||
measure.as_us() as usize,
|
||||
1000,
|
||||
1000
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -52,24 +52,4 @@ impl AccountsDbPluginManager {
|
||||
drop(lib);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there is any plugin interested in account data
|
||||
pub fn account_data_notifications_enabled(&self) -> bool {
|
||||
for plugin in &self.plugins {
|
||||
if plugin.account_data_notifications_enabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if there is any plugin interested in transaction data
|
||||
pub fn transaction_notifications_enabled(&self) -> bool {
|
||||
for plugin in &self.plugins {
|
||||
if plugin.transaction_notifications_enabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@@ -2,16 +2,12 @@ use {
|
||||
crate::{
|
||||
accounts_update_notifier::AccountsUpdateNotifierImpl,
|
||||
accountsdb_plugin_manager::AccountsDbPluginManager,
|
||||
slot_status_notifier::SlotStatusNotifierImpl, slot_status_observer::SlotStatusObserver,
|
||||
transaction_notifier::TransactionNotifierImpl,
|
||||
slot_status_observer::SlotStatusObserver,
|
||||
},
|
||||
crossbeam_channel::Receiver,
|
||||
log::*,
|
||||
serde_json,
|
||||
solana_rpc::{
|
||||
optimistically_confirmed_bank_tracker::BankNotification,
|
||||
transaction_notifier_interface::TransactionNotifierLock,
|
||||
},
|
||||
solana_rpc::optimistically_confirmed_bank_tracker::BankNotification,
|
||||
solana_runtime::accounts_update_notifier_interface::AccountsUpdateNotifier,
|
||||
std::{
|
||||
fs::File,
|
||||
@@ -46,10 +42,9 @@ pub enum AccountsdbPluginServiceError {
|
||||
|
||||
/// The service managing the AccountsDb plugin workflow.
|
||||
pub struct AccountsDbPluginService {
|
||||
slot_status_observer: Option<SlotStatusObserver>,
|
||||
slot_status_observer: SlotStatusObserver,
|
||||
plugin_manager: Arc<RwLock<AccountsDbPluginManager>>,
|
||||
accounts_update_notifier: Option<AccountsUpdateNotifier>,
|
||||
transaction_notifier: Option<TransactionNotifierLock>,
|
||||
accounts_update_notifier: AccountsUpdateNotifier,
|
||||
}
|
||||
|
||||
impl AccountsDbPluginService {
|
||||
@@ -79,47 +74,19 @@ impl AccountsDbPluginService {
|
||||
for accountsdb_plugin_config_file in accountsdb_plugin_config_files {
|
||||
Self::load_plugin(&mut plugin_manager, accountsdb_plugin_config_file)?;
|
||||
}
|
||||
let account_data_notifications_enabled =
|
||||
plugin_manager.account_data_notifications_enabled();
|
||||
let transaction_notifications_enabled = plugin_manager.transaction_notifications_enabled();
|
||||
|
||||
let plugin_manager = Arc::new(RwLock::new(plugin_manager));
|
||||
|
||||
let accounts_update_notifier: Option<AccountsUpdateNotifier> =
|
||||
if account_data_notifications_enabled {
|
||||
let accounts_update_notifier =
|
||||
AccountsUpdateNotifierImpl::new(plugin_manager.clone());
|
||||
Some(Arc::new(RwLock::new(accounts_update_notifier)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let transaction_notifier: Option<TransactionNotifierLock> =
|
||||
if transaction_notifications_enabled {
|
||||
let transaction_notifier = TransactionNotifierImpl::new(plugin_manager.clone());
|
||||
Some(Arc::new(RwLock::new(transaction_notifier)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let accounts_update_notifier = Arc::new(RwLock::new(AccountsUpdateNotifierImpl::new(
|
||||
plugin_manager.clone(),
|
||||
)));
|
||||
let slot_status_observer =
|
||||
if account_data_notifications_enabled || transaction_notifications_enabled {
|
||||
let slot_status_notifier = SlotStatusNotifierImpl::new(plugin_manager.clone());
|
||||
let slot_status_notifier = Arc::new(RwLock::new(slot_status_notifier));
|
||||
Some(SlotStatusObserver::new(
|
||||
confirmed_bank_receiver,
|
||||
slot_status_notifier,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
SlotStatusObserver::new(confirmed_bank_receiver, accounts_update_notifier.clone());
|
||||
|
||||
info!("Started AccountsDbPluginService");
|
||||
Ok(AccountsDbPluginService {
|
||||
slot_status_observer,
|
||||
plugin_manager,
|
||||
accounts_update_notifier,
|
||||
transaction_notifier,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,18 +145,12 @@ impl AccountsDbPluginService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_accounts_update_notifier(&self) -> Option<AccountsUpdateNotifier> {
|
||||
pub fn get_accounts_update_notifier(&self) -> AccountsUpdateNotifier {
|
||||
self.accounts_update_notifier.clone()
|
||||
}
|
||||
|
||||
pub fn get_transaction_notifier(&self) -> Option<TransactionNotifierLock> {
|
||||
self.transaction_notifier.clone()
|
||||
}
|
||||
|
||||
pub fn join(self) -> thread::Result<()> {
|
||||
if let Some(mut slot_status_observer) = self.slot_status_observer {
|
||||
slot_status_observer.join()?;
|
||||
}
|
||||
pub fn join(mut self) -> thread::Result<()> {
|
||||
self.slot_status_observer.join()?;
|
||||
self.plugin_manager.write().unwrap().unload();
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
pub mod accounts_update_notifier;
|
||||
pub mod accountsdb_plugin_manager;
|
||||
pub mod accountsdb_plugin_service;
|
||||
pub mod slot_status_notifier;
|
||||
pub mod slot_status_observer;
|
||||
pub mod transaction_notifier;
|
||||
|
@@ -1,81 +0,0 @@
|
||||
use {
|
||||
crate::accountsdb_plugin_manager::AccountsDbPluginManager,
|
||||
log::*,
|
||||
solana_accountsdb_plugin_interface::accountsdb_plugin_interface::SlotStatus,
|
||||
solana_measure::measure::Measure,
|
||||
solana_metrics::*,
|
||||
solana_sdk::clock::Slot,
|
||||
std::sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
pub trait SlotStatusNotifierInterface {
|
||||
/// Notified when a slot is optimistically confirmed
|
||||
fn notify_slot_confirmed(&self, slot: Slot, parent: Option<Slot>);
|
||||
|
||||
/// Notified when a slot is marked frozen.
|
||||
fn notify_slot_processed(&self, slot: Slot, parent: Option<Slot>);
|
||||
|
||||
/// Notified when a slot is rooted.
|
||||
fn notify_slot_rooted(&self, slot: Slot, parent: Option<Slot>);
|
||||
}
|
||||
|
||||
pub type SlotStatusNotifier = Arc<RwLock<dyn SlotStatusNotifierInterface + Sync + Send>>;
|
||||
|
||||
pub struct SlotStatusNotifierImpl {
|
||||
plugin_manager: Arc<RwLock<AccountsDbPluginManager>>,
|
||||
}
|
||||
|
||||
impl SlotStatusNotifierInterface for SlotStatusNotifierImpl {
|
||||
fn notify_slot_confirmed(&self, slot: Slot, parent: Option<Slot>) {
|
||||
self.notify_slot_status(slot, parent, SlotStatus::Confirmed);
|
||||
}
|
||||
|
||||
fn notify_slot_processed(&self, slot: Slot, parent: Option<Slot>) {
|
||||
self.notify_slot_status(slot, parent, SlotStatus::Processed);
|
||||
}
|
||||
|
||||
fn notify_slot_rooted(&self, slot: Slot, parent: Option<Slot>) {
|
||||
self.notify_slot_status(slot, parent, SlotStatus::Rooted);
|
||||
}
|
||||
}
|
||||
|
||||
impl SlotStatusNotifierImpl {
|
||||
pub fn new(plugin_manager: Arc<RwLock<AccountsDbPluginManager>>) -> Self {
|
||||
Self { plugin_manager }
|
||||
}
|
||||
|
||||
pub fn notify_slot_status(&self, slot: Slot, parent: Option<Slot>, slot_status: SlotStatus) {
|
||||
let mut plugin_manager = self.plugin_manager.write().unwrap();
|
||||
if plugin_manager.plugins.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for plugin in plugin_manager.plugins.iter_mut() {
|
||||
let mut measure = Measure::start("accountsdb-plugin-update-slot");
|
||||
match plugin.update_slot_status(slot, parent, slot_status.clone()) {
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to update slot status at slot {}, error: {} to plugin {}",
|
||||
slot,
|
||||
err,
|
||||
plugin.name()
|
||||
)
|
||||
}
|
||||
Ok(_) => {
|
||||
trace!(
|
||||
"Successfully updated slot status at slot {} to plugin {}",
|
||||
slot,
|
||||
plugin.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
measure.stop();
|
||||
inc_new_counter_debug!(
|
||||
"accountsdb-plugin-update-slot-us",
|
||||
measure.as_us() as usize,
|
||||
1000,
|
||||
1000
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
use {
|
||||
crate::slot_status_notifier::SlotStatusNotifier,
|
||||
crossbeam_channel::Receiver,
|
||||
solana_rpc::optimistically_confirmed_bank_tracker::BankNotification,
|
||||
solana_runtime::accounts_update_notifier_interface::AccountsUpdateNotifier,
|
||||
std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -20,7 +20,7 @@ pub(crate) struct SlotStatusObserver {
|
||||
impl SlotStatusObserver {
|
||||
pub fn new(
|
||||
bank_notification_receiver: Receiver<BankNotification>,
|
||||
slot_status_notifier: SlotStatusNotifier,
|
||||
accounts_update_notifier: AccountsUpdateNotifier,
|
||||
) -> Self {
|
||||
let exit_updated_slot_server = Arc::new(AtomicBool::new(false));
|
||||
|
||||
@@ -28,7 +28,7 @@ impl SlotStatusObserver {
|
||||
bank_notification_receiver_service: Some(Self::run_bank_notification_receiver(
|
||||
bank_notification_receiver,
|
||||
exit_updated_slot_server.clone(),
|
||||
slot_status_notifier,
|
||||
accounts_update_notifier,
|
||||
)),
|
||||
exit_updated_slot_server,
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl SlotStatusObserver {
|
||||
fn run_bank_notification_receiver(
|
||||
bank_notification_receiver: Receiver<BankNotification>,
|
||||
exit: Arc<AtomicBool>,
|
||||
slot_status_notifier: SlotStatusNotifier,
|
||||
accounts_update_notifier: AccountsUpdateNotifier,
|
||||
) -> JoinHandle<()> {
|
||||
Builder::new()
|
||||
.name("bank_notification_receiver".to_string())
|
||||
@@ -54,19 +54,19 @@ impl SlotStatusObserver {
|
||||
if let Ok(slot) = bank_notification_receiver.recv() {
|
||||
match slot {
|
||||
BankNotification::OptimisticallyConfirmed(slot) => {
|
||||
slot_status_notifier
|
||||
accounts_update_notifier
|
||||
.read()
|
||||
.unwrap()
|
||||
.notify_slot_confirmed(slot, None);
|
||||
}
|
||||
BankNotification::Frozen(bank) => {
|
||||
slot_status_notifier
|
||||
accounts_update_notifier
|
||||
.read()
|
||||
.unwrap()
|
||||
.notify_slot_processed(bank.slot(), Some(bank.parent_slot()));
|
||||
}
|
||||
BankNotification::Root(bank) => {
|
||||
slot_status_notifier
|
||||
accounts_update_notifier
|
||||
.read()
|
||||
.unwrap()
|
||||
.notify_slot_rooted(bank.slot(), Some(bank.parent_slot()));
|
||||
|
@@ -1,93 +0,0 @@
|
||||
/// Module responsible for notifying plugins of transactions
|
||||
use {
|
||||
crate::accountsdb_plugin_manager::AccountsDbPluginManager,
|
||||
log::*,
|
||||
solana_accountsdb_plugin_interface::accountsdb_plugin_interface::{
|
||||
ReplicaTransactionInfo, ReplicaTransactionInfoVersions,
|
||||
},
|
||||
solana_measure::measure::Measure,
|
||||
solana_metrics::*,
|
||||
solana_rpc::transaction_notifier_interface::TransactionNotifier,
|
||||
solana_runtime::bank,
|
||||
solana_sdk::{clock::Slot, signature::Signature, transaction::SanitizedTransaction},
|
||||
solana_transaction_status::TransactionStatusMeta,
|
||||
std::sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
/// This implementation of TransactionNotifier is passed to the rpc's TransactionStatusService
|
||||
/// at the validator startup. TransactionStatusService invokes the notify_transaction method
|
||||
/// for new transactions. The implementation in turn invokes the notify_transaction of each
|
||||
/// plugin enabled with transaction notification managed by the AccountsDbPluginManager.
|
||||
pub(crate) struct TransactionNotifierImpl {
|
||||
plugin_manager: Arc<RwLock<AccountsDbPluginManager>>,
|
||||
}
|
||||
|
||||
impl TransactionNotifier for TransactionNotifierImpl {
|
||||
fn notify_transaction(
|
||||
&self,
|
||||
slot: Slot,
|
||||
signature: &Signature,
|
||||
transaction_status_meta: &TransactionStatusMeta,
|
||||
transaction: &SanitizedTransaction,
|
||||
) {
|
||||
let mut measure = Measure::start("accountsdb-plugin-notify_plugins_of_transaction_info");
|
||||
let transaction_log_info =
|
||||
Self::build_replica_transaction_info(signature, transaction_status_meta, transaction);
|
||||
|
||||
let mut plugin_manager = self.plugin_manager.write().unwrap();
|
||||
|
||||
if plugin_manager.plugins.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for plugin in plugin_manager.plugins.iter_mut() {
|
||||
if !plugin.transaction_notifications_enabled() {
|
||||
continue;
|
||||
}
|
||||
match plugin.notify_transaction(
|
||||
ReplicaTransactionInfoVersions::V0_0_1(&transaction_log_info),
|
||||
slot,
|
||||
) {
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to notify transaction, error: ({}) to plugin {}",
|
||||
err,
|
||||
plugin.name()
|
||||
)
|
||||
}
|
||||
Ok(_) => {
|
||||
trace!(
|
||||
"Successfully notified transaction to plugin {}",
|
||||
plugin.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
measure.stop();
|
||||
inc_new_counter_debug!(
|
||||
"accountsdb-plugin-notify_plugins_of_transaction_info-us",
|
||||
measure.as_us() as usize,
|
||||
10000,
|
||||
10000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl TransactionNotifierImpl {
|
||||
pub fn new(plugin_manager: Arc<RwLock<AccountsDbPluginManager>>) -> Self {
|
||||
Self { plugin_manager }
|
||||
}
|
||||
|
||||
fn build_replica_transaction_info<'a>(
|
||||
signature: &'a Signature,
|
||||
transaction_status_meta: &'a TransactionStatusMeta,
|
||||
transaction: &'a SanitizedTransaction,
|
||||
) -> ReplicaTransactionInfo<'a> {
|
||||
ReplicaTransactionInfo {
|
||||
signature,
|
||||
is_vote: bank::is_simple_vote_transaction(transaction),
|
||||
transaction,
|
||||
transaction_status_meta,
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-accountsdb-plugin-postgres"
|
||||
description = "The Solana AccountsDb plugin for PostgreSQL database."
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -17,23 +17,17 @@ bs58 = "0.4.0"
|
||||
chrono = { version = "0.4.11", features = ["serde"] }
|
||||
crossbeam-channel = "0.5"
|
||||
log = "0.4.14"
|
||||
postgres = { version = "0.19.2", features = ["with-chrono-0_4"] }
|
||||
postgres-types = { version = "0.2.2", features = ["derive"] }
|
||||
serde = "1.0.131"
|
||||
postgres = { version = "0.19.1", features = ["with-chrono-0_4"] }
|
||||
serde = "1.0.130"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.72"
|
||||
solana-accountsdb-plugin-interface = { path = "../accountsdb-plugin-interface", version = "=1.10.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.10.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.10.0" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.10.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.10.0" }
|
||||
thiserror = "1.0.30"
|
||||
tokio-postgres = "0.7.4"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-account-decoder = { path = "../account-decoder", version = "=1.10.0" }
|
||||
serde_json = "1.0.67"
|
||||
solana-accountsdb-plugin-interface = { path = "../accountsdb-plugin-interface", version = "=1.8.14" }
|
||||
solana-logger = { path = "../logger", version = "=1.8.14" }
|
||||
solana-measure = { path = "../measure", version = "=1.8.14" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
thiserror = "1.0.21"
|
||||
tokio-postgres = "0.7.3"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -20,138 +20,10 @@ CREATE TABLE account (
|
||||
CREATE TABLE slot (
|
||||
slot BIGINT PRIMARY KEY,
|
||||
parent BIGINT,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
status varchar(16) NOT NULL,
|
||||
updated_on TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
-- Types for Transactions
|
||||
|
||||
Create TYPE "TransactionErrorCode" AS ENUM (
|
||||
'AccountInUse',
|
||||
'AccountLoadedTwice',
|
||||
'AccountNotFound',
|
||||
'ProgramAccountNotFound',
|
||||
'InsufficientFundsForFee',
|
||||
'InvalidAccountForFee',
|
||||
'AlreadyProcessed',
|
||||
'BlockhashNotFound',
|
||||
'InstructionError',
|
||||
'CallChainTooDeep',
|
||||
'MissingSignatureForFee',
|
||||
'InvalidAccountIndex',
|
||||
'SignatureFailure',
|
||||
'InvalidProgramForExecution',
|
||||
'SanitizeFailure',
|
||||
'ClusterMaintenance',
|
||||
'AccountBorrowOutstanding',
|
||||
'WouldExceedMaxAccountCostLimit',
|
||||
'WouldExceedMaxBlockCostLimit',
|
||||
'UnsupportedVersion',
|
||||
'InvalidWritableAccount'
|
||||
);
|
||||
|
||||
CREATE TYPE "TransactionError" AS (
|
||||
error_code "TransactionErrorCode",
|
||||
error_detail VARCHAR(256)
|
||||
);
|
||||
|
||||
CREATE TYPE "CompiledInstruction" AS (
|
||||
program_id_index SMALLINT,
|
||||
accounts SMALLINT[],
|
||||
data BYTEA
|
||||
);
|
||||
|
||||
CREATE TYPE "InnerInstructions" AS (
|
||||
index SMALLINT,
|
||||
instructions "CompiledInstruction"[]
|
||||
);
|
||||
|
||||
CREATE TYPE "TransactionTokenBalance" AS (
|
||||
account_index SMALLINT,
|
||||
mint VARCHAR(44),
|
||||
ui_token_amount DOUBLE PRECISION,
|
||||
owner VARCHAR(44)
|
||||
);
|
||||
|
||||
Create TYPE "RewardType" AS ENUM (
|
||||
'Fee',
|
||||
'Rent',
|
||||
'Staking',
|
||||
'Voting'
|
||||
);
|
||||
|
||||
CREATE TYPE "Reward" AS (
|
||||
pubkey VARCHAR(44),
|
||||
lamports BIGINT,
|
||||
post_balance BIGINT,
|
||||
reward_type "RewardType",
|
||||
commission SMALLINT
|
||||
);
|
||||
|
||||
CREATE TYPE "TransactionStatusMeta" AS (
|
||||
error "TransactionError",
|
||||
fee BIGINT,
|
||||
pre_balances BIGINT[],
|
||||
post_balances BIGINT[],
|
||||
inner_instructions "InnerInstructions"[],
|
||||
log_messages TEXT[],
|
||||
pre_token_balances "TransactionTokenBalance"[],
|
||||
post_token_balances "TransactionTokenBalance"[],
|
||||
rewards "Reward"[]
|
||||
);
|
||||
|
||||
CREATE TYPE "TransactionMessageHeader" AS (
|
||||
num_required_signatures SMALLINT,
|
||||
num_readonly_signed_accounts SMALLINT,
|
||||
num_readonly_unsigned_accounts SMALLINT
|
||||
);
|
||||
|
||||
CREATE TYPE "TransactionMessage" AS (
|
||||
header "TransactionMessageHeader",
|
||||
account_keys BYTEA[],
|
||||
recent_blockhash BYTEA,
|
||||
instructions "CompiledInstruction"[]
|
||||
);
|
||||
|
||||
CREATE TYPE "TransactionMessageAddressTableLookup" AS (
|
||||
account_key: BYTEA[],
|
||||
writable_indexes SMALLINT[],
|
||||
readonly_indexes SMALLINT[]
|
||||
);
|
||||
|
||||
CREATE TYPE "TransactionMessageV0" AS (
|
||||
header "TransactionMessageHeader",
|
||||
account_keys BYTEA[],
|
||||
recent_blockhash BYTEA,
|
||||
instructions "CompiledInstruction"[],
|
||||
address_table_lookups "TransactionMessageAddressTableLookup"[]
|
||||
);
|
||||
|
||||
CREATE TYPE "LoadedAddresses" AS (
|
||||
writable BYTEA[],
|
||||
readonly BYTEA[]
|
||||
);
|
||||
|
||||
CREATE TYPE "LoadedMessageV0" AS (
|
||||
message "TransactionMessageV0",
|
||||
loaded_addresses "LoadedAddresses"
|
||||
);
|
||||
|
||||
-- The table storing transactions
|
||||
CREATE TABLE transaction (
|
||||
slot BIGINT NOT NULL,
|
||||
signature BYTEA NOT NULL,
|
||||
is_vote BOOL NOT NULL,
|
||||
message_type SMALLINT, -- 0: legacy, 1: v0 message
|
||||
legacy_message "TransactionMessage",
|
||||
v0_loaded_message "LoadedMessageV0",
|
||||
signatures BYTEA[],
|
||||
message_hash BYTEA,
|
||||
meta "TransactionStatusMeta",
|
||||
updated_on TIMESTAMP NOT NULL,
|
||||
CONSTRAINT transaction_pk PRIMARY KEY (slot, signature)
|
||||
);
|
||||
|
||||
/**
|
||||
* The following is for keeping historical data for accounts and is not required for plugin to work.
|
||||
*/
|
||||
@@ -168,8 +40,6 @@ CREATE TABLE account_audit (
|
||||
updated_on TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX account_audit_account_key ON account_audit (pubkey, write_version);
|
||||
|
||||
CREATE FUNCTION audit_account_update() RETURNS trigger AS $audit_account_update$
|
||||
BEGIN
|
||||
INSERT INTO account_audit (pubkey, owner, lamports, slot, executable, rent_epoch, data, write_version, updated_on)
|
||||
|
@@ -7,19 +7,3 @@ DROP FUNCTION audit_account_update;
|
||||
DROP TABLE account_audit;
|
||||
DROP TABLE account;
|
||||
DROP TABLE slot;
|
||||
DROP TABLE transaction;
|
||||
|
||||
DROP TYPE "TransactionError" CASCADE;
|
||||
DROP TYPE "TransactionErrorCode" CASCADE;
|
||||
DROP TYPE "LoadedMessageV0" CASCADE;
|
||||
DROP TYPE "LoadedAddresses" CASCADE;
|
||||
DROP TYPE "TransactionMessageV0" CASCADE;
|
||||
DROP TYPE "TransactionMessage" CASCADE;
|
||||
DROP TYPE "TransactionMessageHeader" CASCADE;
|
||||
DROP TYPE "TransactionMessageAddressTableLookup" CASCADE;
|
||||
DROP TYPE "TransactionStatusMeta" CASCADE;
|
||||
DROP TYPE "RewardType" CASCADE;
|
||||
DROP TYPE "Reward" CASCADE;
|
||||
DROP TYPE "TransactionTokenBalance" CASCADE;
|
||||
DROP TYPE "InnerInstructions" CASCADE;
|
||||
DROP TYPE "CompiledInstruction" CASCADE;
|
||||
|
@@ -48,11 +48,6 @@ impl AccountsSelector {
|
||||
pub fn is_account_selected(&self, account: &[u8], owner: &[u8]) -> bool {
|
||||
self.select_all_accounts || self.accounts.contains(account) || self.owners.contains(owner)
|
||||
}
|
||||
|
||||
/// Check if any account is of interested at all
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.select_all_accounts || !self.accounts.is_empty() || !self.owners.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@@ -4,15 +4,13 @@ use {
|
||||
crate::{
|
||||
accounts_selector::AccountsSelector,
|
||||
postgres_client::{ParallelPostgresClient, PostgresClientBuilder},
|
||||
transaction_selector::TransactionSelector,
|
||||
},
|
||||
bs58,
|
||||
log::*,
|
||||
serde_derive::{Deserialize, Serialize},
|
||||
serde_json,
|
||||
solana_accountsdb_plugin_interface::accountsdb_plugin_interface::{
|
||||
AccountsDbPlugin, AccountsDbPluginError, ReplicaAccountInfoVersions,
|
||||
ReplicaTransactionInfoVersions, Result, SlotStatus,
|
||||
AccountsDbPlugin, AccountsDbPluginError, ReplicaAccountInfoVersions, Result, SlotStatus,
|
||||
},
|
||||
solana_metrics::*,
|
||||
std::{fs::File, io::Read},
|
||||
@@ -23,7 +21,6 @@ use {
|
||||
pub struct AccountsDbPluginPostgres {
|
||||
client: Option<ParallelPostgresClient>,
|
||||
accounts_selector: Option<AccountsSelector>,
|
||||
transaction_selector: Option<TransactionSelector>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AccountsDbPluginPostgres {
|
||||
@@ -91,20 +88,6 @@ impl AccountsDbPlugin for AccountsDbPluginPostgres {
|
||||
/// from restoring a snapshot. The default is '10'.
|
||||
/// * "panic_on_db_errors", optional, contols if to panic when there are errors replicating data to the
|
||||
/// PostgreSQL database. The default is 'false'.
|
||||
/// * "transaction_selector", optional, controls if and what transaction to store. If this field is missing
|
||||
/// None of the transction is stored.
|
||||
/// "transaction_selector" : {
|
||||
/// "mentions" : \["pubkey-1", "pubkey-2", ..., "pubkey-n"\],
|
||||
/// }
|
||||
/// The `mentions` field support wildcard to select all transaction or all 'vote' transactions:
|
||||
/// For example, to select all transactions:
|
||||
/// "transaction_selector" : {
|
||||
/// "mentions" : \["*"\],
|
||||
/// }
|
||||
/// To select all vote transactions:
|
||||
/// "transaction_selector" : {
|
||||
/// "mentions" : \["all_votes"\],
|
||||
/// }
|
||||
/// # Examples
|
||||
///
|
||||
/// {
|
||||
@@ -130,7 +113,6 @@ impl AccountsDbPlugin for AccountsDbPluginPostgres {
|
||||
|
||||
let result: serde_json::Value = serde_json::from_str(&contents).unwrap();
|
||||
self.accounts_selector = Some(Self::create_accounts_selector_from_config(&result));
|
||||
self.transaction_selector = Some(Self::create_transaction_selector_from_config(&result));
|
||||
|
||||
let result: serde_json::Result<AccountsDbPluginPostgresConfig> =
|
||||
serde_json::from_str(&contents);
|
||||
@@ -293,62 +275,6 @@ impl AccountsDbPlugin for AccountsDbPluginPostgres {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_transaction(
|
||||
&mut self,
|
||||
transaction_info: ReplicaTransactionInfoVersions,
|
||||
slot: u64,
|
||||
) -> Result<()> {
|
||||
match &mut self.client {
|
||||
None => {
|
||||
return Err(AccountsDbPluginError::Custom(Box::new(
|
||||
AccountsDbPluginPostgresError::DataStoreConnectionError {
|
||||
msg: "There is no connection to the PostgreSQL database.".to_string(),
|
||||
},
|
||||
)));
|
||||
}
|
||||
Some(client) => match transaction_info {
|
||||
ReplicaTransactionInfoVersions::V0_0_1(transaction_info) => {
|
||||
if let Some(transaction_selector) = &self.transaction_selector {
|
||||
if !transaction_selector.is_transaction_selected(
|
||||
transaction_info.is_vote,
|
||||
transaction_info.transaction.message().account_keys_iter(),
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let result = client.log_transaction_info(transaction_info, slot);
|
||||
|
||||
if let Err(err) = result {
|
||||
return Err(AccountsDbPluginError::SlotStatusUpdateError{
|
||||
msg: format!("Failed to persist the transaction info to the PostgreSQL database. Error: {:?}", err)
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the plugin is interested in account data
|
||||
/// Default is true -- if the plugin is not interested in
|
||||
/// account data, please return false.
|
||||
fn account_data_notifications_enabled(&self) -> bool {
|
||||
self.accounts_selector
|
||||
.as_ref()
|
||||
.map_or_else(|| false, |selector| selector.is_enabled())
|
||||
}
|
||||
|
||||
/// Check if the plugin is interested in transaction data
|
||||
fn transaction_notifications_enabled(&self) -> bool {
|
||||
self.transaction_selector
|
||||
.as_ref()
|
||||
.map_or_else(|| false, |selector| selector.is_enabled())
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountsDbPluginPostgres {
|
||||
@@ -384,29 +310,11 @@ impl AccountsDbPluginPostgres {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_transaction_selector_from_config(config: &serde_json::Value) -> TransactionSelector {
|
||||
let transaction_selector = &config["transaction_selector"];
|
||||
|
||||
if transaction_selector.is_null() {
|
||||
TransactionSelector::default()
|
||||
} else {
|
||||
let accounts = &transaction_selector["mentions"];
|
||||
let accounts: Vec<String> = if accounts.is_array() {
|
||||
accounts
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|val| val.as_str().unwrap().to_string())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::default()
|
||||
};
|
||||
TransactionSelector::new(&accounts)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
AccountsDbPluginPostgres {
|
||||
client: None,
|
||||
accounts_selector: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
pub mod accounts_selector;
|
||||
pub mod accountsdb_plugin_postgres;
|
||||
pub mod postgres_client;
|
||||
pub mod transaction_selector;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
mod postgres_client_transaction;
|
||||
|
||||
/// A concurrent implementation for writing accounts into the PostgreSQL in parallel.
|
||||
use {
|
||||
@@ -10,7 +9,6 @@ use {
|
||||
crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender},
|
||||
log::*,
|
||||
postgres::{Client, NoTls, Statement},
|
||||
postgres_client_transaction::LogTransactionRequest,
|
||||
solana_accountsdb_plugin_interface::accountsdb_plugin_interface::{
|
||||
AccountsDbPluginError, ReplicaAccountInfo, SlotStatus,
|
||||
},
|
||||
@@ -25,7 +23,7 @@ use {
|
||||
thread::{self, sleep, Builder, JoinHandle},
|
||||
time::Duration,
|
||||
},
|
||||
tokio_postgres::types,
|
||||
tokio_postgres::types::ToSql,
|
||||
};
|
||||
|
||||
/// The maximum asynchronous requests allowed in the channel to avoid excessive
|
||||
@@ -43,7 +41,6 @@ struct PostgresSqlClientWrapper {
|
||||
bulk_account_insert_stmt: Statement,
|
||||
update_slot_with_parent_stmt: Statement,
|
||||
update_slot_without_parent_stmt: Statement,
|
||||
update_transaction_log_stmt: Statement,
|
||||
}
|
||||
|
||||
pub struct SimplePostgresClient {
|
||||
@@ -190,11 +187,6 @@ pub trait PostgresClient {
|
||||
) -> Result<(), AccountsDbPluginError>;
|
||||
|
||||
fn notify_end_of_startup(&mut self) -> Result<(), AccountsDbPluginError>;
|
||||
|
||||
fn log_transaction(
|
||||
&mut self,
|
||||
transaction_log_info: LogTransactionRequest,
|
||||
) -> Result<(), AccountsDbPluginError>;
|
||||
}
|
||||
|
||||
impl SimplePostgresClient {
|
||||
@@ -415,7 +407,7 @@ impl SimplePostgresClient {
|
||||
if self.pending_account_updates.len() == self.batch_size {
|
||||
let mut measure = Measure::start("accountsdb-plugin-postgres-prepare-values");
|
||||
|
||||
let mut values: Vec<&(dyn types::ToSql + Sync)> =
|
||||
let mut values: Vec<&(dyn ToSql + Sync)> =
|
||||
Vec::with_capacity(self.batch_size * ACCOUNT_COLUMN_COUNT);
|
||||
let updated_on = Utc::now().naive_utc();
|
||||
for j in 0..self.batch_size {
|
||||
@@ -499,8 +491,6 @@ impl SimplePostgresClient {
|
||||
Self::build_slot_upsert_statement_with_parent(&mut client, config)?;
|
||||
let update_slot_without_parent_stmt =
|
||||
Self::build_slot_upsert_statement_without_parent(&mut client, config)?;
|
||||
let update_transaction_log_stmt =
|
||||
Self::build_transaction_info_upsert_statement(&mut client, config)?;
|
||||
|
||||
let batch_size = config
|
||||
.batch_size
|
||||
@@ -515,7 +505,6 @@ impl SimplePostgresClient {
|
||||
bulk_account_insert_stmt,
|
||||
update_slot_with_parent_stmt,
|
||||
update_slot_without_parent_stmt,
|
||||
update_transaction_log_stmt,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -584,13 +573,6 @@ impl PostgresClient for SimplePostgresClient {
|
||||
fn notify_end_of_startup(&mut self) -> Result<(), AccountsDbPluginError> {
|
||||
self.flush_buffered_writes()
|
||||
}
|
||||
|
||||
fn log_transaction(
|
||||
&mut self,
|
||||
transaction_log_info: LogTransactionRequest,
|
||||
) -> Result<(), AccountsDbPluginError> {
|
||||
self.log_transaction_impl(transaction_log_info)
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateAccountRequest {
|
||||
@@ -604,11 +586,9 @@ struct UpdateSlotRequest {
|
||||
slot_status: SlotStatus,
|
||||
}
|
||||
|
||||
#[warn(clippy::large_enum_variant)]
|
||||
enum DbWorkItem {
|
||||
UpdateAccount(Box<UpdateAccountRequest>),
|
||||
UpdateSlot(Box<UpdateSlotRequest>),
|
||||
LogTransaction(Box<LogTransactionRequest>),
|
||||
UpdateAccount(UpdateAccountRequest),
|
||||
UpdateSlot(UpdateSlotRequest),
|
||||
}
|
||||
|
||||
impl PostgresClientWorker {
|
||||
@@ -669,14 +649,6 @@ impl PostgresClientWorker {
|
||||
}
|
||||
}
|
||||
}
|
||||
DbWorkItem::LogTransaction(transaction_log_info) => {
|
||||
if let Err(err) = self.client.log_transaction(*transaction_log_info) {
|
||||
error!("Failed to update transaction: ({})", err);
|
||||
if panic_on_db_errors {
|
||||
abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => match err {
|
||||
RecvTimeoutError::Timeout => {
|
||||
@@ -810,10 +782,10 @@ impl ParallelPostgresClient {
|
||||
);
|
||||
}
|
||||
let mut measure = Measure::start("accountsdb-plugin-posgres-create-work-item");
|
||||
let wrk_item = DbWorkItem::UpdateAccount(Box::new(UpdateAccountRequest {
|
||||
let wrk_item = DbWorkItem::UpdateAccount(UpdateAccountRequest {
|
||||
account: DbAccountInfo::new(account, slot),
|
||||
is_startup,
|
||||
}));
|
||||
});
|
||||
|
||||
measure.stop();
|
||||
|
||||
@@ -853,14 +825,11 @@ impl ParallelPostgresClient {
|
||||
parent: Option<u64>,
|
||||
status: SlotStatus,
|
||||
) -> Result<(), AccountsDbPluginError> {
|
||||
if let Err(err) = self
|
||||
.sender
|
||||
.send(DbWorkItem::UpdateSlot(Box::new(UpdateSlotRequest {
|
||||
slot,
|
||||
parent,
|
||||
slot_status: status,
|
||||
})))
|
||||
{
|
||||
if let Err(err) = self.sender.send(DbWorkItem::UpdateSlot(UpdateSlotRequest {
|
||||
slot,
|
||||
parent,
|
||||
slot_status: status,
|
||||
})) {
|
||||
return Err(AccountsDbPluginError::SlotStatusUpdateError {
|
||||
msg: format!("Failed to update the slot {:?}, error: {:?}", slot, err),
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,194 +0,0 @@
|
||||
/// The transaction selector is responsible for filtering transactions
|
||||
/// in the plugin framework.
|
||||
use {log::*, solana_sdk::pubkey::Pubkey, std::collections::HashSet};
|
||||
|
||||
pub(crate) struct TransactionSelector {
|
||||
pub mentioned_addresses: HashSet<Vec<u8>>,
|
||||
pub select_all_transactions: bool,
|
||||
pub select_all_vote_transactions: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TransactionSelector {
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
mentioned_addresses: HashSet::default(),
|
||||
select_all_transactions: false,
|
||||
select_all_vote_transactions: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a selector based on the mentioned addresses
|
||||
/// To select all transactions use ["*"] or ["all"]
|
||||
/// To select all vote transactions, use ["all_votes"]
|
||||
/// To select transactions mentioning specific addresses use ["<pubkey1>", "<pubkey2>", ...]
|
||||
pub fn new(mentioned_addresses: &[String]) -> Self {
|
||||
info!(
|
||||
"Creating TransactionSelector from addresses: {:?}",
|
||||
mentioned_addresses
|
||||
);
|
||||
|
||||
let select_all_transactions = mentioned_addresses
|
||||
.iter()
|
||||
.any(|key| key == "*" || key == "all");
|
||||
if select_all_transactions {
|
||||
return Self {
|
||||
mentioned_addresses: HashSet::default(),
|
||||
select_all_transactions,
|
||||
select_all_vote_transactions: true,
|
||||
};
|
||||
}
|
||||
let select_all_vote_transactions = mentioned_addresses.iter().any(|key| key == "all_votes");
|
||||
if select_all_vote_transactions {
|
||||
return Self {
|
||||
mentioned_addresses: HashSet::default(),
|
||||
select_all_transactions,
|
||||
select_all_vote_transactions: true,
|
||||
};
|
||||
}
|
||||
|
||||
let mentioned_addresses = mentioned_addresses
|
||||
.iter()
|
||||
.map(|key| bs58::decode(key).into_vec().unwrap())
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
mentioned_addresses,
|
||||
select_all_transactions: false,
|
||||
select_all_vote_transactions: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a transaction is of interest.
|
||||
pub fn is_transaction_selected(
|
||||
&self,
|
||||
is_vote: bool,
|
||||
mentioned_addresses: Box<dyn Iterator<Item = &Pubkey> + '_>,
|
||||
) -> bool {
|
||||
if !self.is_enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.select_all_transactions || (self.select_all_vote_transactions && is_vote) {
|
||||
return true;
|
||||
}
|
||||
for address in mentioned_addresses {
|
||||
if self.mentioned_addresses.contains(address.as_ref()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if any transaction is of interest at all
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.select_all_transactions
|
||||
|| self.select_all_vote_transactions
|
||||
|| !self.mentioned_addresses.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_select_transaction() {
|
||||
let pubkey1 = Pubkey::new_unique();
|
||||
let pubkey2 = Pubkey::new_unique();
|
||||
|
||||
let selector = TransactionSelector::new(&[pubkey1.to_string()]);
|
||||
|
||||
assert!(selector.is_enabled());
|
||||
|
||||
let addresses = [pubkey1];
|
||||
|
||||
assert!(selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey2];
|
||||
assert!(!selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey1, pubkey2];
|
||||
assert!(selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_all_transaction_using_wildcard() {
|
||||
let pubkey1 = Pubkey::new_unique();
|
||||
let pubkey2 = Pubkey::new_unique();
|
||||
|
||||
let selector = TransactionSelector::new(&["*".to_string()]);
|
||||
|
||||
assert!(selector.is_enabled());
|
||||
|
||||
let addresses = [pubkey1];
|
||||
|
||||
assert!(selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey2];
|
||||
assert!(selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey1, pubkey2];
|
||||
assert!(selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_all_transaction_all() {
|
||||
let pubkey1 = Pubkey::new_unique();
|
||||
let pubkey2 = Pubkey::new_unique();
|
||||
|
||||
let selector = TransactionSelector::new(&["all".to_string()]);
|
||||
|
||||
assert!(selector.is_enabled());
|
||||
|
||||
let addresses = [pubkey1];
|
||||
|
||||
assert!(selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey2];
|
||||
assert!(selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey1, pubkey2];
|
||||
assert!(selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_all_vote_transaction() {
|
||||
let pubkey1 = Pubkey::new_unique();
|
||||
let pubkey2 = Pubkey::new_unique();
|
||||
|
||||
let selector = TransactionSelector::new(&["all_votes".to_string()]);
|
||||
|
||||
assert!(selector.is_enabled());
|
||||
|
||||
let addresses = [pubkey1];
|
||||
|
||||
assert!(!selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey2];
|
||||
assert!(selector.is_transaction_selected(true, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey1, pubkey2];
|
||||
assert!(selector.is_transaction_selected(true, Box::new(addresses.iter())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_no_transaction() {
|
||||
let pubkey1 = Pubkey::new_unique();
|
||||
let pubkey2 = Pubkey::new_unique();
|
||||
|
||||
let selector = TransactionSelector::new(&[]);
|
||||
|
||||
assert!(!selector.is_enabled());
|
||||
|
||||
let addresses = [pubkey1];
|
||||
|
||||
assert!(!selector.is_transaction_selected(false, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey2];
|
||||
assert!(!selector.is_transaction_selected(true, Box::new(addresses.iter())));
|
||||
|
||||
let addresses = [pubkey1, pubkey2];
|
||||
assert!(!selector.is_transaction_selected(true, Box::new(addresses.iter())));
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-banking-bench"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -10,21 +10,22 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.1"
|
||||
crossbeam-channel = "0.5"
|
||||
log = "0.4.14"
|
||||
crossbeam-channel = "0.4"
|
||||
log = "0.4.11"
|
||||
rand = "0.7.0"
|
||||
rayon = "1.5.1"
|
||||
solana-core = { path = "../core", version = "=1.10.0" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.10.0" }
|
||||
solana-ledger = { path = "../ledger", version = "=1.10.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.10.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.10.0" }
|
||||
solana-perf = { path = "../perf", version = "=1.10.0" }
|
||||
solana-poh = { path = "../poh", version = "=1.10.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.10.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-version = { path = "../version", version = "=1.10.0" }
|
||||
rayon = "1.5.0"
|
||||
solana-core = { path = "../core", version = "=1.8.14" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.14" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.8.14" }
|
||||
solana-ledger = { path = "../ledger", version = "=1.8.14" }
|
||||
solana-logger = { path = "../logger", version = "=1.8.14" }
|
||||
solana-measure = { path = "../measure", version = "=1.8.14" }
|
||||
solana-perf = { path = "../perf", version = "=1.8.14" }
|
||||
solana-poh = { path = "../poh", version = "=1.8.14" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.8.14" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
solana-version = { path = "../version", version = "=1.8.14" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -170,7 +170,7 @@ fn main() {
|
||||
let (vote_sender, vote_receiver) = unbounded();
|
||||
let (tpu_vote_sender, tpu_vote_receiver) = unbounded();
|
||||
let (replay_vote_sender, _replay_vote_receiver) = unbounded();
|
||||
let bank0 = Bank::new_for_benches(&genesis_config);
|
||||
let bank0 = Bank::new(&genesis_config);
|
||||
let mut bank_forks = BankForks::new(bank0);
|
||||
let mut bank = bank_forks.working_bank();
|
||||
|
||||
@@ -204,8 +204,7 @@ fn main() {
|
||||
});
|
||||
bank.clear_signatures();
|
||||
//sanity check, make sure all the transactions can execute in parallel
|
||||
|
||||
let res = bank.process_transactions(transactions.iter());
|
||||
let res = bank.process_transactions(&transactions);
|
||||
for r in res {
|
||||
assert!(r.is_ok(), "sanity parallel execution error: {:?}", r);
|
||||
}
|
||||
@@ -314,10 +313,11 @@ fn main() {
|
||||
tx_total_us += duration_as_us(&now.elapsed());
|
||||
|
||||
let mut poh_time = Measure::start("poh_time");
|
||||
poh_recorder
|
||||
.lock()
|
||||
.unwrap()
|
||||
.reset(bank.clone(), Some((bank.slot(), bank.slot() + 1)));
|
||||
poh_recorder.lock().unwrap().reset(
|
||||
bank.last_blockhash(),
|
||||
bank.slot(),
|
||||
Some((bank.slot(), bank.slot() + 1)),
|
||||
);
|
||||
poh_time.stop();
|
||||
|
||||
let mut new_bank_time = Measure::start("new_bank");
|
||||
|
@@ -1,27 +1,30 @@
|
||||
[package]
|
||||
name = "solana-banks-client"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
description = "Solana banks client"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-banks-client"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
borsh = "0.9.1"
|
||||
bincode = "1.3.1"
|
||||
borsh = "0.9.0"
|
||||
borsh-derive = "0.9.0"
|
||||
futures = "0.3"
|
||||
solana-banks-interface = { path = "../banks-interface", version = "=1.10.0" }
|
||||
solana-program = { path = "../sdk/program", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
tarpc = { version = "0.26.2", features = ["full"] }
|
||||
mio = "0.7.6"
|
||||
solana-banks-interface = { path = "../banks-interface", version = "=1.8.14" }
|
||||
solana-program = { path = "../sdk/program", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
tarpc = { version = "0.24.1", features = ["full"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-serde = { version = "0.8", features = ["bincode"] }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../runtime", version = "=1.10.0" }
|
||||
solana-banks-server = { path = "../banks-server", version = "=1.10.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.8.14" }
|
||||
solana-banks-server = { path = "../banks-server", version = "=1.8.14" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
|
@@ -11,23 +11,28 @@ use {
|
||||
futures::{future::join_all, Future, FutureExt},
|
||||
solana_banks_interface::{BanksRequest, BanksResponse},
|
||||
solana_program::{
|
||||
clock::Slot, fee_calculator::FeeCalculator, hash::Hash, program_pack::Pack, pubkey::Pubkey,
|
||||
rent::Rent, sysvar::Sysvar,
|
||||
clock::{Clock, Slot},
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
program_pack::Pack,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::{self, Sysvar},
|
||||
},
|
||||
solana_sdk::{
|
||||
account::{from_account, Account},
|
||||
commitment_config::CommitmentLevel,
|
||||
message::Message,
|
||||
signature::Signature,
|
||||
transaction::{self, Transaction},
|
||||
transport,
|
||||
},
|
||||
std::io::{self, Error, ErrorKind},
|
||||
tarpc::{
|
||||
client::{self, NewClient, RequestDispatch},
|
||||
client::{self, channel::RequestDispatch, NewClient},
|
||||
context::{self, Context},
|
||||
rpc::{ClientMessage, Response},
|
||||
serde_transport::tcp,
|
||||
ClientMessage, Response, Transport,
|
||||
Transport,
|
||||
},
|
||||
tokio::{net::ToSocketAddrs, time::Duration},
|
||||
tokio_serde::formats::Bincode,
|
||||
@@ -61,16 +66,11 @@ impl BanksClient {
|
||||
self.inner.send_transaction_with_context(ctx, transaction)
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "1.9.0",
|
||||
note = "Please use `get_fee_for_message` or `is_blockhash_valid` instead"
|
||||
)]
|
||||
pub fn get_fees_with_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = io::Result<(FeeCalculator, Hash, u64)>> + '_ {
|
||||
#[allow(deprecated)]
|
||||
self.inner
|
||||
.get_fees_with_commitment_and_context(ctx, commitment)
|
||||
}
|
||||
@@ -130,17 +130,23 @@ impl BanksClient {
|
||||
self.send_transaction_with_context(context::current(), transaction)
|
||||
}
|
||||
|
||||
/// Return the cluster clock
|
||||
pub fn get_clock(&mut self) -> impl Future<Output = io::Result<Clock>> + '_ {
|
||||
self.get_account(sysvar::clock::id()).map(|result| {
|
||||
let clock_sysvar = result?
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Clock sysvar not present"))?;
|
||||
from_account::<Clock, _>(&clock_sysvar).ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::Other, "Failed to deserialize Clock sysvar")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the fee parameters associated with a recent, rooted blockhash. The cluster
|
||||
/// will use the transaction's blockhash to look up these same fee parameters and
|
||||
/// use them to calculate the transaction fee.
|
||||
#[deprecated(
|
||||
since = "1.9.0",
|
||||
note = "Please use `get_fee_for_message` or `is_blockhash_valid` instead"
|
||||
)]
|
||||
pub fn get_fees(
|
||||
&mut self,
|
||||
) -> impl Future<Output = io::Result<(FeeCalculator, Hash, u64)>> + '_ {
|
||||
#[allow(deprecated)]
|
||||
self.get_fees_with_commitment_and_context(context::current(), CommitmentLevel::default())
|
||||
}
|
||||
|
||||
@@ -162,9 +168,8 @@ impl BanksClient {
|
||||
/// Return a recent, rooted blockhash from the server. The cluster will only accept
|
||||
/// transactions with a blockhash that has not yet expired. Use the `get_fees`
|
||||
/// method to get both a blockhash and the blockhash's last valid slot.
|
||||
#[deprecated(since = "1.9.0", note = "Please use `get_latest_blockhash` instead")]
|
||||
pub fn get_recent_blockhash(&mut self) -> impl Future<Output = io::Result<Hash>> + '_ {
|
||||
self.get_latest_blockhash()
|
||||
self.get_fees().map(|result| Ok(result?.1))
|
||||
}
|
||||
|
||||
/// Send a transaction and return after the transaction has been rejected or
|
||||
@@ -324,41 +329,6 @@ impl BanksClient {
|
||||
// Convert Vec<Result<_, _>> to Result<Vec<_>>
|
||||
statuses.into_iter().collect()
|
||||
}
|
||||
|
||||
pub fn get_latest_blockhash(&mut self) -> impl Future<Output = io::Result<Hash>> + '_ {
|
||||
self.get_latest_blockhash_with_commitment(CommitmentLevel::default())
|
||||
.map(|result| {
|
||||
result?
|
||||
.map(|x| x.0)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "account not found"))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_latest_blockhash_with_commitment(
|
||||
&mut self,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = io::Result<Option<(Hash, u64)>>> + '_ {
|
||||
self.get_latest_blockhash_with_commitment_and_context(context::current(), commitment)
|
||||
}
|
||||
|
||||
pub fn get_latest_blockhash_with_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = io::Result<Option<(Hash, u64)>>> + '_ {
|
||||
self.inner
|
||||
.get_latest_blockhash_with_commitment_and_context(ctx, commitment)
|
||||
}
|
||||
|
||||
pub fn get_fee_for_message_with_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
commitment: CommitmentLevel,
|
||||
message: Message,
|
||||
) -> impl Future<Output = io::Result<Option<u64>>> + '_ {
|
||||
self.inner
|
||||
.get_fee_for_message_with_commitment_and_context(ctx, commitment, message)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_client<C>(transport: C) -> io::Result<BanksClient>
|
||||
@@ -366,14 +336,14 @@ where
|
||||
C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>> + Send + 'static,
|
||||
{
|
||||
Ok(BanksClient {
|
||||
inner: TarpcClient::new(client::Config::default(), transport).spawn(),
|
||||
inner: TarpcClient::new(client::Config::default(), transport).spawn()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start_tcp_client<T: ToSocketAddrs>(addr: T) -> io::Result<BanksClient> {
|
||||
let transport = tcp::connect(addr, Bincode::default).await?;
|
||||
Ok(BanksClient {
|
||||
inner: TarpcClient::new(client::Config::default(), transport).spawn(),
|
||||
inner: TarpcClient::new(client::Config::default(), transport).spawn()?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -405,7 +375,7 @@ mod tests {
|
||||
// `runtime.block_on()` just once, to run all the async code.
|
||||
|
||||
let genesis = create_genesis_config(10);
|
||||
let bank = Bank::new_for_tests(&genesis.genesis_config);
|
||||
let bank = Bank::new(&genesis.genesis_config);
|
||||
let slot = bank.slot();
|
||||
let block_commitment_cache = Arc::new(RwLock::new(
|
||||
BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
|
||||
@@ -423,7 +393,7 @@ mod tests {
|
||||
.await;
|
||||
let mut banks_client = start_client(client_transport).await?;
|
||||
|
||||
let recent_blockhash = banks_client.get_latest_blockhash().await?;
|
||||
let recent_blockhash = banks_client.get_recent_blockhash().await?;
|
||||
let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
|
||||
@@ -438,7 +408,7 @@ mod tests {
|
||||
// server-side functionality is available to the client.
|
||||
|
||||
let genesis = create_genesis_config(10);
|
||||
let bank = Bank::new_for_tests(&genesis.genesis_config);
|
||||
let bank = Bank::new(&genesis.genesis_config);
|
||||
let slot = bank.slot();
|
||||
let block_commitment_cache = Arc::new(RwLock::new(
|
||||
BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
|
||||
@@ -455,10 +425,7 @@ mod tests {
|
||||
start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
|
||||
.await;
|
||||
let mut banks_client = start_client(client_transport).await?;
|
||||
let (recent_blockhash, last_valid_block_height) = banks_client
|
||||
.get_latest_blockhash_with_commitment(CommitmentLevel::default())
|
||||
.await?
|
||||
.unwrap();
|
||||
let (_, recent_blockhash, last_valid_block_height) = banks_client.get_fees().await?;
|
||||
let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
|
||||
let signature = transaction.signatures[0];
|
||||
banks_client.send_transaction(transaction).await?;
|
||||
|
@@ -1,18 +1,22 @@
|
||||
[package]
|
||||
name = "solana-banks-interface"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
description = "Solana banks RPC interface"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-banks-interface"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.131", features = ["derive"] }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
tarpc = { version = "0.26.2", features = ["full"] }
|
||||
mio = "0.7.6"
|
||||
serde = { version = "1.0.122", features = ["derive"] }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
tarpc = { version = "0.24.1", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
|
@@ -1,5 +1,3 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use {
|
||||
serde::{Deserialize, Serialize},
|
||||
solana_sdk::{
|
||||
@@ -8,7 +6,6 @@ use {
|
||||
commitment_config::CommitmentLevel,
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
transaction::{self, Transaction, TransactionError},
|
||||
@@ -33,10 +30,6 @@ pub struct TransactionStatus {
|
||||
#[tarpc::service]
|
||||
pub trait Banks {
|
||||
async fn send_transaction_with_context(transaction: Transaction);
|
||||
#[deprecated(
|
||||
since = "1.9.0",
|
||||
note = "Please use `get_fee_for_message_with_commitment_and_context` instead"
|
||||
)]
|
||||
async fn get_fees_with_commitment_and_context(
|
||||
commitment: CommitmentLevel,
|
||||
) -> (FeeCalculator, Hash, Slot);
|
||||
@@ -52,14 +45,6 @@ pub trait Banks {
|
||||
address: Pubkey,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<Account>;
|
||||
async fn get_latest_blockhash_with_context() -> Hash;
|
||||
async fn get_latest_blockhash_with_commitment_and_context(
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<(Hash, u64)>;
|
||||
async fn get_fee_for_message_with_commitment_and_context(
|
||||
commitment: CommitmentLevel,
|
||||
message: Message,
|
||||
) -> Option<u64>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@@ -1,22 +1,24 @@
|
||||
[package]
|
||||
name = "solana-banks-server"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
description = "Solana banks server"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-banks-server"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.3"
|
||||
bincode = "1.3.1"
|
||||
futures = "0.3"
|
||||
solana-banks-interface = { path = "../banks-interface", version = "=1.10.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-send-transaction-service = { path = "../send-transaction-service", version = "=1.10.0" }
|
||||
tarpc = { version = "0.26.2", features = ["full"] }
|
||||
log = "0.4.11"
|
||||
mio = "0.7.6"
|
||||
solana-banks-interface = { path = "../banks-interface", version = "=1.8.14" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.8.14" }
|
||||
tarpc = { version = "0.24.1", features = ["full"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-serde = { version = "0.8", features = ["bincode"] }
|
||||
tokio-stream = "0.1"
|
||||
|
@@ -1,6 +1,10 @@
|
||||
use {
|
||||
crate::send_transaction_service::{SendTransactionService, TransactionInfo},
|
||||
bincode::{deserialize, serialize},
|
||||
futures::{future, prelude::stream::StreamExt},
|
||||
futures::{
|
||||
future,
|
||||
prelude::stream::{self, StreamExt},
|
||||
},
|
||||
solana_banks_interface::{
|
||||
Banks, BanksRequest, BanksResponse, TransactionConfirmationStatus, TransactionStatus,
|
||||
},
|
||||
@@ -12,17 +16,11 @@ use {
|
||||
feature_set::FeatureSet,
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
message::{Message, SanitizedMessage},
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
transaction::{self, Transaction},
|
||||
},
|
||||
solana_send_transaction_service::{
|
||||
send_transaction_service::{SendTransactionService, TransactionInfo},
|
||||
tpu_info::NullTpuInfo,
|
||||
},
|
||||
std::{
|
||||
convert::TryFrom,
|
||||
io,
|
||||
net::{Ipv4Addr, SocketAddr},
|
||||
sync::{
|
||||
@@ -34,10 +32,10 @@ use {
|
||||
},
|
||||
tarpc::{
|
||||
context::Context,
|
||||
rpc::{transport::channel::UnboundedChannel, ClientMessage, Response},
|
||||
serde_transport::tcp,
|
||||
server::{self, Channel, Incoming},
|
||||
transport::{self, channel::UnboundedChannel},
|
||||
ClientMessage, Response,
|
||||
server::{self, Channel, Handler},
|
||||
transport,
|
||||
},
|
||||
tokio::time::sleep,
|
||||
tokio_serde::formats::Bincode,
|
||||
@@ -81,7 +79,7 @@ impl BanksServer {
|
||||
.map(|info| deserialize(&info.wire_transaction).unwrap())
|
||||
.collect();
|
||||
let bank = bank_forks.read().unwrap().working_bank();
|
||||
let _ = bank.try_process_transactions(transactions.iter());
|
||||
let _ = bank.process_transactions(&transactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +172,6 @@ impl Banks for BanksServer {
|
||||
signature,
|
||||
serialize(&transaction).unwrap(),
|
||||
last_valid_block_height,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
self.transaction_sender.send(info).unwrap();
|
||||
}
|
||||
@@ -186,16 +182,11 @@ impl Banks for BanksServer {
|
||||
commitment: CommitmentLevel,
|
||||
) -> (FeeCalculator, Hash, u64) {
|
||||
let bank = self.bank(commitment);
|
||||
let blockhash = bank.last_blockhash();
|
||||
let lamports_per_signature = bank.get_lamports_per_signature();
|
||||
let (blockhash, fee_calculator) = bank.last_blockhash_with_fee_calculator();
|
||||
let last_valid_block_height = bank
|
||||
.get_blockhash_last_valid_block_height(&blockhash)
|
||||
.unwrap();
|
||||
(
|
||||
FeeCalculator::new(lamports_per_signature),
|
||||
blockhash,
|
||||
last_valid_block_height,
|
||||
)
|
||||
(fee_calculator, blockhash, last_valid_block_height)
|
||||
}
|
||||
|
||||
async fn get_transaction_status_with_context(
|
||||
@@ -262,8 +253,6 @@ impl Banks for BanksServer {
|
||||
signature,
|
||||
serialize(&transaction).unwrap(),
|
||||
last_valid_block_height,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
self.transaction_sender.send(info).unwrap();
|
||||
self.poll_signature_status(&signature, blockhash, last_valid_block_height, commitment)
|
||||
@@ -279,33 +268,6 @@ impl Banks for BanksServer {
|
||||
let bank = self.bank(commitment);
|
||||
bank.get_account(&address).map(Account::from)
|
||||
}
|
||||
|
||||
async fn get_latest_blockhash_with_context(self, _: Context) -> Hash {
|
||||
let bank = self.bank(CommitmentLevel::default());
|
||||
bank.last_blockhash()
|
||||
}
|
||||
|
||||
async fn get_latest_blockhash_with_commitment_and_context(
|
||||
self,
|
||||
_: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<(Hash, u64)> {
|
||||
let bank = self.bank(commitment);
|
||||
let blockhash = bank.last_blockhash();
|
||||
let last_valid_block_height = bank.get_blockhash_last_valid_block_height(&blockhash)?;
|
||||
Some((blockhash, last_valid_block_height))
|
||||
}
|
||||
|
||||
async fn get_fee_for_message_with_commitment_and_context(
|
||||
self,
|
||||
_: Context,
|
||||
commitment: CommitmentLevel,
|
||||
message: Message,
|
||||
) -> Option<u64> {
|
||||
let bank = self.bank(commitment);
|
||||
let sanitized_message = SanitizedMessage::try_from(message).ok()?;
|
||||
bank.get_fee_for_message(&sanitized_message)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_local_server(
|
||||
@@ -319,7 +281,9 @@ pub async fn start_local_server(
|
||||
poll_signature_status_sleep_duration,
|
||||
);
|
||||
let (client_transport, server_transport) = transport::channel::unbounded();
|
||||
let server = server::BaseChannel::with_defaults(server_transport).execute(banks_server.serve());
|
||||
let server = server::new(server::Config::default())
|
||||
.incoming(stream::once(future::ready(server_transport)))
|
||||
.respond_with(banks_server.serve());
|
||||
tokio::spawn(server);
|
||||
client_transport
|
||||
}
|
||||
@@ -348,14 +312,7 @@ pub async fn start_tcp_server(
|
||||
.map(move |chan| {
|
||||
let (sender, receiver) = channel();
|
||||
|
||||
SendTransactionService::new::<NullTpuInfo>(
|
||||
tpu_addr,
|
||||
&bank_forks,
|
||||
None,
|
||||
receiver,
|
||||
5_000,
|
||||
0,
|
||||
);
|
||||
SendTransactionService::new(tpu_addr, &bank_forks, receiver);
|
||||
|
||||
let server = BanksServer::new(
|
||||
bank_forks.clone(),
|
||||
@@ -363,7 +320,7 @@ pub async fn start_tcp_server(
|
||||
sender,
|
||||
Duration::from_millis(200),
|
||||
);
|
||||
chan.execute(server.serve())
|
||||
chan.respond_with(server.serve()).execute()
|
||||
})
|
||||
// Max 10 channels.
|
||||
.buffer_unordered(10)
|
||||
|
@@ -1,3 +1,7 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
pub mod banks_server;
|
||||
pub mod rpc_banks_service;
|
||||
pub mod send_transaction_service;
|
||||
|
||||
#[macro_use]
|
||||
extern crate solana_metrics;
|
||||
|
@@ -107,7 +107,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rpc_banks_server_exit() {
|
||||
let bank_forks = Arc::new(RwLock::new(BankForks::new(Bank::default_for_tests())));
|
||||
let bank_forks = Arc::new(RwLock::new(BankForks::new(Bank::default())));
|
||||
let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default()));
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
let addr = "127.0.0.1:0".parse().unwrap();
|
||||
|
351
banks-server/src/send_transaction_service.rs
Normal file
351
banks-server/src/send_transaction_service.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
// TODO: Merge this implementation with the one at `core/src/send_transaction_service.rs`
|
||||
use {
|
||||
log::*,
|
||||
solana_metrics::{datapoint_warn, inc_new_counter_info},
|
||||
solana_runtime::{bank::Bank, bank_forks::BankForks},
|
||||
solana_sdk::signature::Signature,
|
||||
std::{
|
||||
collections::HashMap,
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::{
|
||||
mpsc::{Receiver, RecvTimeoutError},
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread::{self, Builder, JoinHandle},
|
||||
time::{Duration, Instant},
|
||||
},
|
||||
};
|
||||
|
||||
/// Maximum size of the transaction queue
|
||||
const MAX_TRANSACTION_QUEUE_SIZE: usize = 10_000; // This seems like a lot but maybe it needs to be bigger one day
|
||||
|
||||
pub struct SendTransactionService {
|
||||
thread: JoinHandle<()>,
|
||||
}
|
||||
|
||||
pub struct TransactionInfo {
|
||||
pub signature: Signature,
|
||||
pub wire_transaction: Vec<u8>,
|
||||
pub last_valid_block_height: u64,
|
||||
}
|
||||
|
||||
impl TransactionInfo {
|
||||
pub fn new(
|
||||
signature: Signature,
|
||||
wire_transaction: Vec<u8>,
|
||||
last_valid_block_height: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
signature,
|
||||
wire_transaction,
|
||||
last_valid_block_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
struct ProcessTransactionsResult {
|
||||
rooted: u64,
|
||||
expired: u64,
|
||||
retried: u64,
|
||||
failed: u64,
|
||||
retained: u64,
|
||||
}
|
||||
|
||||
impl SendTransactionService {
|
||||
pub fn new(
|
||||
tpu_address: SocketAddr,
|
||||
bank_forks: &Arc<RwLock<BankForks>>,
|
||||
receiver: Receiver<TransactionInfo>,
|
||||
) -> Self {
|
||||
let thread = Self::retry_thread(receiver, bank_forks.clone(), tpu_address);
|
||||
Self { thread }
|
||||
}
|
||||
|
||||
fn retry_thread(
|
||||
receiver: Receiver<TransactionInfo>,
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
tpu_address: SocketAddr,
|
||||
) -> JoinHandle<()> {
|
||||
let mut last_status_check = Instant::now();
|
||||
let mut transactions = HashMap::new();
|
||||
let send_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
|
||||
Builder::new()
|
||||
.name("send-tx-svc".to_string())
|
||||
.spawn(move || loop {
|
||||
match receiver.recv_timeout(Duration::from_secs(1)) {
|
||||
Err(RecvTimeoutError::Disconnected) => break,
|
||||
Err(RecvTimeoutError::Timeout) => {}
|
||||
Ok(transaction_info) => {
|
||||
Self::send_transaction(
|
||||
&send_socket,
|
||||
&tpu_address,
|
||||
&transaction_info.wire_transaction,
|
||||
);
|
||||
if transactions.len() < MAX_TRANSACTION_QUEUE_SIZE {
|
||||
transactions.insert(transaction_info.signature, transaction_info);
|
||||
} else {
|
||||
datapoint_warn!("send_transaction_service-queue-overflow");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Instant::now().duration_since(last_status_check).as_secs() >= 5 {
|
||||
if !transactions.is_empty() {
|
||||
datapoint_info!(
|
||||
"send_transaction_service-queue-size",
|
||||
("len", transactions.len(), i64)
|
||||
);
|
||||
let bank_forks = bank_forks.read().unwrap();
|
||||
let root_bank = bank_forks.root_bank();
|
||||
let working_bank = bank_forks.working_bank();
|
||||
|
||||
let _result = Self::process_transactions(
|
||||
&working_bank,
|
||||
&root_bank,
|
||||
&send_socket,
|
||||
&tpu_address,
|
||||
&mut transactions,
|
||||
);
|
||||
}
|
||||
last_status_check = Instant::now();
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn process_transactions(
|
||||
working_bank: &Arc<Bank>,
|
||||
root_bank: &Arc<Bank>,
|
||||
send_socket: &UdpSocket,
|
||||
tpu_address: &SocketAddr,
|
||||
transactions: &mut HashMap<Signature, TransactionInfo>,
|
||||
) -> ProcessTransactionsResult {
|
||||
let mut result = ProcessTransactionsResult::default();
|
||||
|
||||
transactions.retain(|signature, transaction_info| {
|
||||
if root_bank.has_signature(signature) {
|
||||
info!("Transaction is rooted: {}", signature);
|
||||
result.rooted += 1;
|
||||
inc_new_counter_info!("send_transaction_service-rooted", 1);
|
||||
false
|
||||
} else if transaction_info.last_valid_block_height < root_bank.block_height() {
|
||||
info!("Dropping expired transaction: {}", signature);
|
||||
result.expired += 1;
|
||||
inc_new_counter_info!("send_transaction_service-expired", 1);
|
||||
false
|
||||
} else {
|
||||
match working_bank.get_signature_status_slot(signature) {
|
||||
None => {
|
||||
// Transaction is unknown to the working bank, it might have been
|
||||
// dropped or landed in another fork. Re-send it
|
||||
info!("Retrying transaction: {}", signature);
|
||||
result.retried += 1;
|
||||
inc_new_counter_info!("send_transaction_service-retry", 1);
|
||||
Self::send_transaction(
|
||||
send_socket,
|
||||
tpu_address,
|
||||
&transaction_info.wire_transaction,
|
||||
);
|
||||
true
|
||||
}
|
||||
Some((_slot, status)) => {
|
||||
if status.is_err() {
|
||||
info!("Dropping failed transaction: {}", signature);
|
||||
result.failed += 1;
|
||||
inc_new_counter_info!("send_transaction_service-failed", 1);
|
||||
false
|
||||
} else {
|
||||
result.retained += 1;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn send_transaction(
|
||||
send_socket: &UdpSocket,
|
||||
tpu_address: &SocketAddr,
|
||||
wire_transaction: &[u8],
|
||||
) {
|
||||
if let Err(err) = send_socket.send_to(wire_transaction, tpu_address) {
|
||||
warn!("Failed to send transaction to {}: {:?}", tpu_address, err);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(self) -> thread::Result<()> {
|
||||
self.thread.join()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {
|
||||
super::*,
|
||||
solana_sdk::{
|
||||
genesis_config::create_genesis_config, pubkey::Pubkey, signature::Signer,
|
||||
system_transaction,
|
||||
},
|
||||
std::sync::mpsc::channel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn service_exit() {
|
||||
let tpu_address = "127.0.0.1:0".parse().unwrap();
|
||||
let bank = Bank::default();
|
||||
let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
|
||||
let (sender, receiver) = channel();
|
||||
|
||||
let send_tranaction_service =
|
||||
SendTransactionService::new(tpu_address, &bank_forks, receiver);
|
||||
|
||||
drop(sender);
|
||||
send_tranaction_service.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_transactions() {
|
||||
let (genesis_config, mint_keypair) = create_genesis_config(4);
|
||||
let bank = Bank::new(&genesis_config);
|
||||
let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
|
||||
let send_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
let tpu_address = "127.0.0.1:0".parse().unwrap();
|
||||
|
||||
let root_bank = Arc::new(Bank::new_from_parent(
|
||||
&bank_forks.read().unwrap().working_bank(),
|
||||
&Pubkey::default(),
|
||||
1,
|
||||
));
|
||||
let rooted_signature = root_bank
|
||||
.transfer(1, &mint_keypair, &mint_keypair.pubkey())
|
||||
.unwrap();
|
||||
|
||||
let working_bank = Arc::new(Bank::new_from_parent(&root_bank, &Pubkey::default(), 2));
|
||||
|
||||
let non_rooted_signature = working_bank
|
||||
.transfer(2, &mint_keypair, &mint_keypair.pubkey())
|
||||
.unwrap();
|
||||
|
||||
let failed_signature = {
|
||||
let blockhash = working_bank.last_blockhash();
|
||||
let transaction =
|
||||
system_transaction::transfer(&mint_keypair, &Pubkey::default(), 1, blockhash);
|
||||
let signature = transaction.signatures[0];
|
||||
working_bank.process_transaction(&transaction).unwrap_err();
|
||||
signature
|
||||
};
|
||||
|
||||
let mut transactions = HashMap::new();
|
||||
|
||||
info!("Expired transactions are dropped..");
|
||||
transactions.insert(
|
||||
Signature::default(),
|
||||
TransactionInfo::new(Signature::default(), vec![], root_bank.slot() - 1),
|
||||
);
|
||||
let result = SendTransactionService::process_transactions(
|
||||
&working_bank,
|
||||
&root_bank,
|
||||
&send_socket,
|
||||
&tpu_address,
|
||||
&mut transactions,
|
||||
);
|
||||
assert!(transactions.is_empty());
|
||||
assert_eq!(
|
||||
result,
|
||||
ProcessTransactionsResult {
|
||||
expired: 1,
|
||||
..ProcessTransactionsResult::default()
|
||||
}
|
||||
);
|
||||
|
||||
info!("Rooted transactions are dropped...");
|
||||
transactions.insert(
|
||||
rooted_signature,
|
||||
TransactionInfo::new(rooted_signature, vec![], working_bank.slot()),
|
||||
);
|
||||
let result = SendTransactionService::process_transactions(
|
||||
&working_bank,
|
||||
&root_bank,
|
||||
&send_socket,
|
||||
&tpu_address,
|
||||
&mut transactions,
|
||||
);
|
||||
assert!(transactions.is_empty());
|
||||
assert_eq!(
|
||||
result,
|
||||
ProcessTransactionsResult {
|
||||
rooted: 1,
|
||||
..ProcessTransactionsResult::default()
|
||||
}
|
||||
);
|
||||
|
||||
info!("Failed transactions are dropped...");
|
||||
transactions.insert(
|
||||
failed_signature,
|
||||
TransactionInfo::new(failed_signature, vec![], working_bank.slot()),
|
||||
);
|
||||
let result = SendTransactionService::process_transactions(
|
||||
&working_bank,
|
||||
&root_bank,
|
||||
&send_socket,
|
||||
&tpu_address,
|
||||
&mut transactions,
|
||||
);
|
||||
assert!(transactions.is_empty());
|
||||
assert_eq!(
|
||||
result,
|
||||
ProcessTransactionsResult {
|
||||
failed: 1,
|
||||
..ProcessTransactionsResult::default()
|
||||
}
|
||||
);
|
||||
|
||||
info!("Non-rooted transactions are kept...");
|
||||
transactions.insert(
|
||||
non_rooted_signature,
|
||||
TransactionInfo::new(non_rooted_signature, vec![], working_bank.slot()),
|
||||
);
|
||||
let result = SendTransactionService::process_transactions(
|
||||
&working_bank,
|
||||
&root_bank,
|
||||
&send_socket,
|
||||
&tpu_address,
|
||||
&mut transactions,
|
||||
);
|
||||
assert_eq!(transactions.len(), 1);
|
||||
assert_eq!(
|
||||
result,
|
||||
ProcessTransactionsResult {
|
||||
retained: 1,
|
||||
..ProcessTransactionsResult::default()
|
||||
}
|
||||
);
|
||||
transactions.clear();
|
||||
|
||||
info!("Unknown transactions are retried...");
|
||||
transactions.insert(
|
||||
Signature::default(),
|
||||
TransactionInfo::new(Signature::default(), vec![], working_bank.slot()),
|
||||
);
|
||||
let result = SendTransactionService::process_transactions(
|
||||
&working_bank,
|
||||
&root_bank,
|
||||
&send_socket,
|
||||
&tpu_address,
|
||||
&mut transactions,
|
||||
);
|
||||
assert_eq!(transactions.len(), 1);
|
||||
assert_eq!(
|
||||
result,
|
||||
ProcessTransactionsResult {
|
||||
retried: 1,
|
||||
..ProcessTransactionsResult::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
4
bench-exchange/.gitignore
vendored
Normal file
4
bench-exchange/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target/
|
||||
/config/
|
||||
/config-local/
|
||||
/farf/
|
40
bench-exchange/Cargo.toml
Normal file
40
bench-exchange/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-exchange"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.1"
|
||||
itertools = "0.9.0"
|
||||
log = "0.4.11"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
rand = "0.7.0"
|
||||
rayon = "1.5.0"
|
||||
serde_json = "1.0.56"
|
||||
serde_yaml = "0.8.13"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.14" }
|
||||
solana-core = { path = "../core", version = "=1.8.14" }
|
||||
solana-genesis = { path = "../genesis", version = "=1.8.14" }
|
||||
solana-client = { path = "../client", version = "=1.8.14" }
|
||||
solana-exchange-program = { path = "../programs/exchange", version = "=1.8.14" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.8.14" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.8.14" }
|
||||
solana-logger = { path = "../logger", version = "=1.8.14" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.8.14" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.8.14" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.8.14" }
|
||||
solana-version = { path = "../version", version = "=1.8.14" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-local-cluster = { path = "../local-cluster", version = "=1.8.14" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
479
bench-exchange/README.md
Normal file
479
bench-exchange/README.md
Normal file
@@ -0,0 +1,479 @@
|
||||
# token-exchange
|
||||
Solana Token Exchange Bench
|
||||
|
||||
If you can't wait; jump to [Running the exchange](#Running-the-exchange) to
|
||||
learn how to start and interact with the exchange.
|
||||
|
||||
### Table of Contents
|
||||
[Overview](#Overview)<br>
|
||||
[Premise](#Premise)<br>
|
||||
[Exchange startup](#Exchange-startup)<br>
|
||||
[Order Requests](#Trade-requests)<br>
|
||||
[Order Cancellations](#Trade-cancellations)<br>
|
||||
[Trade swap](#Trade-swap)<br>
|
||||
[Exchange program operations](#Exchange-program-operations)<br>
|
||||
[Quotes and OHLCV](#Quotes-and-OHLCV)<br>
|
||||
[Investor strategies](#Investor-strategies)<br>
|
||||
[Running the exchange](#Running-the-exchange)<br>
|
||||
|
||||
## Overview
|
||||
|
||||
An exchange is a marketplace where one asset can be traded for another. This
|
||||
demo demonstrates one way to host an exchange on the Solana blockchain by
|
||||
emulating a currency exchange.
|
||||
|
||||
The assets are virtual tokens held by investors who may post order requests to
|
||||
the exchange. A Matcher monitors the exchange and posts swap requests for
|
||||
matching orders. All the transactions can execute concurrently.
|
||||
|
||||
## Premise
|
||||
|
||||
- Exchange
|
||||
- An exchange is a marketplace where one asset can be traded for another.
|
||||
The exchange in this demo is the on-chain program that implements the
|
||||
tokens and the policies for trading those tokens.
|
||||
- Token
|
||||
- A virtual asset that can be owned, traded, and holds virtual intrinsic value
|
||||
compared to other assets. There are four types of tokens in this demo, A,
|
||||
B, C, D. Each one may be traded for another.
|
||||
- Token account
|
||||
- An account owned by the exchange that holds a quantity of one type of token.
|
||||
- Account request
|
||||
- A request to create a token account
|
||||
- Token request
|
||||
- A request to deposit tokens of a particular type into a token account.
|
||||
- Asset pair
|
||||
- A struct with fields Base and Quote, representing the two assets which make up a
|
||||
trading pair, which themselves are Tokens. The Base or 'primary' asset is the
|
||||
numerator and the Quote is the denominator for pricing purposes.
|
||||
- Order side
|
||||
- Describes which side of the market an investor wants to place a trade on. Options
|
||||
are "Bid" or "Ask", where a bid represents an offer to purchase the Base asset of
|
||||
the AssetPair for a sum of the Quote Asset and an Ask is an offer to sell Base asset
|
||||
for the Quote asset.
|
||||
- Price ratio
|
||||
- An expression of the relative prices of two tokens. Calculated with the Base
|
||||
Asset as the numerator and the Quote Asset as the denominator. Ratios are
|
||||
represented as fixed point numbers. The fixed point scaler is defined in
|
||||
[exchange_state.rs](https://github.com/solana-labs/solana/blob/c2fdd1362a029dcf89c8907c562d2079d977df11/programs/exchange_api/src/exchange_state.rs#L7)
|
||||
- Order request
|
||||
- A Solana transaction sent by a trader to the exchange to submit an order.
|
||||
Order requests are made up of the token pair, the order side (bid or ask),
|
||||
quantity of the primary token, the price ratio, and the two token accounts
|
||||
to be credited/deducted. An example trade request looks like "T AB 5 2"
|
||||
which reads "Exchange 5 A tokens to B tokens at a price ratio of 1:2" A fulfilled trade would result in 5 A tokens
|
||||
deducted and 10 B tokens credited to the trade initiator's token accounts.
|
||||
Successful order requests result in an order.
|
||||
- Order
|
||||
- The result of a successful order request. orders are stored in
|
||||
accounts owned by the submitter of the order request. They can only be
|
||||
canceled by their owner but can be used by anyone in a trade swap. They
|
||||
contain the same information as the order request.
|
||||
- Price spread
|
||||
- The difference between the two matching orders. The spread is the
|
||||
profit of the Matcher initiating the swap request.
|
||||
- Match requirements
|
||||
- Policies that result in a successful trade swap.
|
||||
- Match request
|
||||
- A request to fill two complementary orders (bid/ask), resulting if successful,
|
||||
in a trade being created.
|
||||
- Trade
|
||||
- A successful trade is created from two matching orders that meet
|
||||
swap requirements which are submitted in a Match Request by a Matcher and
|
||||
executed by the exchange. A trade may not wholly satisfy one or both of the
|
||||
orders in which case the orders are adjusted appropriately. Upon execution,
|
||||
tokens are distributed to the traders' accounts and any overlap or
|
||||
"negative spread" between orders is deposited into the Matcher's profit
|
||||
account. All successful trades are recorded in the data of a new solana
|
||||
account for posterity.
|
||||
- Investor
|
||||
- Individual investors who hold a number of tokens and wish to trade them on
|
||||
the exchange. Investors operate as Solana thin clients who own a set of
|
||||
accounts containing tokens and/or order requests. Investors post
|
||||
transactions to the exchange in order to request tokens and post or cancel
|
||||
order requests.
|
||||
- Matcher
|
||||
- An agent who facilitates trading between investors. Matchers operate as
|
||||
Solana thin clients who monitor all the orders looking for a trade
|
||||
match. Once found, the Matcher issues a swap request to the exchange.
|
||||
Matchers are the engine of the exchange and are rewarded for their efforts by
|
||||
accumulating the price spreads of the swaps they initiate. Matchers also
|
||||
provide current bid/ask price and OHLCV (Open, High, Low, Close, Volume)
|
||||
information on demand via a public network port.
|
||||
- Transaction fees
|
||||
- Solana transaction fees are paid for by the transaction submitters who are
|
||||
the Investors and Matchers.
|
||||
|
||||
## Exchange startup
|
||||
|
||||
The exchange is up and running when it reaches a state where it can take
|
||||
investors' trades and Matchers' match requests. To achieve this state the
|
||||
following must occur in order:
|
||||
|
||||
- Start the Solana blockchain
|
||||
- Start the thin-client
|
||||
- The Matcher subscribes to change notifications for all the accounts owned by
|
||||
the exchange program id. The subscription is managed via Solana's JSON RPC
|
||||
interface.
|
||||
- The Matcher starts responding to queries for bid/ask price and OHLCV
|
||||
|
||||
The Matcher responding successfully to price and OHLCV requests is the signal to
|
||||
the investors that trades submitted after that point will be analyzed. <!--This
|
||||
is not ideal, and instead investors should be able to submit trades at any time,
|
||||
and the Matcher could come and go without missing a trade. One way to achieve
|
||||
this is for the Matcher to read the current state of all accounts looking for all
|
||||
open orders.-->
|
||||
|
||||
Investors will initially query the exchange to discover their current balance
|
||||
for each type of token. If the investor does not already have an account for
|
||||
each type of token, they will submit account requests. Matcher as well will
|
||||
request accounts to hold the tokens they earn by initiating trade swaps.
|
||||
|
||||
```rust
|
||||
/// Supported token types
|
||||
pub enum Token {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
}
|
||||
|
||||
/// Supported token pairs
|
||||
pub enum TokenPair {
|
||||
AB,
|
||||
AC,
|
||||
AD,
|
||||
BC,
|
||||
BD,
|
||||
CD,
|
||||
}
|
||||
|
||||
pub enum ExchangeInstruction {
|
||||
/// New token account
|
||||
/// key 0 - Signer
|
||||
/// key 1 - New token account
|
||||
AccountRequest,
|
||||
}
|
||||
|
||||
/// Token accounts are populated with this structure
|
||||
pub struct TokenAccountInfo {
|
||||
/// Investor who owns this account
|
||||
pub owner: Pubkey,
|
||||
/// Current number of tokens this account holds
|
||||
pub tokens: Tokens,
|
||||
}
|
||||
```
|
||||
|
||||
For this demo investors or Matcher can request more tokens from the exchange at
|
||||
any time by submitting token requests. In non-demos, an exchange of this type
|
||||
would provide another way to exchange a 3rd party asset into tokens.
|
||||
|
||||
To request tokens, investors submit transfer requests:
|
||||
|
||||
```rust
|
||||
pub enum ExchangeInstruction {
|
||||
/// Transfer tokens between two accounts
|
||||
/// key 0 - Account to transfer tokens to
|
||||
/// key 1 - Account to transfer tokens from. This can be the exchange program itself,
|
||||
/// the exchange has a limitless number of tokens it can transfer.
|
||||
TransferRequest(Token, u64),
|
||||
}
|
||||
```
|
||||
|
||||
## Order Requests
|
||||
|
||||
When an investor decides to exchange a token of one type for another, they
|
||||
submit a transaction to the Solana Blockchain containing an order request, which,
|
||||
if successful, is turned into an order. orders do not expire but are
|
||||
cancellable. <!-- orders should have a timestamp to enable trade
|
||||
expiration --> When an order is created, tokens are deducted from a token
|
||||
account and the order acts as an escrow. The tokens are held until the
|
||||
order is fulfilled or canceled. If the direction is `To`, then the number
|
||||
of `tokens` are deducted from the primary account, if `From` then `tokens`
|
||||
multiplied by `price` are deducted from the secondary account. orders are
|
||||
no longer valid when the number of `tokens` goes to zero, at which point they
|
||||
can no longer be used. <!-- Could support refilling orders, so order
|
||||
accounts are refilled rather than accumulating -->
|
||||
|
||||
```rust
|
||||
/// Direction of the exchange between two tokens in a pair
|
||||
pub enum Direction {
|
||||
/// Trade first token type (primary) in the pair 'To' the second
|
||||
To,
|
||||
/// Trade first token type in the pair 'From' the second (secondary)
|
||||
From,
|
||||
}
|
||||
|
||||
pub struct OrderRequestInfo {
|
||||
/// Direction of trade
|
||||
pub direction: Direction,
|
||||
|
||||
/// Token pair to trade
|
||||
pub pair: TokenPair,
|
||||
|
||||
/// Number of tokens to exchange; refers to the primary or the secondary depending on the direction
|
||||
pub tokens: u64,
|
||||
|
||||
/// The price ratio the primary price over the secondary price. The primary price is fixed
|
||||
/// and equal to the variable `SCALER`.
|
||||
pub price: u64,
|
||||
|
||||
/// Token account to deposit tokens on successful swap
|
||||
pub dst_account: Pubkey,
|
||||
}
|
||||
|
||||
pub enum ExchangeInstruction {
|
||||
/// order request
|
||||
/// key 0 - Signer
|
||||
/// key 1 - Account in which to record the swap
|
||||
/// key 2 - Token account associated with this trade
|
||||
TradeRequest(TradeRequestInfo),
|
||||
}
|
||||
|
||||
/// Trade accounts are populated with this structure
|
||||
pub struct TradeOrderInfo {
|
||||
/// Owner of the order
|
||||
pub owner: Pubkey,
|
||||
/// Direction of the exchange
|
||||
pub direction: Direction,
|
||||
/// Token pair indicating two tokens to exchange, first is primary
|
||||
pub pair: TokenPair,
|
||||
/// Number of tokens to exchange; primary or secondary depending on direction
|
||||
pub tokens: u64,
|
||||
/// Scaled price of the secondary token given the primary is equal to the scale value
|
||||
/// If scale is 1 and price is 2 then ratio is 1:2 or 1 primary token for 2 secondary tokens
|
||||
pub price: u64,
|
||||
/// account which the tokens were source from. The trade account holds the tokens in escrow
|
||||
/// until either one or more part of a swap or the trade is canceled.
|
||||
pub src_account: Pubkey,
|
||||
/// account which the tokens the tokens will be deposited into on a successful trade
|
||||
pub dst_account: Pubkey,
|
||||
}
|
||||
```
|
||||
|
||||
## Order cancellations
|
||||
|
||||
An investor may cancel a trade at anytime, but only trades they own. If the
|
||||
cancellation is successful, any tokens held in escrow are returned to the
|
||||
account from which they came.
|
||||
|
||||
```rust
|
||||
pub enum ExchangeInstruction {
|
||||
/// order cancellation
|
||||
/// key 0 - Signer
|
||||
/// key 1 -order to cancel
|
||||
TradeCancellation,
|
||||
}
|
||||
```
|
||||
|
||||
## Trade swaps
|
||||
|
||||
The Matcher is monitoring the accounts assigned to the exchange program and
|
||||
building a trade-order table. The order table is used to identify
|
||||
matching orders which could be fulfilled. When a match is found the
|
||||
Matcher should issue a swap request. Swap requests may not satisfy the entirety
|
||||
of either order, but the exchange will greedily fulfill it. Any leftover tokens
|
||||
in either account will keep the order valid for further swap requests in
|
||||
the future.
|
||||
|
||||
Matching orders are defined by the following swap requirements:
|
||||
|
||||
- Opposite polarity (one `To` and one `From`)
|
||||
- Operate on the same token pair
|
||||
- The price ratio of the `From` order is greater than or equal to the `To` order
|
||||
- There are sufficient tokens to perform the trade
|
||||
|
||||
Orders can be written in the following format:
|
||||
|
||||
`investor direction pair quantity price-ratio`
|
||||
|
||||
For example:
|
||||
|
||||
- `1 T AB 2 1`
|
||||
- Investor 1 wishes to exchange 2 A tokens to B tokens at a ratio of 1 A to 1
|
||||
B
|
||||
- `2 F AC 6 1.2`
|
||||
- Investor 2 wishes to exchange A tokens from 6 B tokens at a ratio of 1 A
|
||||
from 1.2 B
|
||||
|
||||
An order table could look something like the following. Notice how the columns
|
||||
are sorted low to high and high to low, respectively. Prices are dramatic and
|
||||
whole for clarity.
|
||||
|
||||
|Row| To | From |
|
||||
|---|-------------|------------|
|
||||
| 1 | 1 T AB 2 4 | 2 F AB 2 8 |
|
||||
| 2 | 1 T AB 1 4 | 2 F AB 2 8 |
|
||||
| 3 | 1 T AB 6 6 | 2 F AB 2 7 |
|
||||
| 4 | 1 T AB 2 8 | 2 F AB 3 6 |
|
||||
| 5 | 1 T AB 2 10 | 2 F AB 1 5 |
|
||||
|
||||
As part of a successful swap request, the exchange will credit tokens to the
|
||||
Matcher's account equal to the difference in the price ratios or the two orders.
|
||||
These tokens are considered the Matcher's profit for initiating the trade.
|
||||
|
||||
The Matcher would initiate the following swap on the order table above:
|
||||
|
||||
- Row 1, To: Investor 1 trades 2 A tokens to 8 B tokens
|
||||
- Row 1, From: Investor 2 trades 2 A tokens from 8 B tokens
|
||||
- Matcher takes 8 B tokens as profit
|
||||
|
||||
Both row 1 trades are fully realized, table becomes:
|
||||
|
||||
|Row| To | From |
|
||||
|---|-------------|------------|
|
||||
| 1 | 1 T AB 1 4 | 2 F AB 2 8 |
|
||||
| 2 | 1 T AB 6 6 | 2 F AB 2 7 |
|
||||
| 3 | 1 T AB 2 8 | 2 F AB 3 6 |
|
||||
| 4 | 1 T AB 2 10 | 2 F AB 1 5 |
|
||||
|
||||
The Matcher would initiate the following swap:
|
||||
|
||||
- Row 1, To: Investor 1 trades 1 A token to 4 B tokens
|
||||
- Row 1, From: Investor 2 trades 1 A token from 4 B tokens
|
||||
- Matcher takes 4 B tokens as profit
|
||||
|
||||
Row 1 From is not fully realized, table becomes:
|
||||
|
||||
|Row| To | From |
|
||||
|---|-------------|------------|
|
||||
| 1 | 1 T AB 6 6 | 2 F AB 1 8 |
|
||||
| 2 | 1 T AB 2 8 | 2 F AB 2 7 |
|
||||
| 3 | 1 T AB 2 10 | 2 F AB 3 6 |
|
||||
| 4 | | 2 F AB 1 5 |
|
||||
|
||||
The Matcher would initiate the following swap:
|
||||
|
||||
- Row 1, To: Investor 1 trades 1 A token to 6 B tokens
|
||||
- Row 1, From: Investor 2 trades 1 A token from 6 B tokens
|
||||
- Matcher takes 2 B tokens as profit
|
||||
|
||||
Row 1 To is now fully realized, table becomes:
|
||||
|
||||
|Row| To | From |
|
||||
|---|-------------|------------|
|
||||
| 1 | 1 T AB 5 6 | 2 F AB 2 7 |
|
||||
| 2 | 1 T AB 2 8 | 2 F AB 3 5 |
|
||||
| 3 | 1 T AB 2 10 | 2 F AB 1 5 |
|
||||
|
||||
The Matcher would initiate the following last swap:
|
||||
|
||||
- Row 1, To: Investor 1 trades 2 A token to 12 B tokens
|
||||
- Row 1, From: Investor 2 trades 2 A token from 12 B tokens
|
||||
- Matcher takes 2 B tokens as profit
|
||||
|
||||
Table becomes:
|
||||
|
||||
|Row| To | From |
|
||||
|---|-------------|------------|
|
||||
| 1 | 1 T AB 3 6 | 2 F AB 3 5 |
|
||||
| 2 | 1 T AB 2 8 | 2 F AB 1 5 |
|
||||
| 3 | 1 T AB 2 10 | |
|
||||
|
||||
At this point the lowest To's price is larger than the largest From's price so
|
||||
no more swaps would be initiated until new orders came in.
|
||||
|
||||
```rust
|
||||
pub enum ExchangeInstruction {
|
||||
/// Trade swap request
|
||||
/// key 0 - Signer
|
||||
/// key 1 - Account in which to record the swap
|
||||
/// key 2 - 'To' order
|
||||
/// key 3 - `From` order
|
||||
/// key 4 - Token account associated with the To Trade
|
||||
/// key 5 - Token account associated with From trade
|
||||
/// key 6 - Token account in which to deposit the Matcher profit from the swap.
|
||||
SwapRequest,
|
||||
}
|
||||
|
||||
/// Swap accounts are populated with this structure
|
||||
pub struct TradeSwapInfo {
|
||||
/// Pair swapped
|
||||
pub pair: TokenPair,
|
||||
/// `To` order
|
||||
pub to_trade_order: Pubkey,
|
||||
/// `From` order
|
||||
pub from_trade_order: Pubkey,
|
||||
/// Number of primary tokens exchanged
|
||||
pub primary_tokens: u64,
|
||||
/// Price the primary tokens were exchanged for
|
||||
pub primary_price: u64,
|
||||
/// Number of secondary tokens exchanged
|
||||
pub secondary_tokens: u64,
|
||||
/// Price the secondary tokens were exchanged for
|
||||
pub secondary_price: u64,
|
||||
}
|
||||
```
|
||||
|
||||
## Exchange program operations
|
||||
|
||||
Putting all the commands together from above, the following operations will be
|
||||
supported by the on-chain exchange program:
|
||||
|
||||
```rust
|
||||
pub enum ExchangeInstruction {
|
||||
/// New token account
|
||||
/// key 0 - Signer
|
||||
/// key 1 - New token account
|
||||
AccountRequest,
|
||||
|
||||
/// Transfer tokens between two accounts
|
||||
/// key 0 - Account to transfer tokens to
|
||||
/// key 1 - Account to transfer tokens from. This can be the exchange program itself,
|
||||
/// the exchange has a limitless number of tokens it can transfer.
|
||||
TransferRequest(Token, u64),
|
||||
|
||||
/// order request
|
||||
/// key 0 - Signer
|
||||
/// key 1 - Account in which to record the swap
|
||||
/// key 2 - Token account associated with this trade
|
||||
TradeRequest(TradeRequestInfo),
|
||||
|
||||
/// order cancellation
|
||||
/// key 0 - Signer
|
||||
/// key 1 -order to cancel
|
||||
TradeCancellation,
|
||||
|
||||
/// Trade swap request
|
||||
/// key 0 - Signer
|
||||
/// key 1 - Account in which to record the swap
|
||||
/// key 2 - 'To' order
|
||||
/// key 3 - `From` order
|
||||
/// key 4 - Token account associated with the To Trade
|
||||
/// key 5 - Token account associated with From trade
|
||||
/// key 6 - Token account in which to deposit the Matcher profit from the swap.
|
||||
SwapRequest,
|
||||
}
|
||||
```
|
||||
|
||||
## Quotes and OHLCV
|
||||
|
||||
The Matcher will provide current bid/ask price quotes based on trade actively and
|
||||
also provide OHLCV based on some time window. The details of how the bid/ask
|
||||
price quotes are calculated are yet to be decided.
|
||||
|
||||
## Investor strategies
|
||||
|
||||
To make a compelling demo, the investors needs to provide interesting trade
|
||||
behavior. Something as simple as a randomly twiddled baseline would be a
|
||||
minimum starting point.
|
||||
|
||||
## Running the exchange
|
||||
|
||||
The exchange bench posts trades and swaps matches as fast as it can.
|
||||
|
||||
You might want to bump the duration up
|
||||
to 60 seconds and the batch size to 1000 for better numbers. You can modify those
|
||||
in client_demo/src/demo.rs::test_exchange_local_cluster.
|
||||
|
||||
The following command runs the bench:
|
||||
|
||||
```bash
|
||||
$ RUST_LOG=solana_bench_exchange=info cargo test --release -- --nocapture test_exchange_local_cluster
|
||||
```
|
||||
|
||||
To also see the cluster messages:
|
||||
|
||||
```bash
|
||||
$ RUST_LOG=solana_bench_exchange=info,solana=info cargo test --release -- --nocapture test_exchange_local_cluster
|
||||
```
|
1030
bench-exchange/src/bench.rs
Normal file
1030
bench-exchange/src/bench.rs
Normal file
File diff suppressed because it is too large
Load Diff
221
bench-exchange/src/cli.rs
Normal file
221
bench-exchange/src/cli.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use {
|
||||
clap::{crate_description, crate_name, value_t, App, Arg, ArgMatches},
|
||||
solana_core::gen_keys::GenKeys,
|
||||
solana_faucet::faucet::FAUCET_PORT,
|
||||
solana_sdk::signature::{read_keypair_file, Keypair},
|
||||
std::{net::SocketAddr, process::exit, time::Duration},
|
||||
};
|
||||
|
||||
pub struct Config {
|
||||
pub entrypoint_addr: SocketAddr,
|
||||
pub faucet_addr: SocketAddr,
|
||||
pub identity: Keypair,
|
||||
pub threads: usize,
|
||||
pub num_nodes: usize,
|
||||
pub duration: Duration,
|
||||
pub transfer_delay: u64,
|
||||
pub fund_amount: u64,
|
||||
pub batch_size: usize,
|
||||
pub chunk_size: usize,
|
||||
pub account_groups: usize,
|
||||
pub client_ids_and_stake_file: String,
|
||||
pub write_to_client_file: bool,
|
||||
pub read_from_client_file: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||
faucet_addr: SocketAddr::from(([127, 0, 0, 1], FAUCET_PORT)),
|
||||
identity: Keypair::new(),
|
||||
num_nodes: 1,
|
||||
threads: 4,
|
||||
duration: Duration::new(u64::max_value(), 0),
|
||||
transfer_delay: 0,
|
||||
fund_amount: 100_000,
|
||||
batch_size: 100,
|
||||
chunk_size: 100,
|
||||
account_groups: 100,
|
||||
client_ids_and_stake_file: String::new(),
|
||||
write_to_client_file: false,
|
||||
read_from_client_file: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
||||
App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.version(version)
|
||||
.arg(
|
||||
Arg::with_name("entrypoint")
|
||||
.short("n")
|
||||
.long("entrypoint")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("127.0.0.1:8001")
|
||||
.help("Cluster entry point; defaults to 127.0.0.1:8001"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("faucet")
|
||||
.short("d")
|
||||
.long("faucet")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("127.0.0.1:9900")
|
||||
.help("Location of the faucet; defaults to 127.0.0.1:9900"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("identity")
|
||||
.short("i")
|
||||
.long("identity")
|
||||
.value_name("PATH")
|
||||
.takes_value(true)
|
||||
.help("File containing a client identity (keypair)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("threads")
|
||||
.long("threads")
|
||||
.value_name("<threads>")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("1")
|
||||
.help("Number of threads submitting transactions"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("num-nodes")
|
||||
.long("num-nodes")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("1")
|
||||
.help("Wait for NUM nodes to converge"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("duration")
|
||||
.long("duration")
|
||||
.value_name("SECS")
|
||||
.takes_value(true)
|
||||
.default_value("60")
|
||||
.help("Seconds to run benchmark, then exit; default is forever"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("transfer-delay")
|
||||
.long("transfer-delay")
|
||||
.value_name("<delay>")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("0")
|
||||
.help("Delay between each chunk"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("fund-amount")
|
||||
.long("fund-amount")
|
||||
.value_name("<fund>")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("100000")
|
||||
.help("Number of lamports to fund to each signer"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("batch-size")
|
||||
.long("batch-size")
|
||||
.value_name("<batch>")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("1000")
|
||||
.help("Number of transactions before the signer rolls over"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("chunk-size")
|
||||
.long("chunk-size")
|
||||
.value_name("<cunk>")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("500")
|
||||
.help("Number of transactions to generate and send at a time"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("account-groups")
|
||||
.long("account-groups")
|
||||
.value_name("<groups>")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("10")
|
||||
.help("Number of account groups to cycle for each batch"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("write-client-keys")
|
||||
.long("write-client-keys")
|
||||
.value_name("FILENAME")
|
||||
.takes_value(true)
|
||||
.help("Generate client keys and stakes and write the list to YAML file"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("read-client-keys")
|
||||
.long("read-client-keys")
|
||||
.value_name("FILENAME")
|
||||
.takes_value(true)
|
||||
.help("Read client keys and stakes from the YAML file"),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
pub fn extract_args(matches: &ArgMatches) -> Config {
|
||||
let mut args = Config::default();
|
||||
|
||||
args.entrypoint_addr = solana_net_utils::parse_host_port(
|
||||
matches.value_of("entrypoint").unwrap(),
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse entrypoint address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
|
||||
args.faucet_addr = solana_net_utils::parse_host_port(matches.value_of("faucet").unwrap())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse faucet address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
|
||||
if matches.is_present("identity") {
|
||||
args.identity = read_keypair_file(matches.value_of("identity").unwrap())
|
||||
.expect("can't read client identity");
|
||||
} else {
|
||||
args.identity = {
|
||||
let seed = [42_u8; 32];
|
||||
let mut rnd = GenKeys::new(seed);
|
||||
rnd.gen_keypair()
|
||||
};
|
||||
}
|
||||
args.threads = value_t!(matches.value_of("threads"), usize).expect("Failed to parse threads");
|
||||
args.num_nodes =
|
||||
value_t!(matches.value_of("num-nodes"), usize).expect("Failed to parse num-nodes");
|
||||
let duration = value_t!(matches.value_of("duration"), u64).expect("Failed to parse duration");
|
||||
args.duration = Duration::from_secs(duration);
|
||||
args.transfer_delay =
|
||||
value_t!(matches.value_of("transfer-delay"), u64).expect("Failed to parse transfer-delay");
|
||||
args.fund_amount =
|
||||
value_t!(matches.value_of("fund-amount"), u64).expect("Failed to parse fund-amount");
|
||||
args.batch_size =
|
||||
value_t!(matches.value_of("batch-size"), usize).expect("Failed to parse batch-size");
|
||||
args.chunk_size =
|
||||
value_t!(matches.value_of("chunk-size"), usize).expect("Failed to parse chunk-size");
|
||||
args.account_groups = value_t!(matches.value_of("account-groups"), usize)
|
||||
.expect("Failed to parse account-groups");
|
||||
|
||||
if let Some(s) = matches.value_of("write-client-keys") {
|
||||
args.write_to_client_file = true;
|
||||
args.client_ids_and_stake_file = s.to_string();
|
||||
}
|
||||
|
||||
if let Some(s) = matches.value_of("read-client-keys") {
|
||||
assert!(!args.write_to_client_file);
|
||||
args.read_from_client_file = true;
|
||||
args.client_ids_and_stake_file = s.to_string();
|
||||
}
|
||||
args
|
||||
}
|
3
bench-exchange/src/lib.rs
Normal file
3
bench-exchange/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod bench;
|
||||
pub mod cli;
|
||||
mod order_book;
|
87
bench-exchange/src/main.rs
Normal file
87
bench-exchange/src/main.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
pub mod bench;
|
||||
mod cli;
|
||||
pub mod order_book;
|
||||
|
||||
use {
|
||||
crate::bench::{airdrop_lamports, create_client_accounts_file, do_bench_exchange, Config},
|
||||
log::*,
|
||||
solana_gossip::gossip_service::{discover_cluster, get_multi_client},
|
||||
solana_sdk::signature::Signer,
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup();
|
||||
solana_metrics::set_panic_hook("bench-exchange");
|
||||
|
||||
let matches = cli::build_args(solana_version::version!()).get_matches();
|
||||
let cli_config = cli::extract_args(&matches);
|
||||
|
||||
let cli::Config {
|
||||
entrypoint_addr,
|
||||
faucet_addr,
|
||||
identity,
|
||||
threads,
|
||||
num_nodes,
|
||||
duration,
|
||||
transfer_delay,
|
||||
fund_amount,
|
||||
batch_size,
|
||||
chunk_size,
|
||||
account_groups,
|
||||
client_ids_and_stake_file,
|
||||
write_to_client_file,
|
||||
read_from_client_file,
|
||||
..
|
||||
} = cli_config;
|
||||
|
||||
let config = Config {
|
||||
identity,
|
||||
threads,
|
||||
duration,
|
||||
transfer_delay,
|
||||
fund_amount,
|
||||
batch_size,
|
||||
chunk_size,
|
||||
account_groups,
|
||||
client_ids_and_stake_file,
|
||||
read_from_client_file,
|
||||
};
|
||||
|
||||
if write_to_client_file {
|
||||
create_client_accounts_file(
|
||||
&config.client_ids_and_stake_file,
|
||||
config.batch_size,
|
||||
config.account_groups,
|
||||
config.fund_amount,
|
||||
);
|
||||
} else {
|
||||
info!("Connecting to the cluster");
|
||||
let nodes = discover_cluster(&entrypoint_addr, num_nodes, SocketAddrSpace::Unspecified)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("Failed to discover nodes");
|
||||
});
|
||||
|
||||
let (client, num_clients) = get_multi_client(&nodes, &SocketAddrSpace::Unspecified);
|
||||
|
||||
info!("{} nodes found", num_clients);
|
||||
if num_clients < num_nodes {
|
||||
panic!("Error: Insufficient nodes discovered");
|
||||
}
|
||||
|
||||
if !read_from_client_file {
|
||||
info!("Funding keypair: {}", config.identity.pubkey());
|
||||
|
||||
let accounts_in_groups = batch_size * account_groups;
|
||||
const NUM_SIGNERS: u64 = 2;
|
||||
airdrop_lamports(
|
||||
&client,
|
||||
&faucet_addr,
|
||||
&config.identity,
|
||||
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
|
||||
);
|
||||
}
|
||||
do_bench_exchange(vec![client], config);
|
||||
}
|
||||
}
|
136
bench-exchange/src/order_book.rs
Normal file
136
bench-exchange/src/order_book.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use {
|
||||
itertools::{
|
||||
EitherOrBoth::{Both, Left, Right},
|
||||
Itertools,
|
||||
},
|
||||
log::*,
|
||||
solana_exchange_program::exchange_state::*,
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{cmp::Ordering, collections::BinaryHeap, error, fmt},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ToOrder {
|
||||
pub pubkey: Pubkey,
|
||||
pub info: OrderInfo,
|
||||
}
|
||||
|
||||
impl Ord for ToOrder {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
other.info.price.cmp(&self.info.price)
|
||||
}
|
||||
}
|
||||
impl PartialOrd for ToOrder {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FromOrder {
|
||||
pub pubkey: Pubkey,
|
||||
pub info: OrderInfo,
|
||||
}
|
||||
|
||||
impl Ord for FromOrder {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.info.price.cmp(&other.info.price)
|
||||
}
|
||||
}
|
||||
impl PartialOrd for FromOrder {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OrderBook {
|
||||
// TODO scale to x token types
|
||||
to_ab: BinaryHeap<ToOrder>,
|
||||
from_ab: BinaryHeap<FromOrder>,
|
||||
}
|
||||
impl fmt::Display for OrderBook {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(
|
||||
f,
|
||||
"+-Order Book--------------------------+-------------------------------------+"
|
||||
)?;
|
||||
for (i, it) in self
|
||||
.to_ab
|
||||
.iter()
|
||||
.zip_longest(self.from_ab.iter())
|
||||
.enumerate()
|
||||
{
|
||||
match it {
|
||||
Both(to, from) => writeln!(
|
||||
f,
|
||||
"| T AB {:8} for {:8}/{:8} | F AB {:8} for {:8}/{:8} |{}",
|
||||
to.info.tokens,
|
||||
SCALER,
|
||||
to.info.price,
|
||||
from.info.tokens,
|
||||
SCALER,
|
||||
from.info.price,
|
||||
i
|
||||
)?,
|
||||
Left(to) => writeln!(
|
||||
f,
|
||||
"| T AB {:8} for {:8}/{:8} | |{}",
|
||||
to.info.tokens, SCALER, to.info.price, i
|
||||
)?,
|
||||
Right(from) => writeln!(
|
||||
f,
|
||||
"| | F AB {:8} for {:8}/{:8} |{}",
|
||||
from.info.tokens, SCALER, from.info.price, i
|
||||
)?,
|
||||
}
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
"+-------------------------------------+-------------------------------------+"
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl OrderBook {
|
||||
// TODO
|
||||
// pub fn cancel(&mut self, pubkey: Pubkey) -> Result<(), Box<dyn error::Error>> {
|
||||
// Ok(())
|
||||
// }
|
||||
pub fn push(&mut self, pubkey: Pubkey, info: OrderInfo) -> Result<(), Box<dyn error::Error>> {
|
||||
check_trade(info.side, info.tokens, info.price)?;
|
||||
match info.side {
|
||||
OrderSide::Ask => {
|
||||
self.to_ab.push(ToOrder { pubkey, info });
|
||||
}
|
||||
OrderSide::Bid => {
|
||||
self.from_ab.push(FromOrder { pubkey, info });
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn pop(&mut self) -> Option<(ToOrder, FromOrder)> {
|
||||
if let Some(pair) = Self::pop_pair(&mut self.to_ab, &mut self.from_ab) {
|
||||
return Some(pair);
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn get_num_outstanding(&self) -> (usize, usize) {
|
||||
(self.to_ab.len(), self.from_ab.len())
|
||||
}
|
||||
|
||||
fn pop_pair(
|
||||
to_ab: &mut BinaryHeap<ToOrder>,
|
||||
from_ab: &mut BinaryHeap<FromOrder>,
|
||||
) -> Option<(ToOrder, FromOrder)> {
|
||||
let to = to_ab.peek()?;
|
||||
let from = from_ab.peek()?;
|
||||
if from.info.price < to.info.price {
|
||||
debug!("Trade not viable");
|
||||
return None;
|
||||
}
|
||||
let to = to_ab.pop()?;
|
||||
let from = from_ab.pop()?;
|
||||
Some((to, from))
|
||||
}
|
||||
}
|
126
bench-exchange/tests/bench_exchange.rs
Normal file
126
bench-exchange/tests/bench_exchange.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use {
|
||||
log::*,
|
||||
solana_bench_exchange::bench::{airdrop_lamports, do_bench_exchange, Config},
|
||||
solana_core::validator::ValidatorConfig,
|
||||
solana_exchange_program::{
|
||||
exchange_processor::process_instruction, id, solana_exchange_program,
|
||||
},
|
||||
solana_faucet::faucet::run_local_faucet_with_port,
|
||||
solana_gossip::gossip_service::{discover_cluster, get_multi_client},
|
||||
solana_local_cluster::{
|
||||
local_cluster::{ClusterConfig, LocalCluster},
|
||||
validator_configs::make_identical_validator_configs,
|
||||
},
|
||||
solana_runtime::{bank::Bank, bank_client::BankClient},
|
||||
solana_sdk::{
|
||||
genesis_config::create_genesis_config,
|
||||
signature::{Keypair, Signer},
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
std::{process::exit, sync::mpsc::channel, time::Duration},
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_exchange_local_cluster() {
|
||||
solana_logger::setup();
|
||||
|
||||
const NUM_NODES: usize = 1;
|
||||
|
||||
let config = Config {
|
||||
identity: Keypair::new(),
|
||||
duration: Duration::from_secs(1),
|
||||
fund_amount: 100_000,
|
||||
threads: 1,
|
||||
transfer_delay: 20, // 15
|
||||
batch_size: 100, // 1000
|
||||
chunk_size: 10, // 200
|
||||
account_groups: 1, // 10
|
||||
..Config::default()
|
||||
};
|
||||
let Config {
|
||||
fund_amount,
|
||||
batch_size,
|
||||
account_groups,
|
||||
..
|
||||
} = config;
|
||||
let accounts_in_groups = batch_size * account_groups;
|
||||
|
||||
let cluster = LocalCluster::new(
|
||||
&mut ClusterConfig {
|
||||
node_stakes: vec![100_000; NUM_NODES],
|
||||
cluster_lamports: 100_000_000_000_000,
|
||||
validator_configs: make_identical_validator_configs(
|
||||
&ValidatorConfig::default(),
|
||||
NUM_NODES,
|
||||
),
|
||||
native_instruction_processors: [solana_exchange_program!()].to_vec(),
|
||||
..ClusterConfig::default()
|
||||
},
|
||||
SocketAddrSpace::Unspecified,
|
||||
);
|
||||
|
||||
let faucet_keypair = Keypair::new();
|
||||
cluster.transfer(
|
||||
&cluster.funding_keypair,
|
||||
&faucet_keypair.pubkey(),
|
||||
2_000_000_000_000,
|
||||
);
|
||||
|
||||
let (addr_sender, addr_receiver) = channel();
|
||||
run_local_faucet_with_port(faucet_keypair, addr_sender, Some(1_000_000_000_000), 0);
|
||||
let faucet_addr = addr_receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("run_local_faucet")
|
||||
.expect("faucet_addr");
|
||||
|
||||
info!("Connecting to the cluster");
|
||||
let nodes = discover_cluster(
|
||||
&cluster.entry_point_info.gossip,
|
||||
NUM_NODES,
|
||||
SocketAddrSpace::Unspecified,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("Failed to discover {} nodes: {:?}", NUM_NODES, err);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
let (client, num_clients) = get_multi_client(&nodes, &SocketAddrSpace::Unspecified);
|
||||
|
||||
info!("clients: {}", num_clients);
|
||||
assert!(num_clients >= NUM_NODES);
|
||||
|
||||
const NUM_SIGNERS: u64 = 2;
|
||||
airdrop_lamports(
|
||||
&client,
|
||||
&faucet_addr,
|
||||
&config.identity,
|
||||
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
|
||||
);
|
||||
|
||||
do_bench_exchange(vec![client], config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exchange_bank_client() {
|
||||
solana_logger::setup();
|
||||
let (genesis_config, identity) = create_genesis_config(100_000_000_000_000);
|
||||
let mut bank = Bank::new(&genesis_config);
|
||||
bank.add_builtin("exchange_program", id(), process_instruction);
|
||||
let clients = vec![BankClient::new(bank)];
|
||||
|
||||
do_bench_exchange(
|
||||
clients,
|
||||
Config {
|
||||
identity,
|
||||
duration: Duration::from_secs(1),
|
||||
fund_amount: 100_000,
|
||||
threads: 1,
|
||||
transfer_delay: 20, // 0;
|
||||
batch_size: 100, // 1500;
|
||||
chunk_size: 10, // 1500;
|
||||
account_groups: 1, // 50;
|
||||
..Config::default()
|
||||
},
|
||||
);
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-bench-streamer"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -10,11 +10,11 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.1"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.10.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.10.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.10.0" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.10.0" }
|
||||
solana-version = { path = "../version", version = "=1.10.0" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.14" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.8.14" }
|
||||
solana-logger = { path = "../logger", version = "=1.8.14" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.8.14" }
|
||||
solana-version = { path = "../version", version = "=1.8.14" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -1,36 +1,38 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-bench-tps"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.1"
|
||||
clap = "2.33.1"
|
||||
log = "0.4.14"
|
||||
rayon = "1.5.1"
|
||||
serde_json = "1.0.72"
|
||||
serde_yaml = "0.8.21"
|
||||
solana-core = { path = "../core", version = "=1.10.0" }
|
||||
solana-genesis = { path = "../genesis", version = "=1.10.0" }
|
||||
solana-client = { path = "../client", version = "=1.10.0" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.10.0" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.10.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.10.0" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.10.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.10.0" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.10.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.10.0" }
|
||||
solana-version = { path = "../version", version = "=1.10.0" }
|
||||
log = "0.4.11"
|
||||
rayon = "1.5.0"
|
||||
serde_json = "1.0.56"
|
||||
serde_yaml = "0.8.13"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.14" }
|
||||
solana-core = { path = "../core", version = "=1.8.14" }
|
||||
solana-genesis = { path = "../genesis", version = "=1.8.14" }
|
||||
solana-client = { path = "../client", version = "=1.8.14" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.8.14" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.8.14" }
|
||||
solana-logger = { path = "../logger", version = "=1.8.14" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.8.14" }
|
||||
solana-measure = { path = "../measure", version = "=1.8.14" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.8.14" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.8.14" }
|
||||
solana-version = { path = "../version", version = "=1.8.14" }
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "0.5.1"
|
||||
solana-local-cluster = { path = "../local-cluster", version = "=1.10.0" }
|
||||
serial_test = "0.4.0"
|
||||
solana-local-cluster = { path = "../local-cluster", version = "=1.8.14" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -9,10 +9,10 @@ use {
|
||||
solana_metrics::{self, datapoint_info},
|
||||
solana_sdk::{
|
||||
client::Client,
|
||||
clock::{DEFAULT_MS_PER_SLOT, DEFAULT_S_PER_SLOT, MAX_PROCESSING_AGE},
|
||||
clock::{DEFAULT_S_PER_SLOT, MAX_PROCESSING_AGE},
|
||||
commitment_config::CommitmentConfig,
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
@@ -47,12 +47,14 @@ pub type Result<T> = std::result::Result<T, BenchTpsError>;
|
||||
|
||||
pub type SharedTransactions = Arc<RwLock<VecDeque<Vec<(Transaction, u64)>>>>;
|
||||
|
||||
fn get_latest_blockhash<T: Client>(client: &T) -> Hash {
|
||||
fn get_recent_blockhash<T: Client>(client: &T) -> (Hash, FeeCalculator) {
|
||||
loop {
|
||||
match client.get_latest_blockhash_with_commitment(CommitmentConfig::processed()) {
|
||||
Ok((blockhash, _)) => return blockhash,
|
||||
match client.get_recent_blockhash_with_commitment(CommitmentConfig::processed()) {
|
||||
Ok((blockhash, fee_calculator, _last_valid_slot)) => {
|
||||
return (blockhash, fee_calculator)
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Couldn't get last blockhash: {:?}", err);
|
||||
info!("Couldn't get recent blockhash: {:?}", err);
|
||||
sleep(Duration::from_secs(1));
|
||||
}
|
||||
};
|
||||
@@ -239,19 +241,19 @@ where
|
||||
|
||||
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
|
||||
|
||||
let blockhash = Arc::new(RwLock::new(get_latest_blockhash(client.as_ref())));
|
||||
let recent_blockhash = Arc::new(RwLock::new(get_recent_blockhash(client.as_ref()).0));
|
||||
let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0));
|
||||
let total_tx_sent_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let blockhash_thread = {
|
||||
let exit_signal = exit_signal.clone();
|
||||
let blockhash = blockhash.clone();
|
||||
let recent_blockhash = recent_blockhash.clone();
|
||||
let client = client.clone();
|
||||
let id = id.pubkey();
|
||||
Builder::new()
|
||||
.name("solana-blockhash-poller".to_string())
|
||||
.spawn(move || {
|
||||
poll_blockhash(&exit_signal, &blockhash, &client, &id);
|
||||
poll_blockhash(&exit_signal, &recent_blockhash, &client, &id);
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
@@ -271,7 +273,7 @@ where
|
||||
let start = Instant::now();
|
||||
|
||||
generate_chunked_transfers(
|
||||
blockhash,
|
||||
recent_blockhash,
|
||||
&shared_txs,
|
||||
shared_tx_active_thread_count,
|
||||
source_keypair_chunks,
|
||||
@@ -391,22 +393,6 @@ fn generate_txs(
|
||||
}
|
||||
}
|
||||
|
||||
fn get_new_latest_blockhash<T: Client>(client: &Arc<T>, blockhash: &Hash) -> Option<Hash> {
|
||||
let start = Instant::now();
|
||||
while start.elapsed().as_secs() < 5 {
|
||||
if let Ok(new_blockhash) = client.get_latest_blockhash() {
|
||||
if new_blockhash != *blockhash {
|
||||
return Some(new_blockhash);
|
||||
}
|
||||
}
|
||||
debug!("Got same blockhash ({:?}), will retry...", blockhash);
|
||||
|
||||
// Retry ~twice during a slot
|
||||
sleep(Duration::from_millis(DEFAULT_MS_PER_SLOT / 2));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn poll_blockhash<T: Client>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
blockhash: &Arc<RwLock<Hash>>,
|
||||
@@ -418,7 +404,7 @@ fn poll_blockhash<T: Client>(
|
||||
loop {
|
||||
let blockhash_updated = {
|
||||
let old_blockhash = *blockhash.read().unwrap();
|
||||
if let Some(new_blockhash) = get_new_latest_blockhash(client, &old_blockhash) {
|
||||
if let Ok((new_blockhash, _fee)) = client.get_new_blockhash(&old_blockhash) {
|
||||
*blockhash.write().unwrap() = new_blockhash;
|
||||
blockhash_last_updated = Instant::now();
|
||||
true
|
||||
@@ -556,7 +542,7 @@ impl<'a> FundingTransactions<'a> for Vec<(&'a Keypair, Transaction)> {
|
||||
self.len(),
|
||||
);
|
||||
|
||||
let blockhash = get_latest_blockhash(client.as_ref());
|
||||
let (blockhash, _fee_calculator) = get_recent_blockhash(client.as_ref());
|
||||
|
||||
// re-sign retained to_fund_txes with updated blockhash
|
||||
self.sign(blockhash);
|
||||
@@ -748,7 +734,7 @@ pub fn airdrop_lamports<T: Client>(
|
||||
id.pubkey(),
|
||||
);
|
||||
|
||||
let blockhash = get_latest_blockhash(client);
|
||||
let (blockhash, _fee_calculator) = get_recent_blockhash(client);
|
||||
match request_airdrop_transaction(faucet_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
||||
Ok(transaction) => {
|
||||
let mut tries = 0;
|
||||
@@ -906,16 +892,8 @@ pub fn generate_and_fund_keypairs<T: 'static + Client + Send + Sync>(
|
||||
// pay for the transaction fees in a new run.
|
||||
let enough_lamports = 8 * lamports_per_account / 10;
|
||||
if first_keypair_balance < enough_lamports || last_keypair_balance < enough_lamports {
|
||||
let single_sig_message = Message::new_with_blockhash(
|
||||
&[Instruction::new_with_bytes(
|
||||
Pubkey::new_unique(),
|
||||
&[],
|
||||
vec![AccountMeta::new(Pubkey::new_unique(), true)],
|
||||
)],
|
||||
None,
|
||||
&client.get_latest_blockhash().unwrap(),
|
||||
);
|
||||
let max_fee = client.get_fee_for_message(&single_sig_message).unwrap();
|
||||
let fee_rate_governor = client.get_fee_rate_governor().unwrap();
|
||||
let max_fee = fee_rate_governor.max_lamports_per_signature;
|
||||
let extra_fees = extra * max_fee;
|
||||
let total_keypairs = keypairs.len() as u64 + 1; // Add one for funding keypair
|
||||
let total = lamports_per_account * total_keypairs + extra_fees;
|
||||
@@ -960,7 +938,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_bench_tps_bank_client() {
|
||||
let (genesis_config, id) = create_genesis_config(10_000);
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let bank = Bank::new(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
|
||||
let config = Config {
|
||||
@@ -981,7 +959,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_bench_tps_fund_keys() {
|
||||
let (genesis_config, id) = create_genesis_config(10_000);
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let bank = Bank::new(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
let keypair_count = 20;
|
||||
let lamports = 20;
|
||||
@@ -1004,7 +982,7 @@ mod tests {
|
||||
let (mut genesis_config, id) = create_genesis_config(10_000);
|
||||
let fee_rate_governor = FeeRateGovernor::new(11, 0);
|
||||
genesis_config.fee_rate_governor = fee_rate_governor;
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let bank = Bank::new(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
let keypair_count = 20;
|
||||
let lamports = 20;
|
||||
|
32
bloom/Cargo.toml
Normal file
32
bloom/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "solana-bloom"
|
||||
version = "1.8.14"
|
||||
description = "Solana bloom filter"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-bloom"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bv = { version = "0.11.1", features = ["serde"] }
|
||||
fnv = "1.0.7"
|
||||
rand = "0.7.0"
|
||||
serde = { version = "1.0.133", features = ["rc"] }
|
||||
rayon = "1.5.1"
|
||||
serde_derive = "1.0.103"
|
||||
solana-frozen-abi = { path = "../frozen-abi", version = "=1.8.14" }
|
||||
solana-frozen-abi-macro = { path = "../frozen-abi/macro", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
log = "0.4.14"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_bloom"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4"
|
@@ -5,7 +5,7 @@ use {
|
||||
bv::BitVec,
|
||||
fnv::FnvHasher,
|
||||
rand::Rng,
|
||||
solana_runtime::bloom::{AtomicBloom, Bloom, BloomHashIndex},
|
||||
solana_bloom::bloom::{AtomicBloom, Bloom, BloomHashIndex},
|
||||
solana_sdk::{
|
||||
hash::{hash, Hash},
|
||||
signature::Signature,
|
@@ -6,7 +6,9 @@ use {
|
||||
serde::{Deserialize, Serialize},
|
||||
solana_sdk::sanitize::{Sanitize, SanitizeError},
|
||||
std::{
|
||||
cmp, fmt,
|
||||
cmp,
|
||||
convert::TryFrom,
|
||||
fmt,
|
||||
hash::Hasher,
|
||||
marker::PhantomData,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
@@ -72,12 +74,10 @@ impl<T: BloomHashIndex> Bloom<T> {
|
||||
_phantom: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
/// Create filter optimal for num size given the `FALSE_RATE`.
|
||||
///
|
||||
/// The keys are randomized for picking data out of a collision resistant hash of size
|
||||
/// `keysize` bytes.
|
||||
///
|
||||
/// See <https://hur.st/bloomfilter/>.
|
||||
/// create filter optimal for num size given the `FALSE_RATE`
|
||||
/// the keys are randomized for picking data out of a collision resistant hash of size
|
||||
/// `keysize` bytes
|
||||
/// https://hur.st/bloomfilter/
|
||||
pub fn random(num_items: usize, false_rate: f64, max_bits: usize) -> Self {
|
||||
let m = Self::num_bits(num_items as f64, false_rate);
|
||||
let num_bits = cmp::max(1, cmp::min(m as usize, max_bits));
|
||||
@@ -101,7 +101,7 @@ impl<T: BloomHashIndex> Bloom<T> {
|
||||
}
|
||||
}
|
||||
fn pos(&self, key: &T, k: u64) -> u64 {
|
||||
key.hash_at_index(k) % self.bits.len()
|
||||
key.hash_at_index(k).wrapping_rem(self.bits.len())
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.bits = BitVec::new_fill(false, self.bits.len());
|
||||
@@ -111,7 +111,7 @@ impl<T: BloomHashIndex> Bloom<T> {
|
||||
for k in &self.keys {
|
||||
let pos = self.pos(key, *k);
|
||||
if !self.bits.get(pos) {
|
||||
self.num_bits_set += 1;
|
||||
self.num_bits_set = self.num_bits_set.saturating_add(1);
|
||||
self.bits.set(pos, true);
|
||||
}
|
||||
}
|
||||
@@ -164,21 +164,26 @@ impl<T: BloomHashIndex> From<Bloom<T>> for AtomicBloom<T> {
|
||||
|
||||
impl<T: BloomHashIndex> AtomicBloom<T> {
|
||||
fn pos(&self, key: &T, hash_index: u64) -> (usize, u64) {
|
||||
let pos = key.hash_at_index(hash_index) % self.num_bits;
|
||||
let pos = key.hash_at_index(hash_index).wrapping_rem(self.num_bits);
|
||||
// Divide by 64 to figure out which of the
|
||||
// AtomicU64 bit chunks we need to modify.
|
||||
let index = pos >> 6;
|
||||
let index = pos.wrapping_shr(6);
|
||||
// (pos & 63) is equivalent to mod 64 so that we can find
|
||||
// the index of the bit within the AtomicU64 to modify.
|
||||
let mask = 1u64 << (pos & 63);
|
||||
let mask = 1u64.wrapping_shl(u32::try_from(pos & 63).unwrap());
|
||||
(index as usize, mask)
|
||||
}
|
||||
|
||||
pub fn add(&self, key: &T) {
|
||||
/// Adds an item to the bloom filter and returns true if the item
|
||||
/// was not in the filter before.
|
||||
pub fn add(&self, key: &T) -> bool {
|
||||
let mut added = false;
|
||||
for k in &self.keys {
|
||||
let (index, mask) = self.pos(key, *k);
|
||||
self.bits[index].fetch_or(mask, Ordering::Relaxed);
|
||||
let prev_val = self.bits[index].fetch_or(mask, Ordering::Relaxed);
|
||||
added = added || prev_val & mask == 0u64;
|
||||
}
|
||||
added
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &T) -> bool {
|
||||
@@ -189,6 +194,12 @@ impl<T: BloomHashIndex> AtomicBloom<T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_for_tests(&mut self) {
|
||||
self.bits.iter().for_each(|bit| {
|
||||
bit.store(0u64, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
|
||||
// Only for tests and simulations.
|
||||
pub fn mock_clone(&self) -> Self {
|
||||
Self {
|
||||
@@ -320,7 +331,9 @@ mod test {
|
||||
assert_eq!(bloom.keys.len(), 3);
|
||||
assert_eq!(bloom.num_bits, 6168);
|
||||
assert_eq!(bloom.bits.len(), 97);
|
||||
hash_values.par_iter().for_each(|v| bloom.add(v));
|
||||
hash_values.par_iter().for_each(|v| {
|
||||
bloom.add(v);
|
||||
});
|
||||
let bloom: Bloom<Hash> = bloom.into();
|
||||
assert_eq!(bloom.keys.len(), 3);
|
||||
assert_eq!(bloom.bits.len(), 6168);
|
||||
@@ -362,7 +375,9 @@ mod test {
|
||||
}
|
||||
// Round trip, re-inserting the same hash values.
|
||||
let bloom: AtomicBloom<_> = bloom.into();
|
||||
hash_values.par_iter().for_each(|v| bloom.add(v));
|
||||
hash_values.par_iter().for_each(|v| {
|
||||
bloom.add(v);
|
||||
});
|
||||
for hash_value in &hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
@@ -380,7 +395,9 @@ mod test {
|
||||
let bloom: AtomicBloom<_> = bloom.into();
|
||||
assert_eq!(bloom.num_bits, 9731);
|
||||
assert_eq!(bloom.bits.len(), (9731 + 63) / 64);
|
||||
more_hash_values.par_iter().for_each(|v| bloom.add(v));
|
||||
more_hash_values.par_iter().for_each(|v| {
|
||||
bloom.add(v);
|
||||
});
|
||||
for hash_value in &hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
5
bloom/src/lib.rs
Normal file
5
bloom/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))]
|
||||
pub mod bloom;
|
||||
|
||||
#[macro_use]
|
||||
extern crate solana_frozen_abi_macro;
|
@@ -1,88 +0,0 @@
|
||||
# Leader Duplicate Block Slashing
|
||||
|
||||
This design describes how the cluster slashes leaders that produce duplicate
|
||||
blocks.
|
||||
|
||||
Leaders that produce multiple blocks for the same slot increase the number of
|
||||
potential forks that the cluster has to resolve.
|
||||
|
||||
## Primitives
|
||||
1. gossip_root: Nodes now gossip their current root
|
||||
2. gossip_duplicate_slots: Nodes can gossip up to `N` duplicate slot proofs.
|
||||
3. `DUPLICATE_THRESHOLD`: The minimum percentage of stake that needs to vote on a fork with version `X` of a duplicate slot, in order for that fork to become votable.
|
||||
|
||||
## Protocol
|
||||
1. When WindowStage detects a duplicate slot proof `P`, it checks the new `gossip_root` to see if `<= 1/3` of the nodes have rooted a slot `S >= P`. If so, it pushes a proof to `gossip_duplicate_slots` to gossip. WindowStage then signals ReplayStage about this duplicate slot `S`. These proofs can be purged from gossip once the validator sees > 2/3 of people gossiping roots `R > S`.
|
||||
|
||||
2. When ReplayStage receives the signal for a duplicate slot `S` from `1)` above, the validator monitors gossip and replay waiting for`>= DUPLICATE_THRESHOLD` votes for the same hash which implies the same version of the slot. If this conditon is met for some version with hash `H` of slot `S`, this is then known as the `duplicate_confirmed` version of the slot.
|
||||
|
||||
Before a duplicate slot `S` is `duplicate_confirmed`, it's first excluded from the vote candidate set in the fork choice rules. In addition, ReplayStage also resets PoH to the *latest* ancestor of the *earliest* `non-duplicate/confirmed_duplicate_slot`, so that block generation can start happening on the earliest known *safe* block.
|
||||
|
||||
Some notes about the `DUPLICATE_THRESHOLD`. In the cases below, assume `DUPLICATE_THRESHOLD = 52`:
|
||||
|
||||
a) If less than `2 * DUPLICATE_THRESHOLD - 1` percentage of the network is malicious, then there can only be one such `duplicate_confirmed` version of the slot. With `DUPLICATE_THRESHOLD = 52`, this is
|
||||
a malcious tolerance of `4%`
|
||||
|
||||
b) The liveness of the network is at most `1 - DUPLICATE_THRESHOLD - SWITCH_THRESHOLD`. This is because if you need at least `SWITCH_THRESHOLD` percentage of the stake voting on a different fork in order to switch off of a duplicate fork that has `< DUPLICATE_THRESHOLD` stake voting on it, and is *not* `duplicate_confirmed`. For `DUPLICATE_THRESHOLD = 52` and `DUPLICATE_THRESHOLD = 38`, this implies a liveness tolerance of `10%`.
|
||||
|
||||
For example in the situation below, validators that voted on `2` can't vote any further on fork `2` because it's been removed from fork choice. Now slot 6 better have enough stake for a switching proof, or the network halts.
|
||||
|
||||
```text
|
||||
|-------- 2 (51% voted, then detected this slot was a duplicate and removed this slot from fork choice)
|
||||
0---|
|
||||
|---------- 6 (39%)
|
||||
|
||||
```
|
||||
|
||||
3. Switching proofs need to be extended to allow including vote hashes from different versions of the same same slot (detected through 1). Right now this is not supported since switching proofs can
|
||||
only be built using votes from banks in BankForks, and two different versions of the same slot cannot
|
||||
simultaneously exist in BankForks. For instance:
|
||||
|
||||
```text
|
||||
|-------- 2
|
||||
|
|
||||
0------------- 1 ------ 2'
|
||||
|
|
||||
|---------- 6
|
||||
|
||||
```
|
||||
|
||||
Imagine each version of slot 2 and 2' have `DUPLICATE_THRESHOLD / 2` of the votes on them, so neither duplicate can be confirmed. At most slot 6 has `1 - DUPLICATE_THRESHOLD / 2` of the votes
|
||||
on it, which is less than the switching threshold. Thus, in order for validators voting on `2` or `2'` to switch to slot 6, and make progress, they need to incorporate votes from the other version of the slot into their switching proofs.
|
||||
|
||||
|
||||
### The repair problem.
|
||||
Now what happens if one of the following occurs:
|
||||
|
||||
1) Due to network blips/latencies, some validators fail to observe the gossip votes before they are overwritten by newer votes? Then some validators may conclude a slot `S` is `duplicate_confirmed` while others don't.
|
||||
|
||||
2) Due to lockouts, no version of duplicate slot `S` reaches `duplicate_confirmed` status, but one of its descendants may reach `duplicate_confirmed` after those lockouts expire, which by definition, means `S` is also `duplicate_confirmed`.
|
||||
|
||||
3) People who are catching up and don't see the votes in gossip encounter a dup block and can't make progress.
|
||||
|
||||
We assume that given a network is eventually stable, if at least one correct validator observed `S` is `duplicate_confirmed`, then if `S` is part of the heaviest fork, then eventually all validators will observe some descendant of `S` is duplicate confirmed.
|
||||
|
||||
This problem we need to solve is modeled simply by the below scenario:
|
||||
|
||||
```text
|
||||
1 -> 2 (duplicate) -> 3 -> 4 (duplicate)
|
||||
```
|
||||
Assume the following:
|
||||
|
||||
1. Due to gossiping duplciate proofs, we assume everyone will eventually see duplicate proofs for 2 and 4, so everyone agrees to remove them from fork choice until they are `duplicate_confirmed`.
|
||||
|
||||
2. Due to lockouts, `> DUPLICATE_THRESHOLD` of the stake votes on 4, but not 2. This means at least `DUPLICATE_THRESHOLD` of people have the "correct" version of both slots 2 and 4.
|
||||
|
||||
3. However, the remaining `1-DUPLICATE_THRESHOLD` of people have wrong version of 2. This means in replay, their slot 3 will be marked dead, *even though the faulty slot is 2*. The goal is to get these people on the right fork again.
|
||||
|
||||
Possible solution:
|
||||
|
||||
1. Change `EpochSlots` to signal when a bank is frozen, not when a slot is complete. If we see > `DUPLICATE_THRESHOLD` have frozen the dead slot 3, then we attempt recovery. Note this does not mean that all `DUPLICATE_THRESHOLD` have frozen the same version of the bank, it's just a signal to us that something may be wrong with our version of the bank.
|
||||
|
||||
2. Recovery takes the form of a special repair request, `RepairDuplicateConfirmed(dead_slot, Vec<(Slot, Hash)>)`, which specifies a dead slot, and then a vector of `(slot, hash)` of `N` of its latest parents.
|
||||
|
||||
3. The repairer sees this request and responds with the correct hash only if any element of the `(slot, hash)` vector is both `duplicate_confirmed` and the hash doesn't match the requester's hash in the vector.
|
||||
|
||||
4. Once the requester sees the "correct" hash is different than their frozen hash, they dump the block so that they can accept a new block, and ask the network for the block with the correct hash.
|
||||
|
||||
Of course the repairer might lie to you, and you'll get the wrong version of the block, in which case you'll end up with another dead block and repeat the procedure.
|
@@ -1,29 +0,0 @@
|
||||
[package]
|
||||
name = "solana-bucket-map"
|
||||
version = "1.10.0"
|
||||
description = "solana-bucket-map"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-bucket-map"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
license = "Apache-2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rayon = "1.5.0"
|
||||
solana-logger = { path = "../logger", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
memmap2 = "0.5.0"
|
||||
log = { version = "0.4.11" }
|
||||
solana-measure = { path = "../measure", version = "=1.10.0" }
|
||||
rand = "0.7.0"
|
||||
fs_extra = "1.2.0"
|
||||
tempfile = "3.2.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_bucket_map"
|
||||
|
||||
[[bench]]
|
||||
name = "bucket_map"
|
@@ -1,77 +0,0 @@
|
||||
#![feature(test)]
|
||||
|
||||
macro_rules! DEFINE_NxM_BENCH {
|
||||
($i:ident, $n:literal, $m:literal) => {
|
||||
mod $i {
|
||||
use super::*;
|
||||
|
||||
#[bench]
|
||||
fn bench_insert_baseline_hashmap(bencher: &mut Bencher) {
|
||||
do_bench_insert_baseline_hashmap(bencher, $n, $m);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_insert_bucket_map(bencher: &mut Bencher) {
|
||||
do_bench_insert_bucket_map(bencher, $n, $m);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extern crate test;
|
||||
use {
|
||||
rayon::prelude::*,
|
||||
solana_bucket_map::bucket_map::{BucketMap, BucketMapConfig},
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{collections::hash_map::HashMap, sync::RwLock},
|
||||
test::Bencher,
|
||||
};
|
||||
|
||||
type IndexValue = u64;
|
||||
|
||||
DEFINE_NxM_BENCH!(dim_01x02, 1, 2);
|
||||
DEFINE_NxM_BENCH!(dim_02x04, 2, 4);
|
||||
DEFINE_NxM_BENCH!(dim_04x08, 4, 8);
|
||||
DEFINE_NxM_BENCH!(dim_08x16, 8, 16);
|
||||
DEFINE_NxM_BENCH!(dim_16x32, 16, 32);
|
||||
DEFINE_NxM_BENCH!(dim_32x64, 32, 64);
|
||||
|
||||
/// Benchmark insert with Hashmap as baseline for N threads inserting M keys each
|
||||
fn do_bench_insert_baseline_hashmap(bencher: &mut Bencher, n: usize, m: usize) {
|
||||
let index = RwLock::new(HashMap::new());
|
||||
(0..n).into_iter().into_par_iter().for_each(|i| {
|
||||
let key = Pubkey::new_unique();
|
||||
index
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(key, vec![(i, IndexValue::default())]);
|
||||
});
|
||||
bencher.iter(|| {
|
||||
(0..n).into_iter().into_par_iter().for_each(|_| {
|
||||
for j in 0..m {
|
||||
let key = Pubkey::new_unique();
|
||||
index
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(key, vec![(j, IndexValue::default())]);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark insert with BucketMap with N buckets for N threads inserting M keys each
|
||||
fn do_bench_insert_bucket_map(bencher: &mut Bencher, n: usize, m: usize) {
|
||||
let index = BucketMap::new(BucketMapConfig::new(n));
|
||||
(0..n).into_iter().into_par_iter().for_each(|i| {
|
||||
let key = Pubkey::new_unique();
|
||||
index.update(&key, |_| Some((vec![(i, IndexValue::default())], 0)));
|
||||
});
|
||||
bencher.iter(|| {
|
||||
(0..n).into_iter().into_par_iter().for_each(|_| {
|
||||
for j in 0..m {
|
||||
let key = Pubkey::new_unique();
|
||||
index.update(&key, |_| Some((vec![(j, IndexValue::default())], 0)));
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
@@ -1,491 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
bucket_item::BucketItem,
|
||||
bucket_map::BucketMapError,
|
||||
bucket_stats::BucketMapStats,
|
||||
bucket_storage::{BucketStorage, Uid, DEFAULT_CAPACITY_POW2, UID_UNLOCKED},
|
||||
index_entry::IndexEntry,
|
||||
MaxSearch, RefCount,
|
||||
},
|
||||
rand::{thread_rng, Rng},
|
||||
solana_measure::measure::Measure,
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
ops::RangeBounds,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ReallocatedItems {
|
||||
// Some if the index was reallocated
|
||||
// u64 is random associated with the new index
|
||||
pub index: Option<(u64, BucketStorage)>,
|
||||
// Some for a data bucket reallocation
|
||||
// u64 is data bucket index
|
||||
pub data: Option<(u64, BucketStorage)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Reallocated {
|
||||
/// > 0 if reallocations are encoded
|
||||
pub active_reallocations: AtomicUsize,
|
||||
|
||||
/// actual reallocated bucket
|
||||
/// mutex because bucket grow code runs with a read lock
|
||||
pub items: Mutex<ReallocatedItems>,
|
||||
}
|
||||
|
||||
impl Reallocated {
|
||||
/// specify that a reallocation has occurred
|
||||
pub fn add_reallocation(&self) {
|
||||
assert_eq!(
|
||||
0,
|
||||
self.active_reallocations.fetch_add(1, Ordering::Relaxed),
|
||||
"Only 1 reallocation can occur at a time"
|
||||
);
|
||||
}
|
||||
/// Return true IFF a reallocation has occurred.
|
||||
/// Calling this takes conceptual ownership of the reallocation encoded in the struct.
|
||||
pub fn get_reallocated(&self) -> bool {
|
||||
self.active_reallocations
|
||||
.compare_exchange(1, 0, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
// >= 2 instances of BucketStorage per 'bucket' in the bucket map. 1 for index, >= 1 for data
|
||||
pub struct Bucket<T> {
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
//index
|
||||
pub index: BucketStorage,
|
||||
//random offset for the index
|
||||
random: u64,
|
||||
//storage buckets to store SlotSlice up to a power of 2 in len
|
||||
pub data: Vec<BucketStorage>,
|
||||
_phantom: PhantomData<T>,
|
||||
stats: Arc<BucketMapStats>,
|
||||
|
||||
pub reallocated: Reallocated,
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy> Bucket<T> {
|
||||
pub fn new(
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
max_search: MaxSearch,
|
||||
stats: Arc<BucketMapStats>,
|
||||
) -> Self {
|
||||
let index = BucketStorage::new(
|
||||
Arc::clone(&drives),
|
||||
1,
|
||||
std::mem::size_of::<IndexEntry>() as u64,
|
||||
max_search,
|
||||
Arc::clone(&stats.index),
|
||||
);
|
||||
Self {
|
||||
random: thread_rng().gen(),
|
||||
drives,
|
||||
index,
|
||||
data: vec![],
|
||||
_phantom: PhantomData::default(),
|
||||
stats,
|
||||
reallocated: Reallocated::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bucket_len(&self) -> u64 {
|
||||
self.index.used.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> Vec<Pubkey> {
|
||||
let mut rv = vec![];
|
||||
for i in 0..self.index.capacity() {
|
||||
if self.index.uid(i) == UID_UNLOCKED {
|
||||
continue;
|
||||
}
|
||||
let ix: &IndexEntry = self.index.get(i);
|
||||
rv.push(ix.key);
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn items_in_range<R>(&self, range: &Option<&R>) -> Vec<BucketItem<T>>
|
||||
where
|
||||
R: RangeBounds<Pubkey>,
|
||||
{
|
||||
let mut result = Vec::with_capacity(self.index.used.load(Ordering::Relaxed) as usize);
|
||||
for i in 0..self.index.capacity() {
|
||||
let ii = i % self.index.capacity();
|
||||
if self.index.uid(ii) == UID_UNLOCKED {
|
||||
continue;
|
||||
}
|
||||
let ix: &IndexEntry = self.index.get(ii);
|
||||
let key = ix.key;
|
||||
if range.map(|r| r.contains(&key)).unwrap_or(true) {
|
||||
let val = ix.read_value(self);
|
||||
result.push(BucketItem {
|
||||
pubkey: key,
|
||||
ref_count: ix.ref_count(),
|
||||
slot_list: val.map(|(v, _ref_count)| v.to_vec()).unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn find_entry(&self, key: &Pubkey) -> Option<(&IndexEntry, u64)> {
|
||||
Self::bucket_find_entry(&self.index, key, self.random)
|
||||
}
|
||||
|
||||
fn find_entry_mut(&self, key: &Pubkey) -> Option<(&mut IndexEntry, u64)> {
|
||||
Self::bucket_find_entry_mut(&self.index, key, self.random)
|
||||
}
|
||||
|
||||
fn bucket_find_entry_mut<'a>(
|
||||
index: &'a BucketStorage,
|
||||
key: &Pubkey,
|
||||
random: u64,
|
||||
) -> Option<(&'a mut IndexEntry, u64)> {
|
||||
let ix = Self::bucket_index_ix(index, key, random);
|
||||
for i in ix..ix + index.max_search() {
|
||||
let ii = i % index.capacity();
|
||||
if index.uid(ii) == UID_UNLOCKED {
|
||||
continue;
|
||||
}
|
||||
let elem: &mut IndexEntry = index.get_mut(ii);
|
||||
if elem.key == *key {
|
||||
return Some((elem, ii));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn bucket_find_entry<'a>(
|
||||
index: &'a BucketStorage,
|
||||
key: &Pubkey,
|
||||
random: u64,
|
||||
) -> Option<(&'a IndexEntry, u64)> {
|
||||
let ix = Self::bucket_index_ix(index, key, random);
|
||||
for i in ix..ix + index.max_search() {
|
||||
let ii = i % index.capacity();
|
||||
if index.uid(ii) == UID_UNLOCKED {
|
||||
continue;
|
||||
}
|
||||
let elem: &IndexEntry = index.get(ii);
|
||||
if elem.key == *key {
|
||||
return Some((elem, ii));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn bucket_create_key(
|
||||
index: &BucketStorage,
|
||||
key: &Pubkey,
|
||||
elem_uid: Uid,
|
||||
random: u64,
|
||||
) -> Result<u64, BucketMapError> {
|
||||
let ix = Self::bucket_index_ix(index, key, random);
|
||||
for i in ix..ix + index.max_search() {
|
||||
let ii = i as u64 % index.capacity();
|
||||
if index.uid(ii) != UID_UNLOCKED {
|
||||
continue;
|
||||
}
|
||||
index.allocate(ii, elem_uid).unwrap();
|
||||
let mut elem: &mut IndexEntry = index.get_mut(ii);
|
||||
elem.key = *key;
|
||||
// These will be overwritten after allocation by callers.
|
||||
// Since this part of the mmapped file could have previously been used by someone else, there can be garbage here.
|
||||
elem.ref_count = 0;
|
||||
elem.storage_offset = 0;
|
||||
elem.storage_capacity_when_created_pow2 = 0;
|
||||
elem.num_slots = 0;
|
||||
//debug!( "INDEX ALLOC {:?} {} {} {}", key, ii, index.capacity, elem_uid );
|
||||
return Ok(ii);
|
||||
}
|
||||
Err(BucketMapError::IndexNoSpace(index.capacity_pow2))
|
||||
}
|
||||
|
||||
pub fn addref(&mut self, key: &Pubkey) -> Option<RefCount> {
|
||||
let (elem, _) = self.find_entry_mut(key)?;
|
||||
elem.ref_count += 1;
|
||||
Some(elem.ref_count)
|
||||
}
|
||||
|
||||
pub fn unref(&mut self, key: &Pubkey) -> Option<RefCount> {
|
||||
let (elem, _) = self.find_entry_mut(key)?;
|
||||
elem.ref_count -= 1;
|
||||
Some(elem.ref_count)
|
||||
}
|
||||
|
||||
fn create_key(&self, key: &Pubkey) -> Result<u64, BucketMapError> {
|
||||
Self::bucket_create_key(&self.index, key, IndexEntry::key_uid(key), self.random)
|
||||
}
|
||||
|
||||
pub fn read_value(&self, key: &Pubkey) -> Option<(&[T], RefCount)> {
|
||||
//debug!("READ_VALUE: {:?}", key);
|
||||
let (elem, _) = self.find_entry(key)?;
|
||||
elem.read_value(self)
|
||||
}
|
||||
|
||||
pub fn try_write(
|
||||
&mut self,
|
||||
key: &Pubkey,
|
||||
data: &[T],
|
||||
ref_count: u64,
|
||||
) -> Result<(), BucketMapError> {
|
||||
let best_fit_bucket = IndexEntry::data_bucket_from_num_slots(data.len() as u64);
|
||||
if self.data.get(best_fit_bucket as usize).is_none() {
|
||||
// fail early if the data bucket we need doesn't exist - we don't want the index entry partially allocated
|
||||
return Err(BucketMapError::DataNoSpace((best_fit_bucket, 0)));
|
||||
}
|
||||
let index_entry = self.find_entry_mut(key);
|
||||
let (elem, elem_ix) = match index_entry {
|
||||
None => {
|
||||
let ii = self.create_key(key)?;
|
||||
let elem: &mut IndexEntry = self.index.get_mut(ii);
|
||||
(elem, ii)
|
||||
}
|
||||
Some(res) => res,
|
||||
};
|
||||
elem.ref_count = ref_count;
|
||||
let elem_uid = self.index.uid(elem_ix);
|
||||
let bucket_ix = elem.data_bucket_ix();
|
||||
let current_bucket = &self.data[bucket_ix as usize];
|
||||
if best_fit_bucket == bucket_ix && elem.num_slots > 0 {
|
||||
// in place update
|
||||
let elem_loc = elem.data_loc(current_bucket);
|
||||
let slice: &mut [T] = current_bucket.get_mut_cell_slice(elem_loc, data.len() as u64);
|
||||
assert!(current_bucket.uid(elem_loc) == elem_uid);
|
||||
elem.num_slots = data.len() as u64;
|
||||
slice.clone_from_slice(data);
|
||||
Ok(())
|
||||
} else {
|
||||
// need to move the allocation to a best fit spot
|
||||
let best_bucket = &self.data[best_fit_bucket as usize];
|
||||
let cap_power = best_bucket.capacity_pow2;
|
||||
let cap = best_bucket.capacity();
|
||||
let pos = thread_rng().gen_range(0, cap);
|
||||
for i in pos..pos + self.index.max_search() {
|
||||
let ix = i % cap;
|
||||
if best_bucket.uid(ix) == UID_UNLOCKED {
|
||||
let elem_loc = elem.data_loc(current_bucket);
|
||||
if elem.num_slots > 0 {
|
||||
current_bucket.free(elem_loc, elem_uid);
|
||||
}
|
||||
elem.storage_offset = ix;
|
||||
elem.storage_capacity_when_created_pow2 = best_bucket.capacity_pow2;
|
||||
elem.num_slots = data.len() as u64;
|
||||
//debug!( "DATA ALLOC {:?} {} {} {}", key, elem.data_location, best_bucket.capacity, elem_uid );
|
||||
if elem.num_slots > 0 {
|
||||
best_bucket.allocate(ix, elem_uid).unwrap();
|
||||
let slice = best_bucket.get_mut_cell_slice(ix, data.len() as u64);
|
||||
slice.copy_from_slice(data);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(BucketMapError::DataNoSpace((best_fit_bucket, cap_power)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_key(&mut self, key: &Pubkey) {
|
||||
if let Some((elem, elem_ix)) = self.find_entry(key) {
|
||||
let elem_uid = self.index.uid(elem_ix);
|
||||
if elem.num_slots > 0 {
|
||||
let data_bucket = &self.data[elem.data_bucket_ix() as usize];
|
||||
let loc = elem.data_loc(data_bucket);
|
||||
//debug!( "DATA FREE {:?} {} {} {}", key, elem.data_location, data_bucket.capacity, elem_uid );
|
||||
data_bucket.free(loc, elem_uid);
|
||||
}
|
||||
//debug!("INDEX FREE {:?} {}", key, elem_uid);
|
||||
self.index.free(elem_ix, elem_uid);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grow_index(&self, current_capacity_pow2: u8) {
|
||||
if self.index.capacity_pow2 == current_capacity_pow2 {
|
||||
let mut m = Measure::start("grow_index");
|
||||
//debug!("GROW_INDEX: {}", current_capacity_pow2);
|
||||
let increment = 1;
|
||||
for i in increment.. {
|
||||
//increasing the capacity by ^4 reduces the
|
||||
//likelyhood of a re-index collision of 2^(max_search)^2
|
||||
//1 in 2^32
|
||||
let index = BucketStorage::new_with_capacity(
|
||||
Arc::clone(&self.drives),
|
||||
1,
|
||||
std::mem::size_of::<IndexEntry>() as u64,
|
||||
// *2 causes rapid growth of index buckets
|
||||
self.index.capacity_pow2 + i, // * 2,
|
||||
self.index.max_search,
|
||||
Arc::clone(&self.stats.index),
|
||||
);
|
||||
let random = thread_rng().gen();
|
||||
let mut valid = true;
|
||||
for ix in 0..self.index.capacity() {
|
||||
let uid = self.index.uid(ix);
|
||||
if UID_UNLOCKED != uid {
|
||||
let elem: &IndexEntry = self.index.get(ix);
|
||||
let new_ix = Self::bucket_create_key(&index, &elem.key, uid, random);
|
||||
if new_ix.is_err() {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
let new_ix = new_ix.unwrap();
|
||||
let new_elem: &mut IndexEntry = index.get_mut(new_ix);
|
||||
*new_elem = *elem;
|
||||
/*
|
||||
let dbg_elem: IndexEntry = *new_elem;
|
||||
assert_eq!(
|
||||
Self::bucket_find_entry(&index, &elem.key, random).unwrap(),
|
||||
(&dbg_elem, new_ix)
|
||||
);
|
||||
*/
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
let sz = index.capacity();
|
||||
{
|
||||
let mut max = self.stats.index.max_size.lock().unwrap();
|
||||
*max = std::cmp::max(*max, sz);
|
||||
}
|
||||
let mut items = self.reallocated.items.lock().unwrap();
|
||||
items.index = Some((random, index));
|
||||
self.reallocated.add_reallocation();
|
||||
break;
|
||||
}
|
||||
}
|
||||
m.stop();
|
||||
self.stats.index.resizes.fetch_add(1, Ordering::Relaxed);
|
||||
self.stats
|
||||
.index
|
||||
.resize_us
|
||||
.fetch_add(m.as_us(), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_grow_index(&mut self, random: u64, index: BucketStorage) {
|
||||
self.random = random;
|
||||
self.index = index;
|
||||
}
|
||||
|
||||
fn elem_size() -> u64 {
|
||||
std::mem::size_of::<T>() as u64
|
||||
}
|
||||
|
||||
pub fn apply_grow_data(&mut self, ix: usize, bucket: BucketStorage) {
|
||||
if self.data.get(ix).is_none() {
|
||||
for i in self.data.len()..ix {
|
||||
// insert empty data buckets
|
||||
self.data.push(BucketStorage::new(
|
||||
Arc::clone(&self.drives),
|
||||
1 << i,
|
||||
Self::elem_size(),
|
||||
self.index.max_search,
|
||||
Arc::clone(&self.stats.data),
|
||||
))
|
||||
}
|
||||
self.data.push(bucket);
|
||||
} else {
|
||||
self.data[ix] = bucket;
|
||||
}
|
||||
}
|
||||
|
||||
/// grow a data bucket
|
||||
/// The application of the new bucket is deferred until the next write lock.
|
||||
pub fn grow_data(&self, data_index: u64, current_capacity_pow2: u8) {
|
||||
let new_bucket = BucketStorage::new_resized(
|
||||
&self.drives,
|
||||
self.index.max_search,
|
||||
self.data.get(data_index as usize),
|
||||
std::cmp::max(current_capacity_pow2 + 1, DEFAULT_CAPACITY_POW2),
|
||||
1 << data_index,
|
||||
Self::elem_size(),
|
||||
&self.stats.data,
|
||||
);
|
||||
self.reallocated.add_reallocation();
|
||||
let mut items = self.reallocated.items.lock().unwrap();
|
||||
items.data = Some((data_index, new_bucket));
|
||||
}
|
||||
|
||||
fn bucket_index_ix(index: &BucketStorage, key: &Pubkey, random: u64) -> u64 {
|
||||
let uid = IndexEntry::key_uid(key);
|
||||
let mut s = DefaultHasher::new();
|
||||
uid.hash(&mut s);
|
||||
//the locally generated random will make it hard for an attacker
|
||||
//to deterministically cause all the pubkeys to land in the same
|
||||
//location in any bucket on all validators
|
||||
random.hash(&mut s);
|
||||
let ix = s.finish();
|
||||
ix % index.capacity()
|
||||
//debug!( "INDEX_IX: {:?} uid:{} loc: {} cap:{}", key, uid, location, index.capacity() );
|
||||
}
|
||||
|
||||
/// grow the appropriate piece. Note this takes an immutable ref.
|
||||
/// The actual grow is set into self.reallocated and applied later on a write lock
|
||||
pub fn grow(&self, err: BucketMapError) {
|
||||
match err {
|
||||
BucketMapError::DataNoSpace((data_index, current_capacity_pow2)) => {
|
||||
//debug!("GROWING SPACE {:?}", (data_index, current_capacity_pow2));
|
||||
self.grow_data(data_index, current_capacity_pow2);
|
||||
}
|
||||
BucketMapError::IndexNoSpace(current_capacity_pow2) => {
|
||||
//debug!("GROWING INDEX {}", sz);
|
||||
self.grow_index(current_capacity_pow2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// if a bucket was resized previously with a read lock, then apply that resize now
|
||||
pub fn handle_delayed_grows(&mut self) {
|
||||
if self.reallocated.get_reallocated() {
|
||||
// swap out the bucket that was resized previously with a read lock
|
||||
let mut items = ReallocatedItems::default();
|
||||
std::mem::swap(&mut items, &mut self.reallocated.items.lock().unwrap());
|
||||
|
||||
if let Some((random, bucket)) = items.index.take() {
|
||||
self.apply_grow_index(random, bucket);
|
||||
} else {
|
||||
// data bucket
|
||||
let (i, new_bucket) = items.data.take().unwrap();
|
||||
self.apply_grow_data(i as usize, new_bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: &Pubkey, value: (&[T], RefCount)) {
|
||||
let (new, refct) = value;
|
||||
loop {
|
||||
let rv = self.try_write(key, new, refct);
|
||||
match rv {
|
||||
Ok(_) => return,
|
||||
Err(err) => {
|
||||
self.grow(err);
|
||||
self.handle_delayed_grows();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update<F>(&mut self, key: &Pubkey, mut updatefn: F)
|
||||
where
|
||||
F: FnMut(Option<(&[T], RefCount)>) -> Option<(Vec<T>, RefCount)>,
|
||||
{
|
||||
let current = self.read_value(key);
|
||||
let new = updatefn(current);
|
||||
if new.is_none() {
|
||||
self.delete_key(key);
|
||||
return;
|
||||
}
|
||||
let (new, refct) = new.unwrap();
|
||||
self.insert(key, (&new, refct));
|
||||
}
|
||||
}
|
@@ -1,148 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
bucket::Bucket, bucket_item::BucketItem, bucket_map::BucketMapError,
|
||||
bucket_stats::BucketMapStats, MaxSearch, RefCount,
|
||||
},
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{
|
||||
ops::RangeBounds,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc, RwLock, RwLockWriteGuard,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type LockedBucket<T> = RwLock<Option<Bucket<T>>>;
|
||||
|
||||
pub struct BucketApi<T: Clone + Copy> {
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
max_search: MaxSearch,
|
||||
pub stats: Arc<BucketMapStats>,
|
||||
|
||||
bucket: LockedBucket<T>,
|
||||
count: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy> BucketApi<T> {
|
||||
pub fn new(
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
max_search: MaxSearch,
|
||||
stats: Arc<BucketMapStats>,
|
||||
count: Arc<AtomicU64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
drives,
|
||||
max_search,
|
||||
stats,
|
||||
bucket: RwLock::default(),
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the items for bucket
|
||||
pub fn items_in_range<R>(&self, range: &Option<&R>) -> Vec<BucketItem<T>>
|
||||
where
|
||||
R: RangeBounds<Pubkey>,
|
||||
{
|
||||
self.bucket
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|bucket| bucket.items_in_range(range))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the Pubkeys
|
||||
pub fn keys(&self) -> Vec<Pubkey> {
|
||||
self.bucket
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map_or_else(Vec::default, |bucket| bucket.keys())
|
||||
}
|
||||
|
||||
/// Get the values for Pubkey `key`
|
||||
pub fn read_value(&self, key: &Pubkey) -> Option<(Vec<T>, RefCount)> {
|
||||
self.bucket.read().unwrap().as_ref().and_then(|bucket| {
|
||||
bucket
|
||||
.read_value(key)
|
||||
.map(|(value, ref_count)| (value.to_vec(), ref_count))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bucket_len(&self) -> u64 {
|
||||
self.bucket
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|bucket| bucket.bucket_len())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn delete_key(&self, key: &Pubkey) {
|
||||
let mut bucket = self.get_write_bucket();
|
||||
if let Some(bucket) = bucket.as_mut() {
|
||||
bucket.delete_key(key)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_write_bucket(&self) -> RwLockWriteGuard<Option<Bucket<T>>> {
|
||||
let mut bucket = self.bucket.write().unwrap();
|
||||
if bucket.is_none() {
|
||||
*bucket = Some(Bucket::new(
|
||||
Arc::clone(&self.drives),
|
||||
self.max_search,
|
||||
Arc::clone(&self.stats),
|
||||
));
|
||||
} else {
|
||||
let write = bucket.as_mut().unwrap();
|
||||
write.handle_delayed_grows();
|
||||
self.count.store(write.bucket_len(), Ordering::Relaxed);
|
||||
}
|
||||
bucket
|
||||
}
|
||||
|
||||
pub fn addref(&self, key: &Pubkey) -> Option<RefCount> {
|
||||
self.get_write_bucket()
|
||||
.as_mut()
|
||||
.and_then(|bucket| bucket.addref(key))
|
||||
}
|
||||
|
||||
pub fn unref(&self, key: &Pubkey) -> Option<RefCount> {
|
||||
self.get_write_bucket()
|
||||
.as_mut()
|
||||
.and_then(|bucket| bucket.unref(key))
|
||||
}
|
||||
|
||||
pub fn insert(&self, pubkey: &Pubkey, value: (&[T], RefCount)) {
|
||||
let mut bucket = self.get_write_bucket();
|
||||
bucket.as_mut().unwrap().insert(pubkey, value)
|
||||
}
|
||||
|
||||
pub fn grow(&self, err: BucketMapError) {
|
||||
// grows are special - they get a read lock and modify 'reallocated'
|
||||
// the grown changes are applied the next time there is a write lock taken
|
||||
if let Some(bucket) = self.bucket.read().unwrap().as_ref() {
|
||||
bucket.grow(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update<F>(&self, key: &Pubkey, updatefn: F)
|
||||
where
|
||||
F: FnMut(Option<(&[T], RefCount)>) -> Option<(Vec<T>, RefCount)>,
|
||||
{
|
||||
let mut bucket = self.get_write_bucket();
|
||||
bucket.as_mut().unwrap().update(key, updatefn)
|
||||
}
|
||||
|
||||
pub fn try_write(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
value: (&[T], RefCount),
|
||||
) -> Result<(), BucketMapError> {
|
||||
let mut bucket = self.get_write_bucket();
|
||||
bucket.as_mut().unwrap().try_write(pubkey, value.0, value.1)
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
use {crate::RefCount, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct BucketItem<T> {
|
||||
pub pubkey: Pubkey,
|
||||
pub ref_count: RefCount,
|
||||
pub slot_list: Vec<T>,
|
||||
}
|
@@ -1,529 +0,0 @@
|
||||
//! BucketMap is a mostly contention free concurrent map backed by MmapMut
|
||||
|
||||
use {
|
||||
crate::{bucket_api::BucketApi, bucket_stats::BucketMapStats, MaxSearch, RefCount},
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{convert::TryInto, fmt::Debug, fs, path::PathBuf, sync::Arc},
|
||||
tempfile::TempDir,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct BucketMapConfig {
|
||||
pub max_buckets: usize,
|
||||
pub drives: Option<Vec<PathBuf>>,
|
||||
pub max_search: Option<MaxSearch>,
|
||||
}
|
||||
|
||||
impl BucketMapConfig {
|
||||
/// Create a new BucketMapConfig
|
||||
/// NOTE: BucketMap requires that max_buckets is a power of two
|
||||
pub fn new(max_buckets: usize) -> BucketMapConfig {
|
||||
BucketMapConfig {
|
||||
max_buckets,
|
||||
..BucketMapConfig::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BucketMap<T: Clone + Copy + Debug> {
|
||||
buckets: Vec<Arc<BucketApi<T>>>,
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
max_buckets_pow2: u8,
|
||||
pub stats: Arc<BucketMapStats>,
|
||||
pub temp_dir: Option<TempDir>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy + Debug> Drop for BucketMap<T> {
|
||||
fn drop(&mut self) {
|
||||
if self.temp_dir.is_none() {
|
||||
BucketMap::<T>::erase_previous_drives(&self.drives);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy + Debug> std::fmt::Debug for BucketMap<T> {
|
||||
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BucketMapError {
|
||||
/// (bucket_index, current_capacity_pow2)
|
||||
DataNoSpace((u64, u8)),
|
||||
/// current_capacity_pow2
|
||||
IndexNoSpace(u8),
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy + Debug> BucketMap<T> {
|
||||
pub fn new(config: BucketMapConfig) -> Self {
|
||||
assert_ne!(
|
||||
config.max_buckets, 0,
|
||||
"Max number of buckets must be non-zero"
|
||||
);
|
||||
assert!(
|
||||
config.max_buckets.is_power_of_two(),
|
||||
"Max number of buckets must be a power of two"
|
||||
);
|
||||
// this should be <= 1 << DEFAULT_CAPACITY or we end up searching the same items over and over - probably not a big deal since it is so small anyway
|
||||
const MAX_SEARCH: MaxSearch = 32;
|
||||
let max_search = config.max_search.unwrap_or(MAX_SEARCH);
|
||||
|
||||
if let Some(drives) = config.drives.as_ref() {
|
||||
Self::erase_previous_drives(drives);
|
||||
}
|
||||
let mut temp_dir = None;
|
||||
let drives = config.drives.unwrap_or_else(|| {
|
||||
temp_dir = Some(TempDir::new().unwrap());
|
||||
vec![temp_dir.as_ref().unwrap().path().to_path_buf()]
|
||||
});
|
||||
let drives = Arc::new(drives);
|
||||
|
||||
let mut per_bucket_count = Vec::with_capacity(config.max_buckets);
|
||||
per_bucket_count.resize_with(config.max_buckets, Arc::default);
|
||||
let stats = Arc::new(BucketMapStats {
|
||||
per_bucket_count,
|
||||
..BucketMapStats::default()
|
||||
});
|
||||
let buckets = stats
|
||||
.per_bucket_count
|
||||
.iter()
|
||||
.map(|per_bucket_count| {
|
||||
Arc::new(BucketApi::new(
|
||||
Arc::clone(&drives),
|
||||
max_search,
|
||||
Arc::clone(&stats),
|
||||
Arc::clone(per_bucket_count),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// A simple log2 function that is correct if x is a power of two
|
||||
let log2 = |x: usize| usize::BITS - x.leading_zeros() - 1;
|
||||
|
||||
Self {
|
||||
buckets,
|
||||
drives,
|
||||
max_buckets_pow2: log2(config.max_buckets) as u8,
|
||||
stats,
|
||||
temp_dir,
|
||||
}
|
||||
}
|
||||
|
||||
fn erase_previous_drives(drives: &[PathBuf]) {
|
||||
drives.iter().for_each(|folder| {
|
||||
let _ = fs::remove_dir_all(&folder);
|
||||
let _ = fs::create_dir_all(&folder);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn num_buckets(&self) -> usize {
|
||||
self.buckets.len()
|
||||
}
|
||||
|
||||
/// Get the values for Pubkey `key`
|
||||
pub fn read_value(&self, key: &Pubkey) -> Option<(Vec<T>, RefCount)> {
|
||||
self.get_bucket(key).read_value(key)
|
||||
}
|
||||
|
||||
/// Delete the Pubkey `key`
|
||||
pub fn delete_key(&self, key: &Pubkey) {
|
||||
self.get_bucket(key).delete_key(key);
|
||||
}
|
||||
|
||||
/// Update Pubkey `key`'s value with 'value'
|
||||
pub fn insert(&self, key: &Pubkey, value: (&[T], RefCount)) {
|
||||
self.get_bucket(key).insert(key, value)
|
||||
}
|
||||
|
||||
/// Update Pubkey `key`'s value with 'value'
|
||||
pub fn try_insert(&self, key: &Pubkey, value: (&[T], RefCount)) -> Result<(), BucketMapError> {
|
||||
self.get_bucket(key).try_write(key, value)
|
||||
}
|
||||
|
||||
/// Update Pubkey `key`'s value with function `updatefn`
|
||||
pub fn update<F>(&self, key: &Pubkey, updatefn: F)
|
||||
where
|
||||
F: FnMut(Option<(&[T], RefCount)>) -> Option<(Vec<T>, RefCount)>,
|
||||
{
|
||||
self.get_bucket(key).update(key, updatefn)
|
||||
}
|
||||
|
||||
pub fn get_bucket(&self, key: &Pubkey) -> &Arc<BucketApi<T>> {
|
||||
self.get_bucket_from_index(self.bucket_ix(key))
|
||||
}
|
||||
|
||||
pub fn get_bucket_from_index(&self, ix: usize) -> &Arc<BucketApi<T>> {
|
||||
&self.buckets[ix]
|
||||
}
|
||||
|
||||
/// Get the bucket index for Pubkey `key`
|
||||
pub fn bucket_ix(&self, key: &Pubkey) -> usize {
|
||||
if self.max_buckets_pow2 > 0 {
|
||||
let location = read_be_u64(key.as_ref());
|
||||
(location >> (u64::BITS - self.max_buckets_pow2 as u32)) as usize
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the refcount for Pubkey `key`
|
||||
pub fn addref(&self, key: &Pubkey) -> Option<RefCount> {
|
||||
let ix = self.bucket_ix(key);
|
||||
let bucket = &self.buckets[ix];
|
||||
bucket.addref(key)
|
||||
}
|
||||
|
||||
/// Decrement the refcount for Pubkey `key`
|
||||
pub fn unref(&self, key: &Pubkey) -> Option<RefCount> {
|
||||
let ix = self.bucket_ix(key);
|
||||
let bucket = &self.buckets[ix];
|
||||
bucket.unref(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Look at the first 8 bytes of the input and reinterpret them as a u64
|
||||
fn read_be_u64(input: &[u8]) -> u64 {
|
||||
assert!(input.len() >= std::mem::size_of::<u64>());
|
||||
u64::from_be_bytes(input[0..std::mem::size_of::<u64>()].try_into().unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::*,
|
||||
rand::{thread_rng, Rng},
|
||||
std::{collections::HashMap, sync::RwLock},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_insert() {
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
index.update(&key, |_| Some((vec![0], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_insert2() {
|
||||
for pass in 0..3 {
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
let bucket = index.get_bucket(&key);
|
||||
if pass == 0 {
|
||||
index.insert(&key, (&[0], 0));
|
||||
} else {
|
||||
let result = index.try_insert(&key, (&[0], 0));
|
||||
assert!(result.is_err());
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
if pass == 2 {
|
||||
// another call to try insert again - should still return an error
|
||||
let result = index.try_insert(&key, (&[0], 0));
|
||||
assert!(result.is_err());
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
}
|
||||
bucket.grow(result.unwrap_err());
|
||||
let result = index.try_insert(&key, (&[0], 0));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 0)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_update2() {
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
index.insert(&key, (&[0], 0));
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 0)));
|
||||
index.insert(&key, (&[1], 0));
|
||||
assert_eq!(index.read_value(&key), Some((vec![1], 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_update() {
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
index.update(&key, |_| Some((vec![0], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 0)));
|
||||
index.update(&key, |_| Some((vec![1], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![1], 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_update_to_0_len() {
|
||||
solana_logger::setup();
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
index.update(&key, |_| Some((vec![0], 1)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 1)));
|
||||
// sets len to 0, updates in place
|
||||
index.update(&key, |_| Some((vec![], 1)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![], 1)));
|
||||
// sets len to 0, doesn't update in place - finds a new place, which causes us to no longer have an allocation in data
|
||||
index.update(&key, |_| Some((vec![], 2)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![], 2)));
|
||||
// sets len to 1, doesn't update in place - finds a new place
|
||||
index.update(&key, |_| Some((vec![1], 2)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![1], 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_delete() {
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
for i in 0..10 {
|
||||
let key = Pubkey::new_unique();
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
|
||||
index.delete_key(&key);
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
index.delete_key(&key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_delete_2() {
|
||||
let config = BucketMapConfig::new(1 << 2);
|
||||
let index = BucketMap::new(config);
|
||||
for i in 0..100 {
|
||||
let key = Pubkey::new_unique();
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
|
||||
index.delete_key(&key);
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
index.delete_key(&key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_n_drives() {
|
||||
let config = BucketMapConfig::new(1 << 2);
|
||||
let index = BucketMap::new(config);
|
||||
for i in 0..100 {
|
||||
let key = Pubkey::new_unique();
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn bucket_map_test_grow_read() {
|
||||
let config = BucketMapConfig::new(1 << 2);
|
||||
let index = BucketMap::new(config);
|
||||
let keys: Vec<Pubkey> = (0..100).into_iter().map(|_| Pubkey::new_unique()).collect();
|
||||
for k in 0..keys.len() {
|
||||
let key = &keys[k];
|
||||
let i = read_be_u64(key.as_ref());
|
||||
index.update(key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(key), Some((vec![i], 0)));
|
||||
for (ix, key) in keys.iter().enumerate() {
|
||||
let i = read_be_u64(key.as_ref());
|
||||
//debug!("READ: {:?} {}", key, i);
|
||||
let expected = if ix <= k { Some((vec![i], 0)) } else { None };
|
||||
assert_eq!(index.read_value(key), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_n_delete() {
|
||||
let config = BucketMapConfig::new(1 << 2);
|
||||
let index = BucketMap::new(config);
|
||||
let keys: Vec<Pubkey> = (0..20).into_iter().map(|_| Pubkey::new_unique()).collect();
|
||||
for key in keys.iter() {
|
||||
let i = read_be_u64(key.as_ref());
|
||||
index.update(key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(key), Some((vec![i], 0)));
|
||||
}
|
||||
for key in keys.iter() {
|
||||
let i = read_be_u64(key.as_ref());
|
||||
//debug!("READ: {:?} {}", key, i);
|
||||
assert_eq!(index.read_value(key), Some((vec![i], 0)));
|
||||
}
|
||||
for k in 0..keys.len() {
|
||||
let key = &keys[k];
|
||||
index.delete_key(key);
|
||||
assert_eq!(index.read_value(key), None);
|
||||
for key in keys.iter().skip(k + 1) {
|
||||
let i = read_be_u64(key.as_ref());
|
||||
assert_eq!(index.read_value(key), Some((vec![i], 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashmap_compare() {
|
||||
use std::sync::Mutex;
|
||||
solana_logger::setup();
|
||||
let maps = (0..2)
|
||||
.into_iter()
|
||||
.map(|max_buckets_pow2| {
|
||||
let config = BucketMapConfig::new(1 << max_buckets_pow2);
|
||||
BucketMap::new(config)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let hash_map = RwLock::new(HashMap::<Pubkey, (Vec<(usize, usize)>, RefCount)>::new());
|
||||
let max_slot_list_len = 3;
|
||||
let all_keys = Mutex::new(vec![]);
|
||||
|
||||
let gen_rand_value = || {
|
||||
let count = thread_rng().gen_range(0, max_slot_list_len);
|
||||
let v = (0..count)
|
||||
.into_iter()
|
||||
.map(|x| (x as usize, x as usize /*thread_rng().gen::<usize>()*/))
|
||||
.collect::<Vec<_>>();
|
||||
let rc = thread_rng().gen::<RefCount>();
|
||||
(v, rc)
|
||||
};
|
||||
|
||||
let get_key = || {
|
||||
let mut keys = all_keys.lock().unwrap();
|
||||
if keys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let len = keys.len();
|
||||
Some(keys.remove(thread_rng().gen_range(0, len)))
|
||||
};
|
||||
let return_key = |key| {
|
||||
let mut keys = all_keys.lock().unwrap();
|
||||
keys.push(key);
|
||||
};
|
||||
|
||||
let verify = || {
|
||||
let mut maps = maps
|
||||
.iter()
|
||||
.map(|map| {
|
||||
let mut r = vec![];
|
||||
for bin in 0..map.num_buckets() {
|
||||
r.append(
|
||||
&mut map.buckets[bin]
|
||||
.items_in_range(&None::<&std::ops::RangeInclusive<Pubkey>>),
|
||||
);
|
||||
}
|
||||
r
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let hm = hash_map.read().unwrap();
|
||||
for (k, v) in hm.iter() {
|
||||
for map in maps.iter_mut() {
|
||||
for i in 0..map.len() {
|
||||
if k == &map[i].pubkey {
|
||||
assert_eq!(map[i].slot_list, v.0);
|
||||
assert_eq!(map[i].ref_count, v.1);
|
||||
map.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for map in maps.iter() {
|
||||
assert!(map.is_empty());
|
||||
}
|
||||
};
|
||||
let mut initial = 100; // put this many items in to start
|
||||
|
||||
// do random operations: insert, update, delete, add/unref in random order
|
||||
// verify consistency between hashmap and all bucket maps
|
||||
for i in 0..10000 {
|
||||
if initial > 0 {
|
||||
initial -= 1;
|
||||
}
|
||||
if initial > 0 || thread_rng().gen_range(0, 5) == 0 {
|
||||
// insert
|
||||
let k = solana_sdk::pubkey::new_rand();
|
||||
let v = gen_rand_value();
|
||||
hash_map.write().unwrap().insert(k, v.clone());
|
||||
let insert = thread_rng().gen_range(0, 2) == 0;
|
||||
maps.iter().for_each(|map| {
|
||||
if insert {
|
||||
map.insert(&k, (&v.0, v.1))
|
||||
} else {
|
||||
map.update(&k, |current| {
|
||||
assert!(current.is_none());
|
||||
Some(v.clone())
|
||||
})
|
||||
}
|
||||
});
|
||||
return_key(k);
|
||||
}
|
||||
if thread_rng().gen_range(0, 10) == 0 {
|
||||
// update
|
||||
if let Some(k) = get_key() {
|
||||
let hm = hash_map.read().unwrap();
|
||||
let (v, rc) = gen_rand_value();
|
||||
let v_old = hm.get(&k);
|
||||
let insert = thread_rng().gen_range(0, 2) == 0;
|
||||
maps.iter().for_each(|map| {
|
||||
if insert {
|
||||
map.insert(&k, (&v, rc))
|
||||
} else {
|
||||
map.update(&k, |current| {
|
||||
assert_eq!(current, v_old.map(|(v, rc)| (&v[..], *rc)), "{}", k);
|
||||
Some((v.clone(), rc))
|
||||
})
|
||||
}
|
||||
});
|
||||
drop(hm);
|
||||
hash_map.write().unwrap().insert(k, (v, rc));
|
||||
return_key(k);
|
||||
}
|
||||
}
|
||||
if thread_rng().gen_range(0, 20) == 0 {
|
||||
// delete
|
||||
if let Some(k) = get_key() {
|
||||
let mut hm = hash_map.write().unwrap();
|
||||
hm.remove(&k);
|
||||
maps.iter().for_each(|map| {
|
||||
map.delete_key(&k);
|
||||
});
|
||||
}
|
||||
}
|
||||
if thread_rng().gen_range(0, 10) == 0 {
|
||||
// add/unref
|
||||
if let Some(k) = get_key() {
|
||||
let mut inc = thread_rng().gen_range(0, 2) == 0;
|
||||
let mut hm = hash_map.write().unwrap();
|
||||
let (v, mut rc) = hm.get(&k).map(|(v, rc)| (v.to_vec(), *rc)).unwrap();
|
||||
if !inc && rc == 0 {
|
||||
// can't decrement rc=0
|
||||
inc = true;
|
||||
}
|
||||
rc = if inc { rc + 1 } else { rc - 1 };
|
||||
hm.insert(k, (v.to_vec(), rc));
|
||||
maps.iter().for_each(|map| {
|
||||
if thread_rng().gen_range(0, 2) == 0 {
|
||||
map.update(&k, |current| Some((current.unwrap().0.to_vec(), rc)))
|
||||
} else if inc {
|
||||
map.addref(&k);
|
||||
} else {
|
||||
map.unref(&k);
|
||||
}
|
||||
});
|
||||
|
||||
return_key(k);
|
||||
}
|
||||
}
|
||||
if i % 1000 == 0 {
|
||||
verify();
|
||||
}
|
||||
}
|
||||
verify();
|
||||
}
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
use std::sync::{atomic::AtomicU64, Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BucketStats {
|
||||
pub resizes: AtomicU64,
|
||||
pub max_size: Mutex<u64>,
|
||||
pub resize_us: AtomicU64,
|
||||
pub new_file_us: AtomicU64,
|
||||
pub flush_file_us: AtomicU64,
|
||||
pub mmap_us: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BucketMapStats {
|
||||
pub index: Arc<BucketStats>,
|
||||
pub data: Arc<BucketStats>,
|
||||
pub per_bucket_count: Vec<Arc<AtomicU64>>,
|
||||
}
|
@@ -1,343 +0,0 @@
|
||||
use {
|
||||
crate::{bucket_stats::BucketStats, MaxSearch},
|
||||
memmap2::MmapMut,
|
||||
rand::{thread_rng, Rng},
|
||||
solana_measure::measure::Measure,
|
||||
std::{
|
||||
fs::{remove_file, OpenOptions},
|
||||
io::{Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
1 2
|
||||
2 4
|
||||
3 8
|
||||
4 16
|
||||
5 32
|
||||
6 64
|
||||
7 128
|
||||
8 256
|
||||
9 512
|
||||
10 1,024
|
||||
11 2,048
|
||||
12 4,096
|
||||
13 8,192
|
||||
14 16,384
|
||||
23 8,388,608
|
||||
24 16,777,216
|
||||
*/
|
||||
pub const DEFAULT_CAPACITY_POW2: u8 = 5;
|
||||
|
||||
/// A Header UID of 0 indicates that the header is unlocked
|
||||
pub(crate) const UID_UNLOCKED: Uid = 0;
|
||||
|
||||
pub(crate) type Uid = u64;
|
||||
|
||||
#[repr(C)]
|
||||
struct Header {
|
||||
lock: AtomicU64,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
fn try_lock(&self, uid: Uid) -> bool {
|
||||
Ok(UID_UNLOCKED)
|
||||
== self
|
||||
.lock
|
||||
.compare_exchange(UID_UNLOCKED, uid, Ordering::AcqRel, Ordering::Relaxed)
|
||||
}
|
||||
fn unlock(&self) -> Uid {
|
||||
self.lock.swap(UID_UNLOCKED, Ordering::Release)
|
||||
}
|
||||
fn uid(&self) -> Uid {
|
||||
self.lock.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BucketStorage {
|
||||
path: PathBuf,
|
||||
mmap: MmapMut,
|
||||
pub cell_size: u64,
|
||||
pub capacity_pow2: u8,
|
||||
pub used: AtomicU64,
|
||||
pub stats: Arc<BucketStats>,
|
||||
pub max_search: MaxSearch,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BucketStorageError {
|
||||
AlreadyAllocated,
|
||||
}
|
||||
|
||||
impl Drop for BucketStorage {
|
||||
fn drop(&mut self) {
|
||||
let _ = remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
impl BucketStorage {
|
||||
pub fn new_with_capacity(
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
num_elems: u64,
|
||||
elem_size: u64,
|
||||
capacity_pow2: u8,
|
||||
max_search: MaxSearch,
|
||||
stats: Arc<BucketStats>,
|
||||
) -> Self {
|
||||
let cell_size = elem_size * num_elems + std::mem::size_of::<Header>() as u64;
|
||||
let (mmap, path) = Self::new_map(&drives, cell_size as usize, capacity_pow2, &stats);
|
||||
Self {
|
||||
path,
|
||||
mmap,
|
||||
cell_size,
|
||||
used: AtomicU64::new(0),
|
||||
capacity_pow2,
|
||||
stats,
|
||||
max_search,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_search(&self) -> u64 {
|
||||
self.max_search as u64
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
num_elems: u64,
|
||||
elem_size: u64,
|
||||
max_search: MaxSearch,
|
||||
stats: Arc<BucketStats>,
|
||||
) -> Self {
|
||||
Self::new_with_capacity(
|
||||
drives,
|
||||
num_elems,
|
||||
elem_size,
|
||||
DEFAULT_CAPACITY_POW2,
|
||||
max_search,
|
||||
stats,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn uid(&self, ix: u64) -> Uid {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let ix = (ix * self.cell_size) as usize;
|
||||
let hdr_slice: &[u8] = &self.mmap[ix..ix + std::mem::size_of::<Header>()];
|
||||
unsafe {
|
||||
let hdr = hdr_slice.as_ptr() as *const Header;
|
||||
return hdr.as_ref().unwrap().uid();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(&self, ix: u64, uid: Uid) -> Result<(), BucketStorageError> {
|
||||
assert!(ix < self.capacity(), "allocate: bad index size");
|
||||
assert!(UID_UNLOCKED != uid, "allocate: bad uid");
|
||||
let mut e = Err(BucketStorageError::AlreadyAllocated);
|
||||
let ix = (ix * self.cell_size) as usize;
|
||||
//debug!("ALLOC {} {}", ix, uid);
|
||||
let hdr_slice: &[u8] = &self.mmap[ix..ix + std::mem::size_of::<Header>()];
|
||||
unsafe {
|
||||
let hdr = hdr_slice.as_ptr() as *const Header;
|
||||
if hdr.as_ref().unwrap().try_lock(uid) {
|
||||
e = Ok(());
|
||||
self.used.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
};
|
||||
e
|
||||
}
|
||||
|
||||
pub fn free(&self, ix: u64, uid: Uid) {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
assert!(UID_UNLOCKED != uid, "free: bad uid");
|
||||
let ix = (ix * self.cell_size) as usize;
|
||||
//debug!("FREE {} {}", ix, uid);
|
||||
let hdr_slice: &[u8] = &self.mmap[ix..ix + std::mem::size_of::<Header>()];
|
||||
unsafe {
|
||||
let hdr = hdr_slice.as_ptr() as *const Header;
|
||||
//debug!("FREE uid: {}", hdr.as_ref().unwrap().uid());
|
||||
let previous_uid = hdr.as_ref().unwrap().unlock();
|
||||
assert_eq!(
|
||||
previous_uid, uid,
|
||||
"free: unlocked a header with a differet uid: {}",
|
||||
previous_uid
|
||||
);
|
||||
self.used.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<T: Sized>(&self, ix: u64) -> &T {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let start = (ix * self.cell_size) as usize + std::mem::size_of::<Header>();
|
||||
let end = start + std::mem::size_of::<T>();
|
||||
let item_slice: &[u8] = &self.mmap[start..end];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *const T;
|
||||
&*item
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_empty_cell_slice<T: Sized>(&self) -> &[T] {
|
||||
let len = 0;
|
||||
let item_slice: &[u8] = &self.mmap[0..0];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *const T;
|
||||
std::slice::from_raw_parts(item, len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cell_slice<T: Sized>(&self, ix: u64, len: u64) -> &[T] {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let ix = self.cell_size * ix;
|
||||
let start = ix as usize + std::mem::size_of::<Header>();
|
||||
let end = start + std::mem::size_of::<T>() * len as usize;
|
||||
//debug!("GET slice {} {}", start, end);
|
||||
let item_slice: &[u8] = &self.mmap[start..end];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *const T;
|
||||
std::slice::from_raw_parts(item, len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub fn get_mut<T: Sized>(&self, ix: u64) -> &mut T {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let start = (ix * self.cell_size) as usize + std::mem::size_of::<Header>();
|
||||
let end = start + std::mem::size_of::<T>();
|
||||
let item_slice: &[u8] = &self.mmap[start..end];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *mut T;
|
||||
&mut *item
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub fn get_mut_cell_slice<T: Sized>(&self, ix: u64, len: u64) -> &mut [T] {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let ix = self.cell_size * ix;
|
||||
let start = ix as usize + std::mem::size_of::<Header>();
|
||||
let end = start + std::mem::size_of::<T>() * len as usize;
|
||||
//debug!("GET mut slice {} {}", start, end);
|
||||
let item_slice: &[u8] = &self.mmap[start..end];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *mut T;
|
||||
std::slice::from_raw_parts_mut(item, len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
fn new_map(
|
||||
drives: &[PathBuf],
|
||||
cell_size: usize,
|
||||
capacity_pow2: u8,
|
||||
stats: &BucketStats,
|
||||
) -> (MmapMut, PathBuf) {
|
||||
let mut measure_new_file = Measure::start("measure_new_file");
|
||||
let capacity = 1u64 << capacity_pow2;
|
||||
let r = thread_rng().gen_range(0, drives.len());
|
||||
let drive = &drives[r];
|
||||
let pos = format!("{}", thread_rng().gen_range(0, u128::MAX),);
|
||||
let file = drive.join(pos);
|
||||
let mut data = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(file.clone())
|
||||
.map_err(|e| {
|
||||
panic!(
|
||||
"Unable to create data file {} in current dir({:?}): {:?}",
|
||||
file.display(),
|
||||
std::env::current_dir(),
|
||||
e
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Theoretical performance optimization: write a zero to the end of
|
||||
// the file so that we won't have to resize it later, which may be
|
||||
// expensive.
|
||||
//debug!("GROWING file {}", capacity * cell_size as u64);
|
||||
data.seek(SeekFrom::Start(capacity * cell_size as u64 - 1))
|
||||
.unwrap();
|
||||
data.write_all(&[0]).unwrap();
|
||||
data.seek(SeekFrom::Start(0)).unwrap();
|
||||
measure_new_file.stop();
|
||||
let mut measure_flush = Measure::start("measure_flush");
|
||||
data.flush().unwrap(); // can we skip this?
|
||||
measure_flush.stop();
|
||||
let mut measure_mmap = Measure::start("measure_mmap");
|
||||
let res = (unsafe { MmapMut::map_mut(&data).unwrap() }, file);
|
||||
measure_mmap.stop();
|
||||
stats
|
||||
.new_file_us
|
||||
.fetch_add(measure_new_file.as_us(), Ordering::Relaxed);
|
||||
stats
|
||||
.flush_file_us
|
||||
.fetch_add(measure_flush.as_us(), Ordering::Relaxed);
|
||||
stats
|
||||
.mmap_us
|
||||
.fetch_add(measure_mmap.as_us(), Ordering::Relaxed);
|
||||
res
|
||||
}
|
||||
|
||||
/// copy contents from 'old_bucket' to 'self'
|
||||
fn copy_contents(&mut self, old_bucket: &Self) {
|
||||
let mut m = Measure::start("grow");
|
||||
let old_cap = old_bucket.capacity();
|
||||
let old_map = &old_bucket.mmap;
|
||||
|
||||
let increment = self.capacity_pow2 - old_bucket.capacity_pow2;
|
||||
let index_grow = 1 << increment;
|
||||
(0..old_cap as usize).into_iter().for_each(|i| {
|
||||
let old_ix = i * old_bucket.cell_size as usize;
|
||||
let new_ix = old_ix * index_grow;
|
||||
let dst_slice: &[u8] = &self.mmap[new_ix..new_ix + old_bucket.cell_size as usize];
|
||||
let src_slice: &[u8] = &old_map[old_ix..old_ix + old_bucket.cell_size as usize];
|
||||
|
||||
unsafe {
|
||||
let dst = dst_slice.as_ptr() as *mut u8;
|
||||
let src = src_slice.as_ptr() as *const u8;
|
||||
std::ptr::copy_nonoverlapping(src, dst, old_bucket.cell_size as usize);
|
||||
};
|
||||
});
|
||||
m.stop();
|
||||
self.stats.resizes.fetch_add(1, Ordering::Relaxed);
|
||||
self.stats.resize_us.fetch_add(m.as_us(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// allocate a new bucket, copying data from 'bucket'
|
||||
pub fn new_resized(
|
||||
drives: &Arc<Vec<PathBuf>>,
|
||||
max_search: MaxSearch,
|
||||
bucket: Option<&Self>,
|
||||
capacity_pow_2: u8,
|
||||
num_elems: u64,
|
||||
elem_size: u64,
|
||||
stats: &Arc<BucketStats>,
|
||||
) -> Self {
|
||||
let mut new_bucket = Self::new_with_capacity(
|
||||
Arc::clone(drives),
|
||||
num_elems,
|
||||
elem_size,
|
||||
capacity_pow_2,
|
||||
max_search,
|
||||
Arc::clone(stats),
|
||||
);
|
||||
if let Some(bucket) = bucket {
|
||||
new_bucket.copy_contents(bucket);
|
||||
}
|
||||
let sz = new_bucket.capacity();
|
||||
{
|
||||
let mut max = new_bucket.stats.max_size.lock().unwrap();
|
||||
*max = std::cmp::max(*max, sz);
|
||||
}
|
||||
new_bucket
|
||||
}
|
||||
|
||||
/// Return the number of cells currently allocated
|
||||
pub fn capacity(&self) -> u64 {
|
||||
1 << self.capacity_pow2
|
||||
}
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
bucket::Bucket,
|
||||
bucket_storage::{BucketStorage, Uid},
|
||||
RefCount,
|
||||
},
|
||||
solana_sdk::{clock::Slot, pubkey::Pubkey},
|
||||
std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
},
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
// one instance of this per item in the index
|
||||
// stored in the index bucket
|
||||
pub struct IndexEntry {
|
||||
pub key: Pubkey, // can this be smaller if we have reduced the keys into buckets already?
|
||||
pub ref_count: RefCount, // can this be smaller? Do we ever need more than 4B refcounts?
|
||||
pub storage_offset: u64, // smaller? since these are variably sized, this could get tricky. well, actually accountinfo is not variable sized...
|
||||
// if the bucket doubled, the index can be recomputed using create_bucket_capacity_pow2
|
||||
pub storage_capacity_when_created_pow2: u8, // see data_location
|
||||
pub num_slots: Slot, // can this be smaller? epoch size should ~ be the max len. this is the num elements in the slot list
|
||||
}
|
||||
|
||||
impl IndexEntry {
|
||||
pub fn data_bucket_from_num_slots(num_slots: Slot) -> u64 {
|
||||
(num_slots as f64).log2().ceil() as u64 // use int log here?
|
||||
}
|
||||
|
||||
pub fn data_bucket_ix(&self) -> u64 {
|
||||
Self::data_bucket_from_num_slots(self.num_slots)
|
||||
}
|
||||
|
||||
pub fn ref_count(&self) -> RefCount {
|
||||
self.ref_count
|
||||
}
|
||||
|
||||
// This function maps the original data location into an index in the current bucket storage.
|
||||
// This is coupled with how we resize bucket storages.
|
||||
pub fn data_loc(&self, storage: &BucketStorage) -> u64 {
|
||||
self.storage_offset << (storage.capacity_pow2 - self.storage_capacity_when_created_pow2)
|
||||
}
|
||||
|
||||
pub fn read_value<'a, T>(&self, bucket: &'a Bucket<T>) -> Option<(&'a [T], RefCount)> {
|
||||
let data_bucket_ix = self.data_bucket_ix();
|
||||
let data_bucket = &bucket.data[data_bucket_ix as usize];
|
||||
let slice = if self.num_slots > 0 {
|
||||
let loc = self.data_loc(data_bucket);
|
||||
let uid = Self::key_uid(&self.key);
|
||||
assert_eq!(uid, bucket.data[data_bucket_ix as usize].uid(loc));
|
||||
bucket.data[data_bucket_ix as usize].get_cell_slice(loc, self.num_slots)
|
||||
} else {
|
||||
// num_slots is 0. This means we don't have an actual allocation.
|
||||
// can we trust that the data_bucket is even safe?
|
||||
bucket.data[data_bucket_ix as usize].get_empty_cell_slice()
|
||||
};
|
||||
Some((slice, self.ref_count))
|
||||
}
|
||||
pub fn key_uid(key: &Pubkey) -> Uid {
|
||||
let mut s = DefaultHasher::new();
|
||||
key.hash(&mut s);
|
||||
s.finish().max(1u64)
|
||||
}
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
mod bucket;
|
||||
pub mod bucket_api;
|
||||
mod bucket_item;
|
||||
pub mod bucket_map;
|
||||
mod bucket_stats;
|
||||
mod bucket_storage;
|
||||
mod index_entry;
|
||||
|
||||
pub type MaxSearch = u8;
|
||||
pub type RefCount = u64;
|
@@ -1,48 +0,0 @@
|
||||
use {
|
||||
rayon::prelude::*,
|
||||
solana_bucket_map::bucket_map::{BucketMap, BucketMapConfig},
|
||||
solana_measure::measure::Measure,
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::path::PathBuf,
|
||||
};
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bucket_map_test_mt() {
|
||||
let threads = 4096;
|
||||
let items = 4096;
|
||||
let tmpdir1 = std::env::temp_dir().join("bucket_map_test_mt");
|
||||
let tmpdir2 = PathBuf::from("/mnt/data/0").join("bucket_map_test_mt");
|
||||
let paths: Vec<PathBuf> = [tmpdir1, tmpdir2]
|
||||
.iter()
|
||||
.filter(|x| std::fs::create_dir_all(x).is_ok())
|
||||
.cloned()
|
||||
.collect();
|
||||
assert!(!paths.is_empty());
|
||||
let index = BucketMap::new(BucketMapConfig {
|
||||
max_buckets: 1 << 12,
|
||||
drives: Some(paths.clone()),
|
||||
..BucketMapConfig::default()
|
||||
});
|
||||
(0..threads).into_iter().into_par_iter().for_each(|_| {
|
||||
let key = Pubkey::new_unique();
|
||||
index.update(&key, |_| Some((vec![0u64], 0)));
|
||||
});
|
||||
let mut timer = Measure::start("bucket_map_test_mt");
|
||||
(0..threads).into_iter().into_par_iter().for_each(|_| {
|
||||
for _ in 0..items {
|
||||
let key = Pubkey::new_unique();
|
||||
let ix: u64 = index.bucket_ix(&key) as u64;
|
||||
index.update(&key, |_| Some((vec![ix], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![ix], 0)));
|
||||
}
|
||||
});
|
||||
timer.stop();
|
||||
println!("time: {}ns per item", timer.as_ns() / (threads * items));
|
||||
let mut total = 0;
|
||||
for tmpdir in paths.iter() {
|
||||
let folder_size = fs_extra::dir::get_size(tmpdir).unwrap();
|
||||
total += folder_size;
|
||||
std::fs::remove_dir_all(tmpdir).unwrap();
|
||||
}
|
||||
println!("overhead: {}bytes per item", total / (threads * items));
|
||||
}
|
@@ -226,19 +226,6 @@ EOF
|
||||
annotate --style info \
|
||||
"downstream-projects skipped as no relevant files were modified"
|
||||
fi
|
||||
|
||||
# Wasm support
|
||||
if affects \
|
||||
^ci/test-wasm.sh \
|
||||
^ci/test-stable.sh \
|
||||
^sdk/ \
|
||||
; then
|
||||
command_step wasm ". ci/rust-version.sh; ci/docker-run.sh \$\$rust_stable_docker_image ci/test-wasm.sh" 20
|
||||
else
|
||||
annotate --style info \
|
||||
"wasm skipped as no relevant files were modified"
|
||||
fi
|
||||
|
||||
# Benches...
|
||||
if affects \
|
||||
.rs$ \
|
||||
@@ -256,7 +243,15 @@ EOF
|
||||
|
||||
command_step "local-cluster" \
|
||||
". ci/rust-version.sh; ci/docker-run.sh \$\$rust_stable_docker_image ci/test-local-cluster.sh" \
|
||||
50
|
||||
40
|
||||
|
||||
command_step "local-cluster-flakey" \
|
||||
". ci/rust-version.sh; ci/docker-run.sh \$\$rust_stable_docker_image ci/test-local-cluster-flakey.sh" \
|
||||
10
|
||||
|
||||
command_step "local-cluster-slow" \
|
||||
". ci/rust-version.sh; ci/docker-run.sh \$\$rust_stable_docker_image ci/test-local-cluster-slow.sh" \
|
||||
30
|
||||
}
|
||||
|
||||
pull_or_push_steps() {
|
||||
@@ -275,7 +270,7 @@ pull_or_push_steps() {
|
||||
all_test_steps
|
||||
fi
|
||||
|
||||
# web3.js, explorer and docs changes run on Travis or Github actions...
|
||||
# web3.js, explorer and docs changes run on Travis...
|
||||
}
|
||||
|
||||
|
||||
|
@@ -19,3 +19,8 @@ steps:
|
||||
timeout_in_minutes: 240
|
||||
name: "publish crate"
|
||||
branches: "!master"
|
||||
- command: "ci/publish-tarball.sh"
|
||||
agents:
|
||||
- "queue=release-build-aarch64-apple-darwin"
|
||||
timeout_in_minutes: 60
|
||||
name: "publish tarball (aarch64-apple-darwin)"
|
||||
|
@@ -23,7 +23,7 @@ echo --- "(FAILING) Backpropagating dependabot-triggered Cargo.lock updates"
|
||||
name="dependabot-buildkite"
|
||||
api_base="https://api.github.com/repos/solana-labs/solana/pulls"
|
||||
pr_num=$(echo "$BUILDKITE_BRANCH" | grep -Eo '[0-9]+')
|
||||
branch=$(curl -s "$api_base/$pr_num" | python3 -c 'import json,sys;print(json.load(sys.stdin)["head"]["ref"])')
|
||||
branch=$(curl -s "$api_base/$pr_num" | python -c 'import json,sys;print json.load(sys.stdin)["head"]["ref"]')
|
||||
|
||||
git add :**/Cargo.lock
|
||||
EMAIL="dependabot-buildkite@noreply.solana.com" \
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM solanalabs/rust:1.57.0
|
||||
FROM solanalabs/rust:1.52.1
|
||||
ARG date
|
||||
|
||||
RUN set -x \
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Note: when the rust version is changed also modify
|
||||
# ci/rust-version.sh to pick up the new image tag
|
||||
FROM rust:1.57.0
|
||||
FROM rust:1.52.1
|
||||
|
||||
# Add Google Protocol Buffers for Libra's metrics library.
|
||||
ENV PROTOC_VERSION 3.8.0
|
||||
@@ -11,34 +11,28 @@ RUN set -x \
|
||||
&& apt-get install apt-transport-https \
|
||||
&& echo deb https://apt.buildkite.com/buildkite-agent stable main > /etc/apt/sources.list.d/buildkite-agent.list \
|
||||
&& apt-key adv --no-tty --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 32A37959C2FA5C3C99EFBC32A79206696452D198 \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_current.x | bash - \
|
||||
&& apt update \
|
||||
&& apt install -y \
|
||||
buildkite-agent \
|
||||
clang \
|
||||
clang-7 \
|
||||
cmake \
|
||||
lcov \
|
||||
libudev-dev \
|
||||
libclang-common-7-dev \
|
||||
mscgen \
|
||||
nodejs \
|
||||
net-tools \
|
||||
rsync \
|
||||
sudo \
|
||||
golang \
|
||||
unzip \
|
||||
\
|
||||
&& apt remove -y libcurl4-openssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& node --version \
|
||||
&& npm --version \
|
||||
&& rustup component add rustfmt \
|
||||
&& rustup component add clippy \
|
||||
&& rustup target add wasm32-unknown-unknown \
|
||||
&& cargo install cargo-audit \
|
||||
&& cargo install svgbob_cli \
|
||||
&& cargo install mdbook \
|
||||
&& cargo install mdbook-linkcheck \
|
||||
&& cargo install svgbob_cli \
|
||||
&& cargo install wasm-pack \
|
||||
&& rustc --version \
|
||||
&& cargo --version \
|
||||
&& curl -OL https://github.com/google/protobuf/releases/download/v$PROTOC_VERSION/$PROTOC_ZIP \
|
||||
|
16
ci/env.sh
16
ci/env.sh
@@ -23,6 +23,9 @@ if [[ -n $CI ]]; then
|
||||
elif [[ -n $BUILDKITE ]]; then
|
||||
export CI_BRANCH=$BUILDKITE_BRANCH
|
||||
export CI_BUILD_ID=$BUILDKITE_BUILD_ID
|
||||
if [[ $BUILDKITE_COMMIT = HEAD ]]; then
|
||||
BUILDKITE_COMMIT="$(git rev-parse HEAD)"
|
||||
fi
|
||||
export CI_COMMIT=$BUILDKITE_COMMIT
|
||||
export CI_JOB_ID=$BUILDKITE_JOB_ID
|
||||
# The standard BUILDKITE_PULL_REQUEST environment variable is always "false" due
|
||||
@@ -35,7 +38,18 @@ if [[ -n $CI ]]; then
|
||||
export CI_BASE_BRANCH=$BUILDKITE_BRANCH
|
||||
export CI_PULL_REQUEST=
|
||||
fi
|
||||
export CI_OS_NAME=linux
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
export CI_OS_NAME=linux
|
||||
;;
|
||||
Darwin)
|
||||
export CI_OS_NAME=osx
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -n $BUILDKITE_TRIGGERED_FROM_BUILD_PIPELINE_SLUG ]]; then
|
||||
# The solana-secondary pipeline should use the slug of the pipeline that
|
||||
# triggered it
|
||||
|
@@ -39,7 +39,11 @@ fi
|
||||
|
||||
case "$CI_OS_NAME" in
|
||||
osx)
|
||||
TARGET=x86_64-apple-darwin
|
||||
_cputype="$(uname -m)"
|
||||
if [[ $_cputype = arm64 ]]; then
|
||||
_cputype=aarch64
|
||||
fi
|
||||
TARGET=${_cputype}-apple-darwin
|
||||
;;
|
||||
linux)
|
||||
TARGET=x86_64-unknown-linux-gnu
|
||||
|
@@ -27,6 +27,8 @@ steps+=(test-stable-perf)
|
||||
steps+=(test-downstream-builds)
|
||||
steps+=(test-bench)
|
||||
steps+=(test-local-cluster)
|
||||
steps+=(test-local-cluster-flakey)
|
||||
steps+=(test-local-cluster-slow)
|
||||
|
||||
step_index=0
|
||||
if [[ -n "$1" ]]; then
|
||||
|
@@ -18,13 +18,13 @@
|
||||
if [[ -n $RUST_STABLE_VERSION ]]; then
|
||||
stable_version="$RUST_STABLE_VERSION"
|
||||
else
|
||||
stable_version=1.57.0
|
||||
stable_version=1.52.1
|
||||
fi
|
||||
|
||||
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
|
||||
nightly_version="$RUST_NIGHTLY_VERSION"
|
||||
else
|
||||
nightly_version=2021-12-03
|
||||
nightly_version=2021-05-18
|
||||
fi
|
||||
|
||||
|
||||
|
@@ -67,7 +67,8 @@ _ ci/order-crates-for-publishing.py
|
||||
|
||||
# -Z... is needed because of clippy bug: https://github.com/rust-lang/rust-clippy/issues/4612
|
||||
# run nightly clippy for `sdk/` as there's a moderate amount of nightly-only code there
|
||||
_ "$cargo" nightly clippy -Zunstable-options --workspace --all-targets -- --deny=warnings --deny=clippy::integer_arithmetic
|
||||
_ "$cargo" nightly clippy -Zunstable-options --workspace --all-targets -- \
|
||||
--deny=warnings --deny=clippy::integer_arithmetic --allow=clippy::inconsistent_struct_constructor
|
||||
|
||||
_ "$cargo" stable fmt --all -- --check
|
||||
|
||||
|
1
ci/test-local-cluster-slow.sh
Symbolic link
1
ci/test-local-cluster-slow.sh
Symbolic link
@@ -0,0 +1 @@
|
||||
test-stable.sh
|
@@ -41,26 +41,26 @@ test-stable-bpf)
|
||||
|
||||
# solana-keygen required when building C programs
|
||||
_ "$cargo" build --manifest-path=keygen/Cargo.toml
|
||||
|
||||
export PATH="$PWD/target/debug":$PATH
|
||||
cargo_build_bpf="$(realpath ./cargo-build-bpf)"
|
||||
cargo_test_bpf="$(realpath ./cargo-test-bpf)"
|
||||
|
||||
# BPF solana-sdk legacy compile test
|
||||
"$cargo_build_bpf" --manifest-path sdk/Cargo.toml
|
||||
|
||||
# BPF C program system tests
|
||||
# BPF Program unit tests
|
||||
"$cargo" test --manifest-path programs/bpf/Cargo.toml
|
||||
"$cargo_build_bpf" --manifest-path programs/bpf/Cargo.toml --bpf-sdk sdk/bpf
|
||||
|
||||
# BPF program system tests
|
||||
_ make -C programs/bpf/c tests
|
||||
_ "$cargo" stable test \
|
||||
--manifest-path programs/bpf/Cargo.toml \
|
||||
--no-default-features --features=bpf_c,bpf_rust -- --nocapture
|
||||
|
||||
# BPF Rust program unit tests
|
||||
# Dump BPF program assembly listings
|
||||
for bpf_test in programs/bpf/rust/*; do
|
||||
if pushd "$bpf_test"; then
|
||||
"$cargo" test
|
||||
"$cargo_build_bpf" --bpf-sdk ../../../../sdk/bpf --dump
|
||||
"$cargo_test_bpf" --bpf-sdk ../../../../sdk/bpf
|
||||
"$cargo_build_bpf" --dump
|
||||
popd
|
||||
fi
|
||||
done
|
||||
@@ -100,20 +100,17 @@ test-stable-perf)
|
||||
;;
|
||||
test-local-cluster)
|
||||
_ "$cargo" stable build --release --bins ${V:+--verbose}
|
||||
_ "$cargo" stable test --release --package solana-local-cluster ${V:+--verbose} -- --nocapture --test-threads=1
|
||||
_ "$cargo" stable test --release --package solana-local-cluster --test local_cluster ${V:+--verbose} -- --nocapture --test-threads=1
|
||||
exit 0
|
||||
;;
|
||||
test-wasm)
|
||||
_ node --version
|
||||
_ npm --version
|
||||
for dir in sdk/{program,}; do
|
||||
if [[ -r "$dir"/package.json ]]; then
|
||||
pushd "$dir"
|
||||
_ npm install
|
||||
_ npm test
|
||||
popd
|
||||
fi
|
||||
done
|
||||
test-local-cluster-flakey)
|
||||
_ "$cargo" stable build --release --bins ${V:+--verbose}
|
||||
_ "$cargo" stable test --release --package solana-local-cluster --test local_cluster_flakey ${V:+--verbose} -- --nocapture --test-threads=1
|
||||
exit 0
|
||||
;;
|
||||
test-local-cluster-slow)
|
||||
_ "$cargo" stable build --release --bins ${V:+--verbose}
|
||||
_ "$cargo" stable test --release --package solana-local-cluster --test local_cluster_slow ${V:+--verbose} -- --nocapture --test-threads=1
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
|
@@ -19,13 +19,24 @@ upload-ci-artifact() {
|
||||
upload-s3-artifact() {
|
||||
echo "--- artifact: $1 to $2"
|
||||
(
|
||||
set -x
|
||||
docker run \
|
||||
--rm \
|
||||
--env AWS_ACCESS_KEY_ID \
|
||||
--env AWS_SECRET_ACCESS_KEY \
|
||||
--volume "$PWD:/solana" \
|
||||
eremite/aws-cli:2018.12.18 \
|
||||
args=(
|
||||
--rm
|
||||
--env AWS_ACCESS_KEY_ID
|
||||
--env AWS_SECRET_ACCESS_KEY
|
||||
--volume "$PWD:/solana"
|
||||
|
||||
)
|
||||
if [[ $(uname -m) = arm64 ]]; then
|
||||
# Ref: https://blog.jaimyn.dev/how-to-build-multi-architecture-docker-images-on-an-m1-mac/#tldr
|
||||
args+=(
|
||||
--platform linux/amd64
|
||||
)
|
||||
fi
|
||||
args+=(
|
||||
eremite/aws-cli:2018.12.18
|
||||
/usr/bin/s3cmd --acl-public put "$1" "$2"
|
||||
)
|
||||
set -x
|
||||
docker run "${args[@]}"
|
||||
)
|
||||
}
|
||||
|
@@ -1,28 +1,28 @@
|
||||
[package]
|
||||
name = "solana-clap-utils"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
description = "Solana utilities for the clap"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-clap-utils"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
rpassword = "5.0"
|
||||
solana-perf = { path = "../perf", version = "=1.10.0" }
|
||||
solana-remote-wallet = { path = "../remote-wallet", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
thiserror = "1.0.30"
|
||||
tiny-bip39 = "0.8.2"
|
||||
rpassword = "4.0"
|
||||
solana-perf = { path = "../perf", version = "=1.8.14" }
|
||||
solana-remote-wallet = { path = "../remote-wallet", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
thiserror = "1.0.21"
|
||||
tiny-bip39 = "0.8.1"
|
||||
uriparse = "0.6.3"
|
||||
url = "2.2.2"
|
||||
url = "2.1.0"
|
||||
chrono = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.2.0"
|
||||
tempfile = "3.1.0"
|
||||
|
||||
[lib]
|
||||
name = "solana_clap_utils"
|
||||
|
@@ -219,7 +219,7 @@ mod tests {
|
||||
use std::env;
|
||||
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
|
||||
|
||||
format!("{}/tmp/{}-{}", out_dir, name, pubkey)
|
||||
format!("{}/tmp/{}-{}", out_dir, name, pubkey.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@@ -236,22 +236,6 @@ where
|
||||
is_parsable_generic::<Slot, _>(slot)
|
||||
}
|
||||
|
||||
pub fn is_pow2<T>(bins: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
{
|
||||
bins.as_ref()
|
||||
.parse::<usize>()
|
||||
.map_err(|e| format!("Unable to parse, provided: {}, err: {}", bins, e))
|
||||
.and_then(|v| {
|
||||
if !v.is_power_of_two() {
|
||||
Err(format!("Must be a power of 2: {}", v))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_port<T>(port: T) -> Result<(), String>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
|
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-cli-config"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -12,13 +12,10 @@ documentation = "https://docs.rs/solana-cli-config"
|
||||
[dependencies]
|
||||
dirs-next = "2.0.0"
|
||||
lazy_static = "1.4.0"
|
||||
serde = "1.0.131"
|
||||
serde = "1.0.122"
|
||||
serde_derive = "1.0.103"
|
||||
serde_yaml = "0.8.21"
|
||||
url = "2.2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.51"
|
||||
serde_yaml = "0.8.13"
|
||||
url = "2.1.1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -6,16 +6,6 @@ use {
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
/// The default path to the CLI configuration file.
|
||||
///
|
||||
/// This is a [lazy_static] of `Option<String>`, the value of which is
|
||||
///
|
||||
/// > `~/.config/solana/cli/config.yml`
|
||||
///
|
||||
/// It will only be `None` if it is unable to identify the user's home
|
||||
/// directory, which should not happen under typical OS environments.
|
||||
///
|
||||
/// [lazy_static]: https://docs.rs/lazy_static
|
||||
pub static ref CONFIG_FILE: Option<String> = {
|
||||
dirs_next::home_dir().map(|mut path| {
|
||||
path.extend(&[".config", "solana", "cli", "config.yml"]);
|
||||
@@ -24,44 +14,13 @@ lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
/// The Solana CLI configuration.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct Config {
|
||||
/// The RPC address of a Solana validator node.
|
||||
///
|
||||
/// Typical values for mainnet, devnet, and testnet are [described in the
|
||||
/// Solana documentation][rpcdocs].
|
||||
///
|
||||
/// For local testing, the typical value is `http://localhost:8899`.
|
||||
///
|
||||
/// [rpcdocs]: https://docs.solana.com/cluster/rpc-endpoints
|
||||
pub json_rpc_url: String,
|
||||
/// The address to connect to for receiving event notifications.
|
||||
///
|
||||
/// If it is an empty string then the correct value will be derived
|
||||
/// from `json_rpc_url`.
|
||||
///
|
||||
/// The default value is the empty string.
|
||||
pub websocket_url: String,
|
||||
/// The default signing source, which may be a keypair file, but may also
|
||||
/// represent several other types of signers, as described in the
|
||||
/// documentation for `solana_clap_utils::keypair::signer_from_path`.
|
||||
/// Because it represents sources other than a simple path, the name
|
||||
/// `keypair_path` is misleading, and exists for backwards compatibility
|
||||
/// reasons.
|
||||
///
|
||||
/// The signing source can be loaded with either the `signer_from_path`
|
||||
/// function, or with `solana_clap_utils::keypair::DefaultSigner`.
|
||||
pub keypair_path: String,
|
||||
/// A mapping from Solana addresses to human-readable names.
|
||||
///
|
||||
/// By default the only value in this map is the system program.
|
||||
#[serde(default)]
|
||||
pub address_labels: HashMap<String, String>,
|
||||
/// The default commitment level.
|
||||
///
|
||||
/// By default the value is "confirmed", as defined by
|
||||
/// `solana_sdk::commitment_config::CommitmentLevel::Confirmed`.
|
||||
#[serde(default)]
|
||||
pub commitment: String,
|
||||
}
|
||||
@@ -98,37 +57,14 @@ impl Default for Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load a configuration from file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function may return typical file I/O errors.
|
||||
pub fn load(config_file: &str) -> Result<Self, io::Error> {
|
||||
crate::load_config_file(config_file)
|
||||
}
|
||||
|
||||
/// Save a configuration to file.
|
||||
///
|
||||
/// If the file's directory does not exist, it will be created. If the file
|
||||
/// already exists, it will be overwritten.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function may return typical file I/O errors.
|
||||
pub fn save(&self, config_file: &str) -> Result<(), io::Error> {
|
||||
crate::save_config_file(self, config_file)
|
||||
}
|
||||
|
||||
/// Compute the websocket URL from the RPC URL.
|
||||
///
|
||||
/// The address is created from the RPC URL by:
|
||||
///
|
||||
/// - adding 1 to the port number,
|
||||
/// - using the "wss" scheme if the RPC URL has an "https" scheme, or the
|
||||
/// "ws" scheme if the RPC URL has an "http" scheme.
|
||||
///
|
||||
/// If `json_rpc_url` cannot be parsed as a URL then this function returns
|
||||
/// the empty string.
|
||||
pub fn compute_websocket_url(json_rpc_url: &str) -> String {
|
||||
let json_rpc_url: Option<Url> = json_rpc_url.parse().ok();
|
||||
if json_rpc_url.is_none() {
|
||||
@@ -147,8 +83,6 @@ impl Config {
|
||||
ws_url.to_string()
|
||||
}
|
||||
|
||||
/// Load a map of address/name pairs from a YAML file at the given path and
|
||||
/// insert them into the configuration.
|
||||
pub fn import_address_labels<P>(&mut self, filename: P) -> Result<(), io::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
@@ -160,8 +94,6 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save the map of address/name pairs contained in the configuration to a
|
||||
/// YAML file at the given path.
|
||||
pub fn export_address_labels<P>(&self, filename: P) -> Result<(), io::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
|
@@ -1,56 +1,3 @@
|
||||
//! Loading and saving the Solana CLI configuration file.
|
||||
//!
|
||||
//! The configuration file used by the Solana CLI includes information about the
|
||||
//! RPC node to connect to, the path to the user's signing source, and more.
|
||||
//! Other software than the Solana CLI may wish to access the same configuration
|
||||
//! and signer.
|
||||
//!
|
||||
//! The default path to the configuration file can be retrieved from
|
||||
//! [`CONFIG_FILE`], which is a [lazy_static] of `Option<String>`, the value of
|
||||
//! which is
|
||||
//!
|
||||
//! > `~/.config/solana/cli/config.yml`
|
||||
//!
|
||||
//! [`CONFIG_FILE`]: struct@CONFIG_FILE
|
||||
//! [lazy_static]: https://docs.rs/lazy_static
|
||||
//!
|
||||
//! `CONFIG_FILE` will only be `None` if it is unable to identify the user's
|
||||
//! home directory, which should not happen under typical OS environments.
|
||||
//!
|
||||
//! The CLI configuration is defined by the [`Config`] struct, and its value is
|
||||
//! loaded with [`Config::load`] and saved with [`Config::save`].
|
||||
//!
|
||||
//! Two important fields of `Config` are
|
||||
//!
|
||||
//! - [`json_rpc_url`], the URL to pass to
|
||||
//! `solana_client::rpc_client::RpcClient`.
|
||||
//! - [`keypair_path`], a signing source, which may be a keypair file, but
|
||||
//! may also represent several other types of signers, as described in
|
||||
//! the documentation for `solana_clap_utils::keypair::signer_from_path`.
|
||||
//!
|
||||
//! [`json_rpc_url`]: Config::json_rpc_url
|
||||
//! [`keypair_path`]: Config::keypair_path
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! Loading and saving the configuration. Note that this uses the [anyhow] crate
|
||||
//! for error handling.
|
||||
//!
|
||||
//! [anyhow]: https://docs.rs/anyhow
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use anyhow::anyhow;
|
||||
//! use solana_cli_config::{CONFIG_FILE, Config};
|
||||
//!
|
||||
//! let config_file = solana_cli_config::CONFIG_FILE.as_ref()
|
||||
//! .ok_or_else(|| anyhow!("unable to get config file path"))?;
|
||||
//! let mut cli_config = Config::load(&config_file)?;
|
||||
//! // Set the RPC URL to devnet
|
||||
//! cli_config.json_rpc_url = "https://api.devnet.solana.com".to_string();
|
||||
//! cli_config.save(&config_file)?;
|
||||
//! # Ok::<(), anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
@@ -62,16 +9,6 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// Load a value from a file in YAML format.
|
||||
///
|
||||
/// Despite the name, this function is generic YAML file deserializer, a thin
|
||||
/// wrapper around serde.
|
||||
///
|
||||
/// Most callers should instead use [`Config::load`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function may return typical file I/O errors.
|
||||
pub fn load_config_file<T, P>(config_file: P) -> Result<T, io::Error>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
@@ -83,19 +20,6 @@ where
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Save a value to a file in YAML format.
|
||||
///
|
||||
/// Despite the name, this function is a generic YAML file serializer, a thin
|
||||
/// wrapper around serde.
|
||||
///
|
||||
/// If the file's directory does not exist, it will be created. If the file
|
||||
/// already exists, it will be overwritten.
|
||||
///
|
||||
/// Most callers should instead use [`Config::save`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function may return typical file I/O errors.
|
||||
pub fn save_config_file<T, P>(config: &T, config_file: P) -> Result<(), io::Error>
|
||||
where
|
||||
T: serde::ser::Serialize,
|
||||
|
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
name = "solana-cli-output"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "1.10.0"
|
||||
version = "1.8.14"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -13,18 +13,19 @@ documentation = "https://docs.rs/solana-cli-output"
|
||||
base64 = "0.13.0"
|
||||
chrono = { version = "0.4.11", features = ["serde"] }
|
||||
clap = "2.33.0"
|
||||
console = "0.15.0"
|
||||
console = "0.14.1"
|
||||
humantime = "2.0.1"
|
||||
Inflector = "0.11.4"
|
||||
indicatif = "0.16.2"
|
||||
serde = "1.0.131"
|
||||
serde_json = "1.0.72"
|
||||
solana-account-decoder = { path = "../account-decoder", version = "=1.10.0" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.10.0" }
|
||||
solana-client = { path = "../client", version = "=1.10.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.10.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.10.0" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "=1.10.0" }
|
||||
indicatif = "0.15.0"
|
||||
serde = "1.0.122"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.56"
|
||||
solana-account-decoder = { path = "../account-decoder", version = "=1.8.14" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.14" }
|
||||
solana-client = { path = "../client", version = "=1.8.14" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.8.14" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.8.14" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "=1.8.14" }
|
||||
spl-memo = { version = "=3.0.1", features = ["no-entrypoint"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user