Compare commits
737 Commits
Author | SHA1 | Date | |
---|---|---|---|
ff9bd2f512 | |||
25810ce729 | |||
82c7f0e366 | |||
012d05f10b | |||
f853595efb | |||
09e4f7e49c | |||
cb37072ed7 | |||
0b109d3340 | |||
dcdc5b8cf7 | |||
1a7c30bb86 | |||
3ebc14f965 | |||
cf589efbbf | |||
94d5c64281 | |||
566de1fd0e | |||
cb0f367084 | |||
e08e1fe6ac | |||
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 | |||
819a0c5c7e | |||
7afd8644b3 | |||
68fc303b9b | |||
2bbed7727f | |||
63b1fd3675 | |||
3fcf03ff3e | |||
80f3568062 | |||
3e1214a871 | |||
149d809e86 | |||
784dbb00ab | |||
87aef92e71 | |||
d026ebb83a | |||
64c6f05da2 | |||
8963500aa8 | |||
175c0090de | |||
5c4689a326 | |||
5e2831f09e | |||
666882fbbd | |||
6c9fba058b | |||
0767c0c07f | |||
6859907df9 | |||
de52747950 | |||
bd1db51e07 | |||
dd005fb50e | |||
542bafeb71 | |||
e57a0ab05d | |||
2c745ce108 | |||
f6aa90e193 | |||
c7a7d6db84 | |||
2277a39dd2 | |||
ee35ed5250 | |||
92b5e131fe | |||
1f35779821 | |||
5b438d917d | |||
bf4d5745c9 | |||
1e8f83a74a | |||
1db80d79fc | |||
1dac4c33b8 | |||
656b3139e3 | |||
8b08fe265a | |||
29dc139a22 | |||
44ebfa736a | |||
b001685e7b | |||
ca6290b117 | |||
767e0a201e | |||
877ec08280 | |||
485013b7ce | |||
efd19b07e7 | |||
d31989f878 | |||
f669ae5868 | |||
a28c3b0e9a | |||
0aa05158c9 | |||
787dc5748a | |||
8ada4bfd1f | |||
5d4624e75f | |||
2f1b0bf4f5 | |||
e1d5bb1a26 | |||
d0f46d6a8a | |||
4b6c0198ad | |||
f1e7237c09 | |||
1b5845ac3e | |||
58a049ebe5 | |||
c0808d01f8 | |||
7fd5e51168 | |||
d2ea782372 | |||
e6f02d1a10 | |||
894135a084 | |||
df9cf92782 | |||
f243a96e01 | |||
842d146b0d | |||
81d43c57a2 | |||
7da4142d33 | |||
88e5b14afc | |||
0b95a5c121 | |||
62c28a8592 | |||
b80c6840da | |||
003fd6545c | |||
393ed978d1 | |||
2c93062f54 | |||
7b2abf2087 | |||
a5254a3f7a | |||
dc6c34da5d | |||
d4eebcc2aa | |||
4f232cbc27 | |||
76e524ae48 | |||
6ac919c71a | |||
1ba4806f8c | |||
20a2c59b70 | |||
6540fa9121 | |||
21287ba554 | |||
7295a84d69 | |||
483cc2fa4e | |||
e551f6b552 | |||
44b391096d | |||
d45d8e9670 | |||
88bda58836 | |||
79bf3cf70d | |||
72b7419e1c | |||
7baff0920c | |||
d9ecc278b4 | |||
0904df327d | |||
444e87f888 | |||
20aa4434e2 | |||
03da63b41b | |||
878a842611 | |||
f3eda38b65 | |||
68e21911eb | |||
95cc36af96 | |||
d3c4e4f7b3 | |||
4068612300 | |||
f349c1f0dc | |||
90c1300bb6 | |||
569a289a6f | |||
89efe67e73 | |||
c3654b0f65 | |||
f5f4434e0a | |||
d30049b8eb | |||
42d8a7d9e7 | |||
adcda3c715 | |||
a5b5248a09 | |||
3fcca5bc0a | |||
9d4c6f6aaa | |||
d570b08134 | |||
8b6d7129f3 | |||
50444181c5 | |||
0c51f156ae | |||
fe2fb40d88 | |||
9ba0439593 | |||
b33a1fa019 | |||
63fd4222aa | |||
ef5df6f3fe | |||
2f90f9fbd4 | |||
12b099ea78 | |||
9f046a023e | |||
46e6911ec1 | |||
d3844ef32a | |||
4507dca342 | |||
c2fdd1362a | |||
4ea19b90a4 | |||
9cd555cad5 | |||
ed78c8d3bb | |||
0b23af324b | |||
1598a02a7a | |||
167f5bdc58 | |||
5cd7bccdf3 | |||
acbc261891 | |||
f97f0c4758 | |||
e6ac5bc546 | |||
ef1e5db0ee | |||
5cdfd79e96 | |||
b441bac7b2 | |||
00cb52c444 | |||
9323a3e257 | |||
35298e01a8 | |||
867f6f107b | |||
43bb813cbe | |||
7b82e96467 | |||
978ff87b76 | |||
4c0bc1fd88 | |||
025b4f90de | |||
20189c5d45 | |||
2e4acba579 | |||
d90b8c331d | |||
efbb49d579 | |||
f0079cd7b3 | |||
a0041cec97 | |||
77bb9e7ffc | |||
f441177840 | |||
cd634801a2 | |||
5f10a87dec | |||
fa1c1e3734 | |||
947cdd8748 | |||
0a9f063d3e | |||
dd4c512954 | |||
d228b6467c | |||
92c66a411b | |||
af97ad3d68 | |||
6ff2a0a75e | |||
5b7d5e2e02 | |||
97bd7a00f1 | |||
25a2f08f8d | |||
3152090a66 | |||
9a0f9b910e | |||
f853c39169 | |||
75ad1305c0 | |||
cb3adea94f | |||
fcef54d062 | |||
32683cac7c | |||
15947b8642 | |||
4e0316f792 | |||
9594b7fdce | |||
1adf8355f2 | |||
8660c3581e | |||
f886b3b12b | |||
5646daa820 | |||
7896e8288d | |||
9369ea86ea | |||
dee5ede16d | |||
3b516c0710 | |||
0887832b00 | |||
8e04fadb05 | |||
31f8b6d352 | |||
98d60e6124 | |||
fc678f53ba | |||
8e25c39564 | |||
78ab79c322 | |||
052fc9b74f | |||
f482c9ab61 | |||
75dcd97f5f | |||
4776dc36ab | |||
10239c3b3c | |||
753d0dcabe | |||
b708998d9d | |||
3759b0d2a5 | |||
c4bc710d3a | |||
857dc2ba47 | |||
981e057363 | |||
37494c67d0 | |||
7a81f327ce | |||
845ddc3496 | |||
c61bb16fdf | |||
15b945a652 | |||
1d48c4dd45 | |||
2ab50cbae8 | |||
0482f153d0 | |||
92e1c4c531 | |||
4bca60861e | |||
50b0a5ae83 | |||
c30eb6185c | |||
a94bc80383 | |||
586b6fc3d7 | |||
a14c202d60 | |||
ed48c495a3 | |||
f0abd06a46 | |||
7d0ff8e713 | |||
e8cc566b2b | |||
e45f7afd85 | |||
054ae3a3e3 | |||
36ea088387 | |||
47b6707c07 | |||
0346b9cb5c | |||
6bfe497ab5 | |||
6956bf635e | |||
e27d6d0988 | |||
3fc09fb23f | |||
cecdb7061e | |||
0ac865f08c | |||
55115d0eeb | |||
16ff4ac1a8 | |||
5ce31168ef | |||
b9ff70c8ab | |||
77498c6efe | |||
8c69c40834 | |||
d497b99abb | |||
ca2ac1e5ea | |||
c09e0eb536 | |||
0d90dfae1a | |||
bf61321cab | |||
591653981b | |||
e651510805 | |||
9d73fbb84a | |||
215b07c1a9 | |||
420cbc45cd | |||
df333e8b6e | |||
9759ac2961 | |||
af9b173dfd | |||
b61aed7250 | |||
e1c0425c2b | |||
615472b52c | |||
4d34102d9c | |||
3e22ce4154 | |||
215f33680b | |||
a5420f19da | |||
4bc3f70150 | |||
e8814b1297 | |||
46ab0e6449 | |||
59b4f40f4e | |||
93c57934cb | |||
e8e1d6b8ce | |||
4916cd8da5 | |||
573dec63da | |||
34c051f183 | |||
51004881f8 | |||
5c536e423c | |||
4efa144916 | |||
f3936c21a3 | |||
caff603497 | |||
aefa9891c0 | |||
6286947697 | |||
33972ef89e | |||
b53cbdd9e6 | |||
c49e84c75b | |||
dcf2337e58 | |||
5a65c3f72e | |||
8ff1987d2d | |||
acedf4ca5a | |||
68c35bfde6 | |||
e1a3708844 | |||
46ecac3310 | |||
028b9da0da | |||
74cea2748c | |||
a478b2a05a | |||
41a52dbfea | |||
4923f889c4 | |||
31b8743052 | |||
6505221629 | |||
de2b6bc9fc | |||
f565292852 | |||
90f17e8fd4 | |||
d6da7dc1b6 | |||
7e2aad2590 | |||
f09b8d3921 | |||
52f6c33ff9 | |||
60dfb35924 | |||
5f41909098 | |||
a28f7db950 | |||
38fdbbba3f | |||
0a5b6154e8 | |||
4542a7042a | |||
6113b64fee | |||
f777ed76a3 | |||
e6b9babf53 | |||
ed8bada439 | |||
06b0c98c75 | |||
dbb145c266 | |||
437481853b | |||
3b5a9f512c | |||
045af04784 | |||
d0761f57e8 | |||
4bb88619fd | |||
412ebfcaf2 | |||
3a7647f611 | |||
d4cc48f99d | |||
852fcbd700 | |||
8ab4b8e6ac | |||
a8095e204f | |||
98979c7d53 | |||
c18fcde385 | |||
f286bbac99 | |||
4e029d81a2 | |||
2b00a42b06 | |||
07d55d0092 | |||
fb44e2bf48 | |||
9b0bf5ad66 | |||
4247fa946e | |||
071b1d8b77 | |||
63aadc4905 | |||
d2415613de | |||
58f071b7a0 | |||
148e08a8a5 | |||
402a733cd7 | |||
78be3652de | |||
b03d9884a3 | |||
799085a105 | |||
7812b67471 | |||
4033fa031b | |||
b41737259a | |||
7c8a4bf6a4 | |||
71314d79a7 | |||
d7ff6645a9 | |||
1824e09d0a | |||
205907d3d7 | |||
d4bcc4d474 | |||
bcb190a12a | |||
63e8496473 | |||
4107d70e93 | |||
4fb0782892 | |||
9b7c1d5650 | |||
985592cf40 | |||
2694654a98 | |||
4126461f87 | |||
791ead6053 | |||
3048de18bb | |||
df9fd2bc0b | |||
13c9d3d4e1 | |||
0dc364c17a | |||
b3cdf58e4b | |||
61f950a60c | |||
da77789881 | |||
61af87972e | |||
fe9e771b9b | |||
94b5835738 | |||
a4652a9aaf | |||
7246d72f03 | |||
70b21b3795 | |||
682b1b89b3 | |||
f1802e592a | |||
ee58c1f960 | |||
07f4dd385d | |||
1be7ee51be | |||
56fcc93ef5 | |||
c70412d7bb | |||
5e21268ca0 | |||
b38e3bef01 | |||
89cc82c71b | |||
1d0f6a5d85 | |||
d0292b1cf1 | |||
15aed9f320 | |||
5a67362b8e | |||
5d73ab299b | |||
ef111dcbe1 | |||
da7e49c880 | |||
f16f88873d | |||
211c81f2a2 | |||
efc39ffdde | |||
61a4b998fa | |||
cedff2fca1 | |||
8d032aba9d | |||
607b368fe3 | |||
a54854abc7 | |||
ce6257a069 | |||
7b28d3a231 | |||
ea01ff2aab | |||
3369019943 | |||
dbd4176b97 | |||
122c7bc2ef | |||
99671472d1 | |||
0c0716abfb | |||
c09accb685 | |||
ae4d14a2ad | |||
55cdbedb52 | |||
ee39f31d81 | |||
70b45de012 | |||
60437a8dcb | |||
a35ebe1186 | |||
c498775a3d | |||
3ad019a176 | |||
9632136cda | |||
42cea7a785 | |||
4c9d852b08 | |||
9566a5cc68 | |||
ac03c59b41 | |||
73ceaf07b1 | |||
7b314f47f7 | |||
23337e08eb | |||
97e73311c5 | |||
e2c24481e4 | |||
ad252fe4c5 | |||
4b04bc8612 | |||
bcc34b906c | |||
c2b1010f18 | |||
e3ef4f25d3 | |||
ad12b0efce | |||
00f005af25 | |||
656fb173f9 | |||
5f58e9cd6e | |||
1d876df8b3 | |||
c8bbca08f8 | |||
971da7325d | |||
ca4f874f52 | |||
a88b36d718 | |||
24d9138067 | |||
aca739b800 | |||
e091aa87ea | |||
968022a1b0 | |||
66fb1bbb2e | |||
fa3e1fa7c9 | |||
36763d0802 | |||
be5f800390 | |||
ca69b7b75b | |||
a15927f8d0 | |||
4ba4ad9878 | |||
be1511a7ff | |||
41b98c603b | |||
5430dd28b6 | |||
e9d687329b | |||
d72cac6e97 | |||
4e51a444f4 | |||
48d86683e2 | |||
42d5dde5b1 | |||
142eeffe5d | |||
6a68df3ebd | |||
8306c1841c | |||
73bd396dfb | |||
36fb0a0aef | |||
4d53be8350 | |||
f8bf9ca218 | |||
7b4568b9bf | |||
bd8502e87e | |||
21815f26d5 | |||
8ef5195037 | |||
57606c6bf8 | |||
0465abf75b | |||
8a142966be | |||
7498488f5f | |||
ede99d5913 | |||
3ced91319f | |||
3d1413e619 | |||
8f25548781 | |||
47ddbbe53b | |||
5741400713 | |||
9f02a8d3d0 | |||
c208f4dbb5 | |||
a17843c8f6 | |||
3f2fc21bb3 | |||
9fac3b26ee | |||
c1eec0290e | |||
de13082347 | |||
48b5d666d0 | |||
70bb49a46d | |||
105fc7029e | |||
77a7ffe543 | |||
7d593e6c61 | |||
bb420cb995 | |||
e58220282a | |||
4ca4038d54 | |||
150cd31ec0 | |||
6fd0d4dcf5 | |||
296415945a | |||
1de5ae1ef0 | |||
6a89c68a1d | |||
c14cce4c85 | |||
959961b596 | |||
6f76c2da6c | |||
8d2bd2b30f | |||
34a8d591fa | |||
d94ff4bf4a | |||
af03df38b9 | |||
242bcf44db | |||
ebd540972d | |||
a17be9f8bd | |||
42ad297778 | |||
0568d7238e | |||
9bc05313a2 | |||
fedbae6f8c | |||
64de639817 | |||
ec9e13d1f4 | |||
5d27f221f7 | |||
61db74d98e | |||
1d689e84f1 | |||
b7f420412b | |||
e3ac9e9679 | |||
12fde77ecd | |||
3fc96c4a18 | |||
cb3eeace56 | |||
76feb2098e | |||
06cb266cfe | |||
866d3f467f | |||
c1e726da87 | |||
7d7528eb18 | |||
9f916f9d47 | |||
a7d8bfdf8b | |||
abdd4f371b | |||
13adee332e | |||
a799f8f4b1 | |||
1ee43a7633 | |||
3d2b7dd1ef | |||
7b35114c0f | |||
b418525464 | |||
8bba11367e | |||
9eb7e63819 | |||
092501039c | |||
6899bd7099 | |||
5a0416b925 | |||
ba2cdd0bf6 |
12
.buildkite/env/secrets.ejson
vendored
12
.buildkite/env/secrets.ejson
vendored
@ -1,10 +1,12 @@
|
||||
{
|
||||
"_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f",
|
||||
"environment": {
|
||||
"CODECOV_TOKEN": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:EzVa4Gpj2Qn5OhZQlVfGFchuROgupvnW:CbWc6sNh1GCrAbrncxDjW00zUAD/Sa+ccg7CFSz8Ua6LnCYnSddTBxJWcJEbEs0MrjuZRQ==]",
|
||||
"CRATES_IO_TOKEN": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:qF7QrUM8j+19mptcE1YS71CqmrCM13Ah:TZCatJeT1egCHiufE6cGFC1VsdJkKaaqV6QKWkEsMPBKvOAdaZbbVz9Kl+lGnIsF]",
|
||||
"INFLUX_DATABASE": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:PetD/4c/EbkQmFEcK21g3cBBAPwFqHEw:wvYmDZRajy2WngVFs9AlwyHk]",
|
||||
"INFLUX_USERNAME": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:WcnqZdmDFtJJ01Zu5LbeGgbYGfRzBdFc:a7c5zDDtCOu5L1Qd2NKkxT6kljyBcbck]",
|
||||
"INFLUX_PASSWORD": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:LIZgP9Tp9yE9OlpV8iogmLOI7iW7SiU3:x0nYdT1A6sxu+O+MMLIN19d2t6rrK1qJ3+HnoWG3PDodsXjz06YJWQKU/mx6saqH+QbGtGV5mk0=]"
|
||||
"CODECOV_TOKEN": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:jA0WqO70coUtF0iokRdgtCR/lF/lETAI:d/Wl8Tdl6xVh/B39cTf1DaQkomR7I/2vMhvxd1msJ++BjI2l3p2dFoGsXqWT+/os8VgiPg==]",
|
||||
"CRATES_IO_TOKEN": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:2FaZ6k4RGH8luyNRaN6yeZUQDNAu2KwC:XeYe0tCAivYE0F9HEWM79mAI6kNbfYaqP7k7yY+SBDvs0341U9BdGZp7SErbHleS]",
|
||||
"GITHUB_TOKEN": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:9kh4DGPiGDcUU7ejSFWg3gTW8nrOM09Q:b+GE07Wu6/bEnkDZcUtf48vTKAFphrCSt3tNNER9h6A+wZ80k499edw4pbDdl9kEvxB30fFwrLQ=]",
|
||||
"INFLUX_DATABASE": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:rCHsYi0rc7dmvr1V3wEgNoaNIyr+9ClM:omjVcOqM7vwt44kJ+As4BjJL]",
|
||||
"INFLUX_PASSWORD": "EJ[1:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:bP5Gw1Vy66viKFKO41o2Gho998XajH/5:khkCYz2LFvkJkk7R4xY1Hfz1yU3/NENjauiUkPhXA+dmg1qOIToxEagCgIkRwyeCiYaoCR6CZyw=]",
|
||||
"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:eSGdiZR0Qi0g7qnsI+qJ5H+/ik+H2qL3ned/cBdv/SY=:Oi2nsRxnvWnnBYsB6KwEDzLPcYgpYojU:ELbvjXkXKlgFCMES45R+fxG7Ex43WHWErjMbxZoqasxyr7GSH66hQzUWqiQSJyT4ukYrRhRC9YrsKKGkjACLU57X4EGIy9TuLgTnyBYhPnxLYStC3y/7o/MB5FCTt5wHJw3/A9p+me5+T4UmyZ7OeP21NhDUCGQcb0040VwYWS78klW2aQESJJ6wTI1xboE8/zC0vtnB/u50+LydbKEyb21r6y3OH9FYNEpSwIspWKcgpruJdQSCnDoKxP9YR1yzvk2rabss13LJNdV1Y6mQNIdP4OIFQhCs6dXT253RTl5qdZ0MruHwlp8wX4btOuYDcCoM5exr]"
|
||||
}
|
||||
}
|
||||
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,10 +1,13 @@
|
||||
/target/
|
||||
/ledger-tool/target/
|
||||
/wallet/target/
|
||||
/core/target/
|
||||
/book/html/
|
||||
/book/src/img/
|
||||
/book/src/tests.ok
|
||||
/core/target/
|
||||
/farf/
|
||||
/ledger-tool/target/
|
||||
/solana-release/
|
||||
solana-release.tar.bz2
|
||||
/target/
|
||||
/wallet/target/
|
||||
|
||||
**/*.rs.bk
|
||||
.cargo
|
||||
|
@ -46,10 +46,17 @@ and longer descriptions detailing what problem it solves and how it solves it.
|
||||
Draft Pull Requests
|
||||
---
|
||||
|
||||
If you want early feedback on your PR, use GitHub's "Draft Pull Request" mechanism. Draft
|
||||
PRs are a convenient way to collaborate with the Solana maintainers without triggering
|
||||
notifications as you make changes. When you feel your PR is ready for a broader audience,
|
||||
you can transition your draft PR to a standard PR with the click of a button.
|
||||
If you want early feedback on your PR, use GitHub's "Draft Pull Request"
|
||||
mechanism. Draft PRs are a convenient way to collaborate with the Solana
|
||||
maintainers without triggering notifications as you make changes. When you feel
|
||||
your PR is ready for a broader audience, you can transition your draft PR to a
|
||||
standard PR with the click of a button.
|
||||
|
||||
Do not add reviewers to draft PRs. GitHub doesn't automatically clear approvals
|
||||
when you click "Ready for Review", so a review that meant "I approve of the
|
||||
direction" suddenly has the appearance of "I approve of these changes." Instead,
|
||||
add a comment that mentions the usernames that you would like a review from. Ask
|
||||
explicitly what you would like feedback on.
|
||||
|
||||
Rust coding conventions
|
||||
---
|
||||
@ -89,24 +96,23 @@ understood. Avoid introducing new 3-letter terms, which can be confused with 3-l
|
||||
[Terms currently in use](book/src/terminology.md)
|
||||
|
||||
|
||||
Proposing architectural changes
|
||||
Design Proposals
|
||||
---
|
||||
|
||||
Solana's architecture is described by a book generated from markdown files in
|
||||
the `book/src/` directory, maintained by an *editor* (currently @garious). To
|
||||
change the architecture, you'll need to at least propose a change the content
|
||||
under the [Proposed
|
||||
Changes](https://solana-labs.github.io/book-edge/proposals.html) chapter. Here's
|
||||
the full process:
|
||||
add a design proposal, you'll need to at least propose a change the content
|
||||
under the [Accepted Design
|
||||
Proposals](https://solana-labs.github.io/book-edge/proposals.html) chapter.
|
||||
Here's the full process:
|
||||
|
||||
1. Propose to a change to the architecture by creating a PR that adds a
|
||||
markdown document to the directory `book/src/` and references it from the
|
||||
[table of contents](book/src/SUMMARY.md). Add the editor and any relevant
|
||||
*maintainers* to the PR review.
|
||||
1. Propose a design by creating a PR that adds a markdown document to the
|
||||
directory `book/src/` and references it from the [table of
|
||||
contents](book/src/SUMMARY.md). Add any relevant *maintainers* to the PR review.
|
||||
2. The PR being merged indicates your proposed change was accepted and that the
|
||||
editor and maintainers support your plan of attack.
|
||||
maintainers support your plan of attack.
|
||||
3. Submit PRs that implement the proposal. When the implementation reveals the
|
||||
need for tweaks to the architecture, be sure to update the proposal and have
|
||||
need for tweaks to the proposal, be sure to update the proposal and have
|
||||
that change reviewed by the same people as in step 1.
|
||||
4. Once the implementation is complete, the editor will then work to integrate
|
||||
the document into the book.
|
||||
4. Once the implementation is complete, submit a PR that moves the link from
|
||||
the Accepted Proposals to the Implemented Proposals section.
|
||||
|
1944
Cargo.lock
generated
1944
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
81
Cargo.toml
81
Cargo.toml
@ -1,86 +1,37 @@
|
||||
[package]
|
||||
name = "solana-workspace"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "0.12.0"
|
||||
documentation = "https://docs.rs/solana"
|
||||
homepage = "https://solana.com/"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[badges]
|
||||
codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
|
||||
|
||||
[features]
|
||||
chacha = ["solana/chacha"]
|
||||
cuda = ["solana/cuda"]
|
||||
erasure = ["solana/erasure"]
|
||||
|
||||
[dev-dependencies]
|
||||
bincode = "1.1.2"
|
||||
bs58 = "0.2.0"
|
||||
hashbrown = "0.1.8"
|
||||
log = "0.4.2"
|
||||
rand = "0.6.5"
|
||||
rayon = "1.0.0"
|
||||
reqwest = "0.9.11"
|
||||
serde_json = "1.0.39"
|
||||
solana = { path = "core", version = "0.12.0" }
|
||||
solana-logger = { path = "logger", version = "0.12.0" }
|
||||
solana-netutil = { path = "netutil", version = "0.12.0" }
|
||||
solana-runtime = { path = "runtime", version = "0.12.0" }
|
||||
solana-sdk = { path = "sdk", version = "0.12.0" }
|
||||
sys-info = "0.5.6"
|
||||
|
||||
[[bench]]
|
||||
name = "banking_stage"
|
||||
|
||||
[[bench]]
|
||||
name = "blocktree"
|
||||
|
||||
[[bench]]
|
||||
name = "ledger"
|
||||
|
||||
[[bench]]
|
||||
name = "gen_keys"
|
||||
|
||||
[[bench]]
|
||||
name = "sigverify"
|
||||
|
||||
[[bench]]
|
||||
required-features = ["chacha"]
|
||||
name = "chacha"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"bench-exchange",
|
||||
"bench-streamer",
|
||||
"bench-tps",
|
||||
"core",
|
||||
"drone",
|
||||
"fullnode",
|
||||
"genesis",
|
||||
"gossip",
|
||||
"install",
|
||||
"keygen",
|
||||
"kvstore",
|
||||
"ledger-tool",
|
||||
"logger",
|
||||
"metrics",
|
||||
"programs/bpf",
|
||||
"programs/bpf_loader",
|
||||
"programs/budget",
|
||||
"programs/budget_api",
|
||||
"programs/token",
|
||||
"programs/budget_program",
|
||||
"programs/config_api",
|
||||
"programs/config_program",
|
||||
"programs/exchange_api",
|
||||
"programs/exchange_program",
|
||||
"programs/token_api",
|
||||
"programs/failure",
|
||||
"programs/noop",
|
||||
"programs/rewards",
|
||||
"programs/rewards_api",
|
||||
"programs/storage",
|
||||
"programs/token_program",
|
||||
"programs/failure_program",
|
||||
"programs/noop_program",
|
||||
"programs/stake_api",
|
||||
"programs/stake_program",
|
||||
"programs/storage_api",
|
||||
"programs/system",
|
||||
"programs/vote",
|
||||
"programs/storage_program",
|
||||
"programs/vote_api",
|
||||
"programs/vote_program",
|
||||
"replicator",
|
||||
"sdk",
|
||||
"upload-perf",
|
||||
|
49
README.md
49
README.md
@ -44,7 +44,7 @@ $ source $HOME/.cargo/env
|
||||
$ rustup component add rustfmt-preview
|
||||
```
|
||||
|
||||
If your rustc version is lower than 1.31.0, please update it:
|
||||
If your rustc version is lower than 1.34.0, please update it:
|
||||
|
||||
```bash
|
||||
$ rustup update
|
||||
@ -83,12 +83,6 @@ Run the test suite:
|
||||
$ cargo test --all
|
||||
```
|
||||
|
||||
To emulate all the tests that will run on a Pull Request, run:
|
||||
|
||||
```bash
|
||||
$ ./ci/run-local.sh
|
||||
```
|
||||
|
||||
Local Testnet
|
||||
---
|
||||
|
||||
@ -131,6 +125,47 @@ can run your own testnet using the scripts in the `net/` directory.
|
||||
Edit `ci/testnet-manager.sh`
|
||||
|
||||
|
||||
## Metrics Server Maintenance
|
||||
Sometimes the dashboard becomes unresponsive. This happens due to glitch in the metrics server.
|
||||
The current solution is to reset the metrics server. Use the following steps.
|
||||
|
||||
1. The server is hosted in a GCP VM instance. Check if the VM instance is down by trying to SSH
|
||||
into it from the GCP console. The name of the VM is ```metrics-solana-com```.
|
||||
2. If the VM is inaccessible, reset it from the GCP console.
|
||||
3. Once VM is up (or, was already up), the metrics services can be restarted from build automation.
|
||||
1. Navigate to https://buildkite.com/solana-labs/metrics-dot-solana-dot-com in your web browser
|
||||
2. Click on ```New Build```
|
||||
3. This will show a pop up dialog. Click on ```options``` drop down.
|
||||
4. Type in ```FORCE_START=true``` in ```Environment Variables``` text box.
|
||||
5. Click ```Create Build```
|
||||
6. This will restart the metrics services, and the dashboards should be accessible afterwards.
|
||||
|
||||
## Debugging Testnet
|
||||
Testnet may exhibit different symptoms of failures. Primary statistics to check are
|
||||
1. Rise in Confirmation Time
|
||||
2. Nodes are not voting
|
||||
3. Panics, and OOM notifications
|
||||
|
||||
Check the following if there are any signs of failure.
|
||||
1. Did testnet deployment fail?
|
||||
1. View buildkite logs for the last deployment: https://buildkite.com/solana-labs/testnet-management
|
||||
2. Use the relevant branch
|
||||
3. If the deployment failed, look at the build logs. The build artifacts for each remote node is uploaded.
|
||||
It's a good first step to triage from these logs.
|
||||
2. You may have to log into remote node if the deployment succeeded, but something failed during runtime.
|
||||
1. Get the private key for the testnet deployment from ```metrics-solana-com``` GCP instance.
|
||||
2. SSH into ```metrics-solana-com``` using GCP console and do the following.
|
||||
```bash
|
||||
sudo bash
|
||||
cd ~buildkite-agent/.ssh
|
||||
ls
|
||||
```
|
||||
3. Copy the relevant private key to your local machine
|
||||
4. Find the public IP address of the AWS instance for the remote node using AWS console
|
||||
5. ```ssh -i <private key file> ubuntu@<ip address of remote node>```
|
||||
6. The logs are in ```~solana\solana``` folder
|
||||
|
||||
|
||||
Benchmarking
|
||||
---
|
||||
|
||||
|
30
RELEASE.md
30
RELEASE.md
@ -63,20 +63,25 @@ There are three release channels that map to branches as follows:
|
||||
|
||||
### Changing channels
|
||||
|
||||
When cutting a new channel branch these pre-steps are required:
|
||||
|
||||
#### Create the new branch
|
||||
1. Pick your branch point for release on master.
|
||||
1. Create the branch. The name should be "v" + the first 2 "version" fields
|
||||
from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies
|
||||
the next branch name is "v0.9".
|
||||
1. Push the new branch to the solana repository
|
||||
1. Update Cargo.toml on master to the next semantic version (e.g. 0.9.0 -> 0.10.0)
|
||||
by running `./scripts/increment-cargo-version.sh`, then rebuild with a
|
||||
`cargo build --all` to cause a refresh of `Cargo.lock`.
|
||||
1. Note the Cargo.toml in the repo root directory does not contain a version. Look at any other Cargo.toml file.
|
||||
1. Create a new branch and push this branch to the solana repository.
|
||||
1. `git checkout -b <branchname>`
|
||||
1. `git push -u origin <branchname>`
|
||||
|
||||
#### Update master with the next version
|
||||
|
||||
1. After the new branch has been created and pushed, update Cargo.toml on **master** to the next semantic version (e.g. 0.9.0 -> 0.10.0)
|
||||
by running `./scripts/increment-cargo-version.sh`, then rebuild with
|
||||
`cargo build` to cause a refresh of `Cargo.lock`.
|
||||
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
||||
master branch
|
||||
|
||||
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".
|
||||
|
||||
### Updating channels (i.e. "making a release")
|
||||
@ -86,7 +91,8 @@ We use [github's Releases UI](https://github.com/solana-labs/solana/releases) fo
|
||||
1. Go [there ;)](https://github.com/solana-labs/solana/releases).
|
||||
1. Click "Draft new release". The release tag must exactly match the `version`
|
||||
field in `/Cargo.toml` prefixed by `v` (ie, `<branchname>.X`).
|
||||
1. If the first major release on the branch (e.g. v0.8.0), paste in [this
|
||||
1. If the Cargo.toml verion field is **0.12.3**, then the release tag must be **v0.12.3**
|
||||
1. If this is the first release on the branch (e.g. v0.13.**0**), paste in [this
|
||||
template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md)
|
||||
and fill it in.
|
||||
1. Test the release by generating a tag using semver's rules. First try at a
|
||||
@ -97,9 +103,9 @@ We use [github's Releases UI](https://github.com/solana-labs/solana/releases) fo
|
||||
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 release 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
|
||||
`./scripts/increment-cargo-version.sh patch`, then rebuild with a `cargo
|
||||
build --all` to cause a refresh of `Cargo.lock`.
|
||||
`./scripts/increment-cargo-version.sh patch`, then rebuild with `cargo
|
||||
build` to cause a refresh of `Cargo.lock`.
|
||||
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
||||
release branch
|
||||
release branch.
|
||||
|
3
bench-exchange/.gitignore
vendored
Normal file
3
bench-exchange/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target/
|
||||
/config/
|
||||
/config-local/
|
39
bench-exchange/Cargo.toml
Normal file
39
bench-exchange/Cargo.toml
Normal file
@ -0,0 +1,39 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-exchange"
|
||||
version = "0.14.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.2"
|
||||
env_logger = "0.6.0"
|
||||
itertools = "0.8.0"
|
||||
log = "0.4.6"
|
||||
num-traits = "0.2"
|
||||
num-derive = "0.2"
|
||||
rayon = "1.0.3"
|
||||
serde = "1.0.87"
|
||||
serde_derive = "1.0.87"
|
||||
serde_json = "1.0.38"
|
||||
# solana-runtime = { path = "../solana/runtime"}
|
||||
solana = { path = "../core", version = "0.14.0" }
|
||||
solana-client = { path = "../client", version = "0.14.0" }
|
||||
solana-drone = { path = "../drone", version = "0.14.0" }
|
||||
solana-exchange-api = { path = "../programs/exchange_api", version = "0.14.0" }
|
||||
solana-exchange-program = { path = "../programs/exchange_program", version = "0.14.0" }
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.14.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.14.0" }
|
||||
ws = "0.8.0"
|
||||
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 broker 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 broker 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 broker'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.
|
||||
- Broker
|
||||
- An agent who facilitates trading between investors. Brokers operate as
|
||||
Solana thin clients who monitor all the trade orders looking for a trade
|
||||
match. Once found, the broker issues a swap request to the exchange.
|
||||
Brokers are the engine of the exchange and are rewarded for their efforts by
|
||||
accumulating the price spreads of the swaps they initiate. Brokers 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 Brokers.
|
||||
|
||||
## Exchange startup
|
||||
|
||||
The exchange is up and running when it reaches a state where it can take
|
||||
investor's trades and broker's swap requests. To achieve this state the
|
||||
following must occur in order:
|
||||
|
||||
- Start the Solana blockchain
|
||||
- Start the broker thin-client
|
||||
- The broker 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 broker starts responding to queries for bid/ask price and OHLCV
|
||||
|
||||
The broker 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 broker could come and go without missing a trade. One way to achieve
|
||||
this is for the broker 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. Brokers 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 brokers 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 broker 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
|
||||
broker 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
|
||||
broker's account equal to the difference in the price ratios or the two orders.
|
||||
These tokens are considered the broker's profit for initiating the trade.
|
||||
|
||||
The broker 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
|
||||
- Broker 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 broker 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
|
||||
- Broker 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 broker 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
|
||||
- Broker 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 broker 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
|
||||
- Broker 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 brokers 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 brokers profit from the swap.
|
||||
SwapRequest,
|
||||
}
|
||||
```
|
||||
|
||||
## Quotes and OHLCV
|
||||
|
||||
The broker 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
|
||||
```
|
||||
|
||||
|
||||
|
1092
bench-exchange/src/bench.rs
Normal file
1092
bench-exchange/src/bench.rs
Normal file
File diff suppressed because it is too large
Load Diff
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 network_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 {
|
||||
network_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("network")
|
||||
.short("n")
|
||||
.long("network")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("127.0.0.1:8001")
|
||||
.help("Network's gossip 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.network_addr = solana_netutil::parse_host_port(matches.value_of("network").unwrap())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse network 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
|
||||
}
|
67
bench-exchange/src/main.rs
Normal file
67
bench-exchange/src/main.rs
Normal file
@ -0,0 +1,67 @@
|
||||
pub mod bench;
|
||||
mod cli;
|
||||
pub mod order_book;
|
||||
|
||||
use crate::bench::{airdrop_lamports, do_bench_exchange, get_clients, Config};
|
||||
use log::*;
|
||||
use solana::gossip_service::discover_nodes;
|
||||
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 {
|
||||
network_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 = discover_nodes(&network_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))
|
||||
}
|
||||
}
|
@ -2,16 +2,17 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-streamer"
|
||||
version = "0.12.0"
|
||||
version = "0.14.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.32.0"
|
||||
solana = { path = "../core", version = "0.12.0" }
|
||||
solana-logger = { path = "../logger", version = "0.12.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.12.0" }
|
||||
clap = "2.33.0"
|
||||
solana = { path = "../core", version = "0.14.0" }
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
|
||||
[features]
|
||||
cuda = ["solana/cuda"]
|
||||
erasure = []
|
||||
|
@ -1,5 +1,5 @@
|
||||
use clap::{App, Arg};
|
||||
use solana::packet::{Packet, SharedPackets, BLOB_SIZE, PACKET_DATA_SIZE};
|
||||
use clap::{crate_description, crate_name, crate_version, App, Arg};
|
||||
use solana::packet::{Packet, Packets, BLOB_SIZE, PACKET_DATA_SIZE};
|
||||
use solana::result::Result;
|
||||
use solana::streamer::{receiver, PacketReceiver};
|
||||
use std::cmp::max;
|
||||
@ -14,19 +14,19 @@ use std::time::SystemTime;
|
||||
|
||||
fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
|
||||
let send = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
let msgs = SharedPackets::default();
|
||||
let msgs_ = msgs.clone();
|
||||
msgs.write().unwrap().packets.resize(10, Packet::default());
|
||||
for w in &mut msgs.write().unwrap().packets {
|
||||
let mut msgs = Packets::default();
|
||||
msgs.packets.resize(10, Packet::default());
|
||||
for w in &mut msgs.packets {
|
||||
w.meta.size = PACKET_DATA_SIZE;
|
||||
w.meta.set_addr(&addr);
|
||||
}
|
||||
let msgs_ = msgs.clone();
|
||||
spawn(move || loop {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let mut num = 0;
|
||||
for p in &msgs_.read().unwrap().packets {
|
||||
for p in &msgs_.packets {
|
||||
let a = p.meta.addr();
|
||||
assert!(p.meta.size < BLOB_SIZE);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -51,7 +51,9 @@ fn sink(exit: Arc<AtomicBool>, rvs: Arc<AtomicUsize>, r: PacketReceiver) -> Join
|
||||
fn main() -> Result<()> {
|
||||
let mut num_sockets = 1usize;
|
||||
|
||||
let matches = App::new("solana-bench-streamer")
|
||||
let matches = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.version(crate_version!())
|
||||
.arg(
|
||||
Arg::with_name("num-recv-sockets")
|
||||
.long("num-recv-sockets")
|
||||
@ -81,7 +83,7 @@ fn main() -> Result<()> {
|
||||
|
||||
let (s_reader, r_reader) = channel();
|
||||
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());
|
||||
|
3
bench-tps/.gitignore
vendored
Normal file
3
bench-tps/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target/
|
||||
/config/
|
||||
/config-local/
|
@ -2,20 +2,24 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-tps"
|
||||
version = "0.12.0"
|
||||
version = "0.14.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.32.0"
|
||||
clap = "2.33.0"
|
||||
rayon = "1.0.3"
|
||||
serde_json = "1.0.39"
|
||||
solana = { path = "../core", version = "0.12.0" }
|
||||
solana-drone = { path = "../drone", version = "0.12.0" }
|
||||
solana-logger = { path = "../logger", version = "0.12.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.12.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.12.0" }
|
||||
solana = { path = "../core", version = "0.14.0" }
|
||||
solana-client = { path = "../client", version = "0.14.0" }
|
||||
solana-drone = { path = "../drone", version = "0.14.0" }
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.14.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.14.0" }
|
||||
|
||||
[features]
|
||||
cuda = ["solana/cuda"]
|
||||
erasure = []
|
||||
|
@ -1,15 +1,13 @@
|
||||
use solana_metrics;
|
||||
|
||||
use rayon::prelude::*;
|
||||
use solana::client::mk_client;
|
||||
use solana::contact_info::ContactInfo;
|
||||
use solana::thin_client::ThinClient;
|
||||
use solana::gen_keys::GenKeys;
|
||||
use solana_drone::drone::request_airdrop_transaction;
|
||||
use solana_metrics::influxdb;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::client::Client;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction::SystemTransaction;
|
||||
use solana_sdk::system_instruction;
|
||||
use solana_sdk::system_transaction;
|
||||
use solana_sdk::timing::timestamp;
|
||||
use solana_sdk::timing::{duration_as_ms, duration_as_s};
|
||||
use solana_sdk::transaction::Transaction;
|
||||
@ -20,6 +18,7 @@ use std::process::exit;
|
||||
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::thread::Builder;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
@ -31,10 +30,173 @@ pub struct NodeStats {
|
||||
}
|
||||
|
||||
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 fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
||||
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,
|
||||
) where
|
||||
T: 'static + Client + Send + Sync,
|
||||
{
|
||||
let Config {
|
||||
id,
|
||||
threads,
|
||||
thread_batch_sleep_ms,
|
||||
duration,
|
||||
tx_count,
|
||||
sustained,
|
||||
} = config;
|
||||
|
||||
let clients: Vec<_> = clients.into_iter().map(Arc::new).collect();
|
||||
let client = &clients[0];
|
||||
|
||||
let start = gen_keypairs.len() - (tx_count * 2) as usize;
|
||||
let keypairs = &gen_keypairs[start..];
|
||||
|
||||
let first_tx_count = client.get_transaction_count().expect("transaction count");
|
||||
println!("Initial transaction count {}", first_tx_count);
|
||||
|
||||
let exit_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Setup a thread per validator to sample every period
|
||||
// collect the max transaction rate and total tx count seen
|
||||
let maxes = Arc::new(RwLock::new(Vec::new()));
|
||||
let sample_period = 1; // in seconds
|
||||
println!("Sampling TPS every {} second...", sample_period);
|
||||
let v_threads: Vec<_> = clients
|
||||
.iter()
|
||||
.map(|client| {
|
||||
let exit_signal = exit_signal.clone();
|
||||
let maxes = maxes.clone();
|
||||
let client = client.clone();
|
||||
Builder::new()
|
||||
.name("solana-client-sample".to_string())
|
||||
.spawn(move || {
|
||||
sample_tx_count(&exit_signal, &maxes, first_tx_count, sample_period, &client);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
|
||||
|
||||
let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0));
|
||||
let total_tx_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 shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
|
||||
let total_tx_sent_count = total_tx_sent_count.clone();
|
||||
let client = client.clone();
|
||||
Builder::new()
|
||||
.name("solana-client-sender".to_string())
|
||||
.spawn(move || {
|
||||
do_tx_transfers(
|
||||
&exit_signal,
|
||||
&shared_txs,
|
||||
&shared_tx_active_thread_count,
|
||||
&total_tx_sent_count,
|
||||
thread_batch_sleep_ms,
|
||||
&client,
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// generate and send transactions for the specified duration
|
||||
let start = Instant::now();
|
||||
let mut reclaim_lamports_back_to_source_account = false;
|
||||
let mut i = keypair0_balance;
|
||||
while start.elapsed() < duration {
|
||||
let balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
|
||||
// ping-pong between source and destination accounts for each loop iteration
|
||||
// this seems to be faster than trying to determine the balance of individual
|
||||
// accounts
|
||||
let len = tx_count as usize;
|
||||
generate_txs(
|
||||
&shared_txs,
|
||||
&keypairs[..len],
|
||||
&keypairs[len..],
|
||||
threads,
|
||||
reclaim_lamports_back_to_source_account,
|
||||
&client,
|
||||
);
|
||||
// In sustained mode overlap the transfers with generation
|
||||
// this has higher average performance but lower peak performance
|
||||
// in tested environments.
|
||||
if !sustained {
|
||||
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
if should_switch_directions(NUM_LAMPORTS_PER_ACCOUNT, i) {
|
||||
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the sampling threads so it will collect the stats
|
||||
exit_signal.store(true, Ordering::Relaxed);
|
||||
|
||||
println!("Waiting for validator threads...");
|
||||
for t in v_threads {
|
||||
if let Err(err) = t.join() {
|
||||
println!(" join() failed with: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// join the tx send threads
|
||||
println!("Waiting for transmit threads...");
|
||||
for t in s_threads {
|
||||
if let Err(err) = t.join() {
|
||||
println!(" join() failed with: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
|
||||
compute_and_report_stats(
|
||||
&maxes,
|
||||
sample_period,
|
||||
&start.elapsed(),
|
||||
total_tx_sent_count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
|
||||
fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
||||
println!("Token balance: {}", lamport_balance);
|
||||
solana_metrics::submit(
|
||||
influxdb::Point::new("bench-tps")
|
||||
@ -44,29 +206,29 @@ pub fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn sample_tx_count(
|
||||
fn sample_tx_count<T: Client>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
maxes: &Arc<RwLock<Vec<(SocketAddr, NodeStats)>>>,
|
||||
maxes: &Arc<RwLock<Vec<(String, NodeStats)>>>,
|
||||
first_tx_count: u64,
|
||||
v: &ContactInfo,
|
||||
sample_period: u64,
|
||||
client: &Arc<T>,
|
||||
) {
|
||||
let mut client = mk_client(&v);
|
||||
let mut now = Instant::now();
|
||||
let mut initial_tx_count = client.transaction_count();
|
||||
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());
|
||||
let log_prefix = format!("{:21}:", client.transactions_addr());
|
||||
|
||||
loop {
|
||||
let tx_count = client.transaction_count();
|
||||
assert!(
|
||||
tx_count >= initial_tx_count,
|
||||
"expected tx_count({}) >= initial_tx_count({})",
|
||||
tx_count,
|
||||
initial_tx_count
|
||||
);
|
||||
let mut tx_count = client.get_transaction_count().expect("transaction count");
|
||||
if tx_count < initial_tx_count {
|
||||
println!(
|
||||
"expected tx_count({}) >= initial_tx_count({})",
|
||||
tx_count, initial_tx_count
|
||||
);
|
||||
tx_count = initial_tx_count;
|
||||
}
|
||||
let duration = now.elapsed();
|
||||
now = Instant::now();
|
||||
let sample = tx_count - initial_tx_count;
|
||||
@ -94,95 +256,24 @@ pub fn sample_tx_count(
|
||||
tps: max_tps,
|
||||
tx: total,
|
||||
};
|
||||
maxes.write().unwrap().push((v.tpu, stats));
|
||||
maxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.push((client.transactions_addr(), stats));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send loopback payment of 0 lamports and confirm the network processed it
|
||||
pub 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();
|
||||
let signature = barrier_client
|
||||
.transfer(0, &source_keypair, dest_id, blockhash)
|
||||
.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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_txs(
|
||||
fn generate_txs<T: Client>(
|
||||
shared_txs: &SharedTransactions,
|
||||
source: &[Keypair],
|
||||
dest: &[Keypair],
|
||||
threads: usize,
|
||||
reclaim: bool,
|
||||
contact_info: &ContactInfo,
|
||||
client: &Arc<T>,
|
||||
) {
|
||||
let mut client = mk_client(contact_info);
|
||||
let blockhash = client.get_recent_blockhash();
|
||||
let blockhash = client.get_recent_blockhash().unwrap();
|
||||
let tx_count = source.len();
|
||||
println!("Signing transactions... {} (reclaim={})", tx_count, reclaim);
|
||||
let signing_start = Instant::now();
|
||||
@ -196,7 +287,7 @@ pub fn generate_txs(
|
||||
.par_iter()
|
||||
.map(|(id, keypair)| {
|
||||
(
|
||||
SystemTransaction::new_account(id, &keypair.pubkey(), 1, blockhash, 0),
|
||||
system_transaction::create_user_account(id, &keypair.pubkey(), 1, blockhash, 0),
|
||||
timestamp(),
|
||||
)
|
||||
})
|
||||
@ -233,15 +324,14 @@ pub fn generate_txs(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_tx_transfers(
|
||||
fn do_tx_transfers<T: Client>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
shared_txs: &SharedTransactions,
|
||||
contact_info: &ContactInfo,
|
||||
shared_tx_thread_count: &Arc<AtomicIsize>,
|
||||
total_tx_sent_count: &Arc<AtomicUsize>,
|
||||
thread_batch_sleep_ms: usize,
|
||||
client: &Arc<T>,
|
||||
) {
|
||||
let client = mk_client(&contact_info);
|
||||
loop {
|
||||
if thread_batch_sleep_ms > 0 {
|
||||
sleep(Duration::from_millis(thread_batch_sleep_ms as u64));
|
||||
@ -256,7 +346,7 @@ pub fn do_tx_transfers(
|
||||
println!(
|
||||
"Transferring 1 unit {} times... to {}",
|
||||
txs0.len(),
|
||||
contact_info.tpu
|
||||
client.as_ref().transactions_addr(),
|
||||
);
|
||||
let tx_len = txs0.len();
|
||||
let transfer_start = Instant::now();
|
||||
@ -265,7 +355,7 @@ pub fn do_tx_transfers(
|
||||
if now > tx.1 && now - tx.1 > 1000 * 30 {
|
||||
continue;
|
||||
}
|
||||
client.transfer_signed(&tx.0).unwrap();
|
||||
client.async_send_transaction(tx.0).unwrap();
|
||||
}
|
||||
shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed);
|
||||
total_tx_sent_count.fetch_add(tx_len, Ordering::Relaxed);
|
||||
@ -291,8 +381,8 @@ pub fn do_tx_transfers(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_funding_transfer(client: &mut ThinClient, tx: &Transaction, amount: u64) -> bool {
|
||||
for a in &tx.account_keys[1..] {
|
||||
fn verify_funding_transfer<T: Client>(client: &T, tx: &Transaction, amount: u64) -> bool {
|
||||
for a in &tx.message().account_keys[1..] {
|
||||
if client.get_balance(a).unwrap_or(0) >= amount {
|
||||
return true;
|
||||
}
|
||||
@ -304,7 +394,7 @@ pub fn verify_funding_transfer(client: &mut ThinClient, tx: &Transaction, amount
|
||||
/// 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,
|
||||
/// or full
|
||||
pub fn fund_keys(client: &mut 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 mut funded: Vec<(&Keypair, u64)> = vec![(source, total)];
|
||||
let mut notfunded: Vec<&Keypair> = dests.iter().collect();
|
||||
@ -348,7 +438,10 @@ pub fn fund_keys(client: &mut ThinClient, source: &Keypair, dests: &[Keypair], l
|
||||
.map(|(k, m)| {
|
||||
(
|
||||
k.clone(),
|
||||
SystemTransaction::new_move_many(k, &m, Hash::default(), 0),
|
||||
Transaction::new_unsigned_instructions(system_instruction::transfer_many(
|
||||
&k.pubkey(),
|
||||
&m,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@ -358,7 +451,7 @@ pub fn fund_keys(client: &mut ThinClient, source: &Keypair, dests: &[Keypair], l
|
||||
while !to_fund_txs.is_empty() {
|
||||
let receivers = to_fund_txs
|
||||
.iter()
|
||||
.fold(0, |len, (_, tx)| len + tx.instructions.len());
|
||||
.fold(0, |len, (_, tx)| len + tx.message().instructions.len());
|
||||
|
||||
println!(
|
||||
"{} {} to {} in {} txs",
|
||||
@ -372,7 +465,7 @@ pub fn fund_keys(client: &mut ThinClient, source: &Keypair, dests: &[Keypair], l
|
||||
to_fund_txs.len(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_recent_blockhash();
|
||||
let blockhash = client.get_recent_blockhash().unwrap();
|
||||
|
||||
// re-sign retained to_fund_txes with updated blockhash
|
||||
to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
|
||||
@ -380,13 +473,19 @@ pub fn fund_keys(client: &mut ThinClient, source: &Keypair, dests: &[Keypair], l
|
||||
});
|
||||
|
||||
to_fund_txs.iter().for_each(|(_, tx)| {
|
||||
client.transfer_signed(&tx).expect("transfer");
|
||||
client.async_send_transaction(tx.clone()).expect("transfer");
|
||||
});
|
||||
|
||||
// retry anything that seems to have dropped through cracks
|
||||
// again since these txs are all or nothing, they're fine to
|
||||
// retry
|
||||
to_fund_txs.retain(|(_, tx)| !verify_funding_transfer(client, &tx, amount));
|
||||
for _ in 0..10 {
|
||||
to_fund_txs.retain(|(_, tx)| !verify_funding_transfer(client, &tx, amount));
|
||||
if to_fund_txs.is_empty() {
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
tries += 1;
|
||||
}
|
||||
@ -397,13 +496,13 @@ pub fn fund_keys(client: &mut ThinClient, source: &Keypair, dests: &[Keypair], l
|
||||
}
|
||||
}
|
||||
|
||||
pub fn airdrop_lamports(
|
||||
client: &mut ThinClient,
|
||||
pub fn airdrop_lamports<T: Client>(
|
||||
client: &T,
|
||||
drone_addr: &SocketAddr,
|
||||
id: &Keypair,
|
||||
tx_count: u64,
|
||||
) {
|
||||
let starting_balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
||||
let starting_balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(starting_balance);
|
||||
println!("starting balance {}", starting_balance);
|
||||
|
||||
@ -416,11 +515,18 @@ pub fn airdrop_lamports(
|
||||
id.pubkey(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_recent_blockhash();
|
||||
let blockhash = client.get_recent_blockhash().unwrap();
|
||||
match request_airdrop_transaction(&drone_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
||||
Ok(transaction) => {
|
||||
let signature = client.transfer_signed(&transaction).unwrap();
|
||||
client.poll_for_signature(&signature).unwrap();
|
||||
let signature = client.async_send_transaction(transaction).unwrap();
|
||||
client
|
||||
.poll_for_signature_confirmation(&signature, 1)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Error requesting airdrop: to addr: {:?} amount: {}",
|
||||
drone_addr, airdrop_amount
|
||||
)
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
panic!(
|
||||
@ -430,7 +536,7 @@ pub fn airdrop_lamports(
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
starting_balance
|
||||
});
|
||||
@ -449,8 +555,8 @@ pub fn airdrop_lamports(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_and_report_stats(
|
||||
maxes: &Arc<RwLock<Vec<(SocketAddr, NodeStats)>>>,
|
||||
fn compute_and_report_stats(
|
||||
maxes: &Arc<RwLock<Vec<(String, NodeStats)>>>,
|
||||
sample_period: u64,
|
||||
tx_send_elapsed: &Duration,
|
||||
total_tx_send_count: usize,
|
||||
@ -471,10 +577,7 @@ pub fn compute_and_report_stats(
|
||||
|
||||
println!(
|
||||
"{:20} | {:13.2} | {} {}",
|
||||
(*sock).to_string(),
|
||||
stats.tps,
|
||||
stats.tx,
|
||||
maybe_flag
|
||||
sock, stats.tps, stats.tx, maybe_flag
|
||||
);
|
||||
|
||||
if stats.tps == 0.0 {
|
||||
@ -516,13 +619,37 @@ pub fn compute_and_report_stats(
|
||||
// First transfer 3/4 of the lamports to the dest accounts
|
||||
// then ping-pong 1/4 of the lamports back to the other account
|
||||
// this leaves 1/4 lamport buffer in each account
|
||||
pub fn should_switch_directions(num_lamports_per_account: u64, i: u64) -> bool {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn generate_keypairs(id: &Keypair, tx_count: usize) -> Vec<Keypair> {
|
||||
let mut seed = [0u8; 32];
|
||||
seed.copy_from_slice(&id.to_bytes()[..32]);
|
||||
let mut rnd = GenKeys::new(seed);
|
||||
|
||||
let mut total_keys = 0;
|
||||
let mut target = tx_count * 2;
|
||||
while target > 0 {
|
||||
total_keys += target;
|
||||
target /= MAX_SPENDS_PER_TX;
|
||||
}
|
||||
rnd.gen_n_keypairs(total_keys as u64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana::cluster_info::FULLNODE_PORT_RANGE;
|
||||
use solana::fullnode::FullnodeConfig;
|
||||
use solana::local_cluster::{ClusterConfig, LocalCluster};
|
||||
use solana_client::thin_client::create_client;
|
||||
use solana_drone::drone::run_local_drone;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
#[test]
|
||||
fn test_switch_directions() {
|
||||
assert_eq!(should_switch_directions(20, 0), false);
|
||||
@ -537,4 +664,53 @@ mod tests {
|
||||
assert_eq!(should_switch_directions(20, 100), true);
|
||||
assert_eq!(should_switch_directions(20, 101), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_bench_tps() {
|
||||
let fullnode_config = FullnodeConfig::default();
|
||||
const NUM_NODES: usize = 1;
|
||||
let cluster = LocalCluster::new(&ClusterConfig {
|
||||
node_stakes: vec![999_990; NUM_NODES],
|
||||
cluster_lamports: 2_000_000,
|
||||
fullnode_config,
|
||||
..ClusterConfig::default()
|
||||
});
|
||||
|
||||
let drone_keypair = Keypair::new();
|
||||
cluster.transfer(&cluster.funding_keypair, &drone_keypair.pubkey(), 1_000_000);
|
||||
|
||||
let (addr_sender, addr_receiver) = channel();
|
||||
run_local_drone(drone_keypair, addr_sender, None);
|
||||
let drone_addr = addr_receiver.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
|
||||
let mut config = Config::default();
|
||||
config.tx_count = 100;
|
||||
config.duration = Duration::from_secs(5);
|
||||
|
||||
let keypairs = generate_keypairs(&config.id, config.tx_count);
|
||||
let client = create_client(
|
||||
(cluster.entry_point_info.gossip, drone_addr),
|
||||
FULLNODE_PORT_RANGE,
|
||||
);
|
||||
|
||||
do_bench_tps(vec![client], config, keypairs, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_bank_client() {
|
||||
let (genesis_block, id) = GenesisBlock::new(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 = generate_keypairs(&config.id, config.tx_count);
|
||||
fund_keys(&clients[0], &config.id, &keypairs, 20);
|
||||
|
||||
do_bench_tps(clients, config, keypairs, 0);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::net::SocketAddr;
|
||||
use std::process::exit;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::{crate_version, App, Arg, ArgMatches};
|
||||
use clap::{crate_description, crate_name, crate_version, App, Arg, ArgMatches};
|
||||
use solana_drone::drone::DRONE_PORT;
|
||||
use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
|
||||
|
||||
@ -17,8 +17,6 @@ pub struct Config {
|
||||
pub tx_count: usize,
|
||||
pub thread_batch_sleep_ms: usize,
|
||||
pub sustained: bool,
|
||||
pub reject_extra_nodes: bool,
|
||||
pub converge_only: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@ -33,15 +31,13 @@ impl Default for Config {
|
||||
tx_count: 500_000,
|
||||
thread_batch_sleep_ms: 0,
|
||||
sustained: false,
|
||||
reject_extra_nodes: false,
|
||||
converge_only: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines and builds the CLI args for a run of the benchmark
|
||||
pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
App::new("solana-bench-tps")
|
||||
App::new(crate_name!()).about(crate_description!())
|
||||
.version(crate_version!())
|
||||
.arg(
|
||||
Arg::with_name("network")
|
||||
@ -75,11 +71,6 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
.takes_value(true)
|
||||
.help("Wait for NUM nodes to converge"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("reject-extra-nodes")
|
||||
.long("reject-extra-nodes")
|
||||
.help("Require exactly `num-nodes` on convergence. Appropriate only for internal networks"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("threads")
|
||||
.short("t")
|
||||
@ -95,11 +86,6 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
.takes_value(true)
|
||||
.help("Seconds to run benchmark, then exit; default is forever"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("converge-only")
|
||||
.long("converge-only")
|
||||
.help("Exit immediately after converging"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("sustained")
|
||||
.long("sustained")
|
||||
@ -131,14 +117,14 @@ pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
||||
let mut args = Config::default();
|
||||
|
||||
if let Some(addr) = matches.value_of("network") {
|
||||
args.network_addr = addr.parse().unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse network: {}", e);
|
||||
args.network_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse network address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(addr) = matches.value_of("drone") {
|
||||
args.drone_addr = addr.parse().unwrap_or_else(|e| {
|
||||
args.drone_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse drone address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
@ -176,8 +162,6 @@ pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
||||
}
|
||||
|
||||
args.sustained = matches.is_present("sustained");
|
||||
args.converge_only = matches.is_present("converge-only");
|
||||
args.reject_extra_nodes = matches.is_present("reject-extra-nodes");
|
||||
|
||||
args
|
||||
}
|
||||
|
@ -1,44 +1,38 @@
|
||||
mod bench;
|
||||
mod cli;
|
||||
|
||||
use crate::bench::*;
|
||||
use solana::client::mk_client;
|
||||
use solana::gen_keys::GenKeys;
|
||||
use solana::gossip_service::discover;
|
||||
use solana_metrics;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use std::collections::VecDeque;
|
||||
use crate::bench::{
|
||||
airdrop_lamports, do_bench_tps, fund_keys, generate_keypairs, Config, NUM_LAMPORTS_PER_ACCOUNT,
|
||||
};
|
||||
use solana::cluster_info::FULLNODE_PORT_RANGE;
|
||||
use solana::contact_info::ContactInfo;
|
||||
use solana::gossip_service::discover_nodes;
|
||||
use solana_client::thin_client::create_client;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
use std::process::exit;
|
||||
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::thread::Builder;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup();
|
||||
solana_metrics::set_panic_hook("bench-tps");
|
||||
|
||||
let matches = cli::build_args().get_matches();
|
||||
|
||||
let cfg = cli::extract_args(&matches);
|
||||
let cli_config = cli::extract_args(&matches);
|
||||
|
||||
let cli::Config {
|
||||
network_addr: network,
|
||||
network_addr,
|
||||
drone_addr,
|
||||
id,
|
||||
threads,
|
||||
thread_batch_sleep_ms,
|
||||
num_nodes,
|
||||
duration,
|
||||
tx_count,
|
||||
thread_batch_sleep_ms,
|
||||
sustained,
|
||||
reject_extra_nodes,
|
||||
converge_only,
|
||||
} = cfg;
|
||||
} = cli_config;
|
||||
|
||||
let nodes = discover(&network, num_nodes).unwrap_or_else(|err| {
|
||||
println!("Connecting to the cluster");
|
||||
let nodes = discover_nodes(&network_addr, num_nodes).unwrap_or_else(|err| {
|
||||
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
||||
exit(1);
|
||||
});
|
||||
@ -49,201 +43,49 @@ fn main() {
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
if reject_extra_nodes && nodes.len() > num_nodes {
|
||||
eprintln!(
|
||||
"Error: Extra nodes discovered. Expecting exactly {}",
|
||||
num_nodes
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if converge_only {
|
||||
return;
|
||||
}
|
||||
let cluster_entrypoint = nodes[0].clone(); // Pick the first node, why not?
|
||||
|
||||
let mut client = mk_client(&cluster_entrypoint);
|
||||
let mut barrier_client = mk_client(&cluster_entrypoint);
|
||||
|
||||
let mut seed = [0u8; 32];
|
||||
seed.copy_from_slice(&id.public_key_bytes()[..32]);
|
||||
let mut rnd = GenKeys::new(seed);
|
||||
let clients: Vec<_> = nodes
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
let cluster_entrypoint = node.clone();
|
||||
let cluster_addrs = cluster_entrypoint.client_facing_addr();
|
||||
if ContactInfo::is_valid_address(&cluster_addrs.0)
|
||||
&& ContactInfo::is_valid_address(&cluster_addrs.1)
|
||||
{
|
||||
let client = create_client(cluster_addrs, FULLNODE_PORT_RANGE);
|
||||
Some(client)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
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 = Keypair::new().pubkey();
|
||||
let keypairs = generate_keypairs(&id, tx_count);
|
||||
|
||||
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())
|
||||
// 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 keypair0_balance = clients[0]
|
||||
.get_balance(&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(&mut client, &drone_addr, &id, total);
|
||||
if NUM_LAMPORTS_PER_ACCOUNT > keypair0_balance {
|
||||
let extra = NUM_LAMPORTS_PER_ACCOUNT - keypair0_balance;
|
||||
let total = extra * (keypairs.len() as u64);
|
||||
airdrop_lamports(&clients[0], &drone_addr, &id, total);
|
||||
println!("adding more lamports {}", extra);
|
||||
fund_keys(&mut client, &id, &gen_keypairs, extra);
|
||||
}
|
||||
let start = gen_keypairs.len() - (tx_count * 2) as usize;
|
||||
let keypairs = &gen_keypairs[start..];
|
||||
airdrop_lamports(&mut barrier_client, &drone_addr, &barrier_source_keypair, 1);
|
||||
|
||||
println!("Get last ID...");
|
||||
let mut blockhash = client.get_recent_blockhash();
|
||||
println!("Got last ID {:?}", blockhash);
|
||||
|
||||
let first_tx_count = client.transaction_count();
|
||||
println!("Initial transaction count {}", first_tx_count);
|
||||
|
||||
let exit_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Setup a thread per validator to sample every period
|
||||
// collect the max transaction rate and total tx count seen
|
||||
let maxes = Arc::new(RwLock::new(Vec::new()));
|
||||
let sample_period = 1; // in seconds
|
||||
println!("Sampling TPS every {} second...", sample_period);
|
||||
let v_threads: Vec<_> = nodes
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
let exit_signal = exit_signal.clone();
|
||||
let maxes = maxes.clone();
|
||||
Builder::new()
|
||||
.name("solana-client-sample".to_string())
|
||||
.spawn(move || {
|
||||
sample_tx_count(&exit_signal, &maxes, first_tx_count, &v, sample_period);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
|
||||
|
||||
let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0));
|
||||
let total_tx_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 cluster_entrypoint = cluster_entrypoint.clone();
|
||||
let shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
|
||||
let total_tx_sent_count = total_tx_sent_count.clone();
|
||||
Builder::new()
|
||||
.name("solana-client-sender".to_string())
|
||||
.spawn(move || {
|
||||
do_tx_transfers(
|
||||
&exit_signal,
|
||||
&shared_txs,
|
||||
&cluster_entrypoint,
|
||||
&shared_tx_active_thread_count,
|
||||
&total_tx_sent_count,
|
||||
thread_batch_sleep_ms,
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// generate and send transactions for the specified duration
|
||||
let start = Instant::now();
|
||||
let mut reclaim_lamports_back_to_source_account = false;
|
||||
let mut i = keypair0_balance;
|
||||
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
|
||||
// this seems to be faster than trying to determine the balance of individual
|
||||
// accounts
|
||||
let len = tx_count as usize;
|
||||
generate_txs(
|
||||
&shared_txs,
|
||||
&keypairs[..len],
|
||||
&keypairs[len..],
|
||||
threads,
|
||||
reclaim_lamports_back_to_source_account,
|
||||
&cluster_entrypoint,
|
||||
);
|
||||
// In sustained mode overlap the transfers with generation
|
||||
// this has higher average performance but lower peak performance
|
||||
// in tested environments.
|
||||
if !sustained {
|
||||
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
if should_switch_directions(num_lamports_per_account, i) {
|
||||
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
|
||||
}
|
||||
fund_keys(&clients[0], &id, &keypairs, extra);
|
||||
}
|
||||
|
||||
// Stop the sampling threads so it will collect the stats
|
||||
exit_signal.store(true, Ordering::Relaxed);
|
||||
let config = Config {
|
||||
id,
|
||||
threads,
|
||||
thread_batch_sleep_ms,
|
||||
duration,
|
||||
tx_count,
|
||||
sustained,
|
||||
};
|
||||
|
||||
println!("Waiting for validator threads...");
|
||||
for t in v_threads {
|
||||
if let Err(err) = t.join() {
|
||||
println!(" join() failed with: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// join the tx send threads
|
||||
println!("Waiting for transmit threads...");
|
||||
for t in s_threads {
|
||||
if let Err(err) = t.join() {
|
||||
println!(" join() failed with: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
|
||||
compute_and_report_stats(
|
||||
&maxes,
|
||||
sample_period,
|
||||
&start.elapsed(),
|
||||
total_tx_sent_count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_switch_directions() {
|
||||
assert_eq!(should_switch_directions(20, 0), false);
|
||||
assert_eq!(should_switch_directions(20, 1), false);
|
||||
assert_eq!(should_switch_directions(20, 14), false);
|
||||
assert_eq!(should_switch_directions(20, 15), true);
|
||||
assert_eq!(should_switch_directions(20, 16), false);
|
||||
assert_eq!(should_switch_directions(20, 19), false);
|
||||
assert_eq!(should_switch_directions(20, 20), true);
|
||||
assert_eq!(should_switch_directions(20, 21), false);
|
||||
assert_eq!(should_switch_directions(20, 99), false);
|
||||
assert_eq!(should_switch_directions(20, 100), true);
|
||||
assert_eq!(should_switch_directions(20, 101), false);
|
||||
}
|
||||
do_bench_tps(clients, config, keypairs, keypair0_balance);
|
||||
}
|
||||
|
@ -1,248 +0,0 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate rand;
|
||||
extern crate test;
|
||||
|
||||
use bincode::{deserialize, serialize_into, serialized_size};
|
||||
use rand::{thread_rng, Rng};
|
||||
use solana_runtime::append_vec::{
|
||||
deserialize_account, get_serialized_size, serialize_account, AppendVec,
|
||||
};
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use std::env;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::spawn;
|
||||
use test::Bencher;
|
||||
|
||||
const START_SIZE: u64 = 4 * 1024 * 1024;
|
||||
const INC_SIZE: u64 = 1 * 1024 * 1024;
|
||||
|
||||
macro_rules! align_up {
|
||||
($addr: expr, $align: expr) => {
|
||||
($addr + ($align - 1)) & !($align - 1)
|
||||
};
|
||||
}
|
||||
|
||||
fn get_append_vec_bench_path(path: &str) -> PathBuf {
|
||||
let out_dir = env::var("OUT_DIR").unwrap_or_else(|_| "target".to_string());
|
||||
let mut buf = PathBuf::new();
|
||||
buf.push(&format!("{}/{}", out_dir, path));
|
||||
buf
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_atomic_append(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_append");
|
||||
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||
bencher.iter(|| {
|
||||
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||
}
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_atomic_random_access(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_ra");
|
||||
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||
let size = 1_000_000;
|
||||
for _ in 0..size {
|
||||
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||
}
|
||||
}
|
||||
bencher.iter(|| {
|
||||
let index = thread_rng().gen_range(0, size as u64);
|
||||
vec.get(index * std::mem::size_of::<AtomicUsize>() as u64);
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_atomic_random_change(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_rax");
|
||||
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||
let size = 1_000_000;
|
||||
for k in 0..size {
|
||||
if vec.append(AtomicUsize::new(k)).is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
assert!(vec.append(AtomicUsize::new(k)).is_some());
|
||||
}
|
||||
}
|
||||
bencher.iter(|| {
|
||||
let index = thread_rng().gen_range(0, size as u64);
|
||||
let atomic1 = vec.get(index * std::mem::size_of::<AtomicUsize>() as u64);
|
||||
let current1 = atomic1.load(Ordering::Relaxed);
|
||||
assert_eq!(current1, index as usize);
|
||||
let next = current1 + 1;
|
||||
let mut index = vec.append(AtomicUsize::new(next));
|
||||
if index.is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
index = vec.append(AtomicUsize::new(next));
|
||||
}
|
||||
let atomic2 = vec.get(index.unwrap());
|
||||
let current2 = atomic2.load(Ordering::Relaxed);
|
||||
assert_eq!(current2, next);
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_atomic_random_read(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_read");
|
||||
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||
let size = 1_000_000;
|
||||
for _ in 0..size {
|
||||
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||
}
|
||||
}
|
||||
bencher.iter(|| {
|
||||
let index = thread_rng().gen_range(0, size);
|
||||
let atomic1 = vec.get((index * std::mem::size_of::<AtomicUsize>()) as u64);
|
||||
let current1 = atomic1.load(Ordering::Relaxed);
|
||||
assert_eq!(current1, 0);
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_concurrent_lock_append(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_lock_append");
|
||||
let vec = Arc::new(RwLock::new(AppendVec::<AtomicUsize>::new(
|
||||
&path, true, START_SIZE, INC_SIZE,
|
||||
)));
|
||||
let vec1 = vec.clone();
|
||||
let size = 1_000_000;
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let count1 = count.clone();
|
||||
spawn(move || loop {
|
||||
let mut len = count.load(Ordering::Relaxed);
|
||||
{
|
||||
let rlock = vec1.read().unwrap();
|
||||
loop {
|
||||
if rlock.append(AtomicUsize::new(0)).is_none() {
|
||||
break;
|
||||
}
|
||||
len = count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
if len >= size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut wlock = vec1.write().unwrap();
|
||||
if len >= size {
|
||||
break;
|
||||
}
|
||||
assert!(wlock.grow_file().is_ok());
|
||||
}
|
||||
});
|
||||
bencher.iter(|| {
|
||||
let _rlock = vec.read().unwrap();
|
||||
let len = count1.load(Ordering::Relaxed);
|
||||
assert!(len < size * 2);
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_concurrent_get_append(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_get_append");
|
||||
let vec = Arc::new(RwLock::new(AppendVec::<AtomicUsize>::new(
|
||||
&path, true, START_SIZE, INC_SIZE,
|
||||
)));
|
||||
let vec1 = vec.clone();
|
||||
let size = 1_000_000;
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let count1 = count.clone();
|
||||
spawn(move || loop {
|
||||
let mut len = count.load(Ordering::Relaxed);
|
||||
{
|
||||
let rlock = vec1.read().unwrap();
|
||||
loop {
|
||||
if rlock.append(AtomicUsize::new(0)).is_none() {
|
||||
break;
|
||||
}
|
||||
len = count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
if len >= size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut wlock = vec1.write().unwrap();
|
||||
if len >= size {
|
||||
break;
|
||||
}
|
||||
assert!(wlock.grow_file().is_ok());
|
||||
}
|
||||
});
|
||||
bencher.iter(|| {
|
||||
let rlock = vec.read().unwrap();
|
||||
let len = count1.load(Ordering::Relaxed);
|
||||
if len > 0 {
|
||||
let index = thread_rng().gen_range(0, len);
|
||||
rlock.get((index * std::mem::size_of::<AtomicUsize>()) as u64);
|
||||
}
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_account_serialize(bencher: &mut Bencher) {
|
||||
let num: usize = 1000;
|
||||
let account = Account::new(2, 100, &Keypair::new().pubkey());
|
||||
let len = get_serialized_size(&account);
|
||||
let ser_len = align_up!(len + std::mem::size_of::<u64>(), std::mem::size_of::<u64>());
|
||||
let mut memory = vec![0; num * ser_len];
|
||||
bencher.iter(|| {
|
||||
for i in 0..num {
|
||||
let start = i * ser_len;
|
||||
serialize_account(&mut memory[start..start + ser_len], &account, len);
|
||||
}
|
||||
});
|
||||
|
||||
// make sure compiler doesn't delete the code.
|
||||
let index = thread_rng().gen_range(0, num);
|
||||
if memory[index] != 0 {
|
||||
println!("memory: {}", memory[index]);
|
||||
}
|
||||
|
||||
let start = index * ser_len;
|
||||
let new_account = deserialize_account(&memory[start..start + ser_len], 0, num * len).unwrap();
|
||||
assert_eq!(new_account, account);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_account_serialize_bincode(bencher: &mut Bencher) {
|
||||
let num: usize = 1000;
|
||||
let account = Account::new(2, 100, &Keypair::new().pubkey());
|
||||
let len = serialized_size(&account).unwrap() as usize;
|
||||
let mut memory = vec![0u8; num * len];
|
||||
bencher.iter(|| {
|
||||
for i in 0..num {
|
||||
let start = i * len;
|
||||
let cursor = Cursor::new(&mut memory[start..start + len]);
|
||||
serialize_into(cursor, &account).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// make sure compiler doesn't delete the code.
|
||||
let index = thread_rng().gen_range(0, len);
|
||||
if memory[index] != 0 {
|
||||
println!("memory: {}", memory[index]);
|
||||
}
|
||||
|
||||
let start = index * len;
|
||||
let new_account: Account = deserialize(&memory[start..start + len]).unwrap();
|
||||
assert_eq!(new_account, account);
|
||||
}
|
1
book/.gitattributes
vendored
Normal file
1
book/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
theme/highlight.js binary
|
@ -25,6 +25,6 @@
|
||||
| | | | | | | Downstream | |
|
||||
| | .--+--. .-------+---. | | | Validators | |
|
||||
`-------->| TPU +---->| Broadcast +--------------->| | |
|
||||
| `-----` | Service | | | `------------` |
|
||||
| `-----` | Stage | | | `------------` |
|
||||
| `-----------` | `------------------`
|
||||
`--------------------------------------`
|
||||
|
30
book/art/passive-staking-callflow.msc
Normal file
30
book/art/passive-staking-callflow.msc
Normal file
@ -0,0 +1,30 @@
|
||||
msc {
|
||||
hscale="2.2";
|
||||
VoteSigner,
|
||||
Validator,
|
||||
Cluster,
|
||||
StakerX,
|
||||
StakerY;
|
||||
|
||||
|||;
|
||||
Validator box Validator [label="boot.."];
|
||||
|
||||
VoteSigner <:> Validator [label="register\n\n(optional)"];
|
||||
Validator => Cluster [label="VoteState::Initialize(VoteSigner)"];
|
||||
StakerX => Cluster [label="StakeState::Delegate(Validator)"];
|
||||
StakerY => Cluster [label="StakeState::Delegate(Validator)"];
|
||||
|
||||
|||;
|
||||
Validator box Cluster [label="\nvalidate\n"];
|
||||
Validator => VoteSigner [label="sign(vote)"];
|
||||
VoteSigner >> Validator [label="signed vote"];
|
||||
|
||||
Validator => Cluster [label="gossip(vote)"];
|
||||
...;
|
||||
... ;
|
||||
Validator abox Validator [label="\nmax\nlockout\n"];
|
||||
|||;
|
||||
StakerX => Cluster [label="StakeState::RedeemCredits()"];
|
||||
StakerY => Cluster [label="StakeState::RedeemCredits()"] ;
|
||||
|
||||
}
|
@ -1,16 +1,17 @@
|
||||
.-------------------------------------------.
|
||||
| TPU .-------------. |
|
||||
| | PoH Service | |
|
||||
| `--------+----` |
|
||||
| ^ | |
|
||||
| | v |
|
||||
| .-------. .-----------. .-+-------. | .------------.
|
||||
.---------. | | Fetch | | SigVerify | | Banking | | | Broadcast |
|
||||
| Clients |--->| Stage |->| Stage |->| Stage |------>| Service |
|
||||
`---------` | | | | | | | | | |
|
||||
| `-------` `-----------` `----+----` | `------------`
|
||||
| | |
|
||||
`---------------------------------|---------`
|
||||
|
||||
.-------------.
|
||||
| PoH Service |
|
||||
`--------+----`
|
||||
^ |
|
||||
.------------------------------|----|--------------------.
|
||||
| TPU | v |
|
||||
| .-------. .-----------. .-+-------. .-----------. | .------------.
|
||||
.---------. | | Fetch | | SigVerify | | Banking | | Broadcast | | | Downstream |
|
||||
| Clients |--->| Stage |->| Stage |->| Stage |->| Stage |---->| Validators |
|
||||
`---------` | | | | | | | | | | | |
|
||||
| `-------` `-----------` `----+----` `-----------` | `------------`
|
||||
| | |
|
||||
`---------------------------------|----------------------`
|
||||
|
|
||||
v
|
||||
.------.
|
||||
|
@ -20,6 +20,7 @@
|
||||
- [Ledger Replication](ledger-replication.md)
|
||||
- [Secure Vote Signing](vote-signing.md)
|
||||
- [Staking Delegation and Rewards](stake-delegation-and-rewards.md)
|
||||
- [Performance Metrics](performance-metrics.md)
|
||||
|
||||
- [Anatomy of a Fullnode](fullnode.md)
|
||||
- [TPU](tpu.md)
|
||||
@ -34,14 +35,13 @@
|
||||
- [JavaScript API](javascript-api.md)
|
||||
- [solana-wallet CLI](wallet.md)
|
||||
|
||||
- [Proposed Architectural Changes](proposals.md)
|
||||
- [Accepted Design Proposals](proposals.md)
|
||||
- [Ledger Replication](ledger-replication-to-implement.md)
|
||||
- [Secure Vote Signing](vote-signing-to-implement.md)
|
||||
- [Staking Rewards](staking-rewards.md)
|
||||
- [Fork Selection](fork-selection.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)
|
||||
- [Leader to Leader Transition](leader-leader-transition.md)
|
||||
- [Cluster Economics](ed_overview.md)
|
||||
- [Validation-client Economics](ed_validation_client_economics.md)
|
||||
- [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md)
|
||||
@ -53,7 +53,16 @@
|
||||
- [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md)
|
||||
- [Economic Sustainability](ed_economic_sustainability.md)
|
||||
- [Attack Vectors](ed_attack_vectors.md)
|
||||
- [Economic Design MVP](ed_mvp.md)
|
||||
- [References](ed_references.md)
|
||||
- [Leader-to-Validator Transition](leader-validator-transition.md)
|
||||
- [Cluster Test Framework](cluster-test-framework.md)
|
||||
- [Testing Programs](testing-programs.md)
|
||||
- [Credit-only Accounts](credit-only-credit-debit-accounts.md)
|
||||
- [Cluster Software Installation and Updates](installer.md)
|
||||
- [Deterministic Transaction Fees](transaction-fees.md)
|
||||
|
||||
- [Implemented Design Proposals](implemented-proposals.md)
|
||||
- [Fork Selection](fork-selection.md)
|
||||
- [Leader-to-Leader Transition](leader-leader-transition.md)
|
||||
- [Leader-to-Validator Transition](leader-validator-transition.md)
|
||||
- [Testnet Participation](testnet-participation.md)
|
||||
|
@ -51,10 +51,10 @@ At test start, the cluster has already been established and is fully connected.
|
||||
The test can discover most of the available nodes over a few second.
|
||||
|
||||
```rust,ignore
|
||||
use crate::gossip_service::discover;
|
||||
use crate::gossip_service::discover_nodes;
|
||||
|
||||
// Discover the cluster over a few seconds.
|
||||
let cluster_nodes = discover(&entry_point_info, num_nodes);
|
||||
let cluster_nodes = discover_nodes(&entry_point_info, num_nodes);
|
||||
```
|
||||
|
||||
## Cluster Configuration
|
||||
@ -99,10 +99,10 @@ pub fn test_large_invalid_gossip_nodes(
|
||||
funding_keypair: &Keypair,
|
||||
num_nodes: usize,
|
||||
) {
|
||||
let cluster = discover(&entry_point_info, num_nodes);
|
||||
let cluster = discover_nodes(&entry_point_info, num_nodes);
|
||||
|
||||
// Poison the cluster.
|
||||
let mut client = mk_client(&entry_point_info);
|
||||
let client = create_client(entry_point_info.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||
for _ in 0..(num_nodes * 100) {
|
||||
client.gossip_push(
|
||||
cluster_info::invalid_contact_info()
|
||||
@ -112,7 +112,7 @@ pub fn test_large_invalid_gossip_nodes(
|
||||
|
||||
// Force refresh of the active set.
|
||||
for node in &cluster {
|
||||
let mut client = mk_client(&node);
|
||||
let client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||
client.gossip_refresh_active_set();
|
||||
}
|
||||
|
||||
|
140
book/src/credit-only-credit-debit-accounts.md
Normal file
140
book/src/credit-only-credit-debit-accounts.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Credit-Only Accounts
|
||||
|
||||
This design covers the handling of credit-only and credit-debit accounts in the
|
||||
[runtime](runtime.md). Accounts already distinguish themselves as credit-only or
|
||||
credit-debit based on the program ID specified by the transaction's instruction.
|
||||
Programs must treat accounts that are not owned by them as credit-only.
|
||||
|
||||
To identify credit-only accounts by program id would require the account to be
|
||||
fetched and loaded from disk. This operation is expensive, and while it is
|
||||
occurring, the runtime would have to reject any transactions referencing the same
|
||||
account.
|
||||
|
||||
The proposal introduces a `num_readonly_accounts` field to the transaction
|
||||
structure, and removes the `program_ids` dedicated vector for program accounts.
|
||||
|
||||
This design doesn't change the runtime transaction processing rules.
|
||||
Programs still can't write or spend accounts that they do not own, but it
|
||||
allows the runtime to optimistically take the correct lock for each account
|
||||
specified in the transaction before loading the accounts from storage.
|
||||
|
||||
Accounts selected as credit-debit by the transaction can still be treated as
|
||||
credit-only by the instructions.
|
||||
|
||||
## Runtime handling
|
||||
|
||||
credit-only accounts have the following properties:
|
||||
|
||||
* Can be deposited into: Deposits can be implemented as a simple `atomic_add`.
|
||||
* read-only access to account data.
|
||||
|
||||
Instructions that debit or modify the credit-only account data will fail.
|
||||
|
||||
## Account Lock Optimizations
|
||||
|
||||
The Accounts module keeps track of current locked accounts in the runtime,
|
||||
which separates credit-only accounts from the credit-debit accounts. The credit-only
|
||||
accounts can be cached in memory and shared between all the threads executing
|
||||
transactions.
|
||||
|
||||
The current runtime can't predict whether an account is credit-only or credit-debit when
|
||||
the transaction account keys are locked at the start of the transaction
|
||||
processing pipeline. Accounts referenced by the transaction have not been
|
||||
loaded from the disk yet.
|
||||
|
||||
An ideal design would cache the credit-only accounts while they are referenced by
|
||||
any transaction moving through the runtime, and release the cache when the last
|
||||
transaction exits the runtime.
|
||||
|
||||
## Credit-only accounts and read-only account data
|
||||
|
||||
Credit-only account data can be treated as read-only. Credit-debit
|
||||
account data is treated as read-write.
|
||||
|
||||
## Transaction changes
|
||||
|
||||
To enable the possibility of caching accounts only while they are in the
|
||||
runtime, the Transaction structure should be changed in the following way:
|
||||
|
||||
* `program_ids: Vec<Pubkey>` - This vector is removed. Program keys can be
|
||||
placed at the end of the `account_keys` vector within the `num_readonly_accounts`
|
||||
number set to the number of programs.
|
||||
|
||||
* `num_readonly_accounts: u8` - The number of keys from the **end** of the
|
||||
transaction's `account_keys` array that is credit-only.
|
||||
|
||||
The following possible accounts are present in an transaction:
|
||||
|
||||
* paying account
|
||||
* RW accounts
|
||||
* R accounts
|
||||
* Program IDs
|
||||
|
||||
The paying account must be credit-debit, and program IDs must be credit-only. The
|
||||
first account in the `account_keys` array is always the account that pays for
|
||||
the transaction fee, therefore it cannot be credit-only. For these reasons the
|
||||
credit-only accounts are all grouped together at the end of the `account_keys`
|
||||
vector. Counting credit-only accounts from the end allow for the default `0`
|
||||
value to still be functionally correct, since a transaction will succeed with
|
||||
all credit-debit accounts.
|
||||
|
||||
Since accounts can only appear once in the transaction's `account_keys` array,
|
||||
an account can only be credit-only or credit-debit in a single transaction, not
|
||||
both. The runtime treats a transaction as one atomic unit of execution. If any
|
||||
instruction needs credit-debit access to an account, a copy needs to be made. The
|
||||
write lock is held for the entire time the transaction is being processed by
|
||||
the runtime.
|
||||
|
||||
## Starvation
|
||||
|
||||
Read locks for credit-only accounts can keep the runtime from executing
|
||||
transactions requesting a write lock to a credit-debit account.
|
||||
|
||||
When a request for a write lock is made while a read lock is open, the
|
||||
transaction requesting the write lock should be cached. Upon closing the read
|
||||
lock, the pending transactions can be pushed through the runtime.
|
||||
|
||||
While a pending write transaction exists, any additional read lock requests for
|
||||
that account should fail. It follows that any other write lock requests will also
|
||||
fail. Currently, clients must retransmit when a transaction fails because of
|
||||
a pending transaction. This approach would mimic that behavior as closely as
|
||||
possible while preventing write starvation.
|
||||
|
||||
## Program execution with credit-only accounts
|
||||
|
||||
Before handing off the accounts to program execution, the runtime can mark each
|
||||
account in each instruction as a credit-only account. The credit-only accounts can
|
||||
be passed as references without an extra copy. The transaction will abort on a
|
||||
write to credit-only.
|
||||
|
||||
An alternative is to detect writes to credit-only accounts and fail the
|
||||
transactions before commit.
|
||||
|
||||
## Alternative design
|
||||
|
||||
This design attempts to cache a credit-only account after loading without the use
|
||||
of a transaction-specified credit-only accounts list. Instead, the credit-only
|
||||
accounts are held in a reference-counted table inside the runtime as the
|
||||
transactions are processed.
|
||||
|
||||
1. Transaction accounts are locked.
|
||||
a. If the account is present in the ‘credit-only' table, the TX does not fail.
|
||||
The pending state for this TX is marked NeedReadLock.
|
||||
2. Transaction accounts are loaded.
|
||||
a. Transaction accounts that are credit-only increase their reference
|
||||
count in the `credit-only` table.
|
||||
b. Transaction accounts that need a write lock and are present in the
|
||||
`credit-only` table fail.
|
||||
3. Transaction accounts are unlocked.
|
||||
a. Decrement the `credit-only` lock table reference count; remove if its 0
|
||||
b. Remove from the `lock` set if the account is not in the `credit-only`
|
||||
table.
|
||||
|
||||
The downside with this approach is that if the `lock` set mutex is released
|
||||
between lock and load to allow better pipelining of transactions, a request for
|
||||
a credit-only account may fail. Therefore, this approach is not suitable for
|
||||
treating programs as credit-only accounts.
|
||||
|
||||
Holding the accounts lock mutex while fetching the account from disk would
|
||||
potentially have a significant performance hit on the runtime. Fetching from
|
||||
disk is expected to be slow, but can be parallelized between multiple disks.
|
@ -10,7 +10,7 @@ client's account.
|
||||
A drone is a simple signing service. It listens for requests to sign
|
||||
*transaction data*. Once received, the drone validates the request however it
|
||||
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)`
|
||||
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`
|
||||
@ -76,7 +76,7 @@ beyond a certain *age*.
|
||||
|
||||
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
|
||||
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
|
||||
indicate `Ok`), consideration for this attack may not be required.
|
||||
|
||||
|
12
book/src/ed_mvp.md
Normal file
12
book/src/ed_mvp.md
Normal file
@ -0,0 +1,12 @@
|
||||
## Proposed MVP of Economic Design
|
||||
|
||||
The preceeding sections, outlined in the [Economic Design Overview](ed_overview.md), describe a long-term vision of a sustainable Solana economy. Of course, we don't expect the final implementation to perfectly match what has been described above. We intend to fully engage with network stakeholders throughout the implementation phases (i.e. pre-testnet, testnet, mainnet) to ensure the system supports, and is representative of, the various network participants' interests. The first step toward this goal, however, is outlining a some desired MVP economic features to be available for early pre-testnet and testnet participants. Below is a rough sketch outlining basic economic functionality from which a more complete and functional system can be developed.
|
||||
|
||||
### MVP Economic Features
|
||||
|
||||
* Faucet to deliver testnet SOLs to validators for staking and dapp development.
|
||||
* Mechanism by which validators are rewarded in proportion to their stake. Interest rate mechansism (i.e. to be determined by total % staked) to come later.
|
||||
* Ability to delegate tokens to validator nodes.
|
||||
* Replicators to receive fixed, arbitrary reward for submitting validated PoReps. Reward size mechanism (i.e. PoRep reward as a function of total ledger redundancy) to come later.
|
||||
* Pooling of replicator PoRep transaction fees and weighted distribution to validators based on PoRep verification (see [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md). It will be useful to test this protection against attacks on testnet.
|
||||
* Nice-to-have: auto-delegation of replicator rewards to validator.
|
@ -8,7 +8,7 @@ These protocol-based rewards, to be distributed to participating validation and
|
||||
|
||||
Transaction fees are market-based participant-to-participant transfers, attached to network interactions as a necessary motivation and compensation for the inclusion and execution of a proposed transaction (be it a state execution or proof-of-replication verification). A mechanism for continuous and long-term funding of the mining pool through a pre-dedicated portion of transaction fees is also discussed below.
|
||||
|
||||
A high-level schematic of Solana’s crypto-economic design is shown below in **Figure 1**. The specifics of validation-client economics are described in sections: [Validation-client Economics](ed_validation_client_economics.md), [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md), [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md) and [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md). Also, the chapter titled [Validation Stake Delegation](ed_vce_validation_stake_delegation.md) closes with a discussion of validator delegation opportunties and marketplace. The [Replication-client Economics](ed_replication_client_economics.md) chapter will review the Solana network design for global ledger storage/redundancy and replicator-client economics ([Storage-replication rewards](ed_rce_storage_replication_rewards.md)) along with a replicator-to-validator delegation mechanism designed to aide participant on-boarding into the Solana economy discussed in [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md). The [Economic Sustainability](ed_economic_sustainability.md) section dives deeper into Solana’s design for long-term economic sustainability and outlines the constraints and conditions for a self-sustaining economy. Finally, in chapter [Attack Vectors](ed_attack_vectors.md), various attack vectors will be described and potential vulnerabilities explored and parameterized.
|
||||
A high-level schematic of Solana’s crypto-economic design is shown below in **Figure 1**. The specifics of validation-client economics are described in sections: [Validation-client Economics](ed_validation_client_economics.md), [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md), [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md) and [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md). Also, the chapter titled [Validation Stake Delegation](ed_vce_validation_stake_delegation.md) closes with a discussion of validator delegation opportunties and marketplace. The [Replication-client Economics](ed_replication_client_economics.md) chapter will review the Solana network design for global ledger storage/redundancy and replicator-client economics ([Storage-replication rewards](ed_rce_storage_replication_rewards.md)) along with a replicator-to-validator delegation mechanism designed to aide participant on-boarding into the Solana economy discussed in [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md). The [Economic Sustainability](ed_economic_sustainability.md) section dives deeper into Solana’s design for long-term economic sustainability and outlines the constraints and conditions for a self-sustaining economy. An outline of features for an MVP economic design is discussed in the [Economic Design MVP](ed_mvp.md) section. Finally, in chapter [Attack Vectors](ed_attack_vectors.md), various attack vectors will be described and potential vulnerabilities explored and parameterized.
|
||||
|
||||
<!--  -->
|
||||
<p style="text-align:center;"><img src="img/solana_economic_design.png" alt="== Solana Economic Design Diagram ==" width="800"/></p>
|
||||
|
@ -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`.
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/client.sh --network $(dig +short testnet.solana.com):8001 --duration 60
|
||||
$ ./multinode-demo/client.sh --network 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)
|
||||
|
3
book/src/implemented-proposals.md
Normal file
3
book/src/implemented-proposals.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Implemented Design Proposals
|
||||
|
||||
The following design proposals are fully implemented.
|
213
book/src/installer.md
Normal file
213
book/src/installer.md
Normal file
@ -0,0 +1,213 @@
|
||||
## Cluster Software Installation and Updates
|
||||
Currently users are required to build the solana cluster software themselves
|
||||
from the git repository and manually update it, which is error prone and
|
||||
inconvenient.
|
||||
|
||||
This document proposes an easy to use software install and updater that can be
|
||||
used to deploy pre-built binaries for supported platforms. Users may elect to
|
||||
use binaries supplied by Solana or any other party they trust. Deployment of
|
||||
updates is managed using an on-chain update manifest program.
|
||||
|
||||
### Motivating Examples
|
||||
#### Fetch and run a pre-built installer using a bootstrap curl/shell script
|
||||
The easiest install method for supported platforms:
|
||||
```bash
|
||||
$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.13.0/install/solana-install-init.sh | sh
|
||||
```
|
||||
|
||||
This script will check github for the latest tagged release and download and run the
|
||||
`solana-install` binary from there.
|
||||
|
||||
|
||||
If additional arguments need to be specified during the installation, the
|
||||
following shell syntax is used:
|
||||
```bash
|
||||
$ init_args=.... # arguments for `solana-installer init ...`
|
||||
$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.13.0/install/solana-install-init.sh | sh -s - ${init_args}
|
||||
```
|
||||
|
||||
#### Fetch and run a pre-built installer from a Github release
|
||||
With a well-known release URL, a pre-built binary can be obtained for supported
|
||||
platforms:
|
||||
|
||||
```bash
|
||||
$ curl -o solana-install https://github.com/solana-labs/solana/releases/download/v0.13.0/solana-install-x86_64-apple-darwin
|
||||
$ chmod +x ./solana-install
|
||||
$ ./solana-install --help
|
||||
```
|
||||
|
||||
#### Build and run the installer from source
|
||||
If a pre-built binary is not available for a given platform, building the
|
||||
installer from source is always an option:
|
||||
```bash
|
||||
$ git clone https://github.com/solana-labs/solana.git
|
||||
$ cd solana/install
|
||||
$ cargo run -- --help
|
||||
```
|
||||
|
||||
#### Deploy a new update to a cluster
|
||||
Given a solana release tarball (as created by `ci/publish-tarball.sh`) that has already been uploaded to a publicly accessible URL,
|
||||
the following commands will deploy the update:
|
||||
```bash
|
||||
$ solana-keygen -o update-manifest.json # <-- only generated once, the public key is shared with users
|
||||
$ solana-install deploy http://example.com/path/to/solana-release.tar.bz2 update-manifest.json
|
||||
```
|
||||
|
||||
#### Run a validator node that auto updates itself
|
||||
```bash
|
||||
$ solana-install init --pubkey 92DMonmBYXwEMHJ99c9ceRSpAmk9v6i3RdvDdXaVcrfj # <-- pubkey is obtained from whoever is deploying the updates
|
||||
$ export PATH=~/.local/share/solana-install/bin:$PATH
|
||||
$ solana-keygen ... # <-- runs the latest solana-keygen
|
||||
$ solana-install run solana-fullnode ... # <-- runs a fullnode, restarting it as necesary when an update is applied
|
||||
```
|
||||
|
||||
### On-chain Update Manifest
|
||||
An update manifest is used to advertise the deployment of new release tarballs
|
||||
on a solana cluster. The update manifest is stored using the `config` program,
|
||||
and each update manifest account describes a logical update channel for a given
|
||||
target triple (eg, `x86_64-apple-darwin`). The account public key is well-known
|
||||
between the entity deploying new updates and users consuming those updates.
|
||||
|
||||
The update tarball itself is hosted elsewhere, off-chain and can be fetched from
|
||||
the specified `download_url`.
|
||||
|
||||
```rust,ignore
|
||||
use solana_sdk::signature::Signature;
|
||||
|
||||
/// Information required to download and apply a given update
|
||||
pub struct UpdateManifest {
|
||||
pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH
|
||||
pub download_url: String, // Download URL to the release tar.bz2
|
||||
pub download_sha256: String, // SHA256 digest of the release tar.bz2 file
|
||||
}
|
||||
|
||||
/// Userdata of an Update Manifest program Account.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
|
||||
pub struct SignedUpdateManifest {
|
||||
pub manifest: UpdateManifest,
|
||||
pub manifest_signature: Signature,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Note that the `manifest` field itself contains a corresponding signature
|
||||
(`manifest_signature`) to guard against man-in-the-middle attacks between the
|
||||
`solana-install` tool and the solana cluster RPC API.
|
||||
|
||||
To guard against rollback attacks, `solana-install` will refuse to install an
|
||||
update with an older `timestamp_secs` than what is currently installed.
|
||||
|
||||
### Release Archive Contents
|
||||
A release archive is expected to be a tar file compressed with
|
||||
bzip2 with the following internal structure:
|
||||
|
||||
* `/version.yml` - a simple YAML file containing the field `"target"` - the
|
||||
target tuple. Any additional fields are ignored.
|
||||
* `/bin/` -- directory containing available programs in the release.
|
||||
`solana-install` will symlink this directory to
|
||||
`~/.local/share/solana-install/bin` for use by the `PATH` environment
|
||||
variable.
|
||||
* `...` -- any additional files and directories are permitted
|
||||
|
||||
### solana-install Tool
|
||||
The `solana-install` tool is used by the user to install and update their cluster software.
|
||||
|
||||
It manages the following files and directories in the user's home directory:
|
||||
* `~/.config/solana/install/config.yml` - user configuration and information about currently installed software version
|
||||
* `~/.local/share/solana/install/bin` - a symlink to the current release. eg, `~/.local/share/solana-update/<update-pubkey>-<manifest_signature>/bin`
|
||||
* `~/.local/share/solana/install/releases/<download_sha256>/` - contents of a release
|
||||
|
||||
#### Command-line Interface
|
||||
```manpage
|
||||
solana-install 0.13.0
|
||||
The solana cluster software installer
|
||||
|
||||
USAGE:
|
||||
solana-install [OPTIONS] <SUBCOMMAND>
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-c, --config <PATH> Configuration file to use [default: /Users/mvines/Library/Preferences/solana/install.yml]
|
||||
|
||||
SUBCOMMANDS:
|
||||
deploy deploys a new update
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
info displays information about the current installation
|
||||
init initializes a new installation
|
||||
run Runs a program while periodically checking and applying software updates
|
||||
update checks for an update, and if available downloads and applies it
|
||||
```
|
||||
|
||||
```manpage
|
||||
solana-install-init
|
||||
initializes a new installation
|
||||
|
||||
USAGE:
|
||||
solana-install init [OPTIONS]
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
|
||||
OPTIONS:
|
||||
-d, --data_dir <PATH> Directory to store install data [default: /Users/mvines/Library/Application Support/solana]
|
||||
-u, --url <URL> JSON RPC URL for the solana cluster [default: https://api.testnet.solana.com/]
|
||||
-p, --pubkey <PUBKEY> Public key of the update manifest [default: 9XX329sPuskWhH4DQh6k16c87dHKhXLBZTL3Gxmve8Gp]
|
||||
```
|
||||
|
||||
```manpage
|
||||
solana-install-info
|
||||
displays information about the current installation
|
||||
|
||||
USAGE:
|
||||
solana-install info [FLAGS]
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-l, --local only display local information, don't check the cluster for new updates
|
||||
```
|
||||
|
||||
```manpage
|
||||
solana-install-deploy
|
||||
deploys a new update
|
||||
|
||||
USAGE:
|
||||
solana-install deploy <download_url> <update_manifest_keypair>
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
|
||||
ARGS:
|
||||
<download_url> URL to the solana release archive
|
||||
<update_manifest_keypair> Keypair file for the update manifest (/path/to/keypair.json)
|
||||
```
|
||||
|
||||
```manpage
|
||||
solana-install-update
|
||||
checks for an update, and if available downloads and applies it
|
||||
|
||||
USAGE:
|
||||
solana-install update
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
```
|
||||
|
||||
```manpage
|
||||
solana-install-run
|
||||
Runs a program while periodically checking and applying software updates
|
||||
|
||||
USAGE:
|
||||
solana-install run <program_name> [program_arguments]...
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
|
||||
ARGS:
|
||||
<program_name> program to run
|
||||
<program_arguments>... arguments to supply to the program
|
||||
|
||||
The program will be restarted upon a successful software update
|
||||
```
|
@ -24,8 +24,11 @@ Methods
|
||||
* [confirmTransaction](#confirmtransaction)
|
||||
* [getAccountInfo](#getaccountinfo)
|
||||
* [getBalance](#getbalance)
|
||||
* [getClusterNodes](#getclusternodes)
|
||||
* [getRecentBlockhash](#getrecentblockhash)
|
||||
* [getSignatureStatus](#getsignaturestatus)
|
||||
* [getSlotLeader](#getslotleader)
|
||||
* [getNumBlocksSinceSignatureConfirmation](#getnumblockssincesignatureconfirmation)
|
||||
* [getTransactionCount](#gettransactioncount)
|
||||
* [requestAirdrop](#requestairdrop)
|
||||
* [sendTransaction](#sendtransaction)
|
||||
@ -113,6 +116,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
|
||||
Returns all information associated with the account of provided Pubkey
|
||||
|
||||
@ -124,7 +151,7 @@ The result field will be a JSON object with the following sub fields:
|
||||
|
||||
* `lamports`, number of lamports assigned to this account, as a signed 64-bit integer
|
||||
* `owner`, array of 32 bytes representing the program this account has been assigned to
|
||||
* `userdata`, array of bytes representing any userdata associated with the account
|
||||
* `data`, array of bytes representing any data associated with the account
|
||||
* `executable`, boolean indicating if the account contains a program (and is strictly read-only)
|
||||
* `loader`, array of 32 bytes representing the loader for this program (if `executable`), otherwise all
|
||||
|
||||
@ -134,7 +161,7 @@ The result field will be a JSON object with the following sub fields:
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getAccountInfo", "params":["2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpdST"]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":{"executable":false,"loader":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"owner":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"lamports":1,"userdata":[3,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,53,48,45,48,49,45,48,49,84,48,48,58,48,48,58,48,48,90,252,10,7,28,246,140,88,177,98,82,10,227,89,81,18,30,194,101,199,16,11,73,133,20,246,62,114,39,20,113,189,32,50,0,0,0,0,0,0,0,247,15,36,102,167,83,225,42,133,127,82,34,36,224,207,130,109,230,224,188,163,33,213,13,5,117,211,251,65,159,197,51,0,0,0,0,0,0]},"id":1}
|
||||
{"jsonrpc":"2.0","result":{"executable":false,"loader":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"owner":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"lamports":1,"data":[3,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,53,48,45,48,49,45,48,49,84,48,48,58,48,48,58,48,48,90,252,10,7,28,246,140,88,177,98,82,10,227,89,81,18,30,194,101,199,16,11,73,133,20,246,62,114,39,20,113,189,32,50,0,0,0,0,0,0,0,247,15,36,102,167,83,225,42,133,127,82,34,36,224,207,130,109,230,224,188,163,33,213,13,5,117,211,251,65,159,197,51,0,0,0,0,0,0]},"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
@ -168,12 +195,10 @@ events.
|
||||
* `string` - Signature of Transaction to confirm, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `string` - Transaction status:
|
||||
* `Confirmed` - Transaction was successful
|
||||
* `SignatureNotFound` - Unknown transaction
|
||||
* `ProgramRuntimeError` - An error occurred in the program that processed this Transaction
|
||||
* `AccountInUse` - Another Transaction had a write lock one of the Accounts specified in this Transaction. The Transaction may succeed if retried
|
||||
* `GenericFailure` - Some other error occurred. **Note**: In the future new Transaction statuses may be added to this list. It's safe to assume that all new statuses will be more specific error conditions that previously presented as `GenericFailure`
|
||||
* `null` - Unknown transaction
|
||||
* `object` - Transaction status:
|
||||
* `"Ok": null` - Transaction was successful
|
||||
* `"Err": <ERR>` - Transaction failed with TransactionError <ERR> [TransactionError definitions](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction.rs#L14)
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
@ -184,7 +209,48 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "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
|
||||
Returns the current number of blocks since signature has been confirmed.
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Signature of Transaction to confirm, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `integer` - count, as unsigned 64-bit integer
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getNumBlocksSinceSignatureConfirmation", "params":["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW"]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":8,"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### getTransactionCount
|
||||
Returns the current Transaction count from the ledger
|
||||
|
||||
@ -254,7 +320,7 @@ After connect to the RPC PubSub websocket at `ws://<ADDRESS>/`:
|
||||
---
|
||||
|
||||
### accountSubscribe
|
||||
Subscribe to an account to receive notifications when the lamports or userdata
|
||||
Subscribe to an account to receive notifications when the lamports or data
|
||||
for a given account public key changes
|
||||
|
||||
##### Parameters:
|
||||
@ -274,7 +340,7 @@ for a given account public key changes
|
||||
|
||||
##### Notification Format:
|
||||
```bash
|
||||
{"jsonrpc": "2.0","method": "accountNotification", "params": {"result": {"executable":false,"loader":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"owner":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"lamports":1,"userdata":[3,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,53,48,45,48,49,45,48,49,84,48,48,58,48,48,58,48,48,90,252,10,7,28,246,140,88,177,98,82,10,227,89,81,18,30,194,101,199,16,11,73,133,20,246,62,114,39,20,113,189,32,50,0,0,0,0,0,0,0,247,15,36,102,167,83,225,42,133,127,82,34,36,224,207,130,109,230,224,188,163,33,213,13,5,117,211,251,65,159,197,51,0,0,0,0,0,0]},"subscription":0}}
|
||||
{"jsonrpc": "2.0","method": "accountNotification", "params": {"result": {"executable":false,"loader":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"owner":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"lamports":1,"data":[3,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,53,48,45,48,49,45,48,49,84,48,48,58,48,48,58,48,48,90,252,10,7,28,246,140,88,177,98,82,10,227,89,81,18,30,194,101,199,16,11,73,133,20,246,62,114,39,20,113,189,32,50,0,0,0,0,0,0,0,247,15,36,102,167,83,225,42,133,127,82,34,36,224,207,130,109,230,224,188,163,33,213,13,5,117,211,251,65,159,197,51,0,0,0,0,0,0]},"subscription":0}}
|
||||
```
|
||||
|
||||
---
|
||||
@ -300,7 +366,7 @@ Unsubscribe from account change notifications
|
||||
---
|
||||
|
||||
### programSubscribe
|
||||
Subscribe to a program to receive notifications when the lamports or userdata
|
||||
Subscribe to a program to receive notifications when the lamports or data
|
||||
for a given account owned by the program changes
|
||||
|
||||
##### Parameters:
|
||||
@ -322,7 +388,7 @@ for a given account owned by the program changes
|
||||
* `string` - account Pubkey, as base-58 encoded string
|
||||
* `object` - account info JSON object (see [getAccountInfo](#getaccountinfo) for field details)
|
||||
```bash
|
||||
{"jsonrpc":"2.0","method":"programNotification","params":{{"result":["8Rshv2oMkPu5E4opXTRyuyBeZBqQ4S477VG26wUTFxUM",{"executable":false,"lamports":1,"owner":[129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"userdata":[1,1,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,49,56,45,49,50,45,50,52,84,50,51,58,53,57,58,48,48,90,235,233,39,152,15,44,117,176,41,89,100,86,45,61,2,44,251,46,212,37,35,118,163,189,247,84,27,235,178,62,55,89,0,0,0,0,50,0,0,0,0,0,0,0,235,233,39,152,15,44,117,176,41,89,100,86,45,61,2,44,251,46,212,37,35,118,163,189,247,84,27,235,178,62,45,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}],"subscription":0}}
|
||||
{"jsonrpc":"2.0","method":"programNotification","params":{{"result":["8Rshv2oMkPu5E4opXTRyuyBeZBqQ4S477VG26wUTFxUM",{"executable":false,"lamports":1,"owner":[129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"data":[1,1,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,49,56,45,49,50,45,50,52,84,50,51,58,53,57,58,48,48,90,235,233,39,152,15,44,117,176,41,89,100,86,45,61,2,44,251,46,212,37,35,118,163,189,247,84,27,235,178,62,55,89,0,0,0,0,50,0,0,0,0,0,0,0,235,233,39,152,15,44,117,176,41,89,100,86,45,61,2,44,251,46,212,37,35,118,163,189,247,84,27,235,178,62,45,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}],"subscription":0}}
|
||||
```
|
||||
|
||||
---
|
||||
|
212
book/src/passive-stake-delegation-and-rewards.md
Normal file
212
book/src/passive-stake-delegation-and-rewards.md
Normal file
@ -0,0 +1,212 @@
|
||||
# Stake Delegation and Reward
|
||||
|
||||
This design proposal focuses on the software architecture for the on-chain
|
||||
voting and staking programs. Incentives for staking is covered in [staking
|
||||
rewards](staking-rewards.md).
|
||||
|
||||
The current architecture requires a vote for each delegated stake from the
|
||||
validator, and therefore does not scale to allow replicator clients to
|
||||
automatically delegate their rewards.
|
||||
|
||||
The design proposes a new set of programs for voting and stake delegation, The
|
||||
proposed programs allow many stake accounts to passively earn rewards with a
|
||||
single validator vote without permission or active involvement from the
|
||||
validator.
|
||||
|
||||
## Current Design Problems
|
||||
|
||||
In the current design each staker creates their own VoteState, and assigns a
|
||||
**delegate** in the VoteState that can submit votes. Since the validator has to
|
||||
actively vote for each stake delegated to it, validators can censor stakes by
|
||||
not voting for them.
|
||||
|
||||
The number of votes is equal to the number of stakers, and not the number of
|
||||
validators. Replicator clients are expected to delegate their replication
|
||||
rewards as they are earned, and therefore the number of stakes is expected to be
|
||||
large compared to the number of validators in a long running cluster.
|
||||
|
||||
## Proposed changes to the current design.
|
||||
|
||||
The general idea is that instead of the staker, the validator will own the
|
||||
VoteState program. In this proposal the VoteState program is there to track
|
||||
validator votes, count validator generated credits and to provide any
|
||||
additional validator specific state. The VoteState program is not aware of any
|
||||
stakes delegated to it, and has no staking weight.
|
||||
|
||||
The rewards generated are proportional to the amount of lamports staked. In
|
||||
this proposal stake state is stored as part of the StakeState program. This
|
||||
program is owned by the staker only. Lamports stored in this program are the
|
||||
stake. Unlike the current design, this program contains a new field to indicate
|
||||
which VoteState program the stake is delegated to.
|
||||
|
||||
### VoteState
|
||||
|
||||
VoteState is the current state of all the votes the **delegate** has submitted
|
||||
to the bank. VoteState contains the following state information:
|
||||
|
||||
* votes - The submitted votes data structure.
|
||||
|
||||
* credits - The total number of rewards this vote program has generated over its
|
||||
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 StakeState 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, and
|
||||
this field can only modified by this entity
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
### 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_id` - 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 a
|
||||
StakeState::MiningPool 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_id` is initialized to `account[1]`
|
||||
|
||||
* `account[1]` - R - The VoteState instance.
|
||||
|
||||
### StakeInstruction::RedeemVoteCredits
|
||||
|
||||
The VoteState program and the StakeState programs maintain a lifetime counter
|
||||
of total rewards generated and claimed. Therefore an explicit `Clear`
|
||||
instruction is not necessary. When claiming rewards, the total lamports
|
||||
deposited into the StakeState 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_id`
|
||||
|
||||
Reward is payed 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 `VoteState` token
|
||||
balance, and the reward is deposited to the `StakeState::Delegate` token balance. The
|
||||
reward and the commission is weighted by the `StakeState::lamports` divided by total lamports staked.
|
||||
|
||||
The Staker or the owner of the Stake program sends a transaction with this
|
||||
instruction to claim the reward.
|
||||
|
||||
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.
|
||||
|
||||
### Benefits
|
||||
|
||||
* Single vote for all the stakers.
|
||||
|
||||
* Clearing of the credit variable is not necessary for claiming rewards.
|
||||
|
||||
* 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.
|
||||
|
||||
## Passive Delegation
|
||||
|
||||
Any number of instances of StakeState::Delegate programs can delegate to a single
|
||||
VoteState program without an interactive action from the identity controlling
|
||||
the VoteState program or submitting votes to the program.
|
||||
|
||||
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
|
||||
`StakeState::Delegate::voter_id`.
|
||||
|
||||
## Example Callflow
|
||||
|
||||
<img alt="Passive Staking Callflow" src="img/passive-staking-callflow.svg" class="center"/>
|
||||
|
||||
## Future work
|
||||
|
||||
Validators may want to split the stake delegated to them amongst many validator
|
||||
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
|
||||
validators instead of a single one.
|
||||
|
||||
Instead of a single `vote_id` and `credits_observed` entry in the StakeState
|
||||
program, the program can be initialized with a vector of tuples.
|
||||
|
||||
```rust,ignore
|
||||
Voter {
|
||||
voter_id: Pubkey,
|
||||
credits_observed: u64,
|
||||
weight: u8,
|
||||
}
|
||||
```
|
||||
|
||||
* voters: Vec<Voter> - Array of VoteState accounts that are voting rewards with
|
||||
this stake.
|
||||
|
||||
A StakeState program would claim a fraction of the reward from each voter in
|
||||
the `voters` array, and each voter would be delegated a fraction of the stake.
|
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 processed.
|
||||
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*
|
||||
with one or more *instructions*. The Solana *runtime* passes those instructions
|
||||
to user-contributed *programs*. An instruction might, for example, tell a
|
||||
program to move *lamports* from one *account* to another or create an interactive
|
||||
contract that governs how lamports are moved. Instructions are executed
|
||||
program to transfer *lamports* from one *account* to another or create an interactive
|
||||
contract that governs how lamports are transfered. Instructions are executed
|
||||
atomically. If any instruction is invalid, any changes made within the
|
||||
transaction are discarded.
|
||||
|
||||
|
@ -40,7 +40,7 @@ retransmitted twice around the network.
|
||||
|
||||
4. CrdsValue for vote should look like this ``` Votes(Vec<Transaction>) ```
|
||||
|
||||
Each vote transaction should maintain a `wallclock` in its userdata. The merge
|
||||
Each vote transaction should maintain a `wallclock` in its data. The merge
|
||||
strategy for Votes will keep the last N set of votes as configured by the local
|
||||
client. For push/pull the vector is traversed recursively and each Transaction
|
||||
is treated as an individual CrdsValue with its own local wallclock and
|
||||
|
@ -6,7 +6,7 @@ separating program code from the state it operates on, the runtime is able to
|
||||
choreograph concurrent access. Transactions accessing only credit-only
|
||||
accounts are executed in parallel whereas transactions accessing writable
|
||||
accounts are serialized. The runtime interacts with the program through an
|
||||
entrypoint with a well-defined interface. The userdata stored in an account is
|
||||
entrypoint with a well-defined interface. The data stored in an account is
|
||||
an opaque type, an array of bytes. The program has full control over its
|
||||
contents.
|
||||
|
||||
@ -42,7 +42,7 @@ programs can be executed in parallel.
|
||||
The runtime enforces the following rules:
|
||||
|
||||
1. Only the *owner* program may modify the contents of an account. This means
|
||||
that upon assignment userdata vector is guaranteed to be zero.
|
||||
that upon assignment data vector is guaranteed to be zero.
|
||||
|
||||
2. Total balances on all the accounts is equal before and after execution of a
|
||||
transaction.
|
||||
@ -59,24 +59,24 @@ accounts.
|
||||
|
||||
## SystemProgram Interface
|
||||
|
||||
The interface is best described by the `Instruction::userdata` that the user
|
||||
The interface is best described by the `Instruction::data` that the user
|
||||
encodes.
|
||||
|
||||
* `CreateAccount` - This allows the user to create an account with an allocated
|
||||
userdata array and assign it to a Program.
|
||||
data array and assign it 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
|
||||
|
||||
For blockchain to function correctly, the program code must be resilient to user
|
||||
inputs. That is why in this design the program specific code is the only code
|
||||
that can change the state of the userdata byte array in the Accounts that are
|
||||
that can change the state of the data byte array in the Accounts that are
|
||||
assigned to it. It is also the reason why `Assign` or `CreateAccount` must zero
|
||||
out the userdata. Otherwise there would be no possible way for the program to
|
||||
distinguish the recently assigned account userdata from a natively generated
|
||||
out the data. Otherwise there would be no possible way for the program to
|
||||
distinguish the recently assigned account data from a natively generated
|
||||
state transition without some additional metadata from the runtime to indicate
|
||||
that this memory is assigned instead of natively generated.
|
||||
|
||||
@ -94,12 +94,12 @@ instruction can be composed into a single transaction with the call to the
|
||||
program itself.
|
||||
|
||||
* `CreateAccount` and `Assign` guarantee that when account is assigned to the
|
||||
program, the Account's userdata is zero initialized.
|
||||
program, the Account's data is zero initialized.
|
||||
|
||||
* Once assigned to program an Account cannot be reassigned.
|
||||
|
||||
* Runtime guarantees that a program's code is the only code that can modify
|
||||
Account userdata that the Account is assigned to.
|
||||
Account data that the Account is assigned to.
|
||||
|
||||
* Runtime guarantees that the program can only spend lamports that are in
|
||||
accounts that are assigned to it.
|
||||
|
@ -25,7 +25,7 @@ When Futures 0.3.0 is released, the Transact trait may look like this:
|
||||
|
||||
```rust,ignore
|
||||
trait Transact {
|
||||
async fn send_transactions(txs: &[Transaction]) -> Vec<Result<(), BankError>>;
|
||||
async fn send_transactions(txs: &[Transaction]) -> Vec<Result<(), TransactionError>>;
|
||||
}
|
||||
```
|
||||
|
||||
|
174
book/src/testnet-participation.md
Normal file
174
book/src/testnet-participation.md
Normal file
@ -0,0 +1,174 @@
|
||||
## Testnet Participation
|
||||
This document describes how to participate in the testnet as a
|
||||
validator node.
|
||||
|
||||
Please note some of the information and instructions described here may change
|
||||
in future releases.
|
||||
|
||||
### Beta Testnet Overview
|
||||
The testnet features a validator running at testnet.solana.com, which
|
||||
serves as the entrypoint to the cluster for your validator.
|
||||
|
||||
Additionally there is a blockexplorer available at
|
||||
[http://testnet.solana.com/](http://testnet.solana.com/).
|
||||
|
||||
The testnet is configured to reset the ledger daily, or sooner
|
||||
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.
|
||||
|
||||
### Machine Requirements
|
||||
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.
|
||||
|
||||
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
|
||||
8000 through 10000 are not blocked for Internet inbound and outbound traffic.**
|
||||
|
||||
Prebuilt binaries are available for Linux x86_64 (Ubuntu 18.04 recommended).
|
||||
MacOS or WSL users may build from source.
|
||||
|
||||
#### 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
|
||||
#### Obtaining The Software
|
||||
##### Bootstrap with `solana-install`
|
||||
|
||||
The `solana-install` tool can be used to easily install and upgrade the cluster
|
||||
software on Linux x86_64 systems.
|
||||
|
||||
```bash
|
||||
$ export SOLANA_RELEASE=v0.14.0 # skip this line to install the latest release
|
||||
$ 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
|
||||
following command to obtain the same result:
|
||||
```bash
|
||||
$ solana-install init
|
||||
```
|
||||
|
||||
After a successful install, `solana-install update` may be used to easily update the cluster
|
||||
software to a newer version.
|
||||
|
||||
##### Download Prebuilt Binaries
|
||||
Binaries are available for Linux x86_64 systems.
|
||||
|
||||
Download the binaries by navigating to
|
||||
[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
|
||||
$ tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2
|
||||
$ cd solana-release/
|
||||
$ export PATH=$PWD/bin:$PATH
|
||||
```
|
||||
##### Build From Source
|
||||
If you are unable to use the prebuilt binaries or prefer to build it yourself
|
||||
from source, navigate to
|
||||
[https://github.com/solana-labs/solana/releases/latest](https://github.com/solana-labs/solana/releases/latest),
|
||||
and download the **Source Code** archive. Extract the code and build the
|
||||
binaries with:
|
||||
```bash
|
||||
$ ./scripts/cargo-install-all.sh .
|
||||
$ export PATH=$PWD/bin:$PATH
|
||||
```
|
||||
|
||||
### Starting The Validator
|
||||
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 -n testnet.solana.com airdrop 123
|
||||
$ solana-wallet -n testnet.solana.com balance
|
||||
```
|
||||
|
||||
Also try running following command to join the gossip network and view all the other nodes in the cluster:
|
||||
```bash
|
||||
$ solana-gossip --network testnet.solana.com:8001 spy
|
||||
# Press ^C to exit
|
||||
```
|
||||
|
||||
Then the following command will start a new validator node.
|
||||
|
||||
If this is a `solana-install`-installation:
|
||||
```bash
|
||||
$ clear-fullnode-config.sh
|
||||
$ fullnode.sh --public-address --poll-for-new-genesis-block testnet.solana.com
|
||||
```
|
||||
|
||||
Alternatively, the `solana-install run` command can be used to run the validator
|
||||
node while periodically checking for and applying software updates:
|
||||
```bash
|
||||
$ clear-fullnode-config.sh
|
||||
$ solana-install run fullnode.sh -- --public-address --poll-for-new-genesis-block testnet.solana.com
|
||||
```
|
||||
|
||||
If you built from source:
|
||||
```bash
|
||||
$ USE_INSTALL=1 ./multinode-demo/clear-fullnode-config.sh
|
||||
$ USE_INSTALL=1 ./multinode-demo/fullnode.sh --public-address --poll-for-new-genesis-block testnet.solana.com
|
||||
```
|
||||
|
||||
#### Controlling local network port allocation
|
||||
By default the validator will dynamically select available network ports in the
|
||||
8000-10000 range, and may be overridden with `--dynamic-port-range`. For
|
||||
example, `fullnode.sh --dynamic-port-range 11000-11010 ...` will restrict the
|
||||
validator to ports 11000-11011.
|
||||
|
||||
### Validator Monitoring
|
||||
From another console, confirm the IP address of your validator is visible in the
|
||||
gossip network by running:
|
||||
```bash
|
||||
$ solana-gossip --network edge.testnet.solana.com:8001 spy
|
||||
```
|
||||
|
||||
When `fullnode.sh` starts, it will output a fullnode configuration that looks
|
||||
similar to:
|
||||
```bash
|
||||
======================[ Fullnode configuration ]======================
|
||||
node id: 4ceWXsL3UJvn7NYZiRkw7NsryMpviaKBDYr8GK7J61Dm
|
||||
vote id: 2ozWvfaXQd1X6uKh8jERoRGApDqSqcEy6fF1oN13LL2G
|
||||
ledger: ...
|
||||
accounts: ...
|
||||
======================================================================
|
||||
```
|
||||
|
||||
Provide the **vote id** 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 id 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/fullnode-vote-id.json
|
||||
# Otherwise run:
|
||||
$ solana-keygen pubkey ./config-local/fullnode-vote-id.json
|
||||
```
|
||||
|
||||
### Sharing Metrics From Your Validator
|
||||
If you'd like to share metrics perform the following steps before starting the
|
||||
validator node:
|
||||
```bash
|
||||
export u="username obtained from the Solana maintainers"
|
||||
export p="password obtained from the Solana maintainers"
|
||||
export SOLANA_METRICS_CONFIG="db=testnet,u=${u:?},p=${p:?}"
|
||||
source scripts/configure-metrics.sh
|
||||
```
|
59
book/src/transaction-fees.md
Normal file
59
book/src/transaction-fees.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Deterministic Transaction Fees
|
||||
|
||||
Transactions currently include a fee field that indicates the maximum fee field
|
||||
a slot leader is permitted to charge to process a transaction. The cluster, on
|
||||
the other hand, agrees on a minimum fee. If the network is congested, the slot
|
||||
leader may prioritize the transactions offering higher fees. That means the
|
||||
client won't know how much was collected until the transaction is confirmed by
|
||||
the cluster and the remaining balance is checked. It smells of exactly what we
|
||||
dislike about Ethereum's "gas", non-determinism.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
This design is not yet implemented, but is written as though it has been. Once
|
||||
implemented, delete this comment.
|
||||
|
||||
### Congestion-driven fees
|
||||
|
||||
Each validator uses *signatures per slot* (SPS) to estimate network congestion
|
||||
and *SPS target* to estimate the desired processing capacity of the cluster.
|
||||
The validator learns the SPS target from the genesis block, whereas it
|
||||
calculates SPS from the ledger data in the previous epoch.
|
||||
|
||||
### Calculating fees
|
||||
|
||||
The client uses the JSON RPC API to query the cluster for the current fee
|
||||
parameters. Those parameters are tagged with a blockhash and remain valid
|
||||
until that blockhash is old enough to be rejected by the slot leader.
|
||||
|
||||
Before sending a transaction to the cluster, a client may submit the
|
||||
transaction and fee account data to an SDK module called the *fee calculator*.
|
||||
So long as the client's SDK version matches the slot leader's version, the
|
||||
client is assured that its account will be changed exactly the same number of
|
||||
lamports as returned by the fee calculator.
|
||||
|
||||
### Fee Parameters
|
||||
|
||||
In the first implementation of this design, the only fee parameter is
|
||||
`lamports_per_signature`. The more signatures the cluster needs to verify, the
|
||||
higher the fee. The exact number of lamports is determined by the ratio of SPS
|
||||
to the SPS target. The cluster lowers `lamports_per_signature` when SPS is
|
||||
below the target and raises it when at or above the target.
|
||||
|
||||
Future parameters might include:
|
||||
|
||||
* `lamports_per_pubkey` - cost to load an account
|
||||
* `lamports_per_slot_distance` - higher cost to load very old accounts
|
||||
* `lamports_per_byte` - cost per size of account loaded
|
||||
* `lamports_per_bpf_instruction` - cost to run a program
|
||||
|
||||
### Attacks
|
||||
|
||||
#### Hijacking the SPS Target
|
||||
|
||||
A group of validators can centralize the cluster if they can convince it to
|
||||
raise the SPS Target above a point where the rest of the validators can keep
|
||||
up. Raising the target will cause fees to drop, presumably creating more demand
|
||||
and therefore higher TPS. If the validator doesn't have hardware that can
|
||||
process that many transactions that fast, its confirmation votes will
|
||||
eventually get so long that the cluster will be forced to boot it.
|
@ -41,7 +41,7 @@ $ solana-wallet balance
|
||||
$ solana-wallet confirm <TX_SIGNATURE>
|
||||
|
||||
// Return
|
||||
"Confirmed" / "Not found"
|
||||
"Confirmed" / "Not found" / "Transaction failed with error <ERR>"
|
||||
```
|
||||
|
||||
#### Deploy program
|
||||
@ -352,4 +352,3 @@ ARGS:
|
||||
<PUBKEY> The pubkey of recipient
|
||||
<PROCESS_ID> The process id of the transfer to unlock
|
||||
```
|
||||
|
||||
|
@ -2,22 +2,24 @@ steps:
|
||||
- command: "ci/shellcheck.sh"
|
||||
name: "shellcheck"
|
||||
timeout_in_minutes: 5
|
||||
- command: "ci/docker-run.sh solanalabs/rust:1.32.0 ci/test-checks.sh"
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-checks.sh"
|
||||
name: "checks"
|
||||
timeout_in_minutes: 15
|
||||
- wait
|
||||
- command: "ci/test-stable-perf.sh"
|
||||
name: "stable-perf"
|
||||
timeout_in_minutes: 20
|
||||
artifact_paths: "log-*.txt"
|
||||
agents:
|
||||
- "queue=cuda"
|
||||
- command: "ci/test-bench.sh"
|
||||
name: "bench"
|
||||
timeout_in_minutes: 20
|
||||
- command: "ci/docker-run.sh solanalabs/rust:1.32.0 ci/test-stable.sh"
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-stable.sh"
|
||||
name: "stable"
|
||||
timeout_in_minutes: 20
|
||||
- command: "ci/docker-run.sh solanalabs/rust-nightly:2019-01-31 ci/test-coverage.sh"
|
||||
timeout_in_minutes: 30
|
||||
artifact_paths: "log-*.txt"
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_nightly_docker_image ci/test-coverage.sh"
|
||||
name: "coverage"
|
||||
timeout_in_minutes: 20
|
||||
# TODO: Fix and re-enable test-large-network.sh
|
||||
|
@ -1,17 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Outputs the current crate version
|
||||
# Outputs the current crate version from a given Cargo.toml
|
||||
#
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
Cargo_toml=$1
|
||||
[[ -n $Cargo_toml ]] || {
|
||||
echo "Usage: $0 path/to/Cargo.toml"
|
||||
exit 0
|
||||
}
|
||||
|
||||
[[ -r $Cargo_toml ]] || {
|
||||
echo "Error: unable to read $Cargo_toml"
|
||||
exit 1
|
||||
}
|
||||
|
||||
while read -r name equals value _; do
|
||||
if [[ $name = version && $equals = = ]]; then
|
||||
echo "${value//\"/}"
|
||||
exit 0
|
||||
fi
|
||||
done < <(cat Cargo.toml)
|
||||
done < <(cat "$Cargo_toml")
|
||||
|
||||
echo Unable to locate version in Cargo.toml 1>&2
|
||||
exit 1
|
||||
|
@ -4,11 +4,9 @@ ARG date
|
||||
RUN set -x \
|
||||
&& rustup install nightly-$date \
|
||||
&& rustup show \
|
||||
&& mv /usr/local/rustup/toolchains/nightly-$date-* \
|
||||
/usr/local/rustup/toolchains/nightly-x86_64-unknown-linux-gnu \
|
||||
&& rustup show \
|
||||
&& rustc --version \
|
||||
&& cargo --version \
|
||||
&& rustc +nightly --version \
|
||||
&& cargo +nightly --version
|
||||
&& rustc +nightly-$date --version \
|
||||
&& cargo +nightly-$date --version
|
||||
|
||||
|
@ -19,7 +19,7 @@ To update the pinned version:
|
||||
to confirm the new nightly image builds. Fix any issues as needed
|
||||
1. Run `docker login` to enable pushing images to Docker Hub, if you're authorized.
|
||||
1. Run `CI=true ci/docker-rust-nightly/build.sh YYYY-MM-DD` to push the new nightly image to dockerhub.com.
|
||||
1. Modify the `solanalabs/rust-nightly:YYYY-MM-DD` reference in `ci/buildkite.yml` from the previous to
|
||||
1. Modify the `solanalabs/rust-nightly:YYYY-MM-DD` reference in `ci/rust-version.sh` from the previous to
|
||||
new *YYYY-MM-DD* value, send a PR with this change and any codebase adjustments needed.
|
||||
|
||||
## Troubleshooting
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Note: when the rust version is changed also modify
|
||||
# ci/buildkite.yml to pick up the new image tag
|
||||
FROM rust:1.32.0
|
||||
FROM rust:1.34.0
|
||||
|
||||
RUN set -x \
|
||||
&& apt update \
|
||||
@ -17,6 +17,7 @@ RUN set -x \
|
||||
lcov \
|
||||
libclang-common-7-dev \
|
||||
llvm-7 \
|
||||
mscgen \
|
||||
rsync \
|
||||
sudo \
|
||||
\
|
||||
|
@ -8,5 +8,4 @@ docker build -t solanalabs/rust .
|
||||
read -r rustc version _ < <(docker run solanalabs/rust rustc --version)
|
||||
[[ $rustc = rustc ]]
|
||||
docker tag solanalabs/rust:latest solanalabs/rust:"$version"
|
||||
|
||||
docker push solanalabs/rust
|
||||
docker push solanalabs/rust:"$version"
|
||||
|
@ -24,9 +24,10 @@ fi
|
||||
|
||||
build() {
|
||||
$genPipeline && return
|
||||
ci/version-check-with-upgrade.sh stable
|
||||
source ci/rust-version.sh stable
|
||||
|
||||
_ scripts/ulimit-n.sh
|
||||
_ cargo build --all
|
||||
_ cargo +$rust_stable build --all
|
||||
}
|
||||
|
||||
runTest() {
|
||||
@ -51,15 +52,6 @@ runTest() {
|
||||
|
||||
build
|
||||
|
||||
runTest "Leader rotation off" \
|
||||
"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"
|
||||
|
||||
|
@ -55,7 +55,7 @@ while getopts "ch?i:k:brxR" opt; do
|
||||
restartInterval=$OPTARG
|
||||
;;
|
||||
b)
|
||||
maybeNoLeaderRotation="--no-leader-rotation"
|
||||
maybeNoLeaderRotation="--stake 0"
|
||||
;;
|
||||
x)
|
||||
extraNodes=$((extraNodes + 1))
|
||||
@ -78,7 +78,6 @@ source scripts/configure-metrics.sh
|
||||
nodes=(
|
||||
"multinode-demo/drone.sh"
|
||||
"multinode-demo/bootstrap-leader.sh \
|
||||
$maybeNoLeaderRotation \
|
||||
--enable-rpc-exit \
|
||||
--init-complete-file init-complete-node1.log"
|
||||
"multinode-demo/fullnode.sh \
|
||||
@ -91,7 +90,7 @@ nodes=(
|
||||
for i in $(seq 1 $extraNodes); do
|
||||
nodes+=(
|
||||
"multinode-demo/fullnode.sh \
|
||||
-X dyn$i \
|
||||
--label dyn$i \
|
||||
--init-complete-file init-complete-node$((2 + i)).log \
|
||||
$maybeNoLeaderRotation"
|
||||
)
|
||||
@ -307,11 +306,7 @@ while [[ $iteration -le $iterations ]]; do
|
||||
set -x
|
||||
client_id=/tmp/client-id.json-$$
|
||||
$solana_keygen -o $client_id || exit $?
|
||||
$solana_bench_tps \
|
||||
--identity $client_id \
|
||||
--num-nodes $numNodes \
|
||||
--reject-extra-nodes \
|
||||
--converge-only || exit $?
|
||||
$solana_gossip spy --num-nodes-exactly $numNodes || exit $?
|
||||
rm -rf $client_id
|
||||
) || flag_error
|
||||
|
||||
|
@ -2,15 +2,7 @@
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
eval "$(ci/channel-info.sh)"
|
||||
if [[ $BUILDKITE_BRANCH = "$STABLE_CHANNEL" ]]; then
|
||||
CHANNEL=stable
|
||||
elif [[ $BUILDKITE_BRANCH = "$EDGE_CHANNEL" ]]; then
|
||||
CHANNEL=edge
|
||||
elif [[ $BUILDKITE_BRANCH = "$BETA_CHANNEL" ]]; then
|
||||
CHANNEL=beta
|
||||
fi
|
||||
|
||||
echo --- Creating tarball
|
||||
(
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ci/semver_bash/semver.sh
|
||||
|
||||
# List of internal crates to publish
|
||||
#
|
||||
@ -11,28 +11,37 @@ cd "$(dirname "$0")/.."
|
||||
# here. (TODO: figure the crate ordering dynamically)
|
||||
#
|
||||
CRATES=(
|
||||
kvstore
|
||||
logger
|
||||
netutil
|
||||
sdk
|
||||
keygen
|
||||
metrics
|
||||
client
|
||||
drone
|
||||
programs/{budget,bpf_loader,native_loader,noop,system,vote}
|
||||
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
|
||||
vote-signer
|
||||
core
|
||||
fullnode
|
||||
genesis
|
||||
gossip
|
||||
ledger-tool
|
||||
wallet
|
||||
runtime
|
||||
install
|
||||
)
|
||||
|
||||
|
||||
# Only package/publish if this is a tagged release
|
||||
[[ -n $TRIGGERED_BUILDKITE_TAG ]] || {
|
||||
echo TRIGGERED_BUILDKITE_TAG unset, skipped
|
||||
exit 0
|
||||
}
|
||||
|
||||
semverParseInto "$TRIGGERED_BUILDKITE_TAG" MAJOR MINOR PATCH SPECIAL
|
||||
expectedCrateVersion="$MAJOR.$MINOR.$PATCH$SPECIAL"
|
||||
|
||||
[[ -n "$CRATES_IO_TOKEN" ]] || {
|
||||
echo CRATES_IO_TOKEN undefined
|
||||
exit 1
|
||||
@ -46,13 +55,17 @@ for crate in "${CRATES[@]}"; do
|
||||
exit 1
|
||||
fi
|
||||
echo "-- $crate"
|
||||
# TODO: Ensure the published version matches the contents of
|
||||
# TRIGGERED_BUILDKITE_TAG
|
||||
grep -q "^version = \"$expectedCrateVersion\"$" "$crate"/Cargo.toml || {
|
||||
echo "Error: $crate/Cargo.toml version is not $expectedCrateVersion"
|
||||
exit 1
|
||||
}
|
||||
|
||||
(
|
||||
set -x
|
||||
# TODO: the rocksdb package does not build with the stock rust docker image,
|
||||
# so use the solana rust docker image until this is resolved upstream
|
||||
ci/docker-run.sh solanalabs/rust:1.31.0 bash -exc "cd $crate; $cargoCommand"
|
||||
source ci/rust-version.sh
|
||||
ci/docker-run.sh "$rust_stable_docker_image" bash -exc "cd $crate; $cargoCommand"
|
||||
#ci/docker-run.sh rust bash -exc "cd $crate; $cargoCommand"
|
||||
)
|
||||
done
|
||||
|
@ -45,11 +45,7 @@ beta)
|
||||
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||
;;
|
||||
stable)
|
||||
if [[ -n $BETA_CHANNEL_LATEST_TAG ]]; then
|
||||
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||
else
|
||||
CHANNEL_BRANCH=$STABLE_CHANNEL
|
||||
fi
|
||||
CHANNEL_BRANCH=$STABLE_CHANNEL
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid PUBLISH_CHANNEL=$PUBLISH_CHANNEL"
|
||||
|
@ -11,10 +11,13 @@ fi
|
||||
|
||||
eval "$(ci/channel-info.sh)"
|
||||
|
||||
TAG=
|
||||
if [[ -n "$BUILDKITE_TAG" ]]; then
|
||||
CHANNEL_OR_TAG=$BUILDKITE_TAG
|
||||
TAG="$BUILDKITE_TAG"
|
||||
elif [[ -n "$TRIGGERED_BUILDKITE_TAG" ]]; then
|
||||
CHANNEL_OR_TAG=$TRIGGERED_BUILDKITE_TAG
|
||||
TAG="$TRIGGERED_BUILDKITE_TAG"
|
||||
else
|
||||
CHANNEL_OR_TAG=$CHANNEL
|
||||
fi
|
||||
@ -24,18 +27,34 @@ if [[ -z $CHANNEL_OR_TAG ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$(uname)" in
|
||||
Darwin)
|
||||
TARGET=x86_64-apple-darwin
|
||||
;;
|
||||
Linux)
|
||||
TARGET=x86_64-unknown-linux-gnu
|
||||
;;
|
||||
*)
|
||||
TARGET=unknown-unknown-unknown
|
||||
;;
|
||||
esac
|
||||
|
||||
echo --- Creating tarball
|
||||
(
|
||||
set -x
|
||||
rm -rf solana-release/
|
||||
mkdir solana-release/
|
||||
(
|
||||
echo "$CHANNEL_OR_TAG"
|
||||
git rev-parse HEAD
|
||||
) > solana-release/version.txt
|
||||
|
||||
scripts/cargo-install-all.sh solana-release
|
||||
COMMIT="$(git rev-parse HEAD)"
|
||||
|
||||
(
|
||||
echo "channel: $CHANNEL"
|
||||
echo "commit: $COMMIT"
|
||||
echo "target: $TARGET"
|
||||
) > solana-release/version.yml
|
||||
|
||||
source ci/rust-version.sh stable
|
||||
scripts/cargo-install-all.sh +"$rust_stable" solana-release
|
||||
|
||||
./fetch-perf-libs.sh
|
||||
# shellcheck source=/dev/null
|
||||
@ -45,33 +64,62 @@ echo --- Creating tarball
|
||||
cargo install --path . --features=cuda --root ../solana-release-cuda
|
||||
)
|
||||
cp solana-release-cuda/bin/solana-fullnode solana-release/bin/solana-fullnode-cuda
|
||||
cp -a scripts multinode-demo solana-release/
|
||||
|
||||
tar jvcf solana-release.tar.bz2 solana-release/
|
||||
# Add a wrapper script for fullnode.sh
|
||||
# TODO: Remove multinode/... from tarball
|
||||
cat > solana-release/bin/fullnode.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
export USE_INSTALL=1
|
||||
exec multinode-demo/fullnode.sh "$@"
|
||||
EOF
|
||||
chmod +x solana-release/bin/fullnode.sh
|
||||
|
||||
# Add a wrapper script for clear-fullnode-config.sh
|
||||
# TODO: Remove multinode/... from tarball
|
||||
cat > solana-release/bin/clear-fullnode-config.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
export USE_INSTALL=1
|
||||
exec multinode-demo/clear-fullnode-config.sh "$@"
|
||||
EOF
|
||||
chmod +x solana-release/bin/clear-fullnode-config.sh
|
||||
|
||||
tar jvcf solana-release-$TARGET.tar.bz2 solana-release/
|
||||
cp solana-release/bin/solana-install solana-install-$TARGET
|
||||
)
|
||||
|
||||
echo --- Saving build artifacts
|
||||
source ci/upload-ci-artifact.sh
|
||||
upload-ci-artifact solana-release.tar.bz2
|
||||
upload-ci-artifact solana-release-$TARGET.tar.bz2
|
||||
|
||||
if [[ -n $DO_NOT_PUBLISH_TAR ]]; then
|
||||
echo Skipped due to DO_NOT_PUBLISH_TAR
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo --- AWS S3 Store
|
||||
(
|
||||
set -x
|
||||
$DRYRUN docker run \
|
||||
--rm \
|
||||
--env AWS_ACCESS_KEY_ID \
|
||||
--env AWS_SECRET_ACCESS_KEY \
|
||||
--volume "$PWD:/solana" \
|
||||
eremite/aws-cli:2018.12.18 \
|
||||
/usr/bin/s3cmd --acl-public put /solana/solana-release.tar.bz2 \
|
||||
s3://solana-release/"$CHANNEL_OR_TAG"/solana-release.tar.bz2
|
||||
for file in solana-release-$TARGET.tar.bz2 solana-install-$TARGET; do
|
||||
echo --- AWS S3 Store: $file
|
||||
(
|
||||
set -x
|
||||
$DRYRUN docker run \
|
||||
--rm \
|
||||
--env AWS_ACCESS_KEY_ID \
|
||||
--env AWS_SECRET_ACCESS_KEY \
|
||||
--volume "$PWD:/solana" \
|
||||
eremite/aws-cli:2018.12.18 \
|
||||
/usr/bin/s3cmd --acl-public put /solana/"$file" s3://release.solana.com/"$CHANNEL_OR_TAG"/"$file"
|
||||
|
||||
echo Published to:
|
||||
$DRYRUN ci/format-url.sh http://solana-release.s3.amazonaws.com/"$CHANNEL_OR_TAG"/solana-release.tar.bz2
|
||||
)
|
||||
echo Published to:
|
||||
$DRYRUN ci/format-url.sh http://release.solana.com/"$CHANNEL_OR_TAG"/"$file"
|
||||
)
|
||||
|
||||
if [[ -n $TAG ]]; then
|
||||
ci/upload-github-release-asset.sh $file
|
||||
fi
|
||||
done
|
||||
|
||||
echo --- ok
|
||||
|
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Run the entire buildkite CI pipeline locally for pre-testing before sending a
|
||||
# Github pull request
|
||||
#
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
BKRUN=ci/node_modules/.bin/bkrun
|
||||
|
||||
if [[ ! -x $BKRUN ]]; then
|
||||
(
|
||||
set -x
|
||||
cd ci/
|
||||
npm install bkrun
|
||||
)
|
||||
fi
|
||||
|
||||
set -x
|
||||
exec ./ci/node_modules/.bin/bkrun ci/buildkite.yml
|
45
ci/rust-version.sh
Normal file
45
ci/rust-version.sh
Normal file
@ -0,0 +1,45 @@
|
||||
#
|
||||
# This file maintains the rust versions for use by CI.
|
||||
#
|
||||
# Build with stable rust, updating the stable toolchain if necessary:
|
||||
# $ source ci/rust-version.sh stable
|
||||
# $ cargo +"$rust_stable" build
|
||||
#
|
||||
# Build with nightly rust, updating the nightly toolchain if necessary:
|
||||
# $ source ci/rust-version.sh nightly
|
||||
# $ cargo +"$rust_nightly" build
|
||||
#
|
||||
# Obtain the environment variables without any automatic toolchain updating:
|
||||
# $ source ci/rust-version.sh
|
||||
#
|
||||
|
||||
export rust_stable=1.34.0
|
||||
export rust_stable_docker_image=solanalabs/rust:1.34.0
|
||||
|
||||
export rust_nightly=nightly-2019-03-14
|
||||
export rust_nightly_docker_image=solanalabs/rust-nightly:2019-03-14
|
||||
|
||||
[[ -z $1 ]] || (
|
||||
|
||||
rustup_install() {
|
||||
declare toolchain=$1
|
||||
if ! cargo +"$toolchain" -V; then
|
||||
rustup install "$toolchain"
|
||||
cargo +"$toolchain" -V
|
||||
fi
|
||||
}
|
||||
|
||||
set -e
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
case $1 in
|
||||
stable)
|
||||
rustup_install "$rust_stable"
|
||||
;;
|
||||
nightly)
|
||||
rustup_install "$rust_nightly"
|
||||
;;
|
||||
*)
|
||||
echo "Note: ignoring unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
)
|
@ -24,7 +24,7 @@ source ci/_
|
||||
source ci/upload-ci-artifact.sh
|
||||
|
||||
eval "$(ci/channel-info.sh)"
|
||||
ci/version-check-with-upgrade.sh nightly
|
||||
source ci/rust-version.sh nightly
|
||||
|
||||
set -o pipefail
|
||||
export RUST_BACKTRACE=1
|
||||
@ -39,19 +39,28 @@ fi
|
||||
|
||||
BENCH_FILE=bench_output.log
|
||||
BENCH_ARTIFACT=current_bench_results.log
|
||||
_ cargo +nightly bench ${V:+--verbose} \
|
||||
-- -Z unstable-options --format=json | tee "$BENCH_FILE"
|
||||
|
||||
# First remove "BENCH_FILE", if it exists so that the following commands can append
|
||||
rm -f "$BENCH_FILE"
|
||||
|
||||
# Run sdk benches
|
||||
_ cargo +$rust_nightly bench --manifest-path sdk/Cargo.toml ${V:+--verbose} \
|
||||
-- -Z unstable-options --format=json | tee -a "$BENCH_FILE"
|
||||
|
||||
# Run runtime benches
|
||||
_ cargo +$rust_nightly bench --manifest-path runtime/Cargo.toml ${V:+--verbose} \
|
||||
-- -Z unstable-options --format=json | tee -a "$BENCH_FILE"
|
||||
|
||||
# Run core benches
|
||||
_ cargo +$rust_nightly bench --manifest-path core/Cargo.toml ${V:+--verbose} \
|
||||
-- -Z unstable-options --format=json | tee -a "$BENCH_FILE"
|
||||
|
||||
# Run bpf benches
|
||||
echo --- program/bpf
|
||||
(
|
||||
set -x
|
||||
cd programs/bpf
|
||||
cargo +nightly bench ${V:+--verbose} --features=bpf_c \
|
||||
-- -Z unstable-options --format=json --nocapture | tee -a ../../../"$BENCH_FILE"
|
||||
)
|
||||
_ 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"
|
||||
|
||||
_ cargo +nightly run --release --package solana-upload-perf \
|
||||
|
||||
_ cargo +$rust_nightly run --release --package solana-upload-perf \
|
||||
-- "$BENCH_FILE" "$TARGET_BRANCH" "$UPLOAD_METRICS" > "$BENCH_ARTIFACT"
|
||||
|
||||
upload-ci-artifact "$BENCH_ARTIFACT"
|
||||
|
@ -4,14 +4,14 @@ set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ci/_
|
||||
ci/version-check.sh stable
|
||||
source ci/rust-version.sh stable
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
export RUSTFLAGS="-D warnings"
|
||||
|
||||
_ cargo fmt --all -- --check
|
||||
_ cargo clippy --all -- --version
|
||||
_ cargo clippy --all -- --deny=warnings
|
||||
_ cargo +"$rust_stable" fmt --all -- --check
|
||||
_ cargo +"$rust_stable" clippy --all -- --version
|
||||
_ cargo +"$rust_stable" clippy --all -- --deny=warnings
|
||||
_ ci/audit.sh
|
||||
_ ci/nits.sh
|
||||
_ book/build.sh
|
||||
|
@ -21,7 +21,6 @@ ci/affects-files.sh \
|
||||
}
|
||||
|
||||
source ci/upload-ci-artifact.sh
|
||||
ci/version-check-with-upgrade.sh nightly
|
||||
source scripts/ulimit-n.sh
|
||||
|
||||
scripts/coverage.sh
|
||||
|
@ -4,9 +4,7 @@ set -e
|
||||
here=$(dirname "$0")
|
||||
cd "$here"/..
|
||||
|
||||
# This job doesn't run within a container, try once to upgrade tooling on a
|
||||
# version check failure
|
||||
ci/version-check-with-upgrade.sh stable
|
||||
source ci/rust-version.sh stable
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
|
||||
@ -39,4 +37,4 @@ fi
|
||||
|
||||
set -x
|
||||
export SOLANA_DYNAMIC_NODES=120
|
||||
exec cargo test --release --features=erasure test_multi_node_dynamic_network -- --ignored
|
||||
exec cargo +"$rust_stable" test --release --features=erasure test_multi_node_dynamic_network -- --ignored
|
||||
|
@ -10,7 +10,8 @@ annotate() {
|
||||
}
|
||||
}
|
||||
|
||||
ci/version-check-with-upgrade.sh stable
|
||||
source ci/rust-version.sh stable
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
export RUSTFLAGS="-D warnings"
|
||||
source scripts/ulimit-n.sh
|
||||
@ -24,9 +25,8 @@ case $testName in
|
||||
test-stable)
|
||||
echo "Executing $testName"
|
||||
|
||||
_ cargo build --all ${V:+--verbose}
|
||||
_ cargo test --all ${V:+--verbose} -- --nocapture --test-threads=1
|
||||
_ cargo test --manifest-path programs/system/Cargo.toml
|
||||
_ cargo +"$rust_stable" build --all ${V:+--verbose}
|
||||
_ cargo +"$rust_stable" test --all ${V:+--verbose} -- --nocapture --test-threads=1
|
||||
;;
|
||||
test-stable-perf)
|
||||
echo "Executing $testName"
|
||||
@ -41,14 +41,16 @@ test-stable-perf)
|
||||
^sdk/ \
|
||||
|| {
|
||||
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
|
||||
}
|
||||
|
||||
# BPF program tests
|
||||
_ make -C programs/bpf/c tests
|
||||
_ programs/bpf/rust/noop/build.sh # Must be built out of band
|
||||
_ cargo test --manifest-path programs/bpf/Cargo.toml --no-default-features --features=bpf_c,bpf_rust
|
||||
_ cargo +"$rust_stable" test \
|
||||
--manifest-path programs/bpf/Cargo.toml \
|
||||
--no-default-features --features=bpf_c,bpf_rust
|
||||
|
||||
# Run root package tests with these features
|
||||
ROOT_FEATURES=erasure,chacha
|
||||
@ -67,20 +69,8 @@ test-stable-perf)
|
||||
fi
|
||||
|
||||
# Run root package library tests
|
||||
_ cargo build --all ${V:+--verbose} --features="$ROOT_FEATURES"
|
||||
_ cargo test --all --lib ${V:+--verbose} --features="$ROOT_FEATURES" -- --nocapture --test-threads=1
|
||||
_ cargo test --manifest-path programs/system/Cargo.toml
|
||||
|
||||
# Run root package integration tests
|
||||
for test in tests/*.rs; do
|
||||
test=${test##*/} # basename x
|
||||
test=${test%.rs} # basename x .rs
|
||||
(
|
||||
export RUST_LOG="$test"=trace,$RUST_LOG
|
||||
_ cargo test --all ${V:+--verbose} --features="$ROOT_FEATURES" --test="$test" \
|
||||
-- --test-threads=1 --nocapture
|
||||
)
|
||||
done
|
||||
_ cargo +"$rust_stable" build --all ${V:+--verbose} --features="$ROOT_FEATURES"
|
||||
_ cargo +"$rust_stable" test --manifest-path=core/Cargo.toml ${V:+--verbose} --features="$ROOT_FEATURES" -- --nocapture --test-threads=1
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown test: $testName"
|
||||
|
@ -14,21 +14,25 @@ source ci/upload-ci-artifact.sh
|
||||
[[ -n $ITERATION_WAIT ]] || ITERATION_WAIT=300
|
||||
[[ -n $NUMBER_OF_NODES ]] || NUMBER_OF_NODES="10 25 50 100"
|
||||
[[ -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 $TESTNET_TAG ]] || TESTNET_TAG=testnet-automation
|
||||
[[ -n $TESTNET_ZONE ]] || TESTNET_ZONE=us-west1-b
|
||||
[[ -n $TESTNET_ZONES ]] || TESTNET_ZONES="us-west1-b"
|
||||
[[ -n $CHANNEL ]] || CHANNEL=beta
|
||||
[[ -n $ADDITIONAL_FLAGS ]] || ADDITIONAL_FLAGS=""
|
||||
|
||||
TESTNET_CLOUD_ZONES=(); while read -r -d, ; do TESTNET_CLOUD_ZONES+=( "$REPLY" ); done <<< "${TESTNET_ZONES},"
|
||||
|
||||
launchTestnet() {
|
||||
declare nodeCount=$1
|
||||
echo --- setup "$nodeCount" node test
|
||||
|
||||
# shellcheck disable=SC2068
|
||||
net/gce.sh create \
|
||||
-b \
|
||||
-d pd-ssd \
|
||||
-n "$nodeCount" -c "$CLIENT_COUNT" \
|
||||
-G "$LEADER_CPU_MACHINE_TYPE" \
|
||||
-p "$TESTNET_TAG" -z "$TESTNET_ZONE"
|
||||
-p "$TESTNET_TAG" ${TESTNET_CLOUD_ZONES[@]/#/-z } "$ADDITIONAL_FLAGS"
|
||||
|
||||
echo --- configure database
|
||||
net/init-metrics.sh -e
|
||||
@ -43,6 +47,8 @@ launchTestnet() {
|
||||
echo --- wait "$ITERATION_WAIT" seconds to complete test
|
||||
sleep "$ITERATION_WAIT"
|
||||
|
||||
set -x
|
||||
|
||||
declare q_mean_tps='
|
||||
SELECT round(mean("sum_count")) AS "mean_tps" FROM (
|
||||
SELECT sum("count") AS "sum_count"
|
||||
@ -59,21 +65,21 @@ launchTestnet() {
|
||||
|
||||
declare q_mean_confirmation='
|
||||
SELECT round(mean("duration_ms")) as "mean_confirmation"
|
||||
FROM "testnet-automation"."autogen"."leader-confirmation"
|
||||
FROM "testnet-automation"."autogen"."validator-confirmation"
|
||||
WHERE time > now() - 300s'
|
||||
|
||||
declare q_max_confirmation='
|
||||
SELECT round(max("duration_ms")) as "max_confirmation"
|
||||
FROM "testnet-automation"."autogen"."leader-confirmation"
|
||||
FROM "testnet-automation"."autogen"."validator-confirmation"
|
||||
WHERE time > now() - 300s'
|
||||
|
||||
declare q_99th_confirmation='
|
||||
SELECT round(percentile("duration_ms", 99)) as "99th_confirmation"
|
||||
FROM "testnet-automation"."autogen"."leader-confirmation"
|
||||
FROM "testnet-automation"."autogen"."validator-confirmation"
|
||||
WHERE time > now() - 300s'
|
||||
|
||||
curl -G "https://metrics.solana.com:8086/query?u=${INFLUX_USERNAME}&p=${INFLUX_PASSWORD}" \
|
||||
--data-urlencode "db=$INFLUX_DATABASE" \
|
||||
curl -G "${INFLUX_HOST}/query?u=ro&p=topsecret" \
|
||||
--data-urlencode "db=testnet-automation" \
|
||||
--data-urlencode "q=$q_mean_tps;$q_max_tps;$q_mean_confirmation;$q_max_confirmation;$q_99th_confirmation" |
|
||||
python ci/testnet-automation-json-parser.py >>TPS"$nodeCount".log
|
||||
|
||||
|
@ -10,7 +10,12 @@ bootstrapFullNodeMachineType=
|
||||
clientNodeCount=0
|
||||
additionalFullNodeCount=10
|
||||
publicNetwork=false
|
||||
skipSetup=false
|
||||
stopNetwork=false
|
||||
reuseLedger=false
|
||||
skipCreate=false
|
||||
skipStart=false
|
||||
externalNode=false
|
||||
failOnValidatorBootupFailure=true
|
||||
tarChannelOrTag=edge
|
||||
delete=false
|
||||
enableGpu=false
|
||||
@ -25,13 +30,14 @@ usage() {
|
||||
echo "Error: $*"
|
||||
fi
|
||||
cat <<EOF
|
||||
usage: $0 [name] [cloud] [zone] [options...]
|
||||
usage: $0 -p network-name -C cloud -z zone1 [-z zone2] ... [-z zoneN] [options...]
|
||||
|
||||
Deploys a CD testnet
|
||||
|
||||
name - name of the network
|
||||
cloud - cloud provider to use (gce, ec2)
|
||||
zone - cloud provider zone to deploy the network into
|
||||
mandatory arguments:
|
||||
-p [network-name] - name of the network
|
||||
-C [cloud] - cloud provider to use (gce, ec2)
|
||||
-z [zone] - cloud provider zone to deploy the network into. Must specify at least one zone
|
||||
|
||||
options:
|
||||
-t edge|beta|stable|vX.Y.Z - Deploy the latest tarball release for the
|
||||
@ -50,6 +56,11 @@ Deploys a CD testnet
|
||||
-D - Delete the network
|
||||
-r - Reuse existing node/ledger configuration from a
|
||||
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
|
||||
|
||||
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
||||
metrics
|
||||
@ -57,19 +68,22 @@ EOF
|
||||
exit $exitcode
|
||||
}
|
||||
|
||||
netName=$1
|
||||
cloudProvider=$2
|
||||
zone=$3
|
||||
[[ -n $netName ]] || usage
|
||||
[[ -n $cloudProvider ]] || usage "Cloud provider not specified"
|
||||
[[ -n $zone ]] || usage "Zone not specified"
|
||||
shift 3
|
||||
zone=()
|
||||
|
||||
while getopts "h?p:Pn:c:t:gG:a:Dbd:ru" opt; do
|
||||
while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:Sfe" opt; do
|
||||
case $opt in
|
||||
h | \?)
|
||||
usage
|
||||
;;
|
||||
p)
|
||||
netName=$OPTARG
|
||||
;;
|
||||
C)
|
||||
cloudProvider=$OPTARG
|
||||
;;
|
||||
z)
|
||||
zone+=("$OPTARG")
|
||||
;;
|
||||
P)
|
||||
publicNetwork=true
|
||||
;;
|
||||
@ -109,17 +123,36 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:ru" opt; do
|
||||
delete=true
|
||||
;;
|
||||
r)
|
||||
skipSetup=true
|
||||
reuseLedger=true
|
||||
;;
|
||||
e)
|
||||
skipCreate=true
|
||||
;;
|
||||
s)
|
||||
skipStart=true
|
||||
;;
|
||||
x)
|
||||
externalNode=true
|
||||
;;
|
||||
f)
|
||||
failOnValidatorBootupFailure=false
|
||||
;;
|
||||
u)
|
||||
blockstreamer=true
|
||||
;;
|
||||
S)
|
||||
stopNetwork=true
|
||||
;;
|
||||
*)
|
||||
usage "Error: unhandled option: $opt"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n $netName ]] || usage
|
||||
[[ -n $cloudProvider ]] || usage "Cloud provider not specified"
|
||||
[[ -n ${zone[*]} ]] || usage "At least one zone must be specified"
|
||||
|
||||
shutdown() {
|
||||
exitcode=$?
|
||||
|
||||
@ -140,9 +173,25 @@ trap shutdown EXIT INT
|
||||
|
||||
set -x
|
||||
|
||||
if ! $skipSetup; then
|
||||
# Build a string to pass zone opts to $cloudProvider.sh: "-z zone1 -z zone2 ..."
|
||||
zone_args=()
|
||||
for val in "${zone[@]}"; do
|
||||
zone_args+=("-z $val")
|
||||
done
|
||||
|
||||
if $stopNetwork; then
|
||||
skipCreate=true
|
||||
fi
|
||||
|
||||
if $delete; then
|
||||
skipCreate=false
|
||||
fi
|
||||
|
||||
# Create the network
|
||||
if ! $skipCreate; then
|
||||
echo "--- $cloudProvider.sh delete"
|
||||
time net/"$cloudProvider".sh delete -z "$zone" -p "$netName"
|
||||
# shellcheck disable=SC2068
|
||||
time net/"$cloudProvider".sh delete ${zone_args[@]} -p "$netName" ${externalNode:+-x}
|
||||
if $delete; then
|
||||
exit 0
|
||||
fi
|
||||
@ -150,11 +199,12 @@ if ! $skipSetup; then
|
||||
echo "--- $cloudProvider.sh create"
|
||||
create_args=(
|
||||
-p "$netName"
|
||||
-z "$zone"
|
||||
-a "$bootstrapFullNodeAddress"
|
||||
-c "$clientNodeCount"
|
||||
-n "$additionalFullNodeCount"
|
||||
)
|
||||
# shellcheck disable=SC2206
|
||||
create_args+=(${zone_args[@]})
|
||||
|
||||
if $blockstreamer; then
|
||||
create_args+=(-u)
|
||||
@ -180,17 +230,34 @@ if ! $skipSetup; then
|
||||
create_args+=(-P)
|
||||
fi
|
||||
|
||||
if $externalNode; then
|
||||
create_args+=(-x)
|
||||
fi
|
||||
|
||||
if ! $failOnValidatorBootupFailure; then
|
||||
create_args+=(-f)
|
||||
fi
|
||||
|
||||
time net/"$cloudProvider".sh create "${create_args[@]}"
|
||||
else
|
||||
echo "--- $cloudProvider.sh config"
|
||||
config_args=(
|
||||
-p "$netName"
|
||||
-z "$zone"
|
||||
)
|
||||
# shellcheck disable=SC2206
|
||||
config_args+=(${zone_args[@]})
|
||||
if $publicNetwork; then
|
||||
config_args+=(-P)
|
||||
fi
|
||||
|
||||
if $externalNode; then
|
||||
config_args+=(-x)
|
||||
fi
|
||||
|
||||
if ! $failOnValidatorBootupFailure; then
|
||||
config_args+=(-f)
|
||||
fi
|
||||
|
||||
time net/"$cloudProvider".sh config "${config_args[@]}"
|
||||
fi
|
||||
net/init-metrics.sh -e
|
||||
@ -198,39 +265,59 @@ net/init-metrics.sh -e
|
||||
echo "+++ $cloudProvider.sh info"
|
||||
net/"$cloudProvider".sh info
|
||||
|
||||
echo --- net.sh start
|
||||
maybeRejectExtraNodes=
|
||||
if ! $publicNetwork; then
|
||||
maybeRejectExtraNodes="-o rejectExtraNodes"
|
||||
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"
|
||||
if $stopNetwork; then
|
||||
echo --- net.sh stop
|
||||
time net/net.sh stop
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ok=true
|
||||
(
|
||||
if $skipSetup; then
|
||||
# TODO: Enable rolling updates
|
||||
#op=update
|
||||
op=restart
|
||||
else
|
||||
op=start
|
||||
fi
|
||||
if ! $skipStart; then
|
||||
(
|
||||
if $skipCreate; then
|
||||
# TODO: Enable rolling updates
|
||||
#op=update
|
||||
op=restart
|
||||
else
|
||||
op=start
|
||||
fi
|
||||
echo "--- net.sh $op"
|
||||
|
||||
# shellcheck disable=SC2086 # Don't want to double quote maybeRejectExtraNodes
|
||||
time net/net.sh $op -t "$tarChannelOrTag" \
|
||||
$maybeSkipSetup $maybeRejectExtraNodes $maybeNoValidatorSanity $maybeNoLedgerVerify
|
||||
) || ok=false
|
||||
maybeRejectExtraNodes=
|
||||
if ! $publicNetwork; then
|
||||
maybeRejectExtraNodes="-o rejectExtraNodes"
|
||||
fi
|
||||
maybeNoValidatorSanity=
|
||||
if [[ -n $NO_VALIDATOR_SANITY ]]; then
|
||||
maybeNoValidatorSanity="-o noValidatorSanity"
|
||||
fi
|
||||
maybeNoLedgerVerify=
|
||||
if [[ -n $NO_LEDGER_VERIFY ]]; then
|
||||
maybeNoLedgerVerify="-o noLedgerVerify"
|
||||
fi
|
||||
|
||||
maybeReuseLedger=
|
||||
if $reuseLedger; then
|
||||
maybeReuseLedger="-r"
|
||||
fi
|
||||
|
||||
maybeUpdateManifestKeypairFile=
|
||||
# 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
|
||||
echo "$SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu" > update_manifest_keypair.json
|
||||
maybeUpdateManifestKeypairFile="-i update_manifest_keypair.json"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086 # Don't want to double quote the $maybeXYZ variables
|
||||
time net/net.sh $op -t "$tarChannelOrTag" \
|
||||
$maybeUpdateManifestKeypairFile \
|
||||
$maybeReuseLedger \
|
||||
$maybeRejectExtraNodes \
|
||||
$maybeNoValidatorSanity \
|
||||
$maybeNoLedgerVerify
|
||||
) || ok=false
|
||||
|
||||
net/net.sh logs
|
||||
fi
|
||||
|
||||
net/net.sh logs
|
||||
$ok
|
||||
|
@ -42,20 +42,32 @@ steps:
|
||||
value: "testnet-beta"
|
||||
- label: "testnet-beta-perf"
|
||||
value: "testnet-beta-perf"
|
||||
- label: "testnet-demo"
|
||||
value: "testnet-demo"
|
||||
- select: "Operation"
|
||||
key: "testnet-operation"
|
||||
default: "sanity-or-restart"
|
||||
options:
|
||||
- label: "Sanity check. Restart network on failure"
|
||||
value: "sanity-or-restart"
|
||||
- label: "Start (or restart) the network"
|
||||
- label: "Create testnet and then start software. If the testnet already exists it will be deleted and re-created"
|
||||
value: "create-and-start"
|
||||
- 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"
|
||||
- label: "Update the network software. Restart network on failure"
|
||||
value: "update-or-restart"
|
||||
- label: "Stop the network"
|
||||
- label: "Stop network software without deleting testnet nodes"
|
||||
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"
|
||||
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")"
|
||||
agents:
|
||||
- "queue=$BUILDKITE_AGENT_META_DATA_QUEUE"
|
||||
@ -64,30 +76,73 @@ EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
export SOLANA_METRICS_CONFIG="db=$TESTNET,$SOLANA_METRICS_PARTIAL_CONFIG"
|
||||
echo "SOLANA_METRICS_CONFIG: $SOLANA_METRICS_CONFIG"
|
||||
source scripts/configure-metrics.sh
|
||||
|
||||
ci/channel-info.sh
|
||||
eval "$(ci/channel-info.sh)"
|
||||
|
||||
|
||||
EC2_ZONES=(us-west-1a sa-east-1a ap-northeast-2a eu-central-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_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
|
||||
;;
|
||||
testnet-beta|testnet-beta-perf)
|
||||
CHANNEL_OR_TAG=beta
|
||||
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||
: "${TESTNET_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
|
||||
;;
|
||||
testnet|testnet-perf)
|
||||
if [[ -n $BETA_CHANNEL_LATEST_TAG ]]; then
|
||||
CHANNEL_OR_TAG=$BETA_CHANNEL_LATEST_TAG
|
||||
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||
else
|
||||
CHANNEL_OR_TAG=$STABLE_CHANNEL_LATEST_TAG
|
||||
CHANNEL_BRANCH=$STABLE_CHANNEL
|
||||
fi
|
||||
testnet)
|
||||
CHANNEL_OR_TAG=$STABLE_CHANNEL_LATEST_TAG
|
||||
CHANNEL_BRANCH=$STABLE_CHANNEL
|
||||
: "${EC2_NODE_COUNT:=10}"
|
||||
: "${GCE_NODE_COUNT:=}"
|
||||
: "${TESTNET_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
|
||||
;;
|
||||
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}"
|
||||
: "${TESTNET_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid TESTNET=$TESTNET"
|
||||
@ -95,9 +150,34 @@ testnet|testnet-perf)
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $BUILDKITE_BRANCH != "$CHANNEL_BRANCH" ]]; then
|
||||
(
|
||||
cat <<EOF
|
||||
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
|
||||
SOLANA_METRICS_PARTIAL_CONFIG="host=$TESTNET_DB_HOST,$SOLANA_METRICS_PARTIAL_CONFIG"
|
||||
fi
|
||||
|
||||
export SOLANA_METRICS_CONFIG="db=$TESTNET,$SOLANA_METRICS_PARTIAL_CONFIG"
|
||||
echo "SOLANA_METRICS_CONFIG: $SOLANA_METRICS_CONFIG"
|
||||
source scripts/configure-metrics.sh
|
||||
|
||||
if [[ -n $TESTNET_TAG ]]; then
|
||||
CHANNEL_OR_TAG=$TESTNET_TAG
|
||||
else
|
||||
|
||||
if [[ $BUILDKITE_BRANCH != "$CHANNEL_BRANCH" ]]; then
|
||||
(
|
||||
cat <<EOF
|
||||
steps:
|
||||
- trigger: "$BUILDKITE_PIPELINE_SLUG"
|
||||
async: true
|
||||
@ -107,19 +187,24 @@ steps:
|
||||
env:
|
||||
TESTNET: "$TESTNET"
|
||||
TESTNET_OP: "$TESTNET_OP"
|
||||
TESTNET_DB_HOST: "$TESTNET_DB_HOST"
|
||||
EC2_NODE_COUNT: "$EC2_NODE_COUNT"
|
||||
GCE_NODE_COUNT: "$GCE_NODE_COUNT"
|
||||
GCE_LOW_QUOTA_NODE_COUNT: "$GCE_LOW_QUOTA_NODE_COUNT"
|
||||
EOF
|
||||
) | buildkite-agent pipeline upload
|
||||
exit 0
|
||||
) | buildkite-agent pipeline upload
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
sanity() {
|
||||
echo "--- sanity $TESTNET"
|
||||
case $TESTNET in
|
||||
testnet-edge)
|
||||
(
|
||||
set -x
|
||||
ci/testnet-sanity.sh edge-testnet-solana-com ec2 us-west-1a
|
||||
NO_LEDGER_VERIFY=1 \
|
||||
ci/testnet-sanity.sh edge-testnet-solana-com ec2 us-west-1a
|
||||
)
|
||||
;;
|
||||
testnet-edge-perf)
|
||||
@ -134,7 +219,8 @@ sanity() {
|
||||
testnet-beta)
|
||||
(
|
||||
set -x
|
||||
ci/testnet-sanity.sh beta-testnet-solana-com ec2 us-west-1a
|
||||
NO_LEDGER_VERIFY=1 \
|
||||
ci/testnet-sanity.sh beta-testnet-solana-com ec2 us-west-1a
|
||||
)
|
||||
;;
|
||||
testnet-beta-perf)
|
||||
@ -149,8 +235,19 @@ sanity() {
|
||||
testnet)
|
||||
(
|
||||
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)
|
||||
@ -163,6 +260,21 @@ sanity() {
|
||||
#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 \
|
||||
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"
|
||||
exit 1
|
||||
@ -170,26 +282,38 @@ sanity() {
|
||||
esac
|
||||
}
|
||||
|
||||
deploy() {
|
||||
declare maybeCreate=$1
|
||||
declare maybeStart=$2
|
||||
declare maybeStop=$3
|
||||
declare maybeDelete=$4
|
||||
|
||||
start() {
|
||||
declare maybeDelete=$1
|
||||
if [[ -z $maybeDelete ]]; then
|
||||
echo "--- start $TESTNET"
|
||||
echo "--- deploy \"$maybeCreate\" \"$maybeStart\" \"$maybeStop\" \"$maybeDelete\""
|
||||
|
||||
# Create or recreate the nodes
|
||||
if [[ -z $maybeCreate ]]; then
|
||||
skipCreate=skip
|
||||
else
|
||||
echo "--- stop $TESTNET"
|
||||
skipCreate=""
|
||||
fi
|
||||
|
||||
# Start or restart the network software on the nodes
|
||||
if [[ -z $maybeStart ]]; then
|
||||
skipStart=skip
|
||||
else
|
||||
skipStart=""
|
||||
fi
|
||||
declare maybeReuseLedger=$2
|
||||
|
||||
case $TESTNET in
|
||||
testnet-edge)
|
||||
(
|
||||
set -x
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
RUST_LOG=solana=info \
|
||||
ci/testnet-deploy.sh edge-testnet-solana-com ec2 us-west-1a \
|
||||
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0ccd4f2239886fa94 \
|
||||
${maybeReuseLedger:+-r} \
|
||||
${maybeDelete:+-D}
|
||||
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 \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
testnet-edge-perf)
|
||||
@ -197,10 +321,13 @@ start() {
|
||||
set -x
|
||||
NO_LEDGER_VERIFY=1 \
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
ci/testnet-deploy.sh edge-perf-testnet-solana-com ec2 us-west-2b \
|
||||
RUST_LOG=solana=warn \
|
||||
ci/testnet-deploy.sh -p edge-perf-testnet-solana-com -C ec2 -z us-west-2b \
|
||||
-g -t "$CHANNEL_OR_TAG" -c 2 \
|
||||
-b \
|
||||
${maybeReuseLedger:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
@ -208,11 +335,12 @@ start() {
|
||||
(
|
||||
set -x
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
RUST_LOG=solana=info \
|
||||
ci/testnet-deploy.sh beta-testnet-solana-com ec2 us-west-1a \
|
||||
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 \
|
||||
-b \
|
||||
${maybeReuseLedger:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
@ -221,27 +349,42 @@ start() {
|
||||
set -x
|
||||
NO_LEDGER_VERIFY=1 \
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
ci/testnet-deploy.sh beta-perf-testnet-solana-com ec2 us-west-2b \
|
||||
RUST_LOG=solana=warn \
|
||||
ci/testnet-deploy.sh -p beta-perf-testnet-solana-com -C ec2 -z us-west-2b \
|
||||
-g -t "$CHANNEL_OR_TAG" -c 2 \
|
||||
-b \
|
||||
${maybeReuseLedger:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
testnet)
|
||||
(
|
||||
set -x
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
RUST_LOG=solana=info \
|
||||
ci/testnet-deploy.sh testnet-solana-com ec2 us-west-1a \
|
||||
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0fa502bf95f6f18b2 \
|
||||
-b \
|
||||
${maybeReuseLedger:+-r} \
|
||||
${maybeDelete:+-D}
|
||||
#ci/testnet-deploy.sh testnet-solana-com gce us-east1-c \
|
||||
# -t "$CHANNEL_OR_TAG" -n 3 -c 0 -P -a testnet-solana-com \
|
||||
# ${maybeReuseLedger:+-r} \
|
||||
# ${maybeDelete:+-D}
|
||||
|
||||
if [[ -n $GCE_NODE_COUNT ]] || [[ -n $skipStart ]]; then
|
||||
maybeSkipStart="skip"
|
||||
fi
|
||||
|
||||
# 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 -a eipalloc-0fa502bf95f6f18b2 \
|
||||
${skipCreate:+-e} \
|
||||
${maybeSkipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
|
||||
if [[ -n $GCE_NODE_COUNT ]]; then
|
||||
# shellcheck disable=SC2068
|
||||
ci/testnet-deploy.sh -p testnet-solana-com -C gce ${GCE_ZONE_ARGS[@]} \
|
||||
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 0 -P \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D} \
|
||||
-x
|
||||
fi
|
||||
)
|
||||
;;
|
||||
testnet-perf)
|
||||
@ -249,18 +392,44 @@ start() {
|
||||
set -x
|
||||
NO_LEDGER_VERIFY=1 \
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
ci/testnet-deploy.sh perf-testnet-solana-com gce us-west1-b \
|
||||
-G "n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100" \
|
||||
RUST_LOG=solana=warn \
|
||||
ci/testnet-deploy.sh -p perf-testnet-solana-com -C gce -z us-west1-b \
|
||||
-G "--machine-type n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100" \
|
||||
-t "$CHANNEL_OR_TAG" -c 2 \
|
||||
-b \
|
||||
-d pd-ssd \
|
||||
${maybeReuseLedger:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
#ci/testnet-deploy.sh perf-testnet-solana-com ec2 us-east-1a \
|
||||
# -g \
|
||||
# -t "$CHANNEL_OR_TAG" -c 2 \
|
||||
# ${maybeReuseLedger:+-r} \
|
||||
# ${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
testnet-demo)
|
||||
(
|
||||
set -x
|
||||
|
||||
if [[ -n $GCE_LOW_QUOTA_NODE_COUNT ]] || [[ -n $skipStart ]]; then
|
||||
maybeSkipStart="skip"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2068
|
||||
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 \
|
||||
-a demo-testnet-solana-com \
|
||||
${skipCreate:+-e} \
|
||||
${maybeSkipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
|
||||
if [[ -n $GCE_LOW_QUOTA_NODE_COUNT ]]; then
|
||||
# shellcheck disable=SC2068
|
||||
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 \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
fi
|
||||
)
|
||||
;;
|
||||
*)
|
||||
@ -270,44 +439,107 @@ start() {
|
||||
esac
|
||||
}
|
||||
|
||||
ENABLED_LOCKFILE="${HOME}/${TESTNET}.is_enabled"
|
||||
|
||||
create-and-start() {
|
||||
deploy create start
|
||||
}
|
||||
create() {
|
||||
deploy create
|
||||
}
|
||||
start() {
|
||||
deploy "" start
|
||||
}
|
||||
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
|
||||
sanity)
|
||||
sanity
|
||||
enable)
|
||||
enable_testnet
|
||||
;;
|
||||
disable)
|
||||
disable_testnet
|
||||
delete
|
||||
;;
|
||||
create-and-start)
|
||||
is_testnet_enabled
|
||||
create-and-start
|
||||
;;
|
||||
create)
|
||||
is_testnet_enabled
|
||||
create
|
||||
;;
|
||||
start)
|
||||
is_testnet_enabled
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
is_testnet_enabled
|
||||
stop
|
||||
;;
|
||||
sanity)
|
||||
is_testnet_enabled
|
||||
sanity
|
||||
;;
|
||||
delete)
|
||||
is_testnet_enabled
|
||||
delete
|
||||
;;
|
||||
update-or-restart)
|
||||
if start "" update; then
|
||||
is_testnet_enabled
|
||||
if start; then
|
||||
echo Update successful
|
||||
else
|
||||
echo "+++ Update failed, restarting the network"
|
||||
$metricsWriteDatapoint "testnet-manager update-failure=1"
|
||||
start
|
||||
create-and-start
|
||||
fi
|
||||
;;
|
||||
sanity-or-restart)
|
||||
is_testnet_enabled
|
||||
if sanity; then
|
||||
echo Pass
|
||||
else
|
||||
echo "+++ Sanity failed, updating the network"
|
||||
$metricsWriteDatapoint "testnet-manager sanity-failure=1"
|
||||
if start "" update; then
|
||||
echo Update successful
|
||||
|
||||
# TODO: Restore attempt to restart the cluster before recreating it
|
||||
# See https://github.com/solana-labs/solana/issues/3774
|
||||
if false; then
|
||||
if start; then
|
||||
echo Update successful
|
||||
else
|
||||
echo "+++ Update failed, restarting the network"
|
||||
$metricsWriteDatapoint "testnet-manager update-failure=1"
|
||||
create-and-start
|
||||
fi
|
||||
else
|
||||
echo "+++ Update failed, restarting the network"
|
||||
$metricsWriteDatapoint "testnet-manager update-failure=1"
|
||||
start
|
||||
create-and-start
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid TESTNET_OP=$TESTNET_OP"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo --- fin
|
||||
|
@ -11,13 +11,13 @@ usage() {
|
||||
echo "Error: $*"
|
||||
fi
|
||||
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
|
||||
cloud - cloud provider to use (gce, ec2)
|
||||
zone - cloud provider zone of the network
|
||||
name - name of the network
|
||||
cloud - cloud provider to use (gce, ec2)
|
||||
zone1 .. zoneN - cloud provider zones to check
|
||||
|
||||
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
||||
metrics
|
||||
@ -27,10 +27,10 @@ EOF
|
||||
|
||||
netName=$1
|
||||
cloudProvider=$2
|
||||
zone=$3
|
||||
[[ -n $netName ]] || usage ""
|
||||
[[ -n $cloudProvider ]] || usage "Cloud provider not specified"
|
||||
[[ -n $zone ]] || usage "Zone not specified"
|
||||
shift 2
|
||||
[[ -n $1 ]] || usage "zone1 not specified"
|
||||
|
||||
shutdown() {
|
||||
exitcode=$?
|
||||
@ -48,20 +48,24 @@ shutdown() {
|
||||
exit $exitcode
|
||||
}
|
||||
rm -rf net/{log,-sanity}
|
||||
rm -f net/config/config
|
||||
trap shutdown EXIT INT
|
||||
|
||||
set -x
|
||||
echo "--- $cloudProvider.sh config"
|
||||
timeout 5m net/"$cloudProvider".sh config -p "$netName" -z "$zone"
|
||||
net/init-metrics.sh -e
|
||||
echo "+++ $cloudProvider.sh info"
|
||||
net/"$cloudProvider".sh info
|
||||
echo --- net.sh sanity
|
||||
ok=true
|
||||
timeout 5m net/net.sh sanity \
|
||||
${NO_LEDGER_VERIFY:+-o noLedgerVerify} \
|
||||
${NO_VALIDATOR_SANITY:+-o noValidatorSanity} \
|
||||
${REJECT_EXTRA_NODES:+-o rejectExtraNodes} || ok=false
|
||||
for zone in "$@"; do
|
||||
echo "--- $cloudProvider config [$zone]"
|
||||
timeout 5m net/"$cloudProvider".sh config -p "$netName" -z "$zone"
|
||||
net/init-metrics.sh -e
|
||||
echo "+++ $cloudProvider.sh info"
|
||||
net/"$cloudProvider".sh info
|
||||
echo "--- net.sh sanity [$cloudProvider:$zone]"
|
||||
ok=true
|
||||
timeout 5m net/net.sh sanity \
|
||||
${NO_LEDGER_VERIFY:+-o noLedgerVerify} \
|
||||
${NO_VALIDATOR_SANITY:+-o noValidatorSanity} \
|
||||
${REJECT_EXTRA_NODES:+-o rejectExtraNodes} \
|
||||
$zone || ok=false
|
||||
|
||||
net/net.sh logs
|
||||
$ok
|
||||
net/net.sh logs
|
||||
$ok
|
||||
done
|
||||
|
50
ci/upload-github-release-asset.sh
Executable file
50
ci/upload-github-release-asset.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Uploads one or more files to a github release
|
||||
#
|
||||
# Prerequisites
|
||||
# 1) GITHUB_TOKEN defined in the environment
|
||||
# 2) TAG defined in the environment
|
||||
#
|
||||
set -e
|
||||
|
||||
REPO_SLUG=solana-labs/solana
|
||||
|
||||
if [[ -z $1 ]]; then
|
||||
echo No files specified
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $GITHUB_TOKEN ]]; then
|
||||
echo Error: GITHUB_TOKEN not defined
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n $BUILDKITE_TAG ]]; then
|
||||
TAG=$BUILDKITE_TAG
|
||||
elif [[ -n $TRIGGERED_BUILDKITE_TAG ]]; then
|
||||
TAG=$TRIGGERED_BUILDKITE_TAG
|
||||
fi
|
||||
|
||||
if [[ -z $TAG ]]; then
|
||||
echo Error: TAG not defined
|
||||
exit 1
|
||||
fi
|
||||
|
||||
releaseId=$( \
|
||||
curl -s "https://api.github.com/repos/$REPO_SLUG/releases/tags/$TAG" \
|
||||
| grep -m 1 \"id\": \
|
||||
| sed -ne 's/^[^0-9]*\([0-9]*\),$/\1/p' \
|
||||
)
|
||||
echo "Github release id for $TAG is $releaseId"
|
||||
|
||||
for file in "$@"; do
|
||||
echo "--- Uploading $file to tag $TAG of $REPO_SLUG"
|
||||
curl \
|
||||
--data-binary @"$file" \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
"https://uploads.github.com/repos/$REPO_SLUG/releases/$releaseId/assets?name=$(basename "$file")"
|
||||
echo
|
||||
done
|
||||
|
@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
channel=${1:-stable}
|
||||
if ! ./version-check.sh "$channel"; then
|
||||
rustup install "$channel"
|
||||
./version-check.sh "$channel"
|
||||
fi
|
@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
require() {
|
||||
declare expectedProgram="$1"
|
||||
declare expectedVersion="$2"
|
||||
shift 2
|
||||
|
||||
read -r program version _ < <($expectedProgram "$@" -V)
|
||||
|
||||
declare ok=true
|
||||
[[ $program = "$expectedProgram" ]] || ok=false
|
||||
[[ $version =~ $expectedVersion ]] || ok=false
|
||||
|
||||
echo "Found $program $version"
|
||||
if ! $ok; then
|
||||
echo Error: expected "$expectedProgram $expectedVersion"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
case ${1:-stable} in
|
||||
nightly)
|
||||
require rustc 1.34.[0-9]+-nightly +nightly
|
||||
require cargo 1.34.[0-9]+-nightly +nightly
|
||||
;;
|
||||
stable)
|
||||
require rustc 1.32.[0-9]+
|
||||
require cargo 1.32.[0-9]+
|
||||
;;
|
||||
*)
|
||||
echo Error: unknown argument: "$1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
26
client/Cargo.toml
Normal file
26
client/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "solana-client"
|
||||
version = "0.14.0"
|
||||
description = "Solana Client"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
homepage = "https://solana.com/"
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.2"
|
||||
bs58 = "0.2.0"
|
||||
log = "0.4.2"
|
||||
jsonrpc-core = "10.1.0"
|
||||
reqwest = "0.9.11"
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.88"
|
||||
serde_json = "1.0.39"
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.14.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-core = "10.1.0"
|
||||
jsonrpc-http-server = "10.1.0"
|
||||
solana-logger = { path = "../logger", version = "0.14.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)
|
||||
}
|
||||
}
|
11
client/src/generic_rpc_client_request.rs
Normal file
11
client/src/generic_rpc_client_request.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use crate::client_error::ClientError;
|
||||
use crate::rpc_request::RpcRequest;
|
||||
|
||||
pub(crate) trait GenericRpcClientRequest {
|
||||
fn send(
|
||||
&self,
|
||||
request: &RpcRequest,
|
||||
params: Option<serde_json::Value>,
|
||||
retries: usize,
|
||||
) -> Result<serde_json::Value, ClientError>;
|
||||
}
|
7
client/src/lib.rs
Normal file
7
client/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod client_error;
|
||||
mod generic_rpc_client_request;
|
||||
pub mod mock_rpc_client_request;
|
||||
pub mod rpc_client;
|
||||
pub mod rpc_client_request;
|
||||
pub mod rpc_request;
|
||||
pub mod thin_client;
|
64
client/src/mock_rpc_client_request.rs
Normal file
64
client/src/mock_rpc_client_request.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use crate::client_error::ClientError;
|
||||
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
||||
use crate::rpc_request::RpcRequest;
|
||||
use serde_json::{Number, Value};
|
||||
use solana_sdk::transaction::{self, TransactionError};
|
||||
|
||||
pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
|
||||
pub const SIGNATURE: &str =
|
||||
"43yNSFC6fYTuPgTNFFhF4axw7AfWxB2BPdurme8yrsWEYwm8299xh8n6TAHjGymiSub1XtyxTNyd9GBfY2hxoBw8";
|
||||
|
||||
pub struct MockRpcClientRequest {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl MockRpcClientRequest {
|
||||
pub fn new(url: String) -> Self {
|
||||
Self { url }
|
||||
}
|
||||
}
|
||||
|
||||
impl GenericRpcClientRequest for MockRpcClientRequest {
|
||||
fn send(
|
||||
&self,
|
||||
request: &RpcRequest,
|
||||
params: Option<serde_json::Value>,
|
||||
_retries: usize,
|
||||
) -> Result<serde_json::Value, ClientError> {
|
||||
if self.url == "fails" {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let val = match request {
|
||||
RpcRequest::ConfirmTransaction => {
|
||||
if let Some(Value::Array(param_array)) = params {
|
||||
if let Value::String(param_string) = ¶m_array[0] {
|
||||
Value::Bool(param_string == SIGNATURE)
|
||||
} else {
|
||||
Value::Null
|
||||
}
|
||||
} else {
|
||||
Value::Null
|
||||
}
|
||||
}
|
||||
RpcRequest::GetBalance => {
|
||||
let n = if self.url == "airdrop" { 0 } else { 50 };
|
||||
Value::Number(Number::from(n))
|
||||
}
|
||||
RpcRequest::GetRecentBlockhash => Value::String(PUBKEY.to_string()),
|
||||
RpcRequest::GetSignatureStatus => {
|
||||
let response: Option<transaction::Result<()>> = if self.url == "account_in_use" {
|
||||
Some(Err(TransactionError::AccountInUse))
|
||||
} else if self.url == "sig_not_found" {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(()))
|
||||
};
|
||||
serde_json::to_value(response).unwrap()
|
||||
}
|
||||
RpcRequest::GetTransactionCount => Value::Number(Number::from(1234)),
|
||||
RpcRequest::SendTransaction => Value::String(SIGNATURE.to_string()),
|
||||
_ => Value::Null,
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
}
|
753
client/src/rpc_client.rs
Normal file
753
client/src/rpc_client.rs
Normal file
@ -0,0 +1,753 @@
|
||||
use crate::client_error::ClientError;
|
||||
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
||||
use crate::mock_rpc_client_request::MockRpcClientRequest;
|
||||
use crate::rpc_client_request::RpcClientRequest;
|
||||
use crate::rpc_request::RpcRequest;
|
||||
use bincode::serialize;
|
||||
use bs58;
|
||||
use log::*;
|
||||
use serde_json::{json, Value};
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
|
||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
|
||||
use solana_sdk::transaction::{self, Transaction, TransactionError};
|
||||
use std::error;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct RpcClient {
|
||||
client: Box<GenericRpcClientRequest + Send + Sync>,
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
pub fn new(url: String) -> Self {
|
||||
Self {
|
||||
client: Box::new(RpcClientRequest::new(url)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mock(url: String) -> Self {
|
||||
Self {
|
||||
client: Box::new(MockRpcClientRequest::new(url)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_socket(addr: SocketAddr) -> Self {
|
||||
Self::new(get_rpc_request_str(addr, false))
|
||||
}
|
||||
|
||||
pub fn new_socket_with_timeout(addr: SocketAddr, timeout: Duration) -> Self {
|
||||
let url = get_rpc_request_str(addr, false);
|
||||
Self {
|
||||
client: Box::new(RpcClientRequest::new_with_timeout(url, timeout)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_transaction(&self, transaction: &Transaction) -> Result<String, ClientError> {
|
||||
let serialized = serialize(transaction).unwrap();
|
||||
let params = json!([serialized]);
|
||||
let signature = self
|
||||
.client
|
||||
.send(&RpcRequest::SendTransaction, Some(params), 5)?;
|
||||
if signature.as_str().is_none() {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Received result of an unexpected type",
|
||||
))?;
|
||||
}
|
||||
Ok(signature.as_str().unwrap().to_string())
|
||||
}
|
||||
|
||||
pub fn get_signature_status(
|
||||
&self,
|
||||
signature: &str,
|
||||
) -> Result<Option<transaction::Result<()>>, ClientError> {
|
||||
let params = json!([signature.to_string()]);
|
||||
let signature_status =
|
||||
self.client
|
||||
.send(&RpcRequest::GetSignatureStatus, Some(params), 5)?;
|
||||
let result: Option<transaction::Result<()>> =
|
||||
serde_json::from_value(signature_status).unwrap();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn send_and_confirm_transaction<T: KeypairUtil>(
|
||||
&self,
|
||||
transaction: &mut Transaction,
|
||||
signer: &T,
|
||||
) -> Result<String, ClientError> {
|
||||
let mut send_retries = 5;
|
||||
loop {
|
||||
let mut status_retries = 4;
|
||||
let signature_str = self.send_transaction(transaction)?;
|
||||
let status = loop {
|
||||
let status = self.get_signature_status(&signature_str)?;
|
||||
if status.is_none() {
|
||||
status_retries -= 1;
|
||||
if status_retries == 0 {
|
||||
break status;
|
||||
}
|
||||
} else {
|
||||
break status;
|
||||
}
|
||||
if cfg!(not(test)) {
|
||||
// Retry ~twice during a slot
|
||||
sleep(Duration::from_millis(
|
||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
||||
));
|
||||
}
|
||||
};
|
||||
send_retries = if let Some(result) = status.clone() {
|
||||
match result {
|
||||
Ok(_) => return Ok(signature_str),
|
||||
Err(TransactionError::AccountInUse) => {
|
||||
// Fetch a new blockhash and re-sign the transaction before sending it again
|
||||
self.resign_transaction(transaction, signer)?;
|
||||
send_retries - 1
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
} else {
|
||||
send_retries - 1
|
||||
};
|
||||
if send_retries == 0 {
|
||||
if status.is_some() {
|
||||
status.unwrap()?
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Transaction {:?} failed: {:?}", signature_str, status),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_and_confirm_transactions(
|
||||
&self,
|
||||
mut transactions: Vec<Transaction>,
|
||||
signer: &Keypair,
|
||||
) -> Result<(), Box<dyn error::Error>> {
|
||||
let mut send_retries = 5;
|
||||
loop {
|
||||
let mut status_retries = 4;
|
||||
|
||||
// Send all transactions
|
||||
let mut transactions_signatures = vec![];
|
||||
for transaction in transactions {
|
||||
if cfg!(not(test)) {
|
||||
// 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
|
||||
// new program)
|
||||
sleep(Duration::from_millis(1000 / NUM_TICKS_PER_SECOND));
|
||||
}
|
||||
|
||||
let signature = self.send_transaction(&transaction).ok();
|
||||
transactions_signatures.push((transaction, signature))
|
||||
}
|
||||
|
||||
// Collect statuses for all the transactions, drop those that are confirmed
|
||||
while status_retries > 0 {
|
||||
status_retries -= 1;
|
||||
|
||||
if cfg!(not(test)) {
|
||||
// Retry ~twice during a slot
|
||||
sleep(Duration::from_millis(
|
||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
||||
));
|
||||
}
|
||||
|
||||
transactions_signatures = transactions_signatures
|
||||
.into_iter()
|
||||
.filter(|(_transaction, signature)| {
|
||||
if let Some(signature) = signature {
|
||||
if let Ok(status) = self.get_signature_status(&signature) {
|
||||
if status.is_none() {
|
||||
return false;
|
||||
}
|
||||
return status.unwrap().is_err();
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
if transactions_signatures.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if send_retries == 0 {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "Transactions failed"))?;
|
||||
}
|
||||
send_retries -= 1;
|
||||
|
||||
// Re-sign any failed transactions with a new blockhash and retry
|
||||
let blockhash =
|
||||
self.get_new_blockhash(&transactions_signatures[0].0.message().recent_blockhash)?;
|
||||
transactions = transactions_signatures
|
||||
.into_iter()
|
||||
.map(|(mut transaction, _)| {
|
||||
transaction.sign(&[signer], blockhash);
|
||||
transaction
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resign_transaction<T: KeypairUtil>(
|
||||
&self,
|
||||
tx: &mut Transaction,
|
||||
signer_key: &T,
|
||||
) -> Result<(), ClientError> {
|
||||
let blockhash = self.get_new_blockhash(&tx.message().recent_blockhash)?;
|
||||
tx.sign(&[signer_key], blockhash);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn retry_get_balance(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
retries: usize,
|
||||
) -> Result<Option<u64>, Box<dyn error::Error>> {
|
||||
let params = json!([format!("{}", pubkey)]);
|
||||
let res = self
|
||||
.client
|
||||
.send(&RpcRequest::GetBalance, Some(params), retries)?
|
||||
.as_u64();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn get_account_data(&self, pubkey: &Pubkey) -> io::Result<Vec<u8>> {
|
||||
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 response = self
|
||||
.client
|
||||
.send(&RpcRequest::GetAccountInfo, Some(params), 0);
|
||||
|
||||
response
|
||||
.and_then(|account_json| {
|
||||
let account: Account =
|
||||
serde_json::from_value(account_json).expect("deserialize account");
|
||||
trace!("Response account {:?} {:?}", pubkey, account);
|
||||
trace!("get_balance {:?}", account.lamports);
|
||||
Ok(account.lamports)
|
||||
})
|
||||
.map_err(|error| {
|
||||
debug!("Response account {}: None (error: {:?})", pubkey, error);
|
||||
io::Error::new(io::ErrorKind::Other, "AccountNotFound")
|
||||
})
|
||||
}
|
||||
|
||||
/// Request the transaction count. If the response packet is dropped by the network,
|
||||
/// this method will try again 5 times.
|
||||
pub fn get_transaction_count(&self) -> io::Result<u64> {
|
||||
debug!("get_transaction_count");
|
||||
|
||||
let mut num_retries = 5;
|
||||
while num_retries > 0 {
|
||||
let response = self.client.send(&RpcRequest::GetTransactionCount, None, 0);
|
||||
|
||||
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,
|
||||
"Unable to get transaction count, too many retries",
|
||||
))?
|
||||
}
|
||||
|
||||
pub fn get_recent_blockhash(&self) -> io::Result<Hash> {
|
||||
let mut num_retries = 5;
|
||||
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,
|
||||
"Unable to get recent blockhash, too many retries",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<Hash> {
|
||||
let mut num_retries = 10;
|
||||
while num_retries > 0 {
|
||||
if let Ok(new_blockhash) = self.get_recent_blockhash() {
|
||||
if new_blockhash != *blockhash {
|
||||
return Ok(new_blockhash);
|
||||
}
|
||||
}
|
||||
debug!("Got same blockhash ({:?}), will retry...", blockhash);
|
||||
|
||||
// Retry ~twice during a slot
|
||||
sleep(Duration::from_millis(
|
||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
||||
));
|
||||
num_retries -= 1;
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Unable to get new blockhash, too many retries",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn poll_balance_with_timeout(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
polling_frequency: &Duration,
|
||||
timeout: &Duration,
|
||||
) -> io::Result<u64> {
|
||||
let now = Instant::now();
|
||||
loop {
|
||||
match self.get_balance(&pubkey) {
|
||||
Ok(bal) => {
|
||||
return Ok(bal);
|
||||
}
|
||||
Err(e) => {
|
||||
sleep(*polling_frequency);
|
||||
if now.elapsed() > *timeout {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
|
||||
self.poll_balance_with_timeout(pubkey, &Duration::from_millis(100), &Duration::from_secs(1))
|
||||
}
|
||||
|
||||
pub fn wait_for_balance(&self, pubkey: &Pubkey, expected_balance: Option<u64>) -> Option<u64> {
|
||||
const LAST: usize = 30;
|
||||
for run in 0..LAST {
|
||||
let balance_result = self.poll_get_balance(pubkey);
|
||||
if expected_balance.is_none() {
|
||||
return balance_result.ok();
|
||||
}
|
||||
trace!(
|
||||
"retry_get_balance[{}] {:?} {:?}",
|
||||
run,
|
||||
balance_result,
|
||||
expected_balance
|
||||
);
|
||||
if let (Some(expected_balance), Ok(balance_result)) = (expected_balance, balance_result)
|
||||
{
|
||||
if expected_balance == balance_result {
|
||||
return Some(balance_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Poll the server to confirm a transaction.
|
||||
pub fn poll_for_signature(&self, signature: &Signature) -> io::Result<()> {
|
||||
let now = Instant::now();
|
||||
while !self.check_signature(signature) {
|
||||
if now.elapsed().as_secs() > 15 {
|
||||
// TODO: Return a better error.
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "signature not found"));
|
||||
}
|
||||
sleep(Duration::from_millis(250));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check a signature in the bank.
|
||||
pub fn check_signature(&self, signature: &Signature) -> bool {
|
||||
trace!("check_signature: {:?}", signature);
|
||||
let params = json!([format!("{}", signature)]);
|
||||
|
||||
for _ in 0..30 {
|
||||
let response =
|
||||
self.client
|
||||
.send(&RpcRequest::ConfirmTransaction, Some(params.clone()), 0);
|
||||
|
||||
match response {
|
||||
Ok(confirmation) => {
|
||||
let signature_status = confirmation.as_bool().unwrap();
|
||||
if signature_status {
|
||||
trace!("Response found signature");
|
||||
} else {
|
||||
trace!("Response signature not found");
|
||||
}
|
||||
|
||||
return signature_status;
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("check_signature request failed: {:?}", err);
|
||||
}
|
||||
};
|
||||
sleep(Duration::from_millis(250));
|
||||
}
|
||||
|
||||
panic!("Couldn't check signature of {}", signature);
|
||||
}
|
||||
|
||||
/// Poll the server to confirm a transaction.
|
||||
pub fn poll_for_signature_confirmation(
|
||||
&self,
|
||||
signature: &Signature,
|
||||
min_confirmed_blocks: usize,
|
||||
) -> io::Result<()> {
|
||||
let mut now = Instant::now();
|
||||
let mut confirmed_blocks = 0;
|
||||
loop {
|
||||
let response = self.get_num_blocks_since_signature_confirmation(signature);
|
||||
match response {
|
||||
Ok(count) => {
|
||||
if confirmed_blocks != count {
|
||||
info!(
|
||||
"signature {} confirmed {} out of {}",
|
||||
signature, count, min_confirmed_blocks
|
||||
);
|
||||
now = Instant::now();
|
||||
confirmed_blocks = count;
|
||||
}
|
||||
if count >= min_confirmed_blocks {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("check_confirmations request failed: {:?}", err);
|
||||
}
|
||||
};
|
||||
if now.elapsed().as_secs() > 15 {
|
||||
// TODO: Return a better error.
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "signature not found"));
|
||||
}
|
||||
sleep(Duration::from_millis(250));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_num_blocks_since_signature_confirmation(
|
||||
&self,
|
||||
sig: &Signature,
|
||||
) -> io::Result<usize> {
|
||||
let params = json!([format!("{}", sig)]);
|
||||
let response = self
|
||||
.client
|
||||
.send(
|
||||
&RpcRequest::GetNumBlocksSinceSignatureConfirmation,
|
||||
Some(params.clone()),
|
||||
1,
|
||||
)
|
||||
.map_err(|error| {
|
||||
debug!(
|
||||
"Response get_num_blocks_since_signature_confirmation: {:?}",
|
||||
error
|
||||
);
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"GetNumBlocksSinceSignatureConfirmation request failure",
|
||||
)
|
||||
})?;
|
||||
serde_json::from_value(response).map_err(|error| {
|
||||
debug!(
|
||||
"ParseError: get_num_blocks_since_signature_confirmation: {}",
|
||||
error
|
||||
);
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"GetNumBlocksSinceSignatureConfirmation parse failure",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fullnode_exit(&self) -> io::Result<bool> {
|
||||
let response = self
|
||||
.client
|
||||
.send(&RpcRequest::FullnodeExit, None, 0)
|
||||
.map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("FullnodeExit request failure: {:?}", err),
|
||||
)
|
||||
})?;
|
||||
serde_json::from_value(response).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("FullnodeExit parse failure: {:?}", err),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
pub fn retry_make_rpc_request(
|
||||
&self,
|
||||
request: &RpcRequest,
|
||||
params: Option<Value>,
|
||||
retries: usize,
|
||||
) -> Result<Value, ClientError> {
|
||||
self.client.send(request, params, retries)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rpc_request_str(rpc_addr: SocketAddr, tls: bool) -> String {
|
||||
if tls {
|
||||
format!("https://{}", rpc_addr)
|
||||
} else {
|
||||
format!("http://{}", rpc_addr)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock_rpc_client_request::{PUBKEY, SIGNATURE};
|
||||
use jsonrpc_core::{Error, IoHandler, Params};
|
||||
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
|
||||
use serde_json::Number;
|
||||
use solana_logger;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction;
|
||||
use solana_sdk::transaction::TransactionError;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn test_make_rpc_request() {
|
||||
let (sender, receiver) = channel();
|
||||
thread::spawn(move || {
|
||||
let rpc_addr = "0.0.0.0:0".parse().unwrap();
|
||||
let mut io = IoHandler::default();
|
||||
// Successful request
|
||||
io.add_method("getBalance", |_params: Params| {
|
||||
Ok(Value::Number(Number::from(50)))
|
||||
});
|
||||
// Failed request
|
||||
io.add_method("getRecentBlockhash", |params: Params| {
|
||||
if params != Params::None {
|
||||
Err(Error::invalid_request())
|
||||
} else {
|
||||
Ok(Value::String(
|
||||
"deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx".to_string(),
|
||||
))
|
||||
}
|
||||
});
|
||||
|
||||
let server = ServerBuilder::new(io)
|
||||
.threads(1)
|
||||
.cors(DomainsValidation::AllowOnly(vec![
|
||||
AccessControlAllowOrigin::Any,
|
||||
]))
|
||||
.start_http(&rpc_addr)
|
||||
.expect("Unable to start RPC server");
|
||||
sender.send(*server.address()).unwrap();
|
||||
server.wait();
|
||||
});
|
||||
|
||||
let rpc_addr = receiver.recv().unwrap();
|
||||
let rpc_client = RpcClient::new_socket(rpc_addr);
|
||||
|
||||
let balance = rpc_client.retry_make_rpc_request(
|
||||
&RpcRequest::GetBalance,
|
||||
Some(json!(["deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx"])),
|
||||
0,
|
||||
);
|
||||
assert_eq!(balance.unwrap().as_u64().unwrap(), 50);
|
||||
|
||||
let blockhash = rpc_client.retry_make_rpc_request(&RpcRequest::GetRecentBlockhash, None, 0);
|
||||
assert_eq!(
|
||||
blockhash.unwrap().as_str().unwrap(),
|
||||
"deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx"
|
||||
);
|
||||
|
||||
// Send erroneous parameter
|
||||
let blockhash = rpc_client.retry_make_rpc_request(
|
||||
&RpcRequest::GetRecentBlockhash,
|
||||
Some(json!("paramter")),
|
||||
0,
|
||||
);
|
||||
assert_eq!(blockhash.is_err(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_make_rpc_request() {
|
||||
solana_logger::setup();
|
||||
let (sender, receiver) = channel();
|
||||
thread::spawn(move || {
|
||||
// 1. Pick a random port
|
||||
// 2. Tell the client to start using it
|
||||
// 3. Delay for 1.5 seconds before starting the server to ensure the client will fail
|
||||
// and need to retry
|
||||
let rpc_addr: SocketAddr = "0.0.0.0:4242".parse().unwrap();
|
||||
sender.send(rpc_addr.clone()).unwrap();
|
||||
sleep(Duration::from_millis(1500));
|
||||
|
||||
let mut io = IoHandler::default();
|
||||
io.add_method("getBalance", move |_params: Params| {
|
||||
Ok(Value::Number(Number::from(5)))
|
||||
});
|
||||
let server = ServerBuilder::new(io)
|
||||
.threads(1)
|
||||
.cors(DomainsValidation::AllowOnly(vec![
|
||||
AccessControlAllowOrigin::Any,
|
||||
]))
|
||||
.start_http(&rpc_addr)
|
||||
.expect("Unable to start RPC server");
|
||||
server.wait();
|
||||
});
|
||||
|
||||
let rpc_addr = receiver.recv().unwrap();
|
||||
let rpc_client = RpcClient::new_socket(rpc_addr);
|
||||
|
||||
let balance = rpc_client.retry_make_rpc_request(
|
||||
&RpcRequest::GetBalance,
|
||||
Some(json!(["deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhw"])),
|
||||
10,
|
||||
);
|
||||
assert_eq!(balance.unwrap().as_u64().unwrap(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_transaction() {
|
||||
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||
|
||||
let key = Keypair::new();
|
||||
let to = Pubkey::new_rand();
|
||||
let blockhash = Hash::default();
|
||||
let tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
||||
|
||||
let signature = rpc_client.send_transaction(&tx);
|
||||
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
|
||||
|
||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||
|
||||
let signature = rpc_client.send_transaction(&tx);
|
||||
assert!(signature.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn test_get_recent_blockhash() {
|
||||
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||
|
||||
let vec = bs58::decode(PUBKEY).into_vec().unwrap();
|
||||
let expected_blockhash = Hash::new(&vec);
|
||||
|
||||
let blockhash = dbg!(rpc_client.get_recent_blockhash()).expect("blockhash ok");
|
||||
assert_eq!(blockhash, expected_blockhash);
|
||||
|
||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||
|
||||
let blockhash = dbg!(rpc_client.get_recent_blockhash());
|
||||
assert!(blockhash.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_signature_status() {
|
||||
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||
let signature = "good_signature";
|
||||
let status = rpc_client.get_signature_status(&signature).unwrap();
|
||||
assert_eq!(status, Some(Ok(())));
|
||||
|
||||
let rpc_client = RpcClient::new_mock("sig_not_found".to_string());
|
||||
let signature = "sig_not_found";
|
||||
let status = rpc_client.get_signature_status(&signature).unwrap();
|
||||
assert_eq!(status, None);
|
||||
|
||||
let rpc_client = RpcClient::new_mock("account_in_use".to_string());
|
||||
let signature = "account_in_use";
|
||||
let status = rpc_client.get_signature_status(&signature).unwrap();
|
||||
assert_eq!(status, Some(Err(TransactionError::AccountInUse)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_and_confirm_transaction() {
|
||||
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||
|
||||
let key = Keypair::new();
|
||||
let to = Pubkey::new_rand();
|
||||
let blockhash = Hash::default();
|
||||
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
||||
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
||||
result.unwrap();
|
||||
|
||||
let rpc_client = RpcClient::new_mock("account_in_use".to_string());
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
||||
assert!(result.is_err());
|
||||
|
||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resign_transaction() {
|
||||
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||
|
||||
let key = Keypair::new();
|
||||
let to = Pubkey::new_rand();
|
||||
let vec = bs58::decode("HUu3LwEzGRsUkuJS121jzkPJW39Kq62pXCTmTa1F9jDL")
|
||||
.into_vec()
|
||||
.unwrap();
|
||||
let blockhash = Hash::new(&vec);
|
||||
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, 0);
|
||||
|
||||
rpc_client.resign_transaction(&mut tx, &key).unwrap();
|
||||
|
||||
assert_ne!(prev_tx, tx);
|
||||
assert_ne!(prev_tx.signatures, tx.signatures);
|
||||
assert_ne!(
|
||||
prev_tx.message().recent_blockhash,
|
||||
tx.message().recent_blockhash
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_client_thread() {
|
||||
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||
thread::spawn(move || rpc_client);
|
||||
}
|
||||
}
|
82
client/src/rpc_client_request.rs
Normal file
82
client/src/rpc_client_request.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use crate::client_error::ClientError;
|
||||
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
||||
use crate::rpc_request::{RpcError, RpcRequest};
|
||||
use log::*;
|
||||
use reqwest;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct RpcClientRequest {
|
||||
client: reqwest::Client,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl RpcClientRequest {
|
||||
pub fn new(url: String) -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_timeout(url: String, timeout: Duration) -> Self {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.expect("build rpc client");
|
||||
|
||||
Self { client, url }
|
||||
}
|
||||
}
|
||||
|
||||
impl GenericRpcClientRequest for RpcClientRequest {
|
||||
fn send(
|
||||
&self,
|
||||
request: &RpcRequest,
|
||||
params: Option<serde_json::Value>,
|
||||
mut retries: usize,
|
||||
) -> Result<serde_json::Value, ClientError> {
|
||||
// Concurrent requests are not supported so reuse the same request id for all requests
|
||||
let request_id = 1;
|
||||
|
||||
let request_json = request.build_request_json(request_id, params);
|
||||
|
||||
loop {
|
||||
match self
|
||||
.client
|
||||
.post(&self.url)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(request_json.to_string())
|
||||
.send()
|
||||
{
|
||||
Ok(mut response) => {
|
||||
let json: serde_json::Value = serde_json::from_str(&response.text()?)?;
|
||||
if json["error"].is_object() {
|
||||
Err(RpcError::RpcRequestError(format!(
|
||||
"RPC Error response: {}",
|
||||
serde_json::to_string(&json["error"]).unwrap()
|
||||
)))?
|
||||
}
|
||||
return Ok(json["result"].clone());
|
||||
}
|
||||
Err(e) => {
|
||||
info!(
|
||||
"make_rpc_request({:?}) failed, {} retries left: {:?}",
|
||||
request, retries, e
|
||||
);
|
||||
if retries == 0 {
|
||||
Err(e)?;
|
||||
}
|
||||
retries -= 1;
|
||||
|
||||
// Sleep for approximately half a slot
|
||||
sleep(Duration::from_millis(
|
||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
client/src/rpc_request.rs
Normal file
117
client/src/rpc_request.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use serde_json::{json, Value};
|
||||
use std::{error, fmt};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum RpcRequest {
|
||||
ConfirmTransaction,
|
||||
DeregisterNode,
|
||||
FullnodeExit,
|
||||
GetAccountInfo,
|
||||
GetBalance,
|
||||
GetClusterNodes,
|
||||
GetNumBlocksSinceSignatureConfirmation,
|
||||
GetRecentBlockhash,
|
||||
GetSignatureStatus,
|
||||
GetSlotLeader,
|
||||
GetStorageBlockhash,
|
||||
GetStorageEntryHeight,
|
||||
GetStoragePubkeysForEntryHeight,
|
||||
GetTransactionCount,
|
||||
RegisterNode,
|
||||
RequestAirdrop,
|
||||
SendTransaction,
|
||||
SignVote,
|
||||
}
|
||||
|
||||
impl RpcRequest {
|
||||
pub(crate) fn build_request_json(&self, id: u64, params: Option<Value>) -> Value {
|
||||
let jsonrpc = "2.0";
|
||||
let method = match self {
|
||||
RpcRequest::ConfirmTransaction => "confirmTransaction",
|
||||
RpcRequest::DeregisterNode => "deregisterNode",
|
||||
RpcRequest::FullnodeExit => "fullnodeExit",
|
||||
RpcRequest::GetAccountInfo => "getAccountInfo",
|
||||
RpcRequest::GetBalance => "getBalance",
|
||||
RpcRequest::GetClusterNodes => "getClusterNodes",
|
||||
RpcRequest::GetNumBlocksSinceSignatureConfirmation => {
|
||||
"getNumBlocksSinceSignatureConfirmation"
|
||||
}
|
||||
RpcRequest::GetRecentBlockhash => "getRecentBlockhash",
|
||||
RpcRequest::GetSignatureStatus => "getSignatureStatus",
|
||||
RpcRequest::GetSlotLeader => "getSlotLeader",
|
||||
RpcRequest::GetStorageBlockhash => "getStorageBlockhash",
|
||||
RpcRequest::GetStorageEntryHeight => "getStorageEntryHeight",
|
||||
RpcRequest::GetStoragePubkeysForEntryHeight => "getStoragePubkeysForEntryHeight",
|
||||
RpcRequest::GetTransactionCount => "getTransactionCount",
|
||||
RpcRequest::RegisterNode => "registerNode",
|
||||
RpcRequest::RequestAirdrop => "requestAirdrop",
|
||||
RpcRequest::SendTransaction => "sendTransaction",
|
||||
RpcRequest::SignVote => "signVote",
|
||||
};
|
||||
let mut request = json!({
|
||||
"jsonrpc": jsonrpc,
|
||||
"id": id,
|
||||
"method": method,
|
||||
});
|
||||
if let Some(param_string) = params {
|
||||
request["params"] = param_string;
|
||||
}
|
||||
request
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RpcError {
|
||||
RpcRequestError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for RpcError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "invalid")
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for RpcError {
|
||||
fn description(&self) -> &str {
|
||||
"invalid"
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&dyn error::Error> {
|
||||
// Generic error, underlying cause isn't tracked.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_request_json() {
|
||||
let test_request = RpcRequest::GetAccountInfo;
|
||||
let addr = json!(["deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx"]);
|
||||
let request = test_request.build_request_json(1, Some(addr.clone()));
|
||||
assert_eq!(request["method"], "getAccountInfo");
|
||||
assert_eq!(request["params"], addr,);
|
||||
|
||||
let test_request = RpcRequest::GetBalance;
|
||||
let request = test_request.build_request_json(1, Some(addr));
|
||||
assert_eq!(request["method"], "getBalance");
|
||||
|
||||
let test_request = RpcRequest::GetRecentBlockhash;
|
||||
let request = test_request.build_request_json(1, None);
|
||||
assert_eq!(request["method"], "getRecentBlockhash");
|
||||
|
||||
let test_request = RpcRequest::GetTransactionCount;
|
||||
let request = test_request.build_request_json(1, None);
|
||||
assert_eq!(request["method"], "getTransactionCount");
|
||||
|
||||
let test_request = RpcRequest::RequestAirdrop;
|
||||
let request = test_request.build_request_json(1, None);
|
||||
assert_eq!(request["method"], "requestAirdrop");
|
||||
|
||||
let test_request = RpcRequest::SendTransaction;
|
||||
let request = test_request.build_request_json(1, None);
|
||||
assert_eq!(request["method"], "sendTransaction");
|
||||
}
|
||||
}
|
297
client/src/thin_client.rs
Normal file
297
client/src/thin_client.rs
Normal file
@ -0,0 +1,297 @@
|
||||
//! The `thin_client` module is a client-side object that interfaces with
|
||||
//! a server-side TPU. Client code should use this object instead of writing
|
||||
//! messages to the network directly. The binary encoding of its messages are
|
||||
//! unstable and may change in future releases.
|
||||
|
||||
use crate::rpc_client::RpcClient;
|
||||
use bincode::{serialize_into, serialized_size};
|
||||
use log::*;
|
||||
use solana_sdk::client::{AsyncClient, Client, SyncClient};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_sdk::message::Message;
|
||||
use solana_sdk::packet::PACKET_DATA_SIZE;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
|
||||
use solana_sdk::system_instruction;
|
||||
use solana_sdk::transaction::{self, Transaction};
|
||||
use solana_sdk::transport::Result as TransportResult;
|
||||
use std::io;
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::time::Duration;
|
||||
|
||||
/// An object for querying and sending transactions to the network.
|
||||
pub struct ThinClient {
|
||||
transactions_addr: SocketAddr,
|
||||
transactions_socket: UdpSocket,
|
||||
rpc_client: RpcClient,
|
||||
}
|
||||
|
||||
impl ThinClient {
|
||||
/// Create a new ThinClient that will interface with the Rpc at `rpc_addr` using TCP
|
||||
/// and the Tpu at `transactions_addr` over `transactions_socket` using UDP.
|
||||
pub fn new(
|
||||
rpc_addr: SocketAddr,
|
||||
transactions_addr: SocketAddr,
|
||||
transactions_socket: UdpSocket,
|
||||
) -> Self {
|
||||
Self::new_from_client(
|
||||
transactions_addr,
|
||||
transactions_socket,
|
||||
RpcClient::new_socket(rpc_addr),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_socket_with_timeout(
|
||||
rpc_addr: SocketAddr,
|
||||
transactions_addr: SocketAddr,
|
||||
transactions_socket: UdpSocket,
|
||||
timeout: Duration,
|
||||
) -> Self {
|
||||
let rpc_client = RpcClient::new_socket_with_timeout(rpc_addr, timeout);
|
||||
Self::new_from_client(transactions_addr, transactions_socket, rpc_client)
|
||||
}
|
||||
|
||||
fn new_from_client(
|
||||
transactions_addr: SocketAddr,
|
||||
transactions_socket: UdpSocket,
|
||||
rpc_client: RpcClient,
|
||||
) -> Self {
|
||||
Self {
|
||||
rpc_client,
|
||||
transactions_addr,
|
||||
transactions_socket,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retry a sending a signed Transaction to the server for processing.
|
||||
pub fn retry_transfer_until_confirmed(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
transaction: &mut Transaction,
|
||||
tries: usize,
|
||||
min_confirmed_blocks: usize,
|
||||
) -> io::Result<Signature> {
|
||||
self.send_and_confirm_transaction(&[keypair], transaction, tries, min_confirmed_blocks)
|
||||
}
|
||||
|
||||
/// Retry sending a signed Transaction with one signing Keypair to the server for processing.
|
||||
pub fn retry_transfer(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
transaction: &mut Transaction,
|
||||
tries: usize,
|
||||
) -> io::Result<Signature> {
|
||||
self.send_and_confirm_transaction(&[keypair], transaction, tries, 0)
|
||||
}
|
||||
|
||||
/// Retry sending a signed Transaction to the server for processing
|
||||
pub fn send_and_confirm_transaction(
|
||||
&self,
|
||||
keypairs: &[&Keypair],
|
||||
transaction: &mut Transaction,
|
||||
tries: usize,
|
||||
min_confirmed_blocks: usize,
|
||||
) -> io::Result<Signature> {
|
||||
for x in 0..tries {
|
||||
let mut buf = vec![0; serialized_size(&transaction).unwrap() as usize];
|
||||
let mut wr = std::io::Cursor::new(&mut buf[..]);
|
||||
serialize_into(&mut wr, &transaction)
|
||||
.expect("serialize Transaction in pub fn transfer_signed");
|
||||
self.transactions_socket
|
||||
.send_to(&buf[..], &self.transactions_addr)?;
|
||||
if self
|
||||
.poll_for_signature_confirmation(&transaction.signatures[0], min_confirmed_blocks)
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(transaction.signatures[0]);
|
||||
}
|
||||
info!("{} tries failed transfer to {}", x, self.transactions_addr);
|
||||
transaction.sign(keypairs, self.rpc_client.get_recent_blockhash()?);
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("retry_transfer failed in {} retries", tries),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<Hash> {
|
||||
self.rpc_client.get_new_blockhash(blockhash)
|
||||
}
|
||||
|
||||
pub fn poll_balance_with_timeout(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
polling_frequency: &Duration,
|
||||
timeout: &Duration,
|
||||
) -> io::Result<u64> {
|
||||
self.rpc_client
|
||||
.poll_balance_with_timeout(pubkey, polling_frequency, timeout)
|
||||
}
|
||||
|
||||
pub fn poll_get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
|
||||
self.rpc_client.poll_get_balance(pubkey)
|
||||
}
|
||||
|
||||
pub fn wait_for_balance(&self, pubkey: &Pubkey, expected_balance: Option<u64>) -> Option<u64> {
|
||||
self.rpc_client.wait_for_balance(pubkey, expected_balance)
|
||||
}
|
||||
|
||||
/// Check a signature in the bank. This method blocks
|
||||
/// until the server sends a response.
|
||||
pub fn check_signature(&self, signature: &Signature) -> bool {
|
||||
self.rpc_client.check_signature(signature)
|
||||
}
|
||||
|
||||
pub fn fullnode_exit(&self) -> io::Result<bool> {
|
||||
self.rpc_client.fullnode_exit()
|
||||
}
|
||||
pub fn get_num_blocks_since_signature_confirmation(
|
||||
&mut self,
|
||||
sig: &Signature,
|
||||
) -> io::Result<usize> {
|
||||
self.rpc_client
|
||||
.get_num_blocks_since_signature_confirmation(sig)
|
||||
}
|
||||
}
|
||||
|
||||
impl Client for ThinClient {
|
||||
fn transactions_addr(&self) -> String {
|
||||
self.transactions_addr.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncClient for ThinClient {
|
||||
fn send_message(&self, keypairs: &[&Keypair], message: Message) -> TransportResult<Signature> {
|
||||
let blockhash = self.get_recent_blockhash()?;
|
||||
let mut transaction = Transaction::new(&keypairs, message, blockhash);
|
||||
let signature = self.send_and_confirm_transaction(keypairs, &mut transaction, 5, 0)?;
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
fn send_instruction(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
instruction: Instruction,
|
||||
) -> TransportResult<Signature> {
|
||||
let message = Message::new(vec![instruction]);
|
||||
self.send_message(&[keypair], message)
|
||||
}
|
||||
|
||||
fn transfer(
|
||||
&self,
|
||||
lamports: u64,
|
||||
keypair: &Keypair,
|
||||
pubkey: &Pubkey,
|
||||
) -> TransportResult<Signature> {
|
||||
let transfer_instruction =
|
||||
system_instruction::transfer(&keypair.pubkey(), pubkey, lamports);
|
||||
self.send_instruction(keypair, transfer_instruction)
|
||||
}
|
||||
|
||||
fn get_account_data(&self, pubkey: &Pubkey) -> TransportResult<Option<Vec<u8>>> {
|
||||
Ok(self.rpc_client.get_account_data(pubkey).ok())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> TransportResult<u64> {
|
||||
let balance = self.rpc_client.get_balance(pubkey)?;
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
fn get_signature_status(
|
||||
&self,
|
||||
signature: &Signature,
|
||||
) -> TransportResult<Option<transaction::Result<()>>> {
|
||||
let status = self
|
||||
.rpc_client
|
||||
.get_signature_status(&signature.to_string())
|
||||
.map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("send_transaction failed with error {:?}", err),
|
||||
)
|
||||
})?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn get_recent_blockhash(&self) -> TransportResult<Hash> {
|
||||
let recent_blockhash = self.rpc_client.get_recent_blockhash()?;
|
||||
Ok(recent_blockhash)
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> TransportResult<u64> {
|
||||
let transaction_count = self.rpc_client.get_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)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncClient for ThinClient {
|
||||
fn async_send_transaction(&self, transaction: Transaction) -> io::Result<Signature> {
|
||||
let mut buf = vec![0; serialized_size(&transaction).unwrap() as usize];
|
||||
let mut wr = std::io::Cursor::new(&mut buf[..]);
|
||||
serialize_into(&mut wr, &transaction)
|
||||
.expect("serialize Transaction in pub fn transfer_signed");
|
||||
assert!(buf.len() < PACKET_DATA_SIZE);
|
||||
self.transactions_socket
|
||||
.send_to(&buf[..], &self.transactions_addr)?;
|
||||
Ok(transaction.signatures[0])
|
||||
}
|
||||
fn async_send_message(
|
||||
&self,
|
||||
keypairs: &[&Keypair],
|
||||
message: Message,
|
||||
recent_blockhash: Hash,
|
||||
) -> io::Result<Signature> {
|
||||
let transaction = Transaction::new(&keypairs, message, recent_blockhash);
|
||||
self.async_send_transaction(transaction)
|
||||
}
|
||||
fn async_send_instruction(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
instruction: Instruction,
|
||||
recent_blockhash: Hash,
|
||||
) -> io::Result<Signature> {
|
||||
let message = Message::new(vec![instruction]);
|
||||
self.async_send_message(&[keypair], message, recent_blockhash)
|
||||
}
|
||||
fn async_transfer(
|
||||
&self,
|
||||
lamports: u64,
|
||||
keypair: &Keypair,
|
||||
pubkey: &Pubkey,
|
||||
recent_blockhash: Hash,
|
||||
) -> io::Result<Signature> {
|
||||
let transfer_instruction =
|
||||
system_instruction::transfer(&keypair.pubkey(), pubkey, lamports);
|
||||
self.async_send_instruction(keypair, transfer_instruction, recent_blockhash)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_client((rpc, tpu): (SocketAddr, SocketAddr), range: (u16, u16)) -> ThinClient {
|
||||
let (_, transactions_socket) = solana_netutil::bind_in_range(range).unwrap();
|
||||
ThinClient::new(rpc, tpu, transactions_socket)
|
||||
}
|
||||
|
||||
pub fn create_client_with_timeout(
|
||||
(rpc, tpu): (SocketAddr, SocketAddr),
|
||||
range: (u16, u16),
|
||||
timeout: Duration,
|
||||
) -> ThinClient {
|
||||
let (_, transactions_socket) = solana_netutil::bind_in_range(range).unwrap();
|
||||
ThinClient::new_socket_with_timeout(rpc, tpu, transactions_socket, timeout)
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "solana"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "0.12.0"
|
||||
version = "0.14.0"
|
||||
documentation = "https://docs.rs/solana"
|
||||
homepage = "https://solana.com/"
|
||||
readme = "README.md"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
license = "Apache-2.0"
|
||||
@ -17,21 +17,23 @@ codecov = { repository = "solana-labs/solana", branch = "master", service = "git
|
||||
chacha = []
|
||||
cuda = []
|
||||
erasure = []
|
||||
kvstore = ["memmap"]
|
||||
kvstore = ["solana-kvstore"]
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.2"
|
||||
bs58 = "0.2.0"
|
||||
byteorder = "1.3.1"
|
||||
chrono = { version = "0.4.0", features = ["serde"] }
|
||||
hashbrown = "0.1.8"
|
||||
crc = { version = "1.8.1", optional = true }
|
||||
ed25519-dalek = "1.0.0-pre.0"
|
||||
hashbrown = "0.2.0"
|
||||
indexmap = "1.0"
|
||||
itertools = "0.8.0"
|
||||
jsonrpc-core = "10.1.0"
|
||||
jsonrpc-derive = "10.1.0"
|
||||
jsonrpc-http-server = "10.1.0"
|
||||
jsonrpc-pubsub = "10.1.0"
|
||||
jsonrpc-ws-server = "10.1.0"
|
||||
jsonrpc-core = "11.0.0"
|
||||
jsonrpc-derive = "11.0.0"
|
||||
jsonrpc-http-server = "11.0.0"
|
||||
jsonrpc-pubsub = "11.0.0"
|
||||
jsonrpc-ws-server = "11.0.0"
|
||||
libc = "0.2.50"
|
||||
log = "0.4.2"
|
||||
memmap = { version = "0.7.0", optional = true }
|
||||
@ -39,30 +41,50 @@ nix = "0.13.0"
|
||||
rand = "0.6.5"
|
||||
rand_chacha = "0.1.1"
|
||||
rayon = "1.0.0"
|
||||
reed-solomon-erasure = "3.1.1"
|
||||
reqwest = "0.9.11"
|
||||
ring = "0.13.2"
|
||||
rocksdb = "0.11.0"
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.88"
|
||||
serde_json = "1.0.39"
|
||||
solana-budget-api = { path = "../programs/budget_api", version = "0.12.0" }
|
||||
solana-drone = { path = "../drone", version = "0.12.0" }
|
||||
solana-logger = { path = "../logger", version = "0.12.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.12.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.12.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.12.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.12.0" }
|
||||
solana-storage-api = { path = "../programs/storage_api", version = "0.12.0" }
|
||||
solana-vote-api = { path = "../programs/vote_api", version = "0.12.0" }
|
||||
solana-vote-signer = { path = "../vote-signer", version = "0.12.0" }
|
||||
solana-budget-api = { path = "../programs/budget_api", version = "0.14.0" }
|
||||
solana-client = { path = "../client", version = "0.14.0" }
|
||||
solana-drone = { path = "../drone", version = "0.14.0" }
|
||||
solana-kvstore = { path = "../kvstore", version = "0.14.0" , optional = true }
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.14.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.14.0" }
|
||||
solana-storage-api = { path = "../programs/storage_api", version = "0.14.0" }
|
||||
solana-vote-api = { path = "../programs/vote_api", version = "0.14.0" }
|
||||
solana-vote-signer = { path = "../vote-signer", version = "0.14.0" }
|
||||
sys-info = "0.5.6"
|
||||
tokio = "0.1"
|
||||
tokio-codec = "0.1"
|
||||
untrusted = "0.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.1.3"
|
||||
hex-literal = "0.1.4"
|
||||
matches = "0.1.6"
|
||||
solana-vote-program = { path = "../programs/vote", version = "0.12.0" }
|
||||
solana-budget-program = { path = "../programs/budget", version = "0.12.0" }
|
||||
solana-vote-program = { path = "../programs/vote_program", version = "0.14.0" }
|
||||
solana-budget-program = { path = "../programs/budget_program", version = "0.14.0" }
|
||||
|
||||
[[bench]]
|
||||
name = "banking_stage"
|
||||
|
||||
[[bench]]
|
||||
name = "blocktree"
|
||||
|
||||
[[bench]]
|
||||
name = "ledger"
|
||||
|
||||
[[bench]]
|
||||
name = "gen_keys"
|
||||
|
||||
[[bench]]
|
||||
name = "sigverify"
|
||||
|
||||
[[bench]]
|
||||
required-features = ["chacha"]
|
||||
name = "chacha"
|
||||
|
@ -1,12 +1,16 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate test;
|
||||
#[macro_use]
|
||||
extern crate solana;
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use rayon::prelude::*;
|
||||
use solana::banking_stage::{create_test_recorder, BankingStage};
|
||||
use solana::blocktree::{get_tmp_ledger_path, Blocktree};
|
||||
use solana::cluster_info::ClusterInfo;
|
||||
use solana::cluster_info::Node;
|
||||
use solana::leader_schedule_cache::LeaderScheduleCache;
|
||||
use solana::packet::to_packets_chunked;
|
||||
use solana::poh_recorder::WorkingBankEntries;
|
||||
use solana::service::Service;
|
||||
@ -15,7 +19,7 @@ use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::hash::hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{KeypairUtil, Signature};
|
||||
use solana_sdk::system_transaction::SystemTransaction;
|
||||
use solana_sdk::system_transaction;
|
||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, MAX_RECENT_BLOCKHASHES};
|
||||
use std::iter;
|
||||
use std::sync::atomic::Ordering;
|
||||
@ -52,8 +56,10 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
||||
|
||||
let (verified_sender, verified_receiver) = channel();
|
||||
let (vote_sender, vote_receiver) = channel();
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
let dummy = SystemTransaction::new_move(
|
||||
let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank));
|
||||
let dummy = system_transaction::transfer(
|
||||
&mint_keypair,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
@ -67,17 +73,17 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
let from: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
let to: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
new.account_keys[0] = Pubkey::new(&from[0..32]);
|
||||
new.account_keys[1] = Pubkey::new(&to[0..32]);
|
||||
new.message.account_keys[0] = Pubkey::new(&from[0..32]);
|
||||
new.message.account_keys[1] = Pubkey::new(&to[0..32]);
|
||||
new.signatures = vec![Signature::new(&sig[0..64])];
|
||||
new
|
||||
})
|
||||
.collect();
|
||||
// fund all the accounts
|
||||
transactions.iter().for_each(|tx| {
|
||||
let fund = SystemTransaction::new_move(
|
||||
let fund = system_transaction::transfer(
|
||||
&mint_keypair,
|
||||
&tx.account_keys[0],
|
||||
&tx.message.account_keys[0],
|
||||
mint_total / txes as u64,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
@ -100,37 +106,52 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 192)
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let len = x.read().unwrap().packets.len();
|
||||
let len = x.packets.len();
|
||||
(x, iter::repeat(1).take(len).collect())
|
||||
})
|
||||
.collect();
|
||||
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(&bank);
|
||||
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
{
|
||||
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 cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||
let _banking_stage = BankingStage::new(
|
||||
&cluster_info,
|
||||
&poh_recorder,
|
||||
verified_receiver,
|
||||
vote_receiver,
|
||||
&leader_schedule_cache,
|
||||
);
|
||||
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 mut start = 0;
|
||||
bencher.iter(move || {
|
||||
// make sure the transactions are still valid
|
||||
bank.register_tick(&genesis_block.hash());
|
||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
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);
|
||||
}
|
||||
check_txs(&signal_receiver, txes / 2);
|
||||
bank.clear_signatures();
|
||||
start += half_len;
|
||||
start %= verified.len();
|
||||
});
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
poh_service.join().unwrap();
|
||||
|
||||
let half_len = verified.len() / 2;
|
||||
let mut start = 0;
|
||||
bencher.iter(move || {
|
||||
// make sure the transactions are still valid
|
||||
bank.register_tick(&genesis_block.hash());
|
||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
}
|
||||
check_txs(&signal_receiver, txes / 2);
|
||||
bank.clear_signatures();
|
||||
start += half_len;
|
||||
start %= verified.len();
|
||||
});
|
||||
drop(vote_sender);
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
poh_service.join().unwrap();
|
||||
}
|
||||
Blocktree::destroy(&ledger_path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
@ -144,8 +165,10 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
||||
|
||||
let (verified_sender, verified_receiver) = channel();
|
||||
let (vote_sender, vote_receiver) = channel();
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
let dummy = SystemTransaction::new_move(
|
||||
let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank));
|
||||
let dummy = system_transaction::transfer(
|
||||
&mint_keypair,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
@ -159,33 +182,33 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||
let from: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
let to: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||
new.account_keys[0] = Pubkey::new(&from[0..32]);
|
||||
new.account_keys[1] = Pubkey::new(&to[0..32]);
|
||||
let prog = new.instructions[0].clone();
|
||||
new.message.account_keys[0] = Pubkey::new(&from[0..32]);
|
||||
new.message.account_keys[1] = Pubkey::new(&to[0..32]);
|
||||
let prog = new.message.instructions[0].clone();
|
||||
for i in 1..progs {
|
||||
//generate programs that spend to random keys
|
||||
let to: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||
let to_key = Pubkey::new(&to[0..32]);
|
||||
new.account_keys.push(to_key);
|
||||
assert_eq!(new.account_keys.len(), i + 2);
|
||||
new.instructions.push(prog.clone());
|
||||
assert_eq!(new.instructions.len(), i + 1);
|
||||
new.instructions[i].accounts[1] = 1 + i as u8;
|
||||
new.message.account_keys.push(to_key);
|
||||
assert_eq!(new.message.account_keys.len(), i + 2);
|
||||
new.message.instructions.push(prog.clone());
|
||||
assert_eq!(new.message.instructions.len(), i + 1);
|
||||
new.message.instructions[i].accounts[1] = 1 + i as u8;
|
||||
assert_eq!(new.key(i, 1), Some(&to_key));
|
||||
assert_eq!(
|
||||
new.account_keys[new.instructions[i].accounts[1] as usize],
|
||||
new.message.account_keys[new.message.instructions[i].accounts[1] as usize],
|
||||
to_key
|
||||
);
|
||||
}
|
||||
assert_eq!(new.instructions.len(), progs);
|
||||
assert_eq!(new.message.instructions.len(), progs);
|
||||
new.signatures = vec![Signature::new(&sig[0..64])];
|
||||
new
|
||||
})
|
||||
.collect();
|
||||
transactions.iter().for_each(|tx| {
|
||||
let fund = SystemTransaction::new_move(
|
||||
let fund = system_transaction::transfer(
|
||||
&mint_keypair,
|
||||
&tx.account_keys[0],
|
||||
&tx.message.account_keys[0],
|
||||
mint_total / txes as u64,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
@ -207,35 +230,51 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 96)
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let len = x.read().unwrap().packets.len();
|
||||
let len = x.packets.len();
|
||||
(x, iter::repeat(1).take(len).collect())
|
||||
})
|
||||
.collect();
|
||||
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(&bank);
|
||||
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
|
||||
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 ledger_path = get_tmp_ledger_path!();
|
||||
{
|
||||
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 cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||
let _banking_stage = BankingStage::new(
|
||||
&cluster_info,
|
||||
&poh_recorder,
|
||||
verified_receiver,
|
||||
vote_receiver,
|
||||
&leader_schedule_cache,
|
||||
);
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
|
||||
let half_len = verified.len() / 2;
|
||||
let mut start = 0;
|
||||
bencher.iter(move || {
|
||||
// make sure the transactions are still valid
|
||||
bank.register_tick(&genesis_block.hash());
|
||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
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);
|
||||
}
|
||||
check_txs(&signal_receiver, txes / 2);
|
||||
bank.clear_signatures();
|
||||
start += half_len;
|
||||
start %= verified.len();
|
||||
});
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
poh_service.join().unwrap();
|
||||
|
||||
let half_len = verified.len() / 2;
|
||||
let mut start = 0;
|
||||
bencher.iter(move || {
|
||||
// make sure the transactions are still valid
|
||||
bank.register_tick(&genesis_block.hash());
|
||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
}
|
||||
check_txs(&signal_receiver, txes / 2);
|
||||
bank.clear_signatures();
|
||||
start += half_len;
|
||||
start %= verified.len();
|
||||
});
|
||||
drop(vote_sender);
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
poh_service.join().unwrap();
|
||||
}
|
||||
Blocktree::destroy(&ledger_path).unwrap();
|
||||
}
|
@ -5,7 +5,7 @@ extern crate test;
|
||||
use solana::entry::{next_entries, reconstruct_entries_from_blobs, EntrySlice};
|
||||
use solana_sdk::hash::{hash, Hash};
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction::SystemTransaction;
|
||||
use solana_sdk::system_transaction;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
@ -13,7 +13,7 @@ fn bench_block_to_blobs_to_block(bencher: &mut Bencher) {
|
||||
let zero = Hash::default();
|
||||
let one = hash(&zero.as_ref());
|
||||
let keypair = Keypair::new();
|
||||
let tx0 = SystemTransaction::new_move(&keypair, &keypair.pubkey(), 1, one, 0);
|
||||
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, one, 0);
|
||||
let transactions = vec![tx0; 10];
|
||||
let entries = next_entries(&zero, 1, transactions);
|
||||
|
@ -24,9 +24,8 @@ fn main() {
|
||||
|
||||
let chacha = !env::var("CARGO_FEATURE_CHACHA").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: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=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,9 +1,12 @@
|
||||
//! The `bank_forks` module implments BankForks a DAG of checkpointed Banks
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use solana_metrics::counter::Counter;
|
||||
use solana_runtime::bank::Bank;
|
||||
use std::collections::HashMap;
|
||||
use solana_sdk::timing;
|
||||
use std::ops::Index;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
pub struct BankForks {
|
||||
banks: HashMap<u64, Arc<Bank>>,
|
||||
@ -27,17 +30,40 @@ impl BankForks {
|
||||
working_bank,
|
||||
}
|
||||
}
|
||||
pub fn frozen_banks(&self) -> HashMap<u64, Arc<Bank>> {
|
||||
let mut frozen_banks: Vec<Arc<Bank>> = vec![];
|
||||
frozen_banks.extend(self.banks.values().filter(|v| v.is_frozen()).cloned());
|
||||
frozen_banks.extend(
|
||||
self.banks
|
||||
.iter()
|
||||
.flat_map(|(_, v)| v.parents())
|
||||
.filter(|v| v.is_frozen()),
|
||||
);
|
||||
frozen_banks.into_iter().map(|b| (b.slot(), b)).collect()
|
||||
|
||||
/// Create a map of bank slot id to the set of ancestors for the bank slot.
|
||||
pub fn ancestors(&self) -> HashMap<u64, HashSet<u64>> {
|
||||
let mut ancestors = HashMap::new();
|
||||
for bank in self.banks.values() {
|
||||
let set = bank.parents().into_iter().map(|b| b.slot()).collect();
|
||||
ancestors.insert(bank.slot(), set);
|
||||
}
|
||||
ancestors
|
||||
}
|
||||
|
||||
/// Create a map of bank slot id to the set of all of its descendants
|
||||
pub fn descendants(&self) -> HashMap<u64, HashSet<u64>> {
|
||||
let mut descendants = HashMap::new();
|
||||
for bank in self.banks.values() {
|
||||
let _ = descendants.entry(bank.slot()).or_insert(HashSet::new());
|
||||
for parent in bank.parents() {
|
||||
descendants
|
||||
.entry(parent.slot())
|
||||
.or_insert(HashSet::new())
|
||||
.insert(bank.slot());
|
||||
}
|
||||
}
|
||||
descendants
|
||||
}
|
||||
|
||||
pub fn frozen_banks(&self) -> HashMap<u64, Arc<Bank>> {
|
||||
self.banks
|
||||
.iter()
|
||||
.filter(|(_, b)| b.is_frozen())
|
||||
.map(|(k, b)| (*k, b.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn active_banks(&self) -> Vec<u64> {
|
||||
self.banks
|
||||
.iter()
|
||||
@ -45,6 +71,7 @@ impl BankForks {
|
||||
.map(|(k, _v)| *k)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get(&self, bank_slot: u64) -> Option<&Arc<Bank>> {
|
||||
self.banks.get(&bank_slot)
|
||||
}
|
||||
@ -61,30 +88,37 @@ impl BankForks {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use the bank's own ID instead of receiving a parameter?
|
||||
pub fn insert(&mut self, bank_slot: u64, bank: Bank) {
|
||||
let mut bank = Arc::new(bank);
|
||||
assert_eq!(bank_slot, bank.slot());
|
||||
let prev = self.banks.insert(bank_slot, bank.clone());
|
||||
pub fn insert(&mut self, bank: Bank) {
|
||||
let bank = Arc::new(bank);
|
||||
let prev = self.banks.insert(bank.slot(), bank.clone());
|
||||
assert!(prev.is_none());
|
||||
|
||||
self.working_bank = bank.clone();
|
||||
|
||||
// TODO: this really only needs to look at the first
|
||||
// parent if we're always calling insert()
|
||||
// when we construct a child bank
|
||||
while let Some(parent) = bank.parent() {
|
||||
if let Some(prev) = self.banks.remove(&parent.slot()) {
|
||||
assert!(Arc::ptr_eq(&prev, &parent));
|
||||
}
|
||||
bank = parent;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: really want to kill this...
|
||||
pub fn working_bank(&self) -> Arc<Bank> {
|
||||
self.working_bank.clone()
|
||||
}
|
||||
|
||||
pub fn set_root(&mut self, root: u64) {
|
||||
let set_root_start = Instant::now();
|
||||
let root_bank = self
|
||||
.banks
|
||||
.get(&root)
|
||||
.expect("root bank didn't exist in bank_forks");
|
||||
root_bank.squash();
|
||||
self.prune_non_root(root);
|
||||
inc_new_counter_info!(
|
||||
"bank-forks_set_root_ms",
|
||||
timing::duration_as_ms(&set_root_start.elapsed()) as usize
|
||||
);
|
||||
}
|
||||
|
||||
fn prune_non_root(&mut self, root: u64) {
|
||||
self.banks
|
||||
.retain(|slot, bank| *slot >= root || bank.is_in_subtree_of(root))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -101,18 +135,53 @@ mod tests {
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||
child_bank.register_tick(&Hash::default());
|
||||
bank_forks.insert(1, child_bank);
|
||||
bank_forks.insert(child_bank);
|
||||
assert_eq!(bank_forks[1u64].tick_height(), 1);
|
||||
assert_eq!(bank_forks.working_bank().tick_height(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bank_forks_descendants() {
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let bank0 = bank_forks[0].clone();
|
||||
let bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1);
|
||||
bank_forks.insert(bank);
|
||||
let bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 2);
|
||||
bank_forks.insert(bank);
|
||||
let descendants = bank_forks.descendants();
|
||||
let children: Vec<u64> = descendants[&0].iter().cloned().collect();
|
||||
assert_eq!(children, vec![1, 2]);
|
||||
assert!(descendants[&1].is_empty());
|
||||
assert!(descendants[&2].is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bank_forks_ancestors() {
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let bank0 = bank_forks[0].clone();
|
||||
let bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1);
|
||||
bank_forks.insert(bank);
|
||||
let bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 2);
|
||||
bank_forks.insert(bank);
|
||||
let ancestors = bank_forks.ancestors();
|
||||
assert!(ancestors[&0].is_empty());
|
||||
let parents: Vec<u64> = ancestors[&1].iter().cloned().collect();
|
||||
assert_eq!(parents, vec![0]);
|
||||
let parents: Vec<u64> = ancestors[&2].iter().cloned().collect();
|
||||
assert_eq!(parents, vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bank_forks_frozen_banks() {
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||
bank_forks.insert(1, child_bank);
|
||||
bank_forks.insert(child_bank);
|
||||
assert!(bank_forks.frozen_banks().get(&0).is_some());
|
||||
assert!(bank_forks.frozen_banks().get(&1).is_none());
|
||||
}
|
||||
@ -123,7 +192,7 @@ mod tests {
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||
bank_forks.insert(1, child_bank);
|
||||
bank_forks.insert(child_bank);
|
||||
assert_eq!(bank_forks.active_banks(), vec![1]);
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,9 @@
|
||||
|
||||
use crate::entry::Entry;
|
||||
use crate::result::Result;
|
||||
use bincode::serialize;
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use serde_json::json;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::cell::RefCell;
|
||||
@ -91,7 +93,13 @@ where
|
||||
leader_id: &Pubkey,
|
||||
entry: &Entry,
|
||||
) -> Result<()> {
|
||||
let json_entry = serde_json::to_string(&entry)?;
|
||||
let transactions: Vec<Vec<u8>> = serialize_transactions(entry);
|
||||
let stream_entry = json!({
|
||||
"num_hashes": entry.num_hashes,
|
||||
"hash": entry.hash,
|
||||
"transactions": transactions
|
||||
});
|
||||
let json_entry = serde_json::to_string(&stream_entry)?;
|
||||
let payload = format!(
|
||||
r#"{{"dt":"{}","t":"entry","s":{},"h":{},"l":"{:?}","entry":{}}}"#,
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true),
|
||||
@ -148,6 +156,14 @@ impl MockBlockstream {
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_transactions(entry: &Entry) -> Vec<Vec<u8>> {
|
||||
entry
|
||||
.transactions
|
||||
.iter()
|
||||
.map(|tx| serialize(&tx).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
@ -156,8 +172,30 @@ mod test {
|
||||
use serde_json::Value;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_transactions() {
|
||||
let entry = Entry::new(&Hash::default(), 1, vec![]);
|
||||
let empty_vec: Vec<Vec<u8>> = vec![];
|
||||
assert_eq!(serialize_transactions(&entry), empty_vec);
|
||||
|
||||
let keypair0 = Keypair::new();
|
||||
let keypair1 = Keypair::new();
|
||||
let tx0 =
|
||||
system_transaction::transfer(&keypair0, &keypair1.pubkey(), 1, Hash::default(), 0);
|
||||
let tx1 =
|
||||
system_transaction::transfer(&keypair1, &keypair0.pubkey(), 2, Hash::default(), 0);
|
||||
let serialized_tx0 = serialize(&tx0).unwrap();
|
||||
let serialized_tx1 = serialize(&tx1).unwrap();
|
||||
let entry = Entry::new(&Hash::default(), 1, vec![tx0, tx1]);
|
||||
assert_eq!(
|
||||
serialize_transactions(&entry),
|
||||
vec![serialized_tx0, serialized_tx1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blockstream() -> () {
|
||||
let blockstream = MockBlockstream::new("test_stream".to_string());
|
||||
@ -170,7 +208,7 @@ mod test {
|
||||
let tick_height_initial = 0;
|
||||
let tick_height_final = tick_height_initial + ticks_per_slot + 2;
|
||||
let mut curr_slot = 0;
|
||||
let leader_id = Keypair::new().pubkey();
|
||||
let leader_id = Pubkey::new_rand();
|
||||
|
||||
for tick_height in tick_height_initial..=tick_height_final {
|
||||
if tick_height == 5 {
|
||||
|
@ -109,25 +109,26 @@ mod test {
|
||||
use super::*;
|
||||
use crate::blocktree::create_new_tmp_ledger;
|
||||
use crate::entry::{create_ticks, Entry};
|
||||
use bincode::{deserialize, serialize};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde_json::Value;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction::SystemTransaction;
|
||||
use solana_sdk::system_transaction;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
#[test]
|
||||
fn test_blockstream_service_process_entries() {
|
||||
let ticks_per_slot = 5;
|
||||
let leader_id = Keypair::new().pubkey();
|
||||
let leader_id = Pubkey::new_rand();
|
||||
|
||||
// Set up genesis block and blocktree
|
||||
let (mut genesis_block, _mint_keypair) = GenesisBlock::new(1000);
|
||||
genesis_block.ticks_per_slot = ticks_per_slot;
|
||||
|
||||
let (ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block);
|
||||
let blocktree = Blocktree::open_config(&ledger_path, ticks_per_slot).unwrap();
|
||||
let blocktree = Blocktree::open(&ledger_path).unwrap();
|
||||
|
||||
// Set up blockstream
|
||||
let mut blockstream = Blockstream::new("test_stream".to_string());
|
||||
@ -140,7 +141,13 @@ mod test {
|
||||
|
||||
let keypair = Keypair::new();
|
||||
let mut blockhash = entries[3].hash;
|
||||
let tx = SystemTransaction::new_account(&keypair, &keypair.pubkey(), 1, Hash::default(), 0);
|
||||
let tx = system_transaction::create_user_account(
|
||||
&keypair,
|
||||
&keypair.pubkey(),
|
||||
1,
|
||||
Hash::default(),
|
||||
0,
|
||||
);
|
||||
let entry = Entry::new(&mut blockhash, 1, vec![tx]);
|
||||
blockhash = entry.hash;
|
||||
entries.push(entry);
|
||||
@ -150,7 +157,9 @@ mod test {
|
||||
let expected_entries = entries.clone();
|
||||
let expected_tick_heights = [5, 6, 7, 8, 8, 9];
|
||||
|
||||
blocktree.write_entries(1, 0, 0, &entries).unwrap();
|
||||
blocktree
|
||||
.write_entries(1, 0, 0, ticks_per_slot, &entries)
|
||||
.unwrap();
|
||||
|
||||
slot_full_sender.send((1, leader_id)).unwrap();
|
||||
BlockstreamService::process_entries(
|
||||
@ -180,13 +189,34 @@ mod test {
|
||||
assert_eq!(height, expected_tick_heights[i]);
|
||||
let entry_obj = json["entry"].clone();
|
||||
let tx = entry_obj["transactions"].as_array().unwrap();
|
||||
let entry: Entry;
|
||||
if tx.len() == 0 {
|
||||
// TODO: There is a bug in Transaction deserialize methods such that
|
||||
// `serde_json::from_str` does not work for populated Entries.
|
||||
// Remove this `if` when fixed.
|
||||
let entry: Entry = serde_json::from_value(entry_obj).unwrap();
|
||||
assert_eq!(entry, expected_entries[i]);
|
||||
entry = serde_json::from_value(entry_obj).unwrap();
|
||||
} else {
|
||||
let entry_json = entry_obj.as_object().unwrap();
|
||||
entry = Entry {
|
||||
num_hashes: entry_json.get("num_hashes").unwrap().as_u64().unwrap(),
|
||||
hash: serde_json::from_value(entry_json.get("hash").unwrap().clone()).unwrap(),
|
||||
transactions: entry_json
|
||||
.get("transactions")
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(j, tx)| {
|
||||
let tx_vec: Vec<u8> = serde_json::from_value(tx.clone()).unwrap();
|
||||
// Check explicitly that transaction matches bincode-serialized format
|
||||
assert_eq!(
|
||||
tx_vec,
|
||||
serialize(&expected_entries[i].transactions[j]).unwrap()
|
||||
);
|
||||
deserialize(&tx_vec).unwrap()
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
}
|
||||
assert_eq!(entry, expected_entries[i]);
|
||||
}
|
||||
for json in block_events {
|
||||
let slot = json["s"].as_u64().unwrap();
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
use crate::entry::Entry;
|
||||
use crate::result::{Error, Result};
|
||||
|
||||
use bincode::{deserialize, serialize};
|
||||
@ -7,24 +6,56 @@ use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::Path;
|
||||
|
||||
pub trait Database: Sized + Send + Sync {
|
||||
type Error: Into<Error>;
|
||||
type Key: Borrow<Self::KeyRef>;
|
||||
type KeyRef: ?Sized;
|
||||
type ColumnFamily;
|
||||
type Cursor: Cursor<Self>;
|
||||
type EntryIter: Iterator<Item = Entry>;
|
||||
pub mod columns {
|
||||
#[derive(Debug)]
|
||||
/// SlotMeta Column
|
||||
pub struct SlotMeta;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Orphans Column
|
||||
pub struct Orphans;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Erasure Column
|
||||
pub struct Coding;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Data Column
|
||||
pub struct Data;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// The erasure meta column
|
||||
pub struct ErasureMeta;
|
||||
}
|
||||
|
||||
pub trait Backend: Sized + Send + Sync {
|
||||
type Key: ?Sized + ToOwned<Owned = Self::OwnedKey>;
|
||||
type OwnedKey: Borrow<Self::Key>;
|
||||
type ColumnFamily: Clone;
|
||||
type Cursor: DbCursor<Self>;
|
||||
type Iter: Iterator<Item = (Box<Self::Key>, Box<[u8]>)>;
|
||||
type WriteBatch: IWriteBatch<Self>;
|
||||
type Error: Into<Error>;
|
||||
|
||||
fn cf_handle(&self, cf: &str) -> Option<Self::ColumnFamily>;
|
||||
fn open(path: &Path) -> Result<Self>;
|
||||
|
||||
fn get_cf(&self, cf: Self::ColumnFamily, key: &Self::KeyRef) -> Result<Option<Vec<u8>>>;
|
||||
fn columns(&self) -> Vec<&'static str>;
|
||||
|
||||
fn put_cf(&self, cf: Self::ColumnFamily, key: &Self::KeyRef, data: &[u8]) -> Result<()>;
|
||||
fn destroy(path: &Path) -> Result<()>;
|
||||
|
||||
fn delete_cf(&self, cf: Self::ColumnFamily, key: &Self::KeyRef) -> Result<()>;
|
||||
fn cf_handle(&self, cf: &str) -> Self::ColumnFamily;
|
||||
|
||||
fn get_cf(&self, cf: Self::ColumnFamily, key: &Self::Key) -> Result<Option<Vec<u8>>>;
|
||||
|
||||
fn put_cf(&self, cf: Self::ColumnFamily, key: &Self::Key, value: &[u8]) -> Result<()>;
|
||||
|
||||
fn delete_cf(&self, cf: Self::ColumnFamily, key: &Self::Key) -> Result<()>;
|
||||
|
||||
fn iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Iter>;
|
||||
|
||||
fn raw_iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Cursor>;
|
||||
|
||||
@ -33,163 +64,355 @@ pub trait Database: Sized + Send + Sync {
|
||||
fn batch(&self) -> Result<Self::WriteBatch>;
|
||||
}
|
||||
|
||||
pub trait Cursor<D: Database> {
|
||||
pub trait Column<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
const NAME: &'static str;
|
||||
type Index;
|
||||
|
||||
fn key(index: Self::Index) -> B::OwnedKey;
|
||||
fn index(key: &B::Key) -> Self::Index;
|
||||
}
|
||||
|
||||
pub trait DbCursor<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn valid(&self) -> bool;
|
||||
|
||||
fn seek(&mut self, key: &D::KeyRef);
|
||||
fn seek(&mut self, key: &B::Key);
|
||||
|
||||
fn seek_to_first(&mut self);
|
||||
|
||||
fn next(&mut self);
|
||||
|
||||
fn key(&self) -> Option<D::Key>;
|
||||
fn key(&self) -> Option<B::OwnedKey>;
|
||||
|
||||
fn value(&self) -> Option<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub trait IWriteBatch<D: Database> {
|
||||
fn put_cf(&mut self, cf: D::ColumnFamily, key: &D::KeyRef, data: &[u8]) -> Result<()>;
|
||||
pub trait IWriteBatch<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn put_cf(&mut self, cf: B::ColumnFamily, key: &B::Key, value: &[u8]) -> Result<()>;
|
||||
fn delete_cf(&mut self, cf: B::ColumnFamily, key: &B::Key) -> Result<()>;
|
||||
}
|
||||
|
||||
pub trait IDataCf<D: Database>: LedgerColumnFamilyRaw<D> {
|
||||
fn new(db: Arc<D>) -> Self;
|
||||
|
||||
fn get_by_slot_index(&self, slot: u64, index: u64) -> Result<Option<Vec<u8>>> {
|
||||
let key = Self::key(slot, index);
|
||||
self.get(key.borrow())
|
||||
}
|
||||
|
||||
fn delete_by_slot_index(&self, slot: u64, index: u64) -> Result<()> {
|
||||
let key = Self::key(slot, index);
|
||||
self.delete(&key.borrow())
|
||||
}
|
||||
|
||||
fn put_by_slot_index(&self, slot: u64, index: u64, serialized_value: &[u8]) -> Result<()> {
|
||||
let key = Self::key(slot, index);
|
||||
self.put(key.borrow(), serialized_value)
|
||||
}
|
||||
|
||||
fn key(slot: u64, index: u64) -> D::Key;
|
||||
|
||||
fn slot_from_key(key: &D::KeyRef) -> Result<u64>;
|
||||
|
||||
fn index_from_key(key: &D::KeyRef) -> Result<u64>;
|
||||
pub trait TypedColumn<B>: Column<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
type Type: Serialize + DeserializeOwned;
|
||||
}
|
||||
|
||||
pub trait IErasureCf<D: Database>: LedgerColumnFamilyRaw<D> {
|
||||
fn new(db: Arc<D>) -> Self;
|
||||
|
||||
fn delete_by_slot_index(&self, slot: u64, index: u64) -> Result<()> {
|
||||
let key = Self::key(slot, index);
|
||||
self.delete(key.borrow())
|
||||
}
|
||||
|
||||
fn get_by_slot_index(&self, slot: u64, index: u64) -> Result<Option<Vec<u8>>> {
|
||||
let key = Self::key(slot, index);
|
||||
self.get(key.borrow())
|
||||
}
|
||||
|
||||
fn put_by_slot_index(&self, slot: u64, index: u64, serialized_value: &[u8]) -> Result<()> {
|
||||
let key = Self::key(slot, index);
|
||||
self.put(key.borrow(), serialized_value)
|
||||
}
|
||||
|
||||
fn key(slot: u64, index: u64) -> D::Key;
|
||||
|
||||
fn slot_from_key(key: &D::KeyRef) -> Result<u64>;
|
||||
|
||||
fn index_from_key(key: &D::KeyRef) -> Result<u64>;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Database<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
backend: B,
|
||||
}
|
||||
|
||||
pub trait IMetaCf<D: Database>: LedgerColumnFamily<D, ValueType = super::SlotMeta> {
|
||||
fn new(db: Arc<D>) -> Self;
|
||||
|
||||
fn key(slot: u64) -> D::Key;
|
||||
|
||||
fn get_slot_meta(&self, slot: u64) -> Result<Option<super::SlotMeta>> {
|
||||
let key = Self::key(slot);
|
||||
self.get(key.borrow())
|
||||
}
|
||||
|
||||
fn put_slot_meta(&self, slot: u64, slot_meta: &super::SlotMeta) -> Result<()> {
|
||||
let key = Self::key(slot);
|
||||
self.put(key.borrow(), slot_meta)
|
||||
}
|
||||
|
||||
fn index_from_key(key: &D::KeyRef) -> Result<u64>;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: Column<B>,
|
||||
{
|
||||
db_cursor: B::Cursor,
|
||||
column: PhantomData<C>,
|
||||
backend: PhantomData<B>,
|
||||
}
|
||||
|
||||
pub trait LedgerColumnFamily<D: Database> {
|
||||
type ValueType: DeserializeOwned + Serialize;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LedgerColumn<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: Column<B>,
|
||||
{
|
||||
backend: PhantomData<B>,
|
||||
column: PhantomData<C>,
|
||||
}
|
||||
|
||||
fn get(&self, key: &D::KeyRef) -> Result<Option<Self::ValueType>> {
|
||||
let db = self.db();
|
||||
let data_bytes = db.get_cf(self.handle(), key)?;
|
||||
#[derive(Debug)]
|
||||
pub struct WriteBatch<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
write_batch: B::WriteBatch,
|
||||
backend: PhantomData<B>,
|
||||
map: HashMap<&'static str, B::ColumnFamily>,
|
||||
}
|
||||
|
||||
if let Some(raw) = data_bytes {
|
||||
let result: Self::ValueType = deserialize(&raw)?;
|
||||
Ok(Some(result))
|
||||
impl<B> Database<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let backend = B::open(path)?;
|
||||
|
||||
Ok(Database { backend })
|
||||
}
|
||||
|
||||
pub fn destroy(path: &Path) -> Result<()> {
|
||||
B::destroy(path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_bytes<C>(&self, key: C::Index) -> Result<Option<Vec<u8>>>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
self.backend
|
||||
.get_cf(self.cf_handle::<C>(), C::key(key).borrow())
|
||||
}
|
||||
|
||||
pub fn put_bytes<C>(&mut self, key: C::Index, data: &[u8]) -> Result<()>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
self.backend
|
||||
.put_cf(self.cf_handle::<C>(), C::key(key).borrow(), data)
|
||||
}
|
||||
|
||||
pub fn delete<C>(&mut self, key: C::Index) -> Result<()>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
self.backend
|
||||
.delete_cf(self.cf_handle::<C>(), C::key(key).borrow())
|
||||
}
|
||||
|
||||
pub fn get<C>(&self, key: C::Index) -> Result<Option<C::Type>>
|
||||
where
|
||||
C: TypedColumn<B>,
|
||||
{
|
||||
if let Some(serialized_value) = self
|
||||
.backend
|
||||
.get_cf(self.cf_handle::<C>(), C::key(key).borrow())?
|
||||
{
|
||||
let value = deserialize(&serialized_value)?;
|
||||
|
||||
Ok(Some(value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bytes(&self, key: &D::KeyRef) -> Result<Option<Vec<u8>>> {
|
||||
let db = self.db();
|
||||
let data_bytes = db.get_cf(self.handle(), key)?;
|
||||
Ok(data_bytes.map(|x| x.to_vec()))
|
||||
pub fn put<C>(&mut self, key: C::Index, value: &C::Type) -> Result<()>
|
||||
where
|
||||
C: TypedColumn<B>,
|
||||
{
|
||||
let serialized_value = serialize(value)?;
|
||||
|
||||
self.backend.put_cf(
|
||||
self.cf_handle::<C>(),
|
||||
C::key(key).borrow(),
|
||||
&serialized_value,
|
||||
)
|
||||
}
|
||||
|
||||
fn put_bytes(&self, key: &D::KeyRef, serialized_value: &[u8]) -> Result<()> {
|
||||
let db = self.db();
|
||||
db.put_cf(self.handle(), key, &serialized_value)?;
|
||||
Ok(())
|
||||
pub fn cursor<C>(&self) -> Result<Cursor<B, C>>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
let db_cursor = self.backend.raw_iterator_cf(self.cf_handle::<C>())?;
|
||||
|
||||
Ok(Cursor {
|
||||
db_cursor,
|
||||
column: PhantomData,
|
||||
backend: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
fn put(&self, key: &D::KeyRef, value: &Self::ValueType) -> Result<()> {
|
||||
let db = self.db();
|
||||
let serialized = serialize(value)?;
|
||||
db.put_cf(self.handle(), key, &serialized)?;
|
||||
Ok(())
|
||||
pub fn iter<C>(&self) -> Result<impl Iterator<Item = (C::Index, Vec<u8>)>>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
let iter = self
|
||||
.backend
|
||||
.iterator_cf(self.cf_handle::<C>())?
|
||||
.map(|(key, value)| (C::index(&key), value.into()));
|
||||
|
||||
Ok(iter)
|
||||
}
|
||||
|
||||
fn delete(&self, key: &D::KeyRef) -> Result<()> {
|
||||
let db = self.db();
|
||||
db.delete_cf(self.handle(), key)?;
|
||||
Ok(())
|
||||
pub fn batch(&mut self) -> Result<WriteBatch<B>> {
|
||||
let db_write_batch = self.backend.batch()?;
|
||||
let map = self
|
||||
.backend
|
||||
.columns()
|
||||
.into_iter()
|
||||
.map(|desc| (desc, self.backend.cf_handle(desc)))
|
||||
.collect();
|
||||
|
||||
Ok(WriteBatch {
|
||||
write_batch: db_write_batch,
|
||||
backend: PhantomData,
|
||||
map,
|
||||
})
|
||||
}
|
||||
|
||||
fn db(&self) -> &Arc<D>;
|
||||
pub fn write(&mut self, batch: WriteBatch<B>) -> Result<()> {
|
||||
self.backend.write(batch.write_batch)
|
||||
}
|
||||
|
||||
fn handle(&self) -> D::ColumnFamily;
|
||||
#[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: PhantomData,
|
||||
column: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LedgerColumnFamilyRaw<D: Database> {
|
||||
fn get(&self, key: &D::KeyRef) -> Result<Option<Vec<u8>>> {
|
||||
let db = self.db();
|
||||
let data_bytes = db.get_cf(self.handle(), key)?;
|
||||
Ok(data_bytes.map(|x| x.to_vec()))
|
||||
impl<B, C> Cursor<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: Column<B>,
|
||||
{
|
||||
pub fn valid(&self) -> bool {
|
||||
self.db_cursor.valid()
|
||||
}
|
||||
|
||||
fn put(&self, key: &D::KeyRef, serialized_value: &[u8]) -> Result<()> {
|
||||
let db = self.db();
|
||||
db.put_cf(self.handle(), &key, &serialized_value)?;
|
||||
Ok(())
|
||||
pub fn seek(&mut self, key: C::Index) {
|
||||
self.db_cursor.seek(C::key(key).borrow());
|
||||
}
|
||||
|
||||
fn delete(&self, key: &D::KeyRef) -> Result<()> {
|
||||
let db = self.db();
|
||||
db.delete_cf(self.handle(), &key)?;
|
||||
Ok(())
|
||||
pub fn seek_to_first(&mut self) {
|
||||
self.db_cursor.seek_to_first();
|
||||
}
|
||||
|
||||
fn raw_iterator(&self) -> D::Cursor {
|
||||
let db = self.db();
|
||||
db.raw_iterator_cf(self.handle())
|
||||
.expect("Expected to be able to open database iterator")
|
||||
pub fn next(&mut self) {
|
||||
self.db_cursor.next();
|
||||
}
|
||||
|
||||
fn handle(&self) -> D::ColumnFamily;
|
||||
pub fn key(&self) -> Option<C::Index> {
|
||||
if let Some(key) = self.db_cursor.key() {
|
||||
Some(C::index(key.borrow()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn db(&self) -> &Arc<D>;
|
||||
pub fn value_bytes(&self) -> Option<Vec<u8>> {
|
||||
self.db_cursor.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C> Cursor<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: TypedColumn<B>,
|
||||
{
|
||||
pub fn value(&self) -> Option<C::Type> {
|
||||
if let Some(bytes) = self.db_cursor.value() {
|
||||
let value = deserialize(&bytes).ok()?;
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C> LedgerColumn<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: Column<B>,
|
||||
{
|
||||
pub fn get_bytes(&self, db: &Database<B>, key: C::Index) -> Result<Option<Vec<u8>>> {
|
||||
db.backend.get_cf(self.handle(db), C::key(key).borrow())
|
||||
}
|
||||
|
||||
pub fn cursor(&self, db: &Database<B>) -> Result<Cursor<B, C>> {
|
||||
db.cursor()
|
||||
}
|
||||
|
||||
pub fn iter(&self, db: &Database<B>) -> Result<impl Iterator<Item = (C::Index, Vec<u8>)>> {
|
||||
db.iter::<C>()
|
||||
}
|
||||
|
||||
pub fn handle(&self, db: &Database<B>) -> B::ColumnFamily {
|
||||
db.cf_handle::<C>()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self, db: &Database<B>) -> Result<bool> {
|
||||
let mut cursor = self.cursor(db)?;
|
||||
cursor.seek_to_first();
|
||||
Ok(!cursor.valid())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C> LedgerColumn<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: Column<B>,
|
||||
{
|
||||
pub fn put_bytes(&self, db: &mut Database<B>, key: C::Index, value: &[u8]) -> Result<()> {
|
||||
db.backend
|
||||
.put_cf(self.handle(db), C::key(key).borrow(), value)
|
||||
}
|
||||
|
||||
pub fn delete(&self, db: &mut Database<B>, key: C::Index) -> Result<()> {
|
||||
db.backend.delete_cf(self.handle(db), C::key(key).borrow())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C> LedgerColumn<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: TypedColumn<B>,
|
||||
{
|
||||
pub fn get(&self, db: &Database<B>, key: C::Index) -> Result<Option<C::Type>> {
|
||||
db.get::<C>(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C> LedgerColumn<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: TypedColumn<B>,
|
||||
{
|
||||
pub fn put(&self, db: &mut Database<B>, key: C::Index, value: &C::Type) -> Result<()> {
|
||||
db.put::<C>(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> WriteBatch<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
pub fn put_bytes<C: Column<B>>(&mut self, key: C::Index, bytes: &[u8]) -> Result<()> {
|
||||
self.write_batch
|
||||
.put_cf(self.get_cf::<C>(), C::key(key).borrow(), bytes)
|
||||
}
|
||||
|
||||
pub fn delete<C: Column<B>>(&mut self, key: C::Index) -> Result<()> {
|
||||
self.write_batch
|
||||
.delete_cf(self.get_cf::<C>(), C::key(key).borrow())
|
||||
}
|
||||
|
||||
pub fn put<C: TypedColumn<B>>(&mut self, key: C::Index, value: &C::Type) -> Result<()> {
|
||||
let serialized_value = serialize(&value)?;
|
||||
self.write_batch
|
||||
.put_cf(self.get_cf::<C>(), C::key(key).borrow(), &serialized_value)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_cf<C: Column<B>>(&self) -> B::ColumnFamily {
|
||||
self.map[C::NAME].clone()
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +1,42 @@
|
||||
use crate::entry::Entry;
|
||||
use crate::kvstore::{self, Key};
|
||||
use crate::packet::Blob;
|
||||
use crate::blocktree::db::columns as cf;
|
||||
use crate::blocktree::db::{Backend, Column, DbCursor, IWriteBatch, TypedColumn};
|
||||
use crate::blocktree::BlocktreeError;
|
||||
use crate::result::{Error, Result};
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use solana_kvstore::{self as kvstore, Key, KvStore};
|
||||
use std::path::Path;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::db::{
|
||||
Cursor, Database, IDataCf, IErasureCf, IMetaCf, IWriteBatch, LedgerColumnFamily,
|
||||
LedgerColumnFamilyRaw,
|
||||
};
|
||||
use super::{Blocktree, BlocktreeError};
|
||||
type ColumnFamily = u64;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Kvs(());
|
||||
pub struct Kvs(KvStore);
|
||||
|
||||
/// The metadata column family
|
||||
#[derive(Debug)]
|
||||
pub struct MetaCf {
|
||||
db: Arc<Kvs>,
|
||||
}
|
||||
/// Dummy struct for now
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Dummy;
|
||||
|
||||
/// The data column family
|
||||
#[derive(Debug)]
|
||||
pub struct DataCf {
|
||||
db: Arc<Kvs>,
|
||||
}
|
||||
|
||||
/// The erasure column family
|
||||
#[derive(Debug)]
|
||||
pub struct ErasureCf {
|
||||
db: Arc<Kvs>,
|
||||
}
|
||||
|
||||
/// Dummy struct to get things compiling
|
||||
/// TODO: all this goes away with Blocktree
|
||||
pub struct EntryIterator(i32);
|
||||
/// Dummy struct to get things compiling
|
||||
pub struct KvsCursor;
|
||||
/// Dummy struct to get things compiling
|
||||
pub struct ColumnFamily;
|
||||
/// Dummy struct to get things compiling
|
||||
pub struct KvsWriteBatch;
|
||||
|
||||
impl Blocktree {
|
||||
/// Opens a Ledger in directory, provides "infinite" window of blobs
|
||||
pub fn open(_ledger_path: &str) -> Result<Blocktree> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
pub fn read_ledger_blobs(&self) -> impl Iterator<Item = Blob> {
|
||||
unimplemented!();
|
||||
self.read_ledger().unwrap().map(|_| Blob::new(&[]))
|
||||
}
|
||||
|
||||
/// Return an iterator for all the entries in the given file.
|
||||
#[allow(unreachable_code)]
|
||||
pub fn read_ledger(&self) -> Result<impl Iterator<Item = Entry>> {
|
||||
Ok(EntryIterator(unimplemented!()))
|
||||
}
|
||||
|
||||
pub fn destroy(_ledger_path: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Database for Kvs {
|
||||
type Error = kvstore::Error;
|
||||
impl Backend for Kvs {
|
||||
type Key = Key;
|
||||
type KeyRef = Key;
|
||||
type OwnedKey = Key;
|
||||
type ColumnFamily = ColumnFamily;
|
||||
type Cursor = KvsCursor;
|
||||
type EntryIter = EntryIterator;
|
||||
type WriteBatch = KvsWriteBatch;
|
||||
type Cursor = Dummy;
|
||||
type Iter = Dummy;
|
||||
type WriteBatch = Dummy;
|
||||
type Error = kvstore::Error;
|
||||
|
||||
fn cf_handle(&self, _cf: &str) -> Option<ColumnFamily> {
|
||||
fn open(_path: &Path) -> Result<Kvs> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn columns(&self) -> Vec<&'static str> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn destroy(_path: &Path) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn cf_handle(&self, _cf: &str) -> ColumnFamily {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@ -82,28 +44,123 @@ impl Database for Kvs {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn put_cf(&self, _cf: ColumnFamily, _key: &Key, _data: &[u8]) -> Result<()> {
|
||||
fn put_cf(&self, _cf: ColumnFamily, _key: &Key, _value: &[u8]) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn delete_cf(&self, _cf: Self::ColumnFamily, _key: &Key) -> Result<()> {
|
||||
fn delete_cf(&self, _cf: ColumnFamily, _key: &Key) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn raw_iterator_cf(&self, _cf: Self::ColumnFamily) -> Result<Self::Cursor> {
|
||||
fn iterator_cf(&self, _cf: ColumnFamily) -> Result<Dummy> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn write(&self, _batch: Self::WriteBatch) -> Result<()> {
|
||||
fn raw_iterator_cf(&self, _cf: ColumnFamily) -> Result<Dummy> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn batch(&self) -> Result<Self::WriteBatch> {
|
||||
fn batch(&self) -> Result<Dummy> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn write(&self, _batch: Dummy) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Cursor<Kvs> for KvsCursor {
|
||||
impl Column<Kvs> for cf::Coding {
|
||||
const NAME: &'static str = super::ERASURE_CF;
|
||||
type Index = (u64, u64);
|
||||
|
||||
fn key(index: (u64, u64)) -> Key {
|
||||
cf::Data::key(index)
|
||||
}
|
||||
|
||||
fn index(key: &Key) -> (u64, u64) {
|
||||
cf::Data::index(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column<Kvs> for cf::Data {
|
||||
const NAME: &'static str = super::DATA_CF;
|
||||
type Index = (u64, u64);
|
||||
|
||||
fn key((slot, index): (u64, u64)) -> Key {
|
||||
let mut key = Key::default();
|
||||
BigEndian::write_u64(&mut key.0[8..16], slot);
|
||||
BigEndian::write_u64(&mut key.0[16..], index);
|
||||
key
|
||||
}
|
||||
|
||||
fn index(key: &Key) -> (u64, u64) {
|
||||
let slot = BigEndian::read_u64(&key.0[8..16]);
|
||||
let index = BigEndian::read_u64(&key.0[16..]);
|
||||
(slot, index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column<Kvs> for cf::Orphans {
|
||||
const NAME: &'static str = super::ORPHANS_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::Orphans {
|
||||
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 TypedColumn<Kvs> for cf::SlotMeta {
|
||||
type Type = super::SlotMeta;
|
||||
}
|
||||
|
||||
impl Column<Kvs> for cf::ErasureMeta {
|
||||
const NAME: &'static str = super::ERASURE_META_CF;
|
||||
type Index = (u64, u64);
|
||||
|
||||
fn key((slot, set_index): (u64, u64)) -> Key {
|
||||
let mut key = Key::default();
|
||||
BigEndian::write_u64(&mut key.0[8..16], slot);
|
||||
BigEndian::write_u64(&mut key.0[16..], set_index);
|
||||
key
|
||||
}
|
||||
|
||||
fn index(key: &Key) -> (u64, u64) {
|
||||
let slot = BigEndian::read_u64(&key.0[8..16]);
|
||||
let set_index = BigEndian::read_u64(&key.0[16..]);
|
||||
(slot, set_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedColumn<Kvs> for cf::ErasureMeta {
|
||||
type Type = super::ErasureMeta;
|
||||
}
|
||||
|
||||
impl DbCursor<Kvs> for Dummy {
|
||||
fn valid(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
@ -129,124 +186,22 @@ impl Cursor<Kvs> for KvsCursor {
|
||||
}
|
||||
}
|
||||
|
||||
impl IWriteBatch<Kvs> for KvsWriteBatch {
|
||||
fn put_cf(&mut self, _cf: ColumnFamily, _key: &Key, _data: &[u8]) -> Result<()> {
|
||||
impl IWriteBatch<Kvs> for Dummy {
|
||||
fn put_cf(&mut self, _cf: ColumnFamily, _key: &Key, _value: &[u8]) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn delete_cf(&mut self, _cf: ColumnFamily, _key: &Key) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl IDataCf<Kvs> for DataCf {
|
||||
fn new(db: Arc<Kvs>) -> Self {
|
||||
DataCf { db }
|
||||
}
|
||||
impl Iterator for Dummy {
|
||||
type Item = (Box<Key>, Box<[u8]>);
|
||||
|
||||
fn get_by_slot_index(&self, _slot: u64, _index: u64) -> Result<Option<Vec<u8>>> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn delete_by_slot_index(&self, _slot: u64, _index: u64) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn put_by_slot_index(&self, _slot: u64, _index: u64, _serialized_value: &[u8]) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn key(_slot: u64, _index: u64) -> Key {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn slot_from_key(_key: &Key) -> Result<u64> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn index_from_key(_key: &Key) -> Result<u64> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl IErasureCf<Kvs> for ErasureCf {
|
||||
fn new(db: Arc<Kvs>) -> Self {
|
||||
ErasureCf { db }
|
||||
}
|
||||
|
||||
fn delete_by_slot_index(&self, _slot: u64, _index: u64) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_by_slot_index(&self, _slot: u64, _index: u64) -> Result<Option<Vec<u8>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn put_by_slot_index(&self, _slot: u64, _index: u64, _serialized_value: &[u8]) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn key(slot: u64, index: u64) -> Key {
|
||||
DataCf::key(slot, index)
|
||||
}
|
||||
|
||||
fn slot_from_key(key: &Key) -> Result<u64> {
|
||||
DataCf::slot_from_key(key)
|
||||
}
|
||||
|
||||
fn index_from_key(key: &Key) -> Result<u64> {
|
||||
DataCf::index_from_key(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl IMetaCf<Kvs> for MetaCf {
|
||||
fn new(db: Arc<Kvs>) -> Self {
|
||||
MetaCf { db }
|
||||
}
|
||||
|
||||
fn key(_slot: u64) -> Key {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_slot_meta(&self, _slot: u64) -> Result<Option<super::SlotMeta>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn put_slot_meta(&self, _slot: u64, _slot_meta: &super::SlotMeta) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn index_from_key(_key: &Key) -> Result<u64> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl LedgerColumnFamilyRaw<Kvs> for DataCf {
|
||||
fn db(&self) -> &Arc<Kvs> {
|
||||
&self.db
|
||||
}
|
||||
|
||||
fn handle(&self) -> ColumnFamily {
|
||||
self.db.cf_handle(super::DATA_CF).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl LedgerColumnFamilyRaw<Kvs> for ErasureCf {
|
||||
fn db(&self) -> &Arc<Kvs> {
|
||||
&self.db
|
||||
}
|
||||
|
||||
fn handle(&self) -> ColumnFamily {
|
||||
self.db.cf_handle(super::ERASURE_CF).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl LedgerColumnFamily<Kvs> for MetaCf {
|
||||
type ValueType = super::SlotMeta;
|
||||
|
||||
fn db(&self) -> &Arc<Kvs> {
|
||||
&self.db
|
||||
}
|
||||
|
||||
fn handle(&self) -> ColumnFamily {
|
||||
self.db.cf_handle(super::META_CF).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<kvstore::Error> for Error {
|
||||
@ -254,12 +209,3 @@ impl std::convert::From<kvstore::Error> for Error {
|
||||
Error::BlocktreeError(BlocktreeError::KvsDb(e))
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: all this goes away with Blocktree
|
||||
impl Iterator for EntryIterator {
|
||||
type Item = Entry;
|
||||
|
||||
fn next(&mut self) -> Option<Entry> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
317
core/src/blocktree/meta.rs
Normal file
317
core/src/blocktree/meta.rs
Normal file
@ -0,0 +1,317 @@
|
||||
use crate::erasure::{NUM_CODING, NUM_DATA};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
|
||||
// The Meta column family
|
||||
pub struct SlotMeta {
|
||||
// The number of slots above the root (the genesis block). The first
|
||||
// slot has slot 0.
|
||||
pub slot: u64,
|
||||
// The total number of consecutive blobs starting from index 0
|
||||
// we have received for this slot.
|
||||
pub consumed: u64,
|
||||
// The index *plus one* of the highest blob received for this slot. Useful
|
||||
// for checking if the slot has received any blobs yet, and to calculate the
|
||||
// range where there is one or more holes: `(consumed..received)`.
|
||||
pub received: u64,
|
||||
// The index of the blob that is flagged as the last blob for this slot.
|
||||
pub last_index: u64,
|
||||
// The slot height of the block this one derives from.
|
||||
pub parent_slot: u64,
|
||||
// The list of slot heights, each of which contains a block that derives
|
||||
// from this one.
|
||||
pub next_slots: Vec<u64>,
|
||||
// True if this slot is full (consumed == last_index + 1) and if every
|
||||
// slot that is a parent of this slot is also connected.
|
||||
pub is_connected: bool,
|
||||
// True if this slot is a root
|
||||
pub is_root: bool,
|
||||
}
|
||||
|
||||
impl SlotMeta {
|
||||
pub fn is_full(&self) -> bool {
|
||||
// last_index is std::u64::MAX when it has no information about how
|
||||
// many blobs will fill this slot.
|
||||
// Note: A full slot with zero blobs is not possible.
|
||||
if self.last_index == std::u64::MAX {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Should never happen
|
||||
if self.consumed > self.last_index + 1 {
|
||||
solana_metrics::submit(
|
||||
solana_metrics::influxdb::Point::new("blocktree_error")
|
||||
.add_field(
|
||||
"error",
|
||||
solana_metrics::influxdb::Value::String(format!(
|
||||
"Observed a slot meta with consumed: {} > meta.last_index + 1: {}",
|
||||
self.consumed,
|
||||
self.last_index + 1
|
||||
)),
|
||||
)
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
self.consumed == self.last_index + 1
|
||||
}
|
||||
|
||||
pub fn is_parent_set(&self) -> bool {
|
||||
self.parent_slot != std::u64::MAX
|
||||
}
|
||||
|
||||
pub(in crate::blocktree) fn new(slot: u64, parent_slot: u64) -> Self {
|
||||
SlotMeta {
|
||||
slot,
|
||||
consumed: 0,
|
||||
received: 0,
|
||||
parent_slot,
|
||||
next_slots: vec![],
|
||||
is_connected: slot == 0,
|
||||
is_root: false,
|
||||
last_index: std::u64::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
|
||||
/// Erasure coding information
|
||||
pub struct ErasureMeta {
|
||||
/// Which erasure set in the slot this is
|
||||
pub set_index: u64,
|
||||
/// Size of shards in this erasure set
|
||||
pub size: usize,
|
||||
/// Bitfield representing presence/absence of data blobs
|
||||
pub data: u64,
|
||||
/// Bitfield representing presence/absence of coding blobs
|
||||
pub coding: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ErasureMetaStatus {
|
||||
CanRecover,
|
||||
DataFull,
|
||||
StillNeed(usize),
|
||||
}
|
||||
|
||||
impl ErasureMeta {
|
||||
pub fn new(set_index: u64) -> ErasureMeta {
|
||||
ErasureMeta {
|
||||
set_index,
|
||||
size: 0,
|
||||
data: 0,
|
||||
coding: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> ErasureMetaStatus {
|
||||
let (data_missing, coding_missing) = (
|
||||
NUM_DATA - self.data.count_ones() as usize,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_coding_present(&self, index: u64) -> bool {
|
||||
if let Some(position) = self.data_index_in_set(index) {
|
||||
self.coding & (1 << position) != 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_size(&mut self, size: usize) {
|
||||
self.size = size;
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
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;
|
||||
} else {
|
||||
self.coding &= !(1 << position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_data_present(&self, index: u64) -> bool {
|
||||
if let Some(position) = self.data_index_in_set(index) {
|
||||
self.data & (1 << position) != 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_data_present(&mut self, index: u64, present: bool) {
|
||||
if let Some(position) = self.data_index_in_set(index) {
|
||||
if present {
|
||||
self.data |= 1 << position;
|
||||
} else {
|
||||
self.data &= !(1 << position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_index_for(index: u64) -> 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 {
|
||||
self.set_index * NUM_DATA as u64
|
||||
}
|
||||
|
||||
/// returns a tuple of (data_end, coding_end)
|
||||
pub fn end_indexes(&self) -> (u64, u64) {
|
||||
let start = self.start_index();
|
||||
(start + NUM_DATA as u64, start + NUM_CODING as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[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, 16);
|
||||
|
||||
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]
|
||||
fn test_meta_coding_present() {
|
||||
let mut e_meta = ErasureMeta::default();
|
||||
|
||||
for i in 0..NUM_CODING as u64 {
|
||||
e_meta.set_coding_present(i, true);
|
||||
assert_eq!(e_meta.is_coding_present(i), true);
|
||||
}
|
||||
for i in NUM_CODING as u64..NUM_DATA as u64 {
|
||||
assert_eq!(e_meta.is_coding_present(i), false);
|
||||
}
|
||||
|
||||
e_meta.set_index = ErasureMeta::set_index_for((NUM_DATA * 17) as u64);
|
||||
|
||||
for i in (NUM_DATA * 17) as u64..((NUM_DATA * 17) + NUM_CODING) as u64 {
|
||||
e_meta.set_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 {
|
||||
assert_eq!(e_meta.is_coding_present(i), false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_erasure_meta_status() {
|
||||
let mut e_meta = ErasureMeta::default();
|
||||
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::StillNeed(NUM_DATA));
|
||||
|
||||
e_meta.data = 0b1111_1111_1111_1111;
|
||||
e_meta.coding = 0x00;
|
||||
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::DataFull);
|
||||
|
||||
e_meta.coding = 0x0e;
|
||||
e_meta.size = 1;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::DataFull);
|
||||
|
||||
e_meta.data = 0b0111_1111_1111_1111;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
|
||||
e_meta.data = 0b0111_1111_1111_1110;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
|
||||
e_meta.data = 0b0111_1111_1011_1110;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
|
||||
e_meta.data = 0b0111_1011_1011_1110;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::StillNeed(1));
|
||||
|
||||
e_meta.data = 0b0111_1011_1011_1110;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::StillNeed(1));
|
||||
|
||||
e_meta.coding = 0b0000_1110;
|
||||
e_meta.data = 0b1111_1111_1111_1100;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
|
||||
e_meta.data = 0b1111_1111_1111_1000;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_meta_data_present() {
|
||||
let mut e_meta = ErasureMeta::default();
|
||||
|
||||
for i in 0..NUM_DATA as u64 {
|
||||
e_meta.set_data_present(i, true);
|
||||
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);
|
||||
|
||||
for i in (NUM_DATA * 23) as u64..(NUM_DATA * 24) as u64 {
|
||||
e_meta.set_data_present(i, true);
|
||||
assert_eq!(e_meta.is_data_present(i), true);
|
||||
}
|
||||
for i in (NUM_DATA * 22) as u64..(NUM_DATA * 23) as u64 {
|
||||
assert_eq!(e_meta.is_data_present(i), false);
|
||||
}
|
||||
}
|
@ -1,29 +1,17 @@
|
||||
use crate::entry::Entry;
|
||||
use crate::packet::{Blob, BLOB_HEADER_SIZE};
|
||||
use crate::blocktree::db::columns as cf;
|
||||
use crate::blocktree::db::{Backend, Column, DbCursor, IWriteBatch, TypedColumn};
|
||||
use crate::blocktree::BlocktreeError;
|
||||
use crate::result::{Error, Result};
|
||||
|
||||
use bincode::deserialize;
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
|
||||
use rocksdb::{
|
||||
self, ColumnFamily, ColumnFamilyDescriptor, DBRawIterator, IteratorMode, Options,
|
||||
self, ColumnFamily, ColumnFamilyDescriptor, DBIterator, DBRawIterator, IteratorMode, Options,
|
||||
WriteBatch as RWriteBatch, DB,
|
||||
};
|
||||
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::timing::DEFAULT_TICKS_PER_SLOT;
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::db::{
|
||||
Cursor, Database, IDataCf, IErasureCf, IMetaCf, IWriteBatch, LedgerColumnFamily,
|
||||
LedgerColumnFamilyRaw,
|
||||
};
|
||||
use super::{Blocktree, BlocktreeError};
|
||||
|
||||
// A good value for this is the number of cores on the machine
|
||||
const TOTAL_THREADS: i32 = 8;
|
||||
@ -32,194 +20,213 @@ const MAX_WRITE_BUFFER_SIZE: usize = 512 * 1024 * 1024;
|
||||
#[derive(Debug)]
|
||||
pub struct Rocks(rocksdb::DB);
|
||||
|
||||
/// The metadata column family
|
||||
#[derive(Debug)]
|
||||
pub struct MetaCf {
|
||||
db: Arc<Rocks>,
|
||||
}
|
||||
impl Backend for Rocks {
|
||||
type Key = [u8];
|
||||
type OwnedKey = Vec<u8>;
|
||||
type ColumnFamily = ColumnFamily;
|
||||
type Cursor = DBRawIterator;
|
||||
type Iter = DBIterator;
|
||||
type WriteBatch = RWriteBatch;
|
||||
type Error = rocksdb::Error;
|
||||
|
||||
/// The data column family
|
||||
#[derive(Debug)]
|
||||
pub struct DataCf {
|
||||
db: Arc<Rocks>,
|
||||
}
|
||||
fn open(path: &Path) -> Result<Rocks> {
|
||||
use crate::blocktree::db::columns::{Coding, Data, ErasureMeta, Orphans, SlotMeta};
|
||||
|
||||
/// The erasure column family
|
||||
#[derive(Debug)]
|
||||
pub struct ErasureCf {
|
||||
db: Arc<Rocks>,
|
||||
}
|
||||
|
||||
/// TODO: all this goes away with Blocktree
|
||||
pub struct EntryIterator {
|
||||
db_iterator: DBRawIterator,
|
||||
|
||||
// TODO: remove me when replay_stage is iterating by block (Blocktree)
|
||||
// this verification is duplicating that of replay_stage, which
|
||||
// can do this in parallel
|
||||
blockhash: Option<Hash>,
|
||||
// https://github.com/rust-rocksdb/rust-rocksdb/issues/234
|
||||
// rocksdb issue: the _blocktree member must be lower in the struct to prevent a crash
|
||||
// when the db_iterator member above is dropped.
|
||||
// _blocktree is unused, but dropping _blocktree results in a broken db_iterator
|
||||
// you have to hold the database open in order to iterate over it, and in order
|
||||
// for db_iterator to be able to run Drop
|
||||
// _blocktree: Blocktree,
|
||||
}
|
||||
|
||||
impl Blocktree {
|
||||
/// Opens a Ledger in directory, provides "infinite" window of blobs
|
||||
pub fn open(ledger_path: &str) -> Result<Blocktree> {
|
||||
fs::create_dir_all(&ledger_path)?;
|
||||
let ledger_path = Path::new(ledger_path).join(super::BLOCKTREE_DIRECTORY);
|
||||
fs::create_dir_all(&path)?;
|
||||
|
||||
// Use default database options
|
||||
let db_options = Blocktree::get_db_options();
|
||||
let db_options = get_db_options();
|
||||
|
||||
// Column family names
|
||||
let meta_cf_descriptor =
|
||||
ColumnFamilyDescriptor::new(super::META_CF, Blocktree::get_cf_options());
|
||||
let data_cf_descriptor =
|
||||
ColumnFamilyDescriptor::new(super::DATA_CF, Blocktree::get_cf_options());
|
||||
let erasure_cf_descriptor =
|
||||
ColumnFamilyDescriptor::new(super::ERASURE_CF, Blocktree::get_cf_options());
|
||||
let meta_cf_descriptor = ColumnFamilyDescriptor::new(SlotMeta::NAME, get_cf_options());
|
||||
let data_cf_descriptor = ColumnFamilyDescriptor::new(Data::NAME, get_cf_options());
|
||||
let erasure_cf_descriptor = ColumnFamilyDescriptor::new(Coding::NAME, get_cf_options());
|
||||
let erasure_meta_cf_descriptor =
|
||||
ColumnFamilyDescriptor::new(ErasureMeta::NAME, get_cf_options());
|
||||
let orphans_cf_descriptor = ColumnFamilyDescriptor::new(Orphans::NAME, get_cf_options());
|
||||
|
||||
let cfs = vec![
|
||||
meta_cf_descriptor,
|
||||
data_cf_descriptor,
|
||||
erasure_cf_descriptor,
|
||||
erasure_meta_cf_descriptor,
|
||||
orphans_cf_descriptor,
|
||||
];
|
||||
|
||||
// Open the database
|
||||
let db = Arc::new(Rocks(DB::open_cf_descriptors(
|
||||
&db_options,
|
||||
ledger_path,
|
||||
cfs,
|
||||
)?));
|
||||
let db = Rocks(DB::open_cf_descriptors(&db_options, path, cfs)?);
|
||||
|
||||
// Create the metadata column family
|
||||
let meta_cf = MetaCf::new(db.clone());
|
||||
|
||||
// Create the data column family
|
||||
let data_cf = DataCf::new(db.clone());
|
||||
|
||||
// Create the erasure column family
|
||||
let erasure_cf = ErasureCf::new(db.clone());
|
||||
|
||||
let ticks_per_slot = DEFAULT_TICKS_PER_SLOT;
|
||||
Ok(Blocktree {
|
||||
db,
|
||||
meta_cf,
|
||||
data_cf,
|
||||
erasure_cf,
|
||||
new_blobs_signals: vec![],
|
||||
ticks_per_slot,
|
||||
})
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
pub fn read_ledger_blobs(&self) -> impl Iterator<Item = Blob> {
|
||||
self.db
|
||||
.0
|
||||
.iterator_cf(self.data_cf.handle(), IteratorMode::Start)
|
||||
.unwrap()
|
||||
.map(|(_, blob_data)| Blob::new(&blob_data))
|
||||
fn columns(&self) -> Vec<&'static str> {
|
||||
use crate::blocktree::db::columns::{Coding, Data, ErasureMeta, Orphans, SlotMeta};
|
||||
|
||||
vec![
|
||||
Coding::NAME,
|
||||
ErasureMeta::NAME,
|
||||
Data::NAME,
|
||||
Orphans::NAME,
|
||||
SlotMeta::NAME,
|
||||
]
|
||||
}
|
||||
|
||||
/// Return an iterator for all the entries in the given file.
|
||||
pub fn read_ledger(&self) -> Result<impl Iterator<Item = Entry>> {
|
||||
let mut db_iterator = self.db.raw_iterator_cf(self.data_cf.handle())?;
|
||||
fn destroy(path: &Path) -> Result<()> {
|
||||
DB::destroy(&Options::default(), path)?;
|
||||
|
||||
db_iterator.seek_to_first();
|
||||
Ok(EntryIterator {
|
||||
db_iterator,
|
||||
blockhash: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn destroy(ledger_path: &str) -> Result<()> {
|
||||
// DB::destroy() fails if `ledger_path` doesn't exist
|
||||
fs::create_dir_all(&ledger_path)?;
|
||||
let ledger_path = Path::new(ledger_path).join(super::BLOCKTREE_DIRECTORY);
|
||||
DB::destroy(&Options::default(), &ledger_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cf_options() -> Options {
|
||||
let mut options = Options::default();
|
||||
options.set_max_write_buffer_number(32);
|
||||
options.set_write_buffer_size(MAX_WRITE_BUFFER_SIZE);
|
||||
options.set_max_bytes_for_level_base(MAX_WRITE_BUFFER_SIZE as u64);
|
||||
options
|
||||
}
|
||||
|
||||
fn get_db_options() -> Options {
|
||||
let mut options = Options::default();
|
||||
options.create_if_missing(true);
|
||||
options.create_missing_column_families(true);
|
||||
options.increase_parallelism(TOTAL_THREADS);
|
||||
options.set_max_background_flushes(4);
|
||||
options.set_max_background_compactions(4);
|
||||
options.set_max_write_buffer_number(32);
|
||||
options.set_write_buffer_size(MAX_WRITE_BUFFER_SIZE);
|
||||
options.set_max_bytes_for_level_base(MAX_WRITE_BUFFER_SIZE as u64);
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
impl Database for Rocks {
|
||||
type Error = rocksdb::Error;
|
||||
type Key = Vec<u8>;
|
||||
type KeyRef = [u8];
|
||||
type ColumnFamily = ColumnFamily;
|
||||
type Cursor = DBRawIterator;
|
||||
type EntryIter = EntryIterator;
|
||||
type WriteBatch = RWriteBatch;
|
||||
|
||||
fn cf_handle(&self, cf: &str) -> Option<ColumnFamily> {
|
||||
self.0.cf_handle(cf)
|
||||
fn cf_handle(&self, cf: &str) -> ColumnFamily {
|
||||
self.0
|
||||
.cf_handle(cf)
|
||||
.expect("should never get an unknown column")
|
||||
}
|
||||
|
||||
fn get_cf(&self, cf: ColumnFamily, key: &[u8]) -> Result<Option<Vec<u8>>> {
|
||||
let opt = self.0.get_cf(cf, key)?;
|
||||
Ok(opt.map(|dbvec| dbvec.to_vec()))
|
||||
let opt = self.0.get_cf(cf, key)?.map(|db_vec| db_vec.to_vec());
|
||||
Ok(opt)
|
||||
}
|
||||
|
||||
fn put_cf(&self, cf: ColumnFamily, key: &[u8], data: &[u8]) -> Result<()> {
|
||||
self.0.put_cf(cf, key, data)?;
|
||||
fn put_cf(&self, cf: ColumnFamily, key: &[u8], value: &[u8]) -> Result<()> {
|
||||
self.0.put_cf(cf, key, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_cf(&self, cf: Self::ColumnFamily, key: &[u8]) -> Result<()> {
|
||||
self.0.delete_cf(cf, key).map_err(From::from)
|
||||
fn delete_cf(&self, cf: ColumnFamily, key: &[u8]) -> Result<()> {
|
||||
self.0.delete_cf(cf, key)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raw_iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Cursor> {
|
||||
Ok(self.0.raw_iterator_cf(cf)?)
|
||||
fn iterator_cf(&self, cf: ColumnFamily) -> Result<DBIterator> {
|
||||
let raw_iter = self.0.iterator_cf(cf, IteratorMode::Start)?;
|
||||
|
||||
Ok(raw_iter)
|
||||
}
|
||||
|
||||
fn write(&self, batch: Self::WriteBatch) -> Result<()> {
|
||||
self.0.write(batch).map_err(From::from)
|
||||
fn raw_iterator_cf(&self, cf: ColumnFamily) -> Result<DBRawIterator> {
|
||||
let raw_iter = self.0.raw_iterator_cf(cf)?;
|
||||
|
||||
Ok(raw_iter)
|
||||
}
|
||||
|
||||
fn batch(&self) -> Result<Self::WriteBatch> {
|
||||
fn batch(&self) -> Result<RWriteBatch> {
|
||||
Ok(RWriteBatch::default())
|
||||
}
|
||||
|
||||
fn write(&self, batch: RWriteBatch) -> Result<()> {
|
||||
self.0.write(batch)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Cursor<Rocks> for DBRawIterator {
|
||||
impl Column<Rocks> for cf::Coding {
|
||||
const NAME: &'static str = super::ERASURE_CF;
|
||||
type Index = (u64, u64);
|
||||
|
||||
fn key(index: (u64, u64)) -> Vec<u8> {
|
||||
cf::Data::key(index)
|
||||
}
|
||||
|
||||
fn index(key: &[u8]) -> (u64, u64) {
|
||||
cf::Data::index(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column<Rocks> for cf::Data {
|
||||
const NAME: &'static str = super::DATA_CF;
|
||||
type Index = (u64, u64);
|
||||
|
||||
fn key((slot, index): (u64, u64)) -> Vec<u8> {
|
||||
let mut key = vec![0; 16];
|
||||
BigEndian::write_u64(&mut key[..8], slot);
|
||||
BigEndian::write_u64(&mut key[8..16], index);
|
||||
key
|
||||
}
|
||||
|
||||
fn index(key: &[u8]) -> (u64, u64) {
|
||||
let slot = BigEndian::read_u64(&key[..8]);
|
||||
let index = BigEndian::read_u64(&key[8..16]);
|
||||
(slot, index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column<Rocks> for cf::Orphans {
|
||||
const NAME: &'static str = super::ORPHANS_CF;
|
||||
type Index = u64;
|
||||
|
||||
fn key(slot: u64) -> Vec<u8> {
|
||||
let mut key = vec![0; 8];
|
||||
BigEndian::write_u64(&mut key[..], slot);
|
||||
key
|
||||
}
|
||||
|
||||
fn index(key: &[u8]) -> u64 {
|
||||
BigEndian::read_u64(&key[..8])
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedColumn<Rocks> for cf::Orphans {
|
||||
type Type = bool;
|
||||
}
|
||||
|
||||
impl Column<Rocks> for cf::SlotMeta {
|
||||
const NAME: &'static str = super::META_CF;
|
||||
type Index = u64;
|
||||
|
||||
fn key(slot: u64) -> Vec<u8> {
|
||||
let mut key = vec![0; 8];
|
||||
BigEndian::write_u64(&mut key[..], slot);
|
||||
key
|
||||
}
|
||||
|
||||
fn index(key: &[u8]) -> u64 {
|
||||
BigEndian::read_u64(&key[..8])
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedColumn<Rocks> for cf::SlotMeta {
|
||||
type Type = super::SlotMeta;
|
||||
}
|
||||
|
||||
impl Column<Rocks> for cf::ErasureMeta {
|
||||
const NAME: &'static str = super::ERASURE_META_CF;
|
||||
type Index = (u64, u64);
|
||||
|
||||
fn index(key: &[u8]) -> (u64, u64) {
|
||||
let slot = BigEndian::read_u64(&key[..8]);
|
||||
let set_index = BigEndian::read_u64(&key[8..]);
|
||||
|
||||
(slot, set_index)
|
||||
}
|
||||
|
||||
fn key((slot, set_index): (u64, u64)) -> Vec<u8> {
|
||||
let mut key = vec![0; 16];
|
||||
BigEndian::write_u64(&mut key[..8], slot);
|
||||
BigEndian::write_u64(&mut key[8..], set_index);
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedColumn<Rocks> for cf::ErasureMeta {
|
||||
type Type = super::ErasureMeta;
|
||||
}
|
||||
|
||||
impl DbCursor<Rocks> for DBRawIterator {
|
||||
fn valid(&self) -> bool {
|
||||
DBRawIterator::valid(self)
|
||||
}
|
||||
|
||||
fn seek(&mut self, key: &[u8]) {
|
||||
DBRawIterator::seek(self, key)
|
||||
DBRawIterator::seek(self, key);
|
||||
}
|
||||
|
||||
fn seek_to_first(&mut self) {
|
||||
DBRawIterator::seek_to_first(self)
|
||||
DBRawIterator::seek_to_first(self);
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
DBRawIterator::next(self)
|
||||
DBRawIterator::next(self);
|
||||
}
|
||||
|
||||
fn key(&self) -> Option<Vec<u8>> {
|
||||
@ -232,141 +239,14 @@ impl Cursor<Rocks> for DBRawIterator {
|
||||
}
|
||||
|
||||
impl IWriteBatch<Rocks> for RWriteBatch {
|
||||
fn put_cf(&mut self, cf: ColumnFamily, key: &[u8], data: &[u8]) -> Result<()> {
|
||||
RWriteBatch::put_cf(self, cf, key, data)?;
|
||||
fn put_cf(&mut self, cf: ColumnFamily, key: &[u8], value: &[u8]) -> Result<()> {
|
||||
RWriteBatch::put_cf(self, cf, key, value)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl IDataCf<Rocks> for DataCf {
|
||||
fn new(db: Arc<Rocks>) -> Self {
|
||||
DataCf { db }
|
||||
}
|
||||
|
||||
fn get_by_slot_index(&self, slot: u64, index: u64) -> Result<Option<Vec<u8>>> {
|
||||
let key = Self::key(slot, index);
|
||||
self.get(&key)
|
||||
}
|
||||
|
||||
fn delete_by_slot_index(&self, slot: u64, index: u64) -> Result<()> {
|
||||
let key = Self::key(slot, index);
|
||||
self.delete(&key)
|
||||
}
|
||||
|
||||
fn put_by_slot_index(&self, slot: u64, index: u64, serialized_value: &[u8]) -> Result<()> {
|
||||
let key = Self::key(slot, index);
|
||||
self.put(&key, serialized_value)
|
||||
}
|
||||
|
||||
fn key(slot: u64, index: u64) -> Vec<u8> {
|
||||
let mut key = vec![0u8; 16];
|
||||
BigEndian::write_u64(&mut key[0..8], slot);
|
||||
BigEndian::write_u64(&mut key[8..16], index);
|
||||
key
|
||||
}
|
||||
|
||||
fn slot_from_key(key: &[u8]) -> Result<u64> {
|
||||
let mut rdr = io::Cursor::new(&key[0..8]);
|
||||
let height = rdr.read_u64::<BigEndian>()?;
|
||||
Ok(height)
|
||||
}
|
||||
|
||||
fn index_from_key(key: &[u8]) -> Result<u64> {
|
||||
let mut rdr = io::Cursor::new(&key[8..16]);
|
||||
let index = rdr.read_u64::<BigEndian>()?;
|
||||
Ok(index)
|
||||
}
|
||||
}
|
||||
|
||||
impl IErasureCf<Rocks> for ErasureCf {
|
||||
fn new(db: Arc<Rocks>) -> Self {
|
||||
ErasureCf { db }
|
||||
}
|
||||
fn delete_by_slot_index(&self, slot: u64, index: u64) -> Result<()> {
|
||||
let key = Self::key(slot, index);
|
||||
self.delete(&key)
|
||||
}
|
||||
|
||||
fn get_by_slot_index(&self, slot: u64, index: u64) -> Result<Option<Vec<u8>>> {
|
||||
let key = Self::key(slot, index);
|
||||
self.get(&key)
|
||||
}
|
||||
|
||||
fn put_by_slot_index(&self, slot: u64, index: u64, serialized_value: &[u8]) -> Result<()> {
|
||||
let key = Self::key(slot, index);
|
||||
self.put(&key, serialized_value)
|
||||
}
|
||||
|
||||
fn key(slot: u64, index: u64) -> Vec<u8> {
|
||||
DataCf::key(slot, index)
|
||||
}
|
||||
|
||||
fn slot_from_key(key: &[u8]) -> Result<u64> {
|
||||
DataCf::slot_from_key(key)
|
||||
}
|
||||
|
||||
fn index_from_key(key: &[u8]) -> Result<u64> {
|
||||
DataCf::index_from_key(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl IMetaCf<Rocks> for MetaCf {
|
||||
fn new(db: Arc<Rocks>) -> Self {
|
||||
MetaCf { db }
|
||||
}
|
||||
|
||||
fn key(slot: u64) -> Vec<u8> {
|
||||
let mut key = vec![0u8; 8];
|
||||
BigEndian::write_u64(&mut key[0..8], slot);
|
||||
key
|
||||
}
|
||||
|
||||
fn get_slot_meta(&self, slot: u64) -> Result<Option<super::SlotMeta>> {
|
||||
let key = Self::key(slot);
|
||||
self.get(&key)
|
||||
}
|
||||
|
||||
fn put_slot_meta(&self, slot: u64, slot_meta: &super::SlotMeta) -> Result<()> {
|
||||
let key = Self::key(slot);
|
||||
self.put(&key, slot_meta)
|
||||
}
|
||||
|
||||
fn index_from_key(key: &[u8]) -> Result<u64> {
|
||||
let mut rdr = io::Cursor::new(&key[..]);
|
||||
let index = rdr.read_u64::<BigEndian>()?;
|
||||
Ok(index)
|
||||
}
|
||||
}
|
||||
|
||||
impl LedgerColumnFamilyRaw<Rocks> for DataCf {
|
||||
fn db(&self) -> &Arc<Rocks> {
|
||||
&self.db
|
||||
}
|
||||
|
||||
fn handle(&self) -> ColumnFamily {
|
||||
self.db.cf_handle(super::DATA_CF).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl LedgerColumnFamilyRaw<Rocks> for ErasureCf {
|
||||
fn db(&self) -> &Arc<Rocks> {
|
||||
&self.db
|
||||
}
|
||||
|
||||
fn handle(&self) -> ColumnFamily {
|
||||
self.db.cf_handle(super::ERASURE_CF).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl LedgerColumnFamily<Rocks> for MetaCf {
|
||||
type ValueType = super::SlotMeta;
|
||||
|
||||
fn db(&self) -> &Arc<Rocks> {
|
||||
&self.db
|
||||
}
|
||||
|
||||
fn handle(&self) -> ColumnFamily {
|
||||
self.db.cf_handle(super::META_CF).unwrap()
|
||||
fn delete_cf(&mut self, cf: ColumnFamily, key: &[u8]) -> Result<()> {
|
||||
RWriteBatch::delete_cf(self, cf, key)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,25 +256,23 @@ impl std::convert::From<rocksdb::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: all this goes away with Blocktree
|
||||
impl Iterator for EntryIterator {
|
||||
type Item = Entry;
|
||||
|
||||
fn next(&mut self) -> Option<Entry> {
|
||||
if self.db_iterator.valid() {
|
||||
if let Some(value) = self.db_iterator.value() {
|
||||
if let Ok(entry) = deserialize::<Entry>(&value[BLOB_HEADER_SIZE..]) {
|
||||
if let Some(blockhash) = self.blockhash {
|
||||
if !entry.verify(&blockhash) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
self.db_iterator.next();
|
||||
self.blockhash = Some(entry.hash);
|
||||
return Some(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
fn get_cf_options() -> Options {
|
||||
let mut options = Options::default();
|
||||
options.set_max_write_buffer_number(32);
|
||||
options.set_write_buffer_size(MAX_WRITE_BUFFER_SIZE);
|
||||
options.set_max_bytes_for_level_base(MAX_WRITE_BUFFER_SIZE as u64);
|
||||
options
|
||||
}
|
||||
|
||||
fn get_db_options() -> Options {
|
||||
let mut options = Options::default();
|
||||
options.create_if_missing(true);
|
||||
options.create_missing_column_families(true);
|
||||
options.increase_parallelism(TOTAL_THREADS);
|
||||
options.set_max_background_flushes(4);
|
||||
options.set_max_background_compactions(4);
|
||||
options.set_max_write_buffer_number(32);
|
||||
options.set_write_buffer_size(MAX_WRITE_BUFFER_SIZE);
|
||||
options.set_max_bytes_for_level_base(MAX_WRITE_BUFFER_SIZE as u64);
|
||||
options
|
||||
}
|
||||
|
@ -1,24 +1,18 @@
|
||||
use crate::bank_forks::BankForks;
|
||||
use crate::blocktree::Blocktree;
|
||||
use crate::entry::{Entry, EntrySlice};
|
||||
use crate::leader_schedule_utils;
|
||||
use crate::leader_schedule_cache::LeaderScheduleCache;
|
||||
use rayon::prelude::*;
|
||||
use solana_metrics::counter::Counter;
|
||||
use solana_runtime::bank::{Bank, BankError, Result};
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::locked_accounts_results::LockedAccountsResults;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::timing::duration_as_ms;
|
||||
use solana_sdk::timing::MAX_RECENT_BLOCKHASHES;
|
||||
use solana_sdk::transaction::Result;
|
||||
use std::result;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
pub fn process_entry(bank: &Bank, entry: &Entry) -> Result<()> {
|
||||
if !entry.is_tick() {
|
||||
first_err(&bank.process_transactions(&entry.transactions))?;
|
||||
} else {
|
||||
bank.register_tick(&entry.hash);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn first_err(results: &[Result<()>]) -> Result<()> {
|
||||
for r in results {
|
||||
@ -27,30 +21,48 @@ fn first_err(results: &[Result<()>]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn par_execute_entries(bank: &Bank, entries: &[(&Entry, Vec<Result<()>>)]) -> Result<()> {
|
||||
fn par_execute_entries(bank: &Bank, entries: &[(&Entry, LockedAccountsResults)]) -> Result<()> {
|
||||
inc_new_counter_info!("bank-par_execute_entries-count", entries.len());
|
||||
let results: Vec<Result<()>> = entries
|
||||
.into_par_iter()
|
||||
.map(|(e, lock_results)| {
|
||||
.map(|(e, locked_accounts)| {
|
||||
let results = bank.load_execute_and_commit_transactions(
|
||||
&e.transactions,
|
||||
lock_results.to_vec(),
|
||||
locked_accounts,
|
||||
MAX_RECENT_BLOCKHASHES,
|
||||
);
|
||||
bank.unlock_accounts(&e.transactions, &results);
|
||||
first_err(&results)
|
||||
let mut first_err = None;
|
||||
for r in results {
|
||||
if let Err(ref e) = r {
|
||||
if first_err.is_none() {
|
||||
first_err = Some(r.clone());
|
||||
}
|
||||
if !Bank::can_commit(&r) {
|
||||
warn!("Unexpected validator error: {:?}", e);
|
||||
solana_metrics::submit(
|
||||
solana_metrics::influxdb::Point::new("validator_process_entry_error")
|
||||
.add_field(
|
||||
"error",
|
||||
solana_metrics::influxdb::Value::String(format!("{:?}", e)),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
first_err.unwrap_or(Ok(()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
first_err(&results)
|
||||
}
|
||||
|
||||
/// process entries in parallel
|
||||
/// Process an ordered list of entries in parallel
|
||||
/// 1. In order lock accounts for each entry while the lock succeeds, up to a Tick entry
|
||||
/// 2. Process the locked group in parallel
|
||||
/// 3. Register the `Tick` if it's available
|
||||
/// 4. Update the leader scheduler, goto 1
|
||||
fn par_process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
|
||||
pub fn process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
|
||||
// accumulator for entries that can be processed in parallel
|
||||
let mut mt_group = vec![];
|
||||
for entry in entries {
|
||||
@ -65,11 +77,12 @@ fn par_process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
|
||||
let lock_results = bank.lock_accounts(&entry.transactions);
|
||||
// if any of the locks error out
|
||||
// execute the current group
|
||||
if first_err(&lock_results).is_err() {
|
||||
if first_err(lock_results.locked_accounts_results()).is_err() {
|
||||
par_execute_entries(bank, &mt_group)?;
|
||||
// Drop all the locks on accounts by clearing the LockedAccountsFinalizer's in the
|
||||
// mt_group
|
||||
mt_group = vec![];
|
||||
//reset the lock and push the entry
|
||||
bank.unlock_accounts(&entry.transactions, &lock_results);
|
||||
drop(lock_results);
|
||||
let lock_results = bank.lock_accounts(&entry.transactions);
|
||||
mt_group.push((entry, lock_results));
|
||||
} else {
|
||||
@ -81,25 +94,24 @@ fn par_process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process an ordered list of entries.
|
||||
pub fn process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
|
||||
par_process_entries(bank, entries)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct BankForksInfo {
|
||||
pub bank_slot: u64,
|
||||
pub entry_height: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BlocktreeProcessorError {
|
||||
LedgerVerificationFailed,
|
||||
}
|
||||
|
||||
pub fn process_blocktree(
|
||||
genesis_block: &GenesisBlock,
|
||||
blocktree: &Blocktree,
|
||||
account_paths: Option<String>,
|
||||
) -> Result<(BankForks, Vec<BankForksInfo>)> {
|
||||
) -> result::Result<(BankForks, Vec<BankForksInfo>, LeaderScheduleCache), BlocktreeProcessorError> {
|
||||
let now = Instant::now();
|
||||
info!("processing ledger...");
|
||||
|
||||
// Setup bank for slot 0
|
||||
let mut pending_slots = {
|
||||
let slot = 0;
|
||||
@ -112,22 +124,30 @@ pub fn process_blocktree(
|
||||
.meta(slot)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to load meta for slot {}: {:?}", slot, err);
|
||||
BankError::LedgerVerificationFailed
|
||||
BlocktreeProcessorError::LedgerVerificationFailed
|
||||
})?
|
||||
.unwrap();
|
||||
|
||||
vec![(slot, meta, bank, entry_height, last_entry_hash)]
|
||||
};
|
||||
|
||||
let leader_schedule_cache = LeaderScheduleCache::new(*pending_slots[0].2.epoch_schedule());
|
||||
|
||||
let mut fork_info = vec![];
|
||||
let mut last_status_report = Instant::now();
|
||||
while !pending_slots.is_empty() {
|
||||
let (slot, meta, bank, mut entry_height, mut last_entry_hash) =
|
||||
pending_slots.pop().unwrap();
|
||||
|
||||
if last_status_report.elapsed() > Duration::from_secs(2) {
|
||||
info!("processing ledger...block {}", slot);
|
||||
last_status_report = Instant::now();
|
||||
}
|
||||
|
||||
// Fetch all entries for this slot
|
||||
let mut entries = blocktree.get_slot_entries(slot, 0, None).map_err(|err| {
|
||||
warn!("Failed to load entries for slot {}: {:?}", slot, err);
|
||||
BankError::LedgerVerificationFailed
|
||||
BlocktreeProcessorError::LedgerVerificationFailed
|
||||
})?;
|
||||
|
||||
if slot == 0 {
|
||||
@ -136,16 +156,15 @@ pub fn process_blocktree(
|
||||
// processed by the bank, skip over it.
|
||||
if entries.is_empty() {
|
||||
warn!("entry0 not present");
|
||||
return Err(BankError::LedgerVerificationFailed);
|
||||
return Err(BlocktreeProcessorError::LedgerVerificationFailed);
|
||||
}
|
||||
let entry0 = &entries[0];
|
||||
let entry0 = entries.remove(0);
|
||||
if !(entry0.is_tick() && entry0.verify(&last_entry_hash)) {
|
||||
warn!("Ledger proof of history failed at entry0");
|
||||
return Err(BankError::LedgerVerificationFailed);
|
||||
return Err(BlocktreeProcessorError::LedgerVerificationFailed);
|
||||
}
|
||||
last_entry_hash = entry0.hash;
|
||||
entry_height += 1;
|
||||
entries = entries.drain(1..).collect();
|
||||
}
|
||||
|
||||
if !entries.is_empty() {
|
||||
@ -154,20 +173,23 @@ pub fn process_blocktree(
|
||||
"Ledger proof of history failed at slot: {}, entry: {}",
|
||||
slot, entry_height
|
||||
);
|
||||
return Err(BankError::LedgerVerificationFailed);
|
||||
return Err(BlocktreeProcessorError::LedgerVerificationFailed);
|
||||
}
|
||||
|
||||
process_entries(&bank, &entries).map_err(|err| {
|
||||
warn!("Failed to process entries for slot {}: {:?}", slot, err);
|
||||
BankError::LedgerVerificationFailed
|
||||
BlocktreeProcessorError::LedgerVerificationFailed
|
||||
})?;
|
||||
|
||||
last_entry_hash = entries.last().unwrap().hash;
|
||||
entry_height += entries.len() as u64;
|
||||
}
|
||||
|
||||
// TODO merge with locktower, voting, bank.vote_accounts()...
|
||||
bank.squash();
|
||||
bank.freeze(); // all banks handled by this routine are created from complete slots
|
||||
|
||||
if blocktree.is_root(slot) {
|
||||
bank.squash();
|
||||
}
|
||||
|
||||
if meta.next_slots.is_empty() {
|
||||
// Reached the end of this fork. Record the final entry height and last entry.hash
|
||||
@ -185,7 +207,7 @@ pub fn process_blocktree(
|
||||
.meta(next_slot)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to load meta for slot {}: {:?}", slot, err);
|
||||
BankError::LedgerVerificationFailed
|
||||
BlocktreeProcessorError::LedgerVerificationFailed
|
||||
})?
|
||||
.unwrap();
|
||||
|
||||
@ -194,7 +216,9 @@ pub fn process_blocktree(
|
||||
if next_meta.is_full() {
|
||||
let next_bank = Arc::new(Bank::new_from_parent(
|
||||
&bank,
|
||||
&leader_schedule_utils::slot_leader_at(next_slot, &bank).unwrap(),
|
||||
&leader_schedule_cache
|
||||
.slot_leader_at_else_compute(next_slot, &bank)
|
||||
.unwrap(),
|
||||
next_slot,
|
||||
));
|
||||
trace!("Add child bank for slot={}", next_slot);
|
||||
@ -223,12 +247,12 @@ pub fn process_blocktree(
|
||||
let (banks, bank_forks_info): (Vec<_>, Vec<_>) = fork_info.into_iter().unzip();
|
||||
let bank_forks = BankForks::new_from_banks(&banks);
|
||||
info!(
|
||||
"processed ledger in {}ms, forks={}...",
|
||||
"processing ledger...complete in {}ms, forks={}...",
|
||||
duration_as_ms(&now.elapsed()),
|
||||
bank_forks_info.len(),
|
||||
);
|
||||
|
||||
Ok((bank_forks, bank_forks_info))
|
||||
Ok((bank_forks, bank_forks_info, leader_schedule_cache))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -239,8 +263,11 @@ mod tests {
|
||||
use crate::entry::{create_ticks, next_entry, Entry};
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction::SystemTransaction;
|
||||
use solana_sdk::system_transaction;
|
||||
use solana_sdk::transaction::TransactionError;
|
||||
|
||||
fn fill_blocktree_slot_with_ticks(
|
||||
blocktree: &Blocktree,
|
||||
@ -281,8 +308,8 @@ mod tests {
|
||||
let (ledger_path, mut blockhash) = create_new_tmp_ledger!(&genesis_block);
|
||||
debug!("ledger_path: {:?}", ledger_path);
|
||||
|
||||
let blocktree = Blocktree::open_config(&ledger_path, ticks_per_slot)
|
||||
.expect("Expected to successfully open database ledger");
|
||||
let blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to successfully open database ledger");
|
||||
|
||||
// Write slot 1
|
||||
// slot 1, points at slot 0. Missing one tick
|
||||
@ -302,7 +329,7 @@ mod tests {
|
||||
// slot 2, points at slot 1
|
||||
fill_blocktree_slot_with_ticks(&blocktree, ticks_per_slot, 2, 1, blockhash);
|
||||
|
||||
let (mut _bank_forks, bank_forks_info) =
|
||||
let (mut _bank_forks, bank_forks_info, _) =
|
||||
process_blocktree(&genesis_block, &blocktree, None).unwrap();
|
||||
|
||||
assert_eq!(bank_forks_info.len(), 1);
|
||||
@ -332,7 +359,7 @@ mod tests {
|
||||
|
||||
slot 0
|
||||
|
|
||||
slot 1
|
||||
slot 1 <-- set_root(true)
|
||||
/ \
|
||||
slot 2 |
|
||||
/ |
|
||||
@ -341,8 +368,8 @@ mod tests {
|
||||
slot 4
|
||||
|
||||
*/
|
||||
let blocktree = Blocktree::open_config(&ledger_path, ticks_per_slot)
|
||||
.expect("Expected to successfully open database ledger");
|
||||
let blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to successfully open database ledger");
|
||||
|
||||
// Fork 1, ending at slot 3
|
||||
let last_slot1_entry_hash =
|
||||
@ -359,7 +386,10 @@ mod tests {
|
||||
info!("last_fork1_entry.hash: {:?}", last_fork1_entry_hash);
|
||||
info!("last_fork2_entry.hash: {:?}", last_fork2_entry_hash);
|
||||
|
||||
let (bank_forks, bank_forks_info) =
|
||||
blocktree.set_root(0).unwrap();
|
||||
blocktree.set_root(1).unwrap();
|
||||
|
||||
let (bank_forks, bank_forks_info, _) =
|
||||
process_blocktree(&genesis_block, &blocktree, None).unwrap();
|
||||
|
||||
assert_eq!(bank_forks_info.len(), 2); // There are two forks
|
||||
@ -370,6 +400,14 @@ mod tests {
|
||||
entry_height: ticks_per_slot * 4,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
&bank_forks[3]
|
||||
.parents()
|
||||
.iter()
|
||||
.map(|bank| bank.slot())
|
||||
.collect::<Vec<_>>(),
|
||||
&[2, 1]
|
||||
);
|
||||
assert_eq!(
|
||||
bank_forks_info[1],
|
||||
BankForksInfo {
|
||||
@ -377,9 +415,16 @@ mod tests {
|
||||
entry_height: ticks_per_slot * 3,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
&bank_forks[4]
|
||||
.parents()
|
||||
.iter()
|
||||
.map(|bank| bank.slot())
|
||||
.collect::<Vec<_>>(),
|
||||
&[1]
|
||||
);
|
||||
|
||||
// Ensure bank_forks holds the right banks, and that everything's
|
||||
// frozen
|
||||
// Ensure bank_forks holds the right banks
|
||||
for info in bank_forks_info {
|
||||
assert_eq!(bank_forks[info.bank_slot].slot(), info.bank_slot);
|
||||
assert!(bank_forks[info.bank_slot].is_frozen());
|
||||
@ -390,32 +435,32 @@ mod tests {
|
||||
fn test_first_err() {
|
||||
assert_eq!(first_err(&[Ok(())]), Ok(()));
|
||||
assert_eq!(
|
||||
first_err(&[Ok(()), Err(BankError::DuplicateSignature)]),
|
||||
Err(BankError::DuplicateSignature)
|
||||
first_err(&[Ok(()), Err(TransactionError::DuplicateSignature)]),
|
||||
Err(TransactionError::DuplicateSignature)
|
||||
);
|
||||
assert_eq!(
|
||||
first_err(&[
|
||||
Ok(()),
|
||||
Err(BankError::DuplicateSignature),
|
||||
Err(BankError::AccountInUse)
|
||||
Err(TransactionError::DuplicateSignature),
|
||||
Err(TransactionError::AccountInUse)
|
||||
]),
|
||||
Err(BankError::DuplicateSignature)
|
||||
Err(TransactionError::DuplicateSignature)
|
||||
);
|
||||
assert_eq!(
|
||||
first_err(&[
|
||||
Ok(()),
|
||||
Err(BankError::AccountInUse),
|
||||
Err(BankError::DuplicateSignature)
|
||||
Err(TransactionError::AccountInUse),
|
||||
Err(TransactionError::DuplicateSignature)
|
||||
]),
|
||||
Err(BankError::AccountInUse)
|
||||
Err(TransactionError::AccountInUse)
|
||||
);
|
||||
assert_eq!(
|
||||
first_err(&[
|
||||
Err(BankError::AccountInUse),
|
||||
Err(TransactionError::AccountInUse),
|
||||
Ok(()),
|
||||
Err(BankError::DuplicateSignature)
|
||||
Err(TransactionError::DuplicateSignature)
|
||||
]),
|
||||
Err(BankError::AccountInUse)
|
||||
Err(TransactionError::AccountInUse)
|
||||
);
|
||||
}
|
||||
|
||||
@ -427,7 +472,7 @@ mod tests {
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair = Keypair::new();
|
||||
let slot_entries = create_ticks(genesis_block.ticks_per_slot - 1, genesis_block.hash());
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair.pubkey(),
|
||||
1,
|
||||
@ -438,18 +483,18 @@ mod tests {
|
||||
// First, ensure the TX is rejected because of the unregistered last ID
|
||||
assert_eq!(
|
||||
bank.process_transaction(&tx),
|
||||
Err(BankError::BlockhashNotFound)
|
||||
Err(TransactionError::BlockhashNotFound)
|
||||
);
|
||||
|
||||
// Now ensure the TX is accepted despite pointing to the ID of an empty entry.
|
||||
par_process_entries(&bank, &slot_entries).unwrap();
|
||||
process_entries(&bank, &slot_entries).unwrap();
|
||||
assert_eq!(bank.process_transaction(&tx), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_ledger_simple() {
|
||||
solana_logger::setup();
|
||||
let leader_pubkey = Keypair::new().pubkey();
|
||||
let leader_pubkey = Pubkey::new_rand();
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new_with_leader(100, &leader_pubkey, 50);
|
||||
let (ledger_path, mut last_entry_hash) = create_new_tmp_ledger!(&genesis_block);
|
||||
debug!("ledger_path: {:?}", ledger_path);
|
||||
@ -459,16 +504,27 @@ mod tests {
|
||||
for _ in 0..3 {
|
||||
// Transfer one token from the mint to a random account
|
||||
let keypair = Keypair::new();
|
||||
let tx =
|
||||
SystemTransaction::new_account(&mint_keypair, &keypair.pubkey(), 1, blockhash, 0);
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair.pubkey(),
|
||||
1,
|
||||
blockhash,
|
||||
0,
|
||||
);
|
||||
let entry = Entry::new(&last_entry_hash, 1, vec![tx]);
|
||||
last_entry_hash = entry.hash;
|
||||
entries.push(entry);
|
||||
|
||||
// Add a second Transaction that will produce a
|
||||
// ProgramError<0, ResultWithNegativeLamports> error when processed
|
||||
// InstructionError<0, ResultWithNegativeLamports> error when processed
|
||||
let keypair2 = Keypair::new();
|
||||
let tx = SystemTransaction::new_account(&keypair, &keypair2.pubkey(), 42, blockhash, 0);
|
||||
let tx = system_transaction::create_user_account(
|
||||
&keypair,
|
||||
&keypair2.pubkey(),
|
||||
42,
|
||||
blockhash,
|
||||
0,
|
||||
);
|
||||
let entry = Entry::new(&last_entry_hash, 1, vec![tx]);
|
||||
last_entry_hash = entry.hash;
|
||||
entries.push(entry);
|
||||
@ -479,9 +535,11 @@ mod tests {
|
||||
|
||||
let blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to successfully open database ledger");
|
||||
blocktree.write_entries(1, 0, 0, &entries).unwrap();
|
||||
blocktree
|
||||
.write_entries(1, 0, 0, genesis_block.ticks_per_slot, &entries)
|
||||
.unwrap();
|
||||
let entry_height = genesis_block.ticks_per_slot + entries.len() as u64;
|
||||
let (bank_forks, bank_forks_info) =
|
||||
let (bank_forks, bank_forks_info, _) =
|
||||
process_blocktree(&genesis_block, &blocktree, None).unwrap();
|
||||
|
||||
assert_eq!(bank_forks_info.len(), 1);
|
||||
@ -506,7 +564,7 @@ mod tests {
|
||||
let (ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block);
|
||||
|
||||
let blocktree = Blocktree::open(&ledger_path).unwrap();
|
||||
let (bank_forks, bank_forks_info) =
|
||||
let (bank_forks, bank_forks_info, _) =
|
||||
process_blocktree(&genesis_block, &blocktree, None).unwrap();
|
||||
|
||||
assert_eq!(bank_forks_info.len(), 1);
|
||||
@ -522,19 +580,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_par_process_entries_tick() {
|
||||
fn test_process_entries_tick() {
|
||||
let (genesis_block, _mint_keypair) = GenesisBlock::new(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
|
||||
// ensure bank can process a tick
|
||||
assert_eq!(bank.tick_height(), 0);
|
||||
let tick = next_entry(&genesis_block.hash(), 1, vec![]);
|
||||
assert_eq!(par_process_entries(&bank, &[tick.clone()]), Ok(()));
|
||||
assert_eq!(process_entries(&bank, &[tick.clone()]), Ok(()));
|
||||
assert_eq!(bank.tick_height(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_par_process_entries_2_entries_collision() {
|
||||
fn test_process_entries_2_entries_collision() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
@ -543,7 +601,7 @@ mod tests {
|
||||
let blockhash = bank.last_blockhash();
|
||||
|
||||
// ensure bank can process 2 entries that have a common account and no tick is registered
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair1.pubkey(),
|
||||
2,
|
||||
@ -551,7 +609,7 @@ mod tests {
|
||||
0,
|
||||
);
|
||||
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair2.pubkey(),
|
||||
2,
|
||||
@ -559,14 +617,14 @@ mod tests {
|
||||
0,
|
||||
);
|
||||
let entry_2 = next_entry(&entry_1.hash, 1, vec![tx]);
|
||||
assert_eq!(par_process_entries(&bank, &[entry_1, entry_2]), Ok(()));
|
||||
assert_eq!(process_entries(&bank, &[entry_1, entry_2]), Ok(()));
|
||||
assert_eq!(bank.get_balance(&keypair1.pubkey()), 2);
|
||||
assert_eq!(bank.get_balance(&keypair2.pubkey()), 2);
|
||||
assert_eq!(bank.last_blockhash(), blockhash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_par_process_entries_2_txes_collision() {
|
||||
fn test_process_entries_2_txes_collision() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
@ -574,20 +632,14 @@ mod tests {
|
||||
let keypair3 = Keypair::new();
|
||||
|
||||
// fund: put 4 in each of 1 and 2
|
||||
assert_matches!(
|
||||
bank.transfer(4, &mint_keypair, &keypair1.pubkey(), bank.last_blockhash()),
|
||||
Ok(_)
|
||||
);
|
||||
assert_matches!(
|
||||
bank.transfer(4, &mint_keypair, &keypair2.pubkey(), bank.last_blockhash()),
|
||||
Ok(_)
|
||||
);
|
||||
assert_matches!(bank.transfer(4, &mint_keypair, &keypair1.pubkey()), Ok(_));
|
||||
assert_matches!(bank.transfer(4, &mint_keypair, &keypair2.pubkey()), Ok(_));
|
||||
|
||||
// construct an Entry whose 2nd transaction would cause a lock conflict with previous entry
|
||||
let entry_1_to_mint = next_entry(
|
||||
&bank.last_blockhash(),
|
||||
1,
|
||||
vec![SystemTransaction::new_account(
|
||||
vec![system_transaction::create_user_account(
|
||||
&keypair1,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
@ -600,14 +652,14 @@ mod tests {
|
||||
&entry_1_to_mint.hash,
|
||||
1,
|
||||
vec![
|
||||
SystemTransaction::new_account(
|
||||
system_transaction::create_user_account(
|
||||
&keypair2,
|
||||
&keypair3.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
), // should be fine
|
||||
SystemTransaction::new_account(
|
||||
system_transaction::create_user_account(
|
||||
&keypair1,
|
||||
&mint_keypair.pubkey(),
|
||||
2,
|
||||
@ -618,7 +670,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
par_process_entries(&bank, &[entry_1_to_mint, entry_2_to_3_mint_to_1]),
|
||||
process_entries(&bank, &[entry_1_to_mint, entry_2_to_3_mint_to_1]),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
@ -628,7 +680,89 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_par_process_entries_2_entries_par() {
|
||||
fn test_process_entries_2_txes_collision_and_error() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
let keypair3 = Keypair::new();
|
||||
let keypair4 = Keypair::new();
|
||||
|
||||
// fund: put 4 in each of 1 and 2
|
||||
assert_matches!(bank.transfer(4, &mint_keypair, &keypair1.pubkey()), Ok(_));
|
||||
assert_matches!(bank.transfer(4, &mint_keypair, &keypair2.pubkey()), Ok(_));
|
||||
assert_matches!(bank.transfer(4, &mint_keypair, &keypair4.pubkey()), Ok(_));
|
||||
|
||||
// construct an Entry whose 2nd transaction would cause a lock conflict with previous entry
|
||||
let entry_1_to_mint = next_entry(
|
||||
&bank.last_blockhash(),
|
||||
1,
|
||||
vec![
|
||||
system_transaction::create_user_account(
|
||||
&keypair1,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
),
|
||||
system_transaction::transfer(
|
||||
&keypair4,
|
||||
&keypair4.pubkey(),
|
||||
1,
|
||||
Hash::default(), // Should cause a transaction failure with BlockhashNotFound
|
||||
0,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let entry_2_to_3_mint_to_1 = next_entry(
|
||||
&entry_1_to_mint.hash,
|
||||
1,
|
||||
vec![
|
||||
system_transaction::create_user_account(
|
||||
&keypair2,
|
||||
&keypair3.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
), // should be fine
|
||||
system_transaction::create_user_account(
|
||||
&keypair1,
|
||||
&mint_keypair.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
), // will collide
|
||||
],
|
||||
);
|
||||
|
||||
assert!(process_entries(
|
||||
&bank,
|
||||
&[entry_1_to_mint.clone(), entry_2_to_3_mint_to_1.clone()]
|
||||
)
|
||||
.is_err());
|
||||
|
||||
// First transaction in first entry succeeded, so keypair1 lost 1 lamport
|
||||
assert_eq!(bank.get_balance(&keypair1.pubkey()), 3);
|
||||
assert_eq!(bank.get_balance(&keypair2.pubkey()), 4);
|
||||
|
||||
// Check all accounts are unlocked
|
||||
let txs1 = &entry_1_to_mint.transactions[..];
|
||||
let txs2 = &entry_2_to_3_mint_to_1.transactions[..];
|
||||
let locked_accounts1 = bank.lock_accounts(txs1);
|
||||
for result in locked_accounts1.locked_accounts_results() {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
// txs1 and txs2 have accounts that conflict, so we must drop txs1 first
|
||||
drop(locked_accounts1);
|
||||
let locked_accounts2 = bank.lock_accounts(txs2);
|
||||
for result in locked_accounts2.locked_accounts_results() {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_entries_2_entries_par() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
@ -637,7 +771,7 @@ mod tests {
|
||||
let keypair4 = Keypair::new();
|
||||
|
||||
//load accounts
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair1.pubkey(),
|
||||
1,
|
||||
@ -645,7 +779,7 @@ mod tests {
|
||||
0,
|
||||
);
|
||||
assert_eq!(bank.process_transaction(&tx), Ok(()));
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair2.pubkey(),
|
||||
1,
|
||||
@ -656,7 +790,7 @@ mod tests {
|
||||
|
||||
// ensure bank can process 2 entries that do not have a common account and no tick is registered
|
||||
let blockhash = bank.last_blockhash();
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&keypair1,
|
||||
&keypair3.pubkey(),
|
||||
1,
|
||||
@ -664,7 +798,7 @@ mod tests {
|
||||
0,
|
||||
);
|
||||
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&keypair2,
|
||||
&keypair4.pubkey(),
|
||||
1,
|
||||
@ -672,14 +806,14 @@ mod tests {
|
||||
0,
|
||||
);
|
||||
let entry_2 = next_entry(&entry_1.hash, 1, vec![tx]);
|
||||
assert_eq!(par_process_entries(&bank, &[entry_1, entry_2]), Ok(()));
|
||||
assert_eq!(process_entries(&bank, &[entry_1, entry_2]), Ok(()));
|
||||
assert_eq!(bank.get_balance(&keypair3.pubkey()), 1);
|
||||
assert_eq!(bank.get_balance(&keypair4.pubkey()), 1);
|
||||
assert_eq!(bank.last_blockhash(), blockhash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_par_process_entries_2_entries_tick() {
|
||||
fn test_process_entries_2_entries_tick() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
@ -688,7 +822,7 @@ mod tests {
|
||||
let keypair4 = Keypair::new();
|
||||
|
||||
//load accounts
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair1.pubkey(),
|
||||
1,
|
||||
@ -696,7 +830,7 @@ mod tests {
|
||||
0,
|
||||
);
|
||||
assert_eq!(bank.process_transaction(&tx), Ok(()));
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair2.pubkey(),
|
||||
1,
|
||||
@ -711,10 +845,11 @@ mod tests {
|
||||
}
|
||||
|
||||
// ensure bank can process 2 entries that do not have a common account and tick is registered
|
||||
let tx = SystemTransaction::new_account(&keypair2, &keypair3.pubkey(), 1, blockhash, 0);
|
||||
let tx =
|
||||
system_transaction::create_user_account(&keypair2, &keypair3.pubkey(), 1, blockhash, 0);
|
||||
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
|
||||
let tick = next_entry(&entry_1.hash, 1, vec![]);
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&keypair1,
|
||||
&keypair4.pubkey(),
|
||||
1,
|
||||
@ -723,14 +858,14 @@ mod tests {
|
||||
);
|
||||
let entry_2 = next_entry(&tick.hash, 1, vec![tx]);
|
||||
assert_eq!(
|
||||
par_process_entries(&bank, &[entry_1.clone(), tick.clone(), entry_2.clone()]),
|
||||
process_entries(&bank, &[entry_1.clone(), tick.clone(), entry_2.clone()]),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(bank.get_balance(&keypair3.pubkey()), 1);
|
||||
assert_eq!(bank.get_balance(&keypair4.pubkey()), 1);
|
||||
|
||||
// ensure that an error is returned for an empty account (keypair2)
|
||||
let tx = SystemTransaction::new_account(
|
||||
let tx = system_transaction::create_user_account(
|
||||
&keypair2,
|
||||
&keypair3.pubkey(),
|
||||
1,
|
||||
@ -739,8 +874,91 @@ mod tests {
|
||||
);
|
||||
let entry_3 = next_entry(&entry_2.hash, 1, vec![tx]);
|
||||
assert_eq!(
|
||||
par_process_entries(&bank, &[entry_3]),
|
||||
Err(BankError::AccountNotFound)
|
||||
process_entries(&bank, &[entry_3]),
|
||||
Err(TransactionError::AccountNotFound)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_transaction_statuses() {
|
||||
// Make sure instruction errors still update the signature cache
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(11_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let pubkey = Pubkey::new_rand();
|
||||
bank.transfer(1_000, &mint_keypair, &pubkey).unwrap();
|
||||
assert_eq!(bank.transaction_count(), 1);
|
||||
assert_eq!(bank.get_balance(&pubkey), 1_000);
|
||||
assert_eq!(
|
||||
bank.transfer(10_001, &mint_keypair, &pubkey),
|
||||
Err(TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::new_result_with_negative_lamports(),
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
bank.transfer(10_001, &mint_keypair, &pubkey),
|
||||
Err(TransactionError::DuplicateSignature)
|
||||
);
|
||||
|
||||
// Make sure other errors don't update the signature cache
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&pubkey,
|
||||
1000,
|
||||
Hash::default(),
|
||||
0,
|
||||
);
|
||||
let signature = tx.signatures[0];
|
||||
|
||||
// Should fail with blockhash not found
|
||||
assert_eq!(
|
||||
bank.process_transaction(&tx).map(|_| signature),
|
||||
Err(TransactionError::BlockhashNotFound)
|
||||
);
|
||||
|
||||
// Should fail again with blockhash not found
|
||||
assert_eq!(
|
||||
bank.process_transaction(&tx).map(|_| signature),
|
||||
Err(TransactionError::BlockhashNotFound)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_transaction_statuses_fail() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(11_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
let success_tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair1.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
let fail_tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair2.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
|
||||
let entry_1_to_mint = next_entry(
|
||||
&bank.last_blockhash(),
|
||||
1,
|
||||
vec![
|
||||
success_tx,
|
||||
fail_tx.clone(), // will collide
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
process_entries(&bank, &[entry_1_to_mint]),
|
||||
Err(TransactionError::AccountInUse)
|
||||
);
|
||||
|
||||
// Should not see duplicate signature error
|
||||
assert_eq!(bank.process_transaction(&fail_tx), Ok(()));
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
//! A stage to broadcast data from a leader node to validators
|
||||
//!
|
||||
use crate::blocktree::Blocktree;
|
||||
use crate::cluster_info::{ClusterInfo, ClusterInfoError, DATA_PLANE_FANOUT};
|
||||
use crate::entry::EntrySlice;
|
||||
#[cfg(feature = "erasure")]
|
||||
use crate::cluster_info::{ClusterInfo, ClusterInfoError, NEIGHBORHOOD_SIZE};
|
||||
use crate::entry::{EntrySender, EntrySlice};
|
||||
use crate::erasure::CodingGenerator;
|
||||
use crate::packet::index_blobs;
|
||||
use crate::packet::index_blobs_with_genesis;
|
||||
use crate::poh_recorder::WorkingBankEntries;
|
||||
use crate::result::{Error, Result};
|
||||
use crate::service::Service;
|
||||
@ -13,6 +12,7 @@ use crate::staking_utils;
|
||||
use rayon::prelude::*;
|
||||
use solana_metrics::counter::Counter;
|
||||
use solana_metrics::{influxdb, submit};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::timing::duration_as_ms;
|
||||
use std::net::UdpSocket;
|
||||
@ -29,8 +29,6 @@ pub enum BroadcastStageReturnType {
|
||||
|
||||
struct Broadcast {
|
||||
id: Pubkey,
|
||||
|
||||
#[cfg(feature = "erasure")]
|
||||
coding_generator: CodingGenerator,
|
||||
}
|
||||
|
||||
@ -41,10 +39,12 @@ impl Broadcast {
|
||||
receiver: &Receiver<WorkingBankEntries>,
|
||||
sock: &UdpSocket,
|
||||
blocktree: &Arc<Blocktree>,
|
||||
storage_entry_sender: &EntrySender,
|
||||
genesis_blockhash: &Hash,
|
||||
) -> Result<()> {
|
||||
let timer = Duration::new(1, 0);
|
||||
let (mut bank, entries) = receiver.recv_timeout(timer)?;
|
||||
let mut max_tick_height = (bank.slot() + 1) * bank.ticks_per_slot() - 1;
|
||||
let mut max_tick_height = bank.max_tick_height();
|
||||
|
||||
let now = Instant::now();
|
||||
let mut num_entries = entries.len();
|
||||
@ -52,7 +52,7 @@ impl Broadcast {
|
||||
let mut last_tick = entries.last().map(|v| v.1).unwrap_or(0);
|
||||
ventries.push(entries);
|
||||
|
||||
assert!(last_tick <= max_tick_height,);
|
||||
assert!(last_tick <= max_tick_height);
|
||||
if last_tick != max_tick_height {
|
||||
while let Ok((same_bank, entries)) = receiver.try_recv() {
|
||||
// If the bank changed, that implies the previous slot was interrupted and we do not have to
|
||||
@ -61,7 +61,7 @@ impl Broadcast {
|
||||
num_entries = 0;
|
||||
ventries.clear();
|
||||
bank = same_bank.clone();
|
||||
max_tick_height = (bank.slot() + 1) * bank.ticks_per_slot() - 1;
|
||||
max_tick_height = bank.max_tick_height();
|
||||
}
|
||||
num_entries += entries.len();
|
||||
last_tick = entries.last().map(|v| v.1).unwrap_or(0);
|
||||
@ -73,24 +73,27 @@ impl Broadcast {
|
||||
}
|
||||
}
|
||||
|
||||
let mut broadcast_table = cluster_info
|
||||
.read()
|
||||
.unwrap()
|
||||
.sorted_tvu_peers(&staking_utils::delegated_stakes(&bank));
|
||||
// Layer 1, leader nodes are limited to the fanout size.
|
||||
broadcast_table.truncate(DATA_PLANE_FANOUT);
|
||||
|
||||
let bank_epoch = bank.get_stakers_epoch(bank.slot());
|
||||
let mut broadcast_table = cluster_info.read().unwrap().sorted_tvu_peers(
|
||||
&staking_utils::delegated_stakes_at_epoch(&bank, bank_epoch).unwrap(),
|
||||
);
|
||||
inc_new_counter_info!("broadcast_service-num_peers", broadcast_table.len() + 1);
|
||||
// Layer 1, leader nodes are limited to the fanout size.
|
||||
broadcast_table.truncate(NEIGHBORHOOD_SIZE);
|
||||
|
||||
inc_new_counter_info!("broadcast_service-entries_received", num_entries);
|
||||
|
||||
let to_blobs_start = Instant::now();
|
||||
|
||||
let blobs: Vec<_> = ventries
|
||||
.into_par_iter()
|
||||
.flat_map(|p| {
|
||||
.map_with(storage_entry_sender.clone(), |s, p| {
|
||||
let entries: Vec<_> = p.into_iter().map(|e| e.0).collect();
|
||||
entries.to_shared_blobs()
|
||||
let blobs = entries.to_shared_blobs();
|
||||
let _ignored = s.send(entries);
|
||||
blobs
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let blob_index = blocktree
|
||||
@ -99,9 +102,10 @@ impl Broadcast {
|
||||
.map(|meta| meta.consumed)
|
||||
.unwrap_or(0);
|
||||
|
||||
index_blobs(
|
||||
index_blobs_with_genesis(
|
||||
&blobs,
|
||||
&self.id,
|
||||
genesis_blockhash,
|
||||
blob_index,
|
||||
bank.slot(),
|
||||
bank.parent().map_or(0, |parent| parent.slot()),
|
||||
@ -115,6 +119,8 @@ impl Broadcast {
|
||||
|
||||
blocktree.write_shared_blobs(&blobs)?;
|
||||
|
||||
let coding = self.coding_generator.next(&blobs);
|
||||
|
||||
let to_blobs_elapsed = duration_as_ms(&to_blobs_start.elapsed());
|
||||
|
||||
let broadcast_start = Instant::now();
|
||||
@ -124,14 +130,8 @@ impl Broadcast {
|
||||
|
||||
inc_new_counter_info!("streamer-broadcast-sent", blobs.len());
|
||||
|
||||
// Fill in the coding blob data from the window data blobs
|
||||
#[cfg(feature = "erasure")]
|
||||
{
|
||||
let coding = self.coding_generator.next(&blobs)?;
|
||||
|
||||
// send out erasures
|
||||
ClusterInfo::broadcast(&self.id, false, &broadcast_table, sock, &coding)?;
|
||||
}
|
||||
// send out erasures
|
||||
ClusterInfo::broadcast(&self.id, false, &broadcast_table, sock, &coding)?;
|
||||
|
||||
let broadcast_elapsed = duration_as_ms(&broadcast_start.elapsed());
|
||||
|
||||
@ -186,17 +186,26 @@ impl BroadcastStage {
|
||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
||||
receiver: &Receiver<WorkingBankEntries>,
|
||||
blocktree: &Arc<Blocktree>,
|
||||
storage_entry_sender: EntrySender,
|
||||
genesis_blockhash: &Hash,
|
||||
) -> BroadcastStageReturnType {
|
||||
let me = cluster_info.read().unwrap().my_data().clone();
|
||||
let coding_generator = CodingGenerator::default();
|
||||
|
||||
let mut broadcast = Broadcast {
|
||||
id: me.id,
|
||||
#[cfg(feature = "erasure")]
|
||||
coding_generator: CodingGenerator::new(),
|
||||
coding_generator,
|
||||
};
|
||||
|
||||
loop {
|
||||
if let Err(e) = broadcast.run(&cluster_info, receiver, sock, blocktree) {
|
||||
if let Err(e) = broadcast.run(
|
||||
&cluster_info,
|
||||
receiver,
|
||||
sock,
|
||||
blocktree,
|
||||
&storage_entry_sender,
|
||||
genesis_blockhash,
|
||||
) {
|
||||
match e {
|
||||
Error::RecvTimeoutError(RecvTimeoutError::Disconnected) | Error::SendError => {
|
||||
return BroadcastStageReturnType::ChannelDisconnected;
|
||||
@ -234,14 +243,24 @@ impl BroadcastStage {
|
||||
receiver: Receiver<WorkingBankEntries>,
|
||||
exit_sender: &Arc<AtomicBool>,
|
||||
blocktree: &Arc<Blocktree>,
|
||||
storage_entry_sender: EntrySender,
|
||||
genesis_blockhash: &Hash,
|
||||
) -> Self {
|
||||
let blocktree = blocktree.clone();
|
||||
let exit_sender = exit_sender.clone();
|
||||
let genesis_blockhash = *genesis_blockhash;
|
||||
let thread_hdl = Builder::new()
|
||||
.name("solana-broadcaster".to_string())
|
||||
.spawn(move || {
|
||||
let _finalizer = Finalizer::new(exit_sender);
|
||||
Self::run(&sock, &cluster_info, &receiver, &blocktree)
|
||||
Self::run(
|
||||
&sock,
|
||||
&cluster_info,
|
||||
&receiver,
|
||||
&blocktree,
|
||||
storage_entry_sender,
|
||||
&genesis_blockhash,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@ -265,9 +284,9 @@ mod test {
|
||||
use crate::entry::create_ticks;
|
||||
use crate::service::Service;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::timing::DEFAULT_TICKS_PER_SLOT;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, RwLock};
|
||||
@ -301,7 +320,10 @@ mod test {
|
||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||
|
||||
let exit_sender = Arc::new(AtomicBool::new(false));
|
||||
let bank = Arc::new(Bank::default());
|
||||
let (storage_sender, _receiver) = channel();
|
||||
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
|
||||
// Start up the broadcast stage
|
||||
let broadcast_service = BroadcastStage::new(
|
||||
@ -310,6 +332,8 @@ mod test {
|
||||
entry_receiver,
|
||||
&exit_sender,
|
||||
&blocktree,
|
||||
storage_sender,
|
||||
&Hash::default(),
|
||||
);
|
||||
|
||||
MockBroadcastStage {
|
||||
@ -320,15 +344,13 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
//TODO this test won't work since broadcast stage no longer edits the ledger
|
||||
fn test_broadcast_ledger() {
|
||||
solana_logger::setup();
|
||||
let ledger_path = get_tmp_ledger_path("test_broadcast_ledger");
|
||||
|
||||
{
|
||||
// Create the leader scheduler
|
||||
let leader_keypair = Keypair::new();
|
||||
let start_tick_height = 0;
|
||||
let max_tick_height = start_tick_height + DEFAULT_TICKS_PER_SLOT;
|
||||
|
||||
let (entry_sender, entry_receiver) = channel();
|
||||
let broadcast_service = setup_dummy_broadcast_service(
|
||||
@ -337,6 +359,9 @@ mod test {
|
||||
entry_receiver,
|
||||
);
|
||||
let bank = broadcast_service.bank.clone();
|
||||
let start_tick_height = bank.tick_height();
|
||||
let max_tick_height = bank.max_tick_height();
|
||||
let ticks_per_slot = bank.ticks_per_slot();
|
||||
|
||||
let ticks = create_ticks(max_tick_height - start_tick_height, Hash::default());
|
||||
for (i, tick) in ticks.into_iter().enumerate() {
|
||||
@ -346,15 +371,23 @@ mod test {
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(2000));
|
||||
|
||||
trace!(
|
||||
"[broadcast_ledger] max_tick_height: {}, start_tick_height: {}, ticks_per_slot: {}",
|
||||
max_tick_height,
|
||||
start_tick_height,
|
||||
ticks_per_slot,
|
||||
);
|
||||
|
||||
let blocktree = broadcast_service.blocktree;
|
||||
let mut blob_index = 0;
|
||||
for i in 0..max_tick_height - start_tick_height {
|
||||
let slot = (start_tick_height + i + 1) / DEFAULT_TICKS_PER_SLOT;
|
||||
let slot = (start_tick_height + i + 1) / ticks_per_slot;
|
||||
|
||||
let result = blocktree.get_data_blob(slot, blob_index).unwrap();
|
||||
|
||||
blob_index += 1;
|
||||
assert!(result.is_some());
|
||||
result.expect("expect blob presence");
|
||||
}
|
||||
|
||||
drop(entry_sender);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user