Compare commits
4445 Commits
Author | SHA1 | Date | |
---|---|---|---|
fe1676bc3a | |||
1a9ef37251 | |||
db5370c5df | |||
804378e8f7 | |||
56b0ba2601 | |||
3073ebb20d | |||
f8e07ef5a3 | |||
a4b6d181a2 | |||
0b8c5d807d | |||
e201136eee | |||
55f660d5f9 | |||
a4acc631ee | |||
3ddf4b6c24 | |||
ccd1173a83 | |||
cd1a9faacd | |||
b60b8ec5ae | |||
536c8accf8 | |||
7beefb3f81 | |||
fe1f67ea9a | |||
069ce71256 | |||
e3cacb9296 | |||
0c592c52f6 | |||
78bb96ee51 | |||
86e2f35ac4 | |||
7696a64891 | |||
799ed24113 | |||
63477dabcd | |||
cd0bc1dea5 | |||
195a880576 | |||
ac226c3e14 | |||
4d5b832775 | |||
79b2542ca4 | |||
17921c9fae | |||
5de38852d2 | |||
0acdbc0d03 | |||
c8c85ff93b | |||
31cbb52654 | |||
cd88f81817 | |||
6de24ff0be | |||
de4d14ddc0 | |||
5b386ec30a | |||
8f0aa956a3 | |||
e04148ff44 | |||
d5d853838c | |||
e18673953c | |||
12f3fd75e8 | |||
7bd0929157 | |||
19488ba42a | |||
f0dc10c67b | |||
f55103498f | |||
639cb49356 | |||
c5e9c6fdb6 | |||
7a4ccc8719 | |||
125a345c90 | |||
3dc22e7323 | |||
17dcd1f62a | |||
a277f3e816 | |||
10b16753af | |||
4625aed3a5 | |||
259c820f15 | |||
e888c90ecf | |||
b053bc2790 | |||
6a81f9e443 | |||
0ef1fa7c76 | |||
02eb234399 | |||
8d80da6b46 | |||
22855def27 | |||
0be59cad4e | |||
5edbd6a7fb | |||
54ff9b3ac2 | |||
5463226184 | |||
b96bccd71f | |||
07a948a0d0 | |||
8f034280dc | |||
83f551d9b9 | |||
f83a64d17f | |||
8bc7d5a172 | |||
96c0222b30 | |||
679a718cbf | |||
b083e4db48 | |||
a3cab470d3 | |||
bb93504965 | |||
4d58bf4b28 | |||
505f77b108 | |||
5b672f8921 | |||
9e9c0785e7 | |||
94882418ab | |||
c6cb3bb0bc | |||
9fedc9513b | |||
0badc90058 | |||
61fbea3ee4 | |||
a4a3995a84 | |||
01fb76f4bd | |||
d09639f7d2 | |||
946ee8a354 | |||
e63b899ca5 | |||
63a4ed74a4 | |||
a3782d699d | |||
97f2c96a7e | |||
5979627258 | |||
9d580e363a | |||
9163e5b004 | |||
0252bf2f46 | |||
283bb84134 | |||
0a4f909566 | |||
516aa44aad | |||
b1763f9187 | |||
b03fd782de | |||
b850f3c1dd | |||
789a9df9f6 | |||
bd39ab9365 | |||
1c0cfb17a3 | |||
9491999a95 | |||
e2d30db7e1 | |||
3129e299e4 | |||
0604bbb473 | |||
545feab6db | |||
3794048c91 | |||
beb45f44ac | |||
f1d1852691 | |||
53f09c44f3 | |||
bd237a2d6f | |||
76a7038335 | |||
c24d95c885 | |||
cb0560df92 | |||
ec034a5cb9 | |||
ca99ebaaf4 | |||
b9e878ee80 | |||
33c4c7e511 | |||
b67ac22336 | |||
6ff2572ebe | |||
a539c9ad67 | |||
1997640094 | |||
e7eafbd24e | |||
378a0f511e | |||
9349f90a59 | |||
0f1d6c6271 | |||
8e70f5bf84 | |||
52fc974cdf | |||
6e9d803091 | |||
fc8489a04d | |||
e248efce06 | |||
b4084c6298 | |||
2fdfa98d55 | |||
f506b0a224 | |||
202adb1bf1 | |||
885eeec3ed | |||
5e9f802d7d | |||
e4be57c3b6 | |||
6ab6e6cb9b | |||
2a849ae268 | |||
4808f6a9f8 | |||
96bfe92334 | |||
e7cde846cb | |||
eb90d8d463 | |||
6a8a97f644 | |||
3fc846d789 | |||
0f77531f09 | |||
20b831264e | |||
43bab23651 | |||
906df5e20e | |||
794e961328 | |||
a481822321 | |||
dc42c12f2b | |||
6d82123125 | |||
4f6d7702c5 | |||
97274030b9 | |||
9ce2bc94bf | |||
51502537b1 | |||
7b49c9f09c | |||
4714dc3a5c | |||
44013855d8 | |||
846fdd3b2d | |||
03d6c9a552 | |||
d0be16b49a | |||
3a4018cd03 | |||
5aaaa7f45c | |||
c299dd390e | |||
a3016aebaf | |||
fb55d1c3d4 | |||
9c44c173df | |||
d708982f27 | |||
bb774173bb | |||
3906b1af6a | |||
de1d7ce312 | |||
1654199b23 | |||
2ec9bc9f05 | |||
e8ae603a01 | |||
e4dba03e12 | |||
8ec10d4de9 | |||
baca3e6b6b | |||
fc5fcd6cd4 | |||
33496ffea2 | |||
b8b7de5522 | |||
109101c2dc | |||
534619f72f | |||
44322124c8 | |||
9923c543e8 | |||
41b5899856 | |||
b830449f23 | |||
037fcf6b3d | |||
e1a1296b9b | |||
3f4ff3f7b5 | |||
cd4bccfd12 | |||
9c3e7e40cf | |||
a9a7fc56eb | |||
398b78dd97 | |||
1edf6c361e | |||
b99e3eafdd | |||
e6486b2824 | |||
d22a13257e | |||
f4c5b9ccb0 | |||
a94880574b | |||
0f1582c196 | |||
85159a0eb4 | |||
258cf21416 | |||
2bfad87a5f | |||
95cbb8a5c0 | |||
ce1b72809a | |||
4f3e149a98 | |||
642d3d903f | |||
81cd461591 | |||
ea110efabd | |||
0743f54dfe | |||
176d5e0d37 | |||
16b71a6be0 | |||
13ee8efd42 | |||
5f5d779ee1 | |||
7b849b042c | |||
d32f5b6cca | |||
fcbcf000c4 | |||
2bc939f535 | |||
d5de5bec4f | |||
61beb42797 | |||
e5be3e1dca | |||
986c54de58 | |||
49b7e67585 | |||
db825b6e26 | |||
8e273caf7d | |||
b1a648113f | |||
2782922f7a | |||
041a06b432 | |||
269a82f796 | |||
6b83ce4937 | |||
ae557104a5 | |||
6a34b11dd0 | |||
54417acfba | |||
29d12d9ff1 | |||
4ee857ab7d | |||
771a88665c | |||
a7c18cc0b4 | |||
e30e4cc603 | |||
fdc31e99df | |||
a72325dbc2 | |||
67b6be66c8 | |||
8ec13d557f | |||
31f570a9f4 | |||
46b7b795bf | |||
38273427ad | |||
46fb0b1b94 | |||
224b705f8d | |||
028f41eb51 | |||
c27726e065 | |||
a57fb00584 | |||
360055ad70 | |||
558f10c862 | |||
c53c351759 | |||
7c4473e0aa | |||
7e7b79ef34 | |||
e993d511e3 | |||
251b0957f1 | |||
b9524217fe | |||
6b228df3df | |||
6cf6a1ccc3 | |||
d889e77fba | |||
93d65aa9cc | |||
f216a7179a | |||
434b8a8970 | |||
cc9191f1b0 | |||
567bbecca0 | |||
07e4f9a611 | |||
b41286919d | |||
564057c812 | |||
20e4edec61 | |||
d5f0e49535 | |||
30bccc0c68 | |||
1c44b738fe | |||
217f30f9c3 | |||
fec867539d | |||
d123d86d84 | |||
485ccd20e4 | |||
8d004ee947 | |||
4704aa1f80 | |||
271115a6be | |||
a79caf7795 | |||
404aa63147 | |||
4610706d9f | |||
8e4cd6fcc3 | |||
6eb09a6901 | |||
e04d2379df | |||
5b72a984a3 | |||
cf545e64b8 | |||
ac1e266588 | |||
0f2226901d | |||
dad1511484 | |||
05646d72b8 | |||
7ccd601100 | |||
d23f8a3e99 | |||
0dc5af62ff | |||
855f1823a4 | |||
7fd40f1eb9 | |||
95f2f05f45 | |||
cd976a8082 | |||
163ed40efb | |||
32aaa5fd06 | |||
163874d4da | |||
873007bae1 | |||
a67a88c8ef | |||
6d1b43f1b1 | |||
3a20a20807 | |||
e45559a1a7 | |||
140954a53c | |||
b5d7ac3ce3 | |||
b5d714eec7 | |||
36cdaffe25 | |||
16e2443f61 | |||
9adbc1dd60 | |||
b6ccb475f1 | |||
ca0f16ccc0 | |||
c241a56fb0 | |||
4149f7fd1c | |||
cc68ecdacf | |||
96b349dcbb | |||
5216952691 | |||
c46b2541fe | |||
2158ba5863 | |||
180d297df8 | |||
c276375a0e | |||
130563cd4c | |||
9e2a7921c8 | |||
9539154a4a | |||
84bd9296cd | |||
88ecce12a2 | |||
5a7b99ecc2 | |||
55a76ed4b0 | |||
033a04129a | |||
789fff2ae2 | |||
9750488200 | |||
46ec5cf765 | |||
ee16cc77a3 | |||
a669241cb1 | |||
0174945853 | |||
ea0837973e | |||
85819983d7 | |||
78841532f7 | |||
72214b2b68 | |||
ee83a2ac29 | |||
82c759b6cb | |||
6de5354b8e | |||
87281f6ed5 | |||
a8cd66ffa2 | |||
d1e1258f97 | |||
4d73bbe48f | |||
10ad536e09 | |||
bc2d4c7681 | |||
a7f200847f | |||
411f154827 | |||
6dcb97af9e | |||
9420ba52e9 | |||
ec35c1fc79 | |||
b752511f41 | |||
af206111e2 | |||
ba50e1ac81 | |||
f9f493ee7a | |||
137233b4a1 | |||
3897b66270 | |||
feefdca969 | |||
25690ff078 | |||
897279eddb | |||
5f5725a4ea | |||
6a61f25735 | |||
454c66f988 | |||
3e893ffddc | |||
58eebd7f6c | |||
ba5077701d | |||
2f44555437 | |||
299b642803 | |||
a2bf59cbba | |||
329382f016 | |||
67c9bbc6b2 | |||
6088b3bfc8 | |||
2be7896157 | |||
0b37f530ae | |||
c13ae10d31 | |||
1e15e6375a | |||
ed684c5ec6 | |||
2fbdec59cb | |||
710f88edda | |||
db899a2813 | |||
aad0d90fdd | |||
72b4834446 | |||
ec48c58df1 | |||
0947ec59c9 | |||
d67211305c | |||
c65046e1a2 | |||
ba7d121724 | |||
a1070e9572 | |||
f89e83ae49 | |||
264f502ed7 | |||
c5876ddca9 | |||
fdf6cae6fb | |||
d26f836212 | |||
da98982732 | |||
cc10e84ab7 | |||
6cd91cd7ec | |||
e19dbdc527 | |||
0b8809da6e | |||
35aefdf1db | |||
66891d9d4e | |||
6bca577d6d | |||
f5400ccefc | |||
a56d717ea8 | |||
11c7aab023 | |||
5541eedcc4 | |||
77ea4cd285 | |||
8353b420d1 | |||
71602fe04b | |||
054c12ea0f | |||
0003dbf3ba | |||
c07b6c30a1 | |||
bad48ce83c | |||
2d03ae2fae | |||
3a7008949f | |||
973ad7554e | |||
3be154490d | |||
3610768888 | |||
4602d3bf46 | |||
778583ad08 | |||
fb904e7a29 | |||
b501090443 | |||
f0f55af35b | |||
3e8d96a95b | |||
9713a3ac02 | |||
5c9777970d | |||
c142a82ae0 | |||
18d48f09f8 | |||
deeabb862d | |||
d8f6865338 | |||
4a0c759795 | |||
a131c90260 | |||
fc48062867 | |||
f77788447c | |||
d25fc7a649 | |||
bf3d2bd2ec | |||
60a6ff80ee | |||
9e1c5e1ab0 | |||
20fffd8abf | |||
98ed785711 | |||
7cb695df12 | |||
c94bc2a0b6 | |||
511085b747 | |||
f76ac94d70 | |||
32caa55d67 | |||
b69475937f | |||
f6ff33db8e | |||
dcf1200d2a | |||
40977fa99f | |||
f4df8ff5b3 | |||
080db1c62d | |||
4d5e2c8a4d | |||
13d018e3e1 | |||
59ee2b8892 | |||
0dde79f42b | |||
a4411ef6a1 | |||
3c62e2332e | |||
1cd88968cf | |||
28a53959e0 | |||
7c26a4d0a0 | |||
6ed2e4c187 | |||
a484c87354 | |||
33c7f92f56 | |||
b8f6280fe5 | |||
822bebea46 | |||
582a7192ec | |||
5492aad61e | |||
27f973c923 | |||
3357cebcdb | |||
7ce9c0a2e9 | |||
e9daf57d7f | |||
1c2169aec7 | |||
cf163a9dab | |||
dfcf3f94dc | |||
b13fb6097f | |||
6e24a4aa50 | |||
fb1c6cf4da | |||
af1b8f8a26 | |||
88d6db8537 | |||
6ce2c06fd6 | |||
136f7e4b3b | |||
0a73bb7efd | |||
2cf00021d9 | |||
8d38c2f800 | |||
9848de6cda | |||
19a3606315 | |||
cc2227d943 | |||
a33921ed34 | |||
2e75ff27ac | |||
a27cdf55e7 | |||
3d00992c95 | |||
77cb70dd80 | |||
8daba3e563 | |||
94f9ac0332 | |||
a17903a89f | |||
dda0a1f39b | |||
0ef670a865 | |||
04f54655c2 | |||
dc5590f2bf | |||
bc52fce810 | |||
b9bb92099e | |||
64dcc31ac7 | |||
36546b4c4c | |||
dde886f058 | |||
781f7ef570 | |||
3e8bb32ffd | |||
df310641fb | |||
21ef55f205 | |||
ade36566ea | |||
08d7a0d52d | |||
1fd2885995 | |||
d357640fbf | |||
ad9cd23202 | |||
5916177dc8 | |||
905b1e2775 | |||
377d45c9dd | |||
a444cac2aa | |||
1e714eb6b2 | |||
3f14466965 | |||
e0b8f4202d | |||
11b14bd3ab | |||
90684483e2 | |||
760a82cb08 | |||
0317583489 | |||
1c3f2bba6d | |||
7d62bf9a3d | |||
7c248cd2ef | |||
e4119268ca | |||
fc2760e761 | |||
c57084de36 | |||
907aff3b43 | |||
2793404116 | |||
d850f67979 | |||
8080063024 | |||
f33c6eb95f | |||
4e3d71c2c9 | |||
a074cb78cd | |||
0dbc33f781 | |||
25bbc3bc2a | |||
5f55a9be84 | |||
300e3d151d | |||
2f7911b62a | |||
54dfe708c1 | |||
8166925f04 | |||
64f1d93cc3 | |||
6d67568037 | |||
5003e97479 | |||
858068cdc0 | |||
65fb307d0f | |||
2f1fe726f5 | |||
e9b0e3cb9d | |||
34fceca7ff | |||
c646845cd3 | |||
eb483bc053 | |||
50d3fa7437 | |||
9f7fc5f054 | |||
a27e9cb3c2 | |||
10270dcbad | |||
4ff4fb6c38 | |||
c8c794e340 | |||
97a1e950ef | |||
9fa8105ae8 | |||
d68b6ea7b1 | |||
58f4709362 | |||
f71cd2c6f3 | |||
8ec1f6ea2e | |||
d63c8ae1ae | |||
e39094ac37 | |||
b539389741 | |||
ac35fe9ed1 | |||
3d70afc578 | |||
b919b3e3b2 | |||
7a7349f2ff | |||
07b57735b1 | |||
e42c95a327 | |||
473af78368 | |||
ab6c7f6ca3 | |||
599516473a | |||
83ac075b22 | |||
3548c6c43a | |||
3bfe2e75b5 | |||
97c93629a5 | |||
643384e1ec | |||
1809277e05 | |||
7981865fd2 | |||
4467d5eb4c | |||
38aed0c886 | |||
02801b3e75 | |||
b79d361e6c | |||
9eb8b67b5c | |||
132c664e18 | |||
288645aeb7 | |||
55f06f5bad | |||
a2cb18bfe9 | |||
d35b3754a2 | |||
7f3aca15dd | |||
2c5cbaff25 | |||
134cd7ab04 | |||
c74b8b6df3 | |||
573116e259 | |||
71ab030ea4 | |||
c4125b80ec | |||
626a381ddc | |||
5333bda234 | |||
cceeb8e52d | |||
94a0d10499 | |||
3f6aba23dd | |||
cd9dac4c7e | |||
f478894729 | |||
97790480c9 | |||
9643c39bf6 | |||
0a08d40237 | |||
d029997aef | |||
ceb27b431e | |||
d3761c2435 | |||
b25d8ce764 | |||
34da362ee6 | |||
de6109c599 | |||
736f08815e | |||
106645d9bd | |||
c55ada2f26 | |||
4e4a1643c4 | |||
e1e84d4465 | |||
4a0009365e | |||
3849b8ece4 | |||
f2ab8f17c8 | |||
48671a1728 | |||
72b6ec4aa8 | |||
8790a92f07 | |||
0f8ff07b51 | |||
dca73068c5 | |||
4094e62ed3 | |||
7a0e897960 | |||
e78fc74e03 | |||
5054e74f7f | |||
72e6a39172 | |||
be73db13e0 | |||
cbaba5cbf3 | |||
c1447b2695 | |||
e58f08b60f | |||
662d62f561 | |||
cf4813a1ec | |||
b03636dc33 | |||
6187779d10 | |||
ddc8bfed29 | |||
f1221d724d | |||
aec44e3761 | |||
aed07f0f48 | |||
c178fc7249 | |||
41554f433b | |||
863956d09c | |||
7118178e2c | |||
1eabe66c85 | |||
2de0a9e453 | |||
0bb6940c1a | |||
e341b33f21 | |||
6abdd6401d | |||
6632c7026d | |||
c474cf1eef | |||
e26cd2eb26 | |||
b33becabca | |||
3c8a8640aa | |||
a1b5ea9cb1 | |||
bc162637a6 | |||
8f1b7c3fff | |||
be71f49d80 | |||
8b39eb5e4e | |||
1173cf7ed4 | |||
b4fd141105 | |||
0002b5dd02 | |||
709598541f | |||
aa781811af | |||
b595bf8f44 | |||
f6979a090e | |||
2e1dcd84f9 | |||
144d321193 | |||
d41dec9395 | |||
f977327c7b | |||
aac1a58651 | |||
095afdfe47 | |||
4ae1783b97 | |||
cd92adb1c6 | |||
7dec40ff05 | |||
4b38ecd916 | |||
02c0098d57 | |||
1e58c585d3 | |||
ed4e9febe0 | |||
1c61415cee | |||
c02625f91a | |||
da5b777ee7 | |||
a6aaca814c | |||
ab3dd2a1b3 | |||
7b7a2fc52b | |||
95b28d4d8c | |||
1278396bd5 | |||
0e29868e34 | |||
0115a1f834 | |||
cf103add54 | |||
766af58cd8 | |||
5200435bab | |||
56734dca3b | |||
dbaf8e66ab | |||
6e7c5f205b | |||
e7df3cfe22 | |||
0e8540417f | |||
c3ad0eebec | |||
c82ffaabdc | |||
4e6a9b029a | |||
3e519faaa8 | |||
e2eb7c1ba7 | |||
87ba5b865d | |||
992f2790e7 | |||
e1a099632e | |||
fd7db7a954 | |||
5bb4ac9873 | |||
31b0d14856 | |||
952ab2bde5 | |||
3c6af52a71 | |||
6317bec7aa | |||
eb3ba5ce2d | |||
1f0b3f954a | |||
cdb2a7bef3 | |||
f6515b2b6a | |||
5128d7d6c3 | |||
731e5e1291 | |||
cedee73548 | |||
8136d52c0b | |||
d1945c29d7 | |||
83b40e4f30 | |||
95ac6305bc | |||
ab4828aae7 | |||
c506423e70 | |||
f0843fc5f1 | |||
c87e035302 | |||
abb9a72b27 | |||
acc6bf1564 | |||
db688207a5 | |||
9681c4d468 | |||
d9e2b94d7a | |||
f789038baa | |||
2e23b03f94 | |||
5181a2a9b1 | |||
2d2572d2cb | |||
fa553029d5 | |||
c986a20bcf | |||
c5a74ada05 | |||
73979d8f5a | |||
f90d96367d | |||
5f565c92c9 | |||
7452486c72 | |||
afdf0efd31 | |||
7fc271ef97 | |||
582ba4f173 | |||
0229c97071 | |||
c0b398c7c9 | |||
549f9676f1 | |||
6248624ee7 | |||
0025d36880 | |||
4985b682c3 | |||
85333c5d62 | |||
3feda8a315 | |||
5375c420c1 | |||
ac9f6a77c9 | |||
58f4e0653a | |||
03e6a56b3c | |||
32f19c5c19 | |||
98e893c69b | |||
fea480526b | |||
4aa6695a13 | |||
a7e5423ede | |||
3ff8bbcf65 | |||
9d34ded5f3 | |||
511d8275d6 | |||
0a9226ec8e | |||
9c07a8c26a | |||
6058bfb687 | |||
7a6d730db3 | |||
2985988f0d | |||
d62c9ac309 | |||
85c8af08b3 | |||
21c09073a1 | |||
40acaee446 | |||
d9a22705ce | |||
dad0bfe447 | |||
1b3e7f734a | |||
0e58023794 | |||
4fb9c8a547 | |||
43cce3a8fc | |||
344427c1dc | |||
82a2080e45 | |||
9a4abe96c7 | |||
d87c2eb903 | |||
65708f234d | |||
b6b179af97 | |||
37003da854 | |||
3f323aba1a | |||
29889a90e5 | |||
ed478675ba | |||
9767468b7f | |||
8ba1d5f426 | |||
84567d36cf | |||
32162ef0f1 | |||
2dd20c38b2 | |||
aa1bd603e6 | |||
e104941569 | |||
2754ceec60 | |||
609e915169 | |||
11f1c00ca7 | |||
a74b24fdf0 | |||
e25992a011 | |||
00bb5925e1 | |||
1b50fbbc90 | |||
a746969995 | |||
c536a0bf04 | |||
5b8e7bfcf2 | |||
3cbbceec78 | |||
e684fafb68 | |||
651342b3db | |||
c01290438f | |||
9e9c82869a | |||
494b143453 | |||
8cc1cde0fe | |||
883fc39c80 | |||
1c0758e3bd | |||
668d353add | |||
06a1681fdc | |||
a16e41002e | |||
16e705dc75 | |||
b52228feb9 | |||
25f25d0f82 | |||
85e7046caf | |||
c741a960b9 | |||
34c8b2cc2f | |||
278effad49 | |||
a0bed5375d | |||
9eecd549e4 | |||
a2c3369713 | |||
1f9ab7f58f | |||
3e1a926aa6 | |||
57f82934f2 | |||
f3a8aec64d | |||
e2e5bc65a9 | |||
df136578d4 | |||
ae7f169027 | |||
6da7a784f2 | |||
12cddf725e | |||
d8861c2a5f | |||
145fb3675d | |||
77e8cb2718 | |||
a8ea6471e7 | |||
bfaf5634a1 | |||
53afa64634 | |||
c9bf9ce094 | |||
a2e29fa71f | |||
637f58364a | |||
1bd04b26e5 | |||
29ef9370a6 | |||
2262f279d5 | |||
e4f477cf90 | |||
33f921235d | |||
1bae87d4b3 | |||
1e43fb587e | |||
d65e7b9fcc | |||
4bb6549895 | |||
06e3cd3d2a | |||
e9e01557b7 | |||
e0f046b7a5 | |||
9845aec007 | |||
81c82b5af9 | |||
a9b083e585 | |||
9abc500269 | |||
b9eb7e14e6 | |||
b7be5b9a7a | |||
ce41760fdd | |||
a7503050c2 | |||
d4eb69ca14 | |||
aba9df8457 | |||
6aa80e431d | |||
bae7612f36 | |||
a0bc8b8af3 | |||
73930b5eac | |||
fbeba259b3 | |||
d1bedeae13 | |||
e84f1f6de7 | |||
cc88f9bcd6 | |||
f630b50902 | |||
9a7082d0d5 | |||
8dc9089611 | |||
222d2d7953 | |||
27c10d4468 | |||
a17467aefd | |||
73b10c196e | |||
965dbbe835 | |||
e3ae10bacc | |||
fcda94b673 | |||
b1109b813e | |||
122a5b2f69 | |||
dea20248c4 | |||
ae90ac238c | |||
3b0ca9f478 | |||
61e79e6d02 | |||
1cdab81a3c | |||
dca0ba6a5d | |||
d666ebc558 | |||
c84b796e17 | |||
7204bb40bf | |||
637d5c6691 | |||
3c86f41769 | |||
f37eb533f1 | |||
6e8b69fc88 | |||
cb23070dfe | |||
5d9d83d312 | |||
823252dd41 | |||
35764225ed | |||
c7e5006bcf | |||
b0149a54d8 | |||
e6030d66eb | |||
6611188edf | |||
abbb037888 | |||
132d59ca6a | |||
200d5e62c2 | |||
b748942d6a | |||
648b6597bf | |||
5b73a8eceb | |||
514bf32b99 | |||
2073188345 | |||
c0b472292b | |||
1b15fd1da6 | |||
6883ea0944 | |||
303289777f | |||
a8bf00fe20 | |||
6282c53fe5 | |||
dac28e0961 | |||
4f86563352 | |||
818afc68c1 | |||
443d8ce7c4 | |||
da5cb0b012 | |||
922ffdfc28 | |||
2f1107ff4f | |||
1fd7bd7ede | |||
c0c38463c7 | |||
c1e142d1dc | |||
6933f2bad1 | |||
b03d1d8894 | |||
8e4a86e329 | |||
1f87d9ba4a | |||
14267e172d | |||
95e83cfe3f | |||
e74574706e | |||
b381d9e06d | |||
a416b53d11 | |||
6fd13e3af0 | |||
4b7dc8200c | |||
b83279848a | |||
a48b278c10 | |||
75e19f4f0f | |||
1a5bf0c689 | |||
3e245f16c0 | |||
2698b7614b | |||
b296a9a0c7 | |||
9c8e853567 | |||
825d8ef6c9 | |||
da1201c552 | |||
e0c05bf437 | |||
a84b6bc7e4 | |||
00c4c30d72 | |||
72c7139d8c | |||
e287ba1a7e | |||
8e67a18551 | |||
590b88f718 | |||
00ee8813f7 | |||
e76f2ea89c | |||
c9f57c2d96 | |||
438d36341d | |||
3ab54b1591 | |||
77a2f186ee | |||
526344c9ac | |||
f8bd19f5db | |||
e4c6e4bf26 | |||
8783563176 | |||
6015a0ff15 | |||
63b76c32f9 | |||
c9264ee12c | |||
0d7b1a84cb | |||
81e17bad40 | |||
97d90b99e2 | |||
03d4d1cb36 | |||
3282cb85ae | |||
9354e797b6 | |||
3f9c2bc33b | |||
4369c1a113 | |||
b1e57e2a30 | |||
4d9489aeb1 | |||
45c247fa5b | |||
4e2663023b | |||
fa4608a95d | |||
fec47a09a9 | |||
022a97da99 | |||
e9116736cd | |||
2b549e3af6 | |||
b2afd1ea0b | |||
ef8e5b40b6 | |||
a6773ad442 | |||
c2add08efb | |||
a33c76a456 | |||
11b1bd278a | |||
e3a96ed3fc | |||
447243f994 | |||
710c0c9980 | |||
f0300c1711 | |||
e3c0bd5a3f | |||
8af61f561b | |||
780360834d | |||
74e503da92 | |||
d28b643c84 | |||
dc1049a6e7 | |||
f965b3de46 | |||
eb54a4fe91 | |||
5d3847d14d | |||
5b92286568 | |||
094bc59553 | |||
e9a0b3a8f3 | |||
1724430489 | |||
23c43ed21b | |||
79b334b7f1 | |||
9328ee4f63 | |||
d7594b19fc | |||
d6b3991d49 | |||
ec63bacdc1 | |||
e713ba06f1 | |||
37cb218437 | |||
4f79a8a204 | |||
7341298a11 | |||
b9c27e3a9d | |||
885fe38c01 | |||
2dbe8fc1a9 | |||
7122139e12 | |||
4e6c03c9da | |||
d5f27f9b1e | |||
86f19a3ab3 | |||
be0eefb0af | |||
c1cd92bbee | |||
44b7684d56 | |||
0c90e1eff6 | |||
491bca5e4b | |||
ebd676faaa | |||
045c5e8556 | |||
45b4cf2887 | |||
4b5acc065a | |||
73eca72f14 | |||
28431ff22c | |||
639bed2f6d | |||
77794eebdb | |||
eb37aa2bba | |||
048fe371aa | |||
0b666ad9fd | |||
87c9af142f | |||
6b46c22b42 | |||
94494b64d7 | |||
b648f37b97 | |||
78d3b83900 | |||
56b6ed6730 | |||
e0c68bf9ad | |||
64ebd9a194 | |||
35fe08b3bc | |||
aedab3f83f | |||
5c87ddc80e | |||
f53810fcd2 | |||
56fa3a09c8 | |||
58bca04a3f | |||
3c6afe7707 | |||
09296e0d71 | |||
4b3d64ec9f | |||
a904e15ecc | |||
a82a5ae184 | |||
bafd90807d | |||
08924ea36a | |||
0f8ea6872e | |||
1b7598e351 | |||
d2431128c7 | |||
8e0e12e5c9 | |||
e883117a7d | |||
cd0e08cae5 | |||
1490c42d9f | |||
789ee9f138 | |||
2c52e82352 | |||
0a981a6606 | |||
534f8d7a4e | |||
c4ca76e39e | |||
a8b9899dee | |||
d2cb4e003c | |||
0a0c62f384 | |||
6000df9779 | |||
24963e547c | |||
3ad3dee4ef | |||
46d44ca99c | |||
06d1af8b18 | |||
d34b2c4ffd | |||
0c52df7569 | |||
91bd38504e | |||
71a2b794b4 | |||
373714bf0b | |||
ee769171b9 | |||
6ebadbcca3 | |||
3f60d98163 | |||
ea00c1274e | |||
b7dc9dbc76 | |||
8b357dcb32 | |||
1f6346d880 | |||
b7bd38744c | |||
f8a67e282a | |||
0a7e199c82 | |||
5143f6d6f1 | |||
30b662df39 | |||
33f2d83506 | |||
4244a14ad3 | |||
f031fe58fa | |||
84cc240f34 | |||
b26906df1b | |||
56a3197f7f | |||
0505d7bd32 | |||
a448c0b81e | |||
8116fe8def | |||
7c6dcc8c73 | |||
1a9401e1f3 | |||
00d310f86d | |||
c4259fc8cc | |||
8c5614daa1 | |||
eb668c6466 | |||
a461c5682d | |||
e3478ee2ab | |||
0bea870b22 | |||
5fbdc6450d | |||
1531a1777a | |||
f38345fdad | |||
04d46ea33f | |||
3a2fa9a650 | |||
f5bbc5e961 | |||
95c9fefbd0 | |||
073a48ab85 | |||
753a783ba9 | |||
58f2598d5d | |||
3c835b692b | |||
7f2fa8bbcb | |||
a6fd1ca3db | |||
58a4905916 | |||
2c9607d5da | |||
371cb4f0f3 | |||
b46c809544 | |||
eb29a2898c | |||
c3a74e5e63 | |||
a1759aed19 | |||
1a3387706d | |||
41f8764232 | |||
4bf797c8f1 | |||
7e3b54f826 | |||
23d3a9ae42 | |||
756156e9db | |||
4807fb4c5c | |||
dd25c5b085 | |||
becfd1e9fa | |||
951d6398a0 | |||
7c98545b33 | |||
bb1060bdad | |||
7ad45a91ec | |||
51045962d3 | |||
034c5d0422 | |||
7148c14178 | |||
93fb61dc8f | |||
b36ceb5be4 | |||
37d7ad819b | |||
6bb6785936 | |||
cb70824ed1 | |||
ddc1082e8c | |||
f98d72a30b | |||
71df71c601 | |||
1d0f7c44e2 | |||
d78f19f8bb | |||
0e567381fb | |||
e2225d3b71 | |||
666af1e62d | |||
2fe3402362 | |||
14a236198f | |||
cc1b43b90a | |||
9448f0ce52 | |||
59fdd8f6be | |||
7b20318ee4 | |||
c3c955b02e | |||
6e56e41461 | |||
d74d5e0e44 | |||
cac08171de | |||
6f6c350781 | |||
506724fc93 | |||
b4fe70d3d8 | |||
3efbffe4e3 | |||
cafa873f06 | |||
b4f4347d6e | |||
5c866dd000 | |||
974249f2a5 | |||
a65022aed7 | |||
b101f40c32 | |||
e8e6c70e19 | |||
c8d27f6424 | |||
287e8cefda | |||
db8f2d9f07 | |||
cd6736d70b | |||
0d2e3788ba | |||
c0dcf67ec8 | |||
bc52336a1b | |||
3bfb052b0a | |||
c71d5a111e | |||
437b62c4d9 | |||
cbca0ae264 | |||
e0cde7dfc5 | |||
e720070945 | |||
a8ab6f4caf | |||
b7b1884950 | |||
755064d3e2 | |||
24a984086e | |||
4b831d58b7 | |||
62f36037ea | |||
ffdc1814c6 | |||
29776c0283 | |||
69d7384cc0 | |||
9720ac0019 | |||
fc56e1e517 | |||
0f4837980f | |||
9a6e27ac36 | |||
07202205c4 | |||
dc56bbeec8 | |||
4be537c51a | |||
66c568ba67 | |||
9ff8abaf29 | |||
b7144560c9 | |||
4be6d01dfb | |||
aef84320e0 | |||
9a5195e79e | |||
cc111941bb | |||
74ee1e5087 | |||
e5d1bd6589 | |||
6a0f7a5ceb | |||
554cd03269 | |||
9995194cf1 | |||
1298ab1647 | |||
b8ab3078fb | |||
50e8666a14 | |||
0659971ecf | |||
fd562cb9e2 | |||
aaa5cd4615 | |||
3f835f8ee3 | |||
5bf9a20d42 | |||
eedc8c7812 | |||
f0d1ed0cc4 | |||
8ba1aed5a3 | |||
9ef5e51c0f | |||
fe5566d642 | |||
4a2933b0b6 | |||
8ee0e9632c | |||
8fcb7112ec | |||
6ac466c0a4 | |||
d45fcc4381 | |||
a22e1199cf | |||
79f12d6b55 | |||
483f6702a6 | |||
f6e3464ab9 | |||
708876e9a7 | |||
29d04aa533 | |||
6fcccedb70 | |||
60f3aeb4ef | |||
c1ad987b04 | |||
9d0b7c6b31 | |||
d489cb1a8b | |||
0fe6d61036 | |||
092edabd2d | |||
1a68bce94c | |||
87fe3ade81 | |||
accabca618 | |||
091b21fae7 | |||
85398c728a | |||
7325b19aef | |||
7cdbbfa88e | |||
3ce3f1adc1 | |||
9880a86f80 | |||
647e5d76b0 | |||
7e4af9382e | |||
282d4a3563 | |||
cafeef33c3 | |||
4f48f1a850 | |||
a05a378db4 | |||
245362db96 | |||
b1b190b80d | |||
3408ce89a7 | |||
59a094cb77 | |||
8782b14842 | |||
0f38b4b856 | |||
75f407e191 | |||
4b07778609 | |||
9b81696a09 | |||
80e19e0ad7 | |||
962e8dca1d | |||
8da4be1b34 | |||
f2ef74d1a1 | |||
546c92751b | |||
ae903f190e | |||
bf33d9d703 | |||
3a89d80a61 | |||
fd45e83651 | |||
27e2fd9b06 | |||
9a49ace606 | |||
3413ecc2bd | |||
ad8b095677 | |||
38c72070fb | |||
93fe1af1a8 | |||
504bf4ba84 | |||
9f9c5fcf10 | |||
90a0237457 | |||
c83538a60c | |||
13d4e3f29f | |||
cefbb7c27d | |||
fa98434096 | |||
af3ca02e35 | |||
5c396c222a | |||
088bab61a4 | |||
080d18b06e | |||
54fb4e370c | |||
17f1f40140 | |||
b011ed6358 | |||
acbc6335af | |||
511c84760e | |||
6cbf82dbe0 | |||
896622de64 | |||
1a160a86fa | |||
11abd3cf6e | |||
9552badb16 | |||
6fd41beccd | |||
c679dea1b7 | |||
4788a4f775 | |||
9243bc58db | |||
2238725d1c | |||
bffa9f914c | |||
eeb31074de | |||
af22de2cfa | |||
1d3f05a9d4 | |||
935524f20c | |||
5847961fec | |||
40d7f5eff8 | |||
c57dedb034 | |||
b2d7b34082 | |||
4d67aca919 | |||
e3dfd7b1ab | |||
166945a461 | |||
46866be21d | |||
154e20484d | |||
aeee25e703 | |||
b51bcb55db | |||
b5784de33f | |||
9556a9be17 | |||
01c524ddd2 | |||
5e703dc70a | |||
bc96bd3410 | |||
094f0a8be3 | |||
3d996bf080 | |||
4b05ee6811 | |||
d7032aeb43 | |||
4ea1c030bc | |||
172e511e56 | |||
4481efd51e | |||
337c2bfd29 | |||
ffc82c027e | |||
e8fd5b4600 | |||
67f8916aa8 | |||
96e01f3a79 | |||
1e755f261f | |||
b2ddac610c | |||
ad05f64b13 | |||
9b472d36fc | |||
b54b0a1d25 | |||
f5794de636 | |||
7ae9d9690b | |||
db3cca7fbe | |||
b9743957fa | |||
0ef099421c | |||
f1ae5b1795 | |||
a8d6c75a24 | |||
1c2394227e | |||
c49e2f8bbd | |||
af403ba6fa | |||
ec5a8141eb | |||
92584bd323 | |||
586d9ee850 | |||
2de45a4da5 | |||
f5569e76db | |||
3a13ecba1f | |||
73b9ee9e84 | |||
b1682558a6 | |||
0a7c07977d | |||
0a83b17cdd | |||
2bad6584f6 | |||
872a3317b5 | |||
38901002b0 | |||
1db6a882bb | |||
571522e738 | |||
b5a80d3d49 | |||
3441d3399b | |||
fa288ab197 | |||
af11562627 | |||
286f08f095 | |||
92c3e26c7a | |||
6516c2532d | |||
82a0cc9d27 | |||
fa58da2401 | |||
1ddf93fd86 | |||
cba9c5619e | |||
70c149c7da | |||
b34e197424 | |||
f4b26247c0 | |||
8f0a1e32d5 | |||
c4b8f0cd2f | |||
aecb06cd2a | |||
e3c4f1f586 | |||
97b1156a7a | |||
02bfcd23a9 | |||
cc2f448d92 | |||
b45d07c8cb | |||
f0fe089013 | |||
a20c1b4547 | |||
56ffb4385d | |||
db3c5f91b6 | |||
17204b4696 | |||
8a83c45bc6 | |||
a6312ba98f | |||
4170f11958 | |||
04a0652614 | |||
b880dafe28 | |||
36530fc7c6 | |||
4fd4218178 | |||
632425c7d7 | |||
ad3e36a7ab | |||
a29b307554 | |||
1bcafca690 | |||
5d80edd969 | |||
e21b6d9db3 | |||
9c30bddb88 | |||
7336645501 | |||
59e6bd115e | |||
8597701b0f | |||
15aef079e3 | |||
42689d4842 | |||
6e9b8e21ae | |||
424612ea9d | |||
5afafd9146 | |||
affa76f81d | |||
340d5d557a | |||
214ed3667c | |||
122627dda2 | |||
7af95eadcc | |||
9ee858a00c | |||
27d456bf93 | |||
ea6e042a6f | |||
a594f56c02 | |||
e6fa74fe69 | |||
f184d69c7a | |||
228a5aa75d | |||
9a4f8199d6 | |||
ae0be1e857 | |||
d010cac8a5 | |||
63a758508a | |||
bf2658cee0 | |||
6ecb00a1d8 | |||
1990501786 | |||
963de90b7f | |||
13c7c3b3a6 | |||
e4049f3733 | |||
3cefa59a14 | |||
0cb5ae41c6 | |||
209040e80e | |||
2112c87e13 | |||
da44b0f0f6 | |||
c1c2f1f0a9 | |||
777a0a858e | |||
68e99c18c0 | |||
c99f93e40a | |||
969016b9e4 | |||
4ae58cc854 | |||
1fbbf13ec9 | |||
3f9dc08984 | |||
1ddf9960a6 | |||
9f45c0eb03 | |||
67155861e5 | |||
5111255942 | |||
b405deb55a | |||
9b5368d0ec | |||
f8aa806d77 | |||
e98ef7306d | |||
188904c318 | |||
9594293804 | |||
814801d321 | |||
0896511b14 | |||
222b177745 | |||
4189a30b13 | |||
f6f0a5d448 | |||
b21facab7b | |||
70312ed77f | |||
ee9255cb1d | |||
f045e19ddc | |||
3f1bececdf | |||
34c3a0cc1f | |||
8ef73eee51 | |||
e52f3f34a4 | |||
27b617b340 | |||
21a73d81ee | |||
7c3e6e8e86 | |||
42dc18ddfc | |||
801df72680 | |||
c8f161d17f | |||
549bfe7412 | |||
b00011a3f1 | |||
3ca826a480 | |||
b8ebb4d609 | |||
5321b606c1 | |||
a1ad74a986 | |||
29d95328ce | |||
b2eeccbcc2 | |||
bad0b55ab6 | |||
0878bd53d9 | |||
de910e1169 | |||
f2cf647508 | |||
9684737de7 | |||
ecc87ab1aa | |||
3cc0dd0d1e | |||
fa359c6fc4 | |||
5c71f2a439 | |||
8cc751d1cc | |||
978fd6858f | |||
41689256c6 | |||
99445f475b | |||
070d6a2faa | |||
3de63570f6 | |||
8d1ac37734 | |||
36503ead70 | |||
f4d3b3f0d6 | |||
acee1f7c6c | |||
c242467fdf | |||
47ae25eeb9 | |||
ddc4e7ffa0 | |||
6a2ffafdb9 | |||
0c091c1b24 | |||
55993ef0ce | |||
30a0820cbe | |||
194e3100a9 | |||
8ad4464d4b | |||
e7b0a736f5 | |||
fa4bdb4613 | |||
167eb01735 | |||
8fb5d72b13 | |||
83c0711760 | |||
8947c5a4aa | |||
a7562c9be1 | |||
08dc169f94 | |||
f549d8ac74 | |||
1ac7536286 | |||
ec0a56cb9c | |||
f0d24a68ee | |||
2c529f2118 | |||
af1d9345e0 | |||
03ce45d93a | |||
1695803248 | |||
58e3dd4cb6 | |||
c7f678688d | |||
7bf4c08f70 | |||
69beee5416 | |||
2200a31331 | |||
88e270723f | |||
a13e25f083 | |||
826ac80e62 | |||
4506584c48 | |||
3d3a30e200 | |||
76b83ac0f4 | |||
903a9bfd05 | |||
655ee1a64b | |||
e0e6c3fdb2 | |||
31f00974f2 | |||
c3218bb9c2 | |||
90fb6ed739 | |||
d2972024de | |||
3f9ad1253d | |||
a556a54dc9 | |||
dc0a2ca656 | |||
e9f986e54d | |||
357d852382 | |||
6e00c6790e | |||
f36604357e | |||
c3fb9d5549 | |||
f5b5c54d7d | |||
9f0b06bb86 | |||
57a384d6a0 | |||
69802e141f | |||
6fc02b7424 | |||
30cdd85028 | |||
871dd47019 | |||
37f8dd57e2 | |||
f827bfd83f | |||
b3af930153 | |||
cd488b7d07 | |||
e2373ff51a | |||
b3d2c900cd | |||
d5adec20a3 | |||
942256a647 | |||
ca39486d06 | |||
db632fcc2a | |||
a3321a5d80 | |||
521de13571 | |||
e6f91269ec | |||
3abf6a8a30 | |||
8d7f380dfd | |||
59163e2dd9 | |||
574021041d | |||
872adf1031 | |||
5fc1167802 | |||
c89a09e5d0 | |||
d9dabdfc74 | |||
6b910d1bd4 | |||
1c4f799845 | |||
bbd9ea8c00 | |||
fc67a968e8 | |||
3d113611cc | |||
c1af48bd85 | |||
07667771ef | |||
3822c29415 | |||
ff386d6585 | |||
e3ddfd8dff | |||
f0c79fdbca | |||
88ddb31477 | |||
077d1a41f1 | |||
857ab8662e | |||
a17f9bd0f4 | |||
f4b9e93b11 | |||
2c11bf2e66 | |||
0e33773e92 | |||
719e14b30a | |||
38883d1de4 | |||
c6c8351fca | |||
043f50487a | |||
3a2b91f1b7 | |||
a76d11d486 | |||
d1f01b5209 | |||
7a54dbf7d5 | |||
33a5d5fe93 | |||
201a4b7b2a | |||
591a28d516 | |||
22d160a3c3 | |||
903c82d7f1 | |||
b2e0395f19 | |||
d96a6b42a5 | |||
cf95708c18 | |||
7fe50d6402 | |||
e1c7b99450 | |||
12ae7b9a6b | |||
6ac5700f2e | |||
a0dd8617be | |||
1576072edb | |||
03d206a7ca | |||
c973de1d76 | |||
71336965a6 | |||
e791d0f74d | |||
3543a9a49f | |||
7dd198a99e | |||
e048116ab2 | |||
cda9ad8565 | |||
928f375683 | |||
d3e521f70e | |||
96e03eca14 | |||
659dfbf51f | |||
a7ee428214 | |||
a41254e18c | |||
4a3230904e | |||
c81a3f6ced | |||
a5412fc0cd | |||
83fc3c10cf | |||
6b6c87e510 | |||
267f9115ba | |||
39c87fd103 | |||
2ad2fdd235 | |||
1fda4b77ef | |||
5a8938209b | |||
0bf2ff6138 | |||
e33f3a2562 | |||
bba19ce667 | |||
9bf2d1d7b4 | |||
9fe210c454 | |||
f99fae3c61 | |||
860dcdb449 | |||
70cebaf74a | |||
317fe19da7 | |||
e7b6c8b7e0 | |||
478ba75d6b | |||
4e553ea095 | |||
0c46f15f94 | |||
7b92497d21 | |||
4668a798ca | |||
729d28d910 | |||
66e9d30fda | |||
6335be803c | |||
a77b1ff767 | |||
1f6ece233f | |||
d53077bb3e | |||
2b44d5fb6a | |||
10e1e0c125 | |||
017c281eaf | |||
c5b1bc1128 | |||
dafdab1bbc | |||
d0ebee5e3b | |||
aa7c741ec0 | |||
9e7b9487b0 | |||
c7a67b5a02 | |||
0e749dad4c | |||
fa72160c95 | |||
851e012c6c | |||
7f76403d0a | |||
126f065cc9 | |||
7ee4dec3f1 | |||
c07d09c011 | |||
4d98da44e3 | |||
15c00ea2ef | |||
522876c808 | |||
7d05cc8c5d | |||
49f4be6a2b | |||
e702515312 | |||
5fce8d2ce1 | |||
2696b22348 | |||
5df4754579 | |||
a00284c727 | |||
3832602ec4 | |||
3466f139a4 | |||
def7d156f6 | |||
33aab094ef | |||
cf6f344ccc | |||
b670b9bcde | |||
fea86b2955 | |||
7c610b216b | |||
bec34496f1 | |||
49014393e1 | |||
818d03c835 | |||
cdf1a96e23 | |||
bfcdec95cb | |||
fc55835932 | |||
3772910bf2 | |||
24379c14dc | |||
23846bcf1c | |||
9dd0a6e6a7 | |||
5ca473ac2d | |||
e1a551e8f2 | |||
0926702269 | |||
0a85347a0d | |||
fb59f73c1a | |||
eaa8b9cb1e | |||
b8261d7d83 | |||
f5827d4a83 | |||
b0f8a983c4 | |||
56c77bf482 | |||
d831c5dcc9 | |||
ce474eaf54 | |||
0da1c06b15 | |||
01edc94a4b | |||
f96563c3f2 | |||
30697f63f1 | |||
433fcef70b | |||
34b5b3d9c5 | |||
ea8b19a40f | |||
b0405db5a9 | |||
f34f0af6b1 | |||
51ed48941b | |||
22b6cbb4da | |||
87ac549689 | |||
2a6046de8e | |||
25dd5145bb | |||
f8f11b7f50 | |||
82f914e0dc | |||
3b41eec199 | |||
9359cc69d5 | |||
b02b636b36 | |||
a537154c28 | |||
39e1bdeb71 | |||
43bd28cdfa | |||
6c10458b5b | |||
3ccbf81646 | |||
2e38cd98c0 | |||
7780d9bab8 | |||
8feed96eac | |||
16d23292dc | |||
812a8bcc6c | |||
63807935cb | |||
92a8b646df | |||
d9f9e347ab | |||
2ef8ebe111 | |||
038a46b5ef | |||
3852ad3048 | |||
1075a73902 | |||
863a0c3f8f | |||
f8673931b8 | |||
dd4fb7aa90 | |||
2af5aad032 | |||
9027141ff8 | |||
c4bc331663 | |||
8be7c13d2d | |||
d7ea66b6a1 | |||
371c69d425 | |||
c9c1564d26 | |||
cd18a1b7db | |||
6aac096c77 | |||
7b58bd621a | |||
9b43b00d5c | |||
76694bfcf4 | |||
bfad138bb3 | |||
d8d23c9971 | |||
f77b30e81d | |||
d379478603 | |||
2600684999 | |||
54968b59bb | |||
6b5d12a8bb | |||
c4b9d5d8b9 | |||
f683817b48 | |||
52491b467a | |||
7789fda016 | |||
22abc27be4 | |||
c9138f964b | |||
c4346e6191 | |||
1a7830f460 | |||
b418c1abab | |||
1fbf1d2cf2 | |||
5a85cc4626 | |||
8041461a07 | |||
2ce72a1683 | |||
eae9372a5d | |||
ed09b2bdb8 | |||
1d7722043f | |||
95f9488a70 | |||
e7cbbd8d45 | |||
c8c255ad73 | |||
a264f8fa9b | |||
40e945b0c8 | |||
f3b04894b9 | |||
35b7e50166 | |||
6b3f684e2a | |||
63c66ce765 | |||
0636399b7a | |||
2c74815cc9 | |||
298bd6479a | |||
a8481215fa | |||
b7545b08fa | |||
cf8f3bcbed | |||
b8534a402d | |||
45b9a7f8e9 | |||
879431ebcd | |||
102354c218 | |||
af1283e92c | |||
6b777b066a | |||
1e01088698 | |||
3ea0651078 | |||
776b1c2294 | |||
dffa2eb04f | |||
5ecb9da801 | |||
00889c5139 | |||
af8dc3fd83 | |||
ba884b4e36 | |||
6ddd494826 | |||
aa2fd3f3bb | |||
cf00354f42 | |||
47f1fa3f2e | |||
db98f7e0b4 | |||
38ee5c4dfb | |||
aca2f9666d | |||
b74e085538 | |||
899de2ff56 | |||
cf521a5bd2 | |||
bc13248e1c | |||
0529f36fde | |||
74b4ecb7f3 | |||
333f658eb6 | |||
7cb5c0708b | |||
85869552e0 | |||
6f9843c14b | |||
7d44f60e45 | |||
8d16f69bb9 | |||
3a73a09391 | |||
009c71f7e2 | |||
073d39df44 | |||
ae7222f0df | |||
4d6c54272a | |||
13bfdde228 | |||
3cc78d3a41 | |||
45bb97cad6 | |||
546e4c5696 | |||
6b1917b931 | |||
30b22c8b78 | |||
6f5e92e5b3 | |||
cce5c70f29 | |||
4af7c82ef0 | |||
52e5fb7e0c | |||
a013e8ceb1 | |||
864632b582 | |||
71d6eaacef | |||
4aba05d749 | |||
7d335165ec | |||
37213209c5 | |||
fbde9bb731 | |||
f6b1b5ab37 | |||
7abd456d45 | |||
f12743de38 | |||
77e10ed757 | |||
ebcb9a2103 | |||
6fb2e080bc | |||
3ac5ffc188 | |||
88187ef282 | |||
489894cb32 | |||
be003970b7 | |||
3488ea7d1c | |||
9a6a399a29 | |||
7ab65352be | |||
b28fbfa13e | |||
07c656093c | |||
c9e8346e6a | |||
9e5ac76855 | |||
f671b7f63f | |||
236113e417 | |||
a340b18b19 | |||
f6c8e1a4bf | |||
160cff4a30 | |||
48685cf766 | |||
0f32102684 | |||
d46682d1f2 | |||
55833e20b1 | |||
02cfa76916 | |||
9314eea7e9 | |||
1733beabf7 | |||
471d8f6ff9 | |||
e47fcb196b | |||
3ae53961c8 | |||
113b002095 | |||
9447537d8c | |||
7404b8739e | |||
7239395d95 | |||
926d459c8f | |||
7cabe203dc | |||
1e53f4266a | |||
24b513c3c7 | |||
b982595c73 | |||
af8a36b7fb | |||
208e7d7943 | |||
557736f1cf | |||
61927e1941 | |||
fc75827aaf | |||
2f2531d921 | |||
d5f20980eb | |||
21eae981f9 | |||
ead7f4287a | |||
3b33150cfb | |||
6d34a68e54 | |||
5c483c9928 | |||
a68c99d782 | |||
0aebbae909 | |||
a3a2215bda | |||
eb377993b3 | |||
5ca52d785c | |||
8d9912b4e2 | |||
c77b1c9687 | |||
8849ecd772 | |||
7977b97227 | |||
4f34822900 | |||
bbb38ac106 | |||
ce934a547e | |||
16b19d35dd | |||
45cfa5b574 | |||
df9ccce5b2 | |||
f8516b677a | |||
dfde83bdce | |||
cb0f19e4f1 | |||
26b99d3f85 | |||
2f9c0d1d9e | |||
0423cafbeb | |||
0bd1412562 | |||
0339642e77 | |||
37a0b7b132 | |||
c30b605047 | |||
76076d6fad | |||
0a819ec4e2 | |||
57a717056e | |||
856c48541f | |||
2045091c4f | |||
03ac5a6eef | |||
32fadc9c30 | |||
15a89d4f17 | |||
d0f43e9934 | |||
31e779d3f2 | |||
30c79fd40d | |||
639c93460a | |||
7611730cdb | |||
9df9c1433a | |||
4ea422bcec | |||
6074e4f962 | |||
d52e6d01ec | |||
63caca33be | |||
64efa62a74 | |||
912eb5e8e9 | |||
bb628e8495 | |||
d0c19c2c97 | |||
926fdb7519 | |||
c886625c83 | |||
f6c10d8a2e | |||
2bd877528f | |||
d09889b1dd | |||
1b2e9122d5 | |||
7424388924 | |||
537436bd5e | |||
32fc0cd7e9 | |||
fb99494858 | |||
5b4d4b97bc | |||
c5180c8092 | |||
515c200d86 | |||
32aab82e32 | |||
6aaa350145 | |||
d3b4dfe104 | |||
9fc30f6db4 | |||
2d0f07091d | |||
3828eda507 | |||
1e736ec16d | |||
bba6437ea9 | |||
e5ab9a856c | |||
1515bba9c6 | |||
14a9ef4bbe | |||
041040c659 | |||
47f69f2d24 | |||
9dd4dc2088 | |||
b534c32ee3 | |||
d2712f1457 | |||
183f560d06 | |||
ae150c0897 | |||
606e1396cf | |||
5c85e037f8 | |||
5c523716aa | |||
5f8cbf359e | |||
e83834e6be | |||
02225aa95c | |||
9931ac9780 | |||
2ba2bc72ca | |||
45b8ba9ede | |||
40968e09b7 | |||
262f26cf76 | |||
785c619198 | |||
24a993710d | |||
c240bb12ae | |||
eed3b9db94 | |||
29a8823db1 | |||
a80955eacb | |||
9716c3de71 | |||
34fa3208e0 | |||
9c4e19958b | |||
0403299728 | |||
95701114e3 | |||
a99d17c3ac | |||
517149d325 | |||
32aa2575b5 | |||
8fe7b96629 | |||
9350619afa | |||
d8d8f0bfc8 | |||
0a39722719 | |||
9c0fa4d1d2 | |||
da0404ad03 | |||
b508fdb62c | |||
680f90df21 | |||
1a68807ad9 | |||
d901767b54 | |||
13d4443d4d | |||
74b63c12a0 | |||
cd42f6591a | |||
5491422b12 | |||
23f3ff3cf0 | |||
f90488c77b | |||
beb4536841 | |||
3fa46dd66d | |||
ad5fcf778f | |||
83b000ae88 | |||
33e179caa6 | |||
b1e941cab9 | |||
6db961d256 | |||
83409ded59 | |||
396b2e9772 | |||
94459deb94 | |||
660af84b8d | |||
7b31020903 | |||
9a4143b4d9 | |||
aebc47ad55 | |||
b6b5455917 | |||
5bc01cd51a | |||
c79acac37b | |||
a5f2aa6777 | |||
4169e5c510 | |||
0727c440b3 | |||
19a7ff0c43 | |||
5f18403199 | |||
9f325fca09 | |||
10d08acefa | |||
52d50e6bc4 | |||
e7de7c32db | |||
a5f07638ec | |||
aa2a3fe201 | |||
abd13ba4ca | |||
485ba093b3 | |||
36b18e4fb5 | |||
8d92232949 | |||
e4d8c094a4 | |||
d26e1c51a9 | |||
675ff64094 | |||
423e7ebc3f | |||
f9fe6a0f72 | |||
8d007bd7f7 | |||
6cdbdfbbcb | |||
35e6343d61 | |||
7fb7839c8f | |||
dbc1ffc75e | |||
1fdbe893c5 | |||
55a542bff0 | |||
e10574c64d | |||
2e00be262e | |||
4172bde081 | |||
9c47e022dc | |||
874addc51a | |||
b7ae5b712a | |||
c6d7cd2d33 | |||
386a96b7e0 | |||
b238c57179 | |||
1821e72812 | |||
a23c230603 | |||
4e01fd5458 | |||
e416cf7adf | |||
25edb9e447 | |||
93c4f6c9b8 | |||
718031ec35 | |||
d546614936 | |||
ac8d738045 | |||
ca962371b8 | |||
e6f8922e35 | |||
7292ece7ad | |||
df3b78c18c | |||
c83dcea87d | |||
be20c99758 | |||
694add9919 | |||
afc764752c | |||
113c8b5880 | |||
a5b28349ed | |||
bb7ecc7cd9 | |||
14bc160674 | |||
d438c22618 | |||
bcbae0a64f | |||
f636408647 | |||
3ffc7aa5bc | |||
7b7e8c0d3f | |||
11ea9e7c4b | |||
2b82121325 | |||
5038e5ccd7 | |||
e943ed8caf | |||
c196952afd | |||
e7383a7e66 | |||
8a7545197f | |||
680072e5e2 | |||
4ca377a655 | |||
751dd7eebb | |||
8f0e0c4440 | |||
50cf73500e | |||
db310a044c | |||
88a609ade5 | |||
304d63623f | |||
407b2682e8 | |||
0f4fd8367d | |||
747ba6a8d3 | |||
bb99fd40de | |||
e972d6639d | |||
22e77c9485 | |||
bc88473030 | |||
95677a81c5 | |||
ea37d29d3a | |||
e030673c9d | |||
3e76efe97e | |||
f5a30615c1 | |||
e5e325154b | |||
9e3d2956d8 | |||
26b1466ef6 | |||
a1f01fb8f8 | |||
b2be0e2e5e | |||
1a45587c08 | |||
3199f174a3 | |||
a51c2f193e | |||
be31da3dce | |||
54b407b4ca | |||
e87cac06da | |||
ad4fef4f09 | |||
e3b3701e13 | |||
9228fe11c9 | |||
5ab38afa51 | |||
e49b8f0ce7 | |||
c50ac96f75 | |||
a9355c33b2 | |||
3dcee9f79e | |||
2614189157 | |||
beeb09646a | |||
67f1fbab5f | |||
c0e7e43e96 | |||
9bfead2e01 | |||
6073cd57fa | |||
5174be5fe7 | |||
62a18d4c02 | |||
a6c15684c9 | |||
5691bf557c | |||
8f01f7cf21 | |||
bb8c94ad2c | |||
d98e35e095 | |||
3163fbad0e | |||
0172422961 | |||
8ccfb26923 | |||
12a474b6ee | |||
270fd6d61c | |||
7b9c7d4150 | |||
55126f5fb6 | |||
431692d9d0 | |||
6732a9078d | |||
2981076a14 | |||
5740ea3807 | |||
cd2d50e06c | |||
8c8a4ba705 | |||
b10de40506 | |||
2030dfa435 | |||
bfe64f5f6e | |||
6d27751365 | |||
1fb1c0a681 | |||
062f654fe0 | |||
d3cb161c36 | |||
98b47d2540 | |||
f28ba3937b | |||
91cf14e641 | |||
7601a8001c | |||
0ee6c5bf9d | |||
6dee632d67 | |||
51e5de4d97 | |||
1f08b22c8e | |||
83ae5bcee2 | |||
339a570b26 | |||
5310b6e5a2 | |||
7d14f44a7c | |||
c830eeeae4 | |||
157fcf1de5 | |||
e050160ce5 | |||
f273351789 | |||
aebf7f88e5 | |||
aac1571670 | |||
8bae75a8a6 | |||
c2f7ca9d8f | |||
6ec0e42220 | |||
072b244575 | |||
7ac9d6c604 | |||
0125163190 | |||
a06f4b1d44 | |||
10daa015c4 | |||
0babee39a4 | |||
7c08b397eb | |||
155ee8792f | |||
f89f121d2b | |||
27986d7abb | |||
8b7edc6d64 | |||
7dfab867fe | |||
fd36954477 | |||
fd51599fa8 | |||
3ca80c676c | |||
be7cce1fd2 | |||
e142aafca9 | |||
4196cf43e8 | |||
a344eb7dd0 | |||
d12537bdb7 | |||
bcb3b3c21f | |||
d8c9a1aae9 | |||
9ca2f5b3f7 | |||
9e24775051 | |||
4dc30ea104 | |||
90df6237c6 | |||
80caa8fdce | |||
8706774ea7 | |||
1d7e87d430 | |||
1a4cd763f8 | |||
ee74b367ce | |||
f06113500d | |||
9ab5692acf | |||
e7a910b664 | |||
b52230097e | |||
a8fdb8a5a7 | |||
297f859631 | |||
5d19b799af | |||
af3eb5a16c | |||
b313b7f6f9 | |||
016ee36808 | |||
c3fc98c48f | |||
40aa0654fa | |||
bace2880d0 | |||
9d80eefb81 | |||
1c17c6dd2b | |||
2be0dbddbb | |||
a91b785ba5 | |||
0ef05de889 | |||
a093d5c809 | |||
fc64e1853c | |||
7f669094de | |||
5025d89c88 | |||
2b44c4504a | |||
d2c9beb843 | |||
9e6d3bf532 | |||
a89b611e9e | |||
ebcac3c2d1 | |||
7029e4395c | |||
5afcdcbbe6 | |||
3840b4b516 | |||
7aeb6d642b | |||
1d6c4aacae | |||
9f5c86e60c | |||
9f413fd656 | |||
97c3125a78 | |||
a77aca75b2 | |||
96bfd9478b | |||
e8206cb2d4 | |||
c3af0d9d25 | |||
932c994dc9 | |||
c34d911eaf | |||
ddd1871840 | |||
db825788fa | |||
b1b03ec13b | |||
73a8441add | |||
bf29590f41 | |||
51b27779c9 | |||
5169c8d08f | |||
0d945e6a92 | |||
1090254ba5 | |||
e51445d857 | |||
4b47abd3bf | |||
71a617b4dc | |||
a722802c95 | |||
e9f44b6661 | |||
9693de1867 | |||
f7ea95aed1 | |||
f07ce59be8 | |||
da423b6cf0 | |||
d5f60b68e4 | |||
78b3a8f7f9 | |||
d77699c126 | |||
09ba0dae15 | |||
a5c7575207 | |||
50f040530b | |||
7f99c90539 | |||
d8564b725c | |||
e4de25442a | |||
3b2ea8fd40 | |||
9a1832ed61 | |||
9e45f1f5e2 | |||
ee682d5bc3 | |||
05decc863f | |||
506a81e8cc | |||
dcb30a8489 | |||
a2631e89f6 | |||
ab208ddb77 | |||
09a48d773a | |||
88298bf321 | |||
d252f7f687 | |||
533ebc17f2 | |||
f4947236dc | |||
e088833b81 | |||
53e16f68d9 | |||
ed5fbaef06 | |||
b1bacf12a6 | |||
66ff602659 | |||
e175c9dea9 | |||
5a57d9b5d9 | |||
03e87e4169 | |||
abfff66d53 | |||
31dee553d5 | |||
9ca6a2d25b | |||
a3178c3bc7 | |||
aa07bdfbaa | |||
eaef9be710 | |||
cae345b416 | |||
acb1171422 | |||
52d8f293b6 | |||
636eb8d058 | |||
0fa27f65bb | |||
8f94e3f7ae | |||
05460eec0d | |||
072d0b67e4 | |||
fdc48d521c | |||
6560b0e2cc | |||
ec38dba209 | |||
d9e4bce6ad | |||
1fd4343621 | |||
8d87627a49 | |||
aacf27fb76 | |||
a51536d107 | |||
1c874fbc1b | |||
0362169671 | |||
e2e569cb43 | |||
8c51b47e85 | |||
017eb10e76 | |||
f50aeb0e58 | |||
48c19d3100 | |||
aaf0a23134 | |||
89db85dbf9 | |||
e677cda027 | |||
db9219ccc8 | |||
06fd945f85 | |||
6ad4a81123 | |||
bcaa0fdcb1 | |||
2cb1375217 | |||
9365a47d42 | |||
6ffe205447 | |||
ec3e62dd58 | |||
fa07c49cc9 | |||
449d7042f0 | |||
7e2b65374d | |||
8e39465700 | |||
43b4207101 | |||
ff991b87da | |||
c81c19234f | |||
399caf343c | |||
ffb72136c8 | |||
1a615bde2b | |||
cf2626a1c5 | |||
68c72d6f34 | |||
65f78905cd | |||
70a8ae4612 | |||
d82ec2634c | |||
b4a7a18334 | |||
c44c5f0b09 | |||
226d3b9471 | |||
2752bde683 | |||
b8816d722c | |||
2aa72cc72e | |||
8cc030ef84 | |||
9a9f89293a | |||
501deeef56 | |||
05f921d544 | |||
ab7a2960b1 | |||
4e2deaa33b | |||
d5ef18337c | |||
d18ea501b7 | |||
c9a1ac9b8c | |||
c2a4cb544e | |||
3ab12076e8 | |||
6a383c45fc | |||
7cc27e7bd1 | |||
0464087327 | |||
c193c7de12 | |||
61abee204f | |||
a99dbb2a0c | |||
e834c76b40 | |||
7b3c7f148b | |||
fb4b33b81b | |||
25d7dc7b96 | |||
d1f1cbe88f | |||
a4e7b6e90c | |||
fbc7c9c431 | |||
8b248dcf09 | |||
4938aad939 | |||
7e882dfe62 | |||
5c8cb96f88 | |||
9d1eb4f9ea | |||
210a4d0640 | |||
176e806d94 | |||
eb4e5a7bd0 | |||
ba27596076 | |||
63e44dcc35 | |||
c0ba676658 | |||
1af4cee63b | |||
cb52a335bd | |||
e308a4279e | |||
513a934ff6 | |||
77d820c842 | |||
30cbe7c6a9 | |||
18ef643dc7 | |||
73a0bf8d30 | |||
9d53208d68 | |||
d26f135159 | |||
c8e3ce26a9 | |||
f88970a964 | |||
51d911e3f4 | |||
bd5c6158ae | |||
cd0db7842c | |||
31d1087103 | |||
0efd64df6f | |||
28bdf346f6 | |||
48762834d9 | |||
8d0d429acd | |||
e5408368f7 | |||
61492fd27e | |||
bbce08a67b | |||
a002148098 | |||
90ae662e4d | |||
60d8f5489f | |||
59dd8b650d | |||
738247ad44 | |||
5b0bb7e607 | |||
f7c0d30167 | |||
8e98c7c9d6 | |||
50661e7b8d | |||
ad159e0906 | |||
d3fac8a06f | |||
c641ba1006 | |||
de379ed915 | |||
d4554c6b78 | |||
6fc21a4223 | |||
71319978df | |||
6147e54686 | |||
0c8eec2563 | |||
4ab58f069a | |||
85f96d926a | |||
816de4f8ec | |||
42229a1105 | |||
d8820053af | |||
731f8512c6 | |||
a133784706 | |||
be58fdf1bb | |||
57daeb35d2 | |||
9c5e69bf3d | |||
cfac127e4c | |||
fda4523cbf | |||
cabe80b129 | |||
d4c41219f9 | |||
4fdd9fbfca | |||
bdf5ac9c1a | |||
f1785c76a4 | |||
2de8fe9c5f | |||
d910ed68a3 | |||
f7f7ecd4c6 | |||
a9c3a28a3b | |||
96787ff4ac | |||
c3ed4d28de | |||
f1e35c3bc6 | |||
db3fb3a27c | |||
8282442956 | |||
a355d9f46c | |||
be4824c955 | |||
86c1d97c13 | |||
0b48aea937 | |||
cdec0cead2 | |||
831709ce7e | |||
b7b8a31532 | |||
15406545d8 | |||
5aced8224f | |||
af20a43b77 | |||
39c3280860 | |||
2d35345c50 | |||
a02910be32 | |||
b9ec97a30b | |||
2e89999d88 | |||
24b0031925 | |||
9eeaf2d502 | |||
c9e6fb36c3 | |||
8de317113c | |||
a1ec549630 | |||
ecddff98f5 | |||
10066d67bf | |||
a07f7435c6 | |||
d3523ebbe5 | |||
133ddb11ff | |||
1bf15ae907 | |||
f73f3941cd | |||
d69d79612b | |||
64ea5126e0 | |||
9df3aa50d5 | |||
cab75b7829 | |||
d9fac86015 | |||
1eb8724a89 | |||
c6662a4512 | |||
d3c09b4e96 | |||
124f6e83d2 | |||
569ff73b39 | |||
fc1dbddd93 | |||
3ae867bdd6 | |||
bc5f29150b | |||
46016b8c7e | |||
5dbecd6b6b | |||
877920e61b | |||
3d1e908dad | |||
6880c2bef0 | |||
78872ffb4b | |||
229d825fe0 | |||
edc5fc098e | |||
bbe815468d | |||
82e7725a42 | |||
dc61cf1c8d | |||
aba63e2c6c | |||
c2ddd056e2 | |||
c9508e84f2 | |||
f6f0900506 | |||
7aeef27b99 | |||
98d0ef6df5 | |||
208a7f16cb | |||
16cf31c3a3 | |||
2b48daaeba | |||
79d24ee227 | |||
a284030ecc | |||
fc0d7f5982 | |||
f697632edb | |||
73797c789b | |||
036fcced31 | |||
1d3157fb80 | |||
0b11c2e119 | |||
96af892d95 | |||
c2983f824e | |||
88d6fea999 | |||
c23fa289c3 | |||
db35f220f7 | |||
982afa87a6 | |||
dccae18b53 | |||
53e86f2fa2 | |||
757dfd36a3 | |||
708add0e64 | |||
d8991ae2ca | |||
5f6cbe0cf8 | |||
f167b0c2c5 | |||
f784500fbb | |||
83df47323a | |||
c75d4abb0b | |||
5216a723b1 | |||
b801ca477d | |||
c830c604f4 | |||
0e66606c7f | |||
8707abe091 | |||
dc2a840985 | |||
2727067b94 | |||
6a8a494f5d | |||
a09d2e252a | |||
3e9c463ff1 | |||
46d50f5bde | |||
e8da903c6c | |||
ab10b7676a | |||
fa44a71d3e | |||
c86e9e8568 | |||
9e22e23ce6 | |||
835f29a178 | |||
9688f8fb64 | |||
df5cde74b0 | |||
231d5e5968 | |||
c2ba72fe1f | |||
d93786c86a | |||
bf15cad36b | |||
288ed7a8ea | |||
f07c038266 | |||
8eed120c38 | |||
5dbcb43abd | |||
dd1eefaf62 | |||
35de159d00 | |||
546a1e90d5 | |||
b033e1d904 | |||
96d6985895 | |||
58f220a3b7 | |||
a206f2570d | |||
2318ffc704 | |||
d4304eea28 | |||
06af9de753 | |||
7f71e1e09f | |||
bb7eccd542 | |||
b04c71acd9 | |||
bbf9ea89c5 | |||
846ad61941 | |||
8b41c415b7 | |||
197ba8b395 | |||
8d2a61a0c9 | |||
7512317243 | |||
bca2294655 | |||
abd55e4159 | |||
4a980568ac | |||
9d436fc5f8 | |||
ad331e6d56 | |||
d7e4e57548 | |||
b2067d2721 | |||
c2bbe4344e | |||
8567253833 | |||
ca7d4c42dd | |||
8ca514a5ca | |||
b605552079 | |||
74f5538bd3 | |||
ff57c7b7df | |||
ce8a4fa831 | |||
8331aab26a | |||
a6857dbaaa | |||
054298d957 | |||
cca240c279 | |||
89f17ceecf | |||
fe97857c62 | |||
75854cc234 | |||
9783d47fd1 | |||
38be61bd22 | |||
c64e2acf8b | |||
a200cedb4b | |||
5fec0ac82f | |||
999534248b | |||
fbc754ea25 | |||
ecea41a0ab | |||
1b6d472cb2 | |||
f0446c7e88 | |||
2a0025bb57 | |||
64d6d3015a | |||
90550c5b58 | |||
53cd2cdd9f | |||
1ac5d300a4 | |||
642c25bd3b | |||
df808dedd1 | |||
02f9cb415b | |||
e3cf1e6598 | |||
7681211c02 | |||
0ee935dd72 | |||
16772d3d51 | |||
1c38e40dee | |||
ceb5a76609 | |||
db2392a691 | |||
9c1b6288a4 | |||
575179be8e | |||
5b6ffaecc0 | |||
efc72b9572 | |||
5dc7177540 | |||
78a4b1287d | |||
c5001869f1 | |||
7c31f217d5 | |||
1152457691 | |||
3beb38ac8a | |||
8cbaa19d2e | |||
63d2b2eb42 | |||
e02da9a15a | |||
ae111a131c | |||
4402e1128f | |||
f55bb6d95c | |||
91741e20fa | |||
0514f5e573 | |||
637d403415 | |||
9fabd34156 | |||
039ed01abf | |||
ead0eb2754 | |||
c3db2df7eb | |||
ee6c15d2db | |||
715a3d50fe | |||
692b125391 | |||
5193819d8e | |||
210b9d346f | |||
4c4b0f551e | |||
6800ff1882 | |||
399a3852b1 | |||
e7d3069f58 | |||
40ea3e3e61 | |||
dc9a11bae0 | |||
906d18a709 | |||
a13058b6c4 | |||
98ee4b4672 | |||
7fd7310b96 | |||
28fa43d2a9 | |||
1a9e6ffdd7 | |||
c998199954 | |||
19792192a7 | |||
4aab413154 | |||
15a6179b97 | |||
83b308983f | |||
f2b1a04bca | |||
3e36e6dcf8 | |||
6feb6a27be | |||
c5ceb15e02 | |||
57e928d1d0 | |||
e2c68d8775 | |||
d173e6ef87 | |||
c230360f4c | |||
384b486b29 | |||
b72e91f681 | |||
46d9ba5ca0 | |||
a9240a42bf | |||
a7204d5353 | |||
f570ef1c66 | |||
ee0195d588 | |||
448b8b1c17 | |||
4d77fa900b | |||
7ccd771ccc | |||
e9f8b5b9db | |||
2366c1ebaf | |||
c5de237276 | |||
aa9bc57b4d | |||
11df477b20 | |||
7141750668 | |||
68675bd1ab | |||
19b3cacd60 | |||
bcfaf5d994 | |||
e9499ac5b8 | |||
7ff721e563 | |||
fda3b9bbd4 | |||
cf70e5ff2f | |||
a86618faf3 | |||
6693386bc5 | |||
4a8a0d03a3 | |||
2c9d288ca9 | |||
bb0aabae75 | |||
5cda0ed964 | |||
0aba74935b | |||
4eb666d4f9 | |||
d5e0cf81ff | |||
3ea784aff7 | |||
fef93958c8 | |||
cae88c90b1 | |||
1a8da769b6 | |||
2b259aeb41 | |||
de7e9b4b4c | |||
0f95031b99 | |||
d622742b84 | |||
ff254fbe5f | |||
05153e4884 | |||
2ece27ee3a | |||
a58df52205 | |||
2ea6f86199 | |||
7c5172a65e | |||
821e3bc3ca | |||
5dd2f737a3 | |||
c9bb5c1f5b | |||
5d936e5c8a | |||
e985c2e7d5 | |||
308b6c3371 | |||
ea7fa11b3e | |||
5a40ea3fd7 | |||
102510ac0e | |||
2158329058 | |||
bc484ffe5f | |||
6fcf4584d5 | |||
1adc83d148 | |||
647053e973 | |||
95b98b3845 | |||
f27613754a | |||
3e351b0b13 | |||
79ece53e3c | |||
f341b2ec10 | |||
167b079e29 | |||
7ded5a70be | |||
fc476ff979 | |||
c3279c8a00 | |||
e471ea41da | |||
552d4adff5 | |||
0c33c9e0d7 | |||
fae9fff24c | |||
79924e407c | |||
18d4da0076 | |||
416c141775 | |||
af1a2e83bc | |||
4cdb9a73f8 | |||
4433730610 | |||
71eb5bdecc | |||
029e2db2cf | |||
81db333490 | |||
c68ee0040d | |||
d96e267624 | |||
0b47404ba6 | |||
7f4844f426 | |||
50e1e0ae47 | |||
538c3b63e1 | |||
678b2870ff | |||
308d8c254d | |||
f11aa4a57b | |||
c52d4eca0b | |||
7672506b45 | |||
80a02359f7 | |||
ab3968e3bf | |||
42ebf9502a | |||
bd4fcf4ac6 | |||
4dceb73909 | |||
dd819cec3d | |||
5115cd7798 | |||
cbb8dee360 | |||
e0cdcb0973 | |||
a6a2a745ae | |||
297896bc49 | |||
f372840354 | |||
4c4659be13 | |||
1b79fe73a1 | |||
5fa072cf16 | |||
212874e155 | |||
75212f40e7 | |||
6fde65577e | |||
80ecef2832 | |||
edf2ffaf4e | |||
6c275ea5ef | |||
23ed65b339 | |||
9c7913ac9e | |||
8b01e6ac0b | |||
ff5854396a | |||
f0725b4900 | |||
327ba5301d | |||
dcce475f0b | |||
aa2104a21b | |||
0206020104 | |||
33bd1229d9 | |||
195098ca2b | |||
9daa7bdbe2 | |||
6bd18e18ea | |||
8f046cb1f8 | |||
735a0ee16d | |||
537be6a29d | |||
2b528e2225 | |||
75505bbd72 | |||
e1fc7444f9 | |||
940caf7876 | |||
fcdb0403ba | |||
caeb55d066 | |||
f11e60b801 | |||
54f2146429 | |||
f60ee87a52 | |||
9c06fe25df | |||
1eec8bf57f | |||
ddb24ebb61 | |||
a58c83d999 | |||
6656ec816c | |||
8d2bd43100 | |||
429ea98ace | |||
3d80926508 | |||
d713e3c2cf | |||
5d20d1ddbf | |||
257acdcda1 | |||
dab98dcd81 | |||
99653a4d04 | |||
dda563a169 | |||
782aa7b23b | |||
813e438d18 | |||
7a71adaa8c | |||
ce8796bc2e | |||
c7e1409f7b | |||
9de9379925 | |||
7d68b6edc8 | |||
48b5344586 | |||
686b7d3737 | |||
7c65e2fbfc | |||
96a6e09050 | |||
b3f823d544 | |||
ea21c7a43e | |||
437fb1a8d7 | |||
166099b9d9 | |||
c707b3d2e7 | |||
f7d294de90 | |||
4ecd0a0e45 | |||
7ebbaaeb2d | |||
cdcf59ede0 | |||
5d065133ef | |||
d403808564 | |||
3ffdca193d | |||
69688a18c7 | |||
7193bf28b6 | |||
637f890b91 | |||
009d5adcba | |||
52c55a0335 | |||
23428b0381 | |||
0e305bd7dd | |||
c068ca4cb7 | |||
6a8379109d | |||
120add0e82 | |||
b92ee51c2d | |||
cba3b35ac9 | |||
313fed375c | |||
1e63702c36 | |||
478ee9a1c4 | |||
eb1e5dcce4 | |||
84225beeef | |||
9cf0bd9b88 | |||
9d25d7611a | |||
1abefb2c7a | |||
bcc247f25f | |||
68ca9b2cb8 | |||
686e61d50c | |||
17d927ac74 | |||
966c55f58e | |||
d76d3162e5 | |||
d0a2d46923 | |||
a67f58e9a5 | |||
fece91c4d1 | |||
9d2d9a0189 | |||
6d3afc774a | |||
88646bf27d | |||
0696f9f497 | |||
b2ea2455e2 | |||
3f659a69fd | |||
2c62be951f | |||
2348733d6c | |||
cc229b535d | |||
7f810a29ff | |||
fc1dfd86d2 | |||
5deb34e5bd | |||
39df087902 | |||
6ff46540b6 | |||
dbab8792e4 | |||
4eb676afaa | |||
a6cb2f1bcf | |||
28af9a39b4 | |||
8cf5620b87 | |||
85d6627ee6 | |||
611a005ec9 | |||
90b3b90391 | |||
fd4f294fd3 | |||
145274c001 | |||
df5d6693f6 | |||
05c5603879 | |||
c2c48a5c3c | |||
4af556f70e | |||
8bad411962 | |||
5b0418793e | |||
4423ee6902 | |||
f0c39cc84d | |||
3d45b04da8 | |||
9e2f26a5d2 | |||
a016f6e82e | |||
eb3e5fd204 | |||
72282dc493 | |||
47a22c66b4 | |||
fb11d8a909 | |||
7d872f52f4 | |||
d882bfe65c | |||
103584ef27 | |||
1fb537deb9 | |||
2bd48b4207 | |||
f5a6db3dc0 | |||
dd0c1ac5b2 | |||
d8c9655128 | |||
09f2d273c5 | |||
f6eb85e7a3 | |||
0d85b43901 | |||
fdf94a77b4 | |||
af40ab0c04 | |||
015b7a1ddb | |||
ab3e460e64 | |||
194a84c8dd | |||
51d932dad1 | |||
561d31cc13 | |||
d6a8e437bb | |||
4631af5011 | |||
5d28729b2a | |||
8c08e614b7 | |||
e76bf1438b | |||
4e177877c9 | |||
60848b9d95 | |||
79b3564a26 | |||
1e8c36c555 | |||
94d015b089 | |||
cfb3736372 | |||
2b77f62233 | |||
e8d23c17ca | |||
a7ed2a304a | |||
0025b42c26 | |||
3f7f492cc0 | |||
490d7875dd | |||
4240edf710 | |||
30e50d0f70 | |||
751c1eba32 | |||
d349d6aa98 | |||
1f9152dc72 | |||
1b9d50172b | |||
084dbd7f58 | |||
58c0508f94 | |||
dcf82c024f | |||
b253ed0c46 | |||
61db53fc19 | |||
b0ead086a1 | |||
a3b22d0d33 | |||
28d24497a3 | |||
05cea4c1da | |||
260f5edfd6 | |||
7105136595 | |||
54db379bf2 | |||
effbf0b978 | |||
8e7a2a9587 | |||
18e6ff4167 | |||
fa1cdaa91a | |||
b538b67524 | |||
2b0f6355af | |||
11b9a0323d | |||
710fa822a0 | |||
aaf6ce5aea | |||
34ea483736 | |||
a3ff40476e | |||
4cca3ff454 | |||
3d9acdd970 | |||
428f220b88 | |||
10add6a8ac | |||
f06a8dceda | |||
545f4f1c87 | |||
77543d83ff | |||
eb6a30cb7c | |||
97372b8e63 | |||
cea29ed772 | |||
b5006b8f2b | |||
81c44c605b | |||
0b66a6626a | |||
e8be4d7eae | |||
30f0c25b65 | |||
73ae3c3301 | |||
f98e9aba48 | |||
84c28a077a | |||
350cf62b90 | |||
aa4f30c491 | |||
3de979aa7c | |||
5bc133985b | |||
87156e1364 | |||
45ff142871 | |||
2710ff271e | |||
468ac9facd | |||
705720f086 | |||
a219e78f00 | |||
7a41868173 | |||
e16acec901 | |||
de44d7475e | |||
c2dd009e0b | |||
5a8da75d06 | |||
848c6e2371 | |||
e3882950cf | |||
28f6fbee23 | |||
3144a70b18 | |||
bed5438831 | |||
6f991b3c11 | |||
03a8a5ed55 | |||
0c6d2ef1f4 | |||
d2be79f38c | |||
cc89801b12 | |||
dfa05a8742 | |||
d7d985365b | |||
0d4e4b18c2 | |||
7687436bef | |||
d531b9645d | |||
6a1b5a222a | |||
be2bf69c93 | |||
0672794692 | |||
c65c0d9b23 | |||
0ee86ff313 | |||
3b1aa846b5 | |||
0a34cb8023 | |||
227aa38c8a | |||
1dd467ed7d | |||
922dffb122 | |||
63985d4595 | |||
97dd1834d7 | |||
2ea030be48 | |||
606cfbfe1e | |||
90a4ab7e57 | |||
412e15fbdc | |||
ed0a590549 | |||
71f05cb23e | |||
5f99657523 | |||
587ae1bf3c | |||
461dea69d9 | |||
22c0e3cd54 | |||
3ed9567f96 | |||
c4fa841aa9 | |||
f284af1c3d | |||
46602ba9c3 | |||
81477246be | |||
9bd63867aa | |||
d1c317fd5f | |||
cbd664ba4b | |||
4bb7cefa15 | |||
82c86daa78 | |||
b95db62be3 | |||
0f7fdd71cc | |||
af1a7da0d5 | |||
d698b3da3a | |||
6d275d571c | |||
63acb82c87 | |||
4d05b74314 | |||
37dd511356 | |||
96c321da76 | |||
4701540cc9 | |||
f54615b4e3 | |||
9c456b2fb0 | |||
77bf17064a | |||
44150b2e85 | |||
8ec2fe15f3 | |||
687af3e3a4 | |||
72ab83cd45 | |||
4b07772e22 | |||
22d2c962b2 | |||
e771d36278 | |||
800c2dd370 | |||
f38842822f | |||
88a6fb86bf | |||
f6fe998ed4 | |||
16337d7c1e | |||
ae309f80f7 | |||
fa70b3bf70 | |||
3a90f138b2 | |||
033f6dcbcb | |||
5d8b2f899a | |||
490205ab84 | |||
2c0e704c82 | |||
253048f72d | |||
e09b8430ce | |||
9ae283dc3a | |||
f95a79d145 | |||
0dabdfd48e | |||
d2bb4dc14a | |||
b4dc180592 | |||
263577773f | |||
7d708be121 | |||
feb1669d39 | |||
2cbfe41422 | |||
b7653865b1 | |||
c72dced8fa | |||
6feed5fd56 | |||
b8fe5ae076 | |||
7e657d65f3 | |||
a166bb816e | |||
2952027d04 | |||
430d9d9314 | |||
fa247196c0 | |||
5d17c2b58f | |||
6ee45d282e | |||
cfc3bd0696 | |||
3e0e09555a | |||
1d8bb5144e | |||
67e0100866 | |||
f2ab08c65e | |||
04a93050e7 | |||
03401041db | |||
6eac744a05 | |||
ae29e2085f | |||
7ce0b58af8 | |||
ea5663c0da | |||
a61bfae8a4 | |||
5716898216 | |||
c0f9e452f2 | |||
4e3526394e | |||
6806a14a3f | |||
ec7e50b37d | |||
e7b7dfebf5 | |||
a9e0b27772 | |||
669164bada | |||
4f3a291391 | |||
56e37ad2f4 | |||
17de79a83a | |||
09e9139855 | |||
76fc5822c9 | |||
c767a854ed | |||
b60802ddff | |||
1c35d59f26 | |||
adcaf715c6 | |||
1f9494221b | |||
466d6f76b9 | |||
b05e6ce3db | |||
1d812e78d5 | |||
fba494343f | |||
0b878eccf8 | |||
98772b16d6 | |||
bb82ff0c80 | |||
71af03dc98 | |||
5671da4a0a | |||
d63493a852 | |||
c06582ba40 | |||
450f271cf7 | |||
a31889f129 | |||
ba6a6f5227 | |||
9a38d61048 | |||
903ec27754 | |||
0b56d603c2 | |||
4ffb5d157a | |||
816246ebee | |||
a9881aee05 | |||
7b5b989cfe | |||
c4b62e19f2 | |||
79a97ada04 | |||
da215d1a21 | |||
9ffc50bead | |||
f8352bac2f | |||
27c1410fdc | |||
9a4733bde7 | |||
f3df5df52c | |||
517d08c637 | |||
90dd794ae5 | |||
e0dbbba8a3 | |||
705df55a7f | |||
d354e85a9a | |||
e4e1f8ec1e | |||
0112a24179 | |||
d680f6b3a5 | |||
47e732717f | |||
ec56abfccb | |||
e7cdb402fb | |||
a3fe1965fb | |||
5256e6833e | |||
051cd2e1ff | |||
51929e7df8 | |||
a094507bb8 | |||
8effa4e3e0 | |||
1c9e7dbc45 | |||
799b249f02 | |||
7b4a378c92 | |||
47917d00d1 | |||
a4c49af859 | |||
1c1d7d1e0e | |||
d28536d76e | |||
63cfbb9497 | |||
231040b93e | |||
7c74afc35a | |||
7878a011eb | |||
c05416e27d | |||
ee200d8fa0 | |||
2f42658cd4 | |||
d95e8030fc | |||
4aedd3f1b6 | |||
bb89d6f54d | |||
ed10841e3d | |||
6dac87f2a7 | |||
a167d0d331 | |||
eed37820b5 | |||
124e1fa350 | |||
ac40434cdf | |||
39354c06f8 | |||
faedb88de0 | |||
5cd1fb486f | |||
5b5df49e6c | |||
86f9277e2d | |||
56b09bf0ac | |||
f4c4b9df9c | |||
6e568c69a7 | |||
14d624ee40 | |||
d5c0557891 | |||
1691060a22 | |||
a5ce578c72 | |||
05edfad13a | |||
136b43f461 | |||
ac40c1818f | |||
eb63dbcd2a | |||
4e2f1a519e | |||
55ec7f9fe9 | |||
b7ddefdbf9 | |||
ce361c2cdc | |||
ed6ba55261 | |||
ec333d2bd6 | |||
551f639259 | |||
da3bb6fb93 | |||
08bcb62016 | |||
8f4ce1e8d0 | |||
4a534d6abb | |||
b48a8c0555 | |||
1919ec247b | |||
3966eb5374 | |||
c22ef50cae | |||
be5f2ef9b9 | |||
adfcb79387 | |||
73c4c0ac5f | |||
6fc601f696 | |||
07111fb7bb | |||
a06d2170b0 | |||
7f53ea3bf3 | |||
b2accd1c2a | |||
8ef8a8dea7 | |||
e929404676 | |||
c2258bedae | |||
215fdbb7ed | |||
ee998f6882 | |||
826e95afca | |||
47583d48e7 | |||
e759cdf061 | |||
88503c2a09 | |||
d5be23dffe | |||
80c01dc085 | |||
45b2549fa9 | |||
c7ce454188 | |||
7059ea42d6 | |||
8ea1c29c9b | |||
33bbfdbc9b | |||
5de54f8853 | |||
a1ac41218a | |||
55fc647568 | |||
e83e898eed | |||
eb07e4588b | |||
563f834c96 | |||
183178681d | |||
8dba53e494 | |||
e4782b19a3 | |||
ec86b1dffa | |||
6cb8266c7b | |||
9c50302a39 | |||
3313c69898 | |||
530c6ca7ec | |||
07ed2fb523 | |||
d9ec380a15 | |||
b60eb3a899 | |||
b4df69791b | |||
c21b8a22b9 | |||
475a76e656 | |||
7ba5d5ef86 | |||
737dc1ddde | |||
164bf19b36 | |||
25976771d9 | |||
f2198c2e9a | |||
eec19c6d2c | |||
30e03feb5f | |||
58cd3bde9f | |||
662bfb7b88 | |||
5f3e3a17d3 | |||
feba2d9975 | |||
e3e3a1c457 | |||
90628f3c8d | |||
f6bcadb79d | |||
d4ac16773c | |||
96f044d2bf | |||
f31868b913 | |||
73b0ff5b55 | |||
64cf69045a | |||
e57dae0f31 | |||
6386e7d5cf | |||
4bad103da9 | |||
30a26adb7c | |||
8be4adfc0a | |||
fed4cc3965 | |||
7d1e074683 | |||
00516e50a1 | |||
e83d76fbd9 | |||
304f152315 | |||
3a82ebf7fd | |||
0253d34467 | |||
9209f9acde | |||
3dbbb398df | |||
17e8ad110f | |||
5e91d31ed3 | |||
fad9d20820 | |||
fe9a1c8580 | |||
cd6d7d5198 | |||
771478bc68 | |||
c4a59896f8 | |||
3eb1608403 | |||
8fde70d4dc | |||
5a047833ed | |||
f6c28e6be1 | |||
0ebf10d19d | |||
d3005d3ef3 | |||
effcef2184 | |||
89fc0ad7a9 | |||
410272ee1d | |||
1c97bf50b6 | |||
4ecd2c9d0b | |||
e592243a09 | |||
2f4a92e352 | |||
ceafc29040 | |||
b20efabfd2 | |||
85b6e7293c | |||
6aced927ad | |||
75997e6c08 | |||
9040d00110 | |||
8ebc5c6b07 | |||
d4807790ff | |||
0de5e7a285 | |||
c40000aeda | |||
31198bc105 | |||
92599acfca | |||
f6e70779fe | |||
3017bde686 | |||
9d84ec4bb3 | |||
586141adb2 | |||
3f763f99e2 | |||
15c7f36ea3 | |||
04d1a083fa | |||
327ee1dae8 | |||
22885c3e64 | |||
94ededb54c | |||
af6a07697a | |||
5f1d8c95eb | |||
7d9e032407 | |||
bc918a5ad5 | |||
ee54ce4727 | |||
e85bf2f2d5 | |||
a7460ffbd1 | |||
7fe1fd2f95 | |||
d30670e92e | |||
9b202c6e1e | |||
87946eafd5 | |||
7575d3c726 | |||
8b9713a934 | |||
ec713c18c4 | |||
c24b0a1a3f | |||
34e0cb0092 | |||
7b7c7cba21 | |||
c45343dd30 | |||
b7f6603c1f | |||
2d3b052dea | |||
dcb6234771 | |||
e44d423e83 | |||
5435bb734c | |||
13f59adf61 | |||
0fce3368d3 | |||
1ee5c81267 | |||
3bb9d5eb50 | |||
efb23f7cf9 | |||
013f4674de | |||
6966b25d9c | |||
d513f56c8c | |||
7aa05618a3 | |||
cdfbbe5e60 | |||
fe7d1cb81c | |||
c2a9395a4b | |||
586279bcfc | |||
8bd10e7c4c | |||
928e6165bc | |||
77c9e801aa | |||
c78132417f | |||
849928887e | |||
ba1163d49f | |||
6f9c89af39 | |||
246b8b1242 | |||
f0db68cb75 | |||
f0d1fdfb46 | |||
3b8b2e030a | |||
b4fee677a5 | |||
fe706583f9 | |||
d0e0c17ece | |||
5aaa38bcaf | |||
6ff9b27f8e | |||
3f4e035506 | |||
57d9fbb927 | |||
ee44e51b30 | |||
5011f24123 | |||
d1eda334f3 | |||
2ae5ce9f2c | |||
4f5ac78b7e | |||
074c9af020 | |||
2da2d4e365 | |||
8eb76ab2a5 | |||
a710d95243 | |||
a06535d7ed | |||
f511ac9be7 | |||
e28ad2177e | |||
cb16fe84cd | |||
ec3569aa39 | |||
246edecf53 | |||
34834c5af9 | |||
b845245614 | |||
5711fb9969 | |||
d1eaecde9a | |||
00c8505d1e | |||
33f01efe69 | |||
377d312c81 | |||
badf5d5412 | |||
0339f90b40 | |||
5455e8e6a9 | |||
6843b71a0d | |||
634408b5e8 | |||
d053f78b74 | |||
93b6fceb2f | |||
ac7860c35d | |||
b0eab8729f | |||
cb81f80b31 | |||
ea97529185 | |||
f1075191fe | |||
74c479fbc9 | |||
7e788d3a17 | |||
69b3c75f0d | |||
b2c2fa40a2 | |||
50458d9524 | |||
9679e3e356 | |||
6db9f92b8a | |||
4a44498d45 | |||
216510c573 | |||
fd338c3097 | |||
b66ebf5dec | |||
5da99de579 | |||
3aa2907bd6 | |||
05d1618659 | |||
86113811f2 | |||
53ecaa03f1 | |||
205c1aa505 | |||
9b54c1542b | |||
93d5d1b2ad | |||
4c0f3ed6f3 | |||
2580155bf2 | |||
6ab0dd4df9 | |||
4b8c36b6b9 | |||
359a8397c0 | |||
c9fd5d74b5 | |||
391744af97 | |||
587ab29e09 | |||
80f07dadc5 | |||
60609a44ba | |||
30c8fa46b4 | |||
7aab7d2f82 | |||
a8e1c44663 | |||
a2b92c35e1 | |||
9f2086c772 | |||
3eb005d492 | |||
68955bfcf4 | |||
9ac7070e08 | |||
e44e81bd17 | |||
f5eedd2d19 | |||
46059a37eb | |||
adc655a3a2 | |||
3058f80489 | |||
df98cae4b6 | |||
d327e0aabd | |||
17d3a6763c | |||
02c5b0343b | |||
2888e45fea | |||
f1311075d9 | |||
6c380e04a3 | |||
cef1c208a5 | |||
ef8eac92e3 | |||
9c9c63572b | |||
6c0c6de1d0 | |||
b57aecc24c | |||
290dde60a0 | |||
38623785f9 | |||
256ecc7208 | |||
76b06b47ba | |||
cf15cf587f | |||
134c7add57 | |||
ac0791826a | |||
d2622b7798 | |||
f82cbf3a27 | |||
aa7e3df8d6 | |||
ad00d7bd9c | |||
8d1f82c34d | |||
0cb2036e3a | |||
2b1e90b0a5 | |||
f2ccc133a2 | |||
5e824b39dd | |||
41efcae64b | |||
cf5671d058 | |||
2570bba6b1 | |||
71cb7d5c97 | |||
0df6541d5e | |||
52145caf7e | |||
86a50ae9e1 | |||
c64cfb74f3 | |||
26153d9919 | |||
5af922722f | |||
b70d730b32 | |||
bf4b856e0c | |||
0cf0ae6755 | |||
29061cff39 | |||
b7eec4c89f | |||
a3854c229e | |||
dcde256433 | |||
931bdbd5cd | |||
b7bd59c344 | |||
2dbf9a6017 | |||
fe93bba457 | |||
6e35f54738 | |||
089294a85e | |||
25c0b44641 | |||
58c1589688 | |||
bb53f69016 | |||
75659ca042 | |||
fc00594ea4 | |||
8d26be8b89 | |||
af4e95ae0f | |||
ffb4a7aa78 | |||
dcaeacc507 | |||
4f377e6710 | |||
122db85727 | |||
a598e4aa74 | |||
733b31ebbd | |||
dac9775de0 | |||
46c19a5783 | |||
aaeb5ba52f | |||
9f5a3d6064 | |||
4cdf873f98 | |||
b43ae748c3 | |||
02ddd89653 | |||
bbe6eccefe | |||
6677a7b66a | |||
75c37fcc73 | |||
5be71a8a9d | |||
b9ae7d1ebb | |||
8b02e0f57c | |||
342cc7350a | |||
2335a51ced | |||
868df1824c | |||
83c11f0f9d | |||
1022f1b0c6 | |||
c2c80232e3 | |||
115f4e54b8 | |||
669b1694b8 | |||
2128c58fbe | |||
e12e154877 | |||
73d3c17507 | |||
7f647a93da | |||
ecb3dbbb60 | |||
cc907ba69d | |||
5a45eef1dc | |||
0d980e89bc | |||
ef87832bff | |||
94507d1aca | |||
89924a38ff | |||
7faa2b8698 | |||
65352ce8e7 | |||
f1988ee1e3 | |||
82ac8eb731 | |||
ae47e34fa5 | |||
28e781efc3 | |||
5c3ceb8355 | |||
c9113b381d | |||
75e69eecfa | |||
f3c4acc723 | |||
2a0095e322 | |||
9ad5f3c65b | |||
579de64d49 | |||
d4200a7b1e | |||
84477835dc | |||
504b318ef1 | |||
f154c8c490 | |||
d4959bc157 | |||
87e025fe22 | |||
8049323ca8 | |||
b38c7ea2ff | |||
239b925fb3 | |||
60da7f7aaf | |||
8646ff4927 | |||
59be94a81f | |||
437c485e5c | |||
79a58da6a9 | |||
ae29641a18 | |||
9c3f65bca9 | |||
086365b4c4 | |||
64044da49c | |||
7b5b7feb63 | |||
2e059f8504 | |||
207b6686d1 | |||
abfd7d6951 | |||
7fc166b5ba | |||
021953d59a | |||
bbe89df2ff | |||
a638ec5911 | |||
26272a3600 | |||
8454eb79d0 | |||
796f4b981b | |||
34514d65bc | |||
2786357082 | |||
4badeacd1d | |||
63a0ba6ec8 | |||
9a4ce6d70e | |||
35ee2d0ce1 | |||
b04716d40d | |||
051fa6f1f1 | |||
8dc1b07e75 | |||
bee1e7ebaf | |||
f3f0b9f0c5 | |||
a5cf745e1c | |||
273b800047 | |||
6c1f1c2a7a | |||
9c62f8d81f | |||
82aef7ebe2 | |||
57636d3d5f | |||
dc87effc0a | |||
f0c9823e9f | |||
0b91dd6163 | |||
4955c6f13a | |||
2e7beca9ba | |||
59c1b9983d | |||
f7083e0923 | |||
6d4defdf96 | |||
b826f837f8 | |||
5855e18a4e | |||
3f38c0a245 | |||
cfe8b3fc55 | |||
e9ee020b5f | |||
1bcf3891b4 | |||
5456de63e9 | |||
9026c70952 | |||
99dc4ea4a9 | |||
0aaa500f7c | |||
5f5be83a17 | |||
7e44005a0f | |||
ee3fb985ea | |||
2a268aa528 | |||
cd262cf860 | |||
a1889c32d4 | |||
d42d024d9c | |||
7b88b8d159 | |||
4131071b9a | |||
ef6bd7e3b8 | |||
374bff6550 | |||
0a46bbe4f9 | |||
f4971be236 | |||
421273f862 | |||
2c7f229883 | |||
904eabad2f | |||
8b233f6be4 | |||
08fc821ca9 | |||
81706f2d75 | |||
7b50c3910f | |||
2d635386af | |||
a604dcb4c4 | |||
7736b9cac6 | |||
d2dd005a59 | |||
6e8f99d9b2 | |||
685de30047 | |||
17cc9ab07f | |||
3f10bf44db | |||
27984e469a | |||
a2c05b112e | |||
a578c1a5e3 | |||
500aaed48e | |||
4a94da8a94 | |||
cc447c0fda | |||
0ae69bdcd9 | |||
5ba20a94e8 | |||
f168c377fd | |||
dfb754dd13 | |||
455050e19c | |||
317031f455 | |||
b132ce1944 | |||
8b226652aa | |||
2c7fe3ed8d | |||
3d5f2b3c28 | |||
7a79afe4a6 | |||
1f7387a39b | |||
0fc2bee144 | |||
791ae852a2 | |||
c2fcd876d7 | |||
d239d4a495 | |||
aec05ef602 | |||
e5d46d998b | |||
b2e3299539 | |||
c308a6459f | |||
4eb1bc08a7 | |||
ff5e1c635f | |||
6149c2fcb5 | |||
d7cd80dce5 | |||
6264508f5e | |||
a3869dd4c1 | |||
a3d2831f8c | |||
4cd1fa8c38 | |||
1511dc43d7 | |||
3d82807965 | |||
4180571660 | |||
421d9aa501 | |||
898f4971a2 | |||
7ab3331f01 | |||
b4ca414492 | |||
73abea088a | |||
2376dfc139 | |||
d2f95d5319 | |||
cd96843699 | |||
ca80bc33c6 | |||
19607886f7 | |||
3c11a91f77 | |||
b781fdbd04 | |||
765d901530 | |||
3cedbc493e | |||
0488d0a82f | |||
f0be595e4c | |||
55100854d6 | |||
600a1f8866 | |||
95bf68f3f5 | |||
bcdb058492 | |||
7f46aef624 | |||
e779496dfb | |||
3d77fa5fbc | |||
250830ade9 | |||
7b2eb7ccfc | |||
458c27c6e9 | |||
a49e664e63 | |||
f20380d6b4 | |||
05a5e551d6 | |||
d278b71cb2 | |||
a485c141d5 | |||
8a9f6b9ae3 | |||
7144090528 | |||
ee0015ac38 | |||
8b7f7f1088 | |||
c95c6a75f8 | |||
44bf79e35f | |||
bb654f286c | |||
1acd2aa8cf | |||
18d3659b91 | |||
63a4bafa72 | |||
4eb2e84c9f | |||
73c7fb87e8 | |||
c1496722aa | |||
d9f81b0c8c | |||
d69beaabe1 | |||
b7a0bd6347 | |||
882ea6b672 | |||
736d3eabae | |||
af53197c04 | |||
cf186c5762 | |||
f384a2ce85 | |||
803b76e997 | |||
230d7c3dd6 | |||
4f629dd982 | |||
4fdd891b54 | |||
64a892321a | |||
a80991f2b3 | |||
c9cd81319a | |||
521ae21632 | |||
bcd6606a16 | |||
52ebb88205 | |||
1e91d09be7 | |||
02c573986b | |||
f2de486658 | |||
900b4f2644 | |||
1cfaa9afb6 | |||
801468d70d | |||
0601e05978 | |||
7ce11b5d1c | |||
f2d4799491 | |||
ebc458cd32 | |||
43cd631579 | |||
bc824c1a6c | |||
4223aff840 | |||
f107c6c2ca | |||
7daf14caa7 | |||
ded28c705f | |||
778bec0777 | |||
6967cf7f86 | |||
0ee3ec86bd | |||
e4c47e8417 | |||
98ae80f4ed | |||
876c77d0bc | |||
d44a6f7541 | |||
9040c04d27 | |||
ebbdef0538 | |||
bfbee988d0 | |||
1d4d0272ca | |||
77a76f0783 | |||
d9079de262 | |||
b3d732a1a1 | |||
52f1a02938 | |||
fe51669e85 | |||
670a6c50c9 | |||
86c1aaf7d8 | |||
658e787b60 | |||
40c50aef50 | |||
a24c2bbe73 | |||
bdbe90b891 | |||
3236be7877 | |||
1dca17fdb4 | |||
785e971698 | |||
2bfa20ff85 | |||
474a9af78d | |||
61425eacb8 | |||
4870def1fb | |||
3e73fb9233 | |||
5ad6061c3f | |||
fae019b974 | |||
3bb06d8364 | |||
c9c9afa472 | |||
bd0671e123 | |||
6f3ec8d21f | |||
9a0bf13feb | |||
9ff1a6f0cd | |||
a59f64cae1 | |||
a4ecd09723 | |||
f159dfd15a | |||
9e8ec86fa3 | |||
62bb78f58d | |||
893011c3ba | |||
880cb8e7cc | |||
85f83f2c74 | |||
4751e459cc | |||
138efa6cec | |||
a68e50935e | |||
e8f5fb35ac | |||
6af27669b0 | |||
e162f24119 | |||
dbcc462a48 | |||
2d5313639a | |||
38af0f436d | |||
888c2ffb20 | |||
588593f619 | |||
2cdd515b12 | |||
0aad71d46e | |||
6f9285322d | |||
68c7f992fa | |||
1feff408ff | |||
f752e02487 | |||
c9c7fb0a27 | |||
de680c2a8e | |||
03695ba4c5 | |||
c2e2960bf7 | |||
385d2a580c | |||
7e02652068 | |||
ae29c9b4a0 | |||
078f917e61 | |||
b65f04d500 | |||
6acaffe581 | |||
e47ef42a33 | |||
b950e33d81 | |||
ec8cfc77ad | |||
00a16db9cd | |||
4b9f115586 | |||
c5cc91443e | |||
48d94143e7 | |||
8174a05156 | |||
63cf6363a2 | |||
cc6de605ac | |||
d0151d2b79 | |||
6b45d453b8 | |||
b992a84d67 | |||
cb362e9052 | |||
ccb478c1f6 | |||
6af3680f99 | |||
e6c3c215ab | |||
5c66bbde01 | |||
77dd1bdd4a | |||
6268d540a8 | |||
5918e38747 | |||
3cfb571356 | |||
5eb80f8027 | |||
f6e5f2439d | |||
edf6272374 | |||
7f6a4b0ce3 | |||
3be5f25f2f | |||
1b6cdd5637 | |||
f752e55929 | |||
ebb089b3f1 | |||
ad6303f031 | |||
828b9d6717 | |||
444adcd1ca | |||
69ac305883 | |||
2ff57df2a0 | |||
7077f4cbe2 | |||
266f85f607 | |||
d90ab90145 | |||
48018b3f5b | |||
15584e7062 | |||
d415b17146 | |||
9ed953e8c3 | |||
b60a98bd6e | |||
a15e30d4b3 | |||
d5d133353f | |||
6badc98510 | |||
ea8bfb46ce | |||
58860ed19f | |||
583f652197 | |||
3215dcff78 | |||
38fdd17067 | |||
807ccd15ba | |||
1c923d2f9e | |||
2676b21400 | |||
fd5ef94b5a | |||
02c7eea236 | |||
34d1805b54 | |||
753eaa8266 | |||
0b39c6f98e | |||
55b8d0db4d | |||
3d7969d8a2 | |||
041de8082a | |||
3da1fa4d88 | |||
39df21de30 | |||
8cbb7d7362 | |||
10a0c47210 | |||
89bf3765f3 | |||
8181bc591b | |||
ca877e689c | |||
c6048e2bab | |||
60015aee04 | |||
43e6741071 | |||
b91f6bcbff | |||
64e2f1b949 | |||
13a2f05776 | |||
903374ae9b | |||
d366a07403 | |||
e94921174a | |||
dea5ab2f79 | |||
5e11078f34 | |||
d7670cd4ff | |||
29f3230089 | |||
d003efb522 | |||
97e772e87a | |||
0b33615979 | |||
249cead13e | |||
7c96dea359 | |||
374c9921fd | |||
fb55ab8c33 | |||
13485074ac | |||
4944c965e4 | |||
83c5b3bc38 | |||
7fc42de758 | |||
0a30bd74c1 | |||
9b12a79c8d | |||
0dcde23b05 | |||
8dc15b88eb | |||
d20c952f92 | |||
c2eeeb27fd | |||
180d8b67e4 | |||
9c989c46ee | |||
51633f509d | |||
705228ecc2 | |||
740f6d2258 | |||
3b9ef5ccab | |||
ab74e7f24f | |||
be9a670fb7 | |||
6e43e7a146 | |||
ab2093926a | |||
916b90f415 | |||
2ef3db9fab | |||
6987b6fd58 | |||
078179e9b8 | |||
50ccecdff5 | |||
e838a8c28a | |||
e5f7eeedbf | |||
d1948b5a00 | |||
c07f700c53 | |||
c934a30f66 | |||
310d01d8a2 | |||
f330739bc7 | |||
58626721ad | |||
584c8c07b8 | |||
a93ec03d2c | |||
7bd3a8e004 | |||
912a5f951e | |||
6869089111 | |||
6fd32fe850 | |||
81e2b36d38 | |||
7d811afab1 | |||
39f5aaab8b | |||
5fc81dd6c8 | |||
491a530d90 | |||
c12da50f9b | |||
41e8500fc5 | |||
a7f59ef3c1 | |||
f4466c8c0a | |||
bc6d6b20fa | |||
01326936e6 | |||
c960e8d351 | |||
fc69d31914 | |||
8d425e127b | |||
3cfb07ea38 | |||
76679ffb92 | |||
dc2ec925d7 | |||
81d6ba3ec5 | |||
014bdaa355 | |||
0c60fdd2ce | |||
43d986d14e | |||
123d7c6a37 | |||
5ac7df17f9 | |||
bc0dde696a | |||
c323bd3c87 | |||
5c672adc21 | |||
2f80747dc7 | |||
95749ed0e3 | |||
94eea3abec | |||
fe32159673 | |||
07aa2e1260 | |||
6fec8fad57 | |||
84df487f7d | |||
49708e92d3 | |||
daadae7987 | |||
2b788d06b7 | |||
90cd9bd533 | |||
d63506f98c | |||
17de6876bb | |||
fc540395f9 | |||
da2b4962a9 | |||
3abe305a21 | |||
46e8c09bd8 | |||
e683c34a89 | |||
54e4f75081 | |||
9f256f0929 | |||
ef169a6652 | |||
eaec25f940 | |||
6a87d8975c | |||
b8cf5f9427 | |||
2f1e585446 | |||
f9309b46aa | |||
22f5985f1b | |||
c59c38e50e | |||
232e1bb8a3 | |||
1fbb34620c | |||
89f5b803c9 | |||
55179101cd | |||
132495b1fc | |||
a03d7bf5cd | |||
3bf225e85f | |||
cc2bb290c4 | |||
878ca8c5c5 | |||
4bc41d81ee | |||
f6ca176fc8 | |||
0bec360a31 | |||
04f30710c5 | |||
98c0a2af87 | |||
9db42c1769 | |||
849bced602 | |||
27f29019ef | |||
8642a41f2b | |||
bf902ef5bc | |||
7656b55c22 | |||
7d3d4b9443 | |||
15c093c5e2 | |||
116166f62d | |||
26b19dde75 | |||
c8ddc68f13 | |||
7c9681007c | |||
13206e4976 | |||
2f18302d32 | |||
ddb21d151d | |||
c64a9fb456 | |||
ee19b4f86e | |||
14239e584f | |||
112aecf6eb | |||
c1783d77d7 | |||
f089abb3c5 | |||
8e551f5e32 | |||
290960c3b5 | |||
62af09adbe | |||
e39c0b34e5 | |||
8ad90807ee | |||
533b3170a7 | |||
7732f3f5fb | |||
f52f02a434 | |||
4d7d4d673e | |||
9a437f0d38 | |||
c385f8bb6e | |||
fa44be2a9d | |||
117ab0c141 | |||
7488d19ae6 | |||
60524ad5f2 | |||
fad7ff8bf0 | |||
383d445ba1 | |||
803dcb0800 | |||
fde320e2f2 | |||
8ea97141ea | |||
9f232bac58 | |||
8295cc11c0 | |||
70f80adb9a | |||
9a7cac1e07 | |||
c584a25ec9 | |||
bff32bf7bc | |||
d0e7450389 | |||
4da89ac8a9 | |||
f7032f7d9a | |||
7c7e3931a0 | |||
6be3d62d89 | |||
6f509a8a1e | |||
4379fabf16 | |||
6b66e1a077 | |||
c11a3e0fdc | |||
3418033c55 | |||
caa9a846ed | |||
8ee76bcea0 | |||
47325cbe01 | |||
e0c8417297 | |||
9238ee9572 | |||
64af37e0cd | |||
9f9b79f30b | |||
265f41887f | |||
4f09e5d04c | |||
434f321336 | |||
f4e0d1be58 | |||
e5bae0604b | |||
e7da083c31 | |||
367c32dabe | |||
e054238af6 | |||
e8faf6d59a | |||
baa4ea3cd8 | |||
75ef0f0329 | |||
65185c0011 | |||
eb94613d7d | |||
67f4f4fb49 | |||
a7ecf4ac4c | |||
45765b625a | |||
aa0a184ebe | |||
069f9f0d5d | |||
c82b520ea8 | |||
9d6e5bde4a | |||
0eb3669fbf | |||
30449b6054 | |||
f5f71a19b8 | |||
0135971769 | |||
8579795c40 | |||
9d77fd7eec | |||
8c40d1bd72 | |||
7a0bc7d888 | |||
1e07014f86 | |||
49281b24e5 | |||
a8b1980de4 | |||
b8cd5f0482 | |||
cc9f0788aa | |||
209910299d | |||
17926ff5d9 | |||
957fb0667c | |||
8d17aed785 | |||
7ef8d5ddde | |||
9930a2e167 | |||
a86be9ebf2 | |||
ad6665c8b6 | |||
923162ae9d | |||
dd2bd67049 | |||
d500bbff04 | |||
e759bd1a99 | |||
94daf4cea4 | |||
2379792e0a | |||
dba6d7a8a6 | |||
086c206b76 | |||
5dd567deef | |||
b6d8f737ca | |||
491ba9da84 | |||
a420a9293f | |||
c1bc5f6a07 | |||
9834c251d0 | |||
54340ed4c6 | |||
96a0a9202c | |||
a4c081d3a1 | |||
d1b6206858 | |||
0eb6849fe3 | |||
b725fdb093 | |||
1436bb1ff2 | |||
5a44c36b1f | |||
5d990502cb | |||
64735da716 | |||
95b82aa6dc | |||
f09952f3d7 | |||
b98e04dc56 | |||
cb436250da | |||
4376032e3a | |||
c231331e05 | |||
624c151ca2 | |||
5d0356f74b | |||
b019416518 | |||
4fcd9e3bd6 | |||
66bf889c39 | |||
a2811842c8 | |||
1929601425 | |||
282afee47e | |||
e701ccc949 | |||
6543497c17 | |||
7d9af5a937 | |||
720c54a5bb | |||
5dca3c41f2 | |||
929546f60b | |||
cb0ce9986c | |||
064eba00fd | |||
a4336a39d6 | |||
298989c4b9 | |||
48c28c2267 | |||
d76ecbc9c9 | |||
79fb9c00aa | |||
c9e03f37ce | |||
aa5f1699a7 | |||
e1e9126d03 | |||
672a4b3723 | |||
955f76baab | |||
7da8a5e2d1 | |||
ff82fbf112 | |||
8503a0a58f | |||
b1e9512f44 | |||
608def9c78 | |||
bcb21bc1d8 | |||
f63096620a | |||
9b26892bae | |||
572475ce14 | |||
876d7995e1 | |||
b8655e30d4 | |||
7cf0d55546 | |||
ce60b960c0 | |||
cebcb5b92d | |||
11a0f96f5e | |||
74ebaf1744 | |||
f7496ea6d1 | |||
bebba7dc1f | |||
afb2bf442c | |||
c7de48c982 | |||
f906112c03 | |||
8ef864fb39 | |||
1c9b5ab53c | |||
c10faae3b5 | |||
2104dd5a0a | |||
fbe64037db | |||
d8c50b150c | |||
8871bb2d8e | |||
a148454376 | |||
be518b569b | |||
c998fbe2ae | |||
9f12cd0c09 | |||
0d0fee1ca1 | |||
a0410c4677 | |||
8fe464cfa3 | |||
3e2d6d9e8b | |||
32d677787b | |||
dfd1c4eab3 | |||
36bb1f989d | |||
684f4c59e0 | |||
1b77e8a69a | |||
662e10c3e0 | |||
c935fdb12f | |||
9e16937914 | |||
f705202381 | |||
f5532ad9f7 | |||
570e71f050 | |||
c9cc4b4369 | |||
7111aa3b18 | |||
12eba4bcc7 | |||
4610de8fdd | |||
3fcc2dd944 | |||
8299bae2d4 | |||
604ccf7552 | |||
f3dd47948a | |||
c3bb207488 | |||
9009d1bfb3 | |||
fa4d9e8bcb | |||
34b77efc87 | |||
5ca0ccbcd2 | |||
6aa4e52480 | |||
f98e9a2ad7 | |||
c6134cc25b | |||
0443b39264 | |||
8b0b8efbcb | |||
97449cee43 | |||
ab5252c750 | |||
05a27cb34d | |||
b02eab57d2 | |||
b8d52cc3e4 | |||
7d9bab9508 | |||
944181a30e | |||
d8dd50505a | |||
d78082f5e4 | |||
08e501e57b | |||
29a607427d | |||
afb830c91f | |||
c1326ac3d5 | |||
513a1adf57 | |||
7871b38c80 | |||
b34d2d7dee | |||
d7dfa8c22d | |||
8df274f0af | |||
07c4ebb7f2 | |||
49605b257d | |||
fa4e232d73 | |||
bd84cf6586 | |||
6e37f70d55 | |||
d97112d7f0 |
31
.buildkite/env/README.md
vendored
Normal file
31
.buildkite/env/README.md
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
[ejson](https://github.com/Shopify/ejson) and
|
||||
[ejson2env](https://github.com/Shopify/ejson2env) are used to manage access
|
||||
tokens and other secrets required for CI.
|
||||
|
||||
#### Setup
|
||||
```bash
|
||||
$ sudo gem install ejson ejson2env
|
||||
```
|
||||
|
||||
then obtain the necessary keypair and place it in `/opt/ejson/keys/`.
|
||||
|
||||
#### Usage
|
||||
Run the following command to decrypt the secrets into the environment:
|
||||
```bash
|
||||
eval $(ejson2env secrets.ejson)
|
||||
```
|
||||
|
||||
#### Managing secrets.ejson
|
||||
To decrypt `secrets.ejson` for modification, run:
|
||||
```bash
|
||||
$ ejson decrypt secrets.ejson -o secrets_unencrypted.ejson
|
||||
```
|
||||
|
||||
Edit, then run the following to re-encrypt the file **BEFORE COMMITING YOUR
|
||||
CHANGES**:
|
||||
```bash
|
||||
$ ejson encrypt secrets_unencrypted.ejson
|
||||
$ mv secrets_unencrypted.ejson secrets.ejson
|
||||
```
|
||||
|
10
.buildkite/env/secrets.ejson
vendored
Normal file
10
.buildkite/env/secrets.ejson
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f",
|
||||
"environment": {
|
||||
"CODECOV_TOKEN": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:EzVa4Gpj2Qn5OhZQlVfGFchuROgupvnW:CbWc6sNh1GCrAbrncxDjW00zUAD/Sa+ccg7CFSz8Ua6LnCYnSddTBxJWcJEbEs0MrjuZRQ==]",
|
||||
"CRATES_IO_TOKEN": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:qF7QrUM8j+19mptcE1YS71CqmrCM13Ah:TZCatJeT1egCHiufE6cGFC1VsdJkKaaqV6QKWkEsMPBKvOAdaZbbVz9Kl+lGnIsF]",
|
||||
"INFLUX_DATABASE": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:PetD/4c/EbkQmFEcK21g3cBBAPwFqHEw:wvYmDZRajy2WngVFs9AlwyHk]",
|
||||
"INFLUX_USERNAME": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:WcnqZdmDFtJJ01Zu5LbeGgbYGfRzBdFc:a7c5zDDtCOu5L1Qd2NKkxT6kljyBcbck]",
|
||||
"INFLUX_PASSWORD": "EJ[1:Kqnm+k1Z4p8nr7GqMczXnzh6azTk39tj3bAbCKPitUc=:LIZgP9Tp9yE9OlpV8iogmLOI7iW7SiU3:x0nYdT1A6sxu+O+MMLIN19d2t6rrK1qJ3+HnoWG3PDodsXjz06YJWQKU/mx6saqH+QbGtGV5mk0=]"
|
||||
}
|
||||
}
|
33
.buildkite/hooks/post-checkout
Normal file
33
.buildkite/hooks/post-checkout
Normal file
@ -0,0 +1,33 @@
|
||||
CI_BUILD_START=$(date +%s)
|
||||
export CI_BUILD_START
|
||||
|
||||
#
|
||||
# Kill any running docker containers, which are potentially left over from the
|
||||
# previous CI job
|
||||
#
|
||||
(
|
||||
containers=$(docker ps -q)
|
||||
if [[ $(hostname) != metrics-solana-com && -n $containers ]]; then
|
||||
echo "+++ Killing stale docker containers"
|
||||
docker ps
|
||||
|
||||
# shellcheck disable=SC2086 # Don't want to double quote $containers
|
||||
docker kill $containers
|
||||
fi
|
||||
)
|
||||
|
||||
# Processes from previously aborted CI jobs seem to loiter, unclear why as one
|
||||
# would expect the buildkite-agent to clean up all child processes of the
|
||||
# aborted CI job.
|
||||
# But as a workaround for now manually kill some known loiterers. These
|
||||
# processes will all have the `init` process as their PPID:
|
||||
(
|
||||
victims=
|
||||
for name in bash cargo docker solana; do
|
||||
victims="$victims $(pgrep -u "$(id -u)" -P 1 -d \ $name)"
|
||||
done
|
||||
for victim in $victims; do
|
||||
echo "Killing pid $victim"
|
||||
kill -9 "$victim" || true
|
||||
done
|
||||
)
|
1
.buildkite/hooks/post-checkout.sh
Symbolic link
1
.buildkite/hooks/post-checkout.sh
Symbolic link
@ -0,0 +1 @@
|
||||
post-checkout
|
44
.buildkite/hooks/post-command
Normal file
44
.buildkite/hooks/post-command
Normal file
@ -0,0 +1,44 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
#
|
||||
# Save target/ for the next CI build on this machine
|
||||
#
|
||||
(
|
||||
set -x
|
||||
d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
|
||||
mkdir -p "$d"
|
||||
set -x
|
||||
rsync -a --delete --link-dest="$PWD" target "$d"
|
||||
du -hs "$d"
|
||||
)
|
||||
|
||||
#
|
||||
# Add job_stats data point
|
||||
#
|
||||
if [[ -z $CI_BUILD_START ]]; then
|
||||
echo Error: CI_BUILD_START empty
|
||||
else
|
||||
CI_BUILD_DURATION=$(( $(date +%s) - CI_BUILD_START + 1 ))
|
||||
|
||||
CI_LABEL=${BUILDKITE_LABEL:-build label missing}
|
||||
|
||||
PR=false
|
||||
if [[ $BUILDKITE_BRANCH =~ pull/* ]]; then
|
||||
PR=true
|
||||
fi
|
||||
|
||||
SUCCESS=true
|
||||
if [[ $BUILDKITE_COMMAND_EXIT_STATUS != 0 ]]; then
|
||||
SUCCESS=false
|
||||
fi
|
||||
|
||||
point_tags="pipeline=$BUILDKITE_PIPELINE_SLUG,job=$CI_LABEL,pr=$PR,success=$SUCCESS"
|
||||
point_tags="${point_tags// /\\ }" # Escape spaces
|
||||
|
||||
point_fields="duration=$CI_BUILD_DURATION"
|
||||
point_fields="${point_fields// /\\ }" # Escape spaces
|
||||
|
||||
point="job_stats,$point_tags $point_fields"
|
||||
|
||||
scripts/metrics-write-datapoint.sh "$point" || true
|
||||
fi
|
1
.buildkite/hooks/post-command.sh
Symbolic link
1
.buildkite/hooks/post-command.sh
Symbolic link
@ -0,0 +1 @@
|
||||
post-command
|
29
.buildkite/hooks/pre-command
Normal file
29
.buildkite/hooks/pre-command
Normal file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
eval "$(ejson2env .buildkite/env/secrets.ejson)"
|
||||
|
||||
# Ensure the pattern "+++ ..." never occurs when |set -x| is set, as buildkite
|
||||
# interprets this as the start of a log group.
|
||||
# Ref: https://buildkite.com/docs/pipelines/managing-log-output
|
||||
export PS4="++"
|
||||
|
||||
#
|
||||
# Restore target/ from the previous CI build on this machine
|
||||
#
|
||||
(
|
||||
set -x
|
||||
d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
|
||||
|
||||
if [[ -d $d ]]; then
|
||||
du -hs "$d"
|
||||
read -r cacheSizeInGB _ < <(du -s --block-size=1000000000 "$d")
|
||||
if [[ $cacheSizeInGB -gt 10 ]]; then
|
||||
echo "$d has gotten too large, removing it"
|
||||
rm -rf "$d"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$d"/target
|
||||
rsync -a --delete --link-dest="$d" "$d"/target .
|
||||
)
|
1
.buildkite/hooks/pre-command.sh
Symbolic link
1
.buildkite/hooks/pre-command.sh
Symbolic link
@ -0,0 +1 @@
|
||||
pre-command
|
20
.buildkite/pipeline-upload.sh
Executable file
20
.buildkite/pipeline-upload.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This script is used to upload the full buildkite pipeline. The steps defined
|
||||
# in the buildkite UI should simply be:
|
||||
#
|
||||
# steps:
|
||||
# - command: ".buildkite/pipeline-upload.sh"
|
||||
#
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
buildkite-agent pipeline upload ci/buildkite.yml
|
||||
|
||||
if [[ $BUILDKITE_BRANCH =~ ^pull ]]; then
|
||||
# Add helpful link back to the corresponding Github Pull Request
|
||||
buildkite-agent annotate --style info --context pr-backlink \
|
||||
"Github Pull Request: https://github.com/solana-labs/solana/$BUILDKITE_BRANCH"
|
||||
fi
|
||||
|
1
.clippy.toml
Normal file
1
.clippy.toml
Normal file
@ -0,0 +1 @@
|
||||
too-many-arguments-threshold = 9
|
14
.codecov.yml
14
.codecov.yml
@ -1,2 +1,12 @@
|
||||
ignore:
|
||||
- "src/bin"
|
||||
coverage:
|
||||
range: 50..100
|
||||
round: down
|
||||
precision: 1
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
|
||||
comment:
|
||||
layout: "diff"
|
||||
behavior: default
|
||||
require_changes: no
|
||||
|
6
.github/ISSUE_TEMPLATE.md
vendored
Normal file
6
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#### Problem
|
||||
|
||||
|
||||
|
||||
#### Proposed Solution
|
||||
|
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
#### Problem
|
||||
|
||||
#### Summary of Changes
|
||||
|
||||
Fixes #
|
28
.github/RELEASE_TEMPLATE.md
vendored
Normal file
28
.github/RELEASE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
# Release v0.X.Y <milestone name>
|
||||
|
||||
fun blurb about the name, what's in the release
|
||||
|
||||
## Major Features And Improvements
|
||||
|
||||
* bulleted
|
||||
* list of features and improvements
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
* bulleted
|
||||
* list
|
||||
* of
|
||||
* protocol changes/breaks
|
||||
* API breaks
|
||||
* CLI changes
|
||||
* etc.
|
||||
|
||||
## Bug Fixes and Other Changes
|
||||
|
||||
* can be pulled from commit log, or synthesized
|
||||
|
||||
## Thanks to our Contributors
|
||||
|
||||
This release contains contributions from many people at Solana, as well as:
|
||||
|
||||
pull from commit log
|
23
.gitignore
vendored
23
.gitignore
vendored
@ -1,4 +1,23 @@
|
||||
|
||||
/target/
|
||||
/ledger-tool/target/
|
||||
/wallet/target/
|
||||
/core/target/
|
||||
/book/html/
|
||||
/book/src/img/
|
||||
/book/src/tests.ok
|
||||
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
.cargo
|
||||
|
||||
# node config that is rsynced
|
||||
/config/
|
||||
# node config that remains local
|
||||
/config-local/
|
||||
|
||||
# log files
|
||||
*.log
|
||||
log-*.txt
|
||||
|
||||
# intellij files
|
||||
/.idea/
|
||||
/solana.iml
|
||||
|
22
.travis.yml
22
.travis.yml
@ -1,22 +0,0 @@
|
||||
language: rust
|
||||
required: sudo
|
||||
services:
|
||||
- docker
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
include:
|
||||
- rust: stable
|
||||
- rust: nightly
|
||||
env:
|
||||
- FEATURES='asm,unstable'
|
||||
before_script: |
|
||||
export PATH="$PATH:$HOME/.cargo/bin"
|
||||
rustup component add rustfmt-preview
|
||||
script:
|
||||
- cargo fmt -- --write-mode=diff
|
||||
- cargo build --verbose --features "$FEATURES"
|
||||
- cargo test --verbose --features "$FEATURES"
|
||||
after_success: |
|
||||
docker run -it --rm --security-opt seccomp=unconfined --volume "$PWD:/volume" elmtai/docker-rust-kcov
|
||||
bash <(curl -s https://codecov.io/bash) -s target/cov
|
112
CONTRIBUTING.md
Normal file
112
CONTRIBUTING.md
Normal file
@ -0,0 +1,112 @@
|
||||
Solana Coding Guidelines
|
||||
===
|
||||
|
||||
The goal of these guidelines is to improve developer productivity by allowing developers to
|
||||
jump any file in the codebase and not need to adapt to inconsistencies in how the code is
|
||||
written. The codebase should appear as if it had been authored by a single developer. If you
|
||||
don't agree with a convention, submit a PR patching this document and let's discuss! Once
|
||||
the PR is accepted, *all* code should be updated as soon as possible to reflect the new
|
||||
conventions.
|
||||
|
||||
Pull Requests
|
||||
---
|
||||
|
||||
Small, frequent PRs are much preferred to large, infrequent ones. A large PR is difficult
|
||||
to review, can block others from making progress, and can quickly get its author into
|
||||
"rebase hell". A large PR oftentimes arises when one change requires another, which requires
|
||||
another, and then another. When you notice those dependencies, put the fix into a commit of
|
||||
its own, then checkout a new branch, and cherrypick it. Open a PR to start the review
|
||||
process and then jump back to your original branch to keep making progress. Once the commit
|
||||
is merged, you can use git-rebase to purge it from your original branch.
|
||||
|
||||
```bash
|
||||
$ git pull --rebase upstream master
|
||||
```
|
||||
|
||||
### How big is too big?
|
||||
|
||||
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 maintainer to review.
|
||||
|
||||
### Should I send small PRs as I develop large, new components?
|
||||
|
||||
Add only code to the codebase that is ready to be deployed. If you are building a large
|
||||
library, consider developing it in a separate git repository. When it is ready to be
|
||||
integrated, the Solana maintainers will work with you to decide on a path forward. Smaller
|
||||
libraries may be copied in whereas very large ones may be pulled in with a package manager.
|
||||
|
||||
### When will my PR be reviewed?
|
||||
|
||||
PRs are typically reviewed and merged in under 7 days. If your PR has been open for longer,
|
||||
it's a strong indicator that the reviewers aren't confident the change meets the quality
|
||||
standards of the codebase. You might consider closing it and coming back with smaller PRs
|
||||
and longer descriptions detailing what problem it solves and how it solves it.
|
||||
|
||||
Draft Pull Requests
|
||||
---
|
||||
|
||||
If you want early feedback on your PR, use GitHub's "Draft Pull Request" mechanism. Draft
|
||||
PRs are a convenient way to collaborate with the Solana maintainers without triggering
|
||||
notifications as you make changes. When you feel your PR is ready for a broader audience,
|
||||
you can transition your draft PR to a standard PR with the click of a button.
|
||||
|
||||
Rust coding conventions
|
||||
---
|
||||
|
||||
* All Rust code is formatted using the latest version of `rustfmt`. Once installed, it will be
|
||||
updated automatically when you update the compiler with `rustup`.
|
||||
|
||||
* All Rust code is linted with Clippy. If you'd prefer to ignore its advice, do so explicitly:
|
||||
|
||||
```rust
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
```
|
||||
|
||||
Note: Clippy defaults can be overridden in the top-level file `.clippy.toml`.
|
||||
|
||||
* For variable names, when in doubt, spell it out. The mapping from type names to variable names
|
||||
is to lowercase the type name, putting an underscore before each capital letter. Variable names
|
||||
should *not* be abbreviated unless being used as closure arguments and the brevity improves
|
||||
readability. When a function has multiple instances of the same type, qualify each with a
|
||||
prefix and underscore (i.e. alice_keypair) or a numeric suffix (i.e. tx0).
|
||||
|
||||
* For function and method names, use `<verb>_<subject>`. For unit tests, that verb should
|
||||
always be `test` and for benchmarks the verb should always be `bench`. Avoid namespacing
|
||||
function names with some arbitrary word. Avoid abbreviating words in function names.
|
||||
|
||||
* As they say, "When in Rome, do as the Romans do." A good patch should acknowledge the coding
|
||||
conventions of the code that surrounds it, even in the case where that code has not yet been
|
||||
updated to meet the conventions described here.
|
||||
|
||||
|
||||
Terminology
|
||||
---
|
||||
|
||||
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)
|
||||
|
||||
|
||||
Proposing architectural changes
|
||||
---
|
||||
|
||||
Solana's architecture is described by a book generated from markdown files in
|
||||
the `book/src/` directory, maintained by an *editor* (currently @garious). To
|
||||
change the architecture, you'll need to at least propose a change the content
|
||||
under the [Proposed
|
||||
Changes](https://solana-labs.github.io/book-edge/proposals.html) chapter. Here's
|
||||
the full process:
|
||||
|
||||
1. Propose to a change to the architecture by creating a PR that adds a
|
||||
markdown document to the directory `book/src/` and references it from the
|
||||
[table of contents](book/src/SUMMARY.md). Add the editor and any relevant
|
||||
*maintainers* to the PR review.
|
||||
2. The PR being merged indicates your proposed change was accepted and that the
|
||||
editor and maintainers support your plan of attack.
|
||||
3. Submit PRs that implement the proposal. When the implementation reveals the
|
||||
need for tweaks to the architecture, be sure to update the proposal and have
|
||||
that change reviewed by the same people as in step 1.
|
||||
4. Once the implementation is complete, the editor will then work to integrate
|
||||
the document into the book.
|
3242
Cargo.lock
generated
Normal file
3242
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
104
Cargo.toml
104
Cargo.toml
@ -1,30 +1,90 @@
|
||||
[package]
|
||||
name = "silk"
|
||||
description = "A silky smooth implementation of the Loom architecture"
|
||||
version = "0.2.1"
|
||||
documentation = "https://docs.rs/silk"
|
||||
homepage = "http://loomprotocol.com/"
|
||||
repository = "https://github.com/loomprotocol/silk"
|
||||
authors = [
|
||||
"Anatoly Yakovenko <aeyakovenko@gmail.com>",
|
||||
"Greg Fitzgerald <garious@gmail.com>",
|
||||
]
|
||||
name = "solana-workspace"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "0.12.0"
|
||||
documentation = "https://docs.rs/solana"
|
||||
homepage = "https://solana.com/"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "silk-demo"
|
||||
path = "src/bin/demo.rs"
|
||||
edition = "2018"
|
||||
|
||||
[badges]
|
||||
codecov = { repository = "loomprotocol/silk", branch = "master", service = "github" }
|
||||
codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
|
||||
|
||||
[features]
|
||||
unstable = []
|
||||
asm = ["sha2-asm"]
|
||||
chacha = ["solana/chacha"]
|
||||
cuda = ["solana/cuda"]
|
||||
erasure = ["solana/erasure"]
|
||||
|
||||
[dependencies]
|
||||
[dev-dependencies]
|
||||
bincode = "1.1.2"
|
||||
bs58 = "0.2.0"
|
||||
hashbrown = "0.1.8"
|
||||
log = "0.4.2"
|
||||
rand = "0.6.5"
|
||||
rayon = "1.0.0"
|
||||
itertools = "0.7.6"
|
||||
sha2 = "0.7.0"
|
||||
sha2-asm = {version="0.3", optional=true}
|
||||
digest = "0.7.2"
|
||||
reqwest = "0.9.11"
|
||||
serde_json = "1.0.39"
|
||||
solana = { path = "core", version = "0.12.0" }
|
||||
solana-logger = { path = "logger", version = "0.12.0" }
|
||||
solana-netutil = { path = "netutil", version = "0.12.0" }
|
||||
solana-runtime = { path = "runtime", version = "0.12.0" }
|
||||
solana-sdk = { path = "sdk", version = "0.12.0" }
|
||||
sys-info = "0.5.6"
|
||||
|
||||
[[bench]]
|
||||
name = "banking_stage"
|
||||
|
||||
[[bench]]
|
||||
name = "blocktree"
|
||||
|
||||
[[bench]]
|
||||
name = "ledger"
|
||||
|
||||
[[bench]]
|
||||
name = "gen_keys"
|
||||
|
||||
[[bench]]
|
||||
name = "sigverify"
|
||||
|
||||
[[bench]]
|
||||
required-features = ["chacha"]
|
||||
name = "chacha"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"bench-streamer",
|
||||
"bench-tps",
|
||||
"core",
|
||||
"drone",
|
||||
"fullnode",
|
||||
"genesis",
|
||||
"keygen",
|
||||
"ledger-tool",
|
||||
"logger",
|
||||
"metrics",
|
||||
"programs/bpf",
|
||||
"programs/bpf_loader",
|
||||
"programs/budget",
|
||||
"programs/budget_api",
|
||||
"programs/token",
|
||||
"programs/token_api",
|
||||
"programs/failure",
|
||||
"programs/noop",
|
||||
"programs/rewards",
|
||||
"programs/rewards_api",
|
||||
"programs/storage",
|
||||
"programs/storage_api",
|
||||
"programs/system",
|
||||
"programs/vote",
|
||||
"programs/vote_api",
|
||||
"replicator",
|
||||
"sdk",
|
||||
"upload-perf",
|
||||
"vote-signer",
|
||||
"wallet",
|
||||
]
|
||||
exclude = ["programs/bpf/rust/noop"]
|
||||
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright 2018 Anatoly Yakovenko <anatoly@loomprotocol.com> and Greg Fitzgerald <garious@gmail.com>
|
||||
Copyright 2018 Solana Labs, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
188
README.md
188
README.md
@ -1,69 +1,37 @@
|
||||
[](https://crates.io/crates/silk)
|
||||
[](https://docs.rs/silk)
|
||||
[](https://travis-ci.org/loomprotocol/silk)
|
||||
[](https://codecov.io/gh/loomprotocol/silk)
|
||||
[](https://crates.io/crates/solana)
|
||||
[](https://docs.rs/solana)
|
||||
[](https://buildkite.com/solana-labs/solana/builds?branch=master)
|
||||
[](https://codecov.io/gh/solana-labs/solana)
|
||||
|
||||
# Silk, a silky smooth implementation of the Loom specification
|
||||
Blockchain Rebuilt for Scale
|
||||
===
|
||||
|
||||
Loom is a new achitecture for a high performance blockchain. Its whitepaper boasts a theoretical
|
||||
throughput of 710k transactions per second on a 1 gbps network. The specification is implemented
|
||||
in two git repositories. Reserach is performed in the loom repository. That work drives the
|
||||
Loom specification forward. This repository, on the other hand, aims to implement the specification
|
||||
as-is. We care a great deal about quality, clarity and short learning curve. We avoid the use
|
||||
of `unsafe` Rust and write tests for *everything*. Optimizations are only added when
|
||||
corresponding benchmarks are also added that demonstrate real performance boots. We expect the
|
||||
feature set here will always be a ways behind the loom repo, but that this is an implementation
|
||||
you can take to the bank, literally.
|
||||
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.
|
||||
|
||||
# Usage
|
||||
Disclaimer
|
||||
===
|
||||
|
||||
Add the latest [silk package](https://crates.io/crates/silk) to the `[dependencies]` section
|
||||
of your Cargo.toml.
|
||||
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.
|
||||
|
||||
Create a *Historian* and send it *events* to generate an *event log*, where each log *entry*
|
||||
is tagged with the historian's latest *hash*. Then ensure the order of events was not tampered
|
||||
with by verifying each entry's hash can be generated from the hash in the previous entry:
|
||||
Introduction
|
||||
===
|
||||
|
||||
```rust
|
||||
extern crate silk;
|
||||
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!
|
||||
|
||||
use silk::historian::Historian;
|
||||
use silk::log::{verify_slice, Entry, Event, Sha256Hash};
|
||||
use std::{thread, time};
|
||||
use std::sync::mpsc::SendError;
|
||||
> 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)
|
||||
|
||||
fn create_log(hist: &Historian) -> Result<(), SendError<Event>> {
|
||||
hist.sender.send(Event::Tick)?;
|
||||
thread::sleep(time::Duration::new(0, 100_000));
|
||||
hist.sender.send(Event::UserDataKey(0xdeadbeef))?;
|
||||
thread::sleep(time::Duration::new(0, 100_000));
|
||||
hist.sender.send(Event::Tick)?;
|
||||
Ok(())
|
||||
}
|
||||
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.
|
||||
|
||||
fn main() {
|
||||
let seed = Sha256Hash::default();
|
||||
let hist = Historian::new(&seed);
|
||||
create_log(&hist).expect("send error");
|
||||
drop(hist.sender);
|
||||
let entries: Vec<Entry> = hist.receiver.iter().collect();
|
||||
for entry in &entries {
|
||||
println!("{:?}", entry);
|
||||
}
|
||||
assert!(verify_slice(&entries, &seed));
|
||||
}
|
||||
```
|
||||
Architecture
|
||||
===
|
||||
|
||||
Running the program should produce a log similar to:
|
||||
Before you jump into the code, review the online book [Solana: Blockchain Rebuilt for Scale](https://solana-labs.github.io/book/).
|
||||
|
||||
```rust
|
||||
Entry { num_hashes: 0, end_hash: [0, ...], event: Tick }
|
||||
Entry { num_hashes: 6, end_hash: [67, ...], event: UserDataKey(3735928559) }
|
||||
Entry { num_hashes: 5, end_hash: [123, ...], event: Tick }
|
||||
```
|
||||
(The _latest_ development version of the online book is also [available here](https://solana-labs.github.io/book-edge/).)
|
||||
|
||||
|
||||
# Developing
|
||||
Developing
|
||||
===
|
||||
|
||||
Building
|
||||
---
|
||||
@ -76,11 +44,34 @@ $ source $HOME/.cargo/env
|
||||
$ rustup component add rustfmt-preview
|
||||
```
|
||||
|
||||
If your rustc version is lower than 1.31.0, please update it:
|
||||
|
||||
```bash
|
||||
$ 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
|
||||
```
|
||||
|
||||
Download the source code:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/loomprotocol/silk.git
|
||||
$ cd silk
|
||||
$ git clone https://github.com/solana-labs/solana.git
|
||||
$ cd solana
|
||||
```
|
||||
|
||||
Build
|
||||
|
||||
```bash
|
||||
$ cargo build --all
|
||||
```
|
||||
|
||||
Then to run a minimal local cluster
|
||||
```bash
|
||||
$ ./run.sh
|
||||
```
|
||||
|
||||
Testing
|
||||
@ -89,13 +80,62 @@ Testing
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
$ cargo test --all
|
||||
```
|
||||
|
||||
To emulate all the tests that will run on a Pull Request, run:
|
||||
|
||||
```bash
|
||||
$ ./ci/run-local.sh
|
||||
```
|
||||
|
||||
Local Testnet
|
||||
---
|
||||
|
||||
Start your own testnet locally, instructions are in the book [Solana: Blockchain Rebuild for Scale: Getting Started](https://solana-labs.github.io/book/getting-started.html).
|
||||
|
||||
Remote Testnets
|
||||
---
|
||||
|
||||
We maintain several testnets:
|
||||
|
||||
* `testnet` - public stable testnet accessible via testnet.solana.com, with an https proxy for web apps at api.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
|
||||
* `testnet-perf` - permissioned stable testnet running a 24/7 soak test
|
||||
* `testnet-beta-perf` - permissioned beta channel testnet running a multi-hour soak test weekday mornings
|
||||
* `testnet-edge-perf` - permissioned edge channel testnet running a multi-hour soak test weekday mornings
|
||||
|
||||
## Deploy process
|
||||
|
||||
They are deployed with the `ci/testnet-manager.sh` script through a list of [scheduled
|
||||
buildkite jobs](https://buildkite.com/solana-labs/testnet-management/settings/schedules).
|
||||
Each testnet can be manually manipulated from buildkite as well.
|
||||
|
||||
## How do I reset the testnet?
|
||||
Manually trigger the [testnet-management](https://buildkite.com/solana-labs/testnet-management) pipeline
|
||||
and when prompted select the desired testnet
|
||||
|
||||
## How can I scale the tx generation rate?
|
||||
|
||||
Increase the TX rate by increasing the number of cores on the client machine which is running
|
||||
`bench-tps` or run multiple clients. Decrease by lowering cores or using the rayon env
|
||||
variable `RAYON_NUM_THREADS=<xx>`
|
||||
|
||||
## How can I test a change on the testnet?
|
||||
|
||||
Currently, a merged PR is the only way to test a change on the testnet. But you
|
||||
can run your own testnet using the scripts in the `net/` directory.
|
||||
|
||||
## Adjusting the number of clients or validators on the testnet
|
||||
Edit `ci/testnet-manager.sh`
|
||||
|
||||
|
||||
Benchmarking
|
||||
---
|
||||
|
||||
First install the nightly build of rustc. `cargo bench` requires unstable features:
|
||||
First install the nightly build of rustc. `cargo bench` requires use of the
|
||||
unstable features only available in the nightly build.
|
||||
|
||||
```bash
|
||||
$ rustup install nightly
|
||||
@ -104,5 +144,35 @@ $ rustup install nightly
|
||||
Run the benchmarks:
|
||||
|
||||
```bash
|
||||
$ cargo +nightly bench --features="asm,unstable"
|
||||
$ cargo +nightly bench
|
||||
```
|
||||
|
||||
Release Process
|
||||
---
|
||||
The release process for this project is described [here](RELEASE.md).
|
||||
|
||||
|
||||
Code coverage
|
||||
---
|
||||
|
||||
To generate code coverage statistics:
|
||||
|
||||
```bash
|
||||
$ scripts/coverage.sh
|
||||
$ open target/cov/lcov-local/index.html
|
||||
```
|
||||
|
||||
|
||||
Why coverage? While most see coverage as a code quality metric, we see it primarily as a developer
|
||||
productivity metric. When a developer makes a change to the codebase, presumably it's a *solution* to
|
||||
some problem. Our unit-test suite is how we encode the set of *problems* the codebase solves. Running
|
||||
the test suite should indicate that your change didn't *infringe* on anyone else's solutions. Adding a
|
||||
test *protects* your solution from future changes. Say you don't understand why a line of code exists,
|
||||
try deleting it and running the unit-tests. The nearest test failure should tell you what problem
|
||||
was solved by that code. If no test fails, go ahead and submit a Pull Request that asks, "what
|
||||
problem is solved by this code?" On the other hand, if a test does fail and you can think of a
|
||||
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!
|
||||
|
||||
|
||||
|
105
RELEASE.md
Normal file
105
RELEASE.md
Normal file
@ -0,0 +1,105 @@
|
||||
# Solana Release process
|
||||
|
||||
## Branches and Tags
|
||||
|
||||
```
|
||||
========================= master branch (edge channel) =======================>
|
||||
\ \ \
|
||||
\___v0.7.0 tag \ \
|
||||
\ \ v0.9.0 tag__\
|
||||
\ v0.8.0 tag__\ \
|
||||
v0.7.1 tag__\ \ v0.9 branch (beta channel)
|
||||
\___v0.7.2 tag \___v0.8.1 tag
|
||||
\ \
|
||||
\ \
|
||||
v0.7 branch v0.8 branch (stable channel)
|
||||
|
||||
```
|
||||
|
||||
### master branch
|
||||
All new development occurs on the `master` branch.
|
||||
|
||||
Bug fixes that affect a `vX.Y` branch are first made on `master`. This is to
|
||||
allow a fix some soak time on `master` before it is applied to one or more
|
||||
stabilization branches.
|
||||
|
||||
Merging to `master` first also helps ensure that fixes applied to one release
|
||||
are present for future releases. (Sometimes the joy of landing a critical
|
||||
release blocker in a branch causes you to forget to propagate back to
|
||||
`master`!)"
|
||||
|
||||
Once the bug fix lands on `master` it is cherry-picked into the `vX.Y` branch
|
||||
and potentially the `vX.Y-1` branch. The exception to this rule is when a bug
|
||||
fix for `vX.Y` doesn't apply to `master` or `vX.Y-1`.
|
||||
|
||||
Immediately after a new stabilization branch is forged, the `Cargo.toml` minor
|
||||
version (*Y*) in the `master` branch is incremented by the release engineer.
|
||||
Incrementing the major version of the `master` branch is outside the scope of
|
||||
this document.
|
||||
|
||||
### v*X.Y* stabilization branches
|
||||
These are stabilization branches for a given milestone. They are created off
|
||||
the `master` branch as late as possible prior to the milestone release.
|
||||
|
||||
### v*X.Y.Z* release tag
|
||||
The release tags are created as desired by the owner of the given stabilization
|
||||
branch, and cause that *X.Y.Z* release to be shipped to https://crates.io
|
||||
|
||||
Immediately after a new v*X.Y.Z* branch tag has been created, the `Cargo.toml`
|
||||
patch version number (*Z*) of the stabilization branch is incremented by the
|
||||
release engineer.
|
||||
|
||||
## Channels
|
||||
Channels are used by end-users (humans and bots) to consume the branches
|
||||
described in the previous section, so they may automatically update to the most
|
||||
recent version matching their desired stability.
|
||||
|
||||
There are three release channels that map to branches as follows:
|
||||
* edge - tracks the `master` branch, least stable.
|
||||
* beta - tracks the largest (and latest) `vX.Y` stabilization branch, more stable.
|
||||
* stable - tracks the second largest `vX.Y` stabilization branch, most stable.
|
||||
|
||||
## Release Steps
|
||||
|
||||
### Changing channels
|
||||
|
||||
When cutting a new channel branch these pre-steps are required:
|
||||
|
||||
1. Pick your branch point for release on master.
|
||||
1. Create the branch. The name should be "v" + the first 2 "version" fields
|
||||
from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies
|
||||
the next branch name is "v0.9".
|
||||
1. Push the new branch to the solana repository
|
||||
1. Update Cargo.toml on master to the next semantic version (e.g. 0.9.0 -> 0.10.0)
|
||||
by running `./scripts/increment-cargo-version.sh`, then rebuild with a
|
||||
`cargo build --all` to cause a refresh of `Cargo.lock`.
|
||||
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
||||
master branch
|
||||
|
||||
At this point, ci/channel-info.sh should show your freshly cut release branch as
|
||||
"BETA_CHANNEL" and the previous release branch as "STABLE_CHANNEL".
|
||||
|
||||
### Updating channels (i.e. "making a release")
|
||||
|
||||
We use [github's Releases UI](https://github.com/solana-labs/solana/releases) for tagging a release.
|
||||
|
||||
1. Go [there ;)](https://github.com/solana-labs/solana/releases).
|
||||
1. Click "Draft new release". The release tag must exactly match the `version`
|
||||
field in `/Cargo.toml` prefixed by `v` (ie, `<branchname>.X`).
|
||||
1. If the first major release on the branch (e.g. v0.8.0), paste in [this
|
||||
template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md)
|
||||
and fill it in.
|
||||
1. Test the release by generating a tag using semver's rules. First try at a
|
||||
release should be `<branchname>.X-rc.0`.
|
||||
1. Verify release automation:
|
||||
1. [Crates.io](https://crates.io/crates/solana) should have an updated Solana version.
|
||||
1. ...
|
||||
1. After testnet deployment, verify that testnets are running correct software.
|
||||
http://metrics.solana.com should show testnet running on a hash from your
|
||||
newly created branch.
|
||||
1. Once the release has been made, update Cargo.toml on release to the next
|
||||
semantic version (e.g. 0.9.0 -> 0.9.1) by running
|
||||
`./scripts/increment-cargo-version.sh patch`, then rebuild with a `cargo
|
||||
build --all` to cause a refresh of `Cargo.lock`.
|
||||
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
||||
release branch
|
17
bench-streamer/Cargo.toml
Normal file
17
bench-streamer/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-streamer"
|
||||
version = "0.12.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.32.0"
|
||||
solana = { path = "../core", version = "0.12.0" }
|
||||
solana-logger = { path = "../logger", version = "0.12.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.12.0" }
|
||||
|
||||
[features]
|
||||
cuda = ["solana/cuda"]
|
116
bench-streamer/src/main.rs
Normal file
116
bench-streamer/src/main.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use clap::{App, Arg};
|
||||
use solana::packet::{Packet, SharedPackets, BLOB_SIZE, PACKET_DATA_SIZE};
|
||||
use solana::result::Result;
|
||||
use solana::streamer::{receiver, PacketReceiver};
|
||||
use std::cmp::max;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
||||
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::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
|
||||
fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
|
||||
let send = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
let msgs = SharedPackets::default();
|
||||
let msgs_ = msgs.clone();
|
||||
msgs.write().unwrap().packets.resize(10, Packet::default());
|
||||
for w in &mut msgs.write().unwrap().packets {
|
||||
w.meta.size = PACKET_DATA_SIZE;
|
||||
w.meta.set_addr(&addr);
|
||||
}
|
||||
spawn(move || loop {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let mut num = 0;
|
||||
for p in &msgs_.read().unwrap().packets {
|
||||
let a = p.meta.addr();
|
||||
assert!(p.meta.size < BLOB_SIZE);
|
||||
send.send_to(&p.data[..p.meta.size], &a).unwrap();
|
||||
num += 1;
|
||||
}
|
||||
assert_eq!(num, 10);
|
||||
})
|
||||
}
|
||||
|
||||
fn sink(exit: Arc<AtomicBool>, rvs: Arc<AtomicUsize>, r: PacketReceiver) -> JoinHandle<()> {
|
||||
spawn(move || loop {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let timer = Duration::new(1, 0);
|
||||
if let Ok(msgs) = r.recv_timeout(timer) {
|
||||
rvs.fetch_add(msgs.read().unwrap().packets.len(), Ordering::Relaxed);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut num_sockets = 1usize;
|
||||
|
||||
let matches = App::new("solana-bench-streamer")
|
||||
.arg(
|
||||
Arg::with_name("num-recv-sockets")
|
||||
.long("num-recv-sockets")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Use NUM receive sockets"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if let Some(n) = matches.value_of("num-recv-sockets") {
|
||||
num_sockets = max(num_sockets, n.to_string().parse().expect("integer"));
|
||||
}
|
||||
|
||||
let mut port = 0;
|
||||
let mut addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
|
||||
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut read_channels = Vec::new();
|
||||
let mut read_threads = Vec::new();
|
||||
for _ in 0..num_sockets {
|
||||
let read = solana_netutil::bind_to(port, false).unwrap();
|
||||
read.set_read_timeout(Some(Duration::new(1, 0))).unwrap();
|
||||
|
||||
addr = read.local_addr().unwrap();
|
||||
port = addr.port();
|
||||
|
||||
let (s_reader, r_reader) = channel();
|
||||
read_channels.push(r_reader);
|
||||
read_threads.push(receiver(Arc::new(read), &exit, s_reader, "bench-streamer"));
|
||||
}
|
||||
|
||||
let t_producer1 = producer(&addr, exit.clone());
|
||||
let t_producer2 = producer(&addr, exit.clone());
|
||||
let t_producer3 = producer(&addr, exit.clone());
|
||||
|
||||
let rvs = Arc::new(AtomicUsize::new(0));
|
||||
let sink_threads: Vec<_> = read_channels
|
||||
.into_iter()
|
||||
.map(|r_reader| sink(exit.clone(), rvs.clone(), r_reader))
|
||||
.collect();
|
||||
let start = SystemTime::now();
|
||||
let start_val = rvs.load(Ordering::Relaxed);
|
||||
sleep(Duration::new(5, 0));
|
||||
let elapsed = start.elapsed().unwrap();
|
||||
let end_val = rvs.load(Ordering::Relaxed);
|
||||
let time = elapsed.as_secs() * 10_000_000_000 + u64::from(elapsed.subsec_nanos());
|
||||
let ftime = (time as f64) / 10_000_000_000_f64;
|
||||
let fcount = (end_val - start_val) as f64;
|
||||
println!("performance: {:?}", fcount / ftime);
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
for t_reader in read_threads {
|
||||
t_reader.join()?;
|
||||
}
|
||||
t_producer1.join()?;
|
||||
t_producer2.join()?;
|
||||
t_producer3.join()?;
|
||||
for t_sink in sink_threads {
|
||||
t_sink.join()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
21
bench-tps/Cargo.toml
Normal file
21
bench-tps/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-tps"
|
||||
version = "0.12.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.32.0"
|
||||
rayon = "1.0.3"
|
||||
serde_json = "1.0.39"
|
||||
solana = { path = "../core", version = "0.12.0" }
|
||||
solana-drone = { path = "../drone", version = "0.12.0" }
|
||||
solana-logger = { path = "../logger", version = "0.12.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.12.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.12.0" }
|
||||
|
||||
[features]
|
||||
cuda = ["solana/cuda"]
|
540
bench-tps/src/bench.rs
Normal file
540
bench-tps/src/bench.rs
Normal file
@ -0,0 +1,540 @@
|
||||
use solana_metrics;
|
||||
|
||||
use rayon::prelude::*;
|
||||
use solana::client::mk_client;
|
||||
use solana::contact_info::ContactInfo;
|
||||
use solana::thin_client::ThinClient;
|
||||
use solana_drone::drone::request_airdrop_transaction;
|
||||
use solana_metrics::influxdb;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction::SystemTransaction;
|
||||
use solana_sdk::timing::timestamp;
|
||||
use solana_sdk::timing::{duration_as_ms, duration_as_s};
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use std::cmp;
|
||||
use std::collections::VecDeque;
|
||||
use std::net::SocketAddr;
|
||||
use std::process::exit;
|
||||
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
pub struct NodeStats {
|
||||
/// Maximum TPS reported by this node
|
||||
pub tps: f64,
|
||||
/// Total transactions reported by this node
|
||||
pub tx: u64,
|
||||
}
|
||||
|
||||
pub const MAX_SPENDS_PER_TX: usize = 4;
|
||||
|
||||
pub type SharedTransactions = Arc<RwLock<VecDeque<Vec<(Transaction, u64)>>>>;
|
||||
|
||||
pub fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
||||
println!("Token balance: {}", lamport_balance);
|
||||
solana_metrics::submit(
|
||||
influxdb::Point::new("bench-tps")
|
||||
.add_tag("op", influxdb::Value::String("lamport_balance".to_string()))
|
||||
.add_field("balance", influxdb::Value::Integer(lamport_balance as i64))
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn sample_tx_count(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
maxes: &Arc<RwLock<Vec<(SocketAddr, NodeStats)>>>,
|
||||
first_tx_count: u64,
|
||||
v: &ContactInfo,
|
||||
sample_period: u64,
|
||||
) {
|
||||
let mut client = mk_client(&v);
|
||||
let mut now = Instant::now();
|
||||
let mut initial_tx_count = client.transaction_count();
|
||||
let mut max_tps = 0.0;
|
||||
let mut total;
|
||||
|
||||
let log_prefix = format!("{:21}:", v.tpu.to_string());
|
||||
|
||||
loop {
|
||||
let tx_count = client.transaction_count();
|
||||
assert!(
|
||||
tx_count >= initial_tx_count,
|
||||
"expected tx_count({}) >= initial_tx_count({})",
|
||||
tx_count,
|
||||
initial_tx_count
|
||||
);
|
||||
let duration = now.elapsed();
|
||||
now = Instant::now();
|
||||
let sample = tx_count - initial_tx_count;
|
||||
initial_tx_count = tx_count;
|
||||
|
||||
let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos());
|
||||
let tps = (sample * 1_000_000_000) as f64 / ns as f64;
|
||||
if tps > max_tps {
|
||||
max_tps = tps;
|
||||
}
|
||||
if tx_count > first_tx_count {
|
||||
total = tx_count - first_tx_count;
|
||||
} else {
|
||||
total = 0;
|
||||
}
|
||||
println!(
|
||||
"{} {:9.2} TPS, Transactions: {:6}, Total transactions: {}",
|
||||
log_prefix, tps, sample, total
|
||||
);
|
||||
sleep(Duration::new(sample_period, 0));
|
||||
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
println!("{} Exiting validator thread", log_prefix);
|
||||
let stats = NodeStats {
|
||||
tps: max_tps,
|
||||
tx: total,
|
||||
};
|
||||
maxes.write().unwrap().push((v.tpu, stats));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send loopback payment of 0 lamports and confirm the network processed it
|
||||
pub fn send_barrier_transaction(
|
||||
barrier_client: &mut ThinClient,
|
||||
blockhash: &mut Hash,
|
||||
source_keypair: &Keypair,
|
||||
dest_id: &Pubkey,
|
||||
) {
|
||||
let transfer_start = Instant::now();
|
||||
|
||||
let mut poll_count = 0;
|
||||
loop {
|
||||
if poll_count > 0 && poll_count % 8 == 0 {
|
||||
println!(
|
||||
"polling for barrier transaction confirmation, attempt {}",
|
||||
poll_count
|
||||
);
|
||||
}
|
||||
|
||||
*blockhash = barrier_client.get_recent_blockhash();
|
||||
let signature = barrier_client
|
||||
.transfer(0, &source_keypair, dest_id, blockhash)
|
||||
.expect("Unable to send barrier transaction");
|
||||
|
||||
let confirmatiom = barrier_client.poll_for_signature(&signature);
|
||||
let duration_ms = duration_as_ms(&transfer_start.elapsed());
|
||||
if confirmatiom.is_ok() {
|
||||
println!("barrier transaction confirmed in {} ms", duration_ms);
|
||||
|
||||
solana_metrics::submit(
|
||||
influxdb::Point::new("bench-tps")
|
||||
.add_tag(
|
||||
"op",
|
||||
influxdb::Value::String("send_barrier_transaction".to_string()),
|
||||
)
|
||||
.add_field("poll_count", influxdb::Value::Integer(poll_count))
|
||||
.add_field("duration", influxdb::Value::Integer(duration_ms as i64))
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
// Sanity check that the client balance is still 1
|
||||
let balance = barrier_client
|
||||
.poll_balance_with_timeout(
|
||||
&source_keypair.pubkey(),
|
||||
&Duration::from_millis(100),
|
||||
&Duration::from_secs(10),
|
||||
)
|
||||
.expect("Failed to get balance");
|
||||
if balance != 1 {
|
||||
panic!("Expected an account balance of 1 (balance: {}", balance);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Timeout after 3 minutes. When running a CPU-only leader+validator+drone+bench-tps on a dev
|
||||
// machine, some batches of transactions can take upwards of 1 minute...
|
||||
if duration_ms > 1000 * 60 * 3 {
|
||||
println!("Error: Couldn't confirm barrier transaction!");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let new_blockhash = barrier_client.get_recent_blockhash();
|
||||
if new_blockhash == *blockhash {
|
||||
if poll_count > 0 && poll_count % 8 == 0 {
|
||||
println!("blockhash is not advancing, still at {:?}", *blockhash);
|
||||
}
|
||||
} else {
|
||||
*blockhash = new_blockhash;
|
||||
}
|
||||
|
||||
poll_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_txs(
|
||||
shared_txs: &SharedTransactions,
|
||||
source: &[Keypair],
|
||||
dest: &[Keypair],
|
||||
threads: usize,
|
||||
reclaim: bool,
|
||||
contact_info: &ContactInfo,
|
||||
) {
|
||||
let mut client = mk_client(contact_info);
|
||||
let blockhash = client.get_recent_blockhash();
|
||||
let tx_count = source.len();
|
||||
println!("Signing transactions... {} (reclaim={})", tx_count, reclaim);
|
||||
let signing_start = Instant::now();
|
||||
|
||||
let pairs: Vec<_> = if !reclaim {
|
||||
source.iter().zip(dest.iter()).collect()
|
||||
} else {
|
||||
dest.iter().zip(source.iter()).collect()
|
||||
};
|
||||
let transactions: Vec<_> = pairs
|
||||
.par_iter()
|
||||
.map(|(id, keypair)| {
|
||||
(
|
||||
SystemTransaction::new_account(id, &keypair.pubkey(), 1, blockhash, 0),
|
||||
timestamp(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let duration = signing_start.elapsed();
|
||||
let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos());
|
||||
let bsps = (tx_count) as f64 / ns as f64;
|
||||
let nsps = ns as f64 / (tx_count) as f64;
|
||||
println!(
|
||||
"Done. {:.2} thousand signatures per second, {:.2} us per signature, {} ms total time, {}",
|
||||
bsps * 1_000_000_f64,
|
||||
nsps / 1_000_f64,
|
||||
duration_as_ms(&duration),
|
||||
blockhash,
|
||||
);
|
||||
solana_metrics::submit(
|
||||
influxdb::Point::new("bench-tps")
|
||||
.add_tag("op", influxdb::Value::String("generate_txs".to_string()))
|
||||
.add_field(
|
||||
"duration",
|
||||
influxdb::Value::Integer(duration_as_ms(&duration) as i64),
|
||||
)
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
let sz = transactions.len() / threads;
|
||||
let chunks: Vec<_> = transactions.chunks(sz).collect();
|
||||
{
|
||||
let mut shared_txs_wl = shared_txs.write().unwrap();
|
||||
for chunk in chunks {
|
||||
shared_txs_wl.push_back(chunk.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_tx_transfers(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
shared_txs: &SharedTransactions,
|
||||
contact_info: &ContactInfo,
|
||||
shared_tx_thread_count: &Arc<AtomicIsize>,
|
||||
total_tx_sent_count: &Arc<AtomicUsize>,
|
||||
thread_batch_sleep_ms: usize,
|
||||
) {
|
||||
let client = mk_client(&contact_info);
|
||||
loop {
|
||||
if thread_batch_sleep_ms > 0 {
|
||||
sleep(Duration::from_millis(thread_batch_sleep_ms as u64));
|
||||
}
|
||||
let txs;
|
||||
{
|
||||
let mut shared_txs_wl = shared_txs.write().unwrap();
|
||||
txs = shared_txs_wl.pop_front();
|
||||
}
|
||||
if let Some(txs0) = txs {
|
||||
shared_tx_thread_count.fetch_add(1, Ordering::Relaxed);
|
||||
println!(
|
||||
"Transferring 1 unit {} times... to {}",
|
||||
txs0.len(),
|
||||
contact_info.tpu
|
||||
);
|
||||
let tx_len = txs0.len();
|
||||
let transfer_start = Instant::now();
|
||||
for tx in txs0 {
|
||||
let now = timestamp();
|
||||
if now > tx.1 && now - tx.1 > 1000 * 30 {
|
||||
continue;
|
||||
}
|
||||
client.transfer_signed(&tx.0).unwrap();
|
||||
}
|
||||
shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed);
|
||||
total_tx_sent_count.fetch_add(tx_len, Ordering::Relaxed);
|
||||
println!(
|
||||
"Tx send done. {} ms {} tps",
|
||||
duration_as_ms(&transfer_start.elapsed()),
|
||||
tx_len as f32 / duration_as_s(&transfer_start.elapsed()),
|
||||
);
|
||||
solana_metrics::submit(
|
||||
influxdb::Point::new("bench-tps")
|
||||
.add_tag("op", influxdb::Value::String("do_tx_transfers".to_string()))
|
||||
.add_field(
|
||||
"duration",
|
||||
influxdb::Value::Integer(duration_as_ms(&transfer_start.elapsed()) as i64),
|
||||
)
|
||||
.add_field("count", influxdb::Value::Integer(tx_len as i64))
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_funding_transfer(client: &mut ThinClient, tx: &Transaction, amount: u64) -> bool {
|
||||
for a in &tx.account_keys[1..] {
|
||||
if client.get_balance(a).unwrap_or(0) >= amount {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// fund the dests keys by spending all of the source keys into MAX_SPENDS_PER_TX
|
||||
/// on every iteration. This allows us to replay the transfers because the source is either empty,
|
||||
/// or full
|
||||
pub fn fund_keys(client: &mut ThinClient, source: &Keypair, dests: &[Keypair], lamports: u64) {
|
||||
let total = lamports * dests.len() as u64;
|
||||
let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)];
|
||||
let mut notfunded: Vec<&Keypair> = dests.iter().collect();
|
||||
|
||||
println!("funding keys {}", dests.len());
|
||||
while !notfunded.is_empty() {
|
||||
let mut new_funded: Vec<(&Keypair, u64)> = vec![];
|
||||
let mut to_fund = vec![];
|
||||
println!("creating from... {}", funded.len());
|
||||
for f in &mut funded {
|
||||
let max_units = cmp::min(notfunded.len(), MAX_SPENDS_PER_TX);
|
||||
if max_units == 0 {
|
||||
break;
|
||||
}
|
||||
let start = notfunded.len() - max_units;
|
||||
let per_unit = f.1 / (max_units as u64);
|
||||
let moves: Vec<_> = notfunded[start..]
|
||||
.iter()
|
||||
.map(|k| (k.pubkey(), per_unit))
|
||||
.collect();
|
||||
notfunded[start..]
|
||||
.iter()
|
||||
.for_each(|k| new_funded.push((k, per_unit)));
|
||||
notfunded.truncate(start);
|
||||
if !moves.is_empty() {
|
||||
to_fund.push((f.0, moves));
|
||||
}
|
||||
}
|
||||
|
||||
// try to transfer a "few" at a time with recent blockhash
|
||||
// assume 4MB network buffers, and 512 byte packets
|
||||
const FUND_CHUNK_LEN: usize = 4 * 1024 * 1024 / 512;
|
||||
|
||||
to_fund.chunks(FUND_CHUNK_LEN).for_each(|chunk| {
|
||||
let mut tries = 0;
|
||||
|
||||
// this set of transactions just initializes us for bookkeeping
|
||||
#[allow(clippy::clone_double_ref)] // sigh
|
||||
let mut to_fund_txs: Vec<_> = chunk
|
||||
.par_iter()
|
||||
.map(|(k, m)| {
|
||||
(
|
||||
k.clone(),
|
||||
SystemTransaction::new_move_many(k, &m, Hash::default(), 0),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let amount = chunk[0].1[0].1;
|
||||
|
||||
while !to_fund_txs.is_empty() {
|
||||
let receivers = to_fund_txs
|
||||
.iter()
|
||||
.fold(0, |len, (_, tx)| len + tx.instructions.len());
|
||||
|
||||
println!(
|
||||
"{} {} to {} in {} txs",
|
||||
if tries == 0 {
|
||||
"transferring"
|
||||
} else {
|
||||
" retrying"
|
||||
},
|
||||
amount,
|
||||
receivers,
|
||||
to_fund_txs.len(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_recent_blockhash();
|
||||
|
||||
// re-sign retained to_fund_txes with updated blockhash
|
||||
to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
|
||||
tx.sign(&[*k], blockhash);
|
||||
});
|
||||
|
||||
to_fund_txs.iter().for_each(|(_, tx)| {
|
||||
client.transfer_signed(&tx).expect("transfer");
|
||||
});
|
||||
|
||||
// retry anything that seems to have dropped through cracks
|
||||
// again since these txs are all or nothing, they're fine to
|
||||
// retry
|
||||
to_fund_txs.retain(|(_, tx)| !verify_funding_transfer(client, &tx, amount));
|
||||
|
||||
tries += 1;
|
||||
}
|
||||
println!("transferred");
|
||||
});
|
||||
println!("funded: {} left: {}", new_funded.len(), notfunded.len());
|
||||
funded = new_funded;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn airdrop_lamports(
|
||||
client: &mut ThinClient,
|
||||
drone_addr: &SocketAddr,
|
||||
id: &Keypair,
|
||||
tx_count: u64,
|
||||
) {
|
||||
let starting_balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(starting_balance);
|
||||
println!("starting balance {}", starting_balance);
|
||||
|
||||
if starting_balance < tx_count {
|
||||
let airdrop_amount = tx_count - starting_balance;
|
||||
println!(
|
||||
"Airdropping {:?} lamports from {} for {}",
|
||||
airdrop_amount,
|
||||
drone_addr,
|
||||
id.pubkey(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_recent_blockhash();
|
||||
match request_airdrop_transaction(&drone_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
||||
Ok(transaction) => {
|
||||
let signature = client.transfer_signed(&transaction).unwrap();
|
||||
client.poll_for_signature(&signature).unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
panic!(
|
||||
"Error requesting airdrop: {:?} to addr: {:?} amount: {}",
|
||||
err, drone_addr, airdrop_amount
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let current_balance = client.poll_get_balance(&id.pubkey()).unwrap_or_else(|e| {
|
||||
println!("airdrop error {}", e);
|
||||
starting_balance
|
||||
});
|
||||
println!("current balance {}...", current_balance);
|
||||
|
||||
metrics_submit_lamport_balance(current_balance);
|
||||
if current_balance - starting_balance != airdrop_amount {
|
||||
println!(
|
||||
"Airdrop failed! {} {} {}",
|
||||
id.pubkey(),
|
||||
current_balance,
|
||||
starting_balance
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_and_report_stats(
|
||||
maxes: &Arc<RwLock<Vec<(SocketAddr, NodeStats)>>>,
|
||||
sample_period: u64,
|
||||
tx_send_elapsed: &Duration,
|
||||
total_tx_send_count: usize,
|
||||
) {
|
||||
// Compute/report stats
|
||||
let mut max_of_maxes = 0.0;
|
||||
let mut max_tx_count = 0;
|
||||
let mut nodes_with_zero_tps = 0;
|
||||
let mut total_maxes = 0.0;
|
||||
println!(" Node address | Max TPS | Total Transactions");
|
||||
println!("---------------------+---------------+--------------------");
|
||||
|
||||
for (sock, stats) in maxes.read().unwrap().iter() {
|
||||
let maybe_flag = match stats.tx {
|
||||
0 => "!!!!!",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
println!(
|
||||
"{:20} | {:13.2} | {} {}",
|
||||
(*sock).to_string(),
|
||||
stats.tps,
|
||||
stats.tx,
|
||||
maybe_flag
|
||||
);
|
||||
|
||||
if stats.tps == 0.0 {
|
||||
nodes_with_zero_tps += 1;
|
||||
}
|
||||
total_maxes += stats.tps;
|
||||
|
||||
if stats.tps > max_of_maxes {
|
||||
max_of_maxes = stats.tps;
|
||||
}
|
||||
if stats.tx > max_tx_count {
|
||||
max_tx_count = stats.tx;
|
||||
}
|
||||
}
|
||||
|
||||
if total_maxes > 0.0 {
|
||||
let num_nodes_with_tps = maxes.read().unwrap().len() - nodes_with_zero_tps;
|
||||
let average_max = total_maxes / num_nodes_with_tps as f64;
|
||||
println!(
|
||||
"\nAverage max TPS: {:.2}, {} nodes had 0 TPS",
|
||||
average_max, nodes_with_zero_tps
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"\nHighest TPS: {:.2} sampling period {}s max transactions: {} clients: {} drop rate: {:.2}",
|
||||
max_of_maxes,
|
||||
sample_period,
|
||||
max_tx_count,
|
||||
maxes.read().unwrap().len(),
|
||||
(total_tx_send_count as u64 - max_tx_count) as f64 / total_tx_send_count as f64,
|
||||
);
|
||||
println!(
|
||||
"\tAverage TPS: {}",
|
||||
max_tx_count as f32 / duration_as_s(tx_send_elapsed)
|
||||
);
|
||||
}
|
||||
|
||||
// First transfer 3/4 of the lamports to the dest accounts
|
||||
// then ping-pong 1/4 of the lamports back to the other account
|
||||
// this leaves 1/4 lamport buffer in each account
|
||||
pub fn should_switch_directions(num_lamports_per_account: u64, i: u64) -> bool {
|
||||
i % (num_lamports_per_account / 4) == 0 && (i >= (3 * num_lamports_per_account) / 4)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_switch_directions() {
|
||||
assert_eq!(should_switch_directions(20, 0), false);
|
||||
assert_eq!(should_switch_directions(20, 1), false);
|
||||
assert_eq!(should_switch_directions(20, 14), false);
|
||||
assert_eq!(should_switch_directions(20, 15), true);
|
||||
assert_eq!(should_switch_directions(20, 16), false);
|
||||
assert_eq!(should_switch_directions(20, 19), false);
|
||||
assert_eq!(should_switch_directions(20, 20), true);
|
||||
assert_eq!(should_switch_directions(20, 21), false);
|
||||
assert_eq!(should_switch_directions(20, 99), false);
|
||||
assert_eq!(should_switch_directions(20, 100), true);
|
||||
assert_eq!(should_switch_directions(20, 101), false);
|
||||
}
|
||||
}
|
183
bench-tps/src/cli.rs
Normal file
183
bench-tps/src/cli.rs
Normal file
@ -0,0 +1,183 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::process::exit;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::{crate_version, App, Arg, ArgMatches};
|
||||
use solana_drone::drone::DRONE_PORT;
|
||||
use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
|
||||
|
||||
/// Holds the configuration for a single run of the benchmark
|
||||
pub struct Config {
|
||||
pub network_addr: SocketAddr,
|
||||
pub drone_addr: SocketAddr,
|
||||
pub id: Keypair,
|
||||
pub threads: usize,
|
||||
pub num_nodes: usize,
|
||||
pub duration: Duration,
|
||||
pub tx_count: usize,
|
||||
pub thread_batch_sleep_ms: usize,
|
||||
pub sustained: bool,
|
||||
pub reject_extra_nodes: bool,
|
||||
pub converge_only: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
network_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||
drone_addr: SocketAddr::from(([127, 0, 0, 1], DRONE_PORT)),
|
||||
id: Keypair::new(),
|
||||
threads: 4,
|
||||
num_nodes: 1,
|
||||
duration: Duration::new(std::u64::MAX, 0),
|
||||
tx_count: 500_000,
|
||||
thread_batch_sleep_ms: 0,
|
||||
sustained: false,
|
||||
reject_extra_nodes: false,
|
||||
converge_only: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines and builds the CLI args for a run of the benchmark
|
||||
pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
App::new("solana-bench-tps")
|
||||
.version(crate_version!())
|
||||
.arg(
|
||||
Arg::with_name("network")
|
||||
.short("n")
|
||||
.long("network")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.help("Rendezvous with the network at this gossip entry point; defaults to 127.0.0.1:8001"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("drone")
|
||||
.short("d")
|
||||
.long("drone")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.help("Location of the drone; defaults to network:DRONE_PORT"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("identity")
|
||||
.short("i")
|
||||
.long("identity")
|
||||
.value_name("PATH")
|
||||
.takes_value(true)
|
||||
.help("File containing a client identity (keypair)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("num-nodes")
|
||||
.short("N")
|
||||
.long("num-nodes")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Wait for NUM nodes to converge"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("reject-extra-nodes")
|
||||
.long("reject-extra-nodes")
|
||||
.help("Require exactly `num-nodes` on convergence. Appropriate only for internal networks"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("threads")
|
||||
.short("t")
|
||||
.long("threads")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Number of threads"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("duration")
|
||||
.long("duration")
|
||||
.value_name("SECS")
|
||||
.takes_value(true)
|
||||
.help("Seconds to run benchmark, then exit; default is forever"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("converge-only")
|
||||
.long("converge-only")
|
||||
.help("Exit immediately after converging"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("sustained")
|
||||
.long("sustained")
|
||||
.help("Use sustained performance mode vs. peak mode. This overlaps the tx generation with transfers."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("tx_count")
|
||||
.long("tx_count")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Number of transactions to send per batch")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("thread-batch-sleep-ms")
|
||||
.short("z")
|
||||
.long("thread-batch-sleep-ms")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Per-thread-per-iteration sleep in ms"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Parses a clap `ArgMatches` structure into a `Config`
|
||||
/// # Arguments
|
||||
/// * `matches` - command line arguments parsed by clap
|
||||
/// # Panics
|
||||
/// Panics if there is trouble parsing any of the arguments
|
||||
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
||||
let mut args = Config::default();
|
||||
|
||||
if let Some(addr) = matches.value_of("network") {
|
||||
args.network_addr = addr.parse().unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse network: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(addr) = matches.value_of("drone") {
|
||||
args.drone_addr = addr.parse().unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse drone address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
}
|
||||
|
||||
if matches.is_present("identity") {
|
||||
args.id = read_keypair(matches.value_of("identity").unwrap())
|
||||
.expect("can't read client identity");
|
||||
}
|
||||
|
||||
if let Some(t) = matches.value_of("threads") {
|
||||
args.threads = t.to_string().parse().expect("can't parse threads");
|
||||
}
|
||||
|
||||
if let Some(n) = matches.value_of("num-nodes") {
|
||||
args.num_nodes = n.to_string().parse().expect("can't parse num-nodes");
|
||||
}
|
||||
|
||||
if let Some(duration) = matches.value_of("duration") {
|
||||
args.duration = Duration::new(
|
||||
duration.to_string().parse().expect("can't parse duration"),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(s) = matches.value_of("tx_count") {
|
||||
args.tx_count = s.to_string().parse().expect("can't parse tx_account");
|
||||
}
|
||||
|
||||
if let Some(t) = matches.value_of("thread-batch-sleep-ms") {
|
||||
args.thread_batch_sleep_ms = t
|
||||
.to_string()
|
||||
.parse()
|
||||
.expect("can't parse thread-batch-sleep-ms");
|
||||
}
|
||||
|
||||
args.sustained = matches.is_present("sustained");
|
||||
args.converge_only = matches.is_present("converge-only");
|
||||
args.reject_extra_nodes = matches.is_present("reject-extra-nodes");
|
||||
|
||||
args
|
||||
}
|
249
bench-tps/src/main.rs
Normal file
249
bench-tps/src/main.rs
Normal file
@ -0,0 +1,249 @@
|
||||
mod bench;
|
||||
mod cli;
|
||||
|
||||
use crate::bench::*;
|
||||
use solana::client::mk_client;
|
||||
use solana::gen_keys::GenKeys;
|
||||
use solana::gossip_service::discover;
|
||||
use solana_metrics;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use std::collections::VecDeque;
|
||||
use std::process::exit;
|
||||
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::thread::Builder;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup();
|
||||
solana_metrics::set_panic_hook("bench-tps");
|
||||
|
||||
let matches = cli::build_args().get_matches();
|
||||
|
||||
let cfg = cli::extract_args(&matches);
|
||||
|
||||
let cli::Config {
|
||||
network_addr: network,
|
||||
drone_addr,
|
||||
id,
|
||||
threads,
|
||||
thread_batch_sleep_ms,
|
||||
num_nodes,
|
||||
duration,
|
||||
tx_count,
|
||||
sustained,
|
||||
reject_extra_nodes,
|
||||
converge_only,
|
||||
} = cfg;
|
||||
|
||||
let nodes = discover(&network, num_nodes).unwrap_or_else(|err| {
|
||||
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
||||
exit(1);
|
||||
});
|
||||
if nodes.len() < num_nodes {
|
||||
eprintln!(
|
||||
"Error: Insufficient nodes discovered. Expecting {} or more",
|
||||
num_nodes
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
if reject_extra_nodes && nodes.len() > num_nodes {
|
||||
eprintln!(
|
||||
"Error: Extra nodes discovered. Expecting exactly {}",
|
||||
num_nodes
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if converge_only {
|
||||
return;
|
||||
}
|
||||
let cluster_entrypoint = nodes[0].clone(); // Pick the first node, why not?
|
||||
|
||||
let mut client = mk_client(&cluster_entrypoint);
|
||||
let mut barrier_client = mk_client(&cluster_entrypoint);
|
||||
|
||||
let mut seed = [0u8; 32];
|
||||
seed.copy_from_slice(&id.public_key_bytes()[..32]);
|
||||
let mut rnd = GenKeys::new(seed);
|
||||
|
||||
println!("Creating {} keypairs...", tx_count * 2);
|
||||
let mut total_keys = 0;
|
||||
let mut target = tx_count * 2;
|
||||
while target > 0 {
|
||||
total_keys += target;
|
||||
target /= MAX_SPENDS_PER_TX;
|
||||
}
|
||||
let gen_keypairs = rnd.gen_n_keypairs(total_keys as u64);
|
||||
let barrier_source_keypair = Keypair::new();
|
||||
let barrier_dest_id = Keypair::new().pubkey();
|
||||
|
||||
println!("Get lamports...");
|
||||
let num_lamports_per_account = 20;
|
||||
|
||||
// Sample the first keypair, see if it has lamports, if so then resume
|
||||
// to avoid lamport loss
|
||||
let keypair0_balance = client
|
||||
.poll_get_balance(&gen_keypairs.last().unwrap().pubkey())
|
||||
.unwrap_or(0);
|
||||
|
||||
if num_lamports_per_account > keypair0_balance {
|
||||
let extra = num_lamports_per_account - keypair0_balance;
|
||||
let total = extra * (gen_keypairs.len() as u64);
|
||||
airdrop_lamports(&mut client, &drone_addr, &id, total);
|
||||
println!("adding more lamports {}", extra);
|
||||
fund_keys(&mut client, &id, &gen_keypairs, extra);
|
||||
}
|
||||
let start = gen_keypairs.len() - (tx_count * 2) as usize;
|
||||
let keypairs = &gen_keypairs[start..];
|
||||
airdrop_lamports(&mut barrier_client, &drone_addr, &barrier_source_keypair, 1);
|
||||
|
||||
println!("Get last ID...");
|
||||
let mut blockhash = client.get_recent_blockhash();
|
||||
println!("Got last ID {:?}", blockhash);
|
||||
|
||||
let first_tx_count = client.transaction_count();
|
||||
println!("Initial transaction count {}", first_tx_count);
|
||||
|
||||
let exit_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Setup a thread per validator to sample every period
|
||||
// collect the max transaction rate and total tx count seen
|
||||
let maxes = Arc::new(RwLock::new(Vec::new()));
|
||||
let sample_period = 1; // in seconds
|
||||
println!("Sampling TPS every {} second...", sample_period);
|
||||
let v_threads: Vec<_> = nodes
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
let exit_signal = exit_signal.clone();
|
||||
let maxes = maxes.clone();
|
||||
Builder::new()
|
||||
.name("solana-client-sample".to_string())
|
||||
.spawn(move || {
|
||||
sample_tx_count(&exit_signal, &maxes, first_tx_count, &v, sample_period);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
|
||||
|
||||
let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0));
|
||||
let total_tx_sent_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let s_threads: Vec<_> = (0..threads)
|
||||
.map(|_| {
|
||||
let exit_signal = exit_signal.clone();
|
||||
let shared_txs = shared_txs.clone();
|
||||
let cluster_entrypoint = cluster_entrypoint.clone();
|
||||
let shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
|
||||
let total_tx_sent_count = total_tx_sent_count.clone();
|
||||
Builder::new()
|
||||
.name("solana-client-sender".to_string())
|
||||
.spawn(move || {
|
||||
do_tx_transfers(
|
||||
&exit_signal,
|
||||
&shared_txs,
|
||||
&cluster_entrypoint,
|
||||
&shared_tx_active_thread_count,
|
||||
&total_tx_sent_count,
|
||||
thread_batch_sleep_ms,
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// generate and send transactions for the specified duration
|
||||
let start = Instant::now();
|
||||
let mut reclaim_lamports_back_to_source_account = false;
|
||||
let mut i = keypair0_balance;
|
||||
while start.elapsed() < duration {
|
||||
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
|
||||
// ping-pong between source and destination accounts for each loop iteration
|
||||
// this seems to be faster than trying to determine the balance of individual
|
||||
// accounts
|
||||
let len = tx_count as usize;
|
||||
generate_txs(
|
||||
&shared_txs,
|
||||
&keypairs[..len],
|
||||
&keypairs[len..],
|
||||
threads,
|
||||
reclaim_lamports_back_to_source_account,
|
||||
&cluster_entrypoint,
|
||||
);
|
||||
// In sustained mode overlap the transfers with generation
|
||||
// this has higher average performance but lower peak performance
|
||||
// in tested environments.
|
||||
if !sustained {
|
||||
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
// It's not feasible (would take too much time) to confirm each of the `tx_count / 2`
|
||||
// transactions sent by `generate_txs()` so instead send and confirm a single transaction
|
||||
// to validate the network is still functional.
|
||||
send_barrier_transaction(
|
||||
&mut barrier_client,
|
||||
&mut blockhash,
|
||||
&barrier_source_keypair,
|
||||
&barrier_dest_id,
|
||||
);
|
||||
|
||||
i += 1;
|
||||
if should_switch_directions(num_lamports_per_account, i) {
|
||||
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the sampling threads so it will collect the stats
|
||||
exit_signal.store(true, Ordering::Relaxed);
|
||||
|
||||
println!("Waiting for validator threads...");
|
||||
for t in v_threads {
|
||||
if let Err(err) = t.join() {
|
||||
println!(" join() failed with: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// join the tx send threads
|
||||
println!("Waiting for transmit threads...");
|
||||
for t in s_threads {
|
||||
if let Err(err) = t.join() {
|
||||
println!(" join() failed with: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
|
||||
compute_and_report_stats(
|
||||
&maxes,
|
||||
sample_period,
|
||||
&start.elapsed(),
|
||||
total_tx_sent_count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_switch_directions() {
|
||||
assert_eq!(should_switch_directions(20, 0), false);
|
||||
assert_eq!(should_switch_directions(20, 1), false);
|
||||
assert_eq!(should_switch_directions(20, 14), false);
|
||||
assert_eq!(should_switch_directions(20, 15), true);
|
||||
assert_eq!(should_switch_directions(20, 16), false);
|
||||
assert_eq!(should_switch_directions(20, 19), false);
|
||||
assert_eq!(should_switch_directions(20, 20), true);
|
||||
assert_eq!(should_switch_directions(20, 21), false);
|
||||
assert_eq!(should_switch_directions(20, 99), false);
|
||||
assert_eq!(should_switch_directions(20, 100), true);
|
||||
assert_eq!(should_switch_directions(20, 101), false);
|
||||
}
|
||||
}
|
248
benches/append_vec.rs
Normal file
248
benches/append_vec.rs
Normal file
@ -0,0 +1,248 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate rand;
|
||||
extern crate test;
|
||||
|
||||
use bincode::{deserialize, serialize_into, serialized_size};
|
||||
use rand::{thread_rng, Rng};
|
||||
use solana_runtime::append_vec::{
|
||||
deserialize_account, get_serialized_size, serialize_account, AppendVec,
|
||||
};
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use std::env;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::spawn;
|
||||
use test::Bencher;
|
||||
|
||||
const START_SIZE: u64 = 4 * 1024 * 1024;
|
||||
const INC_SIZE: u64 = 1 * 1024 * 1024;
|
||||
|
||||
macro_rules! align_up {
|
||||
($addr: expr, $align: expr) => {
|
||||
($addr + ($align - 1)) & !($align - 1)
|
||||
};
|
||||
}
|
||||
|
||||
fn get_append_vec_bench_path(path: &str) -> PathBuf {
|
||||
let out_dir = env::var("OUT_DIR").unwrap_or_else(|_| "target".to_string());
|
||||
let mut buf = PathBuf::new();
|
||||
buf.push(&format!("{}/{}", out_dir, path));
|
||||
buf
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_atomic_append(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_append");
|
||||
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||
bencher.iter(|| {
|
||||
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||
}
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_atomic_random_access(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_ra");
|
||||
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||
let size = 1_000_000;
|
||||
for _ in 0..size {
|
||||
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||
}
|
||||
}
|
||||
bencher.iter(|| {
|
||||
let index = thread_rng().gen_range(0, size as u64);
|
||||
vec.get(index * std::mem::size_of::<AtomicUsize>() as u64);
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_atomic_random_change(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_rax");
|
||||
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||
let size = 1_000_000;
|
||||
for k in 0..size {
|
||||
if vec.append(AtomicUsize::new(k)).is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
assert!(vec.append(AtomicUsize::new(k)).is_some());
|
||||
}
|
||||
}
|
||||
bencher.iter(|| {
|
||||
let index = thread_rng().gen_range(0, size as u64);
|
||||
let atomic1 = vec.get(index * std::mem::size_of::<AtomicUsize>() as u64);
|
||||
let current1 = atomic1.load(Ordering::Relaxed);
|
||||
assert_eq!(current1, index as usize);
|
||||
let next = current1 + 1;
|
||||
let mut index = vec.append(AtomicUsize::new(next));
|
||||
if index.is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
index = vec.append(AtomicUsize::new(next));
|
||||
}
|
||||
let atomic2 = vec.get(index.unwrap());
|
||||
let current2 = atomic2.load(Ordering::Relaxed);
|
||||
assert_eq!(current2, next);
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_atomic_random_read(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_read");
|
||||
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||
let size = 1_000_000;
|
||||
for _ in 0..size {
|
||||
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||
assert!(vec.grow_file().is_ok());
|
||||
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||
}
|
||||
}
|
||||
bencher.iter(|| {
|
||||
let index = thread_rng().gen_range(0, size);
|
||||
let atomic1 = vec.get((index * std::mem::size_of::<AtomicUsize>()) as u64);
|
||||
let current1 = atomic1.load(Ordering::Relaxed);
|
||||
assert_eq!(current1, 0);
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_concurrent_lock_append(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_lock_append");
|
||||
let vec = Arc::new(RwLock::new(AppendVec::<AtomicUsize>::new(
|
||||
&path, true, START_SIZE, INC_SIZE,
|
||||
)));
|
||||
let vec1 = vec.clone();
|
||||
let size = 1_000_000;
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let count1 = count.clone();
|
||||
spawn(move || loop {
|
||||
let mut len = count.load(Ordering::Relaxed);
|
||||
{
|
||||
let rlock = vec1.read().unwrap();
|
||||
loop {
|
||||
if rlock.append(AtomicUsize::new(0)).is_none() {
|
||||
break;
|
||||
}
|
||||
len = count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
if len >= size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut wlock = vec1.write().unwrap();
|
||||
if len >= size {
|
||||
break;
|
||||
}
|
||||
assert!(wlock.grow_file().is_ok());
|
||||
}
|
||||
});
|
||||
bencher.iter(|| {
|
||||
let _rlock = vec.read().unwrap();
|
||||
let len = count1.load(Ordering::Relaxed);
|
||||
assert!(len < size * 2);
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn append_vec_concurrent_get_append(bencher: &mut Bencher) {
|
||||
let path = get_append_vec_bench_path("bench_get_append");
|
||||
let vec = Arc::new(RwLock::new(AppendVec::<AtomicUsize>::new(
|
||||
&path, true, START_SIZE, INC_SIZE,
|
||||
)));
|
||||
let vec1 = vec.clone();
|
||||
let size = 1_000_000;
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let count1 = count.clone();
|
||||
spawn(move || loop {
|
||||
let mut len = count.load(Ordering::Relaxed);
|
||||
{
|
||||
let rlock = vec1.read().unwrap();
|
||||
loop {
|
||||
if rlock.append(AtomicUsize::new(0)).is_none() {
|
||||
break;
|
||||
}
|
||||
len = count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
if len >= size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut wlock = vec1.write().unwrap();
|
||||
if len >= size {
|
||||
break;
|
||||
}
|
||||
assert!(wlock.grow_file().is_ok());
|
||||
}
|
||||
});
|
||||
bencher.iter(|| {
|
||||
let rlock = vec.read().unwrap();
|
||||
let len = count1.load(Ordering::Relaxed);
|
||||
if len > 0 {
|
||||
let index = thread_rng().gen_range(0, len);
|
||||
rlock.get((index * std::mem::size_of::<AtomicUsize>()) as u64);
|
||||
}
|
||||
});
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_account_serialize(bencher: &mut Bencher) {
|
||||
let num: usize = 1000;
|
||||
let account = Account::new(2, 100, &Keypair::new().pubkey());
|
||||
let len = get_serialized_size(&account);
|
||||
let ser_len = align_up!(len + std::mem::size_of::<u64>(), std::mem::size_of::<u64>());
|
||||
let mut memory = vec![0; num * ser_len];
|
||||
bencher.iter(|| {
|
||||
for i in 0..num {
|
||||
let start = i * ser_len;
|
||||
serialize_account(&mut memory[start..start + ser_len], &account, len);
|
||||
}
|
||||
});
|
||||
|
||||
// make sure compiler doesn't delete the code.
|
||||
let index = thread_rng().gen_range(0, num);
|
||||
if memory[index] != 0 {
|
||||
println!("memory: {}", memory[index]);
|
||||
}
|
||||
|
||||
let start = index * ser_len;
|
||||
let new_account = deserialize_account(&memory[start..start + ser_len], 0, num * len).unwrap();
|
||||
assert_eq!(new_account, account);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_account_serialize_bincode(bencher: &mut Bencher) {
|
||||
let num: usize = 1000;
|
||||
let account = Account::new(2, 100, &Keypair::new().pubkey());
|
||||
let len = serialized_size(&account).unwrap() as usize;
|
||||
let mut memory = vec![0u8; num * len];
|
||||
bencher.iter(|| {
|
||||
for i in 0..num {
|
||||
let start = i * len;
|
||||
let cursor = Cursor::new(&mut memory[start..start + len]);
|
||||
serialize_into(cursor, &account).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// make sure compiler doesn't delete the code.
|
||||
let index = thread_rng().gen_range(0, len);
|
||||
if memory[index] != 0 {
|
||||
println!("memory: {}", memory[index]);
|
||||
}
|
||||
|
||||
let start = index * len;
|
||||
let new_account: Account = deserialize(&memory[start..start + len]).unwrap();
|
||||
assert_eq!(new_account, account);
|
||||
}
|
241
benches/banking_stage.rs
Normal file
241
benches/banking_stage.rs
Normal file
@ -0,0 +1,241 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate test;
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use rayon::prelude::*;
|
||||
use solana::banking_stage::{create_test_recorder, BankingStage};
|
||||
use solana::cluster_info::ClusterInfo;
|
||||
use solana::cluster_info::Node;
|
||||
use solana::packet::to_packets_chunked;
|
||||
use solana::poh_recorder::WorkingBankEntries;
|
||||
use solana::service::Service;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::hash::hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{KeypairUtil, Signature};
|
||||
use solana_sdk::system_transaction::SystemTransaction;
|
||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, MAX_RECENT_BLOCKHASHES};
|
||||
use std::iter;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc::{channel, Receiver};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use test::Bencher;
|
||||
|
||||
fn check_txs(receiver: &Receiver<WorkingBankEntries>, ref_tx_count: usize) {
|
||||
let mut total = 0;
|
||||
loop {
|
||||
let entries = receiver.recv_timeout(Duration::new(1, 0));
|
||||
if let Ok((_, entries)) = entries {
|
||||
for (entry, _) in &entries {
|
||||
total += entry.transactions.len();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if total >= ref_tx_count {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(total, ref_tx_count);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
let num_threads = BankingStage::num_threads() as usize;
|
||||
// a multiple of packet chunk 2X duplicates to avoid races
|
||||
let txes = 192 * 50 * num_threads * 2;
|
||||
let mint_total = 1_000_000_000_000;
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
||||
|
||||
let (verified_sender, verified_receiver) = channel();
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
let dummy = SystemTransaction::new_move(
|
||||
&mint_keypair,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
);
|
||||
let transactions: Vec<_> = (0..txes)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
let mut new = dummy.clone();
|
||||
let from: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
let to: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
new.account_keys[0] = Pubkey::new(&from[0..32]);
|
||||
new.account_keys[1] = Pubkey::new(&to[0..32]);
|
||||
new.signatures = vec![Signature::new(&sig[0..64])];
|
||||
new
|
||||
})
|
||||
.collect();
|
||||
// fund all the accounts
|
||||
transactions.iter().for_each(|tx| {
|
||||
let fund = SystemTransaction::new_move(
|
||||
&mint_keypair,
|
||||
&tx.account_keys[0],
|
||||
mint_total / txes as u64,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
);
|
||||
let x = bank.process_transaction(&fund);
|
||||
x.unwrap();
|
||||
});
|
||||
//sanity check, make sure all the transactions can execute sequentially
|
||||
transactions.iter().for_each(|tx| {
|
||||
let res = bank.process_transaction(&tx);
|
||||
assert!(res.is_ok(), "sanity test transactions");
|
||||
});
|
||||
bank.clear_signatures();
|
||||
//sanity check, make sure all the transactions can execute in parallel
|
||||
let res = bank.process_transactions(&transactions);
|
||||
for r in res {
|
||||
assert!(r.is_ok(), "sanity parallel execution");
|
||||
}
|
||||
bank.clear_signatures();
|
||||
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 192)
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let len = x.read().unwrap().packets.len();
|
||||
(x, iter::repeat(1).take(len).collect())
|
||||
})
|
||||
.collect();
|
||||
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(&bank);
|
||||
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
|
||||
let mut id = genesis_block.hash();
|
||||
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
|
||||
id = hash(&id.as_ref());
|
||||
bank.register_tick(&id);
|
||||
}
|
||||
|
||||
let half_len = verified.len() / 2;
|
||||
let mut start = 0;
|
||||
bencher.iter(move || {
|
||||
// make sure the transactions are still valid
|
||||
bank.register_tick(&genesis_block.hash());
|
||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
}
|
||||
check_txs(&signal_receiver, txes / 2);
|
||||
bank.clear_signatures();
|
||||
start += half_len;
|
||||
start %= verified.len();
|
||||
});
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
poh_service.join().unwrap();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||
let progs = 4;
|
||||
let num_threads = BankingStage::num_threads() as usize;
|
||||
// a multiple of packet chunk 2X duplicates to avoid races
|
||||
let txes = 96 * 100 * num_threads * 2;
|
||||
let mint_total = 1_000_000_000_000;
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
||||
|
||||
let (verified_sender, verified_receiver) = channel();
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
let dummy = SystemTransaction::new_move(
|
||||
&mint_keypair,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
);
|
||||
let transactions: Vec<_> = (0..txes)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
let mut new = dummy.clone();
|
||||
let from: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
let to: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||
new.account_keys[0] = Pubkey::new(&from[0..32]);
|
||||
new.account_keys[1] = Pubkey::new(&to[0..32]);
|
||||
let prog = new.instructions[0].clone();
|
||||
for i in 1..progs {
|
||||
//generate programs that spend to random keys
|
||||
let to: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||
let to_key = Pubkey::new(&to[0..32]);
|
||||
new.account_keys.push(to_key);
|
||||
assert_eq!(new.account_keys.len(), i + 2);
|
||||
new.instructions.push(prog.clone());
|
||||
assert_eq!(new.instructions.len(), i + 1);
|
||||
new.instructions[i].accounts[1] = 1 + i as u8;
|
||||
assert_eq!(new.key(i, 1), Some(&to_key));
|
||||
assert_eq!(
|
||||
new.account_keys[new.instructions[i].accounts[1] as usize],
|
||||
to_key
|
||||
);
|
||||
}
|
||||
assert_eq!(new.instructions.len(), progs);
|
||||
new.signatures = vec![Signature::new(&sig[0..64])];
|
||||
new
|
||||
})
|
||||
.collect();
|
||||
transactions.iter().for_each(|tx| {
|
||||
let fund = SystemTransaction::new_move(
|
||||
&mint_keypair,
|
||||
&tx.account_keys[0],
|
||||
mint_total / txes as u64,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
);
|
||||
bank.process_transaction(&fund).unwrap();
|
||||
});
|
||||
//sanity check, make sure all the transactions can execute sequentially
|
||||
transactions.iter().for_each(|tx| {
|
||||
let res = bank.process_transaction(&tx);
|
||||
assert!(res.is_ok(), "sanity test transactions");
|
||||
});
|
||||
bank.clear_signatures();
|
||||
//sanity check, make sure all the transactions can execute in parallel
|
||||
let res = bank.process_transactions(&transactions);
|
||||
for r in res {
|
||||
assert!(r.is_ok(), "sanity parallel execution");
|
||||
}
|
||||
bank.clear_signatures();
|
||||
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 96)
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let len = x.read().unwrap().packets.len();
|
||||
(x, iter::repeat(1).take(len).collect())
|
||||
})
|
||||
.collect();
|
||||
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(&bank);
|
||||
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
|
||||
let mut id = genesis_block.hash();
|
||||
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
|
||||
id = hash(&id.as_ref());
|
||||
bank.register_tick(&id);
|
||||
}
|
||||
|
||||
let half_len = verified.len() / 2;
|
||||
let mut start = 0;
|
||||
bencher.iter(move || {
|
||||
// make sure the transactions are still valid
|
||||
bank.register_tick(&genesis_block.hash());
|
||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
}
|
||||
check_txs(&signal_receiver, txes / 2);
|
||||
bank.clear_signatures();
|
||||
start += half_len;
|
||||
start %= verified.len();
|
||||
});
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
poh_service.join().unwrap();
|
||||
}
|
194
benches/blocktree.rs
Normal file
194
benches/blocktree.rs
Normal file
@ -0,0 +1,194 @@
|
||||
#![feature(test)]
|
||||
use rand;
|
||||
|
||||
extern crate test;
|
||||
|
||||
#[macro_use]
|
||||
extern crate solana;
|
||||
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::{thread_rng, Rng};
|
||||
use solana::blocktree::{get_tmp_ledger_path, Blocktree};
|
||||
use solana::entry::{make_large_test_entries, make_tiny_test_entries, EntrySlice};
|
||||
use solana::packet::{Blob, BLOB_HEADER_SIZE};
|
||||
use test::Bencher;
|
||||
|
||||
// Given some blobs and a ledger at ledger_path, benchmark writing the blobs to the ledger
|
||||
fn bench_write_blobs(bench: &mut Bencher, blobs: &mut Vec<Blob>, ledger_path: &str) {
|
||||
let blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||
|
||||
let num_blobs = blobs.len();
|
||||
|
||||
bench.iter(move || {
|
||||
for blob in blobs.iter_mut() {
|
||||
let index = blob.index();
|
||||
|
||||
blocktree
|
||||
.put_data_blob_bytes(
|
||||
blob.slot(),
|
||||
index,
|
||||
&blob.data[..BLOB_HEADER_SIZE + blob.size()],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
blob.set_index(index + num_blobs as u64);
|
||||
}
|
||||
});
|
||||
|
||||
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||
}
|
||||
|
||||
// Insert some blobs into the ledger in preparation for read benchmarks
|
||||
fn setup_read_bench(
|
||||
blocktree: &mut Blocktree,
|
||||
num_small_blobs: u64,
|
||||
num_large_blobs: u64,
|
||||
slot: u64,
|
||||
) {
|
||||
// Make some big and small entries
|
||||
let mut entries = make_large_test_entries(num_large_blobs as usize);
|
||||
entries.extend(make_tiny_test_entries(num_small_blobs as usize));
|
||||
|
||||
// Convert the entries to blobs, write the blobs to the ledger
|
||||
let mut blobs = entries.to_blobs();
|
||||
for (index, b) in blobs.iter_mut().enumerate() {
|
||||
b.set_index(index as u64);
|
||||
b.set_slot(slot);
|
||||
}
|
||||
blocktree
|
||||
.write_blobs(&blobs)
|
||||
.expect("Expectd successful insertion of blobs into ledger");
|
||||
}
|
||||
|
||||
// Write small blobs to the ledger
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_write_small(bench: &mut Bencher) {
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
let num_entries = 32 * 1024;
|
||||
let entries = make_tiny_test_entries(num_entries);
|
||||
let mut blobs = entries.to_blobs();
|
||||
for (index, b) in blobs.iter_mut().enumerate() {
|
||||
b.set_index(index as u64);
|
||||
}
|
||||
bench_write_blobs(bench, &mut blobs, &ledger_path);
|
||||
}
|
||||
|
||||
// Write big blobs to the ledger
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_write_big(bench: &mut Bencher) {
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
let num_entries = 32 * 1024;
|
||||
let entries = make_large_test_entries(num_entries);
|
||||
let mut blobs = entries.to_blobs();
|
||||
for (index, b) in blobs.iter_mut().enumerate() {
|
||||
b.set_index(index as u64);
|
||||
}
|
||||
|
||||
bench_write_blobs(bench, &mut blobs, &ledger_path);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_read_sequential(bench: &mut Bencher) {
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
let mut blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||
|
||||
// Insert some big and small blobs into the ledger
|
||||
let num_small_blobs = 32 * 1024;
|
||||
let num_large_blobs = 32 * 1024;
|
||||
let total_blobs = num_small_blobs + num_large_blobs;
|
||||
let slot = 0;
|
||||
setup_read_bench(&mut blocktree, num_small_blobs, num_large_blobs, slot);
|
||||
|
||||
let num_reads = total_blobs / 15;
|
||||
let mut rng = rand::thread_rng();
|
||||
bench.iter(move || {
|
||||
// Generate random starting point in the range [0, total_blobs - 1], read num_reads blobs sequentially
|
||||
let start_index = rng.gen_range(0, num_small_blobs + num_large_blobs);
|
||||
for i in start_index..start_index + num_reads {
|
||||
let _ = blocktree.get_data_blob(slot, i as u64 % total_blobs);
|
||||
}
|
||||
});
|
||||
|
||||
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_read_random(bench: &mut Bencher) {
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
let mut blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||
|
||||
// Insert some big and small blobs into the ledger
|
||||
let num_small_blobs = 32 * 1024;
|
||||
let num_large_blobs = 32 * 1024;
|
||||
let total_blobs = num_small_blobs + num_large_blobs;
|
||||
let slot = 0;
|
||||
setup_read_bench(&mut blocktree, num_small_blobs, num_large_blobs, slot);
|
||||
|
||||
let num_reads = total_blobs / 15;
|
||||
|
||||
// Generate a num_reads sized random sample of indexes in range [0, total_blobs - 1],
|
||||
// simulating random reads
|
||||
let mut rng = rand::thread_rng();
|
||||
let indexes: Vec<usize> = (0..num_reads)
|
||||
.map(|_| rng.gen_range(0, total_blobs) as usize)
|
||||
.collect();
|
||||
bench.iter(move || {
|
||||
for i in indexes.iter() {
|
||||
let _ = blocktree.get_data_blob(slot, *i as u64);
|
||||
}
|
||||
});
|
||||
|
||||
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_insert_data_blob_small(bench: &mut Bencher) {
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
let blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||
let num_entries = 32 * 1024;
|
||||
let entries = make_tiny_test_entries(num_entries);
|
||||
let mut blobs = entries.to_blobs();
|
||||
|
||||
blobs.shuffle(&mut thread_rng());
|
||||
|
||||
bench.iter(move || {
|
||||
for blob in blobs.iter_mut() {
|
||||
let index = blob.index();
|
||||
blob.set_index(index + num_entries as u64);
|
||||
}
|
||||
blocktree.write_blobs(&blobs).unwrap();
|
||||
});
|
||||
|
||||
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_insert_data_blob_big(bench: &mut Bencher) {
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
let blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||
let num_entries = 32 * 1024;
|
||||
let entries = make_large_test_entries(num_entries);
|
||||
let mut shared_blobs = entries.to_shared_blobs();
|
||||
shared_blobs.shuffle(&mut thread_rng());
|
||||
|
||||
bench.iter(move || {
|
||||
for blob in shared_blobs.iter_mut() {
|
||||
let index = blob.read().unwrap().index();
|
||||
blocktree.write_shared_blobs(vec![blob.clone()]).unwrap();
|
||||
blob.write().unwrap().set_index(index + num_entries as u64);
|
||||
}
|
||||
});
|
||||
|
||||
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||
}
|
29
benches/chacha.rs
Normal file
29
benches/chacha.rs
Normal file
@ -0,0 +1,29 @@
|
||||
//#![feature(test)]
|
||||
//
|
||||
//extern crate solana;
|
||||
//extern crate test;
|
||||
//
|
||||
//use solana::chacha::chacha_cbc_encrypt_files;
|
||||
//use std::fs::remove_file;
|
||||
//use std::fs::File;
|
||||
//use std::io::Write;
|
||||
//use std::path::Path;
|
||||
//use test::Bencher;
|
||||
//
|
||||
//#[bench]
|
||||
//fn bench_chacha_encrypt(bench: &mut Bencher) {
|
||||
// let in_path = Path::new("bench_chacha_encrypt_file_input.txt");
|
||||
// let out_path = Path::new("bench_chacha_encrypt_file_output.txt.enc");
|
||||
// {
|
||||
// let mut in_file = File::create(in_path).unwrap();
|
||||
// for _ in 0..1024 {
|
||||
// in_file.write("123456foobar".as_bytes()).unwrap();
|
||||
// }
|
||||
// }
|
||||
// bench.iter(move || {
|
||||
// chacha_cbc_encrypt_files(in_path, out_path, "thetestkey".to_string()).unwrap();
|
||||
// });
|
||||
//
|
||||
// remove_file(in_path).unwrap();
|
||||
// remove_file(out_path).unwrap();
|
||||
//}
|
12
benches/gen_keys.rs
Normal file
12
benches/gen_keys.rs
Normal file
@ -0,0 +1,12 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate test;
|
||||
|
||||
use solana::gen_keys::GenKeys;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_gen_keys(b: &mut Bencher) {
|
||||
let mut rnd = GenKeys::new([0u8; 32]);
|
||||
b.iter(|| rnd.gen_n_keypairs(1000));
|
||||
}
|
24
benches/ledger.rs
Normal file
24
benches/ledger.rs
Normal file
@ -0,0 +1,24 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate test;
|
||||
|
||||
use solana::entry::{next_entries, reconstruct_entries_from_blobs, EntrySlice};
|
||||
use solana_sdk::hash::{hash, Hash};
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction::SystemTransaction;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_block_to_blobs_to_block(bencher: &mut Bencher) {
|
||||
let zero = Hash::default();
|
||||
let one = hash(&zero.as_ref());
|
||||
let keypair = Keypair::new();
|
||||
let tx0 = SystemTransaction::new_move(&keypair, &keypair.pubkey(), 1, one, 0);
|
||||
let transactions = vec![tx0; 10];
|
||||
let entries = next_entries(&zero, 1, transactions);
|
||||
|
||||
bencher.iter(|| {
|
||||
let blobs = entries.to_blobs();
|
||||
assert_eq!(reconstruct_entries_from_blobs(blobs).unwrap().0, entries);
|
||||
});
|
||||
}
|
21
benches/sigverify.rs
Normal file
21
benches/sigverify.rs
Normal file
@ -0,0 +1,21 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate test;
|
||||
|
||||
use solana::packet::to_packets;
|
||||
use solana::sigverify;
|
||||
use solana::test_tx::test_tx;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_sigverify(bencher: &mut Bencher) {
|
||||
let tx = test_tx();
|
||||
|
||||
// generate packet vector
|
||||
let batches = to_packets(&vec![tx; 128]);
|
||||
|
||||
// verify packets
|
||||
bencher.iter(|| {
|
||||
let _ans = sigverify::ed25519_verify(&batches);
|
||||
})
|
||||
}
|
26
book/README.md
Normal file
26
book/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
Building the Solana book
|
||||
---
|
||||
|
||||
Install the book's dependnecies, build, and test the book:
|
||||
|
||||
```bash
|
||||
$ ./build.sh
|
||||
```
|
||||
|
||||
Run any Rust tests in the markdown:
|
||||
|
||||
```bash
|
||||
$ make test
|
||||
```
|
||||
|
||||
Render markdown as HTML:
|
||||
|
||||
```bash
|
||||
$ make build
|
||||
```
|
||||
|
||||
Render and view the book:
|
||||
|
||||
```bash
|
||||
$ make open
|
||||
```
|
15
book/art/consensus.msc
Normal file
15
book/art/consensus.msc
Normal file
@ -0,0 +1,15 @@
|
||||
msc {
|
||||
client,leader,verifier_a,verifier_b,verifier_c;
|
||||
|
||||
client=>leader [ label = "SUBMIT" ] ;
|
||||
leader=>client [ label = "CONFIRMED" ] ;
|
||||
leader=>verifier_a [ label = "CONFIRMED" ] ;
|
||||
leader=>verifier_b [ label = "CONFIRMED" ] ;
|
||||
leader=>verifier_c [ label = "CONFIRMED" ] ;
|
||||
verifier_a=>leader [ label = "VERIFIED" ] ;
|
||||
verifier_b=>leader [ label = "VERIFIED" ] ;
|
||||
leader=>client [ label = "FINALIZED" ] ;
|
||||
leader=>verifier_a [ label = "FINALIZED" ] ;
|
||||
leader=>verifier_b [ label = "FINALIZED" ] ;
|
||||
leader=>verifier_c [ label = "FINALIZED" ] ;
|
||||
}
|
25
book/art/data-plane-neighborhood.bob
Normal file
25
book/art/data-plane-neighborhood.bob
Normal file
@ -0,0 +1,25 @@
|
||||
+---------------------------------------------------------------------------------------------------------+
|
||||
| Neighborhood Above |
|
||||
| |
|
||||
| +----------------+ +----------------+ +----------------+ +----------------+ |
|
||||
| | +------>+ +------>+ +------>+ | |
|
||||
| | Neighbor 1 | | Neighbor 2 | | Neighbor 3 | | Neighbor 4 | |
|
||||
| | +<------+ +<------+ +<------+ | |
|
||||
| +--+-------------+ +--+-------------+ +-----+----------+ +--+-------------+ |
|
||||
| | | | | |
|
||||
+---------------------------------------------------------------------------------------------------------+
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
+---------------------------------------------------------------------------------------------------------+
|
||||
| | | Neighborhood Below | | |
|
||||
| v v v v |
|
||||
| +--+-------------+ +--+-------------+ +-----+----------+ +--+-------------+ |
|
||||
| | +------>+ +------>+ +------>+ | |
|
||||
| | Neighbor 1 | | Neighbor 2 | | Neighbor 3 | | Neighbor 4 | |
|
||||
| | +<------+ +<------+ +<------+ | |
|
||||
| +----------------+ +----------------+ +----------------+ +----------------+ |
|
||||
| |
|
||||
+---------------------------------------------------------------------------------------------------------+
|
28
book/art/data-plane.bob
Normal file
28
book/art/data-plane.bob
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
+--------------+
|
||||
| |
|
||||
+------------+ Leader +------------+
|
||||
| | | |
|
||||
| +--------------+ |
|
||||
v v
|
||||
+--------+--------+ +--------+--------+
|
||||
| +--------------------->+ |
|
||||
+-----------------+ Validator 1 | | Validator 2 +-------------+
|
||||
| | +<---------------------+ | |
|
||||
| +------+-+-+------+ +---+-+-+---------+ |
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
| +---------------------------------------------+ | | |
|
||||
| | | | | | | |
|
||||
| | | | | +----------------------+ | |
|
||||
| | | | | | | |
|
||||
| | | | +--------------------------------------------+ |
|
||||
| | | | | | | |
|
||||
| | | +----------------------+ | | |
|
||||
| | | | | | | |
|
||||
v v v v v v v v
|
||||
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
||||
| | | | | | | |
|
||||
| Neighborhood 1 | | Neighborhood 2 | | Neighborhood 3 | | Neighborhood 4 |
|
||||
| | | | | | | |
|
||||
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
13
book/art/fork-generation.bob
Normal file
13
book/art/fork-generation.bob
Normal file
@ -0,0 +1,13 @@
|
||||
validator action
|
||||
+----+ ----------------
|
||||
| | L1 | E1
|
||||
| +----+ / \ vote(E1)
|
||||
| | L2 | E2 x
|
||||
| +----+ / \ / \ vote(E2)
|
||||
time | | L3 | E3 x E3' x
|
||||
| +----+ / \ / \ / \ / \ slash(E3)
|
||||
| | L4 | x x E4 x x x x x
|
||||
| +----+ | | | | | | | | vote(E4)
|
||||
v | L5 | xx xx xx E5 xx xx xx xx
|
||||
+----+ hang on to E4 and E5 for more...
|
||||
|
9
book/art/forks-pruned.bob
Normal file
9
book/art/forks-pruned.bob
Normal file
@ -0,0 +1,9 @@
|
||||
1
|
||||
|
|
||||
2
|
||||
/|
|
||||
/ |
|
||||
| |
|
||||
| 4
|
||||
|
|
||||
5
|
11
book/art/forks-pruned2.bob
Normal file
11
book/art/forks-pruned2.bob
Normal file
@ -0,0 +1,11 @@
|
||||
1
|
||||
|
|
||||
3
|
||||
|\
|
||||
| \
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
6 |
|
||||
|
|
||||
7
|
13
book/art/forks.bob
Normal file
13
book/art/forks.bob
Normal file
@ -0,0 +1,13 @@
|
||||
1
|
||||
|\
|
||||
2 \
|
||||
/| |
|
||||
/ | 3
|
||||
| | |\
|
||||
| 4 | \
|
||||
| | |
|
||||
5 | |
|
||||
| |
|
||||
6 |
|
||||
|
|
||||
7
|
30
book/art/fullnode.bob
Normal file
30
book/art/fullnode.bob
Normal file
@ -0,0 +1,30 @@
|
||||
.--------------------------------------.
|
||||
| Fullnode |
|
||||
| |
|
||||
.--------. | .-------------------. |
|
||||
| |---->| | |
|
||||
| Client | | | JSON RPC Service | |
|
||||
| |<----| | |
|
||||
`----+---` | `-------------------` |
|
||||
| | ^ |
|
||||
| | | .----------------. | .------------------.
|
||||
| | | | Gossip Service |<----------| Validators |
|
||||
| | | `----------------` | | |
|
||||
| | | ^ | | |
|
||||
| | | | | | .------------. |
|
||||
| | .---+---. .----+---. .-----------. | | | | |
|
||||
| | | Bank |<-+ Replay | | BlobFetch |<------+ Upstream | |
|
||||
| | | Forks | | Stage | | Stage | | | | Validators | |
|
||||
| | `-------` `--------` `--+--------` | | | | |
|
||||
| | ^ ^ | | | `------------` |
|
||||
| | | | v | | |
|
||||
| | | .--+--------. | | |
|
||||
| | | | Blocktree | | | |
|
||||
| | | `-----------` | | .------------. |
|
||||
| | | ^ | | | | |
|
||||
| | | | | | | Downstream | |
|
||||
| | .--+--. .-------+---. | | | Validators | |
|
||||
`-------->| TPU +---->| Broadcast +--------------->| | |
|
||||
| `-----` | Service | | | `------------` |
|
||||
| `-----------` | `------------------`
|
||||
`--------------------------------------`
|
10
book/art/runtime.bob
Normal file
10
book/art/runtime.bob
Normal file
@ -0,0 +1,10 @@
|
||||
.------------. .-----------. .---------------. .--------------. .-----------------------.
|
||||
| PoH verify +---> | sigverify +--->| lock accounts +--->| validate fee +--->| allocate new accounts +--->
|
||||
| TVU | `-----------` `---------------` `--------------` `-----------------------`
|
||||
`------------`
|
||||
|
||||
.---------------. .---------. .------------. .-----------------. .-----------------.
|
||||
--->| load accounts +--->| execute +--->| PoH record +--->| commit accounts +-->| unlock accounts |
|
||||
`---------------` `---------` | TPU | `-----------------` `-----------------`
|
||||
`------------`
|
||||
|
20
book/art/sdk-tools.bob
Normal file
20
book/art/sdk-tools.bob
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
.----------------------------------------.
|
||||
| Solana Runtime |
|
||||
| |
|
||||
| .------------. .------------. |
|
||||
| | | | | |
|
||||
.-------->| Verifier +-->| Accounts | |
|
||||
| | | | | | |
|
||||
.----------. | | `------------` `------------` |
|
||||
| +--------` | ^ |
|
||||
| Client | | LoadAccounts | |
|
||||
| +--------. | .----------------` |
|
||||
`----------` | | | |
|
||||
| | .------+-----. .-------------. |
|
||||
| | | | | | |
|
||||
`-------->| Loader +-->| Interpreter | |
|
||||
| | | | | |
|
||||
| `------------` `-------------` |
|
||||
| |
|
||||
`----------------------------------------`
|
18
book/art/tpu.bob
Normal file
18
book/art/tpu.bob
Normal file
@ -0,0 +1,18 @@
|
||||
.-------------------------------------------.
|
||||
| TPU .-------------. |
|
||||
| | PoH Service | |
|
||||
| `--------+----` |
|
||||
| ^ | |
|
||||
| | v |
|
||||
| .-------. .-----------. .-+-------. | .------------.
|
||||
.---------. | | Fetch | | SigVerify | | Banking | | | Broadcast |
|
||||
| Clients |--->| Stage |->| Stage |->| Stage |------>| Service |
|
||||
`---------` | | | | | | | | | |
|
||||
| `-------` `-----------` `----+----` | `------------`
|
||||
| | |
|
||||
`---------------------------------|---------`
|
||||
|
|
||||
v
|
||||
.------.
|
||||
| Bank |
|
||||
`------`
|
22
book/art/tvu.bob
Normal file
22
book/art/tvu.bob
Normal file
@ -0,0 +1,22 @@
|
||||
.--------.
|
||||
| Leader |
|
||||
`--------`
|
||||
^
|
||||
|
|
||||
.------------------------------------|--------------------.
|
||||
| TVU | |
|
||||
| | |
|
||||
| .-------. .------------. .----+---. .---------. |
|
||||
.------------. | | Blob | | Retransmit | | Replay | | Storage | |
|
||||
| Upstream +----->| Fetch +-->| Stage +-->| Stage +-->| Stage | |
|
||||
| Validators | | | Stage | | | | | | | |
|
||||
`------------` | `-------` `----+-------` `----+---` `---------` |
|
||||
| ^ | | |
|
||||
| | | | |
|
||||
`--------|----------|----------------|--------------------`
|
||||
| | |
|
||||
| V v
|
||||
.+-----------. .------.
|
||||
| Gossip | | Bank |
|
||||
| Service | `------`
|
||||
`------------`
|
10
book/book.toml
Normal file
10
book/book.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[book]
|
||||
title = "Solana: Blockchain Rebuilt for Scale"
|
||||
authors = ["The Solana Team"]
|
||||
|
||||
[build]
|
||||
build-dir = "html"
|
||||
create-missing = false
|
||||
|
||||
[output.html]
|
||||
theme = "theme"
|
18
book/build.sh
Executable file
18
book/build.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
cargo_install_unless() {
|
||||
declare crate=$1
|
||||
shift
|
||||
|
||||
"$@" > /dev/null 2>&1 || \
|
||||
cargo install "$crate"
|
||||
}
|
||||
|
||||
export PATH=$CARGO_HOME/bin:$PATH
|
||||
cargo_install_unless mdbook mdbook --help
|
||||
cargo_install_unless svgbob_cli svgbob --help
|
||||
|
||||
make -j"$(nproc)"
|
33
book/makefile
Normal file
33
book/makefile
Normal file
@ -0,0 +1,33 @@
|
||||
BOB_SRCS=$(wildcard art/*.bob)
|
||||
MD_SRCS=$(wildcard src/*.md)
|
||||
|
||||
SVG_IMGS=$(BOB_SRCS:art/%.bob=src/img/%.svg)
|
||||
|
||||
all: html/index.html
|
||||
|
||||
test: src/tests.ok
|
||||
|
||||
open: all
|
||||
mdbook build --open
|
||||
|
||||
watch: $(SVG_IMGS)
|
||||
mdbook watch
|
||||
|
||||
src/img/%.svg: art/%.bob
|
||||
@mkdir -p $(@D)
|
||||
svgbob < $< > $@
|
||||
|
||||
src/%.md: %.md
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
src/tests.ok: $(SVG_IMGS) $(MD_SRCS)
|
||||
mdbook test
|
||||
touch $@
|
||||
|
||||
html/index.html: src/tests.ok
|
||||
mdbook build
|
||||
|
||||
clean:
|
||||
rm -f $(SVG_IMGS) src/tests.ok
|
||||
rm -rf html
|
59
book/src/SUMMARY.md
Normal file
59
book/src/SUMMARY.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Solana Architecture
|
||||
|
||||
- [Introduction](introduction.md)
|
||||
|
||||
- [Terminology](terminology.md)
|
||||
|
||||
- [Getting Started](getting-started.md)
|
||||
- [Example: Web Wallet](webwallet.md)
|
||||
|
||||
- [Programming Model](programs.md)
|
||||
- [Example: Tic-Tac-Toe](tictactoe.md)
|
||||
- [Drones](drones.md)
|
||||
|
||||
- [A Solana Cluster](cluster.md)
|
||||
- [Synchronization](synchronization.md)
|
||||
- [Leader Rotation](leader-rotation.md)
|
||||
- [Fork Generation](fork-generation.md)
|
||||
- [Managing Forks](managing-forks.md)
|
||||
- [Data Plane Fanout](data-plane-fanout.md)
|
||||
- [Ledger Replication](ledger-replication.md)
|
||||
- [Secure Vote Signing](vote-signing.md)
|
||||
- [Staking Delegation and Rewards](stake-delegation-and-rewards.md)
|
||||
|
||||
- [Anatomy of a Fullnode](fullnode.md)
|
||||
- [TPU](tpu.md)
|
||||
- [TVU](tvu.md)
|
||||
- [Blocktree](blocktree.md)
|
||||
- [Gossip Service](gossip.md)
|
||||
- [The Runtime](runtime.md)
|
||||
|
||||
- [API Reference](api-reference.md)
|
||||
- [Blockstreamer](blockstreamer.md)
|
||||
- [JSON RPC API](jsonrpc-api.md)
|
||||
- [JavaScript API](javascript-api.md)
|
||||
- [solana-wallet CLI](wallet.md)
|
||||
|
||||
- [Proposed Architectural Changes](proposals.md)
|
||||
- [Ledger Replication](ledger-replication-to-implement.md)
|
||||
- [Secure Vote Signing](vote-signing-to-implement.md)
|
||||
- [Staking Rewards](staking-rewards.md)
|
||||
- [Fork Selection](fork-selection.md)
|
||||
- [Reliable Vote Transmission](reliable-vote-transmission.md)
|
||||
- [Persistent Account Storage](persistent-account-storage.md)
|
||||
- [Leader to Leader Transition](leader-leader-transition.md)
|
||||
- [Cluster Economics](ed_overview.md)
|
||||
- [Validation-client Economics](ed_validation_client_economics.md)
|
||||
- [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md)
|
||||
- [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md)
|
||||
- [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md)
|
||||
- [Validation Stake Delegation](ed_vce_validation_stake_delegation.md)
|
||||
- [Replication-client Economics](ed_replication_client_economics.md)
|
||||
- [Storage-replication Rewards](ed_rce_storage_replication_rewards.md)
|
||||
- [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md)
|
||||
- [Economic Sustainability](ed_economic_sustainability.md)
|
||||
- [Attack Vectors](ed_attack_vectors.md)
|
||||
- [References](ed_references.md)
|
||||
- [Leader-to-Validator Transition](leader-validator-transition.md)
|
||||
- [Cluster Test Framework](cluster-test-framework.md)
|
||||
- [Testing Programs](testing-programs.md)
|
4
book/src/api-reference.md
Normal file
4
book/src/api-reference.md
Normal file
@ -0,0 +1,4 @@
|
||||
# API Reference
|
||||
|
||||
The following sections contain API references material you may find useful
|
||||
when developing applications utilizing a Solana cluster.
|
84
book/src/block-confirmation.md
Normal file
84
book/src/block-confirmation.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Block Confirmation
|
||||
|
||||
A validator votes on a PoH hash for two purposes. First, the vote indicates it
|
||||
believes the ledger is valid up until that point in time. Second, since many
|
||||
valid forks may exist at a given height, the vote also indicates exclusive
|
||||
support for the fork. This document describes only the former. The latter is
|
||||
described in [fork selection](fork-selection.md).
|
||||
|
||||
## Current Design
|
||||
|
||||
To start voting, a validator first registers an account to which it will send
|
||||
its votes. It then sends votes to that account. The vote contains the tick
|
||||
height of the block it is voting on. The account stores the 32 highest heights.
|
||||
|
||||
### Problems
|
||||
|
||||
* Only the validator knows how to find its own votes directly.
|
||||
|
||||
Other components, such as the one that calculates confirmation time, needs to
|
||||
be baked into the fullnode code. The fullnode code queries the bank for all
|
||||
accounts owned by the vote program.
|
||||
|
||||
* Voting ballots do not contain a PoH hash. The validator is only voting that
|
||||
it has observed an arbitrary block at some height.
|
||||
|
||||
* Voting ballots do not contain a hash of the bank state. Without that hash,
|
||||
there is no evidence that the validator executed the transactions and
|
||||
verified there were no double spends.
|
||||
|
||||
## Proposed Design
|
||||
|
||||
### No Cross-block State Initially
|
||||
|
||||
At the moment a block is produced, the leader shall add a NewBlock transaction
|
||||
to the ledger with a number of tokens that represents the validation reward.
|
||||
It is effectively an incremental multisig transaction that sends tokens from
|
||||
the mining pool to the validators. The account should allocate just enough
|
||||
space to collect the votes required to achieve a supermajority. When a
|
||||
validator observes the NewBlock transaction, it has the option to submit a vote
|
||||
that includes a hash of its ledger state (the bank state). Once the account has
|
||||
sufficient votes, the vote program should disperse the tokens to the
|
||||
validators, which causes the account to be deleted.
|
||||
|
||||
#### Logging Confirmation Time
|
||||
|
||||
The bank will need to be aware of the vote program. After each transaction, it
|
||||
should check if it is a vote transaction and if so, check the state of that
|
||||
account. If the transaction caused the supermajority to be achieved, it should
|
||||
log the time since the NewBlock transaction was submitted.
|
||||
|
||||
### Finality and Payouts
|
||||
|
||||
Locktower is the proposed [fork selection](fork-selection.md) algorithm. It
|
||||
proposes that payment to miners be postponed until the *stack* of validator
|
||||
votes reaches a certain depth, at which point rollback is not economically
|
||||
feasible. The vote program may therefore implement locktower. Vote instructions
|
||||
would need to reference a global locktower account so that it can track
|
||||
cross-block state.
|
||||
|
||||
## Challenges
|
||||
|
||||
### On-chain voting
|
||||
|
||||
Using programs and accounts to implement this is a bit tedious. The hardest
|
||||
part is figuring out how much space to allocate in NewBlock. The two variables
|
||||
are the *active set* and the stakes of those validators. If we calculate the
|
||||
active set at the time NewBlock is submitted, the number of validators to
|
||||
allocate space for is known upfront. If, however, we allow new validators to
|
||||
vote on old blocks, then we'd need a way to allocate space dynamically.
|
||||
|
||||
Similar in spirit, if the leader caches stakes at the time of NewBlock, the
|
||||
vote program doesn't need to interact with the bank when it processes votes. If
|
||||
we don't, then we have the option to allow stakes to float until a vote is
|
||||
submitted. A validator could conceivably reference its own staking account, but
|
||||
that'd be the current account value instead of the account value of the most
|
||||
recently finalized bank state. The bank currently doesn't offer a means to
|
||||
reference accounts from particular points in time.
|
||||
|
||||
### Voting Implications on Previous Blocks
|
||||
|
||||
Does a vote on one height imply a vote on all blocks of lower heights of
|
||||
that fork? If it does, we'll need a way to lookup the accounts of all
|
||||
blocks that haven't yet reached supermajority. If not, the validator could
|
||||
send votes to all blocks explicitly to get the block rewards.
|
37
book/src/blockstreamer.md
Normal file
37
book/src/blockstreamer.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Blockstreamer
|
||||
|
||||
Solana supports a node type called an *blockstreamer*. This fullnode 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/fullnode-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
|
||||
* `id`, the block id, as base-58 encoded string
|
102
book/src/blocktree.md
Normal file
102
book/src/blocktree.md
Normal file
@ -0,0 +1,102 @@
|
||||
# Blocktree
|
||||
|
||||
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](fork-generation.md). The *blocktree* data structure
|
||||
described here is how a validator copes with those forks until blocks
|
||||
are finalized.
|
||||
|
||||
The blocktree allows a validator to record every blob it observes
|
||||
on the network, in any order, as long as the blob is signed by the expected
|
||||
leader for a given slot.
|
||||
|
||||
Blobs are moved to a fork-able key space the tuple of `leader slot` + `blob
|
||||
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 blobs are served out of RAM or recent files and out
|
||||
of deeper storage for less recent blobs, as implemented by the store backing
|
||||
Blocktree.
|
||||
|
||||
### Functionalities of Blocktree
|
||||
|
||||
1. Persistence: the Blocktree lives in the front of the nodes verification
|
||||
pipeline, right behind network receive and signature verification. If the
|
||||
blob 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
|
||||
blob that's been received. Blocktree stores blobs with signatures,
|
||||
preserving the chain of origination.
|
||||
3. Forks: Blocktree supports random access of blobs, so can support a
|
||||
validator's need to rollback and replay from a Bank checkpoint.
|
||||
4. Restart: with proper pruning/culling, the Blocktree 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 Blocktree.
|
||||
|
||||
### Blocktree Design
|
||||
|
||||
1. Entries in the Blocktree are stored as key-value pairs, where the key is the concatenated
|
||||
slot index and blob index for an entry, and the value is the entry data. Note blob indexes are zero-based for each slot (i.e. they're slot-relative).
|
||||
|
||||
2. The Blocktree 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 blob index `n`, such that for all `m < n`, there exists a blob in this slot with blob index equal to `n` (i.e. the highest consecutive blob index).
|
||||
* `received` - The highest received blob 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 blob that is flagged as the last blob for this slot. This flag on a blob will be set by the leader for a slot when they are transmitting the last blob 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 blob for a new slot `x` arrives, we check the number of blocks (`num_blocks`) for that new slot (this information is encoded in the blob). We then know that this new slot chains to slot `x - num_blocks`.
|
||||
|
||||
4. Subscriptions - The Blocktree records a set of slots that have been "subscribed" to. This means entries that chain to these slots will be sent on the Blocktree channel for consumption by the ReplayStage. See the `Blocktree APIs` for details.
|
||||
|
||||
5. Update notifications - The Blocktree notifies listeners when slot(n).is_rooted is flipped from false to true for any `n`.
|
||||
|
||||
### Blocktree APIs
|
||||
|
||||
The Blocktree 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 Blocktree. 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`.
|
||||
|
||||
2. `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 Blocktree.
|
||||
|
||||
### 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 Blocktree 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 Blocktree
|
||||
|
||||
Once Blocktree 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 Blocktree contents
|
||||
that are not on the PoH chain for that vote for can be pruned, expunged.
|
||||
|
||||
Replicator nodes will be responsible for storing really old ledger contents,
|
||||
and validators need only persist their bank periodically.
|
122
book/src/cluster-test-framework.md
Normal file
122
book/src/cluster-test-framework.md
Normal file
@ -0,0 +1,122 @@
|
||||
# Cluster Test Framework
|
||||
|
||||
This document proposes the Cluster Test Framework (CTF). CTF is a test harness
|
||||
that allows tests to execute against a local, in-process cluster or a
|
||||
deployed cluster.
|
||||
|
||||
## Motivation
|
||||
|
||||
The goal of CTF is to provide a framework for writing tests independent of where
|
||||
and how the cluster is deployed. Regressions can be captured in these tests and
|
||||
the tests can be run against deployed clusters to verify the deployment. The
|
||||
focus of these tests should be on cluster stability, consensus, fault tolerance,
|
||||
API stability.
|
||||
|
||||
Tests should verify a single bug or scenario, and should be written with the
|
||||
least amount of internal plumbing exposed to the test.
|
||||
|
||||
## Design Overview
|
||||
|
||||
Tests are provided an entry point, which is a `contact_info::ContactInfo`
|
||||
structure, and a keypair that has already been funded.
|
||||
|
||||
Each node in the cluster is configured with a `fullnode::FullnodeConfig` at boot
|
||||
time. At boot time this configuration specifies any extra cluster configuration
|
||||
required for the test. The cluster should boot with the configuration when it
|
||||
is run in-process or in a data center.
|
||||
|
||||
Once booted, the test will discover the cluster through a gossip entry point and
|
||||
configure any runtime behaviors via fullnode RPC.
|
||||
|
||||
## Test Interface
|
||||
|
||||
Each CTF test starts with an opaque entry point and a funded keypair. The test
|
||||
should not depend on how the cluster is deployed, and should be able to exercise
|
||||
all the cluster functionality through the publicly available interfaces.
|
||||
|
||||
```rust,ignore
|
||||
use crate::contact_info::ContactInfo;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
pub fn test_this_behavior(
|
||||
entry_point_info: &ContactInfo,
|
||||
funding_keypair: &Keypair,
|
||||
num_nodes: usize,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## Cluster Discovery
|
||||
|
||||
At test start, the cluster has already been established and is fully connected.
|
||||
The test can discover most of the available nodes over a few second.
|
||||
|
||||
```rust,ignore
|
||||
use crate::gossip_service::discover;
|
||||
|
||||
// Discover the cluster over a few seconds.
|
||||
let cluster_nodes = discover(&entry_point_info, num_nodes);
|
||||
```
|
||||
|
||||
## Cluster Configuration
|
||||
|
||||
To enable specific scenarios, the cluster needs to be booted with special
|
||||
configurations. These configurations can be captured in
|
||||
`fullnode::FullnodeConfig`.
|
||||
|
||||
For example:
|
||||
|
||||
```rust,ignore
|
||||
let mut fullnode_config = FullnodeConfig::default();
|
||||
fullnode_config.rpc_config.enable_fullnode_exit = true;
|
||||
let local = LocalCluster::new_with_config(
|
||||
num_nodes,
|
||||
10_000,
|
||||
100,
|
||||
&fullnode_config
|
||||
);
|
||||
```
|
||||
|
||||
## How to design a new test
|
||||
|
||||
For example, there is a bug that shows that the cluster fails when it is flooded
|
||||
with invalid advertised gossip nodes. Our gossip library and protocol may
|
||||
change, but the cluster still needs to stay resilient to floods of invalid
|
||||
advertised gossip nodes.
|
||||
|
||||
Configure the RPC service:
|
||||
|
||||
```rust,ignore
|
||||
let mut fullnode_config = FullnodeConfig::default();
|
||||
fullnode_config.rpc_config.enable_rpc_gossip_push = true;
|
||||
fullnode_config.rpc_config.enable_rpc_gossip_refresh_active_set = true;
|
||||
```
|
||||
|
||||
Wire the RPCs and write a new test:
|
||||
|
||||
```rust,ignore
|
||||
pub fn test_large_invalid_gossip_nodes(
|
||||
entry_point_info: &ContactInfo,
|
||||
funding_keypair: &Keypair,
|
||||
num_nodes: usize,
|
||||
) {
|
||||
let cluster = discover(&entry_point_info, num_nodes);
|
||||
|
||||
// Poison the cluster.
|
||||
let mut client = mk_client(&entry_point_info);
|
||||
for _ in 0..(num_nodes * 100) {
|
||||
client.gossip_push(
|
||||
cluster_info::invalid_contact_info()
|
||||
);
|
||||
}
|
||||
sleep(Durration::from_millis(1000));
|
||||
|
||||
// Force refresh of the active set.
|
||||
for node in &cluster {
|
||||
let mut client = mk_client(&node);
|
||||
client.gossip_refresh_active_set();
|
||||
}
|
||||
|
||||
// Verify that spends still work.
|
||||
verify_spends(&cluster);
|
||||
}
|
||||
```
|
100
book/src/cluster.md
Normal file
100
book/src/cluster.md
Normal file
@ -0,0 +1,100 @@
|
||||
# A Solana Cluster
|
||||
|
||||
A Solana cluster is a set of fullnodes working together to serve client
|
||||
transactions and maintain the integrity of the ledger. Many clusters may
|
||||
coexist. When two clusters share a common genesis block, they attempt to
|
||||
converge. Otherwise, they simply ignore the existence of the other.
|
||||
Transactions sent to the wrong one are quietly rejected. In this chapter, we'll
|
||||
discuss how a cluster is created, how nodes join the cluster, how they share
|
||||
the ledger, how they ensure the ledger is replicated, and how they cope with
|
||||
buggy and malicious nodes.
|
||||
|
||||
## Creating a Cluster
|
||||
|
||||
Before starting any fullnodes, one first needs to create a *genesis block*.
|
||||
The block contains entries referencing two public keys, a *mint* and a
|
||||
*bootstrap leader*. The fullnode holding the bootstrap leader's secret key is
|
||||
responsible for appending the first entries to the ledger. It initializes its
|
||||
internal state with the mint's account. That account will hold the number of
|
||||
native tokens defined by the genesis block. The second fullnode then contacts
|
||||
the bootstrap leader to register as a *validator* or *replicator*. Additional
|
||||
fullnodes then register with any registered member of the cluster.
|
||||
|
||||
A validator receives all entries from the leader and submits votes confirming
|
||||
those entries are valid. After voting, the validator is expected to store those
|
||||
entries until replicator nodes submit proofs that they have stored copies of
|
||||
it. Once the validator observes a sufficient number of copies exist, it deletes
|
||||
its copy.
|
||||
|
||||
## Joining a Cluster
|
||||
|
||||
Fullnodes and replicators enter the cluster via registration messages sent to
|
||||
its *control plane*. The control plane is implemented using a *gossip*
|
||||
protocol, meaning that a node may register with any existing node, and expect
|
||||
its registration to propagate to all nodes in the cluster. The time it takes
|
||||
for all nodes to synchronize is proportional to the square of the number of
|
||||
nodes participating in the cluster. Algorithmically, that's considered very
|
||||
slow, but in exchange for that time, a node is assured that it eventually has
|
||||
all the same information as every other node, and that that information cannot
|
||||
be censored by any one node.
|
||||
|
||||
## Sending Transactions to a Cluster
|
||||
|
||||
Clients send transactions to any fullnode's Transaction Processing Unit (TPU)
|
||||
port. If the node is in the validator role, it forwards the transaction to the
|
||||
designated leader. If in the leader role, the node bundles incoming
|
||||
transactions, timestamps them creating an *entry*, and pushes them onto the
|
||||
cluster's *data plane*. Once on the data plane, the transactions are validated
|
||||
by validator nodes and replicated by replicator nodes, effectively appending
|
||||
them to the ledger.
|
||||
|
||||
## Confirming Transactions
|
||||
|
||||
A Solana cluster is capable of subsecond *confirmation* for up to 150 nodes
|
||||
with plans to scale up to hundreds of thousands of nodes. Once fully
|
||||
implemented, confirmation times are expected to increase only with the
|
||||
logarithm of the number of validators, where the logarithm's base is very high.
|
||||
If the base is one thousand, for example, it means that for the first thousand
|
||||
nodes, confirmation will be the duration of three network hops plus the time it
|
||||
takes the slowest validator of a supermajority to vote. For the next million
|
||||
nodes, confirmation increases by only one network hop.
|
||||
|
||||
Solana defines confirmation as the duration of time from when the leader
|
||||
timestamps a new entry to the moment when it recognizes a supermajority of
|
||||
ledger votes.
|
||||
|
||||
A gossip network is much too slow to achieve subsecond confirmation once the
|
||||
network grows beyond a certain size. The time it takes to send messages to all
|
||||
nodes is proportional to the square of the number of nodes. If a blockchain
|
||||
wants to achieve low confirmation and attempts to do it using a gossip network,
|
||||
it will be forced to centralize to just a handful of nodes.
|
||||
|
||||
Scalable confirmation can be achieved using the follow combination of
|
||||
techniques:
|
||||
|
||||
1. Timestamp transactions with a VDF sample and sign the timestamp.
|
||||
2. Split the transactions into batches, send each to separate nodes and have
|
||||
each node share its batch with its peers.
|
||||
3. Repeat the previous step recursively until all nodes have all batches.
|
||||
|
||||
Solana rotates leaders at fixed intervals, called *slots*. Each leader may only
|
||||
produce entries during its allotted slot. The leader therefore timestamps
|
||||
transactions so that validators may lookup the public key of the designated
|
||||
leader. The leader then signs the timestamp so that a validator may verify the
|
||||
signature, proving the signer is owner of the designated leader's public key.
|
||||
|
||||
Next, transactions are broken into batches so that a node can send transactions
|
||||
to multiple parties without making multiple copies. If, for example, the leader
|
||||
needed to send 60 transactions to 6 nodes, it would break that collection of 60
|
||||
into batches of 10 transactions and send one to each node. This allows the
|
||||
leader to put 60 transactions on the wire, not 60 transactions for each node.
|
||||
Each node then shares its batch with its peers. Once the node has collected all
|
||||
6 batches, it reconstructs the original set of 60 transactions.
|
||||
|
||||
A batch of transactions can only be split so many times before it is so small
|
||||
that header information becomes the primary consumer of network bandwidth. At
|
||||
the time of this writing, the approach is scaling well up to about 150
|
||||
validators. To scale up to hundreds of thousands of validators, each node can
|
||||
apply the same technique as the leader node to another set of nodes of equal
|
||||
size. We call the technique *data plane fanout*; learn more in the [data plan
|
||||
fanout](data-plane-fanout.md) section.
|
84
book/src/data-plane-fanout.md
Normal file
84
book/src/data-plane-fanout.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Data Plane Fanout
|
||||
|
||||
A Solana cluster uses a multi-layer mechanism called *data plane fanout* to
|
||||
broadcast transaction blobs to all nodes in a very quick and efficient manner.
|
||||
In order to establish the fanout, the cluster divides itself into small
|
||||
collections of nodes, called *neighborhoods*. Each node is responsible for
|
||||
sharing any data it receives with the other nodes in its neighborhood, as well
|
||||
as propagating the data on to a small set of nodes in other neighborhoods.
|
||||
|
||||
During its slot, the leader node distributes blobs between the validator nodes
|
||||
in one neighborhood (layer 1). Each validator shares its data within its
|
||||
neighborhood, but also retransmits the blobs to one node in each of multiple
|
||||
neighborhoods in the next layer (layer 2). The layer-2 nodes each share their
|
||||
data with their neighborhood peers, and retransmit to nodes in the next layer,
|
||||
etc, until all nodes in the cluster have received all the blobs.
|
||||
|
||||
<img alt="Two layer cluster" src="img/data-plane.svg" class="center"/>
|
||||
|
||||
## Neighborhood Assignment - Weighted Selection
|
||||
|
||||
In order for data plane fanout to work, the entire cluster must agree on how the
|
||||
cluster is divided into neighborhoods. To achieve this, all the recognized
|
||||
validator nodes (the TVU peers) are sorted by stake and stored in a list. This
|
||||
list is then indexed in different ways to figure out neighborhood boundaries and
|
||||
retransmit peers. For example, the leader will simply select the first nodes to
|
||||
make up layer 1. These will automatically be the highest stake holders, allowing
|
||||
the heaviest votes to come back to the leader first. Layer-1 and lower-layer
|
||||
nodes use the same logic to find their neighbors and lower layer peers.
|
||||
|
||||
## Layer and Neighborhood Structure
|
||||
|
||||
The current leader makes its initial broadcasts to at most `DATA_PLANE_FANOUT`
|
||||
nodes. If this layer 1 is smaller than the number of nodes in the cluster, then
|
||||
the data plane fanout mechanism adds layers below. Subsequent layers follow
|
||||
these constraints to determine layer-capacity: Each neighborhood contains
|
||||
`NEIGHBORHOOD_SIZE` nodes and each layer may have up to `DATA_PLANE_FANOUT/2`
|
||||
neighborhoods.
|
||||
|
||||
As mentioned above, each node in a layer only has to broadcast its blobs to its
|
||||
neighbors and to exactly 1 node in each next-layer neighborhood, instead of to
|
||||
every TVU peer in the cluster. In the default mode, each layer contains
|
||||
`DATA_PLANE_FANOUT/2` neighborhoods. The retransmit mechanism also supports a
|
||||
second, `grow`, mode of operation that squares the number of neighborhoods
|
||||
allowed each layer. This dramatically reduces the number of layers needed to
|
||||
support a large cluster, but can also have a negative impact on the network
|
||||
pressure on each node in the lower layers. A good way to think of the default
|
||||
mode (when `grow` is disabled) is to imagine it as chain of layers, where the
|
||||
leader sends blobs to layer-1 and then layer-1 to layer-2 and so on, the `layer
|
||||
capacities` remain constant, so all layers past layer-2 will have the same
|
||||
number of nodes until the whole cluster is covered. When `grow` is enabled, this
|
||||
becomes a traditional fanout where layer-3 will have the square of the number of
|
||||
nodes in layer-2 and so on.
|
||||
|
||||
#### Configuration Values
|
||||
|
||||
`DATA_PLANE_FANOUT` - Determines the size of layer 1. Subsequent
|
||||
layers have `DATA_PLANE_FANOUT/2` neighborhoods when `grow` is inactive.
|
||||
|
||||
`NEIGHBORHOOD_SIZE` - The number of nodes allowed in a neighborhood.
|
||||
Neighborhoods will fill to capacity before new ones are added, i.e if a
|
||||
neighborhood isn't full, it _must_ be the last one.
|
||||
|
||||
`GROW_LAYER_CAPACITY` - Whether or not retransmit should be behave like a
|
||||
_traditional fanout_, i.e if each additional layer should have growing
|
||||
capacities. When this mode is disabled (default), all layers after layer 1 have
|
||||
the same capacity, keeping the network pressure on all nodes equal.
|
||||
|
||||
Currently, configuration is set when the cluster is launched. In the future,
|
||||
these parameters may be hosted on-chain, allowing modification on the fly as the
|
||||
cluster sizes change.
|
||||
|
||||
## Neighborhoods
|
||||
|
||||
The following diagram shows how two neighborhoods in different layers interact.
|
||||
What this diagram doesn't capture is that each neighbor actually receives
|
||||
blobs from one validator per neighborhood above it. This means that, to
|
||||
cripple a neighborhood, enough nodes (erasure codes +1 per neighborhood) from
|
||||
the layer above need to fail. Since multiple neighborhoods exist in the upper
|
||||
layer and a node will receive blobs from a node in each of those neighborhoods,
|
||||
we'd need a big network failure in the upper layers to end up with incomplete
|
||||
data.
|
||||
|
||||
<img alt="Inner workings of a neighborhood"
|
||||
src="img/data-plane-neighborhood.svg" class="center"/>
|
86
book/src/drones.md
Normal file
86
book/src/drones.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Creating Signing Services with Drones
|
||||
|
||||
This chapter defines an off-chain service called a *drone*, which acts as
|
||||
custodian of a user's private key. In its simplest form, it can be used to
|
||||
create *airdrop* transactions, a token transfer from the drone's account to a
|
||||
client's account.
|
||||
|
||||
## Signing Service
|
||||
|
||||
A drone is a simple signing service. It listens for requests to sign
|
||||
*transaction data*. Once received, the drone validates the request however it
|
||||
sees fit. It may, for example, only accept transaction data with a
|
||||
`SystemInstruction::Move` instruction transferring only up to a certain amount
|
||||
of tokens. If the drone accepts the transaction, it returns an `Ok(Signature)`
|
||||
where `Signature` is a signature of the transaction data using the drone's
|
||||
private key. If it rejects the transaction data, it returns a `DroneError`
|
||||
describing why.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### Granting access to an on-chain game
|
||||
|
||||
Creator of on-chain game tic-tac-toe hosts a drone that responds to airdrop
|
||||
requests containing an `InitGame` instruction. The drone signs the transaction
|
||||
data in the request and returns it, thereby authorizing its account to pay the
|
||||
transaction fee and as well as seeding the game's account with enough tokens to
|
||||
play it. The user then creates a transaction for its transaction data and the
|
||||
drones signature and submits it to the Solana cluster. Each time the user
|
||||
interacts with the game, the game pays the user enough tokens to pay the next
|
||||
transaction fee to advance the game. At that point, the user may choose to keep
|
||||
the tokens instead of advancing the game. If the creator wants to defend
|
||||
against that case, they could require the user to return to the drone to sign
|
||||
each instruction.
|
||||
|
||||
### Worldwide airdrop of a new token
|
||||
|
||||
Creator of a new on-chain token (ERC-20 interface), may wish to do a worldwide
|
||||
airdrop to distribute its tokens to millions of users over just a few seconds.
|
||||
That drone cannot spend resources interacting with the Solana cluster. Instead,
|
||||
the drone should only verify the client is unique and human, and then return
|
||||
the signature. It may also want to listen to the Solana cluster for recent
|
||||
entry IDs to support client retries and to ensure the airdrop is targeting the
|
||||
desired cluster.
|
||||
|
||||
|
||||
## Attack vectors
|
||||
|
||||
### Invalid recent_blockhash
|
||||
|
||||
The drone may prefer its airdrops only target a particular Solana cluster. To
|
||||
do that, it listens to the cluster for new entry IDs and ensure any requests
|
||||
reference a recent one.
|
||||
|
||||
Note: to listen for new entry IDs assumes the drone is either a fullnode or a
|
||||
*light* client. At the time of this writing, light clients have not been
|
||||
implemented and no proposal describes them. This document assumes one of the
|
||||
following approaches be taken:
|
||||
|
||||
1. Define and implement a light client
|
||||
2. Embed a fullnode
|
||||
3. Query the jsonrpc API for the latest last id at a rate slightly faster than
|
||||
ticks are produced.
|
||||
|
||||
### Double spends
|
||||
|
||||
A client may request multiple airdrops before the first has been submitted to
|
||||
the ledger. The client may do this maliciously or simply because it thinks the
|
||||
first request was dropped. The drone should not simply query the cluster to
|
||||
ensure the client has not already received an airdrop. Instead, it should use
|
||||
`recent_blockhash` to ensure the previous request is expired before signing another.
|
||||
Note that the Solana cluster will reject any transaction with a `recent_blockhash`
|
||||
beyond a certain *age*.
|
||||
|
||||
### Denial of Service
|
||||
|
||||
If the transaction data size is smaller than the size of the returned signature
|
||||
(or descriptive error), a single client can flood the network. Considering
|
||||
that a simple `Move` operation requires two public keys (each 32 bytes) and a
|
||||
`fee` field, and that the returned signature is 64 bytes (and a byte to
|
||||
indicate `Ok`), consideration for this attack may not be required.
|
||||
|
||||
In the current design, the drone accepts TCP connections. This allows clients
|
||||
to DoS the service by simply opening lots of idle connections. Switching to UDP
|
||||
may be preferred. The transaction data will be smaller than a UDP packet since
|
||||
the transaction sent to the Solana cluster is already pinned to using UDP.
|
11
book/src/ed_attack_vectors.md
Normal file
11
book/src/ed_attack_vectors.md
Normal file
@ -0,0 +1,11 @@
|
||||
## Attack Vectors
|
||||
|
||||
### Colluding validation and replication clients
|
||||
|
||||
A colluding validation-client, may take the strategy to mark PoReps from non-colluding replicator nodes as invalid as an attempt to maximize the rewards for the colluding replicator nodes. In this case, it isn’t feasible for the offended-against replicator nodes to petition the network for resolution as this would result in a network-wide vote on each offending PoRep and create too much overhead for the network to progress adequately. Also, this mitigation attempt would still be vulnerable to a >= 51% staked colluder.
|
||||
|
||||
Alternatively, transaction fees from submitted PoReps are pooled and distributed across validation-clients in proportion to the number of valid PoReps discounted by the number of invalid PoReps as voted by each validator-client. Thus invalid votes are directly dis-incentivized through this reward channel. Invalid votes that are revealed by replicator nodes as fishing PoReps, will not be discounted from the payout PoRep count.
|
||||
|
||||
Another collusion attack involves a validator-client who may take the strategy to ignore invalid PoReps from colluding replicator and vote them as valid. In this case, colluding replicator-clients would not have to store the data while still receiving rewards for validated PoReps. Additionally, colluding validator nodes would also receive rewards for validating these PoReps. To mitigate this attack, validators must randomly sample PoReps corresponding to the ledger block they are validating and because of this, there will be multiple validators that will receive the colluding replicator’s invalid submissions. These non-colluding validators will be incentivized to mark these PoReps as invalid as they have no way to determine whether the proposed invalid PoRep is actually a fishing PoRep, for which a confirmation vote would result in the validator’s stake being slashed.
|
||||
|
||||
In this case, the proportion of time a colluding pair will be successful has an upper limit determined by the % of stake of the network claimed by the colluding validator. This also sets bounds to the value of such an attack. For example, if a colluding validator controls 10% of the total validator stake, transaction fees will be lost (likely sent to mining pool) by the colluding replicator 90% of the time and so the attack vector is only profitable if the per-PoRep reward at least 90% higher than the average PoRep transaction fee. While, probabilistically, some colluding replicator-client PoReps will find their way to colluding validation-clients, the network can also monitor rates of paired (validator + replicator) discrepancies in voting patterns and censor identified colluders in these cases.
|
18
book/src/ed_economic_sustainability.md
Normal file
18
book/src/ed_economic_sustainability.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Economic Sustainability
|
||||
|
||||
Long term economic sustainability is one of the guiding principles of Solana’s economic design. While it is impossible to predict how decentralized economies will develop over time, especially economies with flexible decentralized governances, we can arrange economic components such that, under certain conditions, a sustainable economy may take shape in the long term. In the case of Solana’s network, these components take the form of the remittances and deposits into and out of the reserve ‘mining pool’.
|
||||
|
||||
The dominant remittances from the Solana mining pool are validator and replicator rewards. The deposit mechanism is a flat, protocol-specified and adjusted, % of each transaction fee.
|
||||
|
||||
The Replicator rewards are to be delivered to replicators from the mining pool after successful PoRep validation. The per-PoRep reward amount is determined as a function of the total network storage redundancy at the time of the PoRep validation and the network goal redundancy. This function is likely to take the form of a discount from a base reward to be delivered when the network has achieved and maintained its goal redundancy. An example of such a reward function is shown in **Figure 3**
|
||||
|
||||
<!--  -->
|
||||
<p style="text-align:center;"><img src="img/porep_reward.png" alt="==PoRep Reward Curve ==" width="800"/></p>
|
||||
|
||||
**Figure 3**: Example PoRep reward design as a function of global network storage redundancy.
|
||||
|
||||
In the example shown in Figure 1, multiple per PoRep base rewards are explored (as a % of Tx Fee) to be delivered when the global ledger replication redundancy meets 10X. When the global ledger replication redundancy is less than 10X, the base reward is discounted as a function of the square of the ratio of the actual ledger replication redundancy to the goal redundancy (i.e. 10X).
|
||||
|
||||
The other protocol-based remittance goes to validation-clients as a reward distributed in proportion to stake-weight for voting to validate the ledger state. The functional issuance of this reward is described in [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md) and is designed to reduce over time until validators are incentivized solely through collection of transaction fees. Therefore, in the long-run, protocol-based rewards to replication-nodes will be the only remittances from the mining pool, and will have to be countered by the portion of each non-PoRep transaction fee that is directed back into the mining pool. I.e. for a long-term self-sustaining economy, replicator-client rewards must be subsidized through a minimum fee on each non-PoRep transaction pre-allocated to the mining pool. Through this constraint, we can write the following inequality:
|
||||
|
||||
**== WIP [here](https://docs.google.com/document/d/1HBDasdkjS4Ja9wC_tIUsZPVcxGAWTuYOq9zf6xoQNps/edit?usp=sharing) ==**
|
16
book/src/ed_overview.md
Normal file
16
book/src/ed_overview.md
Normal file
@ -0,0 +1,16 @@
|
||||
## Economic Design Overview
|
||||
|
||||
Solana’s crypto-economic system is designed to promote a healthy, long term self-sustaining economy with participant incentives aligned to the security and decentralization of the network. The main participants in this economy are validation-clients and replication-clients. Their contributions to the network, state validation and data storage respectively, and their requisite remittance mechanisms are discussed below.
|
||||
|
||||
The main channels of participant remittances are referred to as protocol-based rewards and transaction fees. Protocol-based rewards are protocol-derived issuances from a network-controlled reserve of tokens (sometimes referred to as the ‘mining pool’). These rewards will constitute the total reward delivered to replication clients and a portion of the total rewards for validation clients, the remaining sourced from transaction fees. In the early days of the network, it is likely that protocol-based rewards, deployed based on predefined issuance schedule, will drive the majority of participant incentives to join the network.
|
||||
|
||||
These protocol-based rewards, to be distributed to participating validation and replication clients, are to be specified as annual interest rates calculated per, real-time, Solana epoch [DEFINITION]. As discussed further below, the issuance rates are determined as a function of total network validator staked percentage and total replication provided by replicators in each previous epoch. The choice for validator and replicator client rewards to be based on participation rates, rather than a global fixed inflation or interest rate, emphasizes a protocol priority of overall economic security, rather than monetary supply predictability. Due to Solana’s hard total supply cap of 1B tokens and the bounds of client participant rates in the protocol, we believe that global interest, and supply issuance, scenarios should be able to be modeled with reasonable uncertainties.
|
||||
|
||||
Transaction fees are market-based participant-to-participant transfers, attached to network interactions as a necessary motivation and compensation for the inclusion and execution of a proposed transaction (be it a state execution or proof-of-replication verification). A mechanism for continuous and long-term funding of the mining pool through a pre-dedicated portion of transaction fees is also discussed below.
|
||||
|
||||
A high-level schematic of Solana’s crypto-economic design is shown below in **Figure 1**. The specifics of validation-client economics are described in sections: [Validation-client Economics](ed_validation_client_economics.md), [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md), [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md) and [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md). Also, the chapter titled [Validation Stake Delegation](ed_vce_validation_stake_delegation.md) closes with a discussion of validator delegation opportunties and marketplace. The [Replication-client Economics](ed_replication_client_economics.md) chapter will review the Solana network design for global ledger storage/redundancy and replicator-client economics ([Storage-replication rewards](ed_rce_storage_replication_rewards.md)) along with a replicator-to-validator delegation mechanism designed to aide participant on-boarding into the Solana economy discussed in [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md). The [Economic Sustainability](ed_economic_sustainability.md) section dives deeper into Solana’s design for long-term economic sustainability and outlines the constraints and conditions for a self-sustaining economy. Finally, in chapter [Attack Vectors](ed_attack_vectors.md), various attack vectors will be described and potential vulnerabilities explored and parameterized.
|
||||
|
||||
<!--  -->
|
||||
<p style="text-align:center;"><img src="img/solana_economic_design.png" alt="== Solana Economic Design Diagram ==" width="800"/></p>
|
||||
|
||||
**Figure 1**: Schematic overview of Solana economic incentive design.
|
@ -0,0 +1,5 @@
|
||||
### Replication-client Reward Auto-delegation
|
||||
|
||||
The ability for Solana network participant’s to earn rewards by providing storage service is a unique on-boarding path that requires little hardware overhead and minimal upfront capital. It offers an avenue for individuals with extra-storage space on their home laptops or PCs to contribute to the security of the network and become integrated into the Solana economy.
|
||||
|
||||
To enhance this on-boarding ramp and facilitate further participation and investment in the Solana economy, replication-clients have the opportunity to auto-delegate their rewards to validation-clients of their choice. Much like the automatic reinvestment of stock dividends, in this scenario, a replicator-client can earn Solana tokens by providing some storage capacity to the network (i.e. via submitting valid PoReps), have the protocol-based rewards automatically assigned as delegation to a staked validator node and therefore earning interest in the validation-client reward pool.
|
5
book/src/ed_rce_storage_replication_rewards.md
Normal file
5
book/src/ed_rce_storage_replication_rewards.md
Normal file
@ -0,0 +1,5 @@
|
||||
### Storage-replication Rewards
|
||||
|
||||
Replicator-clients download, encrypt and submit PoReps for ledger block sections.3 PoReps submitted to the PoH stream, and subsequently validated, function as evidence that the submitting replicator client is indeed storing the assigned ledger block sections on local hard drive space as a service to the network. Therefore, replicator clients should earn protocol rewards proportional to the amount of storage, and the number of successfully validated PoReps, that they are verifiably providing to the network.
|
||||
|
||||
Additionally, replicator clients have the opportunity to capture a portion of slashed bounties [TBD] of dishonest validator clients. This can be accomplished by a replicator client submitting a verifiably false PoRep for which a dishonest validator client receives and signs as a valid PoRep. This reward incentive is to prevent lazy validators and minimize validator-replicator collusion attacks, more on this below.
|
7
book/src/ed_references.md
Normal file
7
book/src/ed_references.md
Normal file
@ -0,0 +1,7 @@
|
||||
## References
|
||||
|
||||
1. [https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/](https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/)
|
||||
|
||||
2. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281)
|
||||
|
||||
3. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281)
|
3
book/src/ed_replication_client_economics.md
Normal file
3
book/src/ed_replication_client_economics.md
Normal file
@ -0,0 +1,3 @@
|
||||
## Replication-client economics
|
||||
|
||||
Replication-clients should be rewarded for providing the network with storage space. Incentivization of the set of replicators provides data security through redundancy of the historical ledger. Replication nodes are rewarded in proportion to the amount of ledger data storage provided. These rewards are captured by generating and entering Proofs of Replication (PoReps) into the PoH stream which can be validated by Validation nodes as described above in the [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md) chapter.
|
3
book/src/ed_validation_client_economics.md
Normal file
3
book/src/ed_validation_client_economics.md
Normal file
@ -0,0 +1,3 @@
|
||||
## Validation-client Economics
|
||||
|
||||
Validator-clients are eligible to receive protocol-based (i.e. via mining pool) rewards issued via stake-based annual interest rates by providing compute (CPU+GPU) resources to validate and vote on a given PoH state. These protocol-based rewards are determined through an algorithmic schedule as a function of total amount of Solana tokens staked in the system and duration since network launch (genesis block). Additionally, these clients may earn revenue through two types of transaction fees: state-validation transaction fees and pooled Proof-of-Replication (PoRep) transaction fees. The distribution of these two types of transaction fees to the participating validation set are designed independently as economic goals and attack vectors are unique between the state- generation/validation mechanism and the ledger replication/validation mechanism. For clarity, we separately describe the design and motivation of the three types of potential revenue streams for validation-clients below: state-validation protocol-based rewards, state-validation transaction fees and PoRep-validation transaction fees.
|
@ -0,0 +1,9 @@
|
||||
### Replication-validation Transaction Fees
|
||||
|
||||
As previously mentioned, validator-clients will also be responsible for validating PoReps submitted into the PoH stream by replicator-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.2
|
||||
|
||||
While replication-clients are incentivized and rewarded through protocol-based rewards schedule (see [Replication-client Economics](ed_replication_client_economics.md)), validator-clients will be incentivized to include and validate PoReps in PoH through the distribution of the transaction fees associated with the submitted PoRep. 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). It was chosen not to include a distribution of these rewards to PoRep validators, and to rely only on the collection of PoRep attached transaction fees, due to the fact that the confluence of two participation incentive modes (state-validation inflation rate via global staked % and replication-validation rewards based on global redundancy factor) on the incentives of a single network participant (a validator-client) potentially opened up a significant incentive-driven attack surface area.
|
||||
|
||||
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. However, because replication-client rewards are distributed in proportion to and only after submitted PoReps are validated, they are uniquely motivated for the inclusion and validation of their proofs. This pressure is expected to generate an adequate market economy between replication-clients and validation-clients. Additionally, transaction fees submitted with PoReps have no minimum amount pre-allocated to the mining pool, as do state-validation transaction fees.
|
||||
|
||||
There are various attack vectors available for colluding validation and replication clients, as described in detail below in [Economic Sustainability](ed_economic_sustainability). To protect against various collusion attack vectors, for a given epoch, PoRep transaction fees are pooled, and redistributed across participating validation-clients in proportion to the number of validated PoReps in the epoch less the number of invalidated PoReps [DIAGRAM]. 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).
|
46
book/src/ed_vce_state_validation_protocol_based_rewards.md
Normal file
46
book/src/ed_vce_state_validation_protocol_based_rewards.md
Normal file
@ -0,0 +1,46 @@
|
||||
### State-validation protocol-based rewards
|
||||
|
||||
Validator-clients have two functional roles in the Solana network
|
||||
|
||||
* Validate (vote) the current global state of that PoH along with any Proofs-of-Replication (see [Replication Client Economics](ed_replication_client_economics.md)) that they are eligible to validate
|
||||
|
||||
* Be elected as ‘leader’ on a stake-weighted round-robin schedule during which time they are responsible for collecting outstanding transactions and Proofs-of-Replication and incorporating them into the PoH, thus updating the global state of the network and providing chain continuity.
|
||||
|
||||
Validator-client rewards for these services are to be distributed at the end of each Solana epoch. Compensation for validator-clients is provided via a protocol-based annual interest rate dispersed in proportion to the stake-weight of each validator (see below) along with leader-claimed transaction fees available during each leader rotation. I.e. during the time a given validator-client is elected as leader, it has the opportunity to keep a portion of each non-PoRep transaction fee, less a protocol-specified amount that is returned to the mining pool (see [Validation-client State Transaction Fees](ed_vce_state_validation_transaction_fees.md)). PoRep transaction fees are not collected directly by the leader client but pooled and returned to the validator set in proportion to the number of successfully validated PoReps. (see [Replication-client Transaction Fees](ed_vce_replication_validation_transaction_fees.md))
|
||||
|
||||
|
||||
The protocol-based annual interest-rate (%) per epoch to be distributed to validation-clients is to be a function of:
|
||||
|
||||
* the current fraction of staked SOLs out of the current total circulating supply,
|
||||
|
||||
* the global time since the genesis block instantiation
|
||||
|
||||
* the up-time/participation [% of available slots/blocks that validator had opportunity to vote on?] of a given validator over the previous epoch.
|
||||
|
||||
The first two factors are protocol parameters only (i.e. independent of validator behavior in a given epoch) and describe a global validation reward schedule designed to both incentivize early participation and optimal security in the network. This schedule sets a maximum annual validator-client interest rate per epoch.
|
||||
|
||||
At any given point in time, this interest rate is pegged to a defined value given a specific % staked SOL out of the circulating supply (e.g. 10% interest rate when 66% of circulating SOL is staked). The interest rate adjusts as the square-root [TBD] of the % staked, leading to higher validation-client interest rates as the % staked drops below the targeted goal, thus incentivizing more participation leading to more security in the network. An example of such a schedule, for a specified point in time (e.g. network launch) is shown in **Table 1**.
|
||||
|
||||
| Percentage circulating supply staked [%] | Annual validator-client interest rate [%] |
|
||||
| ---: | ---: |
|
||||
| 5 | 13.87 |
|
||||
| 15 | 13.31 |
|
||||
| 25 | 12.73 |
|
||||
| 35 | 12.12 |
|
||||
| 45 | 11.48 |
|
||||
| 55 | 10.80 |
|
||||
| **66** | **10.00** |
|
||||
| 75 | 9.29 |
|
||||
| 85 | 8.44 |
|
||||
|
||||
**Table 1:** Example interest rate schedule based on % SOL staked out of circulating supply. In this case, interest rates are fixed at 10% for 66% of staked circulating supply
|
||||
|
||||
Over time, the interest rate, at any network staked percentage, will drop as described by an algorithmic schedule. Validation-client interest rates are designed to be higher in the early days of the network to incentivize participation and jumpstart the network economy. This mining-pool provided interest rate will reduce over time until a network-chosen baseline value is reached. This is a fixed, long-term, interest rate to be provided to validator-clients. This value does not represent the total interest available to validator-clients as transaction fees for both state-validation and ledger storage replication (PoReps) are not accounted for here. A validation-client interest rate schedule as a function of % network staked and time is shown in** Figure 2**.
|
||||
|
||||
<!--  -->
|
||||
|
||||
<p style="text-align:center;"><img src="img/validation_client_interest_rates.png" alt="drawing" width="800"/></p>
|
||||
|
||||
**Figure 2:** In this example schedule, the annual interest rate [%] reduces at around 16.7% per year, until it reaches the long-term, fixed, 4% rate.
|
||||
|
||||
This epoch-specific protocol-defined interest rate sets an upper limit of *protocol-generated* annual interest rate (not absolute total interest rate) possible to be delivered to any validator-client per epoch. The distributed interest rate per epoch is then discounted from this value based on the participation of the validator-client during the previous epoch. Each epoch is comprised of XXX slots. The protocol-defined interest rate is then discounted by the log [TBD] of the % of slots a given validator submitted a vote on a PoH branch during that epoch, see **Figure XX**
|
20
book/src/ed_vce_state_validation_transaction_fees.md
Normal file
20
book/src/ed_vce_state_validation_transaction_fees.md
Normal file
@ -0,0 +1,20 @@
|
||||
### State-validation Transaction Fees
|
||||
|
||||
Each message sent through the network, to be processed by the current leader validation-client and confirmed as a global state transaction, must contain a transaction fee. Transaction fees offer many benefits in the Solana economic design, for example they:
|
||||
|
||||
* provide unit compensation to the validator network for the CPU/GPU resources necessary to process the state transaction,
|
||||
|
||||
* reduce network spam by introducing real cost to transactions,
|
||||
|
||||
* open avenues for a transaction market to incentivize validation-client to collect and process submitted transactions in their function as leader,
|
||||
|
||||
* and provide potential long-term economic stability of the network through a protocol-captured minimum fee amount per transaction, as described below.
|
||||
|
||||
Many current blockchain economies (e.g. Bitcoin, Ethereum), rely on protocol-based rewards to support the economy in the short term, with the assumption that the revenue generated through transaction fees will support the economy in the long term, when the protocol derived rewards expire. In an attempt to create a sustainable economy through protocol-based rewards and transaction fees, a fixed portion of each transaction fee is sent to the mining pool, with the resulting fee going to the current leader processing the transaction. These pooled fees, then re-enter the system through rewards distributed to validation-clients, through the process described above, and replication-clients, as discussed below.
|
||||
|
||||
The intent of this design is to retain leader incentive to include as many transactions as possible within the leader-slot time, while providing a redistribution avenue that protects against "tax evasion" attacks (i.e. side-channel fee payments)<sup>[1](ed_referenced.md)</sup>. Constraints on the fixed portion of transaction fees going to the mining pool, to establish long-term economic sustainability, are established and discussed in detail in the [Economic Sustainability](ed_economic_sustainability.md) section.
|
||||
|
||||
This minimum, protocol-earmarked, portion of each transaction fee can be dynamically adjusted depending on historical gas usage. In this way, the protocol can use the minimum fee to target a desired hardware utilisation. By monitoring a protocol specified gas usage with respect to a desired, target usage amount (e.g. 50% of a block's capacity), the minimum fee can be raised/lowered which should, in turn, lower/raise the actual gas usage per block until it reaches the target amount. This adjustment process can be thought of as similar to the difficulty adjustment algorithm in the Bitcoin protocol, however in this case it is adjusting the minimum transaction fee to guide the transaction processing hardware usage to a desired level.
|
||||
|
||||
Additionally, the minimum protocol captured fee can be a consideration in fork selection. In the case of a PoH fork with a malicious, censoring leader, we would expect the total procotol captured fee to be less than a comparable honest fork, due to the fees lost from censoring. If the censoring leader is to compensate for these lost protocol fees, they would have to replace the fees on their fork themselves, thus potentially reducing the incentive to censor in the first place.
|
||||
|
29
book/src/ed_vce_validation_stake_delegation.md
Normal file
29
book/src/ed_vce_validation_stake_delegation.md
Normal file
@ -0,0 +1,29 @@
|
||||
### Validation Stake Delegation
|
||||
|
||||
Running a Solana validation-client required relatively modest upfront hardware capital investment. **Table 2** provides an example hardware configuration to support ~1M tx/s with estimated ‘off-the-shelf’ costs:
|
||||
|
||||
|Component|Example|Estimated Cost|
|
||||
|--- |--- |--- |
|
||||
|GPU|2x 2080 Ti|$2500|
|
||||
|or|4x 1080 Ti|$2800|
|
||||
|OS/Ledger Storage|Samsung 860 Evo 2TB|$370|
|
||||
|Accounts storage|2x Samsung 970 Pro M.2 512GB|$340|
|
||||
|RAM|32 Gb|$300|
|
||||
|Motherboard|AMD x399|$400|
|
||||
|CPU|AMD Threadripper 2920x|$650|
|
||||
|Case||$100|
|
||||
|Power supply|EVGA 1600W|$300|
|
||||
|Network|> 500 mbps||
|
||||
|Network (1)|Google webpass business bay area 1gbps unlimited|$5500/mo|
|
||||
|Network (2)|Hurricane Electric bay area colo 1gbps|$500/mo|
|
||||
**Table 2** example high-end hardware setup for running a Solana client.
|
||||
|
||||
Despite the low-barrier to entry as a validation-client, from a capital investment perspective, as in any developing economy, there will be much opportunity and need for trusted validation services as evidenced by node reliability, UX/UI, APIs and other software accessibility tools. Additionally, although Solana’s validator node startup costs are nominal when compared to similar networks, they may still be somewhat restrictive for some potential participants. In the spirit of developing a true decentralized, permissionless network, these interested parties still have two options to become involved in the Solana network/economy:
|
||||
|
||||
1. Delegation of previously acquired tokens with a reliable validation node to earn a portion of interest generated
|
||||
|
||||
2. Provide local storage space as a replication-client and receive rewards by submitting Proof-of-Replication (see [Replication-client Economics](ed_replication_client_economics.md)).
|
||||
|
||||
a. This participant has the additional option to directly delegate their earned storage rewards ([Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md))
|
||||
|
||||
Delegation of tokens to validation-clients, via option 1, provides a way for passive Solana token holders to become part of the active Solana economy and earn interest rates proportional to the interest rate generated by the delegated validation-client. Additionally, this feature creates a healthy validation-client market, with potential validation-client nodes competing to build reliable, transparent and profitable delegation services.
|
104
book/src/fork-generation.md
Normal file
104
book/src/fork-generation.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Fork Generation
|
||||
|
||||
The chapter describes how forks naturally occur as a consequence of [leader
|
||||
rotation](leader-rotation.md).
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
Nodes take turns being leader and generating the PoH that encodes state
|
||||
changes. The cluster can tolerate loss of connection to any leader by
|
||||
synthesizing what the leader ***would*** have generated had it been connected
|
||||
but not ingesting any state changes. The possible number of forks is thereby
|
||||
limited to a "there/not-there" skip list of forks that may arise on leader
|
||||
rotation slot boundaries. At any given slot, only a single leader's
|
||||
transactions will be accepted.
|
||||
|
||||
## Message Flow
|
||||
|
||||
1. Transactions are ingested by the current leader.
|
||||
2. Leader filters valid transactions.
|
||||
3. Leader executes valid transactions updating its state.
|
||||
4. Leader packages transactions into entries based off its current PoH slot.
|
||||
5. Leader transmits the entries to validator nodes (in signed blobs)
|
||||
1. The PoH stream includes ticks; empty entries that indicate liveness of
|
||||
the leader and the passage of time on the cluster.
|
||||
2. A leader's stream begins with the tick entries necessary complete the PoH
|
||||
back to the leaders most recently observed prior leader slot.
|
||||
6. Validators retransmit entries to peers in their set and to further
|
||||
downstream nodes.
|
||||
7. Validators validate the transactions and execute them on their state.
|
||||
8. Validators compute the hash of the state.
|
||||
9. At specific times, i.e. specific PoH tick counts, validators transmit votes
|
||||
to the leader.
|
||||
1. Votes are signatures of the hash of the computed state at that PoH tick
|
||||
count
|
||||
2. Votes are also propagated via gossip
|
||||
10. Leader executes the votes as any other transaction and broadcasts them to
|
||||
the cluster.
|
||||
11. Validators observe their votes and all the votes from the cluster.
|
||||
|
||||
## Partitions, Forks
|
||||
|
||||
Forks can arise at PoH tick counts that correspond to a vote. The next leader
|
||||
may not have observed the last vote slot and may start their slot with
|
||||
generated virtual PoH entries. These empty ticks are generated by all nodes in
|
||||
the cluster at a cluster-configured rate for hashes/per/tick `Z`.
|
||||
|
||||
There are only two possible versions of the PoH during a voting slot: PoH with
|
||||
`T` ticks and entries generated by the current leader, or PoH with just ticks.
|
||||
The "just ticks" version of the PoH can be thought of as a virtual ledger, one
|
||||
that all nodes in the cluster can derive from the last tick in the previous
|
||||
slot.
|
||||
|
||||
Validators can ignore forks at other points (e.g. from the wrong leader), or
|
||||
slash the leader responsible for the fork.
|
||||
|
||||
Validators vote based on a greedy choice to maximize their reward described in
|
||||
[forks selection](fork-selection.md).
|
||||
|
||||
### Validator's View
|
||||
|
||||
#### Time Progression
|
||||
|
||||
The diagram below represents a validator's view of the
|
||||
PoH stream with possible forks over time. L1, L2, etc. are leader slots, and
|
||||
`E`s represent entries from that leader during that leader's slot. The `x`s
|
||||
represent ticks only, and time flows downwards in the diagram.
|
||||
|
||||
|
||||
<img alt="Fork generation" src="img/fork-generation.svg" class="center"/>
|
||||
|
||||
Note that an `E` appearing on 2 forks at the same slot is a slashable
|
||||
condition, so a validator observing `E3` and `E3'` can slash L3 and safely
|
||||
choose `x` for that slot. Once a validator commits to a forks, other forks can
|
||||
be discarded below that tick count. For any slot, validators need only
|
||||
consider a single "has entries" chain or a "ticks only" chain to be proposed by
|
||||
a leader. But multiple virtual entries may overlap as they link back to the a
|
||||
previous slot.
|
||||
|
||||
#### Time Division
|
||||
|
||||
It's useful to consider leader rotation over PoH tick count as time division of
|
||||
the job of encoding state for the cluster. The following table presents the
|
||||
above tree of forks as a time-divided ledger.
|
||||
|
||||
leader slot | L1 | L2 | L3 | L4 | L5
|
||||
-------|----|----|----|----|----
|
||||
data | E1| E2 | E3 | E4 | E5
|
||||
ticks since prev | | | | x | xx
|
||||
|
||||
Note that only data from leader L3 will be accepted during leader slot L3.
|
||||
Data from L3 may include "catchup" ticks back to a slot other than L2 if L3 did
|
||||
not observe L2's data. L4 and L5's transmissions include the "ticks to prev"
|
||||
PoH entries.
|
||||
|
||||
This arrangement of the network data streams permits nodes to save exactly this
|
||||
to the ledger for replay, restart, and checkpoints.
|
||||
|
||||
### Leader's View
|
||||
|
||||
When a new leader begins a slot, it must first transmit any PoH (ticks)
|
||||
required to link the new slot with the most recently observed and voted slot.
|
||||
The fork the leader proposes would link the current slot to a previous fork
|
||||
that the leader has voted on with virtual ticks.
|
233
book/src/fork-selection.md
Normal file
233
book/src/fork-selection.md
Normal file
@ -0,0 +1,233 @@
|
||||
# Fork Selection
|
||||
|
||||
This design describes a *Fork Selection* algorithm. It addresses the following
|
||||
problems:
|
||||
|
||||
* Some forks may not end up accepted by the super-majority of the cluster, and
|
||||
voters need to recover from voting on such forks.
|
||||
|
||||
* Many forks may be votable by different voters, and each voter may see a
|
||||
different set of votable forks. The selected forks should eventually converge
|
||||
for the cluster.
|
||||
|
||||
* Reward based votes have an associated risk. Voters should have the ability to
|
||||
configure how much risk they take on.
|
||||
|
||||
* The [cost of rollback](#cost-of-rollback) needs to be computable. It is
|
||||
important to clients that rely on some measurable form of Consistency. The
|
||||
costs to break consistency need to be computable, and increase super-linearly
|
||||
for older votes.
|
||||
|
||||
* ASIC speeds are different between nodes, and attackers could employ Proof of
|
||||
History ASICS that are much faster than the rest of the cluster. Consensus
|
||||
needs to be resistant to attacks that exploit the variability in Proof of
|
||||
History ASIC speed.
|
||||
|
||||
For brevity this design assumes that a single voter with a stake is deployed as
|
||||
an individual validator in the cluster.
|
||||
|
||||
## Time
|
||||
|
||||
The Solana cluster generates a source of time via a Verifiable Delay Function we
|
||||
are calling [Proof of History](book/src/synchronization.md).
|
||||
|
||||
Proof of History is used to create a deterministic round robin schedule for all
|
||||
the active leaders. At any given time only 1 leader, which can be computed from
|
||||
the ledger itself, can propose a fork. For more details, see [fork
|
||||
generation](fork-generation.md) and [leader rotation](leader-rotation.md).
|
||||
|
||||
## Lockouts
|
||||
|
||||
The purpose of the lockout is to force a validator to commit opportunity cost to
|
||||
a specific fork. Lockouts are measured in slots, and therefor represent a
|
||||
real-time forced delay that a validator needs to wait before breaking the
|
||||
commitment to a fork.
|
||||
|
||||
Validators that violate the lockouts and vote for a diverging fork within the
|
||||
lockout should be punished. The proposed punishment is to slash the validator
|
||||
stake if a concurrent vote within a lockout for a non-descendant fork can be
|
||||
proven to the cluster.
|
||||
|
||||
## Algorithm
|
||||
|
||||
The basic idea to this approach is to stack consensus votes and double lockouts.
|
||||
Each vote in the stack is a confirmation of a fork. Each confirmed fork is an
|
||||
ancestor of the fork above it. Each vote has a `lockout` in units of slots
|
||||
before the validator can submit a vote that does not contain the confirmed fork
|
||||
as an ancestor.
|
||||
|
||||
When a vote is added to the stack, the lockouts of all the previous votes in the
|
||||
stack are doubled (more on this in [Rollback](#Rollback)). With each new vote,
|
||||
a validator commits the previous votes to an ever-increasing lockout. At 32
|
||||
votes we can consider the vote to be at `max lockout` any votes with a lockout
|
||||
equal to or above `1<<32` are dequeued (FIFO). Dequeuing a vote is the trigger
|
||||
for a reward. If a vote expires before it is dequeued, it and all the votes
|
||||
above it are popped (LIFO) from the vote stack. The validator needs to start
|
||||
rebuilding the stack from that point.
|
||||
|
||||
### Rollback
|
||||
|
||||
Before a vote is pushed to the stack, all the votes leading up to vote with a
|
||||
lower lock time than the new vote are popped. After rollback lockouts are not
|
||||
doubled until the validator catches up to the rollback height of votes.
|
||||
|
||||
For example, a vote stack with the following state:
|
||||
|
||||
| vote | vote time | lockout | lock expiration time |
|
||||
|-----:|----------:|--------:|---------------------:|
|
||||
| 4 | 4 | 2 | 6 |
|
||||
| 3 | 3 | 4 | 7 |
|
||||
| 2 | 2 | 8 | 10 |
|
||||
| 1 | 1 | 16 | 17 |
|
||||
|
||||
*Vote 5* is at time 9, and the resulting state is
|
||||
|
||||
| vote | vote time | lockout | lock expiration time |
|
||||
|-----:|----------:|--------:|---------------------:|
|
||||
| 5 | 9 | 2 | 11 |
|
||||
| 2 | 2 | 8 | 10 |
|
||||
| 1 | 1 | 16 | 17 |
|
||||
|
||||
*Vote 6* is at time 10
|
||||
|
||||
| vote | vote time | lockout | lock expiration time |
|
||||
|-----:|----------:|--------:|---------------------:|
|
||||
| 6 | 10 | 2 | 12 |
|
||||
| 5 | 9 | 4 | 13 |
|
||||
| 2 | 2 | 8 | 10 |
|
||||
| 1 | 1 | 16 | 17 |
|
||||
|
||||
At time 10 the new votes caught up to the previous votes. But *vote 2* expires
|
||||
at 10, so the when *vote 7* at time 11 is applied the votes including and above
|
||||
*vote 2* will be popped.
|
||||
|
||||
| vote | vote time | lockout | lock expiration time |
|
||||
|-----:|----------:|--------:|---------------------:|
|
||||
| 7 | 11 | 2 | 13 |
|
||||
| 1 | 1 | 16 | 17 |
|
||||
|
||||
The lockout for vote 1 will not increase from 16 until the stack contains 5
|
||||
votes.
|
||||
|
||||
### Slashing and Rewards
|
||||
|
||||
Validators should be rewarded for selecting the fork that the rest of the
|
||||
cluster selected as often as possible. This is well-aligned with generating a
|
||||
reward when the vote stack is full and the oldest vote needs to be dequeued.
|
||||
Thus a reward should be generated for each successful dequeue.
|
||||
|
||||
### Cost of Rollback
|
||||
|
||||
Cost of rollback of *fork A* is defined as the cost in terms of lockout time to
|
||||
the validator to confirm any other fork that does not include *fork A* as an
|
||||
ancestor.
|
||||
|
||||
The **Economic Finality** of *fork A* can be calculated as the loss of all the
|
||||
rewards from rollback of *fork A* and its descendants, plus the opportunity cost
|
||||
of reward due to the exponentially growing lockout of the votes that have
|
||||
confirmed *fork A*.
|
||||
|
||||
### Thresholds
|
||||
|
||||
Each validator can independently set a threshold of cluster commitment to a fork
|
||||
before that validator commits to a fork. For example, at vote stack index 7,
|
||||
the lockout is 256 time units. A validator may withhold votes and let votes 0-7
|
||||
expire unless the vote at index 7 has at greater than 50% commitment in the
|
||||
cluster. This allows each validator to independently control how much risk to
|
||||
commit to a fork. Committing to forks at a higher frequency would allow the
|
||||
validator to earn more rewards.
|
||||
|
||||
### Algorithm parameters
|
||||
|
||||
The following parameters need to be tuned:
|
||||
|
||||
* Number of votes in the stack before dequeue occurs (32).
|
||||
|
||||
* Rate of growth for lockouts in the stack (2x).
|
||||
|
||||
* Starting default lockout (2).
|
||||
|
||||
* Threshold depth for minimum cluster commitment before committing to the fork
|
||||
(8).
|
||||
|
||||
* Minimum cluster commitment size at threshold depth (50%+).
|
||||
|
||||
### Free Choice
|
||||
|
||||
A "Free Choice" is an unenforcible validator action. There is no way for the
|
||||
protocol to encode and enforce these actions since each validator can modify the
|
||||
code and adjust the algorithm. A validator that maximizes self-reward over all
|
||||
possible futures should behave in such a way that the system is stable, and the
|
||||
local greedy choice should result in a greedy choice over all possible futures.
|
||||
A set of validator that are engaging in choices to disrupt the protocol should
|
||||
be bound by their stake weight to the denial of service. Two options exits for
|
||||
validator:
|
||||
|
||||
* a validator can outrun previous validator in virtual generation and submit a
|
||||
concurrent fork
|
||||
|
||||
* a validator can withhold a vote to observe multiple forks before voting
|
||||
|
||||
In both cases, the validator in the cluster have several forks to pick from
|
||||
concurrently, even though each fork represents a different height. In both
|
||||
cases it is impossible for the protocol to detect if the validator behavior is
|
||||
intentional or not.
|
||||
|
||||
### Greedy Choice for Concurrent Forks
|
||||
|
||||
When evaluating multiple forks, each validator should use the following rules:
|
||||
|
||||
1. Forks must satisfy the *Threshold* rule.
|
||||
|
||||
2. Pick the fork that maximizes the total cluster lockout time for all the
|
||||
ancestor forks.
|
||||
|
||||
3. Pick the fork that has the greatest amount of cluster transaction fees.
|
||||
|
||||
4. Pick the latest fork in terms of PoH.
|
||||
|
||||
Cluster transaction fees are fees that are deposited to the mining pool as
|
||||
described in the [Staking Rewards](book/src/staking-rewards.md) section.
|
||||
|
||||
## PoH ASIC Resistance
|
||||
|
||||
Votes and lockouts grow exponentially while ASIC speed up is linear. There are
|
||||
two possible attack vectors involving a faster ASIC.
|
||||
|
||||
### ASIC censorship
|
||||
|
||||
An attacker generates a concurrent fork that outruns previous leaders in an
|
||||
effort to censor them. A fork proposed by this attacker will be available
|
||||
concurrently with the next available leader. For nodes to pick this fork it
|
||||
must satisfy the *Greedy Choice* rule.
|
||||
|
||||
1. Fork must have equal number of votes for the ancestor fork.
|
||||
|
||||
2. Fork cannot be so far a head as to cause expired votes.
|
||||
|
||||
3. Fork must have a greater amount of cluster transaction fees.
|
||||
|
||||
This attack is then limited to censoring the previous leaders fees, and
|
||||
individual transactions. But it cannot halt the cluster, or reduce the
|
||||
validator set compared to the concurrent fork. Fee censorship is limited to
|
||||
access fees going to the leaders but not the validators.
|
||||
|
||||
### ASIC Rollback
|
||||
|
||||
An attacker generates a concurrent fork from an older block to try to rollback
|
||||
the cluster. In this attack the concurrent fork is competing with forks that
|
||||
have already been voted on. This attack is limited by the exponential growth of
|
||||
the lockouts.
|
||||
|
||||
* 1 vote has a lockout of 2 slots. Concurrent fork must be at least 2 slots
|
||||
ahead, and be produced in 1 slot. Therefore requires an ASIC 2x faster.
|
||||
|
||||
* 2 votes have a lockout of 4 slots. Concurrent fork must be at least 4 slots
|
||||
ahead and produced in 2 slots. Therefore requires an ASIC 2x faster.
|
||||
|
||||
* 3 votes have a lockout of 8 slots. Concurrent fork must be at least 8 slots
|
||||
ahead and produced in 3 slots. Therefore requires an ASIC 2.6x faster.
|
||||
|
||||
* 10 votes have a lockout of 1024 slots. 1024/10, or 102.4x faster ASIC.
|
||||
|
||||
* 20 votes have a lockout of 2^20 slots. 2^20/20, or 52,428.8x faster ASIC.
|
29
book/src/fullnode.md
Normal file
29
book/src/fullnode.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Anatomy of a Fullnode
|
||||
|
||||
<img alt="Fullnode block diagrams" src="img/fullnode.svg" class="center"/>
|
||||
|
||||
## Pipelining
|
||||
|
||||
The fullnodes make extensive use of an optimization common in CPU design,
|
||||
called *pipelining*. Pipelining is the right tool for the job when there's a
|
||||
stream of input data that needs to be processed by a sequence of steps, and
|
||||
there's different hardware responsible for each. The quintessential example is
|
||||
using a washer and dryer to wash/dry/fold several loads of laundry. Washing
|
||||
must occur before drying and drying before folding, but each of the three
|
||||
operations is performed by a separate unit. To maximize efficiency, one creates
|
||||
a pipeline of *stages*. We'll call the washer one stage, the dryer another, and
|
||||
the folding process a third. To run the pipeline, one adds a second load of
|
||||
laundry to the washer just after the first load is added to the dryer.
|
||||
Likewise, the third load is added to the washer after the second is in the
|
||||
dryer and the first is being folded. In this way, one can make progress on
|
||||
three loads of laundry simultaneously. Given infinite loads, the pipeline will
|
||||
consistently complete a load at the rate of the slowest stage in the pipeline.
|
||||
|
||||
## Pipelining in the Fullnode
|
||||
|
||||
The fullnode contains two pipelined processes, one used in leader mode called
|
||||
the TPU and one used in validator mode called the TVU. In both cases, the
|
||||
hardware being pipelined is the same, the network input, the GPU cards, the CPU
|
||||
cores, writes to disk, and the network output. What it does with that hardware
|
||||
is different. The TPU exists to create ledger entries whereas the TVU exists
|
||||
to validate them.
|
168
book/src/getting-started.md
Normal file
168
book/src/getting-started.md
Normal file
@ -0,0 +1,168 @@
|
||||
# Getting Started
|
||||
|
||||
The Solana git repository contains all the scripts you might need to spin up your
|
||||
own local testnet. Depending on what you're looking to achieve, you may want to
|
||||
run a different variation, as the full-fledged, performance-enhanced
|
||||
multinode testnet is considerably more complex to set up than a Rust-only,
|
||||
singlenode testnode. If you are looking to develop high-level features, such
|
||||
as experimenting with smart contracts, save yourself some setup headaches and
|
||||
stick to the Rust-only singlenode demo. If you're doing performance optimization
|
||||
of the transaction pipeline, consider the enhanced singlenode demo. If you're
|
||||
doing consensus work, you'll need at least a Rust-only multinode demo. If you want
|
||||
to reproduce our TPS metrics, run the enhanced multinode demo.
|
||||
|
||||
For all four variations, you'd need the latest Rust toolchain and the Solana
|
||||
source code:
|
||||
|
||||
First, install Rust's package manager Cargo.
|
||||
|
||||
```bash
|
||||
$ curl https://sh.rustup.rs -sSf | sh
|
||||
$ source $HOME/.cargo/env
|
||||
```
|
||||
|
||||
Now checkout the code from github:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/solana-labs/solana.git
|
||||
$ cd solana
|
||||
```
|
||||
|
||||
The demo code is sometimes broken between releases as we add new low-level
|
||||
features, so if this is your first time running the demo, you'll improve
|
||||
your odds of success if you check out the
|
||||
[latest release](https://github.com/solana-labs/solana/releases)
|
||||
before proceeding:
|
||||
|
||||
```bash
|
||||
$ TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
$ git checkout $TAG
|
||||
```
|
||||
|
||||
### Configuration Setup
|
||||
|
||||
Ensure important programs such as the vote program are built before any
|
||||
nodes are started
|
||||
```bash
|
||||
$ cargo build --all
|
||||
```
|
||||
|
||||
The network is initialized with a genesis ledger and fullnode configuration files.
|
||||
These files can be generated by running the following script.
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/setup.sh
|
||||
```
|
||||
|
||||
### Drone
|
||||
|
||||
In order for the fullnodes and clients to work, we'll need to
|
||||
spin up a drone to give out some test tokens. The drone delivers Milton
|
||||
Friedman-style "air drops" (free tokens to requesting clients) to be used in
|
||||
test transactions.
|
||||
|
||||
Start the drone with:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/drone.sh
|
||||
```
|
||||
|
||||
### Singlenode Testnet
|
||||
|
||||
Before you start a fullnode, make sure you know the IP address of the machine you
|
||||
want to be the bootstrap leader for the demo, and make sure that udp ports 8000-10000 are
|
||||
open on all the machines you want to test with.
|
||||
|
||||
Now start the bootstrap leader in a separate shell:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/bootstrap-leader.sh
|
||||
```
|
||||
|
||||
Wait a few seconds for the server to initialize. It will print "leader ready..." when it's ready to
|
||||
receive transactions. The leader will request some tokens from the drone if it doesn't have any.
|
||||
The drone does not need to be running for subsequent leader starts.
|
||||
|
||||
### Multinode Testnet
|
||||
|
||||
To run a multinode testnet, after starting a leader node, spin up some
|
||||
additional full nodes in separate shells:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/fullnode-x.sh
|
||||
```
|
||||
|
||||
To run a performance-enhanced full node on Linux,
|
||||
[CUDA 10.0](https://developer.nvidia.com/cuda-downloads) must be installed on
|
||||
your system:
|
||||
|
||||
```bash
|
||||
$ ./fetch-perf-libs.sh
|
||||
$ SOLANA_CUDA=1 ./multinode-demo/bootstrap-leader.sh
|
||||
$ SOLANA_CUDA=1 ./multinode-demo/fullnode-x.sh
|
||||
```
|
||||
|
||||
### Testnet Client Demo
|
||||
|
||||
Now that your singlenode or multinode testnet is up and running let's send it
|
||||
some transactions!
|
||||
|
||||
In a separate shell start the client:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/client.sh # runs against localhost by default
|
||||
```
|
||||
|
||||
What just happened? The client demo spins up several threads to send 500,000 transactions
|
||||
to the testnet as quickly as it can. The client then pings the testnet periodically to see
|
||||
how many transactions it processed in that time. Take note that the demo intentionally
|
||||
floods the network with UDP packets, such that the network will almost certainly drop a
|
||||
bunch of them. This ensures the testnet has an opportunity to reach 710k TPS. The client
|
||||
demo completes after it has convinced itself the testnet won't process any additional
|
||||
transactions. You should see several TPS measurements printed to the screen. In the
|
||||
multinode variation, you'll see TPS measurements for each validator node as well.
|
||||
|
||||
### Testnet Debugging
|
||||
|
||||
There are some useful debug messages in the code, you can enable them on a per-module and per-level
|
||||
basis. Before running a leader or validator set the normal RUST\_LOG environment variable.
|
||||
|
||||
For example
|
||||
|
||||
* To enable `info` everywhere and `debug` only in the solana::banking_stage module:
|
||||
|
||||
```bash
|
||||
$ export RUST_LOG=solana=info,solana::banking_stage=debug
|
||||
```
|
||||
|
||||
* To enable BPF program logging:
|
||||
|
||||
```bash
|
||||
$ export RUST_LOG=solana_bpf_loader=trace
|
||||
```
|
||||
|
||||
Generally we are using `debug` for infrequent debug messages, `trace` for potentially frequent
|
||||
messages and `info` for performance-related logging.
|
||||
|
||||
You can also attach to a running process with GDB. The leader's process is named
|
||||
_solana-fullnode_:
|
||||
|
||||
```bash
|
||||
$ sudo gdb
|
||||
attach <PID>
|
||||
set logging on
|
||||
thread apply all bt
|
||||
```
|
||||
|
||||
This will dump all the threads stack traces into gdb.txt
|
||||
|
||||
## Public Testnet
|
||||
|
||||
In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`.
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/client.sh --network $(dig +short testnet.solana.com):8001 --duration 60
|
||||
```
|
||||
|
||||
You can observe the effects of your client's transactions on our [dashboard](https://metrics.solana.com:3000/d/testnet/testnet-hud?orgId=2&from=now-30m&to=now&refresh=5s&var-testnet=testnet)
|
||||
|
128
book/src/gossip.md
Normal file
128
book/src/gossip.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Gossip Service
|
||||
|
||||
The Gossip Service acts as a gateway to nodes in the control plane. Fullnodes
|
||||
use the service to ensure information is available to all other nodes in a cluster.
|
||||
The service broadcasts information using a gossip protocol.
|
||||
|
||||
## Gossip Overview
|
||||
|
||||
Nodes continuously share signed data objects among themselves in order to
|
||||
manage a cluster. For example, they share their contact information, ledger
|
||||
height, and votes.
|
||||
|
||||
Every tenth of a second, each node sends a "push" message and/or a "pull"
|
||||
message. Push and pull messages may elicit responses, and push messages may be
|
||||
forwarded on to others in the cluster.
|
||||
|
||||
Gossip runs on a well-known UDP/IP port or a port in a well-known range. Once
|
||||
a cluster is bootstrapped, nodes advertise to each other where to find their
|
||||
gossip endpoint (a socket address).
|
||||
|
||||
## Gossip Records
|
||||
|
||||
Records shared over gossip are arbitrary, but signed and versioned (with a
|
||||
timestamp) as needed to make sense to the node receiving them. If a node
|
||||
recieves two records from the same source, it it updates its own copy with the
|
||||
record with the most recent timestamp.
|
||||
|
||||
## Gossip Service Interface
|
||||
|
||||
### Push Message
|
||||
|
||||
A node sends a push message to tells the cluster it has information to share.
|
||||
Nodes send push messages to `PUSH_FANOUT` push peers.
|
||||
|
||||
Upon receiving a push message, a node examines the message for:
|
||||
|
||||
1. Duplication: if the message has been seen before, the node responds with
|
||||
`PushMessagePrune` and drops the message
|
||||
|
||||
2. New data: if the message is new to the node
|
||||
* Stores the new information with an updated version in its cluster info and
|
||||
purges any previous older value
|
||||
* Stores the message in `pushed_once` (used for detecting duplicates,
|
||||
purged after `PUSH_MSG_TIMEOUT * 5` ms)
|
||||
* Retransmits the messages to its own push peers
|
||||
|
||||
3. Expiration: nodes drop push messages that are older than `PUSH_MSG_TIMEOUT`
|
||||
|
||||
### Push Peers, Prune Message
|
||||
|
||||
A nodes selects its push peers at random from the active set of known peers.
|
||||
The node keeps this selection for a relatively long time. When a prune message
|
||||
is received, the node drops the push peer that sent the prune. Prune is an
|
||||
indication that there is another, faster path to that node than direct push.
|
||||
|
||||
The set of push peers is kept fresh by rotating a new node into the set every
|
||||
`PUSH_MSG_TIMEOUT/2` milliseconds.
|
||||
|
||||
### Pull Message
|
||||
|
||||
A node sends a pull message to ask the cluster if there is any new information.
|
||||
A pull message is sent to a single peer at random and comprises a Bloom filter
|
||||
that represents things it already has. A node receiving a pull message
|
||||
iterates over its values and constructs a pull response of things that miss the
|
||||
filter and would fit in a message.
|
||||
|
||||
A node constructs the pull Bloom filter by iterating over current values and
|
||||
recently purged values.
|
||||
|
||||
A node handles items in a pull response the same way it handles new data in a
|
||||
push message.
|
||||
|
||||
|
||||
## Purging
|
||||
|
||||
Nodes retain prior versions of values (those updated by a pull or push) and
|
||||
expired values (those older than `GOSSIP_PULL_CRDS_TIMEOUT_MS`) in
|
||||
`purged_values` (things I recently had). Nodes purge `purged_values` that are
|
||||
older than `5 * GOSSIP_PULL_CRDS_TIMEOUT_MS`.
|
||||
|
||||
## Eclipse Attacks
|
||||
|
||||
An eclipse attack is an attempt to take over the set of node connections with
|
||||
adversarial endpoints.
|
||||
|
||||
This is relevant to our implementation in the following ways.
|
||||
|
||||
* Pull messages select a random node from the network. An eclipse attack on
|
||||
*pull* would require an attacker to influence the random selection in such a way
|
||||
that only adversarial nodes are selected for pull.
|
||||
|
||||
* Push messages maintain an active set of nodes and select a random fanout for
|
||||
every push message. An eclipse attack on *push* would influence the active set
|
||||
selection, or the random fanout selection.
|
||||
|
||||
### Time and Stake based weights
|
||||
|
||||
Weights are calculated based on `time since last picked` and the `natural log` of the `stake weight`.
|
||||
|
||||
Taking the `ln` of the stake weight allows giving all nodes a fairer chance of network
|
||||
coverage in a reasonable amount of time. It helps normalize the large possible `stake weight` differences between nodes.
|
||||
This way a node with low `stake weight`, compared to a node with large `stake weight` will only have to wait a
|
||||
few multiples of ln(`stake`) seconds before it gets picked.
|
||||
|
||||
There is no way for an adversary to influence these parameters.
|
||||
|
||||
### Pull Message
|
||||
|
||||
A node is selected as a pull target based on the weights described above.
|
||||
|
||||
### Push Message
|
||||
|
||||
A prune message can only remove an adversary from a potential connection.
|
||||
|
||||
Just like *pull message*, nodes are selected into the active set based on weights.
|
||||
|
||||
## Notable differences from PlumTree
|
||||
|
||||
The active push protocol described here is based on (Plum
|
||||
Tree)[https://haslab.uminho.pt/jop/files/lpr07a.pdf]. The main differences are:
|
||||
|
||||
* Push messages have a wallclock that is signed by the originator. Once the
|
||||
wallclock expires the message is dropped. A hop limit is difficult to implement
|
||||
in an adversarial setting.
|
||||
|
||||
* Lazy Push is not implemented because its not obvious how to prevent an
|
||||
adversary from forging the message fingerprint. A naive approach would allow an
|
||||
adversary to be prioritized for pull based on their input.
|
BIN
book/src/img/porep_reward.png
Normal file
BIN
book/src/img/porep_reward.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 372 KiB |
BIN
book/src/img/solana_economic_design.png
Normal file
BIN
book/src/img/solana_economic_design.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
BIN
book/src/img/validation_client_interest_rates.png
Normal file
BIN
book/src/img/validation_client_interest_rates.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 401 KiB |
117
book/src/introduction.md
Normal file
117
book/src/introduction.md
Normal file
@ -0,0 +1,117 @@
|
||||
# What is Solana?
|
||||
|
||||
Solana is the name of an open source project that is 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 it's 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 Solana protocol defines that only 1 billion sols will ever exist,
|
||||
but that the system may perform micropayments of fractional sols, and that a sol
|
||||
may be split as many as 34 times. The fractional sol is called a *lamport*. It
|
||||
is named in honor of Solana's biggest technical influence, [Leslie
|
||||
Lamport](https://en.wikipedia.org/wiki/Leslie_Lamport). A lamport has a value
|
||||
of approximately 0.0000000000582 sol (2^-34).
|
3
book/src/javascript-api.md
Normal file
3
book/src/javascript-api.md
Normal file
@ -0,0 +1,3 @@
|
||||
# JavaScript API
|
||||
|
||||
See [solana-web3](https://solana-labs.github.io/solana-web3.js/).
|
392
book/src/jsonrpc-api.md
Normal file
392
book/src/jsonrpc-api.md
Normal file
@ -0,0 +1,392 @@
|
||||
JSON RPC API
|
||||
===
|
||||
|
||||
Solana nodes accept HTTP requests using the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) specification.
|
||||
|
||||
To interact with a Solana node inside a JavaScript application, use the [solana-web3.js](https://github.com/solana-labs/solana-web3.js) library, which gives a convenient interface for the RPC methods.
|
||||
|
||||
RPC HTTP Endpoint
|
||||
---
|
||||
|
||||
**Default port:** 8899
|
||||
eg. http://localhost:8899, http://192.168.1.88:8899
|
||||
|
||||
RPC PubSub WebSocket Endpoint
|
||||
---
|
||||
|
||||
**Default port:** 8900
|
||||
eg. ws://localhost:8900, http://192.168.1.88:8900
|
||||
|
||||
|
||||
Methods
|
||||
---
|
||||
|
||||
* [confirmTransaction](#confirmtransaction)
|
||||
* [getAccountInfo](#getaccountinfo)
|
||||
* [getBalance](#getbalance)
|
||||
* [getRecentBlockhash](#getrecentblockhash)
|
||||
* [getSignatureStatus](#getsignaturestatus)
|
||||
* [getTransactionCount](#gettransactioncount)
|
||||
* [requestAirdrop](#requestairdrop)
|
||||
* [sendTransaction](#sendtransaction)
|
||||
* [startSubscriptionChannel](#startsubscriptionchannel)
|
||||
|
||||
* [Subscription Websocket](#subscription-websocket)
|
||||
* [accountSubscribe](#accountsubscribe)
|
||||
* [accountUnsubscribe](#accountunsubscribe)
|
||||
* [programSubscribe](#programsubscribe)
|
||||
* [programUnsubscribe](#programunsubscribe)
|
||||
* [signatureSubscribe](#signaturesubscribe)
|
||||
* [signatureUnsubscribe](#signatureunsubscribe)
|
||||
|
||||
Request Formatting
|
||||
---
|
||||
|
||||
To make a JSON-RPC request, send an HTTP POST request with a `Content-Type: application/json` header. The JSON request data should contain 4 fields:
|
||||
|
||||
* `jsonrpc`, set to `"2.0"`
|
||||
* `id`, a unique client-generated identifying integer
|
||||
* `method`, a string containing the method to be invoked
|
||||
* `params`, a JSON array of ordered parameter values
|
||||
|
||||
Example using curl:
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getBalance", "params":["83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri"]}' 192.168.1.88:8899
|
||||
```
|
||||
|
||||
The response output will be a JSON object with the following fields:
|
||||
|
||||
* `jsonrpc`, matching the request specification
|
||||
* `id`, matching the request identifier
|
||||
* `result`, requested data or success confirmation
|
||||
|
||||
Requests can be sent in batches by sending an array of JSON-RPC request objects as the data for a single POST.
|
||||
|
||||
Definitions
|
||||
---
|
||||
|
||||
* Hash: A SHA-256 hash of a chunk of data.
|
||||
* Pubkey: The public key of a Ed25519 key-pair.
|
||||
* Signature: An Ed25519 signature of a chunk of data.
|
||||
* Transaction: A Solana instruction signed by a client key-pair.
|
||||
|
||||
JSON RPC API Reference
|
||||
---
|
||||
|
||||
### confirmTransaction
|
||||
Returns a transaction receipt
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Signature of Transaction to confirm, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `boolean` - Transaction status, true if Transaction is confirmed
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"confirmTransaction", "params":["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW"]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":true,"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### getBalance
|
||||
Returns the balance of the account of provided Pubkey
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Pubkey of account to query, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `integer` - quantity, as a signed 64-bit integer
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getBalance", "params":["83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri"]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":0,"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### getAccountInfo
|
||||
Returns all information associated with the account of provided Pubkey
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Pubkey of account to query, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
The result field will be a JSON object with the following sub fields:
|
||||
|
||||
* `lamports`, number of lamports assigned to this account, as a signed 64-bit integer
|
||||
* `owner`, array of 32 bytes representing the program this account has been assigned to
|
||||
* `userdata`, array of bytes representing any userdata associated with the account
|
||||
* `executable`, boolean indicating if the account contains a program (and is strictly read-only)
|
||||
* `loader`, array of 32 bytes representing the loader for this program (if `executable`), otherwise all
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getAccountInfo", "params":["2gVkYWexTHR5Hb2aLeQN3tnngvWzisFKXDUPrgMHpdST"]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":{"executable":false,"loader":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"owner":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"lamports":1,"userdata":[3,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,53,48,45,48,49,45,48,49,84,48,48,58,48,48,58,48,48,90,252,10,7,28,246,140,88,177,98,82,10,227,89,81,18,30,194,101,199,16,11,73,133,20,246,62,114,39,20,113,189,32,50,0,0,0,0,0,0,0,247,15,36,102,167,83,225,42,133,127,82,34,36,224,207,130,109,230,224,188,163,33,213,13,5,117,211,251,65,159,197,51,0,0,0,0,0,0]},"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### getRecentBlockhash
|
||||
Returns a recent block hash from the ledger
|
||||
|
||||
##### Parameters:
|
||||
None
|
||||
|
||||
##### Results:
|
||||
* `string` - a Hash as base-58 encoded string
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getRecentBlockhash"}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":"GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC","id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### getSignatureStatus
|
||||
Returns the status of a given signature. This method is similar to
|
||||
[confirmTransaction](#confirmtransaction) but provides more resolution for error
|
||||
events.
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Signature of Transaction to confirm, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `string` - Transaction status:
|
||||
* `Confirmed` - Transaction was successful
|
||||
* `SignatureNotFound` - Unknown transaction
|
||||
* `ProgramRuntimeError` - An error occurred in the program that processed this Transaction
|
||||
* `AccountInUse` - Another Transaction had a write lock one of the Accounts specified in this Transaction. The Transaction may succeed if retried
|
||||
* `GenericFailure` - Some other error occurred. **Note**: In the future new Transaction statuses may be added to this list. It's safe to assume that all new statuses will be more specific error conditions that previously presented as `GenericFailure`
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getSignatureStatus", "params":["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW"]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":"SignatureNotFound","id":1}
|
||||
```
|
||||
|
||||
---
|
||||
### getTransactionCount
|
||||
Returns the current Transaction count from the ledger
|
||||
|
||||
##### Parameters:
|
||||
None
|
||||
|
||||
##### Results:
|
||||
* `integer` - count, as unsigned 64-bit integer
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getTransactionCount"}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":268,"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### requestAirdrop
|
||||
Requests an airdrop of lamports to a Pubkey
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Pubkey of account to receive lamports, as base-58 encoded string
|
||||
* `integer` - lamports, as a signed 64-bit integer
|
||||
|
||||
##### Results:
|
||||
* `string` - Transaction Signature of airdrop, as base-58 encoded string
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"requestAirdrop", "params":["83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri", 50]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":"5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW","id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### sendTransaction
|
||||
Creates new transaction
|
||||
|
||||
##### Parameters:
|
||||
* `array` - array of octets containing a fully-signed Transaction
|
||||
|
||||
##### Results:
|
||||
* `string` - Transaction Signature, as base-58 encoded string
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"sendTransaction", "params":[[61, 98, 55, 49, 15, 187, 41, 215, 176, 49, 234, 229, 228, 77, 129, 221, 239, 88, 145, 227, 81, 158, 223, 123, 14, 229, 235, 247, 191, 115, 199, 71, 121, 17, 32, 67, 63, 209, 239, 160, 161, 2, 94, 105, 48, 159, 235, 235, 93, 98, 172, 97, 63, 197, 160, 164, 192, 20, 92, 111, 57, 145, 251, 6, 40, 240, 124, 194, 149, 155, 16, 138, 31, 113, 119, 101, 212, 128, 103, 78, 191, 80, 182, 234, 216, 21, 121, 243, 35, 100, 122, 68, 47, 57, 13, 39, 0, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 40, 240, 124, 194, 149, 155, 16, 138, 31, 113, 119, 101, 212, 128, 103, 78, 191, 80, 182, 234, 216, 21, 121, 243, 35, 100, 122, 68, 47, 57, 11, 12, 106, 49, 74, 226, 201, 16, 161, 192, 28, 84, 124, 97, 190, 201, 171, 186, 6, 18, 70, 142, 89, 185, 176, 154, 115, 61, 26, 163, 77, 1, 88, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":"2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b","id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Subscription Websocket
|
||||
After connect to the RPC PubSub websocket at `ws://<ADDRESS>/`:
|
||||
- Submit subscription requests to the websocket using the methods below
|
||||
- Multiple subscriptions may be active at once
|
||||
|
||||
---
|
||||
|
||||
### accountSubscribe
|
||||
Subscribe to an account to receive notifications when the lamports or userdata
|
||||
for a given account public key changes
|
||||
|
||||
##### Parameters:
|
||||
* `string` - account Pubkey, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `integer` - Subscription id (needed to unsubscribe)
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12"]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||
```
|
||||
|
||||
##### Notification Format:
|
||||
```bash
|
||||
{"jsonrpc": "2.0","method": "accountNotification", "params": {"result": {"executable":false,"loader":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"owner":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"lamports":1,"userdata":[3,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,53,48,45,48,49,45,48,49,84,48,48,58,48,48,58,48,48,90,252,10,7,28,246,140,88,177,98,82,10,227,89,81,18,30,194,101,199,16,11,73,133,20,246,62,114,39,20,113,189,32,50,0,0,0,0,0,0,0,247,15,36,102,167,83,225,42,133,127,82,34,36,224,207,130,109,230,224,188,163,33,213,13,5,117,211,251,65,159,197,51,0,0,0,0,0,0]},"subscription":0}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### accountUnsubscribe
|
||||
Unsubscribe from account change notifications
|
||||
|
||||
##### Parameters:
|
||||
* `integer` - id of account Subscription to cancel
|
||||
|
||||
##### Results:
|
||||
* `bool` - unsubscribe success message
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"accountUnsubscribe", "params":[0]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": true,"id": 1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### programSubscribe
|
||||
Subscribe to a program to receive notifications when the lamports or userdata
|
||||
for a given account owned by the program changes
|
||||
|
||||
##### Parameters:
|
||||
* `string` - program_id Pubkey, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `integer` - Subscription id (needed to unsubscribe)
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["9gZbPtbtHrs6hEWgd6MbVY9VPFtS5Z8xKtnYwA2NynHV"]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||
```
|
||||
|
||||
##### Notification Format:
|
||||
* `string` - account Pubkey, as base-58 encoded string
|
||||
* `object` - account info JSON object (see [getAccountInfo](#getaccountinfo) for field details)
|
||||
```bash
|
||||
{"jsonrpc":"2.0","method":"programNotification","params":{{"result":["8Rshv2oMkPu5E4opXTRyuyBeZBqQ4S477VG26wUTFxUM",{"executable":false,"lamports":1,"owner":[129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"userdata":[1,1,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,49,56,45,49,50,45,50,52,84,50,51,58,53,57,58,48,48,90,235,233,39,152,15,44,117,176,41,89,100,86,45,61,2,44,251,46,212,37,35,118,163,189,247,84,27,235,178,62,55,89,0,0,0,0,50,0,0,0,0,0,0,0,235,233,39,152,15,44,117,176,41,89,100,86,45,61,2,44,251,46,212,37,35,118,163,189,247,84,27,235,178,62,45,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}],"subscription":0}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### programUnsubscribe
|
||||
Unsubscribe from program-owned account change notifications
|
||||
|
||||
##### Parameters:
|
||||
* `integer` - id of account Subscription to cancel
|
||||
|
||||
##### Results:
|
||||
* `bool` - unsubscribe success message
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"programUnsubscribe", "params":[0]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": true,"id": 1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### signatureSubscribe
|
||||
Subscribe to a transaction signature to receive notification when the transaction is confirmed
|
||||
On `signatureNotification`, the subscription is automatically cancelled
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Transaction Signature, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `integer` - subscription id (needed to unsubscribe)
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"signatureSubscribe", "params":["2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b"]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||
```
|
||||
|
||||
##### Notification Format:
|
||||
```bash
|
||||
{"jsonrpc": "2.0","method": "signatureNotification", "params": {"result": "Confirmed","subscription":0}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### signatureUnsubscribe
|
||||
Unsubscribe from signature confirmation notification
|
||||
|
||||
##### Parameters:
|
||||
* `integer` - subscription id to cancel
|
||||
|
||||
##### Results:
|
||||
* `bool` - unsubscribe success message
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"signatureUnsubscribe", "params":[0]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": true,"id": 1}
|
||||
```
|
101
book/src/leader-leader-transition.md
Normal file
101
book/src/leader-leader-transition.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Leader to Leader Transition
|
||||
|
||||
This design describes how leaders transition production of the PoH ledger
|
||||
between each other as each leader generates its own slot.
|
||||
|
||||
## Challenges
|
||||
|
||||
Current leader and the next leader are both racing to generate the final tick
|
||||
for the current slot. The next leader may arrive at that slot while still
|
||||
processing the current leader's entries.
|
||||
|
||||
The ideal scenario would be that the next leader generated its own slot right
|
||||
after it was able to vote for the current leader. It is very likely that the
|
||||
next leader will arrive at their PoH slot height before the current leader
|
||||
finishes broadcasting the entire block.
|
||||
|
||||
The next leader has to make the decision of attaching its own block to the last
|
||||
completed block, or wait to finalize the pending block. It is possible that the
|
||||
next leader will produce a block that proposes that the current leader failed,
|
||||
even though the rest of the network observes that block succeeding.
|
||||
|
||||
The current leader has incentives to start its slot as early as possible to
|
||||
capture economic rewards. Those incentives need to be balanced by the leader's
|
||||
need to attach its block to a block that has the most commitment from the rest
|
||||
of the network.
|
||||
|
||||
## Leader timeout
|
||||
|
||||
While a leader is actively receiving entries for the previous slot, the leader
|
||||
can delay broadcasting the start of its block in real time. The delay is
|
||||
locally configurable by each leader, and can be dynamically based on the
|
||||
previous leader's behavior. If the previous leader's block is confirmed by the
|
||||
leader's TVU before the timeout, the PoH is reset to the start of the slot and
|
||||
this leader produces its block immediately.
|
||||
|
||||
The downsides:
|
||||
|
||||
* Leader delays its own slot, potentially allowing the next leader more time to
|
||||
catch up.
|
||||
|
||||
The upsides compared to guards:
|
||||
|
||||
* All the space in a block is used for entries.
|
||||
|
||||
* The timeout is not fixed.
|
||||
|
||||
* The timeout is local to the leader, and therefore can be clever. The leader's
|
||||
heuristic can take into account avalanche performance.
|
||||
|
||||
* This design doesn't require a ledger hard fork to update.
|
||||
|
||||
* The previous leader can redundantly transmit the last entry in the block to
|
||||
the next leader, and the next leader can speculatively decide to trust it to
|
||||
generate its block without verification of the previous block.
|
||||
|
||||
* The leader can speculatively generate the last tick from the last received
|
||||
entry.
|
||||
|
||||
* The leader can speculatively process transactions and guess which ones are not
|
||||
going to be encoded by the previous leader. This is also a censorship attack
|
||||
vector. The current leader may withhold transactions that it receives from the
|
||||
clients so it can encode them into its own slot. Once processed, entries can be
|
||||
replayed into PoH quickly.
|
||||
|
||||
## Alternative design options
|
||||
|
||||
### Guard tick at the end of the slot
|
||||
|
||||
A leader does not produce entries in its block after the *penultimate tick*,
|
||||
which is the last tick before the first tick of the next slot. The network
|
||||
votes on the *last tick*, so the time difference between the *penultimate tick*
|
||||
and the *last tick* is the forced delay for the entire network, as well as the
|
||||
next leader before a new slot can be generated. The network can produce the
|
||||
*last tick* from the *penultimate tick*.
|
||||
|
||||
If the next leader receives the *penultimate tick* before it produces its own
|
||||
*first tick*, it will reset its PoH and produce the *first tick* from the
|
||||
previous leader's *penultimate tick*. The rest of the network will also reset
|
||||
its PoH to produce the *last tick* as the id to vote on.
|
||||
|
||||
The downsides:
|
||||
|
||||
* Every vote, and therefore confirmation, is delayed by a fixed timeout. 1 tick,
|
||||
or around 100ms.
|
||||
|
||||
* Average case confirmation time for a transaction would be at least 50ms worse.
|
||||
|
||||
* It is part of the ledger definition, so to change this behavior would require
|
||||
a hard fork.
|
||||
|
||||
* Not all the available space is used for entries.
|
||||
|
||||
The upsides compared to leader timeout:
|
||||
|
||||
* The next leader has received all the previous entries, so it can start
|
||||
processing transactions without recording them into PoH.
|
||||
|
||||
* The previous leader can redundantly transmit the last entry containing the
|
||||
*penultimate tick* to the next leader. The next leader can speculatively
|
||||
generate the *last tick* as soon as it receives the *penultimate tick*, even
|
||||
before verifying it.
|
167
book/src/leader-rotation.md
Normal file
167
book/src/leader-rotation.md
Normal file
@ -0,0 +1,167 @@
|
||||
# Leader Rotation
|
||||
|
||||
At any given moment, a cluster expects only one fullnode to produce ledger
|
||||
entries. By having only one leader at a time, all validators are able to replay
|
||||
identical copies of the ledger. The drawback of only one leader at a time,
|
||||
however, is that a malicious leader is capable of censoring votes and
|
||||
transactions. Since censoring cannot be distinguished from the network dropping
|
||||
packets, the cluster cannot simply elect a single node to hold the leader role
|
||||
indefinitely. Instead, the cluster minimizes the influence of a malicious
|
||||
leader by rotating which node takes the lead.
|
||||
|
||||
Each validator selects the expected leader using the same algorithm, described
|
||||
below. When the validator receives a new signed ledger entry, it can be certain
|
||||
that entry was produced by the expected leader. The order of slots which each
|
||||
leader is assigned a slot is called a *leader schedule*.
|
||||
|
||||
## Leader Schedule Rotation
|
||||
|
||||
A validator rejects blocks that are not signed by the *slot leader*. The list
|
||||
of identities of all slot leaders is called a *leader schedule*. The leader
|
||||
schedule is recomputed locally and periodically. It assigns slot leaders for a
|
||||
duration of time called an _epoch_. The schedule must be computed far in advance
|
||||
of the slots it assigns, such that the ledger state it uses to compute the
|
||||
schedule is finalized. That duration is called the *leader schedule offset*.
|
||||
Solana sets the offset to the duration of slots until the next epoch. That is,
|
||||
the leader schedule for an epoch is calculated from the ledger state at the
|
||||
start of the previous epoch. The offset of one epoch is fairly arbitrary and
|
||||
assumed to be sufficiently long such that all validators will have finalized
|
||||
their ledger state before the next schedule is generated. A cluster may choose
|
||||
to shorten the offset to reduce the time between stake changes and leader
|
||||
schedule updates.
|
||||
|
||||
While operating without partitions lasting longer than an epoch, the schedule
|
||||
only needs to be generated when the root fork crosses the epoch boundary. Since
|
||||
the schedule is for the next epoch, any new stakes committed to the root fork
|
||||
will not be active until the next epoch. The block used for generating the
|
||||
leader schedule is the first block to cross the epoch boundary.
|
||||
|
||||
Without a partition lasting longer than an epoch, the cluster will work as
|
||||
follows:
|
||||
|
||||
1. A validator continuously updates its own root fork as it votes.
|
||||
|
||||
2. The validator updates its leader schedule each time the slot height crosses
|
||||
an epoch boundary.
|
||||
|
||||
For example:
|
||||
|
||||
The epoch duration is 100 slots. The root fork is updated from fork computed at
|
||||
slot height 99 to a fork computed at slot height 102. Forks with slots at height
|
||||
100,101 were skipped because of failures. The new leader schedule is computed
|
||||
using fork at slot height 102. It is active from slot 200 until it is updated
|
||||
again.
|
||||
|
||||
No inconsistency can exist because every validator that is voting with the
|
||||
cluster has skipped 100 and 101 when its root passes 102. All validators,
|
||||
regardless of voting pattern, would be committing to a root that is either 102,
|
||||
or a descendant of 102.
|
||||
|
||||
### Leader Schedule Rotation with Epoch Sized Partitions.
|
||||
|
||||
The duration of the leader schedule offset has a direct relationship to the
|
||||
likelihood of a cluster having an inconsistent view of the correct leader
|
||||
schedule.
|
||||
|
||||
Consider the following scenario:
|
||||
|
||||
Two partitions that are generating half of the blocks each. Neither is coming
|
||||
to a definitive supermajority fork. Both will cross epoch 100 and 200 without
|
||||
actually committing to a root and therefore a cluster wide commitment to a new
|
||||
leader schedule.
|
||||
|
||||
In this unstable scenario, multiple valid leader schedules exist.
|
||||
|
||||
* A leader schedule is generated for every fork whose direct parent is in the
|
||||
previous epoch.
|
||||
|
||||
* The leader schedule is valid after the start of the next epoch for descendant
|
||||
forks until it is updated.
|
||||
|
||||
Each partition's schedule will diverge after the partition lasts more than an
|
||||
epoch. For this reason, the epoch duration should be selected to be much much
|
||||
larger then slot time and the expected length for a fork to be committed to
|
||||
root.
|
||||
|
||||
After observing the cluster for a sufficient amount of time, the leader schedule
|
||||
offset can be selected based on the median partition duration and its standard
|
||||
deviation. For example, an offset longer then the median partition duration
|
||||
plus six standard deviations would reduce the likelihood of an inconsistent
|
||||
ledger schedule in the cluster to 1 in 1 million.
|
||||
|
||||
## Leader Schedule Generation at Genesis
|
||||
|
||||
The genesis block declares the first leader for the first epoch. This leader
|
||||
ends up scheduled for the first two epochs because the leader schedule is also
|
||||
generated at slot 0 for the next epoch. The length of the first two epochs can
|
||||
be specified in the genesis block as well. The minimum length of the first
|
||||
epochs must be greater than or equal to the maximum rollback depth as defined in
|
||||
[fork selection](fork-selection.md).
|
||||
|
||||
## Leader Schedule Generation Algorithm
|
||||
|
||||
Leader schedule is generated using a predefined seed. The process is as follows:
|
||||
|
||||
1. Periodically use the PoH tick height (a monotonically increasing counter) to
|
||||
seed a stable pseudo-random algorithm.
|
||||
2. At that height, sample the bank for all the staked accounts with leader
|
||||
identities that have voted within a cluster-configured number of ticks. The
|
||||
sample is called the *active set*.
|
||||
3. Sort the active set by stake weight.
|
||||
4. Use the random seed to select nodes weighted by stake to create a
|
||||
stake-weighted ordering.
|
||||
5. This ordering becomes valid after a cluster-configured number of ticks.
|
||||
|
||||
## Schedule Attack Vectors
|
||||
|
||||
### Seed
|
||||
|
||||
The seed that is selected is predictable but unbiasable. There is no grinding
|
||||
attack to influence its outcome.
|
||||
|
||||
### Active Set
|
||||
|
||||
A leader can bias the active set by censoring validator votes. Two possible
|
||||
ways exist for leaders to censor the active set:
|
||||
|
||||
* Ignore votes from validators
|
||||
* Refuse to vote for blocks with votes from validators
|
||||
|
||||
To reduce the likelihood of censorship, the active set is calculated at the
|
||||
leader schedule offset boundary over an *active set sampling duration*. The
|
||||
active set sampling duration is long enough such that votes will have been
|
||||
collected by multiple leaders.
|
||||
|
||||
### Staking
|
||||
|
||||
Leaders can censor new staking transactions or refuse to validate blocks with
|
||||
new stakes. This attack is similar to censorship of validator votes.
|
||||
|
||||
### Validator operational key loss
|
||||
|
||||
Leaders and validators are expected to use ephemeral keys for operation, and
|
||||
stake owners authorize the validators to do work with their stake via
|
||||
delegation.
|
||||
|
||||
The cluster should be able to recover from the loss of all the ephemeral keys
|
||||
used by leaders and validators, which could occur through a common software
|
||||
vulnerability shared by all the nodes. Stake owners should be able to vote
|
||||
directly co-sign a validator vote even though the stake is currently delegated
|
||||
to a validator.
|
||||
|
||||
## Appending Entries
|
||||
|
||||
The lifetime of a leader schedule is called an *epoch*. The epoch is split into
|
||||
*slots*, where each slot has a duration of `T` PoH ticks.
|
||||
|
||||
A leader transmits entries during its slot. After `T` ticks, all the
|
||||
validators switch to the next scheduled leader. Validators must ignore entries
|
||||
sent outside a leader's assigned slot.
|
||||
|
||||
All `T` ticks must be observed by the next leader for it to build its own
|
||||
entries on. If entries are not observed (leader is down) or entries are invalid
|
||||
(leader is buggy or malicious), the next leader must produce ticks to fill the
|
||||
previous leader's slot. Note that the next leader should do repair requests in
|
||||
parallel, and postpone sending ticks until it is confident other validators
|
||||
also failed to observe the previous leader's entries. If a leader incorrectly
|
||||
builds on its own ticks, the leader following it must replace all its ticks.
|
84
book/src/leader-validator-transition.md
Normal file
84
book/src/leader-validator-transition.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Leader-to-Validator Transition
|
||||
|
||||
A fullnode typically operates as a validator. If, however, a staker delegates
|
||||
its stake to a fullnode, it will occasionally be selected as a *slot leader*.
|
||||
As a slot leader, the fullnode is responsible for producing blocks during an
|
||||
assigned *slot*. A slot has a duration of some number of preconfigured *ticks*.
|
||||
The duration of those ticks are estimated with a *PoH Recorder* described later
|
||||
in this document.
|
||||
|
||||
## BankFork
|
||||
|
||||
BankFork tracks changes to the bank state over a specific slot. Once the final
|
||||
tick has been registered the state is frozen. Any attempts to write to are
|
||||
rejected.
|
||||
|
||||
## Validator
|
||||
|
||||
A validator operates on many different concurrent forks of the bank state until
|
||||
it generates a PoH hash with a height within its leader slot.
|
||||
|
||||
## Slot Leader
|
||||
|
||||
A slot leader builds blocks on top of only one fork, the one it last voted on.
|
||||
|
||||
## PoH Recorder
|
||||
|
||||
Slot leaders and validators use a PoH Recorder for both estimating slot height
|
||||
and for recording transactions.
|
||||
|
||||
### PoH Recorder when Validating
|
||||
|
||||
The PoH Recorder acts as a simple VDF when validating. It tells the validator
|
||||
when it needs to switch to the slot leader role. Every time the validator votes
|
||||
on a fork, it should use the fork's latest block id to re-seed the VDF.
|
||||
Re-seeding solves two problems. First, it synchronizes its VDF to the leader's,
|
||||
allowing it to more accurately determine when its leader slot begins. Second,
|
||||
if the previous leader goes down, all wallclock time is accounted for in the
|
||||
next leader's PoH stream. For example, if one block is missing when the leader
|
||||
starts, the block it produces should have a PoH duration of two blocks. The
|
||||
longer duration ensures the following leader isn't attempting to snip all the
|
||||
transactions from the previous leader's slot.
|
||||
|
||||
### PoH Recorder when Leading
|
||||
|
||||
A slot leader use the PoH Recorder to record transactions, locking their
|
||||
positions in time. The PoH hash must be derived from a previous leader's last
|
||||
block. If it isn't, its block will fail PoH verification and be rejected by
|
||||
the cluster.
|
||||
|
||||
The PoH Recorder also serves to inform the slot leader when its slot is over.
|
||||
The leader needs to take care not to modify its bank if recording the
|
||||
transaction would generate a PoH height outside its designated slot. The
|
||||
leader, therefore, should not commit account changes until after it generates
|
||||
the entry's PoH hash. When the PoH height falls outside its slot any
|
||||
transactions in its pipeline may be dropped or forwarded to the next leader.
|
||||
Forwarding is preferred, as it would minimize network congestion, allowing the
|
||||
cluster to advertise higher TPS capacity.
|
||||
|
||||
|
||||
## Fullnode Loop
|
||||
|
||||
The PoH Recorder manages the transition between modes. Once a ledger is
|
||||
replayed, the validator can run until the recorder indicates it should be
|
||||
the slot leader. As a slot leader, the node can then execute and record
|
||||
transactions.
|
||||
|
||||
The loop is synchronized to PoH and does a synchronous start and stop of the
|
||||
slot leader functionality. After stopping, the validator's TVU should find
|
||||
itself in the same state as if a different leader had sent it the same block.
|
||||
The following is pseudocode for the loop:
|
||||
|
||||
1. Query the LeaderScheduler for the next assigned slot.
|
||||
2. Run the TVU over all the forks.
|
||||
1. TVU will send votes to what it believes is the "best" fork.
|
||||
2. After each vote, restart the PoH Recorder to run until the next assigned
|
||||
slot.
|
||||
3. When time to be a slot leader, start the TPU. Point it to the last fork the
|
||||
TVU voted on.
|
||||
4. Produce entries until the end of the slot.
|
||||
1. For the duration of the slot, the TVU must not vote on other forks.
|
||||
2. After the slot ends, the TPU freezes its BankFork. After freezing,
|
||||
the TVU may resume voting.
|
||||
5. Goto 1.
|
||||
|
39
book/src/ledger-replication-to-implement.md
Normal file
39
book/src/ledger-replication-to-implement.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Ledger Replication
|
||||
|
||||
Replication behavior yet to be implemented.
|
||||
|
||||
### Validator behavior
|
||||
|
||||
3. Every NUM\_KEY\_ROTATION\_TICKS it also validates samples received from
|
||||
replicators. It signs the PoH hash at that point and uses the following
|
||||
algorithm with the signature as the input:
|
||||
- The low 5 bits of the first byte of the signature creates an index into
|
||||
another starting byte of the signature.
|
||||
- The validator then looks at the set of storage proofs where the byte of
|
||||
the proof's sha state vector starting from the low byte matches exactly
|
||||
with the chosen byte(s) of the signature.
|
||||
- If the set of proofs is larger than the validator can handle, then it
|
||||
increases to matching 2 bytes in the signature.
|
||||
- Validator continues to increase the number of matching bytes until a
|
||||
workable set is found.
|
||||
- It then creates a mask of valid proofs and fake proofs and sends it to
|
||||
the leader. This is a storage proof confirmation transaction.
|
||||
5. After a lockout period of NUM\_SECONDS\_STORAGE\_LOCKOUT seconds, the
|
||||
validator then submits a storage proof claim transaction which then causes the
|
||||
distribution of the storage reward if no challenges were seen for the proof to
|
||||
the validators and replicators party to the proofs.
|
||||
|
||||
### Replicator behavior
|
||||
|
||||
9. The replicator then generates another set of offsets which it submits a fake
|
||||
proof with an incorrect sha state. It can be proven to be fake by providing the
|
||||
seed for the hash result.
|
||||
- A fake proof should consist of a replicator hash of a signature of a PoH
|
||||
value. That way when the replicator reveals the fake proof, it can be
|
||||
verified on chain.
|
||||
10. The replicator monitors the ledger, if it sees a fake proof integrated, it
|
||||
creates a challenge transaction and submits it to the current leader. The
|
||||
transacation proves the validator incorrectly validated a fake storage proof.
|
||||
The replicator is rewarded and the validator's staking balance is slashed or
|
||||
frozen.
|
||||
|
190
book/src/ledger-replication.md
Normal file
190
book/src/ledger-replication.md
Normal file
@ -0,0 +1,190 @@
|
||||
# Ledger Replication
|
||||
|
||||
At full capacity on a 1gbps network solana will generate 4 petabytes of data
|
||||
per year. To prevent the network from centralizing around full nodes that have
|
||||
to store the full data set this protocol proposes a way for mining nodes to
|
||||
provide storage capacity for pieces of the network.
|
||||
|
||||
The basic idea to Proof of Replication is encrypting a dataset with a public
|
||||
symmetric key using CBC encryption, then hash the encrypted dataset. The main
|
||||
problem with the naive approach is that a dishonest storage node can stream the
|
||||
encryption and delete the data as its hashed. The simple solution is to force
|
||||
the hash to be done on the reverse of the encryption, or perhaps with a random
|
||||
order. This ensures that all the data is present during the generation of the
|
||||
proof and it also requires the validator to have the entirety of the encrypted
|
||||
data present for verification of every proof of every identity. So the space
|
||||
required to validate is `number_of_proofs * data_size`
|
||||
|
||||
## Optimization with PoH
|
||||
|
||||
Our improvement on this approach is to randomly sample the encrypted segments
|
||||
faster than it takes to encrypt, and record the hash of those samples into the
|
||||
PoH ledger. Thus the segments stay in the exact same order for every PoRep and
|
||||
verification can stream the data and verify all the proofs in a single batch.
|
||||
This way we can verify multiple proofs concurrently, each one on its own CUDA
|
||||
core. The total space required for verification is `1_ledger_segment +
|
||||
2_cbc_blocks * number_of_identities` with core count equal to
|
||||
`number_of_identities`. We use a 64-byte chacha CBC block size.
|
||||
|
||||
## Network
|
||||
|
||||
Validators for PoRep are the same validators that are verifying transactions.
|
||||
They have some stake that they have put up as collateral that ensures that
|
||||
their work is honest. If you can prove that a validator verified a fake PoRep,
|
||||
then the validators stake can be slashed.
|
||||
|
||||
Replicators are specialized *light clients*. They download a part of the ledger
|
||||
and store it, and provide PoReps of storing the ledger. For each verified PoRep
|
||||
replicators earn a reward of sol from the mining pool.
|
||||
|
||||
## Constraints
|
||||
|
||||
We have the following constraints:
|
||||
* Verification requires generating the CBC blocks. That requires space of 2
|
||||
blocks per identity, and 1 CUDA core per identity for the same dataset. So as
|
||||
many identities at once should be batched with as many proofs for those
|
||||
identities verified concurrently for the same dataset.
|
||||
* Validators will randomly sample the set of storage proofs to the set that
|
||||
they can handle, and only the creators of those chosen proofs will be
|
||||
rewarded. The validator can run a benchmark whenever its hardware configuration
|
||||
changes to determine what rate it can validate storage proofs.
|
||||
|
||||
## Validation and Replication Protocol
|
||||
|
||||
### Constants
|
||||
|
||||
1. NUM\_STORAGE\_ENTRIES: Number of entries in a segment of ledger data. The
|
||||
unit of storage for a replicator.
|
||||
2. NUM\_KEY\_ROTATION\_TICKS: Number of ticks to save a PoH value and cause a
|
||||
key generation for the section of ledger just generated and the rotation of
|
||||
another key in the set.
|
||||
3. NUM\_STORAGE\_PROOFS: Number of storage proofs required for a storage proof
|
||||
claim to be successfully rewarded.
|
||||
4. RATIO\_OF\_FAKE\_PROOFS: Ratio of fake proofs to real proofs that a storage
|
||||
mining proof claim has to contain to be valid for a reward.
|
||||
5. NUM\_STORAGE\_SAMPLES: Number of samples required for a storage mining
|
||||
proof.
|
||||
6. NUM\_CHACHA\_ROUNDS: Number of encryption rounds performed to generate
|
||||
encrypted state.
|
||||
|
||||
### Validator behavior
|
||||
|
||||
1. Validator joins the network and submits a storage validation capacity
|
||||
transaction which tells the network how many proofs it can process in a given
|
||||
period defined by NUM\_KEY\_ROTATION\_TICKS.
|
||||
2. Every NUM\_KEY\_ROTATION\_TICKS the validator stores the PoH value at that
|
||||
height.
|
||||
3. Validator generates a storage proof confirmation transaction.
|
||||
4. The storage proof confirmation transaction is integrated into the ledger.
|
||||
6. Validator responds to RPC interfaces for what the last storage epoch PoH
|
||||
value is and its entry\_height.
|
||||
|
||||
### Replicator behavior
|
||||
|
||||
1. Since a replicator is somewhat of a light client and not downloading all the
|
||||
ledger data, they have to rely on other full nodes (validators) for
|
||||
information. Any given validator may or may not be malicious and give incorrect
|
||||
information, although there are not any obvious attack vectors that this could
|
||||
accomplish besides having the replicator do extra wasted work. For many of the
|
||||
operations there are a number of options depending on how paranoid a replicator
|
||||
is:
|
||||
- (a) replicator can ask a validator
|
||||
- (b) replicator can ask multiple validators
|
||||
- (c) replicator can subscribe to the full transaction stream and generate
|
||||
the information itself
|
||||
- (d) replicator can subscribe to an abbreviated transaction stream to
|
||||
generate the information itself
|
||||
2. A replicator obtains the PoH hash corresponding to the last key rotation
|
||||
along with its entry\_height.
|
||||
3. The replicator signs the PoH hash with its keypair. That signature is the
|
||||
seed used to pick the segment to replicate and also the encryption key. The
|
||||
replicator mods the signature with the entry\_height to get which segment to
|
||||
replicate.
|
||||
4. The replicator retrives the ledger by asking peer validators and
|
||||
replicators. See 6.5.
|
||||
5. The replicator then encrypts that segment with the key with chacha algorithm
|
||||
in CBC mode with NUM\_CHACHA\_ROUNDS of encryption.
|
||||
6. The replicator initializes a chacha rng with the signature from step 2 as
|
||||
the seed.
|
||||
7. The replicator generates NUM\_STORAGE\_SAMPLES samples in the range of the
|
||||
entry size and samples the encrypted segment with sha256 for 32-bytes at each
|
||||
offset value. Sampling the state should be faster than generating the encrypted
|
||||
segment.
|
||||
8. The replicator sends a PoRep proof transaction which contains its sha state
|
||||
at the end of the sampling operation, its seed and the samples it used to the
|
||||
current leader and it is put onto the ledger.
|
||||
|
||||
|
||||
### Finding who has a given block of ledger
|
||||
|
||||
1. Validators monitor the transaction stream for storage mining proofs, and
|
||||
keep a mapping of ledger segments by entry\_height to public keys. When it sees
|
||||
a storage mining proof it updates this mapping and provides an RPC interface
|
||||
which takes an entry\_height and hands back a list of public keys. The client
|
||||
then looks up in their cluster\_info table to see which network address that
|
||||
corresponds to and sends a repair request to retrieve the necessary blocks of
|
||||
ledger.
|
||||
2. Validators would need to prune this list which it could do by periodically
|
||||
looking at the oldest entries in its mappings and doing a network query to see
|
||||
if the storage host is still serving the first entry.
|
||||
|
||||
## Sybil attacks
|
||||
|
||||
For any random seed, we force everyone to use a signature that is derived from
|
||||
a PoH hash. Everyone must use the same count, so the same PoH hash is signed by
|
||||
every participant. The signatures are then each cryptographically tied to the
|
||||
keypair, which prevents a leader from grinding on the resulting value for more
|
||||
than 1 identity.
|
||||
|
||||
Since there are many more client identities then encryption identities, we need
|
||||
to split the reward for multiple clients, and prevent Sybil attacks from
|
||||
generating many clients to acquire the same block of data. To remain BFT we
|
||||
want to avoid a single human entity from storing all the replications of a
|
||||
single chunk of the ledger.
|
||||
|
||||
Our solution to this is to force the clients to continue using the same
|
||||
identity. If the first round is used to acquire the same block for many client
|
||||
identities, the second round for the same client identities will force a
|
||||
redistribution of the signatures, and therefore PoRep identities and blocks.
|
||||
Thus to get a reward for replicators need to store the first block for free and
|
||||
the network can reward long lived client identities more than new ones.
|
||||
|
||||
## Validator attacks
|
||||
|
||||
- If a validator approves fake proofs, replicator can easily out them by
|
||||
showing the initial state for the hash.
|
||||
- If a validator marks real proofs as fake, no on-chain computation can be done
|
||||
to distinguish who is correct. Rewards would have to rely on the results from
|
||||
multiple validators in a stake-weighted fashion to catch bad actors and
|
||||
replicators from being locked out of the network.
|
||||
- Validator stealing mining proof results for itself. The proofs are derived
|
||||
from a signature from a replicator, since the validator does not know the
|
||||
private key used to generate the encryption key, it cannot be the generator of
|
||||
the proof.
|
||||
|
||||
## Reward incentives
|
||||
|
||||
Fake proofs are easy to generate but difficult to verify. For this reason,
|
||||
PoRep proof transactions generated by replicators may require a higher fee than
|
||||
a normal transaction to represent the computational cost required by
|
||||
validators.
|
||||
|
||||
Some percentage of fake proofs are also necessary to receive a reward from
|
||||
storage mining.
|
||||
|
||||
## Notes
|
||||
|
||||
* We can reduce the costs of verification of PoRep by using PoH, and actually
|
||||
make it feasible to verify a large number of proofs for a global dataset.
|
||||
* We can eliminate grinding by forcing everyone to sign the same PoH hash and
|
||||
use the signatures as the seed
|
||||
* The game between validators and replicators is over random blocks and random
|
||||
encryption identities and random data samples. The goal of randomization is
|
||||
to prevent colluding groups from having overlap on data or validation.
|
||||
* Replicator clients fish for lazy validators by submitting fake proofs that
|
||||
they can prove are fake.
|
||||
* To defend against Sybil client identities that try to store the same block we
|
||||
force the clients to store for multiple rounds before receiving a reward.
|
||||
* Validators should also get rewarded for validating submitted storage proofs
|
||||
as incentive for storing the ledger. They can only validate proofs if they
|
||||
are storing that slice of the ledger.
|
63
book/src/managing-forks.md
Normal file
63
book/src/managing-forks.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Managing Forks in the Ledger
|
||||
|
||||
The ledger is permitted to fork at slot boundaries. The resulting data
|
||||
structure forms a tree called a *blocktree*. When the fullnode interprets the
|
||||
blocktree, it must maintain state for each fork in the chain. We call each
|
||||
instance an *active fork*. It is the responsibility of a fullnode to weigh
|
||||
those forks, such that it may eventually select a fork.
|
||||
|
||||
A fullnode selects a fork by submiting a vote to a slot leader on that fork.
|
||||
The vote commits the fullnode for a duration of time called a *lockout period*.
|
||||
The fullnode is not permitted to vote on a different fork until that lockout
|
||||
period expires. Each subsequent vote on the same fork doubles the length of the
|
||||
lockout period. After some cluster-configured number of votes (currently 32),
|
||||
the length of the lockout period reaches what's called *max lockout*. Until the
|
||||
max lockout is reached, the fullnode has the option to wait until the lockout
|
||||
period is over and then vote on another fork. When it votes on another fork, it
|
||||
performs a operation called *rollback*, whereby the state rolls back in time to
|
||||
a shared checkpoint and then jumps forward to the tip of the fork that it just
|
||||
voted on. The maximum distance that a fork may roll back is called the
|
||||
*rollback depth*. Rollback depth is the number of votes required to achieve
|
||||
max lockout. Whenever a fullnode votes, any checkpoints beyond the rollback
|
||||
depth become unreachable. That is, there is no scenario in which the fullnode
|
||||
will need to roll back beyond rollback depth. It therefore may safely *prune*
|
||||
unreachable forks and *squash* all checkpoints beyond rollback depth into the
|
||||
root checkpoint.
|
||||
|
||||
## Active Forks
|
||||
|
||||
An active fork is as a sequence of checkpoints that has a length at least one
|
||||
longer than the rollback depth. The shortest fork will have a length exactly
|
||||
one longer than the rollback depth. For example:
|
||||
|
||||
<img alt="Forks" src="img/forks.svg" class="center"/>
|
||||
|
||||
The following sequences are *active forks*:
|
||||
|
||||
* {4, 2, 1}
|
||||
* {5, 2, 1}
|
||||
* {6, 3, 1}
|
||||
* {7, 3, 1}
|
||||
|
||||
## Pruning and Squashing
|
||||
|
||||
A fullnode may vote on any checkpoint in the tree. In the diagram above,
|
||||
that's every node except the leaves of the tree. After voting, the fullnode
|
||||
prunes nodes that fork from a distance farther than the rollback depth and then
|
||||
takes the opportunity to minimize its memory usage by squashing any nodes it
|
||||
can into the root.
|
||||
|
||||
Starting from the example above, wth a rollback depth of 2, consider a vote on
|
||||
5 versus a vote on 6. First, a vote on 5:
|
||||
|
||||
<img alt="Forks after pruning" src="img/forks-pruned.svg" class="center"/>
|
||||
|
||||
The new root is 2, and any active forks that are not descendants from 2 are
|
||||
pruned.
|
||||
|
||||
Alternatively, a vote on 6:
|
||||
|
||||
<img alt="Forks" src="img/forks-pruned2.svg" class="center"/>
|
||||
|
||||
The tree remains with a root of 1, since the active fork starting at 6 is only
|
||||
2 checkpoints from the root.
|
153
book/src/persistent-account-storage.md
Normal file
153
book/src/persistent-account-storage.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Persistent Account Storage
|
||||
|
||||
The set of Accounts represent the current computed state of all the transactions
|
||||
that have been processed by a fullnode. Each fullnode needs to maintain this
|
||||
entire set. Each block that is proposed by the network represents a change to
|
||||
this set, and since each block is a potential rollback point the changes need to
|
||||
be reversible.
|
||||
|
||||
Persistent storage like NVMEs are 20 to 40 times cheaper than DDR. The problem
|
||||
with persistent storage is that write and read performance is much slower than
|
||||
DDR and care must be taken in how data is read or written to. Both reads and
|
||||
writes can be split between multiple storage drives and accessed in parallel.
|
||||
This design proposes a data structure that allows for concurrent reads and
|
||||
concurrent writes of storage. Writes are optimized by using an AppendVec data
|
||||
structure, which allows a single writer to append while allowing access to many
|
||||
concurrent readers. The accounts index maintains a pointer to a spot where the
|
||||
account was appended to every fork, thus removing the need for explicit
|
||||
checkpointing of state.
|
||||
|
||||
# AppendVec
|
||||
|
||||
AppendVec is a data structure that allows for random reads concurrent with a
|
||||
single append-only writer. Growing or resizing the capacity of the AppendVec
|
||||
requires exclusive access. This is implemented with an atomic `offset`, which
|
||||
is updated at the end of a completed append.
|
||||
|
||||
The underlying memory for an AppendVec is a memory-mapped file. Memory-mapped
|
||||
files allow for fast random access and paging is handled by the OS.
|
||||
|
||||
# Account Index
|
||||
|
||||
The account index is designed to support a single index for all the currently
|
||||
forked Accounts.
|
||||
|
||||
```rust,ignore
|
||||
type AppendVecId = usize;
|
||||
|
||||
type Fork = u64;
|
||||
|
||||
struct AccountMap(Hashmap<Fork, (AppendVecId, u64)>);
|
||||
|
||||
type AccountIndex = HashMap<Pubkey, AccountMap>;
|
||||
|
||||
```
|
||||
|
||||
The index is a map of account Pubkeys to a map of Forks and the location of the
|
||||
Account data in an AppendVec. To get the version of an account for a specific Fork:
|
||||
|
||||
```rust,ignore
|
||||
/// Load the account for the pubkey.
|
||||
/// This function will load the account from the specified fork, falling back to the fork's parents
|
||||
/// * fork - a virtual Accounts instance, keyed by Fork. Accounts keep track of their parents with Forks,
|
||||
/// the persistent store
|
||||
/// * pubkey - The Account's public key.
|
||||
pub fn load_slow(&self, id: Fork, pubkey: &Pubkey) -> Option<&Account>
|
||||
```
|
||||
|
||||
The read is satisfied by pointing to a memory-mapped location in the
|
||||
`AppendVecId` at the stored offset. A reference can be returned without a copy.
|
||||
|
||||
## Root Forks
|
||||
|
||||
The [fork selection algorithm](fork-selection.md) eventually selects a fork as a
|
||||
root fork and the fork is squashed. A squashed/root fork cannot be rolled back.
|
||||
|
||||
When a fork is squashed, all accounts in its parents not already present in the
|
||||
fork are pulled up into the fork by updating the indexes. Accounts with zero
|
||||
balance in the squashed fork are removed from fork by updating the indexes.
|
||||
|
||||
An account can be *garbage-collected* when squashing makes it unreachable.
|
||||
|
||||
Three possible options exist:
|
||||
|
||||
* Maintain a HashSet<u64> of root forks. One is expected to be created every
|
||||
second. The entire tree can be garbage-collected later. Alternatively, if
|
||||
every fork keeps a reference count of accounts, garbage collection could occur
|
||||
any time an index location is updated.
|
||||
|
||||
* Remove any pruned forks from the index. Any remaining forks lower in number
|
||||
than the root are can be considered root.
|
||||
|
||||
* Scan the index, migrate any old roots into the new one. Any remaining forks
|
||||
lower than the new root can be deleted later.
|
||||
|
||||
# Append-only Writes
|
||||
|
||||
All the updates to Accounts occur as append-only updates. For every account
|
||||
update, a new version is stored in the AppendVec.
|
||||
|
||||
It is possible to optimize updates within a single fork by returning a mutable
|
||||
reference to an already stored account in a fork. The Bank already tracks
|
||||
concurrent access of accounts and guarantees that a write to a specific account
|
||||
fork will not be concurrent with a read to an account at that fork. To support
|
||||
this operation, AppendVec should implement this function:
|
||||
|
||||
```rust,ignore
|
||||
fn get_mut(&self, index: u64) -> &mut T;
|
||||
```
|
||||
|
||||
This API allows for concurrent mutable access to a memory region at `index`. It
|
||||
relies on the Bank to guarantee exclusive access to that index.
|
||||
|
||||
# Garbage collection
|
||||
|
||||
As accounts get updated, they move to the end of the AppendVec. Once capacity
|
||||
has run out, a new AppendVec can be created and updates can be stored there.
|
||||
Eventually references to an older AppendVec will disappear because all the
|
||||
accounts have been updated, and the old AppendVec can be deleted.
|
||||
|
||||
To speed up this process, it's possible to move Accounts that have not been
|
||||
recently updated to the front of a new AppendVec. This form of garbage
|
||||
collection can be done without requiring exclusive locks to any of the data
|
||||
structures except for the index update.
|
||||
|
||||
The initial implementation for garbage collection is that once all the accounts in
|
||||
an AppendVec become stale versions, it gets reused. The accounts are not updated
|
||||
or moved around once appended.
|
||||
|
||||
# Index Recovery
|
||||
|
||||
Each bank thread has exclusive access to the accounts during append, since the
|
||||
accounts locks cannot be released until the data is committed. But there is no
|
||||
explicit order of writes between the separate AppendVec files. To create an
|
||||
ordering, the index maintains an atomic write version counter. Each append to
|
||||
the AppendVec records the index write version number for that append in the
|
||||
entry for the Account in the AppendVec.
|
||||
|
||||
To recover the index, all the AppendVec files can be read in any order, and the
|
||||
latest write version for every fork should be stored in the index.
|
||||
|
||||
# Snapshots
|
||||
|
||||
To snapshot, the underlying memory-mapped files in the AppendVec need to be
|
||||
flushed to disk. The index can be written out to disk as well.
|
||||
|
||||
# Performance
|
||||
|
||||
* Append-only writes are fast. SSDs and NVMEs, as well as all the OS level
|
||||
kernel data structures, allow for appends to run as fast as PCI or NVMe bandwidth
|
||||
will allow (2,700 MB/s).
|
||||
|
||||
* Each replay and banking thread writes concurrently to its own AppendVec.
|
||||
|
||||
* Each AppendVec could potentially be hosted on a separate NVMe.
|
||||
|
||||
* Each replay and banking thread has concurrent read access to all the
|
||||
AppendVecs without blocking writes.
|
||||
|
||||
* Index requires an exclusive write lock for writes. Single-thread performance
|
||||
for HashMap updates is on the order of 10m per second.
|
||||
|
||||
* Banking and Replay stages should use 32 threads per NVMe. NVMes have
|
||||
optimal performance with 32 concurrent readers or writers.
|
65
book/src/programs.md
Normal file
65
book/src/programs.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Programming Model
|
||||
|
||||
A client *app* interacts with a Solana cluster by sending it *transactions*
|
||||
with one or more *instructions*. The Solana *runtime* passes those instructions
|
||||
to user-contributed *programs*. An instruction might, for example, tell a
|
||||
program to move *lamports* from one *account* to another or create an interactive
|
||||
contract that governs how lamports are moved. Instructions are executed
|
||||
atomically. If any instruction is invalid, any changes made within the
|
||||
transaction are discarded.
|
||||
|
||||
## Deploying Programs to a Cluster
|
||||
|
||||
<img alt="SDK tools" src="img/sdk-tools.svg" class="center"/>
|
||||
|
||||
As shown in the diagram above a client creates a program and compiles it to an
|
||||
ELF shared object containing BPF bytecode and sends it to the Solana cluster.
|
||||
The cluster stores the program locally and makes it available to clients via a
|
||||
*program ID*. The program ID is a *public key* generated by the client and is
|
||||
used to reference the program in subsequent transactions.
|
||||
|
||||
A program may be written in any programming language that can target the
|
||||
Berkley Packet Filter (BPF) safe execution environment. The Solana SDK offers
|
||||
the best support for C programs, which is compiled to BPF using the [LLVM
|
||||
compiler infrastructure](https://llvm.org).
|
||||
|
||||
## Storing State between Transactions
|
||||
|
||||
If the program needs to store state between transactions, it does so using
|
||||
*accounts*. Accounts are similar to files in operating systems such as Linux.
|
||||
Like a file, an account may hold arbitrary data and that data persists beyond
|
||||
the lifetime of a program. Also like a file, an account includes metadata that
|
||||
tells the runtime who is allowed to access the data and how. Unlike a file, the
|
||||
account includes metadata for the lifetime of the file. That lifetime is
|
||||
expressed in "tokens", which is a number of fractional native tokens, called
|
||||
*lamports*. Accounts are held in validator memory and pay "rent" to stay there.
|
||||
Each fullnode periodically scan all accounts and collects rent. Any account
|
||||
that drops to zero lamports is purged.
|
||||
|
||||
If an account is marked "executable", it will only be used by a *loader* to run
|
||||
programs. For example, a BPF-compiled program is marked executable and loaded
|
||||
by the BPF loader. No program is allowed to modify the contents of an
|
||||
executable account.
|
||||
|
||||
An account also includes "owner" metadata. The owner is a program ID. The
|
||||
runtime grants the program write access to the account if its ID matches the
|
||||
owner. If an account is not owned by a program, the program is permitted to
|
||||
read its data and credit the account.
|
||||
|
||||
In the same way that a Linux user uses a path to look up a file, a Solana
|
||||
client uses public keys to look up accounts. To create an account, the client
|
||||
generates a *keypair* and registers its public key using the CreateAccount
|
||||
instruction. Once registered, transactions reference account keys to grant
|
||||
programs access to accounts. The runtime grants programs read access by
|
||||
default. To grant write access, the client must either assign the account to a
|
||||
program or sign the transaction using the keypair's *secret key*. Since only
|
||||
the holder of the secret key can produce valid signatures matching the
|
||||
account's public key, the runtime recognizes the signature as authorization to
|
||||
modify account data or debit the account.
|
||||
|
||||
After the runtime executes each of the transaction's instructions, it uses the
|
||||
account metadata and transaction signatures to verify that none of the access
|
||||
rules were violated. If a program violates an access rule, the runtime discards
|
||||
all account changes made by all instructions and marks the transaction as
|
||||
failed.
|
||||
|
7
book/src/proposals.md
Normal file
7
book/src/proposals.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Proposed Architectural Changes
|
||||
|
||||
The following architectural proposals have been accepted by the Solana team, but
|
||||
are not yet fully implemented. The proposals may be implemented as described,
|
||||
implemented differently as issues in the designs become evident, or not
|
||||
implemented at all. If implemented, the descriptions will be moved from this
|
||||
section to earlier chapters in a future version of this book.
|
124
book/src/reliable-vote-transmission.md
Normal file
124
book/src/reliable-vote-transmission.md
Normal file
@ -0,0 +1,124 @@
|
||||
# Reliable Vote Transmission
|
||||
|
||||
Validator votes are messages that have a critical function for consensus and
|
||||
continuous operation of the network. Therefore it is critical that they are
|
||||
reliably delivered and encoded into the ledger.
|
||||
|
||||
## Challenges
|
||||
|
||||
1. Leader rotation is triggered by PoH, which is clock with high drift. So many
|
||||
nodes are likely to have an incorrect view if the next leader is active in
|
||||
realtime or not.
|
||||
|
||||
2. The next leader may be easily be flooded. Thus a DDOS would not only prevent
|
||||
delivery of regular transactions, but also consensus messages.
|
||||
|
||||
3. UDP is unreliable, and our asynchronous protocol requires any message that is
|
||||
transmitted to be retransmitted until it is observed in the ledger.
|
||||
Retransmittion could potentially cause an unintentional *thundering herd*
|
||||
against the leader with a large number of validators. Worst case flood would be
|
||||
`(num_nodes * num_retransmits)`.
|
||||
|
||||
4. Tracking if the vote has been transmitted or not via the ledger does not
|
||||
guarantee it will appear in a confirmed block. The current observed block may
|
||||
be unrolled. Validators would need to maintain state for each vote and fork.
|
||||
|
||||
|
||||
## Design
|
||||
|
||||
1. Send votes as a push message through gossip. This ensures delivery of the
|
||||
vote to all the next leaders, not just the next future one.
|
||||
|
||||
2. Leaders will read the Crds table for new votes and encode any new received
|
||||
votes into the blocks they propose. This allows for validator votes to be
|
||||
included in rollback forks by all the future leaders.
|
||||
|
||||
3. Validators that receive votes in the ledger will add them to their local crds
|
||||
table, not as a push request, but simply add them to the table. This shortcuts
|
||||
the push message protocol, so the validation messages do not need to be
|
||||
retransmitted twice around the network.
|
||||
|
||||
4. CrdsValue for vote should look like this ``` Votes(Vec<Transaction>) ```
|
||||
|
||||
Each vote transaction should maintain a `wallclock` in its userdata. The merge
|
||||
strategy for Votes will keep the last N set of votes as configured by the local
|
||||
client. For push/pull the vector is traversed recursively and each Transaction
|
||||
is treated as an individual CrdsValue with its own local wallclock and
|
||||
signature.
|
||||
|
||||
Gossip is designed for efficient propagation of state. Messages that are sent
|
||||
through gossip-push are batched and propagated with a minimum spanning tree to
|
||||
the rest of the network. Any partial failures in the tree are actively repaired
|
||||
with the gossip-pull protocol while minimizing the amount of data transfered
|
||||
between any nodes.
|
||||
|
||||
|
||||
## How this design solves the Challenges
|
||||
|
||||
1. Because there is no easy way for validators to be in sync with leaders on the
|
||||
leader's "active" state, gossip allows for eventual delivery regardless of that
|
||||
state.
|
||||
|
||||
2. Gossip will deliver the messages to all the subsequent leaders, so if the
|
||||
current leader is flooded the next leader would have already received these
|
||||
votes and is able to encode them.
|
||||
|
||||
3. Gossip minimizes the number of requests through the network by maintaining an
|
||||
efficient spanning tree, and using bloom filters to repair state. So retransmit
|
||||
back-off is not necessary and messages are batched.
|
||||
|
||||
4. Leaders that read the crds table for votes will encode all the new valid
|
||||
votes that appear in the table. Even if this leader's block is unrolled, the
|
||||
next leader will try to add the same votes without any additional work done by
|
||||
the validator. Thus ensuring not only eventual delivery, but eventual encoding
|
||||
into the ledger.
|
||||
|
||||
|
||||
## Performance
|
||||
|
||||
1. Worst case propagation time to the next leader is Log(N) hops with a base
|
||||
depending on the fanout. With our current default fanout of 6, it is about 6
|
||||
hops to 20k nodes.
|
||||
|
||||
2. The leader should receive 20k validation votes aggregated by gossip-push into
|
||||
64kb blobs. Which would reduce the number of packets for 20k network to 80
|
||||
blobs.
|
||||
|
||||
3. Each validators votes is replicated across the entire network. To maintain a
|
||||
queue of 5 previous votes the Crds table would grow by 25 megabytes. `(20,000
|
||||
nodes * 256 bytes * 5)`.
|
||||
|
||||
## Two step implementation rollout
|
||||
|
||||
Initially the network can perform reliably with just 1 vote transmitted and
|
||||
maintained through the network with the current Vote implementation. For small
|
||||
networks a fanout of 6 is sufficient. With small network the memory and push
|
||||
overhead is minor.
|
||||
|
||||
### Sub 1k validator network
|
||||
|
||||
1. Crds just maintains the validators latest vote.
|
||||
|
||||
2. Votes are pushed and retransmitted regardless if they are appearing in the
|
||||
ledger.
|
||||
|
||||
3. Fanout of 6.
|
||||
|
||||
* Worst case 256kb memory overhead per node.
|
||||
* Worst case 4 hops to propagate to every node.
|
||||
* Leader should receive the entire validator vote set in 4 push message blobs.
|
||||
|
||||
### Sub 20k network
|
||||
|
||||
Everything above plus the following:
|
||||
|
||||
1. CRDS table maintains a vector of 5 latest validator votes.
|
||||
|
||||
2. Votes encode a wallclock. CrdsValue::Votes is a type that recurses into the
|
||||
transaction vector for all the gossip protocols.
|
||||
|
||||
3. Increase fanout to 20.
|
||||
|
||||
* Worst case 25mb memory overhead per node.
|
||||
* Sub 4 hops worst case to deliver to the entire network.
|
||||
* 80 blobs received by the leader for all the validator messages.
|
116
book/src/runtime.md
Normal file
116
book/src/runtime.md
Normal file
@ -0,0 +1,116 @@
|
||||
# The Runtime
|
||||
|
||||
The runtime is a concurrent transaction processor. Transactions specify their
|
||||
data dependencies upfront and dynamic memory allocation is explicit. By
|
||||
separating program code from the state it operates on, the runtime is able to
|
||||
choreograph concurrent access. Transactions accessing only credit-only
|
||||
accounts are executed in parallel whereas transactions accessing writable
|
||||
accounts are serialized. The runtime interacts with the program through an
|
||||
entrypoint with a well-defined interface. The userdata stored in an account is
|
||||
an opaque type, an array of bytes. The program has full control over its
|
||||
contents.
|
||||
|
||||
The transaction structure specifies a list of public keys and signatures for
|
||||
those keys and a sequential list of instructions that will operate over the
|
||||
states associated with the account keys. For the transaction to be committed
|
||||
all the instructions must execute successfully; if any abort the whole
|
||||
transaction fails to commit.
|
||||
|
||||
### Account Structure
|
||||
|
||||
Accounts maintain a lamport balance and program-specific memory.
|
||||
|
||||
# Transaction Engine
|
||||
|
||||
The engine maps public keys to accounts and routes them to the program's
|
||||
entrypoint.
|
||||
|
||||
## Execution
|
||||
|
||||
Transactions are batched and processed in a pipeline. The TPU and TVU follow a
|
||||
slightly different path. The TPU runtime ensures that PoH record occurs before
|
||||
memory is committed.
|
||||
|
||||
The TVU runtime ensures that PoH verification occurs before the runtime
|
||||
processes any transactions.
|
||||
|
||||
<img alt="Runtime pipeline" src="img/runtime.svg" class="center"/>
|
||||
|
||||
At the *execute* stage, the loaded accounts have no data dependencies, so all the
|
||||
programs can be executed in parallel.
|
||||
|
||||
The runtime enforces the following rules:
|
||||
|
||||
1. Only the *owner* program may modify the contents of an account. This means
|
||||
that upon assignment userdata vector is guaranteed to be zero.
|
||||
|
||||
2. Total balances on all the accounts is equal before and after execution of a
|
||||
transaction.
|
||||
|
||||
3. After the transaction is executed, balances of credit-only accounts must be
|
||||
greater than or equal to the balances before the transaction.
|
||||
|
||||
4. All instructions in the transaction executed atomically. If one fails, all
|
||||
account modifications are discarded.
|
||||
|
||||
Execution of the program involves mapping the program's public key to an
|
||||
entrypoint which takes a pointer to the transaction, and an array of loaded
|
||||
accounts.
|
||||
|
||||
## SystemProgram Interface
|
||||
|
||||
The interface is best described by the `Instruction::userdata` that the user
|
||||
encodes.
|
||||
|
||||
* `CreateAccount` - This allows the user to create an account with an allocated
|
||||
userdata array and assign it to a Program.
|
||||
|
||||
* `Assign` - Allows the user to assign an existing account to a program.
|
||||
|
||||
* `Move` - Moves lamports between accounts.
|
||||
|
||||
## Program State Security
|
||||
|
||||
For blockchain to function correctly, the program code must be resilient to user
|
||||
inputs. That is why in this design the program specific code is the only code
|
||||
that can change the state of the userdata byte array in the Accounts that are
|
||||
assigned to it. It is also the reason why `Assign` or `CreateAccount` must zero
|
||||
out the userdata. Otherwise there would be no possible way for the program to
|
||||
distinguish the recently assigned account userdata from a natively generated
|
||||
state transition without some additional metadata from the runtime to indicate
|
||||
that this memory is assigned instead of natively generated.
|
||||
|
||||
To pass messages between programs, the receiving program must accept the message
|
||||
and copy the state over. But in practice a copy isn't needed and is
|
||||
undesirable. The receiving program can read the state belonging to other
|
||||
Accounts without copying it, and during the read it has a guarantee of the
|
||||
sender program's state.
|
||||
|
||||
## Notes
|
||||
|
||||
* There is no dynamic memory allocation. Client's need to use `CreateAccount`
|
||||
instructions to create memory before passing it to another program. This
|
||||
instruction can be composed into a single transaction with the call to the
|
||||
program itself.
|
||||
|
||||
* `CreateAccount` and `Assign` guarantee that when account is assigned to the
|
||||
program, the Account's userdata is zero initialized.
|
||||
|
||||
* Once assigned to program an Account cannot be reassigned.
|
||||
|
||||
* Runtime guarantees that a program's code is the only code that can modify
|
||||
Account userdata that the Account is assigned to.
|
||||
|
||||
* Runtime guarantees that the program can only spend lamports that are in
|
||||
accounts that are assigned to it.
|
||||
|
||||
* Runtime guarantees the balances belonging to accounts are balanced before
|
||||
and after the transaction.
|
||||
|
||||
* Runtime guarantees that instructions all executed successfully when a
|
||||
transaction is committed.
|
||||
|
||||
# Future Work
|
||||
|
||||
* [Continuations and Signals for long running
|
||||
Transactions](https://github.com/solana-labs/solana/issues/1485)
|
68
book/src/stake-delegation-and-rewards.md
Normal file
68
book/src/stake-delegation-and-rewards.md
Normal file
@ -0,0 +1,68 @@
|
||||
# Stake Delegation and Rewards
|
||||
|
||||
Stakers are rewarded for helping validate the ledger. They do it by delegating
|
||||
their stake to fullnodes. Those fullnodes do the legwork and send votes to the
|
||||
stakers' staking accounts. The rest of the cluster uses those stake-weighted
|
||||
votes to select a block when forks arise. Both the fullnode and staker need
|
||||
some economic incentive to play their part. The fullnode needs to be
|
||||
compensated for its hardware and the staker needs to be compensated for risking
|
||||
getting its stake slashed. The economics are covered in [staking
|
||||
rewards](staking-rewards.md). This chapter, on the other hand, describes the
|
||||
underlying mechanics of its implementation.
|
||||
|
||||
## Vote and Rewards accounts
|
||||
|
||||
The rewards process is split into two on-chain programs. The Vote program
|
||||
solves the problem of making stakes slashable. The Rewards account acts as
|
||||
custodian of the rewards pool. It is responsible for paying out each staker
|
||||
once the staker proves to the Rewards program that it participated in
|
||||
validating the ledger.
|
||||
|
||||
The Vote account contains the following state information:
|
||||
|
||||
* votes - The submitted votes.
|
||||
|
||||
* `delegate_id` - An identity that may operate with the weight of this
|
||||
account's stake. It is typically the identity of a fullnode, but may be any
|
||||
identity involved in stake-weighted computations.
|
||||
|
||||
* `authorized_voter_id` - Only this identity is authorized to submit votes.
|
||||
|
||||
* `credits` - The amount of unclaimed rewards.
|
||||
|
||||
* `root_slot` - The last slot to reach the full lockout commitment necessary
|
||||
for rewards.
|
||||
|
||||
The Rewards program is stateless and pays out reward when a staker submits its
|
||||
Vote account to the program. Claiming a reward requires a transaction that
|
||||
includes the following instructions:
|
||||
|
||||
1. `RewardsInstruction::RedeemVoteCredits`
|
||||
2. `VoteInstruction::ClearCredits`
|
||||
|
||||
The Rewards program transfers lamports from the Rewards account to the Vote
|
||||
account's public key. The Rewards program also ensures that the `ClearCredits`
|
||||
instruction follows the `RedeemVoteCredits` instruction, such that a staker may
|
||||
not claim rewards for the same work more than once.
|
||||
|
||||
|
||||
### Delegating Stake
|
||||
|
||||
`VoteInstruction::DelegateStake` allows the staker to choose a fullnode to
|
||||
validate the ledger on its behalf. By being a delegate, the fullnode is
|
||||
entitled to collect transaction fees when its is leader. The larger the stake,
|
||||
the more often the fullnode will be able to collect those fees.
|
||||
|
||||
### Authorizing a Vote Signer
|
||||
|
||||
`VoteInstruction::AuthorizeVoter` allows a staker to choose a signing service
|
||||
for its votes. That service is responsible for ensuring the vote won't cause
|
||||
the staker to be slashed.
|
||||
|
||||
## Limitations
|
||||
|
||||
Many stakers may delegate their stakes to the same fullnode. The fullnode must
|
||||
send a separate vote to each staking account. If there are far more stakers
|
||||
than fullnodes, that's a lot of network traffic. An alternative design might
|
||||
have fullnodes submit each vote to just one account and then have each staker
|
||||
submit that account along with their own to collect its reward.
|
136
book/src/staking-rewards.md
Normal file
136
book/src/staking-rewards.md
Normal file
@ -0,0 +1,136 @@
|
||||
# Staking Rewards
|
||||
|
||||
Initial Proof of Stake (PoS) (i.e. using in-protocol asset, SOL, to provide
|
||||
secure consensus) design ideas outlined here. Solana will implement a proof of
|
||||
stake reward/security scheme for node validators in the cluster. The purpose is
|
||||
threefold:
|
||||
|
||||
- Align validator incentives with that of the greater cluster through
|
||||
skin-in-the-game deposits at risk
|
||||
- Avoid 'nothing at stake' fork voting issues by implementing slashing rules
|
||||
aimed at promoting fork convergence
|
||||
- Provide an avenue for validator rewards provided as a function of validator
|
||||
participation in the cluster.
|
||||
|
||||
While many of the details of the specific implementation are currently under
|
||||
consideration and are expected to come into focus through specific modeling
|
||||
studies and parameter exploration on the Solana testnet, we outline here our
|
||||
current thinking on the main components of the PoS system. Much of this
|
||||
thinking is based on the current status of Casper FFG, with optimizations and
|
||||
specific attributes to be modified as is allowed by Solana's Proof of History
|
||||
(PoH) blockchain data structure.
|
||||
|
||||
### General Overview
|
||||
|
||||
Solana's ledger validation design is based on a rotating, stake-weighted selected leader broadcasting transactions in a PoH data
|
||||
structure to validating nodes. These nodes, upon receiving the leader's
|
||||
broadcast, have the opportunity to vote on the current state and PoH height by
|
||||
signing a transaction into the PoH stream.
|
||||
|
||||
To become a Solana validator, a fullnode must deposit/lock-up some amount
|
||||
of SOL in a contract. This SOL will not be accessible for a specific time
|
||||
period. The precise duration of the staking lockup period has not been
|
||||
determined. However we can consider three phases of this time for which
|
||||
specific parameters will be necessary:
|
||||
|
||||
- *Warm-up period*: which SOL is deposited and inaccessible to the node,
|
||||
however PoH transaction validation has not begun. Most likely on the order of
|
||||
days to weeks
|
||||
- *Validation period*: a minimum duration for which the deposited SOL will be
|
||||
inaccessible, at risk of slashing (see slashing rules below) and earning
|
||||
rewards for the validator participation. Likely duration of months to a
|
||||
year.
|
||||
- *Cool-down period*: a duration of time following the submission of a
|
||||
'withdrawal' transaction. During this period validation responsibilities have
|
||||
been removed and the funds continue to be inaccessible. Accumulated rewards
|
||||
should be delivered at the end of this period, along with the return of the
|
||||
initial deposit.
|
||||
|
||||
Solana's trustless sense of time and ordering provided by its PoH data
|
||||
structure, along with its
|
||||
[avalanche](https://www.youtube.com/watch?v=qt_gDRXHrHQ&t=1s) data broadcast
|
||||
and transmission design, should provide sub-second transaction confirmation times that scale
|
||||
with the log of the number of nodes in the cluster. This means we shouldn't
|
||||
have to restrict the number of validating nodes with a prohibitive 'minimum
|
||||
deposits' and expect nodes to be able to become validators with nominal amounts
|
||||
of SOL staked. At the same time, Solana's focus on high-throughput should create incentive for validation clients to provide high-performant and reliable hardware. Combined with potential a minimum network speed threshold to join as a validation-client, we expect a healthy validation delegation market to emerge. To this end, Solana's testnet will lead into a "Tour de SOL" validation-client competition, focusing on throughput and uptime to rank and reward testnet validators.
|
||||
|
||||
|
||||
### Slashing rules
|
||||
|
||||
Unlike Proof of Work (PoW) where off-chain capital expenses are already
|
||||
deployed at the time of block construction/voting, PoS systems require
|
||||
capital-at-risk to prevent a logical/optimal strategy of multiple chain voting.
|
||||
We intend to implement slashing rules which, if broken, result some amount of
|
||||
the offending validator's deposited stake to be removed from circulation. Given
|
||||
the ordering properties of the PoH data structure, we believe we can simplify
|
||||
our slashing rules to the level of a voting lockout time assigned per vote.
|
||||
|
||||
I.e. Each vote has an associated lockout time (PoH duration) that represents a
|
||||
duration by any additional vote from that validator must be in a PoH that
|
||||
contains the original vote, or a portion of that validator's stake is
|
||||
slashable. This duration time is a function of the initial vote PoH count and
|
||||
all additional vote PoH counts. It will likely take the form:
|
||||
|
||||
Lockout<sub>i</sub>(PoH<sub>i</sub>, PoH<sub>j</sub>) = PoH<sub>j</sub> + K *
|
||||
exp((PoH<sub>j</sub> - PoH<sub>i</sub>) / K)
|
||||
|
||||
Where PoH<sub>i</sub> is the height of the vote that the lockout is to be
|
||||
applied to and PoH<sub>j</sub> is the height of the current vote on the same
|
||||
fork. If the validator submits a vote on a different PoH fork on any
|
||||
PoH<sub>k</sub> where k > j > i and PoH<sub>k</sub> < Lockout(PoH<sub>i</sub>,
|
||||
PoH<sub>j</sub>), then a portion of that validator's stake is at risk of being
|
||||
slashed.
|
||||
|
||||
In addition to the functional form lockout described above, early
|
||||
implementation may be a numerical approximation based on a First In, First Out
|
||||
(FIFO) data structure and the following logic:
|
||||
- FIFO queue holding 32 votes per active validator
|
||||
- new votes are pushed on top of queue (`push_front`)
|
||||
- expired votes are popped off top (`pop_front`)
|
||||
- as votes are pushed into the queue, the lockout of each queued vote doubles
|
||||
- votes are removed from back of queue if `queue.len() > 32`
|
||||
- the earliest and latest height that has been removed from the back of the
|
||||
queue should be stored
|
||||
|
||||
It is likely that a reward will be offered as a % of the slashed amount to any
|
||||
node that submits proof of this slashing condition being violated to the PoH.
|
||||
|
||||
#### Partial Slashing
|
||||
|
||||
In the schema described so far, when a validator votes on a given PoH stream,
|
||||
they are committing themselves to that fork for a time determined by the vote
|
||||
lockout. An open question is whether validators will be hesitant to begin
|
||||
voting on an available fork if the penalties are perceived too harsh for an
|
||||
honest mistake or flipped bit.
|
||||
|
||||
One way to address this concern would be a partial slashing design that results
|
||||
in a slashable amount as a function of either:
|
||||
|
||||
1. the fraction of validators, out of the total validator pool, that were also
|
||||
slashed during the same time period (ala Casper)
|
||||
2. the amount of time since the vote was cast (e.g. a linearly increasing % of
|
||||
total deposited as slashable amount over time), or both.
|
||||
|
||||
This is an area currently under exploration
|
||||
|
||||
|
||||
### Penalties
|
||||
|
||||
As discussed in the [Economic Design](ed_overview.md) section, annual validator interest rates are to be specified as a
|
||||
function of total percentage of circulating supply that has been staked. The cluster rewards validators who are online
|
||||
and actively participating in the validation process throughout the entirety of
|
||||
their *validation period*. For validators that go offline/fail to validate
|
||||
transactions during this period, their annual reward is effectively reduced.
|
||||
|
||||
Similarly, we may consider an algorithmic reduction in a validator's active
|
||||
amount staked amount in the case that they are offline. I.e. if a validator is
|
||||
inactive for some amount of time, either due to a partition or otherwise, the
|
||||
amount of their stake that is considered ‘active’ (eligible to earn rewards)
|
||||
may be reduced. This design would be structured to help long-lived partitions
|
||||
to eventually reach finality on their respective chains as the % of non-voting
|
||||
total stake is reduced over time until a super-majority can be achieved by the
|
||||
active validators in each partition. Similarly, upon re-engaging, the ‘active’
|
||||
amount staked will come back online at some defined rate. Different rates of
|
||||
stake reduction may be considered depending on the size of the partition/active
|
||||
set.
|
87
book/src/synchronization.md
Normal file
87
book/src/synchronization.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Synchronization
|
||||
|
||||
Fast, reliable synchronization is the biggest reason Solana is able to achieve
|
||||
such high throughput. Traditional blockchains synchronize on large chunks of
|
||||
transactions called blocks. By synchronizing on blocks, a transaction cannot be
|
||||
processed until a duration called "block time" has passed. In Proof of Work
|
||||
consensus, these block times need to be very large (~10 minutes) to minimize
|
||||
the odds of multiple fullnodes producing a new valid block at the same time.
|
||||
There's no such constraint in Proof of Stake consensus, but without reliable
|
||||
timestamps, a fullnode cannot determine the order of incoming blocks. The
|
||||
popular workaround is to tag each block with a [wallclock
|
||||
timestamp](https://en.bitcoin.it/wiki/Block_timestamp). Because of clock drift
|
||||
and variance in network latencies, the timestamp is only accurate within an
|
||||
hour or two. To workaround the workaround, these systems lengthen block times
|
||||
to provide reasonable certainty that the median timestamp on each block is
|
||||
always increasing.
|
||||
|
||||
Solana takes a very different approach, which it calls *Proof of History* or
|
||||
*PoH*. Leader nodes "timestamp" blocks with cryptographic proofs that some
|
||||
duration of time has passed since the last proof. All data hashed into the
|
||||
proof most certainly have occurred before the proof was generated. The node
|
||||
then shares the new block with validator nodes, which are able to verify those
|
||||
proofs. The blocks can arrive at validators in any order or even could be
|
||||
replayed years later. With such reliable synchronization guarantees, Solana is
|
||||
able to break blocks into smaller batches of transactions called *entries*.
|
||||
Entries are streamed to validators in realtime, before any notion of block
|
||||
consensus.
|
||||
|
||||
Solana technically never sends a *block*, but uses the term to describe the
|
||||
sequence of entries that fullnodes vote on to achieve *confirmation*. In that
|
||||
way, Solana's confirmation times can be compared apples to apples to
|
||||
block-based systems. The current implementation sets block time to 800ms.
|
||||
|
||||
What's happening under the hood is that entries are streamed to validators as
|
||||
quickly as a leader node can batch a set of valid transactions into an entry.
|
||||
Validators process those entries long before it is time to vote on their
|
||||
validity. By processing the transactions optimistically, there is effectively
|
||||
no delay between the time the last entry is received and the time when the node
|
||||
can vote. In the event consensus is **not** achieved, a node simply rolls back
|
||||
its state. This optimisic processing technique was introduced in 1981 and
|
||||
called [Optimistic Concurrency
|
||||
Control](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.65.4735). It
|
||||
can be applied to blockchain architecture where a cluster votes on a hash that
|
||||
represents the full ledger up to some *block height*. In Solana, it is
|
||||
implemented trivially using the last entry's PoH hash.
|
||||
|
||||
### Relationship to VDFs
|
||||
|
||||
The Proof of History technique was first described for use in blockchain by
|
||||
Solana in November of 2017. In June of the following year, a similar technique
|
||||
was described at Stanford and called a [verifiable delay
|
||||
function](https://eprint.iacr.org/2018/601.pdf) or *VDF*.
|
||||
|
||||
A desirable property of a VDF is that verification time is very fast. Solana's
|
||||
approach to verifying its delay function is proportional to the time it took to
|
||||
create it. Split over a 4000 core GPU, it is sufficiently fast for Solana's
|
||||
needs, but if you asked the authors of the paper cited above, they might tell you
|
||||
([and have](https://github.com/solana-labs/solana/issues/388)) that Solana's
|
||||
approach is algorithmically slow and it shouldn't be called a VDF. We argue the
|
||||
term VDF should represent the category of verifiable delay functions and not
|
||||
just the subset with certain performance characteristics. Until that's
|
||||
resolved, Solana will likely continue using the term PoH for its
|
||||
application-specific VDF.
|
||||
|
||||
Another difference between PoH and VDFs is that a VDF is used only for tracking
|
||||
duration. PoH's hash chain, on the other hand, includes hashes of any data the
|
||||
application observed. That data is a double-edged sword. On one side, the data
|
||||
"proves history" - that the data most certainly existed before hashes after it.
|
||||
On the side, it means the application can manipulate the hash chain by changing
|
||||
*when* the data is hashed. The PoH chain therefore does not serve as a good
|
||||
source of randomness whereas a VDF without that data could. Solana's [leader
|
||||
rotation algorithm](#leader-rotation), for example, is derived only from the
|
||||
VDF *height* and not its hash at that height.
|
||||
|
||||
### Relationship to Consensus Mechanisms
|
||||
|
||||
Proof of History is not a consensus mechanism, but it is used to improve the
|
||||
performance of Solana's Proof of Stake consensus. It is also used to improve
|
||||
the performance of the data plane and replication protocols.
|
||||
|
||||
### More on Proof of History
|
||||
|
||||
* [water clock
|
||||
analogy](https://medium.com/solana-labs/proof-of-history-explained-by-a-water-clock-e682183417b8)
|
||||
|
||||
* [Proof of History
|
||||
overview](https://medium.com/solana-labs/proof-of-history-a-clock-for-blockchain-cf47a61a9274)
|
312
book/src/terminology.md
Normal file
312
book/src/terminology.md
Normal file
@ -0,0 +1,312 @@
|
||||
# Terminology
|
||||
|
||||
The following terms are used throughout this book.
|
||||
|
||||
#### account
|
||||
|
||||
A persistent file addressed by [public key](#public-key) and with
|
||||
[lamports](#lamport) tracking its lifetime.
|
||||
|
||||
#### app
|
||||
|
||||
A front-end application that interacts with a Solana cluster.
|
||||
|
||||
#### blob
|
||||
|
||||
A fraction of a [block](#block); the smallest unit sent between
|
||||
[fullnodes](#fullnode).
|
||||
|
||||
#### block
|
||||
|
||||
A contiguous set of [entries](#entry) on the ledger covered by a
|
||||
[vote](#ledger-vote). A [leader](#leader) produces at most one block per
|
||||
[slot](#slot).
|
||||
|
||||
#### block height
|
||||
|
||||
The number of [blocks](#block) beneath the current block. The first block after
|
||||
the [genesis block](#genesis-block) has height zero.
|
||||
|
||||
#### block id
|
||||
|
||||
The [entry id](#entry-id) of the last entry in a [block](#block).
|
||||
|
||||
#### bootstrap leader
|
||||
|
||||
The first [fullnode](#fullnode) to take the [leader](#leader) role.
|
||||
|
||||
#### CBC block
|
||||
|
||||
Smallest encrypted chunk of ledger, an encrypted ledger segment would be made of
|
||||
many CBC blocks. `ledger_segment_size / cbc_block_size` to be exact.
|
||||
|
||||
#### client
|
||||
|
||||
A [node](#node) that utilizes the [cluster](#cluster).
|
||||
|
||||
#### cluster
|
||||
|
||||
A set of [fullnodes](#fullnode) maintaining a single [ledger](#ledger).
|
||||
|
||||
#### confirmation
|
||||
|
||||
The wallclock duration between a [leader](#leader) creating a [tick
|
||||
entry](#tick) and recognizing a supermajority of [ledger votes](#ledger-vote)
|
||||
with a ledger interpretation that matches the leader's.
|
||||
|
||||
#### control plane
|
||||
|
||||
A gossip network connecting all [nodes](#node) of a [cluster](#cluster).
|
||||
|
||||
#### data plane
|
||||
|
||||
A multicast network used to efficiently validate [entries](#entry) and gain
|
||||
consensus.
|
||||
|
||||
#### drone
|
||||
|
||||
An off-chain service that acts as a custodian for a user's private key. It
|
||||
typically serves to validate and sign transactions.
|
||||
|
||||
#### fake storage proof
|
||||
|
||||
A proof which has the same format as a storage proof, but the sha state is
|
||||
actually from hashing a known ledger value which the storage client can reveal
|
||||
and is also easily verifiable by the network on-chain.
|
||||
|
||||
#### entry
|
||||
|
||||
An entry on the [ledger](#ledger) either a [tick](#tick) or a [transactions
|
||||
entry](#transactions-entry).
|
||||
|
||||
#### entry id
|
||||
|
||||
A globally unique identifier that is also a proof that the [entry](#entry) was
|
||||
generated after a duration of time, all [transactions](#transaction) included
|
||||
in the entry, and all previous entries on the [ledger](#ledger). See [Proof of
|
||||
History](#proof-of-history).
|
||||
|
||||
#### epoch
|
||||
|
||||
The time, i.e. number of [slots](#slot), for which a [leader
|
||||
schedule](#leader-schedule) is valid.
|
||||
|
||||
#### fork
|
||||
|
||||
A [ledger](#ledger) derived from common entries but then diverged.
|
||||
|
||||
#### fullnode
|
||||
|
||||
A full participant in the [cluster](#cluster) either a [leader](#leader) or
|
||||
[validator](#validator) node.
|
||||
|
||||
#### fullnode state
|
||||
|
||||
The result of interpreting all programs on the ledger at a given [tick
|
||||
height](#tick-height). It includes at least the set of all [accounts](#account)
|
||||
holding nonzero [native tokens](#native-tokens).
|
||||
|
||||
#### genesis block
|
||||
|
||||
The configuration file that prepares the [ledger](#ledger) for the first [block](#block).
|
||||
|
||||
#### hash
|
||||
|
||||
A digital fingerprint of a sequence of bytes.
|
||||
|
||||
#### instruction
|
||||
|
||||
The smallest unit of a [program](#program) that a [client](#client) can include
|
||||
in a [transaction](#instruction).
|
||||
|
||||
#### keypair
|
||||
|
||||
A [public key](#public-key) and corresponding [secret key](#secret-key).
|
||||
|
||||
#### lamport
|
||||
|
||||
A fractional [native token](#native-token) with the value of approximately
|
||||
0.0000000000582 [sol](#sol) (2^-34).
|
||||
|
||||
#### loader
|
||||
|
||||
A [program](#program) with the ability to interpret the binary encoding of
|
||||
other on-chain programs.
|
||||
|
||||
#### leader
|
||||
|
||||
The role of a [fullnode](#fullnode) when it is appending [entries](#entry) to
|
||||
the [ledger](#ledger).
|
||||
|
||||
#### leader schedule
|
||||
|
||||
A sequence of [fullnode](#fullnode) [public keys](#public-key). The cluster
|
||||
uses the leader schedule to determine which fullnode is the [leader](#leader)
|
||||
at any moment in time.
|
||||
|
||||
#### ledger
|
||||
|
||||
A list of [entries](#entry) containing [transactions](#transaction) signed by
|
||||
[clients](#client).
|
||||
|
||||
#### ledger segment
|
||||
|
||||
Portion of the ledger which is downloaded by the replicator where storage proof
|
||||
data is derived.
|
||||
|
||||
#### ledger vote
|
||||
|
||||
A [hash](#hash) of the [fullnode's state](#fullnode-state) at a given [tick
|
||||
height](#tick-height). It comprises a validator's affirmation that a
|
||||
[block](#block) it has received has been verified, as well as a promise not to
|
||||
vote for a conflicting [block](#block) (i.e. [fork](#fork)) for a specific
|
||||
amount of time, the [lockout](#lockout) period.
|
||||
|
||||
#### light client
|
||||
|
||||
A type of [client](#client) that can verify it's pointing to a valid
|
||||
[cluster](#cluster). It performs more ledger verification than a [thin
|
||||
client](#thin-client) and less than a [fullnode](#fullnode).
|
||||
|
||||
#### lockout
|
||||
|
||||
The duration of time for which a [fullnode](#fullnode) is unable to
|
||||
[vote](#ledger-vote) on another [fork](#fork).
|
||||
|
||||
#### native token
|
||||
|
||||
The [token](#token) used to track work done by [nodes](#node) in a cluster.
|
||||
|
||||
#### node
|
||||
|
||||
A computer participating in a [cluster](#cluster).
|
||||
|
||||
#### node count
|
||||
|
||||
The number of [fullnodes](#fullnode) participating in a [cluster](#cluster).
|
||||
|
||||
#### PoH
|
||||
|
||||
See [Proof of History](#proof-of-history).
|
||||
|
||||
#### program
|
||||
|
||||
The code that interprets [instructions](#instruction).
|
||||
|
||||
#### program id
|
||||
|
||||
The public key of the [account](#account) containing a [program](#program).
|
||||
|
||||
#### Proof of History
|
||||
|
||||
A stack of proofs, each which proves that some data existed before the proof
|
||||
was created and that a precise duration of time passed before the previous
|
||||
proof. Like a [VDF](#verifiable-delay-function), a Proof of History can be
|
||||
verified in less time than it took to produce.
|
||||
|
||||
#### public key
|
||||
|
||||
The public key of a [keypair](#keypair).
|
||||
|
||||
#### replicator
|
||||
|
||||
Storage mining client, stores some part of the ledger enumerated in blocks and
|
||||
submits storage proofs to the chain. Not a full-node.
|
||||
|
||||
#### runtime
|
||||
|
||||
The component of a [fullnode](#fullnode) responsible for [program](#program)
|
||||
execution.
|
||||
|
||||
#### secret key
|
||||
|
||||
The private key of a [keypair](#keypair).
|
||||
|
||||
#### slot
|
||||
|
||||
The period of time for which a [leader](#leader) ingests transactions and
|
||||
produces a [block](#block).
|
||||
|
||||
#### sol
|
||||
|
||||
The [native token](#native-token) tracked by a [cluster](#cluster) recognized
|
||||
by the company Solana.
|
||||
|
||||
#### stake
|
||||
|
||||
Tokens forfeit to the [cluster](#cluster) if malicious [fullnode](#fullnode)
|
||||
behavior can be proven.
|
||||
|
||||
#### storage proof
|
||||
|
||||
A set of sha hash state which is constructed by sampling the encrypted version
|
||||
of the stored ledger segment at certain offsets.
|
||||
|
||||
#### storage proof challenge
|
||||
|
||||
A transaction from a replicator that verifiably proves that a validator
|
||||
confirmed a fake proof.
|
||||
|
||||
#### storage proof claim
|
||||
|
||||
A transaction from a validator which is after the timeout period given from the
|
||||
storage proof confirmation and which no successful challenges have been
|
||||
observed which rewards the parties of the storage proofs and confirmations.
|
||||
|
||||
#### storage proof confirmation
|
||||
|
||||
A transaction by a validator which indicates the set of real and fake proofs
|
||||
submitted by a storage miner. The transaction would contain a list of proof
|
||||
hash values and a bit which says if this hash is valid or fake.
|
||||
|
||||
#### storage validation capacity
|
||||
|
||||
The number of keys and samples that a validator can verify each storage epoch.
|
||||
|
||||
#### thin client
|
||||
|
||||
A type of [client](#client) that trusts it is communicating with a valid
|
||||
[cluster](#cluster).
|
||||
|
||||
#### tick
|
||||
|
||||
A ledger [entry](#entry) that estimates wallclock duration.
|
||||
|
||||
#### tick height
|
||||
|
||||
The Nth [tick](#tick) in the [ledger](#ledger).
|
||||
|
||||
#### token
|
||||
|
||||
A scarce, fungible member of a set of tokens.
|
||||
|
||||
#### tps
|
||||
|
||||
[Transactions](#transaction) per second.
|
||||
|
||||
#### transaction
|
||||
|
||||
One or more [instructions](#instruction) signed by the [client](#client) and
|
||||
executed atomically.
|
||||
|
||||
#### transactions entry
|
||||
|
||||
A set of [transactions](#transaction) that may be executed in parallel.
|
||||
|
||||
#### validator
|
||||
|
||||
The role of a [fullnode](#fullnode) when it is validating the
|
||||
[leader's](#leader) latest [entries](#entry).
|
||||
|
||||
#### VDF
|
||||
|
||||
See [verifiable delay function](#verifiable-delay-function).
|
||||
|
||||
#### verifiable delay function
|
||||
|
||||
A function that takes a fixed amount of time to execute that produces a proof
|
||||
that it ran, which can then be verified in less time than it took to produce.
|
||||
|
||||
#### vote
|
||||
|
||||
See [ledger vote](#ledger-vote).
|
64
book/src/testing-programs.md
Normal file
64
book/src/testing-programs.md
Normal file
@ -0,0 +1,64 @@
|
||||
## Testing Programs
|
||||
|
||||
Applications send transactions to a Solana cluster and query validators to
|
||||
confirm the transactions were processed and to check each transaction's result.
|
||||
When the cluster doesn't behave as anticipated, it could be for a number of
|
||||
reasons:
|
||||
|
||||
* The program is buggy
|
||||
* The BPF loader rejected an unsafe program instruction
|
||||
* The transaction was too big
|
||||
* The transaction was invalid
|
||||
* The Runtime tried to execute the transaction when another one was accessing
|
||||
the same account
|
||||
* The network dropped the transaction
|
||||
* The cluster rolled back the ledger
|
||||
* A validator responded to queries maliciously
|
||||
|
||||
### The Transact Trait
|
||||
|
||||
To troubleshoot, the application should retarget a lower-level component, where
|
||||
fewer errors are possible. Retargeting can be done with different
|
||||
implementations of the Transact trait.
|
||||
|
||||
When Futures 0.3.0 is released, the Transact trait may look like this:
|
||||
|
||||
```rust,ignore
|
||||
trait Transact {
|
||||
async fn send_transactions(txs: &[Transaction]) -> Vec<Result<(), BankError>>;
|
||||
}
|
||||
```
|
||||
|
||||
Users send transactions and asynchrounously await their results.
|
||||
|
||||
#### Transact with Clusters
|
||||
|
||||
The highest level implementation targets a Solana cluster, which may be a
|
||||
deployed testnet or a local cluster running on a development machine.
|
||||
|
||||
#### Transact with the TPU
|
||||
|
||||
The next level is the TPU implementation of Transact. At the TPU level, the
|
||||
application sends transactions over Rust channels, where there can be no
|
||||
surprises from network queues or dropped packets. The TPU implements all
|
||||
"normal" transaction errors. It does signature verification, may report
|
||||
account-in-use errors, and otherwise results in the ledger, complete with proof
|
||||
of history hashes.
|
||||
|
||||
### Low-level testing
|
||||
|
||||
### Testing with the Bank
|
||||
|
||||
Below the TPU level is the Bank. The Bank doesn't do signature verification or
|
||||
generate a ledger. The Bank is a convenient layer at which to test new on-chain
|
||||
programs. It allows developers to toggle between native program implementations
|
||||
and BPF-compiled variants. No need for the Transact trait here. The Bank's API
|
||||
is synchronous.
|
||||
|
||||
### Unit-testing with the Runtime
|
||||
|
||||
Below the Bank is the Runtime. The Runtime is the ideal test environment for
|
||||
unit-testing. By statically linking the Runtime into a native program
|
||||
implementation, the developer gains the shortest possible edit-compile-run
|
||||
loop. Without any dynamic linking, stack traces include debug symbols and
|
||||
program errors are straightforward to troubleshoot.
|
35
book/src/tictactoe.md
Normal file
35
book/src/tictactoe.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Example app: Tic-Tac-Toe
|
||||
|
||||
[Click here to play
|
||||
Tic-Tac-Toe](https://solana-example-tictactoe.herokuapp.com/) on the Solana
|
||||
testnet. Open the link and wait for another player to join, or open the link
|
||||
in a second browser tab to play against yourself. You will see that every
|
||||
move a player makes stores a transaction on the ledger.
|
||||
|
||||
|
||||
## Build and run Tic-Tac-Toe locally
|
||||
|
||||
First fetch the latest release of the example code:
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/solana-labs/example-tictactoe.git
|
||||
$ cd example-tictactoe
|
||||
$ TAG=$(git describe --tags $(git rev-list --tags
|
||||
--max-count=1))
|
||||
$ git checkout $TAG
|
||||
```
|
||||
|
||||
Next, follow the steps in the git repository's
|
||||
[README](https://github.com/solana-labs/example-tictactoe/blob/master/README.md).
|
||||
|
||||
|
||||
## Getting lamports to users
|
||||
|
||||
You may have noticed you interacted with the Solana cluster without first
|
||||
needing to acquire lamports to pay transaction fees. Under the hood, the web
|
||||
app creates a new ephemeral identity and sends a request to an off-chain
|
||||
service for a signed transaction authorizing a user to start a new game.
|
||||
The service is called a *drone*. When the app sends the signed transaction
|
||||
to the Solana cluster, the drone's lamports are spent to pay the transaction
|
||||
fee and start the game. In a real world app, the drone might request the user
|
||||
watch an ad or pass a CAPTCHA before signing over its lamports.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user