Compare commits
495 Commits
Author | SHA1 | Date | |
---|---|---|---|
b4adb1c266 | |||
b9b541441b | |||
e510d4e272 | |||
9341e64ec7 | |||
d934f94e05 | |||
59dc123fa8 | |||
0faea87c84 | |||
19137ce3f4 | |||
8bdeb2d1ed | |||
d29a45266b | |||
2adb98a4a0 | |||
471465a5f4 | |||
942785b626 | |||
aa3c00231a | |||
d772a27936 | |||
0302f13b97 | |||
16b25d0874 | |||
c2dcbee6af | |||
1f71d05299 | |||
bfa1c025fd | |||
8611b40074 | |||
916844d399 | |||
4c9b7c9d2b | |||
9843c3a5cb | |||
f56955a17c | |||
9784bbf154 | |||
45642c4da1 | |||
8eac199e8b | |||
2e251ccc5c | |||
cf4bb70d80 | |||
57f8a15b96 | |||
cfe5afd34c | |||
94beb4b8c2 | |||
50207a30ef | |||
35e8f966e3 | |||
943cd0a24a | |||
0b892b2579 | |||
fb2eac20bb | |||
b37d2fde3d | |||
6b35e16676 | |||
6a9e0bc593 | |||
591fd72e0b | |||
2ed77b040a | |||
7ada8510c4 | |||
b8f6c17dee | |||
2f976ae460 | |||
36019cb1e3 | |||
99d2428041 | |||
c121498b5b | |||
eef2bdf690 | |||
190656967d | |||
90e73515ed | |||
1d7a758c97 | |||
e5b7aead12 | |||
578c2ad3ea | |||
de6838da78 | |||
604071c5d8 | |||
41a377013f | |||
52d453d06f | |||
58295b825d | |||
f6c7812fcc | |||
2f7561e4ee | |||
1cbd2372fc | |||
28f948aa7f | |||
c9ba9e4eb7 | |||
f877fb8c8f | |||
772ba41ede | |||
6374e69a69 | |||
ef0580bd3d | |||
1a77486f8e | |||
ead15d294e | |||
1acfcf3acf | |||
d15e248cdb | |||
f1e5edee14 | |||
7153abd483 | |||
90fb5d074d | |||
af82b0dce9 | |||
d4da2fbacd | |||
77efe95730 | |||
86e03a6d1b | |||
114e2989fa | |||
7024c73e9b | |||
6d418aa3f1 | |||
f079a78c5e | |||
6365c4c061 | |||
55cee5742f | |||
034eda4546 | |||
44ff25d044 | |||
a7e160e5c4 | |||
6283cc916d | |||
4b6aca6120 | |||
20b2be6e0b | |||
cbebc7a80c | |||
06eb2364f2 | |||
167890ca63 | |||
392a39dd54 | |||
7e1a7862db | |||
458ae3fdac | |||
431cc82032 | |||
18c6729d6c | |||
9476fe5ce3 | |||
788290ad82 | |||
6b5bcfaa58 | |||
4ed0cded9c | |||
035a364122 | |||
b114bc3674 | |||
bc74ee7117 | |||
b2ce5dc9f5 | |||
e920191de0 | |||
39e85a3e53 | |||
41156da4ca | |||
9271ba0039 | |||
b3e45fd6b7 | |||
7bfb60f82e | |||
359c50f1b3 | |||
fff1631a8b | |||
7d42ae30d9 | |||
87414de3e2 | |||
a0ffbf50a5 | |||
d40b66ff7b | |||
abd7f6b090 | |||
d8735df1de | |||
481853e1b1 | |||
778bcbce50 | |||
fd3f2cb910 | |||
915956b94b | |||
4576250342 | |||
2bef1b0433 | |||
628128b376 | |||
916017ca2c | |||
3204a00e73 | |||
1d327a5167 | |||
6e4f9cedf2 | |||
a79fbbafc9 | |||
1d54d29076 | |||
10b9a4806b | |||
0c1191c3ee | |||
18b386cd10 | |||
714b8c7fc8 | |||
216e9a61a0 | |||
0f498e6265 | |||
e8ad822111 | |||
65a82ebf50 | |||
727802684c | |||
e20a8329d3 | |||
88c2d0fad4 | |||
3bd921264a | |||
7501ed65e5 | |||
2eaa64c4e8 | |||
c9b86018c6 | |||
a4fb01b42b | |||
0d2574f8f0 | |||
796000e96f | |||
e2f00dc205 | |||
5e91f8f59d | |||
e2830f5b0e | |||
a2e3a92b01 | |||
23c696706b | |||
1393d26f63 | |||
1b68da7572 | |||
8542006259 | |||
426d06b89b | |||
06378d6db6 | |||
dccfe31e8c | |||
1dce5976cf | |||
340d01665c | |||
50f79e495e | |||
dd12db2f06 | |||
1afccb7351 | |||
bfc65e829e | |||
eb4515525d | |||
55f5f6a033 | |||
7ae421eaf6 | |||
e7da2c0931 | |||
133be2df51 | |||
06a93dcb43 | |||
ad7f04a245 | |||
0da6e1af14 | |||
576524f13b | |||
f567877d1d | |||
9881820444 | |||
ba8f49366d | |||
81fa69d347 | |||
abf2b300da | |||
a8254fd258 | |||
b15848de3b | |||
ab3c988146 | |||
575a0e318b | |||
a031b09190 | |||
df43e721e3 | |||
5f72650c7f | |||
5d0d467287 | |||
994515d0f2 | |||
1e949caa7f | |||
f2b727b534 | |||
f7680752e7 | |||
da4c37beec | |||
d486d2b8ce | |||
bba94c43b9 | |||
9cdffc7d64 | |||
5a86f2506d | |||
518227eac0 | |||
b8fd51e97d | |||
965c1e0000 | |||
a80176496d | |||
5719b8f251 | |||
1a2b131ceb | |||
349306ddf7 | |||
791ee411a5 | |||
f690c64375 | |||
427963f554 | |||
b0f2220ef6 | |||
908b48bf0e | |||
b49f8c0984 | |||
7609a007c6 | |||
674a49f8d7 | |||
d10bde656a | |||
401764ddb1 | |||
69eeb7cf08 | |||
55e3b7d380 | |||
d9e18a71ec | |||
2107e15bd3 | |||
4f3b22d04e | |||
2c78a93001 | |||
2621aeee82 | |||
8e400fc4bd | |||
29c2a63c8b | |||
736ada4e21 | |||
3df9b44d4c | |||
7225b89142 | |||
0cc0d3ab7a | |||
88d9618788 | |||
57038529e0 | |||
5c25eae631 | |||
b8b4d33f72 | |||
673a9417ef | |||
3fd9aada8b | |||
453fdb9e28 | |||
3f6a79b078 | |||
e9f80e5542 | |||
694d28acf8 | |||
88fdba5aca | |||
a19df7a36c | |||
9b50583641 | |||
71f9b44687 | |||
0139e5db21 | |||
586fb15c2c | |||
297328ff9a | |||
6b3384c205 | |||
3ef961fe37 | |||
a7b695c27a | |||
5bb75a5894 | |||
f3f416b7ba | |||
31b74bdf0b | |||
ed48d8323c | |||
f91627a230 | |||
f9c093022c | |||
7fe3c75c6b | |||
c8ed41167a | |||
5b2a82a951 | |||
441e76ebeb | |||
c2dfb9900e | |||
916458e132 | |||
ffb15578ce | |||
abcbbb925f | |||
059755fe59 | |||
ae12dc2c75 | |||
37b5c6afaa | |||
92ed7b36a2 | |||
379d2e6d95 | |||
7f75cc8906 | |||
1ab5098576 | |||
598f765960 | |||
aac626c2c2 | |||
3eec3cfac2 | |||
5eee9e62e5 | |||
a7d18125d3 | |||
8202310073 | |||
1e2ba110eb | |||
62c9b7d850 | |||
4f18fc836f | |||
950d8494ba | |||
cb528af4e2 | |||
ad27c30623 | |||
9add8d0afc | |||
af2e7ea285 | |||
675a78aaa1 | |||
408bdbce7a | |||
1a259d4a3f | |||
c5f8b4960c | |||
21f845ed39 | |||
7a369df9a7 | |||
f02ec31c68 | |||
d21fa4a177 | |||
bd0871cbe7 | |||
2604f8ac0a | |||
a7574f8657 | |||
73f250f03a | |||
bae0aadafa | |||
5524146ddf | |||
3b2adbc9df | |||
4e41c81bc7 | |||
c545e812d0 | |||
c2193a37ce | |||
fabba82173 | |||
c3ec5ad846 | |||
c4945cc04a | |||
e2e55f81d0 | |||
d862565b16 | |||
0cc3956693 | |||
4e5677f116 | |||
acba1d6f9e | |||
3e14af5033 | |||
6f56501034 | |||
0b7269b64e | |||
457a2d948b | |||
528bdf34fd | |||
697cd17b59 | |||
13fcfcb964 | |||
9c1fd55768 | |||
7f9a476660 | |||
b07290df81 | |||
4b599a95b3 | |||
64222cfff7 | |||
e81d434903 | |||
bf0dd158de | |||
18e398131d | |||
4a5837a286 | |||
656e2649a7 | |||
d36af917ea | |||
c81733b41a | |||
b6558a2ef3 | |||
634d8e25ee | |||
fea212e64e | |||
e3ab76f1a7 | |||
87f1bd58b9 | |||
a056c1f18f | |||
8b34fd2c75 | |||
b912ee7fdf | |||
3cf708f019 | |||
070e0e9613 | |||
3e678511d2 | |||
4ce2105548 | |||
721c6a7e2d | |||
08f0fb1e14 | |||
f5f5281f85 | |||
1684a7bd18 | |||
8b1724bb70 | |||
eebdfe8d73 | |||
82776b333d | |||
e71ab55288 | |||
fd60ef8a8d | |||
aa0b67c93c | |||
15aa07f2a0 | |||
e4536621df | |||
a3c302c36a | |||
d12705f9b0 | |||
0add5c1dc8 | |||
a9e63455a1 | |||
4dc0495a1b | |||
5a79676b8a | |||
b67b0bff05 | |||
4c200635b7 | |||
b98200aca4 | |||
d59c1cd412 | |||
c4d9dff590 | |||
cf91ff8694 | |||
e867ce0944 | |||
29a25990d3 | |||
9a40ad76bd | |||
54b44977e0 | |||
9c7ccc0e2b | |||
7710ef8b2b | |||
c969975fde | |||
3eed6a6090 | |||
1661a7a55f | |||
6293d324db | |||
c1ecfec3b0 | |||
05b4dbf148 | |||
4efada6d84 | |||
23c01473a0 | |||
f2e2106f62 | |||
0cbac26591 | |||
4e7e5ace9d | |||
ab11327e34 | |||
3ba93aa8fe | |||
c309cd80aa | |||
d22a1c9b1f | |||
29698fcd38 | |||
7372ec9e1a | |||
840a64ee8b | |||
524bc2b9a6 | |||
62a29a41d1 | |||
5406d82d89 | |||
de6af95061 | |||
43f7cd8149 | |||
69e67d06a7 | |||
4f47fc00bc | |||
4b04c37c36 | |||
b27b515186 | |||
05bcb7f292 | |||
95a16426f3 | |||
bec094bb3e | |||
af9ebf1d1a | |||
6f2f7018e8 | |||
101d6b92ee | |||
349e8a9462 | |||
c0bffb56df | |||
970cc32e65 | |||
3ab492ccf8 | |||
d83a71d89f | |||
efbb573316 | |||
85554087d1 | |||
c3155a6e39 | |||
4abe95abec | |||
e0acd48944 | |||
afb00432d4 | |||
320bd66c84 | |||
1a9ac62f60 | |||
809b051f10 | |||
baac21209e | |||
5fb8baed04 | |||
512bfc93cb | |||
0f88872650 | |||
f4e40d2c41 | |||
6eac5951ed | |||
475a74d37f | |||
b8ee952135 | |||
15bed29afa | |||
6dbe7e8bee | |||
2cd556e43c | |||
060793f451 | |||
7e409a13cd | |||
aab410380e | |||
67b8ad6a0f | |||
c1e39a3b98 | |||
7e1a7b1f64 | |||
a9cfae486c | |||
8514d27c2f | |||
8999bfef65 | |||
96425fb520 | |||
ce505d24b1 | |||
f2187780d2 | |||
6a878602f2 | |||
f8543a268f | |||
e9b82bacda | |||
684e1c73dd | |||
901c74b653 | |||
2c0afe71b2 | |||
2f4a3ed190 | |||
26a7eb6fa5 | |||
aa21f5343a | |||
9c2809db21 | |||
9ccd362461 | |||
596f611ede | |||
78d5ace754 | |||
2b3218b5f2 | |||
d0fb55d9b1 | |||
a2c8e3952f | |||
beb8c7914e | |||
6bef16a6a1 | |||
e03215c4c0 | |||
8d1fd29fa6 | |||
46f655eddd | |||
ca36a6f4e0 | |||
fdb12b54fa | |||
09dd4bb702 | |||
01657ddfe7 | |||
51a2988bb2 | |||
083090817a | |||
f3676e2d03 | |||
4b8cb72977 | |||
2518e95fb0 | |||
bc17edcda3 | |||
eb185b9ea5 | |||
aa6c82cfdc | |||
b9bb5af4a5 | |||
1e20d449ce | |||
e94f268346 | |||
7ec198b9cc | |||
b2e762ccc6 | |||
bee411e826 | |||
34344982a9 | |||
f73d38739a | |||
63d66ece57 | |||
a4b5493ba1 | |||
8d613f3977 | |||
0ff2bfdd0c | |||
141e25d567 | |||
c67cc694ae | |||
d77359914f | |||
9293a54234 | |||
d9983905b3 | |||
3dc47a46d5 | |||
8638b3bb19 |
14
.buildkite/env/secrets.ejson
vendored
14
.buildkite/env/secrets.ejson
vendored
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f",
|
"_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f",
|
||||||
"environment": {
|
"environment": {
|
||||||
"CODECOV_TOKEN": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:rHBSqXK7uSnveA4qwUxARZjTNZcA0hXU:ko8lLGwPECpVm19znWBRxKEpMF7xpTHBCEzVOxRar2wDThw4lNDAKqTS61vtkJLtdkHtug==]",
|
"CODECOV_TOKEN": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:jA0WqO70coUtF0iokRdgtCR/lF/lETAI:d/Wl8Tdl6xVh/B39cTf1DaQkomR7I/2vMhvxd1msJ++BjI2l3p2dFoGsXqWT+/os8VgiPg==]",
|
||||||
"CRATES_IO_TOKEN": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:NzN6y0ooXJBYvxB589khepthSxhKFkLB:ZTTFZh2A/kB2SAgjJJAMbwAfanRlzxOCNMVcA2MXBCpQHJeeZGULg+0MLACYswfS]",
|
"CRATES_IO_TOKEN": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:2FaZ6k4RGH8luyNRaN6yeZUQDNAu2KwC:XeYe0tCAivYE0F9HEWM79mAI6kNbfYaqP7k7yY+SBDvs0341U9BdGZp7SErbHleS]",
|
||||||
"GITHUB_TOKEN": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:iy0Fnxeo0aslTCvgXc5Ddj2ly6ZsQ8gK:GNOOj/kZUJ2rYKxTbLyVKtajWNoGQ3PcChwfEB4HdN18qDHlB96Z7gx01Pcf0qeIHODOWRtxlH4=]",
|
"GITHUB_TOKEN": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:9kh4DGPiGDcUU7ejSFWg3gTW8nrOM09Q:b+GE07Wu6/bEnkDZcUtf48vTKAFphrCSt3tNNER9h6A+wZ80k499edw4pbDdl9kEvxB30fFwrLQ=]",
|
||||||
"INFLUX_DATABASE": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:Ly/TpIRF0oCxmiBWv225S3mX8s6pfQR+:+tXGB2c9rRCVDcgNO1IDOo89]",
|
"INFLUX_DATABASE": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:rCHsYi0rc7dmvr1V3wEgNoaNIyr+9ClM:omjVcOqM7vwt44kJ+As4BjJL]",
|
||||||
"INFLUX_PASSWORD": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:ycrq1uQLoSfI932czD+krUOaJeLWpeq6:2iS7ukp/C7wVD3IT0GvQVcwccWGyLr4UocStF/XiDi0OB/N3YKIKN8SQU4ob1b6StAPZ/XOHmag=]",
|
"INFLUX_PASSWORD": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:bP5Gw1Vy66viKFKO41o2Gho998XajH/5:khkCYz2LFvkJkk7R4xY1Hfz1yU3/NENjauiUkPhXA+dmg1qOIToxEagCgIkRwyeCiYaoCR6CZyw=]",
|
||||||
"INFLUX_USERNAME": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:35hBKofakZ4Db/u0TOW53RXoNWzJTIcl:HWREcMTrgZ8DGB0ZupgSzNWr/tVyE06P]",
|
"INFLUX_USERNAME": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:ZamCvza2W9/bZRGSkqDu55xNN04XKKhp:5jlmCOdFbpL7EFez41zCbLfk3ZZlfmhI]",
|
||||||
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:kRz8CyJYKAg/AiwgLrcRNDJAmlRX2zvX:uV1XV6y2Fb+dN4Z9BIMPBRiNS3n+NL8GlJXyu1i7meIsph1DzfLg4Thcp5Mj9nUsFNLgqQgjnsa5C4XNY/h5AgMSzRrJxVj7RhVTRmDJ5/Vjq6v7wCMRfBOvF3rITsV4zTwWSV8yafFmS+ZQ+QJTRgtYsuoYAUNZ06IEebfDHcuNwws72hEGoD9w43hOLSpyEOmXbtZ9h1lIRxrgsrhYDpBlU5LkhDeTXAX5M5dwYxyquJFRwd5quGDV5DYsCh9bAkbjAyjWYymVJ78U9YJIQHT9izzQqTDlMQN49EbLo7MDIaC7O7HVtb7unDJs+DRejbHacoyWVulqVVwu3GRiZezu8zdjwzGHphMMxOtKQaidnqYgflNp/O01I8wZRgR1alsGcmIhEhI8YV/IvQ==]"
|
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:Oi2nsRxnvWnnBYsB6KwEDzLPcYgpYojU:ELbvjXkXKlgFCMES45R+fxG7Ex43WHWErjMbxZoqasxyr7GSH66hQzUWqiQSJyT4ukYrRhRC9YrsKKGkjACLU57X4EGIy9TuLgTnyBYhPnxLYStC3y/7o/MB5FCTt5wHJw3/A9p+me5+T4UmyZ7OeP21NhDUCGQcb0040VwYWS78klW2aQESJJ6wTI1xboE8/zC0vtnB/u50+LydbKEyb21r6y3OH9FYNEpSwIspWKcgpruJdQSCnDoKxP9YR1yzvk2rabss13LJNdV1Y6mQNIdP4OIFQhCs6dXT253RTl5qdZ0MruHwlp8wX4btOuYDcCoM5exr]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
set -x
|
set -x
|
||||||
rsync -a --delete --link-dest="$PWD" target "$d"
|
rsync -a --delete --link-dest="$PWD" target "$d"
|
||||||
du -hs "$d"
|
du -hs "$d"
|
||||||
|
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$d")
|
||||||
|
echo "--- ${cacheSizeInGB}GB: $d"
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -14,14 +14,18 @@ export PS4="++"
|
|||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
|
d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
|
||||||
|
MAX_CACHE_SIZE=18 # gigabytes
|
||||||
|
|
||||||
if [[ -d $d ]]; then
|
if [[ -d $d ]]; then
|
||||||
du -hs "$d"
|
du -hs "$d"
|
||||||
read -r cacheSizeInGB _ < <(du -s --block-size=1000000000 "$d")
|
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$d")
|
||||||
if [[ $cacheSizeInGB -gt 10 ]]; then
|
echo "--- ${cacheSizeInGB}GB: $d"
|
||||||
echo "$d has gotten too large, removing it"
|
if [[ $cacheSizeInGB -gt $MAX_CACHE_SIZE ]]; then
|
||||||
|
echo "--- $d is too large, removing it"
|
||||||
rm -rf "$d"
|
rm -rf "$d"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echo "--- $d not present"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$d"/target
|
mkdir -p "$d"/target
|
||||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,10 +1,11 @@
|
|||||||
/target/
|
|
||||||
/ledger-tool/target/
|
|
||||||
/wallet/target/
|
|
||||||
/core/target/
|
|
||||||
/book/html/
|
/book/html/
|
||||||
/book/src/img/
|
/book/src/img/
|
||||||
/book/src/tests.ok
|
/book/src/tests.ok
|
||||||
|
/farf/
|
||||||
|
/metrics/scripts/lib/
|
||||||
|
/solana-release/
|
||||||
|
solana-release.tar.bz2
|
||||||
|
/target/
|
||||||
|
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
.cargo
|
.cargo
|
||||||
|
1179
Cargo.lock
generated
1179
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@ -1,10 +1,12 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
".",
|
"bench-exchange",
|
||||||
"bench-streamer",
|
"bench-streamer",
|
||||||
"bench-tps",
|
"bench-tps",
|
||||||
|
"client",
|
||||||
|
"core",
|
||||||
"drone",
|
"drone",
|
||||||
"fullnode",
|
"validator",
|
||||||
"genesis",
|
"genesis",
|
||||||
"gossip",
|
"gossip",
|
||||||
"install",
|
"install",
|
||||||
@ -13,6 +15,7 @@ members = [
|
|||||||
"ledger-tool",
|
"ledger-tool",
|
||||||
"logger",
|
"logger",
|
||||||
"metrics",
|
"metrics",
|
||||||
|
"netutil",
|
||||||
"programs/bpf",
|
"programs/bpf",
|
||||||
"programs/bpf_loader",
|
"programs/bpf_loader",
|
||||||
"programs/budget_api",
|
"programs/budget_api",
|
||||||
@ -21,17 +24,18 @@ members = [
|
|||||||
"programs/config_program",
|
"programs/config_program",
|
||||||
"programs/exchange_api",
|
"programs/exchange_api",
|
||||||
"programs/exchange_program",
|
"programs/exchange_program",
|
||||||
"programs/token_api",
|
|
||||||
"programs/token_program",
|
|
||||||
"programs/failure_program",
|
"programs/failure_program",
|
||||||
"programs/noop_program",
|
"programs/noop_program",
|
||||||
"programs/stake_api",
|
"programs/stake_api",
|
||||||
"programs/stake_program",
|
"programs/stake_program",
|
||||||
"programs/storage_api",
|
"programs/storage_api",
|
||||||
"programs/storage_program",
|
"programs/storage_program",
|
||||||
|
"programs/token_api",
|
||||||
|
"programs/token_program",
|
||||||
"programs/vote_api",
|
"programs/vote_api",
|
||||||
"programs/vote_program",
|
"programs/vote_program",
|
||||||
"replicator",
|
"replicator",
|
||||||
|
"runtime",
|
||||||
"sdk",
|
"sdk",
|
||||||
"upload-perf",
|
"upload-perf",
|
||||||
"vote-signer",
|
"vote-signer",
|
||||||
|
@ -41,7 +41,7 @@ Install rustc, cargo and rustfmt:
|
|||||||
```bash
|
```bash
|
||||||
$ curl https://sh.rustup.rs -sSf | sh
|
$ curl https://sh.rustup.rs -sSf | sh
|
||||||
$ source $HOME/.cargo/env
|
$ source $HOME/.cargo/env
|
||||||
$ rustup component add rustfmt-preview
|
$ rustup component add rustfmt
|
||||||
```
|
```
|
||||||
|
|
||||||
If your rustc version is lower than 1.34.0, please update it:
|
If your rustc version is lower than 1.34.0, please update it:
|
||||||
@ -66,7 +66,7 @@ $ cd solana
|
|||||||
Build
|
Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cargo build --all
|
$ cargo build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then to run a minimal local cluster
|
Then to run a minimal local cluster
|
||||||
@ -80,7 +80,7 @@ Testing
|
|||||||
Run the test suite:
|
Run the test suite:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cargo test --all
|
$ cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
Local Testnet
|
Local Testnet
|
||||||
|
58
RELEASE.md
58
RELEASE.md
@ -61,7 +61,7 @@ There are three release channels that map to branches as follows:
|
|||||||
|
|
||||||
## Release Steps
|
## Release Steps
|
||||||
|
|
||||||
### Changing channels
|
### Advance the Channels
|
||||||
|
|
||||||
#### Create the new branch
|
#### Create the new branch
|
||||||
1. Pick your branch point for release on master.
|
1. Pick your branch point for release on master.
|
||||||
@ -84,7 +84,7 @@ There are three release channels that map to branches as follows:
|
|||||||
At this point, `ci/channel-info.sh` should show your freshly cut release branch as
|
At this point, `ci/channel-info.sh` should show your freshly cut release branch as
|
||||||
"BETA_CHANNEL" and the previous release branch as "STABLE_CHANNEL".
|
"BETA_CHANNEL" and the previous release branch as "STABLE_CHANNEL".
|
||||||
|
|
||||||
### Updating channels (i.e. "making a release")
|
### Make the Release
|
||||||
|
|
||||||
We use [github's Releases UI](https://github.com/solana-labs/solana/releases) for tagging a release.
|
We use [github's Releases UI](https://github.com/solana-labs/solana/releases) for tagging a release.
|
||||||
|
|
||||||
@ -99,13 +99,59 @@ We use [github's Releases UI](https://github.com/solana-labs/solana/releases) fo
|
|||||||
release should be `<branchname>.X-rc.0`.
|
release should be `<branchname>.X-rc.0`.
|
||||||
1. Verify release automation:
|
1. Verify release automation:
|
||||||
1. [Crates.io](https://crates.io/crates/solana) should have an updated Solana version.
|
1. [Crates.io](https://crates.io/crates/solana) should have an updated Solana version.
|
||||||
1. ...
|
|
||||||
1. After testnet deployment, verify that testnets are running correct software.
|
|
||||||
http://metrics.solana.com should show testnet running on a hash from your
|
|
||||||
newly created branch.
|
|
||||||
1. Once the release has been made, update Cargo.toml on the release branch to the next
|
1. Once the release has been made, update Cargo.toml on the release branch to the next
|
||||||
semantic version (e.g. 0.9.0 -> 0.9.1) by running
|
semantic version (e.g. 0.9.0 -> 0.9.1) by running
|
||||||
`./scripts/increment-cargo-version.sh patch`, then rebuild with `cargo
|
`./scripts/increment-cargo-version.sh patch`, then rebuild with `cargo
|
||||||
build` to cause a refresh of `Cargo.lock`.
|
build` to cause a refresh of `Cargo.lock`.
|
||||||
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
||||||
release branch.
|
release branch.
|
||||||
|
|
||||||
|
### Update software on testnet.solana.com
|
||||||
|
|
||||||
|
The testnet running on testnet.solana.com is set to use a fixed release tag
|
||||||
|
which is set in the Buildkite testnet-management pipeline.
|
||||||
|
This tag needs to be updated and the testnet restarted after a new release
|
||||||
|
tag is created.
|
||||||
|
|
||||||
|
#### Update testnet schedules
|
||||||
|
|
||||||
|
Go to https://buildkite.com/solana-labs and click through: Pipelines ->
|
||||||
|
testnet-management -> Pipeline Settings -> Schedules
|
||||||
|
Or just click here:
|
||||||
|
https://buildkite.com/solana-labs/testnet-management/settings/schedules
|
||||||
|
|
||||||
|
There are two scheduled jobs for testnet: a daily restart and an hourly sanity-or-restart. \
|
||||||
|
https://buildkite.com/solana-labs/testnet-management/settings/schedules/0efd7856-7143-4713-8817-47e6bdb05387
|
||||||
|
https://buildkite.com/solana-labs/testnet-management/settings/schedules/2a926646-d972-42b5-aeb9-bb6759592a53
|
||||||
|
|
||||||
|
On each schedule:
|
||||||
|
1. Set TESTNET_TAG environment variable to the desired release tag.
|
||||||
|
1. Example, TESTNET_TAG=v0.13.2
|
||||||
|
1. Set the Build Branch to the branch that TESTNET_TAG is from.
|
||||||
|
1. Example: v0.13
|
||||||
|
|
||||||
|
#### Restart the testnet
|
||||||
|
|
||||||
|
Trigger a TESTNET_OP=create-and-start to refresh the cluster with the new version
|
||||||
|
|
||||||
|
1. Go to https://buildkite.com/solana-labs/testnet-management
|
||||||
|
2. Click "New Build" and use the following settings, then click "Create Build"
|
||||||
|
1. Commit: HEAD
|
||||||
|
1. Branch: [channel branch as set in the schedules]
|
||||||
|
1. Environment Variables:
|
||||||
|
```
|
||||||
|
TESTNET=testnet
|
||||||
|
TESTNET_TAG=[same value as used in TESTNET_TAG in the schedules]
|
||||||
|
TESTNET_OP=create-and-start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update documentation
|
||||||
|
|
||||||
|
Document the new recommended version by updating
|
||||||
|
```export SOLANA_RELEASE=[new scheduled TESTNET_TAG value]```
|
||||||
|
in book/src/testnet-participation.md for both edge and beta channel branches.
|
||||||
|
|
||||||
|
### Alert the community
|
||||||
|
|
||||||
|
Notify Discord users on #validator-support that a new release for
|
||||||
|
testnet.solana.com is available
|
||||||
|
3
bench-exchange/.gitignore
vendored
Normal file
3
bench-exchange/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target/
|
||||||
|
/config/
|
||||||
|
/config-local/
|
40
bench-exchange/Cargo.toml
Normal file
40
bench-exchange/Cargo.toml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
name = "solana-bench-exchange"
|
||||||
|
version = "0.15.0"
|
||||||
|
repository = "https://github.com/solana-labs/solana"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
homepage = "https://solana.com/"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bs58 = "0.2.0"
|
||||||
|
clap = "2.32.0"
|
||||||
|
bincode = "1.1.4"
|
||||||
|
env_logger = "0.6.0"
|
||||||
|
itertools = "0.8.0"
|
||||||
|
log = "0.4.6"
|
||||||
|
num-traits = "0.2"
|
||||||
|
num-derive = "0.2"
|
||||||
|
rand = "0.6.5"
|
||||||
|
rayon = "1.0.3"
|
||||||
|
serde = "1.0.91"
|
||||||
|
serde_derive = "1.0.91"
|
||||||
|
serde_json = "1.0.38"
|
||||||
|
# solana-runtime = { path = "../solana/runtime"}
|
||||||
|
solana = { path = "../core", version = "0.15.0" }
|
||||||
|
solana-client = { path = "../client", version = "0.15.0" }
|
||||||
|
solana-drone = { path = "../drone", version = "0.15.0" }
|
||||||
|
solana-exchange-api = { path = "../programs/exchange_api", version = "0.15.0" }
|
||||||
|
solana-exchange-program = { path = "../programs/exchange_program", version = "0.15.0" }
|
||||||
|
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||||
|
solana-metrics = { path = "../metrics", version = "0.15.0" }
|
||||||
|
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||||
|
solana-runtime = { path = "../runtime", version = "0.15.0" }
|
||||||
|
solana-sdk = { path = "../sdk", version = "0.15.0" }
|
||||||
|
ws = "0.8.1"
|
||||||
|
untrusted = "0.6.2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
cuda = ["solana/cuda"]
|
||||||
|
erasure = []
|
483
bench-exchange/README.md
Normal file
483
bench-exchange/README.md
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
# 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>
|
||||||
|
[Premiss](#Premiss)<br>
|
||||||
|
[Exchange startup](#Exchange-startup)<br>
|
||||||
|
[Trade requests](#Trade-requests)<br>
|
||||||
|
[Trade 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 trade requests to
|
||||||
|
the exchange. A Swapper monitors the exchange and posts swap requests for
|
||||||
|
matching trade 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.
|
||||||
|
- Token pair
|
||||||
|
- A unique ordered list of two tokens. For the four types of tokens used in
|
||||||
|
this demo, the valid pairs are AB, AC, AD, BC, BD, CD.
|
||||||
|
- Direction of trade
|
||||||
|
- Describes which token in the pair the investor wants to sell and buy and can
|
||||||
|
be either "To" or "From". For example, if an investor issues a "To" trade
|
||||||
|
for "AB" then they which to exchange A tokens to B tokens. A "From" order
|
||||||
|
would read the other way, A tokens from B tokens.
|
||||||
|
- Price ratio
|
||||||
|
- An expression of the relative prices of two tokens. They consist of the
|
||||||
|
price of the primary token and the price of the secondary token. For
|
||||||
|
simplicity sake, the primary token's price is always 1, which forces the
|
||||||
|
secondary to be the common denominator. For example, if token A was worth
|
||||||
|
2 and token B was worth 6, the price ratio would be 1:3 or just 3. Price
|
||||||
|
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)
|
||||||
|
- Trade request
|
||||||
|
- A Solana transaction executed by the exchange requesting the trade of one
|
||||||
|
type of token for another. Trade requests are made up of the token pair,
|
||||||
|
the direction of the trade, 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 trade requests result in a trade order.
|
||||||
|
- Trade order
|
||||||
|
- The result of a successful trade request. Trade orders are stored in
|
||||||
|
accounts owned by the submitter of the trade 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 trade request.
|
||||||
|
- Price spread
|
||||||
|
- The difference between the two matching trade orders. The spread is the
|
||||||
|
profit of the Swapper initiating the swap request.
|
||||||
|
- Swap requirements
|
||||||
|
- Policies that result in a successful trade swap.
|
||||||
|
- Swap request
|
||||||
|
- A request to exchange tokens between to trade orders
|
||||||
|
- Trade swap
|
||||||
|
- A successful trade. A swap consists of two matching trade orders that meet
|
||||||
|
swap requirements. A trade swap may not wholly satisfy one or both of the
|
||||||
|
trade orders in which case the trade orders are adjusted appropriately. As
|
||||||
|
long as the swap requirements are met there will be an exchange of tokens
|
||||||
|
between accounts. Any price spread is deposited into the Swapper's profit
|
||||||
|
account. All trade swaps are recorded in a new 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 trade requests. Investors post
|
||||||
|
transactions to the exchange in order to request tokens and post or cancel
|
||||||
|
trade requests.
|
||||||
|
- Swapper
|
||||||
|
- An agent who facilitates trading between investors. Swappers operate as
|
||||||
|
Solana thin clients who monitor all the trade orders looking for a trade
|
||||||
|
match. Once found, the Swapper issues a swap request to the exchange.
|
||||||
|
Swappers are the engine of the exchange and are rewarded for their efforts by
|
||||||
|
accumulating the price spreads of the swaps they initiate. Swappers 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 Swappers.
|
||||||
|
|
||||||
|
## Exchange startup
|
||||||
|
|
||||||
|
The exchange is up and running when it reaches a state where it can take
|
||||||
|
investor's trades and Swapper's swap requests. To achieve this state the
|
||||||
|
following must occur in order:
|
||||||
|
|
||||||
|
- Start the Solana blockchain
|
||||||
|
- Start the Swapper thin-client
|
||||||
|
- The Swapper 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 Swapper starts responding to queries for bid/ask price and OHLCV
|
||||||
|
|
||||||
|
The Swapper 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 Swapper could come and go without missing a trade. One way to achieve
|
||||||
|
this is for the Swapper to read the current state of all accounts looking for all
|
||||||
|
open trade 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. Swappers 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 Swappers 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),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trade requests
|
||||||
|
|
||||||
|
When an investor decides to exchange a token of one type for another, they
|
||||||
|
submit a transaction to the Solana Blockchain containing a trade request, which,
|
||||||
|
if successful, is turned into a trade order. Trade orders do not expire but are
|
||||||
|
cancellable. <!-- Trade orders should have a timestamp to enable trade
|
||||||
|
expiration --> When a trade order is created, tokens are deducted from a token
|
||||||
|
account and the trade order acts as an escrow. The tokens are held until the
|
||||||
|
trade 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. Trade 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 trade orders, so trade 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 TradeRequestInfo {
|
||||||
|
/// 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 {
|
||||||
|
/// Trade 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 trade 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,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trade 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 {
|
||||||
|
/// Trade cancellation
|
||||||
|
/// key 0 - Signer
|
||||||
|
/// key 1 -Trade order to cancel
|
||||||
|
TradeCancellation,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trade swaps
|
||||||
|
|
||||||
|
The Swapper is monitoring the accounts assigned to the exchange program and
|
||||||
|
building a trade-order table. The trade order table is used to identify
|
||||||
|
matching trade orders which could be fulfilled. When a match is found the
|
||||||
|
Swapper 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 trade order valid for further swap requests in
|
||||||
|
the future.
|
||||||
|
|
||||||
|
Matching trade 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
|
||||||
|
Swapper's account equal to the difference in the price ratios or the two orders.
|
||||||
|
These tokens are considered the Swapper's profit for initiating the trade.
|
||||||
|
|
||||||
|
The Swapper 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
|
||||||
|
- Swapper 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 Swapper 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
|
||||||
|
- Swapper 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 Swapper 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
|
||||||
|
- Swapper 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 Swapper 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
|
||||||
|
- Swapper takes 4 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' trade order
|
||||||
|
/// key 3 - `From` trade 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 Swappers profit from the swap.
|
||||||
|
SwapRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap accounts are populated with this structure
|
||||||
|
pub struct TradeSwapInfo {
|
||||||
|
/// Pair swapped
|
||||||
|
pub pair: TokenPair,
|
||||||
|
/// `To` trade order
|
||||||
|
pub to_trade_order: Pubkey,
|
||||||
|
/// `From` trade 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),
|
||||||
|
|
||||||
|
/// Trade request
|
||||||
|
/// key 0 - Signer
|
||||||
|
/// key 1 - Account in which to record the swap
|
||||||
|
/// key 2 - Token account associated with this trade
|
||||||
|
TradeRequest(TradeRequestInfo),
|
||||||
|
|
||||||
|
/// Trade cancellation
|
||||||
|
/// key 0 - Signer
|
||||||
|
/// key 1 -Trade order to cancel
|
||||||
|
TradeCancellation,
|
||||||
|
|
||||||
|
/// Trade swap request
|
||||||
|
/// key 0 - Signer
|
||||||
|
/// key 1 - Account in which to record the swap
|
||||||
|
/// key 2 - 'To' trade order
|
||||||
|
/// key 3 - `From` trade 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 Swappers profit from the swap.
|
||||||
|
SwapRequest,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quotes and OHLCV
|
||||||
|
|
||||||
|
The Swapper 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
996
bench-exchange/src/bench.rs
Normal file
996
bench-exchange/src/bench.rs
Normal file
@ -0,0 +1,996 @@
|
|||||||
|
#![allow(clippy::useless_attribute)]
|
||||||
|
|
||||||
|
use crate::order_book::*;
|
||||||
|
use itertools::izip;
|
||||||
|
use log::*;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use solana::gen_keys::GenKeys;
|
||||||
|
use solana_client::perf_utils::{sample_txs, SampleStats};
|
||||||
|
use solana_drone::drone::request_airdrop_transaction;
|
||||||
|
use solana_exchange_api::exchange_instruction;
|
||||||
|
use solana_exchange_api::exchange_state::*;
|
||||||
|
use solana_exchange_api::id;
|
||||||
|
use solana_metrics::datapoint_info;
|
||||||
|
use solana_sdk::client::Client;
|
||||||
|
use solana_sdk::client::SyncClient;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
|
use solana_sdk::system_instruction;
|
||||||
|
use solana_sdk::timing::{duration_as_ms, duration_as_s};
|
||||||
|
use solana_sdk::transaction::Transaction;
|
||||||
|
use std::cmp;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::mem;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::process::exit;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread::{sleep, Builder};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
// TODO Chunk length as specified results in a bunch of failures, divide by 10 helps...
|
||||||
|
// Assume 4MB network buffers, and 512 byte packets
|
||||||
|
const FUND_CHUNK_LEN: usize = 4 * 1024 * 1024 / 512;
|
||||||
|
|
||||||
|
// Maximum system transfers per transaction
|
||||||
|
const MAX_TRANSFERS_PER_TX: u64 = 4;
|
||||||
|
|
||||||
|
pub type SharedTransactions = Arc<RwLock<VecDeque<Vec<Transaction>>>>;
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
pub identity: Keypair,
|
||||||
|
pub threads: usize,
|
||||||
|
pub duration: Duration,
|
||||||
|
pub transfer_delay: u64,
|
||||||
|
pub fund_amount: u64,
|
||||||
|
pub batch_size: usize,
|
||||||
|
pub chunk_size: usize,
|
||||||
|
pub account_groups: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
identity: Keypair::new(),
|
||||||
|
threads: 4,
|
||||||
|
duration: Duration::new(u64::max_value(), 0),
|
||||||
|
transfer_delay: 0,
|
||||||
|
fund_amount: 100_000,
|
||||||
|
batch_size: 10,
|
||||||
|
chunk_size: 10,
|
||||||
|
account_groups: 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn do_bench_exchange<T>(clients: Vec<T>, config: Config)
|
||||||
|
where
|
||||||
|
T: 'static + Client + Send + Sync,
|
||||||
|
{
|
||||||
|
let Config {
|
||||||
|
identity,
|
||||||
|
threads,
|
||||||
|
duration,
|
||||||
|
transfer_delay,
|
||||||
|
fund_amount,
|
||||||
|
batch_size,
|
||||||
|
chunk_size,
|
||||||
|
account_groups,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Exchange client: threads {} duration {} fund_amount {}",
|
||||||
|
threads,
|
||||||
|
duration_as_s(&duration),
|
||||||
|
fund_amount
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"Exchange client: transfer delay {} batch size {} chunk size {}",
|
||||||
|
transfer_delay, batch_size, chunk_size
|
||||||
|
);
|
||||||
|
|
||||||
|
let accounts_in_groups = batch_size * account_groups;
|
||||||
|
let exit_signal = Arc::new(AtomicBool::new(false));
|
||||||
|
let clients: Vec<_> = clients.into_iter().map(Arc::new).collect();
|
||||||
|
let client = clients[0].as_ref();
|
||||||
|
|
||||||
|
const NUM_KEYPAIR_GROUPS: u64 = 4;
|
||||||
|
let total_keys = accounts_in_groups as u64 * NUM_KEYPAIR_GROUPS;
|
||||||
|
info!("Generating {:?} keys", total_keys);
|
||||||
|
let mut keypairs = generate_keypairs(total_keys);
|
||||||
|
let trader_signers: Vec<_> = keypairs
|
||||||
|
.drain(0..accounts_in_groups)
|
||||||
|
.map(Arc::new)
|
||||||
|
.collect();
|
||||||
|
let swapper_signers: Vec<_> = keypairs
|
||||||
|
.drain(0..accounts_in_groups)
|
||||||
|
.map(Arc::new)
|
||||||
|
.collect();
|
||||||
|
let src_pubkeys: Vec<_> = keypairs
|
||||||
|
.drain(0..accounts_in_groups)
|
||||||
|
.map(|keypair| keypair.pubkey())
|
||||||
|
.collect();
|
||||||
|
let profit_pubkeys: Vec<_> = keypairs
|
||||||
|
.drain(0..accounts_in_groups)
|
||||||
|
.map(|keypair| keypair.pubkey())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Fund trader accounts");
|
||||||
|
fund_keys(client, &identity, &trader_signers, fund_amount);
|
||||||
|
info!("Fund swapper accounts");
|
||||||
|
fund_keys(client, &identity, &swapper_signers, fund_amount);
|
||||||
|
|
||||||
|
info!("Create {:?} source token accounts", src_pubkeys.len());
|
||||||
|
create_token_accounts(client, &trader_signers, &src_pubkeys);
|
||||||
|
info!("Create {:?} profit token accounts", profit_pubkeys.len());
|
||||||
|
create_token_accounts(client, &swapper_signers, &profit_pubkeys);
|
||||||
|
|
||||||
|
// Collect the max transaction rate and total tx count seen (single node only)
|
||||||
|
let sample_stats = Arc::new(RwLock::new(Vec::new()));
|
||||||
|
let sample_period = 1; // in seconds
|
||||||
|
info!("Sampling clients for tps every {} s", sample_period);
|
||||||
|
info!(
|
||||||
|
"Requesting and swapping trades with {} ms delay per thread...",
|
||||||
|
transfer_delay
|
||||||
|
);
|
||||||
|
|
||||||
|
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
|
||||||
|
let total_txs_sent_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
let s_threads: Vec<_> = (0..threads)
|
||||||
|
.map(|_| {
|
||||||
|
let exit_signal = exit_signal.clone();
|
||||||
|
let shared_txs = shared_txs.clone();
|
||||||
|
let total_txs_sent_count = total_txs_sent_count.clone();
|
||||||
|
let client = clients[0].clone();
|
||||||
|
Builder::new()
|
||||||
|
.name("solana-exchange-transfer".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
do_tx_transfers(&exit_signal, &shared_txs, &total_txs_sent_count, &client)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
trace!("Start swapper thread");
|
||||||
|
let (swapper_sender, swapper_receiver) = channel();
|
||||||
|
let swapper_thread = {
|
||||||
|
let exit_signal = exit_signal.clone();
|
||||||
|
let shared_txs = shared_txs.clone();
|
||||||
|
let client = clients[0].clone();
|
||||||
|
Builder::new()
|
||||||
|
.name("solana-exchange-swapper".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
swapper(
|
||||||
|
&exit_signal,
|
||||||
|
&swapper_receiver,
|
||||||
|
&shared_txs,
|
||||||
|
&swapper_signers,
|
||||||
|
&profit_pubkeys,
|
||||||
|
transfer_delay,
|
||||||
|
batch_size,
|
||||||
|
chunk_size,
|
||||||
|
account_groups,
|
||||||
|
&client,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!("Start trader thread");
|
||||||
|
let trader_thread = {
|
||||||
|
let exit_signal = exit_signal.clone();
|
||||||
|
let shared_txs = shared_txs.clone();
|
||||||
|
let client = clients[0].clone();
|
||||||
|
Builder::new()
|
||||||
|
.name("solana-exchange-trader".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
trader(
|
||||||
|
&exit_signal,
|
||||||
|
&swapper_sender,
|
||||||
|
&shared_txs,
|
||||||
|
&trader_signers,
|
||||||
|
&src_pubkeys,
|
||||||
|
transfer_delay,
|
||||||
|
batch_size,
|
||||||
|
chunk_size,
|
||||||
|
account_groups,
|
||||||
|
&client,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sample_threads: Vec<_> = clients
|
||||||
|
.iter()
|
||||||
|
.map(|client| {
|
||||||
|
let exit_signal = exit_signal.clone();
|
||||||
|
let sample_stats = sample_stats.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
Builder::new()
|
||||||
|
.name("solana-exchange-sample".to_string())
|
||||||
|
.spawn(move || sample_txs(&exit_signal, &sample_stats, sample_period, &client))
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
sleep(duration);
|
||||||
|
|
||||||
|
info!("Stopping threads");
|
||||||
|
exit_signal.store(true, Ordering::Relaxed);
|
||||||
|
info!("Wait for trader thread");
|
||||||
|
let _ = trader_thread.join();
|
||||||
|
info!("Waiting for swapper thread");
|
||||||
|
let _ = swapper_thread.join();
|
||||||
|
info!("Wait for tx threads");
|
||||||
|
for t in s_threads {
|
||||||
|
let _ = t.join();
|
||||||
|
}
|
||||||
|
info!("Wait for sample threads");
|
||||||
|
for t in sample_threads {
|
||||||
|
let _ = t.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_and_report_stats(
|
||||||
|
&sample_stats,
|
||||||
|
total_txs_sent_count.load(Ordering::Relaxed) as u64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_tx_transfers<T>(
|
||||||
|
exit_signal: &Arc<AtomicBool>,
|
||||||
|
shared_txs: &SharedTransactions,
|
||||||
|
total_txs_sent_count: &Arc<AtomicUsize>,
|
||||||
|
client: &Arc<T>,
|
||||||
|
) where
|
||||||
|
T: Client,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
let txs;
|
||||||
|
{
|
||||||
|
let mut shared_txs_wl = shared_txs.write().unwrap();
|
||||||
|
txs = shared_txs_wl.pop_front();
|
||||||
|
}
|
||||||
|
if let Some(txs0) = txs {
|
||||||
|
let n = txs0.len();
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
for tx in txs0 {
|
||||||
|
client.async_send_transaction(tx).expect("Transfer");
|
||||||
|
}
|
||||||
|
let duration = now.elapsed();
|
||||||
|
|
||||||
|
total_txs_sent_count.fetch_add(n, Ordering::Relaxed);
|
||||||
|
datapoint_info!(
|
||||||
|
"bench-exchange-do_tx_transfers",
|
||||||
|
("duration", duration_as_ms(&duration), i64),
|
||||||
|
("count", n, i64)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if exit_signal.load(Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TradeInfo {
|
||||||
|
trade_account: Pubkey,
|
||||||
|
order_info: TradeOrderInfo,
|
||||||
|
}
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn swapper<T>(
|
||||||
|
exit_signal: &Arc<AtomicBool>,
|
||||||
|
receiver: &Receiver<Vec<TradeInfo>>,
|
||||||
|
shared_txs: &SharedTransactions,
|
||||||
|
signers: &[Arc<Keypair>],
|
||||||
|
profit_pubkeys: &[Pubkey],
|
||||||
|
transfer_delay: u64,
|
||||||
|
batch_size: usize,
|
||||||
|
chunk_size: usize,
|
||||||
|
account_groups: usize,
|
||||||
|
client: &Arc<T>,
|
||||||
|
) where
|
||||||
|
T: Client,
|
||||||
|
{
|
||||||
|
let mut order_book = OrderBook::default();
|
||||||
|
let mut account_group: usize = 0;
|
||||||
|
|
||||||
|
let mut txs = 0;
|
||||||
|
let mut total_txs = 0;
|
||||||
|
let mut now = Instant::now();
|
||||||
|
let start_time = now;
|
||||||
|
let mut total_elapsed = start_time.elapsed();
|
||||||
|
|
||||||
|
// Chunks may have been dropped and we don't want to wait a long time
|
||||||
|
// for each time, Back-off each time we fail to confirm a chunk
|
||||||
|
const CHECK_TX_TIMEOUT_MAX_MS: u64 = 15000;
|
||||||
|
const CHECK_TX_DELAY_MS: u64 = 100;
|
||||||
|
let mut max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS;
|
||||||
|
|
||||||
|
// If we dump too many chunks maybe we are just waiting on a back-log
|
||||||
|
// rather than a series of dropped packets, reset to max waits
|
||||||
|
const MAX_DUMPS: u64 = 50;
|
||||||
|
let mut dumps = 0;
|
||||||
|
|
||||||
|
'outer: loop {
|
||||||
|
if let Ok(trade_infos) = receiver.try_recv() {
|
||||||
|
let mut tries = 0;
|
||||||
|
let mut trade_index = 0;
|
||||||
|
while client
|
||||||
|
.get_balance(&trade_infos[trade_index].trade_account)
|
||||||
|
.unwrap_or(0)
|
||||||
|
== 0
|
||||||
|
{
|
||||||
|
tries += 1;
|
||||||
|
if tries >= max_tries {
|
||||||
|
if exit_signal.load(Ordering::Relaxed) {
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
error!("Give up and dump batch");
|
||||||
|
if dumps >= MAX_DUMPS {
|
||||||
|
error!("Max batches dumped, reset wait back-off");
|
||||||
|
max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS;
|
||||||
|
dumps = 0;
|
||||||
|
} else {
|
||||||
|
dumps += 1;
|
||||||
|
max_tries /= 2;
|
||||||
|
}
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
debug!("{} waiting for trades batch to clear", tries);
|
||||||
|
sleep(Duration::from_millis(CHECK_TX_DELAY_MS));
|
||||||
|
trade_index = thread_rng().gen_range(0, trade_infos.len());
|
||||||
|
}
|
||||||
|
max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS;
|
||||||
|
dumps = 0;
|
||||||
|
|
||||||
|
trade_infos.iter().for_each(|info| {
|
||||||
|
order_book
|
||||||
|
.push(info.trade_account, info.order_info)
|
||||||
|
.expect("Failed to push to order_book");
|
||||||
|
});
|
||||||
|
let mut swaps = Vec::new();
|
||||||
|
while let Some((to, from)) = order_book.pop() {
|
||||||
|
swaps.push((to, from));
|
||||||
|
if swaps.len() >= batch_size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let swaps_size = swaps.len();
|
||||||
|
|
||||||
|
let mut to_swap = vec![];
|
||||||
|
let start = account_group * swaps_size as usize;
|
||||||
|
let end = account_group * swaps_size as usize + batch_size as usize;
|
||||||
|
for (signer, swap, profit) in izip!(
|
||||||
|
signers[start..end].iter(),
|
||||||
|
swaps,
|
||||||
|
profit_pubkeys[start..end].iter(),
|
||||||
|
) {
|
||||||
|
to_swap.push((signer, swap, profit));
|
||||||
|
}
|
||||||
|
account_group = (account_group + 1) % account_groups as usize;
|
||||||
|
|
||||||
|
let (blockhash, _fee_calculator) = client
|
||||||
|
.get_recent_blockhash()
|
||||||
|
.expect("Failed to get blockhash");
|
||||||
|
let to_swap_txs: Vec<_> = to_swap
|
||||||
|
.par_iter()
|
||||||
|
.map(|(signer, swap, profit)| {
|
||||||
|
let s: &Keypair = &signer;
|
||||||
|
let owner = &signer.pubkey();
|
||||||
|
Transaction::new_signed_instructions(
|
||||||
|
&[s],
|
||||||
|
vec![exchange_instruction::swap_request(
|
||||||
|
owner,
|
||||||
|
&swap.0.pubkey,
|
||||||
|
&swap.1.pubkey,
|
||||||
|
&profit,
|
||||||
|
)],
|
||||||
|
blockhash,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
txs += to_swap_txs.len() as u64;
|
||||||
|
total_txs += to_swap_txs.len() as u64;
|
||||||
|
total_elapsed = start_time.elapsed();
|
||||||
|
let duration = now.elapsed();
|
||||||
|
if duration_as_s(&duration) >= 1_f32 {
|
||||||
|
now = Instant::now();
|
||||||
|
let tps = txs as f32 / duration_as_s(&duration);
|
||||||
|
info!(
|
||||||
|
"Swapper {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s",
|
||||||
|
tps,
|
||||||
|
txs,
|
||||||
|
total_txs,
|
||||||
|
total_elapsed.as_secs(),
|
||||||
|
);
|
||||||
|
txs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoint_info!("bench-exchange-swaps", ("count", to_swap_txs.len(), i64));
|
||||||
|
|
||||||
|
let chunks: Vec<_> = to_swap_txs.chunks(chunk_size).collect();
|
||||||
|
{
|
||||||
|
let mut shared_txs_wl = shared_txs.write().unwrap();
|
||||||
|
for chunk in chunks {
|
||||||
|
shared_txs_wl.push_back(chunk.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Throttle the swapper so it doesn't try to catchup unbridled
|
||||||
|
sleep(Duration::from_millis(transfer_delay / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
if exit_signal.load(Ordering::Relaxed) {
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"Swapper sent {} at {:9.2} TPS",
|
||||||
|
total_txs,
|
||||||
|
total_txs as f32 / duration_as_s(&total_elapsed)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
order_book.get_num_outstanding().0 + order_book.get_num_outstanding().1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn trader<T>(
|
||||||
|
exit_signal: &Arc<AtomicBool>,
|
||||||
|
sender: &Sender<Vec<TradeInfo>>,
|
||||||
|
shared_txs: &SharedTransactions,
|
||||||
|
signers: &[Arc<Keypair>],
|
||||||
|
srcs: &[Pubkey],
|
||||||
|
transfer_delay: u64,
|
||||||
|
batch_size: usize,
|
||||||
|
chunk_size: usize,
|
||||||
|
account_groups: usize,
|
||||||
|
client: &Arc<T>,
|
||||||
|
) where
|
||||||
|
T: Client,
|
||||||
|
{
|
||||||
|
// TODO Hard coded for now
|
||||||
|
let pair = TokenPair::AB;
|
||||||
|
let tokens = 1;
|
||||||
|
let price = 1000;
|
||||||
|
let mut account_group: usize = 0;
|
||||||
|
|
||||||
|
let mut txs = 0;
|
||||||
|
let mut total_txs = 0;
|
||||||
|
let mut now = Instant::now();
|
||||||
|
let start_time = now;
|
||||||
|
let mut total_elapsed = start_time.elapsed();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let trade_keys = generate_keypairs(batch_size as u64);
|
||||||
|
|
||||||
|
let mut trades = vec![];
|
||||||
|
let mut trade_infos = vec![];
|
||||||
|
let start = account_group * batch_size as usize;
|
||||||
|
let end = account_group * batch_size as usize + batch_size as usize;
|
||||||
|
let mut direction = Direction::To;
|
||||||
|
for (signer, trade, src) in izip!(
|
||||||
|
signers[start..end].iter(),
|
||||||
|
trade_keys,
|
||||||
|
srcs[start..end].iter(),
|
||||||
|
) {
|
||||||
|
direction = if direction == Direction::To {
|
||||||
|
Direction::From
|
||||||
|
} else {
|
||||||
|
Direction::To
|
||||||
|
};
|
||||||
|
let order_info = TradeOrderInfo {
|
||||||
|
/// Owner of the trade order
|
||||||
|
owner: Pubkey::default(), // don't care
|
||||||
|
direction,
|
||||||
|
pair,
|
||||||
|
tokens,
|
||||||
|
price,
|
||||||
|
tokens_settled: 0,
|
||||||
|
};
|
||||||
|
trade_infos.push(TradeInfo {
|
||||||
|
trade_account: trade.pubkey(),
|
||||||
|
order_info,
|
||||||
|
});
|
||||||
|
trades.push((signer, trade.pubkey(), direction, src));
|
||||||
|
}
|
||||||
|
account_group = (account_group + 1) % account_groups as usize;
|
||||||
|
|
||||||
|
let (blockhash, _fee_calculator) = client
|
||||||
|
.get_recent_blockhash()
|
||||||
|
.expect("Failed to get blockhash");
|
||||||
|
|
||||||
|
trades.chunks(chunk_size).for_each(|chunk| {
|
||||||
|
let trades_txs: Vec<_> = chunk
|
||||||
|
.par_iter()
|
||||||
|
.map(|(signer, trade, direction, src)| {
|
||||||
|
let s: &Keypair = &signer;
|
||||||
|
let owner = &signer.pubkey();
|
||||||
|
let space = mem::size_of::<ExchangeState>() as u64;
|
||||||
|
Transaction::new_signed_instructions(
|
||||||
|
&[s],
|
||||||
|
vec![
|
||||||
|
system_instruction::create_account(owner, trade, 1, space, &id()),
|
||||||
|
exchange_instruction::trade_request(
|
||||||
|
owner, trade, *direction, pair, tokens, price, src,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
blockhash,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
{
|
||||||
|
txs += chunk_size as u64;
|
||||||
|
total_txs += chunk_size as u64;
|
||||||
|
total_elapsed = start_time.elapsed();
|
||||||
|
let duration = now.elapsed();
|
||||||
|
if duration_as_s(&duration) >= 1_f32 {
|
||||||
|
now = Instant::now();
|
||||||
|
let tps = txs as f32 / duration_as_s(&duration);
|
||||||
|
info!(
|
||||||
|
"Trader {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s",
|
||||||
|
tps,
|
||||||
|
txs,
|
||||||
|
total_txs,
|
||||||
|
total_elapsed.as_secs(),
|
||||||
|
);
|
||||||
|
txs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoint_info!("bench-exchange-trades", ("count", trades_txs.len(), i64));
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut shared_txs_wl = shared_txs
|
||||||
|
.write()
|
||||||
|
.expect("Failed to send tx to transfer threads");
|
||||||
|
shared_txs_wl.push_back(trades_txs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if transfer_delay > 0 {
|
||||||
|
sleep(Duration::from_millis(transfer_delay));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if exit_signal.load(Ordering::Relaxed) {
|
||||||
|
info!(
|
||||||
|
"Trader sent {} at {:9.2} TPS",
|
||||||
|
total_txs,
|
||||||
|
total_txs as f32 / duration_as_s(&total_elapsed)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO chunk the trade infos and send them when the batch is sent
|
||||||
|
sender
|
||||||
|
.send(trade_infos)
|
||||||
|
.expect("Failed to send trades to swapper");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_transfer<T>(sync_client: &T, tx: &Transaction) -> bool
|
||||||
|
where
|
||||||
|
T: SyncClient + ?Sized,
|
||||||
|
{
|
||||||
|
for s in &tx.signatures {
|
||||||
|
if let Ok(Some(r)) = sync_client.get_signature_status(s) {
|
||||||
|
match r {
|
||||||
|
Ok(_) => {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
info!("error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fund_keys(client: &Client, source: &Keypair, dests: &[Arc<Keypair>], lamports: u64) {
|
||||||
|
let total = lamports * (dests.len() as u64 + 1);
|
||||||
|
let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)];
|
||||||
|
let mut notfunded: Vec<&Arc<Keypair>> = dests.iter().collect();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
" Funding {} keys with {} lamports each",
|
||||||
|
dests.len(),
|
||||||
|
lamports
|
||||||
|
);
|
||||||
|
while !notfunded.is_empty() {
|
||||||
|
if funded.is_empty() {
|
||||||
|
panic!("No funded accounts left to fund remaining");
|
||||||
|
}
|
||||||
|
let mut new_funded: Vec<(&Keypair, u64)> = vec![];
|
||||||
|
let mut to_fund = vec![];
|
||||||
|
debug!(" Creating from... {}", funded.len());
|
||||||
|
for f in &mut funded {
|
||||||
|
let max_units = cmp::min(
|
||||||
|
cmp::min(notfunded.len() as u64, MAX_TRANSFERS_PER_TX),
|
||||||
|
(f.1 - lamports) / lamports,
|
||||||
|
);
|
||||||
|
if max_units == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let per_unit = ((f.1 - lamports) / lamports / max_units) * lamports;
|
||||||
|
f.1 -= per_unit * max_units;
|
||||||
|
let start = notfunded.len() - max_units as usize;
|
||||||
|
let moves: Vec<_> = notfunded[start..]
|
||||||
|
.iter()
|
||||||
|
.map(|k| (k.pubkey(), per_unit))
|
||||||
|
.collect();
|
||||||
|
notfunded[start..]
|
||||||
|
.iter()
|
||||||
|
.for_each(|k| new_funded.push((k, per_unit)));
|
||||||
|
notfunded.truncate(start);
|
||||||
|
if !moves.is_empty() {
|
||||||
|
to_fund.push((f.0, moves));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
to_fund.chunks(FUND_CHUNK_LEN).for_each(|chunk| {
|
||||||
|
#[allow(clippy::clone_double_ref)] // sigh
|
||||||
|
let mut to_fund_txs: Vec<_> = chunk
|
||||||
|
.par_iter()
|
||||||
|
.map(|(k, m)| {
|
||||||
|
(
|
||||||
|
k.clone(),
|
||||||
|
Transaction::new_unsigned_instructions(system_instruction::transfer_many(
|
||||||
|
&k.pubkey(),
|
||||||
|
&m,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut retries = 0;
|
||||||
|
while !to_fund_txs.is_empty() {
|
||||||
|
let receivers = to_fund_txs
|
||||||
|
.iter()
|
||||||
|
.fold(0, |len, (_, tx)| len + tx.message().instructions.len());
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
" {} to {} in {} txs",
|
||||||
|
if retries == 0 {
|
||||||
|
" Transferring"
|
||||||
|
} else {
|
||||||
|
" Retrying"
|
||||||
|
},
|
||||||
|
receivers,
|
||||||
|
to_fund_txs.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (blockhash, _fee_calculator) =
|
||||||
|
client.get_recent_blockhash().expect("blockhash");
|
||||||
|
to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
|
||||||
|
tx.sign(&[*k], blockhash);
|
||||||
|
});
|
||||||
|
to_fund_txs.iter().for_each(|(_, tx)| {
|
||||||
|
client.async_send_transaction(tx.clone()).expect("transfer");
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut waits = 0;
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_millis(200));
|
||||||
|
to_fund_txs.retain(|(_, tx)| !verify_transfer(client, &tx));
|
||||||
|
if to_fund_txs.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
debug!(
|
||||||
|
" {} transactions outstanding, {:?} waits",
|
||||||
|
to_fund_txs.len(),
|
||||||
|
waits
|
||||||
|
);
|
||||||
|
waits += 1;
|
||||||
|
if waits >= 5 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !to_fund_txs.is_empty() {
|
||||||
|
retries += 1;
|
||||||
|
debug!(" Retry {:?}", retries);
|
||||||
|
if retries >= 10 {
|
||||||
|
error!(" Too many retries, give up");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
funded.append(&mut new_funded);
|
||||||
|
funded.retain(|(k, b)| {
|
||||||
|
client.get_balance(&k.pubkey()).unwrap_or(0) > lamports && *b > lamports
|
||||||
|
});
|
||||||
|
debug!(" Funded: {} left: {}", funded.len(), notfunded.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_token_accounts(client: &Client, signers: &[Arc<Keypair>], accounts: &[Pubkey]) {
|
||||||
|
let mut notfunded: Vec<(&Arc<Keypair>, &Pubkey)> = signers.iter().zip(accounts).collect();
|
||||||
|
|
||||||
|
while !notfunded.is_empty() {
|
||||||
|
notfunded.chunks(FUND_CHUNK_LEN).for_each(|chunk| {
|
||||||
|
let mut to_create_txs: Vec<_> = chunk
|
||||||
|
.par_iter()
|
||||||
|
.map(|(signer, new)| {
|
||||||
|
let owner_pubkey = &signer.pubkey();
|
||||||
|
let space = mem::size_of::<ExchangeState>() as u64;
|
||||||
|
let create_ix =
|
||||||
|
system_instruction::create_account(owner_pubkey, new, 1, space, &id());
|
||||||
|
let request_ix = exchange_instruction::account_request(owner_pubkey, new);
|
||||||
|
(
|
||||||
|
signer,
|
||||||
|
Transaction::new_unsigned_instructions(vec![create_ix, request_ix]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let accounts = to_create_txs
|
||||||
|
.iter()
|
||||||
|
.fold(0, |len, (_, tx)| len + tx.message().instructions.len() / 2);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
" Creating {} accounts in {} txs",
|
||||||
|
accounts,
|
||||||
|
to_create_txs.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut retries = 0;
|
||||||
|
while !to_create_txs.is_empty() {
|
||||||
|
let (blockhash, _fee_calculator) = client
|
||||||
|
.get_recent_blockhash()
|
||||||
|
.expect("Failed to get blockhash");
|
||||||
|
to_create_txs.par_iter_mut().for_each(|(k, tx)| {
|
||||||
|
let kp: &Keypair = k;
|
||||||
|
tx.sign(&[kp], blockhash);
|
||||||
|
});
|
||||||
|
to_create_txs.iter().for_each(|(_, tx)| {
|
||||||
|
client.async_send_transaction(tx.clone()).expect("transfer");
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut waits = 0;
|
||||||
|
while !to_create_txs.is_empty() {
|
||||||
|
sleep(Duration::from_millis(200));
|
||||||
|
to_create_txs.retain(|(_, tx)| !verify_transfer(client, &tx));
|
||||||
|
if to_create_txs.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
debug!(
|
||||||
|
" {} transactions outstanding, waits {:?}",
|
||||||
|
to_create_txs.len(),
|
||||||
|
waits
|
||||||
|
);
|
||||||
|
waits += 1;
|
||||||
|
if waits >= 5 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !to_create_txs.is_empty() {
|
||||||
|
retries += 1;
|
||||||
|
debug!(" Retry {:?}", retries);
|
||||||
|
if retries >= 20 {
|
||||||
|
error!(" Too many retries, give up");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut new_notfunded: Vec<(&Arc<Keypair>, &Pubkey)> = vec![];
|
||||||
|
for f in ¬funded {
|
||||||
|
if client.get_balance(&f.1).unwrap_or(0) == 0 {
|
||||||
|
new_notfunded.push(*f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notfunded = new_notfunded;
|
||||||
|
debug!(" Left: {}", notfunded.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_and_report_stats(maxes: &Arc<RwLock<Vec<(String, SampleStats)>>>, total_txs_sent: u64) {
|
||||||
|
let mut max_txs = 0;
|
||||||
|
let mut max_elapsed = Duration::new(0, 0);
|
||||||
|
info!("| Max TPS | Total Transactions");
|
||||||
|
info!("+---------------+--------------------");
|
||||||
|
|
||||||
|
for (_sock, stats) in maxes.read().unwrap().iter() {
|
||||||
|
let maybe_flag = match stats.txs {
|
||||||
|
0 => "!!!!!",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("| {:13.2} | {} {}", stats.tps, stats.txs, maybe_flag);
|
||||||
|
|
||||||
|
if stats.elapsed > max_elapsed {
|
||||||
|
max_elapsed = stats.elapsed;
|
||||||
|
}
|
||||||
|
if stats.txs > max_txs {
|
||||||
|
max_txs = stats.txs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("+---------------+--------------------");
|
||||||
|
|
||||||
|
if max_txs >= total_txs_sent {
|
||||||
|
info!(
|
||||||
|
"Warning: Average TPS might be under reported, there were no txs sent for a portion of the duration"
|
||||||
|
);
|
||||||
|
max_txs = total_txs_sent;
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"{} txs outstanding when test ended (lag) ({:.2}%)",
|
||||||
|
total_txs_sent - max_txs,
|
||||||
|
(total_txs_sent - max_txs) as f64 / total_txs_sent as f64 * 100_f64
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"\tAverage TPS: {:.2}",
|
||||||
|
max_txs as f32 / max_elapsed.as_secs() as f32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_keypairs(num: u64) -> Vec<Keypair> {
|
||||||
|
let mut seed = [0_u8; 32];
|
||||||
|
seed.copy_from_slice(&Keypair::new().pubkey().as_ref());
|
||||||
|
let mut rnd = GenKeys::new(seed);
|
||||||
|
rnd.gen_n_keypairs(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn airdrop_lamports(client: &Client, drone_addr: &SocketAddr, id: &Keypair, amount: u64) {
|
||||||
|
let balance = client.get_balance(&id.pubkey());
|
||||||
|
let balance = balance.unwrap_or(0);
|
||||||
|
if balance >= amount {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let amount_to_drop = amount - balance;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Airdropping {:?} lamports from {} for {}",
|
||||||
|
amount_to_drop,
|
||||||
|
drone_addr,
|
||||||
|
id.pubkey(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut tries = 0;
|
||||||
|
loop {
|
||||||
|
let (blockhash, _fee_calculator) = client
|
||||||
|
.get_recent_blockhash()
|
||||||
|
.expect("Failed to get blockhash");
|
||||||
|
match request_airdrop_transaction(&drone_addr, &id.pubkey(), amount_to_drop, blockhash) {
|
||||||
|
Ok(transaction) => {
|
||||||
|
let signature = client.async_send_transaction(transaction).unwrap();
|
||||||
|
|
||||||
|
for _ in 0..30 {
|
||||||
|
if let Ok(Some(_)) = client.get_signature_status(&signature) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
if client.get_balance(&id.pubkey()).unwrap_or(0) >= amount {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
panic!(
|
||||||
|
"Error requesting airdrop: {:?} to addr: {:?} amount: {}",
|
||||||
|
err, drone_addr, amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!(" Retry...");
|
||||||
|
tries += 1;
|
||||||
|
if tries > 50 {
|
||||||
|
error!("Too many retries, give up");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
sleep(Duration::from_secs(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use solana::gossip_service::{discover_cluster, get_clients};
|
||||||
|
use solana::local_cluster::{ClusterConfig, LocalCluster};
|
||||||
|
use solana::validator::ValidatorConfig;
|
||||||
|
use solana_drone::drone::run_local_drone;
|
||||||
|
use solana_exchange_api::exchange_processor::process_instruction;
|
||||||
|
use solana_runtime::bank::Bank;
|
||||||
|
use solana_runtime::bank_client::BankClient;
|
||||||
|
use solana_sdk::genesis_block::create_genesis_block;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exchange_local_cluster() {
|
||||||
|
solana_logger::setup();
|
||||||
|
|
||||||
|
const NUM_NODES: usize = 1;
|
||||||
|
let validator_config = ValidatorConfig::default();
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.identity = Keypair::new();
|
||||||
|
config.duration = Duration::from_secs(1);
|
||||||
|
config.fund_amount = 100_000;
|
||||||
|
config.threads = 1;
|
||||||
|
config.transfer_delay = 20; // 15
|
||||||
|
config.batch_size = 100; // 1000;
|
||||||
|
config.chunk_size = 10; // 200;
|
||||||
|
config.account_groups = 1; // 10;
|
||||||
|
let Config {
|
||||||
|
fund_amount,
|
||||||
|
batch_size,
|
||||||
|
account_groups,
|
||||||
|
..
|
||||||
|
} = config;
|
||||||
|
let accounts_in_groups = batch_size * account_groups;
|
||||||
|
|
||||||
|
let cluster = LocalCluster::new(&ClusterConfig {
|
||||||
|
node_stakes: vec![100_000; NUM_NODES],
|
||||||
|
cluster_lamports: 100_000_000_000_000,
|
||||||
|
validator_config,
|
||||||
|
native_instruction_processors: [solana_exchange_program!()].to_vec(),
|
||||||
|
..ClusterConfig::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let drone_keypair = Keypair::new();
|
||||||
|
cluster.transfer(
|
||||||
|
&cluster.funding_keypair,
|
||||||
|
&drone_keypair.pubkey(),
|
||||||
|
2_000_000_000_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (addr_sender, addr_receiver) = channel();
|
||||||
|
run_local_drone(drone_keypair, addr_sender, Some(1_000_000_000_000));
|
||||||
|
let drone_addr = addr_receiver.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
|
|
||||||
|
info!("Connecting to the cluster");
|
||||||
|
let (nodes, _) = discover_cluster(&cluster.entry_point_info.gossip, NUM_NODES)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
error!("Failed to discover {} nodes: {:?}", NUM_NODES, err);
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let clients = get_clients(&nodes);
|
||||||
|
|
||||||
|
if clients.len() < NUM_NODES {
|
||||||
|
error!(
|
||||||
|
"Error: Insufficient nodes discovered. Expecting {} or more",
|
||||||
|
NUM_NODES
|
||||||
|
);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NUM_SIGNERS: u64 = 2;
|
||||||
|
airdrop_lamports(
|
||||||
|
&clients[0],
|
||||||
|
&drone_addr,
|
||||||
|
&config.identity,
|
||||||
|
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
|
||||||
|
);
|
||||||
|
|
||||||
|
do_bench_exchange(clients, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exchange_bank_client() {
|
||||||
|
solana_logger::setup();
|
||||||
|
let (genesis_block, identity) = create_genesis_block(100_000_000_000_000);
|
||||||
|
let mut bank = Bank::new(&genesis_block);
|
||||||
|
bank.add_instruction_processor(id(), process_instruction);
|
||||||
|
let clients = vec![BankClient::new(bank)];
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.identity = identity;
|
||||||
|
config.duration = Duration::from_secs(1);
|
||||||
|
config.fund_amount = 100_000;
|
||||||
|
config.threads = 1;
|
||||||
|
config.transfer_delay = 20; // 0;
|
||||||
|
config.batch_size = 100; // 1500;
|
||||||
|
config.chunk_size = 10; // 1500;
|
||||||
|
config.account_groups = 1; // 50;
|
||||||
|
|
||||||
|
do_bench_exchange(clients, config);
|
||||||
|
}
|
||||||
|
}
|
188
bench-exchange/src/cli.rs
Normal file
188
bench-exchange/src/cli.rs
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
use clap::{crate_description, crate_name, crate_version, value_t, App, Arg, ArgMatches};
|
||||||
|
use solana::gen_keys::GenKeys;
|
||||||
|
use solana_drone::drone::DRONE_PORT;
|
||||||
|
use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::process::exit;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
pub entrypoint_addr: SocketAddr,
|
||||||
|
pub drone_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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||||
|
drone_addr: SocketAddr::from(([127, 0, 0, 1], DRONE_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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||||
|
App::new(crate_name!())
|
||||||
|
.about(crate_description!())
|
||||||
|
.version(crate_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("drone")
|
||||||
|
.short("d")
|
||||||
|
.long("drone")
|
||||||
|
.value_name("HOST:PORT")
|
||||||
|
.takes_value(true)
|
||||||
|
.required(false)
|
||||||
|
.default_value("127.0.0.1:9900")
|
||||||
|
.help("Location of the drone; 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"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
||||||
|
let mut args = Config::default();
|
||||||
|
|
||||||
|
args.entrypoint_addr = solana_netutil::parse_host_port(matches.value_of("entrypoint").unwrap())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("failed to parse entrypoint address: {}", e);
|
||||||
|
exit(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
args.drone_addr = solana_netutil::parse_host_port(matches.value_of("drone").unwrap())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("failed to parse drone address: {}", e);
|
||||||
|
exit(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
if matches.is_present("identity") {
|
||||||
|
args.identity = read_keypair(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");
|
||||||
|
|
||||||
|
args
|
||||||
|
}
|
72
bench-exchange/src/main.rs
Normal file
72
bench-exchange/src/main.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
pub mod bench;
|
||||||
|
mod cli;
|
||||||
|
pub mod order_book;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate solana_exchange_program;
|
||||||
|
|
||||||
|
use crate::bench::{airdrop_lamports, do_bench_exchange, Config};
|
||||||
|
use log::*;
|
||||||
|
use solana::gossip_service::{discover_cluster, get_clients};
|
||||||
|
use solana_sdk::signature::KeypairUtil;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
solana_logger::setup();
|
||||||
|
solana_metrics::set_panic_hook("bench-exchange");
|
||||||
|
|
||||||
|
let matches = cli::build_args().get_matches();
|
||||||
|
let cli_config = cli::extract_args(&matches);
|
||||||
|
|
||||||
|
let cli::Config {
|
||||||
|
entrypoint_addr,
|
||||||
|
drone_addr,
|
||||||
|
identity,
|
||||||
|
threads,
|
||||||
|
num_nodes,
|
||||||
|
duration,
|
||||||
|
transfer_delay,
|
||||||
|
fund_amount,
|
||||||
|
batch_size,
|
||||||
|
chunk_size,
|
||||||
|
account_groups,
|
||||||
|
..
|
||||||
|
} = cli_config;
|
||||||
|
|
||||||
|
info!("Connecting to the cluster");
|
||||||
|
let (nodes, _replicators) =
|
||||||
|
discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|_| {
|
||||||
|
panic!("Failed to discover nodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
let clients = get_clients(&nodes);
|
||||||
|
|
||||||
|
info!("{} nodes found", clients.len());
|
||||||
|
if clients.len() < num_nodes {
|
||||||
|
panic!("Error: Insufficient nodes discovered");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Funding keypair: {}", identity.pubkey());
|
||||||
|
|
||||||
|
let accounts_in_groups = batch_size * account_groups;
|
||||||
|
const NUM_SIGNERS: u64 = 2;
|
||||||
|
airdrop_lamports(
|
||||||
|
&clients[0],
|
||||||
|
&drone_addr,
|
||||||
|
&identity,
|
||||||
|
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
identity,
|
||||||
|
threads,
|
||||||
|
duration,
|
||||||
|
transfer_delay,
|
||||||
|
fund_amount,
|
||||||
|
batch_size,
|
||||||
|
chunk_size,
|
||||||
|
account_groups,
|
||||||
|
};
|
||||||
|
|
||||||
|
do_bench_exchange(clients, config);
|
||||||
|
}
|
138
bench-exchange/src/order_book.rs
Normal file
138
bench-exchange/src/order_book.rs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
use itertools::EitherOrBoth::{Both, Left, Right};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use log::*;
|
||||||
|
use solana_exchange_api::exchange_state::*;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::BinaryHeap;
|
||||||
|
use std::{error, fmt};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ToOrder {
|
||||||
|
pub pubkey: Pubkey,
|
||||||
|
pub info: TradeOrderInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
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: TradeOrderInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
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: TradeOrderInfo,
|
||||||
|
) -> Result<(), Box<dyn error::Error>> {
|
||||||
|
check_trade(info.direction, info.tokens, info.price)?;
|
||||||
|
match info.direction {
|
||||||
|
Direction::To => {
|
||||||
|
self.to_ab.push(ToOrder { pubkey, info });
|
||||||
|
}
|
||||||
|
Direction::From => {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
1
bench-streamer/.gitignore
vendored
Normal file
1
bench-streamer/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target/
|
@ -2,16 +2,16 @@
|
|||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
name = "solana-bench-streamer"
|
name = "solana-bench-streamer"
|
||||||
version = "0.13.0"
|
version = "0.15.0"
|
||||||
repository = "https://github.com/solana-labs/solana"
|
repository = "https://github.com/solana-labs/solana"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
homepage = "https://solana.com/"
|
homepage = "https://solana.com/"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33.0"
|
clap = "2.33.0"
|
||||||
solana = { path = "../core", version = "0.13.0" }
|
solana = { path = "../core", version = "0.15.0" }
|
||||||
solana-logger = { path = "../logger", version = "0.13.0" }
|
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||||
solana-netutil = { path = "../netutil", version = "0.13.0" }
|
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
cuda = ["solana/cuda"]
|
cuda = ["solana/cuda"]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use clap::{crate_description, crate_name, crate_version, App, Arg};
|
use clap::{crate_description, crate_name, crate_version, App, Arg};
|
||||||
use solana::packet::{Packet, SharedPackets, BLOB_SIZE, PACKET_DATA_SIZE};
|
use solana::packet::{Packet, Packets, BLOB_SIZE, PACKET_DATA_SIZE};
|
||||||
use solana::result::Result;
|
use solana::result::Result;
|
||||||
use solana::streamer::{receiver, PacketReceiver};
|
use solana::streamer::{receiver, PacketReceiver};
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
@ -14,19 +14,19 @@ use std::time::SystemTime;
|
|||||||
|
|
||||||
fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
|
fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
|
||||||
let send = UdpSocket::bind("0.0.0.0:0").unwrap();
|
let send = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||||
let msgs = SharedPackets::default();
|
let mut msgs = Packets::default();
|
||||||
let msgs_ = msgs.clone();
|
msgs.packets.resize(10, Packet::default());
|
||||||
msgs.write().unwrap().packets.resize(10, Packet::default());
|
for w in &mut msgs.packets {
|
||||||
for w in &mut msgs.write().unwrap().packets {
|
|
||||||
w.meta.size = PACKET_DATA_SIZE;
|
w.meta.size = PACKET_DATA_SIZE;
|
||||||
w.meta.set_addr(&addr);
|
w.meta.set_addr(&addr);
|
||||||
}
|
}
|
||||||
|
let msgs = Arc::new(msgs);
|
||||||
spawn(move || loop {
|
spawn(move || loop {
|
||||||
if exit.load(Ordering::Relaxed) {
|
if exit.load(Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut num = 0;
|
let mut num = 0;
|
||||||
for p in &msgs_.read().unwrap().packets {
|
for p in &msgs.packets {
|
||||||
let a = p.meta.addr();
|
let a = p.meta.addr();
|
||||||
assert!(p.meta.size < BLOB_SIZE);
|
assert!(p.meta.size < BLOB_SIZE);
|
||||||
send.send_to(&p.data[..p.meta.size], &a).unwrap();
|
send.send_to(&p.data[..p.meta.size], &a).unwrap();
|
||||||
@ -43,7 +43,7 @@ fn sink(exit: Arc<AtomicBool>, rvs: Arc<AtomicUsize>, r: PacketReceiver) -> Join
|
|||||||
}
|
}
|
||||||
let timer = Duration::new(1, 0);
|
let timer = Duration::new(1, 0);
|
||||||
if let Ok(msgs) = r.recv_timeout(timer) {
|
if let Ok(msgs) = r.recv_timeout(timer) {
|
||||||
rvs.fetch_add(msgs.read().unwrap().packets.len(), Ordering::Relaxed);
|
rvs.fetch_add(msgs.packets.len(), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
let (s_reader, r_reader) = channel();
|
let (s_reader, r_reader) = channel();
|
||||||
read_channels.push(r_reader);
|
read_channels.push(r_reader);
|
||||||
read_threads.push(receiver(Arc::new(read), &exit, s_reader, "bench-streamer"));
|
read_threads.push(receiver(Arc::new(read), &exit, s_reader));
|
||||||
}
|
}
|
||||||
|
|
||||||
let t_producer1 = producer(&addr, exit.clone());
|
let t_producer1 = producer(&addr, exit.clone());
|
||||||
|
3
bench-tps/.gitignore
vendored
Normal file
3
bench-tps/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target/
|
||||||
|
/config/
|
||||||
|
/config-local/
|
@ -2,22 +2,24 @@
|
|||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
name = "solana-bench-tps"
|
name = "solana-bench-tps"
|
||||||
version = "0.13.0"
|
version = "0.15.0"
|
||||||
repository = "https://github.com/solana-labs/solana"
|
repository = "https://github.com/solana-labs/solana"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
homepage = "https://solana.com/"
|
homepage = "https://solana.com/"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33.0"
|
clap = "2.33.0"
|
||||||
|
log = "0.4.6"
|
||||||
rayon = "1.0.3"
|
rayon = "1.0.3"
|
||||||
serde_json = "1.0.39"
|
serde_json = "1.0.39"
|
||||||
solana = { path = "../core", version = "0.13.0" }
|
solana = { path = "../core", version = "0.15.0" }
|
||||||
solana-client = { path = "../client", version = "0.13.0" }
|
solana-client = { path = "../client", version = "0.15.0" }
|
||||||
solana-drone = { path = "../drone", version = "0.13.0" }
|
solana-drone = { path = "../drone", version = "0.15.0" }
|
||||||
solana-logger = { path = "../logger", version = "0.13.0" }
|
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||||
solana-metrics = { path = "../metrics", version = "0.13.0" }
|
solana-metrics = { path = "../metrics", version = "0.15.0" }
|
||||||
solana-netutil = { path = "../netutil", version = "0.13.0" }
|
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||||
solana-sdk = { path = "../sdk", version = "0.13.0" }
|
solana-runtime = { path = "../runtime", version = "0.15.0" }
|
||||||
|
solana-sdk = { path = "../sdk", version = "0.15.0" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
cuda = ["solana/cuda"]
|
cuda = ["solana/cuda"]
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
use solana_metrics;
|
use solana_metrics;
|
||||||
|
|
||||||
use crate::cli::Config;
|
use log::*;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use solana::cluster_info::FULLNODE_PORT_RANGE;
|
|
||||||
use solana::contact_info::ContactInfo;
|
|
||||||
use solana::gen_keys::GenKeys;
|
use solana::gen_keys::GenKeys;
|
||||||
use solana::gossip_service::discover_nodes;
|
use solana_client::perf_utils::{sample_txs, SampleStats};
|
||||||
use solana_client::thin_client::create_client;
|
|
||||||
use solana_client::thin_client::ThinClient;
|
|
||||||
use solana_drone::drone::request_airdrop_transaction;
|
use solana_drone::drone::request_airdrop_transaction;
|
||||||
use solana_metrics::influxdb;
|
use solana_metrics::datapoint_info;
|
||||||
use solana_sdk::client::{AsyncClient, SyncClient};
|
use solana_sdk::client::Client;
|
||||||
use solana_sdk::hash::Hash;
|
use solana_sdk::hash::Hash;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
use solana_sdk::system_instruction;
|
use solana_sdk::system_instruction;
|
||||||
use solana_sdk::system_transaction;
|
use solana_sdk::system_transaction;
|
||||||
@ -30,85 +25,56 @@ use std::thread::Builder;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
pub struct NodeStats {
|
|
||||||
/// Maximum TPS reported by this node
|
|
||||||
pub tps: f64,
|
|
||||||
/// Total transactions reported by this node
|
|
||||||
pub tx: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MAX_SPENDS_PER_TX: usize = 4;
|
pub const MAX_SPENDS_PER_TX: usize = 4;
|
||||||
|
pub const NUM_LAMPORTS_PER_ACCOUNT: u64 = 20;
|
||||||
|
|
||||||
pub type SharedTransactions = Arc<RwLock<VecDeque<Vec<(Transaction, u64)>>>>;
|
pub type SharedTransactions = Arc<RwLock<VecDeque<Vec<(Transaction, u64)>>>>;
|
||||||
|
|
||||||
pub fn do_bench_tps(config: Config) {
|
pub struct Config {
|
||||||
|
pub id: Keypair,
|
||||||
|
pub threads: usize,
|
||||||
|
pub thread_batch_sleep_ms: usize,
|
||||||
|
pub duration: Duration,
|
||||||
|
pub tx_count: usize,
|
||||||
|
pub sustained: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Keypair::new(),
|
||||||
|
threads: 4,
|
||||||
|
thread_batch_sleep_ms: 0,
|
||||||
|
duration: Duration::new(std::u64::MAX, 0),
|
||||||
|
tx_count: 500_000,
|
||||||
|
sustained: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn do_bench_tps<T>(
|
||||||
|
clients: Vec<T>,
|
||||||
|
config: Config,
|
||||||
|
gen_keypairs: Vec<Keypair>,
|
||||||
|
keypair0_balance: u64,
|
||||||
|
) -> u64
|
||||||
|
where
|
||||||
|
T: 'static + Client + Send + Sync,
|
||||||
|
{
|
||||||
let Config {
|
let Config {
|
||||||
network_addr: network,
|
|
||||||
drone_addr,
|
|
||||||
id,
|
id,
|
||||||
threads,
|
threads,
|
||||||
thread_batch_sleep_ms,
|
thread_batch_sleep_ms,
|
||||||
num_nodes,
|
|
||||||
duration,
|
duration,
|
||||||
tx_count,
|
tx_count,
|
||||||
sustained,
|
sustained,
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
let nodes = discover_nodes(&network, num_nodes).unwrap_or_else(|err| {
|
let clients: Vec<_> = clients.into_iter().map(Arc::new).collect();
|
||||||
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
let client = &clients[0];
|
||||||
exit(1);
|
|
||||||
});
|
|
||||||
if nodes.len() < num_nodes {
|
|
||||||
eprintln!(
|
|
||||||
"Error: Insufficient nodes discovered. Expecting {} or more",
|
|
||||||
num_nodes
|
|
||||||
);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
let cluster_entrypoint = nodes[0].clone(); // Pick the first node, why not?
|
|
||||||
|
|
||||||
let client = create_client(cluster_entrypoint.client_facing_addr(), FULLNODE_PORT_RANGE);
|
|
||||||
let mut barrier_client =
|
|
||||||
create_client(cluster_entrypoint.client_facing_addr(), FULLNODE_PORT_RANGE);
|
|
||||||
|
|
||||||
let mut seed = [0u8; 32];
|
|
||||||
seed.copy_from_slice(&id.public_key_bytes()[..32]);
|
|
||||||
let mut rnd = GenKeys::new(seed);
|
|
||||||
|
|
||||||
println!("Creating {} keypairs...", tx_count * 2);
|
|
||||||
let mut total_keys = 0;
|
|
||||||
let mut target = tx_count * 2;
|
|
||||||
while target > 0 {
|
|
||||||
total_keys += target;
|
|
||||||
target /= MAX_SPENDS_PER_TX;
|
|
||||||
}
|
|
||||||
let gen_keypairs = rnd.gen_n_keypairs(total_keys as u64);
|
|
||||||
let barrier_source_keypair = Keypair::new();
|
|
||||||
let barrier_dest_id = Pubkey::new_rand();
|
|
||||||
|
|
||||||
println!("Get lamports...");
|
|
||||||
let num_lamports_per_account = 20;
|
|
||||||
|
|
||||||
// Sample the first keypair, see if it has lamports, if so then resume
|
|
||||||
// to avoid lamport loss
|
|
||||||
let keypair0_balance = client
|
|
||||||
.poll_get_balance(&gen_keypairs.last().unwrap().pubkey())
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
if num_lamports_per_account > keypair0_balance {
|
|
||||||
let extra = num_lamports_per_account - keypair0_balance;
|
|
||||||
let total = extra * (gen_keypairs.len() as u64);
|
|
||||||
airdrop_lamports(&client, &drone_addr, &id, total);
|
|
||||||
println!("adding more lamports {}", extra);
|
|
||||||
fund_keys(&client, &id, &gen_keypairs, extra);
|
|
||||||
}
|
|
||||||
let start = gen_keypairs.len() - (tx_count * 2) as usize;
|
let start = gen_keypairs.len() - (tx_count * 2) as usize;
|
||||||
let keypairs = &gen_keypairs[start..];
|
let keypairs = &gen_keypairs[start..];
|
||||||
airdrop_lamports(&barrier_client, &drone_addr, &barrier_source_keypair, 1);
|
|
||||||
|
|
||||||
println!("Get last ID...");
|
|
||||||
let mut blockhash = client.get_recent_blockhash().unwrap();
|
|
||||||
println!("Got last ID {:?}", blockhash);
|
|
||||||
|
|
||||||
let first_tx_count = client.get_transaction_count().expect("transaction count");
|
let first_tx_count = client.get_transaction_count().expect("transaction count");
|
||||||
println!("Initial transaction count {}", first_tx_count);
|
println!("Initial transaction count {}", first_tx_count);
|
||||||
@ -120,15 +86,16 @@ pub fn do_bench_tps(config: Config) {
|
|||||||
let maxes = Arc::new(RwLock::new(Vec::new()));
|
let maxes = Arc::new(RwLock::new(Vec::new()));
|
||||||
let sample_period = 1; // in seconds
|
let sample_period = 1; // in seconds
|
||||||
println!("Sampling TPS every {} second...", sample_period);
|
println!("Sampling TPS every {} second...", sample_period);
|
||||||
let v_threads: Vec<_> = nodes
|
let v_threads: Vec<_> = clients
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|v| {
|
.map(|client| {
|
||||||
let exit_signal = exit_signal.clone();
|
let exit_signal = exit_signal.clone();
|
||||||
let maxes = maxes.clone();
|
let maxes = maxes.clone();
|
||||||
|
let client = client.clone();
|
||||||
Builder::new()
|
Builder::new()
|
||||||
.name("solana-client-sample".to_string())
|
.name("solana-client-sample".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
sample_tx_count(&exit_signal, &maxes, first_tx_count, &v, sample_period);
|
sample_txs(&exit_signal, &maxes, sample_period, &client);
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
})
|
})
|
||||||
@ -143,19 +110,19 @@ pub fn do_bench_tps(config: Config) {
|
|||||||
.map(|_| {
|
.map(|_| {
|
||||||
let exit_signal = exit_signal.clone();
|
let exit_signal = exit_signal.clone();
|
||||||
let shared_txs = shared_txs.clone();
|
let shared_txs = shared_txs.clone();
|
||||||
let cluster_entrypoint = cluster_entrypoint.clone();
|
|
||||||
let shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
|
let shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
|
||||||
let total_tx_sent_count = total_tx_sent_count.clone();
|
let total_tx_sent_count = total_tx_sent_count.clone();
|
||||||
|
let client = client.clone();
|
||||||
Builder::new()
|
Builder::new()
|
||||||
.name("solana-client-sender".to_string())
|
.name("solana-client-sender".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
do_tx_transfers(
|
do_tx_transfers(
|
||||||
&exit_signal,
|
&exit_signal,
|
||||||
&shared_txs,
|
&shared_txs,
|
||||||
&cluster_entrypoint,
|
|
||||||
&shared_tx_active_thread_count,
|
&shared_tx_active_thread_count,
|
||||||
&total_tx_sent_count,
|
&total_tx_sent_count,
|
||||||
thread_batch_sleep_ms,
|
thread_batch_sleep_ms,
|
||||||
|
&client,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -166,42 +133,44 @@ pub fn do_bench_tps(config: Config) {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut reclaim_lamports_back_to_source_account = false;
|
let mut reclaim_lamports_back_to_source_account = false;
|
||||||
let mut i = keypair0_balance;
|
let mut i = keypair0_balance;
|
||||||
|
let mut blockhash = Hash::default();
|
||||||
|
let mut blockhash_time = Instant::now();
|
||||||
while start.elapsed() < duration {
|
while start.elapsed() < duration {
|
||||||
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
|
||||||
metrics_submit_lamport_balance(balance);
|
|
||||||
|
|
||||||
// ping-pong between source and destination accounts for each loop iteration
|
// ping-pong between source and destination accounts for each loop iteration
|
||||||
// this seems to be faster than trying to determine the balance of individual
|
// this seems to be faster than trying to determine the balance of individual
|
||||||
// accounts
|
// accounts
|
||||||
let len = tx_count as usize;
|
let len = tx_count as usize;
|
||||||
|
if let Ok((new_blockhash, _fee_calculator)) = client.get_new_blockhash(&blockhash) {
|
||||||
|
blockhash = new_blockhash;
|
||||||
|
} else {
|
||||||
|
if blockhash_time.elapsed().as_secs() > 30 {
|
||||||
|
panic!("Blockhash is not updating");
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
blockhash_time = Instant::now();
|
||||||
|
let balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||||
|
metrics_submit_lamport_balance(balance);
|
||||||
generate_txs(
|
generate_txs(
|
||||||
&shared_txs,
|
&shared_txs,
|
||||||
|
&blockhash,
|
||||||
&keypairs[..len],
|
&keypairs[..len],
|
||||||
&keypairs[len..],
|
&keypairs[len..],
|
||||||
threads,
|
threads,
|
||||||
reclaim_lamports_back_to_source_account,
|
reclaim_lamports_back_to_source_account,
|
||||||
&cluster_entrypoint,
|
|
||||||
);
|
);
|
||||||
// In sustained mode overlap the transfers with generation
|
// In sustained mode overlap the transfers with generation
|
||||||
// this has higher average performance but lower peak performance
|
// this has higher average performance but lower peak performance
|
||||||
// in tested environments.
|
// in tested environments.
|
||||||
if !sustained {
|
if !sustained {
|
||||||
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
|
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
|
||||||
sleep(Duration::from_millis(100));
|
sleep(Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// It's not feasible (would take too much time) to confirm each of the `tx_count / 2`
|
|
||||||
// transactions sent by `generate_txs()` so instead send and confirm a single transaction
|
|
||||||
// to validate the network is still functional.
|
|
||||||
send_barrier_transaction(
|
|
||||||
&mut barrier_client,
|
|
||||||
&mut blockhash,
|
|
||||||
&barrier_source_keypair,
|
|
||||||
&barrier_dest_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
i += 1;
|
i += 1;
|
||||||
if should_switch_directions(num_lamports_per_account, i) {
|
if should_switch_directions(NUM_LAMPORTS_PER_ACCOUNT, i) {
|
||||||
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
|
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +193,7 @@ pub fn do_bench_tps(config: Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
let balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||||
metrics_submit_lamport_balance(balance);
|
metrics_submit_lamport_balance(balance);
|
||||||
|
|
||||||
compute_and_report_stats(
|
compute_and_report_stats(
|
||||||
@ -233,160 +202,27 @@ pub fn do_bench_tps(config: Config) {
|
|||||||
&start.elapsed(),
|
&start.elapsed(),
|
||||||
total_tx_sent_count.load(Ordering::Relaxed),
|
total_tx_sent_count.load(Ordering::Relaxed),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let r_maxes = maxes.read().unwrap();
|
||||||
|
r_maxes.first().unwrap().1.txs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
||||||
println!("Token balance: {}", lamport_balance);
|
println!("Token balance: {}", lamport_balance);
|
||||||
solana_metrics::submit(
|
datapoint_info!(
|
||||||
influxdb::Point::new("bench-tps")
|
"bench-tps-lamport_balance",
|
||||||
.add_tag("op", influxdb::Value::String("lamport_balance".to_string()))
|
("balance", lamport_balance, i64)
|
||||||
.add_field("balance", influxdb::Value::Integer(lamport_balance as i64))
|
|
||||||
.to_owned(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_tx_count(
|
|
||||||
exit_signal: &Arc<AtomicBool>,
|
|
||||||
maxes: &Arc<RwLock<Vec<(SocketAddr, NodeStats)>>>,
|
|
||||||
first_tx_count: u64,
|
|
||||||
v: &ContactInfo,
|
|
||||||
sample_period: u64,
|
|
||||||
) {
|
|
||||||
let client = create_client(v.client_facing_addr(), FULLNODE_PORT_RANGE);
|
|
||||||
let mut now = Instant::now();
|
|
||||||
let mut initial_tx_count = client.get_transaction_count().expect("transaction count");
|
|
||||||
let mut max_tps = 0.0;
|
|
||||||
let mut total;
|
|
||||||
|
|
||||||
let log_prefix = format!("{:21}:", v.tpu.to_string());
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let tx_count = client.get_transaction_count().expect("transaction count");
|
|
||||||
assert!(
|
|
||||||
tx_count >= initial_tx_count,
|
|
||||||
"expected tx_count({}) >= initial_tx_count({})",
|
|
||||||
tx_count,
|
|
||||||
initial_tx_count
|
|
||||||
);
|
|
||||||
let duration = now.elapsed();
|
|
||||||
now = Instant::now();
|
|
||||||
let sample = tx_count - initial_tx_count;
|
|
||||||
initial_tx_count = tx_count;
|
|
||||||
|
|
||||||
let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos());
|
|
||||||
let tps = (sample * 1_000_000_000) as f64 / ns as f64;
|
|
||||||
if tps > max_tps {
|
|
||||||
max_tps = tps;
|
|
||||||
}
|
|
||||||
if tx_count > first_tx_count {
|
|
||||||
total = tx_count - first_tx_count;
|
|
||||||
} else {
|
|
||||||
total = 0;
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"{} {:9.2} TPS, Transactions: {:6}, Total transactions: {}",
|
|
||||||
log_prefix, tps, sample, total
|
|
||||||
);
|
|
||||||
sleep(Duration::new(sample_period, 0));
|
|
||||||
|
|
||||||
if exit_signal.load(Ordering::Relaxed) {
|
|
||||||
println!("{} Exiting validator thread", log_prefix);
|
|
||||||
let stats = NodeStats {
|
|
||||||
tps: max_tps,
|
|
||||||
tx: total,
|
|
||||||
};
|
|
||||||
maxes.write().unwrap().push((v.tpu, stats));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send loopback payment of 0 lamports and confirm the network processed it
|
|
||||||
fn send_barrier_transaction(
|
|
||||||
barrier_client: &mut ThinClient,
|
|
||||||
blockhash: &mut Hash,
|
|
||||||
source_keypair: &Keypair,
|
|
||||||
dest_id: &Pubkey,
|
|
||||||
) {
|
|
||||||
let transfer_start = Instant::now();
|
|
||||||
|
|
||||||
let mut poll_count = 0;
|
|
||||||
loop {
|
|
||||||
if poll_count > 0 && poll_count % 8 == 0 {
|
|
||||||
println!(
|
|
||||||
"polling for barrier transaction confirmation, attempt {}",
|
|
||||||
poll_count
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
*blockhash = barrier_client.get_recent_blockhash().unwrap();
|
|
||||||
|
|
||||||
let transaction =
|
|
||||||
system_transaction::create_user_account(&source_keypair, dest_id, 0, *blockhash, 0);
|
|
||||||
let signature = barrier_client
|
|
||||||
.async_send_transaction(transaction)
|
|
||||||
.expect("Unable to send barrier transaction");
|
|
||||||
|
|
||||||
let confirmatiom = barrier_client.poll_for_signature(&signature);
|
|
||||||
let duration_ms = duration_as_ms(&transfer_start.elapsed());
|
|
||||||
if confirmatiom.is_ok() {
|
|
||||||
println!("barrier transaction confirmed in {} ms", duration_ms);
|
|
||||||
|
|
||||||
solana_metrics::submit(
|
|
||||||
influxdb::Point::new("bench-tps")
|
|
||||||
.add_tag(
|
|
||||||
"op",
|
|
||||||
influxdb::Value::String("send_barrier_transaction".to_string()),
|
|
||||||
)
|
|
||||||
.add_field("poll_count", influxdb::Value::Integer(poll_count))
|
|
||||||
.add_field("duration", influxdb::Value::Integer(duration_ms as i64))
|
|
||||||
.to_owned(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sanity check that the client balance is still 1
|
|
||||||
let balance = barrier_client
|
|
||||||
.poll_balance_with_timeout(
|
|
||||||
&source_keypair.pubkey(),
|
|
||||||
&Duration::from_millis(100),
|
|
||||||
&Duration::from_secs(10),
|
|
||||||
)
|
|
||||||
.expect("Failed to get balance");
|
|
||||||
if balance != 1 {
|
|
||||||
panic!("Expected an account balance of 1 (balance: {}", balance);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeout after 3 minutes. When running a CPU-only leader+validator+drone+bench-tps on a dev
|
|
||||||
// machine, some batches of transactions can take upwards of 1 minute...
|
|
||||||
if duration_ms > 1000 * 60 * 3 {
|
|
||||||
println!("Error: Couldn't confirm barrier transaction!");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_blockhash = barrier_client.get_recent_blockhash().unwrap();
|
|
||||||
if new_blockhash == *blockhash {
|
|
||||||
if poll_count > 0 && poll_count % 8 == 0 {
|
|
||||||
println!("blockhash is not advancing, still at {:?}", *blockhash);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*blockhash = new_blockhash;
|
|
||||||
}
|
|
||||||
|
|
||||||
poll_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_txs(
|
fn generate_txs(
|
||||||
shared_txs: &SharedTransactions,
|
shared_txs: &SharedTransactions,
|
||||||
|
blockhash: &Hash,
|
||||||
source: &[Keypair],
|
source: &[Keypair],
|
||||||
dest: &[Keypair],
|
dest: &[Keypair],
|
||||||
threads: usize,
|
threads: usize,
|
||||||
reclaim: bool,
|
reclaim: bool,
|
||||||
contact_info: &ContactInfo,
|
|
||||||
) {
|
) {
|
||||||
let client = create_client(contact_info.client_facing_addr(), FULLNODE_PORT_RANGE);
|
|
||||||
let blockhash = client.get_recent_blockhash().unwrap();
|
|
||||||
let tx_count = source.len();
|
let tx_count = source.len();
|
||||||
println!("Signing transactions... {} (reclaim={})", tx_count, reclaim);
|
println!("Signing transactions... {} (reclaim={})", tx_count, reclaim);
|
||||||
let signing_start = Instant::now();
|
let signing_start = Instant::now();
|
||||||
@ -400,7 +236,7 @@ fn generate_txs(
|
|||||||
.par_iter()
|
.par_iter()
|
||||||
.map(|(id, keypair)| {
|
.map(|(id, keypair)| {
|
||||||
(
|
(
|
||||||
system_transaction::create_user_account(id, &keypair.pubkey(), 1, blockhash, 0),
|
system_transaction::create_user_account(id, &keypair.pubkey(), 1, *blockhash),
|
||||||
timestamp(),
|
timestamp(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -417,14 +253,9 @@ fn generate_txs(
|
|||||||
duration_as_ms(&duration),
|
duration_as_ms(&duration),
|
||||||
blockhash,
|
blockhash,
|
||||||
);
|
);
|
||||||
solana_metrics::submit(
|
datapoint_info!(
|
||||||
influxdb::Point::new("bench-tps")
|
"bench-tps-generate_txs",
|
||||||
.add_tag("op", influxdb::Value::String("generate_txs".to_string()))
|
("duration", duration_as_ms(&duration), i64)
|
||||||
.add_field(
|
|
||||||
"duration",
|
|
||||||
influxdb::Value::Integer(duration_as_ms(&duration) as i64),
|
|
||||||
)
|
|
||||||
.to_owned(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let sz = transactions.len() / threads;
|
let sz = transactions.len() / threads;
|
||||||
@ -437,22 +268,21 @@ fn generate_txs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_tx_transfers(
|
fn do_tx_transfers<T: Client>(
|
||||||
exit_signal: &Arc<AtomicBool>,
|
exit_signal: &Arc<AtomicBool>,
|
||||||
shared_txs: &SharedTransactions,
|
shared_txs: &SharedTransactions,
|
||||||
contact_info: &ContactInfo,
|
|
||||||
shared_tx_thread_count: &Arc<AtomicIsize>,
|
shared_tx_thread_count: &Arc<AtomicIsize>,
|
||||||
total_tx_sent_count: &Arc<AtomicUsize>,
|
total_tx_sent_count: &Arc<AtomicUsize>,
|
||||||
thread_batch_sleep_ms: usize,
|
thread_batch_sleep_ms: usize,
|
||||||
|
client: &Arc<T>,
|
||||||
) {
|
) {
|
||||||
let client = create_client(contact_info.client_facing_addr(), FULLNODE_PORT_RANGE);
|
|
||||||
loop {
|
loop {
|
||||||
if thread_batch_sleep_ms > 0 {
|
if thread_batch_sleep_ms > 0 {
|
||||||
sleep(Duration::from_millis(thread_batch_sleep_ms as u64));
|
sleep(Duration::from_millis(thread_batch_sleep_ms as u64));
|
||||||
}
|
}
|
||||||
let txs;
|
let txs;
|
||||||
{
|
{
|
||||||
let mut shared_txs_wl = shared_txs.write().unwrap();
|
let mut shared_txs_wl = shared_txs.write().expect("write lock in do_tx_transfers");
|
||||||
txs = shared_txs_wl.pop_front();
|
txs = shared_txs_wl.pop_front();
|
||||||
}
|
}
|
||||||
if let Some(txs0) = txs {
|
if let Some(txs0) = txs {
|
||||||
@ -460,7 +290,7 @@ fn do_tx_transfers(
|
|||||||
println!(
|
println!(
|
||||||
"Transferring 1 unit {} times... to {}",
|
"Transferring 1 unit {} times... to {}",
|
||||||
txs0.len(),
|
txs0.len(),
|
||||||
contact_info.tpu
|
client.as_ref().transactions_addr(),
|
||||||
);
|
);
|
||||||
let tx_len = txs0.len();
|
let tx_len = txs0.len();
|
||||||
let transfer_start = Instant::now();
|
let transfer_start = Instant::now();
|
||||||
@ -469,7 +299,9 @@ fn do_tx_transfers(
|
|||||||
if now > tx.1 && now - tx.1 > 1000 * 30 {
|
if now > tx.1 && now - tx.1 > 1000 * 30 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
client.async_send_transaction(tx.0).unwrap();
|
client
|
||||||
|
.async_send_transaction(tx.0)
|
||||||
|
.expect("async_send_transaction in do_tx_transfers");
|
||||||
}
|
}
|
||||||
shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed);
|
shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed);
|
||||||
total_tx_sent_count.fetch_add(tx_len, Ordering::Relaxed);
|
total_tx_sent_count.fetch_add(tx_len, Ordering::Relaxed);
|
||||||
@ -478,15 +310,10 @@ fn do_tx_transfers(
|
|||||||
duration_as_ms(&transfer_start.elapsed()),
|
duration_as_ms(&transfer_start.elapsed()),
|
||||||
tx_len as f32 / duration_as_s(&transfer_start.elapsed()),
|
tx_len as f32 / duration_as_s(&transfer_start.elapsed()),
|
||||||
);
|
);
|
||||||
solana_metrics::submit(
|
datapoint_info!(
|
||||||
influxdb::Point::new("bench-tps")
|
"bench-tps-do_tx_transfers",
|
||||||
.add_tag("op", influxdb::Value::String("do_tx_transfers".to_string()))
|
("duration", duration_as_ms(&transfer_start.elapsed()), i64),
|
||||||
.add_field(
|
("count", tx_len, i64)
|
||||||
"duration",
|
|
||||||
influxdb::Value::Integer(duration_as_ms(&transfer_start.elapsed()) as i64),
|
|
||||||
)
|
|
||||||
.add_field("count", influxdb::Value::Integer(tx_len as i64))
|
|
||||||
.to_owned(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if exit_signal.load(Ordering::Relaxed) {
|
if exit_signal.load(Ordering::Relaxed) {
|
||||||
@ -495,7 +322,7 @@ fn do_tx_transfers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_funding_transfer(client: &ThinClient, tx: &Transaction, amount: u64) -> bool {
|
fn verify_funding_transfer<T: Client>(client: &T, tx: &Transaction, amount: u64) -> bool {
|
||||||
for a in &tx.message().account_keys[1..] {
|
for a in &tx.message().account_keys[1..] {
|
||||||
if client.get_balance(a).unwrap_or(0) >= amount {
|
if client.get_balance(a).unwrap_or(0) >= amount {
|
||||||
return true;
|
return true;
|
||||||
@ -508,7 +335,7 @@ fn verify_funding_transfer(client: &ThinClient, tx: &Transaction, amount: u64) -
|
|||||||
/// fund the dests keys by spending all of the source keys into MAX_SPENDS_PER_TX
|
/// fund the dests keys by spending all of the source keys into MAX_SPENDS_PER_TX
|
||||||
/// on every iteration. This allows us to replay the transfers because the source is either empty,
|
/// on every iteration. This allows us to replay the transfers because the source is either empty,
|
||||||
/// or full
|
/// or full
|
||||||
fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports: u64) {
|
pub fn fund_keys<T: Client>(client: &T, source: &Keypair, dests: &[Keypair], lamports: u64) {
|
||||||
let total = lamports * dests.len() as u64;
|
let total = lamports * dests.len() as u64;
|
||||||
let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)];
|
let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)];
|
||||||
let mut notfunded: Vec<&Keypair> = dests.iter().collect();
|
let mut notfunded: Vec<&Keypair> = dests.iter().collect();
|
||||||
@ -579,7 +406,7 @@ fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports:
|
|||||||
to_fund_txs.len(),
|
to_fund_txs.len(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let blockhash = client.get_recent_blockhash().unwrap();
|
let (blockhash, _fee_calculator) = client.get_recent_blockhash().unwrap();
|
||||||
|
|
||||||
// re-sign retained to_fund_txes with updated blockhash
|
// re-sign retained to_fund_txes with updated blockhash
|
||||||
to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
|
to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
|
||||||
@ -610,8 +437,13 @@ fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn airdrop_lamports(client: &ThinClient, drone_addr: &SocketAddr, id: &Keypair, tx_count: u64) {
|
pub fn airdrop_lamports<T: Client>(
|
||||||
let starting_balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
client: &T,
|
||||||
|
drone_addr: &SocketAddr,
|
||||||
|
id: &Keypair,
|
||||||
|
tx_count: u64,
|
||||||
|
) {
|
||||||
|
let starting_balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||||
metrics_submit_lamport_balance(starting_balance);
|
metrics_submit_lamport_balance(starting_balance);
|
||||||
println!("starting balance {}", starting_balance);
|
println!("starting balance {}", starting_balance);
|
||||||
|
|
||||||
@ -624,11 +456,18 @@ fn airdrop_lamports(client: &ThinClient, drone_addr: &SocketAddr, id: &Keypair,
|
|||||||
id.pubkey(),
|
id.pubkey(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let blockhash = client.get_recent_blockhash().unwrap();
|
let (blockhash, _fee_calculator) = client.get_recent_blockhash().unwrap();
|
||||||
match request_airdrop_transaction(&drone_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
match request_airdrop_transaction(&drone_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
||||||
Ok(transaction) => {
|
Ok(transaction) => {
|
||||||
let signature = client.async_send_transaction(transaction).unwrap();
|
let signature = client.async_send_transaction(transaction).unwrap();
|
||||||
client.poll_for_signature(&signature).unwrap();
|
client
|
||||||
|
.poll_for_signature_confirmation(&signature, 1)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
panic!(
|
||||||
|
"Error requesting airdrop: to addr: {:?} amount: {}",
|
||||||
|
drone_addr, airdrop_amount
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
panic!(
|
panic!(
|
||||||
@ -638,7 +477,7 @@ fn airdrop_lamports(client: &ThinClient, drone_addr: &SocketAddr, id: &Keypair,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let current_balance = client.poll_get_balance(&id.pubkey()).unwrap_or_else(|e| {
|
let current_balance = client.get_balance(&id.pubkey()).unwrap_or_else(|e| {
|
||||||
println!("airdrop error {}", e);
|
println!("airdrop error {}", e);
|
||||||
starting_balance
|
starting_balance
|
||||||
});
|
});
|
||||||
@ -658,7 +497,7 @@ fn airdrop_lamports(client: &ThinClient, drone_addr: &SocketAddr, id: &Keypair,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn compute_and_report_stats(
|
fn compute_and_report_stats(
|
||||||
maxes: &Arc<RwLock<Vec<(SocketAddr, NodeStats)>>>,
|
maxes: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||||
sample_period: u64,
|
sample_period: u64,
|
||||||
tx_send_elapsed: &Duration,
|
tx_send_elapsed: &Duration,
|
||||||
total_tx_send_count: usize,
|
total_tx_send_count: usize,
|
||||||
@ -672,17 +511,14 @@ fn compute_and_report_stats(
|
|||||||
println!("---------------------+---------------+--------------------");
|
println!("---------------------+---------------+--------------------");
|
||||||
|
|
||||||
for (sock, stats) in maxes.read().unwrap().iter() {
|
for (sock, stats) in maxes.read().unwrap().iter() {
|
||||||
let maybe_flag = match stats.tx {
|
let maybe_flag = match stats.txs {
|
||||||
0 => "!!!!!",
|
0 => "!!!!!",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{:20} | {:13.2} | {} {}",
|
"{:20} | {:13.2} | {} {}",
|
||||||
(*sock).to_string(),
|
sock, stats.tps, stats.txs, maybe_flag
|
||||||
stats.tps,
|
|
||||||
stats.tx,
|
|
||||||
maybe_flag
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if stats.tps == 0.0 {
|
if stats.tps == 0.0 {
|
||||||
@ -693,27 +529,33 @@ fn compute_and_report_stats(
|
|||||||
if stats.tps > max_of_maxes {
|
if stats.tps > max_of_maxes {
|
||||||
max_of_maxes = stats.tps;
|
max_of_maxes = stats.tps;
|
||||||
}
|
}
|
||||||
if stats.tx > max_tx_count {
|
if stats.txs > max_tx_count {
|
||||||
max_tx_count = stats.tx;
|
max_tx_count = stats.txs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if total_maxes > 0.0 {
|
if total_maxes > 0.0 {
|
||||||
let num_nodes_with_tps = maxes.read().unwrap().len() - nodes_with_zero_tps;
|
let num_nodes_with_tps = maxes.read().unwrap().len() - nodes_with_zero_tps;
|
||||||
let average_max = total_maxes / num_nodes_with_tps as f64;
|
let average_max = total_maxes / num_nodes_with_tps as f32;
|
||||||
println!(
|
println!(
|
||||||
"\nAverage max TPS: {:.2}, {} nodes had 0 TPS",
|
"\nAverage max TPS: {:.2}, {} nodes had 0 TPS",
|
||||||
average_max, nodes_with_zero_tps
|
average_max, nodes_with_zero_tps
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total_tx_send_count = total_tx_send_count as u64;
|
||||||
|
let drop_rate = if total_tx_send_count > max_tx_count {
|
||||||
|
(total_tx_send_count - max_tx_count) as f64 / total_tx_send_count as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
println!(
|
println!(
|
||||||
"\nHighest TPS: {:.2} sampling period {}s max transactions: {} clients: {} drop rate: {:.2}",
|
"\nHighest TPS: {:.2} sampling period {}s max transactions: {} clients: {} drop rate: {:.2}",
|
||||||
max_of_maxes,
|
max_of_maxes,
|
||||||
sample_period,
|
sample_period,
|
||||||
max_tx_count,
|
max_tx_count,
|
||||||
maxes.read().unwrap().len(),
|
maxes.read().unwrap().len(),
|
||||||
(total_tx_send_count as u64 - max_tx_count) as f64 / total_tx_send_count as f64,
|
drop_rate,
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"\tAverage TPS: {}",
|
"\tAverage TPS: {}",
|
||||||
@ -728,12 +570,67 @@ fn should_switch_directions(num_lamports_per_account: u64, i: u64) -> bool {
|
|||||||
i % (num_lamports_per_account / 4) == 0 && (i >= (3 * num_lamports_per_account) / 4)
|
i % (num_lamports_per_account / 4) == 0 && (i >= (3 * num_lamports_per_account) / 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_keypairs(seed_keypair: &Keypair, count: usize) -> Vec<Keypair> {
|
||||||
|
let mut seed = [0u8; 32];
|
||||||
|
seed.copy_from_slice(&seed_keypair.to_bytes()[..32]);
|
||||||
|
let mut rnd = GenKeys::new(seed);
|
||||||
|
|
||||||
|
let mut total_keys = 0;
|
||||||
|
let mut target = count;
|
||||||
|
while target > 1 {
|
||||||
|
total_keys += target;
|
||||||
|
// Use the upper bound for this division otherwise it may not generate enough keys
|
||||||
|
target = (target + MAX_SPENDS_PER_TX - 1) / MAX_SPENDS_PER_TX;
|
||||||
|
}
|
||||||
|
rnd.gen_n_keypairs(total_keys as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_and_fund_keypairs<T: Client>(
|
||||||
|
client: &T,
|
||||||
|
drone_addr: Option<SocketAddr>,
|
||||||
|
funding_pubkey: &Keypair,
|
||||||
|
tx_count: usize,
|
||||||
|
lamports_per_account: u64,
|
||||||
|
) -> (Vec<Keypair>, u64) {
|
||||||
|
info!("Creating {} keypairs...", tx_count * 2);
|
||||||
|
let mut keypairs = generate_keypairs(funding_pubkey, tx_count * 2);
|
||||||
|
|
||||||
|
info!("Get lamports...");
|
||||||
|
|
||||||
|
// Sample the first keypair, see if it has lamports, if so then resume.
|
||||||
|
// This logic is to prevent lamport loss on repeated solana-bench-tps executions
|
||||||
|
let last_keypair_balance = client
|
||||||
|
.get_balance(&keypairs[tx_count * 2 - 1].pubkey())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if lamports_per_account > last_keypair_balance {
|
||||||
|
let extra = lamports_per_account - last_keypair_balance;
|
||||||
|
let total = extra * (keypairs.len() as u64);
|
||||||
|
if client.get_balance(&funding_pubkey.pubkey()).unwrap_or(0) < total {
|
||||||
|
airdrop_lamports(client, &drone_addr.unwrap(), funding_pubkey, total);
|
||||||
|
}
|
||||||
|
info!("adding more lamports {}", extra);
|
||||||
|
fund_keys(client, funding_pubkey, &keypairs, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'generate_keypairs' generates extra keys to be able to have size-aligned funding batches for fund_keys.
|
||||||
|
keypairs.truncate(2 * tx_count);
|
||||||
|
|
||||||
|
(keypairs, last_keypair_balance)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use solana::fullnode::FullnodeConfig;
|
use solana::cluster_info::FULLNODE_PORT_RANGE;
|
||||||
use solana::local_cluster::{ClusterConfig, LocalCluster};
|
use solana::local_cluster::{ClusterConfig, LocalCluster};
|
||||||
|
use solana::validator::ValidatorConfig;
|
||||||
|
use solana_client::thin_client::create_client;
|
||||||
use solana_drone::drone::run_local_drone;
|
use solana_drone::drone::run_local_drone;
|
||||||
|
use solana_runtime::bank::Bank;
|
||||||
|
use solana_runtime::bank_client::BankClient;
|
||||||
|
use solana_sdk::client::SyncClient;
|
||||||
|
use solana_sdk::genesis_block::create_genesis_block;
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -752,14 +649,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
fn test_bench_tps_local_cluster() {
|
||||||
fn test_bench_tps() {
|
solana_logger::setup();
|
||||||
let fullnode_config = FullnodeConfig::default();
|
let validator_config = ValidatorConfig::default();
|
||||||
const NUM_NODES: usize = 1;
|
const NUM_NODES: usize = 1;
|
||||||
let cluster = LocalCluster::new(&ClusterConfig {
|
let cluster = LocalCluster::new(&ClusterConfig {
|
||||||
node_stakes: vec![999_990; NUM_NODES],
|
node_stakes: vec![999_990; NUM_NODES],
|
||||||
cluster_lamports: 2_000_000,
|
cluster_lamports: 2_000_000,
|
||||||
fullnode_config,
|
validator_config,
|
||||||
..ClusterConfig::default()
|
..ClusterConfig::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -770,13 +667,59 @@ mod tests {
|
|||||||
run_local_drone(drone_keypair, addr_sender, None);
|
run_local_drone(drone_keypair, addr_sender, None);
|
||||||
let drone_addr = addr_receiver.recv_timeout(Duration::from_secs(2)).unwrap();
|
let drone_addr = addr_receiver.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
|
|
||||||
let mut cfg = Config::default();
|
let mut config = Config::default();
|
||||||
cfg.network_addr = cluster.entry_point_info.gossip;
|
config.tx_count = 100;
|
||||||
cfg.drone_addr = drone_addr;
|
config.duration = Duration::from_secs(5);
|
||||||
cfg.tx_count = 100;
|
|
||||||
cfg.duration = Duration::from_secs(5);
|
|
||||||
cfg.num_nodes = NUM_NODES;
|
|
||||||
|
|
||||||
do_bench_tps(cfg);
|
let client = create_client(
|
||||||
|
(cluster.entry_point_info.rpc, cluster.entry_point_info.tpu),
|
||||||
|
FULLNODE_PORT_RANGE,
|
||||||
|
);
|
||||||
|
|
||||||
|
let lamports_per_account = 100;
|
||||||
|
let (keypairs, _keypair_balance) = generate_and_fund_keypairs(
|
||||||
|
&client,
|
||||||
|
Some(drone_addr),
|
||||||
|
&config.id,
|
||||||
|
config.tx_count,
|
||||||
|
lamports_per_account,
|
||||||
|
);
|
||||||
|
|
||||||
|
let total = do_bench_tps(vec![client], config, keypairs, 0);
|
||||||
|
assert!(total > 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bench_tps_bank_client() {
|
||||||
|
let (genesis_block, id) = create_genesis_block(10_000);
|
||||||
|
let bank = Bank::new(&genesis_block);
|
||||||
|
let clients = vec![BankClient::new(bank)];
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.id = id;
|
||||||
|
config.tx_count = 10;
|
||||||
|
config.duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
let (keypairs, _keypair_balance) =
|
||||||
|
generate_and_fund_keypairs(&clients[0], None, &config.id, config.tx_count, 20);
|
||||||
|
|
||||||
|
do_bench_tps(clients, config, keypairs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bench_tps_fund_keys() {
|
||||||
|
let (genesis_block, id) = create_genesis_block(10_000);
|
||||||
|
let bank = Bank::new(&genesis_block);
|
||||||
|
let client = BankClient::new(bank);
|
||||||
|
let tx_count = 10;
|
||||||
|
let lamports = 20;
|
||||||
|
|
||||||
|
let (keypairs, _keypair_balance) =
|
||||||
|
generate_and_fund_keypairs(&client, None, &id, tx_count, lamports);
|
||||||
|
|
||||||
|
for kp in &keypairs {
|
||||||
|
// TODO: This should be >= lamports, but fails at the moment
|
||||||
|
assert_ne!(client.get_balance(&kp.pubkey()).unwrap(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
|
|||||||
|
|
||||||
/// Holds the configuration for a single run of the benchmark
|
/// Holds the configuration for a single run of the benchmark
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub network_addr: SocketAddr,
|
pub entrypoint_addr: SocketAddr,
|
||||||
pub drone_addr: SocketAddr,
|
pub drone_addr: SocketAddr,
|
||||||
pub id: Keypair,
|
pub id: Keypair,
|
||||||
pub threads: usize,
|
pub threads: usize,
|
||||||
@ -22,7 +22,7 @@ pub struct Config {
|
|||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Config {
|
fn default() -> Config {
|
||||||
Config {
|
Config {
|
||||||
network_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||||
drone_addr: SocketAddr::from(([127, 0, 0, 1], DRONE_PORT)),
|
drone_addr: SocketAddr::from(([127, 0, 0, 1], DRONE_PORT)),
|
||||||
id: Keypair::new(),
|
id: Keypair::new(),
|
||||||
threads: 4,
|
threads: 4,
|
||||||
@ -40,12 +40,12 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
|||||||
App::new(crate_name!()).about(crate_description!())
|
App::new(crate_name!()).about(crate_description!())
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("network")
|
Arg::with_name("entrypoint")
|
||||||
.short("n")
|
.short("n")
|
||||||
.long("network")
|
.long("entrypoint")
|
||||||
.value_name("HOST:PORT")
|
.value_name("HOST:PORT")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Rendezvous with the network at this gossip entry point; defaults to 127.0.0.1:8001"),
|
.help("Rendezvous with the cluster at this entry point; defaults to 127.0.0.1:8001"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("drone")
|
Arg::with_name("drone")
|
||||||
@ -53,7 +53,7 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.long("drone")
|
.long("drone")
|
||||||
.value_name("HOST:PORT")
|
.value_name("HOST:PORT")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Location of the drone; defaults to network:DRONE_PORT"),
|
.help("Location of the drone; defaults to entrypoint:DRONE_PORT"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("identity")
|
Arg::with_name("identity")
|
||||||
@ -116,9 +116,9 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
|||||||
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
||||||
let mut args = Config::default();
|
let mut args = Config::default();
|
||||||
|
|
||||||
if let Some(addr) = matches.value_of("network") {
|
if let Some(addr) = matches.value_of("entrypoint") {
|
||||||
args.network_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
|
args.entrypoint_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
|
||||||
eprintln!("failed to parse network address: {}", e);
|
eprintln!("failed to parse entrypoint address: {}", e);
|
||||||
exit(1)
|
exit(1)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,61 @@
|
|||||||
mod bench;
|
mod bench;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
||||||
use crate::bench::do_bench_tps;
|
use crate::bench::{do_bench_tps, generate_and_fund_keypairs, Config, NUM_LAMPORTS_PER_ACCOUNT};
|
||||||
|
use solana::gossip_service::{discover_cluster, get_clients};
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
solana_logger::setup();
|
solana_logger::setup();
|
||||||
solana_metrics::set_panic_hook("bench-tps");
|
solana_metrics::set_panic_hook("bench-tps");
|
||||||
|
|
||||||
let matches = cli::build_args().get_matches();
|
let matches = cli::build_args().get_matches();
|
||||||
|
let cli_config = cli::extract_args(&matches);
|
||||||
|
|
||||||
let cfg = cli::extract_args(&matches);
|
let cli::Config {
|
||||||
|
entrypoint_addr,
|
||||||
|
drone_addr,
|
||||||
|
id,
|
||||||
|
threads,
|
||||||
|
num_nodes,
|
||||||
|
duration,
|
||||||
|
tx_count,
|
||||||
|
thread_batch_sleep_ms,
|
||||||
|
sustained,
|
||||||
|
} = cli_config;
|
||||||
|
|
||||||
do_bench_tps(cfg);
|
println!("Connecting to the cluster");
|
||||||
|
let (nodes, _replicators) =
|
||||||
|
discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|err| {
|
||||||
|
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
if nodes.len() < num_nodes {
|
||||||
|
eprintln!(
|
||||||
|
"Error: Insufficient nodes discovered. Expecting {} or more",
|
||||||
|
num_nodes
|
||||||
|
);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let clients = get_clients(&nodes);
|
||||||
|
|
||||||
|
let (keypairs, keypair_balance) = generate_and_fund_keypairs(
|
||||||
|
&clients[0],
|
||||||
|
Some(drone_addr),
|
||||||
|
&id,
|
||||||
|
tx_count,
|
||||||
|
NUM_LAMPORTS_PER_ACCOUNT,
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
id,
|
||||||
|
threads,
|
||||||
|
thread_batch_sleep_ms,
|
||||||
|
duration,
|
||||||
|
tx_count,
|
||||||
|
sustained,
|
||||||
|
};
|
||||||
|
|
||||||
|
do_bench_tps(clients, config, keypairs, keypair_balance);
|
||||||
}
|
}
|
||||||
|
19
book/art/data-plane-fanout.bob
Normal file
19
book/art/data-plane-fanout.bob
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
+------------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +-----------------+ Neighborhood 0 +-----------------+ |
|
||||||
|
| | +--------------------->+ | |
|
||||||
|
| | Validator 1 | | Validator 2 | |
|
||||||
|
| | +<---------------------+ | |
|
||||||
|
| +--------+-+------+ +------+-+--------+ |
|
||||||
|
| | | | | |
|
||||||
|
| | +-----------------------------+ | | |
|
||||||
|
| | +------------------------+------+ | |
|
||||||
|
| | | | | |
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| | | |
|
||||||
|
v v v v
|
||||||
|
+---------+------+---+ +-+--------+---------+
|
||||||
|
| | | |
|
||||||
|
| Neighborhood 1 | | Neighborhood 2 |
|
||||||
|
| | | |
|
||||||
|
+--------------------+ +--------------------+
|
15
book/art/data-plane-seeding.bob
Normal file
15
book/art/data-plane-seeding.bob
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
+--------------+
|
||||||
|
| |
|
||||||
|
+------------+ Leader +------------+
|
||||||
|
| | | |
|
||||||
|
| +--------------+ |
|
||||||
|
v v
|
||||||
|
+------------+----------------------------------------+------------+
|
||||||
|
| |
|
||||||
|
| +-----------------+ Neighborhood 0 +-----------------+ |
|
||||||
|
| | +--------------------->+ | |
|
||||||
|
| | Validator 1 | | Validator 2 | |
|
||||||
|
| | +<---------------------+ | |
|
||||||
|
| +-----------------+ +-----------------+ |
|
||||||
|
| |
|
||||||
|
+------------------------------------------------------------------+
|
@ -1,28 +1,18 @@
|
|||||||
|
+--------------------+
|
||||||
+--------------+
|
|
||||||
| |
|
| |
|
||||||
+------------+ Leader +------------+
|
+--------+ Neighborhood 0 +----------+
|
||||||
| | | |
|
| | | |
|
||||||
| +--------------+ |
|
| +--------------------+ |
|
||||||
v v
|
v v
|
||||||
+--------+--------+ +--------+--------+
|
+---------+----------+ +----------+---------+
|
||||||
| +--------------------->+ |
|
| | | |
|
||||||
+-----------------+ Validator 1 | | Validator 2 +-------------+
|
| Neighborhood 1 | | Neighborhood 2 |
|
||||||
| | +<---------------------+ | |
|
| | | |
|
||||||
| +------+-+-+------+ +---+-+-+---------+ |
|
+---+-----+----------+ +----------+-----+---+
|
||||||
| | | | | | | |
|
| | | |
|
||||||
| | | | | | | |
|
v v v v
|
||||||
| +---------------------------------------------+ | | |
|
+------------------+-+ +-+------------------+ +------------------+-+ +-+------------------+
|
||||||
| | | | | | | |
|
|
||||||
| | | | | +----------------------+ | |
|
|
||||||
| | | | | | | |
|
|
||||||
| | | | +--------------------------------------------+ |
|
|
||||||
| | | | | | | |
|
|
||||||
| | | +----------------------+ | | |
|
|
||||||
| | | | | | | |
|
|
||||||
v v v v v v v v
|
|
||||||
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
|
||||||
| | | | | | | |
|
| | | | | | | |
|
||||||
| Neighborhood 1 | | Neighborhood 2 | | Neighborhood 3 | | Neighborhood 4 |
|
| Neighborhood 3 | | Neighborhood 4 | | Neighborhood 5 | | Neighborhood 6 |
|
||||||
| | | | | | | |
|
| | | | | | | |
|
||||||
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
||||||
|
60
book/art/validator-proposal.bob
Normal file
60
book/art/validator-proposal.bob
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
.------------.
|
||||||
|
| Upstream |
|
||||||
|
| Validators |
|
||||||
|
`----+-------`
|
||||||
|
|
|
||||||
|
|
|
||||||
|
.-----------------------------------.
|
||||||
|
| Validator | |
|
||||||
|
| v |
|
||||||
|
| .-----------. .------------. |
|
||||||
|
.--------. | | Fetch | | Repair | |
|
||||||
|
| Client +---->| Stage | | Stage | |
|
||||||
|
`--------` | `---+-------` `----+-------` |
|
||||||
|
| | | |
|
||||||
|
| v v |
|
||||||
|
| .-----------. .------------. |
|
||||||
|
| | TPU |<-->| Blockstore | |
|
||||||
|
| | | | | |
|
||||||
|
| `-----------` `----+-------` |
|
||||||
|
| | |
|
||||||
|
| v |
|
||||||
|
| .------------. |
|
||||||
|
| | Multicast | |
|
||||||
|
| | Stage | |
|
||||||
|
| `----+-------` |
|
||||||
|
| | |
|
||||||
|
`-----------------------------------`
|
||||||
|
|
|
||||||
|
v
|
||||||
|
.------------.
|
||||||
|
| Downstream |
|
||||||
|
| Validators |
|
||||||
|
`------------`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.------------.
|
||||||
|
| PoH |
|
||||||
|
| Service |
|
||||||
|
`-------+----`
|
||||||
|
^ |
|
||||||
|
| |
|
||||||
|
.-----------------------------------.
|
||||||
|
| TPU | | |
|
||||||
|
| | v |
|
||||||
|
.-------. | .-----------. .---+--------. | .------------.
|
||||||
|
| Fetch +---->| SigVerify +--->| Banking |<--->| Blockstore |
|
||||||
|
| Stage | | | Stage | | Stage | | | |
|
||||||
|
`-------` | `-----------` `-----+------` | `------------`
|
||||||
|
| | |
|
||||||
|
| | |
|
||||||
|
`-----------------------------------`
|
||||||
|
|
|
||||||
|
v
|
||||||
|
.------------.
|
||||||
|
| Banktree |
|
||||||
|
| |
|
||||||
|
`------------`
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
.--------------------------------------.
|
.--------------------------------------.
|
||||||
| Fullnode |
|
| Validator |
|
||||||
| |
|
| |
|
||||||
.--------. | .-------------------. |
|
.--------. | .-------------------. |
|
||||||
| |---->| | |
|
| |---->| | |
|
@ -3,16 +3,4 @@ set -e
|
|||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
cargo_install_unless() {
|
|
||||||
declare crate=$1
|
|
||||||
shift
|
|
||||||
|
|
||||||
"$@" > /dev/null 2>&1 || \
|
|
||||||
cargo install "$crate"
|
|
||||||
}
|
|
||||||
|
|
||||||
export PATH=$CARGO_HOME/bin:$PATH
|
|
||||||
cargo_install_unless mdbook mdbook --help
|
|
||||||
cargo_install_unless svgbob_cli svgbob --help
|
|
||||||
|
|
||||||
make -j"$(nproc)"
|
make -j"$(nproc)"
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
BOB_SRCS=$(wildcard art/*.bob)
|
BOB_SRCS=$(wildcard art/*.bob)
|
||||||
|
MSC_SRCS=$(wildcard art/*.msc)
|
||||||
MD_SRCS=$(wildcard src/*.md)
|
MD_SRCS=$(wildcard src/*.md)
|
||||||
|
|
||||||
SVG_IMGS=$(BOB_SRCS:art/%.bob=src/img/%.svg)
|
SVG_IMGS=$(BOB_SRCS:art/%.bob=src/img/%.svg) $(MSC_SRCS:art/%.msc=src/img/%.svg)
|
||||||
|
|
||||||
all: html/index.html
|
all: html/index.html
|
||||||
|
|
||||||
@ -17,6 +18,10 @@ src/img/%.svg: art/%.bob
|
|||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
svgbob < $< > $@
|
svgbob < $< > $@
|
||||||
|
|
||||||
|
src/img/%.svg: art/%.msc
|
||||||
|
@mkdir -p $(@D)
|
||||||
|
mscgen -T svg -i $< -o $@
|
||||||
|
|
||||||
src/%.md: %.md
|
src/%.md: %.md
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
@cp $< $@
|
@cp $< $@
|
||||||
|
@ -19,9 +19,10 @@
|
|||||||
- [Data Plane Fanout](data-plane-fanout.md)
|
- [Data Plane Fanout](data-plane-fanout.md)
|
||||||
- [Ledger Replication](ledger-replication.md)
|
- [Ledger Replication](ledger-replication.md)
|
||||||
- [Secure Vote Signing](vote-signing.md)
|
- [Secure Vote Signing](vote-signing.md)
|
||||||
- [Staking Delegation and Rewards](stake-delegation-and-rewards.md)
|
- [Stake Delegation and Rewards](stake-delegation-and-rewards.md)
|
||||||
|
- [Performance Metrics](performance-metrics.md)
|
||||||
|
|
||||||
- [Anatomy of a Fullnode](fullnode.md)
|
- [Anatomy of a Validator](validator.md)
|
||||||
- [TPU](tpu.md)
|
- [TPU](tpu.md)
|
||||||
- [TVU](tvu.md)
|
- [TVU](tvu.md)
|
||||||
- [Blocktree](blocktree.md)
|
- [Blocktree](blocktree.md)
|
||||||
@ -38,9 +39,6 @@
|
|||||||
- [Ledger Replication](ledger-replication-to-implement.md)
|
- [Ledger Replication](ledger-replication-to-implement.md)
|
||||||
- [Secure Vote Signing](vote-signing-to-implement.md)
|
- [Secure Vote Signing](vote-signing-to-implement.md)
|
||||||
- [Staking Rewards](staking-rewards.md)
|
- [Staking Rewards](staking-rewards.md)
|
||||||
- [Passive Stake Delegation and Rewards](passive-stake-delegation-and-rewards.md)
|
|
||||||
- [Reliable Vote Transmission](reliable-vote-transmission.md)
|
|
||||||
- [Persistent Account Storage](persistent-account-storage.md)
|
|
||||||
- [Cluster Economics](ed_overview.md)
|
- [Cluster Economics](ed_overview.md)
|
||||||
- [Validation-client Economics](ed_validation_client_economics.md)
|
- [Validation-client Economics](ed_validation_client_economics.md)
|
||||||
- [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md)
|
- [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md)
|
||||||
@ -55,13 +53,17 @@
|
|||||||
- [Economic Design MVP](ed_mvp.md)
|
- [Economic Design MVP](ed_mvp.md)
|
||||||
- [References](ed_references.md)
|
- [References](ed_references.md)
|
||||||
- [Cluster Test Framework](cluster-test-framework.md)
|
- [Cluster Test Framework](cluster-test-framework.md)
|
||||||
- [Testing Programs](testing-programs.md)
|
|
||||||
- [Credit-only Accounts](credit-only-credit-debit-accounts.md)
|
- [Credit-only Accounts](credit-only-credit-debit-accounts.md)
|
||||||
- [Cluster Software Installation and Updates](installer.md)
|
|
||||||
- [Deterministic Transaction Fees](transaction-fees.md)
|
- [Deterministic Transaction Fees](transaction-fees.md)
|
||||||
|
- [Validator](validator-proposal.md)
|
||||||
|
|
||||||
- [Implemented Design Proposals](implemented-proposals.md)
|
- [Implemented Design Proposals](implemented-proposals.md)
|
||||||
- [Fork Selection](fork-selection.md)
|
- [Fork Selection](fork-selection.md)
|
||||||
- [Leader-to-Leader Transition](leader-leader-transition.md)
|
- [Leader-to-Leader Transition](leader-leader-transition.md)
|
||||||
- [Leader-to-Validator Transition](leader-validator-transition.md)
|
- [Leader-to-Validator Transition](leader-validator-transition.md)
|
||||||
- [Testnet Participation](testnet-participation.md)
|
- [Testnet Participation](testnet-participation.md)
|
||||||
|
- [Testing Programs](testing-programs.md)
|
||||||
|
- [Reliable Vote Transmission](reliable-vote-transmission.md)
|
||||||
|
- [Persistent Account Storage](persistent-account-storage.md)
|
||||||
|
- [Cluster Software Installation and Updates](installer.md)
|
||||||
|
- [Passive Stake Delegation and Rewards](passive-stake-delegation-and-rewards.md)
|
||||||
|
@ -12,7 +12,7 @@ To run a blockstreamer, include the argument `no-signer` and (optional)
|
|||||||
`blockstream` socket location:
|
`blockstream` socket location:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ./multinode-demo/fullnode-x.sh --no-signer --blockstream <SOCKET>
|
$ ./multinode-demo/validator-x.sh --no-signer --blockstream <SOCKET>
|
||||||
```
|
```
|
||||||
|
|
||||||
The stream will output a series of JSON objects:
|
The stream will output a series of JSON objects:
|
||||||
|
@ -20,7 +20,7 @@ least amount of internal plumbing exposed to the test.
|
|||||||
Tests are provided an entry point, which is a `contact_info::ContactInfo`
|
Tests are provided an entry point, which is a `contact_info::ContactInfo`
|
||||||
structure, and a keypair that has already been funded.
|
structure, and a keypair that has already been funded.
|
||||||
|
|
||||||
Each node in the cluster is configured with a `fullnode::FullnodeConfig` at boot
|
Each node in the cluster is configured with a `fullnode::ValidatorConfig` at boot
|
||||||
time. At boot time this configuration specifies any extra cluster configuration
|
time. At boot time this configuration specifies any extra cluster configuration
|
||||||
required for the test. The cluster should boot with the configuration when it
|
required for the test. The cluster should boot with the configuration when it
|
||||||
is run in-process or in a data center.
|
is run in-process or in a data center.
|
||||||
@ -61,18 +61,18 @@ let cluster_nodes = discover_nodes(&entry_point_info, num_nodes);
|
|||||||
|
|
||||||
To enable specific scenarios, the cluster needs to be booted with special
|
To enable specific scenarios, the cluster needs to be booted with special
|
||||||
configurations. These configurations can be captured in
|
configurations. These configurations can be captured in
|
||||||
`fullnode::FullnodeConfig`.
|
`fullnode::ValidatorConfig`.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
let mut fullnode_config = FullnodeConfig::default();
|
let mut validator_config = ValidatorConfig::default();
|
||||||
fullnode_config.rpc_config.enable_fullnode_exit = true;
|
validator_config.rpc_config.enable_fullnode_exit = true;
|
||||||
let local = LocalCluster::new_with_config(
|
let local = LocalCluster::new_with_config(
|
||||||
num_nodes,
|
num_nodes,
|
||||||
10_000,
|
10_000,
|
||||||
100,
|
100,
|
||||||
&fullnode_config
|
&validator_config
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -86,9 +86,9 @@ advertised gossip nodes.
|
|||||||
Configure the RPC service:
|
Configure the RPC service:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
let mut fullnode_config = FullnodeConfig::default();
|
let mut validator_config = ValidatorConfig::default();
|
||||||
fullnode_config.rpc_config.enable_rpc_gossip_push = true;
|
validator_config.rpc_config.enable_rpc_gossip_push = true;
|
||||||
fullnode_config.rpc_config.enable_rpc_gossip_refresh_active_set = true;
|
validator_config.rpc_config.enable_rpc_gossip_refresh_active_set = true;
|
||||||
```
|
```
|
||||||
|
|
||||||
Wire the RPCs and write a new test:
|
Wire the RPCs and write a new test:
|
||||||
|
@ -28,7 +28,7 @@ its copy.
|
|||||||
|
|
||||||
## Joining a Cluster
|
## Joining a Cluster
|
||||||
|
|
||||||
Fullnodes and replicators enter the cluster via registration messages sent to
|
Validators and replicators enter the cluster via registration messages sent to
|
||||||
its *control plane*. The control plane is implemented using a *gossip*
|
its *control plane*. The control plane is implemented using a *gossip*
|
||||||
protocol, meaning that a node may register with any existing node, and expect
|
protocol, meaning that a node may register with any existing node, and expect
|
||||||
its registration to propagate to all nodes in the cluster. The time it takes
|
its registration to propagate to all nodes in the cluster. The time it takes
|
||||||
|
@ -6,15 +6,14 @@ In order to establish the fanout, the cluster divides itself into small
|
|||||||
collections of nodes, called *neighborhoods*. Each node is responsible for
|
collections of nodes, called *neighborhoods*. Each node is responsible for
|
||||||
sharing any data it receives with the other nodes in its neighborhood, as well
|
sharing any data it receives with the other nodes in its neighborhood, as well
|
||||||
as propagating the data on to a small set of nodes in other neighborhoods.
|
as propagating the data on to a small set of nodes in other neighborhoods.
|
||||||
|
This way each node only has to communicate with a small number of nodes.
|
||||||
|
|
||||||
During its slot, the leader node distributes blobs between the validator nodes
|
During its slot, the leader node distributes blobs between the validator nodes
|
||||||
in one neighborhood (layer 1). Each validator shares its data within its
|
in the first neighborhood (layer 0). Each validator shares its data within its
|
||||||
neighborhood, but also retransmits the blobs to one node in each of multiple
|
neighborhood, but also retransmits the blobs to one node in some neighborhoods
|
||||||
neighborhoods in the next layer (layer 2). The layer-2 nodes each share their
|
in the next layer (layer 1). The layer-1 nodes each share their data with their
|
||||||
data with their neighborhood peers, and retransmit to nodes in the next layer,
|
neighborhood peers, and retransmit to nodes in the next layer, etc, until all
|
||||||
etc, until all nodes in the cluster have received all the blobs.
|
nodes in the cluster have received all the blobs.
|
||||||
|
|
||||||
<img alt="Two layer cluster" src="img/data-plane.svg" class="center"/>
|
|
||||||
|
|
||||||
## Neighborhood Assignment - Weighted Selection
|
## Neighborhood Assignment - Weighted Selection
|
||||||
|
|
||||||
@ -23,48 +22,50 @@ cluster is divided into neighborhoods. To achieve this, all the recognized
|
|||||||
validator nodes (the TVU peers) are sorted by stake and stored in a list. This
|
validator nodes (the TVU peers) are sorted by stake and stored in a list. This
|
||||||
list is then indexed in different ways to figure out neighborhood boundaries and
|
list is then indexed in different ways to figure out neighborhood boundaries and
|
||||||
retransmit peers. For example, the leader will simply select the first nodes to
|
retransmit peers. For example, the leader will simply select the first nodes to
|
||||||
make up layer 1. These will automatically be the highest stake holders, allowing
|
make up layer 0. These will automatically be the highest stake holders, allowing
|
||||||
the heaviest votes to come back to the leader first. Layer-1 and lower-layer
|
the heaviest votes to come back to the leader first. Layer-0 and lower-layer
|
||||||
nodes use the same logic to find their neighbors and lower layer peers.
|
nodes use the same logic to find their neighbors and next layer peers.
|
||||||
|
|
||||||
## Layer and Neighborhood Structure
|
## Layer and Neighborhood Structure
|
||||||
|
|
||||||
The current leader makes its initial broadcasts to at most `DATA_PLANE_FANOUT`
|
The current leader makes its initial broadcasts to at most `DATA_PLANE_FANOUT`
|
||||||
nodes. If this layer 1 is smaller than the number of nodes in the cluster, then
|
nodes. If this layer 0 is smaller than the number of nodes in the cluster, then
|
||||||
the data plane fanout mechanism adds layers below. Subsequent layers follow
|
the data plane fanout mechanism adds layers below. Subsequent layers follow
|
||||||
these constraints to determine layer-capacity: Each neighborhood contains
|
these constraints to determine layer-capacity: Each neighborhood contains
|
||||||
`NEIGHBORHOOD_SIZE` nodes and each layer may have up to `DATA_PLANE_FANOUT/2`
|
`DATA_PLANE_FANOUT` nodes. Layer-0 starts with 1 neighborhood with fanout nodes.
|
||||||
neighborhoods.
|
The number of nodes in each additional layer grows by a factor of fanout.
|
||||||
|
|
||||||
As mentioned above, each node in a layer only has to broadcast its blobs to its
|
As mentioned above, each node in a layer only has to broadcast its blobs to its
|
||||||
neighbors and to exactly 1 node in each next-layer neighborhood, instead of to
|
neighbors and to exactly 1 node in some next-layer neighborhoods,
|
||||||
every TVU peer in the cluster. In the default mode, each layer contains
|
instead of to every TVU peer in the cluster. A good way to think about this is,
|
||||||
`DATA_PLANE_FANOUT/2` neighborhoods. The retransmit mechanism also supports a
|
layer-0 starts with 1 neighborhood with fanout nodes, layer-1 adds "fanout"
|
||||||
second, `grow`, mode of operation that squares the number of neighborhoods
|
neighborhoods, each with fanout nodes and layer-2 will have
|
||||||
allowed each layer. This dramatically reduces the number of layers needed to
|
`fanout * number of nodes in layer-1` and so on.
|
||||||
support a large cluster, but can also have a negative impact on the network
|
|
||||||
pressure on each node in the lower layers. A good way to think of the default
|
This way each node only has to communicate with a maximum of `2 * DATA_PLANE_FANOUT - 1` nodes.
|
||||||
mode (when `grow` is disabled) is to imagine it as chain of layers, where the
|
|
||||||
leader sends blobs to layer-1 and then layer-1 to layer-2 and so on, the `layer
|
The following diagram shows how the Leader sends blobs with a Fanout of 2 to
|
||||||
capacities` remain constant, so all layers past layer-2 will have the same
|
Neighborhood 0 in Layer 0 and how the nodes in Neighborhood 0 share their data
|
||||||
number of nodes until the whole cluster is covered. When `grow` is enabled, this
|
with each other.
|
||||||
becomes a traditional fanout where layer-3 will have the square of the number of
|
|
||||||
nodes in layer-2 and so on.
|
<img alt="Leader sends blobs to Neighborhood 0 in Layer 0" src="img/data-plane-seeding.svg" class="center"/>
|
||||||
|
|
||||||
|
The following diagram shows how Neighborhood 0 fans out to Neighborhoods 1 and 2.
|
||||||
|
|
||||||
|
<img alt="Neighborhood 0 Fanout to Neighborhood 1 and 2" src="img/data-plane-fanout.svg" class="center"/>
|
||||||
|
|
||||||
|
Finally, the following diagram shows a two layer cluster with a Fanout of 2.
|
||||||
|
|
||||||
|
<img alt="Two layer cluster with a Fanout of 2" src="img/data-plane.svg" class="center"/>
|
||||||
|
|
||||||
#### Configuration Values
|
#### Configuration Values
|
||||||
|
|
||||||
`DATA_PLANE_FANOUT` - Determines the size of layer 1. Subsequent
|
`DATA_PLANE_FANOUT` - Determines the size of layer 0. Subsequent
|
||||||
layers have `DATA_PLANE_FANOUT/2` neighborhoods when `grow` is inactive.
|
layers grow by a factor of `DATA_PLANE_FANOUT`.
|
||||||
|
The number of nodes in a neighborhood is equal to the fanout value.
|
||||||
`NEIGHBORHOOD_SIZE` - The number of nodes allowed in a neighborhood.
|
|
||||||
Neighborhoods will fill to capacity before new ones are added, i.e if a
|
Neighborhoods will fill to capacity before new ones are added, i.e if a
|
||||||
neighborhood isn't full, it _must_ be the last one.
|
neighborhood isn't full, it _must_ be the last one.
|
||||||
|
|
||||||
`GROW_LAYER_CAPACITY` - Whether or not retransmit should be behave like a
|
|
||||||
_traditional fanout_, i.e if each additional layer should have growing
|
|
||||||
capacities. When this mode is disabled (default), all layers after layer 1 have
|
|
||||||
the same capacity, keeping the network pressure on all nodes equal.
|
|
||||||
|
|
||||||
Currently, configuration is set when the cluster is launched. In the future,
|
Currently, configuration is set when the cluster is launched. In the future,
|
||||||
these parameters may be hosted on-chain, allowing modification on the fly as the
|
these parameters may be hosted on-chain, allowing modification on the fly as the
|
||||||
cluster sizes change.
|
cluster sizes change.
|
||||||
@ -72,13 +73,10 @@ cluster sizes change.
|
|||||||
## Neighborhoods
|
## Neighborhoods
|
||||||
|
|
||||||
The following diagram shows how two neighborhoods in different layers interact.
|
The following diagram shows how two neighborhoods in different layers interact.
|
||||||
What this diagram doesn't capture is that each neighbor actually receives
|
To cripple a neighborhood, enough nodes (erasure codes +1) from the neighborhood
|
||||||
blobs from one validator per neighborhood above it. This means that, to
|
above need to fail. Since each neighborhood receives blobs from multiple nodes
|
||||||
cripple a neighborhood, enough nodes (erasure codes +1 per neighborhood) from
|
in a neighborhood in the upper layer, we'd need a big network failure in the upper
|
||||||
the layer above need to fail. Since multiple neighborhoods exist in the upper
|
layers to end up with incomplete data.
|
||||||
layer and a node will receive blobs from a node in each of those neighborhoods,
|
|
||||||
we'd need a big network failure in the upper layers to end up with incomplete
|
|
||||||
data.
|
|
||||||
|
|
||||||
<img alt="Inner workings of a neighborhood"
|
<img alt="Inner workings of a neighborhood"
|
||||||
src="img/data-plane-neighborhood.svg" class="center"/>
|
src="img/data-plane-neighborhood.svg" class="center"/>
|
||||||
|
@ -10,7 +10,7 @@ client's account.
|
|||||||
A drone is a simple signing service. It listens for requests to sign
|
A drone is a simple signing service. It listens for requests to sign
|
||||||
*transaction data*. Once received, the drone validates the request however it
|
*transaction data*. Once received, the drone validates the request however it
|
||||||
sees fit. It may, for example, only accept transaction data with a
|
sees fit. It may, for example, only accept transaction data with a
|
||||||
`SystemInstruction::Move` instruction transferring only up to a certain amount
|
`SystemInstruction::Transfer` instruction transferring only up to a certain amount
|
||||||
of tokens. If the drone accepts the transaction, it returns an `Ok(Signature)`
|
of tokens. If the drone accepts the transaction, it returns an `Ok(Signature)`
|
||||||
where `Signature` is a signature of the transaction data using the drone's
|
where `Signature` is a signature of the transaction data using the drone's
|
||||||
private key. If it rejects the transaction data, it returns a `DroneError`
|
private key. If it rejects the transaction data, it returns a `DroneError`
|
||||||
@ -76,7 +76,7 @@ beyond a certain *age*.
|
|||||||
|
|
||||||
If the transaction data size is smaller than the size of the returned signature
|
If the transaction data size is smaller than the size of the returned signature
|
||||||
(or descriptive error), a single client can flood the network. Considering
|
(or descriptive error), a single client can flood the network. Considering
|
||||||
that a simple `Move` operation requires two public keys (each 32 bytes) and a
|
that a simple `Transfer` operation requires two public keys (each 32 bytes) and a
|
||||||
`fee` field, and that the returned signature is 64 bytes (and a byte to
|
`fee` field, and that the returned signature is 64 bytes (and a byte to
|
||||||
indicate `Ok`), consideration for this attack may not be required.
|
indicate `Ok`), consideration for this attack may not be required.
|
||||||
|
|
||||||
|
@ -47,8 +47,8 @@ nodes are started
|
|||||||
$ cargo build --all
|
$ cargo build --all
|
||||||
```
|
```
|
||||||
|
|
||||||
The network is initialized with a genesis ledger and fullnode configuration files.
|
The network is initialized with a genesis ledger generated by running the
|
||||||
These files can be generated by running the following script.
|
following script.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ./multinode-demo/setup.sh
|
$ ./multinode-demo/setup.sh
|
||||||
@ -69,7 +69,7 @@ $ ./multinode-demo/drone.sh
|
|||||||
|
|
||||||
### Singlenode Testnet
|
### Singlenode Testnet
|
||||||
|
|
||||||
Before you start a fullnode, make sure you know the IP address of the machine you
|
Before you start a validator, make sure you know the IP address of the machine you
|
||||||
want to be the bootstrap leader for the demo, and make sure that udp ports 8000-10000 are
|
want to be the bootstrap leader for the demo, and make sure that udp ports 8000-10000 are
|
||||||
open on all the machines you want to test with.
|
open on all the machines you want to test with.
|
||||||
|
|
||||||
@ -86,10 +86,10 @@ The drone does not need to be running for subsequent leader starts.
|
|||||||
### Multinode Testnet
|
### Multinode Testnet
|
||||||
|
|
||||||
To run a multinode testnet, after starting a leader node, spin up some
|
To run a multinode testnet, after starting a leader node, spin up some
|
||||||
additional full nodes in separate shells:
|
additional validators in separate shells:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ./multinode-demo/fullnode-x.sh
|
$ ./multinode-demo/validator-x.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
To run a performance-enhanced full node on Linux,
|
To run a performance-enhanced full node on Linux,
|
||||||
@ -99,7 +99,7 @@ your system:
|
|||||||
```bash
|
```bash
|
||||||
$ ./fetch-perf-libs.sh
|
$ ./fetch-perf-libs.sh
|
||||||
$ SOLANA_CUDA=1 ./multinode-demo/bootstrap-leader.sh
|
$ SOLANA_CUDA=1 ./multinode-demo/bootstrap-leader.sh
|
||||||
$ SOLANA_CUDA=1 ./multinode-demo/fullnode-x.sh
|
$ SOLANA_CUDA=1 ./multinode-demo/validator.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testnet Client Demo
|
### Testnet Client Demo
|
||||||
@ -145,7 +145,7 @@ Generally we are using `debug` for infrequent debug messages, `trace` for potent
|
|||||||
messages and `info` for performance-related logging.
|
messages and `info` for performance-related logging.
|
||||||
|
|
||||||
You can also attach to a running process with GDB. The leader's process is named
|
You can also attach to a running process with GDB. The leader's process is named
|
||||||
_solana-fullnode_:
|
_solana-validator_:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo gdb
|
$ sudo gdb
|
||||||
@ -161,7 +161,7 @@ This will dump all the threads stack traces into gdb.txt
|
|||||||
In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`.
|
In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ./multinode-demo/client.sh --network testnet.solana.com:8001 --duration 60
|
$ ./multinode-demo/client.sh --entrypoint testnet.solana.com:8001 --duration 60
|
||||||
```
|
```
|
||||||
|
|
||||||
You can observe the effects of your client's transactions on our [dashboard](https://metrics.solana.com:3000/d/testnet/testnet-hud?orgId=2&from=now-30m&to=now&refresh=5s&var-testnet=testnet)
|
You can observe the effects of your client's transactions on our [dashboard](https://metrics.solana.com:3000/d/testnet/testnet-hud?orgId=2&from=now-30m&to=now&refresh=5s&var-testnet=testnet)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Gossip Service
|
# Gossip Service
|
||||||
|
|
||||||
The Gossip Service acts as a gateway to nodes in the control plane. Fullnodes
|
The Gossip Service acts as a gateway to nodes in the control plane. Validators
|
||||||
use the service to ensure information is available to all other nodes in a cluster.
|
use the service to ensure information is available to all other nodes in a cluster.
|
||||||
The service broadcasts information using a gossip protocol.
|
The service broadcasts information using a gossip protocol.
|
||||||
|
|
||||||
@ -116,8 +116,8 @@ Just like *pull message*, nodes are selected into the active set based on weight
|
|||||||
|
|
||||||
## Notable differences from PlumTree
|
## Notable differences from PlumTree
|
||||||
|
|
||||||
The active push protocol described here is based on (Plum
|
The active push protocol described here is based on [Plum
|
||||||
Tree)[https://haslab.uminho.pt/jop/files/lpr07a.pdf]. The main differences are:
|
Tree](https://haslab.uminho.pt/jop/files/lpr07a.pdf). The main differences are:
|
||||||
|
|
||||||
* Push messages have a wallclock that is signed by the originator. Once the
|
* Push messages have a wallclock that is signed by the originator. Once the
|
||||||
wallclock expires the message is dropped. A hop limit is difficult to implement
|
wallclock expires the message is dropped. A hop limit is difficult to implement
|
||||||
|
@ -58,7 +58,7 @@ $ solana-install deploy http://example.com/path/to/solana-release.tar.bz2 update
|
|||||||
$ solana-install init --pubkey 92DMonmBYXwEMHJ99c9ceRSpAmk9v6i3RdvDdXaVcrfj # <-- pubkey is obtained from whoever is deploying the updates
|
$ solana-install init --pubkey 92DMonmBYXwEMHJ99c9ceRSpAmk9v6i3RdvDdXaVcrfj # <-- pubkey is obtained from whoever is deploying the updates
|
||||||
$ export PATH=~/.local/share/solana-install/bin:$PATH
|
$ export PATH=~/.local/share/solana-install/bin:$PATH
|
||||||
$ solana-keygen ... # <-- runs the latest solana-keygen
|
$ solana-keygen ... # <-- runs the latest solana-keygen
|
||||||
$ solana-install run solana-fullnode ... # <-- runs a fullnode, restarting it as necesary when an update is applied
|
$ solana-install run solana-validator ... # <-- runs a validator, restarting it as necesary when an update is applied
|
||||||
```
|
```
|
||||||
|
|
||||||
### On-chain Update Manifest
|
### On-chain Update Manifest
|
||||||
|
@ -24,10 +24,13 @@ Methods
|
|||||||
* [confirmTransaction](#confirmtransaction)
|
* [confirmTransaction](#confirmtransaction)
|
||||||
* [getAccountInfo](#getaccountinfo)
|
* [getAccountInfo](#getaccountinfo)
|
||||||
* [getBalance](#getbalance)
|
* [getBalance](#getbalance)
|
||||||
|
* [getClusterNodes](#getclusternodes)
|
||||||
* [getRecentBlockhash](#getrecentblockhash)
|
* [getRecentBlockhash](#getrecentblockhash)
|
||||||
* [getSignatureStatus](#getsignaturestatus)
|
* [getSignatureStatus](#getsignaturestatus)
|
||||||
|
* [getSlotLeader](#getslotleader)
|
||||||
* [getNumBlocksSinceSignatureConfirmation](#getnumblockssincesignatureconfirmation)
|
* [getNumBlocksSinceSignatureConfirmation](#getnumblockssincesignatureconfirmation)
|
||||||
* [getTransactionCount](#gettransactioncount)
|
* [getTransactionCount](#gettransactioncount)
|
||||||
|
* [getEpochVoteAccounts](#getepochvoteaccounts)
|
||||||
* [requestAirdrop](#requestairdrop)
|
* [requestAirdrop](#requestairdrop)
|
||||||
* [sendTransaction](#sendtransaction)
|
* [sendTransaction](#sendtransaction)
|
||||||
* [startSubscriptionChannel](#startsubscriptionchannel)
|
* [startSubscriptionChannel](#startsubscriptionchannel)
|
||||||
@ -114,6 +117,30 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### getClusterNodes
|
||||||
|
Returns information about all the nodes participating in the cluster
|
||||||
|
|
||||||
|
##### Parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
##### Results:
|
||||||
|
The result field will be an array of JSON objects, each with the following sub fields:
|
||||||
|
* `id` - Node identifier, as base-58 encoded string
|
||||||
|
* `gossip` - Gossip network address for the node
|
||||||
|
* `tpu` - TPU network address for the node
|
||||||
|
* `rpc` - JSON RPC network address for the node, or `null` if the JSON RPC service is not enabled
|
||||||
|
|
||||||
|
##### Example:
|
||||||
|
```bash
|
||||||
|
// Request
|
||||||
|
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getClusterNodes"}' http://localhost:8899
|
||||||
|
|
||||||
|
// Result
|
||||||
|
{"jsonrpc":"2.0","result":[{"gossip":"10.239.6.48:8001","id":"9QzsJf7LPLj8GkXbYT3LFDKqsj2hHG7TA3xinJHu8epQ","rpc":"10.239.6.48:8899","tpu":"10.239.6.48:8856"}],"id":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### getAccountInfo
|
### getAccountInfo
|
||||||
Returns all information associated with the account of provided Pubkey
|
Returns all information associated with the account of provided Pubkey
|
||||||
|
|
||||||
@ -141,13 +168,16 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "
|
|||||||
---
|
---
|
||||||
|
|
||||||
### getRecentBlockhash
|
### getRecentBlockhash
|
||||||
Returns a recent block hash from the ledger
|
Returns a recent block hash from the ledger, and a fee schedule that can be used
|
||||||
|
to compute the cost of submitting a transaction using it.
|
||||||
|
|
||||||
##### Parameters:
|
##### Parameters:
|
||||||
None
|
None
|
||||||
|
|
||||||
##### Results:
|
##### Results:
|
||||||
|
An array consisting of
|
||||||
* `string` - a Hash as base-58 encoded string
|
* `string` - a Hash as base-58 encoded string
|
||||||
|
* `FeeCalculator object` - the fee schedule for this block hash
|
||||||
|
|
||||||
##### Example:
|
##### Example:
|
||||||
```bash
|
```bash
|
||||||
@ -155,7 +185,7 @@ None
|
|||||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getRecentBlockhash"}' http://localhost:8899
|
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getRecentBlockhash"}' http://localhost:8899
|
||||||
|
|
||||||
// Result
|
// Result
|
||||||
{"jsonrpc":"2.0","result":"GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC","id":1}
|
{"jsonrpc":"2.0","result":["GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC",{"lamportsPerSignature": 0}],"id":1}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -183,7 +213,27 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "
|
|||||||
{"jsonrpc":"2.0","result":"SignatureNotFound","id":1}
|
{"jsonrpc":"2.0","result":"SignatureNotFound","id":1}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
-----
|
||||||
|
|
||||||
|
### getSlotLeader
|
||||||
|
Returns the current slot leader
|
||||||
|
|
||||||
|
##### Parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
##### Results:
|
||||||
|
* `string` - Node Id as base-58 encoded string
|
||||||
|
|
||||||
|
##### Example:
|
||||||
|
```bash
|
||||||
|
// Request
|
||||||
|
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getSlotLeader"}' http://localhost:8899
|
||||||
|
|
||||||
|
// Result
|
||||||
|
{"jsonrpc":"2.0","result":"ENvAW7JScgYq6o4zKZwewtkzzJgDzuJAFxYasvmEQdpS","id":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
### getNumBlocksSinceSignatureConfirmation
|
### getNumBlocksSinceSignatureConfirmation
|
||||||
Returns the current number of blocks since signature has been confirmed.
|
Returns the current number of blocks since signature has been confirmed.
|
||||||
@ -225,6 +275,39 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### getEpochVoteAccounts
|
||||||
|
Returns the account info and associated stake for all the voting accounts in the current epoch.
|
||||||
|
|
||||||
|
##### Parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
##### Results:
|
||||||
|
An array consisting of vote accounts:
|
||||||
|
* `string` - the vote account's Pubkey as base-58 encoded string
|
||||||
|
* `integer` - the stake, in lamports, delegated to this vote account
|
||||||
|
* `VoteState` - the vote account's state
|
||||||
|
|
||||||
|
Each VoteState will be a JSON object with the following sub fields:
|
||||||
|
|
||||||
|
* `votes`, array of most recent vote lockouts
|
||||||
|
* `node_pubkey`, the pubkey of the node that votes using this account
|
||||||
|
* `authorized_voter_pubkey`, the pubkey of the authorized vote signer for this account
|
||||||
|
* `commission`, a 32-bit integer used as a fraction (commission/MAX_U32) for rewards payout
|
||||||
|
* `root_slot`, the most recent slot this account has achieved maximum lockout
|
||||||
|
* `credits`, credits accrued by this account for reaching lockouts
|
||||||
|
|
||||||
|
##### Example:
|
||||||
|
```bash
|
||||||
|
// Request
|
||||||
|
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getEpochVoteAccounts"}' http://localhost:8899
|
||||||
|
|
||||||
|
// Result
|
||||||
|
{"jsonrpc":"2.0","result":[[[84,115,89,23,41,83,221,72,58,23,53,245,195,188,140,161,242,189,200,164,139,214,12,180,84,161,28,151,24,243,159,125],10000000,{"authorized_voter_pubkey":[84,115,89,23,41,83,221,72,58,23,53,245,195,188,140,161,242,189,200,164,139,214,12,180,84,161,28,151,24,243,159,125],"commission":0,"credits":0,"node_pubkey":[49,139,227,211,47,39,69,86,131,244,160,144,228,169,84,143,142,253,83,81,212,110,254,12,242,71,219,135,30,60,157,213],"root_slot":null,"votes":[{"confirmation_count":1,"slot":0}]}]],"id":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
### requestAirdrop
|
### requestAirdrop
|
||||||
Requests an airdrop of lamports to a Pubkey
|
Requests an airdrop of lamports to a Pubkey
|
||||||
|
|
||||||
@ -270,6 +353,14 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m
|
|||||||
After connect to the RPC PubSub websocket at `ws://<ADDRESS>/`:
|
After connect to the RPC PubSub websocket at `ws://<ADDRESS>/`:
|
||||||
- Submit subscription requests to the websocket using the methods below
|
- Submit subscription requests to the websocket using the methods below
|
||||||
- Multiple subscriptions may be active at once
|
- Multiple subscriptions may be active at once
|
||||||
|
- All subscriptions take an optional `confirmations` parameter, which defines
|
||||||
|
how many confirmed blocks the node should wait before sending a notification.
|
||||||
|
The greater the number, the more likely the notification is to represent
|
||||||
|
consensus across the cluster, and the less likely it is to be affected by
|
||||||
|
forking or rollbacks. If unspecified, the default value is 0; the node will
|
||||||
|
send a notification as soon as it witnesses the event. The maximum
|
||||||
|
`confirmations` wait length is the cluster's `MAX_LOCKOUT_HISTORY`, which
|
||||||
|
represents the economic finality of the chain.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -279,6 +370,8 @@ for a given account public key changes
|
|||||||
|
|
||||||
##### Parameters:
|
##### Parameters:
|
||||||
* `string` - account Pubkey, as base-58 encoded string
|
* `string` - account Pubkey, as base-58 encoded string
|
||||||
|
* `integer` - optional, number of confirmed blocks to wait before notification.
|
||||||
|
Default: 0, Max: `MAX_LOCKOUT_HISTORY` (greater integers rounded down)
|
||||||
|
|
||||||
##### Results:
|
##### Results:
|
||||||
* `integer` - Subscription id (needed to unsubscribe)
|
* `integer` - Subscription id (needed to unsubscribe)
|
||||||
@ -288,6 +381,8 @@ for a given account public key changes
|
|||||||
// Request
|
// Request
|
||||||
{"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12"]}
|
{"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12"]}
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12", 15]}
|
||||||
|
|
||||||
// Result
|
// Result
|
||||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||||
```
|
```
|
||||||
@ -325,6 +420,8 @@ for a given account owned by the program changes
|
|||||||
|
|
||||||
##### Parameters:
|
##### Parameters:
|
||||||
* `string` - program_id Pubkey, as base-58 encoded string
|
* `string` - program_id Pubkey, as base-58 encoded string
|
||||||
|
* `integer` - optional, number of confirmed blocks to wait before notification.
|
||||||
|
Default: 0, Max: `MAX_LOCKOUT_HISTORY` (greater integers rounded down)
|
||||||
|
|
||||||
##### Results:
|
##### Results:
|
||||||
* `integer` - Subscription id (needed to unsubscribe)
|
* `integer` - Subscription id (needed to unsubscribe)
|
||||||
@ -334,6 +431,8 @@ for a given account owned by the program changes
|
|||||||
// Request
|
// Request
|
||||||
{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["9gZbPtbtHrs6hEWgd6MbVY9VPFtS5Z8xKtnYwA2NynHV"]}
|
{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["9gZbPtbtHrs6hEWgd6MbVY9VPFtS5Z8xKtnYwA2NynHV"]}
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["9gZbPtbtHrs6hEWgd6MbVY9VPFtS5Z8xKtnYwA2NynHV", 15]}
|
||||||
|
|
||||||
// Result
|
// Result
|
||||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||||
```
|
```
|
||||||
@ -373,6 +472,8 @@ On `signatureNotification`, the subscription is automatically cancelled
|
|||||||
|
|
||||||
##### Parameters:
|
##### Parameters:
|
||||||
* `string` - Transaction Signature, as base-58 encoded string
|
* `string` - Transaction Signature, as base-58 encoded string
|
||||||
|
* `integer` - optional, number of confirmed blocks to wait before notification.
|
||||||
|
Default: 0, Max: `MAX_LOCKOUT_HISTORY` (greater integers rounded down)
|
||||||
|
|
||||||
##### Results:
|
##### Results:
|
||||||
* `integer` - subscription id (needed to unsubscribe)
|
* `integer` - subscription id (needed to unsubscribe)
|
||||||
@ -382,6 +483,8 @@ On `signatureNotification`, the subscription is automatically cancelled
|
|||||||
// Request
|
// Request
|
||||||
{"jsonrpc":"2.0", "id":1, "method":"signatureSubscribe", "params":["2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b"]}
|
{"jsonrpc":"2.0", "id":1, "method":"signatureSubscribe", "params":["2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b"]}
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0", "id":1, "method":"signatureSubscribe", "params":["2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b", 15]}
|
||||||
|
|
||||||
// Result
|
// Result
|
||||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||||
```
|
```
|
||||||
|
@ -57,7 +57,7 @@ Forwarding is preferred, as it would minimize network congestion, allowing the
|
|||||||
cluster to advertise higher TPS capacity.
|
cluster to advertise higher TPS capacity.
|
||||||
|
|
||||||
|
|
||||||
## Fullnode Loop
|
## Validator Loop
|
||||||
|
|
||||||
The PoH Recorder manages the transition between modes. Once a ledger is
|
The PoH Recorder manages the transition between modes. Once a ledger is
|
||||||
replayed, the validator can run until the recorder indicates it should be
|
replayed, the validator can run until the recorder indicates it should be
|
||||||
|
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
Replication behavior yet to be implemented.
|
Replication behavior yet to be implemented.
|
||||||
|
|
||||||
|
### Storage epoch
|
||||||
|
|
||||||
|
The storage epoch should be the number of slots which results in around 100GB-1TB of
|
||||||
|
ledger to be generated for replicators to store. Replicators will start storing ledger
|
||||||
|
when a given fork has a high probability of not being rolled back.
|
||||||
|
|
||||||
### Validator behavior
|
### Validator behavior
|
||||||
|
|
||||||
3. Every NUM\_KEY\_ROTATION\_TICKS it also validates samples received from
|
3. Every NUM\_KEY\_ROTATION\_TICKS it also validates samples received from
|
||||||
@ -37,3 +43,100 @@ transacation proves the validator incorrectly validated a fake storage proof.
|
|||||||
The replicator is rewarded and the validator's staking balance is slashed or
|
The replicator is rewarded and the validator's staking balance is slashed or
|
||||||
frozen.
|
frozen.
|
||||||
|
|
||||||
|
### Storage proof contract logic
|
||||||
|
|
||||||
|
Each replicator and validator will have their own storage account. The validator's
|
||||||
|
account would be separate from their gossip id similiar to their vote account.
|
||||||
|
These should be implemented as two programs one which handles the validator as the keysigner
|
||||||
|
and one for the replicator. In that way when the programs reference other accounts, they
|
||||||
|
can check the program id to ensure it is a validator or replicator account they are
|
||||||
|
referencing.
|
||||||
|
|
||||||
|
#### SubmitMiningProof
|
||||||
|
```rust,ignore
|
||||||
|
SubmitMiningProof {
|
||||||
|
slot: u64,
|
||||||
|
sha_state: Hash,
|
||||||
|
signature: Signature,
|
||||||
|
};
|
||||||
|
keys = [replicator_keypair]
|
||||||
|
```
|
||||||
|
Replicators create these after mining their stored ledger data for a certain hash value.
|
||||||
|
The slot is the end slot of the segment of ledger they are storing, the sha\_state
|
||||||
|
the result of the replicator using the hash function to sample their encrypted ledger segment.
|
||||||
|
The signature is the signature that was created when they signed a PoH value for the
|
||||||
|
current storage epoch. The list of proofs from the current storage epoch should be saved
|
||||||
|
in the account state, and then transfered to a list of proofs for the previous epoch when
|
||||||
|
the epoch passes. In a given storage epoch a given replicator should only submit proofs
|
||||||
|
for one segment.
|
||||||
|
|
||||||
|
The program should have a list of slots which are valid storage mining slots.
|
||||||
|
This list should be maintained by keeping track of slots which are rooted slots in which a significant
|
||||||
|
portion of the network has voted on with a high lockout value, maybe 32-votes old. Every SLOTS\_PER\_SEGMENT
|
||||||
|
number of slots would be added to this set. The program should check that the slot is in this set. The set can
|
||||||
|
be maintained by receiving a AdvertiseStorageRecentBlockHash and checking with its bank/locktower state.
|
||||||
|
|
||||||
|
The program should do a signature verify check on the signature, public key from the transaction submitter and the message of
|
||||||
|
the previous storage epoch PoH value.
|
||||||
|
|
||||||
|
#### ProofValidation
|
||||||
|
```rust,ignore
|
||||||
|
ProofValidation {
|
||||||
|
proof_mask: Vec<ProofStatus>,
|
||||||
|
}
|
||||||
|
keys = [validator_keypair, replicator_keypair(s) (unsigned)]
|
||||||
|
```
|
||||||
|
A validator will submit this transaction to indicate that a set of proofs for a given
|
||||||
|
segment are valid/not-valid or skipped where the validator did not look at it. The
|
||||||
|
keypairs for the replicators that it looked at should be referenced in the keys so the program
|
||||||
|
logic can go to those accounts and see that the proofs are generated in the previous epoch. The
|
||||||
|
sampling of the storage proofs should be verified ensuring that the correct proofs are skipped by
|
||||||
|
the validator according to the logic outlined in the validator behavior of sampling.
|
||||||
|
|
||||||
|
The included replicator keys will indicate the the storage samples which are being referenced; the
|
||||||
|
length of the proof\_mask should be verified against the set of storage proofs in the referenced
|
||||||
|
replicator account(s), and should match with the number of proofs submitted in the previous storage
|
||||||
|
epoch in the state of said replicator account.
|
||||||
|
|
||||||
|
#### ClaimStorageReward
|
||||||
|
```rust,ignore
|
||||||
|
ClaimStorageReward {
|
||||||
|
}
|
||||||
|
keys = [validator_keypair or replicator_keypair, validator/replicator_keypairs (unsigned)]
|
||||||
|
```
|
||||||
|
Replicators and validators will use this transaction to get paid tokens from a program state
|
||||||
|
where SubmitStorageProof, ProofValidation and ChallengeProofValidations are in a state where
|
||||||
|
proofs have been submitted and validated and there are no ChallengeProofValidations referencing
|
||||||
|
those proofs. For a validator, it should reference the replicator keypairs to which it has validated
|
||||||
|
proofs in the relevant epoch. And for a replicator it should reference validator keypairs for which it
|
||||||
|
has validated and wants to be rewarded.
|
||||||
|
|
||||||
|
#### ChallengeProofValidation
|
||||||
|
```rust,ignore
|
||||||
|
ChallengeProofValidation {
|
||||||
|
proof_index: u64,
|
||||||
|
hash_seed_value: Vec<u8>,
|
||||||
|
}
|
||||||
|
keys = [replicator_keypair, validator_keypair]
|
||||||
|
```
|
||||||
|
|
||||||
|
This transaction is for catching lazy validators who are not doing the work to validate proofs.
|
||||||
|
A replicator will submit this transaction when it sees a validator has approved a fake SubmitMiningProof
|
||||||
|
transaction. Since the replicator is a light client not looking at the full chain, it will have to ask
|
||||||
|
a validator or some set of validators for this information maybe via RPC call to obtain all ProofValidations for
|
||||||
|
a certain segment in the previous storage epoch. The program will look in the validator account
|
||||||
|
state see that a ProofValidation is submitted in the previous storage epoch and hash the hash\_seed\_value and
|
||||||
|
see that the hash matches the SubmitMiningProof transaction and that the validator marked it as valid. If so,
|
||||||
|
then it will save the challenge to the list of challenges that it has in its state.
|
||||||
|
|
||||||
|
#### AdvertiseStorageRecentBlockhash
|
||||||
|
```rust,ignore
|
||||||
|
AdvertiseStorageRecentBlockhash {
|
||||||
|
hash: Hash,
|
||||||
|
slot: u64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Validators and replicators will submit this to indicate that a new storage epoch has passed and that the
|
||||||
|
storage proofs which are current proofs should now be for the previous epoch. Other transactions should
|
||||||
|
check to see that the epoch that they are referencing is accurate according to current chain state.
|
||||||
|
@ -31,7 +31,7 @@ core. The total space required for verification is `1_ledger_segment +
|
|||||||
Validators for PoRep are the same validators that are verifying transactions.
|
Validators for PoRep are the same validators that are verifying transactions.
|
||||||
They have some stake that they have put up as collateral that ensures that
|
They have some stake that they have put up as collateral that ensures that
|
||||||
their work is honest. If you can prove that a validator verified a fake PoRep,
|
their work is honest. If you can prove that a validator verified a fake PoRep,
|
||||||
then the validators stake can be slashed.
|
then the validator will not receive a reward for that storage epoch.
|
||||||
|
|
||||||
Replicators are specialized *light clients*. They download a part of the ledger
|
Replicators are specialized *light clients*. They download a part of the ledger
|
||||||
and store it, and provide PoReps of storing the ledger. For each verified PoRep
|
and store it, and provide PoReps of storing the ledger. For each verified PoRep
|
||||||
@ -53,7 +53,7 @@ changes to determine what rate it can validate storage proofs.
|
|||||||
|
|
||||||
### Constants
|
### Constants
|
||||||
|
|
||||||
1. NUM\_STORAGE\_ENTRIES: Number of entries in a segment of ledger data. The
|
1. SLOTS\_PER\_SEGMENT: Number of slots in a segment of ledger data. The
|
||||||
unit of storage for a replicator.
|
unit of storage for a replicator.
|
||||||
2. NUM\_KEY\_ROTATION\_TICKS: Number of ticks to save a PoH value and cause a
|
2. NUM\_KEY\_ROTATION\_TICKS: Number of ticks to save a PoH value and cause a
|
||||||
key generation for the section of ledger just generated and the rotation of
|
key generation for the section of ledger just generated and the rotation of
|
||||||
@ -77,7 +77,7 @@ height.
|
|||||||
3. Validator generates a storage proof confirmation transaction.
|
3. Validator generates a storage proof confirmation transaction.
|
||||||
4. The storage proof confirmation transaction is integrated into the ledger.
|
4. The storage proof confirmation transaction is integrated into the ledger.
|
||||||
6. Validator responds to RPC interfaces for what the last storage epoch PoH
|
6. Validator responds to RPC interfaces for what the last storage epoch PoH
|
||||||
value is and its entry\_height.
|
value is and its slot.
|
||||||
|
|
||||||
### Replicator behavior
|
### Replicator behavior
|
||||||
|
|
||||||
@ -95,10 +95,10 @@ is:
|
|||||||
- (d) replicator can subscribe to an abbreviated transaction stream to
|
- (d) replicator can subscribe to an abbreviated transaction stream to
|
||||||
generate the information itself
|
generate the information itself
|
||||||
2. A replicator obtains the PoH hash corresponding to the last key rotation
|
2. A replicator obtains the PoH hash corresponding to the last key rotation
|
||||||
along with its entry\_height.
|
along with its slot.
|
||||||
3. The replicator signs the PoH hash with its keypair. That signature is the
|
3. The replicator signs the PoH hash with its keypair. That signature is the
|
||||||
seed used to pick the segment to replicate and also the encryption key. The
|
seed used to pick the segment to replicate and also the encryption key. The
|
||||||
replicator mods the signature with the entry\_height to get which segment to
|
replicator mods the signature with the slot to get which segment to
|
||||||
replicate.
|
replicate.
|
||||||
4. The replicator retrives the ledger by asking peer validators and
|
4. The replicator retrives the ledger by asking peer validators and
|
||||||
replicators. See 6.5.
|
replicators. See 6.5.
|
||||||
@ -118,9 +118,9 @@ current leader and it is put onto the ledger.
|
|||||||
### Finding who has a given block of ledger
|
### Finding who has a given block of ledger
|
||||||
|
|
||||||
1. Validators monitor the transaction stream for storage mining proofs, and
|
1. Validators monitor the transaction stream for storage mining proofs, and
|
||||||
keep a mapping of ledger segments by entry\_height to public keys. When it sees
|
keep a mapping of ledger segments by slot to public keys. When it sees
|
||||||
a storage mining proof it updates this mapping and provides an RPC interface
|
a storage mining proof it updates this mapping and provides an RPC interface
|
||||||
which takes an entry\_height and hands back a list of public keys. The client
|
which takes a slot and hands back a list of public keys. The client
|
||||||
then looks up in their cluster\_info table to see which network address that
|
then looks up in their cluster\_info table to see which network address that
|
||||||
corresponds to and sends a repair request to retrieve the necessary blocks of
|
corresponds to and sends a repair request to retrieve the necessary blocks of
|
||||||
ledger.
|
ledger.
|
||||||
|
@ -85,7 +85,7 @@ contains the following state information:
|
|||||||
|
|
||||||
* Account::lamports - The staked lamports.
|
* Account::lamports - The staked lamports.
|
||||||
|
|
||||||
* `voter_id` - The pubkey of the VoteState instance the lamports are
|
* `voter_pubkey` - The pubkey of the VoteState instance the lamports are
|
||||||
delegated to.
|
delegated to.
|
||||||
|
|
||||||
* `credits_observed` - The total credits claimed over the lifetime of the
|
* `credits_observed` - The total credits claimed over the lifetime of the
|
||||||
@ -109,7 +109,7 @@ program.
|
|||||||
|
|
||||||
* `account[0]` - RW - The StakeState::Delegate instance.
|
* `account[0]` - RW - The StakeState::Delegate instance.
|
||||||
`StakeState::Delegate::credits_observed` is initialized to `VoteState::credits`.
|
`StakeState::Delegate::credits_observed` is initialized to `VoteState::credits`.
|
||||||
`StakeState::Delegate::voter_id` is initialized to `account[1]`
|
`StakeState::Delegate::voter_pubkey` is initialized to `account[1]`
|
||||||
|
|
||||||
* `account[1]` - R - The VoteState instance.
|
* `account[1]` - R - The VoteState instance.
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ reward.
|
|||||||
* `account[1]` - RW - The StakeState::Delegate instance that is redeeming votes
|
* `account[1]` - RW - The StakeState::Delegate instance that is redeeming votes
|
||||||
credits.
|
credits.
|
||||||
* `account[2]` - R - The VoteState instance, must be the same as
|
* `account[2]` - R - The VoteState instance, must be the same as
|
||||||
`StakeState::voter_id`
|
`StakeState::voter_pubkey`
|
||||||
|
|
||||||
Reward is payed out for the difference between `VoteState::credits` to
|
Reward is payed out for the difference between `VoteState::credits` to
|
||||||
`StakeState::Delgate.credits_observed`, and `credits_observed` is updated to
|
`StakeState::Delgate.credits_observed`, and `credits_observed` is updated to
|
||||||
@ -181,7 +181,7 @@ the VoteState program or submitting votes to the program.
|
|||||||
|
|
||||||
The total stake allocated to a VoteState program can be calculated by the sum of
|
The total stake allocated to a VoteState program can be calculated by the sum of
|
||||||
all the StakeState programs that have the VoteState pubkey as the
|
all the StakeState programs that have the VoteState pubkey as the
|
||||||
`StakeState::Delegate::voter_id`.
|
`StakeState::Delegate::voter_pubkey`.
|
||||||
|
|
||||||
## Example Callflow
|
## Example Callflow
|
||||||
|
|
||||||
@ -194,12 +194,12 @@ nodes since stake is used as weight in the network control and data planes. One
|
|||||||
way to implement this would be for the StakeState to delegate to a pool of
|
way to implement this would be for the StakeState to delegate to a pool of
|
||||||
validators instead of a single one.
|
validators instead of a single one.
|
||||||
|
|
||||||
Instead of a single `vote_id` and `credits_observed` entry in the StakeState
|
Instead of a single `vote_pubkey` and `credits_observed` entry in the StakeState
|
||||||
program, the program can be initialized with a vector of tuples.
|
program, the program can be initialized with a vector of tuples.
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
Voter {
|
Voter {
|
||||||
voter_id: Pubkey,
|
voter_pubkey: Pubkey,
|
||||||
credits_observed: u64,
|
credits_observed: u64,
|
||||||
weight: u8,
|
weight: u8,
|
||||||
}
|
}
|
||||||
|
29
book/src/performance-metrics.md
Normal file
29
book/src/performance-metrics.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Performance Metrics
|
||||||
|
|
||||||
|
Solana cluster performance is measured as average number of transactions per second
|
||||||
|
that the network can sustain (TPS). And, how long it takes for a transaction to be
|
||||||
|
confirmed by super majority of the cluster (Confirmation Time).
|
||||||
|
|
||||||
|
Each cluster node maintains various counters that are incremented on certain events.
|
||||||
|
These counters are periodically uploaded to a cloud based database. Solana's metrics
|
||||||
|
dashboard fetches these counters, and computes the performance metrics and displays
|
||||||
|
it on the dashboard.
|
||||||
|
|
||||||
|
## TPS
|
||||||
|
|
||||||
|
The leader node's banking stage maintains a count of transactions that it recorded.
|
||||||
|
The dashboard displays the count averaged over 2 second period in the TPS time series
|
||||||
|
graph. The dashboard also shows per second mean, maximum and total TPS as a running
|
||||||
|
counter.
|
||||||
|
|
||||||
|
## Confirmation Time
|
||||||
|
|
||||||
|
Each validator node maintains a list of active ledger forks that are visible to the node.
|
||||||
|
A fork is considered to be frozen when the node has received and processed all entries
|
||||||
|
corresponding to the fork. A fork is considered to be confirmed when it receives cumulative
|
||||||
|
super majority vote, and when one of its children forks is frozen.
|
||||||
|
|
||||||
|
The node assigns a timestamp to every new fork, and computes the time it took to confirm
|
||||||
|
the fork. This time is reflected as validator confirmation time in performance metrics.
|
||||||
|
The performance dashboard displays the average of each validator node's confirmation time
|
||||||
|
as a time series graph.
|
@ -3,8 +3,8 @@
|
|||||||
A client *app* interacts with a Solana cluster by sending it *transactions*
|
A client *app* interacts with a Solana cluster by sending it *transactions*
|
||||||
with one or more *instructions*. The Solana *runtime* passes those instructions
|
with one or more *instructions*. The Solana *runtime* passes those instructions
|
||||||
to user-contributed *programs*. An instruction might, for example, tell a
|
to user-contributed *programs*. An instruction might, for example, tell a
|
||||||
program to move *lamports* from one *account* to another or create an interactive
|
program to transfer *lamports* from one *account* to another or create an interactive
|
||||||
contract that governs how lamports are moved. Instructions are executed
|
contract that governs how lamports are transfered. Instructions are executed
|
||||||
atomically. If any instruction is invalid, any changes made within the
|
atomically. If any instruction is invalid, any changes made within the
|
||||||
transaction are discarded.
|
transaction are discarded.
|
||||||
|
|
||||||
|
51
book/src/repair-service.md
Normal file
51
book/src/repair-service.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Repair Service
|
||||||
|
|
||||||
|
The RepairService is in charge of retrieving missing blobs that failed to be delivered by primary communication protocols like Avalanche. It is in charge of managing the protocols described below in the `Repair Protocols` section below.
|
||||||
|
|
||||||
|
# Challenges:
|
||||||
|
|
||||||
|
1) Validators can fail to receive particular blobs due to network failures
|
||||||
|
|
||||||
|
2) Consider a scenario where blocktree contains the set of slots {1, 3, 5}. Then Blocktree receives blobs for some slot 7, where for each of the blobs b, b.parent == 6, so then the parent-child relation 6 -> 7 is stored in blocktree. However, there is no way to chain these slots to any of the existing banks in Blocktree, and thus the `Blob Repair` protocol will not repair these slots. If these slots happen to be part of the main chain, this will halt replay progress on this node.
|
||||||
|
|
||||||
|
3) Validators that find themselves behind the cluster by an entire epoch struggle/fail to catch up because they do not have a leader schedule for future epochs. If nodes were to blindly accept repair blobs in these future epochs, this exposes nodes to spam.
|
||||||
|
|
||||||
|
# Repair Protocols
|
||||||
|
|
||||||
|
The repair protocol makes best attempts to progress the forking structure of Blocktree.
|
||||||
|
|
||||||
|
The different protocol strategies to address the above challenges:
|
||||||
|
|
||||||
|
1. Blob Repair (Addresses Challenge #1):
|
||||||
|
This is the most basic repair protocol, with the purpose of detecting and filling "holes" in the ledger. Blocktree tracks the latest root slot. RepairService will then periodically iterate every fork in blocktree starting from the root slot, sending repair requests to validators for any missing blobs. It will send at most some `N` repair reqeusts per iteration.
|
||||||
|
|
||||||
|
Note: Validators will only accept blobs within the current verifiable epoch (epoch the validator has a leader schedule for).
|
||||||
|
|
||||||
|
2. Preemptive Slot Repair (Addresses Challenge #2):
|
||||||
|
The goal of this protocol is to discover the chaining relationship of "orphan" slots that do not currently chain to any known fork.
|
||||||
|
|
||||||
|
* Blocktree will track the set of "orphan" slots in a separate column family.
|
||||||
|
|
||||||
|
* RepairService will periodically make `RequestOrphan` requests for each of the orphans in blocktree.
|
||||||
|
|
||||||
|
`RequestOrphan(orphan)` request - `orphan` is the orphan slot that the requestor wants to know the parents of
|
||||||
|
`RequestOrphan(orphan)` response - The highest blobs for each of the first `N` parents of the requested `orphan`
|
||||||
|
|
||||||
|
On receiving the responses `p`, where `p` is some blob in a parent slot, validators will:
|
||||||
|
* Insert an empty `SlotMeta` in blocktree for `p.slot` if it doesn't already exist.
|
||||||
|
* If `p.slot` does exist, update the parent of `p` based on `parents`
|
||||||
|
|
||||||
|
Note: that once these empty slots are added to blocktree, the `Blob Repair` protocol should attempt to fill those slots.
|
||||||
|
|
||||||
|
Note: Validators will only accept responses containing blobs within the current verifiable epoch (epoch the validator has a leader schedule for).
|
||||||
|
|
||||||
|
3. Repairmen (Addresses Challenge #3):
|
||||||
|
This part of the repair protocol is the primary mechanism by which new nodes joining the cluster catch up after loading a snapshot. This protocol works in a "forward" fashion, so validators can verify every blob that they receive against a known leader schedule.
|
||||||
|
|
||||||
|
Each validator advertises in gossip:
|
||||||
|
* Current root
|
||||||
|
* The set of all completed slots in the confirmed epochs (an epoch that was calculated based on a bank <= current root) past the current root
|
||||||
|
|
||||||
|
Observers of this gossip message with higher epochs (repairmen) send blobs to catch the lagging node up with the rest of the cluster. The repairmen are responsible for sending the slots within the epochs that are confrimed by the advertised `root` in gossip. The repairmen divide the responsibility of sending each of the missing slots in these epochs based on a random seed (simple blob.index iteration by N, seeded with the repairman's node_pubkey). Ideally, each repairman in an N node cluster (N nodes whose epochs are higher than that of the repairee) sends 1/N of the missing blobs. Both data and coding blobs for missing slots are sent. Repairmen do not send blobs again to the same validator until they see the message in gossip updated, at which point they perform another iteration of this protocol.
|
||||||
|
|
||||||
|
Gossip messages are updated every time a validator receives a complete slot within the epoch. Completed slots are detected by blocktree and sent over a channel to RepairService. It is important to note that we know that by the time a slot X is complete, the epoch schedule must exist for the epoch that contains slot X because WindowService will reject blobs for unconfirmed epochs. When a newly completed slot is detected, we also update the current root if it has changed since the last update. The root is made available to RepairService through Blocktree, which holds the latest root.
|
@ -67,7 +67,7 @@ data array and assign it to a Program.
|
|||||||
|
|
||||||
* `Assign` - Allows the user to assign an existing account to a program.
|
* `Assign` - Allows the user to assign an existing account to a program.
|
||||||
|
|
||||||
* `Move` - Moves lamports between accounts.
|
* `Transfer` - Transfers lamports between accounts.
|
||||||
|
|
||||||
## Program State Security
|
## Program State Security
|
||||||
|
|
||||||
|
@ -1,68 +1,195 @@
|
|||||||
# Stake Delegation and Rewards
|
# Stake Delegation and Rewards
|
||||||
|
|
||||||
Stakers are rewarded for helping validate the ledger. They do it by delegating
|
Stakers are rewarded for helping to validate the ledger. They do this by
|
||||||
their stake to fullnodes. Those fullnodes do the legwork and send votes to the
|
delegating their stake to validator nodes. Those validators do the legwork of
|
||||||
stakers' staking accounts. The rest of the cluster uses those stake-weighted
|
replaying the ledger and send votes to a per-node vote account to which stakers
|
||||||
votes to select a block when forks arise. Both the fullnode and staker need
|
can delegate their stakes. The rest of the cluster uses those stake-weighted
|
||||||
some economic incentive to play their part. The fullnode needs to be
|
votes to select a block when forks arise. Both the validator and staker need
|
||||||
compensated for its hardware and the staker needs to be compensated for risking
|
some economic incentive to play their part. The validator needs to be
|
||||||
getting its stake slashed. The economics are covered in [staking
|
compensated for its hardware and the staker needs to be compensated for the risk
|
||||||
|
of getting its stake slashed. The economics are covered in [staking
|
||||||
rewards](staking-rewards.md). This chapter, on the other hand, describes the
|
rewards](staking-rewards.md). This chapter, on the other hand, describes the
|
||||||
underlying mechanics of its implementation.
|
underlying mechanics of its implementation.
|
||||||
|
|
||||||
## Vote and Rewards accounts
|
## Basic Besign
|
||||||
|
|
||||||
The rewards process is split into two on-chain programs. The Vote program
|
The general idea is that the validator owns a Vote account. The Vote account
|
||||||
solves the problem of making stakes slashable. The Rewards account acts as
|
tracks validator votes, counts validator generated credits, and provides any
|
||||||
custodian of the rewards pool. It is responsible for paying out each staker
|
additional validator specific state. The Vote account is not aware of any
|
||||||
once the staker proves to the Rewards program that it participated in
|
stakes delegated to it and has no staking weight.
|
||||||
validating the ledger.
|
|
||||||
|
|
||||||
The Vote account contains the following state information:
|
A separate Stake account (created by a staker) names a Vote account to which the
|
||||||
|
stake is delegated. Rewards generated are proportional to the amount of
|
||||||
|
lamports staked. The Stake account is owned by the staker only. Lamports
|
||||||
|
stored in this account are the stake.
|
||||||
|
|
||||||
* votes - The submitted votes.
|
## Passive Delegation
|
||||||
|
|
||||||
* `delegate_id` - An identity that may operate with the weight of this
|
Any number of Stake accounts can delegate to a single
|
||||||
account's stake. It is typically the identity of a fullnode, but may be any
|
Vote account without an interactive action from the identity controlling
|
||||||
identity involved in stake-weighted computations.
|
the Vote account or submitting votes to the account.
|
||||||
|
|
||||||
* `authorized_voter_id` - Only this identity is authorized to submit votes.
|
The total stake allocated to a Vote account can be calculated by the sum of
|
||||||
|
all the Stake accounts that have the Vote account pubkey as the
|
||||||
|
`StakeState::Delegate::voter_pubkey`.
|
||||||
|
|
||||||
* `credits` - The amount of unclaimed rewards.
|
## Vote and Stake accounts
|
||||||
|
|
||||||
* `root_slot` - The last slot to reach the full lockout commitment necessary
|
The rewards process is split into two on-chain programs. The Vote program solves
|
||||||
for rewards.
|
the problem of making stakes slashable. The Stake account acts as custodian of
|
||||||
|
the rewards pool, and provides passive delegation. The Stake program is
|
||||||
|
responsible for paying out each staker once the staker proves to the Stake
|
||||||
|
program that its delegate has participated in validating the ledger.
|
||||||
|
|
||||||
The Rewards program is stateless and pays out reward when a staker submits its
|
### VoteState
|
||||||
Vote account to the program. Claiming a reward requires a transaction that
|
|
||||||
includes the following instructions:
|
|
||||||
|
|
||||||
1. `RewardsInstruction::RedeemVoteCredits`
|
VoteState is the current state of all the votes the validator has submitted to
|
||||||
2. `VoteInstruction::ClearCredits`
|
the network. VoteState contains the following state information:
|
||||||
|
|
||||||
The Rewards program transfers lamports from the Rewards account to the Vote
|
* votes - The submitted votes data structure.
|
||||||
account's public key. The Rewards program also ensures that the `ClearCredits`
|
|
||||||
instruction follows the `RedeemVoteCredits` instruction, such that a staker may
|
* credits - The total number of rewards this vote program has generated over its
|
||||||
not claim rewards for the same work more than once.
|
lifetime.
|
||||||
|
|
||||||
|
* root\_slot - The last slot to reach the full lockout commitment necessary for
|
||||||
|
rewards.
|
||||||
|
|
||||||
|
* commission - The commission taken by this VoteState for any rewards claimed by
|
||||||
|
staker's Stake accounts. This is the percentage ceiling of the reward.
|
||||||
|
|
||||||
|
* Account::lamports - The accumulated lamports from the commission. These do not
|
||||||
|
count as stakes.
|
||||||
|
|
||||||
|
* `authorized_vote_signer` - Only this identity is authorized to submit votes. This field can only modified by this identity.
|
||||||
|
|
||||||
|
### VoteInstruction::Initialize
|
||||||
|
|
||||||
|
* `account[0]` - RW - The VoteState
|
||||||
|
`VoteState::authorized_vote_signer` is initialized to `account[0]`
|
||||||
|
other VoteState members defaulted
|
||||||
|
|
||||||
|
### VoteInstruction::AuthorizeVoteSigner(Pubkey)
|
||||||
|
|
||||||
|
* `account[0]` - RW - The VoteState
|
||||||
|
`VoteState::authorized_vote_signer` is set to to `Pubkey`, instruction must by
|
||||||
|
signed by Pubkey
|
||||||
|
|
||||||
|
### VoteInstruction::Vote(Vec<Vote>)
|
||||||
|
|
||||||
|
* `account[0]` - RW - The VoteState
|
||||||
|
`VoteState::lockouts` and `VoteState::credits` are updated according to voting lockout rules see [Fork Selection](fork-selection.md)
|
||||||
|
|
||||||
|
|
||||||
### Delegating Stake
|
* `account[1]` - RO - A list of some N most recent slots and their hashes for the vote to be verified against.
|
||||||
|
|
||||||
`VoteInstruction::DelegateStake` allows the staker to choose a fullnode to
|
|
||||||
validate the ledger on its behalf. By being a delegate, the fullnode is
|
|
||||||
entitled to collect transaction fees when its is leader. The larger the stake,
|
|
||||||
the more often the fullnode will be able to collect those fees.
|
|
||||||
|
|
||||||
### Authorizing a Vote Signer
|
### StakeState
|
||||||
|
|
||||||
|
A StakeState takes one of two forms, StakeState::Delegate and StakeState::MiningPool.
|
||||||
|
|
||||||
|
### StakeState::Delegate
|
||||||
|
|
||||||
|
StakeState is the current delegation preference of the **staker**. StakeState
|
||||||
|
contains the following state information:
|
||||||
|
|
||||||
|
* Account::lamports - The staked lamports.
|
||||||
|
|
||||||
|
* `voter_pubkey` - The pubkey of the VoteState instance the lamports are
|
||||||
|
delegated to.
|
||||||
|
|
||||||
|
* `credits_observed` - The total credits claimed over the lifetime of the
|
||||||
|
program.
|
||||||
|
|
||||||
|
### StakeState::MiningPool
|
||||||
|
|
||||||
|
There are two approaches to the mining pool. The bank could allow the
|
||||||
|
StakeState program to bypass the token balance check, or a program representing
|
||||||
|
the mining pool could run on the network. To avoid a single network wide lock,
|
||||||
|
the pool can be split into several mining pools. This design focuses on using
|
||||||
|
StakeState::MiningPool instances as the cluster wide mining pools.
|
||||||
|
|
||||||
|
* 256 StakeState::MiningPool are initialized, each with 1/256 number of mining pool
|
||||||
|
tokens stored as `Account::lamports`.
|
||||||
|
|
||||||
|
The stakes and the MiningPool are accounts that are owned by the same `Stake`
|
||||||
|
program.
|
||||||
|
|
||||||
|
### StakeInstruction::Initialize
|
||||||
|
|
||||||
|
* `account[0]` - RW - The StakeState::Delegate instance.
|
||||||
|
`StakeState::Delegate::credits_observed` is initialized to `VoteState::credits`.
|
||||||
|
`StakeState::Delegate::voter_pubkey` is initialized to `account[1]`
|
||||||
|
|
||||||
|
* `account[1]` - R - The VoteState instance.
|
||||||
|
|
||||||
|
### StakeInstruction::RedeemVoteCredits
|
||||||
|
|
||||||
|
The Staker or the owner of the Stake account sends a transaction with this
|
||||||
|
instruction to claim rewards.
|
||||||
|
|
||||||
|
The Vote account and the Stake account pair maintain a lifetime counter
|
||||||
|
of total rewards generated and claimed. When claiming rewards, the total lamports
|
||||||
|
deposited into the Stake account and as validator commission is proportional to
|
||||||
|
`VoteState::credits - StakeState::credits_observed`.
|
||||||
|
|
||||||
|
|
||||||
|
* `account[0]` - RW - The StakeState::MiningPool instance that will fulfill the
|
||||||
|
reward.
|
||||||
|
* `account[1]` - RW - The StakeState::Delegate instance that is redeeming votes
|
||||||
|
credits.
|
||||||
|
* `account[2]` - R - The VoteState instance, must be the same as
|
||||||
|
`StakeState::voter_pubkey`
|
||||||
|
|
||||||
|
Reward is paid out for the difference between `VoteState::credits` to
|
||||||
|
`StakeState::Delgate.credits_observed`, and `credits_observed` is updated to
|
||||||
|
`VoteState::credits`. The commission is deposited into the Vote account token
|
||||||
|
balance, and the reward is deposited to the Stake account token balance.
|
||||||
|
|
||||||
|
The total lamports paid is a percentage-rate of the lamports staked muiltplied by
|
||||||
|
the ratio of rewards being redeemed to rewards that could have been generated
|
||||||
|
during the rate period.
|
||||||
|
|
||||||
|
Any random MiningPool can be used to redeem the credits.
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
let credits_to_claim = vote_state.credits - stake_state.credits_observed;
|
||||||
|
stake_state.credits_observed = vote_state.credits;
|
||||||
|
```
|
||||||
|
|
||||||
|
`credits_to_claim` is used to compute the reward and commission, and
|
||||||
|
`StakeState::Delegate::credits_observed` is updated to the latest
|
||||||
|
`VoteState::credits` value.
|
||||||
|
|
||||||
|
## Collecting network fees into the MiningPool
|
||||||
|
|
||||||
|
At the end of the block, before the bank is frozen, but after it processed all
|
||||||
|
the transactions for the block, a virtual instruction is executed to collect
|
||||||
|
the transaction fees.
|
||||||
|
|
||||||
|
* A portion of the fees are deposited into the leader's account.
|
||||||
|
* A portion of the fees are deposited into the smallest StakeState::MiningPool
|
||||||
|
account.
|
||||||
|
|
||||||
|
## Authorizing a Vote Signer
|
||||||
|
|
||||||
`VoteInstruction::AuthorizeVoter` allows a staker to choose a signing service
|
`VoteInstruction::AuthorizeVoter` allows a staker to choose a signing service
|
||||||
for its votes. That service is responsible for ensuring the vote won't cause
|
for its votes. That service is responsible for ensuring the vote won't cause
|
||||||
the staker to be slashed.
|
the staker to be slashed.
|
||||||
|
|
||||||
## Limitations
|
## Benefits of the design
|
||||||
|
|
||||||
Many stakers may delegate their stakes to the same fullnode. The fullnode must
|
* Single vote for all the stakers.
|
||||||
send a separate vote to each staking account. If there are far more stakers
|
|
||||||
than fullnodes, that's a lot of network traffic. An alternative design might
|
* Clearing of the credit variable is not necessary for claiming rewards.
|
||||||
have fullnodes submit each vote to just one account and then have each staker
|
|
||||||
submit that account along with their own to collect its reward.
|
* Each delegated stake can claim its rewards independently.
|
||||||
|
|
||||||
|
* Commission for the work is deposited when a reward is claimed by the delegated
|
||||||
|
stake.
|
||||||
|
|
||||||
|
This proposal would benefit from the `read-only` accounts proposal to allow for
|
||||||
|
many rewards to be claimed concurrently.
|
||||||
|
|
||||||
|
## Example Callflow
|
||||||
|
|
||||||
|
<img alt="Passive Staking Callflow" src="img/passive-staking-callflow.svg" class="center"/>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Staking Rewards
|
# Staking Rewards
|
||||||
|
|
||||||
Initial Proof of Stake (PoS) (i.e. using in-protocol asset, SOL, to provide
|
A Proof of Stake (PoS), (i.e. using in-protocol asset, SOL, to provide
|
||||||
secure consensus) design ideas outlined here. Solana will implement a proof of
|
secure consensus) design is outlined here. Solana implements a proof of
|
||||||
stake reward/security scheme for node validators in the cluster. The purpose is
|
stake reward/security scheme for validator nodes in the cluster. The purpose is
|
||||||
threefold:
|
threefold:
|
||||||
|
|
||||||
- Align validator incentives with that of the greater cluster through
|
- Align validator incentives with that of the greater cluster through
|
||||||
|
@ -15,39 +15,43 @@ reasons:
|
|||||||
* The cluster rolled back the ledger
|
* The cluster rolled back the ledger
|
||||||
* A validator responded to queries maliciously
|
* A validator responded to queries maliciously
|
||||||
|
|
||||||
### The Transact Trait
|
### The AsyncClient and SyncClient Traits
|
||||||
|
|
||||||
To troubleshoot, the application should retarget a lower-level component, where
|
To troubleshoot, the application should retarget a lower-level component, where
|
||||||
fewer errors are possible. Retargeting can be done with different
|
fewer errors are possible. Retargeting can be done with different
|
||||||
implementations of the Transact trait.
|
implementations of the AsyncClient and SyncClient traits.
|
||||||
|
|
||||||
When Futures 0.3.0 is released, the Transact trait may look like this:
|
Components implement the following primary methods:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
trait Transact {
|
trait AsyncClient {
|
||||||
async fn send_transactions(txs: &[Transaction]) -> Vec<Result<(), TransactionError>>;
|
fn async_send_transaction(&self, transaction: Transaction) -> io::Result<Signature>;
|
||||||
|
}
|
||||||
|
|
||||||
|
trait SyncClient {
|
||||||
|
fn get_signature_status(&self, signature: &Signature) -> Result<Option<transaction::Result<()>>>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Users send transactions and asynchrounously await their results.
|
Users send transactions and asynchrounously and synchrounously await results.
|
||||||
|
|
||||||
#### Transact with Clusters
|
#### ThinClient for Clusters
|
||||||
|
|
||||||
The highest level implementation targets a Solana cluster, which may be a
|
The highest level implementation, ThinClient, targets a Solana cluster, which
|
||||||
deployed testnet or a local cluster running on a development machine.
|
may be a deployed testnet or a local cluster running on a development machine.
|
||||||
|
|
||||||
#### Transact with the TPU
|
#### TpuClient for the TPU
|
||||||
|
|
||||||
The next level is the TPU implementation of Transact. At the TPU level, the
|
The next level is the TPU implementation, which is not yet implemented. At the
|
||||||
application sends transactions over Rust channels, where there can be no
|
TPU level, the application sends transactions over Rust channels, where there
|
||||||
surprises from network queues or dropped packets. The TPU implements all
|
can be no surprises from network queues or dropped packets. The TPU implements
|
||||||
"normal" transaction errors. It does signature verification, may report
|
all "normal" transaction errors. It does signature verification, may report
|
||||||
account-in-use errors, and otherwise results in the ledger, complete with proof
|
account-in-use errors, and otherwise results in the ledger, complete with proof
|
||||||
of history hashes.
|
of history hashes.
|
||||||
|
|
||||||
### Low-level testing
|
### Low-level testing
|
||||||
|
|
||||||
### Testing with the Bank
|
#### BankClient for the Bank
|
||||||
|
|
||||||
Below the TPU level is the Bank. The Bank doesn't do signature verification or
|
Below the TPU level is the Bank. The Bank doesn't do signature verification or
|
||||||
generate a ledger. The Bank is a convenient layer at which to test new on-chain
|
generate a ledger. The Bank is a convenient layer at which to test new on-chain
|
||||||
|
@ -1,30 +1,64 @@
|
|||||||
## Testnet Participation
|
## Testnet Participation
|
||||||
This document describes how to participate in the beta testnet as a
|
This document describes how to participate in the testnet as a
|
||||||
validator node.
|
validator node.
|
||||||
|
|
||||||
Please note some of the information and instructions described here may change
|
Please note some of the information and instructions described here may change
|
||||||
in future releases.
|
in future releases.
|
||||||
|
|
||||||
### Beta Testnet Overview
|
### Overview
|
||||||
The beta testnet features a validator running at beta.testnet.solana.com, which
|
The testnet features a validator running at testnet.solana.com, which
|
||||||
serves as the entrypoint to the cluster for your validator.
|
serves as the entrypoint to the cluster for your validator.
|
||||||
|
|
||||||
Additionally there is a blockexplorer available at http://beta.testnet.solana.com/.
|
Additionally there is a blockexplorer available at
|
||||||
|
[http://testnet.solana.com/](http://testnet.solana.com/).
|
||||||
|
|
||||||
The beta testnet is configured to reset the ledger every 24hours, or sooner
|
The testnet is configured to reset the ledger daily, or sooner
|
||||||
should an hourly automated sanity test fail.
|
should the hourly automated cluster sanity test fail.
|
||||||
|
|
||||||
|
There is a **#validator-support** Discord channel available to reach other
|
||||||
|
testnet participants, [https://discord.gg/pquxPsq](https://discord.gg/pquxPsq).
|
||||||
|
|
||||||
|
Also we'd love it if you choose to register your validator node with us at
|
||||||
|
[https://forms.gle/LfFscZqJELbuUP139](https://forms.gle/LfFscZqJELbuUP139).
|
||||||
|
|
||||||
### Machine Requirements
|
### Machine Requirements
|
||||||
Since the beta testnet is not intended for stress testing of max transaction
|
Since the testnet is not intended for stress testing of max transaction
|
||||||
throughput, a higher-end machine with a GPU is not necessary to participate.
|
throughput, a higher-end machine with a GPU is not necessary to participate.
|
||||||
|
|
||||||
However ensure the machine used is not behind a residential NAT to avoid NAT
|
However ensure the machine used is not behind a residential NAT to avoid NAT
|
||||||
traversal issues. A cloud-hosted machine works best. Ensure that IP ports
|
traversal issues. A cloud-hosted machine works best. **Ensure that IP ports
|
||||||
8000 through 10000 are not blocked for Internet traffic.
|
8000 through 10000 are not blocked for Internet inbound and outbound traffic.**
|
||||||
|
|
||||||
Prebuilt binaries are available for Linux x86_64 (Ubuntu 18.04 recommended).
|
Prebuilt binaries are available for Linux x86_64 (Ubuntu 18.04 recommended).
|
||||||
MacOS or WSL users may build from source.
|
MacOS or WSL users may build from source.
|
||||||
|
|
||||||
|
For a performance testnet with many transactions we have some preliminary recomended setups:
|
||||||
|
|
||||||
|
| | Low end | Medium end | High end | Notes |
|
||||||
|
| --- | ---------|------------|----------| -- |
|
||||||
|
| CPU | AMD Threadripper 1900x | AMD Threadripper 2920x | AMD Threadripper 2950x | Consider a 10Gb-capable motherboard with as many PCIe lanes and m.2 slots as possible. |
|
||||||
|
| RAM | 16GB | 32GB | 64GB | |
|
||||||
|
| OS Drive | Samsung 860 Evo 2TB | Samsung 860 Evo 4TB | Samsung 860 Evo 4TB | Or equivalent SSD |
|
||||||
|
| Accounts Drive(s) | None | Samsung 970 Pro 1TB | 2x Samsung 970 Pro 1TB | |
|
||||||
|
| GPU | 4x Nvidia 1070 or 2x Nvidia 1080 Ti or 2x Nvidia 2070 | 2x Nvidia 2080 Ti | 4x Nvidia 2080 Ti | Any number of cuda-capable GPUs are supported on Linux platforms. |
|
||||||
|
|
||||||
|
#### Confirm The Testnet Is Reachable
|
||||||
|
Before attaching a validator node, sanity check that the cluster is accessible
|
||||||
|
to your machine by running some simple commands. If any of the commands fail,
|
||||||
|
please retry 5-10 minutes later to confirm the testnet is not just restarting
|
||||||
|
itself before debugging further.
|
||||||
|
|
||||||
|
Fetch the current transaction count over JSON RPC:
|
||||||
|
```bash
|
||||||
|
$ curl -X POST -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1, "method":"getTransactionCount"}' http://testnet.solana.com:8899
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect the blockexplorer at [http://testnet.solana.com/](http://testnet.solana.com/) for activity.
|
||||||
|
|
||||||
|
View the [metrics dashboard](
|
||||||
|
https://metrics.solana.com:3000/d/testnet-beta/testnet-monitor-beta?var-testnet=testnet)
|
||||||
|
for more detail on cluster activity.
|
||||||
|
|
||||||
### Validator Setup
|
### Validator Setup
|
||||||
#### Obtaining The Software
|
#### Obtaining The Software
|
||||||
##### Bootstrap with `solana-install`
|
##### Bootstrap with `solana-install`
|
||||||
@ -32,16 +66,15 @@ MacOS or WSL users may build from source.
|
|||||||
The `solana-install` tool can be used to easily install and upgrade the cluster
|
The `solana-install` tool can be used to easily install and upgrade the cluster
|
||||||
software on Linux x86_64 systems.
|
software on Linux x86_64 systems.
|
||||||
|
|
||||||
Install the latest release with a single shell command:
|
|
||||||
```bash
|
```bash
|
||||||
$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.13.0/install/solana-install-init.sh | \
|
$ export SOLANA_RELEASE=v0.14.2 # skip this line to install the latest release
|
||||||
sh -c - --url https://api.beta.testnet.solana.com
|
$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.14.0/install/solana-install-init.sh | sh -s
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively build the `solana-install` program from source and run the
|
Alternatively build the `solana-install` program from source and run the
|
||||||
following command to obtain the same result:
|
following command to obtain the same result:
|
||||||
```bash
|
```bash
|
||||||
$ solana-install init --url https://api.beta.testnet.solana.com
|
$ solana-install init
|
||||||
```
|
```
|
||||||
|
|
||||||
After a successful install, `solana-install update` may be used to easily update the cluster
|
After a successful install, `solana-install update` may be used to easily update the cluster
|
||||||
@ -50,87 +83,117 @@ software to a newer version.
|
|||||||
##### Download Prebuilt Binaries
|
##### Download Prebuilt Binaries
|
||||||
Binaries are available for Linux x86_64 systems.
|
Binaries are available for Linux x86_64 systems.
|
||||||
|
|
||||||
Download the binaries by navigating to https://github.com/solana-labs/solana/releases/latest, download
|
Download the binaries by navigating to
|
||||||
**solana-release-x86_64-unknown-linux-gnu.tar.bz2**, then extract the archive:
|
[https://github.com/solana-labs/solana/releases/latest](https://github.com/solana-labs/solana/releases/latest),
|
||||||
|
download **solana-release-x86_64-unknown-linux-gnu.tar.bz2**, then extract the
|
||||||
|
archive:
|
||||||
```bash
|
```bash
|
||||||
$ tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2
|
$ tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2
|
||||||
$ cd solana-release/
|
$ cd solana-release/
|
||||||
$ export PATH=$PWD/bin:$PATH
|
$ export PATH=$PWD/bin:$PATH
|
||||||
```
|
```
|
||||||
##### Build From Source
|
##### Build From Source
|
||||||
If you are unable to use the prebuilt binaries or prefer to build it yourself from source, navigate to:
|
If you are unable to use the prebuilt binaries or prefer to build it yourself
|
||||||
> https://github.com/solana-labs/solana/releases/latest
|
from source, navigate to
|
||||||
|
[https://github.com/solana-labs/solana/releases/latest](https://github.com/solana-labs/solana/releases/latest),
|
||||||
Download the source code tarball (solana-*[release]*.tar.gz) from our latest release tag. Extract the code and build the binaries with:
|
and download the **Source Code** archive. Extract the code and build the
|
||||||
|
binaries with:
|
||||||
```bash
|
```bash
|
||||||
$ ./scripts/cargo-install-all.sh .
|
$ ./scripts/cargo-install-all.sh .
|
||||||
$ export PATH=$PWD/bin:$PATH
|
$ export PATH=$PWD/bin:$PATH
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Confirm The Testnet Is Reachable
|
|
||||||
Before attaching a validator node, sanity check that the cluster is accessible
|
|
||||||
to your machine by running some simple wallet commands. If any of these
|
|
||||||
commands fail, please retry 5-10 minutes later to confirm the testnet is not
|
|
||||||
just restarting itself before debugging further.
|
|
||||||
|
|
||||||
Receive an airdrop of lamports from the testnet drone:
|
|
||||||
```bash
|
|
||||||
$ solana-wallet -n beta.testnet.solana.com airdrop 123
|
|
||||||
$ solana-wallet -n beta.testnet.solana.com balance
|
|
||||||
```
|
|
||||||
|
|
||||||
Fetch the current testnet transaction count over JSON RPC:
|
|
||||||
```bash
|
|
||||||
$ curl -X POST -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1, "method":"getTransactionCount"}' http://beta.testnet.solana.com:8899
|
|
||||||
```
|
|
||||||
|
|
||||||
Inspect the blockexplorer at http://beta.testnet.solana.com/ for activity.
|
|
||||||
|
|
||||||
Run the following command to join the gossip network and view all the other nodes in the cluster:
|
|
||||||
```bash
|
|
||||||
$ solana-gossip --network beta.testnet.solana.com:8001
|
|
||||||
```
|
|
||||||
|
|
||||||
### Starting The Validator
|
### Starting The Validator
|
||||||
The following command will start a new validator node.
|
Sanity check that you are able to interact with the cluster by receiving a small
|
||||||
|
airdrop of lamports from the testnet drone:
|
||||||
|
```bash
|
||||||
|
$ solana-wallet airdrop 123
|
||||||
|
$ solana-wallet balance
|
||||||
|
```
|
||||||
|
|
||||||
|
Also try running following command to join the gossip network and view all the other nodes in the cluster:
|
||||||
|
```bash
|
||||||
|
$ solana-gossip --entrypoint testnet.solana.com:8001 spy
|
||||||
|
# Press ^C to exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Now configure a key pair for your validator by running:
|
||||||
|
```bash
|
||||||
|
$ solana-keygen -o validator-keypair.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use one of the following commands, depending on your installation
|
||||||
|
choice, to start the node:
|
||||||
|
|
||||||
If this is a `solana-install`-installation:
|
If this is a `solana-install`-installation:
|
||||||
```bash
|
```bash
|
||||||
$ fullnode-x.sh --public-address --poll-for-new-genesis-block beta.testnet.solana.com:8001
|
$ clear-config.sh
|
||||||
|
$ validator.sh --identity validator-keypair.json --poll-for-new-genesis-block testnet.solana.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, the `solana-install run` command can be used to run the validator
|
Alternatively, the `solana-install run` command can be used to run the validator
|
||||||
node while periodically checking for and applying software updates:
|
node while periodically checking for and applying software updates:
|
||||||
```bash
|
```bash
|
||||||
$ solana-install run fullnode-x.sh --public-address --poll-for-new-genesis-block beta.testnet.solana.com:8001
|
$ clear-config.sh
|
||||||
|
$ solana-install run validator.sh -- --identity validator-keypair.json --poll-for-new-genesis-block testnet.solana.com
|
||||||
```
|
```
|
||||||
|
|
||||||
When not using `solana-install`:
|
If you built from source:
|
||||||
```bash
|
```bash
|
||||||
$ USE_INSTALL=1 ./multinode-demo/fullnode-x.sh --public-address --poll-for-new-genesis-block beta.testnet.solana.com:8001
|
$ USE_INSTALL=1 ./multinode-demo/clear-config.sh
|
||||||
|
$ USE_INSTALL=1 ./multinode-demo/validator.sh --identity validator-keypair.json --poll-for-new-genesis-block testnet.solana.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Then from another console, confirm the IP address if your node is now visible in
|
|
||||||
the gossip network by running:
|
|
||||||
```bash
|
|
||||||
$ solana-gossip --network beta.testnet.solana.com:8001
|
|
||||||
```
|
|
||||||
|
|
||||||
Congratulations, you're now participating in the testnet cluster!
|
|
||||||
|
|
||||||
#### Controlling local network port allocation
|
#### Controlling local network port allocation
|
||||||
By default the validator will dynamically select available network ports in the
|
By default the validator will dynamically select available network ports in the
|
||||||
8000-10000 range, and may be overridden with `--dynamic-port-range`. For
|
8000-10000 range, and may be overridden with `--dynamic-port-range`. For
|
||||||
example, `fullnode-x.sh --dynamic-port-range 11000-11010 ...` will restrict the
|
example, `validator.sh --dynamic-port-range 11000-11010 ...` will restrict the
|
||||||
validator to ports 11000-11011.
|
validator to ports 11000-11011.
|
||||||
|
|
||||||
|
### Validator Monitoring
|
||||||
|
When `validator.sh` starts, it will output a validator configuration that looks
|
||||||
|
similar to:
|
||||||
|
```bash
|
||||||
|
======================[ validator configuration ]======================
|
||||||
|
identity pubkey: 4ceWXsL3UJvn7NYZiRkw7NsryMpviaKBDYr8GK7J61Dm
|
||||||
|
vote pubkey: 2ozWvfaXQd1X6uKh8jERoRGApDqSqcEy6fF1oN13LL2G
|
||||||
|
ledger: ...
|
||||||
|
accounts: ...
|
||||||
|
======================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
The **identity pubkey** for your validator can also be found by running:
|
||||||
|
```bash
|
||||||
|
$ solana-keygen pubkey validator-keypair.json
|
||||||
|
```
|
||||||
|
|
||||||
|
From another console, confirm the IP address and **identity pubkey** of your validator is visible in the
|
||||||
|
gossip network by running:
|
||||||
|
```bash
|
||||||
|
$ solana-gossip --entrypoint testnet.solana.com:8001 spy
|
||||||
|
```
|
||||||
|
|
||||||
|
Provide the **vote pubkey** to the `solana-wallet show-vote-account` command to view
|
||||||
|
the recent voting activity from your validator:
|
||||||
|
```bash
|
||||||
|
$ solana-wallet -n testnet.solana.com show-vote-account 2ozWvfaXQd1X6uKh8jERoRGApDqSqcEy6fF1oN13LL2G
|
||||||
|
```
|
||||||
|
|
||||||
|
The vote pubkey for the validator can also be found by running:
|
||||||
|
```bash
|
||||||
|
# If this is a `solana-install`-installation run:
|
||||||
|
$ solana-keygen pubkey ~/.local/share/solana/install/active_release/config-local/validator-vote-keypair.json
|
||||||
|
# Otherwise run:
|
||||||
|
$ solana-keygen pubkey ./config-local/validator-vote-keypair.json
|
||||||
|
```
|
||||||
|
|
||||||
### Sharing Metrics From Your Validator
|
### Sharing Metrics From Your Validator
|
||||||
If you'd like to share metrics perform the following steps before starting the
|
If you have obtained a metrics username/password from the Solana maintainers to
|
||||||
validator node:
|
help us monitor the health of the testnet, please perform the following steps
|
||||||
|
before starting the validator to activate metrics reporting:
|
||||||
```bash
|
```bash
|
||||||
export u="username obtained from the Solana maintainers"
|
export u="username obtained from the Solana maintainers"
|
||||||
export p="password obtained from the Solana maintainers"
|
export p="password obtained from the Solana maintainers"
|
||||||
export SOLANA_METRICS_CONFIG="db=testnet-beta,u=${u:?},p=${p:?}"
|
export SOLANA_METRICS_CONFIG="db=testnet,u=${u:?},p=${p:?}"
|
||||||
source scripts/configure-metrics.sh
|
source scripts/configure-metrics.sh
|
||||||
```
|
```
|
||||||
Inspect for your contributions to our [metrics dashboard](https://metrics.solana.com:3000/d/U9-26Cqmk/testnet-monitor-cloud?refresh=60s&orgId=2&var-hostid=All).
|
|
||||||
|
56
book/src/validator-proposal.md
Normal file
56
book/src/validator-proposal.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Anatomy of a Validator
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
When we first started Solana, the goal was to de-risk our TPS claims. We knew
|
||||||
|
that between optimistic concurrency control and sufficiently long leader slots,
|
||||||
|
that PoS consensus was not the biggest risk to TPS. It was GPU-based signature
|
||||||
|
verification, software pipelining and concurrent banking. Thus, the TPU was
|
||||||
|
born. After topping 100k TPS, we split the team into one group working toward
|
||||||
|
710k TPS and another to flesh out the validator pipeline. Hence, the TVU was
|
||||||
|
born. The current architecture is a consequence of incremental development with
|
||||||
|
that ordering and project priorities. It is not a reflection of what we ever
|
||||||
|
believed was the most technically elegant cross-section of those technologies.
|
||||||
|
In the context of leader rotation, the strong distinction between leading and
|
||||||
|
validating is blurred.
|
||||||
|
|
||||||
|
## Difference between validating and leading
|
||||||
|
|
||||||
|
The fundamental difference between the pipelines is when the PoH is present. In
|
||||||
|
a leader, we process transactions, removing bad ones, and then tag the result
|
||||||
|
with a PoH hash. In the validator, we verify that hash, peel it off, and
|
||||||
|
process the transactions in exactly the same way. The only difference is that
|
||||||
|
if a validator sees a bad transaction, it can't simply remove it like the
|
||||||
|
leader does, because that would cause the PoH hash to change. Instead, it
|
||||||
|
rejects the whole block. The other difference between the pipelines is what
|
||||||
|
happens *after* banking. The leader broadcasts entries to downstream validators
|
||||||
|
whereas the validator will have already done that in RetransmitStage, which is
|
||||||
|
a confirmation time optimization. The validation pipeline, on the other hand,
|
||||||
|
has one last step. Any time it finishes processing a block, it needs to weigh
|
||||||
|
any forks it's observing, possibly cast a vote, and if so, reset its PoH hash
|
||||||
|
to the block hash it just voted on.
|
||||||
|
|
||||||
|
## Proposed Design
|
||||||
|
|
||||||
|
We unwrap the many abstraction layers and build a single pipeline that can
|
||||||
|
toggle leader mode on whenever the validator's ID shows up in the leader
|
||||||
|
schedule.
|
||||||
|
|
||||||
|
<img alt="Validator block diagram" src="img/validator-proposal.svg" class="center"/>
|
||||||
|
|
||||||
|
## Notable changes
|
||||||
|
|
||||||
|
* No threads are shut down to switch out of leader mode. Instead, FetchStage
|
||||||
|
should forward transactions to the next leader.
|
||||||
|
* Hoist FetchStage and BroadcastStage out of TPU
|
||||||
|
* Blocktree renamed to Blockstore
|
||||||
|
* BankForks renamed to Banktree
|
||||||
|
* TPU moves to new socket-free crate called solana-tpu.
|
||||||
|
* TPU's BankingStage absorbs ReplayStage
|
||||||
|
* TVU goes away
|
||||||
|
* New RepairStage absorbs Blob Fetch Stage and repair requests
|
||||||
|
* JSON RPC Service is optional - used for debugging. It should instead be part
|
||||||
|
of a separate `solana-blockstreamer` executable.
|
||||||
|
* New MulticastStage absorbs retransmit part of RetransmitStage
|
||||||
|
* MulticastStage downstream of Blockstore
|
||||||
|
|
@ -1,10 +1,10 @@
|
|||||||
# Anatomy of a Fullnode
|
# Anatomy of a Validator
|
||||||
|
|
||||||
<img alt="Fullnode block diagrams" src="img/fullnode.svg" class="center"/>
|
<img alt="Validator block diagrams" src="img/validator.svg" class="center"/>
|
||||||
|
|
||||||
## Pipelining
|
## Pipelining
|
||||||
|
|
||||||
The fullnodes make extensive use of an optimization common in CPU design,
|
The validators make extensive use of an optimization common in CPU design,
|
||||||
called *pipelining*. Pipelining is the right tool for the job when there's a
|
called *pipelining*. Pipelining is the right tool for the job when there's a
|
||||||
stream of input data that needs to be processed by a sequence of steps, and
|
stream of input data that needs to be processed by a sequence of steps, and
|
||||||
there's different hardware responsible for each. The quintessential example is
|
there's different hardware responsible for each. The quintessential example is
|
||||||
@ -19,9 +19,9 @@ dryer and the first is being folded. In this way, one can make progress on
|
|||||||
three loads of laundry simultaneously. Given infinite loads, the pipeline will
|
three loads of laundry simultaneously. Given infinite loads, the pipeline will
|
||||||
consistently complete a load at the rate of the slowest stage in the pipeline.
|
consistently complete a load at the rate of the slowest stage in the pipeline.
|
||||||
|
|
||||||
## Pipelining in the Fullnode
|
## Pipelining in the Validator
|
||||||
|
|
||||||
The fullnode contains two pipelined processes, one used in leader mode called
|
The validator contains two pipelined processes, one used in leader mode called
|
||||||
the TPU and one used in validator mode called the TVU. In both cases, the
|
the TPU and one used in validator mode called the TVU. In both cases, the
|
||||||
hardware being pipelined is the same, the network input, the GPU cards, the CPU
|
hardware being pipelined is the same, the network input, the GPU cards, the CPU
|
||||||
cores, writes to disk, and the network output. What it does with that hardware
|
cores, writes to disk, and the network output. What it does with that hardware
|
@ -12,8 +12,12 @@ if [[ -d target/perf-libs ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set -x
|
(
|
||||||
git clone git@github.com:solana-labs/solana-perf-libs.git target/perf-libs
|
set -x
|
||||||
cd target/perf-libs
|
git clone git@github.com:solana-labs/solana-perf-libs.git target/perf-libs
|
||||||
make -j"$(nproc)"
|
cd target/perf-libs
|
||||||
make DESTDIR=. install
|
make -j"$(nproc)"
|
||||||
|
make DESTDIR=. install
|
||||||
|
)
|
||||||
|
|
||||||
|
./fetch-perf-libs.sh
|
||||||
|
20
ci/audit.sh
20
ci/audit.sh
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# Audits project dependencies for security vulnerabilities
|
|
||||||
#
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
source ci/_
|
|
||||||
|
|
||||||
cargo_install_unless() {
|
|
||||||
declare crate=$1
|
|
||||||
shift
|
|
||||||
|
|
||||||
"$@" > /dev/null 2>&1 || \
|
|
||||||
_ cargo install "$crate"
|
|
||||||
}
|
|
||||||
|
|
||||||
cargo_install_unless cargo-audit cargo audit --version
|
|
||||||
|
|
||||||
_ cargo audit
|
|
@ -14,10 +14,10 @@ steps:
|
|||||||
- "queue=cuda"
|
- "queue=cuda"
|
||||||
- command: "ci/test-bench.sh"
|
- command: "ci/test-bench.sh"
|
||||||
name: "bench"
|
name: "bench"
|
||||||
timeout_in_minutes: 20
|
timeout_in_minutes: 60
|
||||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-stable.sh"
|
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-stable.sh"
|
||||||
name: "stable"
|
name: "stable"
|
||||||
timeout_in_minutes: 25
|
timeout_in_minutes: 40
|
||||||
artifact_paths: "log-*.txt"
|
artifact_paths: "log-*.txt"
|
||||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_nightly_docker_image ci/test-coverage.sh"
|
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_nightly_docker_image ci/test-coverage.sh"
|
||||||
name: "coverage"
|
name: "coverage"
|
||||||
|
@ -4,9 +4,8 @@ ARG date
|
|||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& rustup install nightly-$date \
|
&& rustup install nightly-$date \
|
||||||
&& rustup show \
|
&& rustup show \
|
||||||
&& rustup show \
|
|
||||||
&& rustc --version \
|
&& rustc --version \
|
||||||
&& cargo --version \
|
&& cargo --version \
|
||||||
|
&& cargo install grcov \
|
||||||
&& rustc +nightly-$date --version \
|
&& rustc +nightly-$date --version \
|
||||||
&& cargo +nightly-$date --version
|
&& cargo +nightly-$date --version
|
||||||
|
|
||||||
|
@ -21,9 +21,11 @@ RUN set -x \
|
|||||||
rsync \
|
rsync \
|
||||||
sudo \
|
sudo \
|
||||||
\
|
\
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& rustup component add rustfmt \
|
&& rustup component add rustfmt \
|
||||||
&& rustup component add clippy \
|
&& rustup component add clippy \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& cargo install cargo-audit \
|
||||||
|
&& cargo install svgbob_cli \
|
||||||
|
&& cargo install mdbook \
|
||||||
&& rustc --version \
|
&& rustc --version \
|
||||||
&& cargo --version
|
&& cargo --version
|
||||||
|
|
||||||
|
@ -9,3 +9,4 @@ read -r rustc version _ < <(docker run solanalabs/rust rustc --version)
|
|||||||
[[ $rustc = rustc ]]
|
[[ $rustc = rustc ]]
|
||||||
docker tag solanalabs/rust:latest solanalabs/rust:"$version"
|
docker tag solanalabs/rust:latest solanalabs/rust:"$version"
|
||||||
docker push solanalabs/rust:"$version"
|
docker push solanalabs/rust:"$version"
|
||||||
|
docker push solanalabs/rust:latest
|
||||||
|
@ -25,8 +25,7 @@ fi
|
|||||||
build() {
|
build() {
|
||||||
$genPipeline && return
|
$genPipeline && return
|
||||||
source ci/rust-version.sh stable
|
source ci/rust-version.sh stable
|
||||||
|
source scripts/ulimit-n.sh
|
||||||
_ scripts/ulimit-n.sh
|
|
||||||
_ cargo +$rust_stable build --all
|
_ cargo +$rust_stable build --all
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,20 +51,11 @@ runTest() {
|
|||||||
|
|
||||||
build
|
build
|
||||||
|
|
||||||
runTest "Leader rotation off" \
|
runTest "basic" \
|
||||||
"ci/localnet-sanity.sh -i 128 -b"
|
|
||||||
|
|
||||||
runTest "Leader rotation off, restart" \
|
|
||||||
"ci/localnet-sanity.sh -i 128 -k 16 -b"
|
|
||||||
|
|
||||||
runTest "Leader rotation off, incremental restart, extra node" \
|
|
||||||
"ci/localnet-sanity.sh -i 128 -k 16 -R -x -b"
|
|
||||||
|
|
||||||
runTest "Leader rotation on" \
|
|
||||||
"ci/localnet-sanity.sh -i 128"
|
"ci/localnet-sanity.sh -i 128"
|
||||||
|
|
||||||
runTest "Leader rotation on, restart" \
|
runTest "restart" \
|
||||||
"ci/localnet-sanity.sh -i 128 -k 16"
|
"ci/localnet-sanity.sh -i 128 -k 16"
|
||||||
|
|
||||||
runTest "Leader rotation on, incremental restart, extra node" \
|
runTest "incremental restart, extra node" \
|
||||||
"ci/localnet-sanity.sh -i 128 -k 16 -R -x"
|
"ci/localnet-sanity.sh -i 128 -k 16 -R -x"
|
||||||
|
@ -7,7 +7,7 @@ restartInterval=never
|
|||||||
rollingRestart=false
|
rollingRestart=false
|
||||||
maybeNoLeaderRotation=
|
maybeNoLeaderRotation=
|
||||||
extraNodes=0
|
extraNodes=0
|
||||||
walletRpcEndpoint=
|
walletRpcPort=:8899
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
exitcode=0
|
exitcode=0
|
||||||
@ -27,7 +27,7 @@ Start a local cluster and run sanity on it
|
|||||||
nodes (at the cadence specified by -k). When disabled all
|
nodes (at the cadence specified by -k). When disabled all
|
||||||
nodes will be first killed then restarted (default: $rollingRestart)
|
nodes will be first killed then restarted (default: $rollingRestart)
|
||||||
-b - Disable leader rotation
|
-b - Disable leader rotation
|
||||||
-x - Add an extra fullnode (may be supplied multiple times)
|
-x - Add an extra validator (may be supplied multiple times)
|
||||||
-r - Select the RPC endpoint hosted by a node that starts as
|
-r - Select the RPC endpoint hosted by a node that starts as
|
||||||
a validator node. If unspecified the RPC endpoint hosted by
|
a validator node. If unspecified the RPC endpoint hosted by
|
||||||
the bootstrap leader will be used.
|
the bootstrap leader will be used.
|
||||||
@ -55,13 +55,13 @@ while getopts "ch?i:k:brxR" opt; do
|
|||||||
restartInterval=$OPTARG
|
restartInterval=$OPTARG
|
||||||
;;
|
;;
|
||||||
b)
|
b)
|
||||||
maybeNoLeaderRotation="--only-bootstrap-stake"
|
maybeNoLeaderRotation="--stake 0"
|
||||||
;;
|
;;
|
||||||
x)
|
x)
|
||||||
extraNodes=$((extraNodes + 1))
|
extraNodes=$((extraNodes + 1))
|
||||||
;;
|
;;
|
||||||
r)
|
r)
|
||||||
walletRpcEndpoint="--rpc-port 18899"
|
walletRpcPort=":18899"
|
||||||
;;
|
;;
|
||||||
R)
|
R)
|
||||||
rollingRestart=true
|
rollingRestart=true
|
||||||
@ -78,20 +78,22 @@ source scripts/configure-metrics.sh
|
|||||||
nodes=(
|
nodes=(
|
||||||
"multinode-demo/drone.sh"
|
"multinode-demo/drone.sh"
|
||||||
"multinode-demo/bootstrap-leader.sh \
|
"multinode-demo/bootstrap-leader.sh \
|
||||||
$maybeNoLeaderRotation \
|
|
||||||
--enable-rpc-exit \
|
--enable-rpc-exit \
|
||||||
|
--no-restart \
|
||||||
--init-complete-file init-complete-node1.log"
|
--init-complete-file init-complete-node1.log"
|
||||||
"multinode-demo/fullnode.sh \
|
"multinode-demo/validator.sh \
|
||||||
$maybeNoLeaderRotation \
|
$maybeNoLeaderRotation \
|
||||||
--enable-rpc-exit \
|
--enable-rpc-exit \
|
||||||
|
--no-restart \
|
||||||
--init-complete-file init-complete-node2.log \
|
--init-complete-file init-complete-node2.log \
|
||||||
--rpc-port 18899"
|
--rpc-port 18899"
|
||||||
)
|
)
|
||||||
|
|
||||||
for i in $(seq 1 $extraNodes); do
|
for i in $(seq 1 $extraNodes); do
|
||||||
nodes+=(
|
nodes+=(
|
||||||
"multinode-demo/fullnode.sh \
|
"multinode-demo/validator.sh \
|
||||||
-X dyn$i \
|
--no-restart \
|
||||||
|
--label dyn$i \
|
||||||
--init-complete-file init-complete-node$((2 + i)).log \
|
--init-complete-file init-complete-node$((2 + i)).log \
|
||||||
$maybeNoLeaderRotation"
|
$maybeNoLeaderRotation"
|
||||||
)
|
)
|
||||||
@ -167,13 +169,11 @@ startNodes() {
|
|||||||
|
|
||||||
killNode() {
|
killNode() {
|
||||||
declare pid=$1
|
declare pid=$1
|
||||||
echo "kill $pid"
|
|
||||||
set +e
|
set +e
|
||||||
if kill "$pid"; then
|
if kill "$pid"; then
|
||||||
|
echo "Waiting for $pid to exit..."
|
||||||
wait "$pid"
|
wait "$pid"
|
||||||
else
|
echo "$pid exited with $?"
|
||||||
echo "^^^ +++"
|
|
||||||
echo "Warning: unable to kill $pid"
|
|
||||||
fi
|
fi
|
||||||
set -e
|
set -e
|
||||||
}
|
}
|
||||||
@ -197,10 +197,11 @@ killNodes() {
|
|||||||
# Give the nodes a splash of time to cleanly exit before killing them
|
# Give the nodes a splash of time to cleanly exit before killing them
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
echo "--- Killing nodes"
|
echo "--- Killing nodes: ${pids[*]}"
|
||||||
for pid in "${pids[@]}"; do
|
for pid in "${pids[@]}"; do
|
||||||
killNode "$pid"
|
killNode "$pid"
|
||||||
done
|
done
|
||||||
|
echo "done killing nodes"
|
||||||
pids=()
|
pids=()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,7 +256,7 @@ rollingNodeRestart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
verifyLedger() {
|
verifyLedger() {
|
||||||
for ledger in bootstrap-leader fullnode; do
|
for ledger in bootstrap-leader validator; do
|
||||||
echo "--- $ledger ledger verification"
|
echo "--- $ledger ledger verification"
|
||||||
(
|
(
|
||||||
source multinode-demo/common.sh
|
source multinode-demo/common.sh
|
||||||
@ -293,7 +294,7 @@ flag_error() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ! $skipSetup; then
|
if ! $skipSetup; then
|
||||||
multinode-demo/setup.sh
|
multinode-demo/setup.sh --hashes-per-tick auto
|
||||||
else
|
else
|
||||||
verifyLedger
|
verifyLedger
|
||||||
fi
|
fi
|
||||||
@ -305,11 +306,10 @@ while [[ $iteration -le $iterations ]]; do
|
|||||||
(
|
(
|
||||||
source multinode-demo/common.sh
|
source multinode-demo/common.sh
|
||||||
set -x
|
set -x
|
||||||
client_id=/tmp/client-id.json-$$
|
client_keypair=/tmp/client-id.json-$$
|
||||||
$solana_keygen -o $client_id || exit $?
|
$solana_keygen -o $client_keypair || exit $?
|
||||||
$solana_gossip \
|
$solana_gossip spy --num-nodes-exactly $numNodes || exit $?
|
||||||
--num-nodes-exactly $numNodes || exit $?
|
rm -rf $client_keypair
|
||||||
rm -rf $client_id
|
|
||||||
) || flag_error
|
) || flag_error
|
||||||
|
|
||||||
echo "--- RPC API: bootstrap-leader getTransactionCount ($iteration)"
|
echo "--- RPC API: bootstrap-leader getTransactionCount ($iteration)"
|
||||||
@ -323,7 +323,7 @@ while [[ $iteration -le $iterations ]]; do
|
|||||||
cat log-transactionCount.txt
|
cat log-transactionCount.txt
|
||||||
) || flag_error
|
) || flag_error
|
||||||
|
|
||||||
echo "--- RPC API: fullnode getTransactionCount ($iteration)"
|
echo "--- RPC API: validator getTransactionCount ($iteration)"
|
||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
curl --retry 5 --retry-delay 2 --retry-connrefused \
|
curl --retry 5 --retry-delay 2 --retry-connrefused \
|
||||||
@ -364,8 +364,7 @@ while [[ $iteration -le $iterations ]]; do
|
|||||||
}
|
}
|
||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
# shellcheck disable=SC2086 # Don't want to double quote $walletRpcEndpoint
|
timeout 60s scripts/wallet-sanity.sh --url http://127.0.0.1"$walletRpcPort"
|
||||||
timeout 60s scripts/wallet-sanity.sh $walletRpcEndpoint
|
|
||||||
) || flag_error_if_no_leader_rotation
|
) || flag_error_if_no_leader_rotation
|
||||||
|
|
||||||
iteration=$((iteration + 1))
|
iteration=$((iteration + 1))
|
||||||
|
33
ci/nits.sh
33
ci/nits.sh
@ -13,19 +13,24 @@ declare prints=(
|
|||||||
'println!'
|
'println!'
|
||||||
'eprint!'
|
'eprint!'
|
||||||
'eprintln!'
|
'eprintln!'
|
||||||
|
'dbg!'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parts of the tree that are expected to be print free
|
# Parts of the tree that are expected to be print free
|
||||||
declare print_free_tree=(
|
declare print_free_tree=(
|
||||||
'core/src'
|
'core/src'
|
||||||
'drone'
|
'drone/src'
|
||||||
'metrics'
|
'metrics/src'
|
||||||
'netutil'
|
'netutil/src'
|
||||||
'runtime'
|
'runtime/src'
|
||||||
'sdk'
|
'sdk/src'
|
||||||
|
'programs/vote_api/src'
|
||||||
|
'programs/vote_program/src'
|
||||||
|
'programs/stake_api/src'
|
||||||
|
'programs/stake_program/src'
|
||||||
)
|
)
|
||||||
|
|
||||||
if _ git grep "${prints[@]/#/-e }" -- "${print_free_tree[@]}"; then
|
if _ git --no-pager grep -n --max-depth=0 "${prints[@]/#/-e }" -- "${print_free_tree[@]}"; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -34,7 +39,21 @@ fi
|
|||||||
# Default::default()
|
# Default::default()
|
||||||
#
|
#
|
||||||
# Ref: https://github.com/solana-labs/solana/issues/2630
|
# Ref: https://github.com/solana-labs/solana/issues/2630
|
||||||
if _ git grep 'Default::default()' -- '*.rs'; then
|
if _ git --no-pager grep -n 'Default::default()' -- '*.rs'; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Let's keep a .gitignore for every crate, ensure it's got
|
||||||
|
# /target/ in it
|
||||||
|
declare gitignores_ok=true
|
||||||
|
for i in $(git --no-pager ls-files \*/Cargo.toml ); do
|
||||||
|
dir=$(dirname "$i")
|
||||||
|
if [[ ! -f $dir/.gitignore ]]; then
|
||||||
|
echo 'error: nits.sh .gitnore missing for crate '"$dir" >&2
|
||||||
|
gitignores_ok=false
|
||||||
|
elif ! grep -q -e '^/target/$' "$dir"/.gitignore; then
|
||||||
|
echo 'error: nits.sh "/target/" apparently missing from '"$dir"'/.gitignore' >&2
|
||||||
|
gitignores_ok=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
"$gitignores_ok"
|
||||||
|
@ -19,12 +19,13 @@ CRATES=(
|
|||||||
metrics
|
metrics
|
||||||
client
|
client
|
||||||
drone
|
drone
|
||||||
programs/{budget_api,config_api,storage_api,token_api,vote_api}
|
programs/{budget_api,config_api,stake_api,storage_api,token_api,vote_api,exchange_api}
|
||||||
|
programs/{vote_program,budget_program,bpf_loader,config_program,exchange_program,failure_program}
|
||||||
|
programs/{noop_program,stake_program,storage_program,token_program}
|
||||||
runtime
|
runtime
|
||||||
programs/{budget,bpf_loader,config,vote,storage,token,vote}
|
|
||||||
vote-signer
|
vote-signer
|
||||||
core
|
core
|
||||||
fullnode
|
validator
|
||||||
genesis
|
genesis
|
||||||
gossip
|
gossip
|
||||||
ledger-tool
|
ledger-tool
|
||||||
@ -32,7 +33,6 @@ CRATES=(
|
|||||||
install
|
install
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Only package/publish if this is a tagged release
|
# Only package/publish if this is a tagged release
|
||||||
[[ -n $TRIGGERED_BUILDKITE_TAG ]] || {
|
[[ -n $TRIGGERED_BUILDKITE_TAG ]] || {
|
||||||
echo TRIGGERED_BUILDKITE_TAG unset, skipped
|
echo TRIGGERED_BUILDKITE_TAG unset, skipped
|
||||||
@ -55,7 +55,7 @@ for crate in "${CRATES[@]}"; do
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "-- $crate"
|
echo "-- $crate"
|
||||||
grep -q "^version = \"$expectedCrateVersion\"$" Cargo.toml || {
|
grep -q "^version = \"$expectedCrateVersion\"$" "$crate"/Cargo.toml || {
|
||||||
echo "Error: $crate/Cargo.toml version is not $expectedCrateVersion"
|
echo "Error: $crate/Cargo.toml version is not $expectedCrateVersion"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ echo --- Creating tarball
|
|||||||
COMMIT="$(git rev-parse HEAD)"
|
COMMIT="$(git rev-parse HEAD)"
|
||||||
|
|
||||||
(
|
(
|
||||||
echo "channel: $CHANNEL"
|
echo "channel: $CHANNEL_OR_TAG"
|
||||||
echo "commit: $COMMIT"
|
echo "commit: $COMMIT"
|
||||||
echo "target: $TARGET"
|
echo "target: $TARGET"
|
||||||
) > solana-release/version.yml
|
) > solana-release/version.yml
|
||||||
@ -56,37 +56,41 @@ echo --- Creating tarball
|
|||||||
source ci/rust-version.sh stable
|
source ci/rust-version.sh stable
|
||||||
scripts/cargo-install-all.sh +"$rust_stable" solana-release
|
scripts/cargo-install-all.sh +"$rust_stable" solana-release
|
||||||
|
|
||||||
|
rm -rf target/perf-libs
|
||||||
./fetch-perf-libs.sh
|
./fetch-perf-libs.sh
|
||||||
|
mkdir solana-release/target
|
||||||
|
cp -a target/perf-libs solana-release/target/
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
source ./target/perf-libs/env.sh
|
source ./target/perf-libs/env.sh
|
||||||
(
|
(
|
||||||
cd fullnode
|
cd validator
|
||||||
cargo install --path . --features=cuda --root ../solana-release-cuda
|
cargo +"$rust_stable" install --path . --features=cuda --root ../solana-release-cuda
|
||||||
)
|
)
|
||||||
cp solana-release-cuda/bin/solana-fullnode solana-release/bin/solana-fullnode-cuda
|
cp solana-release-cuda/bin/solana-validator solana-release/bin/solana-validator-cuda
|
||||||
cp -a scripts multinode-demo solana-release/
|
cp -a scripts multinode-demo solana-release/
|
||||||
|
|
||||||
# Add a wrapper script for fullnode.sh
|
# Add a wrapper script for validator.sh
|
||||||
# TODO: Remove multinode/... from tarball
|
# TODO: Remove multinode/... from tarball
|
||||||
cat > solana-release/bin/fullnode.sh <<'EOF'
|
cat > solana-release/bin/validator.sh <<'EOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")"/..
|
cd "$(dirname "$0")"/..
|
||||||
export USE_INSTALL=1
|
export USE_INSTALL=1
|
||||||
exec multinode-demo/fullnode.sh "$@"
|
exec multinode-demo/validator.sh "$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x solana-release/bin/fullnode.sh
|
chmod +x solana-release/bin/validator.sh
|
||||||
|
|
||||||
# Add a wrapper script for fullnode-x.sh
|
# Add a wrapper script for clear-config.sh
|
||||||
# TODO: Remove multinode/... from tarball
|
# TODO: Remove multinode/... from tarball
|
||||||
cat > solana-release/bin/fullnode-x.sh <<'EOF'
|
cat > solana-release/bin/clear-config.sh <<'EOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")"/..
|
cd "$(dirname "$0")"/..
|
||||||
export USE_INSTALL=1
|
export USE_INSTALL=1
|
||||||
exec multinode-demo/fullnode-x.sh "$@"
|
exec multinode-demo/clear-validator-config.sh "$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x solana-release/bin/fullnode-x.sh
|
chmod +x solana-release/bin/clear-config.sh
|
||||||
|
|
||||||
tar jvcf solana-release-$TARGET.tar.bz2 solana-release/
|
tar jvcf solana-release-$TARGET.tar.bz2 solana-release/
|
||||||
cp solana-release/bin/solana-install solana-install-$TARGET
|
cp solana-release/bin/solana-install solana-install-$TARGET
|
||||||
@ -111,10 +115,10 @@ for file in solana-release-$TARGET.tar.bz2 solana-install-$TARGET; do
|
|||||||
--env AWS_SECRET_ACCESS_KEY \
|
--env AWS_SECRET_ACCESS_KEY \
|
||||||
--volume "$PWD:/solana" \
|
--volume "$PWD:/solana" \
|
||||||
eremite/aws-cli:2018.12.18 \
|
eremite/aws-cli:2018.12.18 \
|
||||||
/usr/bin/s3cmd --acl-public put /solana/"$file" s3://solana-release/"$CHANNEL_OR_TAG"/"$file"
|
/usr/bin/s3cmd --acl-public put /solana/"$file" s3://release.solana.com/"$CHANNEL_OR_TAG"/"$file"
|
||||||
|
|
||||||
echo Published to:
|
echo Published to:
|
||||||
$DRYRUN ci/format-url.sh http://solana-release.s3.amazonaws.com/"$CHANNEL_OR_TAG"/"$file"
|
$DRYRUN ci/format-url.sh http://release.solana.com/"$CHANNEL_OR_TAG"/"$file"
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ -n $TAG ]]; then
|
if [[ -n $TAG ]]; then
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
export rust_stable=1.34.0
|
export rust_stable=1.34.0
|
||||||
export rust_stable_docker_image=solanalabs/rust:1.34.0
|
export rust_stable_docker_image=solanalabs/rust:1.34.0
|
||||||
|
|
||||||
export rust_nightly=nightly-2019-03-14
|
export rust_nightly=nightly-2019-05-01
|
||||||
export rust_nightly_docker_image=solanalabs/rust-nightly:2019-03-14
|
export rust_nightly_docker_image=solanalabs/rust-nightly:2019-05-01
|
||||||
|
|
||||||
[[ -z $1 ]] || (
|
[[ -z $1 ]] || (
|
||||||
|
|
||||||
|
@ -40,7 +40,10 @@ fi
|
|||||||
BENCH_FILE=bench_output.log
|
BENCH_FILE=bench_output.log
|
||||||
BENCH_ARTIFACT=current_bench_results.log
|
BENCH_ARTIFACT=current_bench_results.log
|
||||||
|
|
||||||
# First remove "BENCH_FILE", if it exists so that the following commands can append
|
# Ensure all dependencies are built
|
||||||
|
_ cargo +$rust_nightly build --all --release
|
||||||
|
|
||||||
|
# Remove "BENCH_FILE", if it exists so that the following commands can append
|
||||||
rm -f "$BENCH_FILE"
|
rm -f "$BENCH_FILE"
|
||||||
|
|
||||||
# Run sdk benches
|
# Run sdk benches
|
||||||
@ -59,8 +62,11 @@ _ cargo +$rust_nightly bench --manifest-path core/Cargo.toml ${V:+--verbose} \
|
|||||||
_ cargo +$rust_nightly bench --manifest-path programs/bpf/Cargo.toml ${V:+--verbose} --features=bpf_c \
|
_ cargo +$rust_nightly bench --manifest-path programs/bpf/Cargo.toml ${V:+--verbose} --features=bpf_c \
|
||||||
-- -Z unstable-options --format=json --nocapture | tee -a "$BENCH_FILE"
|
-- -Z unstable-options --format=json --nocapture | tee -a "$BENCH_FILE"
|
||||||
|
|
||||||
|
# TODO: debug why solana-upload-perf takes over 30 minutes to complete.
|
||||||
|
exit 0
|
||||||
|
|
||||||
_ cargo +$rust_nightly run --release --package solana-upload-perf \
|
_ cargo +$rust_nightly run --release --package solana-upload-perf \
|
||||||
-- "$BENCH_FILE" "$TARGET_BRANCH" "$UPLOAD_METRICS" > "$BENCH_ARTIFACT"
|
-- "$BENCH_FILE" "$TARGET_BRANCH" "$UPLOAD_METRICS" | tee "$BENCH_ARTIFACT"
|
||||||
|
|
||||||
|
upload-ci-artifact "$BENCH_FILE"
|
||||||
upload-ci-artifact "$BENCH_ARTIFACT"
|
upload-ci-artifact "$BENCH_ARTIFACT"
|
||||||
|
@ -12,7 +12,7 @@ export RUSTFLAGS="-D warnings"
|
|||||||
_ cargo +"$rust_stable" fmt --all -- --check
|
_ cargo +"$rust_stable" fmt --all -- --check
|
||||||
_ cargo +"$rust_stable" clippy --all -- --version
|
_ cargo +"$rust_stable" clippy --all -- --version
|
||||||
_ cargo +"$rust_stable" clippy --all -- --deny=warnings
|
_ cargo +"$rust_stable" clippy --all -- --deny=warnings
|
||||||
_ ci/audit.sh
|
_ cargo +"$rust_stable" audit
|
||||||
_ ci/nits.sh
|
_ ci/nits.sh
|
||||||
_ book/build.sh
|
_ book/build.sh
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ source ci/rust-version.sh stable
|
|||||||
|
|
||||||
export RUST_BACKTRACE=1
|
export RUST_BACKTRACE=1
|
||||||
|
|
||||||
|
rm -rf target/perf-libs
|
||||||
./fetch-perf-libs.sh
|
./fetch-perf-libs.sh
|
||||||
export LD_LIBRARY_PATH=$PWD/target/perf-libs:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=$PWD/target/perf-libs:$LD_LIBRARY_PATH
|
||||||
|
|
||||||
|
@ -41,13 +41,12 @@ test-stable-perf)
|
|||||||
^sdk/ \
|
^sdk/ \
|
||||||
|| {
|
|| {
|
||||||
annotate --style info \
|
annotate --style info \
|
||||||
"Skipped test-stable-perf as no relavant files were modified"
|
"Skipped test-stable-perf as no relevant files were modified"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# BPF program tests
|
# BPF program tests
|
||||||
_ make -C programs/bpf/c tests
|
_ make -C programs/bpf/c tests
|
||||||
_ programs/bpf/rust/noop/build.sh # Must be built out of band
|
|
||||||
_ cargo +"$rust_stable" test \
|
_ cargo +"$rust_stable" test \
|
||||||
--manifest-path programs/bpf/Cargo.toml \
|
--manifest-path programs/bpf/Cargo.toml \
|
||||||
--no-default-features --features=bpf_c,bpf_rust
|
--no-default-features --features=bpf_c,bpf_rust
|
||||||
@ -62,6 +61,7 @@ test-stable-perf)
|
|||||||
# is not yet loaded.
|
# is not yet loaded.
|
||||||
sudo --non-interactive ./net/scripts/enable-nvidia-persistence-mode.sh
|
sudo --non-interactive ./net/scripts/enable-nvidia-persistence-mode.sh
|
||||||
|
|
||||||
|
rm -rf target/perf-libs
|
||||||
./fetch-perf-libs.sh
|
./fetch-perf-libs.sh
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
source ./target/perf-libs/env.sh
|
source ./target/perf-libs/env.sh
|
||||||
|
@ -14,7 +14,7 @@ source ci/upload-ci-artifact.sh
|
|||||||
[[ -n $ITERATION_WAIT ]] || ITERATION_WAIT=300
|
[[ -n $ITERATION_WAIT ]] || ITERATION_WAIT=300
|
||||||
[[ -n $NUMBER_OF_NODES ]] || NUMBER_OF_NODES="10 25 50 100"
|
[[ -n $NUMBER_OF_NODES ]] || NUMBER_OF_NODES="10 25 50 100"
|
||||||
[[ -n $LEADER_CPU_MACHINE_TYPE ]] ||
|
[[ -n $LEADER_CPU_MACHINE_TYPE ]] ||
|
||||||
LEADER_CPU_MACHINE_TYPE="n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100"
|
LEADER_CPU_MACHINE_TYPE="--machine-type n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100"
|
||||||
[[ -n $CLIENT_COUNT ]] || CLIENT_COUNT=2
|
[[ -n $CLIENT_COUNT ]] || CLIENT_COUNT=2
|
||||||
[[ -n $TESTNET_TAG ]] || TESTNET_TAG=testnet-automation
|
[[ -n $TESTNET_TAG ]] || TESTNET_TAG=testnet-automation
|
||||||
[[ -n $TESTNET_ZONES ]] || TESTNET_ZONES="us-west1-b"
|
[[ -n $TESTNET_ZONES ]] || TESTNET_ZONES="us-west1-b"
|
||||||
@ -52,14 +52,14 @@ launchTestnet() {
|
|||||||
declare q_mean_tps='
|
declare q_mean_tps='
|
||||||
SELECT round(mean("sum_count")) AS "mean_tps" FROM (
|
SELECT round(mean("sum_count")) AS "mean_tps" FROM (
|
||||||
SELECT sum("count") AS "sum_count"
|
SELECT sum("count") AS "sum_count"
|
||||||
FROM "testnet-automation"."autogen"."counter-bank-process_transactions-txs"
|
FROM "testnet-automation"."autogen"."banking_stage-record_transactions"
|
||||||
WHERE time > now() - 300s GROUP BY time(1s)
|
WHERE time > now() - 300s GROUP BY time(1s)
|
||||||
)'
|
)'
|
||||||
|
|
||||||
declare q_max_tps='
|
declare q_max_tps='
|
||||||
SELECT round(max("sum_count")) AS "max_tps" FROM (
|
SELECT round(max("sum_count")) AS "max_tps" FROM (
|
||||||
SELECT sum("count") AS "sum_count"
|
SELECT sum("count") AS "sum_count"
|
||||||
FROM "testnet-automation"."autogen"."counter-bank-process_transactions-txs"
|
FROM "testnet-automation"."autogen"."banking_stage-record_transactions"
|
||||||
WHERE time > now() - 300s GROUP BY time(1s)
|
WHERE time > now() - 300s GROUP BY time(1s)
|
||||||
)'
|
)'
|
||||||
|
|
||||||
|
@ -10,15 +10,20 @@ bootstrapFullNodeMachineType=
|
|||||||
clientNodeCount=0
|
clientNodeCount=0
|
||||||
additionalFullNodeCount=10
|
additionalFullNodeCount=10
|
||||||
publicNetwork=false
|
publicNetwork=false
|
||||||
skipSetup=false
|
stopNetwork=false
|
||||||
|
reuseLedger=false
|
||||||
|
skipCreate=false
|
||||||
skipStart=false
|
skipStart=false
|
||||||
externalNode=false
|
externalNode=false
|
||||||
|
failOnValidatorBootupFailure=true
|
||||||
tarChannelOrTag=edge
|
tarChannelOrTag=edge
|
||||||
delete=false
|
delete=false
|
||||||
enableGpu=false
|
enableGpu=false
|
||||||
bootDiskType=""
|
bootDiskType=""
|
||||||
leaderRotation=true
|
|
||||||
blockstreamer=false
|
blockstreamer=false
|
||||||
|
deployUpdateManifest=true
|
||||||
|
fetchLogs=true
|
||||||
|
maybeHashesPerTick=
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
exitcode=0
|
exitcode=0
|
||||||
@ -45,14 +50,23 @@ Deploys a CD testnet
|
|||||||
-c [number] - Number of client bencher nodes (default: $clientNodeCount)
|
-c [number] - Number of client bencher nodes (default: $clientNodeCount)
|
||||||
-u - Include a Blockstreamer (default: $blockstreamer)
|
-u - Include a Blockstreamer (default: $blockstreamer)
|
||||||
-P - Use public network IP addresses (default: $publicNetwork)
|
-P - Use public network IP addresses (default: $publicNetwork)
|
||||||
-G - Enable GPU, and set count/type of GPUs to use (e.g n1-standard-16 --accelerator count=4,type=nvidia-tesla-k80)
|
-G - Enable GPU, and set count/type of GPUs to use (e.g n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100)
|
||||||
-g - Enable GPU (default: $enableGpu)
|
-g - Enable GPU (default: $enableGpu)
|
||||||
-b - Disable leader rotation
|
|
||||||
-a [address] - Set the bootstrap fullnode's external IP address to this GCE address
|
-a [address] - Set the bootstrap fullnode's external IP address to this GCE address
|
||||||
-d [disk-type] - Specify a boot disk type (default None) Use pd-ssd to get ssd on GCE.
|
-d [disk-type] - Specify a boot disk type (default None) Use pd-ssd to get ssd on GCE.
|
||||||
-D - Delete the network
|
-D - Delete the network
|
||||||
-r - Reuse existing node/ledger configuration from a
|
-r - Reuse existing node/ledger configuration from a
|
||||||
previous |start| (ie, don't run ./multinode-demo/setup.sh).
|
previous |start| (ie, don't run ./multinode-demo/setup.sh).
|
||||||
|
-x - External node. Default: false
|
||||||
|
-e - Skip create. Assume the nodes have already been created
|
||||||
|
-s - Skip start. Nodes will still be created or configured, but network software will not be started.
|
||||||
|
-S - Stop network software without tearing down nodes.
|
||||||
|
-f - Discard validator nodes that didn't bootup successfully
|
||||||
|
-w - Skip time-consuming "bells and whistles" that are
|
||||||
|
unnecessary for a high-node count demo testnet
|
||||||
|
|
||||||
|
--hashes-per-tick NUM_HASHES|sleep|auto
|
||||||
|
- Override the default --hashes-per-tick for the cluster
|
||||||
|
|
||||||
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
||||||
metrics
|
metrics
|
||||||
@ -62,7 +76,22 @@ EOF
|
|||||||
|
|
||||||
zone=()
|
zone=()
|
||||||
|
|
||||||
while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:" opt; do
|
shortArgs=()
|
||||||
|
while [[ -n $1 ]]; do
|
||||||
|
if [[ ${1:0:2} = -- ]]; then
|
||||||
|
if [[ $1 = --hashes-per-tick ]]; then
|
||||||
|
maybeHashesPerTick="$1 $2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
usage "Unknown long option: $1"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
shortArgs+=("$1")
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
while getopts "h?p:Pn:c:t:gG:a:Dd:rusxz:p:C:Sfew" opt "${shortArgs[@]}"; do
|
||||||
case $opt in
|
case $opt in
|
||||||
h | \?)
|
h | \?)
|
||||||
usage
|
usage
|
||||||
@ -95,9 +124,6 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:" opt; do
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
b)
|
|
||||||
leaderRotation=false
|
|
||||||
;;
|
|
||||||
g)
|
g)
|
||||||
enableGpu=true
|
enableGpu=true
|
||||||
;;
|
;;
|
||||||
@ -115,7 +141,10 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:" opt; do
|
|||||||
delete=true
|
delete=true
|
||||||
;;
|
;;
|
||||||
r)
|
r)
|
||||||
skipSetup=true
|
reuseLedger=true
|
||||||
|
;;
|
||||||
|
e)
|
||||||
|
skipCreate=true
|
||||||
;;
|
;;
|
||||||
s)
|
s)
|
||||||
skipStart=true
|
skipStart=true
|
||||||
@ -123,11 +152,21 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:" opt; do
|
|||||||
x)
|
x)
|
||||||
externalNode=true
|
externalNode=true
|
||||||
;;
|
;;
|
||||||
|
f)
|
||||||
|
failOnValidatorBootupFailure=false
|
||||||
|
;;
|
||||||
u)
|
u)
|
||||||
blockstreamer=true
|
blockstreamer=true
|
||||||
;;
|
;;
|
||||||
|
S)
|
||||||
|
stopNetwork=true
|
||||||
|
;;
|
||||||
|
w)
|
||||||
|
fetchLogs=false
|
||||||
|
deployUpdateManifest=false
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
usage "Error: unhandled option: $opt"
|
usage "Unknown option: $opt"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@ -162,7 +201,16 @@ for val in "${zone[@]}"; do
|
|||||||
zone_args+=("-z $val")
|
zone_args+=("-z $val")
|
||||||
done
|
done
|
||||||
|
|
||||||
if ! $skipSetup; then
|
if $stopNetwork; then
|
||||||
|
skipCreate=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $delete; then
|
||||||
|
skipCreate=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the network
|
||||||
|
if ! $skipCreate; then
|
||||||
echo "--- $cloudProvider.sh delete"
|
echo "--- $cloudProvider.sh delete"
|
||||||
# shellcheck disable=SC2068
|
# shellcheck disable=SC2068
|
||||||
time net/"$cloudProvider".sh delete ${zone_args[@]} -p "$netName" ${externalNode:+-x}
|
time net/"$cloudProvider".sh delete ${zone_args[@]} -p "$netName" ${externalNode:+-x}
|
||||||
@ -196,10 +244,6 @@ if ! $skipSetup; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! $leaderRotation; then
|
|
||||||
create_args+=(-b)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $publicNetwork; then
|
if $publicNetwork; then
|
||||||
create_args+=(-P)
|
create_args+=(-P)
|
||||||
fi
|
fi
|
||||||
@ -208,6 +252,10 @@ if ! $skipSetup; then
|
|||||||
create_args+=(-x)
|
create_args+=(-x)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! $failOnValidatorBootupFailure; then
|
||||||
|
create_args+=(-f)
|
||||||
|
fi
|
||||||
|
|
||||||
time net/"$cloudProvider".sh create "${create_args[@]}"
|
time net/"$cloudProvider".sh create "${create_args[@]}"
|
||||||
else
|
else
|
||||||
echo "--- $cloudProvider.sh config"
|
echo "--- $cloudProvider.sh config"
|
||||||
@ -220,6 +268,14 @@ else
|
|||||||
config_args+=(-P)
|
config_args+=(-P)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if $externalNode; then
|
||||||
|
config_args+=(-x)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $failOnValidatorBootupFailure; then
|
||||||
|
config_args+=(-f)
|
||||||
|
fi
|
||||||
|
|
||||||
time net/"$cloudProvider".sh config "${config_args[@]}"
|
time net/"$cloudProvider".sh config "${config_args[@]}"
|
||||||
fi
|
fi
|
||||||
net/init-metrics.sh -e
|
net/init-metrics.sh -e
|
||||||
@ -227,53 +283,60 @@ net/init-metrics.sh -e
|
|||||||
echo "+++ $cloudProvider.sh info"
|
echo "+++ $cloudProvider.sh info"
|
||||||
net/"$cloudProvider".sh info
|
net/"$cloudProvider".sh info
|
||||||
|
|
||||||
echo --- net.sh start
|
if $stopNetwork; then
|
||||||
maybeRejectExtraNodes=
|
echo --- net.sh stop
|
||||||
if ! $publicNetwork; then
|
time net/net.sh stop
|
||||||
maybeRejectExtraNodes="-o rejectExtraNodes"
|
exit 0
|
||||||
fi
|
|
||||||
maybeNoValidatorSanity=
|
|
||||||
if [[ -n $NO_VALIDATOR_SANITY ]]; then
|
|
||||||
maybeNoValidatorSanity="-o noValidatorSanity"
|
|
||||||
fi
|
|
||||||
maybeNoLedgerVerify=
|
|
||||||
if [[ -n $NO_LEDGER_VERIFY ]]; then
|
|
||||||
maybeNoLedgerVerify="-o noLedgerVerify"
|
|
||||||
fi
|
|
||||||
|
|
||||||
maybeSkipSetup=
|
|
||||||
if $skipSetup; then
|
|
||||||
maybeSkipSetup="-r"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ok=true
|
ok=true
|
||||||
if ! $skipStart; then
|
if ! $skipStart; then
|
||||||
(
|
(
|
||||||
if $skipSetup; then
|
if $skipCreate; then
|
||||||
# TODO: Enable rolling updates
|
# TODO: Enable rolling updates
|
||||||
#op=update
|
#op=update
|
||||||
op=restart
|
op=restart
|
||||||
else
|
else
|
||||||
op=start
|
op=start
|
||||||
fi
|
fi
|
||||||
|
echo "--- net.sh $op"
|
||||||
|
args=("$op" -t "$tarChannelOrTag")
|
||||||
|
|
||||||
|
if ! $publicNetwork; then
|
||||||
|
args+=(-o rejectExtraNodes)
|
||||||
|
fi
|
||||||
|
if [[ -n $NO_VALIDATOR_SANITY ]]; then
|
||||||
|
args+=(-o noValidatorSanity)
|
||||||
|
fi
|
||||||
|
if [[ -n $NO_LEDGER_VERIFY ]]; then
|
||||||
|
args+=(-o noLedgerVerify)
|
||||||
|
fi
|
||||||
|
if [[ -n $maybeHashesPerTick ]]; then
|
||||||
|
# shellcheck disable=SC2206 # Do not want to quote $maybeHashesPerTick
|
||||||
|
args+=($maybeHashesPerTick)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $reuseLedger; then
|
||||||
|
args+=(-r)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $failOnValidatorBootupFailure; then
|
||||||
|
args+=(-F)
|
||||||
|
fi
|
||||||
|
|
||||||
maybeUpdateManifestKeypairFile=
|
|
||||||
# shellcheck disable=SC2154 # SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu comes from .buildkite/env/
|
# shellcheck disable=SC2154 # SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu comes from .buildkite/env/
|
||||||
if [[ -n $SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu ]]; then
|
if $deployUpdateManifest && [[ -n $SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu ]]; then
|
||||||
echo "$SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu" > update_manifest_keypair.json
|
echo "$SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu" > update_manifest_keypair.json
|
||||||
maybeUpdateManifestKeypairFile="-i update_manifest_keypair.json"
|
args+=(-i update_manifest_keypair.json)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# shellcheck disable=SC2086 # Don't want to double quote the $maybeXYZ variables
|
# shellcheck disable=SC2086 # Don't want to double quote the $maybeXYZ variables
|
||||||
time net/net.sh $op -t "$tarChannelOrTag" \
|
time net/net.sh "${args[@]}"
|
||||||
$maybeUpdateManifestKeypairFile \
|
|
||||||
$maybeSkipSetup \
|
|
||||||
$maybeRejectExtraNodes \
|
|
||||||
$maybeNoValidatorSanity \
|
|
||||||
$maybeNoLedgerVerify
|
|
||||||
) || ok=false
|
) || ok=false
|
||||||
|
|
||||||
|
if $fetchLogs; then
|
||||||
net/net.sh logs
|
net/net.sh logs
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
$ok
|
$ok
|
||||||
|
@ -42,20 +42,32 @@ steps:
|
|||||||
value: "testnet-beta"
|
value: "testnet-beta"
|
||||||
- label: "testnet-beta-perf"
|
- label: "testnet-beta-perf"
|
||||||
value: "testnet-beta-perf"
|
value: "testnet-beta-perf"
|
||||||
|
- label: "testnet-demo"
|
||||||
|
value: "testnet-demo"
|
||||||
- select: "Operation"
|
- select: "Operation"
|
||||||
key: "testnet-operation"
|
key: "testnet-operation"
|
||||||
default: "sanity-or-restart"
|
default: "sanity-or-restart"
|
||||||
options:
|
options:
|
||||||
- label: "Sanity check. Restart network on failure"
|
- label: "Create testnet and then start software. If the testnet already exists it will be deleted and re-created"
|
||||||
value: "sanity-or-restart"
|
value: "create-and-start"
|
||||||
- label: "Start (or restart) the network"
|
- label: "Create testnet, but do not start software. If the testnet already exists it will be deleted and re-created"
|
||||||
|
value: "create"
|
||||||
|
- label: "Start network software on an existing testnet. If software is already running it will be restarted"
|
||||||
value: "start"
|
value: "start"
|
||||||
- label: "Update the network software. Restart network on failure"
|
- label: "Stop network software without deleting testnet nodes"
|
||||||
value: "update-or-restart"
|
|
||||||
- label: "Stop the network"
|
|
||||||
value: "stop"
|
value: "stop"
|
||||||
|
- label: "Update the network software. Restart network software on failure"
|
||||||
|
value: "update-or-restart"
|
||||||
|
- label: "Sanity check. Restart network software on failure"
|
||||||
|
value: "sanity-or-restart"
|
||||||
- label: "Sanity check only"
|
- label: "Sanity check only"
|
||||||
value: "sanity"
|
value: "sanity"
|
||||||
|
- label: "Delete the testnet"
|
||||||
|
value: "delete"
|
||||||
|
- label: "Enable/unlock the testnet"
|
||||||
|
value: "enable"
|
||||||
|
- label: "Delete and then lock the testnet from further operation until it is re-enabled"
|
||||||
|
value: "disable"
|
||||||
- command: "ci/$(basename "$0")"
|
- command: "ci/$(basename "$0")"
|
||||||
agents:
|
agents:
|
||||||
- "queue=$BUILDKITE_AGENT_META_DATA_QUEUE"
|
- "queue=$BUILDKITE_AGENT_META_DATA_QUEUE"
|
||||||
@ -64,6 +76,102 @@ EOF
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ci/channel-info.sh
|
||||||
|
eval "$(ci/channel-info.sh)"
|
||||||
|
|
||||||
|
|
||||||
|
EC2_ZONES=(
|
||||||
|
us-west-1a
|
||||||
|
us-west-2a
|
||||||
|
us-east-1a
|
||||||
|
us-east-2a
|
||||||
|
sa-east-1a
|
||||||
|
eu-west-1a
|
||||||
|
eu-west-2a
|
||||||
|
eu-central-1a
|
||||||
|
ap-northeast-2a
|
||||||
|
ap-southeast-2a
|
||||||
|
ap-south-1a
|
||||||
|
ca-central-1a
|
||||||
|
)
|
||||||
|
|
||||||
|
# GCE zones with _lots_ of quota
|
||||||
|
GCE_ZONES=(
|
||||||
|
us-west1-a
|
||||||
|
us-central1-a
|
||||||
|
us-east1-b
|
||||||
|
europe-west4-a
|
||||||
|
|
||||||
|
us-west1-b
|
||||||
|
us-central1-b
|
||||||
|
us-east1-c
|
||||||
|
europe-west4-b
|
||||||
|
|
||||||
|
us-west1-c
|
||||||
|
us-east1-d
|
||||||
|
europe-west4-c
|
||||||
|
)
|
||||||
|
|
||||||
|
# GCE zones with enough quota for one CPU-only fullnode
|
||||||
|
GCE_LOW_QUOTA_ZONES=(
|
||||||
|
asia-east2-a
|
||||||
|
asia-northeast1-b
|
||||||
|
asia-northeast2-b
|
||||||
|
asia-south1-c
|
||||||
|
asia-southeast1-b
|
||||||
|
australia-southeast1-b
|
||||||
|
europe-north1-a
|
||||||
|
europe-west2-b
|
||||||
|
europe-west3-c
|
||||||
|
europe-west6-a
|
||||||
|
northamerica-northeast1-a
|
||||||
|
southamerica-east1-b
|
||||||
|
)
|
||||||
|
|
||||||
|
case $TESTNET in
|
||||||
|
testnet-edge|testnet-edge-perf)
|
||||||
|
CHANNEL_OR_TAG=edge
|
||||||
|
CHANNEL_BRANCH=$EDGE_CHANNEL
|
||||||
|
;;
|
||||||
|
testnet-beta|testnet-beta-perf)
|
||||||
|
CHANNEL_OR_TAG=beta
|
||||||
|
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||||
|
;;
|
||||||
|
testnet)
|
||||||
|
CHANNEL_OR_TAG=$STABLE_CHANNEL_LATEST_TAG
|
||||||
|
CHANNEL_BRANCH=$STABLE_CHANNEL
|
||||||
|
: "${EC2_NODE_COUNT:=10}"
|
||||||
|
: "${GCE_NODE_COUNT:=}"
|
||||||
|
;;
|
||||||
|
testnet-perf)
|
||||||
|
CHANNEL_OR_TAG=$STABLE_CHANNEL_LATEST_TAG
|
||||||
|
CHANNEL_BRANCH=$STABLE_CHANNEL
|
||||||
|
;;
|
||||||
|
testnet-demo)
|
||||||
|
CHANNEL_OR_TAG=beta
|
||||||
|
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||||
|
: "${GCE_NODE_COUNT:=150}"
|
||||||
|
: "${GCE_LOW_QUOTA_NODE_COUNT:=70}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Invalid TESTNET=$TESTNET"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
EC2_ZONE_ARGS=()
|
||||||
|
for val in "${EC2_ZONES[@]}"; do
|
||||||
|
EC2_ZONE_ARGS+=("-z $val")
|
||||||
|
done
|
||||||
|
GCE_ZONE_ARGS=()
|
||||||
|
for val in "${GCE_ZONES[@]}"; do
|
||||||
|
GCE_ZONE_ARGS+=("-z $val")
|
||||||
|
done
|
||||||
|
GCE_LOW_QUOTA_ZONE_ARGS=()
|
||||||
|
for val in "${GCE_LOW_QUOTA_ZONES[@]}"; do
|
||||||
|
GCE_LOW_QUOTA_ZONE_ARGS+=("-z $val")
|
||||||
|
done
|
||||||
|
|
||||||
if [[ -n $TESTNET_DB_HOST ]]; then
|
if [[ -n $TESTNET_DB_HOST ]]; then
|
||||||
SOLANA_METRICS_PARTIAL_CONFIG="host=$TESTNET_DB_HOST,$SOLANA_METRICS_PARTIAL_CONFIG"
|
SOLANA_METRICS_PARTIAL_CONFIG="host=$TESTNET_DB_HOST,$SOLANA_METRICS_PARTIAL_CONFIG"
|
||||||
fi
|
fi
|
||||||
@ -72,30 +180,9 @@ export SOLANA_METRICS_CONFIG="db=$TESTNET,$SOLANA_METRICS_PARTIAL_CONFIG"
|
|||||||
echo "SOLANA_METRICS_CONFIG: $SOLANA_METRICS_CONFIG"
|
echo "SOLANA_METRICS_CONFIG: $SOLANA_METRICS_CONFIG"
|
||||||
source scripts/configure-metrics.sh
|
source scripts/configure-metrics.sh
|
||||||
|
|
||||||
ci/channel-info.sh
|
|
||||||
eval "$(ci/channel-info.sh)"
|
|
||||||
|
|
||||||
if [[ -n $TESTNET_TAG ]]; then
|
if [[ -n $TESTNET_TAG ]]; then
|
||||||
CHANNEL_OR_TAG=$TESTNET_TAG
|
CHANNEL_OR_TAG=$TESTNET_TAG
|
||||||
else
|
else
|
||||||
case $TESTNET in
|
|
||||||
testnet-edge|testnet-edge-perf)
|
|
||||||
CHANNEL_OR_TAG=edge
|
|
||||||
CHANNEL_BRANCH=$EDGE_CHANNEL
|
|
||||||
;;
|
|
||||||
testnet-beta|testnet-beta-perf)
|
|
||||||
CHANNEL_OR_TAG=beta
|
|
||||||
CHANNEL_BRANCH=$BETA_CHANNEL
|
|
||||||
;;
|
|
||||||
testnet|testnet-perf)
|
|
||||||
CHANNEL_OR_TAG=$STABLE_CHANNEL_LATEST_TAG
|
|
||||||
CHANNEL_BRANCH=$STABLE_CHANNEL
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Error: Invalid TESTNET=$TESTNET"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ $BUILDKITE_BRANCH != "$CHANNEL_BRANCH" ]]; then
|
if [[ $BUILDKITE_BRANCH != "$CHANNEL_BRANCH" ]]; then
|
||||||
(
|
(
|
||||||
@ -112,6 +199,7 @@ steps:
|
|||||||
TESTNET_DB_HOST: "$TESTNET_DB_HOST"
|
TESTNET_DB_HOST: "$TESTNET_DB_HOST"
|
||||||
EC2_NODE_COUNT: "$EC2_NODE_COUNT"
|
EC2_NODE_COUNT: "$EC2_NODE_COUNT"
|
||||||
GCE_NODE_COUNT: "$GCE_NODE_COUNT"
|
GCE_NODE_COUNT: "$GCE_NODE_COUNT"
|
||||||
|
GCE_LOW_QUOTA_NODE_COUNT: "$GCE_LOW_QUOTA_NODE_COUNT"
|
||||||
EOF
|
EOF
|
||||||
) | buildkite-agent pipeline upload
|
) | buildkite-agent pipeline upload
|
||||||
exit 0
|
exit 0
|
||||||
@ -124,6 +212,7 @@ sanity() {
|
|||||||
testnet-edge)
|
testnet-edge)
|
||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
|
NO_LEDGER_VERIFY=1 \
|
||||||
ci/testnet-sanity.sh edge-testnet-solana-com ec2 us-west-1a
|
ci/testnet-sanity.sh edge-testnet-solana-com ec2 us-west-1a
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
@ -139,23 +228,8 @@ sanity() {
|
|||||||
testnet-beta)
|
testnet-beta)
|
||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
EC2_ZONES=(us-west-1a sa-east-1a ap-northeast-2a eu-central-1a ca-central-1a)
|
NO_LEDGER_VERIFY=1 \
|
||||||
ok=true
|
ci/testnet-sanity.sh beta-testnet-solana-com ec2 us-west-1a
|
||||||
for zone in "${EC2_ZONES[@]}"; do
|
|
||||||
if ! $ok; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
ci/testnet-sanity.sh beta-testnet-solana-com ec2 "$zone" || ok=false
|
|
||||||
done
|
|
||||||
|
|
||||||
GCE_ZONES=(us-west1-b asia-east2-a europe-west4-a southamerica-east1-b us-east4-c)
|
|
||||||
for zone in "${GCE_ZONES[@]}"; do
|
|
||||||
if ! $ok; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
ci/testnet-sanity.sh beta-testnet-solana-com gce "$zone" || ok=false
|
|
||||||
done
|
|
||||||
$ok
|
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
testnet-beta-perf)
|
testnet-beta-perf)
|
||||||
@ -170,8 +244,19 @@ sanity() {
|
|||||||
testnet)
|
testnet)
|
||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
ci/testnet-sanity.sh testnet-solana-com ec2 us-west-1a
|
|
||||||
#ci/testnet-sanity.sh testnet-solana-com gce us-east1-c
|
ok=true
|
||||||
|
if [[ -n $EC2_NODE_COUNT ]]; then
|
||||||
|
NO_LEDGER_VERIFY=1 \
|
||||||
|
ci/testnet-sanity.sh testnet-solana-com ec2 "${EC2_ZONES[0]}" || ok=false
|
||||||
|
elif [[ -n $GCE_NODE_COUNT ]]; then
|
||||||
|
NO_LEDGER_VERIFY=1 \
|
||||||
|
ci/testnet-sanity.sh testnet-solana-com gce "${GCE_ZONES[0]}" || ok=false
|
||||||
|
else
|
||||||
|
echo "Error: no EC2 or GCE nodes"
|
||||||
|
ok=false
|
||||||
|
fi
|
||||||
|
$ok
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
testnet-perf)
|
testnet-perf)
|
||||||
@ -184,6 +269,22 @@ sanity() {
|
|||||||
#ci/testnet-sanity.sh perf-testnet-solana-com ec2 us-east-1a
|
#ci/testnet-sanity.sh perf-testnet-solana-com ec2 us-east-1a
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
|
testnet-demo)
|
||||||
|
(
|
||||||
|
set -x
|
||||||
|
|
||||||
|
ok=true
|
||||||
|
if [[ -n $GCE_NODE_COUNT ]]; then
|
||||||
|
NO_LEDGER_VERIFY=1 \
|
||||||
|
NO_VALIDATOR_SANITY=1 \
|
||||||
|
ci/testnet-sanity.sh demo-testnet-solana-com gce "${GCE_ZONES[0]}" -f || ok=false
|
||||||
|
else
|
||||||
|
echo "Error: no GCE nodes"
|
||||||
|
ok=false
|
||||||
|
fi
|
||||||
|
$ok
|
||||||
|
)
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Error: Invalid TESTNET=$TESTNET"
|
echo "Error: Invalid TESTNET=$TESTNET"
|
||||||
exit 1
|
exit 1
|
||||||
@ -191,15 +292,27 @@ sanity() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deploy() {
|
||||||
|
declare maybeCreate=$1
|
||||||
|
declare maybeStart=$2
|
||||||
|
declare maybeStop=$3
|
||||||
|
declare maybeDelete=$4
|
||||||
|
|
||||||
start() {
|
echo "--- deploy \"$maybeCreate\" \"$maybeStart\" \"$maybeStop\" \"$maybeDelete\""
|
||||||
declare maybeDelete=$1
|
|
||||||
if [[ -z $maybeDelete ]]; then
|
# Create or recreate the nodes
|
||||||
echo "--- start $TESTNET"
|
if [[ -z $maybeCreate ]]; then
|
||||||
|
skipCreate=skip
|
||||||
else
|
else
|
||||||
echo "--- stop $TESTNET"
|
skipCreate=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start or restart the network software on the nodes
|
||||||
|
if [[ -z $maybeStart ]]; then
|
||||||
|
skipStart=skip
|
||||||
|
else
|
||||||
|
skipStart=""
|
||||||
fi
|
fi
|
||||||
declare maybeReuseLedger=$2
|
|
||||||
|
|
||||||
case $TESTNET in
|
case $TESTNET in
|
||||||
testnet-edge)
|
testnet-edge)
|
||||||
@ -207,8 +320,11 @@ start() {
|
|||||||
set -x
|
set -x
|
||||||
ci/testnet-deploy.sh -p edge-testnet-solana-com -C ec2 -z us-west-1a \
|
ci/testnet-deploy.sh -p edge-testnet-solana-com -C ec2 -z us-west-1a \
|
||||||
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0ccd4f2239886fa94 \
|
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0ccd4f2239886fa94 \
|
||||||
${maybeReuseLedger:+-r} \
|
${skipCreate:+-e} \
|
||||||
${maybeDelete:+-D}
|
${skipStart:+-s} \
|
||||||
|
${maybeStop:+-S} \
|
||||||
|
${maybeDelete:+-D} \
|
||||||
|
--hashes-per-tick auto
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
testnet-edge-perf)
|
testnet-edge-perf)
|
||||||
@ -219,41 +335,24 @@ start() {
|
|||||||
RUST_LOG=solana=warn \
|
RUST_LOG=solana=warn \
|
||||||
ci/testnet-deploy.sh -p edge-perf-testnet-solana-com -C ec2 -z us-west-2b \
|
ci/testnet-deploy.sh -p edge-perf-testnet-solana-com -C ec2 -z us-west-2b \
|
||||||
-g -t "$CHANNEL_OR_TAG" -c 2 \
|
-g -t "$CHANNEL_OR_TAG" -c 2 \
|
||||||
-b \
|
${skipCreate:+-e} \
|
||||||
${maybeReuseLedger:+-r} \
|
${skipStart:+-s} \
|
||||||
${maybeDelete:+-D}
|
${maybeStop:+-S} \
|
||||||
|
${maybeDelete:+-D} \
|
||||||
|
--hashes-per-tick auto
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
testnet-beta)
|
testnet-beta)
|
||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
EC2_ZONES=(us-west-1a sa-east-1a ap-northeast-2a eu-central-1a ca-central-1a)
|
NO_VALIDATOR_SANITY=1 \
|
||||||
GCE_ZONES=(us-west1-b asia-east2-a europe-west4-a southamerica-east1-b us-east4-c)
|
ci/testnet-deploy.sh -p beta-testnet-solana-com -C ec2 -z us-west-1a \
|
||||||
|
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0f286cf8a0771ce35 \
|
||||||
# Build an array to pass as opts to testnet-deploy.sh: "-z zone1 -z zone2 ..."
|
${skipCreate:+-e} \
|
||||||
GCE_ZONE_ARGS=()
|
${skipStart:+-s} \
|
||||||
for val in "${GCE_ZONES[@]}"; do
|
${maybeStop:+-S} \
|
||||||
GCE_ZONE_ARGS+=("-z $val")
|
${maybeDelete:+-D} \
|
||||||
done
|
--hashes-per-tick auto
|
||||||
|
|
||||||
EC2_ZONE_ARGS=()
|
|
||||||
for val in "${EC2_ZONES[@]}"; do
|
|
||||||
EC2_ZONE_ARGS+=("-z $val")
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -n $EC2_NODE_COUNT ]] || EC2_NODE_COUNT=60
|
|
||||||
[[ -n $GCE_NODE_COUNT ]] || GCE_NODE_COUNT=40
|
|
||||||
|
|
||||||
# shellcheck disable=SC2068
|
|
||||||
ci/testnet-deploy.sh -p beta-testnet-solana-com -C ec2 ${EC2_ZONE_ARGS[@]} \
|
|
||||||
-t "$CHANNEL_OR_TAG" -n "$EC2_NODE_COUNT" -c 0 -s -u -P -a eipalloc-0f286cf8a0771ce35 \
|
|
||||||
${maybeReuseLedger:+-r} \
|
|
||||||
${maybeDelete:+-D}
|
|
||||||
# shellcheck disable=SC2068
|
|
||||||
ci/testnet-deploy.sh -p beta-testnet-solana-com -C gce ${GCE_ZONE_ARGS[@]} \
|
|
||||||
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 0 -x -P \
|
|
||||||
${maybeReuseLedger:+-r} \
|
|
||||||
${maybeDelete:+-D}
|
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
testnet-beta-perf)
|
testnet-beta-perf)
|
||||||
@ -264,24 +363,39 @@ start() {
|
|||||||
RUST_LOG=solana=warn \
|
RUST_LOG=solana=warn \
|
||||||
ci/testnet-deploy.sh -p beta-perf-testnet-solana-com -C ec2 -z us-west-2b \
|
ci/testnet-deploy.sh -p beta-perf-testnet-solana-com -C ec2 -z us-west-2b \
|
||||||
-g -t "$CHANNEL_OR_TAG" -c 2 \
|
-g -t "$CHANNEL_OR_TAG" -c 2 \
|
||||||
-b \
|
${skipCreate:+-e} \
|
||||||
${maybeReuseLedger:+-r} \
|
${skipStart:+-s} \
|
||||||
${maybeDelete:+-D}
|
${maybeStop:+-S} \
|
||||||
|
${maybeDelete:+-D} \
|
||||||
|
--hashes-per-tick auto
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
testnet)
|
testnet)
|
||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
NO_VALIDATOR_SANITY=1 \
|
|
||||||
ci/testnet-deploy.sh -p testnet-solana-com -C ec2 -z us-west-1a \
|
if [[ -n $GCE_NODE_COUNT ]] || [[ -n $skipStart ]]; then
|
||||||
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0fa502bf95f6f18b2 \
|
maybeSkipStart="skip"
|
||||||
-b \
|
fi
|
||||||
${maybeReuseLedger:+-r} \
|
|
||||||
|
# shellcheck disable=SC2068
|
||||||
|
ci/testnet-deploy.sh -p testnet-solana-com -C ec2 ${EC2_ZONE_ARGS[@]} \
|
||||||
|
-t "$CHANNEL_OR_TAG" -n "$EC2_NODE_COUNT" -c 0 -u -P -f -a eipalloc-0fa502bf95f6f18b2 \
|
||||||
|
${skipCreate:+-e} \
|
||||||
|
${maybeSkipStart:+-s} \
|
||||||
|
${maybeStop:+-S} \
|
||||||
${maybeDelete:+-D}
|
${maybeDelete:+-D}
|
||||||
#ci/testnet-deploy.sh -p testnet-solana-com -C gce -z us-east1-c \
|
|
||||||
# -t "$CHANNEL_OR_TAG" -n 3 -c 0 -P -a testnet-solana-com \
|
if [[ -n $GCE_NODE_COUNT ]]; then
|
||||||
# ${maybeReuseLedger:+-r} \
|
# shellcheck disable=SC2068
|
||||||
# ${maybeDelete:+-D}
|
ci/testnet-deploy.sh -p testnet-solana-com -C gce ${GCE_ZONE_ARGS[@]} \
|
||||||
|
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 0 -P -f \
|
||||||
|
${skipCreate:+-e} \
|
||||||
|
${skipStart:+-s} \
|
||||||
|
${maybeStop:+-S} \
|
||||||
|
${maybeDelete:+-D} \
|
||||||
|
-x
|
||||||
|
fi
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
testnet-perf)
|
testnet-perf)
|
||||||
@ -291,17 +405,48 @@ start() {
|
|||||||
NO_VALIDATOR_SANITY=1 \
|
NO_VALIDATOR_SANITY=1 \
|
||||||
RUST_LOG=solana=warn \
|
RUST_LOG=solana=warn \
|
||||||
ci/testnet-deploy.sh -p perf-testnet-solana-com -C gce -z us-west1-b \
|
ci/testnet-deploy.sh -p perf-testnet-solana-com -C gce -z us-west1-b \
|
||||||
-G "n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100" \
|
-G "--machine-type n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100" \
|
||||||
-t "$CHANNEL_OR_TAG" -c 2 \
|
-t "$CHANNEL_OR_TAG" -c 2 \
|
||||||
-b \
|
|
||||||
-d pd-ssd \
|
-d pd-ssd \
|
||||||
${maybeReuseLedger:+-r} \
|
${skipCreate:+-e} \
|
||||||
${maybeDelete:+-D}
|
${skipStart:+-s} \
|
||||||
#ci/testnet-deploy.sh -p perf-testnet-solana-com -C ec2 -z us-east-1a \
|
${maybeStop:+-S} \
|
||||||
# -g \
|
${maybeDelete:+-D} \
|
||||||
# -t "$CHANNEL_OR_TAG" -c 2 \
|
--hashes-per-tick auto
|
||||||
# ${maybeReuseLedger:+-r} \
|
)
|
||||||
# ${maybeDelete:+-D}
|
;;
|
||||||
|
testnet-demo)
|
||||||
|
(
|
||||||
|
set -x
|
||||||
|
|
||||||
|
if [[ -n $GCE_LOW_QUOTA_NODE_COUNT ]] || [[ -n $skipStart ]]; then
|
||||||
|
maybeSkipStart="skip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC2068
|
||||||
|
NO_LEDGER_VERIFY=1 \
|
||||||
|
NO_VALIDATOR_SANITY=1 \
|
||||||
|
ci/testnet-deploy.sh -p demo-testnet-solana-com -C gce ${GCE_ZONE_ARGS[@]} \
|
||||||
|
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 0 -P -u -f -w \
|
||||||
|
-a demo-testnet-solana-com \
|
||||||
|
${skipCreate:+-e} \
|
||||||
|
${maybeSkipStart:+-s} \
|
||||||
|
${maybeStop:+-S} \
|
||||||
|
${maybeDelete:+-D} \
|
||||||
|
--hashes-per-tick auto
|
||||||
|
|
||||||
|
if [[ -n $GCE_LOW_QUOTA_NODE_COUNT ]]; then
|
||||||
|
# shellcheck disable=SC2068
|
||||||
|
NO_LEDGER_VERIFY=1 \
|
||||||
|
NO_VALIDATOR_SANITY=1 \
|
||||||
|
ci/testnet-deploy.sh -p demo-testnet-solana-com2 -C gce ${GCE_LOW_QUOTA_ZONE_ARGS[@]} \
|
||||||
|
-t "$CHANNEL_OR_TAG" -n "$GCE_LOW_QUOTA_NODE_COUNT" -c 0 -P -f -x -w \
|
||||||
|
${skipCreate:+-e} \
|
||||||
|
${skipStart:+-s} \
|
||||||
|
${maybeStop:+-S} \
|
||||||
|
${maybeDelete:+-D} \
|
||||||
|
--hashes-per-tick auto
|
||||||
|
fi
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@ -311,30 +456,82 @@ start() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ENABLED_LOCKFILE="${HOME}/${TESTNET}.is_enabled"
|
||||||
|
|
||||||
|
create-and-start() {
|
||||||
|
deploy create start
|
||||||
|
}
|
||||||
|
create() {
|
||||||
|
deploy create
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
deploy "" start
|
||||||
|
}
|
||||||
stop() {
|
stop() {
|
||||||
start delete
|
deploy "" ""
|
||||||
|
}
|
||||||
|
delete() {
|
||||||
|
deploy "" "" "" delete
|
||||||
|
}
|
||||||
|
enable_testnet() {
|
||||||
|
touch "${ENABLED_LOCKFILE}"
|
||||||
|
echo "+++ $TESTNET now enabled"
|
||||||
|
}
|
||||||
|
disable_testnet() {
|
||||||
|
rm -f "${ENABLED_LOCKFILE}"
|
||||||
|
echo "+++ $TESTNET now disabled"
|
||||||
|
}
|
||||||
|
is_testnet_enabled() {
|
||||||
|
if [[ ! -f ${ENABLED_LOCKFILE} ]]; then
|
||||||
|
echo "+++ ${TESTNET} is currently disabled. Enable ${TESTNET} by running ci/testnet-manager.sh with \$TESTNET_OP=enable, then re-run with current settings."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
case $TESTNET_OP in
|
case $TESTNET_OP in
|
||||||
sanity)
|
enable)
|
||||||
sanity
|
enable_testnet
|
||||||
|
;;
|
||||||
|
disable)
|
||||||
|
disable_testnet
|
||||||
|
delete
|
||||||
|
;;
|
||||||
|
create-and-start)
|
||||||
|
is_testnet_enabled
|
||||||
|
create-and-start
|
||||||
|
;;
|
||||||
|
create)
|
||||||
|
is_testnet_enabled
|
||||||
|
create
|
||||||
;;
|
;;
|
||||||
start)
|
start)
|
||||||
|
is_testnet_enabled
|
||||||
start
|
start
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
|
is_testnet_enabled
|
||||||
stop
|
stop
|
||||||
;;
|
;;
|
||||||
|
sanity)
|
||||||
|
is_testnet_enabled
|
||||||
|
sanity
|
||||||
|
;;
|
||||||
|
delete)
|
||||||
|
is_testnet_enabled
|
||||||
|
delete
|
||||||
|
;;
|
||||||
update-or-restart)
|
update-or-restart)
|
||||||
if start "" update; then
|
is_testnet_enabled
|
||||||
|
if start; then
|
||||||
echo Update successful
|
echo Update successful
|
||||||
else
|
else
|
||||||
echo "+++ Update failed, restarting the network"
|
echo "+++ Update failed, restarting the network"
|
||||||
$metricsWriteDatapoint "testnet-manager update-failure=1"
|
$metricsWriteDatapoint "testnet-manager update-failure=1"
|
||||||
start
|
create-and-start
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
sanity-or-restart)
|
sanity-or-restart)
|
||||||
|
is_testnet_enabled
|
||||||
if sanity; then
|
if sanity; then
|
||||||
echo Pass
|
echo Pass
|
||||||
else
|
else
|
||||||
@ -344,18 +541,22 @@ sanity-or-restart)
|
|||||||
# TODO: Restore attempt to restart the cluster before recreating it
|
# TODO: Restore attempt to restart the cluster before recreating it
|
||||||
# See https://github.com/solana-labs/solana/issues/3774
|
# See https://github.com/solana-labs/solana/issues/3774
|
||||||
if false; then
|
if false; then
|
||||||
if start "" update; then
|
if start; then
|
||||||
echo Update successful
|
echo Update successful
|
||||||
else
|
else
|
||||||
echo "+++ Update failed, restarting the network"
|
echo "+++ Update failed, restarting the network"
|
||||||
$metricsWriteDatapoint "testnet-manager update-failure=1"
|
$metricsWriteDatapoint "testnet-manager update-failure=1"
|
||||||
start
|
create-and-start
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
start
|
create-and-start
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Invalid TESTNET_OP=$TESTNET_OP"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo --- fin
|
echo --- fin
|
||||||
|
@ -11,13 +11,13 @@ usage() {
|
|||||||
echo "Error: $*"
|
echo "Error: $*"
|
||||||
fi
|
fi
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
usage: $0 [name] [cloud] [zone]
|
usage: $0 [name] [cloud] [zone1] ... [zoneN]
|
||||||
|
|
||||||
Sanity check a CD testnet
|
Sanity check a testnet
|
||||||
|
|
||||||
name - name of the network
|
name - name of the network
|
||||||
cloud - cloud provider to use (gce, ec2)
|
cloud - cloud provider to use (gce, ec2)
|
||||||
zone - cloud provider zone of the network
|
zone1 .. zoneN - cloud provider zones to check
|
||||||
|
|
||||||
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
||||||
metrics
|
metrics
|
||||||
@ -27,10 +27,10 @@ EOF
|
|||||||
|
|
||||||
netName=$1
|
netName=$1
|
||||||
cloudProvider=$2
|
cloudProvider=$2
|
||||||
zone=$3
|
|
||||||
[[ -n $netName ]] || usage ""
|
[[ -n $netName ]] || usage ""
|
||||||
[[ -n $cloudProvider ]] || usage "Cloud provider not specified"
|
[[ -n $cloudProvider ]] || usage "Cloud provider not specified"
|
||||||
[[ -n $zone ]] || usage "Zone not specified"
|
shift 2
|
||||||
|
[[ -n $1 ]] || usage "zone1 not specified"
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
exitcode=$?
|
exitcode=$?
|
||||||
@ -52,17 +52,20 @@ rm -f net/config/config
|
|||||||
trap shutdown EXIT INT
|
trap shutdown EXIT INT
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
echo "--- $cloudProvider.sh config"
|
for zone in "$@"; do
|
||||||
timeout 5m net/"$cloudProvider".sh config -p "$netName" -z "$zone"
|
echo "--- $cloudProvider config [$zone]"
|
||||||
net/init-metrics.sh -e
|
timeout 5m net/"$cloudProvider".sh config -p "$netName" -z "$zone"
|
||||||
echo "+++ $cloudProvider.sh info"
|
net/init-metrics.sh -e
|
||||||
net/"$cloudProvider".sh info
|
echo "+++ $cloudProvider.sh info"
|
||||||
echo --- net.sh sanity
|
net/"$cloudProvider".sh info
|
||||||
ok=true
|
echo "--- net.sh sanity [$cloudProvider:$zone]"
|
||||||
timeout 5m net/net.sh sanity \
|
ok=true
|
||||||
|
timeout 5m net/net.sh sanity \
|
||||||
${NO_LEDGER_VERIFY:+-o noLedgerVerify} \
|
${NO_LEDGER_VERIFY:+-o noLedgerVerify} \
|
||||||
${NO_VALIDATOR_SANITY:+-o noValidatorSanity} \
|
${NO_VALIDATOR_SANITY:+-o noValidatorSanity} \
|
||||||
${REJECT_EXTRA_NODES:+-o rejectExtraNodes} || ok=false
|
${REJECT_EXTRA_NODES:+-o rejectExtraNodes} \
|
||||||
|
$zone || ok=false
|
||||||
|
|
||||||
net/net.sh logs
|
net/net.sh logs
|
||||||
$ok
|
$ok
|
||||||
|
done
|
||||||
|
1
client/.gitignore
vendored
Normal file
1
client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target/
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solana-client"
|
name = "solana-client"
|
||||||
version = "0.13.0"
|
version = "0.15.0"
|
||||||
description = "Solana Client"
|
description = "Solana Client"
|
||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
repository = "https://github.com/solana-labs/solana"
|
repository = "https://github.com/solana-labs/solana"
|
||||||
@ -9,18 +9,18 @@ license = "Apache-2.0"
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bincode = "1.1.2"
|
bincode = "1.1.4"
|
||||||
bs58 = "0.2.0"
|
bs58 = "0.2.0"
|
||||||
log = "0.4.2"
|
log = "0.4.2"
|
||||||
jsonrpc-core = "10.1.0"
|
jsonrpc-core = "10.1.0"
|
||||||
reqwest = "0.9.11"
|
reqwest = "0.9.17"
|
||||||
serde = "1.0.89"
|
serde = "1.0.89"
|
||||||
serde_derive = "1.0.88"
|
serde_derive = "1.0.91"
|
||||||
serde_json = "1.0.39"
|
serde_json = "1.0.39"
|
||||||
solana-netutil = { path = "../netutil", version = "0.13.0" }
|
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||||
solana-sdk = { path = "../sdk", version = "0.13.0" }
|
solana-sdk = { path = "../sdk", version = "0.15.0" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
jsonrpc-core = "10.1.0"
|
jsonrpc-core = "10.1.0"
|
||||||
jsonrpc-http-server = "10.1.0"
|
jsonrpc-http-server = "10.1.0"
|
||||||
solana-logger = { path = "../logger", version = "0.13.0" }
|
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||||
|
50
client/src/client_error.rs
Normal file
50
client/src/client_error.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use crate::rpc_request;
|
||||||
|
use solana_sdk::transaction::TransactionError;
|
||||||
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ClientError {
|
||||||
|
Io(io::Error),
|
||||||
|
Reqwest(reqwest::Error),
|
||||||
|
RpcError(rpc_request::RpcError),
|
||||||
|
SerdeJson(serde_json::error::Error),
|
||||||
|
TransactionError(TransactionError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ClientError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "solana client error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ClientError {}
|
||||||
|
|
||||||
|
impl From<io::Error> for ClientError {
|
||||||
|
fn from(err: io::Error) -> ClientError {
|
||||||
|
ClientError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for ClientError {
|
||||||
|
fn from(err: reqwest::Error) -> ClientError {
|
||||||
|
ClientError::Reqwest(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rpc_request::RpcError> for ClientError {
|
||||||
|
fn from(err: rpc_request::RpcError) -> ClientError {
|
||||||
|
ClientError::RpcError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::error::Error> for ClientError {
|
||||||
|
fn from(err: serde_json::error::Error) -> ClientError {
|
||||||
|
ClientError::SerdeJson(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TransactionError> for ClientError {
|
||||||
|
fn from(err: TransactionError) -> ClientError {
|
||||||
|
ClientError::TransactionError(err)
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::client_error::ClientError;
|
||||||
use crate::rpc_request::RpcRequest;
|
use crate::rpc_request::RpcRequest;
|
||||||
|
|
||||||
pub(crate) trait GenericRpcClientRequest {
|
pub(crate) trait GenericRpcClientRequest {
|
||||||
@ -6,5 +7,5 @@ pub(crate) trait GenericRpcClientRequest {
|
|||||||
request: &RpcRequest,
|
request: &RpcRequest,
|
||||||
params: Option<serde_json::Value>,
|
params: Option<serde_json::Value>,
|
||||||
retries: usize,
|
retries: usize,
|
||||||
) -> Result<serde_json::Value, Box<dyn std::error::Error>>;
|
) -> Result<serde_json::Value, ClientError>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
pub mod client_error;
|
||||||
mod generic_rpc_client_request;
|
mod generic_rpc_client_request;
|
||||||
pub mod mock_rpc_client_request;
|
pub mod mock_rpc_client_request;
|
||||||
|
pub mod perf_utils;
|
||||||
pub mod rpc_client;
|
pub mod rpc_client;
|
||||||
pub mod rpc_client_request;
|
pub mod rpc_client_request;
|
||||||
pub mod rpc_request;
|
pub mod rpc_request;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
use crate::client_error::ClientError;
|
||||||
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
||||||
use crate::rpc_request::RpcRequest;
|
use crate::rpc_request::RpcRequest;
|
||||||
use serde_json::{Number, Value};
|
use serde_json::{Number, Value};
|
||||||
|
use solana_sdk::fee_calculator::FeeCalculator;
|
||||||
use solana_sdk::transaction::{self, TransactionError};
|
use solana_sdk::transaction::{self, TransactionError};
|
||||||
|
|
||||||
pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
|
pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
|
||||||
@ -23,7 +25,7 @@ impl GenericRpcClientRequest for MockRpcClientRequest {
|
|||||||
request: &RpcRequest,
|
request: &RpcRequest,
|
||||||
params: Option<serde_json::Value>,
|
params: Option<serde_json::Value>,
|
||||||
_retries: usize,
|
_retries: usize,
|
||||||
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
) -> Result<serde_json::Value, ClientError> {
|
||||||
if self.url == "fails" {
|
if self.url == "fails" {
|
||||||
return Ok(Value::Null);
|
return Ok(Value::Null);
|
||||||
}
|
}
|
||||||
@ -43,7 +45,10 @@ impl GenericRpcClientRequest for MockRpcClientRequest {
|
|||||||
let n = if self.url == "airdrop" { 0 } else { 50 };
|
let n = if self.url == "airdrop" { 0 } else { 50 };
|
||||||
Value::Number(Number::from(n))
|
Value::Number(Number::from(n))
|
||||||
}
|
}
|
||||||
RpcRequest::GetRecentBlockhash => Value::String(PUBKEY.to_string()),
|
RpcRequest::GetRecentBlockhash => Value::Array(vec![
|
||||||
|
Value::String(PUBKEY.to_string()),
|
||||||
|
serde_json::to_value(FeeCalculator::default()).unwrap(),
|
||||||
|
]),
|
||||||
RpcRequest::GetSignatureStatus => {
|
RpcRequest::GetSignatureStatus => {
|
||||||
let response: Option<transaction::Result<()>> = if self.url == "account_in_use" {
|
let response: Option<transaction::Result<()>> = if self.url == "account_in_use" {
|
||||||
Some(Err(TransactionError::AccountInUse))
|
Some(Err(TransactionError::AccountInUse))
|
||||||
|
76
client/src/perf_utils.rs
Normal file
76
client/src/perf_utils.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use log::*;
|
||||||
|
use solana_sdk::client::Client;
|
||||||
|
use solana_sdk::timing::duration_as_s;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SampleStats {
|
||||||
|
/// Maximum TPS reported by this node
|
||||||
|
pub tps: f32,
|
||||||
|
/// Total time taken for those txs
|
||||||
|
pub elapsed: Duration,
|
||||||
|
/// Total transactions reported by this node
|
||||||
|
pub txs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_txs<T>(
|
||||||
|
exit_signal: &Arc<AtomicBool>,
|
||||||
|
sample_stats: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||||
|
sample_period: u64,
|
||||||
|
client: &Arc<T>,
|
||||||
|
) where
|
||||||
|
T: Client,
|
||||||
|
{
|
||||||
|
let mut max_tps = 0.0;
|
||||||
|
let mut total_elapsed;
|
||||||
|
let mut total_txs;
|
||||||
|
let mut now = Instant::now();
|
||||||
|
let start_time = now;
|
||||||
|
let initial_txs = client.get_transaction_count().expect("transaction count");
|
||||||
|
let mut last_txs = initial_txs;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
total_elapsed = start_time.elapsed();
|
||||||
|
let elapsed = now.elapsed();
|
||||||
|
now = Instant::now();
|
||||||
|
let mut txs = client.get_transaction_count().expect("transaction count");
|
||||||
|
|
||||||
|
if txs < last_txs {
|
||||||
|
info!("Expected txs({}) >= last_txs({})", txs, last_txs);
|
||||||
|
txs = last_txs;
|
||||||
|
}
|
||||||
|
total_txs = txs - initial_txs;
|
||||||
|
let sample_txs = txs - last_txs;
|
||||||
|
last_txs = txs;
|
||||||
|
|
||||||
|
let tps = sample_txs as f32 / duration_as_s(&elapsed);
|
||||||
|
if tps > max_tps {
|
||||||
|
max_tps = tps;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Sampler {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s",
|
||||||
|
tps,
|
||||||
|
sample_txs,
|
||||||
|
total_txs,
|
||||||
|
total_elapsed.as_secs(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if exit_signal.load(Ordering::Relaxed) {
|
||||||
|
let stats = SampleStats {
|
||||||
|
tps: max_tps,
|
||||||
|
elapsed: total_elapsed,
|
||||||
|
txs: total_txs,
|
||||||
|
};
|
||||||
|
sample_stats
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.push((client.transactions_addr(), stats));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sleep(Duration::from_secs(sample_period));
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,17 @@
|
|||||||
|
use crate::client_error::ClientError;
|
||||||
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
||||||
use crate::mock_rpc_client_request::MockRpcClientRequest;
|
use crate::mock_rpc_client_request::MockRpcClientRequest;
|
||||||
use crate::rpc_client_request::RpcClientRequest;
|
use crate::rpc_client_request::RpcClientRequest;
|
||||||
use crate::rpc_request::RpcRequest;
|
use crate::rpc_request::RpcRequest;
|
||||||
use bincode::serialize;
|
use bincode::serialize;
|
||||||
use bs58;
|
|
||||||
use log::*;
|
use log::*;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use solana_sdk::account::Account;
|
use solana_sdk::account::Account;
|
||||||
|
use solana_sdk::fee_calculator::FeeCalculator;
|
||||||
use solana_sdk::hash::Hash;
|
use solana_sdk::hash::Hash;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
|
use solana_sdk::signature::{KeypairUtil, Signature};
|
||||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
|
use solana_sdk::timing::{DEFAULT_NUM_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT};
|
||||||
use solana_sdk::transaction::{self, Transaction, TransactionError};
|
use solana_sdk::transaction::{self, Transaction, TransactionError};
|
||||||
use std::error;
|
use std::error;
|
||||||
use std::io;
|
use std::io;
|
||||||
@ -19,7 +20,7 @@ use std::thread::sleep;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
pub struct RpcClient {
|
pub struct RpcClient {
|
||||||
client: Box<GenericRpcClientRequest>,
|
client: Box<GenericRpcClientRequest + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RpcClient {
|
impl RpcClient {
|
||||||
@ -46,10 +47,7 @@ impl RpcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_transaction(
|
pub fn send_transaction(&self, transaction: &Transaction) -> Result<String, ClientError> {
|
||||||
&self,
|
|
||||||
transaction: &Transaction,
|
|
||||||
) -> Result<String, Box<dyn error::Error>> {
|
|
||||||
let serialized = serialize(transaction).unwrap();
|
let serialized = serialize(transaction).unwrap();
|
||||||
let params = json!([serialized]);
|
let params = json!([serialized]);
|
||||||
let signature = self
|
let signature = self
|
||||||
@ -67,7 +65,7 @@ impl RpcClient {
|
|||||||
pub fn get_signature_status(
|
pub fn get_signature_status(
|
||||||
&self,
|
&self,
|
||||||
signature: &str,
|
signature: &str,
|
||||||
) -> Result<Option<transaction::Result<()>>, Box<dyn error::Error>> {
|
) -> Result<Option<transaction::Result<()>>, ClientError> {
|
||||||
let params = json!([signature.to_string()]);
|
let params = json!([signature.to_string()]);
|
||||||
let signature_status =
|
let signature_status =
|
||||||
self.client
|
self.client
|
||||||
@ -80,8 +78,8 @@ impl RpcClient {
|
|||||||
pub fn send_and_confirm_transaction<T: KeypairUtil>(
|
pub fn send_and_confirm_transaction<T: KeypairUtil>(
|
||||||
&self,
|
&self,
|
||||||
transaction: &mut Transaction,
|
transaction: &mut Transaction,
|
||||||
signer: &T,
|
signer_keys: &[&T],
|
||||||
) -> Result<String, Box<dyn error::Error>> {
|
) -> Result<String, ClientError> {
|
||||||
let mut send_retries = 5;
|
let mut send_retries = 5;
|
||||||
loop {
|
loop {
|
||||||
let mut status_retries = 4;
|
let mut status_retries = 4;
|
||||||
@ -99,7 +97,7 @@ impl RpcClient {
|
|||||||
if cfg!(not(test)) {
|
if cfg!(not(test)) {
|
||||||
// Retry ~twice during a slot
|
// Retry ~twice during a slot
|
||||||
sleep(Duration::from_millis(
|
sleep(Duration::from_millis(
|
||||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
500 * DEFAULT_TICKS_PER_SLOT / DEFAULT_NUM_TICKS_PER_SECOND,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -108,7 +106,7 @@ impl RpcClient {
|
|||||||
Ok(_) => return Ok(signature_str),
|
Ok(_) => return Ok(signature_str),
|
||||||
Err(TransactionError::AccountInUse) => {
|
Err(TransactionError::AccountInUse) => {
|
||||||
// Fetch a new blockhash and re-sign the transaction before sending it again
|
// Fetch a new blockhash and re-sign the transaction before sending it again
|
||||||
self.resign_transaction(transaction, signer)?;
|
self.resign_transaction(transaction, signer_keys)?;
|
||||||
send_retries - 1
|
send_retries - 1
|
||||||
}
|
}
|
||||||
Err(_) => 0,
|
Err(_) => 0,
|
||||||
@ -117,6 +115,9 @@ impl RpcClient {
|
|||||||
send_retries - 1
|
send_retries - 1
|
||||||
};
|
};
|
||||||
if send_retries == 0 {
|
if send_retries == 0 {
|
||||||
|
if status.is_some() {
|
||||||
|
status.unwrap()?
|
||||||
|
} else {
|
||||||
Err(io::Error::new(
|
Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
format!("Transaction {:?} failed: {:?}", signature_str, status),
|
format!("Transaction {:?} failed: {:?}", signature_str, status),
|
||||||
@ -124,11 +125,12 @@ impl RpcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_and_confirm_transactions(
|
pub fn send_and_confirm_transactions<T: KeypairUtil>(
|
||||||
&self,
|
&self,
|
||||||
mut transactions: Vec<Transaction>,
|
mut transactions: Vec<Transaction>,
|
||||||
signer: &Keypair,
|
signer_keys: &[&T],
|
||||||
) -> Result<(), Box<dyn error::Error>> {
|
) -> Result<(), Box<dyn error::Error>> {
|
||||||
let mut send_retries = 5;
|
let mut send_retries = 5;
|
||||||
loop {
|
loop {
|
||||||
@ -141,7 +143,7 @@ impl RpcClient {
|
|||||||
// Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors
|
// Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors
|
||||||
// when all the write transactions modify the same program account (eg, deploying a
|
// when all the write transactions modify the same program account (eg, deploying a
|
||||||
// new program)
|
// new program)
|
||||||
sleep(Duration::from_millis(1000 / NUM_TICKS_PER_SECOND));
|
sleep(Duration::from_millis(1000 / DEFAULT_NUM_TICKS_PER_SECOND));
|
||||||
}
|
}
|
||||||
|
|
||||||
let signature = self.send_transaction(&transaction).ok();
|
let signature = self.send_transaction(&transaction).ok();
|
||||||
@ -155,7 +157,7 @@ impl RpcClient {
|
|||||||
if cfg!(not(test)) {
|
if cfg!(not(test)) {
|
||||||
// Retry ~twice during a slot
|
// Retry ~twice during a slot
|
||||||
sleep(Duration::from_millis(
|
sleep(Duration::from_millis(
|
||||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
500 * DEFAULT_TICKS_PER_SLOT / DEFAULT_NUM_TICKS_PER_SECOND,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,12 +187,12 @@ impl RpcClient {
|
|||||||
send_retries -= 1;
|
send_retries -= 1;
|
||||||
|
|
||||||
// Re-sign any failed transactions with a new blockhash and retry
|
// Re-sign any failed transactions with a new blockhash and retry
|
||||||
let blockhash =
|
let (blockhash, _fee_calculator) =
|
||||||
self.get_new_blockhash(&transactions_signatures[0].0.message().recent_blockhash)?;
|
self.get_new_blockhash(&transactions_signatures[0].0.message().recent_blockhash)?;
|
||||||
transactions = transactions_signatures
|
transactions = transactions_signatures
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(mut transaction, _)| {
|
.map(|(mut transaction, _)| {
|
||||||
transaction.sign(&[signer], blockhash);
|
transaction.sign(signer_keys, blockhash);
|
||||||
transaction
|
transaction
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -200,10 +202,11 @@ impl RpcClient {
|
|||||||
pub fn resign_transaction<T: KeypairUtil>(
|
pub fn resign_transaction<T: KeypairUtil>(
|
||||||
&self,
|
&self,
|
||||||
tx: &mut Transaction,
|
tx: &mut Transaction,
|
||||||
signer_key: &T,
|
signer_keys: &[&T],
|
||||||
) -> Result<(), Box<dyn error::Error>> {
|
) -> Result<(), ClientError> {
|
||||||
let blockhash = self.get_new_blockhash(&tx.message().recent_blockhash)?;
|
let (blockhash, _fee_calculator) =
|
||||||
tx.sign(&[signer_key], blockhash);
|
self.get_new_blockhash(&tx.message().recent_blockhash)?;
|
||||||
|
tx.sign(signer_keys, blockhash);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,31 +223,7 @@ impl RpcClient {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_account_data(&self, pubkey: &Pubkey) -> io::Result<Vec<u8>> {
|
pub fn get_account(&self, pubkey: &Pubkey) -> io::Result<Account> {
|
||||||
let params = json!([format!("{}", pubkey)]);
|
|
||||||
let response = self
|
|
||||||
.client
|
|
||||||
.send(&RpcRequest::GetAccountInfo, Some(params), 0);
|
|
||||||
match response {
|
|
||||||
Ok(account_json) => {
|
|
||||||
let account: Account =
|
|
||||||
serde_json::from_value(account_json).expect("deserialize account");
|
|
||||||
Ok(account.data)
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
debug!("get_account_data failed: {:?}", error);
|
|
||||||
Err(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
|
||||||
"get_account_data failed",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request the balance of the user holding `pubkey`. This method blocks
|
|
||||||
/// until the server sends a response. If the response packet is dropped
|
|
||||||
/// by the network, this method will hang indefinitely.
|
|
||||||
pub fn get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
|
|
||||||
let params = json!([format!("{}", pubkey)]);
|
let params = json!([format!("{}", pubkey)]);
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
@ -255,80 +234,89 @@ impl RpcClient {
|
|||||||
let account: Account =
|
let account: Account =
|
||||||
serde_json::from_value(account_json).expect("deserialize account");
|
serde_json::from_value(account_json).expect("deserialize account");
|
||||||
trace!("Response account {:?} {:?}", pubkey, account);
|
trace!("Response account {:?} {:?}", pubkey, account);
|
||||||
trace!("get_balance {:?}", account.lamports);
|
Ok(account)
|
||||||
Ok(account.lamports)
|
|
||||||
})
|
})
|
||||||
.map_err(|error| {
|
.map_err(|err| {
|
||||||
debug!("Response account {}: None (error: {:?})", pubkey, error);
|
io::Error::new(
|
||||||
io::Error::new(io::ErrorKind::Other, "AccountNotFound")
|
io::ErrorKind::Other,
|
||||||
|
format!("AccountNotFound: pubkey={}: {}", pubkey, err),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_account_data(&self, pubkey: &Pubkey) -> io::Result<Vec<u8>> {
|
||||||
|
self.get_account(pubkey).map(|account| account.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request the balance of the user holding `pubkey`. This method blocks
|
||||||
|
/// until the server sends a response. If the response packet is dropped
|
||||||
|
/// by the network, this method will hang indefinitely.
|
||||||
|
pub fn get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
|
||||||
|
self.get_account(pubkey).map(|account| account.lamports)
|
||||||
|
}
|
||||||
|
|
||||||
/// Request the transaction count. If the response packet is dropped by the network,
|
/// Request the transaction count. If the response packet is dropped by the network,
|
||||||
/// this method will try again 5 times.
|
/// this method will try again 5 times.
|
||||||
pub fn get_transaction_count(&self) -> io::Result<u64> {
|
pub fn get_transaction_count(&self) -> io::Result<u64> {
|
||||||
debug!("get_transaction_count");
|
let response = self
|
||||||
|
.client
|
||||||
let mut num_retries = 5;
|
.send(&RpcRequest::GetTransactionCount, None, 0)
|
||||||
while num_retries > 0 {
|
.map_err(|err| {
|
||||||
let response = self.client.send(&RpcRequest::GetTransactionCount, None, 0);
|
io::Error::new(
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(value) => {
|
|
||||||
debug!("transaction_count response: {:?}", value);
|
|
||||||
if let Some(transaction_count) = value.as_u64() {
|
|
||||||
return Ok(transaction_count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
debug!("transaction_count failed: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
num_retries -= 1;
|
|
||||||
}
|
|
||||||
Err(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
"Unable to get transaction count, too many retries",
|
format!("GetTransactionCount request failure: {:?}", err),
|
||||||
))?
|
)
|
||||||
}
|
})?;
|
||||||
|
|
||||||
pub fn get_recent_blockhash(&self) -> io::Result<Hash> {
|
serde_json::from_value(response).map_err(|err| {
|
||||||
let mut num_retries = 5;
|
io::Error::new(
|
||||||
while num_retries > 0 {
|
|
||||||
match self.client.send(&RpcRequest::GetRecentBlockhash, None, 0) {
|
|
||||||
Ok(value) => {
|
|
||||||
if let Some(blockhash_str) = value.as_str() {
|
|
||||||
let blockhash_vec = bs58::decode(blockhash_str)
|
|
||||||
.into_vec()
|
|
||||||
.expect("bs58::decode");
|
|
||||||
return Ok(Hash::new(&blockhash_vec));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
debug!("retry_get_recent_blockhash failed: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
num_retries -= 1;
|
|
||||||
}
|
|
||||||
Err(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
"Unable to get recent blockhash, too many retries",
|
format!("GetTransactionCount parse failure: {}", err),
|
||||||
))
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<Hash> {
|
pub fn get_recent_blockhash(&self) -> io::Result<(Hash, FeeCalculator)> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.send(&RpcRequest::GetRecentBlockhash, None, 0)
|
||||||
|
.map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("GetRecentBlockhash request failure: {:?}", err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (blockhash, fee_calculator) =
|
||||||
|
serde_json::from_value::<(String, FeeCalculator)>(response).map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("GetRecentBlockhash parse failure: {:?}", err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let blockhash = blockhash.parse().map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("GetRecentBlockhash parse failure: {:?}", err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok((blockhash, fee_calculator))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<(Hash, FeeCalculator)> {
|
||||||
let mut num_retries = 10;
|
let mut num_retries = 10;
|
||||||
while num_retries > 0 {
|
while num_retries > 0 {
|
||||||
if let Ok(new_blockhash) = self.get_recent_blockhash() {
|
if let Ok((new_blockhash, fee_calculator)) = self.get_recent_blockhash() {
|
||||||
if new_blockhash != *blockhash {
|
if new_blockhash != *blockhash {
|
||||||
return Ok(new_blockhash);
|
return Ok((new_blockhash, fee_calculator));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug!("Got same blockhash ({:?}), will retry...", blockhash);
|
debug!("Got same blockhash ({:?}), will retry...", blockhash);
|
||||||
|
|
||||||
// Retry ~twice during a slot
|
// Retry ~twice during a slot
|
||||||
sleep(Duration::from_millis(
|
sleep(Duration::from_millis(
|
||||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
500 * DEFAULT_TICKS_PER_SLOT / DEFAULT_NUM_TICKS_PER_SECOND,
|
||||||
));
|
));
|
||||||
num_retries -= 1;
|
num_retries -= 1;
|
||||||
}
|
}
|
||||||
@ -480,24 +468,22 @@ impl RpcClient {
|
|||||||
Some(params.clone()),
|
Some(params.clone()),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
.map_err(|error| {
|
.map_err(|err| {
|
||||||
debug!(
|
|
||||||
"Response get_num_blocks_since_signature_confirmation: {}",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
"GetNumBlocksSinceSignatureConfirmation request failure",
|
format!(
|
||||||
|
"GetNumBlocksSinceSignatureConfirmation request failure: {}",
|
||||||
|
err
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
serde_json::from_value(response).map_err(|error| {
|
serde_json::from_value(response).map_err(|err| {
|
||||||
debug!(
|
|
||||||
"ParseError: get_num_blocks_since_signature_confirmation: {}",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
"GetNumBlocksSinceSignatureConfirmation parse failure",
|
format!(
|
||||||
|
"GetNumBlocksSinceSignatureConfirmation parse failure: {}",
|
||||||
|
err
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -526,7 +512,7 @@ impl RpcClient {
|
|||||||
request: &RpcRequest,
|
request: &RpcRequest,
|
||||||
params: Option<Value>,
|
params: Option<Value>,
|
||||||
retries: usize,
|
retries: usize,
|
||||||
) -> Result<Value, Box<dyn error::Error>> {
|
) -> Result<Value, ClientError> {
|
||||||
self.client.send(request, params, retries)
|
self.client.send(request, params, retries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -604,7 +590,7 @@ mod tests {
|
|||||||
// Send erroneous parameter
|
// Send erroneous parameter
|
||||||
let blockhash = rpc_client.retry_make_rpc_request(
|
let blockhash = rpc_client.retry_make_rpc_request(
|
||||||
&RpcRequest::GetRecentBlockhash,
|
&RpcRequest::GetRecentBlockhash,
|
||||||
Some(json!("paramter")),
|
Some(json!("parameter")),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
assert_eq!(blockhash.is_err(), true);
|
assert_eq!(blockhash.is_err(), true);
|
||||||
@ -655,7 +641,7 @@ mod tests {
|
|||||||
let key = Keypair::new();
|
let key = Keypair::new();
|
||||||
let to = Pubkey::new_rand();
|
let to = Pubkey::new_rand();
|
||||||
let blockhash = Hash::default();
|
let blockhash = Hash::default();
|
||||||
let tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
let tx = system_transaction::create_user_account(&key, &to, 50, blockhash);
|
||||||
|
|
||||||
let signature = rpc_client.send_transaction(&tx);
|
let signature = rpc_client.send_transaction(&tx);
|
||||||
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
|
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
|
||||||
@ -669,16 +655,14 @@ mod tests {
|
|||||||
fn test_get_recent_blockhash() {
|
fn test_get_recent_blockhash() {
|
||||||
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||||
|
|
||||||
let vec = bs58::decode(PUBKEY).into_vec().unwrap();
|
let expected_blockhash: Hash = PUBKEY.parse().unwrap();
|
||||||
let expected_blockhash = Hash::new(&vec);
|
|
||||||
|
|
||||||
let blockhash = dbg!(rpc_client.get_recent_blockhash()).expect("blockhash ok");
|
let (blockhash, _fee_calculator) = rpc_client.get_recent_blockhash().expect("blockhash ok");
|
||||||
assert_eq!(blockhash, expected_blockhash);
|
assert_eq!(blockhash, expected_blockhash);
|
||||||
|
|
||||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||||
|
|
||||||
let blockhash = dbg!(rpc_client.get_recent_blockhash());
|
assert!(rpc_client.get_recent_blockhash().is_err());
|
||||||
assert!(blockhash.is_err());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -706,17 +690,17 @@ mod tests {
|
|||||||
let key = Keypair::new();
|
let key = Keypair::new();
|
||||||
let to = Pubkey::new_rand();
|
let to = Pubkey::new_rand();
|
||||||
let blockhash = Hash::default();
|
let blockhash = Hash::default();
|
||||||
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash);
|
||||||
|
|
||||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&key]);
|
||||||
result.unwrap();
|
result.unwrap();
|
||||||
|
|
||||||
let rpc_client = RpcClient::new_mock("account_in_use".to_string());
|
let rpc_client = RpcClient::new_mock("account_in_use".to_string());
|
||||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&key]);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&key]);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -726,14 +710,13 @@ mod tests {
|
|||||||
|
|
||||||
let key = Keypair::new();
|
let key = Keypair::new();
|
||||||
let to = Pubkey::new_rand();
|
let to = Pubkey::new_rand();
|
||||||
let vec = bs58::decode("HUu3LwEzGRsUkuJS121jzkPJW39Kq62pXCTmTa1F9jDL")
|
let blockhash: Hash = "HUu3LwEzGRsUkuJS121jzkPJW39Kq62pXCTmTa1F9jDL"
|
||||||
.into_vec()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let blockhash = Hash::new(&vec);
|
let prev_tx = system_transaction::create_user_account(&key, &to, 50, blockhash);
|
||||||
let prev_tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash);
|
||||||
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
|
||||||
|
|
||||||
rpc_client.resign_transaction(&mut tx, &key).unwrap();
|
rpc_client.resign_transaction(&mut tx, &[&key]).unwrap();
|
||||||
|
|
||||||
assert_ne!(prev_tx, tx);
|
assert_ne!(prev_tx, tx);
|
||||||
assert_ne!(prev_tx.signatures, tx.signatures);
|
assert_ne!(prev_tx.signatures, tx.signatures);
|
||||||
@ -743,4 +726,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rpc_client_thread() {
|
||||||
|
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||||
|
thread::spawn(move || rpc_client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
use crate::client_error::ClientError;
|
||||||
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
||||||
use crate::rpc_request::{RpcError, RpcRequest};
|
use crate::rpc_request::{RpcError, RpcRequest};
|
||||||
use log::*;
|
use log::*;
|
||||||
use reqwest;
|
use reqwest;
|
||||||
use reqwest::header::CONTENT_TYPE;
|
use reqwest::header::CONTENT_TYPE;
|
||||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
|
use solana_sdk::timing::{DEFAULT_NUM_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT};
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ impl GenericRpcClientRequest for RpcClientRequest {
|
|||||||
request: &RpcRequest,
|
request: &RpcRequest,
|
||||||
params: Option<serde_json::Value>,
|
params: Option<serde_json::Value>,
|
||||||
mut retries: usize,
|
mut retries: usize,
|
||||||
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
) -> Result<serde_json::Value, ClientError> {
|
||||||
// Concurrent requests are not supported so reuse the same request id for all requests
|
// Concurrent requests are not supported so reuse the same request id for all requests
|
||||||
let request_id = 1;
|
let request_id = 1;
|
||||||
|
|
||||||
@ -62,8 +63,8 @@ impl GenericRpcClientRequest for RpcClientRequest {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!(
|
info!(
|
||||||
"make_rpc_request() failed, {} retries left: {:?}",
|
"make_rpc_request({:?}) failed, {} retries left: {:?}",
|
||||||
retries, e
|
request, retries, e
|
||||||
);
|
);
|
||||||
if retries == 0 {
|
if retries == 0 {
|
||||||
Err(e)?;
|
Err(e)?;
|
||||||
@ -72,7 +73,7 @@ impl GenericRpcClientRequest for RpcClientRequest {
|
|||||||
|
|
||||||
// Sleep for approximately half a slot
|
// Sleep for approximately half a slot
|
||||||
sleep(Duration::from_millis(
|
sleep(Duration::from_millis(
|
||||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
500 * DEFAULT_TICKS_PER_SLOT / DEFAULT_NUM_TICKS_PER_SECOND,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,21 +4,24 @@ use std::{error, fmt};
|
|||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum RpcRequest {
|
pub enum RpcRequest {
|
||||||
ConfirmTransaction,
|
ConfirmTransaction,
|
||||||
|
DeregisterNode,
|
||||||
|
FullnodeExit,
|
||||||
GetAccountInfo,
|
GetAccountInfo,
|
||||||
GetBalance,
|
GetBalance,
|
||||||
|
GetClusterNodes,
|
||||||
|
GetNumBlocksSinceSignatureConfirmation,
|
||||||
GetRecentBlockhash,
|
GetRecentBlockhash,
|
||||||
GetSignatureStatus,
|
GetSignatureStatus,
|
||||||
|
GetSlotLeader,
|
||||||
|
GetEpochVoteAccounts,
|
||||||
|
GetStorageBlockhash,
|
||||||
|
GetStorageSlot,
|
||||||
|
GetStoragePubkeysForSlot,
|
||||||
GetTransactionCount,
|
GetTransactionCount,
|
||||||
|
RegisterNode,
|
||||||
RequestAirdrop,
|
RequestAirdrop,
|
||||||
SendTransaction,
|
SendTransaction,
|
||||||
RegisterNode,
|
|
||||||
SignVote,
|
SignVote,
|
||||||
DeregisterNode,
|
|
||||||
GetStorageBlockhash,
|
|
||||||
GetStorageEntryHeight,
|
|
||||||
GetStoragePubkeysForEntryHeight,
|
|
||||||
FullnodeExit,
|
|
||||||
GetNumBlocksSinceSignatureConfirmation,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RpcRequest {
|
impl RpcRequest {
|
||||||
@ -26,23 +29,26 @@ impl RpcRequest {
|
|||||||
let jsonrpc = "2.0";
|
let jsonrpc = "2.0";
|
||||||
let method = match self {
|
let method = match self {
|
||||||
RpcRequest::ConfirmTransaction => "confirmTransaction",
|
RpcRequest::ConfirmTransaction => "confirmTransaction",
|
||||||
|
RpcRequest::DeregisterNode => "deregisterNode",
|
||||||
|
RpcRequest::FullnodeExit => "fullnodeExit",
|
||||||
RpcRequest::GetAccountInfo => "getAccountInfo",
|
RpcRequest::GetAccountInfo => "getAccountInfo",
|
||||||
RpcRequest::GetBalance => "getBalance",
|
RpcRequest::GetBalance => "getBalance",
|
||||||
RpcRequest::GetRecentBlockhash => "getRecentBlockhash",
|
RpcRequest::GetClusterNodes => "getClusterNodes",
|
||||||
RpcRequest::GetSignatureStatus => "getSignatureStatus",
|
|
||||||
RpcRequest::GetTransactionCount => "getTransactionCount",
|
|
||||||
RpcRequest::RequestAirdrop => "requestAirdrop",
|
|
||||||
RpcRequest::SendTransaction => "sendTransaction",
|
|
||||||
RpcRequest::RegisterNode => "registerNode",
|
|
||||||
RpcRequest::SignVote => "signVote",
|
|
||||||
RpcRequest::DeregisterNode => "deregisterNode",
|
|
||||||
RpcRequest::GetStorageBlockhash => "getStorageBlockhash",
|
|
||||||
RpcRequest::GetStorageEntryHeight => "getStorageEntryHeight",
|
|
||||||
RpcRequest::GetStoragePubkeysForEntryHeight => "getStoragePubkeysForEntryHeight",
|
|
||||||
RpcRequest::FullnodeExit => "fullnodeExit",
|
|
||||||
RpcRequest::GetNumBlocksSinceSignatureConfirmation => {
|
RpcRequest::GetNumBlocksSinceSignatureConfirmation => {
|
||||||
"getNumBlocksSinceSignatureConfirmation"
|
"getNumBlocksSinceSignatureConfirmation"
|
||||||
}
|
}
|
||||||
|
RpcRequest::GetRecentBlockhash => "getRecentBlockhash",
|
||||||
|
RpcRequest::GetSignatureStatus => "getSignatureStatus",
|
||||||
|
RpcRequest::GetSlotLeader => "getSlotLeader",
|
||||||
|
RpcRequest::GetEpochVoteAccounts => "getEpochVoteAccounts",
|
||||||
|
RpcRequest::GetStorageBlockhash => "getStorageBlockhash",
|
||||||
|
RpcRequest::GetStorageSlot => "getStorageSlot",
|
||||||
|
RpcRequest::GetStoragePubkeysForSlot => "getStoragePubkeysForSlot",
|
||||||
|
RpcRequest::GetTransactionCount => "getTransactionCount",
|
||||||
|
RpcRequest::RegisterNode => "registerNode",
|
||||||
|
RpcRequest::RequestAirdrop => "requestAirdrop",
|
||||||
|
RpcRequest::SendTransaction => "sendTransaction",
|
||||||
|
RpcRequest::SignVote => "signVote",
|
||||||
};
|
};
|
||||||
let mut request = json!({
|
let mut request = json!({
|
||||||
"jsonrpc": jsonrpc,
|
"jsonrpc": jsonrpc,
|
||||||
|
@ -7,6 +7,7 @@ use crate::rpc_client::RpcClient;
|
|||||||
use bincode::{serialize_into, serialized_size};
|
use bincode::{serialize_into, serialized_size};
|
||||||
use log::*;
|
use log::*;
|
||||||
use solana_sdk::client::{AsyncClient, Client, SyncClient};
|
use solana_sdk::client::{AsyncClient, Client, SyncClient};
|
||||||
|
use solana_sdk::fee_calculator::FeeCalculator;
|
||||||
use solana_sdk::hash::Hash;
|
use solana_sdk::hash::Hash;
|
||||||
use solana_sdk::instruction::Instruction;
|
use solana_sdk::instruction::Instruction;
|
||||||
use solana_sdk::message::Message;
|
use solana_sdk::message::Message;
|
||||||
@ -107,7 +108,8 @@ impl ThinClient {
|
|||||||
return Ok(transaction.signatures[0]);
|
return Ok(transaction.signatures[0]);
|
||||||
}
|
}
|
||||||
info!("{} tries failed transfer to {}", x, self.transactions_addr);
|
info!("{} tries failed transfer to {}", x, self.transactions_addr);
|
||||||
transaction.sign(keypairs, self.rpc_client.get_recent_blockhash()?);
|
let (blockhash, _fee_calculator) = self.rpc_client.get_recent_blockhash()?;
|
||||||
|
transaction.sign(keypairs, blockhash);
|
||||||
}
|
}
|
||||||
Err(io::Error::new(
|
Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
@ -115,10 +117,6 @@ impl ThinClient {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<Hash> {
|
|
||||||
self.rpc_client.get_new_blockhash(blockhash)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll_balance_with_timeout(
|
pub fn poll_balance_with_timeout(
|
||||||
&self,
|
&self,
|
||||||
pubkey: &Pubkey,
|
pubkey: &Pubkey,
|
||||||
@ -137,19 +135,6 @@ impl ThinClient {
|
|||||||
self.rpc_client.wait_for_balance(pubkey, expected_balance)
|
self.rpc_client.wait_for_balance(pubkey, expected_balance)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll_for_signature(&self, signature: &Signature) -> io::Result<()> {
|
|
||||||
self.rpc_client.poll_for_signature(signature)
|
|
||||||
}
|
|
||||||
/// Poll the server until the signature has been confirmed by at least `min_confirmed_blocks`
|
|
||||||
pub fn poll_for_signature_confirmation(
|
|
||||||
&self,
|
|
||||||
signature: &Signature,
|
|
||||||
min_confirmed_blocks: usize,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
self.rpc_client
|
|
||||||
.poll_for_signature_confirmation(signature, min_confirmed_blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check a signature in the bank. This method blocks
|
/// Check a signature in the bank. This method blocks
|
||||||
/// until the server sends a response.
|
/// until the server sends a response.
|
||||||
pub fn check_signature(&self, signature: &Signature) -> bool {
|
pub fn check_signature(&self, signature: &Signature) -> bool {
|
||||||
@ -168,11 +153,15 @@ impl ThinClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client for ThinClient {}
|
impl Client for ThinClient {
|
||||||
|
fn transactions_addr(&self) -> String {
|
||||||
|
self.transactions_addr.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SyncClient for ThinClient {
|
impl SyncClient for ThinClient {
|
||||||
fn send_message(&self, keypairs: &[&Keypair], message: Message) -> TransportResult<Signature> {
|
fn send_message(&self, keypairs: &[&Keypair], message: Message) -> TransportResult<Signature> {
|
||||||
let blockhash = self.get_recent_blockhash()?;
|
let (blockhash, _fee_calculator) = self.get_recent_blockhash()?;
|
||||||
let mut transaction = Transaction::new(&keypairs, message, blockhash);
|
let mut transaction = Transaction::new(&keypairs, message, blockhash);
|
||||||
let signature = self.send_and_confirm_transaction(keypairs, &mut transaction, 5, 0)?;
|
let signature = self.send_and_confirm_transaction(keypairs, &mut transaction, 5, 0)?;
|
||||||
Ok(signature)
|
Ok(signature)
|
||||||
@ -217,21 +206,39 @@ impl SyncClient for ThinClient {
|
|||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
format!("send_transaction failed with error {}", err),
|
format!("send_transaction failed with error {:?}", err),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_recent_blockhash(&self) -> TransportResult<Hash> {
|
fn get_recent_blockhash(&self) -> TransportResult<(Hash, FeeCalculator)> {
|
||||||
let recent_blockhash = self.rpc_client.get_recent_blockhash()?;
|
Ok(self.rpc_client.get_recent_blockhash()?)
|
||||||
Ok(recent_blockhash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_transaction_count(&self) -> TransportResult<u64> {
|
fn get_transaction_count(&self) -> TransportResult<u64> {
|
||||||
let transaction_count = self.rpc_client.get_transaction_count()?;
|
let transaction_count = self.rpc_client.get_transaction_count()?;
|
||||||
Ok(transaction_count)
|
Ok(transaction_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Poll the server until the signature has been confirmed by at least `min_confirmed_blocks`
|
||||||
|
fn poll_for_signature_confirmation(
|
||||||
|
&self,
|
||||||
|
signature: &Signature,
|
||||||
|
min_confirmed_blocks: usize,
|
||||||
|
) -> TransportResult<()> {
|
||||||
|
Ok(self
|
||||||
|
.rpc_client
|
||||||
|
.poll_for_signature_confirmation(signature, min_confirmed_blocks)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_for_signature(&self, signature: &Signature) -> TransportResult<()> {
|
||||||
|
Ok(self.rpc_client.poll_for_signature(signature)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_new_blockhash(&self, blockhash: &Hash) -> TransportResult<(Hash, FeeCalculator)> {
|
||||||
|
Ok(self.rpc_client.get_new_blockhash(blockhash)?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncClient for ThinClient {
|
impl AsyncClient for ThinClient {
|
||||||
|
1
core/.gitignore
vendored
Normal file
1
core/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target/
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "solana"
|
name = "solana"
|
||||||
description = "Blockchain, Rebuilt for Scale"
|
description = "Blockchain, Rebuilt for Scale"
|
||||||
version = "0.13.0"
|
version = "0.15.0"
|
||||||
documentation = "https://docs.rs/solana"
|
documentation = "https://docs.rs/solana"
|
||||||
homepage = "https://solana.com/"
|
homepage = "https://solana.com/"
|
||||||
readme = "../README.md"
|
readme = "../README.md"
|
||||||
@ -20,11 +20,12 @@ erasure = []
|
|||||||
kvstore = ["solana-kvstore"]
|
kvstore = ["solana-kvstore"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bincode = "1.1.2"
|
bincode = "1.1.4"
|
||||||
bs58 = "0.2.0"
|
bs58 = "0.2.0"
|
||||||
byteorder = "1.3.1"
|
byteorder = "1.3.1"
|
||||||
chrono = { version = "0.4.0", features = ["serde"] }
|
chrono = { version = "0.4.0", features = ["serde"] }
|
||||||
crc = { version = "1.8.1", optional = true }
|
crc = { version = "1.8.1", optional = true }
|
||||||
|
core_affinity = "0.5.9"
|
||||||
hashbrown = "0.2.0"
|
hashbrown = "0.2.0"
|
||||||
indexmap = "1.0"
|
indexmap = "1.0"
|
||||||
itertools = "0.8.0"
|
itertools = "0.8.0"
|
||||||
@ -33,41 +34,48 @@ jsonrpc-derive = "11.0.0"
|
|||||||
jsonrpc-http-server = "11.0.0"
|
jsonrpc-http-server = "11.0.0"
|
||||||
jsonrpc-pubsub = "11.0.0"
|
jsonrpc-pubsub = "11.0.0"
|
||||||
jsonrpc-ws-server = "11.0.0"
|
jsonrpc-ws-server = "11.0.0"
|
||||||
libc = "0.2.50"
|
libc = "0.2.55"
|
||||||
log = "0.4.2"
|
log = "0.4.2"
|
||||||
memmap = { version = "0.7.0", optional = true }
|
memmap = { version = "0.7.0", optional = true }
|
||||||
nix = "0.13.0"
|
nix = "0.14.0"
|
||||||
rand = "0.6.5"
|
rand = "0.6.5"
|
||||||
rand_chacha = "0.1.1"
|
rand_chacha = "0.1.1"
|
||||||
rayon = "1.0.0"
|
rayon = "1.0.0"
|
||||||
reqwest = "0.9.11"
|
reed-solomon-erasure = "3.1.1"
|
||||||
ring = "0.13.2"
|
reqwest = "0.9.17"
|
||||||
rocksdb = "0.11.0"
|
rocksdb = "0.11.0"
|
||||||
serde = "1.0.89"
|
serde = "1.0.89"
|
||||||
serde_derive = "1.0.88"
|
serde_derive = "1.0.91"
|
||||||
serde_json = "1.0.39"
|
serde_json = "1.0.39"
|
||||||
solana-budget-api = { path = "../programs/budget_api", version = "0.13.0" }
|
solana-budget-api = { path = "../programs/budget_api", version = "0.15.0" }
|
||||||
solana-client = { path = "../client", version = "0.13.0" }
|
solana-budget-program = { path = "../programs/budget_program", version = "0.15.0" }
|
||||||
solana-drone = { path = "../drone", version = "0.13.0" }
|
solana-client = { path = "../client", version = "0.15.0" }
|
||||||
solana-kvstore = { path = "../kvstore", version = "0.13.0", optional = true }
|
solana-drone = { path = "../drone", version = "0.15.0" }
|
||||||
solana-logger = { path = "../logger", version = "0.13.0" }
|
solana-ed25519-dalek = "0.2.0"
|
||||||
solana-metrics = { path = "../metrics", version = "0.13.0" }
|
solana-kvstore = { path = "../kvstore", version = "0.15.0" , optional = true }
|
||||||
solana-netutil = { path = "../netutil", version = "0.13.0" }
|
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||||
solana-runtime = { path = "../runtime", version = "0.13.0" }
|
solana-metrics = { path = "../metrics", version = "0.15.0" }
|
||||||
solana-sdk = { path = "../sdk", version = "0.13.0" }
|
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||||
solana-storage-api = { path = "../programs/storage_api", version = "0.13.0" }
|
solana-runtime = { path = "../runtime", version = "0.15.0" }
|
||||||
solana-vote-api = { path = "../programs/vote_api", version = "0.13.0" }
|
solana-sdk = { path = "../sdk", version = "0.15.0" }
|
||||||
solana-vote-signer = { path = "../vote-signer", version = "0.13.0" }
|
solana-stake-api = { path = "../programs/stake_api", version = "0.15.0" }
|
||||||
|
solana-stake-program = { path = "../programs/stake_program", version = "0.15.0" }
|
||||||
|
solana-storage-api = { path = "../programs/storage_api", version = "0.15.0" }
|
||||||
|
solana-storage-program = { path = "../programs/storage_program", version = "0.15.0" }
|
||||||
|
solana-vote-api = { path = "../programs/vote_api", version = "0.15.0" }
|
||||||
|
solana-vote-program = { path = "../programs/vote_program", version = "0.15.0" }
|
||||||
|
solana-exchange-program = { path = "../programs/exchange_program", version = "0.15.0" }
|
||||||
|
solana-config-program = { path = "../programs/config_program", version = "0.15.0" }
|
||||||
|
solana-vote-signer = { path = "../vote-signer", version = "0.15.0" }
|
||||||
sys-info = "0.5.6"
|
sys-info = "0.5.6"
|
||||||
tokio = "0.1"
|
tokio = "0.1"
|
||||||
tokio-codec = "0.1"
|
tokio-codec = "0.1"
|
||||||
untrusted = "0.6.2"
|
untrusted = "0.6.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = "0.1.4"
|
hex-literal = "0.2.0"
|
||||||
matches = "0.1.6"
|
matches = "0.1.6"
|
||||||
solana-vote-program = { path = "../programs/vote_program", version = "0.13.0" }
|
|
||||||
solana-budget-program = { path = "../programs/budget_program", version = "0.13.0" }
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "banking_stage"
|
name = "banking_stage"
|
||||||
@ -84,6 +92,12 @@ name = "gen_keys"
|
|||||||
[[bench]]
|
[[bench]]
|
||||||
name = "sigverify"
|
name = "sigverify"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "sigverify_stage"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "poh"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
required-features = ["chacha"]
|
required-features = ["chacha"]
|
||||||
name = "chacha"
|
name = "chacha"
|
||||||
|
@ -4,30 +4,34 @@ extern crate test;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate solana;
|
extern crate solana;
|
||||||
|
|
||||||
|
use log::*;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use solana::banking_stage::{create_test_recorder, BankingStage};
|
use solana::banking_stage::{create_test_recorder, BankingStage};
|
||||||
use solana::blocktree::{get_tmp_ledger_path, Blocktree};
|
use solana::blocktree::{get_tmp_ledger_path, Blocktree};
|
||||||
use solana::cluster_info::ClusterInfo;
|
use solana::cluster_info::ClusterInfo;
|
||||||
use solana::cluster_info::Node;
|
use solana::cluster_info::Node;
|
||||||
|
use solana::genesis_utils::{create_genesis_block, GenesisBlockInfo};
|
||||||
use solana::packet::to_packets_chunked;
|
use solana::packet::to_packets_chunked;
|
||||||
use solana::poh_recorder::WorkingBankEntries;
|
use solana::poh_recorder::WorkingBankEntries;
|
||||||
use solana::service::Service;
|
use solana::service::Service;
|
||||||
|
use solana::test_tx::test_tx;
|
||||||
use solana_runtime::bank::Bank;
|
use solana_runtime::bank::Bank;
|
||||||
use solana_sdk::genesis_block::GenesisBlock;
|
|
||||||
use solana_sdk::hash::hash;
|
use solana_sdk::hash::hash;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
use solana_sdk::signature::{KeypairUtil, Signature};
|
use solana_sdk::signature::Signature;
|
||||||
use solana_sdk::system_transaction;
|
use solana_sdk::system_transaction;
|
||||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, MAX_RECENT_BLOCKHASHES};
|
use solana_sdk::timing::{
|
||||||
|
duration_as_ms, timestamp, DEFAULT_TICKS_PER_SLOT, MAX_RECENT_BLOCKHASHES,
|
||||||
|
};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc::{channel, Receiver};
|
use std::sync::mpsc::{channel, Receiver};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
fn check_txs(receiver: &Receiver<WorkingBankEntries>, ref_tx_count: usize) {
|
fn check_txs(receiver: &Arc<Receiver<WorkingBankEntries>>, ref_tx_count: usize) {
|
||||||
let mut total = 0;
|
let mut total = 0;
|
||||||
loop {
|
loop {
|
||||||
let entries = receiver.recv_timeout(Duration::new(1, 0));
|
let entries = receiver.recv_timeout(Duration::new(1, 0));
|
||||||
@ -46,23 +50,63 @@ fn check_txs(receiver: &Receiver<WorkingBankEntries>, ref_tx_count: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
#[ignore]
|
fn bench_consume_buffered(bencher: &mut Bencher) {
|
||||||
|
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(100_000);
|
||||||
|
let bank = Arc::new(Bank::new(&genesis_block));
|
||||||
|
let ledger_path = get_tmp_ledger_path!();
|
||||||
|
let my_pubkey = Pubkey::new_rand();
|
||||||
|
{
|
||||||
|
let blocktree = Arc::new(
|
||||||
|
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger"),
|
||||||
|
);
|
||||||
|
let (exit, poh_recorder, poh_service, _signal_receiver) =
|
||||||
|
create_test_recorder(&bank, &blocktree);
|
||||||
|
|
||||||
|
let tx = test_tx();
|
||||||
|
let len = 4096;
|
||||||
|
let chunk_size = 1024;
|
||||||
|
let batches = to_packets_chunked(&vec![tx; len], chunk_size);
|
||||||
|
let mut packets = vec![];
|
||||||
|
for batch in batches {
|
||||||
|
let batch_len = batch.packets.len();
|
||||||
|
packets.push((batch, vec![0usize; batch_len]));
|
||||||
|
}
|
||||||
|
// This tests the performance of buffering packets.
|
||||||
|
// If the packet buffers are copied, performance will be poor.
|
||||||
|
bencher.iter(move || {
|
||||||
|
let _ignored =
|
||||||
|
BankingStage::consume_buffered_packets(&my_pubkey, &poh_recorder, &mut packets);
|
||||||
|
});
|
||||||
|
|
||||||
|
exit.store(true, Ordering::Relaxed);
|
||||||
|
poh_service.join().unwrap();
|
||||||
|
}
|
||||||
|
let _unused = Blocktree::destroy(&ledger_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||||
|
solana_logger::setup();
|
||||||
let num_threads = BankingStage::num_threads() as usize;
|
let num_threads = BankingStage::num_threads() as usize;
|
||||||
// a multiple of packet chunk 2X duplicates to avoid races
|
// a multiple of packet chunk 2X duplicates to avoid races
|
||||||
let txes = 192 * 50 * num_threads * 2;
|
let txes = 192 * num_threads * 2;
|
||||||
let mint_total = 1_000_000_000_000;
|
let mint_total = 1_000_000_000_000;
|
||||||
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
let GenesisBlockInfo {
|
||||||
|
mut genesis_block,
|
||||||
|
mint_keypair,
|
||||||
|
..
|
||||||
|
} = create_genesis_block(mint_total);
|
||||||
|
|
||||||
|
// Set a high ticks_per_slot so we don't run out of ticks
|
||||||
|
// during the benchmark
|
||||||
|
genesis_block.ticks_per_slot = 10_000;
|
||||||
|
|
||||||
let (verified_sender, verified_receiver) = channel();
|
let (verified_sender, verified_receiver) = channel();
|
||||||
|
let (vote_sender, vote_receiver) = channel();
|
||||||
let bank = Arc::new(Bank::new(&genesis_block));
|
let bank = Arc::new(Bank::new(&genesis_block));
|
||||||
let dummy = system_transaction::transfer(
|
let to_pubkey = Pubkey::new_rand();
|
||||||
&mint_keypair,
|
let dummy = system_transaction::transfer(&mint_keypair, &to_pubkey, 1, genesis_block.hash());
|
||||||
&mint_keypair.pubkey(),
|
trace!("txs: {}", txes);
|
||||||
1,
|
|
||||||
genesis_block.hash(),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
let transactions: Vec<_> = (0..txes)
|
let transactions: Vec<_> = (0..txes)
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|_| {
|
.map(|_| {
|
||||||
@ -83,7 +127,6 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
|||||||
&tx.message.account_keys[0],
|
&tx.message.account_keys[0],
|
||||||
mint_total / txes as u64,
|
mint_total / txes as u64,
|
||||||
genesis_block.hash(),
|
genesis_block.hash(),
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
let x = bank.process_transaction(&fund);
|
let x = bank.process_transaction(&fund);
|
||||||
x.unwrap();
|
x.unwrap();
|
||||||
@ -103,7 +146,7 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
|||||||
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 192)
|
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 192)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
let len = x.read().unwrap().packets.len();
|
let len = x.packets.len();
|
||||||
(x, iter::repeat(1).take(len).collect())
|
(x, iter::repeat(1).take(len).collect())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -116,32 +159,43 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
|||||||
create_test_recorder(&bank, &blocktree);
|
create_test_recorder(&bank, &blocktree);
|
||||||
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||||
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
|
let _banking_stage = BankingStage::new(
|
||||||
|
&cluster_info,
|
||||||
|
&poh_recorder,
|
||||||
|
verified_receiver,
|
||||||
|
vote_receiver,
|
||||||
|
);
|
||||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||||
|
|
||||||
let mut id = genesis_block.hash();
|
|
||||||
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
|
|
||||||
id = hash(&id.as_ref());
|
|
||||||
bank.register_tick(&id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let half_len = verified.len() / 2;
|
let half_len = verified.len() / 2;
|
||||||
let mut start = 0;
|
let mut start = 0;
|
||||||
|
|
||||||
|
// This is so that the signal_receiver does not go out of scope after the closure.
|
||||||
|
// If it is dropped before poh_service, then poh_service will error when
|
||||||
|
// calling send() on the channel.
|
||||||
|
let signal_receiver = Arc::new(signal_receiver);
|
||||||
|
let signal_receiver2 = signal_receiver.clone();
|
||||||
bencher.iter(move || {
|
bencher.iter(move || {
|
||||||
// make sure the transactions are still valid
|
let now = Instant::now();
|
||||||
bank.register_tick(&genesis_block.hash());
|
|
||||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||||
|
trace!("sending... {}..{} {}", start, start + half_len, timestamp());
|
||||||
verified_sender.send(v.to_vec()).unwrap();
|
verified_sender.send(v.to_vec()).unwrap();
|
||||||
}
|
}
|
||||||
check_txs(&signal_receiver, txes / 2);
|
check_txs(&signal_receiver2, txes / 2);
|
||||||
|
trace!(
|
||||||
|
"time: {} checked: {}",
|
||||||
|
duration_as_ms(&now.elapsed()),
|
||||||
|
txes / 2
|
||||||
|
);
|
||||||
bank.clear_signatures();
|
bank.clear_signatures();
|
||||||
start += half_len;
|
start += half_len;
|
||||||
start %= verified.len();
|
start %= verified.len();
|
||||||
});
|
});
|
||||||
|
drop(vote_sender);
|
||||||
exit.store(true, Ordering::Relaxed);
|
exit.store(true, Ordering::Relaxed);
|
||||||
poh_service.join().unwrap();
|
poh_service.join().unwrap();
|
||||||
}
|
}
|
||||||
Blocktree::destroy(&ledger_path).unwrap();
|
let _unused = Blocktree::destroy(&ledger_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
@ -152,17 +206,17 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
|||||||
// a multiple of packet chunk 2X duplicates to avoid races
|
// a multiple of packet chunk 2X duplicates to avoid races
|
||||||
let txes = 96 * 100 * num_threads * 2;
|
let txes = 96 * 100 * num_threads * 2;
|
||||||
let mint_total = 1_000_000_000_000;
|
let mint_total = 1_000_000_000_000;
|
||||||
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
let GenesisBlockInfo {
|
||||||
|
genesis_block,
|
||||||
|
mint_keypair,
|
||||||
|
..
|
||||||
|
} = create_genesis_block(mint_total);
|
||||||
|
|
||||||
let (verified_sender, verified_receiver) = channel();
|
let (verified_sender, verified_receiver) = channel();
|
||||||
|
let (vote_sender, vote_receiver) = channel();
|
||||||
let bank = Arc::new(Bank::new(&genesis_block));
|
let bank = Arc::new(Bank::new(&genesis_block));
|
||||||
let dummy = system_transaction::transfer(
|
let to_pubkey = Pubkey::new_rand();
|
||||||
&mint_keypair,
|
let dummy = system_transaction::transfer(&mint_keypair, &to_pubkey, 1, genesis_block.hash());
|
||||||
&mint_keypair.pubkey(),
|
|
||||||
1,
|
|
||||||
genesis_block.hash(),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
let transactions: Vec<_> = (0..txes)
|
let transactions: Vec<_> = (0..txes)
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|_| {
|
.map(|_| {
|
||||||
@ -199,7 +253,6 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
|||||||
&tx.message.account_keys[0],
|
&tx.message.account_keys[0],
|
||||||
mint_total / txes as u64,
|
mint_total / txes as u64,
|
||||||
genesis_block.hash(),
|
genesis_block.hash(),
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
bank.process_transaction(&fund).unwrap();
|
bank.process_transaction(&fund).unwrap();
|
||||||
});
|
});
|
||||||
@ -218,7 +271,7 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
|||||||
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 96)
|
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 96)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
let len = x.read().unwrap().packets.len();
|
let len = x.packets.len();
|
||||||
(x, iter::repeat(1).take(len).collect())
|
(x, iter::repeat(1).take(len).collect())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -232,7 +285,12 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
|||||||
create_test_recorder(&bank, &blocktree);
|
create_test_recorder(&bank, &blocktree);
|
||||||
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||||
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
|
let _banking_stage = BankingStage::new(
|
||||||
|
&cluster_info,
|
||||||
|
&poh_recorder,
|
||||||
|
verified_receiver,
|
||||||
|
vote_receiver,
|
||||||
|
);
|
||||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||||
|
|
||||||
let mut id = genesis_block.hash();
|
let mut id = genesis_block.hash();
|
||||||
@ -243,17 +301,20 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
|||||||
|
|
||||||
let half_len = verified.len() / 2;
|
let half_len = verified.len() / 2;
|
||||||
let mut start = 0;
|
let mut start = 0;
|
||||||
|
let signal_receiver = Arc::new(signal_receiver);
|
||||||
|
let signal_receiver2 = signal_receiver.clone();
|
||||||
bencher.iter(move || {
|
bencher.iter(move || {
|
||||||
// make sure the transactions are still valid
|
// make sure the transactions are still valid
|
||||||
bank.register_tick(&genesis_block.hash());
|
bank.register_tick(&genesis_block.hash());
|
||||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||||
verified_sender.send(v.to_vec()).unwrap();
|
verified_sender.send(v.to_vec()).unwrap();
|
||||||
}
|
}
|
||||||
check_txs(&signal_receiver, txes / 2);
|
check_txs(&signal_receiver2, txes / 2);
|
||||||
bank.clear_signatures();
|
bank.clear_signatures();
|
||||||
start += half_len;
|
start += half_len;
|
||||||
start %= verified.len();
|
start %= verified.len();
|
||||||
});
|
});
|
||||||
|
drop(vote_sender);
|
||||||
exit.store(true, Ordering::Relaxed);
|
exit.store(true, Ordering::Relaxed);
|
||||||
poh_service.join().unwrap();
|
poh_service.join().unwrap();
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ fn bench_block_to_blobs_to_block(bencher: &mut Bencher) {
|
|||||||
let zero = Hash::default();
|
let zero = Hash::default();
|
||||||
let one = hash(&zero.as_ref());
|
let one = hash(&zero.as_ref());
|
||||||
let keypair = Keypair::new();
|
let keypair = Keypair::new();
|
||||||
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, one, 0);
|
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, one);
|
||||||
let transactions = vec![tx0; 10];
|
let transactions = vec![tx0; 10];
|
||||||
let entries = next_entries(&zero, 1, transactions);
|
let entries = next_entries(&zero, 1, transactions);
|
||||||
|
|
||||||
|
63
core/benches/poh.rs
Normal file
63
core/benches/poh.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// This bench attempts to justify the value of `solana::poh_service::NUM_HASHES_PER_BATCH`
|
||||||
|
|
||||||
|
#![feature(test)]
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use solana::poh::Poh;
|
||||||
|
use solana::poh_service::NUM_HASHES_PER_BATCH;
|
||||||
|
use solana_sdk::hash::Hash;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
const NUM_HASHES: u64 = 30_000; // Should require ~10ms on a 2017 MacBook Pro
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
// No locking. Fastest.
|
||||||
|
fn bench_poh_hash(bencher: &mut Bencher) {
|
||||||
|
let mut poh = Poh::new(Hash::default(), None);
|
||||||
|
bencher.iter(|| {
|
||||||
|
poh.hash(NUM_HASHES);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
// Lock on each iteration. Slowest.
|
||||||
|
fn bench_arc_mutex_poh_hash(bencher: &mut Bencher) {
|
||||||
|
let poh = Arc::new(Mutex::new(Poh::new(Hash::default(), None)));
|
||||||
|
bencher.iter(|| {
|
||||||
|
for _ in 0..NUM_HASHES {
|
||||||
|
poh.lock().unwrap().hash(1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
// Acquire lock every NUM_HASHES_PER_BATCH iterations.
|
||||||
|
// Speed should be close to bench_poh_hash() if NUM_HASHES_PER_BATCH is set well.
|
||||||
|
fn bench_arc_mutex_poh_batched_hash(bencher: &mut Bencher) {
|
||||||
|
let poh = Arc::new(Mutex::new(Poh::new(Hash::default(), Some(NUM_HASHES))));
|
||||||
|
//let exit = Arc::new(AtomicBool::new(false));
|
||||||
|
let exit = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
|
bencher.iter(|| {
|
||||||
|
// NOTE: This block attempts to look as close as possible to `PohService::tick_producer()`
|
||||||
|
loop {
|
||||||
|
if poh.lock().unwrap().hash(NUM_HASHES_PER_BATCH) {
|
||||||
|
poh.lock().unwrap().tick().unwrap();
|
||||||
|
if exit.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
// Worst case transaction record delay due to batch hashing at NUM_HASHES_PER_BATCH
|
||||||
|
fn bench_poh_lock_time_per_batch(bencher: &mut Bencher) {
|
||||||
|
let mut poh = Poh::new(Hash::default(), None);
|
||||||
|
bencher.iter(|| {
|
||||||
|
poh.hash(NUM_HASHES_PER_BATCH);
|
||||||
|
})
|
||||||
|
}
|
83
core/benches/sigverify_stage.rs
Normal file
83
core/benches/sigverify_stage.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate solana;
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use log::*;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
use solana::packet::to_packets_chunked;
|
||||||
|
use solana::service::Service;
|
||||||
|
use solana::sigverify_stage::SigVerifyStage;
|
||||||
|
use solana::test_tx::test_tx;
|
||||||
|
use solana_sdk::hash::Hash;
|
||||||
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
|
use solana_sdk::system_transaction;
|
||||||
|
use solana_sdk::timing::duration_as_ms;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn bench_sigverify_stage(bencher: &mut Bencher) {
|
||||||
|
solana_logger::setup();
|
||||||
|
let (packet_s, packet_r) = channel();
|
||||||
|
let (verified_s, verified_r) = channel();
|
||||||
|
let sigverify_disabled = false;
|
||||||
|
let stage = SigVerifyStage::new(packet_r, sigverify_disabled, verified_s);
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let len = 4096;
|
||||||
|
let use_same_tx = true;
|
||||||
|
let chunk_size = 1024;
|
||||||
|
let mut batches = if use_same_tx {
|
||||||
|
let tx = test_tx();
|
||||||
|
to_packets_chunked(&vec![tx; len], chunk_size)
|
||||||
|
} else {
|
||||||
|
let from_keypair = Keypair::new();
|
||||||
|
let to_keypair = Keypair::new();
|
||||||
|
let txs: Vec<_> = (0..len)
|
||||||
|
.into_iter()
|
||||||
|
.map(|_| {
|
||||||
|
let amount = thread_rng().gen();
|
||||||
|
let tx = system_transaction::transfer(
|
||||||
|
&from_keypair,
|
||||||
|
&to_keypair.pubkey(),
|
||||||
|
amount,
|
||||||
|
Hash::default(),
|
||||||
|
);
|
||||||
|
tx
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
to_packets_chunked(&txs, chunk_size)
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"starting... generation took: {} ms batches: {}",
|
||||||
|
duration_as_ms(&now.elapsed()),
|
||||||
|
batches.len()
|
||||||
|
);
|
||||||
|
bencher.iter(move || {
|
||||||
|
let mut sent_len = 0;
|
||||||
|
for _ in 0..batches.len() {
|
||||||
|
if let Some(batch) = batches.pop() {
|
||||||
|
sent_len += batch.packets.len();
|
||||||
|
packet_s.send(batch).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut received = 0;
|
||||||
|
trace!("sent: {}", sent_len);
|
||||||
|
loop {
|
||||||
|
if let Ok(mut verifieds) = verified_r.recv_timeout(Duration::from_millis(10)) {
|
||||||
|
while let Some(v) = verifieds.pop() {
|
||||||
|
received += v.0.packets.len();
|
||||||
|
batches.push(v.0);
|
||||||
|
}
|
||||||
|
if received >= sent_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace!("received: {}", received);
|
||||||
|
});
|
||||||
|
stage.join().unwrap();
|
||||||
|
}
|
@ -24,9 +24,8 @@ fn main() {
|
|||||||
|
|
||||||
let chacha = !env::var("CARGO_FEATURE_CHACHA").is_err();
|
let chacha = !env::var("CARGO_FEATURE_CHACHA").is_err();
|
||||||
let cuda = !env::var("CARGO_FEATURE_CUDA").is_err();
|
let cuda = !env::var("CARGO_FEATURE_CUDA").is_err();
|
||||||
let erasure = !env::var("CARGO_FEATURE_ERASURE").is_err();
|
|
||||||
|
|
||||||
if chacha || cuda || erasure {
|
if chacha || cuda {
|
||||||
println!("cargo:rerun-if-changed={}", perf_libs_dir);
|
println!("cargo:rerun-if-changed={}", perf_libs_dir);
|
||||||
println!("cargo:rustc-link-search=native={}", perf_libs_dir);
|
println!("cargo:rustc-link-search=native={}", perf_libs_dir);
|
||||||
}
|
}
|
||||||
@ -46,30 +45,4 @@ fn main() {
|
|||||||
println!("cargo:rustc-link-lib=dylib=cuda");
|
println!("cargo:rustc-link-lib=dylib=cuda");
|
||||||
println!("cargo:rustc-link-lib=dylib=cudadevrt");
|
println!("cargo:rustc-link-lib=dylib=cudadevrt");
|
||||||
}
|
}
|
||||||
if erasure {
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
|
||||||
{
|
|
||||||
println!(
|
|
||||||
"cargo:rerun-if-changed={}/libgf_complete.dylib",
|
|
||||||
perf_libs_dir
|
|
||||||
);
|
|
||||||
println!("cargo:rerun-if-changed={}/libJerasure.dylib", perf_libs_dir);
|
|
||||||
}
|
|
||||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
|
|
||||||
{
|
|
||||||
println!("cargo:rerun-if-changed={}/libgf_complete.so", perf_libs_dir);
|
|
||||||
println!("cargo:rerun-if-changed={}/libJerasure.so", perf_libs_dir);
|
|
||||||
}
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
println!(
|
|
||||||
"cargo:rerun-if-changed={}/libgf_complete.dll",
|
|
||||||
perf_libs_dir
|
|
||||||
);
|
|
||||||
println!("cargo:rerun-if-changed={}/libJerasure.dll", perf_libs_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("cargo:rustc-link-lib=dylib=Jerasure");
|
|
||||||
println!("cargo:rustc-link-lib=dylib=gf_complete");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//! The `bank_forks` module implments BankForks a DAG of checkpointed Banks
|
//! The `bank_forks` module implments BankForks a DAG of checkpointed Banks
|
||||||
|
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use solana_metrics::counter::Counter;
|
use solana_metrics::inc_new_counter_info;
|
||||||
use solana_runtime::bank::Bank;
|
use solana_runtime::bank::Bank;
|
||||||
use solana_sdk::timing;
|
use solana_sdk::timing;
|
||||||
use std::ops::Index;
|
use std::ops::Index;
|
||||||
@ -11,6 +11,7 @@ use std::time::Instant;
|
|||||||
pub struct BankForks {
|
pub struct BankForks {
|
||||||
banks: HashMap<u64, Arc<Bank>>,
|
banks: HashMap<u64, Arc<Bank>>,
|
||||||
working_bank: Arc<Bank>,
|
working_bank: Arc<Bank>,
|
||||||
|
root: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Index<u64> for BankForks {
|
impl Index<u64> for BankForks {
|
||||||
@ -28,6 +29,7 @@ impl BankForks {
|
|||||||
Self {
|
Self {
|
||||||
banks,
|
banks,
|
||||||
working_bank,
|
working_bank,
|
||||||
|
root: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +37,8 @@ impl BankForks {
|
|||||||
pub fn ancestors(&self) -> HashMap<u64, HashSet<u64>> {
|
pub fn ancestors(&self) -> HashMap<u64, HashSet<u64>> {
|
||||||
let mut ancestors = HashMap::new();
|
let mut ancestors = HashMap::new();
|
||||||
for bank in self.banks.values() {
|
for bank in self.banks.values() {
|
||||||
let set = bank.parents().into_iter().map(|b| b.slot()).collect();
|
let mut set: HashSet<u64> = bank.ancestors.keys().cloned().collect();
|
||||||
|
set.remove(&bank.slot());
|
||||||
ancestors.insert(bank.slot(), set);
|
ancestors.insert(bank.slot(), set);
|
||||||
}
|
}
|
||||||
ancestors
|
ancestors
|
||||||
@ -46,9 +49,11 @@ impl BankForks {
|
|||||||
let mut descendants = HashMap::new();
|
let mut descendants = HashMap::new();
|
||||||
for bank in self.banks.values() {
|
for bank in self.banks.values() {
|
||||||
let _ = descendants.entry(bank.slot()).or_insert(HashSet::new());
|
let _ = descendants.entry(bank.slot()).or_insert(HashSet::new());
|
||||||
for parent in bank.parents() {
|
let mut set: HashSet<u64> = bank.ancestors.keys().cloned().collect();
|
||||||
|
set.remove(&bank.slot());
|
||||||
|
for parent in set {
|
||||||
descendants
|
descendants
|
||||||
.entry(parent.slot())
|
.entry(parent)
|
||||||
.or_insert(HashSet::new())
|
.or_insert(HashSet::new())
|
||||||
.insert(bank.slot());
|
.insert(bank.slot());
|
||||||
}
|
}
|
||||||
@ -76,13 +81,14 @@ impl BankForks {
|
|||||||
self.banks.get(&bank_slot)
|
self.banks.get(&bank_slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_from_banks(initial_banks: &[Arc<Bank>]) -> Self {
|
pub fn new_from_banks(initial_banks: &[Arc<Bank>], root: u64) -> Self {
|
||||||
let mut banks = HashMap::new();
|
let mut banks = HashMap::new();
|
||||||
let working_bank = initial_banks[0].clone();
|
let working_bank = initial_banks[0].clone();
|
||||||
for bank in initial_banks {
|
for bank in initial_banks {
|
||||||
banks.insert(bank.slot(), bank.clone());
|
banks.insert(bank.slot(), bank.clone());
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
|
root,
|
||||||
banks,
|
banks,
|
||||||
working_bank,
|
working_bank,
|
||||||
}
|
}
|
||||||
@ -102,35 +108,52 @@ impl BankForks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_root(&mut self, root: u64) {
|
pub fn set_root(&mut self, root: u64) {
|
||||||
|
self.root = root;
|
||||||
let set_root_start = Instant::now();
|
let set_root_start = Instant::now();
|
||||||
let root_bank = self
|
let root_bank = self
|
||||||
.banks
|
.banks
|
||||||
.get(&root)
|
.get(&root)
|
||||||
.expect("root bank didn't exist in bank_forks");
|
.expect("root bank didn't exist in bank_forks");
|
||||||
|
let root_tx_count = root_bank
|
||||||
|
.parents()
|
||||||
|
.last()
|
||||||
|
.map(|bank| bank.transaction_count())
|
||||||
|
.unwrap_or(0);
|
||||||
root_bank.squash();
|
root_bank.squash();
|
||||||
|
let new_tx_count = root_bank.transaction_count();
|
||||||
self.prune_non_root(root);
|
self.prune_non_root(root);
|
||||||
|
|
||||||
inc_new_counter_info!(
|
inc_new_counter_info!(
|
||||||
"bank-forks_set_root_ms",
|
"bank-forks_set_root_ms",
|
||||||
timing::duration_as_ms(&set_root_start.elapsed()) as usize
|
timing::duration_as_ms(&set_root_start.elapsed()) as usize
|
||||||
);
|
);
|
||||||
|
inc_new_counter_info!(
|
||||||
|
"bank-forks_set_root_tx_count",
|
||||||
|
(new_tx_count - root_tx_count) as usize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root(&self) -> u64 {
|
||||||
|
self.root
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune_non_root(&mut self, root: u64) {
|
fn prune_non_root(&mut self, root: u64) {
|
||||||
|
let descendants = self.descendants();
|
||||||
self.banks
|
self.banks
|
||||||
.retain(|slot, bank| *slot >= root || bank.is_in_subtree_of(root))
|
.retain(|slot, _| descendants[&root].contains(slot))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use solana_sdk::genesis_block::GenesisBlock;
|
use crate::genesis_utils::{create_genesis_block, GenesisBlockInfo};
|
||||||
use solana_sdk::hash::Hash;
|
use solana_sdk::hash::Hash;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bank_forks() {
|
fn test_bank_forks() {
|
||||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||||
let bank = Bank::new(&genesis_block);
|
let bank = Bank::new(&genesis_block);
|
||||||
let mut bank_forks = BankForks::new(0, bank);
|
let mut bank_forks = BankForks::new(0, bank);
|
||||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||||
@ -142,7 +165,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bank_forks_descendants() {
|
fn test_bank_forks_descendants() {
|
||||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||||
let bank = Bank::new(&genesis_block);
|
let bank = Bank::new(&genesis_block);
|
||||||
let mut bank_forks = BankForks::new(0, bank);
|
let mut bank_forks = BankForks::new(0, bank);
|
||||||
let bank0 = bank_forks[0].clone();
|
let bank0 = bank_forks[0].clone();
|
||||||
@ -159,7 +182,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bank_forks_ancestors() {
|
fn test_bank_forks_ancestors() {
|
||||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||||
let bank = Bank::new(&genesis_block);
|
let bank = Bank::new(&genesis_block);
|
||||||
let mut bank_forks = BankForks::new(0, bank);
|
let mut bank_forks = BankForks::new(0, bank);
|
||||||
let bank0 = bank_forks[0].clone();
|
let bank0 = bank_forks[0].clone();
|
||||||
@ -177,7 +200,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bank_forks_frozen_banks() {
|
fn test_bank_forks_frozen_banks() {
|
||||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||||
let bank = Bank::new(&genesis_block);
|
let bank = Bank::new(&genesis_block);
|
||||||
let mut bank_forks = BankForks::new(0, bank);
|
let mut bank_forks = BankForks::new(0, bank);
|
||||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||||
@ -188,7 +211,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bank_forks_active_banks() {
|
fn test_bank_forks_active_banks() {
|
||||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||||
let bank = Bank::new(&genesis_block);
|
let bank = Bank::new(&genesis_block);
|
||||||
let mut bank_forks = BankForks::new(0, bank);
|
let mut bank_forks = BankForks::new(0, bank);
|
||||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -65,14 +65,14 @@ pub trait BlockstreamEvents {
|
|||||||
&self,
|
&self,
|
||||||
slot: u64,
|
slot: u64,
|
||||||
tick_height: u64,
|
tick_height: u64,
|
||||||
leader_id: &Pubkey,
|
leader_pubkey: &Pubkey,
|
||||||
entries: &Entry,
|
entries: &Entry,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
fn emit_block_event(
|
fn emit_block_event(
|
||||||
&self,
|
&self,
|
||||||
slot: u64,
|
slot: u64,
|
||||||
tick_height: u64,
|
tick_height: u64,
|
||||||
leader_id: &Pubkey,
|
leader_pubkey: &Pubkey,
|
||||||
blockhash: Hash,
|
blockhash: Hash,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ where
|
|||||||
&self,
|
&self,
|
||||||
slot: u64,
|
slot: u64,
|
||||||
tick_height: u64,
|
tick_height: u64,
|
||||||
leader_id: &Pubkey,
|
leader_pubkey: &Pubkey,
|
||||||
entry: &Entry,
|
entry: &Entry,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let transactions: Vec<Vec<u8>> = serialize_transactions(entry);
|
let transactions: Vec<Vec<u8>> = serialize_transactions(entry);
|
||||||
@ -105,7 +105,7 @@ where
|
|||||||
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true),
|
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true),
|
||||||
slot,
|
slot,
|
||||||
tick_height,
|
tick_height,
|
||||||
leader_id,
|
leader_pubkey,
|
||||||
json_entry,
|
json_entry,
|
||||||
);
|
);
|
||||||
self.output.write(payload)?;
|
self.output.write(payload)?;
|
||||||
@ -116,7 +116,7 @@ where
|
|||||||
&self,
|
&self,
|
||||||
slot: u64,
|
slot: u64,
|
||||||
tick_height: u64,
|
tick_height: u64,
|
||||||
leader_id: &Pubkey,
|
leader_pubkey: &Pubkey,
|
||||||
blockhash: Hash,
|
blockhash: Hash,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let payload = format!(
|
let payload = format!(
|
||||||
@ -124,7 +124,7 @@ where
|
|||||||
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true),
|
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true),
|
||||||
slot,
|
slot,
|
||||||
tick_height,
|
tick_height,
|
||||||
leader_id,
|
leader_pubkey,
|
||||||
blockhash,
|
blockhash,
|
||||||
);
|
);
|
||||||
self.output.write(payload)?;
|
self.output.write(payload)?;
|
||||||
@ -183,10 +183,8 @@ mod test {
|
|||||||
|
|
||||||
let keypair0 = Keypair::new();
|
let keypair0 = Keypair::new();
|
||||||
let keypair1 = Keypair::new();
|
let keypair1 = Keypair::new();
|
||||||
let tx0 =
|
let tx0 = system_transaction::transfer(&keypair0, &keypair1.pubkey(), 1, Hash::default());
|
||||||
system_transaction::transfer(&keypair0, &keypair1.pubkey(), 1, Hash::default(), 0);
|
let tx1 = system_transaction::transfer(&keypair1, &keypair0.pubkey(), 2, Hash::default());
|
||||||
let tx1 =
|
|
||||||
system_transaction::transfer(&keypair1, &keypair0.pubkey(), 2, Hash::default(), 0);
|
|
||||||
let serialized_tx0 = serialize(&tx0).unwrap();
|
let serialized_tx0 = serialize(&tx0).unwrap();
|
||||||
let serialized_tx1 = serialize(&tx1).unwrap();
|
let serialized_tx1 = serialize(&tx1).unwrap();
|
||||||
let entry = Entry::new(&Hash::default(), 1, vec![tx0, tx1]);
|
let entry = Entry::new(&Hash::default(), 1, vec![tx0, tx1]);
|
||||||
@ -208,19 +206,19 @@ mod test {
|
|||||||
let tick_height_initial = 0;
|
let tick_height_initial = 0;
|
||||||
let tick_height_final = tick_height_initial + ticks_per_slot + 2;
|
let tick_height_final = tick_height_initial + ticks_per_slot + 2;
|
||||||
let mut curr_slot = 0;
|
let mut curr_slot = 0;
|
||||||
let leader_id = Pubkey::new_rand();
|
let leader_pubkey = Pubkey::new_rand();
|
||||||
|
|
||||||
for tick_height in tick_height_initial..=tick_height_final {
|
for tick_height in tick_height_initial..=tick_height_final {
|
||||||
if tick_height == 5 {
|
if tick_height == 5 {
|
||||||
blockstream
|
blockstream
|
||||||
.emit_block_event(curr_slot, tick_height - 1, &leader_id, blockhash)
|
.emit_block_event(curr_slot, tick_height - 1, &leader_pubkey, blockhash)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
curr_slot += 1;
|
curr_slot += 1;
|
||||||
}
|
}
|
||||||
let entry = Entry::new(&mut blockhash, 1, vec![]); // just ticks
|
let entry = Entry::new(&mut blockhash, 1, vec![]); // just ticks
|
||||||
blockhash = entry.hash;
|
blockhash = entry.hash;
|
||||||
blockstream
|
blockstream
|
||||||
.emit_entry_event(curr_slot, tick_height, &leader_id, &entry)
|
.emit_entry_event(curr_slot, tick_height, &leader_pubkey, &entry)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
expected_entries.push(entry.clone());
|
expected_entries.push(entry.clone());
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
|
@ -109,10 +109,10 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::blocktree::create_new_tmp_ledger;
|
use crate::blocktree::create_new_tmp_ledger;
|
||||||
use crate::entry::{create_ticks, Entry};
|
use crate::entry::{create_ticks, Entry};
|
||||||
|
use crate::genesis_utils::{create_genesis_block, GenesisBlockInfo};
|
||||||
use bincode::{deserialize, serialize};
|
use bincode::{deserialize, serialize};
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use solana_sdk::genesis_block::GenesisBlock;
|
|
||||||
use solana_sdk::hash::Hash;
|
use solana_sdk::hash::Hash;
|
||||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
use solana_sdk::system_transaction;
|
use solana_sdk::system_transaction;
|
||||||
@ -121,10 +121,12 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_blockstream_service_process_entries() {
|
fn test_blockstream_service_process_entries() {
|
||||||
let ticks_per_slot = 5;
|
let ticks_per_slot = 5;
|
||||||
let leader_id = Pubkey::new_rand();
|
let leader_pubkey = Pubkey::new_rand();
|
||||||
|
|
||||||
// Set up genesis block and blocktree
|
// Set up genesis block and blocktree
|
||||||
let (mut genesis_block, _mint_keypair) = GenesisBlock::new(1000);
|
let GenesisBlockInfo {
|
||||||
|
mut genesis_block, ..
|
||||||
|
} = create_genesis_block(1000);
|
||||||
genesis_block.ticks_per_slot = ticks_per_slot;
|
genesis_block.ticks_per_slot = ticks_per_slot;
|
||||||
|
|
||||||
let (ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block);
|
let (ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block);
|
||||||
@ -146,7 +148,6 @@ mod test {
|
|||||||
&keypair.pubkey(),
|
&keypair.pubkey(),
|
||||||
1,
|
1,
|
||||||
Hash::default(),
|
Hash::default(),
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
let entry = Entry::new(&mut blockhash, 1, vec![tx]);
|
let entry = Entry::new(&mut blockhash, 1, vec![tx]);
|
||||||
blockhash = entry.hash;
|
blockhash = entry.hash;
|
||||||
@ -161,7 +162,7 @@ mod test {
|
|||||||
.write_entries(1, 0, 0, ticks_per_slot, &entries)
|
.write_entries(1, 0, 0, ticks_per_slot, &entries)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
slot_full_sender.send((1, leader_id)).unwrap();
|
slot_full_sender.send((1, leader_pubkey)).unwrap();
|
||||||
BlockstreamService::process_entries(
|
BlockstreamService::process_entries(
|
||||||
&slot_full_receiver,
|
&slot_full_receiver,
|
||||||
&Arc::new(blocktree),
|
&Arc::new(blocktree),
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -28,10 +28,13 @@ pub mod columns {
|
|||||||
/// Data Column
|
/// Data Column
|
||||||
pub struct Data;
|
pub struct Data;
|
||||||
|
|
||||||
#[cfg(feature = "erasure")]
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// The erasure meta column
|
/// The erasure meta column
|
||||||
pub struct ErasureMeta;
|
pub struct ErasureMeta;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// The root column
|
||||||
|
pub struct Root;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Backend: Sized + Send + Sync {
|
pub trait Backend: Sized + Send + Sync {
|
||||||
@ -57,7 +60,7 @@ pub trait Backend: Sized + Send + Sync {
|
|||||||
|
|
||||||
fn delete_cf(&self, cf: Self::ColumnFamily, key: &Self::Key) -> Result<()>;
|
fn delete_cf(&self, cf: Self::ColumnFamily, key: &Self::Key) -> Result<()>;
|
||||||
|
|
||||||
fn iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Iter>;
|
fn iterator_cf(&self, cf: Self::ColumnFamily, from: Option<&Self::Key>) -> Result<Self::Iter>;
|
||||||
|
|
||||||
fn raw_iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Cursor>;
|
fn raw_iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Cursor>;
|
||||||
|
|
||||||
@ -114,7 +117,15 @@ pub struct Database<B>
|
|||||||
where
|
where
|
||||||
B: Backend,
|
B: Backend,
|
||||||
{
|
{
|
||||||
backend: B,
|
backend: Arc<B>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BatchProcessor<B>
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
backend: Arc<B>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -134,7 +145,7 @@ where
|
|||||||
B: Backend,
|
B: Backend,
|
||||||
C: Column<B>,
|
C: Column<B>,
|
||||||
{
|
{
|
||||||
pub db: Arc<Database<B>>,
|
backend: Arc<B>,
|
||||||
column: PhantomData<C>,
|
column: PhantomData<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +164,7 @@ where
|
|||||||
B: Backend,
|
B: Backend,
|
||||||
{
|
{
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
let backend = B::open(path)?;
|
let backend = Arc::new(B::open(path)?);
|
||||||
|
|
||||||
Ok(Database { backend })
|
Ok(Database { backend })
|
||||||
}
|
}
|
||||||
@ -230,19 +241,61 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter<C>(&self) -> Result<impl Iterator<Item = (C::Index, Vec<u8>)>>
|
pub fn iter<C>(
|
||||||
|
&self,
|
||||||
|
start_from: Option<C::Index>,
|
||||||
|
) -> Result<impl Iterator<Item = (C::Index, Box<[u8]>)>>
|
||||||
where
|
where
|
||||||
C: Column<B>,
|
C: Column<B>,
|
||||||
{
|
{
|
||||||
let iter = self
|
let iter = {
|
||||||
.backend
|
if let Some(index) = start_from {
|
||||||
.iterator_cf(self.cf_handle::<C>())?
|
let key = C::key(index);
|
||||||
.map(|(key, value)| (C::index(&key), value.into()));
|
self.backend
|
||||||
|
.iterator_cf(self.cf_handle::<C>(), Some(key.borrow()))?
|
||||||
|
} else {
|
||||||
|
self.backend.iterator_cf(self.cf_handle::<C>(), None)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(iter)
|
Ok(iter.map(|(key, value)| (C::index(&key), value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn batch(&self) -> Result<WriteBatch<B>> {
|
#[inline]
|
||||||
|
pub fn cf_handle<C>(&self) -> B::ColumnFamily
|
||||||
|
where
|
||||||
|
C: Column<B>,
|
||||||
|
{
|
||||||
|
self.backend.cf_handle(C::NAME).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column<C>(&self) -> LedgerColumn<B, C>
|
||||||
|
where
|
||||||
|
C: Column<B>,
|
||||||
|
{
|
||||||
|
LedgerColumn {
|
||||||
|
backend: Arc::clone(&self.backend),
|
||||||
|
column: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note this returns an object that can be used to directly write to multiple column families.
|
||||||
|
// This circumvents the synchronization around APIs that in Blocktree that use
|
||||||
|
// blocktree.batch_processor, so this API should only be used if the caller is sure they
|
||||||
|
// are writing to data in columns that will not be corrupted by any simultaneous blocktree
|
||||||
|
// operations.
|
||||||
|
pub unsafe fn batch_processor(&self) -> BatchProcessor<B> {
|
||||||
|
BatchProcessor {
|
||||||
|
backend: Arc::clone(&self.backend),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B> BatchProcessor<B>
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
pub fn batch(&mut self) -> Result<WriteBatch<B>> {
|
||||||
let db_write_batch = self.backend.batch()?;
|
let db_write_batch = self.backend.batch()?;
|
||||||
let map = self
|
let map = self
|
||||||
.backend
|
.backend
|
||||||
@ -258,17 +311,9 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&self, batch: WriteBatch<B>) -> Result<()> {
|
pub fn write(&mut self, batch: WriteBatch<B>) -> Result<()> {
|
||||||
self.backend.write(batch.write_batch)
|
self.backend.write(batch.write_batch)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn cf_handle<C>(&self) -> B::ColumnFamily
|
|
||||||
where
|
|
||||||
C: Column<B>,
|
|
||||||
{
|
|
||||||
self.backend.cf_handle(C::NAME).clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B, C> Cursor<B, C>
|
impl<B, C> Cursor<B, C>
|
||||||
@ -325,39 +370,40 @@ where
|
|||||||
B: Backend,
|
B: Backend,
|
||||||
C: Column<B>,
|
C: Column<B>,
|
||||||
{
|
{
|
||||||
pub fn new(db: &Arc<Database<B>>) -> Self {
|
|
||||||
LedgerColumn {
|
|
||||||
db: Arc::clone(db),
|
|
||||||
column: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn put_bytes(&self, key: C::Index, value: &[u8]) -> Result<()> {
|
|
||||||
self.db
|
|
||||||
.backend
|
|
||||||
.put_cf(self.handle(), C::key(key).borrow(), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_bytes(&self, key: C::Index) -> Result<Option<Vec<u8>>> {
|
pub fn get_bytes(&self, key: C::Index) -> Result<Option<Vec<u8>>> {
|
||||||
self.db.backend.get_cf(self.handle(), C::key(key).borrow())
|
self.backend.get_cf(self.handle(), C::key(key).borrow())
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(&self, key: C::Index) -> Result<()> {
|
|
||||||
self.db
|
|
||||||
.backend
|
|
||||||
.delete_cf(self.handle(), C::key(key).borrow())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor(&self) -> Result<Cursor<B, C>> {
|
pub fn cursor(&self) -> Result<Cursor<B, C>> {
|
||||||
self.db.cursor()
|
let db_cursor = self.backend.raw_iterator_cf(self.handle())?;
|
||||||
|
|
||||||
|
Ok(Cursor {
|
||||||
|
db_cursor,
|
||||||
|
column: PhantomData,
|
||||||
|
backend: PhantomData,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> Result<impl Iterator<Item = (C::Index, Vec<u8>)>> {
|
pub fn iter(
|
||||||
self.db.iter::<C>()
|
&self,
|
||||||
|
start_from: Option<C::Index>,
|
||||||
|
) -> Result<impl Iterator<Item = (C::Index, Box<[u8]>)>> {
|
||||||
|
let iter = {
|
||||||
|
if let Some(index) = start_from {
|
||||||
|
let key = C::key(index);
|
||||||
|
self.backend
|
||||||
|
.iterator_cf(self.handle(), Some(key.borrow()))?
|
||||||
|
} else {
|
||||||
|
self.backend.iterator_cf(self.handle(), None)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(iter.map(|(key, value)| (C::index(&key), value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn handle(&self) -> B::ColumnFamily {
|
pub fn handle(&self) -> B::ColumnFamily {
|
||||||
self.db.cf_handle::<C>()
|
self.backend.cf_handle(C::NAME).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> Result<bool> {
|
pub fn is_empty(&self) -> Result<bool> {
|
||||||
@ -365,6 +411,15 @@ where
|
|||||||
cursor.seek_to_first();
|
cursor.seek_to_first();
|
||||||
Ok(!cursor.valid())
|
Ok(!cursor.valid())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn put_bytes(&self, key: C::Index, value: &[u8]) -> Result<()> {
|
||||||
|
self.backend
|
||||||
|
.put_cf(self.handle(), C::key(key).borrow(), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&self, key: C::Index) -> Result<()> {
|
||||||
|
self.backend.delete_cf(self.handle(), C::key(key).borrow())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B, C> LedgerColumn<B, C>
|
impl<B, C> LedgerColumn<B, C>
|
||||||
@ -372,12 +427,21 @@ where
|
|||||||
B: Backend,
|
B: Backend,
|
||||||
C: TypedColumn<B>,
|
C: TypedColumn<B>,
|
||||||
{
|
{
|
||||||
pub fn put(&self, key: C::Index, value: &C::Type) -> Result<()> {
|
pub fn get(&self, key: C::Index) -> Result<Option<C::Type>> {
|
||||||
self.db.put::<C>(key, value)
|
if let Some(serialized_value) = self.backend.get_cf(self.handle(), C::key(key).borrow())? {
|
||||||
|
let value = deserialize(&serialized_value)?;
|
||||||
|
|
||||||
|
Ok(Some(value))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: C::Index) -> Result<Option<C::Type>> {
|
pub fn put(&self, key: C::Index, value: &C::Type) -> Result<()> {
|
||||||
self.db.get::<C>(key)
|
let serialized_value = serialize(value)?;
|
||||||
|
|
||||||
|
self.backend
|
||||||
|
.put_cf(self.handle(), C::key(key).borrow(), &serialized_value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +119,40 @@ impl TypedColumn<Kvs> for cf::Orphans {
|
|||||||
type Type = bool;
|
type Type = bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Column<Kvs> for cf::Root {
|
||||||
|
const NAME: &'static str = super::ROOT_CF;
|
||||||
|
type Index = u64;
|
||||||
|
|
||||||
|
fn key(slot: u64) -> Key {
|
||||||
|
let mut key = Key::default();
|
||||||
|
BigEndian::write_u64(&mut key.0[8..16], slot);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(key: &Key) -> u64 {
|
||||||
|
BigEndian::read_u64(&key.0[8..16])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypedColumn<Kvs> for cf::Root {
|
||||||
|
type Type = bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Column<Kvs> for cf::SlotMeta {
|
||||||
|
const NAME: &'static str = super::META_CF;
|
||||||
|
type Index = u64;
|
||||||
|
|
||||||
|
fn key(slot: u64) -> Key {
|
||||||
|
let mut key = Key::default();
|
||||||
|
BigEndian::write_u64(&mut key.0[8..16], slot);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(key: &Key) -> u64 {
|
||||||
|
BigEndian::read_u64(&key.0[8..16])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Column<Kvs> for cf::SlotMeta {
|
impl Column<Kvs> for cf::SlotMeta {
|
||||||
const NAME: &'static str = super::META_CF;
|
const NAME: &'static str = super::META_CF;
|
||||||
type Index = u64;
|
type Index = u64;
|
||||||
@ -138,7 +172,6 @@ impl TypedColumn<Kvs> for cf::SlotMeta {
|
|||||||
type Type = super::SlotMeta;
|
type Type = super::SlotMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "erasure")]
|
|
||||||
impl Column<Kvs> for cf::ErasureMeta {
|
impl Column<Kvs> for cf::ErasureMeta {
|
||||||
const NAME: &'static str = super::ERASURE_META_CF;
|
const NAME: &'static str = super::ERASURE_META_CF;
|
||||||
type Index = (u64, u64);
|
type Index = (u64, u64);
|
||||||
@ -157,7 +190,6 @@ impl Column<Kvs> for cf::ErasureMeta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "erasure")]
|
|
||||||
impl TypedColumn<Kvs> for cf::ErasureMeta {
|
impl TypedColumn<Kvs> for cf::ErasureMeta {
|
||||||
type Type = super::ErasureMeta;
|
type Type = super::ErasureMeta;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#[cfg(feature = "erasure")]
|
|
||||||
use crate::erasure::{NUM_CODING, NUM_DATA};
|
use crate::erasure::{NUM_CODING, NUM_DATA};
|
||||||
|
use solana_metrics::datapoint;
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
|
||||||
// The Meta column family
|
// The Meta column family
|
||||||
@ -24,8 +25,6 @@ pub struct SlotMeta {
|
|||||||
// True if this slot is full (consumed == last_index + 1) and if every
|
// True if this slot is full (consumed == last_index + 1) and if every
|
||||||
// slot that is a parent of this slot is also connected.
|
// slot that is a parent of this slot is also connected.
|
||||||
pub is_connected: bool,
|
pub is_connected: bool,
|
||||||
// True if this slot is a root
|
|
||||||
pub is_root: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlotMeta {
|
impl SlotMeta {
|
||||||
@ -36,7 +35,22 @@ impl SlotMeta {
|
|||||||
if self.last_index == std::u64::MAX {
|
if self.last_index == std::u64::MAX {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
assert!(self.consumed <= self.last_index + 1);
|
|
||||||
|
// Should never happen
|
||||||
|
if self.consumed > self.last_index + 1 {
|
||||||
|
datapoint!(
|
||||||
|
"blocktree_error",
|
||||||
|
(
|
||||||
|
"error",
|
||||||
|
format!(
|
||||||
|
"Observed a slot meta with consumed: {} > meta.last_index + 1: {}",
|
||||||
|
self.consumed,
|
||||||
|
self.last_index + 1
|
||||||
|
),
|
||||||
|
String
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
self.consumed == self.last_index + 1
|
self.consumed == self.last_index + 1
|
||||||
}
|
}
|
||||||
@ -53,74 +67,123 @@ impl SlotMeta {
|
|||||||
parent_slot,
|
parent_slot,
|
||||||
next_slots: vec![],
|
next_slots: vec![],
|
||||||
is_connected: slot == 0,
|
is_connected: slot == 0,
|
||||||
is_root: false,
|
|
||||||
last_index: std::u64::MAX,
|
last_index: std::u64::MAX,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "erasure")]
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
|
|
||||||
/// Erasure coding information
|
/// Erasure coding information
|
||||||
pub struct ErasureMeta {
|
pub struct ErasureMeta {
|
||||||
/// Which erasure set in the slot this is
|
/// Which erasure set in the slot this is
|
||||||
pub set_index: u64,
|
pub set_index: u64,
|
||||||
|
/// Size of shards in this erasure set
|
||||||
|
pub size: usize,
|
||||||
/// Bitfield representing presence/absence of data blobs
|
/// Bitfield representing presence/absence of data blobs
|
||||||
pub data: u64,
|
data: u64,
|
||||||
/// Bitfield representing presence/absence of coding blobs
|
/// Bitfield representing presence/absence of coding blobs
|
||||||
pub coding: u64,
|
coding: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum ErasureMetaStatus {
|
||||||
|
CanRecover,
|
||||||
|
DataFull,
|
||||||
|
StillNeed(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "erasure")]
|
|
||||||
impl ErasureMeta {
|
impl ErasureMeta {
|
||||||
pub fn new(set_index: u64) -> ErasureMeta {
|
pub fn new(set_index: u64) -> ErasureMeta {
|
||||||
ErasureMeta {
|
ErasureMeta {
|
||||||
set_index,
|
set_index,
|
||||||
|
size: 0,
|
||||||
data: 0,
|
data: 0,
|
||||||
coding: 0,
|
coding: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_recover(&self) -> bool {
|
pub fn status(&self) -> ErasureMetaStatus {
|
||||||
let (data_missing, coding_missing) = (
|
let (data_missing, coding_missing) =
|
||||||
NUM_DATA - self.data.count_ones() as usize,
|
(NUM_DATA - self.num_data(), NUM_CODING - self.num_coding());
|
||||||
NUM_CODING - self.coding.count_ones() as usize,
|
if data_missing > 0 && data_missing + coding_missing <= NUM_CODING {
|
||||||
);
|
assert!(self.size != 0);
|
||||||
|
ErasureMetaStatus::CanRecover
|
||||||
|
} else if data_missing == 0 {
|
||||||
|
ErasureMetaStatus::DataFull
|
||||||
|
} else {
|
||||||
|
ErasureMetaStatus::StillNeed(data_missing + coding_missing - NUM_CODING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data_missing > 0 && data_missing + coding_missing <= NUM_CODING
|
pub fn num_coding(&self) -> usize {
|
||||||
|
self.coding.count_ones() as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_data(&self) -> usize {
|
||||||
|
self.data.count_ones() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_coding_present(&self, index: u64) -> bool {
|
pub fn is_coding_present(&self, index: u64) -> bool {
|
||||||
let set_index = Self::set_index_for(index);
|
if let Some(position) = self.data_index_in_set(index) {
|
||||||
let position = index - self.start_index();
|
self.coding & (1 << position) != 0
|
||||||
|
} else {
|
||||||
set_index == self.set_index && self.coding & (1 << position) != 0
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_coding_present(&mut self, index: u64) {
|
pub fn set_size(&mut self, size: usize) {
|
||||||
let set_index = Self::set_index_for(index);
|
self.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
if set_index as u64 == self.set_index {
|
pub fn size(&self) -> usize {
|
||||||
let position = index - self.start_index();
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_coding_present(&mut self, index: u64, present: bool) {
|
||||||
|
if let Some(position) = self.data_index_in_set(index) {
|
||||||
|
if present {
|
||||||
self.coding |= 1 << position;
|
self.coding |= 1 << position;
|
||||||
|
} else {
|
||||||
|
self.coding &= !(1 << position);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_data_present(&self, index: u64) -> bool {
|
pub fn is_data_present(&self, index: u64) -> bool {
|
||||||
let set_index = Self::set_index_for(index);
|
if let Some(position) = self.data_index_in_set(index) {
|
||||||
let position = index - self.start_index();
|
self.data & (1 << position) != 0
|
||||||
|
} else {
|
||||||
set_index == self.set_index && self.data & (1 << position) != 0
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_data_present(&mut self, index: u64) {
|
pub fn set_data_present(&mut self, index: u64, present: bool) {
|
||||||
let set_index = Self::set_index_for(index);
|
if let Some(position) = self.data_index_in_set(index) {
|
||||||
|
if present {
|
||||||
if set_index as u64 == self.set_index {
|
|
||||||
let position = index - self.start_index();
|
|
||||||
|
|
||||||
self.data |= 1 << position;
|
self.data |= 1 << position;
|
||||||
|
} else {
|
||||||
|
self.data &= !(1 << position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_data_multi<I, Idx>(&mut self, indexes: I, present: bool)
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Idx>,
|
||||||
|
Idx: Borrow<u64>,
|
||||||
|
{
|
||||||
|
for index in indexes.into_iter() {
|
||||||
|
self.set_data_present(*index.borrow(), present);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_coding_multi<I, Idx>(&mut self, indexes: I, present: bool)
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Idx>,
|
||||||
|
Idx: Borrow<u64>,
|
||||||
|
{
|
||||||
|
for index in indexes.into_iter() {
|
||||||
|
self.set_coding_present(*index.borrow(), present);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +191,20 @@ impl ErasureMeta {
|
|||||||
index / NUM_DATA as u64
|
index / NUM_DATA as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn data_index_in_set(&self, index: u64) -> Option<u64> {
|
||||||
|
let set_index = Self::set_index_for(index);
|
||||||
|
|
||||||
|
if set_index == self.set_index {
|
||||||
|
Some(index - self.start_index())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn coding_index_in_set(&self, index: u64) -> Option<u64> {
|
||||||
|
self.data_index_in_set(index).map(|i| i + NUM_DATA as u64)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start_index(&self) -> u64 {
|
pub fn start_index(&self) -> u64 {
|
||||||
self.set_index * NUM_DATA as u64
|
self.set_index * NUM_DATA as u64
|
||||||
}
|
}
|
||||||
@ -139,18 +216,53 @@ impl ErasureMeta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "erasure")]
|
#[test]
|
||||||
|
fn test_meta_indexes() {
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
// to avoid casts everywhere
|
||||||
|
const NUM_DATA: u64 = crate::erasure::NUM_DATA as u64;
|
||||||
|
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
for _ in 0..100 {
|
||||||
|
let set_index = rng.gen_range(0, 1_000);
|
||||||
|
let blob_index = (set_index * NUM_DATA) + rng.gen_range(0, NUM_DATA);
|
||||||
|
|
||||||
|
assert_eq!(set_index, ErasureMeta::set_index_for(blob_index));
|
||||||
|
let e_meta = ErasureMeta::new(set_index);
|
||||||
|
|
||||||
|
assert_eq!(e_meta.start_index(), set_index * NUM_DATA);
|
||||||
|
let (data_end_idx, coding_end_idx) = e_meta.end_indexes();
|
||||||
|
assert_eq!(data_end_idx, (set_index + 1) * NUM_DATA);
|
||||||
|
assert_eq!(coding_end_idx, set_index * NUM_DATA + NUM_CODING as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut e_meta = ErasureMeta::new(0);
|
||||||
|
|
||||||
|
assert_eq!(e_meta.data_index_in_set(0), Some(0));
|
||||||
|
assert_eq!(e_meta.data_index_in_set(NUM_DATA / 2), Some(NUM_DATA / 2));
|
||||||
|
assert_eq!(e_meta.data_index_in_set(NUM_DATA - 1), Some(NUM_DATA - 1));
|
||||||
|
assert_eq!(e_meta.data_index_in_set(NUM_DATA), None);
|
||||||
|
assert_eq!(e_meta.data_index_in_set(std::u64::MAX), None);
|
||||||
|
|
||||||
|
e_meta.set_index = 1;
|
||||||
|
|
||||||
|
assert_eq!(e_meta.data_index_in_set(0), None);
|
||||||
|
assert_eq!(e_meta.data_index_in_set(NUM_DATA - 1), None);
|
||||||
|
assert_eq!(e_meta.data_index_in_set(NUM_DATA), Some(0));
|
||||||
|
assert_eq!(
|
||||||
|
e_meta.data_index_in_set(NUM_DATA * 2 - 1),
|
||||||
|
Some(NUM_DATA - 1)
|
||||||
|
);
|
||||||
|
assert_eq!(e_meta.data_index_in_set(std::u64::MAX), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_meta_coding_present() {
|
fn test_meta_coding_present() {
|
||||||
let set_index = 0;
|
let mut e_meta = ErasureMeta::default();
|
||||||
let mut e_meta = ErasureMeta {
|
|
||||||
set_index,
|
|
||||||
data: 0,
|
|
||||||
coding: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
e_meta.set_coding_multi(0..NUM_CODING as u64, true);
|
||||||
for i in 0..NUM_CODING as u64 {
|
for i in 0..NUM_CODING as u64 {
|
||||||
e_meta.set_coding_present(i);
|
|
||||||
assert_eq!(e_meta.is_coding_present(i), true);
|
assert_eq!(e_meta.is_coding_present(i), true);
|
||||||
}
|
}
|
||||||
for i in NUM_CODING as u64..NUM_DATA as u64 {
|
for i in NUM_CODING as u64..NUM_DATA as u64 {
|
||||||
@ -158,56 +270,76 @@ fn test_meta_coding_present() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e_meta.set_index = ErasureMeta::set_index_for((NUM_DATA * 17) as u64);
|
e_meta.set_index = ErasureMeta::set_index_for((NUM_DATA * 17) as u64);
|
||||||
|
let start_idx = e_meta.start_index();
|
||||||
|
e_meta.set_coding_multi(start_idx..start_idx + NUM_CODING as u64, true);
|
||||||
|
|
||||||
for i in (NUM_DATA * 17) as u64..((NUM_DATA * 17) + NUM_CODING) as u64 {
|
for i in start_idx..start_idx + NUM_CODING as u64 {
|
||||||
e_meta.set_coding_present(i);
|
e_meta.set_coding_present(i, true);
|
||||||
assert_eq!(e_meta.is_coding_present(i), true);
|
assert_eq!(e_meta.is_coding_present(i), true);
|
||||||
}
|
}
|
||||||
for i in (NUM_DATA * 17 + NUM_CODING) as u64..((NUM_DATA * 17) + NUM_DATA) as u64 {
|
for i in start_idx + NUM_CODING as u64..start_idx + NUM_DATA as u64 {
|
||||||
assert_eq!(e_meta.is_coding_present(i), false);
|
assert_eq!(e_meta.is_coding_present(i), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "erasure")]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_can_recover() {
|
fn test_erasure_meta_status() {
|
||||||
let set_index = 0;
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
let mut e_meta = ErasureMeta {
|
// Local constansts just used to avoid repetitive casts
|
||||||
set_index,
|
const N_DATA: u64 = crate::erasure::NUM_DATA as u64;
|
||||||
data: 0,
|
const N_CODING: u64 = crate::erasure::NUM_CODING as u64;
|
||||||
coding: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(!e_meta.can_recover());
|
let mut e_meta = ErasureMeta::default();
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let data_indexes: Vec<u64> = (0..N_DATA).collect();
|
||||||
|
let coding_indexes: Vec<u64> = (0..N_CODING).collect();
|
||||||
|
|
||||||
e_meta.data = 0b1111_1111_1111_1111;
|
assert_eq!(e_meta.status(), ErasureMetaStatus::StillNeed(NUM_DATA));
|
||||||
e_meta.coding = 0x00;
|
|
||||||
|
|
||||||
assert!(!e_meta.can_recover());
|
e_meta.set_data_multi(0..N_DATA, true);
|
||||||
|
|
||||||
e_meta.coding = 0x0e;
|
assert_eq!(e_meta.status(), ErasureMetaStatus::DataFull);
|
||||||
assert_eq!(0x0fu8, 0b0000_1111u8);
|
|
||||||
assert!(!e_meta.can_recover());
|
|
||||||
|
|
||||||
e_meta.data = 0b0111_1111_1111_1111;
|
e_meta.size = 1;
|
||||||
assert!(e_meta.can_recover());
|
e_meta.set_coding_multi(0..N_CODING, true);
|
||||||
|
|
||||||
e_meta.data = 0b0111_1111_1111_1110;
|
assert_eq!(e_meta.status(), ErasureMetaStatus::DataFull);
|
||||||
assert!(e_meta.can_recover());
|
|
||||||
|
|
||||||
e_meta.data = 0b0111_1111_1011_1110;
|
for &idx in data_indexes.choose_multiple(&mut rng, NUM_CODING) {
|
||||||
assert!(e_meta.can_recover());
|
e_meta.set_data_present(idx, false);
|
||||||
|
|
||||||
e_meta.data = 0b0111_1011_1011_1110;
|
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||||
assert!(!e_meta.can_recover());
|
}
|
||||||
|
|
||||||
e_meta.data = 0b0111_1011_1011_1110;
|
e_meta.set_data_multi(0..N_DATA, true);
|
||||||
assert!(!e_meta.can_recover());
|
|
||||||
|
|
||||||
e_meta.coding = 0b0000_1110;
|
for &idx in coding_indexes.choose_multiple(&mut rng, NUM_CODING) {
|
||||||
e_meta.data = 0b1111_1111_1111_1100;
|
e_meta.set_coding_present(idx, false);
|
||||||
assert!(e_meta.can_recover());
|
|
||||||
|
|
||||||
e_meta.data = 0b1111_1111_1111_1000;
|
assert_eq!(e_meta.status(), ErasureMetaStatus::DataFull);
|
||||||
assert!(e_meta.can_recover());
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_meta_data_present() {
|
||||||
|
let mut e_meta = ErasureMeta::default();
|
||||||
|
|
||||||
|
e_meta.set_data_multi(0..NUM_DATA as u64, true);
|
||||||
|
for i in 0..NUM_DATA as u64 {
|
||||||
|
assert_eq!(e_meta.is_data_present(i), true);
|
||||||
|
}
|
||||||
|
for i in NUM_DATA as u64..2 * NUM_DATA as u64 {
|
||||||
|
assert_eq!(e_meta.is_data_present(i), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
e_meta.set_index = ErasureMeta::set_index_for((NUM_DATA * 23) as u64);
|
||||||
|
let start_idx = e_meta.start_index();
|
||||||
|
e_meta.set_data_multi(start_idx..start_idx + NUM_DATA as u64, true);
|
||||||
|
|
||||||
|
for i in start_idx..start_idx + NUM_DATA as u64 {
|
||||||
|
assert_eq!(e_meta.is_data_present(i), true);
|
||||||
|
}
|
||||||
|
for i in start_idx - NUM_DATA as u64..start_idx {
|
||||||
|
assert_eq!(e_meta.is_data_present(i), false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user