Compare commits
840 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
825c0e2b6e | ||
|
4334fef955 | ||
|
2333468350 | ||
|
86968cb311 | ||
|
3ce9a16e7f | ||
|
3746c0c6ac | ||
|
2b71bf37f9 | ||
|
3b526cc2de | ||
|
198f87ffea | ||
|
859d4db87e | ||
|
1163144914 | ||
|
49f212247a | ||
|
e26840cb09 | ||
|
14bbcef722 | ||
|
5326f3ec73 | ||
|
fca4554d3f | ||
|
15de250c2c | ||
|
a7b0fcc21e | ||
|
4999fa6263 | ||
|
1f54be66c9 | ||
|
2e1c3a8338 | ||
|
fe934eb7a0 | ||
|
62a0c2f348 | ||
|
cfeef3a9eb | ||
|
52009788ee | ||
|
2acf4d874d | ||
|
f951d7d33f | ||
|
20fe24f348 | ||
|
2e5cc41945 | ||
|
bc76b20e6d | ||
|
3f6befe012 | ||
|
ee201cfb84 | ||
|
66ec12e869 | ||
|
addfc99ff4 | ||
|
91f0faa72d | ||
|
2deebe4d73 | ||
|
5bc57ea004 | ||
|
c63bd05458 | ||
|
b4933f4c74 | ||
|
e7748c603b | ||
|
6a5d782d6c | ||
|
89fad8f566 | ||
|
01cac89867 | ||
|
dfb4729b02 | ||
|
6ab5f823b3 | ||
|
bf1ceab6ed | ||
|
dbaff495c8 | ||
|
f65caa66bf | ||
|
2f455e18ef | ||
|
7b155f384d | ||
|
fd405239d9 | ||
|
8698156e27 | ||
|
2f0f218ad9 | ||
|
3cc75b4bab | ||
|
0a0f8470d7 | ||
|
e46026f1fb | ||
|
fef5089d7e | ||
|
a844bd70da | ||
|
cbc01bd1b9 | ||
|
e096cc1101 | ||
|
0dc559d3cf | ||
|
34f537adad | ||
|
9558628537 | ||
|
940bf7081a | ||
|
38c31c3b4e | ||
|
253272d757 | ||
|
9b1bd8065f | ||
|
b2a5cf57ad | ||
|
33c51ee75d | ||
|
d8aa107fae | ||
|
82e02d0734 | ||
|
dae59bb3e1 | ||
|
e0e7fc8e52 | ||
|
9abc84c583 | ||
|
db6540772c | ||
|
840ebfe625 | ||
|
953282dbf4 | ||
|
788d1199ac | ||
|
f6a8f718a8 | ||
|
f225bf6f01 | ||
|
b5f03a380b | ||
|
140c75f613 | ||
|
6a59f67fdc | ||
|
5943747001 | ||
|
f26f18d29d | ||
|
9b58d72f52 | ||
|
0c885d6a04 | ||
|
70b51c0a5a | ||
|
560660ae11 | ||
|
5436855b67 | ||
|
7351e5ed2c | ||
|
7ee993fadf | ||
|
9bf459e82f | ||
|
545090ff17 | ||
|
bd8074507f | ||
|
cfc7b22c4c | ||
|
5f1c637508 | ||
|
d888e0a6d7 | ||
|
10e808e1bd | ||
|
d9b03ca38b | ||
|
608d75b348 | ||
|
cee3cee4ef | ||
|
09e51323f0 | ||
|
da6f702129 | ||
|
02a83e7c6e | ||
|
83263e1737 | ||
|
1f7ac22b60 | ||
|
747debae56 | ||
|
00b4186469 | ||
|
b087dabf4f | ||
|
e00eb0a069 | ||
|
d4e49ffd06 | ||
|
0f34a190ea | ||
|
df2fb8f5b3 | ||
|
80d2a6046b | ||
|
24273b826f | ||
|
68d2616e35 | ||
|
f506d39339 | ||
|
bc58c9ec7e | ||
|
b57a52cd85 | ||
|
8631be42ac | ||
|
09367369ef | ||
|
23b8c95cc4 | ||
|
e61392b057 | ||
|
7deba20395 | ||
|
274c097f84 | ||
|
1c7cea1af4 | ||
|
4406496d2f | ||
|
a15fa4840c | ||
|
c9030660d6 | ||
|
fdeda769d0 | ||
|
53edd26578 | ||
|
2433cdd6d6 | ||
|
77b34c278e | ||
|
90e993fd9a | ||
|
251054d8c9 | ||
|
d4bb7cec69 | ||
|
a3f6b04345 | ||
|
ba4a5053dd | ||
|
6b47a259c3 | ||
|
c9ec13cf1f | ||
|
906a6ab837 | ||
|
d0e478a9f8 | ||
|
b560b64d33 | ||
|
057af41716 | ||
|
a44b8abd48 | ||
|
8778ecaed5 | ||
|
a02542ada3 | ||
|
ea17c6883f | ||
|
706306645b | ||
|
da9e930788 | ||
|
8b8e066bbe | ||
|
3473350b62 | ||
|
59d7eb5216 | ||
|
55ba934137 | ||
|
4c3dcb7f7e | ||
|
3a879db8af | ||
|
d2107270ea | ||
|
007afe22d0 | ||
|
93a1d10e15 | ||
|
d57a7c8f21 | ||
|
6db39829c8 | ||
|
ec76826493 | ||
|
d4ddb6265b | ||
|
7a8233d7ca | ||
|
95bc051129 | ||
|
02bcf4f8e2 | ||
|
4b0d4e9834 | ||
|
bf4cdc091a | ||
|
5f2cf2b44d | ||
|
03aae5eb5f | ||
|
5f1ce81fbc | ||
|
fc582aa57c | ||
|
13676e9614 | ||
|
7636a0521f | ||
|
ad06354a18 | ||
|
953cb93e44 | ||
|
38b2957a8e | ||
|
9eb39df93f | ||
|
f34ce94347 | ||
|
5c6411fc06 | ||
|
3ab428693a | ||
|
7ffaf2ad29 | ||
|
a5e4a1f2d8 | ||
|
6ae99848f4 | ||
|
5342b3a53f | ||
|
50a3c98d21 | ||
|
ca2b7a2c5b | ||
|
e900623817 | ||
|
a46e953ebb | ||
|
3bbc51a6ae | ||
|
7ae2464cf3 | ||
|
58a36ce484 | ||
|
61bd9e6a28 | ||
|
495ab631d6 | ||
|
93906847f9 | ||
|
70f4c4a974 | ||
|
ad6078da2d | ||
|
2a617f2d07 | ||
|
6af3c6ecbc | ||
|
a5938e5a21 | ||
|
8eaa8d8788 | ||
|
51940eec9b | ||
|
d1e16b2bc4 | ||
|
97051c87b4 | ||
|
2521913654 | ||
|
803c87b4bd | ||
|
8ff9ee3a06 | ||
|
25bf2c6062 | ||
|
fc80b77fc4 | ||
|
5a7707362c | ||
|
0102ee3fa9 | ||
|
d29cb19a73 | ||
|
1cc66f0cd7 | ||
|
e2cfc513eb | ||
|
8115a962e9 | ||
|
0c804e2ef2 | ||
|
cbe36a7a63 | ||
|
174825fecf | ||
|
c7e80bebdc | ||
|
57abc370fa | ||
|
42ab421a87 | ||
|
9ab27291f5 | ||
|
27e4e9cb8d | ||
|
b0cf65dfc8 | ||
|
bfc97c682c | ||
|
231c9b25d1 | ||
|
f536d805ed | ||
|
cb6e7426a4 | ||
|
71f391ae04 | ||
|
12bf34f059 | ||
|
a3a14d6b5b | ||
|
5e22db017a | ||
|
2cd09957b4 | ||
|
d1ec6c0b8b | ||
|
7722723400 | ||
|
4a42cfc42a | ||
|
976d744b0d | ||
|
62de02c8d4 | ||
|
2741a5ce3b | ||
|
0c818acd90 | ||
|
4d04915302 | ||
|
7576411c59 | ||
|
ab2bed6e8f | ||
|
f66a5ba579 | ||
|
9253d786f8 | ||
|
d1be1ee49e | ||
|
65833eacd8 | ||
|
921994e588 | ||
|
91dfd962e5 | ||
|
18067dcb55 | ||
|
7501e1b0f0 | ||
|
1bd2c1de20 | ||
|
a9ef20d43f | ||
|
00a9d66fed | ||
|
7508ffe703 | ||
|
6c2368bfc9 | ||
|
6bcb797260 | ||
|
329945e56f | ||
|
1f80346d97 | ||
|
38e661d7ba | ||
|
592c4efd17 | ||
|
1629745bd3 | ||
|
02e078c22e | ||
|
ea9e7c710a | ||
|
ee5aa0c1a2 | ||
|
55dee2901e | ||
|
2b0824d18b | ||
|
b0709ea0ac | ||
|
81b5499f7a | ||
|
c96ce99705 | ||
|
8dcd2d11e1 | ||
|
777aae9059 | ||
|
4dd1340236 | ||
|
889b06e1d4 | ||
|
f511296ee8 | ||
|
c19eb717b4 | ||
|
dd54369e1b | ||
|
bb563b4835 | ||
|
d061fadede | ||
|
577cd2bd3a | ||
|
9d1c8657e2 | ||
|
67216ac7f5 | ||
|
e63b24c39f | ||
|
d963f7afb4 | ||
|
a07bf4870a | ||
|
8422d4b3fb | ||
|
ff4731cce2 | ||
|
659aaafff6 | ||
|
175651c497 | ||
|
085e773f27 | ||
|
5d3140c040 | ||
|
d8d7238920 | ||
|
418c3cd4cf | ||
|
9f532cb50f | ||
|
c35f4927cd | ||
|
2d0f4b5c8c | ||
|
fe59ee61e6 | ||
|
bda2acb06d | ||
|
ac3fe1da02 | ||
|
57813041d2 | ||
|
7e446da82c | ||
|
d94f5c94a3 | ||
|
4d9aee4794 | ||
|
f32c152bce | ||
|
5caf9110e0 | ||
|
d1c80143ea | ||
|
bb132df121 | ||
|
b3af1c7e57 | ||
|
14cba53338 | ||
|
dc8abbe9e3 | ||
|
dd06001ed8 | ||
|
298b7de2e2 | ||
|
74cbc6953f | ||
|
27e5203078 | ||
|
73787e162c | ||
|
74ae40be41 | ||
|
8dd58e9cea | ||
|
7d86179c60 | ||
|
8115cf1360 | ||
|
3e5d45053d | ||
|
061319f35a | ||
|
dc75837500 | ||
|
dfe26f5275 | ||
|
f4385f7ad2 | ||
|
8df4cf2895 | ||
|
dad62e132e | ||
|
0d4131ae68 | ||
|
a2539e1892 | ||
|
210659e6c3 | ||
|
15a0fb1fa9 | ||
|
4db31f5b48 | ||
|
b38a535c63 | ||
|
218b02aaf8 | ||
|
d6e7cbd4e8 | ||
|
f2fda14333 | ||
|
1c576d4a68 | ||
|
f6232e1b3c | ||
|
ad71fa3f12 | ||
|
9c326c7c71 | ||
|
ac545cadaf | ||
|
082d8fff36 | ||
|
2c3632a042 | ||
|
7b23e79922 | ||
|
9afd14a0c6 | ||
|
f42aa34ab6 | ||
|
43af91ff4b | ||
|
a920a0f946 | ||
|
4a8a6d0b3f | ||
|
a64b8a2705 | ||
|
0198f9e8af | ||
|
1582a3a927 | ||
|
e713f0e5fb | ||
|
77031000c4 | ||
|
635a962fba | ||
|
c59ec2dcff | ||
|
abc6c5e264 | ||
|
87cfac12dd | ||
|
60b43e34b6 | ||
|
9ab6222f03 | ||
|
2298dd5c07 | ||
|
822d166115 | ||
|
8f71580615 | ||
|
cc3352ff06 | ||
|
8f5928b7c7 | ||
|
888e9617ff | ||
|
4695c4cf7d | ||
|
bbfc56ff7f | ||
|
4103d99018 | ||
|
c375ce1fcd | ||
|
df813b31c5 | ||
|
7db92e2951 | ||
|
6585518f78 | ||
|
6398e256e4 | ||
|
b64ebb45e7 | ||
|
307ac66d9c | ||
|
dc02f2ea8b | ||
|
b7386f9d84 | ||
|
223f9707ca | ||
|
ea5b00364f | ||
|
fb98df76b7 | ||
|
4ddbf8d509 | ||
|
aa80f69171 | ||
|
0ace22d03f | ||
|
0e6aca5a7e | ||
|
3f04226864 | ||
|
d308eed136 | ||
|
ed1149c8e0 | ||
|
48f58a88bc | ||
|
d238371b0c | ||
|
0b7e8d0162 | ||
|
18fd52367e | ||
|
2d665da3e1 | ||
|
5ef06a9d36 | ||
|
f4622d67e9 | ||
|
b65c9ea544 | ||
|
cc7c6c960e | ||
|
01697a9f5c | ||
|
ab361a8073 | ||
|
ec5c02cb7f | ||
|
e8124324ff | ||
|
1720fe6a46 | ||
|
e50bc0d34b | ||
|
45774dc4aa | ||
|
ea8d9d1aea | ||
|
221866f74e | ||
|
3e96d59359 | ||
|
8c19b6268c | ||
|
8ae26867c5 | ||
|
19baaea0da | ||
|
e3cebcf82d | ||
|
ccad5d5aaf | ||
|
d0bcde001e | ||
|
83a8e82626 | ||
|
7305a1f407 | ||
|
3975c7f8c9 | ||
|
ac1d075d73 | ||
|
73a278dc64 | ||
|
a042ee609a | ||
|
0d5c1239c6 | ||
|
027ec71aa9 | ||
|
ef718c651e | ||
|
fc2a0d53d9 | ||
|
bb47844ae6 | ||
|
b997d3eb4e | ||
|
9bcca268a3 | ||
|
8a2d4e2f72 | ||
|
335675c51c | ||
|
1bf2285fa2 | ||
|
71f77a8e0a | ||
|
644a7f9a44 | ||
|
965361ff69 | ||
|
4593d333c7 | ||
|
940519ea5a | ||
|
a0bcbf70d5 | ||
|
17fb8258e5 | ||
|
c350543b46 | ||
|
5b4ecb01ca | ||
|
28b115497f | ||
|
0604029661 | ||
|
2374cf09e2 | ||
|
ab475e4849 | ||
|
1c97b31eaf | ||
|
bd257050e3 | ||
|
2d362ed337 | ||
|
cb7117beac | ||
|
b358ff66e1 | ||
|
6309c97697 | ||
|
58727463e1 | ||
|
741d148a0d | ||
|
127553ce4b | ||
|
ecb055a252 | ||
|
dfa6fbaa0c | ||
|
cf11d4c7dc | ||
|
d0a4686990 | ||
|
2542d5dd42 | ||
|
1e0f2b2446 | ||
|
a8028fbb93 | ||
|
ed87229cec | ||
|
c4fd81fc1c | ||
|
ad43babe3d | ||
|
36c0cb052b | ||
|
ed58bcda4c | ||
|
268bb1b59b | ||
|
059764586a | ||
|
72b11081a4 | ||
|
0bbee9456f | ||
|
fcac910989 | ||
|
2e9ba149f2 | ||
|
d3712dd26d | ||
|
60877f9ba4 | ||
|
4f2c76150f | ||
|
137577fb86 | ||
|
bf623219d2 | ||
|
25d1f841ee | ||
|
517fe73734 | ||
|
890919d140 | ||
|
33ea1e0edd | ||
|
7614af2a45 | ||
|
1528959327 | ||
|
46b6cedff4 | ||
|
8d8f28c1d0 | ||
|
df782b93ae | ||
|
124f77cdb1 | ||
|
fc15f74c3c | ||
|
1d06aa3b31 | ||
|
0b263f8714 | ||
|
84b3e12e1f | ||
|
669282ae69 | ||
|
485806c488 | ||
|
1412ee1ca6 | ||
|
ef5fb6fa46 | ||
|
99432833d2 | ||
|
fa00803fbf | ||
|
04ef977509 | ||
|
87c6508305 | ||
|
ed0c1d3b52 | ||
|
8b5598fabd | ||
|
5b070ad014 | ||
|
6246405afd | ||
|
27c8ba6afc | ||
|
b832a03315 | ||
|
09686290bc | ||
|
4aaa7b30ca | ||
|
fe590da3b6 | ||
|
a7fa92b372 | ||
|
a25e57c397 | ||
|
eed676113e | ||
|
b57f24f1bc | ||
|
0e084358b4 | ||
|
f016c9a669 | ||
|
59ba1df910 | ||
|
71a2c90f21 | ||
|
8436457e75 | ||
|
3ac0192d40 | ||
|
3db159f616 | ||
|
e21f5c784e | ||
|
65c24db83c | ||
|
ed5101b031 | ||
|
1420628b28 | ||
|
15ab966ed1 | ||
|
b5a735878a | ||
|
b6d09f1901 | ||
|
78f6ddc5b7 | ||
|
4e595e8e3c | ||
|
79249360f7 | ||
|
336d5136bf | ||
|
0c8cee8c4a | ||
|
4c0420b884 | ||
|
0d7e093415 | ||
|
c835749563 | ||
|
0172d2a065 | ||
|
927f272f0e | ||
|
5e2891ae5d | ||
|
4f85481a2b | ||
|
d314e0395a | ||
|
69a6d07371 | ||
|
fab8ef379f | ||
|
408ef8b2cb | ||
|
a2a2f1c2d2 | ||
|
dc2888c9a3 | ||
|
9739be9ecf | ||
|
e61257695f | ||
|
b9988b62e4 | ||
|
d6b3961530 | ||
|
6d0be323ad | ||
|
09256adbc3 | ||
|
8e3a7da596 | ||
|
7d96510d17 | ||
|
0fd795a676 | ||
|
eff876881b | ||
|
9adf0d4ee0 | ||
|
3bc9789e8d | ||
|
fd207b6907 | ||
|
2226c1b75c | ||
|
a0964bb2c2 | ||
|
b5383b8b54 | ||
|
39f86050a6 | ||
|
a03d441e6f | ||
|
3900d09f6f | ||
|
e218f4e56e | ||
|
81ba18eea6 | ||
|
1671ece9df | ||
|
775fa0c968 | ||
|
dd276138c2 | ||
|
0c55b37976 | ||
|
966d077431 | ||
|
400412d76c | ||
|
c7e77a2238 | ||
|
64c42e28dc | ||
|
c2baf7b07d | ||
|
a52a9afa3c | ||
|
669502ede7 | ||
|
b19f730527 | ||
|
d5ff5f4739 | ||
|
1c82f84595 | ||
|
bea9cd9684 | ||
|
1bc9a9c23b | ||
|
c4faccc77f | ||
|
e6803daf10 | ||
|
effe6e3ff3 | ||
|
0d6c233747 | ||
|
015e696077 | ||
|
7faab2072c | ||
|
83718a3b3e | ||
|
4a074133f7 | ||
|
12eff5a2f9 | ||
|
4197cce8c9 | ||
|
fed3817ed3 | ||
|
4ffd7693d6 | ||
|
1596c961d9 | ||
|
fd7d5cbe0d | ||
|
7058287273 | ||
|
912aafcefd | ||
|
2f34f433b3 | ||
|
1ff4dd9a9a | ||
|
fdcaad96c7 | ||
|
14a72b0fc0 | ||
|
c13ab9f14e | ||
|
cff1bc6e71 | ||
|
bb6c4efe9b | ||
|
c324e71768 | ||
|
e2570c98ee | ||
|
b5125479ec | ||
|
989355e885 | ||
|
a2f2c46f87 | ||
|
605623baf5 | ||
|
fdc452c536 | ||
|
1b391dd36b | ||
|
917067741a | ||
|
34ed93d57c | ||
|
d400a64b9a | ||
|
2c7447b73e | ||
|
c0f0fa24f8 | ||
|
bda5f949bb | ||
|
992e985972 | ||
|
afaa359b0d | ||
|
3c17db41dc | ||
|
d62ed4f6b3 | ||
|
79f3194d0c | ||
|
b045f9a50d | ||
|
ce231602dc | ||
|
6f5e0cd161 | ||
|
1269a79a4d | ||
|
1b3424ff61 | ||
|
8b8033c72b | ||
|
7ca0109732 | ||
|
6b5172d002 | ||
|
9e19a635bb | ||
|
15193d0e1f | ||
|
f1c5c72e62 | ||
|
25dfed207c | ||
|
006cbee88a | ||
|
c95e5346a4 | ||
|
e54bf563b5 | ||
|
8f79327190 | ||
|
a197ac092a | ||
|
1e2b55c0d7 | ||
|
964ff522be | ||
|
934c32cbc6 | ||
|
9bd6be779f | ||
|
ce70d6eedc | ||
|
3a0d13aa77 | ||
|
f9323c5273 | ||
|
7587656cf6 | ||
|
023074650f | ||
|
d854e90c23 | ||
|
3aabeb2b81 | ||
|
65f5885bce | ||
|
7a132eabb4 | ||
|
7e1b380f01 | ||
|
1a2d9b8eed | ||
|
6eefa0b72d | ||
|
44372db955 | ||
|
e24cce4aed | ||
|
a8595c0418 | ||
|
340424e03a | ||
|
93036bec01 | ||
|
663e98969d | ||
|
37d1daf58e | ||
|
1a18f0ca55 | ||
|
bb950ec93e | ||
|
39ab3557a3 | ||
|
dcdc46b97c | ||
|
da3ed0dfb3 | ||
|
e391b9fb90 | ||
|
e346cdad26 | ||
|
7e4c6ff218 | ||
|
356f246a74 | ||
|
80da552834 | ||
|
2dd8ab197d | ||
|
1fe11e9ae2 | ||
|
21d5fe6272 | ||
|
52bc4a3598 | ||
|
cccaacee36 | ||
|
ebf6e1c0e9 | ||
|
5cf090c896 | ||
|
cc299053cc | ||
|
82b75796f9 | ||
|
a560d94a9f | ||
|
0827d52c6f | ||
|
a8d33c9950 | ||
|
43c32ea280 | ||
|
30d40e9a32 | ||
|
e28508ad56 | ||
|
182e4cec86 | ||
|
a32de96ab1 | ||
|
0de35fdd1f | ||
|
470d9cd752 | ||
|
87598c7612 | ||
|
57bf618627 | ||
|
c576a707b0 | ||
|
b78b1bbfa9 | ||
|
e710964d05 | ||
|
2d00657756 | ||
|
0526d4ff21 | ||
|
76e20015a4 | ||
|
f5e797e3aa | ||
|
787e36a28f | ||
|
8572b57834 | ||
|
ed0129f881 | ||
|
78836a9e22 | ||
|
4c08184379 | ||
|
da165d6943 | ||
|
8ffccfbaff | ||
|
a6d083d69d | ||
|
91bae9d510 | ||
|
f0f185509f | ||
|
5947ef7706 | ||
|
4f663a2a86 | ||
|
1d01777a13 | ||
|
6d3b8b6d7d | ||
|
50c1c08235 | ||
|
b16c30b4c6 | ||
|
ff1ca1e0d3 | ||
|
721c4378c1 | ||
|
5f4e0c7e3e | ||
|
e6af4511a8 | ||
|
965ad778dd | ||
|
3b78be83cf | ||
|
564cd4e09d | ||
|
699ca5fec1 | ||
|
f91ffbbfdf | ||
|
156292e408 | ||
|
81ae44f858 | ||
|
c948814eae | ||
|
b5dba77056 | ||
|
ef06d165b4 | ||
|
5cb23c814d | ||
|
8f7ded33e0 | ||
|
a17d5795fb | ||
|
ad4d41e602 | ||
|
9754fc789e | ||
|
fd3c6eb320 | ||
|
b7b68ecdba | ||
|
08ba27627d | ||
|
27d2c0aaf3 | ||
|
b714a4be63 | ||
|
2356b25c58 | ||
|
05cad05505 | ||
|
1e3082fbc0 | ||
|
80d2573b10 | ||
|
6adcdc41f4 | ||
|
2d08dddfc8 | ||
|
6da8f49d8b | ||
|
bcd072c5e8 | ||
|
e90a31781c | ||
|
2e89ec9105 | ||
|
865c42465a | ||
|
73c93cc345 | ||
|
cf32fdf672 | ||
|
c33b54794c | ||
|
6775e83420 | ||
|
719785a8d3 | ||
|
287995ffdf | ||
|
0e506a53b5 | ||
|
70e1a15973 | ||
|
09cff5e4cc | ||
|
57858b8015 | ||
|
07855e3125 | ||
|
2f5f8e7afd | ||
|
43897de12e | ||
|
4b577aa77b | ||
|
85c3d64f29 | ||
|
47dd293904 | ||
|
c4220a4853 | ||
|
48ab88a2af | ||
|
d9cf9709d2 | ||
|
9720c894f1 | ||
|
8dad3af36d | ||
|
e5425d4a27 | ||
|
58e6d4aabb | ||
|
9ce142606c | ||
|
e75a64a8a2 | ||
|
bc71e1b612 | ||
|
580ca36a62 | ||
|
447fe48d2a | ||
|
e8a6c8cd6d | ||
|
a8fd42c1df | ||
|
e782c26908 | ||
|
cd65a1e172 | ||
|
6e51c5685e | ||
|
84a37a2c0c | ||
|
7e94cc2cc3 | ||
|
7002ccb866 | ||
|
4fe0b116ae | ||
|
a0fb9de515 | ||
|
5d42dcc9ec | ||
|
96e88c90e8 | ||
|
75d94240ed | ||
|
6c544708e1 | ||
|
078e7246ac | ||
|
06cff1fb9f | ||
|
2e8bbed75b | ||
|
a707c9410e | ||
|
a956bb08d8 | ||
|
db52cc6749 | ||
|
73c6224a95 | ||
|
a217920561 | ||
|
48a36f59a6 | ||
|
965b132664 | ||
|
63f185f9bf | ||
|
e97b0088f2 | ||
|
374c17a0d9 | ||
|
4b3bc587ab | ||
|
06c63f2026 | ||
|
6b7d9942a7 | ||
|
760a56964f | ||
|
6ca575b5a3 | ||
|
ce1d36cacb | ||
|
87b2525e03 | ||
|
faa77aca2e | ||
|
5d2158792c | ||
|
e1ebaa902b | ||
|
e0564f628e | ||
|
44e45aa090 | ||
|
89f5f336af | ||
|
727be309b2 | ||
|
ce2d7a2d5a | ||
|
fad6c7201e | ||
|
8f0e1f3349 | ||
|
6f7d0c6928 | ||
|
120c8f244c | ||
|
352a367570 | ||
|
9f65d22909 | ||
|
141131f3a6 | ||
|
488420fdf2 | ||
|
10e6b8f769 | ||
|
419da18405 | ||
|
7329d4bf3a | ||
|
c8fe4043b6 | ||
|
3d133d61ca | ||
|
d51e42c707 | ||
|
79e39d6f0b | ||
|
7dec934bb3 | ||
|
83f866df01 | ||
|
d88d8e2dbb | ||
|
3a40dff999 | ||
|
3f69d58498 | ||
|
ca10cf081f |
5
.buildkite/env/secrets.ejson
vendored
5
.buildkite/env/secrets.ejson
vendored
@@ -7,9 +7,6 @@
|
||||
"GITHUB_TOKEN": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:Vq2dkGTOzfEpRht0BAGHFp/hDogMvXJe:tFXHg1epVt2mq9hkuc5sRHe+KAnVREi/p8S+IZu67XRyzdiA/nGak1k860FXYuuzuaE0QWekaEc=]",
|
||||
"INFLUX_DATABASE": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:5KI9WBkXx3R/W4m256mU5MJOE7N8aAT9:Cb8QFELZ9I60t5zhJ9h55Kcs]",
|
||||
"INFLUX_PASSWORD": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:hQRMpLCrav+OYkNphkeM4hagdVoZv5Iw:AUO76rr6+gF1OLJA8ZLSG8wHKXgYCPNk6gRCV8rBhZBJ4KwDaxpvOhMl7bxxXG6jol7v4aRa/Lk=]",
|
||||
"INFLUX_USERNAME": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:R7BNmQjfeqoGDAFTJu9bYTGHol2NgnYN:Q2tOT/EBcFvhFk+DKLKmVU7tLCpVC3Ui]",
|
||||
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:Egc2dMrHDU0NcZ71LwGv/V66shUhwYUE:04VoIb8CKy7KYhQ5W4cEW9SDKZltxWBL5Hob106lMBbUOD/yUvKYcG3Ep8JfTMwO3K8zowW5HpU/IdGoilX0XWLiJJ6t+p05WWK0TA16nOEtwrEG+UK8wm3sN+xCO20i4jDhpNpgg3FYFHT5rKTHW8+zaBTNUX/SFxkN67Lm+92IM28CXYE43SU1WV6H99hGFFVpTK5JVM3JuYU1ex/dHRE+xCzTr4MYUB/F+nGoNFW8HUDV/y0e1jxT9to3x0SmnytEEuk+5RUzFuEt9cKNFeNml3fOCi4qL+sfj/Y5pjH9xDiUxsvH/8NL35jbLP244aFHgWcp]",
|
||||
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_apple_darwin": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:NeOxSoWCvXB9AL4H6OK26l/7bmsKd/oz:Ijfoxtvk2CHlN1ZXHup3Gg/914kbbAkEGWJfvozA8UIe+aUzUObMyTrKkVOeNAH8Q8YH9tNzk7RRnrTcpnzeCCBLlWcVEeruMxHox3mPRzmSeDLxtbzCl9VePlRO3T7jg90K5hW+ZAkd5J/WJNzpAcmr93ts/of3MbvGHSujId/efCTzJEcP6JInnBb8Vrj7TlgKbzUlnqpq1+NjYPSXN3maKa9pKeo2JWxZlGBMoy6QWUUY5GbYEylw9smwh1LJcHZjlaZNMuOl4gNKtaSr38IXQkAXaRUJDPAmPras00YObKzXU8RkTrP4EoP/jx5LPR7f]",
|
||||
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_pc_windows_msvc": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:7t+56twjW+jR7fpFNNeRFLPd7E4lbmyN:JuviDpkQrfVcNUGRGsa2e/UhvH6tTYyk1s4cHHE5xZH1NByL7Kpqx36VG/+o1AUGEeSQdsBnKgzYdMoFYbO8o50DoRPc86QIEVXCupD6J9avxLFtQgOWgJp+/mCdUVXlqXiFs/vQgS/L4psrcKdF6WHd77BeUr6ll8DjH+9m5FC9Rcai2pXno6VbPpunHQ0oUdYzhFR64+LiRacBaefQ9igZ+nSEWDLqbaZSyfm9viWkijoVFTq8gAgdXXEh7g0QdxVE5T6bPristJhT6jWBhWunPUCDNFFErWIsbRGctepl4pbCWqh2hNTw9btSgVfeY6uGCOsdy9E=]"
|
||||
"INFLUX_USERNAME": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:R7BNmQjfeqoGDAFTJu9bYTGHol2NgnYN:Q2tOT/EBcFvhFk+DKLKmVU7tLCpVC3Ui]"
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
root: ./book/src
|
||||
root: ./docs/src
|
||||
|
||||
structure:
|
||||
readme: introduction.md
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
/book/html/
|
||||
/book/src/tests.ok
|
||||
/book/src/.gitbook/assets/*.svg
|
||||
/docs/html/
|
||||
/docs/src/tests.ok
|
||||
/docs/src/.gitbook/assets/*.svg
|
||||
/farf/
|
||||
/solana-release/
|
||||
/solana-release.tar.bz2
|
||||
|
40
.mergify.yml
40
.mergify.yml
@@ -19,22 +19,6 @@ pull_request_rules:
|
||||
label:
|
||||
add:
|
||||
- automerge
|
||||
- name: v0.21 backport
|
||||
conditions:
|
||||
- base=master
|
||||
- label=v0.21
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- v0.21
|
||||
- name: v0.22 backport
|
||||
conditions:
|
||||
- base=master
|
||||
- label=v0.22
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- v0.22
|
||||
- name: v0.23 backport
|
||||
conditions:
|
||||
- base=master
|
||||
@@ -43,3 +27,27 @@ pull_request_rules:
|
||||
backport:
|
||||
branches:
|
||||
- v0.23
|
||||
- name: v1.0 backport
|
||||
conditions:
|
||||
- base=master
|
||||
- label=v1.0
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- v1.0
|
||||
- name: v1.1 backport
|
||||
conditions:
|
||||
- base=master
|
||||
- label=v1.1
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- v1.1
|
||||
- name: v1.2 backport
|
||||
conditions:
|
||||
- base=master
|
||||
- label=v1.2
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- v1.2
|
||||
|
@@ -1,5 +1,6 @@
|
||||
os:
|
||||
- osx
|
||||
- windows
|
||||
|
||||
language: rust
|
||||
rust:
|
||||
|
@@ -45,7 +45,7 @@ $ git pull --rebase upstream master
|
||||
|
||||
If there are no functional changes, PRs can be very large and that's no
|
||||
problem. If, however, your changes are making meaningful changes or additions,
|
||||
then about 1,000 lines of changes is about the most you should ask a Solana
|
||||
then about 1000 lines of changes is about the most you should ask a Solana
|
||||
maintainer to review.
|
||||
|
||||
### Should I send small PRs as I develop large, new components?
|
||||
@@ -224,21 +224,20 @@ Inventing new terms is allowed, but should only be done when the term is widely
|
||||
used and understood. Avoid introducing new 3-letter terms, which can be
|
||||
confused with 3-letter acronyms.
|
||||
|
||||
[Terms currently in use](book/src/terminology.md)
|
||||
[Terms currently in use](docs/src/terminology.md)
|
||||
|
||||
|
||||
## 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
|
||||
add a design proposal, you'll need to at least propose a change the content
|
||||
under the [Accepted Design
|
||||
Proposals](https://docs.solana.com/book/v/master/proposals) chapter. Here's
|
||||
the full process:
|
||||
Solana's architecture is described by docs generated from markdown files in
|
||||
the `docs/src/` directory, maintained by an *editor* (currently @garious). To
|
||||
add a design proposal, you'll need to include it in the
|
||||
[Accepted Design Proposals](https://docs.solana.com/proposals)
|
||||
section of the Solana docs. Here's the full process:
|
||||
|
||||
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
|
||||
`docs/src/proposals` directory and references it from the [table of
|
||||
contents](docs/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
|
||||
maintainers support your plan of attack.
|
||||
|
6334
Cargo.lock
generated
6334
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -4,7 +4,10 @@ members = [
|
||||
"bench-streamer",
|
||||
"bench-tps",
|
||||
"banking-bench",
|
||||
"chacha",
|
||||
"chacha-cuda",
|
||||
"chacha-sys",
|
||||
"cli-config",
|
||||
"client",
|
||||
"core",
|
||||
"faucet",
|
||||
@@ -38,14 +41,19 @@ members = [
|
||||
"programs/vest",
|
||||
"programs/vote",
|
||||
"archiver",
|
||||
"archiver-lib",
|
||||
"archiver-utils",
|
||||
"remote-wallet",
|
||||
"runtime",
|
||||
"sdk",
|
||||
"sdk-c",
|
||||
"scripts",
|
||||
"stake-monitor",
|
||||
"sys-tuner",
|
||||
"transaction-status",
|
||||
"upload-perf",
|
||||
"net-utils",
|
||||
"fixed-buf",
|
||||
"version",
|
||||
"vote-signer",
|
||||
"cli",
|
||||
"rayon-threadlimit",
|
||||
|
35
README.md
35
README.md
@@ -9,26 +9,12 @@ Blockchain Rebuilt for Scale
|
||||
Solana™ is a new blockchain architecture built from the ground up for scale. The architecture supports
|
||||
up to 710 thousand transactions per second on a gigabit network.
|
||||
|
||||
Disclaimer
|
||||
Documentation
|
||||
===
|
||||
|
||||
All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the author's best effort. It is up to the reader to check and validate their accuracy and truthfulness. Furthermore nothing in this project constitutes a solicitation for investment.
|
||||
Before you jump into the code, review the documentation [Solana: Blockchain Rebuilt for Scale](https://docs.solana.com).
|
||||
|
||||
Introduction
|
||||
===
|
||||
|
||||
It's possible for a centralized database to process 710,000 transactions per second on a standard gigabit network if the transactions are, on average, no more than 176 bytes. A centralized database can also replicate itself and maintain high availability without significantly compromising that transaction rate using the distributed system technique known as Optimistic Concurrency Control [\[H.T.Kung, J.T.Robinson (1981)\]](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.65.4735). At Solana, we're demonstrating that these same theoretical limits apply just as well to blockchain on an adversarial network. The key ingredient? Finding a way to share time when nodes can't trust one-another. Once nodes can trust time, suddenly ~40 years of distributed systems research becomes applicable to blockchain!
|
||||
|
||||
> Perhaps the most striking difference between algorithms obtained by our method and ones based upon timeout is that using timeout produces a traditional distributed algorithm in which the processes operate asynchronously, while our method produces a globally synchronous one in which every process does the same thing at (approximately) the same time. Our method seems to contradict the whole purpose of distributed processing, which is to permit different processes to operate independently and perform different functions. However, if a distributed system is really a single system, then the processes must be synchronized in some way. Conceptually, the easiest way to synchronize processes is to get them all to do the same thing at the same time. Therefore, our method is used to implement a kernel that performs the necessary synchronization--for example, making sure that two different processes do not try to modify a file at the same time. Processes might spend only a small fraction of their time executing the synchronizing kernel; the rest of the time, they can operate independently--e.g., accessing different files. This is an approach we have advocated even when fault-tolerance is not required. The method's basic simplicity makes it easier to understand the precise properties of a system, which is crucial if one is to know just how fault-tolerant the system is. [\[L.Lamport (1984)\]](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.1078)
|
||||
|
||||
Furthermore, and much to our surprise, it can be implemented using a mechanism that has existed in Bitcoin since day one. The Bitcoin feature is called nLocktime and it can be used to postdate transactions using block height instead of a timestamp. As a Bitcoin client, you'd use block height instead of a timestamp if you don't trust the network. Block height turns out to be an instance of what's being called a Verifiable Delay Function in cryptography circles. It's a cryptographically secure way to say time has passed. In Solana, we use a far more granular verifiable delay function, a SHA 256 hash chain, to checkpoint the ledger and coordinate consensus. With it, we implement Optimistic Concurrency Control and are now well en route towards that theoretical limit of 710,000 transactions per second.
|
||||
|
||||
Architecture
|
||||
===
|
||||
|
||||
Before you jump into the code, review the online book [Solana: Blockchain Rebuilt for Scale](https://docs.solana.com/book/).
|
||||
|
||||
(The _latest_ development version of the online book is also [available here](https://docs.solana.com/book/v/master/).)
|
||||
(The _latest_ development version of the docs is [available here](https://docs.solana.com/v/master).)
|
||||
|
||||
Release Binaries
|
||||
===
|
||||
@@ -87,7 +73,8 @@ $ rustup update
|
||||
On Linux systems you may need to install libssl-dev, pkg-config, zlib1g-dev, etc. On Ubuntu:
|
||||
|
||||
```bash
|
||||
$ sudo apt-get install libssl-dev pkg-config zlib1g-dev llvm clang
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang
|
||||
```
|
||||
|
||||
Download the source code:
|
||||
@@ -120,16 +107,13 @@ $ cargo test
|
||||
Local Testnet
|
||||
---
|
||||
|
||||
Start your own testnet locally, instructions are in the book [Solana: Blockchain Rebuild for Scale: Getting Started](https://docs.solana.com/book/getting-started).
|
||||
Start your own testnet locally, instructions are in the online docs [Solana: Blockchain Rebuild for Scale: Getting Started](https://docs.solana.com/building-from-source).
|
||||
|
||||
Remote Testnets
|
||||
---
|
||||
|
||||
We maintain several testnets:
|
||||
* `testnet` - public stable testnet accessible via devnet.solana.com. Runs 24/7
|
||||
|
||||
* `testnet` - public stable testnet accessible via testnet.solana.com. Runs 24/7
|
||||
* `testnet-beta` - public beta channel testnet accessible via beta.testnet.solana.com. Runs 24/7
|
||||
* `testnet-edge` - public edge channel testnet accessible via edge.testnet.solana.com. Runs 24/7
|
||||
|
||||
## Deploy process
|
||||
|
||||
@@ -240,3 +224,8 @@ problem is solved by this code?" On the other hand, if a test does fail and you
|
||||
better way to solve the same problem, a Pull Request with your solution would most certainly be
|
||||
welcome! Likewise, if rewriting a test can better communicate what code it's protecting, please
|
||||
send us that patch!
|
||||
|
||||
Disclaimer
|
||||
===
|
||||
|
||||
All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the author's best effort. It is up to the reader to check and validate their accuracy and truthfulness. Furthermore nothing in this project constitutes a solicitation for investment.
|
||||
|
27
RELEASE.md
27
RELEASE.md
@@ -138,30 +138,11 @@ There are three release channels that map to branches as follows:
|
||||
### Update documentation
|
||||
TODO: Documentation update procedure is WIP as we move to gitbook
|
||||
|
||||
Document the new recommended version by updating `book/src/running-archiver.md` and `book/src/validator-testnet.md` on the release (beta) branch to point at the `solana-install` for the upcoming release version.
|
||||
Document the new recommended version by updating `docs/src/running-archiver.md` and `docs/src/validator-testnet.md` on the release (beta) branch to point at the `solana-install` for the upcoming release version.
|
||||
|
||||
#### Publish updated Book
|
||||
We maintain three copies of the "book" as official documentation:
|
||||
### Update software on devnet.solana.com
|
||||
|
||||
1) "Book" is the documentation for the latest official release. This should get manually updated whenever a new release is made. It is published here:
|
||||
https://solana-labs.github.io/book/
|
||||
|
||||
2) "Book-edge" tracks the tip of the master branch and updates automatically.
|
||||
https://solana-labs.github.io/book-edge/
|
||||
|
||||
3) "Book-beta" tracks the tip of the beta branch and updates automatically.
|
||||
https://solana-labs.github.io/book-beta/
|
||||
|
||||
To manually trigger an update of the "Book", create a new job of the manual-update-book pipeline.
|
||||
Set the tag of the latest release as the PUBLISH_BOOK_TAG environment variable.
|
||||
```bash
|
||||
PUBLISH_BOOK_TAG=v0.16.6
|
||||
```
|
||||
https://buildkite.com/solana-labs/manual-update-book
|
||||
|
||||
### Update software on testnet.solana.com
|
||||
|
||||
The testnet running on testnet.solana.com is set to use a fixed release tag
|
||||
The testnet running on devnet.solana.com is set to use a fixed release tag
|
||||
which is set in the Buildkite testnet-management pipeline.
|
||||
This tag needs to be updated and the testnet restarted after a new release
|
||||
tag is created.
|
||||
@@ -201,4 +182,4 @@ TESTNET_OP=create-and-start
|
||||
### Alert the community
|
||||
|
||||
Notify Discord users on #validator-support that a new release for
|
||||
testnet.solana.com is available
|
||||
devnet.solana.com is available
|
||||
|
42
archiver-lib/Cargo.toml
Normal file
42
archiver-lib/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "solana-archiver-lib"
|
||||
version = "1.0.23"
|
||||
description = "Solana Archiver Library"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.2.1"
|
||||
crossbeam-channel = "0.3"
|
||||
ed25519-dalek = "=1.0.0-pre.3"
|
||||
log = "0.4.8"
|
||||
rand = "0.7.0"
|
||||
rand_chacha = "0.2.2"
|
||||
solana-client = { path = "../client", version = "1.0.23" }
|
||||
solana-storage-program = { path = "../programs/storage", version = "1.0.23" }
|
||||
thiserror = "1.0"
|
||||
serde = "1.0.104"
|
||||
serde_json = "1.0.46"
|
||||
serde_derive = "1.0.103"
|
||||
solana-net-utils = { path = "../net-utils", version = "1.0.23" }
|
||||
solana-chacha = { path = "../chacha", version = "1.0.23" }
|
||||
solana-chacha-sys = { path = "../chacha-sys", version = "1.0.23" }
|
||||
solana-ledger = { path = "../ledger", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-perf = { path = "../perf", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
solana-core = { path = "../core", version = "1.0.23" }
|
||||
solana-archiver-utils = { path = "../archiver-utils", version = "1.0.23" }
|
||||
solana-metrics = { path = "../metrics", version = "1.0.23" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4.0"
|
||||
|
||||
[lib]
|
||||
name = "solana_archiver_lib"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@@ -1,26 +1,26 @@
|
||||
use crate::{
|
||||
chacha::{chacha_cbc_encrypt_ledger, CHACHA_BLOCK_SIZE},
|
||||
use crate::result::ArchiverError;
|
||||
use crossbeam_channel::unbounded;
|
||||
use rand::{thread_rng, Rng};
|
||||
use rand_chacha::{rand_core::SeedableRng, ChaChaRng};
|
||||
use solana_archiver_utils::sample_file;
|
||||
use solana_chacha::chacha::{chacha_cbc_encrypt_ledger, CHACHA_BLOCK_SIZE};
|
||||
use solana_client::{
|
||||
rpc_client::RpcClient, rpc_request::RpcRequest, rpc_response::RpcStorageTurn,
|
||||
thin_client::ThinClient,
|
||||
};
|
||||
use solana_core::{
|
||||
cluster_info::{ClusterInfo, Node, VALIDATOR_PORT_RANGE},
|
||||
contact_info::ContactInfo,
|
||||
gossip_service::GossipService,
|
||||
packet::{limited_deserialize, PACKET_DATA_SIZE},
|
||||
repair_service,
|
||||
repair_service::{RepairService, RepairSlotRange, RepairStrategy},
|
||||
result::{Error, Result},
|
||||
repair_service::{self, RepairService, RepairSlotRange, RepairStats, RepairStrategy},
|
||||
serve_repair::ServeRepair,
|
||||
shred_fetch_stage::ShredFetchStage,
|
||||
sigverify_stage::{DisabledSigVerifier, SigVerifyStage},
|
||||
storage_stage::NUM_STORAGE_SAMPLES,
|
||||
streamer::{receiver, responder, PacketReceiver},
|
||||
window_service::WindowService,
|
||||
};
|
||||
use crossbeam_channel::unbounded;
|
||||
use ed25519_dalek;
|
||||
use rand::{thread_rng, Rng, SeedableRng};
|
||||
use rand_chacha::ChaChaRng;
|
||||
use solana_client::{
|
||||
rpc_client::RpcClient, rpc_request::RpcRequest, rpc_response::RpcStorageTurn,
|
||||
thin_client::ThinClient,
|
||||
};
|
||||
use solana_ledger::{
|
||||
blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache, shred::Shred,
|
||||
};
|
||||
@@ -29,13 +29,13 @@ use solana_perf::packet::Packets;
|
||||
use solana_perf::recycler::Recycler;
|
||||
use solana_sdk::packet::Packet;
|
||||
use solana_sdk::{
|
||||
account_utils::State,
|
||||
account_utils::StateMut,
|
||||
client::{AsyncClient, SyncClient},
|
||||
clock::{get_complete_segment_from_slot, get_segment_from_slot, Slot},
|
||||
commitment_config::CommitmentConfig,
|
||||
hash::{Hash, Hasher},
|
||||
hash::Hash,
|
||||
message::Message,
|
||||
signature::{Keypair, KeypairUtil, Signature},
|
||||
signature::{Keypair, Signature, Signer},
|
||||
timing::timestamp,
|
||||
transaction::Transaction,
|
||||
transport::TransportError,
|
||||
@@ -45,10 +45,8 @@ use solana_storage_program::{
|
||||
storage_instruction::{self, StorageAccountType},
|
||||
};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, ErrorKind, Read, Seek, SeekFrom},
|
||||
mem::size_of,
|
||||
net::{SocketAddr, UdpSocket},
|
||||
io::{self, ErrorKind},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||
path::{Path, PathBuf},
|
||||
result,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
@@ -58,6 +56,8 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, ArchiverError>;
|
||||
|
||||
static ENCRYPTED_FILENAME: &str = "ledger.enc";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -85,47 +85,12 @@ struct ArchiverMeta {
|
||||
client_commitment: CommitmentConfig,
|
||||
}
|
||||
|
||||
pub(crate) fn sample_file(in_path: &Path, sample_offsets: &[u64]) -> io::Result<Hash> {
|
||||
let in_file = File::open(in_path)?;
|
||||
let metadata = in_file.metadata()?;
|
||||
let mut buffer_file = BufReader::new(in_file);
|
||||
|
||||
let mut hasher = Hasher::default();
|
||||
let sample_size = size_of::<Hash>();
|
||||
let sample_size64 = sample_size as u64;
|
||||
let mut buf = vec![0; sample_size];
|
||||
|
||||
let file_len = metadata.len();
|
||||
if file_len < sample_size64 {
|
||||
return Err(io::Error::new(ErrorKind::Other, "file too short!"));
|
||||
}
|
||||
for offset in sample_offsets {
|
||||
if *offset > (file_len - sample_size64) / sample_size64 {
|
||||
return Err(io::Error::new(ErrorKind::Other, "offset too large"));
|
||||
}
|
||||
buffer_file.seek(SeekFrom::Start(*offset * sample_size64))?;
|
||||
trace!("sampling @ {} ", *offset);
|
||||
match buffer_file.read(&mut buf) {
|
||||
Ok(size) => {
|
||||
assert_eq!(size, buf.len());
|
||||
hasher.hash(&buf);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error sampling file");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hasher.result())
|
||||
}
|
||||
|
||||
fn get_slot_from_signature(
|
||||
signature: &ed25519_dalek::Signature,
|
||||
signature: &Signature,
|
||||
storage_turn: u64,
|
||||
slots_per_segment: u64,
|
||||
) -> u64 {
|
||||
let signature_vec = signature.to_bytes();
|
||||
let signature_vec = signature.as_ref();
|
||||
let mut segment_index = u64::from(signature_vec[0])
|
||||
| (u64::from(signature_vec[1]) << 8)
|
||||
| (u64::from(signature_vec[1]) << 16)
|
||||
@@ -229,34 +194,25 @@ impl Archiver {
|
||||
Blockstore::open(ledger_path).expect("Expected to be able to open database ledger"),
|
||||
);
|
||||
|
||||
let gossip_service = GossipService::new(
|
||||
&cluster_info,
|
||||
Some(blockstore.clone()),
|
||||
None,
|
||||
node.sockets.gossip,
|
||||
&exit,
|
||||
);
|
||||
let gossip_service = GossipService::new(&cluster_info, None, node.sockets.gossip, &exit);
|
||||
|
||||
info!("Connecting to the cluster via {:?}", cluster_entrypoint);
|
||||
let (nodes, _) =
|
||||
match crate::gossip_service::discover_cluster(&cluster_entrypoint.gossip, 1) {
|
||||
match solana_core::gossip_service::discover_cluster(&cluster_entrypoint.gossip, 2) {
|
||||
Ok(nodes_and_archivers) => nodes_and_archivers,
|
||||
Err(e) => {
|
||||
//shutdown services before exiting
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
gossip_service.join()?;
|
||||
return Err(Error::from(e));
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
let client = crate::gossip_service::get_client(&nodes);
|
||||
let client = solana_core::gossip_service::get_client(&nodes);
|
||||
|
||||
info!("Setting up mining account...");
|
||||
if let Err(e) = Self::setup_mining_account(
|
||||
&client,
|
||||
&keypair,
|
||||
&storage_keypair,
|
||||
client_commitment.clone(),
|
||||
) {
|
||||
if let Err(e) =
|
||||
Self::setup_mining_account(&client, &keypair, &storage_keypair, client_commitment)
|
||||
{
|
||||
//shutdown services before exiting
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
gossip_service.join()?;
|
||||
@@ -278,6 +234,7 @@ impl Archiver {
|
||||
shred_forward_sockets,
|
||||
repair_socket.clone(),
|
||||
&shred_fetch_sender,
|
||||
None,
|
||||
&exit,
|
||||
);
|
||||
let (slot_sender, slot_receiver) = channel();
|
||||
@@ -398,7 +355,7 @@ impl Archiver {
|
||||
&cluster_info,
|
||||
archiver_keypair,
|
||||
storage_keypair,
|
||||
meta.client_commitment.clone(),
|
||||
meta.client_commitment,
|
||||
);
|
||||
}
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
@@ -411,10 +368,10 @@ impl Archiver {
|
||||
client_commitment: CommitmentConfig,
|
||||
) {
|
||||
let nodes = cluster_info.read().unwrap().tvu_peers();
|
||||
let client = crate::gossip_service::get_client(&nodes);
|
||||
let client = solana_core::gossip_service::get_client(&nodes);
|
||||
|
||||
if let Ok(Some(account)) =
|
||||
client.get_account_with_commitment(&storage_keypair.pubkey(), client_commitment.clone())
|
||||
client.get_account_with_commitment(&storage_keypair.pubkey(), client_commitment)
|
||||
{
|
||||
if let Ok(StorageContract::ArchiverStorage { validations, .. }) = account.state() {
|
||||
if !validations.is_empty() {
|
||||
@@ -422,9 +379,8 @@ impl Archiver {
|
||||
&archiver_keypair.pubkey(),
|
||||
&storage_keypair.pubkey(),
|
||||
);
|
||||
let message =
|
||||
Message::new_with_payer(vec![ix], Some(&archiver_keypair.pubkey()));
|
||||
if let Err(e) = client.send_message(&[&archiver_keypair], message) {
|
||||
let message = Message::new_with_payer(&[ix], Some(&archiver_keypair.pubkey()));
|
||||
if let Err(e) = client.send_message(&[archiver_keypair.as_ref()], message) {
|
||||
error!("unable to redeem reward, tx failed: {:?}", e);
|
||||
} else {
|
||||
info!(
|
||||
@@ -455,7 +411,7 @@ impl Archiver {
|
||||
slot_sender: Sender<u64>,
|
||||
) -> Result<WindowService> {
|
||||
let slots_per_segment =
|
||||
match Self::get_segment_config(&cluster_info, meta.client_commitment.clone()) {
|
||||
match Self::get_segment_config(&cluster_info, meta.client_commitment) {
|
||||
Ok(slots_per_segment) => slots_per_segment,
|
||||
Err(e) => {
|
||||
error!("unable to get segment size configuration, exiting...");
|
||||
@@ -477,13 +433,13 @@ impl Archiver {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let signature = storage_keypair.sign(segment_blockhash.as_ref());
|
||||
let signature = storage_keypair.sign_message(segment_blockhash.as_ref());
|
||||
let slot = get_slot_from_signature(&signature, segment_slot, slots_per_segment);
|
||||
info!("replicating slot: {}", slot);
|
||||
slot_sender.send(slot)?;
|
||||
meta.slot = slot;
|
||||
meta.slots_per_segment = slots_per_segment;
|
||||
meta.signature = Signature::new(&signature.to_bytes());
|
||||
meta.signature = signature;
|
||||
meta.blockhash = segment_blockhash;
|
||||
|
||||
let mut repair_slot_range = RepairSlotRange::default();
|
||||
@@ -621,29 +577,23 @@ impl Archiver {
|
||||
&keypair.pubkey(),
|
||||
&Duration::from_millis(100),
|
||||
&Duration::from_secs(5),
|
||||
client_commitment.clone(),
|
||||
client_commitment,
|
||||
)? == 0
|
||||
{
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::Other, "keypair account has no balance").into(),
|
||||
);
|
||||
return Err(ArchiverError::EmptyStorageAccountBalance);
|
||||
}
|
||||
|
||||
info!("checking storage account keypair...");
|
||||
// check if the storage account exists
|
||||
let balance = client
|
||||
.poll_get_balance_with_commitment(&storage_keypair.pubkey(), client_commitment.clone());
|
||||
let balance =
|
||||
client.poll_get_balance_with_commitment(&storage_keypair.pubkey(), client_commitment);
|
||||
if balance.is_err() || balance.unwrap() == 0 {
|
||||
let blockhash =
|
||||
match client.get_recent_blockhash_with_commitment(client_commitment.clone()) {
|
||||
Ok((blockhash, _)) => blockhash,
|
||||
Err(_) => {
|
||||
return Err(Error::IO(<io::Error>::new(
|
||||
io::ErrorKind::Other,
|
||||
"unable to get recent blockhash, can't submit proof",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let blockhash = match client.get_recent_blockhash_with_commitment(client_commitment) {
|
||||
Ok((blockhash, _)) => blockhash,
|
||||
Err(e) => {
|
||||
return Err(ArchiverError::TransportError(e));
|
||||
}
|
||||
};
|
||||
|
||||
let ix = storage_instruction::create_storage_account(
|
||||
&keypair.pubkey(),
|
||||
@@ -662,6 +612,7 @@ impl Archiver {
|
||||
ErrorKind::Other,
|
||||
"setup_mining_account: signature not found",
|
||||
),
|
||||
TransportError::Custom(e) => io::Error::new(ErrorKind::Other, e),
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -675,33 +626,28 @@ impl Archiver {
|
||||
) {
|
||||
// No point if we've got no storage account...
|
||||
let nodes = cluster_info.read().unwrap().tvu_peers();
|
||||
let client = crate::gossip_service::get_client(&nodes);
|
||||
let storage_balance = client.poll_get_balance_with_commitment(
|
||||
&storage_keypair.pubkey(),
|
||||
meta.client_commitment.clone(),
|
||||
);
|
||||
let client = solana_core::gossip_service::get_client(&nodes);
|
||||
let storage_balance = client
|
||||
.poll_get_balance_with_commitment(&storage_keypair.pubkey(), meta.client_commitment);
|
||||
if storage_balance.is_err() || storage_balance.unwrap() == 0 {
|
||||
error!("Unable to submit mining proof, no storage account");
|
||||
return;
|
||||
}
|
||||
// ...or no lamports for fees
|
||||
let balance = client.poll_get_balance_with_commitment(
|
||||
&archiver_keypair.pubkey(),
|
||||
meta.client_commitment.clone(),
|
||||
);
|
||||
let balance = client
|
||||
.poll_get_balance_with_commitment(&archiver_keypair.pubkey(), meta.client_commitment);
|
||||
if balance.is_err() || balance.unwrap() == 0 {
|
||||
error!("Unable to submit mining proof, insufficient Archiver Account balance");
|
||||
return;
|
||||
}
|
||||
|
||||
let blockhash =
|
||||
match client.get_recent_blockhash_with_commitment(meta.client_commitment.clone()) {
|
||||
Ok((blockhash, _)) => blockhash,
|
||||
Err(_) => {
|
||||
error!("unable to get recent blockhash, can't submit proof");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let blockhash = match client.get_recent_blockhash_with_commitment(meta.client_commitment) {
|
||||
Ok((blockhash, _)) => blockhash,
|
||||
Err(_) => {
|
||||
error!("unable to get recent blockhash, can't submit proof");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let instruction = storage_instruction::mining_proof(
|
||||
&storage_keypair.pubkey(),
|
||||
meta.sha_state,
|
||||
@@ -709,14 +655,14 @@ impl Archiver {
|
||||
Signature::new(&meta.signature.as_ref()),
|
||||
meta.blockhash,
|
||||
);
|
||||
let message = Message::new_with_payer(vec![instruction], Some(&archiver_keypair.pubkey()));
|
||||
let message = Message::new_with_payer(&[instruction], Some(&archiver_keypair.pubkey()));
|
||||
let mut transaction = Transaction::new(
|
||||
&[archiver_keypair.as_ref(), storage_keypair.as_ref()],
|
||||
message,
|
||||
blockhash,
|
||||
);
|
||||
if let Err(err) = client.send_and_confirm_transaction(
|
||||
&[&archiver_keypair, &storage_keypair],
|
||||
&[archiver_keypair.as_ref(), storage_keypair.as_ref()],
|
||||
&mut transaction,
|
||||
10,
|
||||
0,
|
||||
@@ -739,7 +685,7 @@ impl Archiver {
|
||||
fn get_segment_config(
|
||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
||||
client_commitment: CommitmentConfig,
|
||||
) -> result::Result<u64, Error> {
|
||||
) -> Result<u64> {
|
||||
let rpc_peers = {
|
||||
let cluster_info = cluster_info.read().unwrap();
|
||||
cluster_info.all_rpc_peers()
|
||||
@@ -758,12 +704,12 @@ impl Archiver {
|
||||
)
|
||||
.map_err(|err| {
|
||||
warn!("Error while making rpc request {:?}", err);
|
||||
Error::IO(io::Error::new(ErrorKind::Other, "rpc error"))
|
||||
ArchiverError::ClientError(err)
|
||||
})?
|
||||
.as_u64()
|
||||
.unwrap())
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "No RPC peers...".to_string()).into())
|
||||
Err(ArchiverError::NoRpcPeers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,7 +719,7 @@ impl Archiver {
|
||||
slots_per_segment: u64,
|
||||
previous_blockhash: &Hash,
|
||||
exit: &Arc<AtomicBool>,
|
||||
) -> result::Result<(Hash, u64), Error> {
|
||||
) -> Result<(Hash, u64)> {
|
||||
loop {
|
||||
let (blockhash, turn_slot) = Self::poll_for_blockhash_and_slot(
|
||||
cluster_info,
|
||||
@@ -793,7 +739,7 @@ impl Archiver {
|
||||
slots_per_segment: u64,
|
||||
previous_blockhash: &Hash,
|
||||
exit: &Arc<AtomicBool>,
|
||||
) -> result::Result<(Hash, u64), Error> {
|
||||
) -> Result<(Hash, u64)> {
|
||||
info!("waiting for the next turn...");
|
||||
loop {
|
||||
let rpc_peers = {
|
||||
@@ -814,17 +760,13 @@ impl Archiver {
|
||||
)
|
||||
.map_err(|err| {
|
||||
warn!("Error while making rpc request {:?}", err);
|
||||
Error::IO(io::Error::new(ErrorKind::Other, "rpc error"))
|
||||
ArchiverError::ClientError(err)
|
||||
})?;
|
||||
let RpcStorageTurn {
|
||||
blockhash: storage_blockhash,
|
||||
slot: turn_slot,
|
||||
} = serde_json::from_value::<RpcStorageTurn>(response).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Couldn't parse response: {:?}", err),
|
||||
)
|
||||
})?;
|
||||
} = serde_json::from_value::<RpcStorageTurn>(response)
|
||||
.map_err(ArchiverError::JsonError)?;
|
||||
let turn_blockhash = storage_blockhash.parse().map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
@@ -842,7 +784,7 @@ impl Archiver {
|
||||
}
|
||||
}
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
return Err(Error::IO(io::Error::new(
|
||||
return Err(ArchiverError::IO(io::Error::new(
|
||||
ErrorKind::Other,
|
||||
"exit signalled...",
|
||||
)));
|
||||
@@ -857,19 +799,20 @@ impl Archiver {
|
||||
/// It is recommended to use a temporary blockstore for this since the download will not verify
|
||||
/// shreds received and might impact the chaining of shreds across slots
|
||||
pub fn download_from_archiver(
|
||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
||||
serve_repair: &ServeRepair,
|
||||
archiver_info: &ContactInfo,
|
||||
blockstore: &Arc<Blockstore>,
|
||||
slots_per_segment: u64,
|
||||
) -> Result<u64> {
|
||||
let ip_addr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
|
||||
// Create a client which downloads from the archiver and see that it
|
||||
// can respond with shreds.
|
||||
let start_slot = Self::get_archiver_segment_slot(archiver_info.storage_addr);
|
||||
let start_slot = Self::get_archiver_segment_slot(ip_addr, archiver_info.storage_addr);
|
||||
info!("Archiver download: start at {}", start_slot);
|
||||
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
let (s_reader, r_reader) = channel();
|
||||
let repair_socket = Arc::new(bind_in_range(VALIDATOR_PORT_RANGE).unwrap().1);
|
||||
let repair_socket = Arc::new(bind_in_range(ip_addr, VALIDATOR_PORT_RANGE).unwrap().1);
|
||||
let t_receiver = receiver(
|
||||
repair_socket.clone(),
|
||||
&exit,
|
||||
@@ -877,10 +820,10 @@ impl Archiver {
|
||||
Recycler::default(),
|
||||
"archiver_reeciver",
|
||||
);
|
||||
let id = cluster_info.read().unwrap().id();
|
||||
let id = serve_repair.keypair().pubkey();
|
||||
info!(
|
||||
"Sending repair requests from: {} to: {}",
|
||||
cluster_info.read().unwrap().my_data().id,
|
||||
serve_repair.my_info().id,
|
||||
archiver_info.gossip
|
||||
);
|
||||
let repair_slot_range = RepairSlotRange {
|
||||
@@ -895,15 +838,14 @@ impl Archiver {
|
||||
repair_service::MAX_REPAIR_LENGTH,
|
||||
&repair_slot_range,
|
||||
);
|
||||
let mut repair_stats = RepairStats::default();
|
||||
//iter over the repairs and send them
|
||||
if let Ok(repairs) = repairs {
|
||||
let reqs: Vec<_> = repairs
|
||||
.into_iter()
|
||||
.filter_map(|repair_request| {
|
||||
cluster_info
|
||||
.read()
|
||||
.unwrap()
|
||||
.map_repair_request(&repair_request)
|
||||
serve_repair
|
||||
.map_repair_request(&repair_request, &mut repair_stats, Some(0))
|
||||
.map(|result| ((archiver_info.gossip, result), repair_request))
|
||||
.ok()
|
||||
})
|
||||
@@ -950,9 +892,7 @@ impl Archiver {
|
||||
|
||||
// check if all the slots in the segment are complete
|
||||
if !Self::segment_complete(start_slot, slots_per_segment, blockstore) {
|
||||
return Err(
|
||||
io::Error::new(ErrorKind::Other, "Unable to download the full segment").into(),
|
||||
);
|
||||
return Err(ArchiverError::SegmentDownloadError);
|
||||
}
|
||||
Ok(start_slot)
|
||||
}
|
||||
@@ -970,8 +910,8 @@ impl Archiver {
|
||||
true
|
||||
}
|
||||
|
||||
fn get_archiver_segment_slot(to: SocketAddr) -> u64 {
|
||||
let (_port, socket) = bind_in_range(VALIDATOR_PORT_RANGE).unwrap();
|
||||
fn get_archiver_segment_slot(bind_ip_addr: IpAddr, to: SocketAddr) -> u64 {
|
||||
let (_port, socket) = bind_in_range(bind_ip_addr, VALIDATOR_PORT_RANGE).unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
@@ -995,74 +935,3 @@ impl Archiver {
|
||||
panic!("Couldn't get segment slot from archiver!");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs::{create_dir_all, remove_file};
|
||||
use std::io::Write;
|
||||
|
||||
fn tmp_file_path(name: &str) -> PathBuf {
|
||||
use std::env;
|
||||
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
|
||||
let keypair = Keypair::new();
|
||||
|
||||
let mut path = PathBuf::new();
|
||||
path.push(out_dir);
|
||||
path.push("tmp");
|
||||
create_dir_all(&path).unwrap();
|
||||
|
||||
path.push(format!("{}-{}", name, keypair.pubkey()));
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_file() {
|
||||
solana_logger::setup();
|
||||
let in_path = tmp_file_path("test_sample_file_input.txt");
|
||||
let num_strings = 4096;
|
||||
let string = "12foobar";
|
||||
{
|
||||
let mut in_file = File::create(&in_path).unwrap();
|
||||
for _ in 0..num_strings {
|
||||
in_file.write(string.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
let num_samples = (string.len() * num_strings / size_of::<Hash>()) as u64;
|
||||
let samples: Vec<_> = (0..num_samples).collect();
|
||||
let res = sample_file(&in_path, samples.as_slice());
|
||||
let ref_hash: Hash = Hash::new(&[
|
||||
173, 251, 182, 165, 10, 54, 33, 150, 133, 226, 106, 150, 99, 192, 179, 1, 230, 144,
|
||||
151, 126, 18, 191, 54, 67, 249, 140, 230, 160, 56, 30, 170, 52,
|
||||
]);
|
||||
let res = res.unwrap();
|
||||
assert_eq!(res, ref_hash);
|
||||
|
||||
// Sample just past the end
|
||||
assert!(sample_file(&in_path, &[num_samples]).is_err());
|
||||
remove_file(&in_path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_file_invalid_offset() {
|
||||
let in_path = tmp_file_path("test_sample_file_invalid_offset_input.txt");
|
||||
{
|
||||
let mut in_file = File::create(&in_path).unwrap();
|
||||
for _ in 0..4096 {
|
||||
in_file.write("123456foobar".as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
let samples = [0, 200000];
|
||||
let res = sample_file(&in_path, &samples);
|
||||
assert!(res.is_err());
|
||||
remove_file(in_path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_file_missing_file() {
|
||||
let in_path = tmp_file_path("test_sample_file_that_doesnt_exist.txt");
|
||||
let samples = [0, 5];
|
||||
let res = sample_file(&in_path, &samples);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
11
archiver-lib/src/lib.rs
Normal file
11
archiver-lib/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
#[macro_use]
|
||||
extern crate solana_metrics;
|
||||
|
||||
pub mod archiver;
|
||||
mod result;
|
47
archiver-lib/src/result.rs
Normal file
47
archiver-lib/src/result.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use solana_client::client_error;
|
||||
use solana_ledger::blockstore;
|
||||
use solana_sdk::transport;
|
||||
use std::any::Any;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ArchiverError {
|
||||
#[error("IO error")]
|
||||
IO(#[from] std::io::Error),
|
||||
|
||||
#[error("blockstore error")]
|
||||
BlockstoreError(#[from] blockstore::BlockstoreError),
|
||||
|
||||
#[error("crossbeam error")]
|
||||
CrossbeamSendError(#[from] crossbeam_channel::SendError<u64>),
|
||||
|
||||
#[error("send error")]
|
||||
SendError(#[from] std::sync::mpsc::SendError<u64>),
|
||||
|
||||
#[error("join error")]
|
||||
JoinError(Box<dyn Any + Send + 'static>),
|
||||
|
||||
#[error("transport error")]
|
||||
TransportError(#[from] transport::TransportError),
|
||||
|
||||
#[error("client error")]
|
||||
ClientError(#[from] client_error::ClientError),
|
||||
|
||||
#[error("Json parsing error")]
|
||||
JsonError(#[from] serde_json::error::Error),
|
||||
|
||||
#[error("Storage account has no balance")]
|
||||
EmptyStorageAccountBalance,
|
||||
|
||||
#[error("No RPC peers..")]
|
||||
NoRpcPeers,
|
||||
|
||||
#[error("Couldn't download full segment")]
|
||||
SegmentDownloadError,
|
||||
}
|
||||
|
||||
impl std::convert::From<Box<dyn Any + Send + 'static>> for ArchiverError {
|
||||
fn from(e: Box<dyn Any + Send + 'static>) -> ArchiverError {
|
||||
ArchiverError::JoinError(e)
|
||||
}
|
||||
}
|
28
archiver-utils/Cargo.toml
Normal file
28
archiver-utils/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "solana-archiver-utils"
|
||||
version = "1.0.23"
|
||||
description = "Solana Archiver Utils"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
rand = "0.7.0"
|
||||
solana-chacha = { path = "../chacha", version = "1.0.23" }
|
||||
solana-chacha-sys = { path = "../chacha-sys", version = "1.0.23" }
|
||||
solana-ledger = { path = "../ledger", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-perf = { path = "../perf", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4.0"
|
||||
|
||||
[lib]
|
||||
name = "solana_archiver_utils"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
120
archiver-utils/src/lib.rs
Normal file
120
archiver-utils/src/lib.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use solana_sdk::hash::{Hash, Hasher};
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufReader, ErrorKind, Read, Seek, SeekFrom};
|
||||
use std::mem::size_of;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn sample_file(in_path: &Path, sample_offsets: &[u64]) -> io::Result<Hash> {
|
||||
let in_file = File::open(in_path)?;
|
||||
let metadata = in_file.metadata()?;
|
||||
let mut buffer_file = BufReader::new(in_file);
|
||||
|
||||
let mut hasher = Hasher::default();
|
||||
let sample_size = size_of::<Hash>();
|
||||
let sample_size64 = sample_size as u64;
|
||||
let mut buf = vec![0; sample_size];
|
||||
|
||||
let file_len = metadata.len();
|
||||
if file_len < sample_size64 {
|
||||
return Err(io::Error::new(ErrorKind::Other, "file too short!"));
|
||||
}
|
||||
for offset in sample_offsets {
|
||||
if *offset > (file_len - sample_size64) / sample_size64 {
|
||||
return Err(io::Error::new(ErrorKind::Other, "offset too large"));
|
||||
}
|
||||
buffer_file.seek(SeekFrom::Start(*offset * sample_size64))?;
|
||||
trace!("sampling @ {} ", *offset);
|
||||
match buffer_file.read(&mut buf) {
|
||||
Ok(size) => {
|
||||
assert_eq!(size, buf.len());
|
||||
hasher.hash(&buf);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error sampling file");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hasher.result())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::fs::{create_dir_all, remove_file};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
extern crate hex;
|
||||
|
||||
fn tmp_file_path(name: &str) -> PathBuf {
|
||||
use std::env;
|
||||
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
|
||||
let mut rand_bits = [0u8; 32];
|
||||
thread_rng().fill(&mut rand_bits[..]);
|
||||
|
||||
let mut path = PathBuf::new();
|
||||
path.push(out_dir);
|
||||
path.push("tmp");
|
||||
create_dir_all(&path).unwrap();
|
||||
|
||||
path.push(format!("{}-{:?}", name, hex::encode(rand_bits)));
|
||||
println!("path: {:?}", path);
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_file() {
|
||||
solana_logger::setup();
|
||||
let in_path = tmp_file_path("test_sample_file_input.txt");
|
||||
let num_strings = 4096;
|
||||
let string = "12foobar";
|
||||
{
|
||||
let mut in_file = File::create(&in_path).unwrap();
|
||||
for _ in 0..num_strings {
|
||||
in_file.write(string.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
let num_samples = (string.len() * num_strings / size_of::<Hash>()) as u64;
|
||||
let samples: Vec<_> = (0..num_samples).collect();
|
||||
let res = sample_file(&in_path, samples.as_slice());
|
||||
let ref_hash: Hash = Hash::new(&[
|
||||
173, 251, 182, 165, 10, 54, 33, 150, 133, 226, 106, 150, 99, 192, 179, 1, 230, 144,
|
||||
151, 126, 18, 191, 54, 67, 249, 140, 230, 160, 56, 30, 170, 52,
|
||||
]);
|
||||
let res = res.unwrap();
|
||||
assert_eq!(res, ref_hash);
|
||||
|
||||
// Sample just past the end
|
||||
assert!(sample_file(&in_path, &[num_samples]).is_err());
|
||||
remove_file(&in_path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_file_invalid_offset() {
|
||||
let in_path = tmp_file_path("test_sample_file_invalid_offset_input.txt");
|
||||
{
|
||||
let mut in_file = File::create(&in_path).unwrap();
|
||||
for _ in 0..4096 {
|
||||
in_file.write("123456foobar".as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
let samples = [0, 200000];
|
||||
let res = sample_file(&in_path, &samples);
|
||||
assert!(res.is_err());
|
||||
remove_file(in_path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_file_missing_file() {
|
||||
let in_path = tmp_file_path("test_sample_file_that_doesnt_exist.txt");
|
||||
let samples = [0, 5];
|
||||
let res = sample_file(&in_path, &samples);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
@@ -2,18 +2,22 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-archiver"
|
||||
version = "0.22.10"
|
||||
version = "1.0.23"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
console = "0.9.1"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "0.22.10" }
|
||||
solana-core = { path = "../core", version = "0.22.10" }
|
||||
solana-logger = { path = "../logger", version = "0.22.10" }
|
||||
solana-metrics = { path = "../metrics", version = "0.22.10" }
|
||||
solana-net-utils = { path = "../net-utils", version = "0.22.10" }
|
||||
solana-sdk = { path = "../sdk", version = "0.22.10" }
|
||||
console = "0.9.2"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.23" }
|
||||
solana-core = { path = "../core", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-metrics = { path = "../metrics", version = "1.0.23" }
|
||||
solana-archiver-lib = { path = "../archiver-lib", version = "1.0.23" }
|
||||
solana-net-utils = { path = "../net-utils", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -1,19 +1,23 @@
|
||||
use clap::{crate_description, crate_name, App, Arg};
|
||||
use console::style;
|
||||
use solana_archiver_lib::archiver::Archiver;
|
||||
use solana_clap_utils::{
|
||||
input_validators::is_keypair,
|
||||
keypair::{
|
||||
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
|
||||
SKIP_SEED_PHRASE_VALIDATION_ARG,
|
||||
},
|
||||
input_parsers::keypair_of, input_validators::is_keypair_or_ask_keyword,
|
||||
keypair::SKIP_SEED_PHRASE_VALIDATION_ARG,
|
||||
};
|
||||
use solana_core::{
|
||||
archiver::Archiver,
|
||||
cluster_info::{Node, VALIDATOR_PORT_RANGE},
|
||||
contact_info::ContactInfo,
|
||||
};
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, signature::KeypairUtil};
|
||||
use std::{net::SocketAddr, path::PathBuf, process::exit, sync::Arc};
|
||||
use solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
signature::{Keypair, Signer},
|
||||
};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup();
|
||||
@@ -24,10 +28,10 @@ fn main() {
|
||||
.arg(
|
||||
Arg::with_name("identity_keypair")
|
||||
.short("i")
|
||||
.long("identity-keypair")
|
||||
.long("identity")
|
||||
.value_name("PATH")
|
||||
.takes_value(true)
|
||||
.validator(is_keypair)
|
||||
.validator(is_keypair_or_ask_keyword)
|
||||
.help("File containing an identity (keypair)"),
|
||||
)
|
||||
.arg(
|
||||
@@ -55,48 +59,27 @@ fn main() {
|
||||
.long("storage-keypair")
|
||||
.value_name("PATH")
|
||||
.takes_value(true)
|
||||
.validator(is_keypair)
|
||||
.validator(is_keypair_or_ask_keyword)
|
||||
.help("File containing the storage account keypair"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(ASK_SEED_PHRASE_ARG.name)
|
||||
.long(ASK_SEED_PHRASE_ARG.long)
|
||||
.value_name("KEYPAIR NAME")
|
||||
.multiple(true)
|
||||
.takes_value(true)
|
||||
.possible_values(&["identity-keypair", "storage-keypair"])
|
||||
.help(ASK_SEED_PHRASE_ARG.help),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
|
||||
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
|
||||
.requires(ASK_SEED_PHRASE_ARG.name)
|
||||
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let ledger_path = PathBuf::from(matches.value_of("ledger").unwrap());
|
||||
|
||||
let identity_keypair = keypair_input(&matches, "identity_keypair")
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("Identity keypair input failed: {}", err);
|
||||
exit(1);
|
||||
})
|
||||
.keypair;
|
||||
let KeypairWithSource {
|
||||
keypair: storage_keypair,
|
||||
source: storage_keypair_source,
|
||||
} = keypair_input(&matches, "storage_keypair").unwrap_or_else(|err| {
|
||||
eprintln!("Storage keypair input failed: {}", err);
|
||||
exit(1);
|
||||
});
|
||||
if storage_keypair_source == keypair::Source::Generated {
|
||||
let identity_keypair = keypair_of(&matches, "identity_keypair").unwrap_or_else(Keypair::new);
|
||||
|
||||
let storage_keypair = keypair_of(&matches, "storage_keypair").unwrap_or_else(|| {
|
||||
clap::Error::with_description(
|
||||
"The `storage-keypair` argument was not found",
|
||||
clap::ErrorKind::ArgumentNotFound,
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
});
|
||||
|
||||
let entrypoint_addr = matches
|
||||
.value_of("entrypoint")
|
||||
@@ -116,6 +99,7 @@ fn main() {
|
||||
&identity_keypair.pubkey(),
|
||||
&gossip_addr,
|
||||
VALIDATOR_PORT_RANGE,
|
||||
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
||||
);
|
||||
|
||||
println!(
|
||||
|
@@ -2,7 +2,7 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-banking-bench"
|
||||
version = "0.22.10"
|
||||
version = "1.0.23"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -10,11 +10,14 @@ homepage = "https://solana.com/"
|
||||
[dependencies]
|
||||
log = "0.4.6"
|
||||
rayon = "1.2.0"
|
||||
solana-core = { path = "../core", version = "0.22.10" }
|
||||
solana-ledger = { path = "../ledger", version = "0.22.10" }
|
||||
solana-logger = { path = "../logger", version = "0.22.10" }
|
||||
solana-runtime = { path = "../runtime", version = "0.22.10" }
|
||||
solana-measure = { path = "../measure", version = "0.22.10" }
|
||||
solana-sdk = { path = "../sdk", version = "0.22.10" }
|
||||
rand = "0.6.5"
|
||||
solana-core = { path = "../core", version = "1.0.23" }
|
||||
solana-ledger = { path = "../ledger", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-runtime = { path = "../runtime", version = "1.0.23" }
|
||||
solana-measure = { path = "../measure", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
rand = "0.7.0"
|
||||
crossbeam-channel = "0.3"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -162,8 +162,8 @@ fn main() {
|
||||
// If it is dropped before poh_service, then poh_service will error when
|
||||
// calling send() on the channel.
|
||||
let signal_receiver = Arc::new(signal_receiver);
|
||||
let mut total = 0;
|
||||
let mut tx_total = 0;
|
||||
let mut total_us = 0;
|
||||
let mut tx_total_us = 0;
|
||||
let mut txs_processed = 0;
|
||||
let mut root = 1;
|
||||
let collector = Pubkey::new_rand();
|
||||
@@ -173,6 +173,7 @@ fn main() {
|
||||
chunk_len,
|
||||
num_threads,
|
||||
};
|
||||
let mut total_sent = 0;
|
||||
for _ in 0..ITERS {
|
||||
let now = Instant::now();
|
||||
let mut sent = 0;
|
||||
@@ -223,7 +224,7 @@ fn main() {
|
||||
);
|
||||
assert!(txs_processed < bank.transaction_count());
|
||||
txs_processed = bank.transaction_count();
|
||||
tx_total += duration_as_us(&now.elapsed());
|
||||
tx_total_us += duration_as_us(&now.elapsed());
|
||||
|
||||
let mut poh_time = Measure::start("poh_time");
|
||||
poh_recorder.lock().unwrap().reset(
|
||||
@@ -245,7 +246,7 @@ fn main() {
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
assert!(poh_recorder.lock().unwrap().bank().is_some());
|
||||
if bank.slot() > 32 {
|
||||
bank_forks.set_root(root, &None);
|
||||
bank_forks.set_root(root, &None, None);
|
||||
root += 1;
|
||||
}
|
||||
debug!(
|
||||
@@ -255,20 +256,21 @@ fn main() {
|
||||
poh_time.as_us(),
|
||||
);
|
||||
} else {
|
||||
tx_total += duration_as_us(&now.elapsed());
|
||||
tx_total_us += duration_as_us(&now.elapsed());
|
||||
}
|
||||
|
||||
// This signature clear may not actually clear the signatures
|
||||
// in this chunk, but since we rotate between CHUNKS then
|
||||
// we should clear them by the time we come around again to re-use that chunk.
|
||||
bank.clear_signatures();
|
||||
total += duration_as_us(&now.elapsed());
|
||||
total_us += duration_as_us(&now.elapsed());
|
||||
debug!(
|
||||
"time: {} us checked: {} sent: {}",
|
||||
duration_as_us(&now.elapsed()),
|
||||
txes / CHUNKS,
|
||||
sent,
|
||||
);
|
||||
total_sent += sent;
|
||||
|
||||
if bank.slot() > 0 && bank.slot() % 16 == 0 {
|
||||
for tx in transactions.iter_mut() {
|
||||
@@ -284,11 +286,11 @@ fn main() {
|
||||
}
|
||||
eprintln!(
|
||||
"{{'name': 'banking_bench_total', 'median': '{}'}}",
|
||||
total / ITERS as u64,
|
||||
(1000.0 * 1000.0 * total_sent as f64) / (total_us as f64),
|
||||
);
|
||||
eprintln!(
|
||||
"{{'name': 'banking_bench_tx_total', 'median': '{}'}}",
|
||||
tx_total / ITERS as u64,
|
||||
(1000.0 * 1000.0 * total_sent as f64) / (tx_total_us as f64),
|
||||
);
|
||||
|
||||
drop(verified_sender);
|
||||
|
@@ -2,40 +2,36 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-exchange"
|
||||
version = "0.22.10"
|
||||
version = "1.0.23"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.2.1"
|
||||
bs58 = "0.3.0"
|
||||
clap = "2.32.0"
|
||||
env_logger = "0.7.1"
|
||||
itertools = "0.8.2"
|
||||
log = "0.4.8"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
rand = "0.6.5"
|
||||
rand = "0.7.0"
|
||||
rayon = "1.2.0"
|
||||
serde = "1.0.104"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.44"
|
||||
serde_json = "1.0.46"
|
||||
serde_yaml = "0.8.11"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "0.22.10" }
|
||||
solana-core = { path = "../core", version = "0.22.10" }
|
||||
solana-genesis = { path = "../genesis", version = "0.22.10" }
|
||||
solana-client = { path = "../client", version = "0.22.10" }
|
||||
solana-faucet = { path = "../faucet", version = "0.22.10" }
|
||||
solana-exchange-program = { path = "../programs/exchange", version = "0.22.10" }
|
||||
solana-logger = { path = "../logger", version = "0.22.10" }
|
||||
solana-metrics = { path = "../metrics", version = "0.22.10" }
|
||||
solana-net-utils = { path = "../net-utils", version = "0.22.10" }
|
||||
solana-runtime = { path = "../runtime", version = "0.22.10" }
|
||||
solana-sdk = { path = "../sdk", version = "0.22.10" }
|
||||
untrusted = "0.7.0"
|
||||
ws = "0.9.1"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.23" }
|
||||
solana-core = { path = "../core", version = "1.0.23" }
|
||||
solana-genesis = { path = "../genesis", version = "1.0.23" }
|
||||
solana-client = { path = "../client", version = "1.0.23" }
|
||||
solana-faucet = { path = "../faucet", version = "1.0.23" }
|
||||
solana-exchange-program = { path = "../programs/exchange", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-metrics = { path = "../metrics", version = "1.0.23" }
|
||||
solana-net-utils = { path = "../net-utils", version = "1.0.23" }
|
||||
solana-runtime = { path = "../runtime", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-local-cluster = { path = "../local-cluster", version = "0.22.10" }
|
||||
solana-local-cluster = { path = "../local-cluster", version = "1.0.23" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -15,7 +15,7 @@ use solana_sdk::{
|
||||
client::{Client, SyncClient},
|
||||
commitment_config::CommitmentConfig,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, KeypairUtil},
|
||||
signature::{Keypair, Signer},
|
||||
timing::{duration_as_ms, duration_as_s},
|
||||
transaction::Transaction,
|
||||
{system_instruction, system_program},
|
||||
@@ -701,7 +701,7 @@ fn verify_funding_transfer<T: SyncClient + ?Sized>(
|
||||
false
|
||||
}
|
||||
|
||||
pub fn fund_keys(client: &dyn Client, source: &Keypair, dests: &[Arc<Keypair>], lamports: u64) {
|
||||
pub fn fund_keys<T: Client>(client: &T, source: &Keypair, dests: &[Arc<Keypair>], lamports: u64) {
|
||||
let total = lamports * (dests.len() as u64 + 1);
|
||||
let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)];
|
||||
let mut notfunded: Vec<&Arc<Keypair>> = dests.iter().collect();
|
||||
@@ -824,7 +824,11 @@ pub fn fund_keys(client: &dyn Client, source: &Keypair, dests: &[Arc<Keypair>],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_token_accounts(client: &dyn Client, signers: &[Arc<Keypair>], accounts: &[Keypair]) {
|
||||
pub fn create_token_accounts<T: Client>(
|
||||
client: &T,
|
||||
signers: &[Arc<Keypair>],
|
||||
accounts: &[Keypair],
|
||||
) {
|
||||
let mut notfunded: Vec<(&Arc<Keypair>, &Keypair)> = signers.iter().zip(accounts).collect();
|
||||
|
||||
while !notfunded.is_empty() {
|
||||
@@ -968,7 +972,12 @@ fn generate_keypairs(num: u64) -> Vec<Keypair> {
|
||||
rnd.gen_n_keypairs(num)
|
||||
}
|
||||
|
||||
pub fn airdrop_lamports(client: &dyn Client, faucet_addr: &SocketAddr, id: &Keypair, amount: u64) {
|
||||
pub fn airdrop_lamports<T: Client>(
|
||||
client: &T,
|
||||
faucet_addr: &SocketAddr,
|
||||
id: &Keypair,
|
||||
amount: u64,
|
||||
) {
|
||||
let balance = client.get_balance_with_commitment(&id.pubkey(), CommitmentConfig::recent());
|
||||
let balance = balance.unwrap_or(0);
|
||||
if balance >= amount {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use clap::{crate_description, crate_name, value_t, App, Arg, ArgMatches};
|
||||
use solana_core::gen_keys::GenKeys;
|
||||
use solana_faucet::faucet::FAUCET_PORT;
|
||||
use solana_sdk::signature::{read_keypair_file, Keypair, KeypairUtil};
|
||||
use solana_sdk::signature::{read_keypair_file, Keypair};
|
||||
use std::net::SocketAddr;
|
||||
use std::process::exit;
|
||||
use std::time::Duration;
|
||||
|
@@ -5,7 +5,7 @@ pub mod order_book;
|
||||
use crate::bench::{airdrop_lamports, create_client_accounts_file, do_bench_exchange, Config};
|
||||
use log::*;
|
||||
use solana_core::gossip_service::{discover_cluster, get_multi_client};
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
use solana_sdk::signature::Signer;
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup();
|
||||
|
@@ -10,12 +10,13 @@ use solana_local_cluster::local_cluster::{ClusterConfig, LocalCluster};
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::genesis_config::create_genesis_config;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use std::process::exit;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_exchange_local_cluster() {
|
||||
solana_logger::setup();
|
||||
|
||||
|
@@ -2,14 +2,17 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-streamer"
|
||||
version = "0.22.10"
|
||||
version = "1.0.23"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "0.22.10" }
|
||||
solana-core = { path = "../core", version = "0.22.10" }
|
||||
solana-logger = { path = "../logger", version = "0.22.10" }
|
||||
solana-net-utils = { path = "../net-utils", version = "0.22.10" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.23" }
|
||||
solana-core = { path = "../core", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-net-utils = { path = "../net-utils", version = "1.0.23" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -1,6 +1,5 @@
|
||||
use clap::{crate_description, crate_name, App, Arg};
|
||||
use solana_core::packet::{Packet, Packets, PacketsRecycler, PACKET_DATA_SIZE};
|
||||
use solana_core::result::Result;
|
||||
use solana_core::streamer::{receiver, PacketReceiver};
|
||||
use std::cmp::max;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
||||
@@ -8,7 +7,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
use std::thread::sleep;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
use std::thread::{spawn, JoinHandle, Result};
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
|
||||
@@ -68,7 +67,8 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
let mut port = 0;
|
||||
let mut addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
|
||||
let ip_addr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
|
||||
let mut addr = SocketAddr::new(ip_addr, 0);
|
||||
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
|
||||
@@ -76,7 +76,7 @@ fn main() -> Result<()> {
|
||||
let mut read_threads = Vec::new();
|
||||
let recycler = PacketsRecycler::default();
|
||||
for _ in 0..num_sockets {
|
||||
let read = solana_net_utils::bind_to(port, false).unwrap();
|
||||
let read = solana_net_utils::bind_to(ip_addr, port, false).unwrap();
|
||||
read.set_read_timeout(Some(Duration::new(1, 0))).unwrap();
|
||||
|
||||
addr = read.local_addr().unwrap();
|
||||
|
@@ -2,7 +2,7 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-tps"
|
||||
version = "0.22.10"
|
||||
version = "1.0.23"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -12,28 +12,29 @@ bincode = "1.2.1"
|
||||
clap = "2.33.0"
|
||||
log = "0.4.8"
|
||||
rayon = "1.2.0"
|
||||
serde = "1.0.104"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.44"
|
||||
serde_json = "1.0.46"
|
||||
serde_yaml = "0.8.11"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "0.22.10" }
|
||||
solana-core = { path = "../core", version = "0.22.10" }
|
||||
solana-genesis = { path = "../genesis", version = "0.22.10" }
|
||||
solana-client = { path = "../client", version = "0.22.10" }
|
||||
solana-faucet = { path = "../faucet", version = "0.22.10" }
|
||||
solana-librapay = { path = "../programs/librapay", version = "0.22.10", optional = true }
|
||||
solana-logger = { path = "../logger", version = "0.22.10" }
|
||||
solana-metrics = { path = "../metrics", version = "0.22.10" }
|
||||
solana-measure = { path = "../measure", version = "0.22.10" }
|
||||
solana-net-utils = { path = "../net-utils", version = "0.22.10" }
|
||||
solana-runtime = { path = "../runtime", version = "0.22.10" }
|
||||
solana-sdk = { path = "../sdk", version = "0.22.10" }
|
||||
solana-move-loader-program = { path = "../programs/move_loader", version = "0.22.10", optional = true }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.23" }
|
||||
solana-core = { path = "../core", version = "1.0.23" }
|
||||
solana-genesis = { path = "../genesis", version = "1.0.23" }
|
||||
solana-client = { path = "../client", version = "1.0.23" }
|
||||
solana-faucet = { path = "../faucet", version = "1.0.23" }
|
||||
#solana-librapay = { path = "../programs/librapay", version = "1.0.20", optional = true }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-metrics = { path = "../metrics", version = "1.0.23" }
|
||||
solana-measure = { path = "../measure", version = "1.0.23" }
|
||||
solana-net-utils = { path = "../net-utils", version = "1.0.23" }
|
||||
solana-runtime = { path = "../runtime", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
#solana-move-loader-program = { path = "../programs/move_loader", version = "1.0.20", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "0.3.2"
|
||||
serial_test_derive = "0.3.1"
|
||||
solana-local-cluster = { path = "../local-cluster", version = "0.22.10" }
|
||||
serial_test_derive = "0.4.0"
|
||||
solana-local-cluster = { path = "../local-cluster", version = "1.0.23" }
|
||||
|
||||
[features]
|
||||
move = ["solana-librapay", "solana-move-loader-program"]
|
||||
#[features]
|
||||
#move = ["solana-librapay", "solana-move-loader-program"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -7,7 +7,7 @@ use solana_faucet::faucet::request_airdrop_transaction;
|
||||
#[cfg(feature = "move")]
|
||||
use solana_librapay::{create_genesis, upload_mint_script, upload_payment_script};
|
||||
use solana_measure::measure::Measure;
|
||||
use solana_metrics::{self, datapoint_debug};
|
||||
use solana_metrics::{self, datapoint_info};
|
||||
use solana_sdk::{
|
||||
client::Client,
|
||||
clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE},
|
||||
@@ -15,7 +15,7 @@ use solana_sdk::{
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, KeypairUtil},
|
||||
signature::{Keypair, Signer},
|
||||
system_instruction, system_transaction,
|
||||
timing::{duration_as_ms, duration_as_s, duration_as_us, timestamp},
|
||||
transaction::Transaction,
|
||||
@@ -187,7 +187,9 @@ where
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
} else {
|
||||
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
|
||||
while !shared_txs.read().unwrap().is_empty()
|
||||
|| shared_tx_active_thread_count.load(Ordering::Relaxed) > 0
|
||||
{
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
@@ -242,7 +244,7 @@ where
|
||||
|
||||
fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
||||
info!("Token balance: {}", lamport_balance);
|
||||
datapoint_debug!(
|
||||
datapoint_info!(
|
||||
"bench-tps-lamport_balance",
|
||||
("balance", lamport_balance, i64)
|
||||
);
|
||||
@@ -373,7 +375,7 @@ fn generate_txs(
|
||||
duration_as_ms(&duration),
|
||||
blockhash,
|
||||
);
|
||||
datapoint_debug!(
|
||||
datapoint_info!(
|
||||
"bench-tps-generate_txs",
|
||||
("duration", duration_as_us(&duration), i64)
|
||||
);
|
||||
@@ -479,7 +481,7 @@ fn do_tx_transfers<T: Client>(
|
||||
duration_as_ms(&transfer_start.elapsed()),
|
||||
tx_len as f32 / duration_as_s(&transfer_start.elapsed()),
|
||||
);
|
||||
datapoint_debug!(
|
||||
datapoint_info!(
|
||||
"bench-tps-do_tx_transfers",
|
||||
("duration", duration_as_us(&transfer_start.elapsed()), i64),
|
||||
("count", tx_len, i64)
|
||||
@@ -1057,8 +1059,8 @@ pub fn generate_and_fund_keypairs<T: 'static + Client + Send + Sync>(
|
||||
// pay for the transaction fees in a new run.
|
||||
let enough_lamports = 8 * lamports_per_account / 10;
|
||||
if first_keypair_balance < enough_lamports || last_keypair_balance < enough_lamports {
|
||||
let (_blockhash, fee_calculator) = get_recent_blockhash(client.as_ref());
|
||||
let max_fee = fee_calculator.max_lamports_per_signature;
|
||||
let fee_rate_governor = client.get_fee_rate_governor().unwrap();
|
||||
let max_fee = fee_rate_governor.max_lamports_per_signature;
|
||||
let extra_fees = extra * max_fee;
|
||||
let total_keypairs = keypairs.len() as u64 + 1; // Add one for funding keypair
|
||||
let mut total = lamports_per_account * total_keypairs + extra_fees;
|
||||
@@ -1132,7 +1134,7 @@ mod tests {
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::fee_calculator::FeeCalculator;
|
||||
use solana_sdk::fee_calculator::FeeRateGovernor;
|
||||
use solana_sdk::genesis_config::create_genesis_config;
|
||||
|
||||
#[test]
|
||||
@@ -1179,8 +1181,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_bench_tps_fund_keys_with_fees() {
|
||||
let (mut genesis_config, id) = create_genesis_config(10_000);
|
||||
let fee_calculator = FeeCalculator::new(11, 0);
|
||||
genesis_config.fee_calculator = fee_calculator;
|
||||
let fee_rate_governor = FeeRateGovernor::new(11, 0);
|
||||
genesis_config.fee_rate_governor = fee_rate_governor;
|
||||
let bank = Bank::new(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
let keypair_count = 20;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
use clap::{crate_description, crate_name, App, Arg, ArgMatches};
|
||||
use solana_faucet::faucet::FAUCET_PORT;
|
||||
use solana_sdk::fee_calculator::FeeCalculator;
|
||||
use solana_sdk::signature::{read_keypair_file, Keypair, KeypairUtil};
|
||||
use solana_sdk::fee_calculator::FeeRateGovernor;
|
||||
use solana_sdk::signature::{read_keypair_file, Keypair};
|
||||
use std::{net::SocketAddr, process::exit, time::Duration};
|
||||
|
||||
const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = solana_sdk::native_token::SOL_LAMPORTS;
|
||||
const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = solana_sdk::native_token::LAMPORTS_PER_SOL;
|
||||
|
||||
/// Holds the configuration for a single run of the benchmark
|
||||
pub struct Config {
|
||||
@@ -43,7 +43,7 @@ impl Default for Config {
|
||||
client_ids_and_stake_file: String::new(),
|
||||
write_to_client_file: false,
|
||||
read_from_client_file: false,
|
||||
target_lamports_per_signature: FeeCalculator::default().target_lamports_per_signature,
|
||||
target_lamports_per_signature: FeeRateGovernor::default().target_lamports_per_signature,
|
||||
multi_client: true,
|
||||
use_move: false,
|
||||
num_lamports_per_account: NUM_LAMPORTS_PER_ACCOUNT_DEFAULT,
|
||||
|
@@ -3,8 +3,8 @@ use solana_bench_tps::bench::{do_bench_tps, generate_and_fund_keypairs, generate
|
||||
use solana_bench_tps::cli;
|
||||
use solana_core::gossip_service::{discover_cluster, get_client, get_multi_client};
|
||||
use solana_genesis::Base64Account;
|
||||
use solana_sdk::fee_calculator::FeeCalculator;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::fee_calculator::FeeRateGovernor;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use solana_sdk::system_program;
|
||||
use std::{collections::HashMap, fs::File, io::prelude::*, path::Path, process::exit, sync::Arc};
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{collections::HashMap, fs::File, io::prelude::*, path::Path, process::e
|
||||
pub const NUM_SIGNATURES_FOR_TXS: u64 = 100_000 * 60 * 60 * 24 * 7;
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup_with_filter("solana=info");
|
||||
solana_logger::setup_with_default("solana=info");
|
||||
solana_metrics::set_panic_hook("bench-tps");
|
||||
|
||||
let matches = cli::build_args(solana_clap_utils::version!()).get_matches();
|
||||
@@ -41,7 +41,7 @@ fn main() {
|
||||
let (keypairs, _) = generate_keypairs(&id, keypair_count as u64);
|
||||
let num_accounts = keypairs.len() as u64;
|
||||
let max_fee =
|
||||
FeeCalculator::new(*target_lamports_per_signature, 0).max_lamports_per_signature;
|
||||
FeeRateGovernor::new(*target_lamports_per_signature, 0).max_lamports_per_signature;
|
||||
let num_lamports_per_account = (num_accounts - 1 + NUM_SIGNATURES_FOR_TXS * max_fee)
|
||||
/ num_accounts
|
||||
+ num_lamports_per_account;
|
||||
|
@@ -8,7 +8,7 @@ use solana_faucet::faucet::run_local_faucet;
|
||||
use solana_local_cluster::local_cluster::{ClusterConfig, LocalCluster};
|
||||
#[cfg(feature = "move")]
|
||||
use solana_sdk::move_loader::solana_move_loader_program;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use std::sync::{mpsc::channel, Arc};
|
||||
use std::time::Duration;
|
||||
|
||||
|
@@ -1,18 +0,0 @@
|
||||
+------------+
|
||||
| Bank-Merkle|
|
||||
+------------+
|
||||
^ ^
|
||||
/ \
|
||||
+-----------------+ +-------------+
|
||||
| Bank-Diff-Merkle| | Block-Merkle|
|
||||
+-----------------+ +-------------+
|
||||
^ ^
|
||||
/ \
|
||||
+------+ +--------------------------+
|
||||
| Hash | | Previous Bank-Diff-Merkle|
|
||||
+------+ +--------------------------+
|
||||
^ ^
|
||||
/ \
|
||||
+---------------+ +---------------+
|
||||
| Hash(Account1)| | Hash(Account2)|
|
||||
+---------------+ +---------------+
|
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
make -j"$(nproc)" test
|
@@ -1,4 +0,0 @@
|
||||
# API Reference
|
||||
|
||||
The following sections contain API references material you may find useful when developing applications utilizing a Solana cluster.
|
||||
|
@@ -1,28 +0,0 @@
|
||||
# Blockstreamer
|
||||
|
||||
Solana supports a node type called an _blockstreamer_. This validator variation is intended for applications that need to observe the data plane without participating in transaction validation or ledger replication.
|
||||
|
||||
A blockstreamer runs without a vote signer, and can optionally stream ledger entries out to a Unix domain socket as they are processed. The JSON-RPC service still functions as on any other node.
|
||||
|
||||
To run a blockstreamer, include the argument `no-signer` and \(optional\) `blockstream` socket location:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/validator-x.sh --no-signer --blockstream <SOCKET>
|
||||
```
|
||||
|
||||
The stream will output a series of JSON objects:
|
||||
|
||||
* An Entry event JSON object is sent when each ledger entry is processed, with the following fields:
|
||||
* `dt`, the system datetime, as RFC3339-formatted string
|
||||
* `t`, the event type, always "entry"
|
||||
* `s`, the slot height, as unsigned 64-bit integer
|
||||
* `h`, the tick height, as unsigned 64-bit integer
|
||||
* `entry`, the entry, as JSON object
|
||||
* A Block event JSON object is sent when a block is complete, with the following fields:
|
||||
* `dt`, the system datetime, as RFC3339-formatted string
|
||||
* `t`, the event type, always "block"
|
||||
* `s`, the slot height, as unsigned 64-bit integer
|
||||
* `h`, the tick height, as unsigned 64-bit integer
|
||||
* `l`, the slot leader id, as base-58 encoded string
|
||||
* `hash`, the [blockhash](terminology.md#blockhash), as base-58 encoded string
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
||||
# Instruction
|
||||
|
||||
For the purposes of building a [Transaction](../transaction.md), a more verbose instruction format is used:
|
||||
|
||||
* **Instruction:**
|
||||
* **program\_id:** The pubkey of the on-chain program that executes the
|
||||
|
||||
instruction
|
||||
|
||||
* **accounts:** An ordered list of accounts that should be passed to
|
||||
|
||||
the program processing the instruction, including metadata detailing
|
||||
|
||||
if an account is a signer of the transaction and if it is a credit
|
||||
|
||||
only account.
|
||||
|
||||
* **data:** A byte array that is passed to the program executing the
|
||||
|
||||
instruction
|
||||
|
||||
A more compact form is actually included in a `Transaction`:
|
||||
|
||||
* **CompiledInstruction:**
|
||||
* **program\_id\_index:** The index of the `program_id` in the
|
||||
|
||||
`account_keys` list
|
||||
|
||||
* **accounts:** An ordered list of indices into `account_keys`
|
||||
|
||||
specifying the accounds that should be passed to the program
|
||||
|
||||
processing the instruction.
|
||||
|
||||
* **data:** A byte array that is passed to the program executing the
|
||||
|
||||
instruction
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
# Transaction
|
||||
|
||||
## Components of a `Transaction`
|
||||
|
||||
* **Transaction:**
|
||||
* **message:** Defines the transaction
|
||||
* **header:** Details the account types of and signatures required by
|
||||
|
||||
the transaction
|
||||
|
||||
* **num\_required\_signatures:** The total number of signatures
|
||||
|
||||
required to make the transaction valid.
|
||||
|
||||
* **num\_credit\_only\_signed\_accounts:** The last
|
||||
|
||||
`num_readonly_signed_accounts` signatures refer to signing
|
||||
|
||||
credit only accounts. Credit only accounts can be used concurrently
|
||||
|
||||
by multiple parallel transactions, but their balance may only be
|
||||
|
||||
increased, and their account data is read-only.
|
||||
|
||||
* **num\_credit\_only\_unsigned\_accounts:** The last
|
||||
|
||||
`num_readonly_unsigned_accounts` public keys in `account_keys` refer
|
||||
|
||||
to non-signing credit only accounts
|
||||
|
||||
* **account\_keys:** List of public keys used by the transaction, including
|
||||
|
||||
by the instructions and for signatures. The first
|
||||
|
||||
`num_required_signatures` public keys must sign the transaction.
|
||||
|
||||
* **recent\_blockhash:** The ID of a recent ledger entry. Validators will
|
||||
|
||||
reject transactions with a `recent_blockhash` that is too old.
|
||||
|
||||
* **instructions:** A list of [instructions](https://github.com/solana-labs/solana/tree/aacead62c0eb052068172eba6b53fc85874d6d54/book/src/instruction.md) that are
|
||||
|
||||
run sequentially and committed in one atomic transaction if all
|
||||
|
||||
succeed.
|
||||
* **signatures:** A list of signatures applied to the transaction. The
|
||||
|
||||
list is always of length `num_required_signatures`, and the signature
|
||||
|
||||
at index `i` corresponds to the public key at index `i` in `account_keys`.
|
||||
|
||||
The list is initialized with empty signatures \(i.e. zeros\), and
|
||||
|
||||
populated as signatures are added.
|
||||
|
||||
## Transaction Signing
|
||||
|
||||
A `Transaction` is signed by using an ed25519 keypair to sign the serialization of the `message`. The resulting signature is placed at the index of `signatures` matching the index of the keypair's public key in `account_keys`.
|
||||
|
||||
## Transaction Serialization
|
||||
|
||||
`Transaction`s \(and their `message`s\) are serialized and deserialized using the [bincode](https://crates.io/crates/bincode) crate with a non-standard vector serialization that uses only one byte for the length if it can be encoded in 7 bits, 2 bytes if it fits in 14 bits, or 3 bytes if it requires 15 or 16 bits. The vector serialization is defined by Solana's [short-vec](https://github.com/solana-labs/solana/blob/master/sdk/src/short_vec.rs).
|
@@ -1,7 +0,0 @@
|
||||
# Testnet Participation
|
||||
|
||||
Participate in our testnet:
|
||||
|
||||
* [Running a Validator](../running-validator/)
|
||||
* [Running an Archiver](../running-archiver.md)
|
||||
|
@@ -1,12 +0,0 @@
|
||||
# Replication-validation Transaction Fees
|
||||
|
||||
**Subject to change.**
|
||||
|
||||
As previously mentioned, validator-clients will also be responsible for validating PoReps submitted into the PoH stream by archiver-clients. In this case, validators are providing compute \(CPU/GPU\) and light storage resources to confirm that these replication proofs could only be generated by a client that is storing the referenced PoH leger block.
|
||||
|
||||
While replication-clients are incentivized and rewarded through protocol-based rewards schedule \(see [Replication-client Economics](../ed_replication_client_economics/)\), validator-clients will be incentivized to include and validate PoReps in PoH through collection of transaction fees associated with the submitted PoReps and distribution of protocol rewards proportional to the validated PoReps. As will be described in detail in the Section 3.1, replication-client rewards are protocol-based and designed to reward based on a global data redundancy factor. I.e. the protocol will incentivize replication-client participation through rewards based on a target ledger redundancy \(e.g. 10x data redundancy\).
|
||||
|
||||
The validation of PoReps by validation-clients is computationally more expensive than state-validation \(detail in the [Economic Sustainability](../ed_economic_sustainability.md) chapter\), thus the transaction fees are expected to be proportionally higher.
|
||||
|
||||
There are various attack vectors available for colluding validation and replication clients, also described in detail below in [Economic Sustainability](https://github.com/solana-labs/solana/tree/aacead62c0eb052068172eba6b53fc85874d6d54/book/src/ed_economic_sustainability/README.md). To protect against various collusion attack vectors, for a given epoch, validator rewards are distributed across participating validation-clients in proportion to the number of validated PoReps in the epoch less the number of PoReps that mismatch the archivers challenge. The PoRep challenge game is described in [Ledger Replication](https://github.com/solana-labs/solana/blob/master/book/src/ledger-replication.md#the-porep-game). This design rewards validators proportional to the number of PoReps they process and validate, while providing negative pressure for validation-clients to submit lazy or malicious invalid votes on submitted PoReps \(note that it is computationally prohibitive to determine whether a validator-client has marked a valid PoRep as invalid\).
|
||||
|
@@ -1,49 +0,0 @@
|
||||
# Repair Service
|
||||
|
||||
## Repair Service
|
||||
|
||||
The RepairService is in charge of retrieving missing shreds that failed to be delivered by primary communication protocols like Avalanche. It is in charge of managing the protocols described below in the `Repair Protocols` section below.
|
||||
|
||||
## Challenges:
|
||||
|
||||
1\) Validators can fail to receive particular shreds due to network failures
|
||||
|
||||
2\) Consider a scenario where blockstore contains the set of slots {1, 3, 5}. Then Blockstore receives shreds for some slot 7, where for each of the shreds b, b.parent == 6, so then the parent-child relation 6 -> 7 is stored in blockstore. However, there is no way to chain these slots to any of the existing banks in Blockstore, and thus the `Shred Repair` protocol will not repair these slots. If these slots happen to be part of the main chain, this will halt replay progress on this node.
|
||||
|
||||
3\) Validators that find themselves behind the cluster by an entire epoch struggle/fail to catch up because they do not have a leader schedule for future epochs. If nodes were to blindly accept repair shreds in these future epochs, this exposes nodes to spam.
|
||||
|
||||
## Repair Protocols
|
||||
|
||||
The repair protocol makes best attempts to progress the forking structure of Blockstore.
|
||||
|
||||
The different protocol strategies to address the above challenges:
|
||||
|
||||
1. Shred Repair \(Addresses Challenge \#1\): This is the most basic repair protocol, with the purpose of detecting and filling "holes" in the ledger. Blockstore tracks the latest root slot. RepairService will then periodically iterate every fork in blockstore starting from the root slot, sending repair requests to validators for any missing shreds. It will send at most some `N` repair reqeusts per iteration.
|
||||
|
||||
Note: Validators will only accept shreds within the current verifiable epoch \(epoch the validator has a leader schedule for\).
|
||||
|
||||
2. Preemptive Slot Repair \(Addresses Challenge \#2\): The goal of this protocol is to discover the chaining relationship of "orphan" slots that do not currently chain to any known fork.
|
||||
* Blockstore will track the set of "orphan" slots in a separate column family.
|
||||
* RepairService will periodically make `RequestOrphan` requests for each of the orphans in blockstore.
|
||||
|
||||
`RequestOrphan(orphan)` request - `orphan` is the orphan slot that the requestor wants to know the parents of `RequestOrphan(orphan)` response - The highest shreds for each of the first `N` parents of the requested `orphan`
|
||||
|
||||
On receiving the responses `p`, where `p` is some shred in a parent slot, validators will:
|
||||
|
||||
* Insert an empty `SlotMeta` in blockstore for `p.slot` if it doesn't already exist.
|
||||
* If `p.slot` does exist, update the parent of `p` based on `parents`
|
||||
|
||||
Note: that once these empty slots are added to blockstore, the `Shred Repair` protocol should attempt to fill those slots.
|
||||
|
||||
Note: Validators will only accept responses containing shreds within the current verifiable epoch \(epoch the validator has a leader schedule for\).
|
||||
3. Repairmen \(Addresses Challenge \#3\): This part of the repair protocol is the primary mechanism by which new nodes joining the cluster catch up after loading a snapshot. This protocol works in a "forward" fashion, so validators can verify every shred that they receive against a known leader schedule.
|
||||
|
||||
Each validator advertises in gossip:
|
||||
|
||||
* Current root
|
||||
* The set of all completed slots in the confirmed epochs \(an epoch that was calculated based on a bank <= current root\) past the current root
|
||||
|
||||
Observers of this gossip message with higher epochs \(repairmen\) send shreds to catch the lagging node up with the rest of the cluster. The repairmen are responsible for sending the slots within the epochs that are confrimed by the advertised `root` in gossip. The repairmen divide the responsibility of sending each of the missing slots in these epochs based on a random seed \(simple shred.index iteration by N, seeded with the repairman's node\_pubkey\). Ideally, each repairman in an N node cluster \(N nodes whose epochs are higher than that of the repairee\) sends 1/N of the missing shreds. Both data and coding shreds for missing slots are sent. Repairmen do not send shreds again to the same validator until they see the message in gossip updated, at which point they perform another iteration of this protocol.
|
||||
|
||||
Gossip messages are updated every time a validator receives a complete slot within the epoch. Completed slots are detected by blockstore and sent over a channel to RepairService. It is important to note that we know that by the time a slot X is complete, the epoch schedule must exist for the epoch that contains slot X because WindowService will reject shreds for unconfirmed epochs. When a newly completed slot is detected, we also update the current root if it has changed since the last update. The root is made available to RepairService through Blockstore, which holds the latest root.
|
||||
|
@@ -1,37 +0,0 @@
|
||||
# Introduction
|
||||
|
||||
## What is Solana?
|
||||
|
||||
Solana is an open source project implementing a new, high-performance, permissionless blockchain. Solana is also the name of a company headquartered in San Francisco that maintains the open source project.
|
||||
|
||||
## About this Book
|
||||
|
||||
This book describes the Solana open source project, a blockchain built from the ground up for scale. The book covers why Solana is useful, how to use it, how it works, and why it will continue to work long after the company Solana closes its doors. The goal of the Solana architecture is to demonstrate there exists a set of software algorithms that when used in combination to implement a blockchain, removes software as a performance bottleneck, allowing transaction throughput to scale proportionally with network bandwidth. The architecture goes on to satisfy all three desirable properties of a proper blockchain: it is scalable, secure and decentralized.
|
||||
|
||||
The architecture describes a theoretical upper bound of 710 thousand transactions per second \(tps\) on a standard gigabit network and 28.4 million tps on 40 gigabit. Furthermore, the architecture supports safe, concurrent execution of programs authored in general purpose programming languages such as C or Rust.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the author's best effort. It is up to the reader to check and validate their accuracy and truthfulness. Furthermore, nothing in this project constitutes a solicitation for investment.
|
||||
|
||||
## History of the Solana Codebase
|
||||
|
||||
In November of 2017, Anatoly Yakovenko published a whitepaper describing Proof of History, a technique for keeping time between computers that do not trust one another. From Anatoly's previous experience designing distributed systems at Qualcomm, Mesosphere and Dropbox, he knew that a reliable clock makes network synchronization very simple. When synchronization is simple the resulting network can be blazing fast, bound only by network bandwidth.
|
||||
|
||||
Anatoly watched as blockchain systems without clocks, such as Bitcoin and Ethereum, struggled to scale beyond 15 transactions per second worldwide when centralized payment systems such as Visa required peaks of 65,000 tps. Without a clock, it was clear they'd never graduate to being the global payment system or global supercomputer most had dreamed them to be. When Anatoly solved the problem of getting computers that don’t trust each other to agree on time, he knew he had the key to bring 40 years of distributed systems research to the world of blockchain. The resulting cluster wouldn't be just 10 times faster, or a 100 times, or a 1,000 times, but 10,000 times faster, right out of the gate!
|
||||
|
||||
Anatoly's implementation began in a private codebase and was implemented in the C programming language. Greg Fitzgerald, who had previously worked with Anatoly at semiconductor giant Qualcomm Incorporated, encouraged him to reimplement the project in the Rust programming language. Greg had worked on the LLVM compiler infrastructure, which underlies both the Clang C/C++ compiler as well as the Rust compiler. Greg claimed that the language's safety guarantees would improve software productivity and that its lack of a garbage collector would allow programs to perform as well as those written in C. Anatoly gave it a shot and just two weeks later, had migrated his entire codebase to Rust. Sold. With plans to weave all the world's transactions together on a single, scalable blockchain, Anatoly called the project Loom.
|
||||
|
||||
On February 13th of 2018, Greg began prototyping the first open source implementation of Anatoly's whitepaper. The project was published to GitHub under the name Silk in the loomprotocol organization. On February 28th, Greg made his first release, demonstrating 10 thousand signed transactions could be verified and processed in just over half a second. Shortly after, another former Qualcomm cohort, Stephen Akridge, demonstrated throughput could be massively improved by offloading signature verification to graphics processors. Anatoly recruited Greg, Stephen and three others to co-found a company, then called Loom.
|
||||
|
||||
Around the same time, Ethereum-based project Loom Network sprung up and many people were confused about whether they were the same project. The Loom team decided it would rebrand. They chose the name Solana, a nod to a small beach town North of San Diego called Solana Beach, where Anatoly, Greg and Stephen lived and surfed for three years when they worked for Qualcomm. On March 28th, the team created the Solana Labs GitHub organization and renamed Greg's prototype Silk to Solana.
|
||||
|
||||
In June of 2018, the team scaled up the technology to run on cloud-based networks and on July 19th, published a 50-node, permissioned, public testnet consistently supporting bursts of 250,000 transactions per second. In a later release in December, called v0.10 Pillbox, the team published a permissioned testnet running 150 nodes on a gigabit network and demonstrated soak tests processing an _average_ of 200 thousand transactions per second with bursts over 500 thousand. The project was also extended to support on-chain programs written in the C programming language and run concurrently in a safe execution environment called BPF.
|
||||
|
||||
## What is a Solana Cluster?
|
||||
|
||||
A cluster is a set of computers that work together and can be viewed from the outside as a single system. A Solana cluster is a set of independently owned computers working together \(and sometimes against each other\) to verify the output of untrusted, user-submitted programs. A Solana cluster can be utilized any time a user wants to preserve an immutable record of events in time or programmatic interpretations of those events. One use is to track which of the computers did meaningful work to keep the cluster running. Another use might be to track the possession of real-world assets. In each case, the cluster produces a record of events called the ledger. It will be preserved for the lifetime of the cluster. As long as someone somewhere in the world maintains a copy of the ledger, the output of its programs \(which may contain a record of who possesses what\) will forever be reproducible, independent of the organization that launched it.
|
||||
|
||||
## What are SOLs?
|
||||
|
||||
A SOL is the name of Solana's native token, which can be passed to nodes in a Solana cluster in exchange for running an on-chain program or validating its output. The system may perform micropayments of fractional SOLs, which are called _lamports_. They are named in honor of Solana's biggest technical influence, [Leslie Lamport](https://en.wikipedia.org/wiki/Leslie_Lamport). A lamport has a value of 0.000000001 SOL.
|
@@ -1,51 +0,0 @@
|
||||
# Installation Guide
|
||||
Follow this guide to setup Solana's key generation tool called `solana-keygen`
|
||||
|
||||
{% hint style="warn" %}
|
||||
After installation, ensure your version is `0.21.1` or higher by running `solana-keygen -V`
|
||||
{% endhint %}
|
||||
|
||||
## Download
|
||||
First, download the latest release tarball from GitHub.
|
||||
|
||||
1. Setup download url
|
||||
|
||||
```bash
|
||||
solana_downloads=https://github.com/solana-labs/solana/releases/latest/download
|
||||
```
|
||||
|
||||
2. Specify the download file based on your machine
|
||||
|
||||
**MacOS**
|
||||
```bash
|
||||
solana_release=solana-release-x86_64-apple-darwin.tar.bz2
|
||||
```
|
||||
|
||||
**Linux**
|
||||
```bash
|
||||
solana_release=solana-release-x86_64-unknown-linux-gnu.tar.bz2
|
||||
```
|
||||
|
||||
3. Download
|
||||
|
||||
```bash
|
||||
curl -L -sSf -o solana-release.tar.bz2 $solana_downloads/$solana_release
|
||||
```
|
||||
|
||||
## Extract
|
||||
Next, extract the tarball
|
||||
```bash
|
||||
tar xf solana-release.tar.bz2
|
||||
```
|
||||
|
||||
## Add to "PATH"
|
||||
Now add the tool to your PATH environment variable with the following command
|
||||
```bash
|
||||
export PATH="$(pwd)/solana-release/bin:${PATH}"
|
||||
```
|
||||
|
||||
## Check
|
||||
Finally, check that `solana-keygen` can be run by running
|
||||
```bash
|
||||
solana-keygen -V
|
||||
```
|
@@ -1,109 +0,0 @@
|
||||
# Simple Payment and State Verification
|
||||
|
||||
It is often useful to allow low resourced clients to participate in a Solana cluster. Be this participation economic or contract execution, verification that a client's activity has been accepted by the network is typically expensive. This proposal lays out a mechanism for such clients to confirm that their actions have been committed to the ledger state with minimal resource expenditure and third-party trust.
|
||||
|
||||
## A Naive Approach
|
||||
|
||||
Validators store the signatures of recently confirmed transactions for a short period of time to ensure that they are not processed more than once. Validators provide a JSON RPC endpoint, which clients can use to query the cluster if a transaction has been recently processed. Validators also provide a PubSub notification, whereby a client registers to be notified when a given signature is observed by the validator. While these two mechanisms allow a client to verify a payment, they are not a proof and rely on completely trusting a validator.
|
||||
|
||||
We will describe a way to minimize this trust using Merkle Proofs to anchor the validator's response in the ledger, allowing the client to confirm on their own that a sufficient number of their preferred validators have confirmed a transaction. Requiring multiple validator attestations further reduces trust in the validator, as it increases both the technical and economic difficulty of compromising several other network participants.
|
||||
|
||||
## Light Clients
|
||||
|
||||
A 'light client' is a cluster participant that does not itself run a validator. This light client would provide a level of security greater than trusting a remote validator, without requiring the light client to spend a lot of resources verifying the ledger.
|
||||
|
||||
Rather than providing transaction signatures directly to a light client, the validator instead generates a Merkle Proof from the transaction of interest to the root of a Merkle Tree of all transactions in the including block. This Merkle Root is stored in a ledger entry which is voted on by validators, providing it consensus legitimacy. The additional level of security for a light client depends on an initial canonical set of validators the light client considers to be the stakeholders of the cluster. As that set is changed, the client can update its internal set of known validators with [receipts](simple-payment-and-state-verification.md#receipts). This may become challenging with a large number of delegated stakes.
|
||||
|
||||
Validators themselves may want to use light client APIs for performance reasons. For example, during the initial launch of a validator, the validator may use a cluster provided checkpoint of the state and verify it with a receipt.
|
||||
|
||||
## Receipts
|
||||
|
||||
A receipt is a minimal proof that; a transaction has been included in a block, that the block has been voted on by the client's preferred set of validators and that the votes have reached the desired confirmation depth.
|
||||
|
||||
The receipts for both state and payments start with a Merkle Path from the value into a Bank-Merkle that has been voted on and included in the ledger. A chain of PoH Entries containing subsequent validator votes, deriving from the Bank-Merkle, is the confirmation proof.
|
||||
|
||||
Clients can examine this ledger data and compute the finality using Solana's fork selection rules.
|
||||
|
||||
### Payment Merkle Path
|
||||
|
||||
A payment receipt is a data structure that contains a Merkle Path from a transaction to the required set of validator votes.
|
||||
|
||||
An Entry-Merkle is a Merkle Root including all transactions in the entry, sorted by signature.
|
||||
|
||||

|
||||
|
||||
A Block-Merkle is a Merkle root of all the Entry-Merkles sequenced in the block. Transaction status is necessary for the receipt because the state receipt is constructed for the block. Two transactions over the same state can appear in the block, and therefore, there is no way to infer from just the state whether a transaction that is committed to the ledger has succeeded or failed in modifying the intended state. It may not be necessary to encode the full status code, but a single status bit to indicate the transaction's success.
|
||||
|
||||
### State Merkle Path
|
||||
|
||||
A state receipt provides a confirmation that a specific state is committed at the end of the block. Inter-block state transitions do not generate a receipt.
|
||||
|
||||
For example:
|
||||
|
||||
* A sends 5 Lamports to B
|
||||
* B spends 5 Lamports
|
||||
* C sends 5 Lamports to A
|
||||
|
||||
At the end of the block, A and B are in the exact same starting state, and any state receipt would point to the same value for A or B.
|
||||
|
||||
The Bank-Merkle is computed from the Merkle Tree of the new state changes, along with the Previous Bank-Merkle, and the Block-Merkle.
|
||||
|
||||

|
||||
|
||||
A state receipt contains only the state changes occurring in the block. A direct Merkle Path to the current Bank-Merkle guarantees the state value at that bank hash, but it cannot be used to generate a “current” receipt to the latest state if the state modification occurred in some previous block. There is no guarantee that the path provided by the validator is the latest one available out of all the previous Bank-Merkles.
|
||||
|
||||
Clients that want to query the chain for a receipt of the "latest" state would need to create a transaction that would update the Merkle Path for that account, such as a credit of 0 Lamports.
|
||||
|
||||
### Validator Votes
|
||||
|
||||
Leaders should coalesce the validator votes by stake weight into a single entry. This will reduce the number of entries necessary to create a receipt.
|
||||
|
||||
### Chain of Entries
|
||||
|
||||
A receipt has a PoH link from the payment or state Merkle Path root to a list of consecutive validation votes.
|
||||
|
||||
It contains the following:
|
||||
|
||||
* State -> Bank-Merkle
|
||||
|
||||
or
|
||||
|
||||
* Transaction -> Entry-Merkle -> Block-Merkle -> Bank-Merkle
|
||||
|
||||
And a vector of PoH entries:
|
||||
|
||||
* Validator vote entries
|
||||
* Ticks
|
||||
* Light entries
|
||||
|
||||
```text
|
||||
/// This Entry definition skips over the transactions and only contains the
|
||||
/// hash of the transactions used to modify PoH.
|
||||
LightEntry {
|
||||
/// The number of hashes since the previous Entry ID.
|
||||
pub num_hashes: u64,
|
||||
/// The SHA-256 hash `num_hashes` after the previous Entry ID.
|
||||
hash: Hash,
|
||||
/// The Merkle Root of the transactions encoded into the Entry.
|
||||
entry_hash: Hash,
|
||||
}
|
||||
```
|
||||
|
||||
The light entries are reconstructed from Entries and simply show the entry Merkle Root that was mixed in to the PoH hash, instead of the full transaction set.
|
||||
|
||||
Clients do not need the starting vote state. The [fork selection](https://github.com/solana-labs/solana/tree/aacead62c0eb052068172eba6b53fc85874d6d54/book/src/book/src/fork-selection.md) algorithm is defined such that only votes that appear after the transaction provide finality for the transaction, and finality is independent of the starting state.
|
||||
|
||||
### Verification
|
||||
|
||||
A light client that is aware of the supermajority set validators can verify a receipt by following the Merkle Path to the PoH chain. The Bank-Merkle is the Merkle Root and will appear in votes included in an Entry. The light client can simulate [fork selection](https://github.com/solana-labs/solana/tree/aacead62c0eb052068172eba6b53fc85874d6d54/book/src/book/src/fork-selection.md) for the consecutive votes and verify that the receipt is confirmed at the desired lockout threshold.
|
||||
|
||||
### Synthetic State
|
||||
|
||||
Synthetic state should be computed into the Bank-Merkle along with the bank generated state.
|
||||
|
||||
For example:
|
||||
|
||||
* Epoch validator accounts and their stakes and weights.
|
||||
* Computed fee rates
|
||||
|
||||
These values should have an entry in the Bank-Merkle. They should live under known accounts, and therefore have an exact address in the Merkle Path.
|
@@ -1,34 +0,0 @@
|
||||
# Validator
|
||||
|
||||
## History
|
||||
|
||||
When we first started Solana, the goal was to de-risk our TPS claims. We knew that between optimistic concurrency control and sufficiently long leader slots, that PoS consensus was not the biggest risk to TPS. It was GPU-based signature verification, software pipelining and concurrent banking. Thus, the TPU was born. After topping 100k TPS, we split the team into one group working toward 710k TPS and another to flesh out the validator pipeline. Hence, the TVU was born. The current architecture is a consequence of incremental development with that ordering and project priorities. It is not a reflection of what we ever believed was the most technically elegant cross-section of those technologies. In the context of leader rotation, the strong distinction between leading and validating is blurred.
|
||||
|
||||
## Difference between validating and leading
|
||||
|
||||
The fundamental difference between the pipelines is when the PoH is present. In a leader, we process transactions, removing bad ones, and then tag the result with a PoH hash. In the validator, we verify that hash, peel it off, and process the transactions in exactly the same way. The only difference is that if a validator sees a bad transaction, it can't simply remove it like the leader does, because that would cause the PoH hash to change. Instead, it rejects the whole block. The other difference between the pipelines is what happens _after_ banking. The leader broadcasts entries to downstream validators whereas the validator will have already done that in RetransmitStage, which is a confirmation time optimization. The validation pipeline, on the other hand, has one last step. Any time it finishes processing a block, it needs to weigh any forks it's observing, possibly cast a vote, and if so, reset its PoH hash to the block hash it just voted on.
|
||||
|
||||
## Proposed Design
|
||||
|
||||
We unwrap the many abstraction layers and build a single pipeline that can toggle leader mode on whenever the validator's ID shows up in the leader schedule.
|
||||
|
||||

|
||||
|
||||
## Notable changes
|
||||
|
||||
* No threads are shut down to switch out of leader mode. Instead, FetchStage
|
||||
|
||||
should forward transactions to the next leader.
|
||||
|
||||
* Hoist FetchStage and BroadcastStage out of TPU
|
||||
* BankForks renamed to Banktree
|
||||
* TPU moves to new socket-free crate called solana-tpu.
|
||||
* TPU's BankingStage absorbs ReplayStage
|
||||
* TVU goes away
|
||||
* New RepairStage absorbs Shred Fetch Stage and repair requests
|
||||
* JSON RPC Service is optional - used for debugging. It should instead be part
|
||||
|
||||
of a separate `solana-blockstreamer` executable.
|
||||
|
||||
* New MulticastStage absorbs retransmit part of RetransmitStage
|
||||
* MulticastStage downstream of Blockstore
|
@@ -1,156 +0,0 @@
|
||||
# Running an Archiver
|
||||
|
||||
This document describes how to setup an archiver in the testnet
|
||||
|
||||
Please note some of the information and instructions described here may change in future releases.
|
||||
|
||||
## Overview
|
||||
|
||||
Archivers are specialized light clients. They download a part of the ledger \(a.k.a Segment\) and store it. They earn rewards for storing segments.
|
||||
|
||||
The testnet features a validator running at testnet.solana.com, which serves as the entrypoint to the cluster for your archiver node.
|
||||
|
||||
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.
|
||||
|
||||
## Machine Requirements
|
||||
|
||||
Archivers don't need specialized hardware. Anything with more than 128GB of disk space will be able to participate in the cluster as an archiver node.
|
||||
|
||||
Currently the disk space requirements are very low but we expect them to change in the future.
|
||||
|
||||
Prebuilt binaries are available for Linux x86\_64 \(Ubuntu 18.04 recommended\), macOS, and Windows.
|
||||
|
||||
### Confirm The Testnet Is Reachable
|
||||
|
||||
Before starting an archiver 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.
|
||||
|
||||
## Archiver Setup
|
||||
|
||||
#### Obtaining The Software
|
||||
|
||||
#### Bootstrap with `solana-install`
|
||||
|
||||
The `solana-install` tool can be used to easily install and upgrade the cluster software.
|
||||
|
||||
#### Linux and mac OS
|
||||
|
||||
```bash
|
||||
curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.18.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
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
Download and install **solana-install-init** from [https://github.com/solana-labs/solana/releases/latest](https://github.com/solana-labs/solana/releases/latest)
|
||||
|
||||
After a successful install, `solana-install update` may be used to easily update the software to a newer version at any time.
|
||||
|
||||
#### Download Prebuilt Binaries
|
||||
|
||||
If you would rather not use `solana-install` to manage the install, you can manually download and install the binaries.
|
||||
|
||||
#### Linux
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
#### mac OS
|
||||
|
||||
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-apple-darwin.tar.bz2**, then extract the archive:
|
||||
|
||||
```bash
|
||||
tar jxf solana-release-x86_64-apple-darwin.tar.bz2
|
||||
cd solana-release/
|
||||
export PATH=$PWD/bin:$PATH
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
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-pc-windows-msvc.tar.bz2**, then extract it into a folder. It is a good idea to add this extracted folder to your windows PATH.
|
||||
|
||||
## Starting The Archiver
|
||||
|
||||
Try running following command to join the gossip network and view all the other nodes in the cluster:
|
||||
|
||||
```bash
|
||||
solana-gossip spy --entrypoint testnet.solana.com:8001
|
||||
# Press ^C to exit
|
||||
```
|
||||
|
||||
Now configure the keypairs for your archiver by running:
|
||||
|
||||
Navigate to the solana install location and open a cmd prompt
|
||||
|
||||
```bash
|
||||
solana-keygen new -o archiver-keypair.json
|
||||
solana-keygen new -o storage-keypair.json
|
||||
```
|
||||
|
||||
Use solana-keygen to show the public keys for each of the keypairs, they will be needed in the next step:
|
||||
|
||||
* Windows
|
||||
|
||||
```bash
|
||||
# The archiver's identity
|
||||
solana-keygen pubkey archiver-keypair.json
|
||||
solana-keygen pubkey storage-keypair.json
|
||||
```
|
||||
|
||||
* Linux and mac OS
|
||||
|
||||
\`\`\`bash
|
||||
|
||||
export ARCHIVER\_IDENTITY=$\(solana-keygen pubkey archiver-keypair.json\)
|
||||
|
||||
export STORAGE\_IDENTITY=$\(solana-keygen pubkey storage-keypair.json\)
|
||||
|
||||
```text
|
||||
Then set up the storage accounts for your archiver by running:
|
||||
```bash
|
||||
solana --keypair archiver-keypair.json airdrop 100000 lamports
|
||||
solana --keypair archiver-keypair.json create-archiver-storage-account $ARCHIVER_IDENTITY $STORAGE_IDENTITY
|
||||
```
|
||||
|
||||
Note: Every time the testnet restarts, run the steps to setup the archiver accounts again.
|
||||
|
||||
To start the archiver:
|
||||
|
||||
```bash
|
||||
solana-archiver --entrypoint testnet.solana.com:8001 --identity-keypair archiver-keypair.json --storage-keypair storage-keypair.json --ledger archiver-ledger
|
||||
```
|
||||
|
||||
## Verify Archiver Setup
|
||||
|
||||
From another console, confirm the IP address and **identity pubkey** of your archiver is visible in the gossip network by running:
|
||||
|
||||
```bash
|
||||
solana-gossip spy --entrypoint testnet.solana.com:8001
|
||||
```
|
||||
|
||||
Provide the **storage account pubkey** to the `solana show-storage-account` command to view the recent mining activity from your archiver:
|
||||
|
||||
```bash
|
||||
solana --keypair storage-keypair.json show-storage-account $STORAGE_IDENTITY
|
||||
```
|
@@ -1,40 +0,0 @@
|
||||
# Choosing a Testnet
|
||||
|
||||
Solana maintains several testnets, each featuring a Solana-owned validator
|
||||
that serves as an entrypoint to the cluster.
|
||||
|
||||
Current testnet entrypoints:
|
||||
|
||||
* Stable: testnet.solana.com
|
||||
* Beta: beta.testnet.solana.com
|
||||
|
||||
Application developers should target the Stable testnet. Key differences
|
||||
between the Stable testnet and what will be mainnet:
|
||||
|
||||
* Stable testnet tokens are not real
|
||||
* Stable testnet includes a token faucet for application testing
|
||||
* Stable testnet may be subject to ledger resets
|
||||
* Stable testnet typically runs a newer software version than mainnet
|
||||
* Stable testnet may be maintained by different validators than mainnet
|
||||
|
||||
The Beta testnet is used to showcase and stabilize new features before they
|
||||
are tagged for release. Application developers are free to target the Beta
|
||||
testnet, but should expect instability and periodic ledger resets. Regarding
|
||||
stability, all that can be said is that CI automation was successful.
|
||||
|
||||
### Get Testnet Version
|
||||
|
||||
You can submit a JSON-RPC request to see the specific software version of the
|
||||
cluster. Use this to specify [the software version to install](validator-software.md).
|
||||
|
||||
```bash
|
||||
curl -X POST -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1, "method":"getVersion"}' testnet.solana.com:8899
|
||||
```
|
||||
Example result:
|
||||
`{"jsonrpc":"2.0","result":{"solana-core":"0.21.0"},"id":1}`
|
||||
|
||||
## Using a Different Testnet
|
||||
|
||||
This guide is written in the context of testnet.solana.com, our most stable
|
||||
cluster. To participate in another testnet, modify the commands in the following
|
||||
pages, replacing `testnet.solana.com` with your desired testnet.
|
@@ -1,17 +0,0 @@
|
||||
# Anatomy of a Transaction
|
||||
|
||||
Transactions encode lists of instructions that are executed sequentially, and only committed if all the instructions complete successfully. All account updates are reverted upon the failure of a transaction. Each transaction details the accounts used, including which must sign and which are read only, a recent blockhash, the instructions, and any signatures.
|
||||
|
||||
## Accounts and Signatures
|
||||
|
||||
Each transaction explicitly lists all account public keys referenced by the transaction's instructions. A subset of those public keys are each accompanied by a transaction signature. Those signatures signal on-chain programs that the account holder has authorized the transaction. Typically, the program uses the authorization to permit debiting the account or modifying its data.
|
||||
|
||||
The transaction also marks some accounts as _read-only accounts_. The runtime permits read-only accounts to be read concurrently. If a program attempts to modify a read-only account, the transaction is rejected by the runtime.
|
||||
|
||||
## Recent Blockhash
|
||||
|
||||
A Transaction includes a recent blockhash to prevent duplication and to give transactions lifetimes. Any transaction that is completely identical to a previous one is rejected, so adding a newer blockhash allows multiple transactions to repeat the exact same action. Transactions also have lifetimes that are defined by the blockhash, as any transaction whose blockhash is too old will be rejected.
|
||||
|
||||
## Instructions
|
||||
|
||||
Each instruction specifies a single program account \(which must be marked executable\), a subset of the transaction's accounts that should be passed to the program, and a data byte array instruction that is passed to the program. The program interprets the data array and operates on the accounts specified by the instructions. The program can return successfully, or with an error code. An error return causes the entire transaction to fail immediately.
|
@@ -1,90 +0,0 @@
|
||||
# Blockstore
|
||||
|
||||
After a block reaches finality, all blocks from that one on down to the genesis block form a linear chain with the familiar name blockchain. Until that point, however, the validator must maintain all potentially valid chains, called _forks_. The process by which forks naturally form as a result of leader rotation is described in [fork generation](../../cluster/fork-generation.md). The _blockstore_ data structure described here is how a validator copes with those forks until blocks are finalized.
|
||||
|
||||
The blockstore allows a validator to record every shred it observes on the network, in any order, as long as the shred is signed by the expected leader for a given slot.
|
||||
|
||||
Shreds are moved to a fork-able key space the tuple of `leader slot` + `shred index` \(within the slot\). This permits the skip-list structure of the Solana protocol to be stored in its entirety, without a-priori choosing which fork to follow, which Entries to persist or when to persist them.
|
||||
|
||||
Repair requests for recent shreds are served out of RAM or recent files and out of deeper storage for less recent shreds, as implemented by the store backing Blockstore.
|
||||
|
||||
## Functionalities of Blockstore
|
||||
|
||||
1. Persistence: the Blockstore lives in the front of the nodes verification
|
||||
|
||||
pipeline, right behind network receive and signature verification. If the
|
||||
|
||||
shred received is consistent with the leader schedule \(i.e. was signed by the
|
||||
|
||||
leader for the indicated slot\), it is immediately stored.
|
||||
|
||||
2. Repair: repair is the same as window repair above, but able to serve any
|
||||
|
||||
shred that's been received. Blockstore stores shreds with signatures,
|
||||
|
||||
preserving the chain of origination.
|
||||
|
||||
3. Forks: Blockstore supports random access of shreds, so can support a
|
||||
|
||||
validator's need to rollback and replay from a Bank checkpoint.
|
||||
|
||||
4. Restart: with proper pruning/culling, the Blockstore can be replayed by
|
||||
|
||||
ordered enumeration of entries from slot 0. The logic of the replay stage
|
||||
|
||||
\(i.e. dealing with forks\) will have to be used for the most recent entries in
|
||||
|
||||
the Blockstore.
|
||||
|
||||
## Blockstore Design
|
||||
|
||||
1. Entries in the Blockstore are stored as key-value pairs, where the key is the concatenated slot index and shred index for an entry, and the value is the entry data. Note shred indexes are zero-based for each slot \(i.e. they're slot-relative\).
|
||||
2. The Blockstore maintains metadata for each slot, in the `SlotMeta` struct containing:
|
||||
* `slot_index` - The index of this slot
|
||||
* `num_blocks` - The number of blocks in the slot \(used for chaining to a previous slot\)
|
||||
* `consumed` - The highest shred index `n`, such that for all `m < n`, there exists a shred in this slot with shred index equal to `n` \(i.e. the highest consecutive shred index\).
|
||||
* `received` - The highest received shred index for the slot
|
||||
* `next_slots` - A list of future slots this slot could chain to. Used when rebuilding
|
||||
|
||||
the ledger to find possible fork points.
|
||||
|
||||
* `last_index` - The index of the shred that is flagged as the last shred for this slot. This flag on a shred will be set by the leader for a slot when they are transmitting the last shred for a slot.
|
||||
* `is_rooted` - True iff every block from 0...slot forms a full sequence without any holes. We can derive is\_rooted for each slot with the following rules. Let slot\(n\) be the slot with index `n`, and slot\(n\).is\_full\(\) is true if the slot with index `n` has all the ticks expected for that slot. Let is\_rooted\(n\) be the statement that "the slot\(n\).is\_rooted is true". Then:
|
||||
|
||||
is\_rooted\(0\) is\_rooted\(n+1\) iff \(is\_rooted\(n\) and slot\(n\).is\_full\(\)
|
||||
3. Chaining - When a shred for a new slot `x` arrives, we check the number of blocks \(`num_blocks`\) for that new slot \(this information is encoded in the shred\). We then know that this new slot chains to slot `x - num_blocks`.
|
||||
4. Subscriptions - The Blockstore records a set of slots that have been "subscribed" to. This means entries that chain to these slots will be sent on the Blockstore channel for consumption by the ReplayStage. See the `Blockstore APIs` for details.
|
||||
5. Update notifications - The Blockstore notifies listeners when slot\(n\).is\_rooted is flipped from false to true for any `n`.
|
||||
|
||||
## Blockstore APIs
|
||||
|
||||
The Blockstore offers a subscription based API that ReplayStage uses to ask for entries it's interested in. The entries will be sent on a channel exposed by the Blockstore. These subscription API's are as follows: 1. `fn get_slots_since(slot_indexes: &[u64]) -> Vec<SlotMeta>`: Returns new slots connecting to any element of the list `slot_indexes`.
|
||||
|
||||
1. `fn get_slot_entries(slot_index: u64, entry_start_index: usize, max_entries: Option<u64>) -> Vec<Entry>`: Returns the entry vector for the slot starting with `entry_start_index`, capping the result at `max` if `max_entries == Some(max)`, otherwise, no upper limit on the length of the return vector is imposed.
|
||||
|
||||
Note: Cumulatively, this means that the replay stage will now have to know when a slot is finished, and subscribe to the next slot it's interested in to get the next set of entries. Previously, the burden of chaining slots fell on the Blockstore.
|
||||
|
||||
## Interfacing with Bank
|
||||
|
||||
The bank exposes to replay stage:
|
||||
|
||||
1. `prev_hash`: which PoH chain it's working on as indicated by the hash of the last
|
||||
|
||||
entry it processed
|
||||
|
||||
2. `tick_height`: the ticks in the PoH chain currently being verified by this
|
||||
|
||||
bank
|
||||
|
||||
3. `votes`: a stack of records that contain: 1. `prev_hashes`: what anything after this vote must chain to in PoH 2. `tick_height`: the tick height at which this vote was cast 3. `lockout period`: how long a chain must be observed to be in the ledger to
|
||||
|
||||
be able to be chained below this vote
|
||||
|
||||
Replay stage uses Blockstore APIs to find the longest chain of entries it can hang off a previous vote. If that chain of entries does not hang off the latest vote, the replay stage rolls back the bank to that vote and replays the chain from there.
|
||||
|
||||
## Pruning Blockstore
|
||||
|
||||
Once Blockstore entries are old enough, representing all the possible forks becomes less useful, perhaps even problematic for replay upon restart. Once a validator's votes have reached max lockout, however, any Blockstore contents that are not on the PoH chain for that vote for can be pruned, expunged.
|
||||
|
||||
Archiver nodes will be responsible for storing really old ledger contents, and validators need only persist their bank periodically.
|
||||
|
27
chacha-cuda/Cargo.toml
Normal file
27
chacha-cuda/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "solana-chacha-cuda"
|
||||
version = "1.0.23"
|
||||
description = "Solana Chacha Cuda APIs"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
solana-archiver-utils = { path = "../archiver-utils", version = "1.0.23" }
|
||||
solana-chacha = { path = "../chacha", version = "1.0.23" }
|
||||
solana-ledger = { path = "../ledger", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-perf = { path = "../perf", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.2.1"
|
||||
|
||||
[lib]
|
||||
name = "solana_chacha_cuda"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@@ -1,6 +1,6 @@
|
||||
// Module used by validators to approve storage mining proofs in parallel using the GPU
|
||||
|
||||
use crate::chacha::{CHACHA_BLOCK_SIZE, CHACHA_KEY_SIZE};
|
||||
use solana_chacha::chacha::{CHACHA_BLOCK_SIZE, CHACHA_KEY_SIZE};
|
||||
use solana_ledger::blockstore::Blockstore;
|
||||
use solana_perf::perf_libs;
|
||||
use solana_sdk::hash::Hash;
|
||||
@@ -113,12 +113,12 @@ pub fn chacha_cbc_encrypt_file_many_keys(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::archiver::sample_file;
|
||||
use crate::chacha::chacha_cbc_encrypt_ledger;
|
||||
use solana_archiver_utils::sample_file;
|
||||
use solana_chacha::chacha::chacha_cbc_encrypt_ledger;
|
||||
use solana_ledger::entry::create_ticks;
|
||||
use solana_ledger::get_tmp_ledger_path;
|
||||
use solana_sdk::clock::DEFAULT_SLOTS_PER_SEGMENT;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::signature::Keypair;
|
||||
use std::fs::{remove_dir_all, remove_file};
|
||||
use std::path::Path;
|
||||
|
8
chacha-cuda/src/lib.rs
Normal file
8
chacha-cuda/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate hex_literal;
|
||||
|
||||
pub mod chacha_cuda;
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "solana-chacha-sys"
|
||||
version = "0.22.10"
|
||||
version = "1.0.23"
|
||||
description = "Solana chacha-sys"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
@@ -9,4 +9,7 @@ license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.48"
|
||||
cc = "1.0.49"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
1
chacha/.gitignore
vendored
Normal file
1
chacha/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/farf/
|
28
chacha/Cargo.toml
Normal file
28
chacha/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "solana-chacha"
|
||||
version = "1.0.23"
|
||||
description = "Solana Chacha APIs"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
rand = "0.7.0"
|
||||
rand_chacha = "0.2.2"
|
||||
solana-chacha-sys = { path = "../chacha-sys", version = "1.0.23" }
|
||||
solana-ledger = { path = "../ledger", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-perf = { path = "../perf", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.2.1"
|
||||
|
||||
[lib]
|
||||
name = "solana_chacha"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@@ -74,13 +74,14 @@ pub fn chacha_cbc_encrypt_ledger(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::chacha::chacha_cbc_encrypt_ledger;
|
||||
use crate::gen_keys::GenKeys;
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use solana_ledger::blockstore::Blockstore;
|
||||
use solana_ledger::entry::Entry;
|
||||
use solana_ledger::get_tmp_ledger_path;
|
||||
use solana_sdk::hash::{hash, Hash, Hasher};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use solana_sdk::system_transaction;
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
@@ -92,8 +93,9 @@ mod tests {
|
||||
let one = hash(&zero.as_ref());
|
||||
|
||||
let seed = [2u8; 32];
|
||||
let mut rnd = GenKeys::new(seed);
|
||||
let keypair = rnd.gen_keypair();
|
||||
|
||||
let mut generator = ChaChaRng::from_seed(seed);
|
||||
let keypair = Keypair::generate(&mut generator);
|
||||
|
||||
let mut id = one;
|
||||
let mut num_hashes = 0;
|
||||
@@ -135,8 +137,9 @@ mod tests {
|
||||
let out_path = tmp_file_path("test_encrypt_ledger");
|
||||
|
||||
let seed = [2u8; 32];
|
||||
let mut rnd = GenKeys::new(seed);
|
||||
let keypair = rnd.gen_keypair();
|
||||
|
||||
let mut generator = ChaChaRng::from_seed(seed);
|
||||
let keypair = Keypair::generate(&mut generator);
|
||||
|
||||
let entries = make_tiny_deterministic_test_entries(slots_per_segment);
|
||||
blockstore
|
8
chacha/src/lib.rs
Normal file
8
chacha/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate hex_literal;
|
||||
|
||||
pub mod chacha;
|
8
ci/_
8
ci/_
@@ -5,7 +5,13 @@
|
||||
# |source| me
|
||||
#
|
||||
|
||||
base_dir=$(realpath --strip "$(dirname "$0")/..")
|
||||
|
||||
_() {
|
||||
echo "--- $*"
|
||||
if [[ $(pwd) = $base_dir ]]; then
|
||||
echo "--- $*"
|
||||
else
|
||||
echo "--- $* (wd: $(pwd))"
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
# ./affects-files.sh ^snap/ -- anything under the snap/ subdirectory
|
||||
# ./affects-files.sh snap/ -- also matches foo/snap/
|
||||
# Any pattern starting with the ! character will be negated:
|
||||
# ./affects-files.sh !^book/ -- anything *not* under the book/ subdirectory
|
||||
# ./affects-files.sh !^docs/ -- anything *not* under the docs/ subdirectory
|
||||
#
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
|
@@ -2,19 +2,23 @@
|
||||
# Build steps that run after the primary pipeline on pushes and tags.
|
||||
# Pull requests to not run these steps.
|
||||
steps:
|
||||
- command: "ci/publish-tarball.sh"
|
||||
timeout_in_minutes: 60
|
||||
name: "publish tarball"
|
||||
- command: "ci/publish-docs.sh"
|
||||
timeout_in_minutes: 15
|
||||
name: "publish docs"
|
||||
- command: "ci/publish-bpf-sdk.sh"
|
||||
timeout_in_minutes: 5
|
||||
name: "publish bpf sdk"
|
||||
- wait
|
||||
- command: "sdk/docker-solana/build.sh"
|
||||
timeout_in_minutes: 60
|
||||
name: "publish docker"
|
||||
- command: "ci/publish-crate.sh"
|
||||
timeout_in_minutes: 120
|
||||
timeout_in_minutes: 240
|
||||
name: "publish crate"
|
||||
branches: "!master"
|
||||
- command: "ci/publish-bpf-sdk.sh"
|
||||
timeout_in_minutes: 5
|
||||
name: "publish bpf sdk"
|
||||
- command: "ci/publish-tarball.sh"
|
||||
timeout_in_minutes: 60
|
||||
name: "publish tarball"
|
||||
- command: "ci/publish-book.sh"
|
||||
timeout_in_minutes: 15
|
||||
name: "publish book"
|
||||
# - command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-move.sh"
|
||||
# name: "move"
|
||||
# timeout_in_minutes: 20
|
||||
|
26
ci/buildkite-tests.yml
Normal file
26
ci/buildkite-tests.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
# These steps are conditionally triggered by ci/buildkite.yml when files
|
||||
# other than those in docs/ are modified
|
||||
|
||||
steps:
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_nightly_docker_image ci/test-coverage.sh"
|
||||
name: "coverage"
|
||||
timeout_in_minutes: 30
|
||||
- wait
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-stable.sh"
|
||||
name: "stable"
|
||||
timeout_in_minutes: 60
|
||||
artifact_paths: "log-*.txt"
|
||||
- wait
|
||||
- command: "ci/test-stable-perf.sh"
|
||||
name: "stable-perf"
|
||||
timeout_in_minutes: 40
|
||||
artifact_paths: "log-*.txt"
|
||||
agents:
|
||||
- "queue=cuda"
|
||||
- command: "ci/test-bench.sh"
|
||||
name: "bench"
|
||||
timeout_in_minutes: 30
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-local-cluster.sh"
|
||||
name: "local-cluster"
|
||||
timeout_in_minutes: 45
|
||||
artifact_paths: "log-*.txt"
|
@@ -1,38 +1,25 @@
|
||||
# Build steps that run on pushes and pull requests.
|
||||
# If files other than those in docs/ were modified, this will be followed up by
|
||||
# ci/buildkite-tests.yml
|
||||
#
|
||||
# Release tags use buildkite-release.yml instead
|
||||
|
||||
steps:
|
||||
- command: "ci/shellcheck.sh"
|
||||
name: "shellcheck"
|
||||
timeout_in_minutes: 5
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_nightly_docker_image ci/test-checks.sh"
|
||||
name: "checks"
|
||||
timeout_in_minutes: 20
|
||||
- command: "ci/shellcheck.sh"
|
||||
name: "shellcheck"
|
||||
timeout_in_minutes: 5
|
||||
|
||||
- wait
|
||||
- command: "ci/test-stable-perf.sh"
|
||||
name: "stable-perf"
|
||||
timeout_in_minutes: 40
|
||||
artifact_paths: "log-*.txt"
|
||||
agents:
|
||||
- "queue=cuda"
|
||||
- command: "ci/test-bench.sh"
|
||||
name: "bench"
|
||||
timeout_in_minutes: 30
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-stable.sh"
|
||||
name: "stable"
|
||||
timeout_in_minutes: 60
|
||||
artifact_paths: "log-*.txt"
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-move.sh"
|
||||
name: "move"
|
||||
timeout_in_minutes: 20
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-local-cluster.sh"
|
||||
name: "local-cluster"
|
||||
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: 30
|
||||
|
||||
- command: "ci/maybe-trigger-tests.sh"
|
||||
name: "maybe-trigger-tests"
|
||||
timeout_in_minutes: 2
|
||||
|
||||
- wait
|
||||
|
||||
- trigger: "solana-secondary"
|
||||
branches: "!pull/*"
|
||||
async: true
|
||||
|
@@ -49,7 +49,7 @@ else
|
||||
# ~/.cargo
|
||||
ARGS+=(--volume "$PWD:/home")
|
||||
fi
|
||||
ARGS+=(--env "CARGO_HOME=/home/.cargo")
|
||||
ARGS+=(--env "HOME=/home" --env "CARGO_HOME=/home/.cargo")
|
||||
|
||||
# kcov tries to set the personality of the binary which docker
|
||||
# doesn't allow by default.
|
||||
@@ -67,6 +67,7 @@ ARGS+=(
|
||||
--env BUILDKITE_JOB_ID
|
||||
--env CI
|
||||
--env CI_BRANCH
|
||||
--env CI_TAG
|
||||
--env CI_BUILD_ID
|
||||
--env CI_COMMIT
|
||||
--env CI_JOB_ID
|
||||
@@ -78,7 +79,9 @@ ARGS+=(
|
||||
# Also propagate environment variables needed for codecov
|
||||
# https://docs.codecov.io/docs/testing-with-docker#section-codecov-inside-docker
|
||||
# We normalize CI to `1`; but codecov expects it to be `true` to detect Buildkite...
|
||||
CODECOV_ENVS=$(CI=true bash <(curl -s https://codecov.io/env))
|
||||
# Unfortunately, codecov.io fails sometimes:
|
||||
# curl: (7) Failed to connect to codecov.io port 443: Connection timed out
|
||||
CODECOV_ENVS=$(CI=true bash <(while ! curl -sS --retry 5 --retry-delay 2 --retry-connrefused https://codecov.io/env; do sleep 10; done))
|
||||
|
||||
if $INTERACTIVE; then
|
||||
if [[ -n $1 ]]; then
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM solanalabs/rust:1.40.0
|
||||
FROM solanalabs/rust:1.43.0
|
||||
ARG date
|
||||
|
||||
RUN set -x \
|
||||
|
@@ -15,6 +15,8 @@ To update the pinned version:
|
||||
1. Run `ci/docker-rust-nightly/build.sh` to rebuild the nightly image locally,
|
||||
or potentially `ci/docker-rust-nightly/build.sh YYYY-MM-DD` if there's a
|
||||
specific YYYY-MM-DD that is desired (default is today's build).
|
||||
Check https://rust-lang.github.io/rustup-components-history/ for build
|
||||
status
|
||||
1. Update `ci/rust-version.sh` to reflect the new nightly `YYY-MM-DD`
|
||||
1. Run `SOLANA_DOCKER_RUN_NOSETUID=1 ci/docker-run.sh --nopull solanalabs/rust-nightly:YYYY-MM-DD ci/test-coverage.sh`
|
||||
to confirm the new nightly image builds. Fix any issues as needed
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Note: when the rust version is changed also modify
|
||||
# ci/rust-version.sh to pick up the new image tag
|
||||
FROM rust:1.40.0
|
||||
FROM rust:1.43.0
|
||||
|
||||
# Add Google Protocol Buffers for Libra's metrics library.
|
||||
ENV PROTOC_VERSION 3.8.0
|
||||
@@ -17,6 +17,7 @@ RUN set -x \
|
||||
clang-7 \
|
||||
cmake \
|
||||
lcov \
|
||||
libudev-dev \
|
||||
libclang-common-7-dev \
|
||||
mscgen \
|
||||
net-tools \
|
||||
|
@@ -29,7 +29,7 @@ Start a local cluster and run sanity on it
|
||||
-x - Add an extra validator (may be supplied multiple times)
|
||||
-r - Select the RPC endpoint hosted by a node that starts as
|
||||
a validator node. If unspecified the RPC endpoint hosted by
|
||||
the bootstrap leader will be used.
|
||||
the bootstrap validator will be used.
|
||||
-c - Reuse existing node/ledger configuration from a previous sanity
|
||||
run
|
||||
|
||||
@@ -74,7 +74,7 @@ source multinode-demo/common.sh
|
||||
|
||||
nodes=(
|
||||
"multinode-demo/faucet.sh"
|
||||
"multinode-demo/bootstrap-leader.sh \
|
||||
"multinode-demo/bootstrap-validator.sh \
|
||||
--no-restart \
|
||||
--init-complete-file init-complete-node1.log \
|
||||
--dynamic-port-range 8000-8050"
|
||||
@@ -170,7 +170,7 @@ startNodes() {
|
||||
logs+=("$(getNodeLogFile "$i" "$cmd")")
|
||||
fi
|
||||
|
||||
# 1 == bootstrap leader, wait until it boots before starting
|
||||
# 1 == bootstrap validator, wait until it boots before starting
|
||||
# other validators
|
||||
if [[ "$i" -eq 1 ]]; then
|
||||
SECONDS=
|
||||
@@ -178,8 +178,8 @@ startNodes() {
|
||||
|
||||
(
|
||||
set -x
|
||||
$solana_cli --keypair config/bootstrap-leader/identity-keypair.json \
|
||||
--url http://127.0.0.1:8899 get-genesis-hash
|
||||
$solana_cli --keypair config/bootstrap-validator/identity.json \
|
||||
--url http://127.0.0.1:8899 genesis-hash
|
||||
) | tee genesis-hash.log
|
||||
maybeExpectedGenesisHash="--expected-genesis-hash $(tail -n1 genesis-hash.log)"
|
||||
fi
|
||||
@@ -277,7 +277,7 @@ rollingNodeRestart() {
|
||||
}
|
||||
|
||||
verifyLedger() {
|
||||
for ledger in bootstrap-leader validator; do
|
||||
for ledger in bootstrap-validator validator; do
|
||||
echo "--- $ledger ledger verification"
|
||||
(
|
||||
set -x
|
||||
@@ -331,7 +331,7 @@ while [[ $iteration -le $iterations ]]; do
|
||||
rm -rf $client_keypair
|
||||
) || flag_error
|
||||
|
||||
echo "--- RPC API: bootstrap-leader getTransactionCount ($iteration)"
|
||||
echo "--- RPC API: bootstrap-validator getTransactionCount ($iteration)"
|
||||
(
|
||||
set -x
|
||||
curl --retry 5 --retry-delay 2 --retry-connrefused \
|
||||
@@ -351,7 +351,7 @@ while [[ $iteration -le $iterations ]]; do
|
||||
http://localhost:18899
|
||||
) || flag_error
|
||||
|
||||
# Verify transaction count as reported by the bootstrap-leader node is advancing
|
||||
# Verify transaction count as reported by the bootstrap-validator node is advancing
|
||||
transactionCount=$(sed -e 's/{"jsonrpc":"2.0","result":\([0-9]*\),"id":1}/\1/' log-transactionCount.txt)
|
||||
if [[ -n $lastTransactionCount ]]; then
|
||||
echo "--- Transaction count check: $lastTransactionCount < $transactionCount"
|
||||
|
21
ci/maybe-trigger-tests.sh
Executable file
21
ci/maybe-trigger-tests.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
annotate() {
|
||||
${BUILDKITE:-false} && {
|
||||
buildkite-agent annotate "$@"
|
||||
}
|
||||
}
|
||||
|
||||
# Skip if only the docs have been modified
|
||||
ci/affects-files.sh \
|
||||
\!^docs/ \
|
||||
|| {
|
||||
annotate --style info \
|
||||
"Skipping all further tests as only docs/ files were modified"
|
||||
exit 0
|
||||
}
|
||||
|
||||
annotate --style info "Triggering tests"
|
||||
buildkite-agent pipeline upload ci/buildkite-tests.yml
|
25
ci/nits.sh
25
ci/nits.sh
@@ -18,19 +18,22 @@ declare prints=(
|
||||
|
||||
# Parts of the tree that are expected to be print free
|
||||
declare print_free_tree=(
|
||||
'core/src'
|
||||
'faucet/src'
|
||||
'metrics/src'
|
||||
'net-utils/src'
|
||||
'runtime/src'
|
||||
'sdk/bpf/rust/rust-utils'
|
||||
'sdk/src'
|
||||
'programs/bpf/rust'
|
||||
'programs/stake/src'
|
||||
'programs/vote/src'
|
||||
':core/src/**.rs'
|
||||
':faucet/src/**.rs'
|
||||
':ledger/src/**.rs'
|
||||
':metrics/src/**.rs'
|
||||
':net-utils/src/**.rs'
|
||||
':runtime/src/**.rs'
|
||||
':sdk/bpf/rust/rust-utils/**.rs'
|
||||
':sdk/**.rs'
|
||||
':programs/**.rs'
|
||||
':^**bin**.rs'
|
||||
':^**bench**.rs'
|
||||
':^**test**.rs'
|
||||
':^**/build.rs'
|
||||
)
|
||||
|
||||
if _ git --no-pager grep -n --max-depth=0 "${prints[@]/#/-e }" -- "${print_free_tree[@]}"; then
|
||||
if _ git --no-pager grep -n "${prints[@]/#/-e}" -- "${print_free_tree[@]}"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
me=$(basename "$0")
|
||||
|
||||
echo --- update gitbook-cage
|
||||
if [[ -n $CI_BRANCH ]]; then
|
||||
(
|
||||
|
||||
set -x
|
||||
(
|
||||
. ci/rust-version.sh stable
|
||||
ci/docker-run.sh "$rust_stable_docker_image" make -Cbook -B svg
|
||||
)
|
||||
# make a local commit for the svgs
|
||||
git add -A -f book/src/.gitbook/assets/.
|
||||
if ! git diff-index --quiet HEAD; then
|
||||
git config user.email maintainers@solana.com
|
||||
git config user.name "$me"
|
||||
git commit -m "gitbook-cage update $(date -Is)"
|
||||
git push -f git@github.com:solana-labs/solana-gitbook-cage.git HEAD:refs/heads/"$CI_BRANCH"
|
||||
# pop off the local commit
|
||||
git reset --hard HEAD~
|
||||
fi
|
||||
)
|
||||
fi
|
||||
|
||||
|
||||
source ci/rust-version.sh stable
|
||||
eval "$(ci/channel-info.sh)"
|
||||
|
||||
if [[ -n $PUBLISH_BOOK_TAG ]]; then
|
||||
CURRENT_TAG="$(git describe --tags)"
|
||||
COMMIT_TO_PUBLISH="$(git rev-list -n 1 "${PUBLISH_BOOK_TAG}")"
|
||||
|
||||
# book is manually published at a specified release tag
|
||||
if [[ $PUBLISH_BOOK_TAG != "$CURRENT_TAG" ]]; then
|
||||
(
|
||||
cat <<EOF
|
||||
steps:
|
||||
- trigger: "$BUILDKITE_PIPELINE_SLUG"
|
||||
async: true
|
||||
build:
|
||||
message: "$BUILDKITE_MESSAGE"
|
||||
commit: "$COMMIT_TO_PUBLISH"
|
||||
env:
|
||||
PUBLISH_BOOK_TAG: "$PUBLISH_BOOK_TAG"
|
||||
EOF
|
||||
) | buildkite-agent pipeline upload
|
||||
exit 0
|
||||
fi
|
||||
repo=git@github.com:solana-labs/book.git
|
||||
BOOK="book"
|
||||
else
|
||||
# book-edge and book-beta are published automatically on the tip of the branch
|
||||
case $CHANNEL in
|
||||
edge)
|
||||
repo=git@github.com:solana-labs/book-edge.git
|
||||
;;
|
||||
beta)
|
||||
repo=git@github.com:solana-labs/book-beta.git
|
||||
;;
|
||||
*)
|
||||
echo "--- publish skipped"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
BOOK=$CHANNEL
|
||||
fi
|
||||
|
||||
ci/docker-run.sh "$rust_stable_docker_image" bash -exc "book/build.sh"
|
||||
|
||||
echo --- create book repo
|
||||
(
|
||||
set -x
|
||||
cd book/html/
|
||||
git init .
|
||||
git add ./* ./.nojekyll
|
||||
git config user.email maintainers@solana.com
|
||||
git config user.name "$me"
|
||||
git commit -m "${CI_COMMIT:-local}"
|
||||
)
|
||||
|
||||
echo "--- publish $BOOK"
|
||||
(
|
||||
cd book/html/
|
||||
git remote add origin $repo
|
||||
git fetch origin master
|
||||
if ! git diff HEAD origin/master --quiet; then
|
||||
git push -f origin HEAD:master
|
||||
else
|
||||
echo "Content unchanged, publish skipped"
|
||||
fi
|
||||
)
|
||||
|
||||
exit 0
|
31
ci/publish-docs.sh
Executable file
31
ci/publish-docs.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
me=$(basename "$0")
|
||||
|
||||
echo --- update gitbook-cage
|
||||
if [[ -n $CI_BRANCH ]]; then
|
||||
(
|
||||
set -x
|
||||
(
|
||||
. ci/rust-version.sh stable
|
||||
ci/docker-run.sh "$rust_stable_docker_image" make -C docs
|
||||
)
|
||||
# make a local commit for the svgs and generated/updated markdown
|
||||
git add -f docs/src
|
||||
if ! git diff-index --quiet HEAD; then
|
||||
git config user.email maintainers@solana.com
|
||||
git config user.name "$me"
|
||||
git commit -m "gitbook-cage update $(date -Is)"
|
||||
git push -f git@github.com:solana-labs/solana-gitbook-cage.git HEAD:refs/heads/"$CI_BRANCH"
|
||||
# pop off the local commit
|
||||
git reset --hard HEAD~
|
||||
fi
|
||||
)
|
||||
else
|
||||
echo CI_BRANCH not set
|
||||
fi
|
||||
|
||||
exit 0
|
@@ -45,7 +45,7 @@ linux)
|
||||
TARGET=x86_64-unknown-linux-gnu
|
||||
;;
|
||||
windows)
|
||||
TARGET=x86_64-pc-windows-msvc
|
||||
TARGET=x86_64-pc-windows-gnu
|
||||
;;
|
||||
*)
|
||||
echo CI_OS_NAME unset
|
||||
@@ -71,16 +71,7 @@ echo --- Creating release tarball
|
||||
export CHANNEL
|
||||
|
||||
source ci/rust-version.sh stable
|
||||
scripts/cargo-install-all.sh +"$rust_stable" --use-move solana-release
|
||||
|
||||
# Reduce the Windows archive size until
|
||||
# https://github.com/appveyor/ci/issues/2997 is fixed
|
||||
if [[ -n $APPVEYOR ]]; then
|
||||
rm -f \
|
||||
solana-release/bin/solana-validator.exe \
|
||||
solana-release/bin/solana-bench-exchange.exe \
|
||||
|
||||
fi
|
||||
scripts/cargo-install-all.sh +"$rust_stable" solana-release
|
||||
|
||||
tar cvf solana-release-$TARGET.tar solana-release
|
||||
bzip2 solana-release-$TARGET.tar
|
||||
@@ -104,9 +95,8 @@ fi
|
||||
source ci/upload-ci-artifact.sh
|
||||
|
||||
for file in solana-release-$TARGET.tar.bz2 solana-release-$TARGET.yml solana-install-init-"$TARGET"* $MAYBE_TARBALLS; do
|
||||
upload-ci-artifact "$file"
|
||||
|
||||
if [[ -n $DO_NOT_PUBLISH_TAR ]]; then
|
||||
upload-ci-artifact "$file"
|
||||
echo "Skipped $file due to DO_NOT_PUBLISH_TAR"
|
||||
continue
|
||||
fi
|
||||
|
@@ -1,28 +1,30 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# Obtain the environment variables updating both stable and nightly, only stable, or
|
||||
# only nightly:
|
||||
# $ source ci/rust-version.sh all
|
||||
# $ source ci/rust-version.sh stable
|
||||
# $ source ci/rust-version.sh nightly
|
||||
|
||||
# Then to build with either stable or nightly:
|
||||
# $ cargo +"$rust_stable" build
|
||||
# $ cargo +"$rust_nightly" build
|
||||
#
|
||||
|
||||
if [[ -n $RUST_STABLE_VERSION ]]; then
|
||||
stable_version="$RUST_STABLE_VERSION"
|
||||
else
|
||||
stable_version=1.40.0
|
||||
stable_version=1.43.0
|
||||
fi
|
||||
|
||||
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
|
||||
nightly_version="$RUST_NIGHTLY_VERSION"
|
||||
else
|
||||
nightly_version=2019-12-19
|
||||
nightly_version=2020-04-23
|
||||
fi
|
||||
|
||||
|
||||
@@ -51,6 +53,10 @@ export rust_nightly_docker_image=solanalabs/rust-nightly:"$nightly_version"
|
||||
nightly)
|
||||
rustup_install "$rust_nightly"
|
||||
;;
|
||||
all)
|
||||
rustup_install "$rust_stable"
|
||||
rustup_install "$rust_nightly"
|
||||
;;
|
||||
*)
|
||||
echo "Note: ignoring unknown argument: $1"
|
||||
;;
|
||||
|
@@ -25,7 +25,7 @@ source ci/_
|
||||
source ci/upload-ci-artifact.sh
|
||||
|
||||
eval "$(ci/channel-info.sh)"
|
||||
source ci/rust-version.sh nightly
|
||||
source ci/rust-version.sh all
|
||||
|
||||
set -o pipefail
|
||||
export RUST_BACKTRACE=1
|
||||
|
@@ -22,15 +22,15 @@ _ cargo +"$rust_stable" clippy --all --exclude solana-sdk-c -- --deny=warnings
|
||||
_ cargo +"$rust_stable" clippy --manifest-path sdk-c/Cargo.toml -- --deny=warnings
|
||||
|
||||
_ cargo +"$rust_stable" audit --version
|
||||
_ cargo +"$rust_stable" audit --ignore RUSTSEC-2019-0013 --ignore RUSTSEC-2018-0015 --ignore RUSTSEC-2019-0031 --ignore RUSTSEC-2020-0002
|
||||
_ cargo +"$rust_stable" audit --ignore RUSTSEC-2020-0002 --ignore RUSTSEC-2020-0008
|
||||
_ ci/nits.sh
|
||||
_ ci/order-crates-for-publishing.py
|
||||
_ book/build.sh
|
||||
_ docs/build.sh
|
||||
_ ci/check-ssh-keys.sh
|
||||
|
||||
{
|
||||
cd programs/bpf
|
||||
_ cargo +"$rust_stable" audit --ignore RUSTSEC-2019-0031
|
||||
_ cargo +"$rust_stable" audit
|
||||
for project in rust/*/ ; do
|
||||
echo "+++ do_bpf_checks $project"
|
||||
(
|
||||
|
@@ -42,7 +42,9 @@ if [[ -z "$CODECOV_TOKEN" ]]; then
|
||||
echo CODECOV_TOKEN undefined, codecov.io upload skipped
|
||||
else
|
||||
# We normalize CI to `1`; but codecov expects it to be `true` to detect Buildkite...
|
||||
CI=true bash <(curl -s https://codecov.io/bash) -X gcov -f target/cov/lcov.info
|
||||
# Unfortunately, codecov.io fails sometimes:
|
||||
# curl: (7) Failed to connect to codecov.io port 443: Connection timed out
|
||||
CI=true bash <(while ! curl -sS --retry 5 --retry-delay 2 --retry-connrefused https://codecov.io/bash; do sleep 10; done) -Z -X gcov -f target/cov/lcov.info
|
||||
|
||||
annotate --style success --context codecov.io \
|
||||
"CodeCov report: https://codecov.io/github/solana-labs/solana/commit/${CI_COMMIT:0:9}"
|
||||
|
@@ -13,12 +13,12 @@ annotate() {
|
||||
# Run the appropriate test based on entrypoint
|
||||
testName=$(basename "$0" .sh)
|
||||
|
||||
# Skip if only the book has been modified
|
||||
# Skip if only the docs have been modified
|
||||
ci/affects-files.sh \
|
||||
\!^book/ \
|
||||
\!^docs/ \
|
||||
|| {
|
||||
annotate --style info \
|
||||
"Skipped $testName as only book files were modified"
|
||||
"Skipped $testName as only docs/ files were modified"
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -38,11 +38,16 @@ test -d target/release/bpf && find target/release/bpf -name '*.d' -delete
|
||||
# Clear the BPF sysroot files, they are not automatically rebuilt
|
||||
rm -rf target/xargo # Issue #3105
|
||||
|
||||
# Limit compiler jobs to reduce memory usage
|
||||
# on machines with 1gb/thread of memory
|
||||
NPROC=$(nproc)
|
||||
NPROC=$((NPROC>16 ? 16 : NPROC))
|
||||
|
||||
echo "Executing $testName"
|
||||
case $testName in
|
||||
test-stable)
|
||||
_ cargo +"$rust_stable" test --all --exclude solana-local-cluster ${V:+--verbose} -- --nocapture
|
||||
_ cargo +"$rust_stable" test --manifest-path bench-tps/Cargo.toml --features=move ${V:+--verbose} test_bench_tps_local_cluster_move -- --nocapture
|
||||
_ cargo +"$rust_stable" test --jobs "$NPROC" --all --exclude solana-local-cluster ${V:+--verbose} -- --nocapture
|
||||
#_ cargo +"$rust_stable" test --manifest-path bench-tps/Cargo.toml --features=move ${V:+--verbose} test_bench_tps_local_cluster_move -- --nocapture
|
||||
;;
|
||||
test-stable-perf)
|
||||
ci/affects-files.sh \
|
||||
@@ -86,7 +91,7 @@ test-stable-perf)
|
||||
fi
|
||||
|
||||
_ cargo +"$rust_stable" build --bins ${V:+--verbose}
|
||||
_ cargo +"$rust_stable" test --package solana-perf --package solana-ledger --package solana-core --lib ${V:+--verbose} -- --nocapture
|
||||
_ cargo +"$rust_stable" test --package solana-chacha-cuda --package solana-perf --package solana-ledger --package solana-core --lib ${V:+--verbose} -- --nocapture
|
||||
;;
|
||||
test-move)
|
||||
ci/affects-files.sh \
|
||||
|
@@ -32,20 +32,10 @@ steps:
|
||||
options:
|
||||
- label: "testnet"
|
||||
value: "testnet"
|
||||
- label: "testnet-perf"
|
||||
value: "testnet-perf"
|
||||
- label: "testnet-edge"
|
||||
value: "testnet-edge"
|
||||
- label: "testnet-edge-perf"
|
||||
value: "testnet-edge-perf"
|
||||
- label: "testnet-beta"
|
||||
value: "testnet-beta"
|
||||
- label: "testnet-beta-perf"
|
||||
value: "testnet-beta-perf"
|
||||
- label: "testnet-demo"
|
||||
value: "testnet-demo"
|
||||
- label: "tds"
|
||||
value: "tds"
|
||||
- select: "Operation"
|
||||
key: "testnet-operation"
|
||||
default: "sanity-or-restart"
|
||||
@@ -131,11 +121,11 @@ GCE_LOW_QUOTA_ZONES=(
|
||||
)
|
||||
|
||||
case $TESTNET in
|
||||
testnet-edge|testnet-edge-perf)
|
||||
testnet-edge)
|
||||
CHANNEL_OR_TAG=edge
|
||||
CHANNEL_BRANCH=$EDGE_CHANNEL
|
||||
;;
|
||||
testnet-beta|testnet-beta-perf)
|
||||
testnet-beta)
|
||||
CHANNEL_OR_TAG=beta
|
||||
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||
;;
|
||||
@@ -144,22 +134,6 @@ testnet)
|
||||
CHANNEL_BRANCH=$STABLE_CHANNEL
|
||||
export CLOUDSDK_CORE_PROJECT=testnet-solana-com
|
||||
;;
|
||||
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}"
|
||||
;;
|
||||
tds)
|
||||
: "${TDS_CHANNEL_OR_TAG:=edge}"
|
||||
CHANNEL_OR_TAG="$TDS_CHANNEL_OR_TAG"
|
||||
CHANNEL_BRANCH="$CI_BRANCH"
|
||||
export CLOUDSDK_CORE_PROJECT=tour-de-sol
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid TESTNET=$TESTNET"
|
||||
exit 1
|
||||
@@ -206,6 +180,7 @@ steps:
|
||||
TESTNET_DB_HOST: "$TESTNET_DB_HOST"
|
||||
GCE_NODE_COUNT: "$GCE_NODE_COUNT"
|
||||
GCE_LOW_QUOTA_NODE_COUNT: "$GCE_LOW_QUOTA_NODE_COUNT"
|
||||
RUST_LOG: "$RUST_LOG"
|
||||
EOF
|
||||
) | buildkite-agent pipeline upload
|
||||
exit 0
|
||||
@@ -233,64 +208,22 @@ sanity() {
|
||||
(
|
||||
set -x
|
||||
NO_INSTALL_CHECK=1 \
|
||||
ci/testnet-sanity.sh edge-testnet-solana-com gce -P us-west1-b
|
||||
ci/testnet-sanity.sh edge-devnet-solana-com gce -P us-west1-b
|
||||
maybe_deploy_software
|
||||
)
|
||||
;;
|
||||
testnet-edge-perf)
|
||||
(
|
||||
set -x
|
||||
REJECT_EXTRA_NODES=1 \
|
||||
ci/testnet-sanity.sh edge-perf-testnet-solana-com ec2 us-west-2b
|
||||
)
|
||||
;;
|
||||
testnet-beta)
|
||||
(
|
||||
set -x
|
||||
NO_INSTALL_CHECK=1 \
|
||||
ci/testnet-sanity.sh beta-testnet-solana-com gce -P us-west1-b
|
||||
ci/testnet-sanity.sh beta-devnet-solana-com gce -P us-west1-b
|
||||
maybe_deploy_software --deploy-if-newer
|
||||
)
|
||||
;;
|
||||
testnet-beta-perf)
|
||||
(
|
||||
set -x
|
||||
REJECT_EXTRA_NODES=1 \
|
||||
ci/testnet-sanity.sh beta-perf-testnet-solana-com ec2 us-west-2b
|
||||
)
|
||||
;;
|
||||
testnet)
|
||||
(
|
||||
set -x
|
||||
ci/testnet-sanity.sh testnet-solana-com gce -P us-west1-b
|
||||
)
|
||||
;;
|
||||
testnet-perf)
|
||||
(
|
||||
set -x
|
||||
REJECT_EXTRA_NODES=1 \
|
||||
ci/testnet-sanity.sh perf-testnet-solana-com gce us-west1-b
|
||||
#ci/testnet-sanity.sh perf-testnet-solana-com ec2 us-east-1a
|
||||
)
|
||||
;;
|
||||
testnet-demo)
|
||||
(
|
||||
set -x
|
||||
|
||||
ok=true
|
||||
if [[ -n $GCE_NODE_COUNT ]]; then
|
||||
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
|
||||
)
|
||||
;;
|
||||
tds)
|
||||
(
|
||||
set -x
|
||||
ci/testnet-sanity.sh tds-solana-com gce "${GCE_ZONES[0]}" -f
|
||||
ci/testnet-sanity.sh devnet-solana-com gce -P us-west1-b
|
||||
)
|
||||
;;
|
||||
*)
|
||||
@@ -326,9 +259,9 @@ deploy() {
|
||||
testnet-edge)
|
||||
(
|
||||
set -x
|
||||
ci/testnet-deploy.sh -p edge-testnet-solana-com -C gce -z us-west1-b \
|
||||
ci/testnet-deploy.sh -p edge-devnet-solana-com -C gce -z us-west1-b \
|
||||
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P \
|
||||
-a edge-testnet-solana-com --letsencrypt edge.testnet.solana.com \
|
||||
-a edge-devnet-solana-com --letsencrypt edge.devnet.solana.com \
|
||||
--limit-ledger-size \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
@@ -336,24 +269,12 @@ deploy() {
|
||||
${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
testnet-edge-perf)
|
||||
(
|
||||
set -x
|
||||
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 \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
testnet-beta)
|
||||
(
|
||||
set -x
|
||||
ci/testnet-deploy.sh -p beta-testnet-solana-com -C gce -z us-west1-b \
|
||||
ci/testnet-deploy.sh -p beta-devnet-solana-com -C gce -z us-west1-b \
|
||||
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P \
|
||||
-a beta-testnet-solana-com --letsencrypt beta.testnet.solana.com \
|
||||
-a beta-devnet-solana-com --letsencrypt beta.devnet.solana.com \
|
||||
--limit-ledger-size \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
@@ -361,24 +282,12 @@ deploy() {
|
||||
${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
testnet-beta-perf)
|
||||
(
|
||||
set -x
|
||||
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 \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
)
|
||||
;;
|
||||
testnet)
|
||||
(
|
||||
set -x
|
||||
ci/testnet-deploy.sh -p testnet-solana-com -C gce -z us-west1-b \
|
||||
-t "$CHANNEL_OR_TAG" -n 1 -c 0 -u -P \
|
||||
-a testnet-solana-com --letsencrypt testnet.solana.com \
|
||||
ci/testnet-deploy.sh -p devnet-solana-com -C gce -z us-west1-b \
|
||||
-t "$CHANNEL_OR_TAG" -n 0 -c 0 -u -P \
|
||||
-a testnet-solana-com --letsencrypt devnet.solana.com \
|
||||
--limit-ledger-size \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
@@ -388,159 +297,7 @@ deploy() {
|
||||
(
|
||||
echo "--- net.sh update"
|
||||
set -x
|
||||
time net/net.sh update -t "$CHANNEL_OR_TAG" --platform linux --platform osx --platform windows
|
||||
)
|
||||
;;
|
||||
testnet-perf)
|
||||
(
|
||||
set -x
|
||||
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 \
|
||||
-d pd-ssd \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${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 --allow-boot-failures \
|
||||
--skip-remote-log-retrieval \
|
||||
-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 --allow-boot-failures -x \
|
||||
--skip-remote-log-retrieval \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
fi
|
||||
)
|
||||
;;
|
||||
tds)
|
||||
(
|
||||
set -x
|
||||
|
||||
# Allow cluster configuration to be overridden from env vars
|
||||
|
||||
if [[ -z $TDS_ZONES ]]; then
|
||||
TDS_ZONES="us-west1-a,us-central1-a,europe-west4-a"
|
||||
fi
|
||||
GCE_CLOUD_ZONES=(); while read -r -d, ; do GCE_CLOUD_ZONES+=( "$REPLY" ); done <<< "${TDS_ZONES},"
|
||||
|
||||
if [[ -z $TDS_NODE_COUNT ]]; then
|
||||
TDS_NODE_COUNT="3"
|
||||
fi
|
||||
|
||||
if [[ -z $TDS_CLIENT_COUNT ]]; then
|
||||
TDS_CLIENT_COUNT="1"
|
||||
fi
|
||||
|
||||
if [[ -z $ENABLE_GPU ]]; then
|
||||
maybeGpu=(-G "--machine-type n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100")
|
||||
elif [[ $ENABLE_GPU == skip ]]; then
|
||||
maybeGpu=()
|
||||
else
|
||||
maybeGpu=(-G "${ENABLE_GPU}")
|
||||
fi
|
||||
|
||||
if [[ -z $HASHES_PER_TICK ]]; then
|
||||
maybeHashesPerTick="--hashes-per-tick auto"
|
||||
elif [[ $HASHES_PER_TICK == skip ]]; then
|
||||
maybeHashesPerTick=""
|
||||
else
|
||||
maybeHashesPerTick="--hashes-per-tick ${HASHES_PER_TICK}"
|
||||
fi
|
||||
|
||||
if [[ -z $DISABLE_AIRDROPS ]]; then
|
||||
DISABLE_AIRDROPS="true"
|
||||
fi
|
||||
|
||||
if [[ $DISABLE_AIRDROPS == true ]] ; then
|
||||
maybeDisableAirdrops="--no-airdrop"
|
||||
else
|
||||
maybeDisableAirdrops=""
|
||||
fi
|
||||
|
||||
if [[ -z $INTERNAL_NODES_STAKE_LAMPORTS ]]; then
|
||||
maybeInternalNodesStakeLamports="--internal-nodes-stake-lamports 1000000000" # 1 SOL
|
||||
elif [[ $INTERNAL_NODES_STAKE_LAMPORTS == skip ]]; then
|
||||
maybeInternalNodesStakeLamports=""
|
||||
else
|
||||
maybeInternalNodesStakeLamports="--internal-nodes-stake-lamports ${INTERNAL_NODES_STAKE_LAMPORTS}"
|
||||
fi
|
||||
|
||||
if [[ -z $INTERNAL_NODES_LAMPORTS ]]; then
|
||||
maybeInternalNodesLamports="--internal-nodes-lamports 500000000000" # 500 SOL
|
||||
elif [[ $INTERNAL_NODES_LAMPORTS == skip ]]; then
|
||||
maybeInternalNodesLamports=""
|
||||
else
|
||||
maybeInternalNodesLamports="--internal-nodes-lamports ${INTERNAL_NODES_LAMPORTS}"
|
||||
fi
|
||||
|
||||
EXTERNAL_ACCOUNTS_FILE=/tmp/validator.yml
|
||||
if [[ -z $EXTERNAL_ACCOUNTS_FILE_URL ]]; then
|
||||
EXTERNAL_ACCOUNTS_FILE_URL=https://raw.githubusercontent.com/solana-labs/tour-de-sol/master/validators/all.yml
|
||||
wget ${EXTERNAL_ACCOUNTS_FILE_URL} -O ${EXTERNAL_ACCOUNTS_FILE}
|
||||
maybeExternalAccountsFile="--external-accounts-file ${EXTERNAL_ACCOUNTS_FILE}"
|
||||
elif [[ $EXTERNAL_ACCOUNTS_FILE_URL == skip ]]; then
|
||||
maybeExternalAccountsFile=""
|
||||
else
|
||||
wget ${EXTERNAL_ACCOUNTS_FILE_URL} -O ${EXTERNAL_ACCOUNTS_FILE}
|
||||
maybeExternalAccountsFile="--external-accounts-file ${EXTERNAL_ACCOUNTS_FILE}"
|
||||
fi
|
||||
|
||||
if [[ -z $ADDITIONAL_DISK_SIZE_GB ]]; then
|
||||
maybeAdditionalDisk="--validator-additional-disk-size-gb 32000"
|
||||
elif [[ $ADDITIONAL_DISK_SIZE_GB == skip ]]; then
|
||||
maybeAdditionalDisk=""
|
||||
else
|
||||
maybeAdditionalDisk="--validator-additional-disk-size-gb ${ADDITIONAL_DISK_SIZE_GB}"
|
||||
fi
|
||||
|
||||
# Multiple V100 GPUs are available in us-west1, us-central1 and europe-west4
|
||||
# shellcheck disable=SC2068
|
||||
# shellcheck disable=SC2086
|
||||
ci/testnet-deploy.sh -p tds-solana-com -C gce \
|
||||
"${maybeGpu[@]}" \
|
||||
-d pd-ssd \
|
||||
${GCE_CLOUD_ZONES[@]/#/-z } \
|
||||
-t "$CHANNEL_OR_TAG" \
|
||||
-n ${TDS_NODE_COUNT} \
|
||||
-c ${TDS_CLIENT_COUNT} \
|
||||
--idle-clients \
|
||||
-P -u \
|
||||
-a tds-solana-com --letsencrypt tds.solana.com \
|
||||
${maybeHashesPerTick} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D} \
|
||||
${maybeDisableAirdrops} \
|
||||
${maybeInternalNodesStakeLamports} \
|
||||
${maybeInternalNodesLamports} \
|
||||
${maybeExternalAccountsFile} \
|
||||
--target-lamports-per-signature 0 \
|
||||
--slots-per-epoch 4096 \
|
||||
${maybeAdditionalDisk}
|
||||
time net/net.sh update -t "$CHANNEL_OR_TAG" --platform linux --platform osx #--platform windows
|
||||
)
|
||||
;;
|
||||
*)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "solana-clap-utils"
|
||||
version = "0.22.10"
|
||||
version = "1.0.23"
|
||||
description = "Solana utilities for the clap"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
@@ -11,11 +11,15 @@ edition = "2018"
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
rpassword = "4.0"
|
||||
semver = "0.9.0"
|
||||
solana-sdk = { path = "../sdk", version = "0.22.10" }
|
||||
tiny-bip39 = "0.6.2"
|
||||
solana-remote-wallet = { path = "../remote-wallet", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
thiserror = "1.0.11"
|
||||
tiny-bip39 = "0.7.0"
|
||||
url = "2.1.0"
|
||||
chrono = "0.4"
|
||||
|
||||
[lib]
|
||||
name = "solana_clap_utils"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
18
clap-utils/src/commitment.rs
Normal file
18
clap-utils/src/commitment.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use crate::ArgConstant;
|
||||
use clap::Arg;
|
||||
|
||||
pub const COMMITMENT_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "commitment",
|
||||
long: "commitment",
|
||||
help: "Return information at the selected commitment level",
|
||||
};
|
||||
|
||||
pub fn commitment_arg<'a, 'b>() -> Arg<'a, 'b> {
|
||||
Arg::with_name(COMMITMENT_ARG.name)
|
||||
.long(COMMITMENT_ARG.long)
|
||||
.takes_value(true)
|
||||
.possible_values(&["recent", "root", "max"])
|
||||
.default_value("recent")
|
||||
.value_name("COMMITMENT_LEVEL")
|
||||
.help(COMMITMENT_ARG.help)
|
||||
}
|
@@ -1,13 +1,18 @@
|
||||
use crate::keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG};
|
||||
use crate::keypair::{
|
||||
keypair_from_seed_phrase, pubkey_from_path, resolve_signer_from_path, signer_from_path,
|
||||
ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
|
||||
};
|
||||
use chrono::DateTime;
|
||||
use clap::ArgMatches;
|
||||
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
|
||||
use solana_sdk::{
|
||||
clock::UnixTimestamp,
|
||||
commitment_config::CommitmentConfig,
|
||||
native_token::sol_to_lamports,
|
||||
pubkey::Pubkey,
|
||||
signature::{read_keypair_file, Keypair, KeypairUtil, Signature},
|
||||
signature::{read_keypair_file, Keypair, Signature, Signer},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
// Return parsed values from matches at `name`
|
||||
pub fn values_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<T>>
|
||||
@@ -64,6 +69,20 @@ pub fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option<Pubkey> {
|
||||
value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey()))
|
||||
}
|
||||
|
||||
pub fn pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<Pubkey>> {
|
||||
matches.values_of(name).map(|values| {
|
||||
values
|
||||
.map(|value| {
|
||||
value.parse::<Pubkey>().unwrap_or_else(|_| {
|
||||
read_keypair_file(value)
|
||||
.expect("read_keypair_file failed")
|
||||
.pubkey()
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
// Return pubkey/signature pairs for a string of the form pubkey=signature
|
||||
pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<(Pubkey, Signature)>> {
|
||||
matches.values_of(name).map(|values| {
|
||||
@@ -78,14 +97,81 @@ pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<(Pubk
|
||||
})
|
||||
}
|
||||
|
||||
pub fn amount_of(matches: &ArgMatches<'_>, name: &str, unit: &str) -> Option<u64> {
|
||||
if matches.value_of(unit) == Some("lamports") {
|
||||
value_of(matches, name)
|
||||
// Return a signer from matches at `name`
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn signer_of(
|
||||
matches: &ArgMatches<'_>,
|
||||
name: &str,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<(Option<Box<dyn Signer>>, Option<Pubkey>), Box<dyn std::error::Error>> {
|
||||
if let Some(location) = matches.value_of(name) {
|
||||
let signer = signer_from_path(matches, location, name, wallet_manager)?;
|
||||
let signer_pubkey = signer.pubkey();
|
||||
Ok((Some(signer), Some(signer_pubkey)))
|
||||
} else {
|
||||
value_of(matches, name).map(sol_to_lamports)
|
||||
Ok((None, None))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pubkey_of_signer(
|
||||
matches: &ArgMatches<'_>,
|
||||
name: &str,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<Option<Pubkey>, Box<dyn std::error::Error>> {
|
||||
if let Some(location) = matches.value_of(name) {
|
||||
Ok(Some(pubkey_from_path(
|
||||
matches,
|
||||
location,
|
||||
name,
|
||||
wallet_manager,
|
||||
)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pubkeys_of_multiple_signers(
|
||||
matches: &ArgMatches<'_>,
|
||||
name: &str,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<Option<Vec<Pubkey>>, Box<dyn std::error::Error>> {
|
||||
if let Some(pubkey_matches) = matches.values_of(name) {
|
||||
let mut pubkeys: Vec<Pubkey> = vec![];
|
||||
for signer in pubkey_matches {
|
||||
pubkeys.push(pubkey_from_path(matches, signer, name, wallet_manager)?);
|
||||
}
|
||||
Ok(Some(pubkeys))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_signer(
|
||||
matches: &ArgMatches<'_>,
|
||||
name: &str,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
Ok(resolve_signer_from_path(
|
||||
matches,
|
||||
matches.value_of(name).unwrap(),
|
||||
name,
|
||||
wallet_manager,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn lamports_of_sol(matches: &ArgMatches<'_>, name: &str) -> Option<u64> {
|
||||
value_of(matches, name).map(sol_to_lamports)
|
||||
}
|
||||
|
||||
pub fn commitment_of(matches: &ArgMatches<'_>, name: &str) -> Option<CommitmentConfig> {
|
||||
matches.value_of(name).map(|value| match value {
|
||||
"max" => CommitmentConfig::max(),
|
||||
"recent" => CommitmentConfig::recent(),
|
||||
"root" => CommitmentConfig::root(),
|
||||
_ => CommitmentConfig::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -154,7 +240,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_keypair_of() {
|
||||
let keypair = Keypair::new();
|
||||
let outfile = tmp_file_path("test_gen_keypair_file.json", &keypair.pubkey());
|
||||
let outfile = tmp_file_path("test_keypair_of.json", &keypair.pubkey());
|
||||
let _ = write_keypair_file(&keypair, &outfile).unwrap();
|
||||
|
||||
let matches = app()
|
||||
@@ -178,7 +264,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_pubkey_of() {
|
||||
let keypair = Keypair::new();
|
||||
let outfile = tmp_file_path("test_gen_keypair_file.json", &keypair.pubkey());
|
||||
let outfile = tmp_file_path("test_pubkey_of.json", &keypair.pubkey());
|
||||
let _ = write_keypair_file(&keypair, &outfile).unwrap();
|
||||
|
||||
let matches = app()
|
||||
@@ -202,6 +288,26 @@ mod tests {
|
||||
fs::remove_file(&outfile).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pubkeys_of() {
|
||||
let keypair = Keypair::new();
|
||||
let outfile = tmp_file_path("test_pubkeys_of.json", &keypair.pubkey());
|
||||
let _ = write_keypair_file(&keypair, &outfile).unwrap();
|
||||
|
||||
let matches = app().clone().get_matches_from(vec![
|
||||
"test",
|
||||
"--multiple",
|
||||
&keypair.pubkey().to_string(),
|
||||
"--multiple",
|
||||
&outfile,
|
||||
]);
|
||||
assert_eq!(
|
||||
pubkeys_of(&matches, "multiple"),
|
||||
Some(vec![keypair.pubkey(), keypair.pubkey()])
|
||||
);
|
||||
fs::remove_file(&outfile).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pubkeys_sigs_of() {
|
||||
let key1 = Pubkey::new_rand();
|
||||
@@ -224,23 +330,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_amount_of() {
|
||||
fn test_lamports_of_sol() {
|
||||
let matches = app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", "50", "--unit", "lamports"]);
|
||||
assert_eq!(amount_of(&matches, "single", "unit"), Some(50));
|
||||
assert_eq!(amount_of(&matches, "multiple", "unit"), None);
|
||||
.get_matches_from(vec!["test", "--single", "50"]);
|
||||
assert_eq!(lamports_of_sol(&matches, "single"), Some(50000000000));
|
||||
assert_eq!(lamports_of_sol(&matches, "multiple"), None);
|
||||
let matches = app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", "50", "--unit", "SOL"]);
|
||||
assert_eq!(amount_of(&matches, "single", "unit"), Some(50000000000));
|
||||
.get_matches_from(vec!["test", "--single", "1.5"]);
|
||||
assert_eq!(lamports_of_sol(&matches, "single"), Some(1500000000));
|
||||
assert_eq!(lamports_of_sol(&matches, "multiple"), None);
|
||||
let matches = app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", "1.5", "--unit", "SOL"]);
|
||||
assert_eq!(amount_of(&matches, "single", "unit"), Some(1500000000));
|
||||
let matches = app()
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "--single", "1.5", "--unit", "lamports"]);
|
||||
assert_eq!(amount_of(&matches, "single", "unit"), None);
|
||||
.get_matches_from(vec!["test", "--single", "0.03"]);
|
||||
assert_eq!(lamports_of_sol(&matches, "single"), Some(30000000));
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,18 @@
|
||||
use crate::keypair::ASK_KEYWORD;
|
||||
use crate::keypair::{parse_keypair_path, KeypairUrl, ASK_KEYWORD};
|
||||
use chrono::DateTime;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{read_keypair_file, Signature};
|
||||
use solana_sdk::{
|
||||
clock::Slot,
|
||||
hash::Hash,
|
||||
pubkey::Pubkey,
|
||||
signature::{read_keypair_file, Signature},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
// Return an error if a pubkey cannot be parsed.
|
||||
pub fn is_pubkey(string: String) -> Result<(), String> {
|
||||
match string.parse::<Pubkey>() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +20,7 @@ pub fn is_pubkey(string: String) -> Result<(), String> {
|
||||
pub fn is_hash(string: String) -> Result<(), String> {
|
||||
match string.parse::<Hash>() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +28,7 @@ pub fn is_hash(string: String) -> Result<(), String> {
|
||||
pub fn is_keypair(string: String) -> Result<(), String> {
|
||||
read_keypair_file(&string)
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("{:?}", err))
|
||||
.map_err(|err| format!("{}", err))
|
||||
}
|
||||
|
||||
// Return an error if a keypair file cannot be parsed
|
||||
@@ -35,7 +38,7 @@ pub fn is_keypair_or_ask_keyword(string: String) -> Result<(), String> {
|
||||
}
|
||||
read_keypair_file(&string)
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("{:?}", err))
|
||||
.map_err(|err| format!("{}", err))
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as pubkey string or keypair file location
|
||||
@@ -43,9 +46,25 @@ pub fn is_pubkey_or_keypair(string: String) -> Result<(), String> {
|
||||
is_pubkey(string.clone()).or_else(|_| is_keypair(string))
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as pubkey or keypair file or keypair ask keyword
|
||||
pub fn is_pubkey_or_keypair_or_ask_keyword(string: String) -> Result<(), String> {
|
||||
is_pubkey(string.clone()).or_else(|_| is_keypair_or_ask_keyword(string))
|
||||
// Return an error if string cannot be parsed as a pubkey string, or a valid Signer that can
|
||||
// produce a pubkey()
|
||||
pub fn is_valid_pubkey(string: String) -> Result<(), String> {
|
||||
match parse_keypair_path(&string) {
|
||||
KeypairUrl::Filepath(path) => is_keypair(path),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as a valid Signer. This is an alias of
|
||||
// `is_valid_pubkey`, and does accept pubkey strings, even though a Pubkey is not by itself
|
||||
// sufficient to sign a transaction.
|
||||
//
|
||||
// In the current offline-signing implementation, a pubkey is the valid input for a signer field
|
||||
// when paired with an offline `--signer` argument to provide a Presigner (pubkey + signature).
|
||||
// Clap validators can't check multiple fields at once, so the verification that a `--signer` is
|
||||
// also provided and correct happens in parsing, not in validation.
|
||||
pub fn is_valid_signer(string: String) -> Result<(), String> {
|
||||
is_valid_pubkey(string)
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as pubkey=signature string
|
||||
@@ -63,10 +82,10 @@ pub fn is_pubkey_sig(string: String) -> Result<(), String> {
|
||||
.ok_or_else(|| "Malformed signer string".to_string())?,
|
||||
) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,28 +99,20 @@ pub fn is_url(string: String) -> Result<(), String> {
|
||||
Err("no host provided".to_string())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
Err(err) => Err(format!("{}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_semver(semver: &str) -> Result<(), String> {
|
||||
match semver::Version::parse(&semver) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_release_channel(channel: &str) -> Result<(), String> {
|
||||
match channel {
|
||||
"edge" | "beta" | "stable" => Ok(()),
|
||||
_ => Err(format!("Invalid release channel {}", channel)),
|
||||
}
|
||||
pub fn is_slot(slot: String) -> Result<(), String> {
|
||||
slot.parse::<Slot>()
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("{}", e))
|
||||
}
|
||||
|
||||
pub fn is_port(port: String) -> Result<(), String> {
|
||||
port.parse::<u16>()
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
.map_err(|e| format!("{}", e))
|
||||
}
|
||||
|
||||
pub fn is_valid_percentage(percentage: String) -> Result<(), String> {
|
||||
@@ -109,7 +120,7 @@ pub fn is_valid_percentage(percentage: String) -> Result<(), String> {
|
||||
.parse::<u8>()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Unable to parse input percentage, provided: {}, err: {:?}",
|
||||
"Unable to parse input percentage, provided: {}, err: {}",
|
||||
percentage, e
|
||||
)
|
||||
})
|
||||
@@ -139,5 +150,50 @@ pub fn is_amount(amount: String) -> Result<(), String> {
|
||||
pub fn is_rfc3339_datetime(value: String) -> Result<(), String> {
|
||||
DateTime::parse_from_rfc3339(&value)
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
.map_err(|e| format!("{}", e))
|
||||
}
|
||||
|
||||
pub fn is_derivation(value: String) -> Result<(), String> {
|
||||
let value = value.replace("'", "");
|
||||
let mut parts = value.split('/');
|
||||
let account = parts.next().unwrap();
|
||||
account
|
||||
.parse::<u32>()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Unable to parse derivation, provided: {}, err: {}",
|
||||
account, e
|
||||
)
|
||||
})
|
||||
.and_then(|_| {
|
||||
if let Some(change) = parts.next() {
|
||||
change.parse::<u32>().map_err(|e| {
|
||||
format!(
|
||||
"Unable to parse derivation, provided: {}, err: {}",
|
||||
change, e
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_derivation() {
|
||||
assert_eq!(is_derivation("2".to_string()), Ok(()));
|
||||
assert_eq!(is_derivation("0".to_string()), Ok(()));
|
||||
assert_eq!(is_derivation("65537".to_string()), Ok(()));
|
||||
assert_eq!(is_derivation("0/2".to_string()), Ok(()));
|
||||
assert_eq!(is_derivation("0'/2'".to_string()), Ok(()));
|
||||
assert!(is_derivation("a".to_string()).is_err());
|
||||
assert!(is_derivation("4294967296".to_string()).is_err());
|
||||
assert!(is_derivation("a/b".to_string()).is_err());
|
||||
assert!(is_derivation("0/4294967296".to_string()).is_err());
|
||||
}
|
||||
}
|
||||
|
@@ -1,53 +1,189 @@
|
||||
use crate::ArgConstant;
|
||||
use crate::{
|
||||
input_parsers::pubkeys_sigs_of,
|
||||
offline::{SIGNER_ARG, SIGN_ONLY_ARG},
|
||||
ArgConstant,
|
||||
};
|
||||
use bip39::{Language, Mnemonic, Seed};
|
||||
use clap::values_t;
|
||||
use clap::ArgMatches;
|
||||
use rpassword::prompt_password_stderr;
|
||||
use solana_remote_wallet::{
|
||||
remote_keypair::generate_remote_keypair,
|
||||
remote_wallet::{RemoteWalletError, RemoteWalletManager},
|
||||
};
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
signature::{
|
||||
keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair_file, Keypair,
|
||||
KeypairUtil,
|
||||
keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair,
|
||||
read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
error,
|
||||
io::{stdin, stdout, Write},
|
||||
process::exit,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub enum KeypairUrl {
|
||||
Ask,
|
||||
Filepath(String),
|
||||
Usb(String),
|
||||
Stdin,
|
||||
Pubkey(Pubkey),
|
||||
}
|
||||
|
||||
pub fn parse_keypair_path(path: &str) -> KeypairUrl {
|
||||
if path == "-" {
|
||||
KeypairUrl::Stdin
|
||||
} else if path == ASK_KEYWORD {
|
||||
KeypairUrl::Ask
|
||||
} else if path.starts_with("usb://") {
|
||||
KeypairUrl::Usb(path.to_string())
|
||||
} else if let Ok(pubkey) = Pubkey::from_str(path) {
|
||||
KeypairUrl::Pubkey(pubkey)
|
||||
} else {
|
||||
KeypairUrl::Filepath(path.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn presigner_from_pubkey_sigs(
|
||||
pubkey: &Pubkey,
|
||||
signers: &[(Pubkey, Signature)],
|
||||
) -> Option<Presigner> {
|
||||
signers.iter().find_map(|(signer, sig)| {
|
||||
if *signer == *pubkey {
|
||||
Some(Presigner::new(signer, sig))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn signer_from_path(
|
||||
matches: &ArgMatches,
|
||||
path: &str,
|
||||
keypair_name: &str,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<Box<dyn Signer>, Box<dyn error::Error>> {
|
||||
match parse_keypair_path(path) {
|
||||
KeypairUrl::Ask => {
|
||||
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
||||
Ok(Box::new(keypair_from_seed_phrase(
|
||||
keypair_name,
|
||||
skip_validation,
|
||||
false,
|
||||
)?))
|
||||
}
|
||||
KeypairUrl::Filepath(path) => match read_keypair_file(&path) {
|
||||
Err(e) => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("could not find keypair file: {} error: {}", path, e),
|
||||
)
|
||||
.into()),
|
||||
Ok(file) => Ok(Box::new(file)),
|
||||
},
|
||||
KeypairUrl::Stdin => {
|
||||
let mut stdin = std::io::stdin();
|
||||
Ok(Box::new(read_keypair(&mut stdin)?))
|
||||
}
|
||||
KeypairUrl::Usb(path) => {
|
||||
if let Some(wallet_manager) = wallet_manager {
|
||||
Ok(Box::new(generate_remote_keypair(
|
||||
path,
|
||||
wallet_manager,
|
||||
matches.is_present("confirm_key"),
|
||||
keypair_name,
|
||||
)?))
|
||||
} else {
|
||||
Err(RemoteWalletError::NoDeviceFound.into())
|
||||
}
|
||||
}
|
||||
KeypairUrl::Pubkey(pubkey) => {
|
||||
let presigner = pubkeys_sigs_of(matches, SIGNER_ARG.name)
|
||||
.as_ref()
|
||||
.and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners));
|
||||
if let Some(presigner) = presigner {
|
||||
Ok(Box::new(presigner))
|
||||
} else if matches.is_present(SIGN_ONLY_ARG.name) {
|
||||
Ok(Box::new(NullSigner::new(&pubkey)))
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("missing signature for supplied pubkey: {}", pubkey),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pubkey_from_path(
|
||||
matches: &ArgMatches,
|
||||
path: &str,
|
||||
keypair_name: &str,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<Pubkey, Box<dyn error::Error>> {
|
||||
match parse_keypair_path(path) {
|
||||
KeypairUrl::Pubkey(pubkey) => Ok(pubkey),
|
||||
_ => Ok(signer_from_path(matches, path, keypair_name, wallet_manager)?.pubkey()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_signer_from_path(
|
||||
matches: &ArgMatches,
|
||||
path: &str,
|
||||
keypair_name: &str,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<Option<String>, Box<dyn error::Error>> {
|
||||
match parse_keypair_path(path) {
|
||||
KeypairUrl::Ask => {
|
||||
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
||||
// This method validates the seed phrase, but returns `None` because there is no path
|
||||
// on disk or to a device
|
||||
keypair_from_seed_phrase(keypair_name, skip_validation, false).map(|_| None)
|
||||
}
|
||||
KeypairUrl::Filepath(path) => match read_keypair_file(&path) {
|
||||
Err(e) => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("could not find keypair file: {} error: {}", path, e),
|
||||
)
|
||||
.into()),
|
||||
Ok(_) => Ok(Some(path.to_string())),
|
||||
},
|
||||
KeypairUrl::Stdin => {
|
||||
let mut stdin = std::io::stdin();
|
||||
// This method validates the keypair from stdin, but returns `None` because there is no
|
||||
// path on disk or to a device
|
||||
read_keypair(&mut stdin).map(|_| None)
|
||||
}
|
||||
KeypairUrl::Usb(path) => {
|
||||
if let Some(wallet_manager) = wallet_manager {
|
||||
let path = generate_remote_keypair(
|
||||
path,
|
||||
wallet_manager,
|
||||
matches.is_present("confirm_key"),
|
||||
keypair_name,
|
||||
)
|
||||
.map(|keypair| keypair.path)?;
|
||||
Ok(Some(path))
|
||||
} else {
|
||||
Err(RemoteWalletError::NoDeviceFound.into())
|
||||
}
|
||||
}
|
||||
_ => Ok(Some(path.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
// Keyword used to indicate that the user should be asked for a keypair seed phrase
|
||||
pub const ASK_KEYWORD: &str = "ASK";
|
||||
|
||||
pub const ASK_SEED_PHRASE_ARG: ArgConstant<'static> = ArgConstant {
|
||||
long: "ask-seed-phrase",
|
||||
name: "ask_seed_phrase",
|
||||
help: "Securely recover a keypair using a seed phrase and optional passphrase",
|
||||
};
|
||||
|
||||
pub const SKIP_SEED_PHRASE_VALIDATION_ARG: ArgConstant<'static> = ArgConstant {
|
||||
long: "skip-seed-phrase-validation",
|
||||
name: "skip_seed_phrase_validation",
|
||||
help: "Skip validation of seed phrases. Use this if your phrase does not use the BIP39 official English word list",
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Source {
|
||||
File,
|
||||
Generated,
|
||||
SeedPhrase,
|
||||
}
|
||||
|
||||
pub struct KeypairWithSource {
|
||||
pub keypair: Keypair,
|
||||
pub source: Source,
|
||||
}
|
||||
|
||||
impl KeypairWithSource {
|
||||
fn new(keypair: Keypair, source: Source) -> Self {
|
||||
Self { keypair, source }
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompts user for a passphrase and then asks for confirmirmation to check for mistakes
|
||||
pub fn prompt_passphrase(prompt: &str) -> Result<String, Box<dyn error::Error>> {
|
||||
let passphrase = prompt_password_stderr(&prompt)?;
|
||||
@@ -80,14 +216,14 @@ pub fn keypair_from_seed_phrase(
|
||||
keypair_from_seed_phrase_and_passphrase(&seed_phrase, &passphrase)?
|
||||
} else {
|
||||
let sanitized = sanitize_seed_phrase(seed_phrase);
|
||||
let mnemonic = Mnemonic::from_phrase(sanitized, Language::English)?;
|
||||
let mnemonic = Mnemonic::from_phrase(&sanitized, Language::English)?;
|
||||
let passphrase = prompt_passphrase(&passphrase_prompt)?;
|
||||
let seed = Seed::new(&mnemonic, &passphrase);
|
||||
keypair_from_seed(seed.as_bytes())?
|
||||
};
|
||||
|
||||
if confirm_pubkey {
|
||||
let pubkey = Pubkey::new(keypair.public.as_ref());
|
||||
let pubkey = keypair.pubkey();
|
||||
print!("Recovered pubkey `{:?}`. Continue? (y/n): ", pubkey);
|
||||
let _ignored = stdout().flush();
|
||||
let mut input = String::new();
|
||||
@@ -101,42 +237,6 @@ pub fn keypair_from_seed_phrase(
|
||||
Ok(keypair)
|
||||
}
|
||||
|
||||
/// Checks CLI arguments to determine whether a keypair should be:
|
||||
/// - inputted securely via stdin,
|
||||
/// - read in from a file,
|
||||
/// - or newly generated
|
||||
pub fn keypair_input(
|
||||
matches: &clap::ArgMatches,
|
||||
keypair_name: &str,
|
||||
) -> Result<KeypairWithSource, Box<dyn error::Error>> {
|
||||
let ask_seed_phrase_matches =
|
||||
values_t!(matches.values_of(ASK_SEED_PHRASE_ARG.name), String).unwrap_or_default();
|
||||
let keypair_match_name = keypair_name.replace('-', "_");
|
||||
if ask_seed_phrase_matches
|
||||
.iter()
|
||||
.any(|s| s.as_str() == keypair_name)
|
||||
{
|
||||
if matches.value_of(keypair_match_name).is_some() {
|
||||
clap::Error::with_description(
|
||||
&format!(
|
||||
"`--{} {}` cannot be used with `{} <PATH>`",
|
||||
ASK_SEED_PHRASE_ARG.long, keypair_name, keypair_name
|
||||
),
|
||||
clap::ErrorKind::ArgumentConflict,
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
|
||||
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
||||
keypair_from_seed_phrase(keypair_name, skip_validation, true)
|
||||
.map(|keypair| KeypairWithSource::new(keypair, Source::SeedPhrase))
|
||||
} else if let Some(keypair_file) = matches.value_of(keypair_match_name) {
|
||||
read_keypair_file(keypair_file).map(|keypair| KeypairWithSource::new(keypair, Source::File))
|
||||
} else {
|
||||
Ok(KeypairWithSource::new(Keypair::new(), Source::Generated))
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_seed_phrase(seed_phrase: &str) -> String {
|
||||
seed_phrase
|
||||
.split_whitespace()
|
||||
@@ -147,14 +247,6 @@ fn sanitize_seed_phrase(seed_phrase: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::ArgMatches;
|
||||
|
||||
#[test]
|
||||
fn test_keypair_input() {
|
||||
let arg_matches = ArgMatches::default();
|
||||
let KeypairWithSource { source, .. } = keypair_input(&arg_matches, "").unwrap();
|
||||
assert_eq!(source, Source::Generated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_seed_phrase() {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! version {
|
||||
() => {
|
||||
@@ -23,6 +25,25 @@ pub struct ArgConstant<'a> {
|
||||
pub help: &'a str,
|
||||
}
|
||||
|
||||
/// Error type for forwarding Errors out of `main()` of a `clap` app
|
||||
/// and still using the `Display` formatter
|
||||
#[derive(Error)]
|
||||
#[error("{0}")]
|
||||
pub struct DisplayError(Box<dyn std::error::Error>);
|
||||
impl DisplayError {
|
||||
pub fn new_as_boxed(inner: Box<dyn std::error::Error>) -> Box<Self> {
|
||||
DisplayError(inner).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DisplayError {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod commitment;
|
||||
pub mod input_parsers;
|
||||
pub mod input_validators;
|
||||
pub mod keypair;
|
||||
pub mod offline;
|
||||
|
19
clap-utils/src/offline.rs
Normal file
19
clap-utils/src/offline.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::ArgConstant;
|
||||
|
||||
pub const BLOCKHASH_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "blockhash",
|
||||
long: "blockhash",
|
||||
help: "Use the supplied blockhash",
|
||||
};
|
||||
|
||||
pub const SIGN_ONLY_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "sign_only",
|
||||
long: "sign-only",
|
||||
help: "Sign the transaction offline",
|
||||
};
|
||||
|
||||
pub const SIGNER_ARG: ArgConstant<'static> = ArgConstant {
|
||||
name: "signer",
|
||||
long: "signer",
|
||||
help: "Provide a public-key/signature pair for the transaction",
|
||||
};
|
@@ -1,19 +1,20 @@
|
||||
[package]
|
||||
name = "solana-fixed-buf"
|
||||
version = "0.22.10"
|
||||
description = "A fixed-size byte array that supports bincode serde"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-cli-config"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "1.0.23"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.2.1"
|
||||
dirs = "2.0.2"
|
||||
lazy_static = "1.4.0"
|
||||
serde = "1.0.104"
|
||||
|
||||
[lib]
|
||||
name = "solana_fixed_buf"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_derive = "1.0.103"
|
||||
serde_yaml = "0.8.11"
|
||||
url = "2.1.1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
97
cli-config/src/config.rs
Normal file
97
cli-config/src/config.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
// Wallet settings that can be configured for long-term use
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::io;
|
||||
use url::Url;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG_FILE: Option<String> = {
|
||||
dirs::home_dir().map(|mut path| {
|
||||
path.extend(&[".config", "solana", "cli", "config.yml"]);
|
||||
path.to_str().unwrap().to_string()
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct Config {
|
||||
pub json_rpc_url: String,
|
||||
pub websocket_url: String,
|
||||
pub keypair_path: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let keypair_path = {
|
||||
let mut keypair_path = dirs::home_dir().expect("home directory");
|
||||
keypair_path.extend(&[".config", "solana", "id.json"]);
|
||||
keypair_path.to_str().unwrap().to_string()
|
||||
};
|
||||
let json_rpc_url = "http://127.0.0.1:8899".to_string();
|
||||
|
||||
// Empty websocket_url string indicates the client should
|
||||
// `Config::compute_websocket_url(&json_rpc_url)`
|
||||
let websocket_url = "".to_string();
|
||||
|
||||
Self {
|
||||
json_rpc_url,
|
||||
websocket_url,
|
||||
keypair_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(config_file: &str) -> Result<Self, io::Error> {
|
||||
crate::load_config_file(config_file)
|
||||
}
|
||||
|
||||
pub fn save(&self, config_file: &str) -> Result<(), io::Error> {
|
||||
crate::save_config_file(self, config_file)
|
||||
}
|
||||
|
||||
pub fn compute_websocket_url(json_rpc_url: &str) -> String {
|
||||
let json_rpc_url: Option<Url> = json_rpc_url.parse().ok();
|
||||
if json_rpc_url.is_none() {
|
||||
return "".to_string();
|
||||
}
|
||||
let json_rpc_url = json_rpc_url.unwrap();
|
||||
let is_secure = json_rpc_url.scheme().to_ascii_lowercase() == "https";
|
||||
let mut ws_url = json_rpc_url.clone();
|
||||
ws_url
|
||||
.set_scheme(if is_secure { "wss" } else { "ws" })
|
||||
.expect("unable to set scheme");
|
||||
if let Some(port) = json_rpc_url.port() {
|
||||
ws_url.set_port(Some(port + 1)).expect("unable to set port");
|
||||
}
|
||||
ws_url.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn compute_websocket_url() {
|
||||
assert_eq!(
|
||||
Config::compute_websocket_url(&"http://devnet.solana.com"),
|
||||
"ws://devnet.solana.com/".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Config::compute_websocket_url(&"https://devnet.solana.com"),
|
||||
"wss://devnet.solana.com/".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Config::compute_websocket_url(&"http://example.com:8899"),
|
||||
"ws://example.com:8900/".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Config::compute_websocket_url(&"https://example.com:1234"),
|
||||
"wss://example.com:1235/".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(Config::compute_websocket_url(&"garbage"), String::new());
|
||||
}
|
||||
}
|
39
cli-config/src/lib.rs
Normal file
39
cli-config/src/lib.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
mod config;
|
||||
pub use config::{Config, CONFIG_FILE};
|
||||
|
||||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
pub fn load_config_file<T, P>(config_file: P) -> Result<T, io::Error>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(config_file)?;
|
||||
let config = serde_yaml::from_reader(file)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save_config_file<T, P>(config: &T, config_file: P) -> Result<(), io::Error>
|
||||
where
|
||||
T: serde::ser::Serialize,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let serialized = serde_yaml::to_string(config)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
|
||||
|
||||
if let Some(outdir) = config_file.as_ref().parent() {
|
||||
create_dir_all(outdir)?;
|
||||
}
|
||||
let mut file = File::create(config_file)?;
|
||||
file.write_all(&serialized.into_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -3,7 +3,7 @@ authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-cli"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "0.22.10"
|
||||
version = "1.0.23"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@@ -14,40 +14,46 @@ bs58 = "0.3.0"
|
||||
chrono = { version = "0.4.10", features = ["serde"] }
|
||||
clap = "2.33.0"
|
||||
criterion-stats = "0.3.0"
|
||||
ctrlc = { version = "3.1.3", features = ["termination"] }
|
||||
console = "0.9.1"
|
||||
ctrlc = { version = "3.1.4", features = ["termination"] }
|
||||
console = "0.9.2"
|
||||
dirs = "2.0.2"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.8"
|
||||
indicatif = "0.13.0"
|
||||
humantime = "1.3.0"
|
||||
indicatif = "0.14.0"
|
||||
humantime = "2.0.0"
|
||||
num-traits = "0.2"
|
||||
pretty-hex = "0.1.1"
|
||||
reqwest = { version = "0.9.24", default-features = false, features = ["rustls-tls"] }
|
||||
reqwest = { version = "0.10.1", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||
serde = "1.0.104"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.44"
|
||||
serde_yaml = "0.8.11"
|
||||
solana-budget-program = { path = "../programs/budget", version = "0.22.10" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "0.22.10" }
|
||||
solana-client = { path = "../client", version = "0.22.10" }
|
||||
solana-config-program = { path = "../programs/config", version = "0.22.10" }
|
||||
solana-faucet = { path = "../faucet", version = "0.22.10" }
|
||||
solana-logger = { path = "../logger", version = "0.22.10" }
|
||||
solana-net-utils = { path = "../net-utils", version = "0.22.10" }
|
||||
solana-runtime = { path = "../runtime", version = "0.22.10" }
|
||||
solana-sdk = { path = "../sdk", version = "0.22.10" }
|
||||
solana-stake-program = { path = "../programs/stake", version = "0.22.10" }
|
||||
solana-storage-program = { path = "../programs/storage", version = "0.22.10" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "0.22.10" }
|
||||
solana-vote-signer = { path = "../vote-signer", version = "0.22.10" }
|
||||
url = "2.1.0"
|
||||
serde_json = "1.0.46"
|
||||
solana-budget-program = { path = "../programs/budget", version = "1.0.23" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.23" }
|
||||
solana-cli-config = { path = "../cli-config", version = "1.0.23" }
|
||||
solana-client = { path = "../client", version = "1.0.23" }
|
||||
solana-config-program = { path = "../programs/config", version = "1.0.23" }
|
||||
solana-faucet = { path = "../faucet", version = "1.0.23" }
|
||||
solana-logger = { path = "../logger", version = "1.0.23" }
|
||||
solana-net-utils = { path = "../net-utils", version = "1.0.23" }
|
||||
solana-remote-wallet = { path = "../remote-wallet", version = "1.0.23" }
|
||||
solana-runtime = { path = "../runtime", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
solana-stake-program = { path = "../programs/stake", version = "1.0.23" }
|
||||
solana-storage-program = { path = "../programs/storage", version = "1.0.23" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "1.0.23" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "1.0.23" }
|
||||
solana-vote-signer = { path = "../vote-signer", version = "1.0.23" }
|
||||
titlecase = "1.1.0"
|
||||
thiserror = "1.0.11"
|
||||
url = "2.1.1"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-core = { path = "../core", version = "0.22.10" }
|
||||
solana-budget-program = { path = "../programs/budget", version = "0.22.10" }
|
||||
solana-core = { path = "../core", version = "1.0.23" }
|
||||
solana-budget-program = { path = "../programs/budget", version = "1.0.23" }
|
||||
tempfile = "3.1.0"
|
||||
|
||||
[[bin]]
|
||||
name = "solana"
|
||||
path = "src/main.rs"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
2378
cli/src/cli.rs
2378
cli/src/cli.rs
File diff suppressed because it is too large
Load Diff
@@ -8,20 +8,37 @@ use crate::{
|
||||
use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand};
|
||||
use console::{style, Emoji};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use solana_clap_utils::{input_parsers::*, input_validators::*};
|
||||
use solana_client::{rpc_client::RpcClient, rpc_response::RpcVoteAccountInfo};
|
||||
use solana_clap_utils::{
|
||||
commitment::{commitment_arg, COMMITMENT_ARG},
|
||||
input_parsers::*,
|
||||
input_validators::*,
|
||||
keypair::signer_from_path,
|
||||
};
|
||||
use solana_client::{
|
||||
pubsub_client::{PubsubClient, SlotInfoMessage},
|
||||
rpc_client::RpcClient,
|
||||
rpc_response::RpcVoteAccountInfo,
|
||||
};
|
||||
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
|
||||
use solana_sdk::{
|
||||
account_utils::StateMut,
|
||||
clock::{self, Slot},
|
||||
commitment_config::CommitmentConfig,
|
||||
epoch_schedule::{Epoch, EpochSchedule},
|
||||
epoch_schedule::Epoch,
|
||||
hash::Hash,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, KeypairUtil},
|
||||
system_transaction,
|
||||
signature::{Keypair, Signer},
|
||||
system_instruction,
|
||||
transaction::Transaction,
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -43,10 +60,18 @@ impl ClusterQuerySubCommands for App<'_, '_> {
|
||||
Arg::with_name("node_pubkey")
|
||||
.index(1)
|
||||
.takes_value(true)
|
||||
.value_name("PUBKEY")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("VALIDATOR_PUBKEY")
|
||||
.validator(is_valid_pubkey)
|
||||
.required(true)
|
||||
.help("Identity pubkey of the validator"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("node_json_rpc_url")
|
||||
.index(2)
|
||||
.value_name("URL")
|
||||
.takes_value(true)
|
||||
.validator(is_url)
|
||||
.help("JSON RPC URL for validator, which is useful for validators with a private RPC service")
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
@@ -54,9 +79,9 @@ impl ClusterQuerySubCommands for App<'_, '_> {
|
||||
.about("Get the version of the cluster entrypoint"),
|
||||
)
|
||||
.subcommand(SubCommand::with_name("fees").about("Display current cluster fees"))
|
||||
.subcommand(SubCommand::with_name("leader-schedule").about("Display leader schedule"))
|
||||
.subcommand(SubCommand::with_name("get-block-time")
|
||||
.subcommand(SubCommand::with_name("block-time")
|
||||
.about("Get estimated production time of a block")
|
||||
.alias("get-block-time")
|
||||
.arg(
|
||||
Arg::with_name("slot")
|
||||
.index(1)
|
||||
@@ -66,42 +91,31 @@ impl ClusterQuerySubCommands for App<'_, '_> {
|
||||
.help("Slot number of the block to query")
|
||||
)
|
||||
)
|
||||
.subcommand(SubCommand::with_name("leader-schedule").about("Display leader schedule"))
|
||||
.subcommand(
|
||||
SubCommand::with_name("get-epoch-info")
|
||||
SubCommand::with_name("epoch-info")
|
||||
.about("Get information about the current epoch")
|
||||
.arg(
|
||||
Arg::with_name("confirmed")
|
||||
.long("confirmed")
|
||||
.takes_value(false)
|
||||
.help(
|
||||
"Return information at maximum-lockout commitment level",
|
||||
),
|
||||
),
|
||||
.alias("get-epoch-info")
|
||||
.arg(commitment_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("get-genesis-hash").about("Get the genesis hash"),
|
||||
SubCommand::with_name("genesis-hash")
|
||||
.about("Get the genesis hash")
|
||||
.alias("get-genesis-hash")
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("get-slot").about("Get current slot")
|
||||
.arg(
|
||||
Arg::with_name("confirmed")
|
||||
.long("confirmed")
|
||||
.takes_value(false)
|
||||
.help(
|
||||
"Return slot at maximum-lockout commitment level",
|
||||
),
|
||||
),
|
||||
SubCommand::with_name("slot").about("Get current slot")
|
||||
.alias("get-slot")
|
||||
.arg(commitment_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("get-transaction-count").about("Get current transaction count")
|
||||
.arg(
|
||||
Arg::with_name("confirmed")
|
||||
.long("confirmed")
|
||||
.takes_value(false)
|
||||
.help(
|
||||
"Return count at maximum-lockout commitment level",
|
||||
),
|
||||
),
|
||||
SubCommand::with_name("epoch").about("Get current epoch")
|
||||
.arg(commitment_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("transaction-count").about("Get current transaction count")
|
||||
.alias("get-transaction-count")
|
||||
.arg(commitment_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("ping")
|
||||
@@ -141,18 +155,16 @@ impl ClusterQuerySubCommands for App<'_, '_> {
|
||||
.default_value("15")
|
||||
.help("Wait up to timeout seconds for transaction confirmation"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("confirmed")
|
||||
.long("confirmed")
|
||||
.takes_value(false)
|
||||
.help(
|
||||
"Wait until the transaction is confirmed at maximum-lockout commitment level",
|
||||
),
|
||||
),
|
||||
.arg(commitment_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("show-block-production")
|
||||
SubCommand::with_name("live-slots")
|
||||
.about("Show information about the current slot progression"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("block-production")
|
||||
.about("Show information about block production")
|
||||
.alias("show-block-production")
|
||||
.arg(
|
||||
Arg::with_name("epoch")
|
||||
.long("epoch")
|
||||
@@ -167,12 +179,22 @@ impl ClusterQuerySubCommands for App<'_, '_> {
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("show-gossip")
|
||||
.about("Show the current gossip network nodes"),
|
||||
SubCommand::with_name("gossip")
|
||||
.about("Show the current gossip network nodes")
|
||||
.alias("show-gossip")
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("show-validators")
|
||||
.about("Show information about the current validators")
|
||||
SubCommand::with_name("stakes")
|
||||
.about("Show stake account information")
|
||||
.arg(
|
||||
Arg::with_name("vote_account_pubkeys")
|
||||
.index(1)
|
||||
.value_name("VOTE_ACCOUNT_PUBKEYS")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.validator(is_valid_pubkey)
|
||||
.help("Only show stake accounts delegated to the provided vote accounts"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("lamports")
|
||||
.long("lamports")
|
||||
@@ -180,18 +202,41 @@ impl ClusterQuerySubCommands for App<'_, '_> {
|
||||
.help("Display balance in lamports instead of SOL"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("validators")
|
||||
.about("Show summary information about the current validators")
|
||||
.alias("show-validators")
|
||||
.arg(
|
||||
Arg::with_name("lamports")
|
||||
.long("lamports")
|
||||
.takes_value(false)
|
||||
.help("Display balance in lamports instead of SOL"),
|
||||
)
|
||||
.arg(commitment_arg()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_catchup(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
let node_pubkey = pubkey_of(matches, "node_pubkey").unwrap();
|
||||
pub fn parse_catchup(
|
||||
matches: &ArgMatches<'_>,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<CliCommandInfo, CliError> {
|
||||
let node_pubkey = pubkey_of_signer(matches, "node_pubkey", wallet_manager)?.unwrap();
|
||||
let node_json_rpc_url = value_t!(matches, "node_json_rpc_url", String).ok();
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::Catchup { node_pubkey },
|
||||
require_keypair: false,
|
||||
command: CliCommand::Catchup {
|
||||
node_pubkey,
|
||||
node_json_rpc_url,
|
||||
},
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_cluster_ping(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
pub fn parse_cluster_ping(
|
||||
matches: &ArgMatches<'_>,
|
||||
default_signer_path: &str,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<CliCommandInfo, CliError> {
|
||||
let lamports = value_t_or_exit!(matches, "lamports", u64);
|
||||
let interval = Duration::from_secs(value_t_or_exit!(matches, "interval", u64));
|
||||
let count = if matches.is_present("count") {
|
||||
@@ -200,11 +245,7 @@ pub fn parse_cluster_ping(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, Cl
|
||||
None
|
||||
};
|
||||
let timeout = Duration::from_secs(value_t_or_exit!(matches, "timeout", u64));
|
||||
let commitment_config = if matches.is_present("confirmed") {
|
||||
CommitmentConfig::default()
|
||||
} else {
|
||||
CommitmentConfig::recent()
|
||||
};
|
||||
let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap();
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::Ping {
|
||||
lamports,
|
||||
@@ -213,7 +254,12 @@ pub fn parse_cluster_ping(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, Cl
|
||||
timeout,
|
||||
commitment_config,
|
||||
},
|
||||
require_keypair: true,
|
||||
signers: vec![signer_from_path(
|
||||
matches,
|
||||
default_signer_path,
|
||||
"keypair",
|
||||
wallet_manager,
|
||||
)?],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -221,52 +267,69 @@ pub fn parse_get_block_time(matches: &ArgMatches<'_>) -> Result<CliCommandInfo,
|
||||
let slot = value_t_or_exit!(matches, "slot", u64);
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::GetBlockTime { slot },
|
||||
require_keypair: false,
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_get_epoch_info(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
let commitment_config = if matches.is_present("confirmed") {
|
||||
CommitmentConfig::default()
|
||||
} else {
|
||||
CommitmentConfig::recent()
|
||||
};
|
||||
let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap();
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::GetEpochInfo { commitment_config },
|
||||
require_keypair: false,
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_get_slot(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
let commitment_config = if matches.is_present("confirmed") {
|
||||
CommitmentConfig::default()
|
||||
} else {
|
||||
CommitmentConfig::recent()
|
||||
};
|
||||
let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap();
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::GetSlot { commitment_config },
|
||||
require_keypair: false,
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_get_epoch(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap();
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::GetEpoch { commitment_config },
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_get_transaction_count(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
let commitment_config = if matches.is_present("confirmed") {
|
||||
CommitmentConfig::default()
|
||||
} else {
|
||||
CommitmentConfig::recent()
|
||||
};
|
||||
let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap();
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::GetTransactionCount { commitment_config },
|
||||
require_keypair: false,
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_show_stakes(
|
||||
matches: &ArgMatches<'_>,
|
||||
wallet_manager: Option<&Arc<RemoteWalletManager>>,
|
||||
) -> Result<CliCommandInfo, CliError> {
|
||||
let use_lamports_unit = matches.is_present("lamports");
|
||||
let vote_account_pubkeys =
|
||||
pubkeys_of_multiple_signers(matches, "vote_account_pubkeys", wallet_manager)?;
|
||||
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::ShowStakes {
|
||||
use_lamports_unit,
|
||||
vote_account_pubkeys,
|
||||
},
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_show_validators(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
let use_lamports_unit = matches.is_present("lamports");
|
||||
let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap();
|
||||
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::ShowValidators { use_lamports_unit },
|
||||
require_keypair: false,
|
||||
command: CliCommand::ShowValidators {
|
||||
use_lamports_unit,
|
||||
commitment_config,
|
||||
},
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -279,34 +342,42 @@ fn new_spinner_progress_bar() -> ProgressBar {
|
||||
progress_bar
|
||||
}
|
||||
|
||||
/// Aggregate epoch credit stats and return (total credits, total slots, total epochs)
|
||||
pub fn aggregate_epoch_credits(
|
||||
epoch_credits: &[(Epoch, u64, u64)],
|
||||
epoch_schedule: &EpochSchedule,
|
||||
) -> (u64, u64, u64) {
|
||||
epoch_credits
|
||||
.iter()
|
||||
.fold((0, 0, 0), |acc, (epoch, credits, prev_credits)| {
|
||||
let credits_earned = credits - prev_credits;
|
||||
let slots_in_epoch = epoch_schedule.get_slots_in_epoch(*epoch);
|
||||
(acc.0 + credits_earned, acc.1 + slots_in_epoch, acc.2 + 1)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn process_catchup(rpc_client: &RpcClient, node_pubkey: &Pubkey) -> ProcessResult {
|
||||
pub fn process_catchup(
|
||||
rpc_client: &RpcClient,
|
||||
node_pubkey: &Pubkey,
|
||||
node_json_rpc_url: &Option<String>,
|
||||
) -> ProcessResult {
|
||||
let cluster_nodes = rpc_client.get_cluster_nodes()?;
|
||||
|
||||
let rpc_addr = cluster_nodes
|
||||
.iter()
|
||||
.find(|contact_info| contact_info.pubkey == node_pubkey.to_string())
|
||||
.ok_or_else(|| format!("Contact information not found for {}", node_pubkey))?
|
||||
.rpc
|
||||
.ok_or_else(|| format!("RPC service not found for {}", node_pubkey))?;
|
||||
let node_client = if let Some(node_json_rpc_url) = node_json_rpc_url {
|
||||
RpcClient::new(node_json_rpc_url.to_string())
|
||||
} else {
|
||||
RpcClient::new_socket(
|
||||
cluster_nodes
|
||||
.iter()
|
||||
.find(|contact_info| contact_info.pubkey == node_pubkey.to_string())
|
||||
.ok_or_else(|| format!("Contact information not found for {}", node_pubkey))?
|
||||
.rpc
|
||||
.ok_or_else(|| format!("RPC service not found for {}", node_pubkey))?,
|
||||
)
|
||||
};
|
||||
|
||||
let reported_node_pubkey = node_client.get_identity()?;
|
||||
if reported_node_pubkey != *node_pubkey {
|
||||
return Err(format!(
|
||||
"The identity reported by node RPC URL does not match. Expected: {:?}. Reported: {:?}",
|
||||
node_pubkey, reported_node_pubkey
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if rpc_client.get_identity()? == *node_pubkey {
|
||||
return Err("Both RPC URLs reference the same node, unable to monitor for catchup. Try a different --url".into());
|
||||
}
|
||||
|
||||
let progress_bar = new_spinner_progress_bar();
|
||||
progress_bar.set_message("Connecting...");
|
||||
|
||||
let node_client = RpcClient::new_socket(rpc_addr);
|
||||
let mut previous_rpc_slot = std::u64::MAX;
|
||||
let mut previous_slot_distance = 0;
|
||||
let sleep_interval = 5;
|
||||
@@ -322,25 +393,35 @@ pub fn process_catchup(rpc_client: &RpcClient, node_pubkey: &Pubkey) -> ProcessR
|
||||
}
|
||||
|
||||
let slot_distance = rpc_slot as i64 - node_slot as i64;
|
||||
let slots_per_second =
|
||||
(previous_slot_distance - slot_distance) as f64 / f64::from(sleep_interval);
|
||||
let time_remaining = (slot_distance as f64 / slots_per_second).round();
|
||||
let time_remaining = if !time_remaining.is_normal() || time_remaining <= 0.0 {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(
|
||||
". Time remaining: {}",
|
||||
humantime::format_duration(Duration::from_secs_f64(time_remaining))
|
||||
)
|
||||
};
|
||||
|
||||
progress_bar.set_message(&format!(
|
||||
"Validator is {} slots away (us:{} them:{}){}",
|
||||
"{} slots behind (us:{} them:{}){}",
|
||||
slot_distance,
|
||||
node_slot,
|
||||
rpc_slot,
|
||||
if previous_rpc_slot == std::u64::MAX {
|
||||
"".to_string()
|
||||
} else {
|
||||
let slots_per_second =
|
||||
(previous_slot_distance - slot_distance) as f64 / f64::from(sleep_interval);
|
||||
|
||||
format!(
|
||||
" and {} at {:.1} slots/second",
|
||||
", {} at {:.1} slots/second{}",
|
||||
if slots_per_second < 0.0 {
|
||||
"falling behind"
|
||||
} else {
|
||||
"gaining"
|
||||
},
|
||||
slots_per_second,
|
||||
time_remaining
|
||||
)
|
||||
}
|
||||
));
|
||||
@@ -405,30 +486,50 @@ pub fn process_get_block_time(rpc_client: &RpcClient, slot: Slot) -> ProcessResu
|
||||
Ok(timestamp.to_string())
|
||||
}
|
||||
|
||||
fn slot_to_human_time(slot: Slot) -> String {
|
||||
humantime::format_duration(Duration::from_secs(
|
||||
slot * clock::DEFAULT_TICKS_PER_SLOT / clock::DEFAULT_TICKS_PER_SECOND,
|
||||
))
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn process_get_epoch_info(
|
||||
rpc_client: &RpcClient,
|
||||
commitment_config: &CommitmentConfig,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> ProcessResult {
|
||||
let epoch_info = rpc_client.get_epoch_info_with_commitment(commitment_config.clone())?;
|
||||
println!();
|
||||
println_name_value("Current epoch:", &epoch_info.epoch.to_string());
|
||||
println_name_value("Current slot:", &epoch_info.absolute_slot.to_string());
|
||||
println_name_value("Slot:", &epoch_info.absolute_slot.to_string());
|
||||
println_name_value("Epoch:", &epoch_info.epoch.to_string());
|
||||
let start_slot = epoch_info.absolute_slot - epoch_info.slot_index;
|
||||
let end_slot = start_slot + epoch_info.slots_in_epoch;
|
||||
println_name_value(
|
||||
"Total slots in current epoch:",
|
||||
&epoch_info.slots_in_epoch.to_string(),
|
||||
"Epoch Slot Range:",
|
||||
&format!("[{}..{})", start_slot, end_slot),
|
||||
);
|
||||
println_name_value(
|
||||
"Epoch Completed Percent:",
|
||||
&format!(
|
||||
"{:>3.3}%",
|
||||
epoch_info.slot_index as f64 / epoch_info.slots_in_epoch as f64 * 100_f64
|
||||
),
|
||||
);
|
||||
let remaining_slots_in_epoch = epoch_info.slots_in_epoch - epoch_info.slot_index;
|
||||
println_name_value(
|
||||
"Remaining slots in current epoch:",
|
||||
&remaining_slots_in_epoch.to_string(),
|
||||
);
|
||||
|
||||
let remaining_time_in_epoch = Duration::from_secs(
|
||||
remaining_slots_in_epoch * clock::DEFAULT_TICKS_PER_SLOT / clock::DEFAULT_TICKS_PER_SECOND,
|
||||
"Epoch Completed Slots:",
|
||||
&format!(
|
||||
"{}/{} ({} remaining)",
|
||||
epoch_info.slot_index, epoch_info.slots_in_epoch, remaining_slots_in_epoch
|
||||
),
|
||||
);
|
||||
println_name_value(
|
||||
"Time remaining in current epoch:",
|
||||
&humantime::format_duration(remaining_time_in_epoch).to_string(),
|
||||
"Epoch Completed Time:",
|
||||
&format!(
|
||||
"{}/{} ({} remaining)",
|
||||
slot_to_human_time(epoch_info.slot_index),
|
||||
slot_to_human_time(epoch_info.slots_in_epoch),
|
||||
slot_to_human_time(remaining_slots_in_epoch)
|
||||
),
|
||||
);
|
||||
Ok("".to_string())
|
||||
}
|
||||
@@ -440,19 +541,27 @@ pub fn process_get_genesis_hash(rpc_client: &RpcClient) -> ProcessResult {
|
||||
|
||||
pub fn process_get_slot(
|
||||
rpc_client: &RpcClient,
|
||||
commitment_config: &CommitmentConfig,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> ProcessResult {
|
||||
let slot = rpc_client.get_slot_with_commitment(commitment_config.clone())?;
|
||||
Ok(slot.to_string())
|
||||
}
|
||||
|
||||
pub fn process_get_epoch(
|
||||
rpc_client: &RpcClient,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> ProcessResult {
|
||||
let epoch_info = rpc_client.get_epoch_info_with_commitment(commitment_config.clone())?;
|
||||
Ok(epoch_info.epoch.to_string())
|
||||
}
|
||||
|
||||
pub fn parse_show_block_production(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
let epoch = value_t!(matches, "epoch", Epoch).ok();
|
||||
let slot_limit = value_t!(matches, "slot_limit", u64).ok();
|
||||
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::ShowBlockProduction { epoch, slot_limit },
|
||||
require_keypair: false,
|
||||
signers: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -552,7 +661,7 @@ pub fn process_show_block_production(
|
||||
let skipped_slots = leader_skipped_slots.entry(leader).or_insert(0);
|
||||
|
||||
loop {
|
||||
if !confirmed_blocks.is_empty() {
|
||||
if confirmed_blocks_index < confirmed_blocks.len() {
|
||||
let slot_of_next_confirmed_block = confirmed_blocks[confirmed_blocks_index];
|
||||
if slot_of_next_confirmed_block < slot {
|
||||
confirmed_blocks_index += 1;
|
||||
@@ -629,7 +738,7 @@ pub fn process_show_block_production(
|
||||
|
||||
pub fn process_get_transaction_count(
|
||||
rpc_client: &RpcClient,
|
||||
commitment_config: &CommitmentConfig,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> ProcessResult {
|
||||
let transaction_count =
|
||||
rpc_client.get_transaction_count_with_commitment(commitment_config.clone())?;
|
||||
@@ -643,12 +752,12 @@ pub fn process_ping(
|
||||
interval: &Duration,
|
||||
count: &Option<u64>,
|
||||
timeout: &Duration,
|
||||
commitment_config: &CommitmentConfig,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> ProcessResult {
|
||||
let to = Keypair::new().pubkey();
|
||||
|
||||
println_name_value("Source account:", &config.keypair.pubkey().to_string());
|
||||
println_name_value("Destination account:", &to.to_string());
|
||||
println_name_value("Source Account:", &config.signers[0].pubkey().to_string());
|
||||
println_name_value("Destination Account:", &to.to_string());
|
||||
println!();
|
||||
|
||||
let (signal_sender, signal_receiver) = std::sync::mpsc::channel();
|
||||
@@ -666,11 +775,13 @@ pub fn process_ping(
|
||||
let (recent_blockhash, fee_calculator) = rpc_client.get_new_blockhash(&last_blockhash)?;
|
||||
last_blockhash = recent_blockhash;
|
||||
|
||||
let transaction =
|
||||
system_transaction::transfer(&config.keypair, &to, lamports, recent_blockhash);
|
||||
let ix = system_instruction::transfer(&config.signers[0].pubkey(), &to, lamports);
|
||||
let message = Message::new(&[ix]);
|
||||
let mut transaction = Transaction::new_unsigned(message);
|
||||
transaction.try_sign(&config.signers, recent_blockhash)?;
|
||||
check_account_for_fee(
|
||||
rpc_client,
|
||||
&config.keypair.pubkey(),
|
||||
&config.signers[0].pubkey(),
|
||||
&fee_calculator,
|
||||
&transaction.message,
|
||||
)?;
|
||||
@@ -762,6 +873,101 @@ pub fn process_ping(
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
pub fn process_live_slots(url: &str) -> ProcessResult {
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Disable Ctrl+C handler as sometimes the PubsubClient shutdown can stall. Also it doesn't
|
||||
// really matter that the shutdown is clean because the process is terminating.
|
||||
/*
|
||||
let exit_clone = exit.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
exit_clone.store(true, Ordering::Relaxed);
|
||||
})?;
|
||||
*/
|
||||
|
||||
let mut current: Option<SlotInfoMessage> = None;
|
||||
let mut message = "".to_string();
|
||||
|
||||
let slot_progress = new_spinner_progress_bar();
|
||||
slot_progress.set_message("Connecting...");
|
||||
let (mut client, receiver) = PubsubClient::slot_subscribe(url)?;
|
||||
slot_progress.set_message("Connected.");
|
||||
|
||||
let spacer = "|";
|
||||
slot_progress.println(spacer);
|
||||
|
||||
let mut last_root = std::u64::MAX;
|
||||
let mut last_root_update = Instant::now();
|
||||
let mut slots_per_second = std::f64::NAN;
|
||||
loop {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
eprintln!("{}", message);
|
||||
client.shutdown().unwrap();
|
||||
break;
|
||||
}
|
||||
|
||||
match receiver.recv() {
|
||||
Ok(new_info) => {
|
||||
if last_root == std::u64::MAX {
|
||||
last_root = new_info.root;
|
||||
last_root_update = Instant::now();
|
||||
}
|
||||
if last_root_update.elapsed().as_secs() >= 5 {
|
||||
let root = new_info.root;
|
||||
slots_per_second =
|
||||
(root - last_root) as f64 / last_root_update.elapsed().as_secs() as f64;
|
||||
last_root_update = Instant::now();
|
||||
last_root = root;
|
||||
}
|
||||
|
||||
message = if slots_per_second.is_nan() {
|
||||
format!("{:?}", new_info)
|
||||
} else {
|
||||
format!(
|
||||
"{:?} | root slot advancing at {:.2} slots/second",
|
||||
new_info, slots_per_second
|
||||
)
|
||||
}
|
||||
.to_owned();
|
||||
slot_progress.set_message(&message);
|
||||
|
||||
if let Some(previous) = current {
|
||||
let slot_delta: i64 = new_info.slot as i64 - previous.slot as i64;
|
||||
let root_delta: i64 = new_info.root as i64 - previous.root as i64;
|
||||
|
||||
//
|
||||
// if slot has advanced out of step with the root, we detect
|
||||
// a mismatch and output the slot information
|
||||
//
|
||||
if slot_delta != root_delta {
|
||||
let prev_root = format!(
|
||||
"|<--- {} <- … <- {} <- {} (prev)",
|
||||
previous.root, previous.parent, previous.slot
|
||||
);
|
||||
slot_progress.println(&prev_root);
|
||||
|
||||
let new_root = format!(
|
||||
"| '- {} <- … <- {} <- {} (next)",
|
||||
new_info.root, new_info.parent, new_info.slot
|
||||
);
|
||||
|
||||
slot_progress.println(prev_root);
|
||||
slot_progress.println(new_root);
|
||||
slot_progress.println(spacer);
|
||||
}
|
||||
}
|
||||
current = Some(new_info);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("disconnected: {}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
pub fn process_show_gossip(rpc_client: &RpcClient) -> ProcessResult {
|
||||
let cluster_nodes = rpc_client.get_cluster_nodes()?;
|
||||
|
||||
@@ -798,9 +1004,52 @@ pub fn process_show_gossip(rpc_client: &RpcClient) -> ProcessResult {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) -> ProcessResult {
|
||||
let epoch_schedule = rpc_client.get_epoch_schedule()?;
|
||||
let vote_accounts = rpc_client.get_vote_accounts()?;
|
||||
pub fn process_show_stakes(
|
||||
rpc_client: &RpcClient,
|
||||
use_lamports_unit: bool,
|
||||
vote_account_pubkeys: Option<&[Pubkey]>,
|
||||
) -> ProcessResult {
|
||||
use crate::stake::print_stake_state;
|
||||
use solana_stake_program::stake_state::StakeState;
|
||||
|
||||
let progress_bar = new_spinner_progress_bar();
|
||||
progress_bar.set_message("Fetching stake accounts...");
|
||||
let all_stake_accounts = rpc_client.get_program_accounts(&solana_stake_program::id())?;
|
||||
progress_bar.finish_and_clear();
|
||||
|
||||
for (stake_pubkey, stake_account) in all_stake_accounts {
|
||||
if let Ok(stake_state) = stake_account.state() {
|
||||
match stake_state {
|
||||
StakeState::Initialized(_) => {
|
||||
if vote_account_pubkeys.is_none() {
|
||||
println!("\nstake pubkey: {}", stake_pubkey);
|
||||
print_stake_state(stake_account.lamports, &stake_state, use_lamports_unit);
|
||||
}
|
||||
}
|
||||
StakeState::Stake(_, stake) => {
|
||||
if vote_account_pubkeys.is_none()
|
||||
|| vote_account_pubkeys
|
||||
.unwrap()
|
||||
.contains(&stake.delegation.voter_pubkey)
|
||||
{
|
||||
println!("\nstake pubkey: {}", stake_pubkey);
|
||||
print_stake_state(stake_account.lamports, &stake_state, use_lamports_unit);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
pub fn process_show_validators(
|
||||
rpc_client: &RpcClient,
|
||||
use_lamports_unit: bool,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> ProcessResult {
|
||||
let epoch_info = rpc_client.get_epoch_info_with_commitment(commitment_config)?;
|
||||
let vote_accounts = rpc_client.get_vote_accounts_with_commitment(commitment_config)?;
|
||||
let total_active_stake = vote_accounts
|
||||
.current
|
||||
.iter()
|
||||
@@ -848,7 +1097,7 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
|
||||
"Commission",
|
||||
"Last Vote",
|
||||
"Root Block",
|
||||
"Uptime",
|
||||
"Credits",
|
||||
"Active Stake",
|
||||
))
|
||||
.bold()
|
||||
@@ -856,7 +1105,7 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
|
||||
|
||||
fn print_vote_account(
|
||||
vote_account: RpcVoteAccountInfo,
|
||||
epoch_schedule: &EpochSchedule,
|
||||
current_epoch: Epoch,
|
||||
total_active_stake: f64,
|
||||
use_lamports_unit: bool,
|
||||
delinquent: bool,
|
||||
@@ -869,17 +1118,6 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
|
||||
}
|
||||
}
|
||||
|
||||
fn uptime(epoch_credits: Vec<(Epoch, u64, u64)>, epoch_schedule: &EpochSchedule) -> String {
|
||||
let (total_credits, total_slots, _) =
|
||||
aggregate_epoch_credits(&epoch_credits, &epoch_schedule);
|
||||
if total_slots > 0 {
|
||||
let total_uptime = 100_f64 * total_credits as f64 / total_slots as f64;
|
||||
format!("{:.2}%", total_uptime)
|
||||
} else {
|
||||
"-".into()
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} {:<44} {:<44} {:>9}% {:>8} {:>10} {:>7} {}",
|
||||
if delinquent {
|
||||
@@ -892,7 +1130,15 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
|
||||
vote_account.commission,
|
||||
non_zero_or_dash(vote_account.last_vote),
|
||||
non_zero_or_dash(vote_account.root_slot),
|
||||
uptime(vote_account.epoch_credits, epoch_schedule),
|
||||
vote_account
|
||||
.epoch_credits
|
||||
.iter()
|
||||
.find_map(|(epoch, credits, _)| if *epoch == current_epoch {
|
||||
Some(*credits)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.unwrap_or(0),
|
||||
if vote_account.activated_stake > 0 {
|
||||
format!(
|
||||
"{} ({:.2}%)",
|
||||
@@ -908,7 +1154,7 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
|
||||
for vote_account in vote_accounts.current.into_iter() {
|
||||
print_vote_account(
|
||||
vote_account,
|
||||
&epoch_schedule,
|
||||
epoch_info.epoch,
|
||||
total_active_stake,
|
||||
use_lamports_unit,
|
||||
false,
|
||||
@@ -917,7 +1163,7 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
|
||||
for vote_account in vote_accounts.delinquent.into_iter() {
|
||||
print_vote_account(
|
||||
vote_account,
|
||||
&epoch_schedule,
|
||||
epoch_info.epoch,
|
||||
total_active_stake,
|
||||
use_lamports_unit,
|
||||
true,
|
||||
@@ -931,92 +1177,112 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cli::{app, parse_command};
|
||||
use solana_sdk::signature::{write_keypair, Keypair};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
fn make_tmp_file() -> (String, NamedTempFile) {
|
||||
let tmp_file = NamedTempFile::new().unwrap();
|
||||
(String::from(tmp_file.path().to_str().unwrap()), tmp_file)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_command() {
|
||||
let test_commands = app("test", "desc", "version");
|
||||
let default_keypair = Keypair::new();
|
||||
let (default_keypair_file, mut tmp_file) = make_tmp_file();
|
||||
write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap();
|
||||
|
||||
let test_cluster_version = test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "cluster-version"]);
|
||||
assert_eq!(
|
||||
parse_command(&test_cluster_version).unwrap(),
|
||||
parse_command(&test_cluster_version, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::ClusterVersion,
|
||||
require_keypair: false
|
||||
signers: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
let test_fees = test_commands.clone().get_matches_from(vec!["test", "fees"]);
|
||||
assert_eq!(
|
||||
parse_command(&test_fees).unwrap(),
|
||||
parse_command(&test_fees, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::Fees,
|
||||
require_keypair: false
|
||||
signers: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
let slot = 100;
|
||||
let test_get_block_time = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"get-block-time",
|
||||
&slot.to_string(),
|
||||
]);
|
||||
let test_get_block_time =
|
||||
test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "block-time", &slot.to_string()]);
|
||||
assert_eq!(
|
||||
parse_command(&test_get_block_time).unwrap(),
|
||||
parse_command(&test_get_block_time, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::GetBlockTime { slot },
|
||||
require_keypair: false
|
||||
signers: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
let test_get_epoch_info = test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "get-epoch-info"]);
|
||||
.get_matches_from(vec!["test", "epoch-info"]);
|
||||
assert_eq!(
|
||||
parse_command(&test_get_epoch_info).unwrap(),
|
||||
parse_command(&test_get_epoch_info, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::GetEpochInfo {
|
||||
commitment_config: CommitmentConfig::recent(),
|
||||
},
|
||||
require_keypair: false
|
||||
signers: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
let test_get_genesis_hash = test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "get-genesis-hash"]);
|
||||
.get_matches_from(vec!["test", "genesis-hash"]);
|
||||
assert_eq!(
|
||||
parse_command(&test_get_genesis_hash).unwrap(),
|
||||
parse_command(&test_get_genesis_hash, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::GetGenesisHash,
|
||||
require_keypair: false
|
||||
signers: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
let test_get_slot = test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "get-slot"]);
|
||||
let test_get_slot = test_commands.clone().get_matches_from(vec!["test", "slot"]);
|
||||
assert_eq!(
|
||||
parse_command(&test_get_slot).unwrap(),
|
||||
parse_command(&test_get_slot, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::GetSlot {
|
||||
commitment_config: CommitmentConfig::recent(),
|
||||
},
|
||||
require_keypair: false
|
||||
signers: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
let test_get_epoch = test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "epoch"]);
|
||||
assert_eq!(
|
||||
parse_command(&test_get_epoch, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::GetEpoch {
|
||||
commitment_config: CommitmentConfig::recent(),
|
||||
},
|
||||
signers: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
let test_transaction_count = test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["test", "get-transaction-count"]);
|
||||
.get_matches_from(vec!["test", "transaction-count"]);
|
||||
assert_eq!(
|
||||
parse_command(&test_transaction_count).unwrap(),
|
||||
parse_command(&test_transaction_count, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::GetTransactionCount {
|
||||
commitment_config: CommitmentConfig::recent(),
|
||||
},
|
||||
require_keypair: false
|
||||
signers: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1029,19 +1295,20 @@ mod tests {
|
||||
"2",
|
||||
"-t",
|
||||
"3",
|
||||
"--confirmed",
|
||||
"--commitment",
|
||||
"max",
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_ping).unwrap(),
|
||||
parse_command(&test_ping, &default_keypair_file, None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::Ping {
|
||||
lamports: 1,
|
||||
interval: Duration::from_secs(1),
|
||||
count: Some(2),
|
||||
timeout: Duration::from_secs(3),
|
||||
commitment_config: CommitmentConfig::default(),
|
||||
commitment_config: CommitmentConfig::max(),
|
||||
},
|
||||
require_keypair: true
|
||||
signers: vec![default_keypair.into()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -1,49 +0,0 @@
|
||||
// Wallet settings that can be configured for long-term use
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG_FILE: Option<String> = {
|
||||
dirs::home_dir().map(|mut path| {
|
||||
path.extend(&[".config", "solana", "cli", "config.yml"]);
|
||||
path.to_str().unwrap().to_string()
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
|
||||
pub struct Config {
|
||||
pub url: String,
|
||||
pub keypair_path: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(url: &str, keypair_path: &str) -> Self {
|
||||
Self {
|
||||
url: url.to_string(),
|
||||
keypair_path: keypair_path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(config_file: &str) -> Result<Self, io::Error> {
|
||||
let file = File::open(config_file.to_string())?;
|
||||
let config = serde_yaml::from_reader(file)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save(&self, config_file: &str) -> Result<(), io::Error> {
|
||||
let serialized = serde_yaml::to_string(self)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
|
||||
|
||||
if let Some(outdir) = Path::new(&config_file).parent() {
|
||||
create_dir_all(outdir)?;
|
||||
}
|
||||
let mut file = File::create(config_file)?;
|
||||
file.write_all(&serialized.into_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,5 +1,11 @@
|
||||
use crate::cli::SettingType;
|
||||
use console::style;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use solana_sdk::{
|
||||
hash::Hash, native_token::lamports_to_sol, program_utils::limited_deserialize,
|
||||
transaction::Transaction,
|
||||
};
|
||||
use solana_transaction_status::RpcTransactionStatusMeta;
|
||||
use std::io;
|
||||
|
||||
// Pretty print a "name value"
|
||||
pub fn println_name_value(name: &str, value: &str) {
|
||||
@@ -11,26 +17,177 @@ pub fn println_name_value(name: &str, value: &str) {
|
||||
println!("{} {}", style(name).bold(), styled_value);
|
||||
}
|
||||
|
||||
pub fn println_name_value_or(name: &str, value: &str, default_value: &str) {
|
||||
if value == "" {
|
||||
println!(
|
||||
"{} {} {}",
|
||||
style(name).bold(),
|
||||
style(default_value),
|
||||
style("(default)").italic()
|
||||
);
|
||||
} else {
|
||||
println!("{} {}", style(name).bold(), style(value));
|
||||
pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) {
|
||||
let description = match setting_type {
|
||||
SettingType::Explicit => "",
|
||||
SettingType::Computed => "(computed)",
|
||||
SettingType::SystemDefault => "(default)",
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} {} {}",
|
||||
style(name).bold(),
|
||||
style(value),
|
||||
style(description).italic(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn println_signers(tx: &Transaction) {
|
||||
pub fn println_signers(
|
||||
blockhash: &Hash,
|
||||
signers: &[String],
|
||||
absent: &[String],
|
||||
bad_sig: &[String],
|
||||
) {
|
||||
println!();
|
||||
println!("Blockhash: {}", tx.message.recent_blockhash);
|
||||
println!("Signers (Pubkey=Signature):");
|
||||
tx.signatures
|
||||
.iter()
|
||||
.zip(tx.message.account_keys.clone())
|
||||
.for_each(|(signature, pubkey)| println!(" {:?}={:?}", pubkey, signature));
|
||||
println!("Blockhash: {}", blockhash);
|
||||
if !signers.is_empty() {
|
||||
println!("Signers (Pubkey=Signature):");
|
||||
signers.iter().for_each(|signer| println!(" {}", signer))
|
||||
}
|
||||
if !absent.is_empty() {
|
||||
println!("Absent Signers (Pubkey):");
|
||||
absent.iter().for_each(|pubkey| println!(" {}", pubkey))
|
||||
}
|
||||
if !bad_sig.is_empty() {
|
||||
println!("Bad Signatures (Pubkey):");
|
||||
bad_sig.iter().for_each(|pubkey| println!(" {}", pubkey))
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn write_transaction<W: io::Write>(
|
||||
w: &mut W,
|
||||
transaction: &Transaction,
|
||||
transaction_status: &Option<RpcTransactionStatusMeta>,
|
||||
prefix: &str,
|
||||
) -> io::Result<()> {
|
||||
let message = &transaction.message;
|
||||
writeln!(
|
||||
w,
|
||||
"{}Recent Blockhash: {:?}",
|
||||
prefix, message.recent_blockhash
|
||||
)?;
|
||||
for (signature_index, signature) in transaction.signatures.iter().enumerate() {
|
||||
writeln!(
|
||||
w,
|
||||
"{}Signature {}: {:?}",
|
||||
prefix, signature_index, signature
|
||||
)?;
|
||||
}
|
||||
writeln!(w, "{}{:?}", prefix, message.header)?;
|
||||
for (account_index, account) in message.account_keys.iter().enumerate() {
|
||||
writeln!(w, "{}Account {}: {:?}", prefix, account_index, account)?;
|
||||
}
|
||||
for (instruction_index, instruction) in message.instructions.iter().enumerate() {
|
||||
let program_pubkey = message.account_keys[instruction.program_id_index as usize];
|
||||
writeln!(w, "{}Instruction {}", prefix, instruction_index)?;
|
||||
writeln!(
|
||||
w,
|
||||
"{} Program: {} ({})",
|
||||
prefix, program_pubkey, instruction.program_id_index
|
||||
)?;
|
||||
for (account_index, account) in instruction.accounts.iter().enumerate() {
|
||||
let account_pubkey = message.account_keys[*account as usize];
|
||||
writeln!(
|
||||
w,
|
||||
"{} Account {}: {} ({})",
|
||||
prefix, account_index, account_pubkey, account
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut raw = true;
|
||||
if program_pubkey == solana_vote_program::id() {
|
||||
if let Ok(vote_instruction) = limited_deserialize::<
|
||||
solana_vote_program::vote_instruction::VoteInstruction,
|
||||
>(&instruction.data)
|
||||
{
|
||||
writeln!(w, "{} {:?}", prefix, vote_instruction)?;
|
||||
raw = false;
|
||||
}
|
||||
} else if program_pubkey == solana_stake_program::id() {
|
||||
if let Ok(stake_instruction) = limited_deserialize::<
|
||||
solana_stake_program::stake_instruction::StakeInstruction,
|
||||
>(&instruction.data)
|
||||
{
|
||||
writeln!(w, "{} {:?}", prefix, stake_instruction)?;
|
||||
raw = false;
|
||||
}
|
||||
} else if program_pubkey == solana_sdk::system_program::id() {
|
||||
if let Ok(system_instruction) = limited_deserialize::<
|
||||
solana_sdk::system_instruction::SystemInstruction,
|
||||
>(&instruction.data)
|
||||
{
|
||||
writeln!(w, "{} {:?}", prefix, system_instruction)?;
|
||||
raw = false;
|
||||
}
|
||||
}
|
||||
|
||||
if raw {
|
||||
writeln!(w, "{} Data: {:?}", prefix, instruction.data)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(transaction_status) = transaction_status {
|
||||
writeln!(
|
||||
w,
|
||||
"{}Status: {}",
|
||||
prefix,
|
||||
match &transaction_status.status {
|
||||
Ok(_) => "Ok".into(),
|
||||
Err(err) => err.to_string(),
|
||||
}
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"{} Fee: {} SOL",
|
||||
prefix,
|
||||
lamports_to_sol(transaction_status.fee)
|
||||
)?;
|
||||
assert_eq!(
|
||||
transaction_status.pre_balances.len(),
|
||||
transaction_status.post_balances.len()
|
||||
);
|
||||
for (i, (pre, post)) in transaction_status
|
||||
.pre_balances
|
||||
.iter()
|
||||
.zip(transaction_status.post_balances.iter())
|
||||
.enumerate()
|
||||
{
|
||||
if pre == post {
|
||||
writeln!(
|
||||
w,
|
||||
"{} Account {} balance: {} SOL",
|
||||
prefix,
|
||||
i,
|
||||
lamports_to_sol(*pre)
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
w,
|
||||
"{} Account {} balance: {} SOL -> {} SOL",
|
||||
prefix,
|
||||
i,
|
||||
lamports_to_sol(*pre),
|
||||
lamports_to_sol(*post)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeln!(w, "{}Status: Unavailable", prefix)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn println_transaction(
|
||||
transaction: &Transaction,
|
||||
transaction_status: &Option<RpcTransactionStatusMeta>,
|
||||
prefix: &str,
|
||||
) {
|
||||
let mut w = Vec::new();
|
||||
if write_transaction(&mut w, transaction, transaction_status, prefix).is_ok() {
|
||||
if let Ok(s) = String::from_utf8(w) {
|
||||
print!("{}", s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,8 @@
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
pub mod cli;
|
||||
pub mod cluster_query;
|
||||
pub mod config;
|
||||
pub mod display;
|
||||
pub mod nonce;
|
||||
pub mod offline;
|
||||
pub mod stake;
|
||||
pub mod storage;
|
||||
pub mod validator_info;
|
||||
|
320
cli/src/main.rs
320
cli/src/main.rs
@@ -1,147 +1,144 @@
|
||||
use clap::{crate_description, crate_name, Arg, ArgGroup, ArgMatches, SubCommand};
|
||||
use clap::{crate_description, crate_name, AppSettings, Arg, ArgGroup, ArgMatches, SubCommand};
|
||||
use console::style;
|
||||
|
||||
use solana_clap_utils::{
|
||||
input_validators::is_url,
|
||||
keypair::{
|
||||
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
|
||||
SKIP_SEED_PHRASE_VALIDATION_ARG,
|
||||
},
|
||||
input_validators::is_url, keypair::SKIP_SEED_PHRASE_VALIDATION_ARG, offline::SIGN_ONLY_ARG,
|
||||
DisplayError,
|
||||
};
|
||||
use solana_cli::{
|
||||
cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliError},
|
||||
config::{self, Config},
|
||||
cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliSigners},
|
||||
display::{println_name_value, println_name_value_or},
|
||||
};
|
||||
use solana_sdk::signature::read_keypair_file;
|
||||
|
||||
use std::error;
|
||||
use solana_cli_config::{Config, CONFIG_FILE};
|
||||
use solana_remote_wallet::remote_wallet::{maybe_wallet_manager, RemoteWalletManager};
|
||||
use std::{error, sync::Arc};
|
||||
|
||||
fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error>> {
|
||||
let parse_args = match matches.subcommand() {
|
||||
("get", Some(subcommand_matches)) => {
|
||||
if let Some(config_file) = matches.value_of("config_file") {
|
||||
let config = Config::load(config_file).unwrap_or_default();
|
||||
if let Some(field) = subcommand_matches.value_of("specific_setting") {
|
||||
let (value, default_value) = match field {
|
||||
"url" => (config.url, CliConfig::default_json_rpc_url()),
|
||||
"keypair" => (config.keypair_path, CliConfig::default_keypair_path()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
println_name_value_or(&format!("* {}:", field), &value, &default_value);
|
||||
("config", Some(matches)) => match matches.subcommand() {
|
||||
("get", Some(subcommand_matches)) => {
|
||||
if let Some(config_file) = matches.value_of("config_file") {
|
||||
let config = Config::load(config_file).unwrap_or_default();
|
||||
|
||||
let (url_setting_type, json_rpc_url) =
|
||||
CliConfig::compute_json_rpc_url_setting("", &config.json_rpc_url);
|
||||
let (ws_setting_type, websocket_url) = CliConfig::compute_websocket_url_setting(
|
||||
"",
|
||||
&config.websocket_url,
|
||||
"",
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
let (keypair_setting_type, keypair_path) =
|
||||
CliConfig::compute_keypair_path_setting("", &config.keypair_path);
|
||||
|
||||
if let Some(field) = subcommand_matches.value_of("specific_setting") {
|
||||
let (field_name, value, setting_type) = match field {
|
||||
"json_rpc_url" => ("RPC URL", json_rpc_url, url_setting_type),
|
||||
"websocket_url" => ("WebSocket URL", websocket_url, ws_setting_type),
|
||||
"keypair" => ("Key Path", keypair_path, keypair_setting_type),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
println_name_value_or(&format!("{}:", field_name), &value, setting_type);
|
||||
} else {
|
||||
println_name_value("Config File:", config_file);
|
||||
println_name_value_or("RPC URL:", &json_rpc_url, url_setting_type);
|
||||
println_name_value_or("WebSocket URL:", &websocket_url, ws_setting_type);
|
||||
println_name_value_or("Keypair Path:", &keypair_path, keypair_setting_type);
|
||||
}
|
||||
} else {
|
||||
println_name_value("Wallet Config:", config_file);
|
||||
println_name_value_or(
|
||||
"* url:",
|
||||
&config.url,
|
||||
&CliConfig::default_json_rpc_url(),
|
||||
);
|
||||
println_name_value_or(
|
||||
"* keypair:",
|
||||
&config.keypair_path,
|
||||
&CliConfig::default_keypair_path(),
|
||||
println!(
|
||||
"{} Either provide the `--config` arg or ensure home directory exists to use the default config location",
|
||||
style("No config file found.").bold()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{} Either provide the `--config` arg or ensure home directory exists to use the default config location",
|
||||
style("No config file found.").bold()
|
||||
);
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
("set", Some(subcommand_matches)) => {
|
||||
if let Some(config_file) = matches.value_of("config_file") {
|
||||
let mut config = Config::load(config_file).unwrap_or_default();
|
||||
if let Some(url) = subcommand_matches.value_of("json_rpc_url") {
|
||||
config.url = url.to_string();
|
||||
("set", Some(subcommand_matches)) => {
|
||||
if let Some(config_file) = matches.value_of("config_file") {
|
||||
let mut config = Config::load(config_file).unwrap_or_default();
|
||||
if let Some(url) = subcommand_matches.value_of("json_rpc_url") {
|
||||
config.json_rpc_url = url.to_string();
|
||||
// Revert to a computed `websocket_url` value when `json_rpc_url` is
|
||||
// changed
|
||||
config.websocket_url = "".to_string();
|
||||
}
|
||||
if let Some(url) = subcommand_matches.value_of("websocket_url") {
|
||||
config.websocket_url = url.to_string();
|
||||
}
|
||||
if let Some(keypair) = subcommand_matches.value_of("keypair") {
|
||||
config.keypair_path = keypair.to_string();
|
||||
}
|
||||
config.save(config_file)?;
|
||||
|
||||
let (url_setting_type, json_rpc_url) =
|
||||
CliConfig::compute_json_rpc_url_setting("", &config.json_rpc_url);
|
||||
let (ws_setting_type, websocket_url) = CliConfig::compute_websocket_url_setting(
|
||||
"",
|
||||
&config.websocket_url,
|
||||
"",
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
let (keypair_setting_type, keypair_path) =
|
||||
CliConfig::compute_keypair_path_setting("", &config.keypair_path);
|
||||
|
||||
println_name_value("Config File:", config_file);
|
||||
println_name_value_or("RPC URL:", &json_rpc_url, url_setting_type);
|
||||
println_name_value_or("WebSocket URL:", &websocket_url, ws_setting_type);
|
||||
println_name_value_or("Keypair Path:", &keypair_path, keypair_setting_type);
|
||||
} else {
|
||||
println!(
|
||||
"{} Either provide the `--config` arg or ensure home directory exists to use the default config location",
|
||||
style("No config file found.").bold()
|
||||
);
|
||||
}
|
||||
if let Some(keypair) = subcommand_matches.value_of("keypair") {
|
||||
config.keypair_path = keypair.to_string();
|
||||
}
|
||||
config.save(config_file)?;
|
||||
println_name_value("Wallet Config Updated:", config_file);
|
||||
println_name_value("* url:", &config.url);
|
||||
println_name_value("* keypair:", &config.keypair_path);
|
||||
} else {
|
||||
println!(
|
||||
"{} Either provide the `--config` arg or ensure home directory exists to use the default config location",
|
||||
style("No config file found.").bold()
|
||||
);
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => true,
|
||||
};
|
||||
Ok(parse_args)
|
||||
}
|
||||
|
||||
pub fn parse_args(matches: &ArgMatches<'_>) -> Result<CliConfig, Box<dyn error::Error>> {
|
||||
pub fn parse_args<'a>(
|
||||
matches: &ArgMatches<'_>,
|
||||
wallet_manager: Option<Arc<RemoteWalletManager>>,
|
||||
) -> Result<(CliConfig<'a>, CliSigners), Box<dyn error::Error>> {
|
||||
let config = if let Some(config_file) = matches.value_of("config_file") {
|
||||
Config::load(config_file).unwrap_or_default()
|
||||
} else {
|
||||
Config::default()
|
||||
};
|
||||
let json_rpc_url = if let Some(url) = matches.value_of("json_rpc_url") {
|
||||
url.to_string()
|
||||
} else if config.url != "" {
|
||||
config.url
|
||||
} else {
|
||||
let default = CliConfig::default();
|
||||
default.json_rpc_url
|
||||
};
|
||||
let (_, json_rpc_url) = CliConfig::compute_json_rpc_url_setting(
|
||||
matches.value_of("json_rpc_url").unwrap_or(""),
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
let (_, websocket_url) = CliConfig::compute_websocket_url_setting(
|
||||
matches.value_of("websocket_url").unwrap_or(""),
|
||||
&config.websocket_url,
|
||||
matches.value_of("json_rpc_url").unwrap_or(""),
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
let (_, default_signer_path) = CliConfig::compute_keypair_path_setting(
|
||||
matches.value_of("keypair").unwrap_or(""),
|
||||
&config.keypair_path,
|
||||
);
|
||||
|
||||
let CliCommandInfo {
|
||||
command,
|
||||
require_keypair,
|
||||
} = parse_command(&matches)?;
|
||||
let CliCommandInfo { command, signers } =
|
||||
parse_command(&matches, &default_signer_path, wallet_manager.as_ref())?;
|
||||
|
||||
let (keypair, keypair_path) = if require_keypair {
|
||||
let KeypairWithSource { keypair, source } = keypair_input(&matches, "keypair")?;
|
||||
match source {
|
||||
keypair::Source::File => (
|
||||
keypair,
|
||||
Some(matches.value_of("keypair").unwrap().to_string()),
|
||||
),
|
||||
keypair::Source::SeedPhrase => (keypair, None),
|
||||
keypair::Source::Generated => {
|
||||
let keypair_path = if config.keypair_path != "" {
|
||||
config.keypair_path
|
||||
} else {
|
||||
let default_keypair_path = CliConfig::default_keypair_path();
|
||||
if !std::path::Path::new(&default_keypair_path).exists() {
|
||||
return Err(CliError::KeypairFileNotFound(format!(
|
||||
"Generate a new keypair at {} with `solana-keygen new`",
|
||||
default_keypair_path
|
||||
))
|
||||
.into());
|
||||
}
|
||||
default_keypair_path
|
||||
};
|
||||
|
||||
let keypair = read_keypair_file(&keypair_path).or_else(|err| {
|
||||
Err(CliError::BadParameter(format!(
|
||||
"{}: Unable to open keypair file: {}",
|
||||
err, keypair_path
|
||||
)))
|
||||
})?;
|
||||
|
||||
(keypair, Some(keypair_path))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let default = CliConfig::default();
|
||||
(default.keypair, None)
|
||||
};
|
||||
|
||||
Ok(CliConfig {
|
||||
command,
|
||||
json_rpc_url,
|
||||
keypair,
|
||||
keypair_path,
|
||||
rpc_client: None,
|
||||
verbose: matches.is_present("verbose"),
|
||||
})
|
||||
Ok((
|
||||
CliConfig {
|
||||
command,
|
||||
json_rpc_url,
|
||||
websocket_url,
|
||||
signers: vec![],
|
||||
keypair_path: default_signer_path,
|
||||
rpc_client: None,
|
||||
verbose: matches.is_present("verbose"),
|
||||
},
|
||||
signers,
|
||||
))
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn error::Error>> {
|
||||
@@ -155,11 +152,11 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||
let arg = Arg::with_name("config_file")
|
||||
.short("C")
|
||||
.long("config")
|
||||
.value_name("PATH")
|
||||
.value_name("FILEPATH")
|
||||
.takes_value(true)
|
||||
.global(true)
|
||||
.help("Configuration file to use");
|
||||
if let Some(ref config_file) = *config::CONFIG_FILE {
|
||||
if let Some(ref config_file) = *CONFIG_FILE {
|
||||
arg.default_value(&config_file)
|
||||
} else {
|
||||
arg
|
||||
@@ -175,30 +172,30 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||
.validator(is_url)
|
||||
.help("JSON RPC URL for the solana cluster"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket_url")
|
||||
.long("ws")
|
||||
.value_name("URL")
|
||||
.takes_value(true)
|
||||
.global(true)
|
||||
.validator(is_url)
|
||||
.help("WebSocket URL for the solana cluster"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("keypair")
|
||||
.short("k")
|
||||
.long("keypair")
|
||||
.value_name("PATH")
|
||||
.value_name("KEYPAIR")
|
||||
.global(true)
|
||||
.takes_value(true)
|
||||
.help("/path/to/id.json"),
|
||||
.help("Filepath or URL to a keypair"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("verbose")
|
||||
.long("verbose")
|
||||
.short("v")
|
||||
.global(true)
|
||||
.help("Show extra information header"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(ASK_SEED_PHRASE_ARG.name)
|
||||
.long(ASK_SEED_PHRASE_ARG.long)
|
||||
.value_name("KEYPAIR NAME")
|
||||
.global(true)
|
||||
.takes_value(true)
|
||||
.possible_values(&["keypair"])
|
||||
.help(ASK_SEED_PHRASE_ARG.help),
|
||||
.help("Show additional information"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
|
||||
@@ -207,33 +204,52 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("get")
|
||||
.about("Get cli config settings")
|
||||
.arg(
|
||||
Arg::with_name("specific_setting")
|
||||
.index(1)
|
||||
.value_name("CONFIG_FIELD")
|
||||
.takes_value(true)
|
||||
.possible_values(&["url", "keypair"])
|
||||
.help("Return a specific config setting"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("set")
|
||||
.about("Set a cli config setting")
|
||||
.group(
|
||||
ArgGroup::with_name("config_settings")
|
||||
.args(&["json_rpc_url", "keypair"])
|
||||
.multiple(true)
|
||||
.required(true),
|
||||
SubCommand::with_name("config")
|
||||
.about("Solana command-line tool configuration settings")
|
||||
.aliases(&["get", "set"])
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name("get")
|
||||
.about("Get current config settings")
|
||||
.arg(
|
||||
Arg::with_name("specific_setting")
|
||||
.index(1)
|
||||
.value_name("CONFIG_FIELD")
|
||||
.takes_value(true)
|
||||
.possible_values(&["json_rpc_url", "websocket_url", "keypair"])
|
||||
.help("Return a specific config setting"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("set")
|
||||
.about("Set a config setting")
|
||||
.group(
|
||||
ArgGroup::with_name("config_settings")
|
||||
.args(&["json_rpc_url", "websocket_url", "keypair"])
|
||||
.multiple(true)
|
||||
.required(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
do_main(&matches).map_err(|err| DisplayError::new_as_boxed(err).into())
|
||||
}
|
||||
|
||||
fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
|
||||
if parse_settings(&matches)? {
|
||||
let config = parse_args(&matches)?;
|
||||
let wallet_manager = maybe_wallet_manager()?;
|
||||
|
||||
let (mut config, signers) = parse_args(&matches, wallet_manager)?;
|
||||
config.signers = signers.iter().map(|s| s.as_ref()).collect();
|
||||
let result = process_command(&config)?;
|
||||
println!("{}", result);
|
||||
}
|
||||
let (_, submatches) = matches.subcommand();
|
||||
let sign_only = submatches
|
||||
.map(|m| m.is_present(SIGN_ONLY_ARG.name))
|
||||
.unwrap_or(false);
|
||||
if !sign_only {
|
||||
println!("{}", result);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
807
cli/src/nonce.rs
807
cli/src/nonce.rs
File diff suppressed because it is too large
Load Diff
394
cli/src/offline/blockhash_query.rs
Normal file
394
cli/src/offline/blockhash_query.rs
Normal file
@@ -0,0 +1,394 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Source {
|
||||
Cluster,
|
||||
NonceAccount(Pubkey),
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn get_blockhash_and_fee_calculator(
|
||||
&self,
|
||||
rpc_client: &RpcClient,
|
||||
) -> Result<(Hash, FeeCalculator), Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
Self::Cluster => {
|
||||
let res = rpc_client.get_recent_blockhash()?;
|
||||
Ok(res)
|
||||
}
|
||||
Self::NonceAccount(ref pubkey) => {
|
||||
let data = nonce::get_account(rpc_client, pubkey)
|
||||
.and_then(|ref a| nonce::data_from_account(a))?;
|
||||
Ok((data.blockhash, data.fee_calculator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fee_calculator(
|
||||
&self,
|
||||
rpc_client: &RpcClient,
|
||||
blockhash: &Hash,
|
||||
) -> Result<Option<FeeCalculator>, Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
Self::Cluster => {
|
||||
let res = rpc_client.get_fee_calculator_for_blockhash(blockhash)?;
|
||||
Ok(res)
|
||||
}
|
||||
Self::NonceAccount(ref pubkey) => {
|
||||
let res = nonce::get_account(rpc_client, pubkey)
|
||||
.and_then(|ref a| nonce::data_from_account(a))
|
||||
.and_then(|d| {
|
||||
if d.blockhash == *blockhash {
|
||||
Ok(Some(d.fee_calculator))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum BlockhashQuery {
|
||||
None(Hash),
|
||||
FeeCalculator(Source, Hash),
|
||||
All(Source),
|
||||
}
|
||||
|
||||
impl BlockhashQuery {
|
||||
pub fn new(blockhash: Option<Hash>, sign_only: bool, nonce_account: Option<Pubkey>) -> Self {
|
||||
let source = nonce_account
|
||||
.map(Source::NonceAccount)
|
||||
.unwrap_or(Source::Cluster);
|
||||
match blockhash {
|
||||
Some(hash) if sign_only => Self::None(hash),
|
||||
Some(hash) if !sign_only => Self::FeeCalculator(source, hash),
|
||||
None if !sign_only => Self::All(source),
|
||||
_ => panic!("Cannot resolve blockhash"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_matches(matches: &ArgMatches<'_>) -> Self {
|
||||
let blockhash = value_of(matches, BLOCKHASH_ARG.name);
|
||||
let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
|
||||
let nonce_account = pubkey_of(matches, nonce::NONCE_ARG.name);
|
||||
BlockhashQuery::new(blockhash, sign_only, nonce_account)
|
||||
}
|
||||
|
||||
pub fn get_blockhash_and_fee_calculator(
|
||||
&self,
|
||||
rpc_client: &RpcClient,
|
||||
) -> Result<(Hash, FeeCalculator), Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
BlockhashQuery::None(hash) => Ok((*hash, FeeCalculator::default())),
|
||||
BlockhashQuery::FeeCalculator(source, hash) => {
|
||||
let fee_calculator = source
|
||||
.get_fee_calculator(rpc_client, hash)?
|
||||
.ok_or(format!("Hash has expired {:?}", hash))?;
|
||||
Ok((*hash, fee_calculator))
|
||||
}
|
||||
BlockhashQuery::All(source) => source.get_blockhash_and_fee_calculator(rpc_client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BlockhashQuery {
|
||||
fn default() -> Self {
|
||||
BlockhashQuery::All(Source::Cluster)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{nonce::nonce_arg, offline::blockhash_query::BlockhashQuery};
|
||||
use clap::App;
|
||||
use serde_json::{self, json, Value};
|
||||
use solana_client::{
|
||||
rpc_request::RpcRequest,
|
||||
rpc_response::{Response, RpcAccount, RpcFeeCalculator, RpcResponseContext},
|
||||
};
|
||||
use solana_sdk::{
|
||||
account::Account, fee_calculator::FeeCalculator, hash::hash, nonce, system_program,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_blockhash_query_new_ok() {
|
||||
let blockhash = hash(&[1u8]);
|
||||
let nonce_pubkey = Pubkey::new(&[1u8; 32]);
|
||||
|
||||
assert_eq!(
|
||||
BlockhashQuery::new(Some(blockhash), true, None),
|
||||
BlockhashQuery::None(blockhash),
|
||||
);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new(Some(blockhash), false, None),
|
||||
BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
|
||||
);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new(None, false, None),
|
||||
BlockhashQuery::All(blockhash_query::Source::Cluster)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
BlockhashQuery::new(Some(blockhash), true, Some(nonce_pubkey)),
|
||||
BlockhashQuery::None(blockhash),
|
||||
);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new(Some(blockhash), false, Some(nonce_pubkey)),
|
||||
BlockhashQuery::FeeCalculator(
|
||||
blockhash_query::Source::NonceAccount(nonce_pubkey),
|
||||
blockhash
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new(None, false, Some(nonce_pubkey)),
|
||||
BlockhashQuery::All(blockhash_query::Source::NonceAccount(nonce_pubkey)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_blockhash_query_new_no_nonce_fail() {
|
||||
BlockhashQuery::new(None, true, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_blockhash_query_new_nonce_fail() {
|
||||
let nonce_pubkey = Pubkey::new(&[1u8; 32]);
|
||||
BlockhashQuery::new(None, true, Some(nonce_pubkey));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blockhash_query_new_from_matches_ok() {
|
||||
let test_commands = App::new("blockhash_query_test")
|
||||
.arg(nonce_arg())
|
||||
.offline_args();
|
||||
let blockhash = hash(&[1u8]);
|
||||
let blockhash_string = blockhash.to_string();
|
||||
|
||||
let matches = test_commands.clone().get_matches_from(vec![
|
||||
"blockhash_query_test",
|
||||
"--blockhash",
|
||||
&blockhash_string,
|
||||
"--sign-only",
|
||||
]);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new_from_matches(&matches),
|
||||
BlockhashQuery::None(blockhash),
|
||||
);
|
||||
|
||||
let matches = test_commands.clone().get_matches_from(vec![
|
||||
"blockhash_query_test",
|
||||
"--blockhash",
|
||||
&blockhash_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new_from_matches(&matches),
|
||||
BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
|
||||
);
|
||||
|
||||
let matches = test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["blockhash_query_test"]);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new_from_matches(&matches),
|
||||
BlockhashQuery::All(blockhash_query::Source::Cluster),
|
||||
);
|
||||
|
||||
let nonce_pubkey = Pubkey::new(&[1u8; 32]);
|
||||
let nonce_string = nonce_pubkey.to_string();
|
||||
let matches = test_commands.clone().get_matches_from(vec![
|
||||
"blockhash_query_test",
|
||||
"--blockhash",
|
||||
&blockhash_string,
|
||||
"--sign-only",
|
||||
"--nonce",
|
||||
&nonce_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new_from_matches(&matches),
|
||||
BlockhashQuery::None(blockhash),
|
||||
);
|
||||
|
||||
let matches = test_commands.clone().get_matches_from(vec![
|
||||
"blockhash_query_test",
|
||||
"--blockhash",
|
||||
&blockhash_string,
|
||||
"--nonce",
|
||||
&nonce_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
BlockhashQuery::new_from_matches(&matches),
|
||||
BlockhashQuery::FeeCalculator(
|
||||
blockhash_query::Source::NonceAccount(nonce_pubkey),
|
||||
blockhash
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_blockhash_query_new_from_matches_without_nonce_fail() {
|
||||
let test_commands = App::new("blockhash_query_test")
|
||||
.arg(blockhash_arg())
|
||||
// We can really only hit this case if the arg requirements
|
||||
// are broken, so unset the requires() to recreate that condition
|
||||
.arg(sign_only_arg().requires(""));
|
||||
|
||||
let matches = test_commands
|
||||
.clone()
|
||||
.get_matches_from(vec!["blockhash_query_test", "--sign-only"]);
|
||||
BlockhashQuery::new_from_matches(&matches);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_blockhash_query_new_from_matches_with_nonce_fail() {
|
||||
let test_commands = App::new("blockhash_query_test")
|
||||
.arg(blockhash_arg())
|
||||
// We can really only hit this case if the arg requirements
|
||||
// are broken, so unset the requires() to recreate that condition
|
||||
.arg(sign_only_arg().requires(""));
|
||||
let nonce_pubkey = Pubkey::new(&[1u8; 32]);
|
||||
let nonce_string = nonce_pubkey.to_string();
|
||||
|
||||
let matches = test_commands.clone().get_matches_from(vec![
|
||||
"blockhash_query_test",
|
||||
"--sign-only",
|
||||
"--nonce",
|
||||
&nonce_string,
|
||||
]);
|
||||
BlockhashQuery::new_from_matches(&matches);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blockhash_query_get_blockhash_fee_calc() {
|
||||
let test_blockhash = hash(&[0u8]);
|
||||
let rpc_blockhash = hash(&[1u8]);
|
||||
let rpc_fee_calc = FeeCalculator::new(42);
|
||||
let get_recent_blockhash_response = json!(Response {
|
||||
context: RpcResponseContext { slot: 1 },
|
||||
value: json!((
|
||||
Value::String(rpc_blockhash.to_string()),
|
||||
serde_json::to_value(rpc_fee_calc.clone()).unwrap()
|
||||
)),
|
||||
});
|
||||
let get_fee_calculator_for_blockhash_response = json!(Response {
|
||||
context: RpcResponseContext { slot: 1 },
|
||||
value: json!(RpcFeeCalculator {
|
||||
fee_calculator: rpc_fee_calc.clone()
|
||||
}),
|
||||
});
|
||||
let mut mocks = HashMap::new();
|
||||
mocks.insert(
|
||||
RpcRequest::GetRecentBlockhash,
|
||||
get_recent_blockhash_response.clone(),
|
||||
);
|
||||
let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
|
||||
assert_eq!(
|
||||
BlockhashQuery::default()
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.unwrap(),
|
||||
(rpc_blockhash, rpc_fee_calc.clone()),
|
||||
);
|
||||
let mut mocks = HashMap::new();
|
||||
mocks.insert(
|
||||
RpcRequest::GetRecentBlockhash,
|
||||
get_recent_blockhash_response.clone(),
|
||||
);
|
||||
mocks.insert(
|
||||
RpcRequest::GetFeeCalculatorForBlockhash,
|
||||
get_fee_calculator_for_blockhash_response.clone(),
|
||||
);
|
||||
let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
|
||||
assert_eq!(
|
||||
BlockhashQuery::FeeCalculator(Source::Cluster, test_blockhash)
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.unwrap(),
|
||||
(test_blockhash, rpc_fee_calc.clone()),
|
||||
);
|
||||
let mut mocks = HashMap::new();
|
||||
mocks.insert(
|
||||
RpcRequest::GetRecentBlockhash,
|
||||
get_recent_blockhash_response.clone(),
|
||||
);
|
||||
let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
|
||||
assert_eq!(
|
||||
BlockhashQuery::None(test_blockhash)
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.unwrap(),
|
||||
(test_blockhash, FeeCalculator::default()),
|
||||
);
|
||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||
assert!(BlockhashQuery::default()
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.is_err());
|
||||
|
||||
let nonce_blockhash = Hash::new(&[2u8; 32]);
|
||||
let nonce_fee_calc = FeeCalculator::new(4242);
|
||||
let data = nonce::state::Data {
|
||||
authority: Pubkey::new(&[3u8; 32]),
|
||||
blockhash: nonce_blockhash,
|
||||
fee_calculator: nonce_fee_calc.clone(),
|
||||
};
|
||||
let nonce_account = Account::new_data_with_space(
|
||||
42,
|
||||
&nonce::state::Versions::new_current(nonce::State::Initialized(data)),
|
||||
nonce::State::size(),
|
||||
&system_program::id(),
|
||||
)
|
||||
.unwrap();
|
||||
let nonce_pubkey = Pubkey::new(&[4u8; 32]);
|
||||
let rpc_nonce_account = RpcAccount::encode(nonce_account);
|
||||
let get_account_response = json!(Response {
|
||||
context: RpcResponseContext { slot: 1 },
|
||||
value: json!(Some(rpc_nonce_account.clone())),
|
||||
});
|
||||
|
||||
let mut mocks = HashMap::new();
|
||||
mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
|
||||
let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
|
||||
assert_eq!(
|
||||
BlockhashQuery::All(Source::NonceAccount(nonce_pubkey))
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.unwrap(),
|
||||
(nonce_blockhash, nonce_fee_calc.clone()),
|
||||
);
|
||||
let mut mocks = HashMap::new();
|
||||
mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
|
||||
let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
|
||||
assert_eq!(
|
||||
BlockhashQuery::FeeCalculator(Source::NonceAccount(nonce_pubkey), nonce_blockhash)
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.unwrap(),
|
||||
(nonce_blockhash, nonce_fee_calc.clone()),
|
||||
);
|
||||
let mut mocks = HashMap::new();
|
||||
mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
|
||||
let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
|
||||
assert!(
|
||||
BlockhashQuery::FeeCalculator(Source::NonceAccount(nonce_pubkey), test_blockhash)
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.is_err()
|
||||
);
|
||||
let mut mocks = HashMap::new();
|
||||
mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone());
|
||||
let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
|
||||
assert_eq!(
|
||||
BlockhashQuery::None(nonce_blockhash)
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.unwrap(),
|
||||
(nonce_blockhash, FeeCalculator::default()),
|
||||
);
|
||||
|
||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||
assert!(BlockhashQuery::All(Source::NonceAccount(nonce_pubkey))
|
||||
.get_blockhash_and_fee_calculator(&rpc_client)
|
||||
.is_err());
|
||||
}
|
||||
}
|
114
cli/src/offline/mod.rs
Normal file
114
cli/src/offline/mod.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
pub mod blockhash_query;
|
||||
|
||||
use crate::nonce;
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use serde_json::Value;
|
||||
use solana_clap_utils::{
|
||||
input_parsers::{pubkey_of, value_of},
|
||||
input_validators::{is_hash, is_pubkey_sig},
|
||||
keypair::presigner_from_pubkey_sigs,
|
||||
offline::{BLOCKHASH_ARG, SIGNER_ARG, SIGN_ONLY_ARG},
|
||||
};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::{
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
pubkey::Pubkey,
|
||||
signature::{Presigner, Signature},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
fn blockhash_arg<'a, 'b>() -> Arg<'a, 'b> {
|
||||
Arg::with_name(BLOCKHASH_ARG.name)
|
||||
.long(BLOCKHASH_ARG.long)
|
||||
.takes_value(true)
|
||||
.value_name("BLOCKHASH")
|
||||
.validator(is_hash)
|
||||
.help(BLOCKHASH_ARG.help)
|
||||
}
|
||||
|
||||
fn sign_only_arg<'a, 'b>() -> Arg<'a, 'b> {
|
||||
Arg::with_name(SIGN_ONLY_ARG.name)
|
||||
.long(SIGN_ONLY_ARG.long)
|
||||
.takes_value(false)
|
||||
.requires(BLOCKHASH_ARG.name)
|
||||
.help(SIGN_ONLY_ARG.help)
|
||||
}
|
||||
|
||||
fn signer_arg<'a, 'b>() -> Arg<'a, 'b> {
|
||||
Arg::with_name(SIGNER_ARG.name)
|
||||
.long(SIGNER_ARG.long)
|
||||
.takes_value(true)
|
||||
.value_name("PUBKEY=SIGNATURE")
|
||||
.validator(is_pubkey_sig)
|
||||
.requires(BLOCKHASH_ARG.name)
|
||||
.multiple(true)
|
||||
.help(SIGNER_ARG.help)
|
||||
}
|
||||
|
||||
pub trait OfflineArgs {
|
||||
fn offline_args(self) -> Self;
|
||||
}
|
||||
|
||||
impl OfflineArgs for App<'_, '_> {
|
||||
fn offline_args(self) -> Self {
|
||||
self.arg(blockhash_arg())
|
||||
.arg(sign_only_arg())
|
||||
.arg(signer_arg())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SignOnly {
|
||||
pub blockhash: Hash,
|
||||
pub present_signers: Vec<(Pubkey, Signature)>,
|
||||
pub absent_signers: Vec<Pubkey>,
|
||||
pub bad_signers: Vec<Pubkey>,
|
||||
}
|
||||
|
||||
impl SignOnly {
|
||||
pub fn has_all_signers(&self) -> bool {
|
||||
self.absent_signers.is_empty() && self.bad_signers.is_empty()
|
||||
}
|
||||
|
||||
pub fn presigner_of(&self, pubkey: &Pubkey) -> Option<Presigner> {
|
||||
presigner_from_pubkey_sigs(pubkey, &self.present_signers)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly {
|
||||
let object: Value = serde_json::from_str(&reply).unwrap();
|
||||
let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
|
||||
let blockhash = blockhash_str.parse::<Hash>().unwrap();
|
||||
let signer_strings = object.get("signers").unwrap().as_array().unwrap();
|
||||
let present_signers = signer_strings
|
||||
.iter()
|
||||
.map(|signer_string| {
|
||||
let mut signer = signer_string.as_str().unwrap().split('=');
|
||||
let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
|
||||
let sig = Signature::from_str(signer.next().unwrap()).unwrap();
|
||||
(key, sig)
|
||||
})
|
||||
.collect();
|
||||
let signer_strings = object.get("absent").unwrap().as_array().unwrap();
|
||||
let absent_signers = signer_strings
|
||||
.iter()
|
||||
.map(|val| {
|
||||
let s = val.as_str().unwrap();
|
||||
Pubkey::from_str(s).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let signer_strings = object.get("badSig").unwrap().as_array().unwrap();
|
||||
let bad_signers = signer_strings
|
||||
.iter()
|
||||
.map(|val| {
|
||||
let s = val.as_str().unwrap();
|
||||
Pubkey::from_str(s).unwrap()
|
||||
})
|
||||
.collect();
|
||||
SignOnly {
|
||||
blockhash,
|
||||
present_signers,
|
||||
absent_signers,
|
||||
bad_signers,
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user