Compare commits
329 Commits
Author | SHA1 | Date | |
---|---|---|---|
b4adb1c266 | |||
b9b541441b | |||
e510d4e272 | |||
9341e64ec7 | |||
d934f94e05 | |||
59dc123fa8 | |||
0faea87c84 | |||
19137ce3f4 | |||
8bdeb2d1ed | |||
d29a45266b | |||
2adb98a4a0 | |||
471465a5f4 | |||
942785b626 | |||
aa3c00231a | |||
d772a27936 | |||
0302f13b97 | |||
16b25d0874 | |||
c2dcbee6af | |||
1f71d05299 | |||
bfa1c025fd | |||
8611b40074 | |||
916844d399 | |||
4c9b7c9d2b | |||
9843c3a5cb | |||
f56955a17c | |||
9784bbf154 | |||
45642c4da1 | |||
8eac199e8b | |||
2e251ccc5c | |||
cf4bb70d80 | |||
57f8a15b96 | |||
cfe5afd34c | |||
94beb4b8c2 | |||
50207a30ef | |||
35e8f966e3 | |||
943cd0a24a | |||
0b892b2579 | |||
fb2eac20bb | |||
b37d2fde3d | |||
6b35e16676 | |||
6a9e0bc593 | |||
591fd72e0b | |||
2ed77b040a | |||
7ada8510c4 | |||
b8f6c17dee | |||
2f976ae460 | |||
36019cb1e3 | |||
99d2428041 | |||
c121498b5b | |||
eef2bdf690 | |||
190656967d | |||
90e73515ed | |||
1d7a758c97 | |||
e5b7aead12 | |||
578c2ad3ea | |||
de6838da78 | |||
604071c5d8 | |||
41a377013f | |||
52d453d06f | |||
58295b825d | |||
f6c7812fcc | |||
2f7561e4ee | |||
1cbd2372fc | |||
28f948aa7f | |||
c9ba9e4eb7 | |||
f877fb8c8f | |||
772ba41ede | |||
6374e69a69 | |||
ef0580bd3d | |||
1a77486f8e | |||
ead15d294e | |||
1acfcf3acf | |||
d15e248cdb | |||
f1e5edee14 | |||
7153abd483 | |||
90fb5d074d | |||
af82b0dce9 | |||
d4da2fbacd | |||
77efe95730 | |||
86e03a6d1b | |||
114e2989fa | |||
7024c73e9b | |||
6d418aa3f1 | |||
f079a78c5e | |||
6365c4c061 | |||
55cee5742f | |||
034eda4546 | |||
44ff25d044 | |||
a7e160e5c4 | |||
6283cc916d | |||
4b6aca6120 | |||
20b2be6e0b | |||
cbebc7a80c | |||
06eb2364f2 | |||
167890ca63 | |||
392a39dd54 | |||
7e1a7862db | |||
458ae3fdac | |||
431cc82032 | |||
18c6729d6c | |||
9476fe5ce3 | |||
788290ad82 | |||
6b5bcfaa58 | |||
4ed0cded9c | |||
035a364122 | |||
b114bc3674 | |||
bc74ee7117 | |||
b2ce5dc9f5 | |||
e920191de0 | |||
39e85a3e53 | |||
41156da4ca | |||
9271ba0039 | |||
b3e45fd6b7 | |||
7bfb60f82e | |||
359c50f1b3 | |||
fff1631a8b | |||
7d42ae30d9 | |||
87414de3e2 | |||
a0ffbf50a5 | |||
d40b66ff7b | |||
abd7f6b090 | |||
d8735df1de | |||
481853e1b1 | |||
778bcbce50 | |||
fd3f2cb910 | |||
915956b94b | |||
4576250342 | |||
2bef1b0433 | |||
628128b376 | |||
916017ca2c | |||
3204a00e73 | |||
1d327a5167 | |||
6e4f9cedf2 | |||
a79fbbafc9 | |||
1d54d29076 | |||
10b9a4806b | |||
0c1191c3ee | |||
18b386cd10 | |||
714b8c7fc8 | |||
216e9a61a0 | |||
0f498e6265 | |||
e8ad822111 | |||
65a82ebf50 | |||
727802684c | |||
e20a8329d3 | |||
88c2d0fad4 | |||
3bd921264a | |||
7501ed65e5 | |||
2eaa64c4e8 | |||
c9b86018c6 | |||
a4fb01b42b | |||
0d2574f8f0 | |||
796000e96f | |||
e2f00dc205 | |||
5e91f8f59d | |||
e2830f5b0e | |||
a2e3a92b01 | |||
23c696706b | |||
1393d26f63 | |||
1b68da7572 | |||
8542006259 | |||
426d06b89b | |||
06378d6db6 | |||
dccfe31e8c | |||
1dce5976cf | |||
340d01665c | |||
50f79e495e | |||
dd12db2f06 | |||
1afccb7351 | |||
bfc65e829e | |||
eb4515525d | |||
55f5f6a033 | |||
7ae421eaf6 | |||
e7da2c0931 | |||
133be2df51 | |||
06a93dcb43 | |||
ad7f04a245 | |||
0da6e1af14 | |||
576524f13b | |||
f567877d1d | |||
9881820444 | |||
ba8f49366d | |||
81fa69d347 | |||
abf2b300da | |||
a8254fd258 | |||
b15848de3b | |||
ab3c988146 | |||
575a0e318b | |||
a031b09190 | |||
df43e721e3 | |||
5f72650c7f | |||
5d0d467287 | |||
994515d0f2 | |||
1e949caa7f | |||
f2b727b534 | |||
f7680752e7 | |||
da4c37beec | |||
d486d2b8ce | |||
bba94c43b9 | |||
9cdffc7d64 | |||
5a86f2506d | |||
518227eac0 | |||
b8fd51e97d | |||
965c1e0000 | |||
a80176496d | |||
5719b8f251 | |||
1a2b131ceb | |||
349306ddf7 | |||
791ee411a5 | |||
f690c64375 | |||
427963f554 | |||
b0f2220ef6 | |||
908b48bf0e | |||
b49f8c0984 | |||
7609a007c6 | |||
674a49f8d7 | |||
d10bde656a | |||
401764ddb1 | |||
69eeb7cf08 | |||
55e3b7d380 | |||
d9e18a71ec | |||
2107e15bd3 | |||
4f3b22d04e | |||
2c78a93001 | |||
2621aeee82 | |||
8e400fc4bd | |||
29c2a63c8b | |||
736ada4e21 | |||
3df9b44d4c | |||
7225b89142 | |||
0cc0d3ab7a | |||
88d9618788 | |||
57038529e0 | |||
5c25eae631 | |||
b8b4d33f72 | |||
673a9417ef | |||
3fd9aada8b | |||
453fdb9e28 | |||
3f6a79b078 | |||
e9f80e5542 | |||
694d28acf8 | |||
88fdba5aca | |||
a19df7a36c | |||
9b50583641 | |||
71f9b44687 | |||
0139e5db21 | |||
586fb15c2c | |||
297328ff9a | |||
6b3384c205 | |||
3ef961fe37 | |||
a7b695c27a | |||
5bb75a5894 | |||
f3f416b7ba | |||
31b74bdf0b | |||
ed48d8323c | |||
f91627a230 | |||
f9c093022c | |||
7fe3c75c6b | |||
c8ed41167a | |||
5b2a82a951 | |||
441e76ebeb | |||
c2dfb9900e | |||
916458e132 | |||
ffb15578ce | |||
abcbbb925f | |||
059755fe59 | |||
ae12dc2c75 | |||
37b5c6afaa | |||
92ed7b36a2 | |||
379d2e6d95 | |||
7f75cc8906 | |||
1ab5098576 | |||
598f765960 | |||
aac626c2c2 | |||
3eec3cfac2 | |||
5eee9e62e5 | |||
a7d18125d3 | |||
8202310073 | |||
1e2ba110eb | |||
62c9b7d850 | |||
4f18fc836f | |||
950d8494ba | |||
cb528af4e2 | |||
ad27c30623 | |||
9add8d0afc | |||
af2e7ea285 | |||
675a78aaa1 | |||
408bdbce7a | |||
1a259d4a3f | |||
c5f8b4960c | |||
21f845ed39 | |||
7a369df9a7 | |||
f02ec31c68 | |||
d21fa4a177 | |||
bd0871cbe7 | |||
2604f8ac0a | |||
a7574f8657 | |||
73f250f03a | |||
bae0aadafa | |||
5524146ddf | |||
3b2adbc9df | |||
4e41c81bc7 | |||
c545e812d0 | |||
c2193a37ce | |||
fabba82173 | |||
c3ec5ad846 | |||
c4945cc04a | |||
e2e55f81d0 | |||
d862565b16 | |||
0cc3956693 | |||
4e5677f116 | |||
acba1d6f9e | |||
3e14af5033 | |||
6f56501034 | |||
0b7269b64e | |||
457a2d948b | |||
528bdf34fd | |||
697cd17b59 | |||
13fcfcb964 | |||
9c1fd55768 | |||
7f9a476660 | |||
b07290df81 | |||
4b599a95b3 | |||
64222cfff7 | |||
e81d434903 | |||
bf0dd158de | |||
18e398131d | |||
4a5837a286 | |||
656e2649a7 |
@ -10,6 +10,8 @@
|
||||
set -x
|
||||
rsync -a --delete --link-dest="$PWD" target "$d"
|
||||
du -hs "$d"
|
||||
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$d")
|
||||
echo "--- ${cacheSizeInGB}GB: $d"
|
||||
)
|
||||
|
||||
#
|
||||
|
@ -14,14 +14,18 @@ export PS4="++"
|
||||
(
|
||||
set -x
|
||||
d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
|
||||
MAX_CACHE_SIZE=18 # gigabytes
|
||||
|
||||
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"
|
||||
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$d")
|
||||
echo "--- ${cacheSizeInGB}GB: $d"
|
||||
if [[ $cacheSizeInGB -gt $MAX_CACHE_SIZE ]]; then
|
||||
echo "--- $d is too large, removing it"
|
||||
rm -rf "$d"
|
||||
fi
|
||||
else
|
||||
echo "--- $d not present"
|
||||
fi
|
||||
|
||||
mkdir -p "$d"/target
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,13 +1,11 @@
|
||||
/book/html/
|
||||
/book/src/img/
|
||||
/book/src/tests.ok
|
||||
/core/target/
|
||||
/farf/
|
||||
/ledger-tool/target/
|
||||
/metrics/scripts/lib/
|
||||
/solana-release/
|
||||
solana-release.tar.bz2
|
||||
/target/
|
||||
/wallet/target/
|
||||
|
||||
**/*.rs.bk
|
||||
.cargo
|
||||
|
1132
Cargo.lock
generated
1132
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -1,11 +1,12 @@
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"bench-exchange",
|
||||
"bench-streamer",
|
||||
"bench-tps",
|
||||
"client",
|
||||
"core",
|
||||
"drone",
|
||||
"fullnode",
|
||||
"validator",
|
||||
"genesis",
|
||||
"gossip",
|
||||
"install",
|
||||
@ -14,6 +15,7 @@ members = [
|
||||
"ledger-tool",
|
||||
"logger",
|
||||
"metrics",
|
||||
"netutil",
|
||||
"programs/bpf",
|
||||
"programs/bpf_loader",
|
||||
"programs/budget_api",
|
||||
@ -22,17 +24,18 @@ members = [
|
||||
"programs/config_program",
|
||||
"programs/exchange_api",
|
||||
"programs/exchange_program",
|
||||
"programs/token_api",
|
||||
"programs/token_program",
|
||||
"programs/failure_program",
|
||||
"programs/noop_program",
|
||||
"programs/stake_api",
|
||||
"programs/stake_program",
|
||||
"programs/storage_api",
|
||||
"programs/storage_program",
|
||||
"programs/token_api",
|
||||
"programs/token_program",
|
||||
"programs/vote_api",
|
||||
"programs/vote_program",
|
||||
"replicator",
|
||||
"runtime",
|
||||
"sdk",
|
||||
"upload-perf",
|
||||
"vote-signer",
|
||||
|
@ -41,7 +41,7 @@ Install rustc, cargo and rustfmt:
|
||||
```bash
|
||||
$ curl https://sh.rustup.rs -sSf | sh
|
||||
$ source $HOME/.cargo/env
|
||||
$ rustup component add rustfmt-preview
|
||||
$ rustup component add rustfmt
|
||||
```
|
||||
|
||||
If your rustc version is lower than 1.34.0, please update it:
|
||||
@ -66,7 +66,7 @@ $ cd solana
|
||||
Build
|
||||
|
||||
```bash
|
||||
$ cargo build --all
|
||||
$ cargo build
|
||||
```
|
||||
|
||||
Then to run a minimal local cluster
|
||||
@ -80,7 +80,7 @@ Testing
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
$ cargo test --all
|
||||
$ cargo test
|
||||
```
|
||||
|
||||
Local Testnet
|
||||
|
58
RELEASE.md
58
RELEASE.md
@ -61,7 +61,7 @@ There are three release channels that map to branches as follows:
|
||||
|
||||
## Release Steps
|
||||
|
||||
### Changing channels
|
||||
### Advance the Channels
|
||||
|
||||
#### Create the new branch
|
||||
1. Pick your branch point for release on master.
|
||||
@ -84,7 +84,7 @@ There are three release channels that map to branches as follows:
|
||||
At this point, `ci/channel-info.sh` should show your freshly cut release branch as
|
||||
"BETA_CHANNEL" and the previous release branch as "STABLE_CHANNEL".
|
||||
|
||||
### Updating channels (i.e. "making a release")
|
||||
### Make the Release
|
||||
|
||||
We use [github's Releases UI](https://github.com/solana-labs/solana/releases) for tagging a release.
|
||||
|
||||
@ -99,13 +99,59 @@ We use [github's Releases UI](https://github.com/solana-labs/solana/releases) fo
|
||||
release should be `<branchname>.X-rc.0`.
|
||||
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 the release branch to the next
|
||||
semantic version (e.g. 0.9.0 -> 0.9.1) by running
|
||||
`./scripts/increment-cargo-version.sh patch`, then rebuild with `cargo
|
||||
build` to cause a refresh of `Cargo.lock`.
|
||||
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
||||
release branch.
|
||||
|
||||
### Update software on testnet.solana.com
|
||||
|
||||
The testnet running on testnet.solana.com is set to use a fixed release tag
|
||||
which is set in the Buildkite testnet-management pipeline.
|
||||
This tag needs to be updated and the testnet restarted after a new release
|
||||
tag is created.
|
||||
|
||||
#### Update testnet schedules
|
||||
|
||||
Go to https://buildkite.com/solana-labs and click through: Pipelines ->
|
||||
testnet-management -> Pipeline Settings -> Schedules
|
||||
Or just click here:
|
||||
https://buildkite.com/solana-labs/testnet-management/settings/schedules
|
||||
|
||||
There are two scheduled jobs for testnet: a daily restart and an hourly sanity-or-restart. \
|
||||
https://buildkite.com/solana-labs/testnet-management/settings/schedules/0efd7856-7143-4713-8817-47e6bdb05387
|
||||
https://buildkite.com/solana-labs/testnet-management/settings/schedules/2a926646-d972-42b5-aeb9-bb6759592a53
|
||||
|
||||
On each schedule:
|
||||
1. Set TESTNET_TAG environment variable to the desired release tag.
|
||||
1. Example, TESTNET_TAG=v0.13.2
|
||||
1. Set the Build Branch to the branch that TESTNET_TAG is from.
|
||||
1. Example: v0.13
|
||||
|
||||
#### Restart the testnet
|
||||
|
||||
Trigger a TESTNET_OP=create-and-start to refresh the cluster with the new version
|
||||
|
||||
1. Go to https://buildkite.com/solana-labs/testnet-management
|
||||
2. Click "New Build" and use the following settings, then click "Create Build"
|
||||
1. Commit: HEAD
|
||||
1. Branch: [channel branch as set in the schedules]
|
||||
1. Environment Variables:
|
||||
```
|
||||
TESTNET=testnet
|
||||
TESTNET_TAG=[same value as used in TESTNET_TAG in the schedules]
|
||||
TESTNET_OP=create-and-start
|
||||
```
|
||||
|
||||
#### Update documentation
|
||||
|
||||
Document the new recommended version by updating
|
||||
```export SOLANA_RELEASE=[new scheduled TESTNET_TAG value]```
|
||||
in book/src/testnet-participation.md for both edge and beta channel branches.
|
||||
|
||||
### Alert the community
|
||||
|
||||
Notify Discord users on #validator-support that a new release for
|
||||
testnet.solana.com is available
|
||||
|
@ -2,7 +2,7 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-exchange"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
@ -10,28 +10,29 @@ homepage = "https://solana.com/"
|
||||
[dependencies]
|
||||
bs58 = "0.2.0"
|
||||
clap = "2.32.0"
|
||||
bincode = "1.1.2"
|
||||
bincode = "1.1.4"
|
||||
env_logger = "0.6.0"
|
||||
itertools = "0.8.0"
|
||||
log = "0.4.6"
|
||||
num-traits = "0.2"
|
||||
num-derive = "0.2"
|
||||
rand = "0.6.5"
|
||||
rayon = "1.0.3"
|
||||
serde = "1.0.87"
|
||||
serde_derive = "1.0.87"
|
||||
serde = "1.0.91"
|
||||
serde_derive = "1.0.91"
|
||||
serde_json = "1.0.38"
|
||||
# solana-runtime = { path = "../solana/runtime"}
|
||||
solana = { path = "../core", version = "0.14.0" }
|
||||
solana-client = { path = "../client", version = "0.14.0" }
|
||||
solana-drone = { path = "../drone", version = "0.14.0" }
|
||||
solana-exchange-api = { path = "../programs/exchange_api", version = "0.14.0" }
|
||||
solana-exchange-program = { path = "../programs/exchange_program", version = "0.14.0" }
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.14.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.14.0" }
|
||||
ws = "0.8.0"
|
||||
solana = { path = "../core", version = "0.15.0" }
|
||||
solana-client = { path = "../client", version = "0.15.0" }
|
||||
solana-drone = { path = "../drone", version = "0.15.0" }
|
||||
solana-exchange-api = { path = "../programs/exchange_api", version = "0.15.0" }
|
||||
solana-exchange-program = { path = "../programs/exchange_program", version = "0.15.0" }
|
||||
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.15.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.15.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.15.0" }
|
||||
ws = "0.8.1"
|
||||
untrusted = "0.6.2"
|
||||
|
||||
[features]
|
||||
|
@ -23,7 +23,7 @@ demo demonstrates one way to host an exchange on the Solana blockchain by
|
||||
emulating a currency exchange.
|
||||
|
||||
The assets are virtual tokens held by investors who may post trade requests to
|
||||
the exchange. A broker monitors the exchange and posts swap requests for
|
||||
the exchange. A Swapper monitors the exchange and posts swap requests for
|
||||
matching trade orders. All the transactions can execute concurrently.
|
||||
|
||||
## Premise
|
||||
@ -75,7 +75,7 @@ matching trade orders. All the transactions can execute concurrently.
|
||||
contain the same information as the trade request.
|
||||
- Price spread
|
||||
- The difference between the two matching trade orders. The spread is the
|
||||
profit of the broker initiating the swap request.
|
||||
profit of the Swapper initiating the swap request.
|
||||
- Swap requirements
|
||||
- Policies that result in a successful trade swap.
|
||||
- Swap request
|
||||
@ -85,7 +85,7 @@ matching trade orders. All the transactions can execute concurrently.
|
||||
swap requirements. A trade swap may not wholly satisfy one or both of the
|
||||
trade orders in which case the trade orders are adjusted appropriately. As
|
||||
long as the swap requirements are met there will be an exchange of tokens
|
||||
between accounts. Any price spread is deposited into the broker's profit
|
||||
between accounts. Any price spread is deposited into the Swapper's profit
|
||||
account. All trade swaps are recorded in a new account for posterity.
|
||||
- Investor
|
||||
- Individual investors who hold a number of tokens and wish to trade them on
|
||||
@ -93,41 +93,41 @@ matching trade orders. All the transactions can execute concurrently.
|
||||
accounts containing tokens and/or trade requests. Investors post
|
||||
transactions to the exchange in order to request tokens and post or cancel
|
||||
trade requests.
|
||||
- Broker
|
||||
- An agent who facilitates trading between investors. Brokers operate as
|
||||
- Swapper
|
||||
- An agent who facilitates trading between investors. Swappers operate as
|
||||
Solana thin clients who monitor all the trade orders looking for a trade
|
||||
match. Once found, the broker issues a swap request to the exchange.
|
||||
Brokers are the engine of the exchange and are rewarded for their efforts by
|
||||
accumulating the price spreads of the swaps they initiate. Brokers also
|
||||
match. Once found, the Swapper issues a swap request to the exchange.
|
||||
Swappers are the engine of the exchange and are rewarded for their efforts by
|
||||
accumulating the price spreads of the swaps they initiate. Swappers also
|
||||
provide current bid/ask price and OHLCV (Open, High, Low, Close, Volume)
|
||||
information on demand via a public network port.
|
||||
- Transaction fees
|
||||
- Solana transaction fees are paid for by the transaction submitters who are
|
||||
the Investors and Brokers.
|
||||
the Investors and Swappers.
|
||||
|
||||
## Exchange startup
|
||||
|
||||
The exchange is up and running when it reaches a state where it can take
|
||||
investor's trades and broker's swap requests. To achieve this state the
|
||||
investor's trades and Swapper's swap requests. To achieve this state the
|
||||
following must occur in order:
|
||||
|
||||
- Start the Solana blockchain
|
||||
- Start the broker thin-client
|
||||
- The broker subscribes to change notifications for all the accounts owned by
|
||||
- Start the Swapper thin-client
|
||||
- The Swapper subscribes to change notifications for all the accounts owned by
|
||||
the exchange program id. The subscription is managed via Solana's JSON RPC
|
||||
interface.
|
||||
- The broker starts responding to queries for bid/ask price and OHLCV
|
||||
- The Swapper starts responding to queries for bid/ask price and OHLCV
|
||||
|
||||
The broker responding successfully to price and OHLCV requests is the signal to
|
||||
The Swapper responding successfully to price and OHLCV requests is the signal to
|
||||
the investors that trades submitted after that point will be analyzed. <!--This
|
||||
is not ideal, and instead investors should be able to submit trades at any time,
|
||||
and the broker could come and go without missing a trade. One way to achieve
|
||||
this is for the broker to read the current state of all accounts looking for all
|
||||
and the Swapper could come and go without missing a trade. One way to achieve
|
||||
this is for the Swapper to read the current state of all accounts looking for all
|
||||
open trade orders.-->
|
||||
|
||||
Investors will initially query the exchange to discover their current balance
|
||||
for each type of token. If the investor does not already have an account for
|
||||
each type of token, they will submit account requests. Brokers as well will
|
||||
each type of token, they will submit account requests. Swappers as well will
|
||||
request accounts to hold the tokens they earn by initiating trade swaps.
|
||||
|
||||
```rust
|
||||
@ -165,7 +165,7 @@ pub struct TokenAccountInfo {
|
||||
}
|
||||
```
|
||||
|
||||
For this demo investors or brokers can request more tokens from the exchange at
|
||||
For this demo investors or Swappers can request more tokens from the exchange at
|
||||
any time by submitting token requests. In non-demos, an exchange of this type
|
||||
would provide another way to exchange a 3rd party asset into tokens.
|
||||
|
||||
@ -269,10 +269,10 @@ pub enum ExchangeInstruction {
|
||||
|
||||
## Trade swaps
|
||||
|
||||
The broker is monitoring the accounts assigned to the exchange program and
|
||||
The Swapper is monitoring the accounts assigned to the exchange program and
|
||||
building a trade-order table. The trade order table is used to identify
|
||||
matching trade orders which could be fulfilled. When a match is found the
|
||||
broker should issue a swap request. Swap requests may not satisfy the entirety
|
||||
Swapper should issue a swap request. Swap requests may not satisfy the entirety
|
||||
of either order, but the exchange will greedily fulfill it. Any leftover tokens
|
||||
in either account will keep the trade order valid for further swap requests in
|
||||
the future.
|
||||
@ -310,14 +310,14 @@ whole for clarity.
|
||||
| 5 | 1 T AB 2 10 | 2 F AB 1 5 |
|
||||
|
||||
As part of a successful swap request, the exchange will credit tokens to the
|
||||
broker's account equal to the difference in the price ratios or the two orders.
|
||||
These tokens are considered the broker's profit for initiating the trade.
|
||||
Swapper's account equal to the difference in the price ratios or the two orders.
|
||||
These tokens are considered the Swapper's profit for initiating the trade.
|
||||
|
||||
The broker would initiate the following swap on the order table above:
|
||||
The Swapper would initiate the following swap on the order table above:
|
||||
|
||||
- Row 1, To: Investor 1 trades 2 A tokens to 8 B tokens
|
||||
- Row 1, From: Investor 2 trades 2 A tokens from 8 B tokens
|
||||
- Broker takes 8 B tokens as profit
|
||||
- Swapper takes 8 B tokens as profit
|
||||
|
||||
Both row 1 trades are fully realized, table becomes:
|
||||
|
||||
@ -328,11 +328,11 @@ Both row 1 trades are fully realized, table becomes:
|
||||
| 3 | 1 T AB 2 8 | 2 F AB 3 6 |
|
||||
| 4 | 1 T AB 2 10 | 2 F AB 1 5 |
|
||||
|
||||
The broker would initiate the following swap:
|
||||
The Swapper would initiate the following swap:
|
||||
|
||||
- Row 1, To: Investor 1 trades 1 A token to 4 B tokens
|
||||
- Row 1, From: Investor 2 trades 1 A token from 4 B tokens
|
||||
- Broker takes 4 B tokens as profit
|
||||
- Swapper takes 4 B tokens as profit
|
||||
|
||||
Row 1 From is not fully realized, table becomes:
|
||||
|
||||
@ -343,11 +343,11 @@ Row 1 From is not fully realized, table becomes:
|
||||
| 3 | 1 T AB 2 10 | 2 F AB 3 6 |
|
||||
| 4 | | 2 F AB 1 5 |
|
||||
|
||||
The broker would initiate the following swap:
|
||||
The Swapper would initiate the following swap:
|
||||
|
||||
- Row 1, To: Investor 1 trades 1 A token to 6 B tokens
|
||||
- Row 1, From: Investor 2 trades 1 A token from 6 B tokens
|
||||
- Broker takes 2 B tokens as profit
|
||||
- Swapper takes 2 B tokens as profit
|
||||
|
||||
Row 1 To is now fully realized, table becomes:
|
||||
|
||||
@ -357,11 +357,11 @@ Row 1 To is now fully realized, table becomes:
|
||||
| 2 | 1 T AB 2 8 | 2 F AB 3 5 |
|
||||
| 3 | 1 T AB 2 10 | 2 F AB 1 5 |
|
||||
|
||||
The broker would initiate the following last swap:
|
||||
The Swapper would initiate the following last swap:
|
||||
|
||||
- Row 1, To: Investor 1 trades 2 A token to 12 B tokens
|
||||
- Row 1, From: Investor 2 trades 2 A token from 12 B tokens
|
||||
- Broker takes 4 B tokens as profit
|
||||
- Swapper takes 4 B tokens as profit
|
||||
|
||||
Table becomes:
|
||||
|
||||
@ -383,7 +383,7 @@ pub enum ExchangeInstruction {
|
||||
/// key 3 - `From` trade order
|
||||
/// key 4 - Token account associated with the To Trade
|
||||
/// key 5 - Token account associated with From trade
|
||||
/// key 6 - Token account in which to deposit the brokers profit from the swap.
|
||||
/// key 6 - Token account in which to deposit the Swappers profit from the swap.
|
||||
SwapRequest,
|
||||
}
|
||||
|
||||
@ -442,14 +442,14 @@ pub enum ExchangeInstruction {
|
||||
/// key 3 - `From` trade order
|
||||
/// key 4 - Token account associated with the To Trade
|
||||
/// key 5 - Token account associated with From trade
|
||||
/// key 6 - Token account in which to deposit the brokers profit from the swap.
|
||||
/// key 6 - Token account in which to deposit the Swappers profit from the swap.
|
||||
SwapRequest,
|
||||
}
|
||||
```
|
||||
|
||||
## Quotes and OHLCV
|
||||
|
||||
The broker will provide current bid/ask price quotes based on trade actively and
|
||||
The Swapper will provide current bid/ask price quotes based on trade actively and
|
||||
also provide OHLCV based on some time window. The details of how the bid/ask
|
||||
price quotes are calculated are yet to be decided.
|
||||
|
||||
|
@ -3,23 +3,21 @@
|
||||
use crate::order_book::*;
|
||||
use itertools::izip;
|
||||
use log::*;
|
||||
use rand::{thread_rng, Rng};
|
||||
use rayon::prelude::*;
|
||||
use solana::cluster_info::FULLNODE_PORT_RANGE;
|
||||
use solana::contact_info::ContactInfo;
|
||||
use solana::gen_keys::GenKeys;
|
||||
use solana_client::thin_client::create_client;
|
||||
use solana_client::thin_client::ThinClient;
|
||||
use solana_client::perf_utils::{sample_txs, SampleStats};
|
||||
use solana_drone::drone::request_airdrop_transaction;
|
||||
use solana_exchange_api::exchange_instruction;
|
||||
use solana_exchange_api::exchange_state::*;
|
||||
use solana_exchange_api::id;
|
||||
use solana_metrics::influxdb;
|
||||
use solana_metrics::datapoint_info;
|
||||
use solana_sdk::client::Client;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_instruction;
|
||||
use solana_sdk::timing::{duration_as_ms, duration_as_ns, duration_as_s};
|
||||
use solana_sdk::timing::{duration_as_ms, duration_as_s};
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use std::cmp;
|
||||
use std::collections::VecDeque;
|
||||
@ -67,16 +65,6 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SampleStats {
|
||||
/// Maximum TPS reported by this node
|
||||
pub tps: f32,
|
||||
/// Total time taken for those txs
|
||||
pub elapsed: Duration,
|
||||
/// Total transactions reported by this node
|
||||
pub txs: u64,
|
||||
}
|
||||
|
||||
pub fn do_bench_exchange<T>(clients: Vec<T>, config: Config)
|
||||
where
|
||||
T: 'static + Client + Send + Sync,
|
||||
@ -91,6 +79,18 @@ where
|
||||
chunk_size,
|
||||
account_groups,
|
||||
} = config;
|
||||
|
||||
info!(
|
||||
"Exchange client: threads {} duration {} fund_amount {}",
|
||||
threads,
|
||||
duration_as_s(&duration),
|
||||
fund_amount
|
||||
);
|
||||
info!(
|
||||
"Exchange client: transfer delay {} batch size {} chunk size {}",
|
||||
transfer_delay, batch_size, chunk_size
|
||||
);
|
||||
|
||||
let accounts_in_groups = batch_size * account_groups;
|
||||
let exit_signal = Arc::new(AtomicBool::new(false));
|
||||
let clients: Vec<_> = clients.into_iter().map(Arc::new).collect();
|
||||
@ -168,6 +168,7 @@ where
|
||||
&shared_txs,
|
||||
&swapper_signers,
|
||||
&profit_pubkeys,
|
||||
transfer_delay,
|
||||
batch_size,
|
||||
chunk_size,
|
||||
account_groups,
|
||||
@ -237,62 +238,6 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
fn sample_txs<T>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
sample_stats: &Arc<RwLock<Vec<SampleStats>>>,
|
||||
sample_period: u64,
|
||||
client: &Arc<T>,
|
||||
) where
|
||||
T: Client,
|
||||
{
|
||||
let mut max_tps = 0.0;
|
||||
let mut total_elapsed;
|
||||
let mut total_txs;
|
||||
let mut now = Instant::now();
|
||||
let start_time = now;
|
||||
let initial_txs = client.get_transaction_count().expect("transaction count");
|
||||
let mut last_txs = initial_txs;
|
||||
|
||||
loop {
|
||||
total_elapsed = start_time.elapsed();
|
||||
let elapsed = now.elapsed();
|
||||
now = Instant::now();
|
||||
let mut txs = client.get_transaction_count().expect("transaction count");
|
||||
|
||||
if txs < last_txs {
|
||||
error!("expected txs({}) >= last_txs({})", txs, last_txs);
|
||||
txs = last_txs;
|
||||
}
|
||||
total_txs = txs - initial_txs;
|
||||
let sample_txs = txs - last_txs;
|
||||
last_txs = txs;
|
||||
|
||||
let tps = sample_txs as f32 / duration_as_s(&elapsed);
|
||||
if tps > max_tps {
|
||||
max_tps = tps;
|
||||
}
|
||||
|
||||
info!(
|
||||
"Sampler {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s",
|
||||
tps,
|
||||
sample_txs,
|
||||
total_txs,
|
||||
total_elapsed.as_secs(),
|
||||
);
|
||||
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
let stats = SampleStats {
|
||||
tps: max_tps,
|
||||
elapsed: total_elapsed,
|
||||
txs: total_txs,
|
||||
};
|
||||
sample_stats.write().unwrap().push(stats);
|
||||
return;
|
||||
}
|
||||
sleep(Duration::from_secs(sample_period));
|
||||
}
|
||||
}
|
||||
|
||||
fn do_tx_transfers<T>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
shared_txs: &SharedTransactions,
|
||||
@ -301,7 +246,6 @@ fn do_tx_transfers<T>(
|
||||
) where
|
||||
T: Client,
|
||||
{
|
||||
let mut stats = Stats::default();
|
||||
loop {
|
||||
let txs;
|
||||
{
|
||||
@ -318,48 +262,18 @@ fn do_tx_transfers<T>(
|
||||
let duration = now.elapsed();
|
||||
|
||||
total_txs_sent_count.fetch_add(n, Ordering::Relaxed);
|
||||
stats.total += n as u64;
|
||||
stats.sent_ns += duration_as_ns(&duration);
|
||||
let rate = n as f32 / duration_as_s(&duration);
|
||||
if rate > stats.sent_peak_rate {
|
||||
stats.sent_peak_rate = rate;
|
||||
}
|
||||
trace!(" tx {:?} sent {:.2}/s", n, rate);
|
||||
|
||||
solana_metrics::submit(
|
||||
influxdb::Point::new("bench-exchange")
|
||||
.add_tag("op", influxdb::Value::String("do_tx_transfers".to_string()))
|
||||
.add_field(
|
||||
"duration",
|
||||
influxdb::Value::Integer(duration_as_ms(&duration) as i64),
|
||||
)
|
||||
.add_field("count", influxdb::Value::Integer(n as i64))
|
||||
.to_owned(),
|
||||
datapoint_info!(
|
||||
"bench-exchange-do_tx_transfers",
|
||||
("duration", duration_as_ms(&duration), i64),
|
||||
("count", n, i64)
|
||||
);
|
||||
}
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
info!(
|
||||
" Thread Transferred {} Txs, avg {:.2}/s peak {:.2}/s",
|
||||
stats.total,
|
||||
(stats.total as f64 / stats.sent_ns as f64) * 1_000_000_000_f64,
|
||||
stats.sent_peak_rate,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Stats {
|
||||
total: u64,
|
||||
keygen_ns: u64,
|
||||
keygen_peak_rate: f32,
|
||||
sign_ns: u64,
|
||||
sign_peak_rate: f32,
|
||||
sent_ns: u64,
|
||||
sent_peak_rate: f32,
|
||||
}
|
||||
|
||||
struct TradeInfo {
|
||||
trade_account: Pubkey,
|
||||
order_info: TradeOrderInfo,
|
||||
@ -371,6 +285,7 @@ fn swapper<T>(
|
||||
shared_txs: &SharedTransactions,
|
||||
signers: &[Arc<Keypair>],
|
||||
profit_pubkeys: &[Pubkey],
|
||||
transfer_delay: u64,
|
||||
batch_size: usize,
|
||||
chunk_size: usize,
|
||||
account_groups: usize,
|
||||
@ -378,28 +293,57 @@ fn swapper<T>(
|
||||
) where
|
||||
T: Client,
|
||||
{
|
||||
let mut stats = Stats::default();
|
||||
let mut order_book = OrderBook::default();
|
||||
let mut account_group: usize = 0;
|
||||
|
||||
let mut txs = 0;
|
||||
let mut total_txs = 0;
|
||||
let mut now = Instant::now();
|
||||
let start_time = now;
|
||||
let mut total_elapsed = start_time.elapsed();
|
||||
|
||||
// Chunks may have been dropped and we don't want to wait a long time
|
||||
// for each time, Back-off each time we fail to confirm a chunk
|
||||
const CHECK_TX_TIMEOUT_MAX_MS: u64 = 15000;
|
||||
const CHECK_TX_DELAY_MS: u64 = 100;
|
||||
let mut max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS;
|
||||
|
||||
// If we dump too many chunks maybe we are just waiting on a back-log
|
||||
// rather than a series of dropped packets, reset to max waits
|
||||
const MAX_DUMPS: u64 = 50;
|
||||
let mut dumps = 0;
|
||||
|
||||
'outer: loop {
|
||||
if let Ok(trade_infos) = receiver.try_recv() {
|
||||
let mut tries = 0;
|
||||
let mut trade_index = 0;
|
||||
while client
|
||||
.get_balance(&trade_infos[0].trade_account)
|
||||
.get_balance(&trade_infos[trade_index].trade_account)
|
||||
.unwrap_or(0)
|
||||
== 0
|
||||
{
|
||||
tries += 1;
|
||||
if tries > 300 {
|
||||
if tries >= max_tries {
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
break 'outer;
|
||||
}
|
||||
error!("Give up waiting, dump batch");
|
||||
error!("Give up and dump batch");
|
||||
if dumps >= MAX_DUMPS {
|
||||
error!("Max batches dumped, reset wait back-off");
|
||||
max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS;
|
||||
dumps = 0;
|
||||
} else {
|
||||
dumps += 1;
|
||||
max_tries /= 2;
|
||||
}
|
||||
continue 'outer;
|
||||
}
|
||||
debug!("{} waiting for trades batch to clear", tries);
|
||||
sleep(Duration::from_millis(100));
|
||||
sleep(Duration::from_millis(CHECK_TX_DELAY_MS));
|
||||
trade_index = thread_rng().gen_range(0, trade_infos.len());
|
||||
}
|
||||
max_tries = CHECK_TX_TIMEOUT_MAX_MS / CHECK_TX_DELAY_MS;
|
||||
dumps = 0;
|
||||
|
||||
trade_infos.iter().for_each(|info| {
|
||||
order_book
|
||||
@ -414,9 +358,6 @@ fn swapper<T>(
|
||||
}
|
||||
}
|
||||
let swaps_size = swaps.len();
|
||||
stats.total += swaps_size as u64;
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
let mut to_swap = vec![];
|
||||
let start = account_group * swaps_size as usize;
|
||||
@ -429,17 +370,8 @@ fn swapper<T>(
|
||||
to_swap.push((signer, swap, profit));
|
||||
}
|
||||
account_group = (account_group + 1) % account_groups as usize;
|
||||
let duration = now.elapsed();
|
||||
let rate = swaps_size as f32 / duration_as_s(&duration);
|
||||
stats.keygen_ns += duration_as_ns(&duration);
|
||||
if rate > stats.keygen_peak_rate {
|
||||
stats.keygen_peak_rate = rate;
|
||||
}
|
||||
trace!("sw {:?} keypairs {:.2} /s", swaps_size, rate);
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
let blockhash = client
|
||||
let (blockhash, _fee_calculator) = client
|
||||
.get_recent_blockhash()
|
||||
.expect("Failed to get blockhash");
|
||||
let to_swap_txs: Vec<_> = to_swap
|
||||
@ -459,21 +391,25 @@ fn swapper<T>(
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let duration = now.elapsed();
|
||||
let n = to_swap_txs.len();
|
||||
let rate = n as f32 / duration_as_s(&duration);
|
||||
stats.sign_ns += duration_as_ns(&duration);
|
||||
if rate > stats.sign_peak_rate {
|
||||
stats.sign_peak_rate = rate;
|
||||
}
|
||||
trace!(" sw {:?} signed {:.2} /s ", n, rate);
|
||||
|
||||
solana_metrics::submit(
|
||||
influxdb::Point::new("bench-exchange")
|
||||
.add_tag("op", influxdb::Value::String("swaps".to_string()))
|
||||
.add_field("count", influxdb::Value::Integer(to_swap_txs.len() as i64))
|
||||
.to_owned(),
|
||||
txs += to_swap_txs.len() as u64;
|
||||
total_txs += to_swap_txs.len() as u64;
|
||||
total_elapsed = start_time.elapsed();
|
||||
let duration = now.elapsed();
|
||||
if duration_as_s(&duration) >= 1_f32 {
|
||||
now = Instant::now();
|
||||
let tps = txs as f32 / duration_as_s(&duration);
|
||||
info!(
|
||||
"Swapper {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s",
|
||||
tps,
|
||||
txs,
|
||||
total_txs,
|
||||
total_elapsed.as_secs(),
|
||||
);
|
||||
txs = 0;
|
||||
}
|
||||
|
||||
datapoint_info!("bench-exchange-swaps", ("count", to_swap_txs.len(), i64));
|
||||
|
||||
let chunks: Vec<_> = to_swap_txs.chunks(chunk_size).collect();
|
||||
{
|
||||
@ -482,6 +418,8 @@ fn swapper<T>(
|
||||
shared_txs_wl.push_back(chunk.to_vec());
|
||||
}
|
||||
}
|
||||
// Throttle the swapper so it doesn't try to catchup unbridled
|
||||
sleep(Duration::from_millis(transfer_delay / 2));
|
||||
}
|
||||
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
@ -489,18 +427,9 @@ fn swapper<T>(
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"{} Swaps, batch size {}, chunk size {}",
|
||||
stats.total, batch_size, chunk_size
|
||||
);
|
||||
info!(
|
||||
" Keygen avg {:.2}/s peak {:.2}/s",
|
||||
(stats.total as f64 / stats.keygen_ns as f64) * 1_000_000_000_f64,
|
||||
stats.keygen_peak_rate
|
||||
);
|
||||
info!(
|
||||
" Signed avg {:.2}/s peak {:.2}/s",
|
||||
(stats.total as f64 / stats.sign_ns as f64) * 1_000_000_000_f64,
|
||||
stats.sign_peak_rate
|
||||
"Swapper sent {} at {:9.2} TPS",
|
||||
total_txs,
|
||||
total_txs as f32 / duration_as_s(&total_elapsed)
|
||||
);
|
||||
assert_eq!(
|
||||
order_book.get_num_outstanding().0 + order_book.get_num_outstanding().1,
|
||||
@ -515,7 +444,7 @@ fn trader<T>(
|
||||
shared_txs: &SharedTransactions,
|
||||
signers: &[Arc<Keypair>],
|
||||
srcs: &[Pubkey],
|
||||
delay: u64,
|
||||
transfer_delay: u64,
|
||||
batch_size: usize,
|
||||
chunk_size: usize,
|
||||
account_groups: usize,
|
||||
@ -523,16 +452,19 @@ fn trader<T>(
|
||||
) where
|
||||
T: Client,
|
||||
{
|
||||
let mut stats = Stats::default();
|
||||
|
||||
// TODO Hard coded for now
|
||||
let pair = TokenPair::AB;
|
||||
let tokens = 1;
|
||||
let price = 1000;
|
||||
let mut account_group: usize = 0;
|
||||
|
||||
let mut txs = 0;
|
||||
let mut total_txs = 0;
|
||||
let mut now = Instant::now();
|
||||
let start_time = now;
|
||||
let mut total_elapsed = start_time.elapsed();
|
||||
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
let trade_keys = generate_keypairs(batch_size as u64);
|
||||
|
||||
let mut trades = vec![];
|
||||
@ -566,20 +498,12 @@ fn trader<T>(
|
||||
trades.push((signer, trade.pubkey(), direction, src));
|
||||
}
|
||||
account_group = (account_group + 1) % account_groups as usize;
|
||||
let duration = now.elapsed();
|
||||
let rate = batch_size as f32 / duration_as_s(&duration);
|
||||
stats.keygen_ns += duration_as_ns(&duration);
|
||||
if rate > stats.keygen_peak_rate {
|
||||
stats.keygen_peak_rate = rate;
|
||||
}
|
||||
trace!("sw {:?} keypairs {:.2} /s", batch_size, rate);
|
||||
|
||||
let blockhash = client
|
||||
let (blockhash, _fee_calculator) = client
|
||||
.get_recent_blockhash()
|
||||
.expect("Failed to get blockhash");
|
||||
|
||||
trades.chunks(chunk_size).for_each(|chunk| {
|
||||
let now = Instant::now();
|
||||
let trades_txs: Vec<_> = chunk
|
||||
.par_iter()
|
||||
.map(|(signer, trade, direction, src)| {
|
||||
@ -598,55 +522,52 @@ fn trader<T>(
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let duration = now.elapsed();
|
||||
let n = trades_txs.len();
|
||||
let rate = n as f32 / duration_as_s(&duration);
|
||||
stats.sign_ns += duration_as_ns(&duration);
|
||||
if rate > stats.sign_peak_rate {
|
||||
stats.sign_peak_rate = rate;
|
||||
}
|
||||
trace!(" sw {:?} signed {:.2} /s ", n, rate);
|
||||
|
||||
solana_metrics::submit(
|
||||
influxdb::Point::new("bench-exchange")
|
||||
.add_tag("op", influxdb::Value::String("trades".to_string()))
|
||||
.add_field("count", influxdb::Value::Integer(trades_txs.len() as i64))
|
||||
.to_owned(),
|
||||
{
|
||||
txs += chunk_size as u64;
|
||||
total_txs += chunk_size as u64;
|
||||
total_elapsed = start_time.elapsed();
|
||||
let duration = now.elapsed();
|
||||
if duration_as_s(&duration) >= 1_f32 {
|
||||
now = Instant::now();
|
||||
let tps = txs as f32 / duration_as_s(&duration);
|
||||
info!(
|
||||
"Trader {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s",
|
||||
tps,
|
||||
txs,
|
||||
total_txs,
|
||||
total_elapsed.as_secs(),
|
||||
);
|
||||
txs = 0;
|
||||
}
|
||||
|
||||
datapoint_info!("bench-exchange-trades", ("count", trades_txs.len(), i64));
|
||||
|
||||
{
|
||||
let mut shared_txs_wl = shared_txs
|
||||
.write()
|
||||
.expect("Failed to send tx to transfer threads");
|
||||
stats.total += chunk_size as u64;
|
||||
shared_txs_wl.push_back(trades_txs);
|
||||
}
|
||||
if delay > 0 {
|
||||
sleep(Duration::from_millis(delay));
|
||||
}
|
||||
if transfer_delay > 0 {
|
||||
sleep(Duration::from_millis(transfer_delay));
|
||||
}
|
||||
});
|
||||
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
info!(
|
||||
"Trader sent {} at {:9.2} TPS",
|
||||
total_txs,
|
||||
total_txs as f32 / duration_as_s(&total_elapsed)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO chunk the trade infos and send them when the batch is sent
|
||||
sender
|
||||
.send(trade_infos)
|
||||
.expect("Failed to send trades to swapper");
|
||||
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
info!(
|
||||
"{} Trades with batch size {} chunk size {}",
|
||||
stats.total, batch_size, chunk_size
|
||||
);
|
||||
info!(
|
||||
" Keygen avg {:.2}/s peak {:.2}/s",
|
||||
(stats.total as f64 / stats.keygen_ns as f64) * 1_000_000_000_f64,
|
||||
stats.keygen_peak_rate
|
||||
);
|
||||
info!(
|
||||
" Signed avg {:.2}/s peak {:.2}/s",
|
||||
(stats.total as f64 / stats.sign_ns as f64) * 1_000_000_000_f64,
|
||||
stats.sign_peak_rate
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -742,7 +663,8 @@ pub fn fund_keys(client: &Client, source: &Keypair, dests: &[Arc<Keypair>], lamp
|
||||
to_fund_txs.len(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_recent_blockhash().expect("blockhash");
|
||||
let (blockhash, _fee_calculator) =
|
||||
client.get_recent_blockhash().expect("blockhash");
|
||||
to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
|
||||
tx.sign(&[*k], blockhash);
|
||||
});
|
||||
@ -793,11 +715,11 @@ pub fn create_token_accounts(client: &Client, signers: &[Arc<Keypair>], accounts
|
||||
let mut to_create_txs: Vec<_> = chunk
|
||||
.par_iter()
|
||||
.map(|(signer, new)| {
|
||||
let owner_id = &signer.pubkey();
|
||||
let owner_pubkey = &signer.pubkey();
|
||||
let space = mem::size_of::<ExchangeState>() as u64;
|
||||
let create_ix =
|
||||
system_instruction::create_account(owner_id, new, 1, space, &id());
|
||||
let request_ix = exchange_instruction::account_request(owner_id, new);
|
||||
system_instruction::create_account(owner_pubkey, new, 1, space, &id());
|
||||
let request_ix = exchange_instruction::account_request(owner_pubkey, new);
|
||||
(
|
||||
signer,
|
||||
Transaction::new_unsigned_instructions(vec![create_ix, request_ix]),
|
||||
@ -817,7 +739,7 @@ pub fn create_token_accounts(client: &Client, signers: &[Arc<Keypair>], accounts
|
||||
|
||||
let mut retries = 0;
|
||||
while !to_create_txs.is_empty() {
|
||||
let blockhash = client
|
||||
let (blockhash, _fee_calculator) = client
|
||||
.get_recent_blockhash()
|
||||
.expect("Failed to get blockhash");
|
||||
to_create_txs.par_iter_mut().for_each(|(k, tx)| {
|
||||
@ -868,13 +790,13 @@ pub fn create_token_accounts(client: &Client, signers: &[Arc<Keypair>], accounts
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_and_report_stats(maxes: &Arc<RwLock<Vec<(SampleStats)>>>, total_txs_sent: u64) {
|
||||
fn compute_and_report_stats(maxes: &Arc<RwLock<Vec<(String, SampleStats)>>>, total_txs_sent: u64) {
|
||||
let mut max_txs = 0;
|
||||
let mut max_elapsed = Duration::new(0, 0);
|
||||
info!("| Max TPS | Total Transactions");
|
||||
info!("+---------------+--------------------");
|
||||
|
||||
for stats in maxes.read().unwrap().iter() {
|
||||
for (_sock, stats) in maxes.read().unwrap().iter() {
|
||||
let maybe_flag = match stats.txs {
|
||||
0 => "!!!!!",
|
||||
_ => "",
|
||||
@ -933,7 +855,7 @@ pub fn airdrop_lamports(client: &Client, drone_addr: &SocketAddr, id: &Keypair,
|
||||
|
||||
let mut tries = 0;
|
||||
loop {
|
||||
let blockhash = client
|
||||
let (blockhash, _fee_calculator) = client
|
||||
.get_recent_blockhash()
|
||||
.expect("Failed to get blockhash");
|
||||
match request_airdrop_transaction(&drone_addr, &id.pubkey(), amount_to_drop, blockhash) {
|
||||
@ -967,35 +889,17 @@ pub fn airdrop_lamports(client: &Client, drone_addr: &SocketAddr, id: &Keypair,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_clients(nodes: &[ContactInfo]) -> Vec<ThinClient> {
|
||||
nodes
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
let cluster_entrypoint = node;
|
||||
let cluster_addrs = cluster_entrypoint.client_facing_addr();
|
||||
if ContactInfo::is_valid_address(&cluster_addrs.0)
|
||||
&& ContactInfo::is_valid_address(&cluster_addrs.1)
|
||||
{
|
||||
let client = create_client(cluster_addrs, FULLNODE_PORT_RANGE);
|
||||
Some(client)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana::fullnode::FullnodeConfig;
|
||||
use solana::gossip_service::discover_nodes;
|
||||
use solana::gossip_service::{discover_cluster, get_clients};
|
||||
use solana::local_cluster::{ClusterConfig, LocalCluster};
|
||||
use solana::validator::ValidatorConfig;
|
||||
use solana_drone::drone::run_local_drone;
|
||||
use solana_exchange_api::exchange_processor::process_instruction;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::genesis_block::create_genesis_block;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
#[test]
|
||||
@ -1003,16 +907,16 @@ mod tests {
|
||||
solana_logger::setup();
|
||||
|
||||
const NUM_NODES: usize = 1;
|
||||
let fullnode_config = FullnodeConfig::default();
|
||||
let validator_config = ValidatorConfig::default();
|
||||
|
||||
let mut config = Config::default();
|
||||
config.identity = Keypair::new();
|
||||
config.threads = 1;
|
||||
config.duration = Duration::from_secs(1);
|
||||
config.fund_amount = 100_000;
|
||||
config.transfer_delay = 20;
|
||||
config.threads = 1;
|
||||
config.transfer_delay = 20; // 15
|
||||
config.batch_size = 100; // 1000;
|
||||
config.chunk_size = 10; // 250;
|
||||
config.chunk_size = 10; // 200;
|
||||
config.account_groups = 1; // 10;
|
||||
let Config {
|
||||
fund_amount,
|
||||
@ -1025,8 +929,8 @@ mod tests {
|
||||
let cluster = LocalCluster::new(&ClusterConfig {
|
||||
node_stakes: vec![100_000; NUM_NODES],
|
||||
cluster_lamports: 100_000_000_000_000,
|
||||
fullnode_config,
|
||||
native_instruction_processors: [("solana_exchange_program".to_string(), id())].to_vec(),
|
||||
validator_config,
|
||||
native_instruction_processors: [solana_exchange_program!()].to_vec(),
|
||||
..ClusterConfig::default()
|
||||
});
|
||||
|
||||
@ -1042,8 +946,8 @@ mod tests {
|
||||
let drone_addr = addr_receiver.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
|
||||
info!("Connecting to the cluster");
|
||||
let nodes =
|
||||
discover_nodes(&cluster.entry_point_info.gossip, NUM_NODES).unwrap_or_else(|err| {
|
||||
let (nodes, _) = discover_cluster(&cluster.entry_point_info.gossip, NUM_NODES)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("Failed to discover {} nodes: {:?}", NUM_NODES, err);
|
||||
exit(1);
|
||||
});
|
||||
@ -1072,16 +976,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_exchange_bank_client() {
|
||||
solana_logger::setup();
|
||||
let (genesis_block, identity) = GenesisBlock::new(100_000_000_000_000);
|
||||
let (genesis_block, identity) = create_genesis_block(100_000_000_000_000);
|
||||
let mut bank = Bank::new(&genesis_block);
|
||||
bank.add_instruction_processor(id(), process_instruction);
|
||||
let clients = vec![BankClient::new(bank)];
|
||||
|
||||
let mut config = Config::default();
|
||||
config.identity = identity;
|
||||
config.threads = 1;
|
||||
config.duration = Duration::from_secs(1);
|
||||
config.fund_amount = 100_000;
|
||||
config.threads = 1;
|
||||
config.transfer_delay = 20; // 0;
|
||||
config.batch_size = 100; // 1500;
|
||||
config.chunk_size = 10; // 1500;
|
||||
|
@ -1,13 +1,13 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::{crate_description, crate_name, crate_version, value_t, App, Arg, ArgMatches};
|
||||
use solana::gen_keys::GenKeys;
|
||||
use solana_drone::drone::DRONE_PORT;
|
||||
use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
|
||||
use std::net::SocketAddr;
|
||||
use std::process::exit;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Config {
|
||||
pub network_addr: SocketAddr,
|
||||
pub entrypoint_addr: SocketAddr,
|
||||
pub drone_addr: SocketAddr,
|
||||
pub identity: Keypair,
|
||||
pub threads: usize,
|
||||
@ -23,7 +23,7 @@ pub struct Config {
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
network_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||
drone_addr: SocketAddr::from(([127, 0, 0, 1], DRONE_PORT)),
|
||||
identity: Keypair::new(),
|
||||
num_nodes: 1,
|
||||
@ -43,14 +43,14 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
.about(crate_description!())
|
||||
.version(crate_version!())
|
||||
.arg(
|
||||
Arg::with_name("network")
|
||||
Arg::with_name("entrypoint")
|
||||
.short("n")
|
||||
.long("network")
|
||||
.long("entrypoint")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.default_value("127.0.0.1:8001")
|
||||
.help("Network's gossip entry point; defaults to 127.0.0.1:8001"),
|
||||
.help("Cluster entry point; defaults to 127.0.0.1:8001"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("drone")
|
||||
@ -146,16 +146,17 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
||||
let mut args = Config::default();
|
||||
|
||||
args.network_addr = matches
|
||||
.value_of("network")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("Failed to parse network");
|
||||
args.drone_addr = matches
|
||||
.value_of("drone")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("Failed to parse drone address");
|
||||
args.entrypoint_addr = solana_netutil::parse_host_port(matches.value_of("entrypoint").unwrap())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse entrypoint address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
|
||||
args.drone_addr = solana_netutil::parse_host_port(matches.value_of("drone").unwrap())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse drone address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
|
||||
if matches.is_present("identity") {
|
||||
args.identity = read_keypair(matches.value_of("identity").unwrap())
|
||||
|
@ -2,9 +2,13 @@ pub mod bench;
|
||||
mod cli;
|
||||
pub mod order_book;
|
||||
|
||||
use crate::bench::{airdrop_lamports, do_bench_exchange, get_clients, Config};
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate solana_exchange_program;
|
||||
|
||||
use crate::bench::{airdrop_lamports, do_bench_exchange, Config};
|
||||
use log::*;
|
||||
use solana::gossip_service::discover_nodes;
|
||||
use solana::gossip_service::{discover_cluster, get_clients};
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
|
||||
fn main() {
|
||||
@ -15,7 +19,7 @@ fn main() {
|
||||
let cli_config = cli::extract_args(&matches);
|
||||
|
||||
let cli::Config {
|
||||
network_addr,
|
||||
entrypoint_addr,
|
||||
drone_addr,
|
||||
identity,
|
||||
threads,
|
||||
@ -30,7 +34,8 @@ fn main() {
|
||||
} = cli_config;
|
||||
|
||||
info!("Connecting to the cluster");
|
||||
let nodes = discover_nodes(&network_addr, num_nodes).unwrap_or_else(|_| {
|
||||
let (nodes, _replicators) =
|
||||
discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|_| {
|
||||
panic!("Failed to discover nodes");
|
||||
});
|
||||
|
||||
|
1
bench-streamer/.gitignore
vendored
Normal file
1
bench-streamer/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target/
|
@ -2,16 +2,16 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-streamer"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
solana = { path = "../core", version = "0.14.0" }
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana = { path = "../core", version = "0.15.0" }
|
||||
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||
|
||||
[features]
|
||||
cuda = ["solana/cuda"]
|
||||
|
@ -20,13 +20,13 @@ fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
|
||||
w.meta.size = PACKET_DATA_SIZE;
|
||||
w.meta.set_addr(&addr);
|
||||
}
|
||||
let msgs_ = msgs.clone();
|
||||
let msgs = Arc::new(msgs);
|
||||
spawn(move || loop {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let mut num = 0;
|
||||
for p in &msgs_.packets {
|
||||
for p in &msgs.packets {
|
||||
let a = p.meta.addr();
|
||||
assert!(p.meta.size < BLOB_SIZE);
|
||||
send.send_to(&p.data[..p.meta.size], &a).unwrap();
|
||||
|
@ -2,23 +2,24 @@
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
edition = "2018"
|
||||
name = "solana-bench-tps"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
log = "0.4.6"
|
||||
rayon = "1.0.3"
|
||||
serde_json = "1.0.39"
|
||||
solana = { path = "../core", version = "0.14.0" }
|
||||
solana-client = { path = "../client", version = "0.14.0" }
|
||||
solana-drone = { path = "../drone", version = "0.14.0" }
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.14.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.14.0" }
|
||||
solana = { path = "../core", version = "0.15.0" }
|
||||
solana-client = { path = "../client", version = "0.15.0" }
|
||||
solana-drone = { path = "../drone", version = "0.15.0" }
|
||||
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.15.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.15.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.15.0" }
|
||||
|
||||
[features]
|
||||
cuda = ["solana/cuda"]
|
||||
|
@ -1,10 +1,13 @@
|
||||
use solana_metrics;
|
||||
|
||||
use log::*;
|
||||
use rayon::prelude::*;
|
||||
use solana::gen_keys::GenKeys;
|
||||
use solana_client::perf_utils::{sample_txs, SampleStats};
|
||||
use solana_drone::drone::request_airdrop_transaction;
|
||||
use solana_metrics::influxdb;
|
||||
use solana_metrics::datapoint_info;
|
||||
use solana_sdk::client::Client;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_instruction;
|
||||
use solana_sdk::system_transaction;
|
||||
@ -22,13 +25,6 @@ use std::thread::Builder;
|
||||
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 const NUM_LAMPORTS_PER_ACCOUNT: u64 = 20;
|
||||
|
||||
@ -61,7 +57,8 @@ pub fn do_bench_tps<T>(
|
||||
config: Config,
|
||||
gen_keypairs: Vec<Keypair>,
|
||||
keypair0_balance: u64,
|
||||
) where
|
||||
) -> u64
|
||||
where
|
||||
T: 'static + Client + Send + Sync,
|
||||
{
|
||||
let Config {
|
||||
@ -98,7 +95,7 @@ pub fn do_bench_tps<T>(
|
||||
Builder::new()
|
||||
.name("solana-client-sample".to_string())
|
||||
.spawn(move || {
|
||||
sample_tx_count(&exit_signal, &maxes, first_tx_count, sample_period, &client);
|
||||
sample_txs(&exit_signal, &maxes, sample_period, &client);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
@ -136,28 +133,39 @@ pub fn do_bench_tps<T>(
|
||||
let start = Instant::now();
|
||||
let mut reclaim_lamports_back_to_source_account = false;
|
||||
let mut i = keypair0_balance;
|
||||
let mut blockhash = Hash::default();
|
||||
let mut blockhash_time = Instant::now();
|
||||
while start.elapsed() < duration {
|
||||
let balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
|
||||
// ping-pong between source and destination accounts for each loop iteration
|
||||
// this seems to be faster than trying to determine the balance of individual
|
||||
// accounts
|
||||
let len = tx_count as usize;
|
||||
if let Ok((new_blockhash, _fee_calculator)) = client.get_new_blockhash(&blockhash) {
|
||||
blockhash = new_blockhash;
|
||||
} else {
|
||||
if blockhash_time.elapsed().as_secs() > 30 {
|
||||
panic!("Blockhash is not updating");
|
||||
}
|
||||
sleep(Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
blockhash_time = Instant::now();
|
||||
let balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
generate_txs(
|
||||
&shared_txs,
|
||||
&blockhash,
|
||||
&keypairs[..len],
|
||||
&keypairs[len..],
|
||||
threads,
|
||||
reclaim_lamports_back_to_source_account,
|
||||
&client,
|
||||
);
|
||||
// In sustained mode overlap the transfers with generation
|
||||
// this has higher average performance but lower peak performance
|
||||
// in tested environments.
|
||||
if !sustained {
|
||||
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
|
||||
sleep(Duration::from_millis(100));
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,86 +202,27 @@ pub fn do_bench_tps<T>(
|
||||
&start.elapsed(),
|
||||
total_tx_sent_count.load(Ordering::Relaxed),
|
||||
);
|
||||
|
||||
let r_maxes = maxes.read().unwrap();
|
||||
r_maxes.first().unwrap().1.txs
|
||||
}
|
||||
|
||||
fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
||||
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(),
|
||||
datapoint_info!(
|
||||
"bench-tps-lamport_balance",
|
||||
("balance", lamport_balance, i64)
|
||||
);
|
||||
}
|
||||
|
||||
fn sample_tx_count<T: Client>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
maxes: &Arc<RwLock<Vec<(String, NodeStats)>>>,
|
||||
first_tx_count: u64,
|
||||
sample_period: u64,
|
||||
client: &Arc<T>,
|
||||
) {
|
||||
let mut now = Instant::now();
|
||||
let mut initial_tx_count = client.get_transaction_count().expect("transaction count");
|
||||
let mut max_tps = 0.0;
|
||||
let mut total;
|
||||
|
||||
let log_prefix = format!("{:21}:", client.transactions_addr());
|
||||
|
||||
loop {
|
||||
let mut tx_count = client.get_transaction_count().expect("transaction count");
|
||||
if tx_count < initial_tx_count {
|
||||
println!(
|
||||
"expected tx_count({}) >= initial_tx_count({})",
|
||||
tx_count, initial_tx_count
|
||||
);
|
||||
tx_count = initial_tx_count;
|
||||
}
|
||||
let duration = now.elapsed();
|
||||
now = Instant::now();
|
||||
let sample = tx_count - initial_tx_count;
|
||||
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((client.transactions_addr(), stats));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_txs<T: Client>(
|
||||
fn generate_txs(
|
||||
shared_txs: &SharedTransactions,
|
||||
blockhash: &Hash,
|
||||
source: &[Keypair],
|
||||
dest: &[Keypair],
|
||||
threads: usize,
|
||||
reclaim: bool,
|
||||
client: &Arc<T>,
|
||||
) {
|
||||
let blockhash = client.get_recent_blockhash().unwrap();
|
||||
let tx_count = source.len();
|
||||
println!("Signing transactions... {} (reclaim={})", tx_count, reclaim);
|
||||
let signing_start = Instant::now();
|
||||
@ -287,7 +236,7 @@ fn generate_txs<T: Client>(
|
||||
.par_iter()
|
||||
.map(|(id, keypair)| {
|
||||
(
|
||||
system_transaction::create_user_account(id, &keypair.pubkey(), 1, blockhash, 0),
|
||||
system_transaction::create_user_account(id, &keypair.pubkey(), 1, *blockhash),
|
||||
timestamp(),
|
||||
)
|
||||
})
|
||||
@ -304,14 +253,9 @@ fn generate_txs<T: Client>(
|
||||
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(),
|
||||
datapoint_info!(
|
||||
"bench-tps-generate_txs",
|
||||
("duration", duration_as_ms(&duration), i64)
|
||||
);
|
||||
|
||||
let sz = transactions.len() / threads;
|
||||
@ -338,7 +282,7 @@ fn do_tx_transfers<T: Client>(
|
||||
}
|
||||
let txs;
|
||||
{
|
||||
let mut shared_txs_wl = shared_txs.write().unwrap();
|
||||
let mut shared_txs_wl = shared_txs.write().expect("write lock in do_tx_transfers");
|
||||
txs = shared_txs_wl.pop_front();
|
||||
}
|
||||
if let Some(txs0) = txs {
|
||||
@ -355,7 +299,9 @@ fn do_tx_transfers<T: Client>(
|
||||
if now > tx.1 && now - tx.1 > 1000 * 30 {
|
||||
continue;
|
||||
}
|
||||
client.async_send_transaction(tx.0).unwrap();
|
||||
client
|
||||
.async_send_transaction(tx.0)
|
||||
.expect("async_send_transaction in do_tx_transfers");
|
||||
}
|
||||
shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed);
|
||||
total_tx_sent_count.fetch_add(tx_len, Ordering::Relaxed);
|
||||
@ -364,15 +310,10 @@ fn do_tx_transfers<T: Client>(
|
||||
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(),
|
||||
datapoint_info!(
|
||||
"bench-tps-do_tx_transfers",
|
||||
("duration", duration_as_ms(&transfer_start.elapsed()), i64),
|
||||
("count", tx_len, i64)
|
||||
);
|
||||
}
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
@ -465,7 +406,7 @@ pub fn fund_keys<T: Client>(client: &T, source: &Keypair, dests: &[Keypair], lam
|
||||
to_fund_txs.len(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_recent_blockhash().unwrap();
|
||||
let (blockhash, _fee_calculator) = client.get_recent_blockhash().unwrap();
|
||||
|
||||
// re-sign retained to_fund_txes with updated blockhash
|
||||
to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
|
||||
@ -515,7 +456,7 @@ pub fn airdrop_lamports<T: Client>(
|
||||
id.pubkey(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_recent_blockhash().unwrap();
|
||||
let (blockhash, _fee_calculator) = client.get_recent_blockhash().unwrap();
|
||||
match request_airdrop_transaction(&drone_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
||||
Ok(transaction) => {
|
||||
let signature = client.async_send_transaction(transaction).unwrap();
|
||||
@ -556,7 +497,7 @@ pub fn airdrop_lamports<T: Client>(
|
||||
}
|
||||
|
||||
fn compute_and_report_stats(
|
||||
maxes: &Arc<RwLock<Vec<(String, NodeStats)>>>,
|
||||
maxes: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||
sample_period: u64,
|
||||
tx_send_elapsed: &Duration,
|
||||
total_tx_send_count: usize,
|
||||
@ -570,14 +511,14 @@ fn compute_and_report_stats(
|
||||
println!("---------------------+---------------+--------------------");
|
||||
|
||||
for (sock, stats) in maxes.read().unwrap().iter() {
|
||||
let maybe_flag = match stats.tx {
|
||||
let maybe_flag = match stats.txs {
|
||||
0 => "!!!!!",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
println!(
|
||||
"{:20} | {:13.2} | {} {}",
|
||||
sock, stats.tps, stats.tx, maybe_flag
|
||||
sock, stats.tps, stats.txs, maybe_flag
|
||||
);
|
||||
|
||||
if stats.tps == 0.0 {
|
||||
@ -588,27 +529,33 @@ fn compute_and_report_stats(
|
||||
if stats.tps > max_of_maxes {
|
||||
max_of_maxes = stats.tps;
|
||||
}
|
||||
if stats.tx > max_tx_count {
|
||||
max_tx_count = stats.tx;
|
||||
if stats.txs > max_tx_count {
|
||||
max_tx_count = stats.txs;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
let average_max = total_maxes / num_nodes_with_tps as f32;
|
||||
println!(
|
||||
"\nAverage max TPS: {:.2}, {} nodes had 0 TPS",
|
||||
average_max, nodes_with_zero_tps
|
||||
);
|
||||
}
|
||||
|
||||
let total_tx_send_count = total_tx_send_count as u64;
|
||||
let drop_rate = if total_tx_send_count > max_tx_count {
|
||||
(total_tx_send_count - max_tx_count) as f64 / total_tx_send_count as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
println!(
|
||||
"\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,
|
||||
drop_rate,
|
||||
);
|
||||
println!(
|
||||
"\tAverage TPS: {}",
|
||||
@ -623,31 +570,67 @@ fn should_switch_directions(num_lamports_per_account: u64, i: u64) -> bool {
|
||||
i % (num_lamports_per_account / 4) == 0 && (i >= (3 * num_lamports_per_account) / 4)
|
||||
}
|
||||
|
||||
pub fn generate_keypairs(id: &Keypair, tx_count: usize) -> Vec<Keypair> {
|
||||
pub fn generate_keypairs(seed_keypair: &Keypair, count: usize) -> Vec<Keypair> {
|
||||
let mut seed = [0u8; 32];
|
||||
seed.copy_from_slice(&id.to_bytes()[..32]);
|
||||
seed.copy_from_slice(&seed_keypair.to_bytes()[..32]);
|
||||
let mut rnd = GenKeys::new(seed);
|
||||
|
||||
let mut total_keys = 0;
|
||||
let mut target = tx_count * 2;
|
||||
while target > 0 {
|
||||
let mut target = count;
|
||||
while target > 1 {
|
||||
total_keys += target;
|
||||
target /= MAX_SPENDS_PER_TX;
|
||||
// Use the upper bound for this division otherwise it may not generate enough keys
|
||||
target = (target + MAX_SPENDS_PER_TX - 1) / MAX_SPENDS_PER_TX;
|
||||
}
|
||||
rnd.gen_n_keypairs(total_keys as u64)
|
||||
}
|
||||
|
||||
pub fn generate_and_fund_keypairs<T: Client>(
|
||||
client: &T,
|
||||
drone_addr: Option<SocketAddr>,
|
||||
funding_pubkey: &Keypair,
|
||||
tx_count: usize,
|
||||
lamports_per_account: u64,
|
||||
) -> (Vec<Keypair>, u64) {
|
||||
info!("Creating {} keypairs...", tx_count * 2);
|
||||
let mut keypairs = generate_keypairs(funding_pubkey, tx_count * 2);
|
||||
|
||||
info!("Get lamports...");
|
||||
|
||||
// Sample the first keypair, see if it has lamports, if so then resume.
|
||||
// This logic is to prevent lamport loss on repeated solana-bench-tps executions
|
||||
let last_keypair_balance = client
|
||||
.get_balance(&keypairs[tx_count * 2 - 1].pubkey())
|
||||
.unwrap_or(0);
|
||||
|
||||
if lamports_per_account > last_keypair_balance {
|
||||
let extra = lamports_per_account - last_keypair_balance;
|
||||
let total = extra * (keypairs.len() as u64);
|
||||
if client.get_balance(&funding_pubkey.pubkey()).unwrap_or(0) < total {
|
||||
airdrop_lamports(client, &drone_addr.unwrap(), funding_pubkey, total);
|
||||
}
|
||||
info!("adding more lamports {}", extra);
|
||||
fund_keys(client, funding_pubkey, &keypairs, extra);
|
||||
}
|
||||
|
||||
// 'generate_keypairs' generates extra keys to be able to have size-aligned funding batches for fund_keys.
|
||||
keypairs.truncate(2 * tx_count);
|
||||
|
||||
(keypairs, last_keypair_balance)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana::cluster_info::FULLNODE_PORT_RANGE;
|
||||
use solana::fullnode::FullnodeConfig;
|
||||
use solana::local_cluster::{ClusterConfig, LocalCluster};
|
||||
use solana::validator::ValidatorConfig;
|
||||
use solana_client::thin_client::create_client;
|
||||
use solana_drone::drone::run_local_drone;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_block::create_genesis_block;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
#[test]
|
||||
@ -666,14 +649,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_bench_tps() {
|
||||
let fullnode_config = FullnodeConfig::default();
|
||||
fn test_bench_tps_local_cluster() {
|
||||
solana_logger::setup();
|
||||
let validator_config = ValidatorConfig::default();
|
||||
const NUM_NODES: usize = 1;
|
||||
let cluster = LocalCluster::new(&ClusterConfig {
|
||||
node_stakes: vec![999_990; NUM_NODES],
|
||||
cluster_lamports: 2_000_000,
|
||||
fullnode_config,
|
||||
validator_config,
|
||||
..ClusterConfig::default()
|
||||
});
|
||||
|
||||
@ -688,18 +671,27 @@ mod tests {
|
||||
config.tx_count = 100;
|
||||
config.duration = Duration::from_secs(5);
|
||||
|
||||
let keypairs = generate_keypairs(&config.id, config.tx_count);
|
||||
let client = create_client(
|
||||
(cluster.entry_point_info.gossip, drone_addr),
|
||||
(cluster.entry_point_info.rpc, cluster.entry_point_info.tpu),
|
||||
FULLNODE_PORT_RANGE,
|
||||
);
|
||||
|
||||
do_bench_tps(vec![client], config, keypairs, 0);
|
||||
let lamports_per_account = 100;
|
||||
let (keypairs, _keypair_balance) = generate_and_fund_keypairs(
|
||||
&client,
|
||||
Some(drone_addr),
|
||||
&config.id,
|
||||
config.tx_count,
|
||||
lamports_per_account,
|
||||
);
|
||||
|
||||
let total = do_bench_tps(vec![client], config, keypairs, 0);
|
||||
assert!(total > 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_bank_client() {
|
||||
let (genesis_block, id) = GenesisBlock::new(10_000);
|
||||
let (genesis_block, id) = create_genesis_block(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let clients = vec![BankClient::new(bank)];
|
||||
|
||||
@ -708,9 +700,26 @@ mod tests {
|
||||
config.tx_count = 10;
|
||||
config.duration = Duration::from_secs(5);
|
||||
|
||||
let keypairs = generate_keypairs(&config.id, config.tx_count);
|
||||
fund_keys(&clients[0], &config.id, &keypairs, 20);
|
||||
let (keypairs, _keypair_balance) =
|
||||
generate_and_fund_keypairs(&clients[0], None, &config.id, config.tx_count, 20);
|
||||
|
||||
do_bench_tps(clients, config, keypairs, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_fund_keys() {
|
||||
let (genesis_block, id) = create_genesis_block(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let client = BankClient::new(bank);
|
||||
let tx_count = 10;
|
||||
let lamports = 20;
|
||||
|
||||
let (keypairs, _keypair_balance) =
|
||||
generate_and_fund_keypairs(&client, None, &id, tx_count, lamports);
|
||||
|
||||
for kp in &keypairs {
|
||||
// TODO: This should be >= lamports, but fails at the moment
|
||||
assert_ne!(client.get_balance(&kp.pubkey()).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
|
||||
|
||||
/// Holds the configuration for a single run of the benchmark
|
||||
pub struct Config {
|
||||
pub network_addr: SocketAddr,
|
||||
pub entrypoint_addr: SocketAddr,
|
||||
pub drone_addr: SocketAddr,
|
||||
pub id: Keypair,
|
||||
pub threads: usize,
|
||||
@ -22,7 +22,7 @@ pub struct Config {
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
network_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||
drone_addr: SocketAddr::from(([127, 0, 0, 1], DRONE_PORT)),
|
||||
id: Keypair::new(),
|
||||
threads: 4,
|
||||
@ -40,12 +40,12 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
App::new(crate_name!()).about(crate_description!())
|
||||
.version(crate_version!())
|
||||
.arg(
|
||||
Arg::with_name("network")
|
||||
Arg::with_name("entrypoint")
|
||||
.short("n")
|
||||
.long("network")
|
||||
.long("entrypoint")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.help("Rendezvous with the network at this gossip entry point; defaults to 127.0.0.1:8001"),
|
||||
.help("Rendezvous with the cluster at this entry point; defaults to 127.0.0.1:8001"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("drone")
|
||||
@ -53,7 +53,7 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
.long("drone")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.help("Location of the drone; defaults to network:DRONE_PORT"),
|
||||
.help("Location of the drone; defaults to entrypoint:DRONE_PORT"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("identity")
|
||||
@ -116,9 +116,9 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
||||
let mut args = Config::default();
|
||||
|
||||
if let Some(addr) = matches.value_of("network") {
|
||||
args.network_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse network address: {}", e);
|
||||
if let Some(addr) = matches.value_of("entrypoint") {
|
||||
args.entrypoint_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse entrypoint address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
}
|
||||
|
@ -1,15 +1,8 @@
|
||||
mod bench;
|
||||
mod cli;
|
||||
|
||||
use crate::bench::{
|
||||
airdrop_lamports, do_bench_tps, fund_keys, generate_keypairs, Config, NUM_LAMPORTS_PER_ACCOUNT,
|
||||
};
|
||||
use solana::cluster_info::FULLNODE_PORT_RANGE;
|
||||
use solana::contact_info::ContactInfo;
|
||||
use solana::gossip_service::discover_nodes;
|
||||
use solana_client::thin_client::create_client;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
use crate::bench::{do_bench_tps, generate_and_fund_keypairs, Config, NUM_LAMPORTS_PER_ACCOUNT};
|
||||
use solana::gossip_service::{discover_cluster, get_clients};
|
||||
use std::process::exit;
|
||||
|
||||
fn main() {
|
||||
@ -20,7 +13,7 @@ fn main() {
|
||||
let cli_config = cli::extract_args(&matches);
|
||||
|
||||
let cli::Config {
|
||||
network_addr,
|
||||
entrypoint_addr,
|
||||
drone_addr,
|
||||
id,
|
||||
threads,
|
||||
@ -32,7 +25,8 @@ fn main() {
|
||||
} = cli_config;
|
||||
|
||||
println!("Connecting to the cluster");
|
||||
let nodes = discover_nodes(&network_addr, num_nodes).unwrap_or_else(|err| {
|
||||
let (nodes, _replicators) =
|
||||
discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|err| {
|
||||
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
||||
exit(1);
|
||||
});
|
||||
@ -43,40 +37,16 @@ fn main() {
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
let clients: Vec<_> = nodes
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
let cluster_entrypoint = node.clone();
|
||||
let cluster_addrs = cluster_entrypoint.client_facing_addr();
|
||||
if ContactInfo::is_valid_address(&cluster_addrs.0)
|
||||
&& ContactInfo::is_valid_address(&cluster_addrs.1)
|
||||
{
|
||||
let client = create_client(cluster_addrs, FULLNODE_PORT_RANGE);
|
||||
Some(client)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("Creating {} keypairs...", tx_count * 2);
|
||||
let keypairs = generate_keypairs(&id, tx_count);
|
||||
let clients = get_clients(&nodes);
|
||||
|
||||
println!("Get lamports...");
|
||||
|
||||
// Sample the first keypair, see if it has lamports, if so then resume.
|
||||
// This logic is to prevent lamport loss on repeated solana-bench-tps executions
|
||||
let keypair0_balance = clients[0]
|
||||
.get_balance(&keypairs.last().unwrap().pubkey())
|
||||
.unwrap_or(0);
|
||||
|
||||
if NUM_LAMPORTS_PER_ACCOUNT > keypair0_balance {
|
||||
let extra = NUM_LAMPORTS_PER_ACCOUNT - keypair0_balance;
|
||||
let total = extra * (keypairs.len() as u64);
|
||||
airdrop_lamports(&clients[0], &drone_addr, &id, total);
|
||||
println!("adding more lamports {}", extra);
|
||||
fund_keys(&clients[0], &id, &keypairs, extra);
|
||||
}
|
||||
let (keypairs, keypair_balance) = generate_and_fund_keypairs(
|
||||
&clients[0],
|
||||
Some(drone_addr),
|
||||
&id,
|
||||
tx_count,
|
||||
NUM_LAMPORTS_PER_ACCOUNT,
|
||||
);
|
||||
|
||||
let config = Config {
|
||||
id,
|
||||
@ -87,5 +57,5 @@ fn main() {
|
||||
sustained,
|
||||
};
|
||||
|
||||
do_bench_tps(clients, config, keypairs, keypair0_balance);
|
||||
do_bench_tps(clients, config, keypairs, keypair_balance);
|
||||
}
|
||||
|
19
book/art/data-plane-fanout.bob
Normal file
19
book/art/data-plane-fanout.bob
Normal file
@ -0,0 +1,19 @@
|
||||
+------------------------------------------------------------------+
|
||||
| |
|
||||
| +-----------------+ Neighborhood 0 +-----------------+ |
|
||||
| | +--------------------->+ | |
|
||||
| | Validator 1 | | Validator 2 | |
|
||||
| | +<---------------------+ | |
|
||||
| +--------+-+------+ +------+-+--------+ |
|
||||
| | | | | |
|
||||
| | +-----------------------------+ | | |
|
||||
| | +------------------------+------+ | |
|
||||
| | | | | |
|
||||
+------------------------------------------------------------------+
|
||||
| | | |
|
||||
v v v v
|
||||
+---------+------+---+ +-+--------+---------+
|
||||
| | | |
|
||||
| Neighborhood 1 | | Neighborhood 2 |
|
||||
| | | |
|
||||
+--------------------+ +--------------------+
|
15
book/art/data-plane-seeding.bob
Normal file
15
book/art/data-plane-seeding.bob
Normal file
@ -0,0 +1,15 @@
|
||||
+--------------+
|
||||
| |
|
||||
+------------+ Leader +------------+
|
||||
| | | |
|
||||
| +--------------+ |
|
||||
v v
|
||||
+------------+----------------------------------------+------------+
|
||||
| |
|
||||
| +-----------------+ Neighborhood 0 +-----------------+ |
|
||||
| | +--------------------->+ | |
|
||||
| | Validator 1 | | Validator 2 | |
|
||||
| | +<---------------------+ | |
|
||||
| +-----------------+ +-----------------+ |
|
||||
| |
|
||||
+------------------------------------------------------------------+
|
@ -1,28 +1,18 @@
|
||||
|
||||
+--------------+
|
||||
+--------------------+
|
||||
| |
|
||||
+------------+ Leader +------------+
|
||||
+--------+ Neighborhood 0 +----------+
|
||||
| | | |
|
||||
| +--------------+ |
|
||||
| +--------------------+ |
|
||||
v v
|
||||
+--------+--------+ +--------+--------+
|
||||
| +--------------------->+ |
|
||||
+-----------------+ Validator 1 | | Validator 2 +-------------+
|
||||
| | +<---------------------+ | |
|
||||
| +------+-+-+------+ +---+-+-+---------+ |
|
||||
+---------+----------+ +----------+---------+
|
||||
| | | |
|
||||
| Neighborhood 1 | | Neighborhood 2 |
|
||||
| | | |
|
||||
+---+-----+----------+ +----------+-----+---+
|
||||
| | | |
|
||||
v v v v
|
||||
+------------------+-+ +-+------------------+ +------------------+-+ +-+------------------+
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
| +---------------------------------------------+ | | |
|
||||
| | | | | | | |
|
||||
| | | | | +----------------------+ | |
|
||||
| | | | | | | |
|
||||
| | | | +--------------------------------------------+ |
|
||||
| | | | | | | |
|
||||
| | | +----------------------+ | | |
|
||||
| | | | | | | |
|
||||
v v v v v v v v
|
||||
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
||||
| | | | | | | |
|
||||
| Neighborhood 1 | | Neighborhood 2 | | Neighborhood 3 | | Neighborhood 4 |
|
||||
| Neighborhood 3 | | Neighborhood 4 | | Neighborhood 5 | | Neighborhood 6 |
|
||||
| | | | | | | |
|
||||
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
||||
|
60
book/art/validator-proposal.bob
Normal file
60
book/art/validator-proposal.bob
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
.------------.
|
||||
| Upstream |
|
||||
| Validators |
|
||||
`----+-------`
|
||||
|
|
||||
|
|
||||
.-----------------------------------.
|
||||
| Validator | |
|
||||
| v |
|
||||
| .-----------. .------------. |
|
||||
.--------. | | Fetch | | Repair | |
|
||||
| Client +---->| Stage | | Stage | |
|
||||
`--------` | `---+-------` `----+-------` |
|
||||
| | | |
|
||||
| v v |
|
||||
| .-----------. .------------. |
|
||||
| | TPU |<-->| Blockstore | |
|
||||
| | | | | |
|
||||
| `-----------` `----+-------` |
|
||||
| | |
|
||||
| v |
|
||||
| .------------. |
|
||||
| | Multicast | |
|
||||
| | Stage | |
|
||||
| `----+-------` |
|
||||
| | |
|
||||
`-----------------------------------`
|
||||
|
|
||||
v
|
||||
.------------.
|
||||
| Downstream |
|
||||
| Validators |
|
||||
`------------`
|
||||
|
||||
|
||||
|
||||
.------------.
|
||||
| PoH |
|
||||
| Service |
|
||||
`-------+----`
|
||||
^ |
|
||||
| |
|
||||
.-----------------------------------.
|
||||
| TPU | | |
|
||||
| | v |
|
||||
.-------. | .-----------. .---+--------. | .------------.
|
||||
| Fetch +---->| SigVerify +--->| Banking |<--->| Blockstore |
|
||||
| Stage | | | Stage | | Stage | | | |
|
||||
`-------` | `-----------` `-----+------` | `------------`
|
||||
| | |
|
||||
| | |
|
||||
`-----------------------------------`
|
||||
|
|
||||
v
|
||||
.------------.
|
||||
| Banktree |
|
||||
| |
|
||||
`------------`
|
||||
|
@ -1,5 +1,5 @@
|
||||
.--------------------------------------.
|
||||
| Fullnode |
|
||||
| Validator |
|
||||
| |
|
||||
.--------. | .-------------------. |
|
||||
| |---->| | |
|
@ -3,16 +3,4 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
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)"
|
||||
|
@ -1,7 +1,8 @@
|
||||
BOB_SRCS=$(wildcard art/*.bob)
|
||||
MSC_SRCS=$(wildcard art/*.msc)
|
||||
MD_SRCS=$(wildcard src/*.md)
|
||||
|
||||
SVG_IMGS=$(BOB_SRCS:art/%.bob=src/img/%.svg)
|
||||
SVG_IMGS=$(BOB_SRCS:art/%.bob=src/img/%.svg) $(MSC_SRCS:art/%.msc=src/img/%.svg)
|
||||
|
||||
all: html/index.html
|
||||
|
||||
@ -17,6 +18,10 @@ src/img/%.svg: art/%.bob
|
||||
@mkdir -p $(@D)
|
||||
svgbob < $< > $@
|
||||
|
||||
src/img/%.svg: art/%.msc
|
||||
@mkdir -p $(@D)
|
||||
mscgen -T svg -i $< -o $@
|
||||
|
||||
src/%.md: %.md
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
@ -19,9 +19,10 @@
|
||||
- [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)
|
||||
- [Stake Delegation and Rewards](stake-delegation-and-rewards.md)
|
||||
- [Performance Metrics](performance-metrics.md)
|
||||
|
||||
- [Anatomy of a Fullnode](fullnode.md)
|
||||
- [Anatomy of a Validator](validator.md)
|
||||
- [TPU](tpu.md)
|
||||
- [TVU](tvu.md)
|
||||
- [Blocktree](blocktree.md)
|
||||
@ -38,9 +39,6 @@
|
||||
- [Ledger Replication](ledger-replication-to-implement.md)
|
||||
- [Secure Vote Signing](vote-signing-to-implement.md)
|
||||
- [Staking Rewards](staking-rewards.md)
|
||||
- [Passive Stake Delegation and Rewards](passive-stake-delegation-and-rewards.md)
|
||||
- [Reliable Vote Transmission](reliable-vote-transmission.md)
|
||||
- [Persistent Account Storage](persistent-account-storage.md)
|
||||
- [Cluster Economics](ed_overview.md)
|
||||
- [Validation-client Economics](ed_validation_client_economics.md)
|
||||
- [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md)
|
||||
@ -55,13 +53,17 @@
|
||||
- [Economic Design MVP](ed_mvp.md)
|
||||
- [References](ed_references.md)
|
||||
- [Cluster Test Framework](cluster-test-framework.md)
|
||||
- [Testing Programs](testing-programs.md)
|
||||
- [Credit-only Accounts](credit-only-credit-debit-accounts.md)
|
||||
- [Cluster Software Installation and Updates](installer.md)
|
||||
- [Deterministic Transaction Fees](transaction-fees.md)
|
||||
- [Validator](validator-proposal.md)
|
||||
|
||||
- [Implemented Design Proposals](implemented-proposals.md)
|
||||
- [Fork Selection](fork-selection.md)
|
||||
- [Leader-to-Leader Transition](leader-leader-transition.md)
|
||||
- [Leader-to-Validator Transition](leader-validator-transition.md)
|
||||
- [Testnet Participation](testnet-participation.md)
|
||||
- [Testing Programs](testing-programs.md)
|
||||
- [Reliable Vote Transmission](reliable-vote-transmission.md)
|
||||
- [Persistent Account Storage](persistent-account-storage.md)
|
||||
- [Cluster Software Installation and Updates](installer.md)
|
||||
- [Passive Stake Delegation and Rewards](passive-stake-delegation-and-rewards.md)
|
||||
|
@ -12,7 +12,7 @@ To run a blockstreamer, include the argument `no-signer` and (optional)
|
||||
`blockstream` socket location:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/fullnode-x.sh --no-signer --blockstream <SOCKET>
|
||||
$ ./multinode-demo/validator-x.sh --no-signer --blockstream <SOCKET>
|
||||
```
|
||||
|
||||
The stream will output a series of JSON objects:
|
||||
|
@ -20,7 +20,7 @@ least amount of internal plumbing exposed to the test.
|
||||
Tests are provided an entry point, which is a `contact_info::ContactInfo`
|
||||
structure, and a keypair that has already been funded.
|
||||
|
||||
Each node in the cluster is configured with a `fullnode::FullnodeConfig` at boot
|
||||
Each node in the cluster is configured with a `fullnode::ValidatorConfig` at boot
|
||||
time. At boot time this configuration specifies any extra cluster configuration
|
||||
required for the test. The cluster should boot with the configuration when it
|
||||
is run in-process or in a data center.
|
||||
@ -61,18 +61,18 @@ let cluster_nodes = discover_nodes(&entry_point_info, num_nodes);
|
||||
|
||||
To enable specific scenarios, the cluster needs to be booted with special
|
||||
configurations. These configurations can be captured in
|
||||
`fullnode::FullnodeConfig`.
|
||||
`fullnode::ValidatorConfig`.
|
||||
|
||||
For example:
|
||||
|
||||
```rust,ignore
|
||||
let mut fullnode_config = FullnodeConfig::default();
|
||||
fullnode_config.rpc_config.enable_fullnode_exit = true;
|
||||
let mut validator_config = ValidatorConfig::default();
|
||||
validator_config.rpc_config.enable_fullnode_exit = true;
|
||||
let local = LocalCluster::new_with_config(
|
||||
num_nodes,
|
||||
10_000,
|
||||
100,
|
||||
&fullnode_config
|
||||
&validator_config
|
||||
);
|
||||
```
|
||||
|
||||
@ -86,9 +86,9 @@ 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;
|
||||
let mut validator_config = ValidatorConfig::default();
|
||||
validator_config.rpc_config.enable_rpc_gossip_push = true;
|
||||
validator_config.rpc_config.enable_rpc_gossip_refresh_active_set = true;
|
||||
```
|
||||
|
||||
Wire the RPCs and write a new test:
|
||||
|
@ -28,7 +28,7 @@ its copy.
|
||||
|
||||
## Joining a Cluster
|
||||
|
||||
Fullnodes and replicators enter the cluster via registration messages sent to
|
||||
Validators and replicators enter the cluster via registration messages sent to
|
||||
its *control plane*. The control plane is implemented using a *gossip*
|
||||
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
|
||||
|
@ -6,15 +6,14 @@ In order to establish the fanout, the cluster divides itself into small
|
||||
collections of nodes, called *neighborhoods*. Each node is responsible for
|
||||
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.
|
||||
This way each node only has to communicate with a small number of nodes.
|
||||
|
||||
During its slot, the leader node distributes blobs between the validator nodes
|
||||
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"/>
|
||||
in the first neighborhood (layer 0). Each validator shares its data within its
|
||||
neighborhood, but also retransmits the blobs to one node in some neighborhoods
|
||||
in the next layer (layer 1). The layer-1 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.
|
||||
|
||||
## Neighborhood Assignment - Weighted Selection
|
||||
|
||||
@ -23,48 +22,50 @@ cluster is divided into neighborhoods. To achieve this, all the recognized
|
||||
validator nodes (the TVU peers) are sorted by stake and stored in a list. This
|
||||
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.
|
||||
make up layer 0. These will automatically be the highest stake holders, allowing
|
||||
the heaviest votes to come back to the leader first. Layer-0 and lower-layer
|
||||
nodes use the same logic to find their neighbors and next 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
|
||||
nodes. If this layer 0 is smaller than the number of nodes in the cluster, then
|
||||
the data plane fanout mechanism adds layers below. Subsequent layers follow
|
||||
these constraints to determine layer-capacity: Each neighborhood contains
|
||||
`NEIGHBORHOOD_SIZE` nodes and each layer may have up to `DATA_PLANE_FANOUT/2`
|
||||
neighborhoods.
|
||||
`DATA_PLANE_FANOUT` nodes. Layer-0 starts with 1 neighborhood with fanout nodes.
|
||||
The number of nodes in each additional layer grows by a factor of fanout.
|
||||
|
||||
As mentioned above, each node in a layer only has to broadcast its blobs to its
|
||||
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.
|
||||
neighbors and to exactly 1 node in some next-layer neighborhoods,
|
||||
instead of to every TVU peer in the cluster. A good way to think about this is,
|
||||
layer-0 starts with 1 neighborhood with fanout nodes, layer-1 adds "fanout"
|
||||
neighborhoods, each with fanout nodes and layer-2 will have
|
||||
`fanout * number of nodes in layer-1` and so on.
|
||||
|
||||
This way each node only has to communicate with a maximum of `2 * DATA_PLANE_FANOUT - 1` nodes.
|
||||
|
||||
The following diagram shows how the Leader sends blobs with a Fanout of 2 to
|
||||
Neighborhood 0 in Layer 0 and how the nodes in Neighborhood 0 share their data
|
||||
with each other.
|
||||
|
||||
<img alt="Leader sends blobs to Neighborhood 0 in Layer 0" src="img/data-plane-seeding.svg" class="center"/>
|
||||
|
||||
The following diagram shows how Neighborhood 0 fans out to Neighborhoods 1 and 2.
|
||||
|
||||
<img alt="Neighborhood 0 Fanout to Neighborhood 1 and 2" src="img/data-plane-fanout.svg" class="center"/>
|
||||
|
||||
Finally, the following diagram shows a two layer cluster with a Fanout of 2.
|
||||
|
||||
<img alt="Two layer cluster with a Fanout of 2" src="img/data-plane.svg" class="center"/>
|
||||
|
||||
#### Configuration Values
|
||||
|
||||
`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.
|
||||
`DATA_PLANE_FANOUT` - Determines the size of layer 0. Subsequent
|
||||
layers grow by a factor of `DATA_PLANE_FANOUT`.
|
||||
The number of nodes in a neighborhood is equal to the fanout value.
|
||||
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.
|
||||
@ -72,13 +73,10 @@ 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.
|
||||
To cripple a neighborhood, enough nodes (erasure codes +1) from the neighborhood
|
||||
above need to fail. Since each neighborhood receives blobs from multiple nodes
|
||||
in a neighborhood in the upper layer, 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"/>
|
||||
|
@ -47,8 +47,8 @@ nodes are started
|
||||
$ 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.
|
||||
The network is initialized with a genesis ledger generated by running the
|
||||
following script.
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/setup.sh
|
||||
@ -69,7 +69,7 @@ $ ./multinode-demo/drone.sh
|
||||
|
||||
### Singlenode Testnet
|
||||
|
||||
Before you start a fullnode, make sure you know the IP address of the machine you
|
||||
Before you start a validator, make sure you know the IP address of the machine you
|
||||
want to be the bootstrap leader for the demo, and make sure that udp ports 8000-10000 are
|
||||
open on all the machines you want to test with.
|
||||
|
||||
@ -86,10 +86,10 @@ The drone does not need to be running for subsequent leader starts.
|
||||
### Multinode Testnet
|
||||
|
||||
To run a multinode testnet, after starting a leader node, spin up some
|
||||
additional full nodes in separate shells:
|
||||
additional validators in separate shells:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/fullnode-x.sh
|
||||
$ ./multinode-demo/validator-x.sh
|
||||
```
|
||||
|
||||
To run a performance-enhanced full node on Linux,
|
||||
@ -99,7 +99,7 @@ your system:
|
||||
```bash
|
||||
$ ./fetch-perf-libs.sh
|
||||
$ SOLANA_CUDA=1 ./multinode-demo/bootstrap-leader.sh
|
||||
$ SOLANA_CUDA=1 ./multinode-demo/fullnode-x.sh
|
||||
$ SOLANA_CUDA=1 ./multinode-demo/validator.sh
|
||||
```
|
||||
|
||||
### Testnet Client Demo
|
||||
@ -145,7 +145,7 @@ Generally we are using `debug` for infrequent debug messages, `trace` for potent
|
||||
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_:
|
||||
_solana-validator_:
|
||||
|
||||
```bash
|
||||
$ sudo gdb
|
||||
@ -161,7 +161,7 @@ This will dump all the threads stack traces into gdb.txt
|
||||
In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`.
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/client.sh --network testnet.solana.com:8001 --duration 60
|
||||
$ ./multinode-demo/client.sh --entrypoint testnet.solana.com:8001 --duration 60
|
||||
```
|
||||
|
||||
You can observe the effects of your client's transactions on our [dashboard](https://metrics.solana.com:3000/d/testnet/testnet-hud?orgId=2&from=now-30m&to=now&refresh=5s&var-testnet=testnet)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Gossip Service
|
||||
|
||||
The Gossip Service acts as a gateway to nodes in the control plane. Fullnodes
|
||||
The Gossip Service acts as a gateway to nodes in the control plane. Validators
|
||||
use the service to ensure information is available to all other nodes in a cluster.
|
||||
The service broadcasts information using a gossip protocol.
|
||||
|
||||
@ -116,8 +116,8 @@ Just like *pull message*, nodes are selected into the active set based on weight
|
||||
|
||||
## Notable differences from PlumTree
|
||||
|
||||
The active push protocol described here is based on (Plum
|
||||
Tree)[https://haslab.uminho.pt/jop/files/lpr07a.pdf]. The main differences are:
|
||||
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
|
||||
|
@ -58,7 +58,7 @@ $ solana-install deploy http://example.com/path/to/solana-release.tar.bz2 update
|
||||
$ solana-install init --pubkey 92DMonmBYXwEMHJ99c9ceRSpAmk9v6i3RdvDdXaVcrfj # <-- pubkey is obtained from whoever is deploying the updates
|
||||
$ export PATH=~/.local/share/solana-install/bin:$PATH
|
||||
$ solana-keygen ... # <-- runs the latest solana-keygen
|
||||
$ solana-install run solana-fullnode ... # <-- runs a fullnode, restarting it as necesary when an update is applied
|
||||
$ solana-install run solana-validator ... # <-- runs a validator, restarting it as necesary when an update is applied
|
||||
```
|
||||
|
||||
### On-chain Update Manifest
|
||||
|
@ -30,6 +30,7 @@ Methods
|
||||
* [getSlotLeader](#getslotleader)
|
||||
* [getNumBlocksSinceSignatureConfirmation](#getnumblockssincesignatureconfirmation)
|
||||
* [getTransactionCount](#gettransactioncount)
|
||||
* [getEpochVoteAccounts](#getepochvoteaccounts)
|
||||
* [requestAirdrop](#requestairdrop)
|
||||
* [sendTransaction](#sendtransaction)
|
||||
* [startSubscriptionChannel](#startsubscriptionchannel)
|
||||
@ -167,13 +168,16 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "
|
||||
---
|
||||
|
||||
### getRecentBlockhash
|
||||
Returns a recent block hash from the ledger
|
||||
Returns a recent block hash from the ledger, and a fee schedule that can be used
|
||||
to compute the cost of submitting a transaction using it.
|
||||
|
||||
##### Parameters:
|
||||
None
|
||||
|
||||
##### Results:
|
||||
An array consisting of
|
||||
* `string` - a Hash as base-58 encoded string
|
||||
* `FeeCalculator object` - the fee schedule for this block hash
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
@ -181,7 +185,7 @@ None
|
||||
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}
|
||||
{"jsonrpc":"2.0","result":["GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC",{"lamportsPerSignature": 0}],"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
@ -271,6 +275,39 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m
|
||||
|
||||
---
|
||||
|
||||
### getEpochVoteAccounts
|
||||
Returns the account info and associated stake for all the voting accounts in the current epoch.
|
||||
|
||||
##### Parameters:
|
||||
None
|
||||
|
||||
##### Results:
|
||||
An array consisting of vote accounts:
|
||||
* `string` - the vote account's Pubkey as base-58 encoded string
|
||||
* `integer` - the stake, in lamports, delegated to this vote account
|
||||
* `VoteState` - the vote account's state
|
||||
|
||||
Each VoteState will be a JSON object with the following sub fields:
|
||||
|
||||
* `votes`, array of most recent vote lockouts
|
||||
* `node_pubkey`, the pubkey of the node that votes using this account
|
||||
* `authorized_voter_pubkey`, the pubkey of the authorized vote signer for this account
|
||||
* `commission`, a 32-bit integer used as a fraction (commission/MAX_U32) for rewards payout
|
||||
* `root_slot`, the most recent slot this account has achieved maximum lockout
|
||||
* `credits`, credits accrued by this account for reaching lockouts
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getEpochVoteAccounts"}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":[[[84,115,89,23,41,83,221,72,58,23,53,245,195,188,140,161,242,189,200,164,139,214,12,180,84,161,28,151,24,243,159,125],10000000,{"authorized_voter_pubkey":[84,115,89,23,41,83,221,72,58,23,53,245,195,188,140,161,242,189,200,164,139,214,12,180,84,161,28,151,24,243,159,125],"commission":0,"credits":0,"node_pubkey":[49,139,227,211,47,39,69,86,131,244,160,144,228,169,84,143,142,253,83,81,212,110,254,12,242,71,219,135,30,60,157,213],"root_slot":null,"votes":[{"confirmation_count":1,"slot":0}]}]],"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### requestAirdrop
|
||||
Requests an airdrop of lamports to a Pubkey
|
||||
|
||||
@ -316,6 +353,14 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m
|
||||
After connect to the RPC PubSub websocket at `ws://<ADDRESS>/`:
|
||||
- Submit subscription requests to the websocket using the methods below
|
||||
- Multiple subscriptions may be active at once
|
||||
- All subscriptions take an optional `confirmations` parameter, which defines
|
||||
how many confirmed blocks the node should wait before sending a notification.
|
||||
The greater the number, the more likely the notification is to represent
|
||||
consensus across the cluster, and the less likely it is to be affected by
|
||||
forking or rollbacks. If unspecified, the default value is 0; the node will
|
||||
send a notification as soon as it witnesses the event. The maximum
|
||||
`confirmations` wait length is the cluster's `MAX_LOCKOUT_HISTORY`, which
|
||||
represents the economic finality of the chain.
|
||||
|
||||
---
|
||||
|
||||
@ -325,6 +370,8 @@ for a given account public key changes
|
||||
|
||||
##### Parameters:
|
||||
* `string` - account Pubkey, as base-58 encoded string
|
||||
* `integer` - optional, number of confirmed blocks to wait before notification.
|
||||
Default: 0, Max: `MAX_LOCKOUT_HISTORY` (greater integers rounded down)
|
||||
|
||||
##### Results:
|
||||
* `integer` - Subscription id (needed to unsubscribe)
|
||||
@ -334,6 +381,8 @@ for a given account public key changes
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12"]}
|
||||
|
||||
{"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12", 15]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||
```
|
||||
@ -371,6 +420,8 @@ for a given account owned by the program changes
|
||||
|
||||
##### Parameters:
|
||||
* `string` - program_id Pubkey, as base-58 encoded string
|
||||
* `integer` - optional, number of confirmed blocks to wait before notification.
|
||||
Default: 0, Max: `MAX_LOCKOUT_HISTORY` (greater integers rounded down)
|
||||
|
||||
##### Results:
|
||||
* `integer` - Subscription id (needed to unsubscribe)
|
||||
@ -380,6 +431,8 @@ for a given account owned by the program changes
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["9gZbPtbtHrs6hEWgd6MbVY9VPFtS5Z8xKtnYwA2NynHV"]}
|
||||
|
||||
{"jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":["9gZbPtbtHrs6hEWgd6MbVY9VPFtS5Z8xKtnYwA2NynHV", 15]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||
```
|
||||
@ -419,6 +472,8 @@ On `signatureNotification`, the subscription is automatically cancelled
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Transaction Signature, as base-58 encoded string
|
||||
* `integer` - optional, number of confirmed blocks to wait before notification.
|
||||
Default: 0, Max: `MAX_LOCKOUT_HISTORY` (greater integers rounded down)
|
||||
|
||||
##### Results:
|
||||
* `integer` - subscription id (needed to unsubscribe)
|
||||
@ -428,6 +483,8 @@ On `signatureNotification`, the subscription is automatically cancelled
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"signatureSubscribe", "params":["2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b"]}
|
||||
|
||||
{"jsonrpc":"2.0", "id":1, "method":"signatureSubscribe", "params":["2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b", 15]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||
```
|
||||
|
@ -57,7 +57,7 @@ Forwarding is preferred, as it would minimize network congestion, allowing the
|
||||
cluster to advertise higher TPS capacity.
|
||||
|
||||
|
||||
## Fullnode Loop
|
||||
## Validator 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
|
||||
|
@ -2,6 +2,12 @@
|
||||
|
||||
Replication behavior yet to be implemented.
|
||||
|
||||
### Storage epoch
|
||||
|
||||
The storage epoch should be the number of slots which results in around 100GB-1TB of
|
||||
ledger to be generated for replicators to store. Replicators will start storing ledger
|
||||
when a given fork has a high probability of not being rolled back.
|
||||
|
||||
### Validator behavior
|
||||
|
||||
3. Every NUM\_KEY\_ROTATION\_TICKS it also validates samples received from
|
||||
@ -37,3 +43,100 @@ transacation proves the validator incorrectly validated a fake storage proof.
|
||||
The replicator is rewarded and the validator's staking balance is slashed or
|
||||
frozen.
|
||||
|
||||
### Storage proof contract logic
|
||||
|
||||
Each replicator and validator will have their own storage account. The validator's
|
||||
account would be separate from their gossip id similiar to their vote account.
|
||||
These should be implemented as two programs one which handles the validator as the keysigner
|
||||
and one for the replicator. In that way when the programs reference other accounts, they
|
||||
can check the program id to ensure it is a validator or replicator account they are
|
||||
referencing.
|
||||
|
||||
#### SubmitMiningProof
|
||||
```rust,ignore
|
||||
SubmitMiningProof {
|
||||
slot: u64,
|
||||
sha_state: Hash,
|
||||
signature: Signature,
|
||||
};
|
||||
keys = [replicator_keypair]
|
||||
```
|
||||
Replicators create these after mining their stored ledger data for a certain hash value.
|
||||
The slot is the end slot of the segment of ledger they are storing, the sha\_state
|
||||
the result of the replicator using the hash function to sample their encrypted ledger segment.
|
||||
The signature is the signature that was created when they signed a PoH value for the
|
||||
current storage epoch. The list of proofs from the current storage epoch should be saved
|
||||
in the account state, and then transfered to a list of proofs for the previous epoch when
|
||||
the epoch passes. In a given storage epoch a given replicator should only submit proofs
|
||||
for one segment.
|
||||
|
||||
The program should have a list of slots which are valid storage mining slots.
|
||||
This list should be maintained by keeping track of slots which are rooted slots in which a significant
|
||||
portion of the network has voted on with a high lockout value, maybe 32-votes old. Every SLOTS\_PER\_SEGMENT
|
||||
number of slots would be added to this set. The program should check that the slot is in this set. The set can
|
||||
be maintained by receiving a AdvertiseStorageRecentBlockHash and checking with its bank/locktower state.
|
||||
|
||||
The program should do a signature verify check on the signature, public key from the transaction submitter and the message of
|
||||
the previous storage epoch PoH value.
|
||||
|
||||
#### ProofValidation
|
||||
```rust,ignore
|
||||
ProofValidation {
|
||||
proof_mask: Vec<ProofStatus>,
|
||||
}
|
||||
keys = [validator_keypair, replicator_keypair(s) (unsigned)]
|
||||
```
|
||||
A validator will submit this transaction to indicate that a set of proofs for a given
|
||||
segment are valid/not-valid or skipped where the validator did not look at it. The
|
||||
keypairs for the replicators that it looked at should be referenced in the keys so the program
|
||||
logic can go to those accounts and see that the proofs are generated in the previous epoch. The
|
||||
sampling of the storage proofs should be verified ensuring that the correct proofs are skipped by
|
||||
the validator according to the logic outlined in the validator behavior of sampling.
|
||||
|
||||
The included replicator keys will indicate the the storage samples which are being referenced; the
|
||||
length of the proof\_mask should be verified against the set of storage proofs in the referenced
|
||||
replicator account(s), and should match with the number of proofs submitted in the previous storage
|
||||
epoch in the state of said replicator account.
|
||||
|
||||
#### ClaimStorageReward
|
||||
```rust,ignore
|
||||
ClaimStorageReward {
|
||||
}
|
||||
keys = [validator_keypair or replicator_keypair, validator/replicator_keypairs (unsigned)]
|
||||
```
|
||||
Replicators and validators will use this transaction to get paid tokens from a program state
|
||||
where SubmitStorageProof, ProofValidation and ChallengeProofValidations are in a state where
|
||||
proofs have been submitted and validated and there are no ChallengeProofValidations referencing
|
||||
those proofs. For a validator, it should reference the replicator keypairs to which it has validated
|
||||
proofs in the relevant epoch. And for a replicator it should reference validator keypairs for which it
|
||||
has validated and wants to be rewarded.
|
||||
|
||||
#### ChallengeProofValidation
|
||||
```rust,ignore
|
||||
ChallengeProofValidation {
|
||||
proof_index: u64,
|
||||
hash_seed_value: Vec<u8>,
|
||||
}
|
||||
keys = [replicator_keypair, validator_keypair]
|
||||
```
|
||||
|
||||
This transaction is for catching lazy validators who are not doing the work to validate proofs.
|
||||
A replicator will submit this transaction when it sees a validator has approved a fake SubmitMiningProof
|
||||
transaction. Since the replicator is a light client not looking at the full chain, it will have to ask
|
||||
a validator or some set of validators for this information maybe via RPC call to obtain all ProofValidations for
|
||||
a certain segment in the previous storage epoch. The program will look in the validator account
|
||||
state see that a ProofValidation is submitted in the previous storage epoch and hash the hash\_seed\_value and
|
||||
see that the hash matches the SubmitMiningProof transaction and that the validator marked it as valid. If so,
|
||||
then it will save the challenge to the list of challenges that it has in its state.
|
||||
|
||||
#### AdvertiseStorageRecentBlockhash
|
||||
```rust,ignore
|
||||
AdvertiseStorageRecentBlockhash {
|
||||
hash: Hash,
|
||||
slot: u64,
|
||||
}
|
||||
```
|
||||
|
||||
Validators and replicators will submit this to indicate that a new storage epoch has passed and that the
|
||||
storage proofs which are current proofs should now be for the previous epoch. Other transactions should
|
||||
check to see that the epoch that they are referencing is accurate according to current chain state.
|
||||
|
@ -31,7 +31,7 @@ core. The total space required for verification is `1_ledger_segment +
|
||||
Validators for PoRep are the same validators that are verifying transactions.
|
||||
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.
|
||||
then the validator will not receive a reward for that storage epoch.
|
||||
|
||||
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
|
||||
@ -53,7 +53,7 @@ changes to determine what rate it can validate storage proofs.
|
||||
|
||||
### Constants
|
||||
|
||||
1. NUM\_STORAGE\_ENTRIES: Number of entries in a segment of ledger data. The
|
||||
1. SLOTS\_PER\_SEGMENT: Number of slots in a segment of ledger data. The
|
||||
unit of storage for a replicator.
|
||||
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
|
||||
@ -77,7 +77,7 @@ 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.
|
||||
value is and its slot.
|
||||
|
||||
### Replicator behavior
|
||||
|
||||
@ -95,10 +95,10 @@ is:
|
||||
- (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.
|
||||
along with its slot.
|
||||
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
|
||||
replicator mods the signature with the slot to get which segment to
|
||||
replicate.
|
||||
4. The replicator retrives the ledger by asking peer validators and
|
||||
replicators. See 6.5.
|
||||
@ -118,9 +118,9 @@ current leader and it is put onto the ledger.
|
||||
### Finding who has a given block of ledger
|
||||
|
||||
1. Validators monitor the transaction stream for storage mining proofs, and
|
||||
keep a mapping of ledger segments by entry\_height to public keys. When it sees
|
||||
keep a mapping of ledger segments by slot to public keys. When it sees
|
||||
a storage mining proof it updates this mapping and provides an RPC interface
|
||||
which takes an entry\_height and hands back a list of public keys. The client
|
||||
which takes a slot and hands back a list of public keys. The client
|
||||
then looks up in their cluster\_info table to see which network address that
|
||||
corresponds to and sends a repair request to retrieve the necessary blocks of
|
||||
ledger.
|
||||
|
@ -85,7 +85,7 @@ contains the following state information:
|
||||
|
||||
* Account::lamports - The staked lamports.
|
||||
|
||||
* `voter_id` - The pubkey of the VoteState instance the lamports are
|
||||
* `voter_pubkey` - The pubkey of the VoteState instance the lamports are
|
||||
delegated to.
|
||||
|
||||
* `credits_observed` - The total credits claimed over the lifetime of the
|
||||
@ -109,7 +109,7 @@ program.
|
||||
|
||||
* `account[0]` - RW - The StakeState::Delegate instance.
|
||||
`StakeState::Delegate::credits_observed` is initialized to `VoteState::credits`.
|
||||
`StakeState::Delegate::voter_id` is initialized to `account[1]`
|
||||
`StakeState::Delegate::voter_pubkey` is initialized to `account[1]`
|
||||
|
||||
* `account[1]` - R - The VoteState instance.
|
||||
|
||||
@ -127,7 +127,7 @@ reward.
|
||||
* `account[1]` - RW - The StakeState::Delegate instance that is redeeming votes
|
||||
credits.
|
||||
* `account[2]` - R - The VoteState instance, must be the same as
|
||||
`StakeState::voter_id`
|
||||
`StakeState::voter_pubkey`
|
||||
|
||||
Reward is payed out for the difference between `VoteState::credits` to
|
||||
`StakeState::Delgate.credits_observed`, and `credits_observed` is updated to
|
||||
@ -181,7 +181,7 @@ the VoteState program or submitting votes to the program.
|
||||
|
||||
The total stake allocated to a VoteState program can be calculated by the sum of
|
||||
all the StakeState programs that have the VoteState pubkey as the
|
||||
`StakeState::Delegate::voter_id`.
|
||||
`StakeState::Delegate::voter_pubkey`.
|
||||
|
||||
## Example Callflow
|
||||
|
||||
@ -194,12 +194,12 @@ nodes since stake is used as weight in the network control and data planes. One
|
||||
way to implement this would be for the StakeState to delegate to a pool of
|
||||
validators instead of a single one.
|
||||
|
||||
Instead of a single `vote_id` and `credits_observed` entry in the StakeState
|
||||
Instead of a single `vote_pubkey` and `credits_observed` entry in the StakeState
|
||||
program, the program can be initialized with a vector of tuples.
|
||||
|
||||
```rust,ignore
|
||||
Voter {
|
||||
voter_id: Pubkey,
|
||||
voter_pubkey: Pubkey,
|
||||
credits_observed: u64,
|
||||
weight: u8,
|
||||
}
|
||||
|
29
book/src/performance-metrics.md
Normal file
29
book/src/performance-metrics.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Performance Metrics
|
||||
|
||||
Solana cluster performance is measured as average number of transactions per second
|
||||
that the network can sustain (TPS). And, how long it takes for a transaction to be
|
||||
confirmed by super majority of the cluster (Confirmation Time).
|
||||
|
||||
Each cluster node maintains various counters that are incremented on certain events.
|
||||
These counters are periodically uploaded to a cloud based database. Solana's metrics
|
||||
dashboard fetches these counters, and computes the performance metrics and displays
|
||||
it on the dashboard.
|
||||
|
||||
## TPS
|
||||
|
||||
The leader node's banking stage maintains a count of transactions that it recorded.
|
||||
The dashboard displays the count averaged over 2 second period in the TPS time series
|
||||
graph. The dashboard also shows per second mean, maximum and total TPS as a running
|
||||
counter.
|
||||
|
||||
## Confirmation Time
|
||||
|
||||
Each validator node maintains a list of active ledger forks that are visible to the node.
|
||||
A fork is considered to be frozen when the node has received and processed all entries
|
||||
corresponding to the fork. A fork is considered to be confirmed when it receives cumulative
|
||||
super majority vote, and when one of its children forks is frozen.
|
||||
|
||||
The node assigns a timestamp to every new fork, and computes the time it took to confirm
|
||||
the fork. This time is reflected as validator confirmation time in performance metrics.
|
||||
The performance dashboard displays the average of each validator node's confirmation time
|
||||
as a time series graph.
|
51
book/src/repair-service.md
Normal file
51
book/src/repair-service.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Repair Service
|
||||
|
||||
The RepairService is in charge of retrieving missing blobs that failed to be delivered by primary communication protocols like Avalanche. It is in charge of managing the protocols described below in the `Repair Protocols` section below.
|
||||
|
||||
# Challenges:
|
||||
|
||||
1) Validators can fail to receive particular blobs due to network failures
|
||||
|
||||
2) Consider a scenario where blocktree contains the set of slots {1, 3, 5}. Then Blocktree receives blobs for some slot 7, where for each of the blobs b, b.parent == 6, so then the parent-child relation 6 -> 7 is stored in blocktree. However, there is no way to chain these slots to any of the existing banks in Blocktree, and thus the `Blob Repair` protocol will not repair these slots. If these slots happen to be part of the main chain, this will halt replay progress on this node.
|
||||
|
||||
3) Validators that find themselves behind the cluster by an entire epoch struggle/fail to catch up because they do not have a leader schedule for future epochs. If nodes were to blindly accept repair blobs in these future epochs, this exposes nodes to spam.
|
||||
|
||||
# Repair Protocols
|
||||
|
||||
The repair protocol makes best attempts to progress the forking structure of Blocktree.
|
||||
|
||||
The different protocol strategies to address the above challenges:
|
||||
|
||||
1. Blob Repair (Addresses Challenge #1):
|
||||
This is the most basic repair protocol, with the purpose of detecting and filling "holes" in the ledger. Blocktree tracks the latest root slot. RepairService will then periodically iterate every fork in blocktree starting from the root slot, sending repair requests to validators for any missing blobs. It will send at most some `N` repair reqeusts per iteration.
|
||||
|
||||
Note: Validators will only accept blobs within the current verifiable epoch (epoch the validator has a leader schedule for).
|
||||
|
||||
2. Preemptive Slot Repair (Addresses Challenge #2):
|
||||
The goal of this protocol is to discover the chaining relationship of "orphan" slots that do not currently chain to any known fork.
|
||||
|
||||
* Blocktree will track the set of "orphan" slots in a separate column family.
|
||||
|
||||
* RepairService will periodically make `RequestOrphan` requests for each of the orphans in blocktree.
|
||||
|
||||
`RequestOrphan(orphan)` request - `orphan` is the orphan slot that the requestor wants to know the parents of
|
||||
`RequestOrphan(orphan)` response - The highest blobs for each of the first `N` parents of the requested `orphan`
|
||||
|
||||
On receiving the responses `p`, where `p` is some blob in a parent slot, validators will:
|
||||
* Insert an empty `SlotMeta` in blocktree for `p.slot` if it doesn't already exist.
|
||||
* If `p.slot` does exist, update the parent of `p` based on `parents`
|
||||
|
||||
Note: that once these empty slots are added to blocktree, the `Blob Repair` protocol should attempt to fill those slots.
|
||||
|
||||
Note: Validators will only accept responses containing blobs within the current verifiable epoch (epoch the validator has a leader schedule for).
|
||||
|
||||
3. Repairmen (Addresses Challenge #3):
|
||||
This part of the repair protocol is the primary mechanism by which new nodes joining the cluster catch up after loading a snapshot. This protocol works in a "forward" fashion, so validators can verify every blob that they receive against a known leader schedule.
|
||||
|
||||
Each validator advertises in gossip:
|
||||
* Current root
|
||||
* The set of all completed slots in the confirmed epochs (an epoch that was calculated based on a bank <= current root) past the current root
|
||||
|
||||
Observers of this gossip message with higher epochs (repairmen) send blobs to catch the lagging node up with the rest of the cluster. The repairmen are responsible for sending the slots within the epochs that are confrimed by the advertised `root` in gossip. The repairmen divide the responsibility of sending each of the missing slots in these epochs based on a random seed (simple blob.index iteration by N, seeded with the repairman's node_pubkey). Ideally, each repairman in an N node cluster (N nodes whose epochs are higher than that of the repairee) sends 1/N of the missing blobs. Both data and coding blobs for missing slots are sent. Repairmen do not send blobs again to the same validator until they see the message in gossip updated, at which point they perform another iteration of this protocol.
|
||||
|
||||
Gossip messages are updated every time a validator receives a complete slot within the epoch. Completed slots are detected by blocktree and sent over a channel to RepairService. It is important to note that we know that by the time a slot X is complete, the epoch schedule must exist for the epoch that contains slot X because WindowService will reject blobs for unconfirmed epochs. When a newly completed slot is detected, we also update the current root if it has changed since the last update. The root is made available to RepairService through Blocktree, which holds the latest root.
|
@ -1,68 +1,195 @@
|
||||
# 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
|
||||
Stakers are rewarded for helping to validate the ledger. They do this by
|
||||
delegating their stake to validator nodes. Those validators do the legwork of
|
||||
replaying the ledger and send votes to a per-node vote account to which stakers
|
||||
can delegate their stakes. The rest of the cluster uses those stake-weighted
|
||||
votes to select a block when forks arise. Both the validator and staker need
|
||||
some economic incentive to play their part. The validator needs to be
|
||||
compensated for its hardware and the staker needs to be compensated for the risk
|
||||
of getting its stake slashed. The economics are covered in [staking
|
||||
rewards](staking-rewards.md). This chapter, on the other hand, describes the
|
||||
underlying mechanics of its implementation.
|
||||
|
||||
## Vote and Rewards accounts
|
||||
## Basic Besign
|
||||
|
||||
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 general idea is that the validator owns a Vote account. The Vote account
|
||||
tracks validator votes, counts validator generated credits, and provides any
|
||||
additional validator specific state. The Vote account is not aware of any
|
||||
stakes delegated to it and has no staking weight.
|
||||
|
||||
The Vote account contains the following state information:
|
||||
A separate Stake account (created by a staker) names a Vote account to which the
|
||||
stake is delegated. Rewards generated are proportional to the amount of
|
||||
lamports staked. The Stake account is owned by the staker only. Lamports
|
||||
stored in this account are the stake.
|
||||
|
||||
* votes - The submitted votes.
|
||||
## Passive Delegation
|
||||
|
||||
* `delegate_id` - An identity that may operate with the weight of this
|
||||
account's stake. It is typically the identity of a fullnode, but may be any
|
||||
identity involved in stake-weighted computations.
|
||||
Any number of Stake accounts can delegate to a single
|
||||
Vote account without an interactive action from the identity controlling
|
||||
the Vote account or submitting votes to the account.
|
||||
|
||||
* `authorized_voter_id` - Only this identity is authorized to submit votes.
|
||||
The total stake allocated to a Vote account can be calculated by the sum of
|
||||
all the Stake accounts that have the Vote account pubkey as the
|
||||
`StakeState::Delegate::voter_pubkey`.
|
||||
|
||||
* `credits` - The amount of unclaimed rewards.
|
||||
## Vote and Stake accounts
|
||||
|
||||
* `root_slot` - The last slot to reach the full lockout commitment necessary
|
||||
for rewards.
|
||||
The rewards process is split into two on-chain programs. The Vote program solves
|
||||
the problem of making stakes slashable. The Stake account acts as custodian of
|
||||
the rewards pool, and provides passive delegation. The Stake program is
|
||||
responsible for paying out each staker once the staker proves to the Stake
|
||||
program that its delegate has participated in validating the ledger.
|
||||
|
||||
The Rewards program is stateless and pays out reward when a staker submits its
|
||||
Vote account to the program. Claiming a reward requires a transaction that
|
||||
includes the following instructions:
|
||||
### VoteState
|
||||
|
||||
1. `RewardsInstruction::RedeemVoteCredits`
|
||||
2. `VoteInstruction::ClearCredits`
|
||||
VoteState is the current state of all the votes the validator has submitted to
|
||||
the network. VoteState contains the following state information:
|
||||
|
||||
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.
|
||||
* votes - The submitted votes data structure.
|
||||
|
||||
* credits - The total number of rewards this vote program has generated over its
|
||||
lifetime.
|
||||
|
||||
* root\_slot - The last slot to reach the full lockout commitment necessary for
|
||||
rewards.
|
||||
|
||||
* commission - The commission taken by this VoteState for any rewards claimed by
|
||||
staker's Stake accounts. This is the percentage ceiling of the reward.
|
||||
|
||||
* Account::lamports - The accumulated lamports from the commission. These do not
|
||||
count as stakes.
|
||||
|
||||
* `authorized_vote_signer` - Only this identity is authorized to submit votes. This field can only modified by this identity.
|
||||
|
||||
### VoteInstruction::Initialize
|
||||
|
||||
* `account[0]` - RW - The VoteState
|
||||
`VoteState::authorized_vote_signer` is initialized to `account[0]`
|
||||
other VoteState members defaulted
|
||||
|
||||
### VoteInstruction::AuthorizeVoteSigner(Pubkey)
|
||||
|
||||
* `account[0]` - RW - The VoteState
|
||||
`VoteState::authorized_vote_signer` is set to to `Pubkey`, instruction must by
|
||||
signed by Pubkey
|
||||
|
||||
### VoteInstruction::Vote(Vec<Vote>)
|
||||
|
||||
* `account[0]` - RW - The VoteState
|
||||
`VoteState::lockouts` and `VoteState::credits` are updated according to voting lockout rules see [Fork Selection](fork-selection.md)
|
||||
|
||||
|
||||
### Delegating Stake
|
||||
* `account[1]` - RO - A list of some N most recent slots and their hashes for the vote to be verified against.
|
||||
|
||||
`VoteInstruction::DelegateStake` allows the staker to choose a fullnode to
|
||||
validate the ledger on its behalf. By being a delegate, the fullnode is
|
||||
entitled to collect transaction fees when its is leader. The larger the stake,
|
||||
the more often the fullnode will be able to collect those fees.
|
||||
|
||||
### Authorizing a Vote Signer
|
||||
### StakeState
|
||||
|
||||
A StakeState takes one of two forms, StakeState::Delegate and StakeState::MiningPool.
|
||||
|
||||
### StakeState::Delegate
|
||||
|
||||
StakeState is the current delegation preference of the **staker**. StakeState
|
||||
contains the following state information:
|
||||
|
||||
* Account::lamports - The staked lamports.
|
||||
|
||||
* `voter_pubkey` - The pubkey of the VoteState instance the lamports are
|
||||
delegated to.
|
||||
|
||||
* `credits_observed` - The total credits claimed over the lifetime of the
|
||||
program.
|
||||
|
||||
### StakeState::MiningPool
|
||||
|
||||
There are two approaches to the mining pool. The bank could allow the
|
||||
StakeState program to bypass the token balance check, or a program representing
|
||||
the mining pool could run on the network. To avoid a single network wide lock,
|
||||
the pool can be split into several mining pools. This design focuses on using
|
||||
StakeState::MiningPool instances as the cluster wide mining pools.
|
||||
|
||||
* 256 StakeState::MiningPool are initialized, each with 1/256 number of mining pool
|
||||
tokens stored as `Account::lamports`.
|
||||
|
||||
The stakes and the MiningPool are accounts that are owned by the same `Stake`
|
||||
program.
|
||||
|
||||
### StakeInstruction::Initialize
|
||||
|
||||
* `account[0]` - RW - The StakeState::Delegate instance.
|
||||
`StakeState::Delegate::credits_observed` is initialized to `VoteState::credits`.
|
||||
`StakeState::Delegate::voter_pubkey` is initialized to `account[1]`
|
||||
|
||||
* `account[1]` - R - The VoteState instance.
|
||||
|
||||
### StakeInstruction::RedeemVoteCredits
|
||||
|
||||
The Staker or the owner of the Stake account sends a transaction with this
|
||||
instruction to claim rewards.
|
||||
|
||||
The Vote account and the Stake account pair maintain a lifetime counter
|
||||
of total rewards generated and claimed. When claiming rewards, the total lamports
|
||||
deposited into the Stake account and as validator commission is proportional to
|
||||
`VoteState::credits - StakeState::credits_observed`.
|
||||
|
||||
|
||||
* `account[0]` - RW - The StakeState::MiningPool instance that will fulfill the
|
||||
reward.
|
||||
* `account[1]` - RW - The StakeState::Delegate instance that is redeeming votes
|
||||
credits.
|
||||
* `account[2]` - R - The VoteState instance, must be the same as
|
||||
`StakeState::voter_pubkey`
|
||||
|
||||
Reward is paid out for the difference between `VoteState::credits` to
|
||||
`StakeState::Delgate.credits_observed`, and `credits_observed` is updated to
|
||||
`VoteState::credits`. The commission is deposited into the Vote account token
|
||||
balance, and the reward is deposited to the Stake account token balance.
|
||||
|
||||
The total lamports paid is a percentage-rate of the lamports staked muiltplied by
|
||||
the ratio of rewards being redeemed to rewards that could have been generated
|
||||
during the rate period.
|
||||
|
||||
Any random MiningPool can be used to redeem the credits.
|
||||
|
||||
```rust,ignore
|
||||
let credits_to_claim = vote_state.credits - stake_state.credits_observed;
|
||||
stake_state.credits_observed = vote_state.credits;
|
||||
```
|
||||
|
||||
`credits_to_claim` is used to compute the reward and commission, and
|
||||
`StakeState::Delegate::credits_observed` is updated to the latest
|
||||
`VoteState::credits` value.
|
||||
|
||||
## Collecting network fees into the MiningPool
|
||||
|
||||
At the end of the block, before the bank is frozen, but after it processed all
|
||||
the transactions for the block, a virtual instruction is executed to collect
|
||||
the transaction fees.
|
||||
|
||||
* A portion of the fees are deposited into the leader's account.
|
||||
* A portion of the fees are deposited into the smallest StakeState::MiningPool
|
||||
account.
|
||||
|
||||
## Authorizing a Vote Signer
|
||||
|
||||
`VoteInstruction::AuthorizeVoter` allows a staker to choose a signing service
|
||||
for its votes. That service is responsible for ensuring the vote won't cause
|
||||
the staker to be slashed.
|
||||
|
||||
## Limitations
|
||||
## Benefits of the design
|
||||
|
||||
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.
|
||||
* Single vote for all the stakers.
|
||||
|
||||
* Clearing of the credit variable is not necessary for claiming rewards.
|
||||
|
||||
* Each delegated stake can claim its rewards independently.
|
||||
|
||||
* Commission for the work is deposited when a reward is claimed by the delegated
|
||||
stake.
|
||||
|
||||
This proposal would benefit from the `read-only` accounts proposal to allow for
|
||||
many rewards to be claimed concurrently.
|
||||
|
||||
## Example Callflow
|
||||
|
||||
<img alt="Passive Staking Callflow" src="img/passive-staking-callflow.svg" class="center"/>
|
||||
|
@ -1,8 +1,8 @@
|
||||
# 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
|
||||
A Proof of Stake (PoS), (i.e. using in-protocol asset, SOL, to provide
|
||||
secure consensus) design is outlined here. Solana implements a proof of
|
||||
stake reward/security scheme for validator nodes in the cluster. The purpose is
|
||||
threefold:
|
||||
|
||||
- Align validator incentives with that of the greater cluster through
|
||||
|
@ -15,39 +15,43 @@ reasons:
|
||||
* The cluster rolled back the ledger
|
||||
* A validator responded to queries maliciously
|
||||
|
||||
### The Transact Trait
|
||||
### The AsyncClient and SyncClient Traits
|
||||
|
||||
To troubleshoot, the application should retarget a lower-level component, where
|
||||
fewer errors are possible. Retargeting can be done with different
|
||||
implementations of the Transact trait.
|
||||
implementations of the AsyncClient and SyncClient traits.
|
||||
|
||||
When Futures 0.3.0 is released, the Transact trait may look like this:
|
||||
Components implement the following primary methods:
|
||||
|
||||
```rust,ignore
|
||||
trait Transact {
|
||||
async fn send_transactions(txs: &[Transaction]) -> Vec<Result<(), TransactionError>>;
|
||||
trait AsyncClient {
|
||||
fn async_send_transaction(&self, transaction: Transaction) -> io::Result<Signature>;
|
||||
}
|
||||
|
||||
trait SyncClient {
|
||||
fn get_signature_status(&self, signature: &Signature) -> Result<Option<transaction::Result<()>>>;
|
||||
}
|
||||
```
|
||||
|
||||
Users send transactions and asynchrounously await their results.
|
||||
Users send transactions and asynchrounously and synchrounously await results.
|
||||
|
||||
#### Transact with Clusters
|
||||
#### ThinClient for Clusters
|
||||
|
||||
The highest level implementation targets a Solana cluster, which may be a
|
||||
deployed testnet or a local cluster running on a development machine.
|
||||
The highest level implementation, ThinClient, targets a Solana cluster, which
|
||||
may be a deployed testnet or a local cluster running on a development machine.
|
||||
|
||||
#### Transact with the TPU
|
||||
#### TpuClient for the TPU
|
||||
|
||||
The next level is the TPU implementation of Transact. At the TPU level, the
|
||||
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
|
||||
The next level is the TPU implementation, which is not yet implemented. 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
|
||||
#### BankClient for 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
|
||||
|
@ -5,7 +5,7 @@ validator node.
|
||||
Please note some of the information and instructions described here may change
|
||||
in future releases.
|
||||
|
||||
### Beta Testnet Overview
|
||||
### Overview
|
||||
The testnet features a validator running at testnet.solana.com, which
|
||||
serves as the entrypoint to the cluster for your validator.
|
||||
|
||||
@ -16,7 +16,10 @@ The testnet is configured to reset the ledger daily, or sooner
|
||||
should the hourly automated cluster sanity test fail.
|
||||
|
||||
There is a **#validator-support** Discord channel available to reach other
|
||||
testnet participants, https://discord.gg/pquxPsq.
|
||||
testnet participants, [https://discord.gg/pquxPsq](https://discord.gg/pquxPsq).
|
||||
|
||||
Also we'd love it if you choose to register your validator node with us at
|
||||
[https://forms.gle/LfFscZqJELbuUP139](https://forms.gle/LfFscZqJELbuUP139).
|
||||
|
||||
### Machine Requirements
|
||||
Since the testnet is not intended for stress testing of max transaction
|
||||
@ -29,6 +32,16 @@ traversal issues. A cloud-hosted machine works best. **Ensure that IP ports
|
||||
Prebuilt binaries are available for Linux x86_64 (Ubuntu 18.04 recommended).
|
||||
MacOS or WSL users may build from source.
|
||||
|
||||
For a performance testnet with many transactions we have some preliminary recomended setups:
|
||||
|
||||
| | Low end | Medium end | High end | Notes |
|
||||
| --- | ---------|------------|----------| -- |
|
||||
| CPU | AMD Threadripper 1900x | AMD Threadripper 2920x | AMD Threadripper 2950x | Consider a 10Gb-capable motherboard with as many PCIe lanes and m.2 slots as possible. |
|
||||
| RAM | 16GB | 32GB | 64GB | |
|
||||
| OS Drive | Samsung 860 Evo 2TB | Samsung 860 Evo 4TB | Samsung 860 Evo 4TB | Or equivalent SSD |
|
||||
| Accounts Drive(s) | None | Samsung 970 Pro 1TB | 2x Samsung 970 Pro 1TB | |
|
||||
| GPU | 4x Nvidia 1070 or 2x Nvidia 1080 Ti or 2x Nvidia 2070 | 2x Nvidia 2080 Ti | 4x Nvidia 2080 Ti | Any number of cuda-capable GPUs are supported on Linux platforms. |
|
||||
|
||||
#### Confirm The Testnet Is Reachable
|
||||
Before attaching a validator node, sanity check that the cluster is accessible
|
||||
to your machine by running some simple commands. If any of the commands fail,
|
||||
@ -54,7 +67,7 @@ The `solana-install` tool can be used to easily install and upgrade the cluster
|
||||
software on Linux x86_64 systems.
|
||||
|
||||
```bash
|
||||
$ export SOLANA_RELEASE=v0.14.0 # skip this line to install the latest release
|
||||
$ export SOLANA_RELEASE=v0.14.2 # skip this line to install the latest release
|
||||
$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.14.0/install/solana-install-init.sh | sh -s
|
||||
```
|
||||
|
||||
@ -94,78 +107,90 @@ $ export PATH=$PWD/bin:$PATH
|
||||
Sanity check that you are able to interact with the cluster by receiving a small
|
||||
airdrop of lamports from the testnet drone:
|
||||
```bash
|
||||
$ solana-wallet -n testnet.solana.com airdrop 123
|
||||
$ solana-wallet -n testnet.solana.com balance
|
||||
$ solana-wallet airdrop 123
|
||||
$ solana-wallet balance
|
||||
```
|
||||
|
||||
Also try running following command to join the gossip network and view all the other nodes in the cluster:
|
||||
```bash
|
||||
$ solana-gossip --network testnet.solana.com:8001 spy
|
||||
$ solana-gossip --entrypoint testnet.solana.com:8001 spy
|
||||
# Press ^C to exit
|
||||
```
|
||||
|
||||
Then the following command will start a new validator node.
|
||||
Now configure a key pair for your validator by running:
|
||||
```bash
|
||||
$ solana-keygen -o validator-keypair.json
|
||||
```
|
||||
|
||||
Then use one of the following commands, depending on your installation
|
||||
choice, to start the node:
|
||||
|
||||
If this is a `solana-install`-installation:
|
||||
```bash
|
||||
$ clear-fullnode-config.sh
|
||||
$ fullnode.sh --public-address --poll-for-new-genesis-block testnet.solana.com
|
||||
$ clear-config.sh
|
||||
$ validator.sh --identity validator-keypair.json --poll-for-new-genesis-block testnet.solana.com
|
||||
```
|
||||
|
||||
Alternatively, the `solana-install run` command can be used to run the validator
|
||||
node while periodically checking for and applying software updates:
|
||||
```bash
|
||||
$ clear-fullnode-config.sh
|
||||
$ solana-install run fullnode.sh -- --public-address --poll-for-new-genesis-block testnet.solana.com
|
||||
$ clear-config.sh
|
||||
$ solana-install run validator.sh -- --identity validator-keypair.json --poll-for-new-genesis-block testnet.solana.com
|
||||
```
|
||||
|
||||
If you built from source:
|
||||
```bash
|
||||
$ USE_INSTALL=1 ./multinode-demo/clear-fullnode-config.sh
|
||||
$ USE_INSTALL=1 ./multinode-demo/fullnode.sh --public-address --poll-for-new-genesis-block testnet.solana.com
|
||||
$ USE_INSTALL=1 ./multinode-demo/clear-config.sh
|
||||
$ USE_INSTALL=1 ./multinode-demo/validator.sh --identity validator-keypair.json --poll-for-new-genesis-block testnet.solana.com
|
||||
```
|
||||
|
||||
#### Controlling local network port allocation
|
||||
By default the validator will dynamically select available network ports in the
|
||||
8000-10000 range, and may be overridden with `--dynamic-port-range`. For
|
||||
example, `fullnode.sh --dynamic-port-range 11000-11010 ...` will restrict the
|
||||
example, `validator.sh --dynamic-port-range 11000-11010 ...` will restrict the
|
||||
validator to ports 11000-11011.
|
||||
|
||||
### Validator Monitoring
|
||||
From another console, confirm the IP address of your validator is visible in the
|
||||
gossip network by running:
|
||||
```bash
|
||||
$ solana-gossip --network edge.testnet.solana.com:8001 spy
|
||||
```
|
||||
|
||||
When `fullnode.sh` starts, it will output a fullnode configuration that looks
|
||||
When `validator.sh` starts, it will output a validator configuration that looks
|
||||
similar to:
|
||||
```bash
|
||||
======================[ Fullnode configuration ]======================
|
||||
node id: 4ceWXsL3UJvn7NYZiRkw7NsryMpviaKBDYr8GK7J61Dm
|
||||
vote id: 2ozWvfaXQd1X6uKh8jERoRGApDqSqcEy6fF1oN13LL2G
|
||||
======================[ validator configuration ]======================
|
||||
identity pubkey: 4ceWXsL3UJvn7NYZiRkw7NsryMpviaKBDYr8GK7J61Dm
|
||||
vote pubkey: 2ozWvfaXQd1X6uKh8jERoRGApDqSqcEy6fF1oN13LL2G
|
||||
ledger: ...
|
||||
accounts: ...
|
||||
======================================================================
|
||||
```
|
||||
|
||||
Provide the **vote id** pubkey to the `solana-wallet show-vote-account` command to view
|
||||
The **identity pubkey** for your validator can also be found by running:
|
||||
```bash
|
||||
$ solana-keygen pubkey validator-keypair.json
|
||||
```
|
||||
|
||||
From another console, confirm the IP address and **identity pubkey** of your validator is visible in the
|
||||
gossip network by running:
|
||||
```bash
|
||||
$ solana-gossip --entrypoint testnet.solana.com:8001 spy
|
||||
```
|
||||
|
||||
Provide the **vote pubkey** to the `solana-wallet show-vote-account` command to view
|
||||
the recent voting activity from your validator:
|
||||
```bash
|
||||
$ solana-wallet -n testnet.solana.com show-vote-account 2ozWvfaXQd1X6uKh8jERoRGApDqSqcEy6fF1oN13LL2G
|
||||
```
|
||||
|
||||
The vote id for the validator can also be found by running:
|
||||
The vote pubkey for the validator can also be found by running:
|
||||
```bash
|
||||
# If this is a `solana-install`-installation run:
|
||||
$ solana-keygen pubkey ~/.local/share/solana/install/active_release/config-local/fullnode-vote-id.json
|
||||
$ solana-keygen pubkey ~/.local/share/solana/install/active_release/config-local/validator-vote-keypair.json
|
||||
# Otherwise run:
|
||||
$ solana-keygen pubkey ./config-local/fullnode-vote-id.json
|
||||
$ solana-keygen pubkey ./config-local/validator-vote-keypair.json
|
||||
```
|
||||
|
||||
### Sharing Metrics From Your Validator
|
||||
If you'd like to share metrics perform the following steps before starting the
|
||||
validator node:
|
||||
If you have obtained a metrics username/password from the Solana maintainers to
|
||||
help us monitor the health of the testnet, please perform the following steps
|
||||
before starting the validator to activate metrics reporting:
|
||||
```bash
|
||||
export u="username obtained from the Solana maintainers"
|
||||
export p="password obtained from the Solana maintainers"
|
||||
|
56
book/src/validator-proposal.md
Normal file
56
book/src/validator-proposal.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Anatomy of a Validator
|
||||
|
||||
## History
|
||||
|
||||
When we first started Solana, the goal was to de-risk our TPS claims. We knew
|
||||
that between optimistic concurrency control and sufficiently long leader slots,
|
||||
that PoS consensus was not the biggest risk to TPS. It was GPU-based signature
|
||||
verification, software pipelining and concurrent banking. Thus, the TPU was
|
||||
born. After topping 100k TPS, we split the team into one group working toward
|
||||
710k TPS and another to flesh out the validator pipeline. Hence, the TVU was
|
||||
born. The current architecture is a consequence of incremental development with
|
||||
that ordering and project priorities. It is not a reflection of what we ever
|
||||
believed was the most technically elegant cross-section of those technologies.
|
||||
In the context of leader rotation, the strong distinction between leading and
|
||||
validating is blurred.
|
||||
|
||||
## Difference between validating and leading
|
||||
|
||||
The fundamental difference between the pipelines is when the PoH is present. In
|
||||
a leader, we process transactions, removing bad ones, and then tag the result
|
||||
with a PoH hash. In the validator, we verify that hash, peel it off, and
|
||||
process the transactions in exactly the same way. The only difference is that
|
||||
if a validator sees a bad transaction, it can't simply remove it like the
|
||||
leader does, because that would cause the PoH hash to change. Instead, it
|
||||
rejects the whole block. The other difference between the pipelines is what
|
||||
happens *after* banking. The leader broadcasts entries to downstream validators
|
||||
whereas the validator will have already done that in RetransmitStage, which is
|
||||
a confirmation time optimization. The validation pipeline, on the other hand,
|
||||
has one last step. Any time it finishes processing a block, it needs to weigh
|
||||
any forks it's observing, possibly cast a vote, and if so, reset its PoH hash
|
||||
to the block hash it just voted on.
|
||||
|
||||
## Proposed Design
|
||||
|
||||
We unwrap the many abstraction layers and build a single pipeline that can
|
||||
toggle leader mode on whenever the validator's ID shows up in the leader
|
||||
schedule.
|
||||
|
||||
<img alt="Validator block diagram" src="img/validator-proposal.svg" class="center"/>
|
||||
|
||||
## Notable changes
|
||||
|
||||
* No threads are shut down to switch out of leader mode. Instead, FetchStage
|
||||
should forward transactions to the next leader.
|
||||
* Hoist FetchStage and BroadcastStage out of TPU
|
||||
* Blocktree renamed to Blockstore
|
||||
* BankForks renamed to Banktree
|
||||
* TPU moves to new socket-free crate called solana-tpu.
|
||||
* TPU's BankingStage absorbs ReplayStage
|
||||
* TVU goes away
|
||||
* New RepairStage absorbs Blob Fetch Stage and repair requests
|
||||
* JSON RPC Service is optional - used for debugging. It should instead be part
|
||||
of a separate `solana-blockstreamer` executable.
|
||||
* New MulticastStage absorbs retransmit part of RetransmitStage
|
||||
* MulticastStage downstream of Blockstore
|
||||
|
@ -1,10 +1,10 @@
|
||||
# Anatomy of a Fullnode
|
||||
# Anatomy of a Validator
|
||||
|
||||
<img alt="Fullnode block diagrams" src="img/fullnode.svg" class="center"/>
|
||||
<img alt="Validator block diagrams" src="img/validator.svg" class="center"/>
|
||||
|
||||
## Pipelining
|
||||
|
||||
The fullnodes make extensive use of an optimization common in CPU design,
|
||||
The validators make extensive use of an optimization common in CPU design,
|
||||
called *pipelining*. Pipelining is the right tool for the job when there's a
|
||||
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
|
||||
@ -19,9 +19,9 @@ dryer and the first is being folded. In this way, one can make progress on
|
||||
three loads of laundry simultaneously. Given infinite loads, the pipeline will
|
||||
consistently complete a load at the rate of the slowest stage in the pipeline.
|
||||
|
||||
## Pipelining in the Fullnode
|
||||
## Pipelining in the Validator
|
||||
|
||||
The fullnode contains two pipelined processes, one used in leader mode called
|
||||
The validator contains two pipelined processes, one used in leader mode called
|
||||
the TPU and one used in validator mode called the TVU. In both cases, the
|
||||
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
|
@ -12,8 +12,12 @@ if [[ -d target/perf-libs ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
set -x
|
||||
git clone git@github.com:solana-labs/solana-perf-libs.git target/perf-libs
|
||||
cd target/perf-libs
|
||||
make -j"$(nproc)"
|
||||
make DESTDIR=. install
|
||||
)
|
||||
|
||||
./fetch-perf-libs.sh
|
||||
|
20
ci/audit.sh
20
ci/audit.sh
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Audits project dependencies for security vulnerabilities
|
||||
#
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
source ci/_
|
||||
|
||||
cargo_install_unless() {
|
||||
declare crate=$1
|
||||
shift
|
||||
|
||||
"$@" > /dev/null 2>&1 || \
|
||||
_ cargo install "$crate"
|
||||
}
|
||||
|
||||
cargo_install_unless cargo-audit cargo audit --version
|
||||
|
||||
_ cargo audit
|
@ -14,10 +14,10 @@ steps:
|
||||
- "queue=cuda"
|
||||
- command: "ci/test-bench.sh"
|
||||
name: "bench"
|
||||
timeout_in_minutes: 20
|
||||
timeout_in_minutes: 60
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-stable.sh"
|
||||
name: "stable"
|
||||
timeout_in_minutes: 30
|
||||
timeout_in_minutes: 40
|
||||
artifact_paths: "log-*.txt"
|
||||
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_nightly_docker_image ci/test-coverage.sh"
|
||||
name: "coverage"
|
||||
|
@ -4,9 +4,8 @@ ARG date
|
||||
RUN set -x \
|
||||
&& rustup install nightly-$date \
|
||||
&& rustup show \
|
||||
&& rustup show \
|
||||
&& rustc --version \
|
||||
&& cargo --version \
|
||||
&& cargo install grcov \
|
||||
&& rustc +nightly-$date --version \
|
||||
&& cargo +nightly-$date --version
|
||||
|
||||
|
@ -21,9 +21,11 @@ RUN set -x \
|
||||
rsync \
|
||||
sudo \
|
||||
\
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rustup component add rustfmt \
|
||||
&& rustup component add clippy \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& cargo install cargo-audit \
|
||||
&& cargo install svgbob_cli \
|
||||
&& cargo install mdbook \
|
||||
&& rustc --version \
|
||||
&& cargo --version
|
||||
|
||||
|
@ -9,3 +9,4 @@ read -r rustc version _ < <(docker run solanalabs/rust rustc --version)
|
||||
[[ $rustc = rustc ]]
|
||||
docker tag solanalabs/rust:latest solanalabs/rust:"$version"
|
||||
docker push solanalabs/rust:"$version"
|
||||
docker push solanalabs/rust:latest
|
||||
|
@ -25,8 +25,7 @@ fi
|
||||
build() {
|
||||
$genPipeline && return
|
||||
source ci/rust-version.sh stable
|
||||
|
||||
_ scripts/ulimit-n.sh
|
||||
source scripts/ulimit-n.sh
|
||||
_ cargo +$rust_stable build --all
|
||||
}
|
||||
|
||||
@ -52,11 +51,11 @@ runTest() {
|
||||
|
||||
build
|
||||
|
||||
runTest "Leader rotation on" \
|
||||
runTest "basic" \
|
||||
"ci/localnet-sanity.sh -i 128"
|
||||
|
||||
runTest "Leader rotation on, restart" \
|
||||
runTest "restart" \
|
||||
"ci/localnet-sanity.sh -i 128 -k 16"
|
||||
|
||||
runTest "Leader rotation on, incremental restart, extra node" \
|
||||
runTest "incremental restart, extra node" \
|
||||
"ci/localnet-sanity.sh -i 128 -k 16 -R -x"
|
||||
|
@ -7,7 +7,7 @@ restartInterval=never
|
||||
rollingRestart=false
|
||||
maybeNoLeaderRotation=
|
||||
extraNodes=0
|
||||
walletRpcEndpoint=
|
||||
walletRpcPort=:8899
|
||||
|
||||
usage() {
|
||||
exitcode=0
|
||||
@ -27,7 +27,7 @@ Start a local cluster and run sanity on it
|
||||
nodes (at the cadence specified by -k). When disabled all
|
||||
nodes will be first killed then restarted (default: $rollingRestart)
|
||||
-b - Disable leader rotation
|
||||
-x - Add an extra fullnode (may be supplied multiple times)
|
||||
-x - Add an extra validator (may be supplied multiple times)
|
||||
-r - Select the RPC endpoint hosted by a node that starts as
|
||||
a validator node. If unspecified the RPC endpoint hosted by
|
||||
the bootstrap leader will be used.
|
||||
@ -61,7 +61,7 @@ while getopts "ch?i:k:brxR" opt; do
|
||||
extraNodes=$((extraNodes + 1))
|
||||
;;
|
||||
r)
|
||||
walletRpcEndpoint="--rpc-port 18899"
|
||||
walletRpcPort=":18899"
|
||||
;;
|
||||
R)
|
||||
rollingRestart=true
|
||||
@ -79,17 +79,20 @@ nodes=(
|
||||
"multinode-demo/drone.sh"
|
||||
"multinode-demo/bootstrap-leader.sh \
|
||||
--enable-rpc-exit \
|
||||
--no-restart \
|
||||
--init-complete-file init-complete-node1.log"
|
||||
"multinode-demo/fullnode.sh \
|
||||
"multinode-demo/validator.sh \
|
||||
$maybeNoLeaderRotation \
|
||||
--enable-rpc-exit \
|
||||
--no-restart \
|
||||
--init-complete-file init-complete-node2.log \
|
||||
--rpc-port 18899"
|
||||
)
|
||||
|
||||
for i in $(seq 1 $extraNodes); do
|
||||
nodes+=(
|
||||
"multinode-demo/fullnode.sh \
|
||||
"multinode-demo/validator.sh \
|
||||
--no-restart \
|
||||
--label dyn$i \
|
||||
--init-complete-file init-complete-node$((2 + i)).log \
|
||||
$maybeNoLeaderRotation"
|
||||
@ -166,13 +169,11 @@ startNodes() {
|
||||
|
||||
killNode() {
|
||||
declare pid=$1
|
||||
echo "kill $pid"
|
||||
set +e
|
||||
if kill "$pid"; then
|
||||
echo "Waiting for $pid to exit..."
|
||||
wait "$pid"
|
||||
else
|
||||
echo "^^^ +++"
|
||||
echo "Warning: unable to kill $pid"
|
||||
echo "$pid exited with $?"
|
||||
fi
|
||||
set -e
|
||||
}
|
||||
@ -196,10 +197,11 @@ killNodes() {
|
||||
# Give the nodes a splash of time to cleanly exit before killing them
|
||||
sleep 2
|
||||
|
||||
echo "--- Killing nodes"
|
||||
echo "--- Killing nodes: ${pids[*]}"
|
||||
for pid in "${pids[@]}"; do
|
||||
killNode "$pid"
|
||||
done
|
||||
echo "done killing nodes"
|
||||
pids=()
|
||||
}
|
||||
|
||||
@ -254,7 +256,7 @@ rollingNodeRestart() {
|
||||
}
|
||||
|
||||
verifyLedger() {
|
||||
for ledger in bootstrap-leader fullnode; do
|
||||
for ledger in bootstrap-leader validator; do
|
||||
echo "--- $ledger ledger verification"
|
||||
(
|
||||
source multinode-demo/common.sh
|
||||
@ -292,7 +294,7 @@ flag_error() {
|
||||
}
|
||||
|
||||
if ! $skipSetup; then
|
||||
multinode-demo/setup.sh
|
||||
multinode-demo/setup.sh --hashes-per-tick auto
|
||||
else
|
||||
verifyLedger
|
||||
fi
|
||||
@ -304,10 +306,10 @@ while [[ $iteration -le $iterations ]]; do
|
||||
(
|
||||
source multinode-demo/common.sh
|
||||
set -x
|
||||
client_id=/tmp/client-id.json-$$
|
||||
$solana_keygen -o $client_id || exit $?
|
||||
client_keypair=/tmp/client-id.json-$$
|
||||
$solana_keygen -o $client_keypair || exit $?
|
||||
$solana_gossip spy --num-nodes-exactly $numNodes || exit $?
|
||||
rm -rf $client_id
|
||||
rm -rf $client_keypair
|
||||
) || flag_error
|
||||
|
||||
echo "--- RPC API: bootstrap-leader getTransactionCount ($iteration)"
|
||||
@ -321,7 +323,7 @@ while [[ $iteration -le $iterations ]]; do
|
||||
cat log-transactionCount.txt
|
||||
) || flag_error
|
||||
|
||||
echo "--- RPC API: fullnode getTransactionCount ($iteration)"
|
||||
echo "--- RPC API: validator getTransactionCount ($iteration)"
|
||||
(
|
||||
set -x
|
||||
curl --retry 5 --retry-delay 2 --retry-connrefused \
|
||||
@ -362,8 +364,7 @@ while [[ $iteration -le $iterations ]]; do
|
||||
}
|
||||
(
|
||||
set -x
|
||||
# shellcheck disable=SC2086 # Don't want to double quote $walletRpcEndpoint
|
||||
timeout 60s scripts/wallet-sanity.sh $walletRpcEndpoint
|
||||
timeout 60s scripts/wallet-sanity.sh --url http://127.0.0.1"$walletRpcPort"
|
||||
) || flag_error_if_no_leader_rotation
|
||||
|
||||
iteration=$((iteration + 1))
|
||||
|
33
ci/nits.sh
33
ci/nits.sh
@ -13,19 +13,24 @@ declare prints=(
|
||||
'println!'
|
||||
'eprint!'
|
||||
'eprintln!'
|
||||
'dbg!'
|
||||
)
|
||||
|
||||
# Parts of the tree that are expected to be print free
|
||||
declare print_free_tree=(
|
||||
'core/src'
|
||||
'drone'
|
||||
'metrics'
|
||||
'netutil'
|
||||
'runtime'
|
||||
'sdk'
|
||||
'drone/src'
|
||||
'metrics/src'
|
||||
'netutil/src'
|
||||
'runtime/src'
|
||||
'sdk/src'
|
||||
'programs/vote_api/src'
|
||||
'programs/vote_program/src'
|
||||
'programs/stake_api/src'
|
||||
'programs/stake_program/src'
|
||||
)
|
||||
|
||||
if _ git grep "${prints[@]/#/-e }" -- "${print_free_tree[@]}"; then
|
||||
if _ git --no-pager grep -n --max-depth=0 "${prints[@]/#/-e }" -- "${print_free_tree[@]}"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -34,7 +39,21 @@ fi
|
||||
# Default::default()
|
||||
#
|
||||
# Ref: https://github.com/solana-labs/solana/issues/2630
|
||||
if _ git grep 'Default::default()' -- '*.rs'; then
|
||||
if _ git --no-pager grep -n 'Default::default()' -- '*.rs'; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Let's keep a .gitignore for every crate, ensure it's got
|
||||
# /target/ in it
|
||||
declare gitignores_ok=true
|
||||
for i in $(git --no-pager ls-files \*/Cargo.toml ); do
|
||||
dir=$(dirname "$i")
|
||||
if [[ ! -f $dir/.gitignore ]]; then
|
||||
echo 'error: nits.sh .gitnore missing for crate '"$dir" >&2
|
||||
gitignores_ok=false
|
||||
elif ! grep -q -e '^/target/$' "$dir"/.gitignore; then
|
||||
echo 'error: nits.sh "/target/" apparently missing from '"$dir"'/.gitignore' >&2
|
||||
gitignores_ok=false
|
||||
fi
|
||||
done
|
||||
"$gitignores_ok"
|
||||
|
@ -25,7 +25,7 @@ CRATES=(
|
||||
runtime
|
||||
vote-signer
|
||||
core
|
||||
fullnode
|
||||
validator
|
||||
genesis
|
||||
gossip
|
||||
ledger-tool
|
||||
|
@ -48,7 +48,7 @@ echo --- Creating tarball
|
||||
COMMIT="$(git rev-parse HEAD)"
|
||||
|
||||
(
|
||||
echo "channel: $CHANNEL"
|
||||
echo "channel: $CHANNEL_OR_TAG"
|
||||
echo "commit: $COMMIT"
|
||||
echo "target: $TARGET"
|
||||
) > solana-release/version.yml
|
||||
@ -56,37 +56,41 @@ echo --- Creating tarball
|
||||
source ci/rust-version.sh stable
|
||||
scripts/cargo-install-all.sh +"$rust_stable" solana-release
|
||||
|
||||
rm -rf target/perf-libs
|
||||
./fetch-perf-libs.sh
|
||||
mkdir solana-release/target
|
||||
cp -a target/perf-libs solana-release/target/
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source ./target/perf-libs/env.sh
|
||||
(
|
||||
cd fullnode
|
||||
cargo install --path . --features=cuda --root ../solana-release-cuda
|
||||
cd validator
|
||||
cargo +"$rust_stable" install --path . --features=cuda --root ../solana-release-cuda
|
||||
)
|
||||
cp solana-release-cuda/bin/solana-fullnode solana-release/bin/solana-fullnode-cuda
|
||||
cp solana-release-cuda/bin/solana-validator solana-release/bin/solana-validator-cuda
|
||||
cp -a scripts multinode-demo solana-release/
|
||||
|
||||
# Add a wrapper script for fullnode.sh
|
||||
# Add a wrapper script for validator.sh
|
||||
# TODO: Remove multinode/... from tarball
|
||||
cat > solana-release/bin/fullnode.sh <<'EOF'
|
||||
cat > solana-release/bin/validator.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
export USE_INSTALL=1
|
||||
exec multinode-demo/fullnode.sh "$@"
|
||||
exec multinode-demo/validator.sh "$@"
|
||||
EOF
|
||||
chmod +x solana-release/bin/fullnode.sh
|
||||
chmod +x solana-release/bin/validator.sh
|
||||
|
||||
# Add a wrapper script for clear-fullnode-config.sh
|
||||
# Add a wrapper script for clear-config.sh
|
||||
# TODO: Remove multinode/... from tarball
|
||||
cat > solana-release/bin/clear-fullnode-config.sh <<'EOF'
|
||||
cat > solana-release/bin/clear-config.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
export USE_INSTALL=1
|
||||
exec multinode-demo/clear-fullnode-config.sh "$@"
|
||||
exec multinode-demo/clear-validator-config.sh "$@"
|
||||
EOF
|
||||
chmod +x solana-release/bin/clear-fullnode-config.sh
|
||||
chmod +x solana-release/bin/clear-config.sh
|
||||
|
||||
tar jvcf solana-release-$TARGET.tar.bz2 solana-release/
|
||||
cp solana-release/bin/solana-install solana-install-$TARGET
|
||||
|
@ -16,8 +16,8 @@
|
||||
export rust_stable=1.34.0
|
||||
export rust_stable_docker_image=solanalabs/rust:1.34.0
|
||||
|
||||
export rust_nightly=nightly-2019-03-14
|
||||
export rust_nightly_docker_image=solanalabs/rust-nightly:2019-03-14
|
||||
export rust_nightly=nightly-2019-05-01
|
||||
export rust_nightly_docker_image=solanalabs/rust-nightly:2019-05-01
|
||||
|
||||
[[ -z $1 ]] || (
|
||||
|
||||
|
@ -40,7 +40,10 @@ fi
|
||||
BENCH_FILE=bench_output.log
|
||||
BENCH_ARTIFACT=current_bench_results.log
|
||||
|
||||
# First remove "BENCH_FILE", if it exists so that the following commands can append
|
||||
# Ensure all dependencies are built
|
||||
_ cargo +$rust_nightly build --all --release
|
||||
|
||||
# Remove "BENCH_FILE", if it exists so that the following commands can append
|
||||
rm -f "$BENCH_FILE"
|
||||
|
||||
# Run sdk benches
|
||||
@ -59,8 +62,11 @@ _ cargo +$rust_nightly bench --manifest-path core/Cargo.toml ${V:+--verbose} \
|
||||
_ cargo +$rust_nightly bench --manifest-path programs/bpf/Cargo.toml ${V:+--verbose} --features=bpf_c \
|
||||
-- -Z unstable-options --format=json --nocapture | tee -a "$BENCH_FILE"
|
||||
|
||||
# TODO: debug why solana-upload-perf takes over 30 minutes to complete.
|
||||
exit 0
|
||||
|
||||
_ cargo +$rust_nightly run --release --package solana-upload-perf \
|
||||
-- "$BENCH_FILE" "$TARGET_BRANCH" "$UPLOAD_METRICS" > "$BENCH_ARTIFACT"
|
||||
-- "$BENCH_FILE" "$TARGET_BRANCH" "$UPLOAD_METRICS" | tee "$BENCH_ARTIFACT"
|
||||
|
||||
upload-ci-artifact "$BENCH_FILE"
|
||||
upload-ci-artifact "$BENCH_ARTIFACT"
|
||||
|
@ -12,7 +12,7 @@ export RUSTFLAGS="-D warnings"
|
||||
_ cargo +"$rust_stable" fmt --all -- --check
|
||||
_ cargo +"$rust_stable" clippy --all -- --version
|
||||
_ cargo +"$rust_stable" clippy --all -- --deny=warnings
|
||||
_ ci/audit.sh
|
||||
_ cargo +"$rust_stable" audit
|
||||
_ ci/nits.sh
|
||||
_ book/build.sh
|
||||
|
||||
|
@ -8,6 +8,7 @@ source ci/rust-version.sh stable
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
|
||||
rm -rf target/perf-libs
|
||||
./fetch-perf-libs.sh
|
||||
export LD_LIBRARY_PATH=$PWD/target/perf-libs:$LD_LIBRARY_PATH
|
||||
|
||||
|
@ -47,7 +47,6 @@ test-stable-perf)
|
||||
|
||||
# BPF program tests
|
||||
_ make -C programs/bpf/c tests
|
||||
_ programs/bpf/rust/noop/build.sh # Must be built out of band
|
||||
_ cargo +"$rust_stable" test \
|
||||
--manifest-path programs/bpf/Cargo.toml \
|
||||
--no-default-features --features=bpf_c,bpf_rust
|
||||
@ -62,6 +61,7 @@ test-stable-perf)
|
||||
# is not yet loaded.
|
||||
sudo --non-interactive ./net/scripts/enable-nvidia-persistence-mode.sh
|
||||
|
||||
rm -rf target/perf-libs
|
||||
./fetch-perf-libs.sh
|
||||
# shellcheck source=/dev/null
|
||||
source ./target/perf-libs/env.sh
|
||||
|
@ -52,14 +52,14 @@ launchTestnet() {
|
||||
declare q_mean_tps='
|
||||
SELECT round(mean("sum_count")) AS "mean_tps" FROM (
|
||||
SELECT sum("count") AS "sum_count"
|
||||
FROM "testnet-automation"."autogen"."counter-banking_stage-process_transactions"
|
||||
FROM "testnet-automation"."autogen"."banking_stage-record_transactions"
|
||||
WHERE time > now() - 300s GROUP BY time(1s)
|
||||
)'
|
||||
|
||||
declare q_max_tps='
|
||||
SELECT round(max("sum_count")) AS "max_tps" FROM (
|
||||
SELECT sum("count") AS "sum_count"
|
||||
FROM "testnet-automation"."autogen"."counter-banking_stage-process_transactions"
|
||||
FROM "testnet-automation"."autogen"."banking_stage-record_transactions"
|
||||
WHERE time > now() - 300s GROUP BY time(1s)
|
||||
)'
|
||||
|
||||
|
@ -11,15 +11,19 @@ clientNodeCount=0
|
||||
additionalFullNodeCount=10
|
||||
publicNetwork=false
|
||||
stopNetwork=false
|
||||
skipSetup=false
|
||||
reuseLedger=false
|
||||
skipCreate=false
|
||||
skipStart=false
|
||||
externalNode=false
|
||||
failOnValidatorBootupFailure=true
|
||||
tarChannelOrTag=edge
|
||||
delete=false
|
||||
enableGpu=false
|
||||
bootDiskType=""
|
||||
leaderRotation=true
|
||||
blockstreamer=false
|
||||
deployUpdateManifest=true
|
||||
fetchLogs=true
|
||||
maybeHashesPerTick=
|
||||
|
||||
usage() {
|
||||
exitcode=0
|
||||
@ -46,17 +50,23 @@ Deploys a CD testnet
|
||||
-c [number] - Number of client bencher nodes (default: $clientNodeCount)
|
||||
-u - Include a Blockstreamer (default: $blockstreamer)
|
||||
-P - Use public network IP addresses (default: $publicNetwork)
|
||||
-G - Enable GPU, and set count/type of GPUs to use (e.g n1-standard-16 --accelerator count=4,type=nvidia-tesla-k80)
|
||||
-G - Enable GPU, and set count/type of GPUs to use (e.g n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100)
|
||||
-g - Enable GPU (default: $enableGpu)
|
||||
-b - Disable leader rotation
|
||||
-a [address] - Set the bootstrap fullnode's external IP address to this GCE address
|
||||
-d [disk-type] - Specify a boot disk type (default None) Use pd-ssd to get ssd on GCE.
|
||||
-D - Delete the network
|
||||
-r - Reuse existing node/ledger configuration from a
|
||||
previous |start| (ie, don't run ./multinode-demo/setup.sh).
|
||||
-x - External node. Default: false
|
||||
-e - Skip create. Assume the nodes have already been created
|
||||
-s - Skip start. Nodes will still be created or configured, but network software will not be started.
|
||||
-S - Stop network software without tearing down nodes.
|
||||
-f - Discard validator nodes that didn't bootup successfully
|
||||
-w - Skip time-consuming "bells and whistles" that are
|
||||
unnecessary for a high-node count demo testnet
|
||||
|
||||
--hashes-per-tick NUM_HASHES|sleep|auto
|
||||
- Override the default --hashes-per-tick for the cluster
|
||||
|
||||
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
||||
metrics
|
||||
@ -66,7 +76,22 @@ EOF
|
||||
|
||||
zone=()
|
||||
|
||||
while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:S" opt; do
|
||||
shortArgs=()
|
||||
while [[ -n $1 ]]; do
|
||||
if [[ ${1:0:2} = -- ]]; then
|
||||
if [[ $1 = --hashes-per-tick ]]; then
|
||||
maybeHashesPerTick="$1 $2"
|
||||
shift 2
|
||||
else
|
||||
usage "Unknown long option: $1"
|
||||
fi
|
||||
else
|
||||
shortArgs+=("$1")
|
||||
shift
|
||||
fi
|
||||
done
|
||||
|
||||
while getopts "h?p:Pn:c:t:gG:a:Dd:rusxz:p:C:Sfew" opt "${shortArgs[@]}"; do
|
||||
case $opt in
|
||||
h | \?)
|
||||
usage
|
||||
@ -99,9 +124,6 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:S" opt; do
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
b)
|
||||
leaderRotation=false
|
||||
;;
|
||||
g)
|
||||
enableGpu=true
|
||||
;;
|
||||
@ -119,7 +141,10 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:S" opt; do
|
||||
delete=true
|
||||
;;
|
||||
r)
|
||||
skipSetup=true
|
||||
reuseLedger=true
|
||||
;;
|
||||
e)
|
||||
skipCreate=true
|
||||
;;
|
||||
s)
|
||||
skipStart=true
|
||||
@ -127,14 +152,21 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:S" opt; do
|
||||
x)
|
||||
externalNode=true
|
||||
;;
|
||||
f)
|
||||
failOnValidatorBootupFailure=false
|
||||
;;
|
||||
u)
|
||||
blockstreamer=true
|
||||
;;
|
||||
S)
|
||||
stopNetwork=true
|
||||
;;
|
||||
w)
|
||||
fetchLogs=false
|
||||
deployUpdateManifest=false
|
||||
;;
|
||||
*)
|
||||
usage "Error: unhandled option: $opt"
|
||||
usage "Unknown option: $opt"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
@ -170,15 +202,15 @@ for val in "${zone[@]}"; do
|
||||
done
|
||||
|
||||
if $stopNetwork; then
|
||||
skipSetup=true
|
||||
skipCreate=true
|
||||
fi
|
||||
|
||||
if $delete; then
|
||||
skipSetup=false
|
||||
skipCreate=false
|
||||
fi
|
||||
|
||||
# Create the network
|
||||
if ! $skipSetup; then
|
||||
if ! $skipCreate; then
|
||||
echo "--- $cloudProvider.sh delete"
|
||||
# shellcheck disable=SC2068
|
||||
time net/"$cloudProvider".sh delete ${zone_args[@]} -p "$netName" ${externalNode:+-x}
|
||||
@ -212,10 +244,6 @@ if ! $skipSetup; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! $leaderRotation; then
|
||||
create_args+=(-b)
|
||||
fi
|
||||
|
||||
if $publicNetwork; then
|
||||
create_args+=(-P)
|
||||
fi
|
||||
@ -224,6 +252,10 @@ if ! $skipSetup; then
|
||||
create_args+=(-x)
|
||||
fi
|
||||
|
||||
if ! $failOnValidatorBootupFailure; then
|
||||
create_args+=(-f)
|
||||
fi
|
||||
|
||||
time net/"$cloudProvider".sh create "${create_args[@]}"
|
||||
else
|
||||
echo "--- $cloudProvider.sh config"
|
||||
@ -236,6 +268,14 @@ else
|
||||
config_args+=(-P)
|
||||
fi
|
||||
|
||||
if $externalNode; then
|
||||
config_args+=(-x)
|
||||
fi
|
||||
|
||||
if ! $failOnValidatorBootupFailure; then
|
||||
config_args+=(-f)
|
||||
fi
|
||||
|
||||
time net/"$cloudProvider".sh config "${config_args[@]}"
|
||||
fi
|
||||
net/init-metrics.sh -e
|
||||
@ -249,53 +289,54 @@ if $stopNetwork; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo --- net.sh start
|
||||
maybeRejectExtraNodes=
|
||||
if ! $publicNetwork; then
|
||||
maybeRejectExtraNodes="-o rejectExtraNodes"
|
||||
fi
|
||||
maybeNoValidatorSanity=
|
||||
if [[ -n $NO_VALIDATOR_SANITY ]]; then
|
||||
maybeNoValidatorSanity="-o noValidatorSanity"
|
||||
fi
|
||||
maybeNoLedgerVerify=
|
||||
if [[ -n $NO_LEDGER_VERIFY ]]; then
|
||||
maybeNoLedgerVerify="-o noLedgerVerify"
|
||||
fi
|
||||
|
||||
maybeSkipSetup=
|
||||
if $skipSetup; then
|
||||
maybeSkipSetup="-r"
|
||||
fi
|
||||
|
||||
ok=true
|
||||
if ! $skipStart; then
|
||||
(
|
||||
if $skipSetup; then
|
||||
if $skipCreate; then
|
||||
# TODO: Enable rolling updates
|
||||
#op=update
|
||||
op=restart
|
||||
else
|
||||
op=start
|
||||
fi
|
||||
echo "--- net.sh $op"
|
||||
args=("$op" -t "$tarChannelOrTag")
|
||||
|
||||
if ! $publicNetwork; then
|
||||
args+=(-o rejectExtraNodes)
|
||||
fi
|
||||
if [[ -n $NO_VALIDATOR_SANITY ]]; then
|
||||
args+=(-o noValidatorSanity)
|
||||
fi
|
||||
if [[ -n $NO_LEDGER_VERIFY ]]; then
|
||||
args+=(-o noLedgerVerify)
|
||||
fi
|
||||
if [[ -n $maybeHashesPerTick ]]; then
|
||||
# shellcheck disable=SC2206 # Do not want to quote $maybeHashesPerTick
|
||||
args+=($maybeHashesPerTick)
|
||||
fi
|
||||
|
||||
if $reuseLedger; then
|
||||
args+=(-r)
|
||||
fi
|
||||
|
||||
if ! $failOnValidatorBootupFailure; then
|
||||
args+=(-F)
|
||||
fi
|
||||
|
||||
maybeUpdateManifestKeypairFile=
|
||||
# shellcheck disable=SC2154 # SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu comes from .buildkite/env/
|
||||
if [[ -n $SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu ]]; then
|
||||
if $deployUpdateManifest && [[ -n $SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu ]]; then
|
||||
echo "$SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu" > update_manifest_keypair.json
|
||||
maybeUpdateManifestKeypairFile="-i update_manifest_keypair.json"
|
||||
args+=(-i update_manifest_keypair.json)
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086 # Don't want to double quote the $maybeXYZ variables
|
||||
time net/net.sh $op -t "$tarChannelOrTag" \
|
||||
$maybeUpdateManifestKeypairFile \
|
||||
$maybeSkipSetup \
|
||||
$maybeRejectExtraNodes \
|
||||
$maybeNoValidatorSanity \
|
||||
$maybeNoLedgerVerify
|
||||
time net/net.sh "${args[@]}"
|
||||
) || ok=false
|
||||
|
||||
if $fetchLogs; then
|
||||
net/net.sh logs
|
||||
fi
|
||||
fi
|
||||
|
||||
$ok
|
||||
|
@ -52,7 +52,7 @@ steps:
|
||||
value: "create-and-start"
|
||||
- label: "Create testnet, but do not start software. If the testnet already exists it will be deleted and re-created"
|
||||
value: "create"
|
||||
- label: "Start network software on an existing testnet. If software is already running it will be restarted."
|
||||
- label: "Start network software on an existing testnet. If software is already running it will be restarted"
|
||||
value: "start"
|
||||
- label: "Stop network software without deleting testnet nodes"
|
||||
value: "stop"
|
||||
@ -62,11 +62,11 @@ steps:
|
||||
value: "sanity-or-restart"
|
||||
- label: "Sanity check only"
|
||||
value: "sanity"
|
||||
- label: "Delete the testnet.
|
||||
- label: "Delete the testnet"
|
||||
value: "delete"
|
||||
- label: "Enable/unlock the testnet."
|
||||
- label: "Enable/unlock the testnet"
|
||||
value: "enable"
|
||||
- label: "Delete and then lock the testnet from further operation until it is re-enabled."
|
||||
- label: "Delete and then lock the testnet from further operation until it is re-enabled"
|
||||
value: "disable"
|
||||
- command: "ci/$(basename "$0")"
|
||||
agents:
|
||||
@ -80,25 +80,68 @@ ci/channel-info.sh
|
||||
eval "$(ci/channel-info.sh)"
|
||||
|
||||
|
||||
EC2_ZONES=(us-west-1a sa-east-1a ap-northeast-2a eu-central-1a ca-central-1a)
|
||||
GCE_ZONES=(us-west1-b asia-east2-a europe-west4-a southamerica-east1-b us-east4-c)
|
||||
EC2_ZONES=(
|
||||
us-west-1a
|
||||
us-west-2a
|
||||
us-east-1a
|
||||
us-east-2a
|
||||
sa-east-1a
|
||||
eu-west-1a
|
||||
eu-west-2a
|
||||
eu-central-1a
|
||||
ap-northeast-2a
|
||||
ap-southeast-2a
|
||||
ap-south-1a
|
||||
ca-central-1a
|
||||
)
|
||||
|
||||
# GCE zones with _lots_ of quota
|
||||
GCE_ZONES=(
|
||||
us-west1-a
|
||||
us-central1-a
|
||||
us-east1-b
|
||||
europe-west4-a
|
||||
|
||||
us-west1-b
|
||||
us-central1-b
|
||||
us-east1-c
|
||||
europe-west4-b
|
||||
|
||||
us-west1-c
|
||||
us-east1-d
|
||||
europe-west4-c
|
||||
)
|
||||
|
||||
# GCE zones with enough quota for one CPU-only fullnode
|
||||
GCE_LOW_QUOTA_ZONES=(
|
||||
asia-east2-a
|
||||
asia-northeast1-b
|
||||
asia-northeast2-b
|
||||
asia-south1-c
|
||||
asia-southeast1-b
|
||||
australia-southeast1-b
|
||||
europe-north1-a
|
||||
europe-west2-b
|
||||
europe-west3-c
|
||||
europe-west6-a
|
||||
northamerica-northeast1-a
|
||||
southamerica-east1-b
|
||||
)
|
||||
|
||||
case $TESTNET in
|
||||
testnet-edge|testnet-edge-perf)
|
||||
CHANNEL_OR_TAG=edge
|
||||
CHANNEL_BRANCH=$EDGE_CHANNEL
|
||||
: "${TESTNET_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
|
||||
;;
|
||||
testnet-beta|testnet-beta-perf)
|
||||
CHANNEL_OR_TAG=beta
|
||||
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||
: "${TESTNET_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
|
||||
;;
|
||||
testnet)
|
||||
CHANNEL_OR_TAG=$STABLE_CHANNEL_LATEST_TAG
|
||||
CHANNEL_BRANCH=$STABLE_CHANNEL
|
||||
: "${EC2_NODE_COUNT:=10}"
|
||||
: "${GCE_NODE_COUNT:=}"
|
||||
: "${TESTNET_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
|
||||
;;
|
||||
testnet-perf)
|
||||
CHANNEL_OR_TAG=$STABLE_CHANNEL_LATEST_TAG
|
||||
@ -107,7 +150,8 @@ testnet-perf)
|
||||
testnet-demo)
|
||||
CHANNEL_OR_TAG=beta
|
||||
CHANNEL_BRANCH=$BETA_CHANNEL
|
||||
: "${GCE_NODE_COUNT:=200}"
|
||||
: "${GCE_NODE_COUNT:=150}"
|
||||
: "${GCE_LOW_QUOTA_NODE_COUNT:=70}"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid TESTNET=$TESTNET"
|
||||
@ -123,6 +167,10 @@ GCE_ZONE_ARGS=()
|
||||
for val in "${GCE_ZONES[@]}"; do
|
||||
GCE_ZONE_ARGS+=("-z $val")
|
||||
done
|
||||
GCE_LOW_QUOTA_ZONE_ARGS=()
|
||||
for val in "${GCE_LOW_QUOTA_ZONES[@]}"; do
|
||||
GCE_LOW_QUOTA_ZONE_ARGS+=("-z $val")
|
||||
done
|
||||
|
||||
if [[ -n $TESTNET_DB_HOST ]]; then
|
||||
SOLANA_METRICS_PARTIAL_CONFIG="host=$TESTNET_DB_HOST,$SOLANA_METRICS_PARTIAL_CONFIG"
|
||||
@ -151,6 +199,7 @@ steps:
|
||||
TESTNET_DB_HOST: "$TESTNET_DB_HOST"
|
||||
EC2_NODE_COUNT: "$EC2_NODE_COUNT"
|
||||
GCE_NODE_COUNT: "$GCE_NODE_COUNT"
|
||||
GCE_LOW_QUOTA_NODE_COUNT: "$GCE_LOW_QUOTA_NODE_COUNT"
|
||||
EOF
|
||||
) | buildkite-agent pipeline upload
|
||||
exit 0
|
||||
@ -227,7 +276,8 @@ sanity() {
|
||||
ok=true
|
||||
if [[ -n $GCE_NODE_COUNT ]]; then
|
||||
NO_LEDGER_VERIFY=1 \
|
||||
ci/testnet-sanity.sh demo-testnet-solana-com gce "${GCE_ZONES[0]}" || ok=false
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
ci/testnet-sanity.sh demo-testnet-solana-com gce "${GCE_ZONES[0]}" -f || ok=false
|
||||
else
|
||||
echo "Error: no GCE nodes"
|
||||
ok=false
|
||||
@ -270,10 +320,11 @@ deploy() {
|
||||
set -x
|
||||
ci/testnet-deploy.sh -p edge-testnet-solana-com -C ec2 -z us-west-1a \
|
||||
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0ccd4f2239886fa94 \
|
||||
${skipCreate:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
${maybeDelete:+-D} \
|
||||
--hashes-per-tick auto
|
||||
)
|
||||
;;
|
||||
testnet-edge-perf)
|
||||
@ -284,11 +335,11 @@ deploy() {
|
||||
RUST_LOG=solana=warn \
|
||||
ci/testnet-deploy.sh -p edge-perf-testnet-solana-com -C ec2 -z us-west-2b \
|
||||
-g -t "$CHANNEL_OR_TAG" -c 2 \
|
||||
-b \
|
||||
${skipCreate:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
${maybeDelete:+-D} \
|
||||
--hashes-per-tick auto
|
||||
)
|
||||
;;
|
||||
testnet-beta)
|
||||
@ -297,11 +348,11 @@ deploy() {
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
ci/testnet-deploy.sh -p beta-testnet-solana-com -C ec2 -z us-west-1a \
|
||||
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0f286cf8a0771ce35 \
|
||||
-b \
|
||||
${skipCreate:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
${maybeDelete:+-D} \
|
||||
--hashes-per-tick auto
|
||||
)
|
||||
;;
|
||||
testnet-beta-perf)
|
||||
@ -312,11 +363,11 @@ deploy() {
|
||||
RUST_LOG=solana=warn \
|
||||
ci/testnet-deploy.sh -p beta-perf-testnet-solana-com -C ec2 -z us-west-2b \
|
||||
-g -t "$CHANNEL_OR_TAG" -c 2 \
|
||||
-b \
|
||||
${skipCreate:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
${maybeDelete:+-D} \
|
||||
--hashes-per-tick auto
|
||||
)
|
||||
;;
|
||||
testnet)
|
||||
@ -329,8 +380,8 @@ deploy() {
|
||||
|
||||
# shellcheck disable=SC2068
|
||||
ci/testnet-deploy.sh -p testnet-solana-com -C ec2 ${EC2_ZONE_ARGS[@]} \
|
||||
-t "$CHANNEL_OR_TAG" -n "$EC2_NODE_COUNT" -c 0 -u -P -a eipalloc-0fa502bf95f6f18b2 \
|
||||
${skipCreate:+-r} \
|
||||
-t "$CHANNEL_OR_TAG" -n "$EC2_NODE_COUNT" -c 0 -u -P -f -a eipalloc-0fa502bf95f6f18b2 \
|
||||
${skipCreate:+-e} \
|
||||
${maybeSkipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
@ -338,12 +389,12 @@ deploy() {
|
||||
if [[ -n $GCE_NODE_COUNT ]]; then
|
||||
# shellcheck disable=SC2068
|
||||
ci/testnet-deploy.sh -p testnet-solana-com -C gce ${GCE_ZONE_ARGS[@]} \
|
||||
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 0 -P \
|
||||
${skipCreate:+-r} \
|
||||
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 0 -P -f \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D} \
|
||||
${EC2_NODE_COUNT:+-x}
|
||||
-x
|
||||
fi
|
||||
)
|
||||
;;
|
||||
@ -356,26 +407,45 @@ deploy() {
|
||||
ci/testnet-deploy.sh -p perf-testnet-solana-com -C gce -z us-west1-b \
|
||||
-G "--machine-type n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100" \
|
||||
-t "$CHANNEL_OR_TAG" -c 2 \
|
||||
-b \
|
||||
-d pd-ssd \
|
||||
${skipCreate:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
${maybeDelete:+-D} \
|
||||
--hashes-per-tick auto
|
||||
)
|
||||
;;
|
||||
testnet-demo)
|
||||
(
|
||||
set -x
|
||||
if [[ -n $GCE_NODE_COUNT ]]; then
|
||||
|
||||
if [[ -n $GCE_LOW_QUOTA_NODE_COUNT ]] || [[ -n $skipStart ]]; then
|
||||
maybeSkipStart="skip"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2068
|
||||
ci/testnet-deploy.sh -p testnet-demo -C gce ${GCE_ZONE_ARGS[@]} \
|
||||
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 1 -P -u \
|
||||
NO_LEDGER_VERIFY=1 \
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
ci/testnet-deploy.sh -p demo-testnet-solana-com -C gce ${GCE_ZONE_ARGS[@]} \
|
||||
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 0 -P -u -f -w \
|
||||
-a demo-testnet-solana-com \
|
||||
${skipCreate:+-r} \
|
||||
${skipCreate:+-e} \
|
||||
${maybeSkipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D} \
|
||||
--hashes-per-tick auto
|
||||
|
||||
if [[ -n $GCE_LOW_QUOTA_NODE_COUNT ]]; then
|
||||
# shellcheck disable=SC2068
|
||||
NO_LEDGER_VERIFY=1 \
|
||||
NO_VALIDATOR_SANITY=1 \
|
||||
ci/testnet-deploy.sh -p demo-testnet-solana-com2 -C gce ${GCE_LOW_QUOTA_ZONE_ARGS[@]} \
|
||||
-t "$CHANNEL_OR_TAG" -n "$GCE_LOW_QUOTA_NODE_COUNT" -c 0 -P -f -x -w \
|
||||
${skipCreate:+-e} \
|
||||
${skipStart:+-s} \
|
||||
${maybeStop:+-S} \
|
||||
${maybeDelete:+-D}
|
||||
${maybeDelete:+-D} \
|
||||
--hashes-per-tick auto
|
||||
fi
|
||||
)
|
||||
;;
|
||||
|
1
client/.gitignore
vendored
Normal file
1
client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target/
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "solana-client"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
description = "Solana Client"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
@ -9,18 +9,18 @@ license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.2"
|
||||
bincode = "1.1.4"
|
||||
bs58 = "0.2.0"
|
||||
log = "0.4.2"
|
||||
jsonrpc-core = "10.1.0"
|
||||
reqwest = "0.9.11"
|
||||
reqwest = "0.9.17"
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.88"
|
||||
serde_derive = "1.0.91"
|
||||
serde_json = "1.0.39"
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.15.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-core = "10.1.0"
|
||||
jsonrpc-http-server = "10.1.0"
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||
|
@ -1,6 +1,7 @@
|
||||
pub mod client_error;
|
||||
mod generic_rpc_client_request;
|
||||
pub mod mock_rpc_client_request;
|
||||
pub mod perf_utils;
|
||||
pub mod rpc_client;
|
||||
pub mod rpc_client_request;
|
||||
pub mod rpc_request;
|
||||
|
@ -2,6 +2,7 @@ use crate::client_error::ClientError;
|
||||
use crate::generic_rpc_client_request::GenericRpcClientRequest;
|
||||
use crate::rpc_request::RpcRequest;
|
||||
use serde_json::{Number, Value};
|
||||
use solana_sdk::fee_calculator::FeeCalculator;
|
||||
use solana_sdk::transaction::{self, TransactionError};
|
||||
|
||||
pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
|
||||
@ -44,7 +45,10 @@ impl GenericRpcClientRequest for MockRpcClientRequest {
|
||||
let n = if self.url == "airdrop" { 0 } else { 50 };
|
||||
Value::Number(Number::from(n))
|
||||
}
|
||||
RpcRequest::GetRecentBlockhash => Value::String(PUBKEY.to_string()),
|
||||
RpcRequest::GetRecentBlockhash => Value::Array(vec![
|
||||
Value::String(PUBKEY.to_string()),
|
||||
serde_json::to_value(FeeCalculator::default()).unwrap(),
|
||||
]),
|
||||
RpcRequest::GetSignatureStatus => {
|
||||
let response: Option<transaction::Result<()>> = if self.url == "account_in_use" {
|
||||
Some(Err(TransactionError::AccountInUse))
|
||||
|
76
client/src/perf_utils.rs
Normal file
76
client/src/perf_utils.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use log::*;
|
||||
use solana_sdk::client::Client;
|
||||
use solana_sdk::timing::duration_as_s;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SampleStats {
|
||||
/// Maximum TPS reported by this node
|
||||
pub tps: f32,
|
||||
/// Total time taken for those txs
|
||||
pub elapsed: Duration,
|
||||
/// Total transactions reported by this node
|
||||
pub txs: u64,
|
||||
}
|
||||
|
||||
pub fn sample_txs<T>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
sample_stats: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||
sample_period: u64,
|
||||
client: &Arc<T>,
|
||||
) where
|
||||
T: Client,
|
||||
{
|
||||
let mut max_tps = 0.0;
|
||||
let mut total_elapsed;
|
||||
let mut total_txs;
|
||||
let mut now = Instant::now();
|
||||
let start_time = now;
|
||||
let initial_txs = client.get_transaction_count().expect("transaction count");
|
||||
let mut last_txs = initial_txs;
|
||||
|
||||
loop {
|
||||
total_elapsed = start_time.elapsed();
|
||||
let elapsed = now.elapsed();
|
||||
now = Instant::now();
|
||||
let mut txs = client.get_transaction_count().expect("transaction count");
|
||||
|
||||
if txs < last_txs {
|
||||
info!("Expected txs({}) >= last_txs({})", txs, last_txs);
|
||||
txs = last_txs;
|
||||
}
|
||||
total_txs = txs - initial_txs;
|
||||
let sample_txs = txs - last_txs;
|
||||
last_txs = txs;
|
||||
|
||||
let tps = sample_txs as f32 / duration_as_s(&elapsed);
|
||||
if tps > max_tps {
|
||||
max_tps = tps;
|
||||
}
|
||||
|
||||
info!(
|
||||
"Sampler {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s",
|
||||
tps,
|
||||
sample_txs,
|
||||
total_txs,
|
||||
total_elapsed.as_secs(),
|
||||
);
|
||||
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
let stats = SampleStats {
|
||||
tps: max_tps,
|
||||
elapsed: total_elapsed,
|
||||
txs: total_txs,
|
||||
};
|
||||
sample_stats
|
||||
.write()
|
||||
.unwrap()
|
||||
.push((client.transactions_addr(), stats));
|
||||
return;
|
||||
}
|
||||
sleep(Duration::from_secs(sample_period));
|
||||
}
|
||||
}
|
@ -4,14 +4,14 @@ use crate::mock_rpc_client_request::MockRpcClientRequest;
|
||||
use crate::rpc_client_request::RpcClientRequest;
|
||||
use crate::rpc_request::RpcRequest;
|
||||
use bincode::serialize;
|
||||
use bs58;
|
||||
use log::*;
|
||||
use serde_json::{json, Value};
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::fee_calculator::FeeCalculator;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
|
||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
|
||||
use solana_sdk::signature::{KeypairUtil, Signature};
|
||||
use solana_sdk::timing::{DEFAULT_NUM_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT};
|
||||
use solana_sdk::transaction::{self, Transaction, TransactionError};
|
||||
use std::error;
|
||||
use std::io;
|
||||
@ -78,7 +78,7 @@ impl RpcClient {
|
||||
pub fn send_and_confirm_transaction<T: KeypairUtil>(
|
||||
&self,
|
||||
transaction: &mut Transaction,
|
||||
signer: &T,
|
||||
signer_keys: &[&T],
|
||||
) -> Result<String, ClientError> {
|
||||
let mut send_retries = 5;
|
||||
loop {
|
||||
@ -97,7 +97,7 @@ impl RpcClient {
|
||||
if cfg!(not(test)) {
|
||||
// Retry ~twice during a slot
|
||||
sleep(Duration::from_millis(
|
||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
||||
500 * DEFAULT_TICKS_PER_SLOT / DEFAULT_NUM_TICKS_PER_SECOND,
|
||||
));
|
||||
}
|
||||
};
|
||||
@ -106,7 +106,7 @@ impl RpcClient {
|
||||
Ok(_) => return Ok(signature_str),
|
||||
Err(TransactionError::AccountInUse) => {
|
||||
// Fetch a new blockhash and re-sign the transaction before sending it again
|
||||
self.resign_transaction(transaction, signer)?;
|
||||
self.resign_transaction(transaction, signer_keys)?;
|
||||
send_retries - 1
|
||||
}
|
||||
Err(_) => 0,
|
||||
@ -127,10 +127,10 @@ impl RpcClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_and_confirm_transactions(
|
||||
pub fn send_and_confirm_transactions<T: KeypairUtil>(
|
||||
&self,
|
||||
mut transactions: Vec<Transaction>,
|
||||
signer: &Keypair,
|
||||
signer_keys: &[&T],
|
||||
) -> Result<(), Box<dyn error::Error>> {
|
||||
let mut send_retries = 5;
|
||||
loop {
|
||||
@ -143,7 +143,7 @@ impl RpcClient {
|
||||
// Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors
|
||||
// when all the write transactions modify the same program account (eg, deploying a
|
||||
// new program)
|
||||
sleep(Duration::from_millis(1000 / NUM_TICKS_PER_SECOND));
|
||||
sleep(Duration::from_millis(1000 / DEFAULT_NUM_TICKS_PER_SECOND));
|
||||
}
|
||||
|
||||
let signature = self.send_transaction(&transaction).ok();
|
||||
@ -157,7 +157,7 @@ impl RpcClient {
|
||||
if cfg!(not(test)) {
|
||||
// Retry ~twice during a slot
|
||||
sleep(Duration::from_millis(
|
||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
||||
500 * DEFAULT_TICKS_PER_SLOT / DEFAULT_NUM_TICKS_PER_SECOND,
|
||||
));
|
||||
}
|
||||
|
||||
@ -187,12 +187,12 @@ impl RpcClient {
|
||||
send_retries -= 1;
|
||||
|
||||
// Re-sign any failed transactions with a new blockhash and retry
|
||||
let blockhash =
|
||||
let (blockhash, _fee_calculator) =
|
||||
self.get_new_blockhash(&transactions_signatures[0].0.message().recent_blockhash)?;
|
||||
transactions = transactions_signatures
|
||||
.into_iter()
|
||||
.map(|(mut transaction, _)| {
|
||||
transaction.sign(&[signer], blockhash);
|
||||
transaction.sign(signer_keys, blockhash);
|
||||
transaction
|
||||
})
|
||||
.collect();
|
||||
@ -202,10 +202,11 @@ impl RpcClient {
|
||||
pub fn resign_transaction<T: KeypairUtil>(
|
||||
&self,
|
||||
tx: &mut Transaction,
|
||||
signer_key: &T,
|
||||
signer_keys: &[&T],
|
||||
) -> Result<(), ClientError> {
|
||||
let blockhash = self.get_new_blockhash(&tx.message().recent_blockhash)?;
|
||||
tx.sign(&[signer_key], blockhash);
|
||||
let (blockhash, _fee_calculator) =
|
||||
self.get_new_blockhash(&tx.message().recent_blockhash)?;
|
||||
tx.sign(signer_keys, blockhash);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -222,31 +223,7 @@ impl RpcClient {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn get_account_data(&self, pubkey: &Pubkey) -> io::Result<Vec<u8>> {
|
||||
let params = json!([format!("{}", pubkey)]);
|
||||
let response = self
|
||||
.client
|
||||
.send(&RpcRequest::GetAccountInfo, Some(params), 0);
|
||||
match response {
|
||||
Ok(account_json) => {
|
||||
let account: Account =
|
||||
serde_json::from_value(account_json).expect("deserialize account");
|
||||
Ok(account.data)
|
||||
}
|
||||
Err(error) => {
|
||||
debug!("get_account_data failed: {:?}", error);
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"get_account_data failed",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request the balance of the user holding `pubkey`. This method blocks
|
||||
/// until the server sends a response. If the response packet is dropped
|
||||
/// by the network, this method will hang indefinitely.
|
||||
pub fn get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
|
||||
pub fn get_account(&self, pubkey: &Pubkey) -> io::Result<Account> {
|
||||
let params = json!([format!("{}", pubkey)]);
|
||||
let response = self
|
||||
.client
|
||||
@ -257,80 +234,89 @@ impl RpcClient {
|
||||
let account: Account =
|
||||
serde_json::from_value(account_json).expect("deserialize account");
|
||||
trace!("Response account {:?} {:?}", pubkey, account);
|
||||
trace!("get_balance {:?}", account.lamports);
|
||||
Ok(account.lamports)
|
||||
Ok(account)
|
||||
})
|
||||
.map_err(|error| {
|
||||
debug!("Response account {}: None (error: {:?})", pubkey, error);
|
||||
io::Error::new(io::ErrorKind::Other, "AccountNotFound")
|
||||
.map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("AccountNotFound: pubkey={}: {}", pubkey, err),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_account_data(&self, pubkey: &Pubkey) -> io::Result<Vec<u8>> {
|
||||
self.get_account(pubkey).map(|account| account.data)
|
||||
}
|
||||
|
||||
/// Request the balance of the user holding `pubkey`. This method blocks
|
||||
/// until the server sends a response. If the response packet is dropped
|
||||
/// by the network, this method will hang indefinitely.
|
||||
pub fn get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
|
||||
self.get_account(pubkey).map(|account| account.lamports)
|
||||
}
|
||||
|
||||
/// Request the transaction count. If the response packet is dropped by the network,
|
||||
/// this method will try again 5 times.
|
||||
pub fn get_transaction_count(&self) -> io::Result<u64> {
|
||||
debug!("get_transaction_count");
|
||||
|
||||
let mut num_retries = 5;
|
||||
while num_retries > 0 {
|
||||
let response = self.client.send(&RpcRequest::GetTransactionCount, None, 0);
|
||||
|
||||
match response {
|
||||
Ok(value) => {
|
||||
debug!("transaction_count response: {:?}", value);
|
||||
if let Some(transaction_count) = value.as_u64() {
|
||||
return Ok(transaction_count);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("transaction_count failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
num_retries -= 1;
|
||||
}
|
||||
Err(io::Error::new(
|
||||
let response = self
|
||||
.client
|
||||
.send(&RpcRequest::GetTransactionCount, None, 0)
|
||||
.map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Unable to get transaction count, too many retries",
|
||||
))?
|
||||
}
|
||||
format!("GetTransactionCount request failure: {:?}", err),
|
||||
)
|
||||
})?;
|
||||
|
||||
pub fn get_recent_blockhash(&self) -> io::Result<Hash> {
|
||||
let mut num_retries = 5;
|
||||
while num_retries > 0 {
|
||||
match self.client.send(&RpcRequest::GetRecentBlockhash, None, 0) {
|
||||
Ok(value) => {
|
||||
if let Some(blockhash_str) = value.as_str() {
|
||||
let blockhash_vec = bs58::decode(blockhash_str)
|
||||
.into_vec()
|
||||
.expect("bs58::decode");
|
||||
return Ok(Hash::new(&blockhash_vec));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("retry_get_recent_blockhash failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
num_retries -= 1;
|
||||
}
|
||||
Err(io::Error::new(
|
||||
serde_json::from_value(response).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Unable to get recent blockhash, too many retries",
|
||||
))
|
||||
format!("GetTransactionCount parse failure: {}", err),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<Hash> {
|
||||
pub fn get_recent_blockhash(&self) -> io::Result<(Hash, FeeCalculator)> {
|
||||
let response = self
|
||||
.client
|
||||
.send(&RpcRequest::GetRecentBlockhash, None, 0)
|
||||
.map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("GetRecentBlockhash request failure: {:?}", err),
|
||||
)
|
||||
})?;
|
||||
|
||||
let (blockhash, fee_calculator) =
|
||||
serde_json::from_value::<(String, FeeCalculator)>(response).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("GetRecentBlockhash parse failure: {:?}", err),
|
||||
)
|
||||
})?;
|
||||
|
||||
let blockhash = blockhash.parse().map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("GetRecentBlockhash parse failure: {:?}", err),
|
||||
)
|
||||
})?;
|
||||
Ok((blockhash, fee_calculator))
|
||||
}
|
||||
|
||||
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<(Hash, FeeCalculator)> {
|
||||
let mut num_retries = 10;
|
||||
while num_retries > 0 {
|
||||
if let Ok(new_blockhash) = self.get_recent_blockhash() {
|
||||
if let Ok((new_blockhash, fee_calculator)) = self.get_recent_blockhash() {
|
||||
if new_blockhash != *blockhash {
|
||||
return Ok(new_blockhash);
|
||||
return Ok((new_blockhash, fee_calculator));
|
||||
}
|
||||
}
|
||||
debug!("Got same blockhash ({:?}), will retry...", blockhash);
|
||||
|
||||
// Retry ~twice during a slot
|
||||
sleep(Duration::from_millis(
|
||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
||||
500 * DEFAULT_TICKS_PER_SLOT / DEFAULT_NUM_TICKS_PER_SECOND,
|
||||
));
|
||||
num_retries -= 1;
|
||||
}
|
||||
@ -482,24 +468,22 @@ impl RpcClient {
|
||||
Some(params.clone()),
|
||||
1,
|
||||
)
|
||||
.map_err(|error| {
|
||||
debug!(
|
||||
"Response get_num_blocks_since_signature_confirmation: {:?}",
|
||||
error
|
||||
);
|
||||
.map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"GetNumBlocksSinceSignatureConfirmation request failure",
|
||||
format!(
|
||||
"GetNumBlocksSinceSignatureConfirmation request failure: {}",
|
||||
err
|
||||
),
|
||||
)
|
||||
})?;
|
||||
serde_json::from_value(response).map_err(|error| {
|
||||
debug!(
|
||||
"ParseError: get_num_blocks_since_signature_confirmation: {}",
|
||||
error
|
||||
);
|
||||
serde_json::from_value(response).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"GetNumBlocksSinceSignatureConfirmation parse failure",
|
||||
format!(
|
||||
"GetNumBlocksSinceSignatureConfirmation parse failure: {}",
|
||||
err
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
@ -606,7 +590,7 @@ mod tests {
|
||||
// Send erroneous parameter
|
||||
let blockhash = rpc_client.retry_make_rpc_request(
|
||||
&RpcRequest::GetRecentBlockhash,
|
||||
Some(json!("paramter")),
|
||||
Some(json!("parameter")),
|
||||
0,
|
||||
);
|
||||
assert_eq!(blockhash.is_err(), true);
|
||||
@ -657,7 +641,7 @@ mod tests {
|
||||
let key = Keypair::new();
|
||||
let to = Pubkey::new_rand();
|
||||
let blockhash = Hash::default();
|
||||
let tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
||||
let tx = system_transaction::create_user_account(&key, &to, 50, blockhash);
|
||||
|
||||
let signature = rpc_client.send_transaction(&tx);
|
||||
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
|
||||
@ -671,16 +655,14 @@ mod tests {
|
||||
fn test_get_recent_blockhash() {
|
||||
let rpc_client = RpcClient::new_mock("succeeds".to_string());
|
||||
|
||||
let vec = bs58::decode(PUBKEY).into_vec().unwrap();
|
||||
let expected_blockhash = Hash::new(&vec);
|
||||
let expected_blockhash: Hash = PUBKEY.parse().unwrap();
|
||||
|
||||
let blockhash = dbg!(rpc_client.get_recent_blockhash()).expect("blockhash ok");
|
||||
let (blockhash, _fee_calculator) = rpc_client.get_recent_blockhash().expect("blockhash ok");
|
||||
assert_eq!(blockhash, expected_blockhash);
|
||||
|
||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||
|
||||
let blockhash = dbg!(rpc_client.get_recent_blockhash());
|
||||
assert!(blockhash.is_err());
|
||||
assert!(rpc_client.get_recent_blockhash().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -708,17 +690,17 @@ mod tests {
|
||||
let key = Keypair::new();
|
||||
let to = Pubkey::new_rand();
|
||||
let blockhash = Hash::default();
|
||||
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
||||
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash);
|
||||
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&key]);
|
||||
result.unwrap();
|
||||
|
||||
let rpc_client = RpcClient::new_mock("account_in_use".to_string());
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&key]);
|
||||
assert!(result.is_err());
|
||||
|
||||
let rpc_client = RpcClient::new_mock("fails".to_string());
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&key]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
@ -728,14 +710,13 @@ mod tests {
|
||||
|
||||
let key = Keypair::new();
|
||||
let to = Pubkey::new_rand();
|
||||
let vec = bs58::decode("HUu3LwEzGRsUkuJS121jzkPJW39Kq62pXCTmTa1F9jDL")
|
||||
.into_vec()
|
||||
let blockhash: Hash = "HUu3LwEzGRsUkuJS121jzkPJW39Kq62pXCTmTa1F9jDL"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let blockhash = Hash::new(&vec);
|
||||
let prev_tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
||||
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
|
||||
let prev_tx = system_transaction::create_user_account(&key, &to, 50, blockhash);
|
||||
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash);
|
||||
|
||||
rpc_client.resign_transaction(&mut tx, &key).unwrap();
|
||||
rpc_client.resign_transaction(&mut tx, &[&key]).unwrap();
|
||||
|
||||
assert_ne!(prev_tx, tx);
|
||||
assert_ne!(prev_tx.signatures, tx.signatures);
|
||||
|
@ -4,7 +4,7 @@ use crate::rpc_request::{RpcError, RpcRequest};
|
||||
use log::*;
|
||||
use reqwest;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
|
||||
use solana_sdk::timing::{DEFAULT_NUM_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
@ -73,7 +73,7 @@ impl GenericRpcClientRequest for RpcClientRequest {
|
||||
|
||||
// Sleep for approximately half a slot
|
||||
sleep(Duration::from_millis(
|
||||
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
|
||||
500 * DEFAULT_TICKS_PER_SLOT / DEFAULT_NUM_TICKS_PER_SECOND,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,10 @@ pub enum RpcRequest {
|
||||
GetRecentBlockhash,
|
||||
GetSignatureStatus,
|
||||
GetSlotLeader,
|
||||
GetEpochVoteAccounts,
|
||||
GetStorageBlockhash,
|
||||
GetStorageEntryHeight,
|
||||
GetStoragePubkeysForEntryHeight,
|
||||
GetStorageSlot,
|
||||
GetStoragePubkeysForSlot,
|
||||
GetTransactionCount,
|
||||
RegisterNode,
|
||||
RequestAirdrop,
|
||||
@ -39,9 +40,10 @@ impl RpcRequest {
|
||||
RpcRequest::GetRecentBlockhash => "getRecentBlockhash",
|
||||
RpcRequest::GetSignatureStatus => "getSignatureStatus",
|
||||
RpcRequest::GetSlotLeader => "getSlotLeader",
|
||||
RpcRequest::GetEpochVoteAccounts => "getEpochVoteAccounts",
|
||||
RpcRequest::GetStorageBlockhash => "getStorageBlockhash",
|
||||
RpcRequest::GetStorageEntryHeight => "getStorageEntryHeight",
|
||||
RpcRequest::GetStoragePubkeysForEntryHeight => "getStoragePubkeysForEntryHeight",
|
||||
RpcRequest::GetStorageSlot => "getStorageSlot",
|
||||
RpcRequest::GetStoragePubkeysForSlot => "getStoragePubkeysForSlot",
|
||||
RpcRequest::GetTransactionCount => "getTransactionCount",
|
||||
RpcRequest::RegisterNode => "registerNode",
|
||||
RpcRequest::RequestAirdrop => "requestAirdrop",
|
||||
|
@ -7,6 +7,7 @@ use crate::rpc_client::RpcClient;
|
||||
use bincode::{serialize_into, serialized_size};
|
||||
use log::*;
|
||||
use solana_sdk::client::{AsyncClient, Client, SyncClient};
|
||||
use solana_sdk::fee_calculator::FeeCalculator;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_sdk::message::Message;
|
||||
@ -107,7 +108,8 @@ impl ThinClient {
|
||||
return Ok(transaction.signatures[0]);
|
||||
}
|
||||
info!("{} tries failed transfer to {}", x, self.transactions_addr);
|
||||
transaction.sign(keypairs, self.rpc_client.get_recent_blockhash()?);
|
||||
let (blockhash, _fee_calculator) = self.rpc_client.get_recent_blockhash()?;
|
||||
transaction.sign(keypairs, blockhash);
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
@ -115,10 +117,6 @@ impl ThinClient {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<Hash> {
|
||||
self.rpc_client.get_new_blockhash(blockhash)
|
||||
}
|
||||
|
||||
pub fn poll_balance_with_timeout(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
@ -163,7 +161,7 @@ impl Client for ThinClient {
|
||||
|
||||
impl SyncClient for ThinClient {
|
||||
fn send_message(&self, keypairs: &[&Keypair], message: Message) -> TransportResult<Signature> {
|
||||
let blockhash = self.get_recent_blockhash()?;
|
||||
let (blockhash, _fee_calculator) = self.get_recent_blockhash()?;
|
||||
let mut transaction = Transaction::new(&keypairs, message, blockhash);
|
||||
let signature = self.send_and_confirm_transaction(keypairs, &mut transaction, 5, 0)?;
|
||||
Ok(signature)
|
||||
@ -214,9 +212,8 @@ impl SyncClient for ThinClient {
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn get_recent_blockhash(&self) -> TransportResult<Hash> {
|
||||
let recent_blockhash = self.rpc_client.get_recent_blockhash()?;
|
||||
Ok(recent_blockhash)
|
||||
fn get_recent_blockhash(&self) -> TransportResult<(Hash, FeeCalculator)> {
|
||||
Ok(self.rpc_client.get_recent_blockhash()?)
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> TransportResult<u64> {
|
||||
@ -238,6 +235,10 @@ impl SyncClient for ThinClient {
|
||||
fn poll_for_signature(&self, signature: &Signature) -> TransportResult<()> {
|
||||
Ok(self.rpc_client.poll_for_signature(signature)?)
|
||||
}
|
||||
|
||||
fn get_new_blockhash(&self, blockhash: &Hash) -> TransportResult<(Hash, FeeCalculator)> {
|
||||
Ok(self.rpc_client.get_new_blockhash(blockhash)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncClient for ThinClient {
|
||||
|
1
core/.gitignore
vendored
Normal file
1
core/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target/
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "solana"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
documentation = "https://docs.rs/solana"
|
||||
homepage = "https://solana.com/"
|
||||
readme = "../README.md"
|
||||
@ -20,12 +20,12 @@ erasure = []
|
||||
kvstore = ["solana-kvstore"]
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.2"
|
||||
bincode = "1.1.4"
|
||||
bs58 = "0.2.0"
|
||||
byteorder = "1.3.1"
|
||||
chrono = { version = "0.4.0", features = ["serde"] }
|
||||
crc = { version = "1.8.1", optional = true }
|
||||
ed25519-dalek = "1.0.0-pre.0"
|
||||
core_affinity = "0.5.9"
|
||||
hashbrown = "0.2.0"
|
||||
indexmap = "1.0"
|
||||
itertools = "0.8.0"
|
||||
@ -34,41 +34,48 @@ jsonrpc-derive = "11.0.0"
|
||||
jsonrpc-http-server = "11.0.0"
|
||||
jsonrpc-pubsub = "11.0.0"
|
||||
jsonrpc-ws-server = "11.0.0"
|
||||
libc = "0.2.50"
|
||||
libc = "0.2.55"
|
||||
log = "0.4.2"
|
||||
memmap = { version = "0.7.0", optional = true }
|
||||
nix = "0.13.0"
|
||||
nix = "0.14.0"
|
||||
rand = "0.6.5"
|
||||
rand_chacha = "0.1.1"
|
||||
rayon = "1.0.0"
|
||||
reed-solomon-erasure = "3.1.1"
|
||||
reqwest = "0.9.11"
|
||||
reqwest = "0.9.17"
|
||||
rocksdb = "0.11.0"
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.88"
|
||||
serde_derive = "1.0.91"
|
||||
serde_json = "1.0.39"
|
||||
solana-budget-api = { path = "../programs/budget_api", version = "0.14.0" }
|
||||
solana-client = { path = "../client", version = "0.14.0" }
|
||||
solana-drone = { path = "../drone", version = "0.14.0" }
|
||||
solana-kvstore = { path = "../kvstore", version = "0.14.0" , optional = true }
|
||||
solana-logger = { path = "../logger", version = "0.14.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.14.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.14.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.14.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.14.0" }
|
||||
solana-storage-api = { path = "../programs/storage_api", version = "0.14.0" }
|
||||
solana-vote-api = { path = "../programs/vote_api", version = "0.14.0" }
|
||||
solana-vote-signer = { path = "../vote-signer", version = "0.14.0" }
|
||||
solana-budget-api = { path = "../programs/budget_api", version = "0.15.0" }
|
||||
solana-budget-program = { path = "../programs/budget_program", version = "0.15.0" }
|
||||
solana-client = { path = "../client", version = "0.15.0" }
|
||||
solana-drone = { path = "../drone", version = "0.15.0" }
|
||||
solana-ed25519-dalek = "0.2.0"
|
||||
solana-kvstore = { path = "../kvstore", version = "0.15.0" , optional = true }
|
||||
solana-logger = { path = "../logger", version = "0.15.0" }
|
||||
solana-metrics = { path = "../metrics", version = "0.15.0" }
|
||||
solana-netutil = { path = "../netutil", version = "0.15.0" }
|
||||
solana-runtime = { path = "../runtime", version = "0.15.0" }
|
||||
solana-sdk = { path = "../sdk", version = "0.15.0" }
|
||||
solana-stake-api = { path = "../programs/stake_api", version = "0.15.0" }
|
||||
solana-stake-program = { path = "../programs/stake_program", version = "0.15.0" }
|
||||
solana-storage-api = { path = "../programs/storage_api", version = "0.15.0" }
|
||||
solana-storage-program = { path = "../programs/storage_program", version = "0.15.0" }
|
||||
solana-vote-api = { path = "../programs/vote_api", version = "0.15.0" }
|
||||
solana-vote-program = { path = "../programs/vote_program", version = "0.15.0" }
|
||||
solana-exchange-program = { path = "../programs/exchange_program", version = "0.15.0" }
|
||||
solana-config-program = { path = "../programs/config_program", version = "0.15.0" }
|
||||
solana-vote-signer = { path = "../vote-signer", version = "0.15.0" }
|
||||
sys-info = "0.5.6"
|
||||
tokio = "0.1"
|
||||
tokio-codec = "0.1"
|
||||
untrusted = "0.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.1.4"
|
||||
hex-literal = "0.2.0"
|
||||
matches = "0.1.6"
|
||||
solana-vote-program = { path = "../programs/vote_program", version = "0.14.0" }
|
||||
solana-budget-program = { path = "../programs/budget_program", version = "0.14.0" }
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "banking_stage"
|
||||
@ -85,6 +92,12 @@ name = "gen_keys"
|
||||
[[bench]]
|
||||
name = "sigverify"
|
||||
|
||||
[[bench]]
|
||||
name = "sigverify_stage"
|
||||
|
||||
[[bench]]
|
||||
name = "poh"
|
||||
|
||||
[[bench]]
|
||||
required-features = ["chacha"]
|
||||
name = "chacha"
|
||||
|
@ -4,31 +4,34 @@ extern crate test;
|
||||
#[macro_use]
|
||||
extern crate solana;
|
||||
|
||||
use log::*;
|
||||
use rand::{thread_rng, Rng};
|
||||
use rayon::prelude::*;
|
||||
use solana::banking_stage::{create_test_recorder, BankingStage};
|
||||
use solana::blocktree::{get_tmp_ledger_path, Blocktree};
|
||||
use solana::cluster_info::ClusterInfo;
|
||||
use solana::cluster_info::Node;
|
||||
use solana::leader_schedule_cache::LeaderScheduleCache;
|
||||
use solana::genesis_utils::{create_genesis_block, GenesisBlockInfo};
|
||||
use solana::packet::to_packets_chunked;
|
||||
use solana::poh_recorder::WorkingBankEntries;
|
||||
use solana::service::Service;
|
||||
use solana::test_tx::test_tx;
|
||||
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::signature::Signature;
|
||||
use solana_sdk::system_transaction;
|
||||
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, MAX_RECENT_BLOCKHASHES};
|
||||
use solana_sdk::timing::{
|
||||
duration_as_ms, timestamp, DEFAULT_TICKS_PER_SLOT, MAX_RECENT_BLOCKHASHES,
|
||||
};
|
||||
use std::iter;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc::{channel, Receiver};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
use test::Bencher;
|
||||
|
||||
fn check_txs(receiver: &Receiver<WorkingBankEntries>, ref_tx_count: usize) {
|
||||
fn check_txs(receiver: &Arc<Receiver<WorkingBankEntries>>, ref_tx_count: usize) {
|
||||
let mut total = 0;
|
||||
loop {
|
||||
let entries = receiver.recv_timeout(Duration::new(1, 0));
|
||||
@ -47,25 +50,63 @@ fn check_txs(receiver: &Receiver<WorkingBankEntries>, ref_tx_count: usize) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_consume_buffered(bencher: &mut Bencher) {
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(100_000);
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
let my_pubkey = Pubkey::new_rand();
|
||||
{
|
||||
let blocktree = Arc::new(
|
||||
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger"),
|
||||
);
|
||||
let (exit, poh_recorder, poh_service, _signal_receiver) =
|
||||
create_test_recorder(&bank, &blocktree);
|
||||
|
||||
let tx = test_tx();
|
||||
let len = 4096;
|
||||
let chunk_size = 1024;
|
||||
let batches = to_packets_chunked(&vec![tx; len], chunk_size);
|
||||
let mut packets = vec![];
|
||||
for batch in batches {
|
||||
let batch_len = batch.packets.len();
|
||||
packets.push((batch, vec![0usize; batch_len]));
|
||||
}
|
||||
// This tests the performance of buffering packets.
|
||||
// If the packet buffers are copied, performance will be poor.
|
||||
bencher.iter(move || {
|
||||
let _ignored =
|
||||
BankingStage::consume_buffered_packets(&my_pubkey, &poh_recorder, &mut packets);
|
||||
});
|
||||
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
poh_service.join().unwrap();
|
||||
}
|
||||
let _unused = Blocktree::destroy(&ledger_path);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
solana_logger::setup();
|
||||
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 txes = 192 * num_threads * 2;
|
||||
let mint_total = 1_000_000_000_000;
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
||||
let GenesisBlockInfo {
|
||||
mut genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(mint_total);
|
||||
|
||||
// Set a high ticks_per_slot so we don't run out of ticks
|
||||
// during the benchmark
|
||||
genesis_block.ticks_per_slot = 10_000;
|
||||
|
||||
let (verified_sender, verified_receiver) = channel();
|
||||
let (vote_sender, vote_receiver) = channel();
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank));
|
||||
let dummy = system_transaction::transfer(
|
||||
&mint_keypair,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
);
|
||||
let to_pubkey = Pubkey::new_rand();
|
||||
let dummy = system_transaction::transfer(&mint_keypair, &to_pubkey, 1, genesis_block.hash());
|
||||
trace!("txs: {}", txes);
|
||||
let transactions: Vec<_> = (0..txes)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
@ -86,7 +127,6 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
&tx.message.account_keys[0],
|
||||
mint_total / txes as u64,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
);
|
||||
let x = bank.process_transaction(&fund);
|
||||
x.unwrap();
|
||||
@ -124,25 +164,29 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
&poh_recorder,
|
||||
verified_receiver,
|
||||
vote_receiver,
|
||||
&leader_schedule_cache,
|
||||
);
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
|
||||
let mut id = genesis_block.hash();
|
||||
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
|
||||
id = hash(&id.as_ref());
|
||||
bank.register_tick(&id);
|
||||
}
|
||||
|
||||
let half_len = verified.len() / 2;
|
||||
let mut start = 0;
|
||||
|
||||
// This is so that the signal_receiver does not go out of scope after the closure.
|
||||
// If it is dropped before poh_service, then poh_service will error when
|
||||
// calling send() on the channel.
|
||||
let signal_receiver = Arc::new(signal_receiver);
|
||||
let signal_receiver2 = signal_receiver.clone();
|
||||
bencher.iter(move || {
|
||||
// make sure the transactions are still valid
|
||||
bank.register_tick(&genesis_block.hash());
|
||||
let now = Instant::now();
|
||||
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||
trace!("sending... {}..{} {}", start, start + half_len, timestamp());
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
}
|
||||
check_txs(&signal_receiver, txes / 2);
|
||||
check_txs(&signal_receiver2, txes / 2);
|
||||
trace!(
|
||||
"time: {} checked: {}",
|
||||
duration_as_ms(&now.elapsed()),
|
||||
txes / 2
|
||||
);
|
||||
bank.clear_signatures();
|
||||
start += half_len;
|
||||
start %= verified.len();
|
||||
@ -151,7 +195,7 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
poh_service.join().unwrap();
|
||||
}
|
||||
Blocktree::destroy(&ledger_path).unwrap();
|
||||
let _unused = Blocktree::destroy(&ledger_path);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
@ -162,19 +206,17 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||
// 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 GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(mint_total);
|
||||
|
||||
let (verified_sender, verified_receiver) = channel();
|
||||
let (vote_sender, vote_receiver) = channel();
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank));
|
||||
let dummy = system_transaction::transfer(
|
||||
&mint_keypair,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
);
|
||||
let to_pubkey = Pubkey::new_rand();
|
||||
let dummy = system_transaction::transfer(&mint_keypair, &to_pubkey, 1, genesis_block.hash());
|
||||
let transactions: Vec<_> = (0..txes)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
@ -211,7 +253,6 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||
&tx.message.account_keys[0],
|
||||
mint_total / txes as u64,
|
||||
genesis_block.hash(),
|
||||
0,
|
||||
);
|
||||
bank.process_transaction(&fund).unwrap();
|
||||
});
|
||||
@ -249,7 +290,6 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||
&poh_recorder,
|
||||
verified_receiver,
|
||||
vote_receiver,
|
||||
&leader_schedule_cache,
|
||||
);
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
|
||||
@ -261,13 +301,15 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||
|
||||
let half_len = verified.len() / 2;
|
||||
let mut start = 0;
|
||||
let signal_receiver = Arc::new(signal_receiver);
|
||||
let signal_receiver2 = signal_receiver.clone();
|
||||
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);
|
||||
check_txs(&signal_receiver2, txes / 2);
|
||||
bank.clear_signatures();
|
||||
start += half_len;
|
||||
start %= verified.len();
|
||||
|
@ -13,7 +13,7 @@ fn bench_block_to_blobs_to_block(bencher: &mut Bencher) {
|
||||
let zero = Hash::default();
|
||||
let one = hash(&zero.as_ref());
|
||||
let keypair = Keypair::new();
|
||||
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, one, 0);
|
||||
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, one);
|
||||
let transactions = vec![tx0; 10];
|
||||
let entries = next_entries(&zero, 1, transactions);
|
||||
|
||||
|
63
core/benches/poh.rs
Normal file
63
core/benches/poh.rs
Normal file
@ -0,0 +1,63 @@
|
||||
// This bench attempts to justify the value of `solana::poh_service::NUM_HASHES_PER_BATCH`
|
||||
|
||||
#![feature(test)]
|
||||
extern crate test;
|
||||
|
||||
use solana::poh::Poh;
|
||||
use solana::poh_service::NUM_HASHES_PER_BATCH;
|
||||
use solana_sdk::hash::Hash;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use test::Bencher;
|
||||
|
||||
const NUM_HASHES: u64 = 30_000; // Should require ~10ms on a 2017 MacBook Pro
|
||||
|
||||
#[bench]
|
||||
// No locking. Fastest.
|
||||
fn bench_poh_hash(bencher: &mut Bencher) {
|
||||
let mut poh = Poh::new(Hash::default(), None);
|
||||
bencher.iter(|| {
|
||||
poh.hash(NUM_HASHES);
|
||||
})
|
||||
}
|
||||
|
||||
#[bench]
|
||||
// Lock on each iteration. Slowest.
|
||||
fn bench_arc_mutex_poh_hash(bencher: &mut Bencher) {
|
||||
let poh = Arc::new(Mutex::new(Poh::new(Hash::default(), None)));
|
||||
bencher.iter(|| {
|
||||
for _ in 0..NUM_HASHES {
|
||||
poh.lock().unwrap().hash(1);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[bench]
|
||||
// Acquire lock every NUM_HASHES_PER_BATCH iterations.
|
||||
// Speed should be close to bench_poh_hash() if NUM_HASHES_PER_BATCH is set well.
|
||||
fn bench_arc_mutex_poh_batched_hash(bencher: &mut Bencher) {
|
||||
let poh = Arc::new(Mutex::new(Poh::new(Hash::default(), Some(NUM_HASHES))));
|
||||
//let exit = Arc::new(AtomicBool::new(false));
|
||||
let exit = Arc::new(AtomicBool::new(true));
|
||||
|
||||
bencher.iter(|| {
|
||||
// NOTE: This block attempts to look as close as possible to `PohService::tick_producer()`
|
||||
loop {
|
||||
if poh.lock().unwrap().hash(NUM_HASHES_PER_BATCH) {
|
||||
poh.lock().unwrap().tick().unwrap();
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[bench]
|
||||
// Worst case transaction record delay due to batch hashing at NUM_HASHES_PER_BATCH
|
||||
fn bench_poh_lock_time_per_batch(bencher: &mut Bencher) {
|
||||
let mut poh = Poh::new(Hash::default(), None);
|
||||
bencher.iter(|| {
|
||||
poh.hash(NUM_HASHES_PER_BATCH);
|
||||
})
|
||||
}
|
83
core/benches/sigverify_stage.rs
Normal file
83
core/benches/sigverify_stage.rs
Normal file
@ -0,0 +1,83 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate solana;
|
||||
extern crate test;
|
||||
|
||||
use log::*;
|
||||
use rand::{thread_rng, Rng};
|
||||
use solana::packet::to_packets_chunked;
|
||||
use solana::service::Service;
|
||||
use solana::sigverify_stage::SigVerifyStage;
|
||||
use solana::test_tx::test_tx;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction;
|
||||
use solana_sdk::timing::duration_as_ms;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::{Duration, Instant};
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_sigverify_stage(bencher: &mut Bencher) {
|
||||
solana_logger::setup();
|
||||
let (packet_s, packet_r) = channel();
|
||||
let (verified_s, verified_r) = channel();
|
||||
let sigverify_disabled = false;
|
||||
let stage = SigVerifyStage::new(packet_r, sigverify_disabled, verified_s);
|
||||
|
||||
let now = Instant::now();
|
||||
let len = 4096;
|
||||
let use_same_tx = true;
|
||||
let chunk_size = 1024;
|
||||
let mut batches = if use_same_tx {
|
||||
let tx = test_tx();
|
||||
to_packets_chunked(&vec![tx; len], chunk_size)
|
||||
} else {
|
||||
let from_keypair = Keypair::new();
|
||||
let to_keypair = Keypair::new();
|
||||
let txs: Vec<_> = (0..len)
|
||||
.into_iter()
|
||||
.map(|_| {
|
||||
let amount = thread_rng().gen();
|
||||
let tx = system_transaction::transfer(
|
||||
&from_keypair,
|
||||
&to_keypair.pubkey(),
|
||||
amount,
|
||||
Hash::default(),
|
||||
);
|
||||
tx
|
||||
})
|
||||
.collect();
|
||||
to_packets_chunked(&txs, chunk_size)
|
||||
};
|
||||
|
||||
trace!(
|
||||
"starting... generation took: {} ms batches: {}",
|
||||
duration_as_ms(&now.elapsed()),
|
||||
batches.len()
|
||||
);
|
||||
bencher.iter(move || {
|
||||
let mut sent_len = 0;
|
||||
for _ in 0..batches.len() {
|
||||
if let Some(batch) = batches.pop() {
|
||||
sent_len += batch.packets.len();
|
||||
packet_s.send(batch).unwrap();
|
||||
}
|
||||
}
|
||||
let mut received = 0;
|
||||
trace!("sent: {}", sent_len);
|
||||
loop {
|
||||
if let Ok(mut verifieds) = verified_r.recv_timeout(Duration::from_millis(10)) {
|
||||
while let Some(v) = verifieds.pop() {
|
||||
received += v.0.packets.len();
|
||||
batches.push(v.0);
|
||||
}
|
||||
if received >= sent_len {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
trace!("received: {}", received);
|
||||
});
|
||||
stage.join().unwrap();
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
//! The `bank_forks` module implments BankForks a DAG of checkpointed Banks
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use solana_metrics::counter::Counter;
|
||||
use solana_metrics::inc_new_counter_info;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_sdk::timing;
|
||||
use std::ops::Index;
|
||||
@ -11,6 +11,7 @@ use std::time::Instant;
|
||||
pub struct BankForks {
|
||||
banks: HashMap<u64, Arc<Bank>>,
|
||||
working_bank: Arc<Bank>,
|
||||
root: u64,
|
||||
}
|
||||
|
||||
impl Index<u64> for BankForks {
|
||||
@ -28,6 +29,7 @@ impl BankForks {
|
||||
Self {
|
||||
banks,
|
||||
working_bank,
|
||||
root: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +37,8 @@ impl BankForks {
|
||||
pub fn ancestors(&self) -> HashMap<u64, HashSet<u64>> {
|
||||
let mut ancestors = HashMap::new();
|
||||
for bank in self.banks.values() {
|
||||
let set = bank.parents().into_iter().map(|b| b.slot()).collect();
|
||||
let mut set: HashSet<u64> = bank.ancestors.keys().cloned().collect();
|
||||
set.remove(&bank.slot());
|
||||
ancestors.insert(bank.slot(), set);
|
||||
}
|
||||
ancestors
|
||||
@ -46,9 +49,11 @@ impl BankForks {
|
||||
let mut descendants = HashMap::new();
|
||||
for bank in self.banks.values() {
|
||||
let _ = descendants.entry(bank.slot()).or_insert(HashSet::new());
|
||||
for parent in bank.parents() {
|
||||
let mut set: HashSet<u64> = bank.ancestors.keys().cloned().collect();
|
||||
set.remove(&bank.slot());
|
||||
for parent in set {
|
||||
descendants
|
||||
.entry(parent.slot())
|
||||
.entry(parent)
|
||||
.or_insert(HashSet::new())
|
||||
.insert(bank.slot());
|
||||
}
|
||||
@ -76,13 +81,14 @@ impl BankForks {
|
||||
self.banks.get(&bank_slot)
|
||||
}
|
||||
|
||||
pub fn new_from_banks(initial_banks: &[Arc<Bank>]) -> Self {
|
||||
pub fn new_from_banks(initial_banks: &[Arc<Bank>], root: u64) -> Self {
|
||||
let mut banks = HashMap::new();
|
||||
let working_bank = initial_banks[0].clone();
|
||||
for bank in initial_banks {
|
||||
banks.insert(bank.slot(), bank.clone());
|
||||
}
|
||||
Self {
|
||||
root,
|
||||
banks,
|
||||
working_bank,
|
||||
}
|
||||
@ -102,35 +108,52 @@ impl BankForks {
|
||||
}
|
||||
|
||||
pub fn set_root(&mut self, root: u64) {
|
||||
self.root = root;
|
||||
let set_root_start = Instant::now();
|
||||
let root_bank = self
|
||||
.banks
|
||||
.get(&root)
|
||||
.expect("root bank didn't exist in bank_forks");
|
||||
let root_tx_count = root_bank
|
||||
.parents()
|
||||
.last()
|
||||
.map(|bank| bank.transaction_count())
|
||||
.unwrap_or(0);
|
||||
root_bank.squash();
|
||||
let new_tx_count = root_bank.transaction_count();
|
||||
self.prune_non_root(root);
|
||||
|
||||
inc_new_counter_info!(
|
||||
"bank-forks_set_root_ms",
|
||||
timing::duration_as_ms(&set_root_start.elapsed()) as usize
|
||||
);
|
||||
inc_new_counter_info!(
|
||||
"bank-forks_set_root_tx_count",
|
||||
(new_tx_count - root_tx_count) as usize
|
||||
);
|
||||
}
|
||||
|
||||
pub fn root(&self) -> u64 {
|
||||
self.root
|
||||
}
|
||||
|
||||
fn prune_non_root(&mut self, root: u64) {
|
||||
let descendants = self.descendants();
|
||||
self.banks
|
||||
.retain(|slot, bank| *slot >= root || bank.is_in_subtree_of(root))
|
||||
.retain(|slot, _| descendants[&root].contains(slot))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use crate::genesis_utils::{create_genesis_block, GenesisBlockInfo};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[test]
|
||||
fn test_bank_forks() {
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||
@ -142,7 +165,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_bank_forks_descendants() {
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let bank0 = bank_forks[0].clone();
|
||||
@ -159,7 +182,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_bank_forks_ancestors() {
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let bank0 = bank_forks[0].clone();
|
||||
@ -177,7 +200,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_bank_forks_frozen_banks() {
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||
@ -188,7 +211,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_bank_forks_active_banks() {
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let mut bank_forks = BankForks::new(0, bank);
|
||||
let child_bank = Bank::new_from_parent(&bank_forks[0u64], &Pubkey::default(), 1);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -65,14 +65,14 @@ pub trait BlockstreamEvents {
|
||||
&self,
|
||||
slot: u64,
|
||||
tick_height: u64,
|
||||
leader_id: &Pubkey,
|
||||
leader_pubkey: &Pubkey,
|
||||
entries: &Entry,
|
||||
) -> Result<()>;
|
||||
fn emit_block_event(
|
||||
&self,
|
||||
slot: u64,
|
||||
tick_height: u64,
|
||||
leader_id: &Pubkey,
|
||||
leader_pubkey: &Pubkey,
|
||||
blockhash: Hash,
|
||||
) -> Result<()>;
|
||||
}
|
||||
@ -90,7 +90,7 @@ where
|
||||
&self,
|
||||
slot: u64,
|
||||
tick_height: u64,
|
||||
leader_id: &Pubkey,
|
||||
leader_pubkey: &Pubkey,
|
||||
entry: &Entry,
|
||||
) -> Result<()> {
|
||||
let transactions: Vec<Vec<u8>> = serialize_transactions(entry);
|
||||
@ -105,7 +105,7 @@ where
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true),
|
||||
slot,
|
||||
tick_height,
|
||||
leader_id,
|
||||
leader_pubkey,
|
||||
json_entry,
|
||||
);
|
||||
self.output.write(payload)?;
|
||||
@ -116,7 +116,7 @@ where
|
||||
&self,
|
||||
slot: u64,
|
||||
tick_height: u64,
|
||||
leader_id: &Pubkey,
|
||||
leader_pubkey: &Pubkey,
|
||||
blockhash: Hash,
|
||||
) -> Result<()> {
|
||||
let payload = format!(
|
||||
@ -124,7 +124,7 @@ where
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true),
|
||||
slot,
|
||||
tick_height,
|
||||
leader_id,
|
||||
leader_pubkey,
|
||||
blockhash,
|
||||
);
|
||||
self.output.write(payload)?;
|
||||
@ -183,10 +183,8 @@ mod test {
|
||||
|
||||
let keypair0 = Keypair::new();
|
||||
let keypair1 = Keypair::new();
|
||||
let tx0 =
|
||||
system_transaction::transfer(&keypair0, &keypair1.pubkey(), 1, Hash::default(), 0);
|
||||
let tx1 =
|
||||
system_transaction::transfer(&keypair1, &keypair0.pubkey(), 2, Hash::default(), 0);
|
||||
let tx0 = system_transaction::transfer(&keypair0, &keypair1.pubkey(), 1, Hash::default());
|
||||
let tx1 = system_transaction::transfer(&keypair1, &keypair0.pubkey(), 2, Hash::default());
|
||||
let serialized_tx0 = serialize(&tx0).unwrap();
|
||||
let serialized_tx1 = serialize(&tx1).unwrap();
|
||||
let entry = Entry::new(&Hash::default(), 1, vec![tx0, tx1]);
|
||||
@ -208,19 +206,19 @@ mod test {
|
||||
let tick_height_initial = 0;
|
||||
let tick_height_final = tick_height_initial + ticks_per_slot + 2;
|
||||
let mut curr_slot = 0;
|
||||
let leader_id = Pubkey::new_rand();
|
||||
let leader_pubkey = Pubkey::new_rand();
|
||||
|
||||
for tick_height in tick_height_initial..=tick_height_final {
|
||||
if tick_height == 5 {
|
||||
blockstream
|
||||
.emit_block_event(curr_slot, tick_height - 1, &leader_id, blockhash)
|
||||
.emit_block_event(curr_slot, tick_height - 1, &leader_pubkey, blockhash)
|
||||
.unwrap();
|
||||
curr_slot += 1;
|
||||
}
|
||||
let entry = Entry::new(&mut blockhash, 1, vec![]); // just ticks
|
||||
blockhash = entry.hash;
|
||||
blockstream
|
||||
.emit_entry_event(curr_slot, tick_height, &leader_id, &entry)
|
||||
.emit_entry_event(curr_slot, tick_height, &leader_pubkey, &entry)
|
||||
.unwrap();
|
||||
expected_entries.push(entry.clone());
|
||||
entries.push(entry);
|
||||
|
@ -109,10 +109,10 @@ mod test {
|
||||
use super::*;
|
||||
use crate::blocktree::create_new_tmp_ledger;
|
||||
use crate::entry::{create_ticks, Entry};
|
||||
use crate::genesis_utils::{create_genesis_block, GenesisBlockInfo};
|
||||
use bincode::{deserialize, serialize};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde_json::Value;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_transaction;
|
||||
@ -121,10 +121,12 @@ mod test {
|
||||
#[test]
|
||||
fn test_blockstream_service_process_entries() {
|
||||
let ticks_per_slot = 5;
|
||||
let leader_id = Pubkey::new_rand();
|
||||
let leader_pubkey = Pubkey::new_rand();
|
||||
|
||||
// Set up genesis block and blocktree
|
||||
let (mut genesis_block, _mint_keypair) = GenesisBlock::new(1000);
|
||||
let GenesisBlockInfo {
|
||||
mut genesis_block, ..
|
||||
} = create_genesis_block(1000);
|
||||
genesis_block.ticks_per_slot = ticks_per_slot;
|
||||
|
||||
let (ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block);
|
||||
@ -146,7 +148,6 @@ mod test {
|
||||
&keypair.pubkey(),
|
||||
1,
|
||||
Hash::default(),
|
||||
0,
|
||||
);
|
||||
let entry = Entry::new(&mut blockhash, 1, vec![tx]);
|
||||
blockhash = entry.hash;
|
||||
@ -161,7 +162,7 @@ mod test {
|
||||
.write_entries(1, 0, 0, ticks_per_slot, &entries)
|
||||
.unwrap();
|
||||
|
||||
slot_full_sender.send((1, leader_id)).unwrap();
|
||||
slot_full_sender.send((1, leader_pubkey)).unwrap();
|
||||
BlockstreamService::process_entries(
|
||||
&slot_full_receiver,
|
||||
&Arc::new(blocktree),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod columns {
|
||||
#[derive(Debug)]
|
||||
@ -30,6 +31,10 @@ pub mod columns {
|
||||
#[derive(Debug)]
|
||||
/// The erasure meta column
|
||||
pub struct ErasureMeta;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// The root column
|
||||
pub struct Root;
|
||||
}
|
||||
|
||||
pub trait Backend: Sized + Send + Sync {
|
||||
@ -55,7 +60,7 @@ pub trait Backend: Sized + Send + Sync {
|
||||
|
||||
fn delete_cf(&self, cf: Self::ColumnFamily, key: &Self::Key) -> Result<()>;
|
||||
|
||||
fn iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Iter>;
|
||||
fn iterator_cf(&self, cf: Self::ColumnFamily, from: Option<&Self::Key>) -> Result<Self::Iter>;
|
||||
|
||||
fn raw_iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Cursor>;
|
||||
|
||||
@ -112,7 +117,15 @@ pub struct Database<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
backend: B,
|
||||
backend: Arc<B>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BatchProcessor<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
backend: Arc<B>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -132,7 +145,7 @@ where
|
||||
B: Backend,
|
||||
C: Column<B>,
|
||||
{
|
||||
backend: PhantomData<B>,
|
||||
backend: Arc<B>,
|
||||
column: PhantomData<C>,
|
||||
}
|
||||
|
||||
@ -151,7 +164,7 @@ where
|
||||
B: Backend,
|
||||
{
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let backend = B::open(path)?;
|
||||
let backend = Arc::new(B::open(path)?);
|
||||
|
||||
Ok(Database { backend })
|
||||
}
|
||||
@ -170,7 +183,7 @@ where
|
||||
.get_cf(self.cf_handle::<C>(), C::key(key).borrow())
|
||||
}
|
||||
|
||||
pub fn put_bytes<C>(&mut self, key: C::Index, data: &[u8]) -> Result<()>
|
||||
pub fn put_bytes<C>(&self, key: C::Index, data: &[u8]) -> Result<()>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
@ -178,7 +191,7 @@ where
|
||||
.put_cf(self.cf_handle::<C>(), C::key(key).borrow(), data)
|
||||
}
|
||||
|
||||
pub fn delete<C>(&mut self, key: C::Index) -> Result<()>
|
||||
pub fn delete<C>(&self, key: C::Index) -> Result<()>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
@ -202,7 +215,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put<C>(&mut self, key: C::Index, value: &C::Type) -> Result<()>
|
||||
pub fn put<C>(&self, key: C::Index, value: &C::Type) -> Result<()>
|
||||
where
|
||||
C: TypedColumn<B>,
|
||||
{
|
||||
@ -228,18 +241,60 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter<C>(&self) -> Result<impl Iterator<Item = (C::Index, Vec<u8>)>>
|
||||
pub fn iter<C>(
|
||||
&self,
|
||||
start_from: Option<C::Index>,
|
||||
) -> Result<impl Iterator<Item = (C::Index, Box<[u8]>)>>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
let iter = self
|
||||
.backend
|
||||
.iterator_cf(self.cf_handle::<C>())?
|
||||
.map(|(key, value)| (C::index(&key), value.into()));
|
||||
let iter = {
|
||||
if let Some(index) = start_from {
|
||||
let key = C::key(index);
|
||||
self.backend
|
||||
.iterator_cf(self.cf_handle::<C>(), Some(key.borrow()))?
|
||||
} else {
|
||||
self.backend.iterator_cf(self.cf_handle::<C>(), None)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(iter)
|
||||
Ok(iter.map(|(key, value)| (C::index(&key), value)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cf_handle<C>(&self) -> B::ColumnFamily
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
self.backend.cf_handle(C::NAME).clone()
|
||||
}
|
||||
|
||||
pub fn column<C>(&self) -> LedgerColumn<B, C>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
LedgerColumn {
|
||||
backend: Arc::clone(&self.backend),
|
||||
column: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
// Note this returns an object that can be used to directly write to multiple column families.
|
||||
// This circumvents the synchronization around APIs that in Blocktree that use
|
||||
// blocktree.batch_processor, so this API should only be used if the caller is sure they
|
||||
// are writing to data in columns that will not be corrupted by any simultaneous blocktree
|
||||
// operations.
|
||||
pub unsafe fn batch_processor(&self) -> BatchProcessor<B> {
|
||||
BatchProcessor {
|
||||
backend: Arc::clone(&self.backend),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> BatchProcessor<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
pub fn batch(&mut self) -> Result<WriteBatch<B>> {
|
||||
let db_write_batch = self.backend.batch()?;
|
||||
let map = self
|
||||
@ -259,24 +314,6 @@ where
|
||||
pub fn write(&mut self, batch: WriteBatch<B>) -> Result<()> {
|
||||
self.backend.write(batch.write_batch)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cf_handle<C>(&self) -> B::ColumnFamily
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
self.backend.cf_handle(C::NAME).clone()
|
||||
}
|
||||
|
||||
pub fn column<C>(&self) -> LedgerColumn<B, C>
|
||||
where
|
||||
C: Column<B>,
|
||||
{
|
||||
LedgerColumn {
|
||||
backend: PhantomData,
|
||||
column: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C> Cursor<B, C>
|
||||
@ -333,41 +370,55 @@ where
|
||||
B: Backend,
|
||||
C: Column<B>,
|
||||
{
|
||||
pub fn get_bytes(&self, db: &Database<B>, key: C::Index) -> Result<Option<Vec<u8>>> {
|
||||
db.backend.get_cf(self.handle(db), C::key(key).borrow())
|
||||
pub fn get_bytes(&self, key: C::Index) -> Result<Option<Vec<u8>>> {
|
||||
self.backend.get_cf(self.handle(), C::key(key).borrow())
|
||||
}
|
||||
|
||||
pub fn cursor(&self, db: &Database<B>) -> Result<Cursor<B, C>> {
|
||||
db.cursor()
|
||||
pub fn cursor(&self) -> Result<Cursor<B, C>> {
|
||||
let db_cursor = self.backend.raw_iterator_cf(self.handle())?;
|
||||
|
||||
Ok(Cursor {
|
||||
db_cursor,
|
||||
column: PhantomData,
|
||||
backend: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self, db: &Database<B>) -> Result<impl Iterator<Item = (C::Index, Vec<u8>)>> {
|
||||
db.iter::<C>()
|
||||
pub fn iter(
|
||||
&self,
|
||||
start_from: Option<C::Index>,
|
||||
) -> Result<impl Iterator<Item = (C::Index, Box<[u8]>)>> {
|
||||
let iter = {
|
||||
if let Some(index) = start_from {
|
||||
let key = C::key(index);
|
||||
self.backend
|
||||
.iterator_cf(self.handle(), Some(key.borrow()))?
|
||||
} else {
|
||||
self.backend.iterator_cf(self.handle(), None)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(iter.map(|(key, value)| (C::index(&key), value)))
|
||||
}
|
||||
|
||||
pub fn handle(&self, db: &Database<B>) -> B::ColumnFamily {
|
||||
db.cf_handle::<C>()
|
||||
#[inline]
|
||||
pub fn handle(&self) -> B::ColumnFamily {
|
||||
self.backend.cf_handle(C::NAME).clone()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self, db: &Database<B>) -> Result<bool> {
|
||||
let mut cursor = self.cursor(db)?;
|
||||
pub fn is_empty(&self) -> Result<bool> {
|
||||
let mut cursor = self.cursor()?;
|
||||
cursor.seek_to_first();
|
||||
Ok(!cursor.valid())
|
||||
}
|
||||
|
||||
pub fn put_bytes(&self, key: C::Index, value: &[u8]) -> Result<()> {
|
||||
self.backend
|
||||
.put_cf(self.handle(), C::key(key).borrow(), value)
|
||||
}
|
||||
|
||||
impl<B, C> LedgerColumn<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: Column<B>,
|
||||
{
|
||||
pub fn put_bytes(&self, db: &mut Database<B>, key: C::Index, value: &[u8]) -> Result<()> {
|
||||
db.backend
|
||||
.put_cf(self.handle(db), C::key(key).borrow(), value)
|
||||
}
|
||||
|
||||
pub fn delete(&self, db: &mut Database<B>, key: C::Index) -> Result<()> {
|
||||
db.backend.delete_cf(self.handle(db), C::key(key).borrow())
|
||||
pub fn delete(&self, key: C::Index) -> Result<()> {
|
||||
self.backend.delete_cf(self.handle(), C::key(key).borrow())
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,18 +427,21 @@ where
|
||||
B: Backend,
|
||||
C: TypedColumn<B>,
|
||||
{
|
||||
pub fn get(&self, db: &Database<B>, key: C::Index) -> Result<Option<C::Type>> {
|
||||
db.get::<C>(key)
|
||||
pub fn get(&self, key: C::Index) -> Result<Option<C::Type>> {
|
||||
if let Some(serialized_value) = self.backend.get_cf(self.handle(), C::key(key).borrow())? {
|
||||
let value = deserialize(&serialized_value)?;
|
||||
|
||||
Ok(Some(value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C> LedgerColumn<B, C>
|
||||
where
|
||||
B: Backend,
|
||||
C: TypedColumn<B>,
|
||||
{
|
||||
pub fn put(&self, db: &mut Database<B>, key: C::Index, value: &C::Type) -> Result<()> {
|
||||
db.put::<C>(key, value)
|
||||
pub fn put(&self, key: C::Index, value: &C::Type) -> Result<()> {
|
||||
let serialized_value = serialize(value)?;
|
||||
|
||||
self.backend
|
||||
.put_cf(self.handle(), C::key(key).borrow(), &serialized_value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +119,40 @@ impl TypedColumn<Kvs> for cf::Orphans {
|
||||
type Type = bool;
|
||||
}
|
||||
|
||||
impl Column<Kvs> for cf::Root {
|
||||
const NAME: &'static str = super::ROOT_CF;
|
||||
type Index = u64;
|
||||
|
||||
fn key(slot: u64) -> Key {
|
||||
let mut key = Key::default();
|
||||
BigEndian::write_u64(&mut key.0[8..16], slot);
|
||||
key
|
||||
}
|
||||
|
||||
fn index(key: &Key) -> u64 {
|
||||
BigEndian::read_u64(&key.0[8..16])
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedColumn<Kvs> for cf::Root {
|
||||
type Type = bool;
|
||||
}
|
||||
|
||||
impl Column<Kvs> for cf::SlotMeta {
|
||||
const NAME: &'static str = super::META_CF;
|
||||
type Index = u64;
|
||||
|
||||
fn key(slot: u64) -> Key {
|
||||
let mut key = Key::default();
|
||||
BigEndian::write_u64(&mut key.0[8..16], slot);
|
||||
key
|
||||
}
|
||||
|
||||
fn index(key: &Key) -> u64 {
|
||||
BigEndian::read_u64(&key.0[8..16])
|
||||
}
|
||||
}
|
||||
|
||||
impl Column<Kvs> for cf::SlotMeta {
|
||||
const NAME: &'static str = super::META_CF;
|
||||
type Index = u64;
|
||||
|
@ -1,4 +1,6 @@
|
||||
use crate::erasure::{NUM_CODING, NUM_DATA};
|
||||
use solana_metrics::datapoint;
|
||||
use std::borrow::Borrow;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
|
||||
// The Meta column family
|
||||
@ -23,8 +25,6 @@ pub struct SlotMeta {
|
||||
// True if this slot is full (consumed == last_index + 1) and if every
|
||||
// slot that is a parent of this slot is also connected.
|
||||
pub is_connected: bool,
|
||||
// True if this slot is a root
|
||||
pub is_root: bool,
|
||||
}
|
||||
|
||||
impl SlotMeta {
|
||||
@ -38,17 +38,17 @@ impl SlotMeta {
|
||||
|
||||
// Should never happen
|
||||
if self.consumed > self.last_index + 1 {
|
||||
solana_metrics::submit(
|
||||
solana_metrics::influxdb::Point::new("blocktree_error")
|
||||
.add_field(
|
||||
datapoint!(
|
||||
"blocktree_error",
|
||||
(
|
||||
"error",
|
||||
solana_metrics::influxdb::Value::String(format!(
|
||||
format!(
|
||||
"Observed a slot meta with consumed: {} > meta.last_index + 1: {}",
|
||||
self.consumed,
|
||||
self.last_index + 1
|
||||
)),
|
||||
),
|
||||
String
|
||||
)
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -67,7 +67,6 @@ impl SlotMeta {
|
||||
parent_slot,
|
||||
next_slots: vec![],
|
||||
is_connected: slot == 0,
|
||||
is_root: false,
|
||||
last_index: std::u64::MAX,
|
||||
}
|
||||
}
|
||||
@ -81,9 +80,9 @@ pub struct ErasureMeta {
|
||||
/// Size of shards in this erasure set
|
||||
pub size: usize,
|
||||
/// Bitfield representing presence/absence of data blobs
|
||||
pub data: u64,
|
||||
data: u64,
|
||||
/// Bitfield representing presence/absence of coding blobs
|
||||
pub coding: u64,
|
||||
coding: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -104,10 +103,8 @@ impl ErasureMeta {
|
||||
}
|
||||
|
||||
pub fn status(&self) -> ErasureMetaStatus {
|
||||
let (data_missing, coding_missing) = (
|
||||
NUM_DATA - self.data.count_ones() as usize,
|
||||
NUM_CODING - self.coding.count_ones() as usize,
|
||||
);
|
||||
let (data_missing, coding_missing) =
|
||||
(NUM_DATA - self.num_data(), NUM_CODING - self.num_coding());
|
||||
if data_missing > 0 && data_missing + coding_missing <= NUM_CODING {
|
||||
assert!(self.size != 0);
|
||||
ErasureMetaStatus::CanRecover
|
||||
@ -118,6 +115,14 @@ impl ErasureMeta {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_coding(&self) -> usize {
|
||||
self.coding.count_ones() as usize
|
||||
}
|
||||
|
||||
pub fn num_data(&self) -> usize {
|
||||
self.data.count_ones() as usize
|
||||
}
|
||||
|
||||
pub fn is_coding_present(&self, index: u64) -> bool {
|
||||
if let Some(position) = self.data_index_in_set(index) {
|
||||
self.coding & (1 << position) != 0
|
||||
@ -162,6 +167,26 @@ impl ErasureMeta {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_data_multi<I, Idx>(&mut self, indexes: I, present: bool)
|
||||
where
|
||||
I: IntoIterator<Item = Idx>,
|
||||
Idx: Borrow<u64>,
|
||||
{
|
||||
for index in indexes.into_iter() {
|
||||
self.set_data_present(*index.borrow(), present);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_coding_multi<I, Idx>(&mut self, indexes: I, present: bool)
|
||||
where
|
||||
I: IntoIterator<Item = Idx>,
|
||||
Idx: Borrow<u64>,
|
||||
{
|
||||
for index in indexes.into_iter() {
|
||||
self.set_coding_present(*index.borrow(), present);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_index_for(index: u64) -> u64 {
|
||||
index / NUM_DATA as u64
|
||||
}
|
||||
@ -201,7 +226,7 @@ fn test_meta_indexes() {
|
||||
|
||||
for _ in 0..100 {
|
||||
let set_index = rng.gen_range(0, 1_000);
|
||||
let blob_index = (set_index * NUM_DATA) + rng.gen_range(0, 16);
|
||||
let blob_index = (set_index * NUM_DATA) + rng.gen_range(0, NUM_DATA);
|
||||
|
||||
assert_eq!(set_index, ErasureMeta::set_index_for(blob_index));
|
||||
let e_meta = ErasureMeta::new(set_index);
|
||||
@ -236,8 +261,8 @@ fn test_meta_indexes() {
|
||||
fn test_meta_coding_present() {
|
||||
let mut e_meta = ErasureMeta::default();
|
||||
|
||||
e_meta.set_coding_multi(0..NUM_CODING as u64, true);
|
||||
for i in 0..NUM_CODING as u64 {
|
||||
e_meta.set_coding_present(i, true);
|
||||
assert_eq!(e_meta.is_coding_present(i), true);
|
||||
}
|
||||
for i in NUM_CODING as u64..NUM_DATA as u64 {
|
||||
@ -245,60 +270,62 @@ fn test_meta_coding_present() {
|
||||
}
|
||||
|
||||
e_meta.set_index = ErasureMeta::set_index_for((NUM_DATA * 17) as u64);
|
||||
let start_idx = e_meta.start_index();
|
||||
e_meta.set_coding_multi(start_idx..start_idx + NUM_CODING as u64, true);
|
||||
|
||||
for i in (NUM_DATA * 17) as u64..((NUM_DATA * 17) + NUM_CODING) as u64 {
|
||||
for i in start_idx..start_idx + NUM_CODING as u64 {
|
||||
e_meta.set_coding_present(i, true);
|
||||
assert_eq!(e_meta.is_coding_present(i), true);
|
||||
}
|
||||
for i in (NUM_DATA * 17 + NUM_CODING) as u64..((NUM_DATA * 17) + NUM_DATA) as u64 {
|
||||
for i in start_idx + NUM_CODING as u64..start_idx + NUM_DATA as u64 {
|
||||
assert_eq!(e_meta.is_coding_present(i), false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_erasure_meta_status() {
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
// Local constansts just used to avoid repetitive casts
|
||||
const N_DATA: u64 = crate::erasure::NUM_DATA as u64;
|
||||
const N_CODING: u64 = crate::erasure::NUM_CODING as u64;
|
||||
|
||||
let mut e_meta = ErasureMeta::default();
|
||||
let mut rng = thread_rng();
|
||||
let data_indexes: Vec<u64> = (0..N_DATA).collect();
|
||||
let coding_indexes: Vec<u64> = (0..N_CODING).collect();
|
||||
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::StillNeed(NUM_DATA));
|
||||
|
||||
e_meta.data = 0b1111_1111_1111_1111;
|
||||
e_meta.coding = 0x00;
|
||||
e_meta.set_data_multi(0..N_DATA, true);
|
||||
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::DataFull);
|
||||
|
||||
e_meta.coding = 0x0e;
|
||||
e_meta.size = 1;
|
||||
e_meta.set_coding_multi(0..N_CODING, true);
|
||||
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::DataFull);
|
||||
|
||||
e_meta.data = 0b0111_1111_1111_1111;
|
||||
for &idx in data_indexes.choose_multiple(&mut rng, NUM_CODING) {
|
||||
e_meta.set_data_present(idx, false);
|
||||
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
}
|
||||
|
||||
e_meta.data = 0b0111_1111_1111_1110;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
e_meta.set_data_multi(0..N_DATA, true);
|
||||
|
||||
e_meta.data = 0b0111_1111_1011_1110;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
for &idx in coding_indexes.choose_multiple(&mut rng, NUM_CODING) {
|
||||
e_meta.set_coding_present(idx, false);
|
||||
|
||||
e_meta.data = 0b0111_1011_1011_1110;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::StillNeed(1));
|
||||
|
||||
e_meta.data = 0b0111_1011_1011_1110;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::StillNeed(1));
|
||||
|
||||
e_meta.coding = 0b0000_1110;
|
||||
e_meta.data = 0b1111_1111_1111_1100;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
|
||||
e_meta.data = 0b1111_1111_1111_1000;
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::CanRecover);
|
||||
assert_eq!(e_meta.status(), ErasureMetaStatus::DataFull);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_meta_data_present() {
|
||||
let mut e_meta = ErasureMeta::default();
|
||||
|
||||
e_meta.set_data_multi(0..NUM_DATA as u64, true);
|
||||
for i in 0..NUM_DATA as u64 {
|
||||
e_meta.set_data_present(i, true);
|
||||
assert_eq!(e_meta.is_data_present(i), true);
|
||||
}
|
||||
for i in NUM_DATA as u64..2 * NUM_DATA as u64 {
|
||||
@ -306,12 +333,13 @@ fn test_meta_data_present() {
|
||||
}
|
||||
|
||||
e_meta.set_index = ErasureMeta::set_index_for((NUM_DATA * 23) as u64);
|
||||
let start_idx = e_meta.start_index();
|
||||
e_meta.set_data_multi(start_idx..start_idx + NUM_DATA as u64, true);
|
||||
|
||||
for i in (NUM_DATA * 23) as u64..(NUM_DATA * 24) as u64 {
|
||||
e_meta.set_data_present(i, true);
|
||||
for i in start_idx..start_idx + NUM_DATA as u64 {
|
||||
assert_eq!(e_meta.is_data_present(i), true);
|
||||
}
|
||||
for i in (NUM_DATA * 22) as u64..(NUM_DATA * 23) as u64 {
|
||||
for i in start_idx - NUM_DATA as u64..start_idx {
|
||||
assert_eq!(e_meta.is_data_present(i), false);
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ use crate::result::{Error, Result};
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
|
||||
use rocksdb::{
|
||||
self, ColumnFamily, ColumnFamilyDescriptor, DBIterator, DBRawIterator, IteratorMode, Options,
|
||||
WriteBatch as RWriteBatch, DB,
|
||||
self, ColumnFamily, ColumnFamilyDescriptor, DBIterator, DBRawIterator, Direction, IteratorMode,
|
||||
Options, WriteBatch as RWriteBatch, DB,
|
||||
};
|
||||
|
||||
use std::fs;
|
||||
@ -30,7 +30,7 @@ impl Backend for Rocks {
|
||||
type Error = rocksdb::Error;
|
||||
|
||||
fn open(path: &Path) -> Result<Rocks> {
|
||||
use crate::blocktree::db::columns::{Coding, Data, ErasureMeta, Orphans, SlotMeta};
|
||||
use crate::blocktree::db::columns::{Coding, Data, ErasureMeta, Orphans, Root, SlotMeta};
|
||||
|
||||
fs::create_dir_all(&path)?;
|
||||
|
||||
@ -44,6 +44,7 @@ impl Backend for Rocks {
|
||||
let erasure_meta_cf_descriptor =
|
||||
ColumnFamilyDescriptor::new(ErasureMeta::NAME, get_cf_options());
|
||||
let orphans_cf_descriptor = ColumnFamilyDescriptor::new(Orphans::NAME, get_cf_options());
|
||||
let root_cf_descriptor = ColumnFamilyDescriptor::new(Root::NAME, get_cf_options());
|
||||
|
||||
let cfs = vec![
|
||||
meta_cf_descriptor,
|
||||
@ -51,6 +52,7 @@ impl Backend for Rocks {
|
||||
erasure_cf_descriptor,
|
||||
erasure_meta_cf_descriptor,
|
||||
orphans_cf_descriptor,
|
||||
root_cf_descriptor,
|
||||
];
|
||||
|
||||
// Open the database
|
||||
@ -60,13 +62,14 @@ impl Backend for Rocks {
|
||||
}
|
||||
|
||||
fn columns(&self) -> Vec<&'static str> {
|
||||
use crate::blocktree::db::columns::{Coding, Data, ErasureMeta, Orphans, SlotMeta};
|
||||
use crate::blocktree::db::columns::{Coding, Data, ErasureMeta, Orphans, Root, SlotMeta};
|
||||
|
||||
vec![
|
||||
Coding::NAME,
|
||||
ErasureMeta::NAME,
|
||||
Data::NAME,
|
||||
Orphans::NAME,
|
||||
Root::NAME,
|
||||
SlotMeta::NAME,
|
||||
]
|
||||
}
|
||||
@ -98,10 +101,17 @@ impl Backend for Rocks {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn iterator_cf(&self, cf: ColumnFamily) -> Result<DBIterator> {
|
||||
let raw_iter = self.0.iterator_cf(cf, IteratorMode::Start)?;
|
||||
fn iterator_cf(&self, cf: ColumnFamily, start_from: Option<&[u8]>) -> Result<DBIterator> {
|
||||
let iter = {
|
||||
if let Some(start_from) = start_from {
|
||||
self.0
|
||||
.iterator_cf(cf, IteratorMode::From(start_from, Direction::Forward))?
|
||||
} else {
|
||||
self.0.iterator_cf(cf, IteratorMode::Start)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(raw_iter)
|
||||
Ok(iter)
|
||||
}
|
||||
|
||||
fn raw_iterator_cf(&self, cf: ColumnFamily) -> Result<DBRawIterator> {
|
||||
@ -170,6 +180,25 @@ impl TypedColumn<Rocks> for cf::Orphans {
|
||||
type Type = bool;
|
||||
}
|
||||
|
||||
impl Column<Rocks> for cf::Root {
|
||||
const NAME: &'static str = super::ROOT_CF;
|
||||
type Index = u64;
|
||||
|
||||
fn key(slot: u64) -> Vec<u8> {
|
||||
let mut key = vec![0; 8];
|
||||
BigEndian::write_u64(&mut key[..], slot);
|
||||
key
|
||||
}
|
||||
|
||||
fn index(key: &[u8]) -> u64 {
|
||||
BigEndian::read_u64(&key[..8])
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedColumn<Rocks> for cf::Root {
|
||||
type Type = bool;
|
||||
}
|
||||
|
||||
impl Column<Rocks> for cf::SlotMeta {
|
||||
const NAME: &'static str = super::META_CF;
|
||||
type Index = u64;
|
||||
|
126
core/src/blocktree/rooted_slot_iterator.rs
Normal file
126
core/src/blocktree/rooted_slot_iterator.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use super::*;
|
||||
|
||||
pub struct RootedSlotIterator<'a> {
|
||||
next_slots: Vec<u64>,
|
||||
blocktree: &'a super::Blocktree,
|
||||
}
|
||||
|
||||
impl<'a> RootedSlotIterator<'a> {
|
||||
pub fn new(start_slot: u64, blocktree: &'a super::Blocktree) -> Result<Self> {
|
||||
if blocktree.is_root(start_slot) {
|
||||
Ok(Self {
|
||||
next_slots: vec![start_slot],
|
||||
blocktree,
|
||||
})
|
||||
} else {
|
||||
Err(Error::BlocktreeError(BlocktreeError::SlotNotRooted))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for RootedSlotIterator<'a> {
|
||||
type Item = (u64, super::SlotMeta);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// Clone b/c passing the closure to the map below requires exclusive access to
|
||||
// `self`, which is borrowed here if we don't clone.
|
||||
let rooted_slot = self
|
||||
.next_slots
|
||||
.iter()
|
||||
.find(|x| self.blocktree.is_root(**x))
|
||||
.cloned();
|
||||
|
||||
rooted_slot.map(|rooted_slot| {
|
||||
let slot_meta = self
|
||||
.blocktree
|
||||
.meta(rooted_slot)
|
||||
.expect("Database failure, couldnt fetch SlotMeta")
|
||||
.expect("SlotMeta in iterator didn't exist");
|
||||
|
||||
self.next_slots = slot_meta.next_slots.clone();
|
||||
(rooted_slot, slot_meta)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::blocktree_processor::tests::fill_blocktree_slot_with_ticks;
|
||||
|
||||
#[test]
|
||||
fn test_rooted_slot_iterator() {
|
||||
let blocktree_path = get_tmp_ledger_path("test_rooted_slot_iterator");
|
||||
let blocktree = Blocktree::open(&blocktree_path).unwrap();
|
||||
blocktree.set_root(0, 0).unwrap();
|
||||
let ticks_per_slot = 5;
|
||||
/*
|
||||
Build a blocktree in the ledger with the following fork structure:
|
||||
|
||||
slot 0
|
||||
|
|
||||
slot 1 <-- set_root(true)
|
||||
/ \
|
||||
slot 2 |
|
||||
/ |
|
||||
slot 3 |
|
||||
|
|
||||
slot 4
|
||||
|
||||
*/
|
||||
|
||||
// Fork 1, ending at slot 3
|
||||
let last_entry_hash = Hash::default();
|
||||
let fork_point = 1;
|
||||
let mut fork_hash = Hash::default();
|
||||
for slot in 0..=3 {
|
||||
let parent = {
|
||||
if slot == 0 {
|
||||
0
|
||||
} else {
|
||||
slot - 1
|
||||
}
|
||||
};
|
||||
let last_entry_hash = fill_blocktree_slot_with_ticks(
|
||||
&blocktree,
|
||||
ticks_per_slot,
|
||||
slot,
|
||||
parent,
|
||||
last_entry_hash,
|
||||
);
|
||||
|
||||
if slot == fork_point {
|
||||
fork_hash = last_entry_hash;
|
||||
}
|
||||
}
|
||||
|
||||
// Fork 2, ending at slot 4
|
||||
let _ =
|
||||
fill_blocktree_slot_with_ticks(&blocktree, ticks_per_slot, 4, fork_point, fork_hash);
|
||||
|
||||
// Set a root
|
||||
blocktree.set_root(3, 0).unwrap();
|
||||
|
||||
// Trying to get an iterator on a different fork will error
|
||||
assert!(RootedSlotIterator::new(4, &blocktree).is_err());
|
||||
|
||||
// Trying to get an iterator on any slot on the root fork should succeed
|
||||
let result: Vec<_> = RootedSlotIterator::new(3, &blocktree)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(slot, _)| slot)
|
||||
.collect();
|
||||
let expected = vec![3];
|
||||
assert_eq!(result, expected);
|
||||
|
||||
let result: Vec<_> = RootedSlotIterator::new(0, &blocktree)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(slot, _)| slot)
|
||||
.collect();
|
||||
let expected = vec![0, 1, 2, 3];
|
||||
assert_eq!(result, expected);
|
||||
|
||||
drop(blocktree);
|
||||
Blocktree::destroy(&blocktree_path).expect("Expected successful database destruction");
|
||||
}
|
||||
}
|
@ -3,26 +3,32 @@ use crate::blocktree::Blocktree;
|
||||
use crate::entry::{Entry, EntrySlice};
|
||||
use crate::leader_schedule_cache::LeaderScheduleCache;
|
||||
use rayon::prelude::*;
|
||||
use solana_metrics::counter::Counter;
|
||||
use solana_metrics::{datapoint, datapoint_error, inc_new_counter_debug};
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::locked_accounts_results::LockedAccountsResults;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::timing::duration_as_ms;
|
||||
use solana_sdk::timing::MAX_RECENT_BLOCKHASHES;
|
||||
use solana_sdk::transaction::Result;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use std::result;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn first_err(results: &[Result<()>]) -> Result<()> {
|
||||
for r in results {
|
||||
r.clone()?;
|
||||
if r.is_err() {
|
||||
return r.clone();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn par_execute_entries(bank: &Bank, entries: &[(&Entry, LockedAccountsResults)]) -> Result<()> {
|
||||
inc_new_counter_info!("bank-par_execute_entries-count", entries.len());
|
||||
fn par_execute_entries(
|
||||
bank: &Bank,
|
||||
entries: &[(&Entry, LockedAccountsResults<Transaction>)],
|
||||
) -> Result<()> {
|
||||
inc_new_counter_debug!("bank-par_execute_entries-count", entries.len());
|
||||
let results: Vec<Result<()>> = entries
|
||||
.into_par_iter()
|
||||
.map(|(e, locked_accounts)| {
|
||||
@ -32,21 +38,17 @@ fn par_execute_entries(bank: &Bank, entries: &[(&Entry, LockedAccountsResults)])
|
||||
MAX_RECENT_BLOCKHASHES,
|
||||
);
|
||||
let mut first_err = None;
|
||||
for r in results {
|
||||
for (r, tx) in results.iter().zip(e.transactions.iter()) {
|
||||
if let Err(ref e) = r {
|
||||
if first_err.is_none() {
|
||||
first_err = Some(r.clone());
|
||||
}
|
||||
if !Bank::can_commit(&r) {
|
||||
warn!("Unexpected validator error: {:?}", e);
|
||||
solana_metrics::submit(
|
||||
solana_metrics::influxdb::Point::new("validator_process_entry_error")
|
||||
.add_field(
|
||||
"error",
|
||||
solana_metrics::influxdb::Value::String(format!("{:?}", e)),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
warn!("Unexpected validator error: {:?}, tx: {:?}", e, tx);
|
||||
datapoint_error!(
|
||||
"validator_process_entry_error",
|
||||
("error", format!("error: {:?}, tx: {:?}", e, tx), String)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,25 +71,47 @@ pub fn process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
|
||||
if entry.is_tick() {
|
||||
// if its a tick, execute the group and register the tick
|
||||
par_execute_entries(bank, &mt_group)?;
|
||||
bank.register_tick(&entry.hash);
|
||||
mt_group = vec![];
|
||||
bank.register_tick(&entry.hash);
|
||||
continue;
|
||||
}
|
||||
// else loop on processing the entry
|
||||
loop {
|
||||
// try to lock the accounts
|
||||
let lock_results = bank.lock_accounts(&entry.transactions);
|
||||
// if any of the locks error out
|
||||
// execute the current group
|
||||
if first_err(lock_results.locked_accounts_results()).is_err() {
|
||||
par_execute_entries(bank, &mt_group)?;
|
||||
// Drop all the locks on accounts by clearing the LockedAccountsFinalizer's in the
|
||||
// mt_group
|
||||
mt_group = vec![];
|
||||
drop(lock_results);
|
||||
let lock_results = bank.lock_accounts(&entry.transactions);
|
||||
mt_group.push((entry, lock_results));
|
||||
} else {
|
||||
|
||||
let first_lock_err = first_err(lock_results.locked_accounts_results());
|
||||
|
||||
// if locking worked
|
||||
if first_lock_err.is_ok() {
|
||||
// push the entry to the mt_group
|
||||
mt_group.push((entry, lock_results));
|
||||
// done with this entry
|
||||
break;
|
||||
}
|
||||
// else we failed to lock, 2 possible reasons
|
||||
if mt_group.is_empty() {
|
||||
// An entry has account lock conflicts with *itself*, which should not happen
|
||||
// if generated by a properly functioning leader
|
||||
datapoint!(
|
||||
"validator_process_entry_error",
|
||||
(
|
||||
"error",
|
||||
format!(
|
||||
"Lock accounts error, entry conflicts with itself, txs: {:?}",
|
||||
entry.transactions
|
||||
),
|
||||
String
|
||||
)
|
||||
);
|
||||
// bail
|
||||
first_lock_err?;
|
||||
} else {
|
||||
// else we have an entry that conflicts with a prior entry
|
||||
// execute the current queue and try to process this entry again
|
||||
par_execute_entries(bank, &mt_group)?;
|
||||
mt_group = vec![];
|
||||
}
|
||||
}
|
||||
}
|
||||
par_execute_entries(bank, &mt_group)?;
|
||||
@ -131,10 +155,13 @@ pub fn process_blocktree(
|
||||
vec![(slot, meta, bank, entry_height, last_entry_hash)]
|
||||
};
|
||||
|
||||
let leader_schedule_cache = LeaderScheduleCache::new(*pending_slots[0].2.epoch_schedule());
|
||||
blocktree.set_root(0, 0).expect("Couldn't set first root");
|
||||
|
||||
let leader_schedule_cache = LeaderScheduleCache::new(*pending_slots[0].2.epoch_schedule(), 0);
|
||||
|
||||
let mut fork_info = vec![];
|
||||
let mut last_status_report = Instant::now();
|
||||
let mut root = 0;
|
||||
while !pending_slots.is_empty() {
|
||||
let (slot, meta, bank, mut entry_height, mut last_entry_hash) =
|
||||
pending_slots.pop().unwrap();
|
||||
@ -188,7 +215,11 @@ pub fn process_blocktree(
|
||||
bank.freeze(); // all banks handled by this routine are created from complete slots
|
||||
|
||||
if blocktree.is_root(slot) {
|
||||
root = slot;
|
||||
leader_schedule_cache.set_root(slot);
|
||||
bank.squash();
|
||||
pending_slots.clear();
|
||||
fork_info.clear();
|
||||
}
|
||||
|
||||
if meta.next_slots.is_empty() {
|
||||
@ -217,7 +248,7 @@ pub fn process_blocktree(
|
||||
let next_bank = Arc::new(Bank::new_from_parent(
|
||||
&bank,
|
||||
&leader_schedule_cache
|
||||
.slot_leader_at_else_compute(next_slot, &bank)
|
||||
.slot_leader_at(next_slot, Some(&bank))
|
||||
.unwrap(),
|
||||
next_slot,
|
||||
));
|
||||
@ -245,7 +276,7 @@ pub fn process_blocktree(
|
||||
}
|
||||
|
||||
let (banks, bank_forks_info): (Vec<_>, Vec<_>) = fork_info.into_iter().unzip();
|
||||
let bank_forks = BankForks::new_from_banks(&banks);
|
||||
let bank_forks = BankForks::new_from_banks(&banks, root);
|
||||
info!(
|
||||
"processing ledger...complete in {}ms, forks={}...",
|
||||
duration_as_ms(&now.elapsed()),
|
||||
@ -256,12 +287,15 @@ pub fn process_blocktree(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::blocktree::create_new_tmp_ledger;
|
||||
use crate::blocktree::tests::entries_to_blobs;
|
||||
use crate::entry::{create_ticks, next_entry, Entry};
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use crate::entry::{create_ticks, next_entry, next_entry_mut, Entry};
|
||||
use crate::genesis_utils::{
|
||||
create_genesis_block, create_genesis_block_with_leader, GenesisBlockInfo,
|
||||
};
|
||||
use solana_runtime::epoch_schedule::EpochSchedule;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
@ -269,7 +303,7 @@ mod tests {
|
||||
use solana_sdk::system_transaction;
|
||||
use solana_sdk::transaction::TransactionError;
|
||||
|
||||
fn fill_blocktree_slot_with_ticks(
|
||||
pub fn fill_blocktree_slot_with_ticks(
|
||||
blocktree: &Blocktree,
|
||||
ticks_per_slot: u64,
|
||||
slot: u64,
|
||||
@ -289,7 +323,7 @@ mod tests {
|
||||
fn test_process_blocktree_with_incomplete_slot() {
|
||||
solana_logger::setup();
|
||||
|
||||
let (genesis_block, _mint_keypair) = GenesisBlock::new(10_000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let ticks_per_slot = genesis_block.ticks_per_slot;
|
||||
|
||||
/*
|
||||
@ -342,11 +376,85 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_blocktree_with_two_forks_and_squash() {
|
||||
solana_logger::setup();
|
||||
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let ticks_per_slot = genesis_block.ticks_per_slot;
|
||||
|
||||
// Create a new ledger with slot 0 full of ticks
|
||||
let (ledger_path, blockhash) = create_new_tmp_ledger!(&genesis_block);
|
||||
debug!("ledger_path: {:?}", ledger_path);
|
||||
let mut last_entry_hash = blockhash;
|
||||
|
||||
/*
|
||||
Build a blocktree in the ledger with the following fork structure:
|
||||
|
||||
slot 0
|
||||
|
|
||||
slot 1
|
||||
/ \
|
||||
slot 2 |
|
||||
/ |
|
||||
slot 3 |
|
||||
|
|
||||
slot 4 <-- set_root(true)
|
||||
|
||||
*/
|
||||
let blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to successfully open database ledger");
|
||||
|
||||
// Fork 1, ending at slot 3
|
||||
let last_slot1_entry_hash =
|
||||
fill_blocktree_slot_with_ticks(&blocktree, ticks_per_slot, 1, 0, last_entry_hash);
|
||||
last_entry_hash =
|
||||
fill_blocktree_slot_with_ticks(&blocktree, ticks_per_slot, 2, 1, last_slot1_entry_hash);
|
||||
let last_fork1_entry_hash =
|
||||
fill_blocktree_slot_with_ticks(&blocktree, ticks_per_slot, 3, 2, last_entry_hash);
|
||||
|
||||
// Fork 2, ending at slot 4
|
||||
let last_fork2_entry_hash =
|
||||
fill_blocktree_slot_with_ticks(&blocktree, ticks_per_slot, 4, 1, last_slot1_entry_hash);
|
||||
|
||||
info!("last_fork1_entry.hash: {:?}", last_fork1_entry_hash);
|
||||
info!("last_fork2_entry.hash: {:?}", last_fork2_entry_hash);
|
||||
|
||||
blocktree.set_root(4, 0).unwrap();
|
||||
|
||||
let (bank_forks, bank_forks_info, _) =
|
||||
process_blocktree(&genesis_block, &blocktree, None).unwrap();
|
||||
|
||||
assert_eq!(bank_forks_info.len(), 1); // One fork, other one is ignored b/c not a descendant of the root
|
||||
|
||||
assert_eq!(
|
||||
bank_forks_info[0],
|
||||
BankForksInfo {
|
||||
bank_slot: 4, // Fork 2's head is slot 4
|
||||
entry_height: ticks_per_slot * 3,
|
||||
}
|
||||
);
|
||||
assert!(&bank_forks[4]
|
||||
.parents()
|
||||
.iter()
|
||||
.map(|bank| bank.slot())
|
||||
.collect::<Vec<_>>()
|
||||
.is_empty());
|
||||
|
||||
// Ensure bank_forks holds the right banks
|
||||
for info in bank_forks_info {
|
||||
assert_eq!(bank_forks[info.bank_slot].slot(), info.bank_slot);
|
||||
assert!(bank_forks[info.bank_slot].is_frozen());
|
||||
}
|
||||
|
||||
assert_eq!(bank_forks.root(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_blocktree_with_two_forks() {
|
||||
solana_logger::setup();
|
||||
|
||||
let (genesis_block, _mint_keypair) = GenesisBlock::new(10_000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let ticks_per_slot = genesis_block.ticks_per_slot;
|
||||
|
||||
// Create a new ledger with slot 0 full of ticks
|
||||
@ -386,8 +494,8 @@ mod tests {
|
||||
info!("last_fork1_entry.hash: {:?}", last_fork1_entry_hash);
|
||||
info!("last_fork2_entry.hash: {:?}", last_fork2_entry_hash);
|
||||
|
||||
blocktree.set_root(0).unwrap();
|
||||
blocktree.set_root(1).unwrap();
|
||||
blocktree.set_root(0, 0).unwrap();
|
||||
blocktree.set_root(1, 0).unwrap();
|
||||
|
||||
let (bank_forks, bank_forks_info, _) =
|
||||
process_blocktree(&genesis_block, &blocktree, None).unwrap();
|
||||
@ -424,6 +532,8 @@ mod tests {
|
||||
&[1]
|
||||
);
|
||||
|
||||
assert_eq!(bank_forks.root(), 1);
|
||||
|
||||
// Ensure bank_forks holds the right banks
|
||||
for info in bank_forks_info {
|
||||
assert_eq!(bank_forks[info.bank_slot].slot(), info.bank_slot);
|
||||
@ -431,6 +541,63 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_blocktree_epoch_boundary_root() {
|
||||
solana_logger::setup();
|
||||
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let ticks_per_slot = genesis_block.ticks_per_slot;
|
||||
|
||||
// Create a new ledger with slot 0 full of ticks
|
||||
let (ledger_path, blockhash) = create_new_tmp_ledger!(&genesis_block);
|
||||
let mut last_entry_hash = blockhash;
|
||||
|
||||
let blocktree =
|
||||
Blocktree::open(&ledger_path).expect("Expected to successfully open database ledger");
|
||||
|
||||
// Let last_slot be the number of slots in the first two epochs
|
||||
let epoch_schedule = get_epoch_schedule(&genesis_block, None);
|
||||
let last_slot = epoch_schedule.get_last_slot_in_epoch(1);
|
||||
|
||||
// Create a single chain of slots with all indexes in the range [0, last_slot + 1]
|
||||
for i in 1..=last_slot + 1 {
|
||||
last_entry_hash = fill_blocktree_slot_with_ticks(
|
||||
&blocktree,
|
||||
ticks_per_slot,
|
||||
i,
|
||||
i - 1,
|
||||
last_entry_hash,
|
||||
);
|
||||
}
|
||||
|
||||
// Set a root on the last slot of the last confirmed epoch
|
||||
blocktree.set_root(last_slot, 0).unwrap();
|
||||
|
||||
// Set a root on the next slot of the confrimed epoch
|
||||
blocktree.set_root(last_slot + 1, last_slot).unwrap();
|
||||
|
||||
// Check that we can properly restart the ledger / leader scheduler doesn't fail
|
||||
let (bank_forks, bank_forks_info, _) =
|
||||
process_blocktree(&genesis_block, &blocktree, None).unwrap();
|
||||
|
||||
assert_eq!(bank_forks_info.len(), 1); // There is one fork
|
||||
assert_eq!(
|
||||
bank_forks_info[0],
|
||||
BankForksInfo {
|
||||
bank_slot: last_slot + 1, // Head is last_slot + 1
|
||||
entry_height: ticks_per_slot * (last_slot + 2),
|
||||
}
|
||||
);
|
||||
|
||||
// The latest root should have purged all its parents
|
||||
assert!(&bank_forks[last_slot + 1]
|
||||
.parents()
|
||||
.iter()
|
||||
.map(|bank| bank.slot())
|
||||
.collect::<Vec<_>>()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_err() {
|
||||
assert_eq!(first_err(&[Ok(())]), Ok(()));
|
||||
@ -468,7 +635,11 @@ mod tests {
|
||||
fn test_process_empty_entry_is_registered() {
|
||||
solana_logger::setup();
|
||||
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(2);
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(2);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair = Keypair::new();
|
||||
let slot_entries = create_ticks(genesis_block.ticks_per_slot - 1, genesis_block.hash());
|
||||
@ -477,7 +648,6 @@ mod tests {
|
||||
&keypair.pubkey(),
|
||||
1,
|
||||
slot_entries.last().unwrap().hash,
|
||||
0,
|
||||
);
|
||||
|
||||
// First, ensure the TX is rejected because of the unregistered last ID
|
||||
@ -495,13 +665,19 @@ mod tests {
|
||||
fn test_process_ledger_simple() {
|
||||
solana_logger::setup();
|
||||
let leader_pubkey = Pubkey::new_rand();
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new_with_leader(100, &leader_pubkey, 50);
|
||||
let mint = 100;
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block_with_leader(mint, &leader_pubkey, 50);
|
||||
let (ledger_path, mut last_entry_hash) = create_new_tmp_ledger!(&genesis_block);
|
||||
debug!("ledger_path: {:?}", ledger_path);
|
||||
|
||||
let deducted_from_mint = 3;
|
||||
let mut entries = vec![];
|
||||
let blockhash = genesis_block.hash();
|
||||
for _ in 0..3 {
|
||||
for _ in 0..deducted_from_mint {
|
||||
// Transfer one token from the mint to a random account
|
||||
let keypair = Keypair::new();
|
||||
let tx = system_transaction::create_user_account(
|
||||
@ -509,7 +685,6 @@ mod tests {
|
||||
&keypair.pubkey(),
|
||||
1,
|
||||
blockhash,
|
||||
0,
|
||||
);
|
||||
let entry = Entry::new(&last_entry_hash, 1, vec![tx]);
|
||||
last_entry_hash = entry.hash;
|
||||
@ -523,7 +698,6 @@ mod tests {
|
||||
&keypair2.pubkey(),
|
||||
42,
|
||||
blockhash,
|
||||
0,
|
||||
);
|
||||
let entry = Entry::new(&last_entry_hash, 1, vec![tx]);
|
||||
last_entry_hash = entry.hash;
|
||||
@ -543,6 +717,7 @@ mod tests {
|
||||
process_blocktree(&genesis_block, &blocktree, None).unwrap();
|
||||
|
||||
assert_eq!(bank_forks_info.len(), 1);
|
||||
assert_eq!(bank_forks.root(), 0);
|
||||
assert_eq!(
|
||||
bank_forks_info[0],
|
||||
BankForksInfo {
|
||||
@ -552,14 +727,19 @@ mod tests {
|
||||
);
|
||||
|
||||
let bank = bank_forks[1].clone();
|
||||
assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 50 - 3);
|
||||
assert_eq!(
|
||||
bank.get_balance(&mint_keypair.pubkey()),
|
||||
mint - deducted_from_mint
|
||||
);
|
||||
assert_eq!(bank.tick_height(), 2 * genesis_block.ticks_per_slot - 1);
|
||||
assert_eq!(bank.last_blockhash(), entries.last().unwrap().hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_ledger_with_one_tick_per_slot() {
|
||||
let (mut genesis_block, _mint_keypair) = GenesisBlock::new(123);
|
||||
let GenesisBlockInfo {
|
||||
mut genesis_block, ..
|
||||
} = create_genesis_block(123);
|
||||
genesis_block.ticks_per_slot = 1;
|
||||
let (ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block);
|
||||
|
||||
@ -581,7 +761,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_process_entries_tick() {
|
||||
let (genesis_block, _mint_keypair) = GenesisBlock::new(1000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
|
||||
// ensure bank can process a tick
|
||||
@ -593,7 +773,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_process_entries_2_entries_collision() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
@ -606,7 +790,6 @@ mod tests {
|
||||
&keypair1.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
|
||||
let tx = system_transaction::create_user_account(
|
||||
@ -614,7 +797,6 @@ mod tests {
|
||||
&keypair2.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
let entry_2 = next_entry(&entry_1.hash, 1, vec![tx]);
|
||||
assert_eq!(process_entries(&bank, &[entry_1, entry_2]), Ok(()));
|
||||
@ -625,7 +807,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_process_entries_2_txes_collision() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
@ -644,7 +830,6 @@ mod tests {
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
@ -657,14 +842,12 @@ mod tests {
|
||||
&keypair3.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
), // should be fine
|
||||
system_transaction::create_user_account(
|
||||
&keypair1,
|
||||
&mint_keypair.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
), // will collide
|
||||
],
|
||||
);
|
||||
@ -681,7 +864,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_process_entries_2_txes_collision_and_error() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
@ -703,14 +890,12 @@ mod tests {
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
),
|
||||
system_transaction::transfer(
|
||||
&keypair4,
|
||||
&keypair4.pubkey(),
|
||||
1,
|
||||
Hash::default(), // Should cause a transaction failure with BlockhashNotFound
|
||||
0,
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -724,14 +909,12 @@ mod tests {
|
||||
&keypair3.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
), // should be fine
|
||||
system_transaction::create_user_account(
|
||||
&keypair1,
|
||||
&mint_keypair.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
), // will collide
|
||||
],
|
||||
);
|
||||
@ -761,9 +944,109 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_entries_2nd_entry_collision_with_self_and_error() {
|
||||
solana_logger::setup();
|
||||
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
let keypair3 = Keypair::new();
|
||||
|
||||
// fund: put some money in each of 1 and 2
|
||||
assert_matches!(bank.transfer(5, &mint_keypair, &keypair1.pubkey()), Ok(_));
|
||||
assert_matches!(bank.transfer(4, &mint_keypair, &keypair2.pubkey()), Ok(_));
|
||||
|
||||
// 3 entries: first has a transfer, 2nd has a conflict with 1st, 3rd has a conflict with itself
|
||||
let entry_1_to_mint = next_entry(
|
||||
&bank.last_blockhash(),
|
||||
1,
|
||||
vec![system_transaction::transfer(
|
||||
&keypair1,
|
||||
&mint_keypair.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
)],
|
||||
);
|
||||
// should now be:
|
||||
// keypair1=4
|
||||
// keypair2=4
|
||||
// keypair3=0
|
||||
|
||||
let entry_2_to_3_and_1_to_mint = next_entry(
|
||||
&entry_1_to_mint.hash,
|
||||
1,
|
||||
vec![
|
||||
system_transaction::create_user_account(
|
||||
&keypair2,
|
||||
&keypair3.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
), // should be fine
|
||||
system_transaction::transfer(
|
||||
&keypair1,
|
||||
&mint_keypair.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
), // will collide with predecessor
|
||||
],
|
||||
);
|
||||
// should now be:
|
||||
// keypair1=2
|
||||
// keypair2=2
|
||||
// keypair3=2
|
||||
|
||||
let entry_conflict_itself = next_entry(
|
||||
&entry_2_to_3_and_1_to_mint.hash,
|
||||
1,
|
||||
vec![
|
||||
system_transaction::transfer(
|
||||
&keypair1,
|
||||
&keypair3.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
),
|
||||
system_transaction::transfer(
|
||||
&keypair1,
|
||||
&keypair2.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
), // should be fine
|
||||
],
|
||||
);
|
||||
// would now be:
|
||||
// keypair1=0
|
||||
// keypair2=3
|
||||
// keypair3=3
|
||||
|
||||
assert!(process_entries(
|
||||
&bank,
|
||||
&[
|
||||
entry_1_to_mint.clone(),
|
||||
entry_2_to_3_and_1_to_mint.clone(),
|
||||
entry_conflict_itself.clone()
|
||||
]
|
||||
)
|
||||
.is_err());
|
||||
|
||||
// last entry should have been aborted before par_execute_entries
|
||||
assert_eq!(bank.get_balance(&keypair1.pubkey()), 2);
|
||||
assert_eq!(bank.get_balance(&keypair2.pubkey()), 2);
|
||||
assert_eq!(bank.get_balance(&keypair3.pubkey()), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_entries_2_entries_par() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
@ -776,7 +1059,6 @@ mod tests {
|
||||
&keypair1.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
assert_eq!(bank.process_transaction(&tx), Ok(()));
|
||||
let tx = system_transaction::create_user_account(
|
||||
@ -784,7 +1066,6 @@ mod tests {
|
||||
&keypair2.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
assert_eq!(bank.process_transaction(&tx), Ok(()));
|
||||
|
||||
@ -795,7 +1076,6 @@ mod tests {
|
||||
&keypair3.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
|
||||
let tx = system_transaction::create_user_account(
|
||||
@ -803,7 +1083,6 @@ mod tests {
|
||||
&keypair4.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
let entry_2 = next_entry(&entry_1.hash, 1, vec![tx]);
|
||||
assert_eq!(process_entries(&bank, &[entry_1, entry_2]), Ok(()));
|
||||
@ -814,7 +1093,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_process_entries_2_entries_tick() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(1000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
@ -827,7 +1110,6 @@ mod tests {
|
||||
&keypair1.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
assert_eq!(bank.process_transaction(&tx), Ok(()));
|
||||
let tx = system_transaction::create_user_account(
|
||||
@ -835,7 +1117,6 @@ mod tests {
|
||||
&keypair2.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
assert_eq!(bank.process_transaction(&tx), Ok(()));
|
||||
|
||||
@ -846,7 +1127,7 @@ mod tests {
|
||||
|
||||
// ensure bank can process 2 entries that do not have a common account and tick is registered
|
||||
let tx =
|
||||
system_transaction::create_user_account(&keypair2, &keypair3.pubkey(), 1, blockhash, 0);
|
||||
system_transaction::create_user_account(&keypair2, &keypair3.pubkey(), 1, blockhash);
|
||||
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
|
||||
let tick = next_entry(&entry_1.hash, 1, vec![]);
|
||||
let tx = system_transaction::create_user_account(
|
||||
@ -854,7 +1135,6 @@ mod tests {
|
||||
&keypair4.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
let entry_2 = next_entry(&tick.hash, 1, vec![tx]);
|
||||
assert_eq!(
|
||||
@ -870,7 +1150,6 @@ mod tests {
|
||||
&keypair3.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
let entry_3 = next_entry(&entry_2.hash, 1, vec![tx]);
|
||||
assert_eq!(
|
||||
@ -882,7 +1161,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_update_transaction_statuses() {
|
||||
// Make sure instruction errors still update the signature cache
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(11_000);
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(11_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let pubkey = Pubkey::new_rand();
|
||||
bank.transfer(1_000, &mint_keypair, &pubkey).unwrap();
|
||||
@ -901,13 +1184,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// Make sure other errors don't update the signature cache
|
||||
let tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&pubkey,
|
||||
1000,
|
||||
Hash::default(),
|
||||
0,
|
||||
);
|
||||
let tx =
|
||||
system_transaction::create_user_account(&mint_keypair, &pubkey, 1000, Hash::default());
|
||||
let signature = tx.signatures[0];
|
||||
|
||||
// Should fail with blockhash not found
|
||||
@ -925,7 +1203,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_update_transaction_statuses_fail() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(11_000);
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(11_000);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
@ -934,14 +1216,12 @@ mod tests {
|
||||
&keypair1.pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
let fail_tx = system_transaction::create_user_account(
|
||||
&mint_keypair,
|
||||
&keypair2.pubkey(),
|
||||
2,
|
||||
bank.last_blockhash(),
|
||||
0,
|
||||
);
|
||||
|
||||
let entry_1_to_mint = next_entry(
|
||||
@ -961,4 +1241,87 @@ mod tests {
|
||||
// Should not see duplicate signature error
|
||||
assert_eq!(bank.process_transaction(&fail_tx), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_process_entries_stress() {
|
||||
// this test throws lots of rayon threads at process_entries()
|
||||
// finds bugs in very low-layer stuff
|
||||
solana_logger::setup();
|
||||
let GenesisBlockInfo {
|
||||
genesis_block,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_block(1_000_000_000);
|
||||
let mut bank = Bank::new(&genesis_block);
|
||||
|
||||
const NUM_TRANSFERS: usize = 100;
|
||||
let keypairs: Vec<_> = (0..NUM_TRANSFERS * 2).map(|_| Keypair::new()).collect();
|
||||
|
||||
// give everybody one lamport
|
||||
for keypair in &keypairs {
|
||||
bank.transfer(1, &mint_keypair, &keypair.pubkey())
|
||||
.expect("funding failed");
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
let mut hash = bank.last_blockhash();
|
||||
loop {
|
||||
let entries: Vec<_> = (0..NUM_TRANSFERS)
|
||||
.map(|i| {
|
||||
next_entry_mut(
|
||||
&mut hash,
|
||||
0,
|
||||
vec![system_transaction::transfer(
|
||||
&keypairs[i],
|
||||
&keypairs[i + NUM_TRANSFERS].pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
)],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
info!("paying iteration {}", i);
|
||||
process_entries(&bank, &entries).expect("paying failed");
|
||||
|
||||
let entries: Vec<_> = (0..NUM_TRANSFERS)
|
||||
.map(|i| {
|
||||
next_entry_mut(
|
||||
&mut hash,
|
||||
0,
|
||||
vec![system_transaction::transfer(
|
||||
&keypairs[i + NUM_TRANSFERS],
|
||||
&keypairs[i].pubkey(),
|
||||
1,
|
||||
bank.last_blockhash(),
|
||||
)],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("refunding iteration {}", i);
|
||||
process_entries(&bank, &entries).expect("refunding failed");
|
||||
|
||||
// advance to next block
|
||||
process_entries(
|
||||
&bank,
|
||||
&(0..bank.ticks_per_slot())
|
||||
.map(|_| next_entry_mut(&mut hash, 1, vec![]))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.expect("process ticks failed");
|
||||
|
||||
i += 1;
|
||||
bank = Bank::new_from_parent(&Arc::new(bank), &Pubkey::default(), i as u64);
|
||||
bank.squash();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_epoch_schedule(
|
||||
genesis_block: &GenesisBlock,
|
||||
account_paths: Option<String>,
|
||||
) -> EpochSchedule {
|
||||
let bank = Bank::new_with_paths(&genesis_block, account_paths);
|
||||
bank.epoch_schedule().clone()
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
//! A stage to broadcast data from a leader node to validators
|
||||
//!
|
||||
use crate::blocktree::Blocktree;
|
||||
use crate::cluster_info::{ClusterInfo, ClusterInfoError, NEIGHBORHOOD_SIZE};
|
||||
use crate::entry::{EntrySender, EntrySlice};
|
||||
use crate::cluster_info::{ClusterInfo, ClusterInfoError, DATA_PLANE_FANOUT};
|
||||
use crate::entry::EntrySlice;
|
||||
use crate::erasure::CodingGenerator;
|
||||
use crate::packet::index_blobs_with_genesis;
|
||||
use crate::poh_recorder::WorkingBankEntries;
|
||||
@ -10,8 +10,10 @@ use crate::result::{Error, Result};
|
||||
use crate::service::Service;
|
||||
use crate::staking_utils;
|
||||
use rayon::prelude::*;
|
||||
use solana_metrics::counter::Counter;
|
||||
use solana_metrics::{influxdb, submit};
|
||||
use solana_metrics::{
|
||||
datapoint, inc_new_counter_debug, inc_new_counter_error, inc_new_counter_info,
|
||||
inc_new_counter_warn,
|
||||
};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::timing::duration_as_ms;
|
||||
@ -27,9 +29,17 @@ pub enum BroadcastStageReturnType {
|
||||
ChannelDisconnected,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BroadcastStats {
|
||||
num_entries: Vec<usize>,
|
||||
run_elapsed: Vec<u64>,
|
||||
to_blobs_elapsed: Vec<u64>,
|
||||
}
|
||||
|
||||
struct Broadcast {
|
||||
id: Pubkey,
|
||||
coding_generator: CodingGenerator,
|
||||
stats: BroadcastStats,
|
||||
}
|
||||
|
||||
impl Broadcast {
|
||||
@ -39,14 +49,13 @@ impl Broadcast {
|
||||
receiver: &Receiver<WorkingBankEntries>,
|
||||
sock: &UdpSocket,
|
||||
blocktree: &Arc<Blocktree>,
|
||||
storage_entry_sender: &EntrySender,
|
||||
genesis_blockhash: &Hash,
|
||||
) -> Result<()> {
|
||||
let timer = Duration::new(1, 0);
|
||||
let (mut bank, entries) = receiver.recv_timeout(timer)?;
|
||||
let mut max_tick_height = bank.max_tick_height();
|
||||
|
||||
let now = Instant::now();
|
||||
let run_start = Instant::now();
|
||||
let mut num_entries = entries.len();
|
||||
let mut ventries = Vec::new();
|
||||
let mut last_tick = entries.last().map(|v| v.1).unwrap_or(0);
|
||||
@ -74,12 +83,14 @@ impl Broadcast {
|
||||
}
|
||||
|
||||
let bank_epoch = bank.get_stakers_epoch(bank.slot());
|
||||
let mut broadcast_table = cluster_info.read().unwrap().sorted_tvu_peers(
|
||||
&staking_utils::delegated_stakes_at_epoch(&bank, bank_epoch).unwrap(),
|
||||
);
|
||||
inc_new_counter_info!("broadcast_service-num_peers", broadcast_table.len() + 1);
|
||||
let mut broadcast_table = cluster_info
|
||||
.read()
|
||||
.unwrap()
|
||||
.sorted_tvu_peers(staking_utils::staked_nodes_at_epoch(&bank, bank_epoch).as_ref());
|
||||
|
||||
inc_new_counter_warn!("broadcast_service-num_peers", broadcast_table.len() + 1);
|
||||
// Layer 1, leader nodes are limited to the fanout size.
|
||||
broadcast_table.truncate(NEIGHBORHOOD_SIZE);
|
||||
broadcast_table.truncate(DATA_PLANE_FANOUT);
|
||||
|
||||
inc_new_counter_info!("broadcast_service-entries_received", num_entries);
|
||||
|
||||
@ -87,11 +98,9 @@ impl Broadcast {
|
||||
|
||||
let blobs: Vec<_> = ventries
|
||||
.into_par_iter()
|
||||
.map_with(storage_entry_sender.clone(), |s, p| {
|
||||
.map(|p| {
|
||||
let entries: Vec<_> = p.into_iter().map(|e| e.0).collect();
|
||||
let blobs = entries.to_shared_blobs();
|
||||
let _ignored = s.send(entries);
|
||||
blobs
|
||||
entries.to_shared_blobs()
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
@ -128,33 +137,47 @@ impl Broadcast {
|
||||
// Send out data
|
||||
ClusterInfo::broadcast(&self.id, contains_last_tick, &broadcast_table, sock, &blobs)?;
|
||||
|
||||
inc_new_counter_info!("streamer-broadcast-sent", blobs.len());
|
||||
inc_new_counter_debug!("streamer-broadcast-sent", blobs.len());
|
||||
|
||||
// send out erasures
|
||||
ClusterInfo::broadcast(&self.id, false, &broadcast_table, sock, &coding)?;
|
||||
|
||||
let broadcast_elapsed = duration_as_ms(&broadcast_start.elapsed());
|
||||
|
||||
inc_new_counter_info!(
|
||||
"broadcast_service-time_ms",
|
||||
duration_as_ms(&now.elapsed()) as usize
|
||||
);
|
||||
info!(
|
||||
"broadcast: {} entries, blob time {} broadcast time {}",
|
||||
num_entries, to_blobs_elapsed, broadcast_elapsed
|
||||
);
|
||||
|
||||
submit(
|
||||
influxdb::Point::new("broadcast-service")
|
||||
.add_field(
|
||||
"transmit-index",
|
||||
influxdb::Value::Integer(blob_index as i64),
|
||||
)
|
||||
.to_owned(),
|
||||
self.update_broadcast_stats(
|
||||
duration_as_ms(&broadcast_start.elapsed()),
|
||||
duration_as_ms(&run_start.elapsed()),
|
||||
num_entries,
|
||||
to_blobs_elapsed,
|
||||
blob_index,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_broadcast_stats(
|
||||
&mut self,
|
||||
broadcast_elapsed: u64,
|
||||
run_elapsed: u64,
|
||||
num_entries: usize,
|
||||
to_blobs_elapsed: u64,
|
||||
blob_index: u64,
|
||||
) {
|
||||
inc_new_counter_info!("broadcast_service-time_ms", broadcast_elapsed as usize);
|
||||
|
||||
self.stats.num_entries.push(num_entries);
|
||||
self.stats.to_blobs_elapsed.push(to_blobs_elapsed);
|
||||
self.stats.run_elapsed.push(run_elapsed);
|
||||
if self.stats.num_entries.len() >= 16 {
|
||||
info!(
|
||||
"broadcast: entries: {:?} blob times ms: {:?} broadcast times ms: {:?}",
|
||||
self.stats.num_entries, self.stats.to_blobs_elapsed, self.stats.run_elapsed
|
||||
);
|
||||
self.stats.num_entries.clear();
|
||||
self.stats.to_blobs_elapsed.clear();
|
||||
self.stats.run_elapsed.clear();
|
||||
}
|
||||
|
||||
datapoint!("broadcast-service", ("transmit-index", blob_index, i64));
|
||||
}
|
||||
}
|
||||
|
||||
// Implement a destructor for the BroadcastStage thread to signal it exited
|
||||
@ -186,7 +209,6 @@ impl BroadcastStage {
|
||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
||||
receiver: &Receiver<WorkingBankEntries>,
|
||||
blocktree: &Arc<Blocktree>,
|
||||
storage_entry_sender: EntrySender,
|
||||
genesis_blockhash: &Hash,
|
||||
) -> BroadcastStageReturnType {
|
||||
let me = cluster_info.read().unwrap().my_data().clone();
|
||||
@ -195,17 +217,13 @@ impl BroadcastStage {
|
||||
let mut broadcast = Broadcast {
|
||||
id: me.id,
|
||||
coding_generator,
|
||||
stats: BroadcastStats::default(),
|
||||
};
|
||||
|
||||
loop {
|
||||
if let Err(e) = broadcast.run(
|
||||
&cluster_info,
|
||||
receiver,
|
||||
sock,
|
||||
blocktree,
|
||||
&storage_entry_sender,
|
||||
genesis_blockhash,
|
||||
) {
|
||||
if let Err(e) =
|
||||
broadcast.run(&cluster_info, receiver, sock, blocktree, genesis_blockhash)
|
||||
{
|
||||
match e {
|
||||
Error::RecvTimeoutError(RecvTimeoutError::Disconnected) | Error::SendError => {
|
||||
return BroadcastStageReturnType::ChannelDisconnected;
|
||||
@ -213,7 +231,7 @@ impl BroadcastStage {
|
||||
Error::RecvTimeoutError(RecvTimeoutError::Timeout) => (),
|
||||
Error::ClusterInfoError(ClusterInfoError::NoPeers) => (), // TODO: Why are the unit-tests throwing hundreds of these?
|
||||
_ => {
|
||||
inc_new_counter_info!("streamer-broadcaster-error", 1, 1);
|
||||
inc_new_counter_error!("streamer-broadcaster-error", 1, 1);
|
||||
error!("broadcaster error: {:?}", e);
|
||||
}
|
||||
}
|
||||
@ -243,7 +261,6 @@ impl BroadcastStage {
|
||||
receiver: Receiver<WorkingBankEntries>,
|
||||
exit_sender: &Arc<AtomicBool>,
|
||||
blocktree: &Arc<Blocktree>,
|
||||
storage_entry_sender: EntrySender,
|
||||
genesis_blockhash: &Hash,
|
||||
) -> Self {
|
||||
let blocktree = blocktree.clone();
|
||||
@ -258,7 +275,6 @@ impl BroadcastStage {
|
||||
&cluster_info,
|
||||
&receiver,
|
||||
&blocktree,
|
||||
storage_entry_sender,
|
||||
&genesis_blockhash,
|
||||
)
|
||||
})
|
||||
@ -282,9 +298,9 @@ mod test {
|
||||
use crate::blocktree::{get_tmp_ledger_path, Blocktree};
|
||||
use crate::cluster_info::{ClusterInfo, Node};
|
||||
use crate::entry::create_ticks;
|
||||
use crate::genesis_utils::{create_genesis_block, GenesisBlockInfo};
|
||||
use crate::service::Service;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@ -320,9 +336,8 @@ mod test {
|
||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||
|
||||
let exit_sender = Arc::new(AtomicBool::new(false));
|
||||
let (storage_sender, _receiver) = channel();
|
||||
|
||||
let (genesis_block, _) = GenesisBlock::new(10_000);
|
||||
let GenesisBlockInfo { genesis_block, .. } = create_genesis_block(10_000);
|
||||
let bank = Arc::new(Bank::new(&genesis_block));
|
||||
|
||||
// Start up the broadcast stage
|
||||
@ -332,7 +347,6 @@ mod test {
|
||||
entry_receiver,
|
||||
&exit_sender,
|
||||
&blocktree,
|
||||
storage_sender,
|
||||
&Hash::default(),
|
||||
);
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
use crate::blocktree::Blocktree;
|
||||
use solana_storage_api::SLOTS_PER_SEGMENT;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::storage_stage::ENTRIES_PER_SEGMENT;
|
||||
|
||||
pub const CHACHA_BLOCK_SIZE: usize = 64;
|
||||
pub const CHACHA_KEY_SIZE: usize = 32;
|
||||
|
||||
@ -50,8 +49,7 @@ pub fn chacha_cbc_encrypt_ledger(
|
||||
let mut entry = slice;
|
||||
|
||||
loop {
|
||||
match blocktree.read_blobs_bytes(entry, ENTRIES_PER_SEGMENT - total_entries, &mut buffer, 0)
|
||||
{
|
||||
match blocktree.read_blobs_bytes(0, SLOTS_PER_SEGMENT - total_entries, &mut buffer, entry) {
|
||||
Ok((num_entries, entry_len)) => {
|
||||
debug!(
|
||||
"chacha: encrypting slice: {} num_entries: {} entry_len: {}",
|
||||
@ -124,7 +122,6 @@ mod tests {
|
||||
&keypair.pubkey(),
|
||||
1,
|
||||
one,
|
||||
0,
|
||||
)],
|
||||
)
|
||||
})
|
||||
@ -156,13 +153,11 @@ mod tests {
|
||||
let mut hasher = Hasher::default();
|
||||
hasher.hash(&buf[..size]);
|
||||
|
||||
use bs58;
|
||||
// golden needs to be updated if blob stuff changes....
|
||||
let golden = Hash::new(
|
||||
&bs58::decode("5Pz5KQyNht2nqkJhVd8F9zTFxzoDvbQSzaxQbtCPiyCo")
|
||||
.into_vec()
|
||||
.unwrap(),
|
||||
);
|
||||
let golden: Hash = "9xb2Asf7UK5G8WqPwsvzo5xwLi4dixBSDiYKCtYRikA"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(hasher.result(), golden);
|
||||
remove_file(out_path).unwrap();
|
||||
}
|
||||
|
@ -7,12 +7,11 @@ use crate::sigverify::{
|
||||
chacha_cbc_encrypt_many_sample, chacha_end_sha_state, chacha_init_sha_state,
|
||||
};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_storage_api::SLOTS_PER_SEGMENT;
|
||||
use std::io;
|
||||
use std::mem::size_of;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::storage_stage::ENTRIES_PER_SEGMENT;
|
||||
|
||||
// Encrypt a file with multiple starting IV states, determined by ivecs.len()
|
||||
//
|
||||
// Then sample each block at the offsets provided by samples argument with sha256
|
||||
@ -47,8 +46,7 @@ pub fn chacha_cbc_encrypt_file_many_keys(
|
||||
chacha_init_sha_state(int_sha_states.as_mut_ptr(), num_keys as u32);
|
||||
}
|
||||
loop {
|
||||
match blocktree.read_blobs_bytes(entry, ENTRIES_PER_SEGMENT - total_entries, &mut buffer, 0)
|
||||
{
|
||||
match blocktree.read_blobs_bytes(entry, SLOTS_PER_SEGMENT - total_entries, &mut buffer, 0) {
|
||||
Ok((num_entries, entry_len)) => {
|
||||
debug!(
|
||||
"chacha_cuda: encrypting segment: {} num_entries: {} entry_len: {}",
|
||||
@ -78,9 +76,9 @@ pub fn chacha_cbc_encrypt_file_many_keys(
|
||||
entry += num_entries;
|
||||
debug!(
|
||||
"total entries: {} entry: {} segment: {} entries_per_segment: {}",
|
||||
total_entries, entry, segment, ENTRIES_PER_SEGMENT
|
||||
total_entries, entry, segment, SLOTS_PER_SEGMENT
|
||||
);
|
||||
if (entry - segment) >= ENTRIES_PER_SEGMENT {
|
||||
if (entry - segment) >= SLOTS_PER_SEGMENT {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub trait Cluster {
|
||||
fn get_node_ids(&self) -> Vec<Pubkey>;
|
||||
fn get_node_pubkeys(&self) -> Vec<Pubkey>;
|
||||
fn restart_node(&mut self, pubkey: Pubkey);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
968
core/src/cluster_info_repair_listener.rs
Normal file
968
core/src/cluster_info_repair_listener.rs
Normal file
@ -0,0 +1,968 @@
|
||||
use crate::blocktree::Blocktree;
|
||||
use crate::cluster_info::ClusterInfo;
|
||||
use crate::crds_value::EpochSlots;
|
||||
use crate::result::Result;
|
||||
use crate::service::Service;
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use solana_metrics::datapoint;
|
||||
use solana_runtime::epoch_schedule::EpochSchedule;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::cmp;
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::UdpSocket;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::{self, sleep, Builder, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
pub const REPAIRMEN_SLEEP_MILLIS: usize = 100;
|
||||
pub const REPAIR_REDUNDANCY: usize = 1;
|
||||
pub const NUM_BUFFER_SLOTS: usize = 50;
|
||||
pub const GOSSIP_DELAY_SLOTS: usize = 2;
|
||||
pub const NUM_SLOTS_PER_UPDATE: usize = 2;
|
||||
|
||||
// Represents the blobs that a repairman is responsible for repairing in specific slot. More
|
||||
// specifically, a repairman is responsible for every blob in this slot with index
|
||||
// `(start_index + step_size * i) % num_blobs_in_slot`, for all `0 <= i <= num_blobs_to_send - 1`
|
||||
// in this slot.
|
||||
struct BlobIndexesToRepairIterator {
|
||||
start_index: usize,
|
||||
num_blobs_to_send: usize,
|
||||
step_size: usize,
|
||||
num_blobs_in_slot: usize,
|
||||
blobs_sent: usize,
|
||||
}
|
||||
|
||||
impl BlobIndexesToRepairIterator {
|
||||
fn new(
|
||||
start_index: usize,
|
||||
num_blobs_to_send: usize,
|
||||
step_size: usize,
|
||||
num_blobs_in_slot: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
start_index,
|
||||
num_blobs_to_send,
|
||||
step_size,
|
||||
num_blobs_in_slot,
|
||||
blobs_sent: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for BlobIndexesToRepairIterator {
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.blobs_sent == self.num_blobs_to_send {
|
||||
None
|
||||
} else {
|
||||
let blob_index = Some(
|
||||
(self.start_index + self.step_size * self.blobs_sent) % self.num_blobs_in_slot,
|
||||
);
|
||||
self.blobs_sent += 1;
|
||||
blob_index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClusterInfoRepairListener {
|
||||
thread_hdls: Vec<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl ClusterInfoRepairListener {
|
||||
pub fn new(
|
||||
blocktree: &Arc<Blocktree>,
|
||||
exit: &Arc<AtomicBool>,
|
||||
cluster_info: Arc<RwLock<ClusterInfo>>,
|
||||
epoch_schedule: EpochSchedule,
|
||||
) -> Self {
|
||||
let exit = exit.clone();
|
||||
let blocktree = blocktree.clone();
|
||||
let thread = Builder::new()
|
||||
.name("solana-cluster_info_repair_listener".to_string())
|
||||
.spawn(move || {
|
||||
// Maps a peer to
|
||||
// 1) The latest timestamp of the EpochSlots gossip message at which a repair was
|
||||
// sent to this peer
|
||||
// 2) The latest root the peer gossiped
|
||||
let mut peer_roots: HashMap<Pubkey, (u64, u64)> = HashMap::new();
|
||||
let _ = Self::recv_loop(
|
||||
&blocktree,
|
||||
&mut peer_roots,
|
||||
&exit,
|
||||
&cluster_info,
|
||||
&epoch_schedule,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
Self {
|
||||
thread_hdls: vec![thread],
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_loop(
|
||||
blocktree: &Blocktree,
|
||||
peer_roots: &mut HashMap<Pubkey, (u64, u64)>,
|
||||
exit: &Arc<AtomicBool>,
|
||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
||||
epoch_schedule: &EpochSchedule,
|
||||
) -> Result<()> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
let my_pubkey = cluster_info.read().unwrap().id();
|
||||
let mut my_gossiped_root = 0;
|
||||
|
||||
loop {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let peers = cluster_info.read().unwrap().gossip_peers();
|
||||
let mut peers_needing_repairs: HashMap<Pubkey, EpochSlots> = HashMap::new();
|
||||
|
||||
// Iterate through all the known nodes in the network, looking for ones that
|
||||
// need repairs
|
||||
for peer in peers {
|
||||
if let Some(repairee_epoch_slots) = Self::process_potential_repairee(
|
||||
&my_pubkey,
|
||||
&peer.id,
|
||||
cluster_info,
|
||||
peer_roots,
|
||||
&mut my_gossiped_root,
|
||||
) {
|
||||
peers_needing_repairs.insert(peer.id, repairee_epoch_slots);
|
||||
}
|
||||
}
|
||||
|
||||
// After updating all the peers, send out repairs to those that need it
|
||||
let _ = Self::serve_repairs(
|
||||
&my_pubkey,
|
||||
blocktree,
|
||||
peer_roots,
|
||||
&peers_needing_repairs,
|
||||
&socket,
|
||||
cluster_info,
|
||||
&mut my_gossiped_root,
|
||||
epoch_schedule,
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(REPAIRMEN_SLEEP_MILLIS as u64));
|
||||
}
|
||||
}
|
||||
|
||||
fn process_potential_repairee(
|
||||
my_pubkey: &Pubkey,
|
||||
peer_pubkey: &Pubkey,
|
||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
||||
peer_roots: &mut HashMap<Pubkey, (u64, u64)>,
|
||||
my_gossiped_root: &mut u64,
|
||||
) -> Option<EpochSlots> {
|
||||
let last_cached_repair_ts = Self::get_last_ts(peer_pubkey, peer_roots);
|
||||
let my_root = Self::read_my_gossiped_root(&my_pubkey, cluster_info, my_gossiped_root);
|
||||
{
|
||||
let r_cluster_info = cluster_info.read().unwrap();
|
||||
|
||||
// Update our local map with the updated peers' information
|
||||
if let Some((peer_epoch_slots, updated_ts)) =
|
||||
r_cluster_info.get_epoch_state_for_node(&peer_pubkey, last_cached_repair_ts)
|
||||
{
|
||||
let peer_entry = peer_roots.entry(*peer_pubkey).or_default();
|
||||
let peer_root = cmp::max(peer_epoch_slots.root, peer_entry.1);
|
||||
let mut result = None;
|
||||
let last_repair_ts = {
|
||||
// Following logic needs to be fast because it holds the lock
|
||||
// preventing updates on gossip
|
||||
if Self::should_repair_peer(my_root, peer_epoch_slots.root, NUM_BUFFER_SLOTS) {
|
||||
// Clone out EpochSlots structure to avoid holding lock on gossip
|
||||
result = Some(peer_epoch_slots.clone());
|
||||
updated_ts
|
||||
} else {
|
||||
// No repairs were sent, don't need to update the timestamp
|
||||
peer_entry.0
|
||||
}
|
||||
};
|
||||
|
||||
*peer_entry = (last_repair_ts, peer_root);
|
||||
result
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_repairs(
|
||||
my_pubkey: &Pubkey,
|
||||
blocktree: &Blocktree,
|
||||
peer_roots: &HashMap<Pubkey, (u64, u64)>,
|
||||
repairees: &HashMap<Pubkey, EpochSlots>,
|
||||
socket: &UdpSocket,
|
||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
||||
my_gossiped_root: &mut u64,
|
||||
epoch_schedule: &EpochSchedule,
|
||||
) -> Result<()> {
|
||||
for (repairee_pubkey, repairee_epoch_slots) in repairees {
|
||||
let repairee_root = repairee_epoch_slots.root;
|
||||
|
||||
let repairee_tvu = {
|
||||
let r_cluster_info = cluster_info.read().unwrap();
|
||||
let contact_info = r_cluster_info.get_contact_info_for_node(repairee_pubkey);
|
||||
contact_info.map(|c| c.tvu)
|
||||
};
|
||||
|
||||
if let Some(repairee_tvu) = repairee_tvu {
|
||||
// For every repairee, get the set of repairmen who are responsible for
|
||||
let mut eligible_repairmen = Self::find_eligible_repairmen(
|
||||
my_pubkey,
|
||||
repairee_root,
|
||||
peer_roots,
|
||||
NUM_BUFFER_SLOTS,
|
||||
);
|
||||
|
||||
Self::shuffle_repairmen(
|
||||
&mut eligible_repairmen,
|
||||
repairee_pubkey,
|
||||
repairee_epoch_slots.root,
|
||||
);
|
||||
|
||||
let my_root =
|
||||
Self::read_my_gossiped_root(my_pubkey, cluster_info, my_gossiped_root);
|
||||
|
||||
let _ = Self::serve_repairs_to_repairee(
|
||||
my_pubkey,
|
||||
my_root,
|
||||
blocktree,
|
||||
&repairee_epoch_slots,
|
||||
&eligible_repairmen,
|
||||
socket,
|
||||
&repairee_tvu,
|
||||
NUM_SLOTS_PER_UPDATE,
|
||||
epoch_schedule,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serve_repairs_to_repairee(
|
||||
my_pubkey: &Pubkey,
|
||||
my_root: u64,
|
||||
blocktree: &Blocktree,
|
||||
repairee_epoch_slots: &EpochSlots,
|
||||
eligible_repairmen: &[&Pubkey],
|
||||
socket: &UdpSocket,
|
||||
repairee_tvu: &SocketAddr,
|
||||
num_slots_to_repair: usize,
|
||||
epoch_schedule: &EpochSchedule,
|
||||
) -> Result<()> {
|
||||
let slot_iter = blocktree.rooted_slot_iterator(repairee_epoch_slots.root + 1);
|
||||
|
||||
if slot_iter.is_err() {
|
||||
warn!("Root for repairee is on different fork OR replay_stage hasn't marked this slot as root yet");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let slot_iter = slot_iter?;
|
||||
|
||||
let mut total_data_blobs_sent = 0;
|
||||
let mut total_coding_blobs_sent = 0;
|
||||
let mut num_slots_repaired = 0;
|
||||
let max_confirmed_repairee_epoch =
|
||||
epoch_schedule.get_stakers_epoch(repairee_epoch_slots.root);
|
||||
let max_confirmed_repairee_slot =
|
||||
epoch_schedule.get_last_slot_in_epoch(max_confirmed_repairee_epoch);
|
||||
for (slot, slot_meta) in slot_iter {
|
||||
if slot > my_root
|
||||
|| num_slots_repaired >= num_slots_to_repair
|
||||
|| slot > max_confirmed_repairee_slot
|
||||
{
|
||||
break;
|
||||
}
|
||||
if !repairee_epoch_slots.slots.contains(&slot) {
|
||||
// Calculate the blob indexes this node is responsible for repairing. Note that
|
||||
// because we are only repairing slots that are before our root, the slot.received
|
||||
// should be equal to the actual total number of blobs in the slot. Optimistically
|
||||
// this means that most repairmen should observe the same "total" number of blobs
|
||||
// for a particular slot, and thus the calculation in
|
||||
// calculate_my_repairman_index_for_slot() will divide responsibility evenly across
|
||||
// the cluster
|
||||
let num_blobs_in_slot = slot_meta.received as usize;
|
||||
if let Some(my_repair_indexes) = Self::calculate_my_repairman_index_for_slot(
|
||||
my_pubkey,
|
||||
&eligible_repairmen,
|
||||
num_blobs_in_slot,
|
||||
REPAIR_REDUNDANCY,
|
||||
) {
|
||||
// Repairee is missing this slot, send them the blobs for this slot
|
||||
for blob_index in my_repair_indexes {
|
||||
// Loop over the sblob indexes and query the database for these blob that
|
||||
// this node is reponsible for repairing. This should be faster than using
|
||||
// a database iterator over the slots because by the time this node is
|
||||
// sending the blobs in this slot for repair, we expect these slots
|
||||
// to be full.
|
||||
if let Some(blob_data) = blocktree
|
||||
.get_data_blob_bytes(slot, blob_index as u64)
|
||||
.expect("Failed to read data blob from blocktree")
|
||||
{
|
||||
socket.send_to(&blob_data[..], repairee_tvu)?;
|
||||
total_data_blobs_sent += 1;
|
||||
}
|
||||
|
||||
if let Some(coding_bytes) = blocktree
|
||||
.get_coding_blob_bytes(slot, blob_index as u64)
|
||||
.expect("Failed to read coding blob from blocktree")
|
||||
{
|
||||
socket.send_to(&coding_bytes[..], repairee_tvu)?;
|
||||
total_coding_blobs_sent += 1;
|
||||
}
|
||||
}
|
||||
|
||||
num_slots_repaired += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::report_repair_metrics(total_data_blobs_sent, total_coding_blobs_sent);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_repair_metrics(total_data_blobs_sent: u64, total_coding_blobs_sent: u64) {
|
||||
if total_data_blobs_sent > 0 || total_coding_blobs_sent > 0 {
|
||||
datapoint!(
|
||||
"repairman_activity",
|
||||
("data_sent", total_data_blobs_sent, i64),
|
||||
("coding_sent", total_coding_blobs_sent, i64)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn shuffle_repairmen(
|
||||
eligible_repairmen: &mut Vec<&Pubkey>,
|
||||
repairee_pubkey: &Pubkey,
|
||||
repairee_root: u64,
|
||||
) {
|
||||
// Make a seed from pubkey + repairee root
|
||||
let mut seed = [0u8; mem::size_of::<Pubkey>()];
|
||||
let repairee_pubkey_bytes = repairee_pubkey.as_ref();
|
||||
seed[..repairee_pubkey_bytes.len()].copy_from_slice(repairee_pubkey_bytes);
|
||||
LittleEndian::write_u64(&mut seed[0..], repairee_root);
|
||||
|
||||
// Deterministically shuffle the eligible repairmen based on the seed
|
||||
let mut rng = ChaChaRng::from_seed(seed);
|
||||
eligible_repairmen.shuffle(&mut rng);
|
||||
}
|
||||
|
||||
// The calculation should partition the blobs in the slot across the repairmen in the cluster
|
||||
// such that each blob in the slot is the responsibility of `repair_redundancy` or
|
||||
// `repair_redundancy + 1` number of repairmen in the cluster.
|
||||
fn calculate_my_repairman_index_for_slot(
|
||||
my_pubkey: &Pubkey,
|
||||
eligible_repairmen: &[&Pubkey],
|
||||
num_blobs_in_slot: usize,
|
||||
repair_redundancy: usize,
|
||||
) -> Option<BlobIndexesToRepairIterator> {
|
||||
let total_blobs = num_blobs_in_slot * repair_redundancy;
|
||||
let total_repairmen_for_slot = cmp::min(total_blobs, eligible_repairmen.len());
|
||||
|
||||
let blobs_per_repairman = cmp::min(
|
||||
(total_blobs + total_repairmen_for_slot - 1) / total_repairmen_for_slot,
|
||||
num_blobs_in_slot,
|
||||
);
|
||||
|
||||
// Calculate the indexes this node is responsible for
|
||||
if let Some(my_position) = eligible_repairmen[..total_repairmen_for_slot]
|
||||
.iter()
|
||||
.position(|id| *id == my_pubkey)
|
||||
{
|
||||
let start_index = my_position % num_blobs_in_slot;
|
||||
Some(BlobIndexesToRepairIterator::new(
|
||||
start_index,
|
||||
blobs_per_repairman,
|
||||
total_repairmen_for_slot,
|
||||
num_blobs_in_slot,
|
||||
))
|
||||
} else {
|
||||
// If there are more repairmen than `total_blobs`, then some repairmen
|
||||
// will not have any responsibility to repair this slot
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn find_eligible_repairmen<'a>(
|
||||
my_pubkey: &'a Pubkey,
|
||||
repairee_root: u64,
|
||||
repairman_roots: &'a HashMap<Pubkey, (u64, u64)>,
|
||||
num_buffer_slots: usize,
|
||||
) -> Vec<&'a Pubkey> {
|
||||
let mut repairmen: Vec<_> = repairman_roots
|
||||
.iter()
|
||||
.filter_map(|(repairman_pubkey, (_, repairman_root))| {
|
||||
if Self::should_repair_peer(
|
||||
*repairman_root,
|
||||
repairee_root,
|
||||
num_buffer_slots - GOSSIP_DELAY_SLOTS,
|
||||
) {
|
||||
Some(repairman_pubkey)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
repairmen.push(my_pubkey);
|
||||
repairmen.sort();
|
||||
repairmen
|
||||
}
|
||||
|
||||
// Read my root out of gossip, and update the cached `old_root`
|
||||
fn read_my_gossiped_root(
|
||||
my_pubkey: &Pubkey,
|
||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
||||
old_root: &mut u64,
|
||||
) -> u64 {
|
||||
let new_root = cluster_info
|
||||
.read()
|
||||
.unwrap()
|
||||
.get_gossiped_root_for_node(&my_pubkey, None);
|
||||
|
||||
if let Some(new_root) = new_root {
|
||||
*old_root = new_root;
|
||||
new_root
|
||||
} else {
|
||||
*old_root
|
||||
}
|
||||
}
|
||||
|
||||
// Decide if a repairman with root == `repairman_root` should send repairs to a
|
||||
// potential repairee with root == `repairee_root`
|
||||
fn should_repair_peer(
|
||||
repairman_root: u64,
|
||||
repairee_root: u64,
|
||||
num_buffer_slots: usize,
|
||||
) -> bool {
|
||||
// Check if this potential repairman's root is greater than the repairee root +
|
||||
// num_buffer_slots
|
||||
repairman_root > repairee_root + num_buffer_slots as u64
|
||||
}
|
||||
|
||||
fn get_last_ts(pubkey: &Pubkey, peer_roots: &mut HashMap<Pubkey, (u64, u64)>) -> Option<u64> {
|
||||
peer_roots.get(pubkey).map(|(last_ts, _)| *last_ts)
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for ClusterInfoRepairListener {
|
||||
type JoinReturnType = ();
|
||||
|
||||
fn join(self) -> thread::Result<()> {
|
||||
for thread_hdl in self.thread_hdls {
|
||||
thread_hdl.join()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::blocktree::get_tmp_ledger_path;
|
||||
use crate::blocktree::tests::make_many_slot_entries;
|
||||
use crate::cluster_info::Node;
|
||||
use crate::packet::{Blob, SharedBlob};
|
||||
use crate::streamer;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::Arc;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
struct MockRepairee {
|
||||
id: Pubkey,
|
||||
receiver: Receiver<Vec<SharedBlob>>,
|
||||
tvu_address: SocketAddr,
|
||||
repairee_exit: Arc<AtomicBool>,
|
||||
repairee_receiver_thread_hdl: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl MockRepairee {
|
||||
pub fn new(
|
||||
id: Pubkey,
|
||||
receiver: Receiver<Vec<SharedBlob>>,
|
||||
tvu_address: SocketAddr,
|
||||
repairee_exit: Arc<AtomicBool>,
|
||||
repairee_receiver_thread_hdl: JoinHandle<()>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
receiver,
|
||||
tvu_address,
|
||||
repairee_exit,
|
||||
repairee_receiver_thread_hdl,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_mock_repairee() -> Self {
|
||||
let id = Pubkey::new_rand();
|
||||
let (repairee_sender, repairee_receiver) = channel();
|
||||
let repairee_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").unwrap());
|
||||
let repairee_tvu_addr = repairee_socket.local_addr().unwrap();
|
||||
let repairee_exit = Arc::new(AtomicBool::new(false));
|
||||
let repairee_receiver_thread_hdl =
|
||||
streamer::blob_receiver(repairee_socket, &repairee_exit, repairee_sender);
|
||||
|
||||
Self::new(
|
||||
id,
|
||||
repairee_receiver,
|
||||
repairee_tvu_addr,
|
||||
repairee_exit,
|
||||
repairee_receiver_thread_hdl,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn close(self) -> Result<()> {
|
||||
self.repairee_exit.store(true, Ordering::Relaxed);
|
||||
self.repairee_receiver_thread_hdl.join()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_potential_repairee() {
|
||||
// Set up node ids
|
||||
let my_pubkey = Pubkey::new_rand();
|
||||
let peer_pubkey = Pubkey::new_rand();
|
||||
|
||||
// Set up cluster_info
|
||||
let cluster_info = Arc::new(RwLock::new(ClusterInfo::new_with_invalid_keypair(
|
||||
Node::new_localhost().info,
|
||||
)));
|
||||
|
||||
// Push a repairee's epoch slots into cluster info
|
||||
let repairee_root = 0;
|
||||
let repairee_slots = BTreeSet::new();
|
||||
cluster_info.write().unwrap().push_epoch_slots(
|
||||
peer_pubkey,
|
||||
repairee_root,
|
||||
repairee_slots.clone(),
|
||||
);
|
||||
|
||||
// Set up locally cached information
|
||||
let mut peer_roots = HashMap::new();
|
||||
let mut my_gossiped_root = repairee_root;
|
||||
|
||||
// Root is not sufficiently far ahead, we shouldn't repair
|
||||
assert!(ClusterInfoRepairListener::process_potential_repairee(
|
||||
&my_pubkey,
|
||||
&peer_pubkey,
|
||||
&cluster_info,
|
||||
&mut peer_roots,
|
||||
&mut my_gossiped_root,
|
||||
)
|
||||
.is_none());
|
||||
|
||||
// Update the root to be sufficiently far ahead. A repair should now occur even if the
|
||||
// object in gossip is not updated
|
||||
my_gossiped_root = repairee_root + NUM_BUFFER_SLOTS as u64 + 1;
|
||||
assert!(ClusterInfoRepairListener::process_potential_repairee(
|
||||
&my_pubkey,
|
||||
&peer_pubkey,
|
||||
&cluster_info,
|
||||
&mut peer_roots,
|
||||
&mut my_gossiped_root,
|
||||
)
|
||||
.is_some());
|
||||
|
||||
// An repair was already sent, so if gossip is not updated, no repair should be sent again,
|
||||
// even if our root moves forward
|
||||
my_gossiped_root += 4;
|
||||
assert!(ClusterInfoRepairListener::process_potential_repairee(
|
||||
&my_pubkey,
|
||||
&peer_pubkey,
|
||||
&cluster_info,
|
||||
&mut peer_roots,
|
||||
&mut my_gossiped_root,
|
||||
)
|
||||
.is_none());
|
||||
|
||||
// Sleep to make sure the timestamp is updated in gossip. Update the gossiped EpochSlots.
|
||||
// Now a repair should be sent again
|
||||
sleep(Duration::from_millis(10));
|
||||
cluster_info
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_epoch_slots(peer_pubkey, repairee_root, repairee_slots);
|
||||
assert!(ClusterInfoRepairListener::process_potential_repairee(
|
||||
&my_pubkey,
|
||||
&peer_pubkey,
|
||||
&cluster_info,
|
||||
&mut peer_roots,
|
||||
&mut my_gossiped_root,
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serve_repairs_to_repairee() {
|
||||
let blocktree_path = get_tmp_ledger_path!();
|
||||
let blocktree = Blocktree::open(&blocktree_path).unwrap();
|
||||
let blobs_per_slot = 5;
|
||||
let num_slots = 10;
|
||||
assert_eq!(num_slots % 2, 0);
|
||||
let (blobs, _) = make_many_slot_entries(0, num_slots, blobs_per_slot);
|
||||
|
||||
// Write slots in the range [0, num_slots] to blocktree
|
||||
blocktree.insert_data_blobs(&blobs).unwrap();
|
||||
|
||||
// Write roots so that these slots will qualify to be sent by the repairman
|
||||
blocktree.set_root(0, 0).unwrap();
|
||||
blocktree.set_root(num_slots - 1, 0).unwrap();
|
||||
|
||||
// Set up my information
|
||||
let my_pubkey = Pubkey::new_rand();
|
||||
let my_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
|
||||
// Set up a mock repairee with a socket listening for incoming repairs
|
||||
let mock_repairee = MockRepairee::make_mock_repairee();
|
||||
|
||||
// Set up the repairee's EpochSlots, such that they are missing every odd indexed slot
|
||||
// in the range (repairee_root, num_slots]
|
||||
let repairee_root = 0;
|
||||
let repairee_slots: BTreeSet<_> = (0..=num_slots).step_by(2).collect();
|
||||
let repairee_epoch_slots =
|
||||
EpochSlots::new(mock_repairee.id, repairee_root, repairee_slots, 1);
|
||||
|
||||
// Mock out some other repairmen such that each repairman is responsible for 1 blob in a slot
|
||||
let num_repairmen = blobs_per_slot - 1;
|
||||
let mut eligible_repairmen: Vec<_> =
|
||||
(0..num_repairmen).map(|_| Pubkey::new_rand()).collect();
|
||||
eligible_repairmen.push(my_pubkey);
|
||||
let eligible_repairmen_refs: Vec<_> = eligible_repairmen.iter().collect();
|
||||
|
||||
// Have all the repairman send the repairs
|
||||
let epoch_schedule = EpochSchedule::new(32, 16, false);
|
||||
let num_missing_slots = num_slots / 2;
|
||||
for repairman_pubkey in &eligible_repairmen {
|
||||
ClusterInfoRepairListener::serve_repairs_to_repairee(
|
||||
&repairman_pubkey,
|
||||
num_slots - 1,
|
||||
&blocktree,
|
||||
&repairee_epoch_slots,
|
||||
&eligible_repairmen_refs,
|
||||
&my_socket,
|
||||
&mock_repairee.tvu_address,
|
||||
num_missing_slots as usize,
|
||||
&epoch_schedule,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut received_blobs: Vec<Arc<RwLock<Blob>>> = vec![];
|
||||
|
||||
// This repairee was missing exactly `num_slots / 2` slots, so we expect to get
|
||||
// `(num_slots / 2) * blobs_per_slot * REPAIR_REDUNDANCY` blobs.
|
||||
let num_expected_blobs = (num_slots / 2) * blobs_per_slot * REPAIR_REDUNDANCY as u64;
|
||||
while (received_blobs.len() as u64) < num_expected_blobs {
|
||||
received_blobs.extend(mock_repairee.receiver.recv().unwrap());
|
||||
}
|
||||
|
||||
// Make sure no extra blobs get sent
|
||||
sleep(Duration::from_millis(1000));
|
||||
assert!(mock_repairee.receiver.try_recv().is_err());
|
||||
assert_eq!(received_blobs.len() as u64, num_expected_blobs);
|
||||
|
||||
// Shutdown
|
||||
mock_repairee.close().unwrap();
|
||||
drop(blocktree);
|
||||
Blocktree::destroy(&blocktree_path).expect("Expected successful database destruction");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_repair_past_confirmed_epoch() {
|
||||
let blocktree_path = get_tmp_ledger_path!();
|
||||
let blocktree = Blocktree::open(&blocktree_path).unwrap();
|
||||
let stakers_slot_offset = 16;
|
||||
let slots_per_epoch = stakers_slot_offset * 2;
|
||||
let epoch_schedule = EpochSchedule::new(slots_per_epoch, stakers_slot_offset, false);
|
||||
|
||||
// Create blobs for first two epochs and write them to blocktree
|
||||
let total_slots = slots_per_epoch * 2;
|
||||
let (blobs, _) = make_many_slot_entries(0, total_slots, 1);
|
||||
blocktree.insert_data_blobs(&blobs).unwrap();
|
||||
|
||||
// Write roots so that these slots will qualify to be sent by the repairman
|
||||
blocktree.set_root(0, 0).unwrap();
|
||||
blocktree.set_root(slots_per_epoch * 2 - 1, 0).unwrap();
|
||||
|
||||
// Set up my information
|
||||
let my_pubkey = Pubkey::new_rand();
|
||||
let my_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
|
||||
// Set up a mock repairee with a socket listening for incoming repairs
|
||||
let mock_repairee = MockRepairee::make_mock_repairee();
|
||||
|
||||
// Set up the repairee's EpochSlots, such that:
|
||||
// 1) They are missing all of the second epoch, but have all of the first epoch.
|
||||
// 2) The root only confirms epoch 1, so the leader for epoch 2 is unconfirmed.
|
||||
//
|
||||
// Thus, no repairmen should send any blobs to this repairee b/c this repairee
|
||||
// already has all the slots for which they have a confirmed leader schedule
|
||||
let repairee_root = 0;
|
||||
let repairee_slots: BTreeSet<_> = (0..=slots_per_epoch).collect();
|
||||
let repairee_epoch_slots =
|
||||
EpochSlots::new(mock_repairee.id, repairee_root, repairee_slots.clone(), 1);
|
||||
|
||||
ClusterInfoRepairListener::serve_repairs_to_repairee(
|
||||
&my_pubkey,
|
||||
total_slots - 1,
|
||||
&blocktree,
|
||||
&repairee_epoch_slots,
|
||||
&vec![&my_pubkey],
|
||||
&my_socket,
|
||||
&mock_repairee.tvu_address,
|
||||
1 as usize,
|
||||
&epoch_schedule,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Make sure no blobs get sent
|
||||
sleep(Duration::from_millis(1000));
|
||||
assert!(mock_repairee.receiver.try_recv().is_err());
|
||||
|
||||
// Set the root to stakers_slot_offset, now epoch 2 should be confirmed, so the repairee
|
||||
// is now eligible to get slots from epoch 2:
|
||||
let repairee_epoch_slots =
|
||||
EpochSlots::new(mock_repairee.id, stakers_slot_offset, repairee_slots, 1);
|
||||
ClusterInfoRepairListener::serve_repairs_to_repairee(
|
||||
&my_pubkey,
|
||||
total_slots - 1,
|
||||
&blocktree,
|
||||
&repairee_epoch_slots,
|
||||
&vec![&my_pubkey],
|
||||
&my_socket,
|
||||
&mock_repairee.tvu_address,
|
||||
1 as usize,
|
||||
&epoch_schedule,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Make sure some blobs get sent this time
|
||||
sleep(Duration::from_millis(1000));
|
||||
assert!(mock_repairee.receiver.try_recv().is_ok());
|
||||
|
||||
// Shutdown
|
||||
mock_repairee.close().unwrap();
|
||||
drop(blocktree);
|
||||
Blocktree::destroy(&blocktree_path).expect("Expected successful database destruction");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shuffle_repairmen() {
|
||||
let num_repairmen = 10;
|
||||
let eligible_repairmen: Vec<_> = (0..num_repairmen).map(|_| Pubkey::new_rand()).collect();
|
||||
|
||||
let unshuffled_refs: Vec<_> = eligible_repairmen.iter().collect();
|
||||
let mut expected_order = unshuffled_refs.clone();
|
||||
|
||||
// Find the expected shuffled order based on a fixed seed
|
||||
ClusterInfoRepairListener::shuffle_repairmen(&mut expected_order, unshuffled_refs[0], 0);
|
||||
for _ in 0..10 {
|
||||
let mut copied = unshuffled_refs.clone();
|
||||
ClusterInfoRepairListener::shuffle_repairmen(&mut copied, unshuffled_refs[0], 0);
|
||||
|
||||
// Make sure shuffling repairmen is deterministic every time
|
||||
assert_eq!(copied, expected_order);
|
||||
|
||||
// Make sure shuffling actually changes the order of the keys
|
||||
assert_ne!(copied, unshuffled_refs);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_my_repairman_index_for_slot() {
|
||||
// Test when the number of blobs in the slot > number of repairmen
|
||||
let num_repairmen = 10;
|
||||
let num_blobs_in_slot = 42;
|
||||
let repair_redundancy = 3;
|
||||
|
||||
run_calculate_my_repairman_index_for_slot(
|
||||
num_repairmen,
|
||||
num_blobs_in_slot,
|
||||
repair_redundancy,
|
||||
);
|
||||
|
||||
// Test when num_blobs_in_slot is a multiple of num_repairmen
|
||||
let num_repairmen = 12;
|
||||
let num_blobs_in_slot = 48;
|
||||
let repair_redundancy = 3;
|
||||
|
||||
run_calculate_my_repairman_index_for_slot(
|
||||
num_repairmen,
|
||||
num_blobs_in_slot,
|
||||
repair_redundancy,
|
||||
);
|
||||
|
||||
// Test when num_repairmen and num_blobs_in_slot are relatively prime
|
||||
let num_repairmen = 12;
|
||||
let num_blobs_in_slot = 47;
|
||||
let repair_redundancy = 12;
|
||||
|
||||
run_calculate_my_repairman_index_for_slot(
|
||||
num_repairmen,
|
||||
num_blobs_in_slot,
|
||||
repair_redundancy,
|
||||
);
|
||||
|
||||
// Test 1 repairman
|
||||
let num_repairmen = 1;
|
||||
let num_blobs_in_slot = 30;
|
||||
let repair_redundancy = 3;
|
||||
|
||||
run_calculate_my_repairman_index_for_slot(
|
||||
num_repairmen,
|
||||
num_blobs_in_slot,
|
||||
repair_redundancy,
|
||||
);
|
||||
|
||||
// Test when repair_redundancy is 1, and num_blobs_in_slot does not evenly
|
||||
// divide num_repairmen
|
||||
let num_repairmen = 12;
|
||||
let num_blobs_in_slot = 47;
|
||||
let repair_redundancy = 1;
|
||||
|
||||
run_calculate_my_repairman_index_for_slot(
|
||||
num_repairmen,
|
||||
num_blobs_in_slot,
|
||||
repair_redundancy,
|
||||
);
|
||||
|
||||
// Test when the number of blobs in the slot <= number of repairmen
|
||||
let num_repairmen = 10;
|
||||
let num_blobs_in_slot = 10;
|
||||
let repair_redundancy = 3;
|
||||
run_calculate_my_repairman_index_for_slot(
|
||||
num_repairmen,
|
||||
num_blobs_in_slot,
|
||||
repair_redundancy,
|
||||
);
|
||||
|
||||
// Test when there are more repairmen than repair_redundancy * num_blobs_in_slot
|
||||
let num_repairmen = 42;
|
||||
let num_blobs_in_slot = 10;
|
||||
let repair_redundancy = 3;
|
||||
run_calculate_my_repairman_index_for_slot(
|
||||
num_repairmen,
|
||||
num_blobs_in_slot,
|
||||
repair_redundancy,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_repair_peer() {
|
||||
// If repairee is ahead of us, we don't repair
|
||||
let repairman_root = 0;
|
||||
let repairee_root = 5;
|
||||
assert!(!ClusterInfoRepairListener::should_repair_peer(
|
||||
repairman_root,
|
||||
repairee_root,
|
||||
0,
|
||||
));
|
||||
|
||||
// If repairee is at the same place as us, we don't repair
|
||||
let repairman_root = 5;
|
||||
let repairee_root = 5;
|
||||
assert!(!ClusterInfoRepairListener::should_repair_peer(
|
||||
repairman_root,
|
||||
repairee_root,
|
||||
0,
|
||||
));
|
||||
|
||||
// If repairee is behind with no buffer, we repair
|
||||
let repairman_root = 15;
|
||||
let repairee_root = 5;
|
||||
assert!(ClusterInfoRepairListener::should_repair_peer(
|
||||
repairman_root,
|
||||
repairee_root,
|
||||
0,
|
||||
));
|
||||
|
||||
// If repairee is behind, but within the buffer, we don't repair
|
||||
let repairman_root = 16;
|
||||
let repairee_root = 5;
|
||||
assert!(!ClusterInfoRepairListener::should_repair_peer(
|
||||
repairman_root,
|
||||
repairee_root,
|
||||
11,
|
||||
));
|
||||
|
||||
// If repairee is behind, but outside the buffer, we repair
|
||||
let repairman_root = 16;
|
||||
let repairee_root = 5;
|
||||
assert!(ClusterInfoRepairListener::should_repair_peer(
|
||||
repairman_root,
|
||||
repairee_root,
|
||||
10,
|
||||
));
|
||||
}
|
||||
|
||||
fn run_calculate_my_repairman_index_for_slot(
|
||||
num_repairmen: usize,
|
||||
num_blobs_in_slot: usize,
|
||||
repair_redundancy: usize,
|
||||
) {
|
||||
let eligible_repairmen: Vec<_> = (0..num_repairmen).map(|_| Pubkey::new_rand()).collect();
|
||||
let eligible_repairmen_ref: Vec<_> = eligible_repairmen.iter().collect();
|
||||
let mut results = HashMap::new();
|
||||
let mut none_results = 0;
|
||||
for pk in &eligible_repairmen {
|
||||
if let Some(my_repair_indexes) =
|
||||
ClusterInfoRepairListener::calculate_my_repairman_index_for_slot(
|
||||
pk,
|
||||
&eligible_repairmen_ref[..],
|
||||
num_blobs_in_slot,
|
||||
repair_redundancy,
|
||||
)
|
||||
{
|
||||
for blob_index in my_repair_indexes {
|
||||
results
|
||||
.entry(blob_index)
|
||||
.and_modify(|e| *e += 1)
|
||||
.or_insert(1);
|
||||
}
|
||||
} else {
|
||||
// This repairman isn't responsible for repairing this slot
|
||||
none_results += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze the results:
|
||||
|
||||
// 1) If there are a sufficient number of repairmen, then each blob should be sent
|
||||
// `repair_redundancy` OR `repair_redundancy + 1` times.
|
||||
let num_expected_redundancy = cmp::min(num_repairmen, repair_redundancy);
|
||||
for b in results.keys() {
|
||||
assert!(
|
||||
results[b] == num_expected_redundancy || results[b] == num_expected_redundancy + 1
|
||||
);
|
||||
}
|
||||
|
||||
// 2) The number of times each blob is sent should be evenly distributed
|
||||
let max_times_blob_sent = results.values().min_by(|x, y| x.cmp(y)).unwrap();
|
||||
let min_times_blob_sent = results.values().max_by(|x, y| x.cmp(y)).unwrap();
|
||||
assert!(*max_times_blob_sent <= *min_times_blob_sent + 1);
|
||||
|
||||
// 3) There should only be repairmen who are not responsible for repairing this slot
|
||||
// if we have more repairman than `num_blobs_in_slot * repair_redundancy`. In this case the
|
||||
// first `num_blobs_in_slot * repair_redundancy` repairmen would send one blob, and the rest
|
||||
// would not be responsible for sending any repairs
|
||||
assert_eq!(
|
||||
none_results,
|
||||
num_repairmen.saturating_sub(num_blobs_in_slot * repair_redundancy)
|
||||
);
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ use crate::result::Result;
|
||||
use crate::service::Service;
|
||||
use crate::sigverify_stage::VerifiedPackets;
|
||||
use crate::{packet, sigverify};
|
||||
use solana_metrics::counter::Counter;
|
||||
use solana_metrics::inc_new_counter_debug;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
@ -56,7 +56,7 @@ impl ClusterInfoVoteListener {
|
||||
let (votes, new_ts) = cluster_info.read().unwrap().get_votes(last_ts);
|
||||
if poh_recorder.lock().unwrap().bank().is_some() {
|
||||
last_ts = new_ts;
|
||||
inc_new_counter_info!("cluster_info_vote_listener-recv_count", votes.len());
|
||||
inc_new_counter_debug!("cluster_info_vote_listener-recv_count", votes.len());
|
||||
let msgs = packet::to_packets(&votes);
|
||||
if !msgs.is_empty() {
|
||||
let r = if sigverify_disabled {
|
||||
@ -82,3 +82,41 @@ impl Service for ClusterInfoVoteListener {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::locktower::MAX_RECENT_VOTES;
|
||||
use crate::packet;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use solana_vote_api::vote_instruction;
|
||||
use solana_vote_api::vote_state::Vote;
|
||||
|
||||
#[test]
|
||||
fn test_max_vote_tx_fits() {
|
||||
solana_logger::setup();
|
||||
let node_keypair = Keypair::new();
|
||||
let vote_keypair = Keypair::new();
|
||||
let votes = (0..MAX_RECENT_VOTES)
|
||||
.map(|i| Vote::new(i as u64, Hash::default()))
|
||||
.collect::<Vec<_>>();
|
||||
let vote_ix = vote_instruction::vote(
|
||||
&node_keypair.pubkey(),
|
||||
&vote_keypair.pubkey(),
|
||||
&vote_keypair.pubkey(),
|
||||
votes,
|
||||
);
|
||||
|
||||
let mut vote_tx = Transaction::new_unsigned_instructions(vec![vote_ix]);
|
||||
vote_tx.partial_sign(&[&node_keypair], Hash::default());
|
||||
vote_tx.partial_sign(&[&vote_keypair], Hash::default());
|
||||
|
||||
use bincode::serialized_size;
|
||||
info!("max vote size {}", serialized_size(&vote_tx).unwrap());
|
||||
|
||||
let msgs = packet::to_packets(&[vote_tx]); // panics if won't fit
|
||||
|
||||
assert_eq!(msgs.len(), 1);
|
||||
}
|
||||
}
|
||||
|
@ -6,22 +6,24 @@ use crate::blocktree::Blocktree;
|
||||
use crate::cluster_info::FULLNODE_PORT_RANGE;
|
||||
use crate::contact_info::ContactInfo;
|
||||
use crate::entry::{Entry, EntrySlice};
|
||||
use crate::gossip_service::discover_nodes;
|
||||
use crate::gossip_service::discover_cluster;
|
||||
use crate::locktower::VOTE_THRESHOLD_DEPTH;
|
||||
use crate::poh_service::PohServiceConfig;
|
||||
use solana_client::thin_client::create_client;
|
||||
use solana_runtime::epoch_schedule::MINIMUM_SLOT_LENGTH;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::poh_config::PohConfig;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
|
||||
use solana_sdk::system_transaction;
|
||||
use solana_sdk::timing::{
|
||||
duration_as_ms, DEFAULT_TICKS_PER_SLOT, NUM_CONSECUTIVE_LEADER_SLOTS, NUM_TICKS_PER_SECOND,
|
||||
duration_as_ms, DEFAULT_NUM_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT,
|
||||
NUM_CONSECUTIVE_LEADER_SLOTS,
|
||||
};
|
||||
use solana_sdk::transport::TransportError;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
const SLOT_MILLIS: u64 = (DEFAULT_TICKS_PER_SLOT * 1000) / NUM_TICKS_PER_SECOND;
|
||||
const DEFAULT_SLOT_MILLIS: u64 = (DEFAULT_TICKS_PER_SLOT * 1000) / DEFAULT_NUM_TICKS_PER_SECOND;
|
||||
|
||||
/// Spend and verify from every node in the network
|
||||
pub fn spend_and_verify_all_nodes(
|
||||
@ -29,7 +31,7 @@ pub fn spend_and_verify_all_nodes(
|
||||
funding_keypair: &Keypair,
|
||||
nodes: usize,
|
||||
) {
|
||||
let cluster_nodes = discover_nodes(&entry_point_info.gossip, nodes).unwrap();
|
||||
let (cluster_nodes, _) = discover_cluster(&entry_point_info.gossip, nodes).unwrap();
|
||||
assert!(cluster_nodes.len() >= nodes);
|
||||
for ingress_node in &cluster_nodes {
|
||||
let random_keypair = Keypair::new();
|
||||
@ -38,13 +40,9 @@ pub fn spend_and_verify_all_nodes(
|
||||
.poll_get_balance(&funding_keypair.pubkey())
|
||||
.expect("balance in source");
|
||||
assert!(bal > 0);
|
||||
let mut transaction = system_transaction::transfer(
|
||||
&funding_keypair,
|
||||
&random_keypair.pubkey(),
|
||||
1,
|
||||
client.get_recent_blockhash().unwrap(),
|
||||
0,
|
||||
);
|
||||
let (blockhash, _fee_calculator) = client.get_recent_blockhash().unwrap();
|
||||
let mut transaction =
|
||||
system_transaction::transfer(&funding_keypair, &random_keypair.pubkey(), 1, blockhash);
|
||||
let confs = VOTE_THRESHOLD_DEPTH + 1;
|
||||
let sig = client
|
||||
.retry_transfer_until_confirmed(&funding_keypair, &mut transaction, 5, confs)
|
||||
@ -64,13 +62,9 @@ pub fn send_many_transactions(node: &ContactInfo, funding_keypair: &Keypair, num
|
||||
.poll_get_balance(&funding_keypair.pubkey())
|
||||
.expect("balance in source");
|
||||
assert!(bal > 0);
|
||||
let mut transaction = system_transaction::transfer(
|
||||
&funding_keypair,
|
||||
&random_keypair.pubkey(),
|
||||
1,
|
||||
client.get_recent_blockhash().unwrap(),
|
||||
0,
|
||||
);
|
||||
let (blockhash, _fee_calculator) = client.get_recent_blockhash().unwrap();
|
||||
let mut transaction =
|
||||
system_transaction::transfer(&funding_keypair, &random_keypair.pubkey(), 1, blockhash);
|
||||
client
|
||||
.retry_transfer(&funding_keypair, &mut transaction, 5)
|
||||
.unwrap();
|
||||
@ -78,13 +72,13 @@ pub fn send_many_transactions(node: &ContactInfo, funding_keypair: &Keypair, num
|
||||
}
|
||||
|
||||
pub fn fullnode_exit(entry_point_info: &ContactInfo, nodes: usize) {
|
||||
let cluster_nodes = discover_nodes(&entry_point_info.gossip, nodes).unwrap();
|
||||
let (cluster_nodes, _) = discover_cluster(&entry_point_info.gossip, nodes).unwrap();
|
||||
assert!(cluster_nodes.len() >= nodes);
|
||||
for node in &cluster_nodes {
|
||||
let client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||
assert!(client.fullnode_exit().unwrap());
|
||||
}
|
||||
sleep(Duration::from_millis(SLOT_MILLIS));
|
||||
sleep(Duration::from_millis(DEFAULT_SLOT_MILLIS));
|
||||
for node in &cluster_nodes {
|
||||
let client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||
assert!(client.fullnode_exit().is_err());
|
||||
@ -126,42 +120,45 @@ pub fn verify_ledger_ticks(ledger_path: &str, ticks_per_slot: usize) {
|
||||
|
||||
pub fn sleep_n_epochs(
|
||||
num_epochs: f64,
|
||||
config: &PohServiceConfig,
|
||||
config: &PohConfig,
|
||||
ticks_per_slot: u64,
|
||||
slots_per_epoch: u64,
|
||||
) {
|
||||
let num_ticks_per_second = {
|
||||
match config {
|
||||
PohServiceConfig::Sleep(d) => (1000 / duration_as_ms(d)) as f64,
|
||||
_ => panic!("Unsuppported tick config for testing"),
|
||||
}
|
||||
};
|
||||
|
||||
let num_ticks_per_second = (1000 / duration_as_ms(&config.target_tick_duration)) as f64;
|
||||
let num_ticks_to_sleep = num_epochs * ticks_per_slot as f64 * slots_per_epoch as f64;
|
||||
sleep(Duration::from_secs(
|
||||
((num_ticks_to_sleep + num_ticks_per_second - 1.0) / num_ticks_per_second) as u64,
|
||||
));
|
||||
let secs = ((num_ticks_to_sleep + num_ticks_per_second - 1.0) / num_ticks_per_second) as u64;
|
||||
warn!("sleep_n_epochs: {} seconds", secs);
|
||||
sleep(Duration::from_secs(secs));
|
||||
}
|
||||
|
||||
pub fn kill_entry_and_spend_and_verify_rest(
|
||||
entry_point_info: &ContactInfo,
|
||||
funding_keypair: &Keypair,
|
||||
nodes: usize,
|
||||
slot_millis: u64,
|
||||
) {
|
||||
solana_logger::setup();
|
||||
let cluster_nodes = discover_nodes(&entry_point_info.gossip, nodes).unwrap();
|
||||
let (cluster_nodes, _) = discover_cluster(&entry_point_info.gossip, nodes).unwrap();
|
||||
assert!(cluster_nodes.len() >= nodes);
|
||||
let client = create_client(entry_point_info.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||
let first_two_epoch_slots = MINIMUM_SLOT_LENGTH * 3;
|
||||
|
||||
for ingress_node in &cluster_nodes {
|
||||
client
|
||||
.poll_get_balance(&ingress_node.id)
|
||||
.unwrap_or_else(|err| panic!("Node {} has no balance: {}", ingress_node.id, err));
|
||||
}
|
||||
|
||||
info!("sleeping for 2 leader fortnights");
|
||||
sleep(Duration::from_millis(
|
||||
SLOT_MILLIS * NUM_CONSECUTIVE_LEADER_SLOTS * 2,
|
||||
slot_millis * first_two_epoch_slots as u64,
|
||||
));
|
||||
info!("done sleeping for 2 fortnights");
|
||||
info!("killing entry point");
|
||||
info!("done sleeping for first 2 warmup epochs");
|
||||
info!("killing entry point: {}", entry_point_info.id);
|
||||
assert!(client.fullnode_exit().unwrap());
|
||||
info!("sleeping for 2 leader fortnights");
|
||||
info!("sleeping for some time");
|
||||
sleep(Duration::from_millis(
|
||||
SLOT_MILLIS * NUM_CONSECUTIVE_LEADER_SLOTS,
|
||||
slot_millis * NUM_CONSECUTIVE_LEADER_SLOTS,
|
||||
));
|
||||
info!("done sleeping for 2 fortnights");
|
||||
for ingress_node in &cluster_nodes {
|
||||
@ -170,10 +167,10 @@ pub fn kill_entry_and_spend_and_verify_rest(
|
||||
}
|
||||
|
||||
let client = create_client(ingress_node.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||
let bal = client
|
||||
let balance = client
|
||||
.poll_get_balance(&funding_keypair.pubkey())
|
||||
.expect("balance in source");
|
||||
assert!(bal > 0);
|
||||
assert_ne!(balance, 0);
|
||||
|
||||
let mut result = Ok(());
|
||||
let mut retries = 0;
|
||||
@ -184,12 +181,12 @@ pub fn kill_entry_and_spend_and_verify_rest(
|
||||
}
|
||||
|
||||
let random_keypair = Keypair::new();
|
||||
let (blockhash, _fee_calculator) = client.get_recent_blockhash().unwrap();
|
||||
let mut transaction = system_transaction::transfer(
|
||||
&funding_keypair,
|
||||
&random_keypair.pubkey(),
|
||||
1,
|
||||
client.get_recent_blockhash().unwrap(),
|
||||
0,
|
||||
blockhash,
|
||||
);
|
||||
|
||||
let confs = VOTE_THRESHOLD_DEPTH + 1;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user