Compare commits
94 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8f181b4350 | ||
|
48844924e5 | ||
|
f84593ad5f | ||
|
0469dc52ac | ||
|
4cf418f33f | ||
|
6c46fcfa4e | ||
|
12ec5304f2 | ||
|
e32f798d5f | ||
|
68a8b955bc | ||
|
f479021c0f | ||
|
b91afb7079 | ||
|
e189c429d5 | ||
|
6a1904664c | ||
|
3285cf8047 | ||
|
bdee3a25f2 | ||
|
8655df0520 | ||
|
c43eecb8ca | ||
|
18f45ebc2c | ||
|
fd28642603 | ||
|
038583b466 | ||
|
ed138d392d | ||
|
58f1f0a28b | ||
|
330d9330b0 | ||
|
d626a89c88 | ||
|
db5d22e532 | ||
|
aa8759744e | ||
|
060db36c34 | ||
|
fa1ea1c458 | ||
|
7685ba2805 | ||
|
a0d940acf0 | ||
|
f4c914a630 | ||
|
eede274cfe | ||
|
4df79b653b | ||
|
a2c1fa7cb4 | ||
|
95cead91a5 | ||
|
89c42ecd3f | ||
|
f93c9f052f | ||
|
e2871053bd | ||
|
351c9c33d2 | ||
|
59f2a478b7 | ||
|
3f7cd4adc4 | ||
|
4318854a64 | ||
|
430740b691 | ||
|
797603a0fe | ||
|
f402139991 | ||
|
4db72d85d7 | ||
|
007e17c290 | ||
|
ad7e727938 | ||
|
3d5eeab6d9 | ||
|
8278585545 | ||
|
061d6ec8fd | ||
|
000cc27e53 | ||
|
9b3092b965 | ||
|
ca819fc4fb | ||
|
5ff8f57c0e | ||
|
4798612560 | ||
|
9760cb2e6a | ||
|
46b3b3a1c6 | ||
|
1e70f85e83 | ||
|
b2d6681762 | ||
|
1b51cba778 | ||
|
19ab7333aa | ||
|
b0e6604b9a | ||
|
9ce1d5e990 | ||
|
facc47cb62 | ||
|
3dba8b7952 | ||
|
5e40a5bfc1 | ||
|
c60baf99f3 | ||
|
de04884c1b | ||
|
e666509409 | ||
|
28aff96d21 | ||
|
242975f8cd | ||
|
c6ba6cac83 | ||
|
dc67dd3357 | ||
|
733c2a0b07 | ||
|
07d6212d18 | ||
|
c20d60e4cf | ||
|
7147f03efe | ||
|
6740cb5b02 | ||
|
1e8e99cc3e | ||
|
ef7f30e09f | ||
|
ca8e0ec7ae | ||
|
2a4f4b3e53 | ||
|
7cecd3851a | ||
|
4d189f2c38 | ||
|
9a232475a7 | ||
|
09c9897591 | ||
|
06d7573478 | ||
|
0b55ffa368 | ||
|
ae750bb16b | ||
|
80b2f2f6b7 | ||
|
6684d84fbc | ||
|
dc02abae3c | ||
|
6caec655d3 |
@@ -1,42 +0,0 @@
|
|||||||
version: '{build}'
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- /^v[0-9.]+\.[0-9.]+/
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- '%USERPROFILE%\.cargo'
|
|
||||||
- '%APPVEYOR_BUILD_FOLDER%\target'
|
|
||||||
|
|
||||||
clone_folder: d:\projects\solana
|
|
||||||
|
|
||||||
build_script:
|
|
||||||
- bash ci/publish-tarball.sh
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
- provider: Slack
|
|
||||||
incoming_webhook:
|
|
||||||
secure: GJsBey+F5apAtUm86MHVJ68Uqa6WN1SImcuIc4TsTZrDhA8K1QWUNw9FFQPybUWDyOcS5dly3kubnUqlGt9ux6Ad2efsfRIQYWv0tOVXKeY=
|
|
||||||
channel: ci-status
|
|
||||||
on_build_success: false
|
|
||||||
on_build_failure: true
|
|
||||||
on_build_status_changed: true
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
- provider: S3
|
|
||||||
access_key_id:
|
|
||||||
secure: fTbJl6JpFebR40J7cOWZ2mXBa3kIvEiXgzxAj6L3N7A=
|
|
||||||
secret_access_key:
|
|
||||||
secure: vItsBXb2rEFLvkWtVn/Rcxu5a5+2EwC+b7GsA0waJy9hXh6XuBAD0lnHd9re3g/4
|
|
||||||
bucket: release.solana.com
|
|
||||||
region: us-west-1
|
|
||||||
set_public: true
|
|
||||||
|
|
||||||
- provider: GitHub
|
|
||||||
auth_token:
|
|
||||||
secure: 81fEmPZ0cV1wLtNuUrcmtgxKF6ROQF1+/ft5m+fHX21z6PoeCbaNo8cTyLioWBj7
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
on:
|
|
||||||
appveyor_repo_tag: true
|
|
1
.buildkite/env/.gitignore
vendored
1
.buildkite/env/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/secrets_unencrypted.ejson
|
|
17
.buildkite/env/secrets.ejson
vendored
17
.buildkite/env/secrets.ejson
vendored
@@ -1,15 +1,12 @@
|
|||||||
{
|
{
|
||||||
"_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f",
|
"_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f",
|
||||||
"environment": {
|
"environment": {
|
||||||
"CODECOV_TOKEN": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:JnxhrIxh09AvqdJgrVSYmb7PxSrh19aE:07WzVExCHEd1lJ1m8QizRRthGri+WBNeZRKjjEvsy5eo4gv3HD7zVEm42tVTGkqITKkBNQ==]",
|
"CODECOV_TOKEN": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:rHBSqXK7uSnveA4qwUxARZjTNZcA0hXU:ko8lLGwPECpVm19znWBRxKEpMF7xpTHBCEzVOxRar2wDThw4lNDAKqTS61vtkJLtdkHtug==]",
|
||||||
"CRATES_IO_TOKEN": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:d0jJqC32/axwzq/N7kMRmpxKhnRrhtpt:zvcPHwkOzGnjhNkAQSejwdy1Jkr9wR1qXFFCnfIjyt/XQYubzB1tLkoly/qdmeb5]",
|
"CRATES_IO_TOKEN": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:NzN6y0ooXJBYvxB589khepthSxhKFkLB:ZTTFZh2A/kB2SAgjJJAMbwAfanRlzxOCNMVcA2MXBCpQHJeeZGULg+0MLACYswfS]",
|
||||||
"GEOLOCATION_API_KEY": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:R4gfB6Ey4i50HyfLt4UZDLBqg3qHEUye:UfZCOgt8XI6Y2g+ivCRVoS1fjFycFs7/GSevvCqh1B50mG0+hzpEyzXQLuKG5OeI]",
|
"GITHUB_TOKEN": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:iy0Fnxeo0aslTCvgXc5Ddj2ly6ZsQ8gK:GNOOj/kZUJ2rYKxTbLyVKtajWNoGQ3PcChwfEB4HdN18qDHlB96Z7gx01Pcf0qeIHODOWRtxlH4=]",
|
||||||
"GITHUB_TOKEN": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:Vq2dkGTOzfEpRht0BAGHFp/hDogMvXJe:tFXHg1epVt2mq9hkuc5sRHe+KAnVREi/p8S+IZu67XRyzdiA/nGak1k860FXYuuzuaE0QWekaEc=]",
|
"INFLUX_DATABASE": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:Ly/TpIRF0oCxmiBWv225S3mX8s6pfQR+:+tXGB2c9rRCVDcgNO1IDOo89]",
|
||||||
"INFLUX_DATABASE": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:5KI9WBkXx3R/W4m256mU5MJOE7N8aAT9:Cb8QFELZ9I60t5zhJ9h55Kcs]",
|
"INFLUX_PASSWORD": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:ycrq1uQLoSfI932czD+krUOaJeLWpeq6:2iS7ukp/C7wVD3IT0GvQVcwccWGyLr4UocStF/XiDi0OB/N3YKIKN8SQU4ob1b6StAPZ/XOHmag=]",
|
||||||
"INFLUX_PASSWORD": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:hQRMpLCrav+OYkNphkeM4hagdVoZv5Iw:AUO76rr6+gF1OLJA8ZLSG8wHKXgYCPNk6gRCV8rBhZBJ4KwDaxpvOhMl7bxxXG6jol7v4aRa/Lk=]",
|
"INFLUX_USERNAME": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:35hBKofakZ4Db/u0TOW53RXoNWzJTIcl:HWREcMTrgZ8DGB0ZupgSzNWr/tVyE06P]",
|
||||||
"INFLUX_USERNAME": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:R7BNmQjfeqoGDAFTJu9bYTGHol2NgnYN:Q2tOT/EBcFvhFk+DKLKmVU7tLCpVC3Ui]",
|
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu": "EJ[1:+7nLVR8NlnN48zgaJPPXF9JOZDXVNHDZLeARlCFHyRk=:kRz8CyJYKAg/AiwgLrcRNDJAmlRX2zvX:uV1XV6y2Fb+dN4Z9BIMPBRiNS3n+NL8GlJXyu1i7meIsph1DzfLg4Thcp5Mj9nUsFNLgqQgjnsa5C4XNY/h5AgMSzRrJxVj7RhVTRmDJ5/Vjq6v7wCMRfBOvF3rITsV4zTwWSV8yafFmS+ZQ+QJTRgtYsuoYAUNZ06IEebfDHcuNwws72hEGoD9w43hOLSpyEOmXbtZ9h1lIRxrgsrhYDpBlU5LkhDeTXAX5M5dwYxyquJFRwd5quGDV5DYsCh9bAkbjAyjWYymVJ78U9YJIQHT9izzQqTDlMQN49EbLo7MDIaC7O7HVtb7unDJs+DRejbHacoyWVulqVVwu3GRiZezu8zdjwzGHphMMxOtKQaidnqYgflNp/O01I8wZRgR1alsGcmIhEhI8YV/IvQ==]"
|
||||||
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:Egc2dMrHDU0NcZ71LwGv/V66shUhwYUE:04VoIb8CKy7KYhQ5W4cEW9SDKZltxWBL5Hob106lMBbUOD/yUvKYcG3Ep8JfTMwO3K8zowW5HpU/IdGoilX0XWLiJJ6t+p05WWK0TA16nOEtwrEG+UK8wm3sN+xCO20i4jDhpNpgg3FYFHT5rKTHW8+zaBTNUX/SFxkN67Lm+92IM28CXYE43SU1WV6H99hGFFVpTK5JVM3JuYU1ex/dHRE+xCzTr4MYUB/F+nGoNFW8HUDV/y0e1jxT9to3x0SmnytEEuk+5RUzFuEt9cKNFeNml3fOCi4qL+sfj/Y5pjH9xDiUxsvH/8NL35jbLP244aFHgWcp]",
|
|
||||||
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_apple_darwin": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:NeOxSoWCvXB9AL4H6OK26l/7bmsKd/oz:Ijfoxtvk2CHlN1ZXHup3Gg/914kbbAkEGWJfvozA8UIe+aUzUObMyTrKkVOeNAH8Q8YH9tNzk7RRnrTcpnzeCCBLlWcVEeruMxHox3mPRzmSeDLxtbzCl9VePlRO3T7jg90K5hW+ZAkd5J/WJNzpAcmr93ts/of3MbvGHSujId/efCTzJEcP6JInnBb8Vrj7TlgKbzUlnqpq1+NjYPSXN3maKa9pKeo2JWxZlGBMoy6QWUUY5GbYEylw9smwh1LJcHZjlaZNMuOl4gNKtaSr38IXQkAXaRUJDPAmPras00YObKzXU8RkTrP4EoP/jx5LPR7f]",
|
|
||||||
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_pc_windows_msvc": "EJ[1:yGpTmjdbyjW2kjgIHkFoJv7Ue7EbUvUbqHyw6anGgWg=:7t+56twjW+jR7fpFNNeRFLPd7E4lbmyN:JuviDpkQrfVcNUGRGsa2e/UhvH6tTYyk1s4cHHE5xZH1NByL7Kpqx36VG/+o1AUGEeSQdsBnKgzYdMoFYbO8o50DoRPc86QIEVXCupD6J9avxLFtQgOWgJp+/mCdUVXlqXiFs/vQgS/L4psrcKdF6WHd77BeUr6ll8DjH+9m5FC9Rcai2pXno6VbPpunHQ0oUdYzhFR64+LiRacBaefQ9igZ+nSEWDLqbaZSyfm9viWkijoVFTq8gAgdXXEh7g0QdxVE5T6bPristJhT6jWBhWunPUCDNFFErWIsbRGctepl4pbCWqh2hNTw9btSgVfeY6uGCOsdy9E=]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
CI_BUILD_START=$(date +%s)
|
CI_BUILD_START=$(date +%s)
|
||||||
export CI_BUILD_START
|
export CI_BUILD_START
|
||||||
|
|
||||||
source ci/env.sh
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Kill any running docker containers, which are potentially left over from the
|
# Kill any running docker containers, which are potentially left over from the
|
||||||
# previous CI job
|
# previous CI job
|
||||||
@@ -33,10 +31,3 @@ source ci/env.sh
|
|||||||
kill -9 "$victim" || true
|
kill -9 "$victim" || true
|
||||||
done
|
done
|
||||||
)
|
)
|
||||||
|
|
||||||
# HACK: These are in our docker images, need to be removed from CARGO_HOME
|
|
||||||
# because we try to cache downloads across builds with CARGO_HOME
|
|
||||||
# cargo lacks a facility for "system" tooling, always tries CARGO_HOME first
|
|
||||||
cargo uninstall cargo-audit || true
|
|
||||||
cargo uninstall svgbob_cli || true
|
|
||||||
cargo uninstall mdbook || true
|
|
||||||
|
@@ -10,8 +10,6 @@
|
|||||||
set -x
|
set -x
|
||||||
rsync -a --delete --link-dest="$PWD" target "$d"
|
rsync -a --delete --link-dest="$PWD" target "$d"
|
||||||
du -hs "$d"
|
du -hs "$d"
|
||||||
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$d")
|
|
||||||
echo "--- ${cacheSizeInGB}GB: $d"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@@ -14,18 +14,14 @@ export PS4="++"
|
|||||||
(
|
(
|
||||||
set -x
|
set -x
|
||||||
d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
|
d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
|
||||||
MAX_CACHE_SIZE=18 # gigabytes
|
|
||||||
|
|
||||||
if [[ -d $d ]]; then
|
if [[ -d $d ]]; then
|
||||||
du -hs "$d"
|
du -hs "$d"
|
||||||
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$d")
|
read -r cacheSizeInGB _ < <(du -s --block-size=1000000000 "$d")
|
||||||
echo "--- ${cacheSizeInGB}GB: $d"
|
if [[ $cacheSizeInGB -gt 10 ]]; then
|
||||||
if [[ $cacheSizeInGB -gt $MAX_CACHE_SIZE ]]; then
|
echo "$d has gotten too large, removing it"
|
||||||
echo "--- $d is too large, removing it"
|
|
||||||
rm -rf "$d"
|
rm -rf "$d"
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
echo "--- $d not present"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$d"/target
|
mkdir -p "$d"/target
|
||||||
|
@@ -10,22 +10,11 @@
|
|||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")"/..
|
cd "$(dirname "$0")"/..
|
||||||
|
|
||||||
if [[ -n $BUILDKITE_TAG ]]; then
|
buildkite-agent pipeline upload ci/buildkite.yml
|
||||||
buildkite-agent annotate --style info --context release-tag \
|
|
||||||
"https://github.com/solana-labs/solana/releases/$BUILDKITE_TAG"
|
if [[ $BUILDKITE_BRANCH =~ ^pull ]]; then
|
||||||
buildkite-agent pipeline upload ci/buildkite-release.yml
|
|
||||||
else
|
|
||||||
if [[ $BUILDKITE_BRANCH =~ ^pull ]]; then
|
|
||||||
# Add helpful link back to the corresponding Github Pull Request
|
# Add helpful link back to the corresponding Github Pull Request
|
||||||
buildkite-agent annotate --style info --context pr-backlink \
|
buildkite-agent annotate --style info --context pr-backlink \
|
||||||
"Github Pull Request: https://github.com/solana-labs/solana/$BUILDKITE_BRANCH"
|
"Github Pull Request: https://github.com/solana-labs/solana/$BUILDKITE_BRANCH"
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $BUILDKITE_MESSAGE =~ GitBook: ]]; then
|
|
||||||
buildkite-agent annotate --style info --context gitbook-ci-skip \
|
|
||||||
"GitBook commit detected, CI skipped"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
buildkite-agent pipeline upload ci/buildkite.yml
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
root: ./docs/src
|
|
||||||
|
|
||||||
structure:
|
|
||||||
readme: introduction.md
|
|
||||||
summary: SUMMARY.md
|
|
24
.github/stale.yml
vendored
24
.github/stale.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
only: pulls
|
|
||||||
|
|
||||||
# Number of days of inactivity before a pull request becomes stale
|
|
||||||
daysUntilStale: 7
|
|
||||||
|
|
||||||
# Number of days of inactivity before a stale pull request is closed
|
|
||||||
daysUntilClose: 7
|
|
||||||
|
|
||||||
# Issues with these labels will never be considered stale
|
|
||||||
exemptLabels:
|
|
||||||
- security
|
|
||||||
|
|
||||||
# Label to use when marking a pull request as stale
|
|
||||||
staleLabel: stale
|
|
||||||
|
|
||||||
# Comment to post when marking a pull request as stale. Set to `false` to disable
|
|
||||||
markComment: >
|
|
||||||
This pull request has been automatically marked as stale because it has not had
|
|
||||||
recent activity. It will be closed if no further activity occurs.
|
|
||||||
|
|
||||||
# Comment to post when closing a stale pull request. Set to `false` to disable
|
|
||||||
closeComment: >
|
|
||||||
This stale pull request has been automatically closed.
|
|
||||||
Thank you for your contributions.
|
|
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,24 +1,23 @@
|
|||||||
/docs/html/
|
|
||||||
/docs/src/tests.ok
|
|
||||||
/docs/src/.gitbook/assets/*.svg
|
|
||||||
/farf/
|
|
||||||
/solana-release/
|
|
||||||
/solana-release.tar.bz2
|
|
||||||
/solana-metrics/
|
|
||||||
/solana-metrics.tar.bz2
|
|
||||||
/target/
|
/target/
|
||||||
|
/ledger-tool/target/
|
||||||
|
/wallet/target/
|
||||||
|
/core/target/
|
||||||
|
/book/html/
|
||||||
|
/book/src/img/
|
||||||
|
/book/src/tests.ok
|
||||||
|
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
.cargo
|
.cargo
|
||||||
|
|
||||||
|
# node config that is rsynced
|
||||||
/config/
|
/config/
|
||||||
|
# node config that remains local
|
||||||
|
/config-local/
|
||||||
|
|
||||||
# log files
|
# log files
|
||||||
*.log
|
*.log
|
||||||
log-*.txt
|
log-*.txt
|
||||||
log-*/
|
|
||||||
|
|
||||||
# intellij files
|
# intellij files
|
||||||
/.idea/
|
/.idea/
|
||||||
/solana.iml
|
/solana.iml
|
||||||
/.vscode/
|
|
||||||
|
53
.mergify.yml
53
.mergify.yml
@@ -1,53 +0,0 @@
|
|||||||
# Validate your changes with:
|
|
||||||
#
|
|
||||||
# $ curl -F 'data=@.mergify.yml' https://gh.mergify.io/validate
|
|
||||||
#
|
|
||||||
# https://doc.mergify.io/
|
|
||||||
pull_request_rules:
|
|
||||||
- name: remove outdated reviews
|
|
||||||
conditions:
|
|
||||||
- base=master
|
|
||||||
actions:
|
|
||||||
dismiss_reviews:
|
|
||||||
changes_requested: true
|
|
||||||
- name: set automerge label on mergify backport PRs
|
|
||||||
conditions:
|
|
||||||
- author=mergify[bot]
|
|
||||||
- head~=^mergify/bp/
|
|
||||||
- "#status-failure=0"
|
|
||||||
actions:
|
|
||||||
label:
|
|
||||||
add:
|
|
||||||
- automerge
|
|
||||||
- name: v0.23 backport
|
|
||||||
conditions:
|
|
||||||
- base=master
|
|
||||||
- label=v0.23
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- v0.23
|
|
||||||
- name: v1.0 backport
|
|
||||||
conditions:
|
|
||||||
- base=master
|
|
||||||
- label=v1.0
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- v1.0
|
|
||||||
- name: v1.1 backport
|
|
||||||
conditions:
|
|
||||||
- base=master
|
|
||||||
- label=v1.1
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- v1.1
|
|
||||||
- name: v1.2 backport
|
|
||||||
conditions:
|
|
||||||
- base=master
|
|
||||||
- label=v1.2
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- v1.2
|
|
42
.travis.yml
42
.travis.yml
@@ -1,42 +0,0 @@
|
|||||||
os:
|
|
||||||
- osx
|
|
||||||
|
|
||||||
language: rust
|
|
||||||
rust:
|
|
||||||
- stable
|
|
||||||
|
|
||||||
install:
|
|
||||||
- source ci/rust-version.sh
|
|
||||||
|
|
||||||
script:
|
|
||||||
- source ci/env.sh
|
|
||||||
- ci/publish-tarball.sh
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- /^v\d+\.\d+/
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
slack:
|
|
||||||
on_success: change
|
|
||||||
secure: F4IjOE05MyaMOdPRL+r8qhs7jBvv4yDM3RmFKE1zNXnfUOqV4X38oQM1EI+YVsgpMQLj/pxnEB7wcTE4Bf86N6moLssEULCpvAuMVoXj4QbWdomLX+01WbFa6fLVeNQIg45NHrz2XzVBhoKOrMNnl+QI5mbR2AlS5oqsudHsXDnyLzZtd4Y5SDMdYG1zVWM01+oNNjgNfjcCGmOE/K0CnOMl6GPi3X9C34tJ19P2XT7MTDsz1/IfEF7fro2Q8DHEYL9dchJMoisXSkem5z7IDQkGzXsWdWT4NnndUvmd1MlTCE9qgoXDqRf95Qh8sB1Dz08HtvgfaosP2XjtNTfDI9BBYS15Ibw9y7PchAJE1luteNjF35EOy6OgmCLw/YpnweqfuNViBZz+yOPWXVC0kxnPIXKZ1wyH9ibeH6E4hr7a8o9SV/6SiWIlbYF+IR9jPXyTCLP/cc3sYljPWxDnhWFwFdRVIi3PbVAhVu7uWtVUO17Oc9gtGPgs/GrhOMkJfwQPXaudRJDpVZowxTX4x9kefNotlMAMRgq+Drbmgt4eEBiCNp0ITWgh17BiE1U09WS3myuduhoct85+FoVeaUkp1sxzHVtGsNQH0hcz7WcpZyOM+AwistJA/qzeEDQao5zi1eKWPbO2xAhi2rV1bDH6bPf/4lDBwLRqSiwvlWU=
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
- provider: s3
|
|
||||||
access_key_id: $AWS_ACCESS_KEY_ID
|
|
||||||
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
|
||||||
bucket: release.solana.com
|
|
||||||
region: us-west-1
|
|
||||||
skip_cleanup: true
|
|
||||||
acl: public_read
|
|
||||||
local_dir: travis-s3-upload
|
|
||||||
on:
|
|
||||||
all_branches: true
|
|
||||||
- provider: releases
|
|
||||||
api_key: $GITHUB_TOKEN
|
|
||||||
skip_cleanup: true
|
|
||||||
file_glob: true
|
|
||||||
file: travis-release-upload/*
|
|
||||||
on:
|
|
||||||
tags: true
|
|
284
CONTRIBUTING.md
284
CONTRIBUTING.md
@@ -1,41 +1,23 @@
|
|||||||
# Solana Coding Guidelines
|
Solana Coding Guidelines
|
||||||
|
===
|
||||||
|
|
||||||
The goal of these guidelines is to improve developer productivity by allowing
|
The goal of these guidelines is to improve developer productivity by allowing developers to
|
||||||
developers to jump into any file in the codebase and not need to adapt to
|
jump any file in the codebase and not need to adapt to inconsistencies in how the code is
|
||||||
inconsistencies in how the code is written. The codebase should appear as if it
|
written. The codebase should appear as if it had been authored by a single developer. If you
|
||||||
had been authored by a single developer. If you don't agree with a convention,
|
don't agree with a convention, submit a PR patching this document and let's discuss! Once
|
||||||
submit a PR patching this document and let's discuss! Once the PR is accepted,
|
the PR is accepted, *all* code should be updated as soon as possible to reflect the new
|
||||||
*all* code should be updated as soon as possible to reflect the new
|
|
||||||
conventions.
|
conventions.
|
||||||
|
|
||||||
## Pull Requests
|
Pull Requests
|
||||||
|
---
|
||||||
|
|
||||||
Small, frequent PRs are much preferred to large, infrequent ones. A large PR is
|
Small, frequent PRs are much preferred to large, infrequent ones. A large PR is difficult
|
||||||
difficult to review, can block others from making progress, and can quickly get
|
to review, can block others from making progress, and can quickly get its author into
|
||||||
its author into "rebase hell". A large PR oftentimes arises when one change
|
"rebase hell". A large PR oftentimes arises when one change requires another, which requires
|
||||||
requires another, which requires another, and then another. When you notice
|
another, and then another. When you notice those dependencies, put the fix into a commit of
|
||||||
those dependencies, put the fix into a commit of its own, then checkout a new
|
its own, then checkout a new branch, and cherrypick it. Open a PR to start the review
|
||||||
branch, and cherry-pick it.
|
process and then jump back to your original branch to keep making progress. Once the commit
|
||||||
|
is merged, you can use git-rebase to purge it from your original branch.
|
||||||
```bash
|
|
||||||
$ git commit -am "Fix foo, needed by bar"
|
|
||||||
$ git checkout master
|
|
||||||
$ git checkout -b fix-foo
|
|
||||||
$ git cherry-pick fix-bar
|
|
||||||
$ git push --set-upstream origin fix-foo
|
|
||||||
```
|
|
||||||
|
|
||||||
Open a PR to start the review process and then jump back to your original
|
|
||||||
branch to keep making progress. Consider rebasing to make your fix the first
|
|
||||||
commit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ git checkout fix-bar
|
|
||||||
$ git rebase -i master <Move fix-foo to top>
|
|
||||||
```
|
|
||||||
|
|
||||||
Once the commit is merged, rebase the original branch to purge the
|
|
||||||
cherry-picked commit:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ git pull --rebase upstream master
|
$ git pull --rebase upstream master
|
||||||
@@ -43,206 +25,88 @@ $ git pull --rebase upstream master
|
|||||||
|
|
||||||
### How big is too big?
|
### How big is too big?
|
||||||
|
|
||||||
If there are no functional changes, PRs can be very large and that's no
|
If there are no functional changes, PRs can be very large and that's no problem. If,
|
||||||
problem. If, however, your changes are making meaningful changes or additions,
|
however, your changes are making meaningful changes or additions, then about 1,000 lines of
|
||||||
then about 1000 lines of changes is about the most you should ask a Solana
|
changes is about the most you should ask a Solana maintainer to review.
|
||||||
maintainer to review.
|
|
||||||
|
|
||||||
### Should I send small PRs as I develop large, new components?
|
### Should I send small PRs as I develop large, new components?
|
||||||
|
|
||||||
Add only code to the codebase that is ready to be deployed. If you are building
|
Add only code to the codebase that is ready to be deployed. If you are building a large
|
||||||
a large library, consider developing it in a separate git repository. When it
|
library, consider developing it in a separate git repository. When it is ready to be
|
||||||
is ready to be integrated, the Solana maintainers will work with you to decide
|
integrated, the Solana maintainers will work with you to decide on a path forward. Smaller
|
||||||
on a path forward. Smaller libraries may be copied in whereas very large ones
|
libraries may be copied in whereas very large ones may be pulled in with a package manager.
|
||||||
may be pulled in with a package manager.
|
|
||||||
|
|
||||||
## Getting Pull Requests Merged
|
|
||||||
|
|
||||||
There is no single person assigned to watching GitHub PR queue and ushering you
|
|
||||||
through the process. Typically, you will ask the person that wrote a component
|
|
||||||
to review changes to it. You can find the author using `git blame` or asking on
|
|
||||||
Discord. When working to get your PR merged, it's most important to understand
|
|
||||||
that changing the code is your priority and not necessarily a priority of the
|
|
||||||
person you need an approval from. Also, while you may interact the most with
|
|
||||||
the component author, you should aim to be inclusive of others. Providing a
|
|
||||||
detailed problem description is the most effective means of engaging both the
|
|
||||||
component author and other potentially interested parties.
|
|
||||||
|
|
||||||
Consider opening all PRs as Draft Pull Requests first. Using a draft PR allows
|
|
||||||
you to kickstart the CI automation, which typically takes between 10 and 30
|
|
||||||
minutes to execute. Use that time to write a detailed problem description. Once
|
|
||||||
the description is written and CI succeeds, click the "Ready to Review" button
|
|
||||||
and add reviewers. Adding reviewers before CI succeeds is a fast path to losing
|
|
||||||
reviewer engagement. Not only will they be notified and see the PR is not yet
|
|
||||||
ready for them, they will also be bombarded them with additional notifications
|
|
||||||
each time you push a commit to get past CI or until they "mute" the PR. Once
|
|
||||||
muted, you'll need to reach out over some other medium, such as Discord, to
|
|
||||||
request they have another look. When you use draft PRs, no notifications are
|
|
||||||
sent when you push commits and edit the PR description. Use draft PRs
|
|
||||||
liberally. Don't bug the humans until you have gotten past the bots.
|
|
||||||
|
|
||||||
### What should be in my PR description?
|
|
||||||
|
|
||||||
Reviewing code is hard work and generally involves an attempt to guess the
|
|
||||||
author's intent at various levels. Please assume reviewer time is scarce and do
|
|
||||||
what you can to make your PR as consumable as possible. Inspired by techniques
|
|
||||||
for writing good whitepapers, the guidance here aims to maximize reviewer
|
|
||||||
engagement.
|
|
||||||
|
|
||||||
Assume the reviewer will spend no more than a few seconds reading the PR title.
|
|
||||||
If it doesn't describe a noteworthy change, don't expect the reviewer to click
|
|
||||||
to see more.
|
|
||||||
|
|
||||||
Next, like the abstract of a whitepaper, the reviewer will spend ~30 seconds
|
|
||||||
reading the PR problem description. If what is described there doesn't look
|
|
||||||
more important than competing issues, don't expect the reviewer to read on.
|
|
||||||
|
|
||||||
Next, the reviewer will read the proposed changes. At this point, the reviewer
|
|
||||||
needs to be convinced the proposed changes are a *good* solution to the problem
|
|
||||||
described above. If the proposed changes, not the code changes, generates
|
|
||||||
discussion, consider closing the PR and returning with a design proposal
|
|
||||||
instead.
|
|
||||||
|
|
||||||
Finally, once the reviewer understands the problem and agrees with the approach
|
|
||||||
to solving it, the reviewer will view the code changes. At this point, the
|
|
||||||
reviewer is simply looking to see if the implementation actually implements
|
|
||||||
what was proposed and if that implementation is maintainable. When a concise,
|
|
||||||
readable test for each new code path is present, the reviewer can safely ignore
|
|
||||||
the details of its implementation. When those tests are missing, expect to
|
|
||||||
either lose engagement or get a pile of review comments as the reviewer
|
|
||||||
attempts to consider every ambiguity in your implementation.
|
|
||||||
|
|
||||||
### The PR Title
|
|
||||||
|
|
||||||
The PR title should contain a brief summary of the change, from the perspective
|
|
||||||
of the user. Examples of good titles:
|
|
||||||
|
|
||||||
* Add rent to accounts
|
|
||||||
* Fix out-of-memory error in validator
|
|
||||||
* Clean up `process_message()` in runtime
|
|
||||||
|
|
||||||
The conventions here are all the same as a good git commit title:
|
|
||||||
|
|
||||||
* First word capitalized and in the imperative mood, not past tense ("add", not
|
|
||||||
"added")
|
|
||||||
* No trailing period
|
|
||||||
* What was done, whom it was done to, and in what context
|
|
||||||
|
|
||||||
### The PR Problem Statement
|
|
||||||
|
|
||||||
The git repo implements a product with various features. The problem statement
|
|
||||||
should describe how the product is missing a feature, how a feature is
|
|
||||||
incomplete, or how the implementation of a feature is somehow undesirable. If
|
|
||||||
an issue being fixed already describes the problem, go ahead and copy-paste it.
|
|
||||||
As mentioned above, reviewer time is scarce. Given a queue of PRs to review,
|
|
||||||
the reviewer may ignore PRs that expect them to click through links to see if
|
|
||||||
the PR warrants attention.
|
|
||||||
|
|
||||||
### The Proposed Changes
|
|
||||||
|
|
||||||
Typically the content under the "Proposed changes" section will be a bulleted
|
|
||||||
list of steps taken to solve the problem. Oftentimes, the list is identical to
|
|
||||||
the subject lines of the git commits contained in the PR. It's especially
|
|
||||||
generous (and not expected) to rebase or reword commits such that each change
|
|
||||||
matches the logical flow in your PR description.
|
|
||||||
|
|
||||||
### When will my PR be reviewed?
|
### When will my PR be reviewed?
|
||||||
|
|
||||||
PRs are typically reviewed and merged in under 7 days. If your PR has been open
|
PRs are typically reviewed and merged in under 7 days. If your PR has been open for longer,
|
||||||
for longer, it's a strong indicator that the reviewers aren't confident the
|
it's a strong indicator that the reviewers aren't confident the change meets the quality
|
||||||
change meets the quality standards of the codebase. You might consider closing
|
standards of the codebase. You might consider closing it and coming back with smaller PRs
|
||||||
it and coming back with smaller PRs and longer descriptions detailing what
|
and longer descriptions detailing what problem it solves and how it solves it.
|
||||||
problem it solves and how it solves it. Old PRs will be marked stale and then
|
|
||||||
closed automatically 7 days later.
|
|
||||||
|
|
||||||
### How to manage review feedback?
|
Draft Pull Requests
|
||||||
|
---
|
||||||
|
|
||||||
After a reviewer provides feedback, you can quickly say "acknowledged, will
|
If you want early feedback on your PR, use GitHub's "Draft Pull Request" mechanism. Draft
|
||||||
fix" using a thumb's up emoji. If you're confident your fix is exactly as
|
PRs are a convenient way to collaborate with the Solana maintainers without triggering
|
||||||
prescribed, add a reply "Fixed in COMMIT\_HASH" and mark the comment as
|
notifications as you make changes. When you feel your PR is ready for a broader audience,
|
||||||
resolved. If you're not sure, reply "Is this what you had in mind?
|
you can transition your draft PR to a standard PR with the click of a button.
|
||||||
COMMIT\_HASH" and if so, the reviewer will reply and mark the conversation as
|
|
||||||
resolved. Marking conversations as resolved is an excellent way to engage more
|
|
||||||
reviewers. Leaving conversations open may imply the PR is not yet ready for
|
|
||||||
additional review.
|
|
||||||
|
|
||||||
### When will my PR be re-reviewed?
|
Rust coding conventions
|
||||||
|
---
|
||||||
|
|
||||||
Recall that once your PR is opened, a notification is sent every time you push
|
* All Rust code is formatted using the latest version of `rustfmt`. Once installed, it will be
|
||||||
a commit. After a reviewer adds feedback, they won't be checking on the status
|
updated automatically when you update the compiler with `rustup`.
|
||||||
of that feedback after every new commit. Instead, directly mention the reviewer
|
|
||||||
when you feel your PR is ready for another pass.
|
|
||||||
|
|
||||||
## Draft Pull Requests
|
* All Rust code is linted with Clippy. If you'd prefer to ignore its advice, do so explicitly:
|
||||||
|
|
||||||
If you want early feedback on your PR, use GitHub's "Draft Pull Request"
|
```rust
|
||||||
mechanism. Draft PRs are a convenient way to collaborate with the Solana
|
#[allow(clippy::too_many_arguments)]
|
||||||
maintainers without triggering notifications as you make changes. When you feel
|
```
|
||||||
your PR is ready for a broader audience, you can transition your draft PR to a
|
|
||||||
standard PR with the click of a button.
|
|
||||||
|
|
||||||
Do not add reviewers to draft PRs. GitHub doesn't automatically clear
|
|
||||||
approvals when you click "Ready for Review", so a review that meant "I approve
|
|
||||||
of the direction" suddenly has the appearance of "I approve of these changes."
|
|
||||||
Instead, add a comment that mentions the usernames that you would like a review
|
|
||||||
from. Ask explicitly what you would like feedback on.
|
|
||||||
|
|
||||||
## Rust coding conventions
|
|
||||||
|
|
||||||
* All Rust code is formatted using the latest version of `rustfmt`. Once
|
|
||||||
installed, it will be updated automatically when you update the compiler with
|
|
||||||
`rustup`.
|
|
||||||
|
|
||||||
* All Rust code is linted with Clippy. If you'd prefer to ignore its advice, do
|
|
||||||
so explicitly:
|
|
||||||
|
|
||||||
```rust #[allow(clippy::too_many_arguments)] ```
|
|
||||||
|
|
||||||
Note: Clippy defaults can be overridden in the top-level file `.clippy.toml`.
|
Note: Clippy defaults can be overridden in the top-level file `.clippy.toml`.
|
||||||
|
|
||||||
* For variable names, when in doubt, spell it out. The mapping from type names
|
* For variable names, when in doubt, spell it out. The mapping from type names to variable names
|
||||||
to variable names is to lowercase the type name, putting an underscore before
|
is to lowercase the type name, putting an underscore before each capital letter. Variable names
|
||||||
each capital letter. Variable names should *not* be abbreviated unless being
|
should *not* be abbreviated unless being used as closure arguments and the brevity improves
|
||||||
used as closure arguments and the brevity improves readability. When a function
|
readability. When a function has multiple instances of the same type, qualify each with a
|
||||||
has multiple instances of the same type, qualify each with a prefix and
|
prefix and underscore (i.e. alice_keypair) or a numeric suffix (i.e. tx0).
|
||||||
underscore (i.e. alice\_keypair) or a numeric suffix (i.e. tx0).
|
|
||||||
|
|
||||||
* For function and method names, use `<verb>_<subject>`. For unit tests, that
|
* For function and method names, use `<verb>_<subject>`. For unit tests, that verb should
|
||||||
verb should always be `test` and for benchmarks the verb should always be
|
always be `test` and for benchmarks the verb should always be `bench`. Avoid namespacing
|
||||||
`bench`. Avoid namespacing function names with some arbitrary word. Avoid
|
function names with some arbitrary word. Avoid abbreviating words in function names.
|
||||||
abbreviating words in function names.
|
|
||||||
|
|
||||||
* As they say, "When in Rome, do as the Romans do." A good patch should
|
* As they say, "When in Rome, do as the Romans do." A good patch should acknowledge the coding
|
||||||
acknowledge the coding conventions of the code that surrounds it, even in the
|
conventions of the code that surrounds it, even in the case where that code has not yet been
|
||||||
case where that code has not yet been updated to meet the conventions described
|
updated to meet the conventions described here.
|
||||||
here.
|
|
||||||
|
|
||||||
|
|
||||||
## Terminology
|
Terminology
|
||||||
|
---
|
||||||
|
|
||||||
Inventing new terms is allowed, but should only be done when the term is widely
|
Inventing new terms is allowed, but should only be done when the term is widely used and
|
||||||
used and understood. Avoid introducing new 3-letter terms, which can be
|
understood. Avoid introducing new 3-letter terms, which can be confused with 3-letter acronyms.
|
||||||
confused with 3-letter acronyms.
|
|
||||||
|
|
||||||
[Terms currently in use](docs/src/terminology.md)
|
[Terms currently in use](book/src/terminology.md)
|
||||||
|
|
||||||
|
|
||||||
## Design Proposals
|
Proposing architectural changes
|
||||||
|
---
|
||||||
|
|
||||||
Solana's architecture is described by docs generated from markdown files in
|
Solana's architecture is described by a book generated from markdown files in
|
||||||
the `docs/src/` directory, maintained by an *editor* (currently @garious). To
|
the `book/src/` directory, maintained by an *editor* (currently @garious). To
|
||||||
add a design proposal, you'll need to include it in the
|
change the architecture, you'll need to at least propose a change the content
|
||||||
[Accepted Design Proposals](https://docs.solana.com/proposals)
|
under the [Proposed
|
||||||
section of the Solana docs. Here's the full process:
|
Changes](https://solana-labs.github.io/book-edge/proposals.html) chapter. Here's
|
||||||
|
the full process:
|
||||||
|
|
||||||
1. Propose a design by creating a PR that adds a markdown document to the
|
1. Propose to a change to the architecture by creating a PR that adds a
|
||||||
`docs/src/proposals` directory and references it from the [table of
|
markdown document to the directory `book/src/` and references it from the
|
||||||
contents](docs/src/SUMMARY.md). Add any relevant *maintainers* to the PR
|
[table of contents](book/src/SUMMARY.md). Add the editor and any relevant
|
||||||
review.
|
*maintainers* to the PR review.
|
||||||
2. The PR being merged indicates your proposed change was accepted and that the
|
2. The PR being merged indicates your proposed change was accepted and that the
|
||||||
maintainers support your plan of attack.
|
editor and maintainers support your plan of attack.
|
||||||
3. Submit PRs that implement the proposal. When the implementation reveals the
|
3. Submit PRs that implement the proposal. When the implementation reveals the
|
||||||
need for tweaks to the proposal, be sure to update the proposal and have that
|
need for tweaks to the architecture, be sure to update the proposal and have
|
||||||
change reviewed by the same people as in step 1.
|
that change reviewed by the same people as in step 1.
|
||||||
4. Once the implementation is complete, submit a PR that moves the link from
|
4. Once the implementation is complete, the editor will then work to integrate
|
||||||
the Accepted Proposals to the Implemented Proposals section.
|
the document into the book.
|
||||||
|
6005
Cargo.lock
generated
6005
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
116
Cargo.toml
116
Cargo.toml
@@ -1,64 +1,92 @@
|
|||||||
|
[package]
|
||||||
|
name = "solana-workspace"
|
||||||
|
description = "Blockchain, Rebuilt for Scale"
|
||||||
|
version = "0.12.1"
|
||||||
|
documentation = "https://docs.rs/solana"
|
||||||
|
homepage = "https://solana.com/"
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://github.com/solana-labs/solana"
|
||||||
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
chacha = ["solana/chacha"]
|
||||||
|
cuda = ["solana/cuda"]
|
||||||
|
erasure = ["solana/erasure"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
bincode = "1.1.2"
|
||||||
|
bs58 = "0.2.0"
|
||||||
|
hashbrown = "0.1.8"
|
||||||
|
log = "0.4.2"
|
||||||
|
rand = "0.6.5"
|
||||||
|
rayon = "1.0.0"
|
||||||
|
reqwest = "0.9.11"
|
||||||
|
serde_json = "1.0.39"
|
||||||
|
solana = { path = "core", version = "0.12.1" }
|
||||||
|
solana-budget-program = { path = "programs/budget", version = "0.12.1" }
|
||||||
|
solana-client = { path = "client", version = "0.12.1" }
|
||||||
|
solana-logger = { path = "logger", version = "0.12.1" }
|
||||||
|
solana-netutil = { path = "netutil", version = "0.12.1" }
|
||||||
|
solana-runtime = { path = "runtime", version = "0.12.1" }
|
||||||
|
solana-sdk = { path = "sdk", version = "0.12.1" }
|
||||||
|
solana-vote-api = { path = "programs/vote_api", version = "0.12.1" }
|
||||||
|
sys-info = "0.5.6"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "banking_stage"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "blocktree"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "ledger"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "gen_keys"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "sigverify"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
required-features = ["chacha"]
|
||||||
|
name = "chacha"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"bench-exchange",
|
".",
|
||||||
"bench-streamer",
|
"bench-streamer",
|
||||||
"bench-tps",
|
"bench-tps",
|
||||||
"banking-bench",
|
|
||||||
"chacha",
|
|
||||||
"chacha-cuda",
|
|
||||||
"chacha-sys",
|
|
||||||
"cli-config",
|
|
||||||
"client",
|
|
||||||
"core",
|
"core",
|
||||||
"faucet",
|
"drone",
|
||||||
"perf",
|
"fullnode",
|
||||||
"validator",
|
|
||||||
"genesis",
|
"genesis",
|
||||||
"genesis-programs",
|
|
||||||
"gossip",
|
|
||||||
"install",
|
|
||||||
"keygen",
|
"keygen",
|
||||||
"ledger",
|
|
||||||
"ledger-tool",
|
"ledger-tool",
|
||||||
"local-cluster",
|
|
||||||
"logger",
|
"logger",
|
||||||
"log-analyzer",
|
|
||||||
"merkle-tree",
|
|
||||||
"measure",
|
|
||||||
"metrics",
|
"metrics",
|
||||||
"net-shaper",
|
"programs/bpf",
|
||||||
"programs/bpf_loader",
|
"programs/bpf_loader",
|
||||||
"programs/budget",
|
"programs/budget",
|
||||||
"programs/btc_spv",
|
"programs/budget_api",
|
||||||
"programs/btc_spv_bin",
|
"programs/token",
|
||||||
"programs/config",
|
"programs/token_api",
|
||||||
"programs/exchange",
|
|
||||||
"programs/failure",
|
"programs/failure",
|
||||||
"programs/noop",
|
"programs/noop",
|
||||||
"programs/ownable",
|
"programs/rewards",
|
||||||
"programs/stake",
|
"programs/rewards_api",
|
||||||
"programs/storage",
|
"programs/storage",
|
||||||
"programs/vest",
|
"programs/storage_api",
|
||||||
"programs/vote",
|
"programs/vote",
|
||||||
"archiver",
|
"programs/vote_api",
|
||||||
"archiver-lib",
|
"replicator",
|
||||||
"archiver-utils",
|
|
||||||
"remote-wallet",
|
|
||||||
"runtime",
|
|
||||||
"sdk",
|
"sdk",
|
||||||
"sdk-c",
|
|
||||||
"scripts",
|
|
||||||
"sys-tuner",
|
|
||||||
"upload-perf",
|
"upload-perf",
|
||||||
"net-utils",
|
|
||||||
"vote-signer",
|
"vote-signer",
|
||||||
"cli",
|
"wallet",
|
||||||
"rayon-threadlimit",
|
|
||||||
"watchtower",
|
|
||||||
]
|
|
||||||
|
|
||||||
exclude = [
|
|
||||||
"programs/bpf",
|
|
||||||
"programs/move_loader",
|
|
||||||
"programs/librapay",
|
|
||||||
]
|
]
|
||||||
|
exclude = ["programs/bpf/rust/noop"]
|
||||||
|
121
README.md
121
README.md
@@ -1,5 +1,5 @@
|
|||||||
[](https://crates.io/crates/solana-core)
|
[](https://crates.io/crates/solana)
|
||||||
[](https://docs.rs/solana-core)
|
[](https://docs.rs/solana)
|
||||||
[](https://buildkite.com/solana-labs/solana/builds?branch=master)
|
[](https://buildkite.com/solana-labs/solana/builds?branch=master)
|
||||||
[](https://codecov.io/gh/solana-labs/solana)
|
[](https://codecov.io/gh/solana-labs/solana)
|
||||||
|
|
||||||
@@ -9,46 +9,26 @@ Blockchain Rebuilt for Scale
|
|||||||
Solana™ is a new blockchain architecture built from the ground up for scale. The architecture supports
|
Solana™ is a new blockchain architecture built from the ground up for scale. The architecture supports
|
||||||
up to 710 thousand transactions per second on a gigabit network.
|
up to 710 thousand transactions per second on a gigabit network.
|
||||||
|
|
||||||
Documentation
|
Disclaimer
|
||||||
===
|
===
|
||||||
|
|
||||||
Before you jump into the code, review the documentation [Solana: Blockchain Rebuilt for Scale](https://docs.solana.com).
|
All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the author's best effort. It is up to the reader to check and validate their accuracy and truthfulness. Furthermore nothing in this project constitutes a solicitation for investment.
|
||||||
|
|
||||||
(The _latest_ development version of the docs is [available here](https://docs.solana.com/v/master).)
|
Introduction
|
||||||
|
|
||||||
Release Binaries
|
|
||||||
===
|
===
|
||||||
Official release binaries are available at [Github Releases](https://github.com/solana-labs/solana/releases).
|
|
||||||
|
|
||||||
Additionally we provide pre-release binaries for the latest code on the edge and
|
It's possible for a centralized database to process 710,000 transactions per second on a standard gigabit network if the transactions are, on average, no more than 176 bytes. A centralized database can also replicate itself and maintain high availability without significantly compromising that transaction rate using the distributed system technique known as Optimistic Concurrency Control [\[H.T.Kung, J.T.Robinson (1981)\]](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.65.4735). At Solana, we're demonstrating that these same theoretical limits apply just as well to blockchain on an adversarial network. The key ingredient? Finding a way to share time when nodes can't trust one-another. Once nodes can trust time, suddenly ~40 years of distributed systems research becomes applicable to blockchain!
|
||||||
beta channels. Note that these pre-release binaries may be less stable than an
|
|
||||||
official release.
|
|
||||||
|
|
||||||
### Edge channel
|
> Perhaps the most striking difference between algorithms obtained by our method and ones based upon timeout is that using timeout produces a traditional distributed algorithm in which the processes operate asynchronously, while our method produces a globally synchronous one in which every process does the same thing at (approximately) the same time. Our method seems to contradict the whole purpose of distributed processing, which is to permit different processes to operate independently and perform different functions. However, if a distributed system is really a single system, then the processes must be synchronized in some way. Conceptually, the easiest way to synchronize processes is to get them all to do the same thing at the same time. Therefore, our method is used to implement a kernel that performs the necessary synchronization--for example, making sure that two different processes do not try to modify a file at the same time. Processes might spend only a small fraction of their time executing the synchronizing kernel; the rest of the time, they can operate independently--e.g., accessing different files. This is an approach we have advocated even when fault-tolerance is not required. The method's basic simplicity makes it easier to understand the precise properties of a system, which is crucial if one is to know just how fault-tolerant the system is. [\[L.Lamport (1984)\]](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.1078)
|
||||||
#### Linux (x86_64-unknown-linux-gnu)
|
|
||||||
* [solana.tar.bz2](http://release.solana.com/edge/solana-release-x86_64-unknown-linux-gnu.tar.bz2)
|
|
||||||
* [solana-install-init](http://release.solana.com/edge/solana-install-init-x86_64-unknown-linux-gnu) as a stand-alone executable
|
|
||||||
#### mac OS (x86_64-apple-darwin)
|
|
||||||
* [solana.tar.bz2](http://release.solana.com/edge/solana-release-x86_64-apple-darwin.tar.bz2)
|
|
||||||
* [solana-install-init](http://release.solana.com/edge/solana-install-init-x86_64-apple-darwin) as a stand-alone executable
|
|
||||||
#### Windows (x86_64-pc-windows-msvc)
|
|
||||||
* [solana.tar.bz2](http://release.solana.com/edge/solana-release-x86_64-pc-windows-msvc.tar.bz2)
|
|
||||||
* [solana-install-init.exe](http://release.solana.com/edge/solana-install-init-x86_64-pc-windows-msvc.exe) as a stand-alone executable
|
|
||||||
#### All platforms
|
|
||||||
* [solana-metrics.tar.bz2](http://release.solana.com.s3.amazonaws.com/edge/solana-metrics.tar.bz2)
|
|
||||||
|
|
||||||
### Beta channel
|
Furthermore, and much to our surprise, it can be implemented using a mechanism that has existed in Bitcoin since day one. The Bitcoin feature is called nLocktime and it can be used to postdate transactions using block height instead of a timestamp. As a Bitcoin client, you'd use block height instead of a timestamp if you don't trust the network. Block height turns out to be an instance of what's being called a Verifiable Delay Function in cryptography circles. It's a cryptographically secure way to say time has passed. In Solana, we use a far more granular verifiable delay function, a SHA 256 hash chain, to checkpoint the ledger and coordinate consensus. With it, we implement Optimistic Concurrency Control and are now well en route towards that theoretical limit of 710,000 transactions per second.
|
||||||
#### Linux (x86_64-unknown-linux-gnu)
|
|
||||||
* [solana.tar.bz2](http://release.solana.com/beta/solana-release-x86_64-unknown-linux-gnu.tar.bz2)
|
Architecture
|
||||||
* [solana-install-init](http://release.solana.com/beta/solana-install-init-x86_64-unknown-linux-gnu) as a stand-alone executable
|
===
|
||||||
#### mac OS (x86_64-apple-darwin)
|
|
||||||
* [solana.tar.bz2](http://release.solana.com/beta/solana-release-x86_64-apple-darwin.tar.bz2)
|
Before you jump into the code, review the online book [Solana: Blockchain Rebuilt for Scale](https://solana-labs.github.io/book/).
|
||||||
* [solana-install-init](http://release.solana.com/beta/solana-install-init-x86_64-apple-darwin) as a stand-alone executable
|
|
||||||
#### Windows (x86_64-pc-windows-msvc)
|
(The _latest_ development version of the online book is also [available here](https://solana-labs.github.io/book-edge/).)
|
||||||
* [solana.tar.bz2](http://release.solana.com/beta/solana-release-x86_64-pc-windows-msvc.tar.bz2)
|
|
||||||
* [solana-install-init.exe](http://release.solana.com/beta/solana-install-init-x86_64-pc-windows-msvc.exe) as a stand-alone executable
|
|
||||||
#### All platforms
|
|
||||||
* [solana-metrics.tar.bz2](http://release.solana.com.s3.amazonaws.com/beta/solana-metrics.tar.bz2)
|
|
||||||
|
|
||||||
Developing
|
Developing
|
||||||
===
|
===
|
||||||
@@ -61,10 +41,10 @@ Install rustc, cargo and rustfmt:
|
|||||||
```bash
|
```bash
|
||||||
$ curl https://sh.rustup.rs -sSf | sh
|
$ curl https://sh.rustup.rs -sSf | sh
|
||||||
$ source $HOME/.cargo/env
|
$ source $HOME/.cargo/env
|
||||||
$ rustup component add rustfmt
|
$ rustup component add rustfmt-preview
|
||||||
```
|
```
|
||||||
|
|
||||||
If your rustc version is lower than 1.39.0, please update it:
|
If your rustc version is lower than 1.31.0, please update it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ rustup update
|
$ rustup update
|
||||||
@@ -73,8 +53,7 @@ $ rustup update
|
|||||||
On Linux systems you may need to install libssl-dev, pkg-config, zlib1g-dev, etc. On Ubuntu:
|
On Linux systems you may need to install libssl-dev, pkg-config, zlib1g-dev, etc. On Ubuntu:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo apt-get update
|
$ sudo apt-get install libssl-dev pkg-config zlib1g-dev llvm clang
|
||||||
$ sudo apt-get install libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Download the source code:
|
Download the source code:
|
||||||
@@ -87,7 +66,7 @@ $ cd solana
|
|||||||
Build
|
Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cargo build
|
$ cargo build --all
|
||||||
```
|
```
|
||||||
|
|
||||||
Then to run a minimal local cluster
|
Then to run a minimal local cluster
|
||||||
@@ -101,19 +80,31 @@ Testing
|
|||||||
Run the test suite:
|
Run the test suite:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cargo test
|
$ cargo test --all
|
||||||
|
```
|
||||||
|
|
||||||
|
To emulate all the tests that will run on a Pull Request, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./ci/run-local.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Local Testnet
|
Local Testnet
|
||||||
---
|
---
|
||||||
|
|
||||||
Start your own testnet locally, instructions are in the online docs [Solana: Blockchain Rebuild for Scale: Getting Started](https://docs.solana.com/building-from-source).
|
Start your own testnet locally, instructions are in the book [Solana: Blockchain Rebuild for Scale: Getting Started](https://solana-labs.github.io/book/getting-started.html).
|
||||||
|
|
||||||
Remote Testnets
|
Remote Testnets
|
||||||
---
|
---
|
||||||
|
|
||||||
* `testnet` - public stable testnet accessible via devnet.solana.com. Runs 24/7
|
We maintain several testnets:
|
||||||
|
|
||||||
|
* `testnet` - public stable testnet accessible via testnet.solana.com, with an https proxy for web apps at api.testnet.solana.com. Runs 24/7
|
||||||
|
* `testnet-beta` - public beta channel testnet accessible via beta.testnet.solana.com. Runs 24/7
|
||||||
|
* `testnet-edge` - public edge channel testnet accessible via edge.testnet.solana.com. Runs 24/7
|
||||||
|
* `testnet-perf` - permissioned stable testnet running a 24/7 soak test
|
||||||
|
* `testnet-beta-perf` - permissioned beta channel testnet running a multi-hour soak test weekday mornings
|
||||||
|
* `testnet-edge-perf` - permissioned edge channel testnet running a multi-hour soak test weekday mornings
|
||||||
|
|
||||||
## Deploy process
|
## Deploy process
|
||||||
|
|
||||||
@@ -140,47 +131,6 @@ can run your own testnet using the scripts in the `net/` directory.
|
|||||||
Edit `ci/testnet-manager.sh`
|
Edit `ci/testnet-manager.sh`
|
||||||
|
|
||||||
|
|
||||||
## Metrics Server Maintenance
|
|
||||||
Sometimes the dashboard becomes unresponsive. This happens due to glitch in the metrics server.
|
|
||||||
The current solution is to reset the metrics server. Use the following steps.
|
|
||||||
|
|
||||||
1. The server is hosted in a GCP VM instance. Check if the VM instance is down by trying to SSH
|
|
||||||
into it from the GCP console. The name of the VM is ```metrics-solana-com```.
|
|
||||||
2. If the VM is inaccessible, reset it from the GCP console.
|
|
||||||
3. Once VM is up (or, was already up), the metrics services can be restarted from build automation.
|
|
||||||
1. Navigate to https://buildkite.com/solana-labs/metrics-dot-solana-dot-com in your web browser
|
|
||||||
2. Click on ```New Build```
|
|
||||||
3. This will show a pop up dialog. Click on ```options``` drop down.
|
|
||||||
4. Type in ```FORCE_START=true``` in ```Environment Variables``` text box.
|
|
||||||
5. Click ```Create Build```
|
|
||||||
6. This will restart the metrics services, and the dashboards should be accessible afterwards.
|
|
||||||
|
|
||||||
## Debugging Testnet
|
|
||||||
Testnet may exhibit different symptoms of failures. Primary statistics to check are
|
|
||||||
1. Rise in Confirmation Time
|
|
||||||
2. Nodes are not voting
|
|
||||||
3. Panics, and OOM notifications
|
|
||||||
|
|
||||||
Check the following if there are any signs of failure.
|
|
||||||
1. Did testnet deployment fail?
|
|
||||||
1. View buildkite logs for the last deployment: https://buildkite.com/solana-labs/testnet-management
|
|
||||||
2. Use the relevant branch
|
|
||||||
3. If the deployment failed, look at the build logs. The build artifacts for each remote node is uploaded.
|
|
||||||
It's a good first step to triage from these logs.
|
|
||||||
2. You may have to log into remote node if the deployment succeeded, but something failed during runtime.
|
|
||||||
1. Get the private key for the testnet deployment from ```metrics-solana-com``` GCP instance.
|
|
||||||
2. SSH into ```metrics-solana-com``` using GCP console and do the following.
|
|
||||||
```bash
|
|
||||||
sudo bash
|
|
||||||
cd ~buildkite-agent/.ssh
|
|
||||||
ls
|
|
||||||
```
|
|
||||||
3. Copy the relevant private key to your local machine
|
|
||||||
4. Find the public IP address of the AWS instance for the remote node using AWS console
|
|
||||||
5. ```ssh -i <private key file> ubuntu@<ip address of remote node>```
|
|
||||||
6. The logs are in ```~solana\solana``` folder
|
|
||||||
|
|
||||||
|
|
||||||
Benchmarking
|
Benchmarking
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -225,7 +175,4 @@ better way to solve the same problem, a Pull Request with your solution would mo
|
|||||||
welcome! Likewise, if rewriting a test can better communicate what code it's protecting, please
|
welcome! Likewise, if rewriting a test can better communicate what code it's protecting, please
|
||||||
send us that patch!
|
send us that patch!
|
||||||
|
|
||||||
Disclaimer
|
|
||||||
===
|
|
||||||
|
|
||||||
All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the author's best effort. It is up to the reader to check and validate their accuracy and truthfulness. Furthermore nothing in this project constitutes a solicitation for investment.
|
|
||||||
|
152
RELEASE.md
152
RELEASE.md
@@ -59,127 +59,47 @@ There are three release channels that map to branches as follows:
|
|||||||
* beta - tracks the largest (and latest) `vX.Y` stabilization branch, more stable.
|
* beta - tracks the largest (and latest) `vX.Y` stabilization branch, more stable.
|
||||||
* stable - tracks the second largest `vX.Y` stabilization branch, most stable.
|
* stable - tracks the second largest `vX.Y` stabilization branch, most stable.
|
||||||
|
|
||||||
## Steps to Create a Branch
|
## Release Steps
|
||||||
|
|
||||||
### Create the new branch
|
### Changing channels
|
||||||
1. Check out the latest commit on `master` branch:
|
|
||||||
```
|
When cutting a new channel branch these pre-steps are required:
|
||||||
git fetch --all
|
|
||||||
git checkout upstream/master
|
1. Pick your branch point for release on master.
|
||||||
```
|
1. Create the branch. The name should be "v" + the first 2 "version" fields
|
||||||
1. Determine the new branch name. The name should be "v" + the first 2 version fields
|
|
||||||
from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies
|
from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies
|
||||||
the next branch name is "v0.9".
|
the next branch name is "v0.9".
|
||||||
1. Create the new branch and push this branch to the `solana` repository:
|
1. Push the new branch to the solana repository
|
||||||
```
|
1. Update Cargo.toml on master to the next semantic version (e.g. 0.9.0 -> 0.10.0)
|
||||||
git checkout -b <branchname>
|
by running `./scripts/increment-cargo-version.sh`, then rebuild with a
|
||||||
git push -u origin <branchname>
|
`cargo build --all` to cause a refresh of `Cargo.lock`.
|
||||||
```
|
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
||||||
|
master branch
|
||||||
|
|
||||||
### Update master branch with the next version
|
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".
|
||||||
|
|
||||||
1. After the new branch has been created and pushed, update the Cargo.toml files on **master** to the next semantic version (e.g. 0.9.0 -> 0.10.0) with:
|
### Updating channels (i.e. "making a release")
|
||||||
```
|
|
||||||
scripts/increment-cargo-version.sh minor
|
|
||||||
```
|
|
||||||
1. Rebuild to get an updated version of `Cargo.lock`:
|
|
||||||
```
|
|
||||||
cargo build
|
|
||||||
```
|
|
||||||
1. Push all the changed Cargo.toml and Cargo.lock files to the `master` branch with something like:
|
|
||||||
```
|
|
||||||
git co -b version_update
|
|
||||||
git ls-files -m | xargs git add
|
|
||||||
git commit -m 'Update Cargo.toml versions from X.Y to X.Y+1'
|
|
||||||
git push -u origin version_update
|
|
||||||
```
|
|
||||||
1. Confirm that your freshly cut release branch is shown as `BETA_CHANNEL` and the previous release branch as `STABLE_CHANNEL`:
|
|
||||||
```
|
|
||||||
ci/channel_info.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Steps to Create a Release
|
We use [github's Releases UI](https://github.com/solana-labs/solana/releases) for tagging a release.
|
||||||
|
|
||||||
### Create the Release Tag on GitHub
|
1. Go [there ;)](https://github.com/solana-labs/solana/releases).
|
||||||
|
|
||||||
1. Go to [GitHub's Releases UI](https://github.com/solana-labs/solana/releases) for tagging a release.
|
|
||||||
1. Click "Draft new release". The release tag must exactly match the `version`
|
1. Click "Draft new release". The release tag must exactly match the `version`
|
||||||
field in `/Cargo.toml` prefixed by `v`.
|
field in `/Cargo.toml` prefixed by `v` (ie, `<branchname>.X`).
|
||||||
1. If the Cargo.toml verion field is **0.12.3**, then the release tag must be **v0.12.3**
|
1. If the first major release on the branch (e.g. v0.8.0), paste in [this
|
||||||
1. Make sure the Target Branch field matches the branch you want to make a release on.
|
template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md)
|
||||||
1. If you want to release v0.12.0, the target branch must be v0.12
|
and fill it in.
|
||||||
1. If this is the first release on the branch (e.g. v0.13.**0**), paste in [this
|
1. Test the release by generating a tag using semver's rules. First try at a
|
||||||
template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md). Engineering Lead can provide summary contents for release notes if needed.
|
release should be `<branchname>.X-rc.0`.
|
||||||
1. Click "Save Draft", then confirm the release notes look good and the tag name and branch are correct. Go back into edit the release and click "Publish release" when ready.
|
1. Verify release automation:
|
||||||
|
1. [Crates.io](https://crates.io/crates/solana) should have an updated Solana version.
|
||||||
### Update release branch with the next patch version
|
1. ...
|
||||||
|
1. After testnet deployment, verify that testnets are running correct software.
|
||||||
1. After the new release has been tagged, update the Cargo.toml files on **release branch** to the next semantic version (e.g. 0.9.0 -> 0.9.1) with:
|
http://metrics.solana.com should show testnet running on a hash from your
|
||||||
```
|
newly created branch.
|
||||||
scripts/increment-cargo-version.sh patch
|
1. Once the release has been made, update Cargo.toml on release to the next
|
||||||
```
|
semantic version (e.g. 0.9.0 -> 0.9.1) by running
|
||||||
1. Rebuild to get an updated version of `Cargo.lock`:
|
`./scripts/increment-cargo-version.sh patch`, then rebuild with a `cargo
|
||||||
```
|
build --all` to cause a refresh of `Cargo.lock`.
|
||||||
cargo build
|
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
|
||||||
```
|
release branch
|
||||||
1. Push all the changed Cargo.toml and Cargo.lock files to the **release branch** with something like:
|
|
||||||
```
|
|
||||||
git co -b version_update
|
|
||||||
git ls-files -m | xargs git add
|
|
||||||
git commit -m 'Update Cargo.toml versions from X.Y.Z to X.Y.Z+1'
|
|
||||||
git push -u origin version_update
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verify release automation success
|
|
||||||
1. Go to [Solana Releases](https://github.com/solana-labs/solana/releases) and click on the latest release that you just published. Verify that all of the build artifacts are present. This can take up to 90 minutes after creating the tag.
|
|
||||||
1. The `solana-secondary` Buildkite pipeline handles creating the binary tarballs and updated crates. Look for a job under the tag name of the release: https://buildkite.com/solana-labs/solana-secondary
|
|
||||||
1. [Crates.io](https://crates.io/crates/solana) should have an updated Solana version.
|
|
||||||
|
|
||||||
### Update documentation
|
|
||||||
TODO: Documentation update procedure is WIP as we move to gitbook
|
|
||||||
|
|
||||||
Document the new recommended version by updating `docs/src/running-archiver.md` and `docs/src/validator-testnet.md` on the release (beta) branch to point at the `solana-install` for the upcoming release version.
|
|
||||||
|
|
||||||
### Update software on devnet.solana.com
|
|
||||||
|
|
||||||
The testnet running on devnet.solana.com is set to use a fixed release tag
|
|
||||||
which is set in the Buildkite testnet-management pipeline.
|
|
||||||
This tag needs to be updated and the testnet restarted after a new release
|
|
||||||
tag is created.
|
|
||||||
|
|
||||||
#### 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alert the community
|
|
||||||
|
|
||||||
Notify Discord users on #validator-support that a new release for
|
|
||||||
devnet.solana.com is available
|
|
||||||
|
@@ -1,39 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "solana-archiver-lib"
|
|
||||||
version = "1.0.8"
|
|
||||||
description = "Solana Archiver Library"
|
|
||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
|
||||||
repository = "https://github.com/solana-labs/solana"
|
|
||||||
license = "Apache-2.0"
|
|
||||||
homepage = "https://solana.com/"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bincode = "1.2.1"
|
|
||||||
crossbeam-channel = "0.3"
|
|
||||||
ed25519-dalek = "=1.0.0-pre.1"
|
|
||||||
log = "0.4.8"
|
|
||||||
rand = "0.6.5"
|
|
||||||
rand_chacha = "0.1.1"
|
|
||||||
solana-client = { path = "../client", version = "1.0.8" }
|
|
||||||
solana-storage-program = { path = "../programs/storage", version = "1.0.8" }
|
|
||||||
thiserror = "1.0"
|
|
||||||
serde = "1.0.104"
|
|
||||||
serde_json = "1.0.46"
|
|
||||||
serde_derive = "1.0.103"
|
|
||||||
solana-net-utils = { path = "../net-utils", version = "1.0.8" }
|
|
||||||
solana-chacha = { path = "../chacha", version = "1.0.8" }
|
|
||||||
solana-chacha-sys = { path = "../chacha-sys", version = "1.0.8" }
|
|
||||||
solana-ledger = { path = "../ledger", version = "1.0.8" }
|
|
||||||
solana-logger = { path = "../logger", version = "1.0.8" }
|
|
||||||
solana-perf = { path = "../perf", version = "1.0.8" }
|
|
||||||
solana-sdk = { path = "../sdk", version = "1.0.8" }
|
|
||||||
solana-core = { path = "../core", version = "1.0.8" }
|
|
||||||
solana-archiver-utils = { path = "../archiver-utils", version = "1.0.8" }
|
|
||||||
solana-metrics = { path = "../metrics", version = "1.0.8" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex = "0.4.0"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "solana_archiver_lib"
|
|
@@ -1,936 +0,0 @@
|
|||||||
use crate::result::ArchiverError;
|
|
||||||
use crossbeam_channel::unbounded;
|
|
||||||
use rand::{thread_rng, Rng, SeedableRng};
|
|
||||||
use rand_chacha::ChaChaRng;
|
|
||||||
use solana_archiver_utils::sample_file;
|
|
||||||
use solana_chacha::chacha::{chacha_cbc_encrypt_ledger, CHACHA_BLOCK_SIZE};
|
|
||||||
use solana_client::{
|
|
||||||
rpc_client::RpcClient, rpc_request::RpcRequest, rpc_response::RpcStorageTurn,
|
|
||||||
thin_client::ThinClient,
|
|
||||||
};
|
|
||||||
use solana_core::{
|
|
||||||
cluster_info::{ClusterInfo, Node, VALIDATOR_PORT_RANGE},
|
|
||||||
contact_info::ContactInfo,
|
|
||||||
gossip_service::GossipService,
|
|
||||||
packet::{limited_deserialize, PACKET_DATA_SIZE},
|
|
||||||
repair_service,
|
|
||||||
repair_service::{RepairService, RepairSlotRange, RepairStrategy},
|
|
||||||
serve_repair::ServeRepair,
|
|
||||||
shred_fetch_stage::ShredFetchStage,
|
|
||||||
sigverify_stage::{DisabledSigVerifier, SigVerifyStage},
|
|
||||||
storage_stage::NUM_STORAGE_SAMPLES,
|
|
||||||
streamer::{receiver, responder, PacketReceiver},
|
|
||||||
window_service::WindowService,
|
|
||||||
};
|
|
||||||
use solana_ledger::{
|
|
||||||
blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache, shred::Shred,
|
|
||||||
};
|
|
||||||
use solana_net_utils::bind_in_range;
|
|
||||||
use solana_perf::packet::Packets;
|
|
||||||
use solana_perf::recycler::Recycler;
|
|
||||||
use solana_sdk::packet::Packet;
|
|
||||||
use solana_sdk::{
|
|
||||||
account_utils::StateMut,
|
|
||||||
client::{AsyncClient, SyncClient},
|
|
||||||
clock::{get_complete_segment_from_slot, get_segment_from_slot, Slot},
|
|
||||||
commitment_config::CommitmentConfig,
|
|
||||||
hash::Hash,
|
|
||||||
message::Message,
|
|
||||||
signature::{Keypair, Signature, Signer},
|
|
||||||
timing::timestamp,
|
|
||||||
transaction::Transaction,
|
|
||||||
transport::TransportError,
|
|
||||||
};
|
|
||||||
use solana_storage_program::{
|
|
||||||
storage_contract::StorageContract,
|
|
||||||
storage_instruction::{self, StorageAccountType},
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
io::{self, ErrorKind},
|
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
result,
|
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
|
||||||
sync::mpsc::{channel, Receiver, Sender},
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
thread::{sleep, spawn, JoinHandle},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, ArchiverError>;
|
|
||||||
|
|
||||||
static ENCRYPTED_FILENAME: &str = "ledger.enc";
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub enum ArchiverRequest {
|
|
||||||
GetSlotHeight(SocketAddr),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Archiver {
|
|
||||||
thread_handles: Vec<JoinHandle<()>>,
|
|
||||||
exit: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared Archiver Meta struct used internally
|
|
||||||
#[derive(Default)]
|
|
||||||
struct ArchiverMeta {
|
|
||||||
slot: Slot,
|
|
||||||
slots_per_segment: u64,
|
|
||||||
ledger_path: PathBuf,
|
|
||||||
signature: Signature,
|
|
||||||
ledger_data_file_encrypted: PathBuf,
|
|
||||||
sampling_offsets: Vec<u64>,
|
|
||||||
blockhash: Hash,
|
|
||||||
sha_state: Hash,
|
|
||||||
num_chacha_blocks: usize,
|
|
||||||
client_commitment: CommitmentConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_slot_from_signature(
|
|
||||||
signature: &Signature,
|
|
||||||
storage_turn: u64,
|
|
||||||
slots_per_segment: u64,
|
|
||||||
) -> u64 {
|
|
||||||
let signature_vec = signature.as_ref();
|
|
||||||
let mut segment_index = u64::from(signature_vec[0])
|
|
||||||
| (u64::from(signature_vec[1]) << 8)
|
|
||||||
| (u64::from(signature_vec[1]) << 16)
|
|
||||||
| (u64::from(signature_vec[2]) << 24);
|
|
||||||
let max_segment_index =
|
|
||||||
get_complete_segment_from_slot(storage_turn, slots_per_segment).unwrap();
|
|
||||||
segment_index %= max_segment_index as u64;
|
|
||||||
segment_index * slots_per_segment
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_request_processor(
|
|
||||||
socket: UdpSocket,
|
|
||||||
exit: &Arc<AtomicBool>,
|
|
||||||
slot_receiver: Receiver<u64>,
|
|
||||||
) -> Vec<JoinHandle<()>> {
|
|
||||||
let mut thread_handles = vec![];
|
|
||||||
let (s_reader, r_reader) = channel();
|
|
||||||
let (s_responder, r_responder) = channel();
|
|
||||||
let storage_socket = Arc::new(socket);
|
|
||||||
let recycler = Recycler::default();
|
|
||||||
let t_receiver = receiver(storage_socket.clone(), exit, s_reader, recycler, "archiver");
|
|
||||||
thread_handles.push(t_receiver);
|
|
||||||
|
|
||||||
let t_responder = responder("archiver-responder", storage_socket, r_responder);
|
|
||||||
thread_handles.push(t_responder);
|
|
||||||
|
|
||||||
let exit = exit.clone();
|
|
||||||
let t_processor = spawn(move || {
|
|
||||||
let slot = poll_for_slot(slot_receiver, &exit);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if exit.load(Ordering::Relaxed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let packets = r_reader.recv_timeout(Duration::from_secs(1));
|
|
||||||
|
|
||||||
if let Ok(packets) = packets {
|
|
||||||
for packet in &packets.packets {
|
|
||||||
let req: result::Result<ArchiverRequest, Box<bincode::ErrorKind>> =
|
|
||||||
limited_deserialize(&packet.data[..packet.meta.size]);
|
|
||||||
match req {
|
|
||||||
Ok(ArchiverRequest::GetSlotHeight(from)) => {
|
|
||||||
let packet = Packet::from_data(&from, slot);
|
|
||||||
let _ = s_responder.send(Packets::new(vec![packet]));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
info!("invalid request: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
thread_handles.push(t_processor);
|
|
||||||
thread_handles
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_for_slot(receiver: Receiver<u64>, exit: &Arc<AtomicBool>) -> u64 {
|
|
||||||
loop {
|
|
||||||
let slot = receiver.recv_timeout(Duration::from_secs(1));
|
|
||||||
if let Ok(slot) = slot {
|
|
||||||
return slot;
|
|
||||||
}
|
|
||||||
if exit.load(Ordering::Relaxed) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Archiver {
|
|
||||||
/// Returns a Result that contains an archiver on success
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `ledger_path` - path to where the ledger will be stored.
|
|
||||||
/// Causes panic if none
|
|
||||||
/// * `node` - The archiver node
|
|
||||||
/// * `cluster_entrypoint` - ContactInfo representing an entry into the network
|
|
||||||
/// * `keypair` - Keypair for this archiver
|
|
||||||
#[allow(clippy::new_ret_no_self)]
|
|
||||||
pub fn new(
|
|
||||||
ledger_path: &Path,
|
|
||||||
node: Node,
|
|
||||||
cluster_entrypoint: ContactInfo,
|
|
||||||
keypair: Arc<Keypair>,
|
|
||||||
storage_keypair: Arc<Keypair>,
|
|
||||||
client_commitment: CommitmentConfig,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let exit = Arc::new(AtomicBool::new(false));
|
|
||||||
|
|
||||||
info!("Archiver: id: {}", keypair.pubkey());
|
|
||||||
info!("Creating cluster info....");
|
|
||||||
let mut cluster_info = ClusterInfo::new(node.info.clone(), keypair.clone());
|
|
||||||
cluster_info.set_entrypoint(cluster_entrypoint.clone());
|
|
||||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
|
||||||
|
|
||||||
// Note for now, this ledger will not contain any of the existing entries
|
|
||||||
// in the ledger located at ledger_path, and will only append on newly received
|
|
||||||
// entries after being passed to window_service
|
|
||||||
let blockstore = Arc::new(
|
|
||||||
Blockstore::open(ledger_path).expect("Expected to be able to open database ledger"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let gossip_service = GossipService::new(&cluster_info, None, node.sockets.gossip, &exit);
|
|
||||||
|
|
||||||
info!("Connecting to the cluster via {:?}", cluster_entrypoint);
|
|
||||||
let (nodes, _) =
|
|
||||||
match solana_core::gossip_service::discover_cluster(&cluster_entrypoint.gossip, 1) {
|
|
||||||
Ok(nodes_and_archivers) => nodes_and_archivers,
|
|
||||||
Err(e) => {
|
|
||||||
//shutdown services before exiting
|
|
||||||
exit.store(true, Ordering::Relaxed);
|
|
||||||
gossip_service.join()?;
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let client = solana_core::gossip_service::get_client(&nodes);
|
|
||||||
|
|
||||||
info!("Setting up mining account...");
|
|
||||||
if let Err(e) =
|
|
||||||
Self::setup_mining_account(&client, &keypair, &storage_keypair, client_commitment)
|
|
||||||
{
|
|
||||||
//shutdown services before exiting
|
|
||||||
exit.store(true, Ordering::Relaxed);
|
|
||||||
gossip_service.join()?;
|
|
||||||
return Err(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
let repair_socket = Arc::new(node.sockets.repair);
|
|
||||||
let shred_sockets: Vec<Arc<UdpSocket>> =
|
|
||||||
node.sockets.tvu.into_iter().map(Arc::new).collect();
|
|
||||||
let shred_forward_sockets: Vec<Arc<UdpSocket>> = node
|
|
||||||
.sockets
|
|
||||||
.tvu_forwards
|
|
||||||
.into_iter()
|
|
||||||
.map(Arc::new)
|
|
||||||
.collect();
|
|
||||||
let (shred_fetch_sender, shred_fetch_receiver) = channel();
|
|
||||||
let fetch_stage = ShredFetchStage::new(
|
|
||||||
shred_sockets,
|
|
||||||
shred_forward_sockets,
|
|
||||||
repair_socket.clone(),
|
|
||||||
&shred_fetch_sender,
|
|
||||||
&exit,
|
|
||||||
);
|
|
||||||
let (slot_sender, slot_receiver) = channel();
|
|
||||||
let request_processor =
|
|
||||||
create_request_processor(node.sockets.storage.unwrap(), &exit, slot_receiver);
|
|
||||||
|
|
||||||
let t_archiver = {
|
|
||||||
let exit = exit.clone();
|
|
||||||
let node_info = node.info.clone();
|
|
||||||
let mut meta = ArchiverMeta {
|
|
||||||
ledger_path: ledger_path.to_path_buf(),
|
|
||||||
client_commitment,
|
|
||||||
..ArchiverMeta::default()
|
|
||||||
};
|
|
||||||
spawn(move || {
|
|
||||||
// setup archiver
|
|
||||||
let window_service = match Self::setup(
|
|
||||||
&mut meta,
|
|
||||||
cluster_info.clone(),
|
|
||||||
&blockstore,
|
|
||||||
&exit,
|
|
||||||
&node_info,
|
|
||||||
&storage_keypair,
|
|
||||||
repair_socket,
|
|
||||||
shred_fetch_receiver,
|
|
||||||
slot_sender,
|
|
||||||
) {
|
|
||||||
Ok(window_service) => window_service,
|
|
||||||
Err(e) => {
|
|
||||||
//shutdown services before exiting
|
|
||||||
error!("setup failed {:?}; archiver thread exiting...", e);
|
|
||||||
exit.store(true, Ordering::Relaxed);
|
|
||||||
request_processor
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|t| t.join().unwrap());
|
|
||||||
fetch_stage.join().unwrap();
|
|
||||||
gossip_service.join().unwrap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("setup complete");
|
|
||||||
// run archiver
|
|
||||||
Self::run(
|
|
||||||
&mut meta,
|
|
||||||
&blockstore,
|
|
||||||
cluster_info,
|
|
||||||
&keypair,
|
|
||||||
&storage_keypair,
|
|
||||||
&exit,
|
|
||||||
);
|
|
||||||
// wait until exit
|
|
||||||
request_processor
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|t| t.join().unwrap());
|
|
||||||
fetch_stage.join().unwrap();
|
|
||||||
gossip_service.join().unwrap();
|
|
||||||
window_service.join().unwrap()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
thread_handles: vec![t_archiver],
|
|
||||||
exit,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
meta: &mut ArchiverMeta,
|
|
||||||
blockstore: &Arc<Blockstore>,
|
|
||||||
cluster_info: Arc<RwLock<ClusterInfo>>,
|
|
||||||
archiver_keypair: &Arc<Keypair>,
|
|
||||||
storage_keypair: &Arc<Keypair>,
|
|
||||||
exit: &Arc<AtomicBool>,
|
|
||||||
) {
|
|
||||||
// encrypt segment
|
|
||||||
Self::encrypt_ledger(meta, blockstore).expect("ledger encrypt not successful");
|
|
||||||
let enc_file_path = meta.ledger_data_file_encrypted.clone();
|
|
||||||
// do replicate
|
|
||||||
loop {
|
|
||||||
if exit.load(Ordering::Relaxed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO check if more segments are available - based on space constraints
|
|
||||||
Self::create_sampling_offsets(meta);
|
|
||||||
let sampling_offsets = &meta.sampling_offsets;
|
|
||||||
meta.sha_state =
|
|
||||||
match Self::sample_file_to_create_mining_hash(&enc_file_path, sampling_offsets) {
|
|
||||||
Ok(hash) => hash,
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Error sampling file, exiting: {:?}", err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Self::submit_mining_proof(meta, &cluster_info, archiver_keypair, storage_keypair);
|
|
||||||
|
|
||||||
// TODO make this a lot more frequent by picking a "new" blockhash instead of picking a storage blockhash
|
|
||||||
// prep the next proof
|
|
||||||
let (storage_blockhash, _) = match Self::poll_for_blockhash_and_slot(
|
|
||||||
&cluster_info,
|
|
||||||
meta.slots_per_segment,
|
|
||||||
&meta.blockhash,
|
|
||||||
exit,
|
|
||||||
) {
|
|
||||||
Ok(blockhash_and_slot) => blockhash_and_slot,
|
|
||||||
Err(e) => {
|
|
||||||
warn!(
|
|
||||||
"Error couldn't get a newer blockhash than {:?}. {:?}",
|
|
||||||
meta.blockhash, e
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
meta.blockhash = storage_blockhash;
|
|
||||||
Self::redeem_rewards(
|
|
||||||
&cluster_info,
|
|
||||||
archiver_keypair,
|
|
||||||
storage_keypair,
|
|
||||||
meta.client_commitment,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
exit.store(true, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn redeem_rewards(
|
|
||||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
|
||||||
archiver_keypair: &Arc<Keypair>,
|
|
||||||
storage_keypair: &Arc<Keypair>,
|
|
||||||
client_commitment: CommitmentConfig,
|
|
||||||
) {
|
|
||||||
let nodes = cluster_info.read().unwrap().tvu_peers();
|
|
||||||
let client = solana_core::gossip_service::get_client(&nodes);
|
|
||||||
|
|
||||||
if let Ok(Some(account)) =
|
|
||||||
client.get_account_with_commitment(&storage_keypair.pubkey(), client_commitment)
|
|
||||||
{
|
|
||||||
if let Ok(StorageContract::ArchiverStorage { validations, .. }) = account.state() {
|
|
||||||
if !validations.is_empty() {
|
|
||||||
let ix = storage_instruction::claim_reward(
|
|
||||||
&archiver_keypair.pubkey(),
|
|
||||||
&storage_keypair.pubkey(),
|
|
||||||
);
|
|
||||||
let message = Message::new_with_payer(&[ix], Some(&archiver_keypair.pubkey()));
|
|
||||||
if let Err(e) = client.send_message(&[archiver_keypair.as_ref()], message) {
|
|
||||||
error!("unable to redeem reward, tx failed: {:?}", e);
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"collected mining rewards: Account balance {:?}",
|
|
||||||
client.get_balance_with_commitment(
|
|
||||||
&archiver_keypair.pubkey(),
|
|
||||||
client_commitment
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Redeem mining reward: No account data found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a segment to replicate and download it.
|
|
||||||
fn setup(
|
|
||||||
meta: &mut ArchiverMeta,
|
|
||||||
cluster_info: Arc<RwLock<ClusterInfo>>,
|
|
||||||
blockstore: &Arc<Blockstore>,
|
|
||||||
exit: &Arc<AtomicBool>,
|
|
||||||
node_info: &ContactInfo,
|
|
||||||
storage_keypair: &Arc<Keypair>,
|
|
||||||
repair_socket: Arc<UdpSocket>,
|
|
||||||
shred_fetch_receiver: PacketReceiver,
|
|
||||||
slot_sender: Sender<u64>,
|
|
||||||
) -> Result<WindowService> {
|
|
||||||
let slots_per_segment =
|
|
||||||
match Self::get_segment_config(&cluster_info, meta.client_commitment) {
|
|
||||||
Ok(slots_per_segment) => slots_per_segment,
|
|
||||||
Err(e) => {
|
|
||||||
error!("unable to get segment size configuration, exiting...");
|
|
||||||
//shutdown services before exiting
|
|
||||||
exit.store(true, Ordering::Relaxed);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let (segment_blockhash, segment_slot) = match Self::poll_for_segment(
|
|
||||||
&cluster_info,
|
|
||||||
slots_per_segment,
|
|
||||||
&Hash::default(),
|
|
||||||
exit,
|
|
||||||
) {
|
|
||||||
Ok(blockhash_and_slot) => blockhash_and_slot,
|
|
||||||
Err(e) => {
|
|
||||||
//shutdown services before exiting
|
|
||||||
exit.store(true, Ordering::Relaxed);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let signature = storage_keypair.sign_message(segment_blockhash.as_ref());
|
|
||||||
let slot = get_slot_from_signature(&signature, segment_slot, slots_per_segment);
|
|
||||||
info!("replicating slot: {}", slot);
|
|
||||||
slot_sender.send(slot)?;
|
|
||||||
meta.slot = slot;
|
|
||||||
meta.slots_per_segment = slots_per_segment;
|
|
||||||
meta.signature = signature;
|
|
||||||
meta.blockhash = segment_blockhash;
|
|
||||||
|
|
||||||
let mut repair_slot_range = RepairSlotRange::default();
|
|
||||||
repair_slot_range.end = slot + slots_per_segment;
|
|
||||||
repair_slot_range.start = slot;
|
|
||||||
|
|
||||||
let (retransmit_sender, _) = channel();
|
|
||||||
|
|
||||||
let (verified_sender, verified_receiver) = unbounded();
|
|
||||||
|
|
||||||
let _sigverify_stage = SigVerifyStage::new(
|
|
||||||
shred_fetch_receiver,
|
|
||||||
verified_sender,
|
|
||||||
DisabledSigVerifier::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let window_service = WindowService::new(
|
|
||||||
blockstore.clone(),
|
|
||||||
cluster_info.clone(),
|
|
||||||
verified_receiver,
|
|
||||||
retransmit_sender,
|
|
||||||
repair_socket,
|
|
||||||
&exit,
|
|
||||||
RepairStrategy::RepairRange(repair_slot_range),
|
|
||||||
&Arc::new(LeaderScheduleCache::default()),
|
|
||||||
|_, _, _, _| true,
|
|
||||||
);
|
|
||||||
info!("waiting for ledger download");
|
|
||||||
Self::wait_for_segment_download(
|
|
||||||
slot,
|
|
||||||
slots_per_segment,
|
|
||||||
&blockstore,
|
|
||||||
&exit,
|
|
||||||
&node_info,
|
|
||||||
cluster_info,
|
|
||||||
);
|
|
||||||
Ok(window_service)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_for_segment_download(
|
|
||||||
start_slot: Slot,
|
|
||||||
slots_per_segment: u64,
|
|
||||||
blockstore: &Arc<Blockstore>,
|
|
||||||
exit: &Arc<AtomicBool>,
|
|
||||||
node_info: &ContactInfo,
|
|
||||||
cluster_info: Arc<RwLock<ClusterInfo>>,
|
|
||||||
) {
|
|
||||||
info!(
|
|
||||||
"window created, waiting for ledger download starting at slot {:?}",
|
|
||||||
start_slot
|
|
||||||
);
|
|
||||||
let mut current_slot = start_slot;
|
|
||||||
'outer: loop {
|
|
||||||
while blockstore.is_full(current_slot) {
|
|
||||||
current_slot += 1;
|
|
||||||
info!("current slot: {}", current_slot);
|
|
||||||
if current_slot >= start_slot + slots_per_segment {
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if exit.load(Ordering::Relaxed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Done receiving entries from window_service");
|
|
||||||
|
|
||||||
// Remove archiver from the data plane
|
|
||||||
let mut contact_info = node_info.clone();
|
|
||||||
contact_info.tvu = "0.0.0.0:0".parse().unwrap();
|
|
||||||
contact_info.wallclock = timestamp();
|
|
||||||
// copy over the adopted shred_version from the entrypoint
|
|
||||||
contact_info.shred_version = cluster_info.read().unwrap().my_data().shred_version;
|
|
||||||
{
|
|
||||||
let mut cluster_info_w = cluster_info.write().unwrap();
|
|
||||||
cluster_info_w.insert_self(contact_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encrypt_ledger(meta: &mut ArchiverMeta, blockstore: &Arc<Blockstore>) -> Result<()> {
|
|
||||||
meta.ledger_data_file_encrypted = meta.ledger_path.join(ENCRYPTED_FILENAME);
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut ivec = [0u8; 64];
|
|
||||||
ivec.copy_from_slice(&meta.signature.as_ref());
|
|
||||||
|
|
||||||
let num_encrypted_bytes = chacha_cbc_encrypt_ledger(
|
|
||||||
blockstore,
|
|
||||||
meta.slot,
|
|
||||||
meta.slots_per_segment,
|
|
||||||
&meta.ledger_data_file_encrypted,
|
|
||||||
&mut ivec,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
meta.num_chacha_blocks = num_encrypted_bytes / CHACHA_BLOCK_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Done encrypting the ledger: {:?}",
|
|
||||||
meta.ledger_data_file_encrypted
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_sampling_offsets(meta: &mut ArchiverMeta) {
|
|
||||||
meta.sampling_offsets.clear();
|
|
||||||
let mut rng_seed = [0u8; 32];
|
|
||||||
rng_seed.copy_from_slice(&meta.blockhash.as_ref());
|
|
||||||
let mut rng = ChaChaRng::from_seed(rng_seed);
|
|
||||||
for _ in 0..NUM_STORAGE_SAMPLES {
|
|
||||||
meta.sampling_offsets
|
|
||||||
.push(rng.gen_range(0, meta.num_chacha_blocks) as u64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_file_to_create_mining_hash(
|
|
||||||
enc_file_path: &Path,
|
|
||||||
sampling_offsets: &[u64],
|
|
||||||
) -> Result<Hash> {
|
|
||||||
let sha_state = sample_file(enc_file_path, sampling_offsets)?;
|
|
||||||
info!("sampled sha_state: {}", sha_state);
|
|
||||||
Ok(sha_state)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_mining_account(
|
|
||||||
client: &ThinClient,
|
|
||||||
keypair: &Keypair,
|
|
||||||
storage_keypair: &Keypair,
|
|
||||||
client_commitment: CommitmentConfig,
|
|
||||||
) -> Result<()> {
|
|
||||||
// make sure archiver has some balance
|
|
||||||
info!("checking archiver keypair...");
|
|
||||||
if client.poll_balance_with_timeout_and_commitment(
|
|
||||||
&keypair.pubkey(),
|
|
||||||
&Duration::from_millis(100),
|
|
||||||
&Duration::from_secs(5),
|
|
||||||
client_commitment,
|
|
||||||
)? == 0
|
|
||||||
{
|
|
||||||
return Err(ArchiverError::EmptyStorageAccountBalance);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("checking storage account keypair...");
|
|
||||||
// check if the storage account exists
|
|
||||||
let balance =
|
|
||||||
client.poll_get_balance_with_commitment(&storage_keypair.pubkey(), client_commitment);
|
|
||||||
if balance.is_err() || balance.unwrap() == 0 {
|
|
||||||
let blockhash = match client.get_recent_blockhash_with_commitment(client_commitment) {
|
|
||||||
Ok((blockhash, _)) => blockhash,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(ArchiverError::TransportError(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let ix = storage_instruction::create_storage_account(
|
|
||||||
&keypair.pubkey(),
|
|
||||||
&keypair.pubkey(),
|
|
||||||
&storage_keypair.pubkey(),
|
|
||||||
1,
|
|
||||||
StorageAccountType::Archiver,
|
|
||||||
);
|
|
||||||
let tx = Transaction::new_signed_instructions(&[keypair], ix, blockhash);
|
|
||||||
let signature = client.async_send_transaction(tx)?;
|
|
||||||
client
|
|
||||||
.poll_for_signature_with_commitment(&signature, client_commitment)
|
|
||||||
.map_err(|err| match err {
|
|
||||||
TransportError::IoError(e) => e,
|
|
||||||
TransportError::TransactionError(_) => io::Error::new(
|
|
||||||
ErrorKind::Other,
|
|
||||||
"setup_mining_account: signature not found",
|
|
||||||
),
|
|
||||||
TransportError::Custom(e) => io::Error::new(ErrorKind::Other, e),
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn submit_mining_proof(
|
|
||||||
meta: &ArchiverMeta,
|
|
||||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
|
||||||
archiver_keypair: &Arc<Keypair>,
|
|
||||||
storage_keypair: &Arc<Keypair>,
|
|
||||||
) {
|
|
||||||
// No point if we've got no storage account...
|
|
||||||
let nodes = cluster_info.read().unwrap().tvu_peers();
|
|
||||||
let client = solana_core::gossip_service::get_client(&nodes);
|
|
||||||
let storage_balance = client
|
|
||||||
.poll_get_balance_with_commitment(&storage_keypair.pubkey(), meta.client_commitment);
|
|
||||||
if storage_balance.is_err() || storage_balance.unwrap() == 0 {
|
|
||||||
error!("Unable to submit mining proof, no storage account");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ...or no lamports for fees
|
|
||||||
let balance = client
|
|
||||||
.poll_get_balance_with_commitment(&archiver_keypair.pubkey(), meta.client_commitment);
|
|
||||||
if balance.is_err() || balance.unwrap() == 0 {
|
|
||||||
error!("Unable to submit mining proof, insufficient Archiver Account balance");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let blockhash = match client.get_recent_blockhash_with_commitment(meta.client_commitment) {
|
|
||||||
Ok((blockhash, _)) => blockhash,
|
|
||||||
Err(_) => {
|
|
||||||
error!("unable to get recent blockhash, can't submit proof");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let instruction = storage_instruction::mining_proof(
|
|
||||||
&storage_keypair.pubkey(),
|
|
||||||
meta.sha_state,
|
|
||||||
get_segment_from_slot(meta.slot, meta.slots_per_segment),
|
|
||||||
Signature::new(&meta.signature.as_ref()),
|
|
||||||
meta.blockhash,
|
|
||||||
);
|
|
||||||
let message = Message::new_with_payer(&[instruction], Some(&archiver_keypair.pubkey()));
|
|
||||||
let mut transaction = Transaction::new(
|
|
||||||
&[archiver_keypair.as_ref(), storage_keypair.as_ref()],
|
|
||||||
message,
|
|
||||||
blockhash,
|
|
||||||
);
|
|
||||||
if let Err(err) = client.send_and_confirm_transaction(
|
|
||||||
&[archiver_keypair.as_ref(), storage_keypair.as_ref()],
|
|
||||||
&mut transaction,
|
|
||||||
10,
|
|
||||||
0,
|
|
||||||
) {
|
|
||||||
error!("Error: {:?}; while sending mining proof", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close(self) {
|
|
||||||
self.exit.store(true, Ordering::Relaxed);
|
|
||||||
self.join()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn join(self) {
|
|
||||||
for handle in self.thread_handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_segment_config(
|
|
||||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
|
||||||
client_commitment: CommitmentConfig,
|
|
||||||
) -> Result<u64> {
|
|
||||||
let rpc_peers = {
|
|
||||||
let cluster_info = cluster_info.read().unwrap();
|
|
||||||
cluster_info.all_rpc_peers()
|
|
||||||
};
|
|
||||||
debug!("rpc peers: {:?}", rpc_peers);
|
|
||||||
if !rpc_peers.is_empty() {
|
|
||||||
let rpc_client = {
|
|
||||||
let node_index = thread_rng().gen_range(0, rpc_peers.len());
|
|
||||||
RpcClient::new_socket(rpc_peers[node_index].rpc)
|
|
||||||
};
|
|
||||||
Ok(rpc_client
|
|
||||||
.send(
|
|
||||||
&RpcRequest::GetSlotsPerSegment,
|
|
||||||
serde_json::json!([client_commitment]),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
.map_err(|err| {
|
|
||||||
warn!("Error while making rpc request {:?}", err);
|
|
||||||
ArchiverError::ClientError(err)
|
|
||||||
})?
|
|
||||||
.as_u64()
|
|
||||||
.unwrap())
|
|
||||||
} else {
|
|
||||||
Err(ArchiverError::NoRpcPeers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits until the first segment is ready, and returns the current segment
|
|
||||||
fn poll_for_segment(
|
|
||||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
|
||||||
slots_per_segment: u64,
|
|
||||||
previous_blockhash: &Hash,
|
|
||||||
exit: &Arc<AtomicBool>,
|
|
||||||
) -> Result<(Hash, u64)> {
|
|
||||||
loop {
|
|
||||||
let (blockhash, turn_slot) = Self::poll_for_blockhash_and_slot(
|
|
||||||
cluster_info,
|
|
||||||
slots_per_segment,
|
|
||||||
previous_blockhash,
|
|
||||||
exit,
|
|
||||||
)?;
|
|
||||||
if get_complete_segment_from_slot(turn_slot, slots_per_segment).is_some() {
|
|
||||||
return Ok((blockhash, turn_slot));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Poll for a different blockhash and associated max_slot than `previous_blockhash`
|
|
||||||
fn poll_for_blockhash_and_slot(
|
|
||||||
cluster_info: &Arc<RwLock<ClusterInfo>>,
|
|
||||||
slots_per_segment: u64,
|
|
||||||
previous_blockhash: &Hash,
|
|
||||||
exit: &Arc<AtomicBool>,
|
|
||||||
) -> Result<(Hash, u64)> {
|
|
||||||
info!("waiting for the next turn...");
|
|
||||||
loop {
|
|
||||||
let rpc_peers = {
|
|
||||||
let cluster_info = cluster_info.read().unwrap();
|
|
||||||
cluster_info.all_rpc_peers()
|
|
||||||
};
|
|
||||||
debug!("rpc peers: {:?}", rpc_peers);
|
|
||||||
if !rpc_peers.is_empty() {
|
|
||||||
let rpc_client = {
|
|
||||||
let node_index = thread_rng().gen_range(0, rpc_peers.len());
|
|
||||||
RpcClient::new_socket(rpc_peers[node_index].rpc)
|
|
||||||
};
|
|
||||||
let response = rpc_client
|
|
||||||
.send(
|
|
||||||
&RpcRequest::GetStorageTurn,
|
|
||||||
serde_json::value::Value::Null,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
.map_err(|err| {
|
|
||||||
warn!("Error while making rpc request {:?}", err);
|
|
||||||
ArchiverError::ClientError(err)
|
|
||||||
})?;
|
|
||||||
let RpcStorageTurn {
|
|
||||||
blockhash: storage_blockhash,
|
|
||||||
slot: turn_slot,
|
|
||||||
} = serde_json::from_value::<RpcStorageTurn>(response)
|
|
||||||
.map_err(ArchiverError::JsonError)?;
|
|
||||||
let turn_blockhash = storage_blockhash.parse().map_err(|err| {
|
|
||||||
io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
|
||||||
format!(
|
|
||||||
"Blockhash parse failure: {:?} on {:?}",
|
|
||||||
err, storage_blockhash
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
if turn_blockhash != *previous_blockhash {
|
|
||||||
info!("turn slot: {}", turn_slot);
|
|
||||||
if get_segment_from_slot(turn_slot, slots_per_segment) != 0 {
|
|
||||||
return Ok((turn_blockhash, turn_slot));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if exit.load(Ordering::Relaxed) {
|
|
||||||
return Err(ArchiverError::IO(io::Error::new(
|
|
||||||
ErrorKind::Other,
|
|
||||||
"exit signalled...",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
sleep(Duration::from_secs(5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ask an archiver to populate a given blockstore with its segment.
|
|
||||||
/// Return the slot at the start of the archiver's segment
|
|
||||||
///
|
|
||||||
/// It is recommended to use a temporary blockstore for this since the download will not verify
|
|
||||||
/// shreds received and might impact the chaining of shreds across slots
|
|
||||||
pub fn download_from_archiver(
|
|
||||||
serve_repair: &ServeRepair,
|
|
||||||
archiver_info: &ContactInfo,
|
|
||||||
blockstore: &Arc<Blockstore>,
|
|
||||||
slots_per_segment: u64,
|
|
||||||
) -> Result<u64> {
|
|
||||||
let ip_addr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
|
|
||||||
// Create a client which downloads from the archiver and see that it
|
|
||||||
// can respond with shreds.
|
|
||||||
let start_slot = Self::get_archiver_segment_slot(ip_addr, archiver_info.storage_addr);
|
|
||||||
info!("Archiver download: start at {}", start_slot);
|
|
||||||
|
|
||||||
let exit = Arc::new(AtomicBool::new(false));
|
|
||||||
let (s_reader, r_reader) = channel();
|
|
||||||
let repair_socket = Arc::new(bind_in_range(ip_addr, VALIDATOR_PORT_RANGE).unwrap().1);
|
|
||||||
let t_receiver = receiver(
|
|
||||||
repair_socket.clone(),
|
|
||||||
&exit,
|
|
||||||
s_reader,
|
|
||||||
Recycler::default(),
|
|
||||||
"archiver_reeciver",
|
|
||||||
);
|
|
||||||
let id = serve_repair.keypair().pubkey();
|
|
||||||
info!(
|
|
||||||
"Sending repair requests from: {} to: {}",
|
|
||||||
serve_repair.my_info().id,
|
|
||||||
archiver_info.gossip
|
|
||||||
);
|
|
||||||
let repair_slot_range = RepairSlotRange {
|
|
||||||
start: start_slot,
|
|
||||||
end: start_slot + slots_per_segment,
|
|
||||||
};
|
|
||||||
// try for upto 180 seconds //TODO needs tuning if segments are huge
|
|
||||||
for _ in 0..120 {
|
|
||||||
// Strategy used by archivers
|
|
||||||
let repairs = RepairService::generate_repairs_in_range(
|
|
||||||
blockstore,
|
|
||||||
repair_service::MAX_REPAIR_LENGTH,
|
|
||||||
&repair_slot_range,
|
|
||||||
);
|
|
||||||
//iter over the repairs and send them
|
|
||||||
if let Ok(repairs) = repairs {
|
|
||||||
let reqs: Vec<_> = repairs
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|repair_request| {
|
|
||||||
serve_repair
|
|
||||||
.map_repair_request(&repair_request)
|
|
||||||
.map(|result| ((archiver_info.gossip, result), repair_request))
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for ((to, req), repair_request) in reqs {
|
|
||||||
if let Ok(local_addr) = repair_socket.local_addr() {
|
|
||||||
datapoint_info!(
|
|
||||||
"archiver_download",
|
|
||||||
("repair_request", format!("{:?}", repair_request), String),
|
|
||||||
("to", to.to_string(), String),
|
|
||||||
("from", local_addr.to_string(), String),
|
|
||||||
("id", id.to_string(), String)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
repair_socket
|
|
||||||
.send_to(&req, archiver_info.gossip)
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
error!("{} repair req send_to({}) error {:?}", id, to, e);
|
|
||||||
0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let res = r_reader.recv_timeout(Duration::new(1, 0));
|
|
||||||
if let Ok(mut packets) = res {
|
|
||||||
while let Ok(mut more) = r_reader.try_recv() {
|
|
||||||
packets.packets.append_pinned(&mut more.packets);
|
|
||||||
}
|
|
||||||
let shreds: Vec<Shred> = packets
|
|
||||||
.packets
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|p| Shred::new_from_serialized_shred(p.data.to_vec()).ok())
|
|
||||||
.collect();
|
|
||||||
blockstore.insert_shreds(shreds, None, false)?;
|
|
||||||
}
|
|
||||||
// check if all the slots in the segment are complete
|
|
||||||
if Self::segment_complete(start_slot, slots_per_segment, blockstore) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sleep(Duration::from_millis(500));
|
|
||||||
}
|
|
||||||
exit.store(true, Ordering::Relaxed);
|
|
||||||
t_receiver.join().unwrap();
|
|
||||||
|
|
||||||
// check if all the slots in the segment are complete
|
|
||||||
if !Self::segment_complete(start_slot, slots_per_segment, blockstore) {
|
|
||||||
return Err(ArchiverError::SegmentDownloadError);
|
|
||||||
}
|
|
||||||
Ok(start_slot)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn segment_complete(
|
|
||||||
start_slot: Slot,
|
|
||||||
slots_per_segment: u64,
|
|
||||||
blockstore: &Arc<Blockstore>,
|
|
||||||
) -> bool {
|
|
||||||
for slot in start_slot..(start_slot + slots_per_segment) {
|
|
||||||
if !blockstore.is_full(slot) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_archiver_segment_slot(bind_ip_addr: IpAddr, to: SocketAddr) -> u64 {
|
|
||||||
let (_port, socket) = bind_in_range(bind_ip_addr, VALIDATOR_PORT_RANGE).unwrap();
|
|
||||||
socket
|
|
||||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let req = ArchiverRequest::GetSlotHeight(socket.local_addr().unwrap());
|
|
||||||
let serialized_req = bincode::serialize(&req).unwrap();
|
|
||||||
for _ in 0..10 {
|
|
||||||
socket.send_to(&serialized_req, to).unwrap();
|
|
||||||
let mut buf = [0; 1024];
|
|
||||||
if let Ok((size, _addr)) = socket.recv_from(&mut buf) {
|
|
||||||
// Ignore bad packet and try again
|
|
||||||
if let Ok(slot) = bincode::config()
|
|
||||||
.limit(PACKET_DATA_SIZE as u64)
|
|
||||||
.deserialize(&buf[..size])
|
|
||||||
{
|
|
||||||
return slot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sleep(Duration::from_millis(500));
|
|
||||||
}
|
|
||||||
panic!("Couldn't get segment slot from archiver!");
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
#[macro_use]
|
|
||||||
extern crate log;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_derive;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate solana_metrics;
|
|
||||||
|
|
||||||
pub mod archiver;
|
|
||||||
mod result;
|
|
@@ -1,48 +0,0 @@
|
|||||||
use serde_json;
|
|
||||||
use solana_client::client_error;
|
|
||||||
use solana_ledger::blockstore;
|
|
||||||
use solana_sdk::transport;
|
|
||||||
use std::any::Any;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum ArchiverError {
|
|
||||||
#[error("IO error")]
|
|
||||||
IO(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("blockstore error")]
|
|
||||||
BlockstoreError(#[from] blockstore::BlockstoreError),
|
|
||||||
|
|
||||||
#[error("crossbeam error")]
|
|
||||||
CrossbeamSendError(#[from] crossbeam_channel::SendError<u64>),
|
|
||||||
|
|
||||||
#[error("send error")]
|
|
||||||
SendError(#[from] std::sync::mpsc::SendError<u64>),
|
|
||||||
|
|
||||||
#[error("join error")]
|
|
||||||
JoinError(Box<dyn Any + Send + 'static>),
|
|
||||||
|
|
||||||
#[error("transport error")]
|
|
||||||
TransportError(#[from] transport::TransportError),
|
|
||||||
|
|
||||||
#[error("client error")]
|
|
||||||
ClientError(#[from] client_error::ClientError),
|
|
||||||
|
|
||||||
#[error("Json parsing error")]
|
|
||||||
JsonError(#[from] serde_json::error::Error),
|
|
||||||
|
|
||||||
#[error("Storage account has no balance")]
|
|
||||||
EmptyStorageAccountBalance,
|
|
||||||
|
|
||||||
#[error("No RPC peers..")]
|
|
||||||
NoRpcPeers,
|
|
||||||
|
|
||||||
#[error("Couldn't download full segment")]
|
|
||||||
SegmentDownloadError,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::convert::From<Box<dyn Any + Send + 'static>> for ArchiverError {
|
|
||||||
fn from(e: Box<dyn Any + Send + 'static>) -> ArchiverError {
|
|
||||||
ArchiverError::JoinError(e)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "solana-archiver-utils"
|
|
||||||
version = "1.0.8"
|
|
||||||
description = "Solana Archiver Utils"
|
|
||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
|
||||||
repository = "https://github.com/solana-labs/solana"
|
|
||||||
license = "Apache-2.0"
|
|
||||||
homepage = "https://solana.com/"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
log = "0.4.8"
|
|
||||||
rand = "0.6.5"
|
|
||||||
solana-chacha = { path = "../chacha", version = "1.0.8" }
|
|
||||||
solana-chacha-sys = { path = "../chacha-sys", version = "1.0.8" }
|
|
||||||
solana-ledger = { path = "../ledger", version = "1.0.8" }
|
|
||||||
solana-logger = { path = "../logger", version = "1.0.8" }
|
|
||||||
solana-perf = { path = "../perf", version = "1.0.8" }
|
|
||||||
solana-sdk = { path = "../sdk", version = "1.0.8" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex = "0.4.0"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "solana_archiver_utils"
|
|
@@ -1,120 +0,0 @@
|
|||||||
#[macro_use]
|
|
||||||
extern crate log;
|
|
||||||
|
|
||||||
use solana_sdk::hash::{Hash, Hasher};
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{self, BufReader, ErrorKind, Read, Seek, SeekFrom};
|
|
||||||
use std::mem::size_of;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub fn sample_file(in_path: &Path, sample_offsets: &[u64]) -> io::Result<Hash> {
|
|
||||||
let in_file = File::open(in_path)?;
|
|
||||||
let metadata = in_file.metadata()?;
|
|
||||||
let mut buffer_file = BufReader::new(in_file);
|
|
||||||
|
|
||||||
let mut hasher = Hasher::default();
|
|
||||||
let sample_size = size_of::<Hash>();
|
|
||||||
let sample_size64 = sample_size as u64;
|
|
||||||
let mut buf = vec![0; sample_size];
|
|
||||||
|
|
||||||
let file_len = metadata.len();
|
|
||||||
if file_len < sample_size64 {
|
|
||||||
return Err(io::Error::new(ErrorKind::Other, "file too short!"));
|
|
||||||
}
|
|
||||||
for offset in sample_offsets {
|
|
||||||
if *offset > (file_len - sample_size64) / sample_size64 {
|
|
||||||
return Err(io::Error::new(ErrorKind::Other, "offset too large"));
|
|
||||||
}
|
|
||||||
buffer_file.seek(SeekFrom::Start(*offset * sample_size64))?;
|
|
||||||
trace!("sampling @ {} ", *offset);
|
|
||||||
match buffer_file.read(&mut buf) {
|
|
||||||
Ok(size) => {
|
|
||||||
assert_eq!(size, buf.len());
|
|
||||||
hasher.hash(&buf);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error sampling file");
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(hasher.result())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rand::{thread_rng, Rng};
|
|
||||||
use std::fs::{create_dir_all, remove_file};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
extern crate hex;
|
|
||||||
|
|
||||||
fn tmp_file_path(name: &str) -> PathBuf {
|
|
||||||
use std::env;
|
|
||||||
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
|
|
||||||
let mut rand_bits = [0u8; 32];
|
|
||||||
thread_rng().fill(&mut rand_bits[..]);
|
|
||||||
|
|
||||||
let mut path = PathBuf::new();
|
|
||||||
path.push(out_dir);
|
|
||||||
path.push("tmp");
|
|
||||||
create_dir_all(&path).unwrap();
|
|
||||||
|
|
||||||
path.push(format!("{}-{:?}", name, hex::encode(rand_bits)));
|
|
||||||
println!("path: {:?}", path);
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sample_file() {
|
|
||||||
solana_logger::setup();
|
|
||||||
let in_path = tmp_file_path("test_sample_file_input.txt");
|
|
||||||
let num_strings = 4096;
|
|
||||||
let string = "12foobar";
|
|
||||||
{
|
|
||||||
let mut in_file = File::create(&in_path).unwrap();
|
|
||||||
for _ in 0..num_strings {
|
|
||||||
in_file.write(string.as_bytes()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let num_samples = (string.len() * num_strings / size_of::<Hash>()) as u64;
|
|
||||||
let samples: Vec<_> = (0..num_samples).collect();
|
|
||||||
let res = sample_file(&in_path, samples.as_slice());
|
|
||||||
let ref_hash: Hash = Hash::new(&[
|
|
||||||
173, 251, 182, 165, 10, 54, 33, 150, 133, 226, 106, 150, 99, 192, 179, 1, 230, 144,
|
|
||||||
151, 126, 18, 191, 54, 67, 249, 140, 230, 160, 56, 30, 170, 52,
|
|
||||||
]);
|
|
||||||
let res = res.unwrap();
|
|
||||||
assert_eq!(res, ref_hash);
|
|
||||||
|
|
||||||
// Sample just past the end
|
|
||||||
assert!(sample_file(&in_path, &[num_samples]).is_err());
|
|
||||||
remove_file(&in_path).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sample_file_invalid_offset() {
|
|
||||||
let in_path = tmp_file_path("test_sample_file_invalid_offset_input.txt");
|
|
||||||
{
|
|
||||||
let mut in_file = File::create(&in_path).unwrap();
|
|
||||||
for _ in 0..4096 {
|
|
||||||
in_file.write("123456foobar".as_bytes()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let samples = [0, 200000];
|
|
||||||
let res = sample_file(&in_path, &samples);
|
|
||||||
assert!(res.is_err());
|
|
||||||
remove_file(in_path).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sample_file_missing_file() {
|
|
||||||
let in_path = tmp_file_path("test_sample_file_that_doesnt_exist.txt");
|
|
||||||
let samples = [0, 5];
|
|
||||||
let res = sample_file(&in_path, &samples);
|
|
||||||
assert!(res.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
2
archiver/.gitignore
vendored
2
archiver/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
/target/
|
|
||||||
/farf/
|
|
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
name = "solana-archiver"
|
|
||||||
version = "1.0.8"
|
|
||||||
repository = "https://github.com/solana-labs/solana"
|
|
||||||
license = "Apache-2.0"
|
|
||||||
homepage = "https://solana.com/"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = "2.33.0"
|
|
||||||
console = "0.9.2"
|
|
||||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.8" }
|
|
||||||
solana-core = { path = "../core", version = "1.0.8" }
|
|
||||||
solana-logger = { path = "../logger", version = "1.0.8" }
|
|
||||||
solana-metrics = { path = "../metrics", version = "1.0.8" }
|
|
||||||
solana-archiver-lib = { path = "../archiver-lib", version = "1.0.8" }
|
|
||||||
solana-net-utils = { path = "../net-utils", version = "1.0.8" }
|
|
||||||
solana-sdk = { path = "../sdk", version = "1.0.8" }
|
|
||||||
|
|
@@ -1,131 +0,0 @@
|
|||||||
use clap::{crate_description, crate_name, App, Arg};
|
|
||||||
use console::style;
|
|
||||||
use solana_archiver_lib::archiver::Archiver;
|
|
||||||
use solana_clap_utils::{
|
|
||||||
input_parsers::keypair_of, input_validators::is_keypair_or_ask_keyword,
|
|
||||||
keypair::SKIP_SEED_PHRASE_VALIDATION_ARG,
|
|
||||||
};
|
|
||||||
use solana_core::{
|
|
||||||
cluster_info::{Node, VALIDATOR_PORT_RANGE},
|
|
||||||
contact_info::ContactInfo,
|
|
||||||
};
|
|
||||||
use solana_sdk::{
|
|
||||||
commitment_config::CommitmentConfig,
|
|
||||||
signature::{Keypair, Signer},
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
||||||
path::PathBuf,
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
solana_logger::setup();
|
|
||||||
|
|
||||||
let matches = App::new(crate_name!())
|
|
||||||
.about(crate_description!())
|
|
||||||
.version(solana_clap_utils::version!())
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("identity_keypair")
|
|
||||||
.short("i")
|
|
||||||
.long("identity")
|
|
||||||
.value_name("PATH")
|
|
||||||
.takes_value(true)
|
|
||||||
.validator(is_keypair_or_ask_keyword)
|
|
||||||
.help("File containing an identity (keypair)"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("entrypoint")
|
|
||||||
.short("n")
|
|
||||||
.long("entrypoint")
|
|
||||||
.value_name("HOST:PORT")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(true)
|
|
||||||
.validator(solana_net_utils::is_host_port)
|
|
||||||
.help("Rendezvous with the cluster at this entry point"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("ledger")
|
|
||||||
.short("l")
|
|
||||||
.long("ledger")
|
|
||||||
.value_name("DIR")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(true)
|
|
||||||
.help("use DIR as persistent ledger location"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("storage_keypair")
|
|
||||||
.short("s")
|
|
||||||
.long("storage-keypair")
|
|
||||||
.value_name("PATH")
|
|
||||||
.takes_value(true)
|
|
||||||
.validator(is_keypair_or_ask_keyword)
|
|
||||||
.help("File containing the storage account keypair"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
|
|
||||||
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
|
|
||||||
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
let ledger_path = PathBuf::from(matches.value_of("ledger").unwrap());
|
|
||||||
|
|
||||||
let identity_keypair = keypair_of(&matches, "identity_keypair").unwrap_or_else(Keypair::new);
|
|
||||||
|
|
||||||
let storage_keypair = keypair_of(&matches, "storage_keypair").unwrap_or_else(|| {
|
|
||||||
clap::Error::with_description(
|
|
||||||
"The `storage-keypair` argument was not found",
|
|
||||||
clap::ErrorKind::ArgumentNotFound,
|
|
||||||
)
|
|
||||||
.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
let entrypoint_addr = matches
|
|
||||||
.value_of("entrypoint")
|
|
||||||
.map(|entrypoint| {
|
|
||||||
solana_net_utils::parse_host_port(entrypoint)
|
|
||||||
.expect("failed to parse entrypoint address")
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let gossip_addr = {
|
|
||||||
let ip = solana_net_utils::get_public_ip_addr(&entrypoint_addr).unwrap();
|
|
||||||
let mut addr = SocketAddr::new(ip, 0);
|
|
||||||
addr.set_ip(solana_net_utils::get_public_ip_addr(&entrypoint_addr).unwrap());
|
|
||||||
addr
|
|
||||||
};
|
|
||||||
let node = Node::new_archiver_with_external_ip(
|
|
||||||
&identity_keypair.pubkey(),
|
|
||||||
&gossip_addr,
|
|
||||||
VALIDATOR_PORT_RANGE,
|
|
||||||
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
|
||||||
);
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"{} version {} (branch={}, commit={})",
|
|
||||||
style(crate_name!()).bold(),
|
|
||||||
solana_clap_utils::version!(),
|
|
||||||
option_env!("CI_BRANCH").unwrap_or("unknown"),
|
|
||||||
option_env!("CI_COMMIT").unwrap_or("unknown")
|
|
||||||
);
|
|
||||||
solana_metrics::set_host_id(identity_keypair.pubkey().to_string());
|
|
||||||
println!(
|
|
||||||
"replicating the data with identity_keypair={:?} gossip_addr={:?}",
|
|
||||||
identity_keypair.pubkey(),
|
|
||||||
gossip_addr
|
|
||||||
);
|
|
||||||
|
|
||||||
let entrypoint_info = ContactInfo::new_gossip_entry_point(&entrypoint_addr);
|
|
||||||
let archiver = Archiver::new(
|
|
||||||
&ledger_path,
|
|
||||||
node,
|
|
||||||
entrypoint_info,
|
|
||||||
Arc::new(identity_keypair),
|
|
||||||
Arc::new(storage_keypair),
|
|
||||||
CommitmentConfig::recent(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
archiver.join();
|
|
||||||
}
|
|
2
banking-bench/.gitignore
vendored
2
banking-bench/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
/target/
|
|
||||||
/farf/
|
|
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
name = "solana-banking-bench"
|
|
||||||
version = "1.0.8"
|
|
||||||
repository = "https://github.com/solana-labs/solana"
|
|
||||||
license = "Apache-2.0"
|
|
||||||
homepage = "https://solana.com/"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
log = "0.4.6"
|
|
||||||
rayon = "1.2.0"
|
|
||||||
solana-core = { path = "../core", version = "1.0.8" }
|
|
||||||
solana-ledger = { path = "../ledger", version = "1.0.8" }
|
|
||||||
solana-logger = { path = "../logger", version = "1.0.8" }
|
|
||||||
solana-runtime = { path = "../runtime", version = "1.0.8" }
|
|
||||||
solana-measure = { path = "../measure", version = "1.0.8" }
|
|
||||||
solana-sdk = { path = "../sdk", version = "1.0.8" }
|
|
||||||
rand = "0.6.5"
|
|
||||||
crossbeam-channel = "0.3"
|
|
@@ -1,306 +0,0 @@
|
|||||||
use crossbeam_channel::unbounded;
|
|
||||||
use log::*;
|
|
||||||
use rand::{thread_rng, Rng};
|
|
||||||
use rayon::prelude::*;
|
|
||||||
use solana_core::banking_stage::{create_test_recorder, BankingStage};
|
|
||||||
use solana_core::cluster_info::ClusterInfo;
|
|
||||||
use solana_core::cluster_info::Node;
|
|
||||||
use solana_core::genesis_utils::{create_genesis_config, GenesisConfigInfo};
|
|
||||||
use solana_core::packet::to_packets_chunked;
|
|
||||||
use solana_core::poh_recorder::PohRecorder;
|
|
||||||
use solana_core::poh_recorder::WorkingBankEntry;
|
|
||||||
use solana_ledger::bank_forks::BankForks;
|
|
||||||
use solana_ledger::{blockstore::Blockstore, get_tmp_ledger_path};
|
|
||||||
use solana_measure::measure::Measure;
|
|
||||||
use solana_runtime::bank::Bank;
|
|
||||||
use solana_sdk::hash::Hash;
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
use solana_sdk::signature::Keypair;
|
|
||||||
use solana_sdk::signature::Signature;
|
|
||||||
use solana_sdk::system_transaction;
|
|
||||||
use solana_sdk::timing::{duration_as_us, timestamp};
|
|
||||||
use solana_sdk::transaction::Transaction;
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::mpsc::Receiver;
|
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
|
||||||
use std::thread::sleep;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
fn check_txs(
|
|
||||||
receiver: &Arc<Receiver<WorkingBankEntry>>,
|
|
||||||
ref_tx_count: usize,
|
|
||||||
poh_recorder: &Arc<Mutex<PohRecorder>>,
|
|
||||||
) -> bool {
|
|
||||||
let mut total = 0;
|
|
||||||
let now = Instant::now();
|
|
||||||
let mut no_bank = false;
|
|
||||||
loop {
|
|
||||||
if let Ok((_bank, (entry, _tick_height))) = receiver.recv_timeout(Duration::from_millis(10))
|
|
||||||
{
|
|
||||||
total += entry.transactions.len();
|
|
||||||
}
|
|
||||||
if total >= ref_tx_count {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if now.elapsed().as_secs() > 60 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if poh_recorder.lock().unwrap().bank().is_none() {
|
|
||||||
trace!("no bank");
|
|
||||||
no_bank = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !no_bank {
|
|
||||||
assert!(total >= ref_tx_count);
|
|
||||||
}
|
|
||||||
no_bank
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_accounts_txs(txes: usize, mint_keypair: &Keypair, hash: Hash) -> Vec<Transaction> {
|
|
||||||
let to_pubkey = Pubkey::new_rand();
|
|
||||||
let dummy = system_transaction::transfer(mint_keypair, &to_pubkey, 1, hash);
|
|
||||||
(0..txes)
|
|
||||||
.into_par_iter()
|
|
||||||
.map(|_| {
|
|
||||||
let mut new = dummy.clone();
|
|
||||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
|
||||||
new.message.account_keys[0] = Pubkey::new_rand();
|
|
||||||
new.message.account_keys[1] = Pubkey::new_rand();
|
|
||||||
new.signatures = vec![Signature::new(&sig[0..64])];
|
|
||||||
new
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Config {
|
|
||||||
packets_per_batch: usize,
|
|
||||||
chunk_len: usize,
|
|
||||||
num_threads: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
fn get_transactions_index(&self, chunk_index: usize) -> usize {
|
|
||||||
chunk_index * (self.chunk_len / self.num_threads) * self.packets_per_batch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bytes_as_usize(bytes: &[u8]) -> usize {
|
|
||||||
bytes[0] as usize | (bytes[1] as usize) << 8
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
solana_logger::setup();
|
|
||||||
let num_threads = BankingStage::num_threads() as usize;
|
|
||||||
// a multiple of packet chunk duplicates to avoid races
|
|
||||||
const CHUNKS: usize = 8 * 2;
|
|
||||||
const PACKETS_PER_BATCH: usize = 192;
|
|
||||||
let txes = PACKETS_PER_BATCH * num_threads * CHUNKS;
|
|
||||||
let mint_total = 1_000_000_000_000;
|
|
||||||
let GenesisConfigInfo {
|
|
||||||
genesis_config,
|
|
||||||
mint_keypair,
|
|
||||||
..
|
|
||||||
} = create_genesis_config(mint_total);
|
|
||||||
|
|
||||||
let (verified_sender, verified_receiver) = unbounded();
|
|
||||||
let (vote_sender, vote_receiver) = unbounded();
|
|
||||||
let bank0 = Bank::new(&genesis_config);
|
|
||||||
let mut bank_forks = BankForks::new(0, bank0);
|
|
||||||
let mut bank = bank_forks.working_bank();
|
|
||||||
|
|
||||||
info!("threads: {} txs: {}", num_threads, txes);
|
|
||||||
|
|
||||||
let mut transactions = make_accounts_txs(txes, &mint_keypair, genesis_config.hash());
|
|
||||||
|
|
||||||
// fund all the accounts
|
|
||||||
transactions.iter().for_each(|tx| {
|
|
||||||
let fund = system_transaction::transfer(
|
|
||||||
&mint_keypair,
|
|
||||||
&tx.message.account_keys[0],
|
|
||||||
mint_total / txes as u64,
|
|
||||||
genesis_config.hash(),
|
|
||||||
);
|
|
||||||
let x = bank.process_transaction(&fund);
|
|
||||||
x.unwrap();
|
|
||||||
});
|
|
||||||
//sanity check, make sure all the transactions can execute sequentially
|
|
||||||
transactions.iter().for_each(|tx| {
|
|
||||||
let res = bank.process_transaction(&tx);
|
|
||||||
assert!(res.is_ok(), "sanity test transactions");
|
|
||||||
});
|
|
||||||
bank.clear_signatures();
|
|
||||||
//sanity check, make sure all the transactions can execute in parallel
|
|
||||||
let res = bank.process_transactions(&transactions);
|
|
||||||
for r in res {
|
|
||||||
assert!(r.is_ok(), "sanity parallel execution");
|
|
||||||
}
|
|
||||||
bank.clear_signatures();
|
|
||||||
let mut verified: Vec<_> = to_packets_chunked(&transactions.clone(), PACKETS_PER_BATCH);
|
|
||||||
let ledger_path = get_tmp_ledger_path!();
|
|
||||||
{
|
|
||||||
let blockstore = Arc::new(
|
|
||||||
Blockstore::open(&ledger_path).expect("Expected to be able to open database ledger"),
|
|
||||||
);
|
|
||||||
let (exit, poh_recorder, poh_service, signal_receiver) =
|
|
||||||
create_test_recorder(&bank, &blockstore, None);
|
|
||||||
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
|
||||||
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
|
||||||
let banking_stage = BankingStage::new(
|
|
||||||
&cluster_info,
|
|
||||||
&poh_recorder,
|
|
||||||
verified_receiver,
|
|
||||||
vote_receiver,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
|
||||||
|
|
||||||
let chunk_len = verified.len() / CHUNKS;
|
|
||||||
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 mut total_us = 0;
|
|
||||||
let mut tx_total_us = 0;
|
|
||||||
let mut txs_processed = 0;
|
|
||||||
let mut root = 1;
|
|
||||||
let collector = Pubkey::new_rand();
|
|
||||||
const ITERS: usize = 1_000;
|
|
||||||
let config = Config {
|
|
||||||
packets_per_batch: PACKETS_PER_BATCH,
|
|
||||||
chunk_len,
|
|
||||||
num_threads,
|
|
||||||
};
|
|
||||||
let mut total_sent = 0;
|
|
||||||
for _ in 0..ITERS {
|
|
||||||
let now = Instant::now();
|
|
||||||
let mut sent = 0;
|
|
||||||
|
|
||||||
for (i, v) in verified[start..start + chunk_len]
|
|
||||||
.chunks(chunk_len / num_threads)
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
let mut byte = 0;
|
|
||||||
let index = config.get_transactions_index(start + i);
|
|
||||||
if index < transactions.len() {
|
|
||||||
byte = bytes_as_usize(transactions[index].signatures[0].as_ref());
|
|
||||||
}
|
|
||||||
trace!(
|
|
||||||
"sending... {}..{} {} v.len: {} sig: {} transactions.len: {} index: {}",
|
|
||||||
start + i,
|
|
||||||
start + chunk_len,
|
|
||||||
timestamp(),
|
|
||||||
v.len(),
|
|
||||||
byte,
|
|
||||||
transactions.len(),
|
|
||||||
index,
|
|
||||||
);
|
|
||||||
for xv in v {
|
|
||||||
sent += xv.packets.len();
|
|
||||||
}
|
|
||||||
verified_sender.send(v.to_vec()).unwrap();
|
|
||||||
}
|
|
||||||
let start_tx_index = config.get_transactions_index(start);
|
|
||||||
let end_tx_index = config.get_transactions_index(start + chunk_len);
|
|
||||||
for tx in &transactions[start_tx_index..end_tx_index] {
|
|
||||||
loop {
|
|
||||||
if bank.get_signature_status(&tx.signatures[0]).is_some() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if poh_recorder.lock().unwrap().bank().is_none() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sleep(Duration::from_millis(5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if check_txs(&signal_receiver, txes / CHUNKS, &poh_recorder) {
|
|
||||||
debug!(
|
|
||||||
"resetting bank {} tx count: {} txs_proc: {}",
|
|
||||||
bank.slot(),
|
|
||||||
bank.transaction_count(),
|
|
||||||
txs_processed
|
|
||||||
);
|
|
||||||
assert!(txs_processed < bank.transaction_count());
|
|
||||||
txs_processed = bank.transaction_count();
|
|
||||||
tx_total_us += duration_as_us(&now.elapsed());
|
|
||||||
|
|
||||||
let mut poh_time = Measure::start("poh_time");
|
|
||||||
poh_recorder.lock().unwrap().reset(
|
|
||||||
bank.last_blockhash(),
|
|
||||||
bank.slot(),
|
|
||||||
Some((bank.slot(), bank.slot() + 1)),
|
|
||||||
);
|
|
||||||
poh_time.stop();
|
|
||||||
|
|
||||||
let mut new_bank_time = Measure::start("new_bank");
|
|
||||||
let new_bank = Bank::new_from_parent(&bank, &collector, bank.slot() + 1);
|
|
||||||
new_bank_time.stop();
|
|
||||||
|
|
||||||
let mut insert_time = Measure::start("insert_time");
|
|
||||||
bank_forks.insert(new_bank);
|
|
||||||
bank = bank_forks.working_bank();
|
|
||||||
insert_time.stop();
|
|
||||||
|
|
||||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
|
||||||
assert!(poh_recorder.lock().unwrap().bank().is_some());
|
|
||||||
if bank.slot() > 32 {
|
|
||||||
bank_forks.set_root(root, &None);
|
|
||||||
root += 1;
|
|
||||||
}
|
|
||||||
debug!(
|
|
||||||
"new_bank_time: {}us insert_time: {}us poh_time: {}us",
|
|
||||||
new_bank_time.as_us(),
|
|
||||||
insert_time.as_us(),
|
|
||||||
poh_time.as_us(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
tx_total_us += duration_as_us(&now.elapsed());
|
|
||||||
}
|
|
||||||
|
|
||||||
// This signature clear may not actually clear the signatures
|
|
||||||
// in this chunk, but since we rotate between CHUNKS then
|
|
||||||
// we should clear them by the time we come around again to re-use that chunk.
|
|
||||||
bank.clear_signatures();
|
|
||||||
total_us += duration_as_us(&now.elapsed());
|
|
||||||
debug!(
|
|
||||||
"time: {} us checked: {} sent: {}",
|
|
||||||
duration_as_us(&now.elapsed()),
|
|
||||||
txes / CHUNKS,
|
|
||||||
sent,
|
|
||||||
);
|
|
||||||
total_sent += sent;
|
|
||||||
|
|
||||||
if bank.slot() > 0 && bank.slot() % 16 == 0 {
|
|
||||||
for tx in transactions.iter_mut() {
|
|
||||||
tx.message.recent_blockhash = bank.last_blockhash();
|
|
||||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
|
||||||
tx.signatures[0] = Signature::new(&sig[0..64]);
|
|
||||||
}
|
|
||||||
verified = to_packets_chunked(&transactions.clone(), PACKETS_PER_BATCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
start += chunk_len;
|
|
||||||
start %= verified.len();
|
|
||||||
}
|
|
||||||
eprintln!(
|
|
||||||
"{{'name': 'banking_bench_total', 'median': '{}'}}",
|
|
||||||
(1000.0 * 1000.0 * total_sent as f64) / (total_us as f64),
|
|
||||||
);
|
|
||||||
eprintln!(
|
|
||||||
"{{'name': 'banking_bench_tx_total', 'median': '{}'}}",
|
|
||||||
(1000.0 * 1000.0 * total_sent as f64) / (tx_total_us as f64),
|
|
||||||
);
|
|
||||||
|
|
||||||
drop(verified_sender);
|
|
||||||
drop(vote_sender);
|
|
||||||
exit.store(true, Ordering::Relaxed);
|
|
||||||
banking_stage.join().unwrap();
|
|
||||||
debug!("waited for banking_stage");
|
|
||||||
poh_service.join().unwrap();
|
|
||||||
sleep(Duration::from_secs(1));
|
|
||||||
debug!("waited for poh_service");
|
|
||||||
}
|
|
||||||
let _unused = Blockstore::destroy(&ledger_path);
|
|
||||||
}
|
|
4
bench-exchange/.gitignore
vendored
4
bench-exchange/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
/target/
|
|
||||||
/config/
|
|
||||||
/config-local/
|
|
||||||
/farf/
|
|
@@ -1,34 +0,0 @@
|
|||||||
[package]
|
|
||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
name = "solana-bench-exchange"
|
|
||||||
version = "1.0.8"
|
|
||||||
repository = "https://github.com/solana-labs/solana"
|
|
||||||
license = "Apache-2.0"
|
|
||||||
homepage = "https://solana.com/"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = "2.32.0"
|
|
||||||
itertools = "0.8.2"
|
|
||||||
log = "0.4.8"
|
|
||||||
num-derive = "0.3"
|
|
||||||
num-traits = "0.2"
|
|
||||||
rand = "0.6.5"
|
|
||||||
rayon = "1.2.0"
|
|
||||||
serde_json = "1.0.46"
|
|
||||||
serde_yaml = "0.8.11"
|
|
||||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.8" }
|
|
||||||
solana-core = { path = "../core", version = "1.0.8" }
|
|
||||||
solana-genesis = { path = "../genesis", version = "1.0.8" }
|
|
||||||
solana-client = { path = "../client", version = "1.0.8" }
|
|
||||||
solana-faucet = { path = "../faucet", version = "1.0.8" }
|
|
||||||
solana-exchange-program = { path = "../programs/exchange", version = "1.0.8" }
|
|
||||||
solana-logger = { path = "../logger", version = "1.0.8" }
|
|
||||||
solana-metrics = { path = "../metrics", version = "1.0.8" }
|
|
||||||
solana-net-utils = { path = "../net-utils", version = "1.0.8" }
|
|
||||||
solana-runtime = { path = "../runtime", version = "1.0.8" }
|
|
||||||
solana-sdk = { path = "../sdk", version = "1.0.8" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
solana-local-cluster = { path = "../local-cluster", version = "1.0.8" }
|
|
@@ -1,479 +0,0 @@
|
|||||||
# token-exchange
|
|
||||||
Solana Token Exchange Bench
|
|
||||||
|
|
||||||
If you can't wait; jump to [Running the exchange](#Running-the-exchange) to
|
|
||||||
learn how to start and interact with the exchange.
|
|
||||||
|
|
||||||
### Table of Contents
|
|
||||||
[Overview](#Overview)<br>
|
|
||||||
[Premise](#Premise)<br>
|
|
||||||
[Exchange startup](#Exchange-startup)<br>
|
|
||||||
[Order Requests](#Trade-requests)<br>
|
|
||||||
[Order Cancellations](#Trade-cancellations)<br>
|
|
||||||
[Trade swap](#Trade-swap)<br>
|
|
||||||
[Exchange program operations](#Exchange-program-operations)<br>
|
|
||||||
[Quotes and OHLCV](#Quotes-and-OHLCV)<br>
|
|
||||||
[Investor strategies](#Investor-strategies)<br>
|
|
||||||
[Running the exchange](#Running-the-exchange)<br>
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
An exchange is a marketplace where one asset can be traded for another. This
|
|
||||||
demo demonstrates one way to host an exchange on the Solana blockchain by
|
|
||||||
emulating a currency exchange.
|
|
||||||
|
|
||||||
The assets are virtual tokens held by investors who may post order requests to
|
|
||||||
the exchange. A Matcher monitors the exchange and posts swap requests for
|
|
||||||
matching orders. All the transactions can execute concurrently.
|
|
||||||
|
|
||||||
## Premise
|
|
||||||
|
|
||||||
- Exchange
|
|
||||||
- An exchange is a marketplace where one asset can be traded for another.
|
|
||||||
The exchange in this demo is the on-chain program that implements the
|
|
||||||
tokens and the policies for trading those tokens.
|
|
||||||
- Token
|
|
||||||
- A virtual asset that can be owned, traded, and holds virtual intrinsic value
|
|
||||||
compared to other assets. There are four types of tokens in this demo, A,
|
|
||||||
B, C, D. Each one may be traded for another.
|
|
||||||
- Token account
|
|
||||||
- An account owned by the exchange that holds a quantity of one type of token.
|
|
||||||
- Account request
|
|
||||||
- A request to create a token account
|
|
||||||
- Token request
|
|
||||||
- A request to deposit tokens of a particular type into a token account.
|
|
||||||
- Asset pair
|
|
||||||
- A struct with fields Base and Quote, representing the two assets which make up a
|
|
||||||
trading pair, which themselves are Tokens. The Base or 'primary' asset is the
|
|
||||||
numerator and the Quote is the denominator for pricing purposes.
|
|
||||||
- Order side
|
|
||||||
- Describes which side of the market an investor wants to place a trade on. Options
|
|
||||||
are "Bid" or "Ask", where a bid represents an offer to purchase the Base asset of
|
|
||||||
the AssetPair for a sum of the Quote Asset and an Ask is an offer to sell Base asset
|
|
||||||
for the Quote asset.
|
|
||||||
- Price ratio
|
|
||||||
- An expression of the relative prices of two tokens. Calculated with the Base
|
|
||||||
Asset as the numerator and the Quote Asset as the denominator. Ratios are
|
|
||||||
represented as fixed point numbers. The fixed point scaler is defined in
|
|
||||||
[exchange_state.rs](https://github.com/solana-labs/solana/blob/c2fdd1362a029dcf89c8907c562d2079d977df11/programs/exchange_api/src/exchange_state.rs#L7)
|
|
||||||
- Order request
|
|
||||||
- A Solana transaction sent by a trader to the exchange to submit an order.
|
|
||||||
Order requests are made up of the token pair, the order side (bid or ask),
|
|
||||||
quantity of the primary token, the price ratio, and the two token accounts
|
|
||||||
to be credited/deducted. An example trade request looks like "T AB 5 2"
|
|
||||||
which reads "Exchange 5 A tokens to B tokens at a price ratio of 1:2" A fulfilled trade would result in 5 A tokens
|
|
||||||
deducted and 10 B tokens credited to the trade initiator's token accounts.
|
|
||||||
Successful order requests result in an order.
|
|
||||||
- Order
|
|
||||||
- The result of a successful order request. orders are stored in
|
|
||||||
accounts owned by the submitter of the order request. They can only be
|
|
||||||
canceled by their owner but can be used by anyone in a trade swap. They
|
|
||||||
contain the same information as the order request.
|
|
||||||
- Price spread
|
|
||||||
- The difference between the two matching orders. The spread is the
|
|
||||||
profit of the Matcher initiating the swap request.
|
|
||||||
- Match requirements
|
|
||||||
- Policies that result in a successful trade swap.
|
|
||||||
- Match request
|
|
||||||
- A request to fill two complementary orders (bid/ask), resulting if successful,
|
|
||||||
in a trade being created.
|
|
||||||
- Trade
|
|
||||||
- A successful trade is created from two matching orders that meet
|
|
||||||
swap requirements which are submitted in a Match Request by a Matcher and
|
|
||||||
executed by the exchange. A trade may not wholly satisfy one or both of the
|
|
||||||
orders in which case the orders are adjusted appropriately. Upon execution,
|
|
||||||
tokens are distributed to the traders' accounts and any overlap or
|
|
||||||
"negative spread" between orders is deposited into the Matcher's profit
|
|
||||||
account. All successful trades are recorded in the data of a new solana
|
|
||||||
account for posterity.
|
|
||||||
- Investor
|
|
||||||
- Individual investors who hold a number of tokens and wish to trade them on
|
|
||||||
the exchange. Investors operate as Solana thin clients who own a set of
|
|
||||||
accounts containing tokens and/or order requests. Investors post
|
|
||||||
transactions to the exchange in order to request tokens and post or cancel
|
|
||||||
order requests.
|
|
||||||
- Matcher
|
|
||||||
- An agent who facilitates trading between investors. Matchers operate as
|
|
||||||
Solana thin clients who monitor all the orders looking for a trade
|
|
||||||
match. Once found, the Matcher issues a swap request to the exchange.
|
|
||||||
Matchers are the engine of the exchange and are rewarded for their efforts by
|
|
||||||
accumulating the price spreads of the swaps they initiate. Matchers 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 Matchers.
|
|
||||||
|
|
||||||
## Exchange startup
|
|
||||||
|
|
||||||
The exchange is up and running when it reaches a state where it can take
|
|
||||||
investors' trades and Matchers' match requests. To achieve this state the
|
|
||||||
following must occur in order:
|
|
||||||
|
|
||||||
- Start the Solana blockchain
|
|
||||||
- Start the thin-client
|
|
||||||
- The Matcher 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 Matcher starts responding to queries for bid/ask price and OHLCV
|
|
||||||
|
|
||||||
The Matcher 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 Matcher could come and go without missing a trade. One way to achieve
|
|
||||||
this is for the Matcher to read the current state of all accounts looking for all
|
|
||||||
open 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. Matcher as well will
|
|
||||||
request accounts to hold the tokens they earn by initiating trade swaps.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Supported token types
|
|
||||||
pub enum Token {
|
|
||||||
A,
|
|
||||||
B,
|
|
||||||
C,
|
|
||||||
D,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supported token pairs
|
|
||||||
pub enum TokenPair {
|
|
||||||
AB,
|
|
||||||
AC,
|
|
||||||
AD,
|
|
||||||
BC,
|
|
||||||
BD,
|
|
||||||
CD,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// New token account
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - New token account
|
|
||||||
AccountRequest,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Token accounts are populated with this structure
|
|
||||||
pub struct TokenAccountInfo {
|
|
||||||
/// Investor who owns this account
|
|
||||||
pub owner: Pubkey,
|
|
||||||
/// Current number of tokens this account holds
|
|
||||||
pub tokens: Tokens,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For this demo investors or Matcher can request more tokens from the exchange at
|
|
||||||
any time by submitting token requests. In non-demos, an exchange of this type
|
|
||||||
would provide another way to exchange a 3rd party asset into tokens.
|
|
||||||
|
|
||||||
To request tokens, investors submit transfer requests:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// Transfer tokens between two accounts
|
|
||||||
/// key 0 - Account to transfer tokens to
|
|
||||||
/// key 1 - Account to transfer tokens from. This can be the exchange program itself,
|
|
||||||
/// the exchange has a limitless number of tokens it can transfer.
|
|
||||||
TransferRequest(Token, u64),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Order Requests
|
|
||||||
|
|
||||||
When an investor decides to exchange a token of one type for another, they
|
|
||||||
submit a transaction to the Solana Blockchain containing an order request, which,
|
|
||||||
if successful, is turned into an order. orders do not expire but are
|
|
||||||
cancellable. <!-- orders should have a timestamp to enable trade
|
|
||||||
expiration --> When an order is created, tokens are deducted from a token
|
|
||||||
account and the order acts as an escrow. The tokens are held until the
|
|
||||||
order is fulfilled or canceled. If the direction is `To`, then the number
|
|
||||||
of `tokens` are deducted from the primary account, if `From` then `tokens`
|
|
||||||
multiplied by `price` are deducted from the secondary account. orders are
|
|
||||||
no longer valid when the number of `tokens` goes to zero, at which point they
|
|
||||||
can no longer be used. <!-- Could support refilling orders, so order
|
|
||||||
accounts are refilled rather than accumulating -->
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Direction of the exchange between two tokens in a pair
|
|
||||||
pub enum Direction {
|
|
||||||
/// Trade first token type (primary) in the pair 'To' the second
|
|
||||||
To,
|
|
||||||
/// Trade first token type in the pair 'From' the second (secondary)
|
|
||||||
From,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OrderRequestInfo {
|
|
||||||
/// Direction of trade
|
|
||||||
pub direction: Direction,
|
|
||||||
|
|
||||||
/// Token pair to trade
|
|
||||||
pub pair: TokenPair,
|
|
||||||
|
|
||||||
/// Number of tokens to exchange; refers to the primary or the secondary depending on the direction
|
|
||||||
pub tokens: u64,
|
|
||||||
|
|
||||||
/// The price ratio the primary price over the secondary price. The primary price is fixed
|
|
||||||
/// and equal to the variable `SCALER`.
|
|
||||||
pub price: u64,
|
|
||||||
|
|
||||||
/// Token account to deposit tokens on successful swap
|
|
||||||
pub dst_account: Pubkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// order request
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - Account in which to record the swap
|
|
||||||
/// key 2 - Token account associated with this trade
|
|
||||||
TradeRequest(TradeRequestInfo),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trade accounts are populated with this structure
|
|
||||||
pub struct TradeOrderInfo {
|
|
||||||
/// Owner of the order
|
|
||||||
pub owner: Pubkey,
|
|
||||||
/// Direction of the exchange
|
|
||||||
pub direction: Direction,
|
|
||||||
/// Token pair indicating two tokens to exchange, first is primary
|
|
||||||
pub pair: TokenPair,
|
|
||||||
/// Number of tokens to exchange; primary or secondary depending on direction
|
|
||||||
pub tokens: u64,
|
|
||||||
/// Scaled price of the secondary token given the primary is equal to the scale value
|
|
||||||
/// If scale is 1 and price is 2 then ratio is 1:2 or 1 primary token for 2 secondary tokens
|
|
||||||
pub price: u64,
|
|
||||||
/// account which the tokens were source from. The trade account holds the tokens in escrow
|
|
||||||
/// until either one or more part of a swap or the trade is canceled.
|
|
||||||
pub src_account: Pubkey,
|
|
||||||
/// account which the tokens the tokens will be deposited into on a successful trade
|
|
||||||
pub dst_account: Pubkey,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Order cancellations
|
|
||||||
|
|
||||||
An investor may cancel a trade at anytime, but only trades they own. If the
|
|
||||||
cancellation is successful, any tokens held in escrow are returned to the
|
|
||||||
account from which they came.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// order cancellation
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 -order to cancel
|
|
||||||
TradeCancellation,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Trade swaps
|
|
||||||
|
|
||||||
The Matcher is monitoring the accounts assigned to the exchange program and
|
|
||||||
building a trade-order table. The order table is used to identify
|
|
||||||
matching orders which could be fulfilled. When a match is found the
|
|
||||||
Matcher 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 order valid for further swap requests in
|
|
||||||
the future.
|
|
||||||
|
|
||||||
Matching orders are defined by the following swap requirements:
|
|
||||||
|
|
||||||
- Opposite polarity (one `To` and one `From`)
|
|
||||||
- Operate on the same token pair
|
|
||||||
- The price ratio of the `From` order is greater than or equal to the `To` order
|
|
||||||
- There are sufficient tokens to perform the trade
|
|
||||||
|
|
||||||
Orders can be written in the following format:
|
|
||||||
|
|
||||||
`investor direction pair quantity price-ratio`
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
- `1 T AB 2 1`
|
|
||||||
- Investor 1 wishes to exchange 2 A tokens to B tokens at a ratio of 1 A to 1
|
|
||||||
B
|
|
||||||
- `2 F AC 6 1.2`
|
|
||||||
- Investor 2 wishes to exchange A tokens from 6 B tokens at a ratio of 1 A
|
|
||||||
from 1.2 B
|
|
||||||
|
|
||||||
An order table could look something like the following. Notice how the columns
|
|
||||||
are sorted low to high and high to low, respectively. Prices are dramatic and
|
|
||||||
whole for clarity.
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 2 4 | 2 F AB 2 8 |
|
|
||||||
| 2 | 1 T AB 1 4 | 2 F AB 2 8 |
|
|
||||||
| 3 | 1 T AB 6 6 | 2 F AB 2 7 |
|
|
||||||
| 4 | 1 T AB 2 8 | 2 F AB 3 6 |
|
|
||||||
| 5 | 1 T AB 2 10 | 2 F AB 1 5 |
|
|
||||||
|
|
||||||
As part of a successful swap request, the exchange will credit tokens to the
|
|
||||||
Matcher's account equal to the difference in the price ratios or the two orders.
|
|
||||||
These tokens are considered the Matcher's profit for initiating the trade.
|
|
||||||
|
|
||||||
The Matcher 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
|
|
||||||
- Matcher takes 8 B tokens as profit
|
|
||||||
|
|
||||||
Both row 1 trades are fully realized, table becomes:
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 1 4 | 2 F AB 2 8 |
|
|
||||||
| 2 | 1 T AB 6 6 | 2 F AB 2 7 |
|
|
||||||
| 3 | 1 T AB 2 8 | 2 F AB 3 6 |
|
|
||||||
| 4 | 1 T AB 2 10 | 2 F AB 1 5 |
|
|
||||||
|
|
||||||
The Matcher 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
|
|
||||||
- Matcher takes 4 B tokens as profit
|
|
||||||
|
|
||||||
Row 1 From is not fully realized, table becomes:
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 6 6 | 2 F AB 1 8 |
|
|
||||||
| 2 | 1 T AB 2 8 | 2 F AB 2 7 |
|
|
||||||
| 3 | 1 T AB 2 10 | 2 F AB 3 6 |
|
|
||||||
| 4 | | 2 F AB 1 5 |
|
|
||||||
|
|
||||||
The Matcher 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
|
|
||||||
- Matcher takes 2 B tokens as profit
|
|
||||||
|
|
||||||
Row 1 To is now fully realized, table becomes:
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 5 6 | 2 F AB 2 7 |
|
|
||||||
| 2 | 1 T AB 2 8 | 2 F AB 3 5 |
|
|
||||||
| 3 | 1 T AB 2 10 | 2 F AB 1 5 |
|
|
||||||
|
|
||||||
The Matcher 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
|
|
||||||
- Matcher takes 2 B tokens as profit
|
|
||||||
|
|
||||||
Table becomes:
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 3 6 | 2 F AB 3 5 |
|
|
||||||
| 2 | 1 T AB 2 8 | 2 F AB 1 5 |
|
|
||||||
| 3 | 1 T AB 2 10 | |
|
|
||||||
|
|
||||||
At this point the lowest To's price is larger than the largest From's price so
|
|
||||||
no more swaps would be initiated until new orders came in.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// Trade swap request
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - Account in which to record the swap
|
|
||||||
/// key 2 - 'To' order
|
|
||||||
/// key 3 - `From` 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 Matcher profit from the swap.
|
|
||||||
SwapRequest,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Swap accounts are populated with this structure
|
|
||||||
pub struct TradeSwapInfo {
|
|
||||||
/// Pair swapped
|
|
||||||
pub pair: TokenPair,
|
|
||||||
/// `To` order
|
|
||||||
pub to_trade_order: Pubkey,
|
|
||||||
/// `From` order
|
|
||||||
pub from_trade_order: Pubkey,
|
|
||||||
/// Number of primary tokens exchanged
|
|
||||||
pub primary_tokens: u64,
|
|
||||||
/// Price the primary tokens were exchanged for
|
|
||||||
pub primary_price: u64,
|
|
||||||
/// Number of secondary tokens exchanged
|
|
||||||
pub secondary_tokens: u64,
|
|
||||||
/// Price the secondary tokens were exchanged for
|
|
||||||
pub secondary_price: u64,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Exchange program operations
|
|
||||||
|
|
||||||
Putting all the commands together from above, the following operations will be
|
|
||||||
supported by the on-chain exchange program:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// New token account
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - New token account
|
|
||||||
AccountRequest,
|
|
||||||
|
|
||||||
/// Transfer tokens between two accounts
|
|
||||||
/// key 0 - Account to transfer tokens to
|
|
||||||
/// key 1 - Account to transfer tokens from. This can be the exchange program itself,
|
|
||||||
/// the exchange has a limitless number of tokens it can transfer.
|
|
||||||
TransferRequest(Token, u64),
|
|
||||||
|
|
||||||
/// order request
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - Account in which to record the swap
|
|
||||||
/// key 2 - Token account associated with this trade
|
|
||||||
TradeRequest(TradeRequestInfo),
|
|
||||||
|
|
||||||
/// order cancellation
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 -order to cancel
|
|
||||||
TradeCancellation,
|
|
||||||
|
|
||||||
/// Trade swap request
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - Account in which to record the swap
|
|
||||||
/// key 2 - 'To' order
|
|
||||||
/// key 3 - `From` 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 Matcher profit from the swap.
|
|
||||||
SwapRequest,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quotes and OHLCV
|
|
||||||
|
|
||||||
The Matcher will provide current bid/ask price quotes based on trade actively and
|
|
||||||
also provide OHLCV based on some time window. The details of how the bid/ask
|
|
||||||
price quotes are calculated are yet to be decided.
|
|
||||||
|
|
||||||
## Investor strategies
|
|
||||||
|
|
||||||
To make a compelling demo, the investors needs to provide interesting trade
|
|
||||||
behavior. Something as simple as a randomly twiddled baseline would be a
|
|
||||||
minimum starting point.
|
|
||||||
|
|
||||||
## Running the exchange
|
|
||||||
|
|
||||||
The exchange bench posts trades and swaps matches as fast as it can.
|
|
||||||
|
|
||||||
You might want to bump the duration up
|
|
||||||
to 60 seconds and the batch size to 1000 for better numbers. You can modify those
|
|
||||||
in client_demo/src/demo.rs::test_exchange_local_cluster.
|
|
||||||
|
|
||||||
The following command runs the bench:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ RUST_LOG=solana_bench_exchange=info cargo test --release -- --nocapture test_exchange_local_cluster
|
|
||||||
```
|
|
||||||
|
|
||||||
To also see the cluster messages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ RUST_LOG=solana_bench_exchange=info,solana=info cargo test --release -- --nocapture test_exchange_local_cluster
|
|
||||||
```
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,220 +0,0 @@
|
|||||||
use clap::{crate_description, crate_name, value_t, App, Arg, ArgMatches};
|
|
||||||
use solana_core::gen_keys::GenKeys;
|
|
||||||
use solana_faucet::faucet::FAUCET_PORT;
|
|
||||||
use solana_sdk::signature::{read_keypair_file, Keypair};
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub struct Config {
|
|
||||||
pub entrypoint_addr: SocketAddr,
|
|
||||||
pub faucet_addr: SocketAddr,
|
|
||||||
pub identity: Keypair,
|
|
||||||
pub threads: usize,
|
|
||||||
pub num_nodes: usize,
|
|
||||||
pub duration: Duration,
|
|
||||||
pub transfer_delay: u64,
|
|
||||||
pub fund_amount: u64,
|
|
||||||
pub batch_size: usize,
|
|
||||||
pub chunk_size: usize,
|
|
||||||
pub account_groups: usize,
|
|
||||||
pub client_ids_and_stake_file: String,
|
|
||||||
pub write_to_client_file: bool,
|
|
||||||
pub read_from_client_file: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
|
||||||
faucet_addr: SocketAddr::from(([127, 0, 0, 1], FAUCET_PORT)),
|
|
||||||
identity: Keypair::new(),
|
|
||||||
num_nodes: 1,
|
|
||||||
threads: 4,
|
|
||||||
duration: Duration::new(u64::max_value(), 0),
|
|
||||||
transfer_delay: 0,
|
|
||||||
fund_amount: 100_000,
|
|
||||||
batch_size: 100,
|
|
||||||
chunk_size: 100,
|
|
||||||
account_groups: 100,
|
|
||||||
client_ids_and_stake_file: String::new(),
|
|
||||||
write_to_client_file: false,
|
|
||||||
read_from_client_file: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
|
||||||
App::new(crate_name!())
|
|
||||||
.about(crate_description!())
|
|
||||||
.version(version)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("entrypoint")
|
|
||||||
.short("n")
|
|
||||||
.long("entrypoint")
|
|
||||||
.value_name("HOST:PORT")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("127.0.0.1:8001")
|
|
||||||
.help("Cluster entry point; defaults to 127.0.0.1:8001"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("faucet")
|
|
||||||
.short("d")
|
|
||||||
.long("faucet")
|
|
||||||
.value_name("HOST:PORT")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("127.0.0.1:9900")
|
|
||||||
.help("Location of the faucet; defaults to 127.0.0.1:9900"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("identity")
|
|
||||||
.short("i")
|
|
||||||
.long("identity")
|
|
||||||
.value_name("PATH")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("File containing a client identity (keypair)"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("threads")
|
|
||||||
.long("threads")
|
|
||||||
.value_name("<threads>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("1")
|
|
||||||
.help("Number of threads submitting transactions"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("num-nodes")
|
|
||||||
.long("num-nodes")
|
|
||||||
.value_name("NUM")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("1")
|
|
||||||
.help("Wait for NUM nodes to converge"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("duration")
|
|
||||||
.long("duration")
|
|
||||||
.value_name("SECS")
|
|
||||||
.takes_value(true)
|
|
||||||
.default_value("60")
|
|
||||||
.help("Seconds to run benchmark, then exit; default is forever"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("transfer-delay")
|
|
||||||
.long("transfer-delay")
|
|
||||||
.value_name("<delay>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("0")
|
|
||||||
.help("Delay between each chunk"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("fund-amount")
|
|
||||||
.long("fund-amount")
|
|
||||||
.value_name("<fund>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("100000")
|
|
||||||
.help("Number of lamports to fund to each signer"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("batch-size")
|
|
||||||
.long("batch-size")
|
|
||||||
.value_name("<batch>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("1000")
|
|
||||||
.help("Number of transactions before the signer rolls over"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("chunk-size")
|
|
||||||
.long("chunk-size")
|
|
||||||
.value_name("<cunk>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("500")
|
|
||||||
.help("Number of transactions to generate and send at a time"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("account-groups")
|
|
||||||
.long("account-groups")
|
|
||||||
.value_name("<groups>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("10")
|
|
||||||
.help("Number of account groups to cycle for each batch"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("write-client-keys")
|
|
||||||
.long("write-client-keys")
|
|
||||||
.value_name("FILENAME")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Generate client keys and stakes and write the list to YAML file"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("read-client-keys")
|
|
||||||
.long("read-client-keys")
|
|
||||||
.value_name("FILENAME")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Read client keys and stakes from the YAML file"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
|
||||||
let mut args = Config::default();
|
|
||||||
|
|
||||||
args.entrypoint_addr = solana_net_utils::parse_host_port(
|
|
||||||
matches.value_of("entrypoint").unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
eprintln!("failed to parse entrypoint address: {}", e);
|
|
||||||
exit(1)
|
|
||||||
});
|
|
||||||
|
|
||||||
args.faucet_addr = solana_net_utils::parse_host_port(matches.value_of("faucet").unwrap())
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
eprintln!("failed to parse faucet address: {}", e);
|
|
||||||
exit(1)
|
|
||||||
});
|
|
||||||
|
|
||||||
if matches.is_present("identity") {
|
|
||||||
args.identity = read_keypair_file(matches.value_of("identity").unwrap())
|
|
||||||
.expect("can't read client identity");
|
|
||||||
} else {
|
|
||||||
args.identity = {
|
|
||||||
let seed = [42_u8; 32];
|
|
||||||
let mut rnd = GenKeys::new(seed);
|
|
||||||
rnd.gen_keypair()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
args.threads = value_t!(matches.value_of("threads"), usize).expect("Failed to parse threads");
|
|
||||||
args.num_nodes =
|
|
||||||
value_t!(matches.value_of("num-nodes"), usize).expect("Failed to parse num-nodes");
|
|
||||||
let duration = value_t!(matches.value_of("duration"), u64).expect("Failed to parse duration");
|
|
||||||
args.duration = Duration::from_secs(duration);
|
|
||||||
args.transfer_delay =
|
|
||||||
value_t!(matches.value_of("transfer-delay"), u64).expect("Failed to parse transfer-delay");
|
|
||||||
args.fund_amount =
|
|
||||||
value_t!(matches.value_of("fund-amount"), u64).expect("Failed to parse fund-amount");
|
|
||||||
args.batch_size =
|
|
||||||
value_t!(matches.value_of("batch-size"), usize).expect("Failed to parse batch-size");
|
|
||||||
args.chunk_size =
|
|
||||||
value_t!(matches.value_of("chunk-size"), usize).expect("Failed to parse chunk-size");
|
|
||||||
args.account_groups = value_t!(matches.value_of("account-groups"), usize)
|
|
||||||
.expect("Failed to parse account-groups");
|
|
||||||
|
|
||||||
if let Some(s) = matches.value_of("write-client-keys") {
|
|
||||||
args.write_to_client_file = true;
|
|
||||||
args.client_ids_and_stake_file = s.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(s) = matches.value_of("read-client-keys") {
|
|
||||||
assert!(!args.write_to_client_file);
|
|
||||||
args.read_from_client_file = true;
|
|
||||||
args.client_ids_and_stake_file = s.to_string();
|
|
||||||
}
|
|
||||||
args
|
|
||||||
}
|
|
@@ -1,3 +0,0 @@
|
|||||||
pub mod bench;
|
|
||||||
pub mod cli;
|
|
||||||
mod order_book;
|
|
@@ -1,83 +0,0 @@
|
|||||||
pub mod bench;
|
|
||||||
mod cli;
|
|
||||||
pub mod order_book;
|
|
||||||
|
|
||||||
use crate::bench::{airdrop_lamports, create_client_accounts_file, do_bench_exchange, Config};
|
|
||||||
use log::*;
|
|
||||||
use solana_core::gossip_service::{discover_cluster, get_multi_client};
|
|
||||||
use solana_sdk::signature::Signer;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
solana_logger::setup();
|
|
||||||
solana_metrics::set_panic_hook("bench-exchange");
|
|
||||||
|
|
||||||
let matches = cli::build_args(solana_clap_utils::version!()).get_matches();
|
|
||||||
let cli_config = cli::extract_args(&matches);
|
|
||||||
|
|
||||||
let cli::Config {
|
|
||||||
entrypoint_addr,
|
|
||||||
faucet_addr,
|
|
||||||
identity,
|
|
||||||
threads,
|
|
||||||
num_nodes,
|
|
||||||
duration,
|
|
||||||
transfer_delay,
|
|
||||||
fund_amount,
|
|
||||||
batch_size,
|
|
||||||
chunk_size,
|
|
||||||
account_groups,
|
|
||||||
client_ids_and_stake_file,
|
|
||||||
write_to_client_file,
|
|
||||||
read_from_client_file,
|
|
||||||
..
|
|
||||||
} = cli_config;
|
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
identity,
|
|
||||||
threads,
|
|
||||||
duration,
|
|
||||||
transfer_delay,
|
|
||||||
fund_amount,
|
|
||||||
batch_size,
|
|
||||||
chunk_size,
|
|
||||||
account_groups,
|
|
||||||
client_ids_and_stake_file,
|
|
||||||
read_from_client_file,
|
|
||||||
};
|
|
||||||
|
|
||||||
if write_to_client_file {
|
|
||||||
create_client_accounts_file(
|
|
||||||
&config.client_ids_and_stake_file,
|
|
||||||
config.batch_size,
|
|
||||||
config.account_groups,
|
|
||||||
config.fund_amount,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
info!("Connecting to the cluster");
|
|
||||||
let (nodes, _archivers) =
|
|
||||||
discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|_| {
|
|
||||||
panic!("Failed to discover nodes");
|
|
||||||
});
|
|
||||||
|
|
||||||
let (client, num_clients) = get_multi_client(&nodes);
|
|
||||||
|
|
||||||
info!("{} nodes found", num_clients);
|
|
||||||
if num_clients < num_nodes {
|
|
||||||
panic!("Error: Insufficient nodes discovered");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !read_from_client_file {
|
|
||||||
info!("Funding keypair: {}", config.identity.pubkey());
|
|
||||||
|
|
||||||
let accounts_in_groups = batch_size * account_groups;
|
|
||||||
const NUM_SIGNERS: u64 = 2;
|
|
||||||
airdrop_lamports(
|
|
||||||
&client,
|
|
||||||
&faucet_addr,
|
|
||||||
&config.identity,
|
|
||||||
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
do_bench_exchange(vec![client], config);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,134 +0,0 @@
|
|||||||
use itertools::EitherOrBoth::{Both, Left, Right};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use log::*;
|
|
||||||
use solana_exchange_program::exchange_state::*;
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
use std::collections::BinaryHeap;
|
|
||||||
use std::{error, fmt};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct ToOrder {
|
|
||||||
pub pubkey: Pubkey,
|
|
||||||
pub info: OrderInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for ToOrder {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
other.info.price.cmp(&self.info.price)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialOrd for ToOrder {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct FromOrder {
|
|
||||||
pub pubkey: Pubkey,
|
|
||||||
pub info: OrderInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for FromOrder {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
self.info.price.cmp(&other.info.price)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialOrd for FromOrder {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct OrderBook {
|
|
||||||
// TODO scale to x token types
|
|
||||||
to_ab: BinaryHeap<ToOrder>,
|
|
||||||
from_ab: BinaryHeap<FromOrder>,
|
|
||||||
}
|
|
||||||
impl fmt::Display for OrderBook {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"+-Order Book--------------------------+-------------------------------------+"
|
|
||||||
)?;
|
|
||||||
for (i, it) in self
|
|
||||||
.to_ab
|
|
||||||
.iter()
|
|
||||||
.zip_longest(self.from_ab.iter())
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
match it {
|
|
||||||
Both(to, from) => writeln!(
|
|
||||||
f,
|
|
||||||
"| T AB {:8} for {:8}/{:8} | F AB {:8} for {:8}/{:8} |{}",
|
|
||||||
to.info.tokens,
|
|
||||||
SCALER,
|
|
||||||
to.info.price,
|
|
||||||
from.info.tokens,
|
|
||||||
SCALER,
|
|
||||||
from.info.price,
|
|
||||||
i
|
|
||||||
)?,
|
|
||||||
Left(to) => writeln!(
|
|
||||||
f,
|
|
||||||
"| T AB {:8} for {:8}/{:8} | |{}",
|
|
||||||
to.info.tokens, SCALER, to.info.price, i
|
|
||||||
)?,
|
|
||||||
Right(from) => writeln!(
|
|
||||||
f,
|
|
||||||
"| | F AB {:8} for {:8}/{:8} |{}",
|
|
||||||
from.info.tokens, SCALER, from.info.price, i
|
|
||||||
)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"+-------------------------------------+-------------------------------------+"
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OrderBook {
|
|
||||||
// TODO
|
|
||||||
// pub fn cancel(&mut self, pubkey: Pubkey) -> Result<(), Box<dyn error::Error>> {
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
pub fn push(&mut self, pubkey: Pubkey, info: OrderInfo) -> Result<(), Box<dyn error::Error>> {
|
|
||||||
check_trade(info.side, info.tokens, info.price)?;
|
|
||||||
match info.side {
|
|
||||||
OrderSide::Ask => {
|
|
||||||
self.to_ab.push(ToOrder { pubkey, info });
|
|
||||||
}
|
|
||||||
OrderSide::Bid => {
|
|
||||||
self.from_ab.push(FromOrder { pubkey, info });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn pop(&mut self) -> Option<(ToOrder, FromOrder)> {
|
|
||||||
if let Some(pair) = Self::pop_pair(&mut self.to_ab, &mut self.from_ab) {
|
|
||||||
return Some(pair);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pub fn get_num_outstanding(&self) -> (usize, usize) {
|
|
||||||
(self.to_ab.len(), self.from_ab.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pop_pair(
|
|
||||||
to_ab: &mut BinaryHeap<ToOrder>,
|
|
||||||
from_ab: &mut BinaryHeap<FromOrder>,
|
|
||||||
) -> Option<(ToOrder, FromOrder)> {
|
|
||||||
let to = to_ab.peek()?;
|
|
||||||
let from = from_ab.peek()?;
|
|
||||||
if from.info.price < to.info.price {
|
|
||||||
debug!("Trade not viable");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let to = to_ab.pop()?;
|
|
||||||
let from = from_ab.pop()?;
|
|
||||||
Some((to, from))
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,103 +0,0 @@
|
|||||||
use log::*;
|
|
||||||
use solana_bench_exchange::bench::{airdrop_lamports, do_bench_exchange, Config};
|
|
||||||
use solana_core::gossip_service::{discover_cluster, get_multi_client};
|
|
||||||
use solana_core::validator::ValidatorConfig;
|
|
||||||
use solana_exchange_program::exchange_processor::process_instruction;
|
|
||||||
use solana_exchange_program::id;
|
|
||||||
use solana_exchange_program::solana_exchange_program;
|
|
||||||
use solana_faucet::faucet::run_local_faucet;
|
|
||||||
use solana_local_cluster::local_cluster::{ClusterConfig, LocalCluster};
|
|
||||||
use solana_runtime::bank::Bank;
|
|
||||||
use solana_runtime::bank_client::BankClient;
|
|
||||||
use solana_sdk::genesis_config::create_genesis_config;
|
|
||||||
use solana_sdk::signature::{Keypair, Signer};
|
|
||||||
use std::process::exit;
|
|
||||||
use std::sync::mpsc::channel;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn test_exchange_local_cluster() {
|
|
||||||
solana_logger::setup();
|
|
||||||
|
|
||||||
const NUM_NODES: usize = 1;
|
|
||||||
|
|
||||||
let mut config = Config::default();
|
|
||||||
config.identity = Keypair::new();
|
|
||||||
config.duration = Duration::from_secs(1);
|
|
||||||
config.fund_amount = 100_000;
|
|
||||||
config.threads = 1;
|
|
||||||
config.transfer_delay = 20; // 15
|
|
||||||
config.batch_size = 100; // 1000;
|
|
||||||
config.chunk_size = 10; // 200;
|
|
||||||
config.account_groups = 1; // 10;
|
|
||||||
let Config {
|
|
||||||
fund_amount,
|
|
||||||
batch_size,
|
|
||||||
account_groups,
|
|
||||||
..
|
|
||||||
} = config;
|
|
||||||
let accounts_in_groups = batch_size * account_groups;
|
|
||||||
|
|
||||||
let cluster = LocalCluster::new(&ClusterConfig {
|
|
||||||
node_stakes: vec![100_000; NUM_NODES],
|
|
||||||
cluster_lamports: 100_000_000_000_000,
|
|
||||||
validator_configs: vec![ValidatorConfig::default(); NUM_NODES],
|
|
||||||
native_instruction_processors: [solana_exchange_program!()].to_vec(),
|
|
||||||
..ClusterConfig::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let faucet_keypair = Keypair::new();
|
|
||||||
cluster.transfer(
|
|
||||||
&cluster.funding_keypair,
|
|
||||||
&faucet_keypair.pubkey(),
|
|
||||||
2_000_000_000_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
let (addr_sender, addr_receiver) = channel();
|
|
||||||
run_local_faucet(faucet_keypair, addr_sender, Some(1_000_000_000_000));
|
|
||||||
let faucet_addr = addr_receiver.recv_timeout(Duration::from_secs(2)).unwrap();
|
|
||||||
|
|
||||||
info!("Connecting to the cluster");
|
|
||||||
let (nodes, _) =
|
|
||||||
discover_cluster(&cluster.entry_point_info.gossip, NUM_NODES).unwrap_or_else(|err| {
|
|
||||||
error!("Failed to discover {} nodes: {:?}", NUM_NODES, err);
|
|
||||||
exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
let (client, num_clients) = get_multi_client(&nodes);
|
|
||||||
|
|
||||||
info!("clients: {}", num_clients);
|
|
||||||
assert!(num_clients >= NUM_NODES);
|
|
||||||
|
|
||||||
const NUM_SIGNERS: u64 = 2;
|
|
||||||
airdrop_lamports(
|
|
||||||
&client,
|
|
||||||
&faucet_addr,
|
|
||||||
&config.identity,
|
|
||||||
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
|
|
||||||
);
|
|
||||||
|
|
||||||
do_bench_exchange(vec![client], config);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_exchange_bank_client() {
|
|
||||||
solana_logger::setup();
|
|
||||||
let (genesis_config, identity) = create_genesis_config(100_000_000_000_000);
|
|
||||||
let mut bank = Bank::new(&genesis_config);
|
|
||||||
bank.add_instruction_processor(id(), process_instruction);
|
|
||||||
let clients = vec![BankClient::new(bank)];
|
|
||||||
|
|
||||||
let mut config = Config::default();
|
|
||||||
config.identity = identity;
|
|
||||||
config.duration = Duration::from_secs(1);
|
|
||||||
config.fund_amount = 100_000;
|
|
||||||
config.threads = 1;
|
|
||||||
config.transfer_delay = 20; // 0;
|
|
||||||
config.batch_size = 100; // 1500;
|
|
||||||
config.chunk_size = 10; // 1500;
|
|
||||||
config.account_groups = 1; // 50;
|
|
||||||
|
|
||||||
do_bench_exchange(clients, config);
|
|
||||||
}
|
|
2
bench-streamer/.gitignore
vendored
2
bench-streamer/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
/target/
|
|
||||||
/farf/
|
|
@@ -2,14 +2,16 @@
|
|||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
name = "solana-bench-streamer"
|
name = "solana-bench-streamer"
|
||||||
version = "1.0.8"
|
version = "0.12.1"
|
||||||
repository = "https://github.com/solana-labs/solana"
|
repository = "https://github.com/solana-labs/solana"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
homepage = "https://solana.com/"
|
homepage = "https://solana.com/"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33.0"
|
clap = "2.32.0"
|
||||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.8" }
|
solana = { path = "../core", version = "0.12.1" }
|
||||||
solana-core = { path = "../core", version = "1.0.8" }
|
solana-logger = { path = "../logger", version = "0.12.1" }
|
||||||
solana-logger = { path = "../logger", version = "1.0.8" }
|
solana-netutil = { path = "../netutil", version = "0.12.1" }
|
||||||
solana-net-utils = { path = "../net-utils", version = "1.0.8" }
|
|
||||||
|
[features]
|
||||||
|
cuda = ["solana/cuda"]
|
||||||
|
@@ -1,33 +1,34 @@
|
|||||||
use clap::{crate_description, crate_name, App, Arg};
|
use clap::{App, Arg};
|
||||||
use solana_core::packet::{Packet, Packets, PacketsRecycler, PACKET_DATA_SIZE};
|
use solana::packet::{Packet, SharedPackets, BLOB_SIZE, PACKET_DATA_SIZE};
|
||||||
use solana_core::streamer::{receiver, PacketReceiver};
|
use solana::result::Result;
|
||||||
|
use solana::streamer::{receiver, PacketReceiver};
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::thread::{spawn, JoinHandle, Result};
|
use std::thread::{spawn, JoinHandle};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
|
fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
|
||||||
let send = UdpSocket::bind("0.0.0.0:0").unwrap();
|
let send = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||||
let mut msgs = Packets::default();
|
let msgs = SharedPackets::default();
|
||||||
msgs.packets.resize(10, Packet::default());
|
let msgs_ = msgs.clone();
|
||||||
for w in msgs.packets.iter_mut() {
|
msgs.write().unwrap().packets.resize(10, Packet::default());
|
||||||
|
for w in &mut msgs.write().unwrap().packets {
|
||||||
w.meta.size = PACKET_DATA_SIZE;
|
w.meta.size = PACKET_DATA_SIZE;
|
||||||
w.meta.set_addr(&addr);
|
w.meta.set_addr(&addr);
|
||||||
}
|
}
|
||||||
let msgs = Arc::new(msgs);
|
|
||||||
spawn(move || loop {
|
spawn(move || loop {
|
||||||
if exit.load(Ordering::Relaxed) {
|
if exit.load(Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut num = 0;
|
let mut num = 0;
|
||||||
for p in &msgs.packets {
|
for p in &msgs_.read().unwrap().packets {
|
||||||
let a = p.meta.addr();
|
let a = p.meta.addr();
|
||||||
assert!(p.meta.size < PACKET_DATA_SIZE);
|
assert!(p.meta.size < BLOB_SIZE);
|
||||||
send.send_to(&p.data[..p.meta.size], &a).unwrap();
|
send.send_to(&p.data[..p.meta.size], &a).unwrap();
|
||||||
num += 1;
|
num += 1;
|
||||||
}
|
}
|
||||||
@@ -42,7 +43,7 @@ fn sink(exit: Arc<AtomicBool>, rvs: Arc<AtomicUsize>, r: PacketReceiver) -> Join
|
|||||||
}
|
}
|
||||||
let timer = Duration::new(1, 0);
|
let timer = Duration::new(1, 0);
|
||||||
if let Ok(msgs) = r.recv_timeout(timer) {
|
if let Ok(msgs) = r.recv_timeout(timer) {
|
||||||
rvs.fetch_add(msgs.packets.len(), Ordering::Relaxed);
|
rvs.fetch_add(msgs.read().unwrap().packets.len(), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -50,9 +51,7 @@ fn sink(exit: Arc<AtomicBool>, rvs: Arc<AtomicUsize>, r: PacketReceiver) -> Join
|
|||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let mut num_sockets = 1usize;
|
let mut num_sockets = 1usize;
|
||||||
|
|
||||||
let matches = App::new(crate_name!())
|
let matches = App::new("solana-bench-streamer")
|
||||||
.about(crate_description!())
|
|
||||||
.version(solana_clap_utils::version!())
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("num-recv-sockets")
|
Arg::with_name("num-recv-sockets")
|
||||||
.long("num-recv-sockets")
|
.long("num-recv-sockets")
|
||||||
@@ -67,16 +66,14 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut port = 0;
|
let mut port = 0;
|
||||||
let ip_addr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
|
let mut addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
|
||||||
let mut addr = SocketAddr::new(ip_addr, 0);
|
|
||||||
|
|
||||||
let exit = Arc::new(AtomicBool::new(false));
|
let exit = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
let mut read_channels = Vec::new();
|
let mut read_channels = Vec::new();
|
||||||
let mut read_threads = Vec::new();
|
let mut read_threads = Vec::new();
|
||||||
let recycler = PacketsRecycler::default();
|
|
||||||
for _ in 0..num_sockets {
|
for _ in 0..num_sockets {
|
||||||
let read = solana_net_utils::bind_to(ip_addr, port, false).unwrap();
|
let read = solana_netutil::bind_to(port, false).unwrap();
|
||||||
read.set_read_timeout(Some(Duration::new(1, 0))).unwrap();
|
read.set_read_timeout(Some(Duration::new(1, 0))).unwrap();
|
||||||
|
|
||||||
addr = read.local_addr().unwrap();
|
addr = read.local_addr().unwrap();
|
||||||
@@ -84,13 +81,7 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
let (s_reader, r_reader) = channel();
|
let (s_reader, r_reader) = channel();
|
||||||
read_channels.push(r_reader);
|
read_channels.push(r_reader);
|
||||||
read_threads.push(receiver(
|
read_threads.push(receiver(Arc::new(read), &exit, s_reader, "bench-streamer"));
|
||||||
Arc::new(read),
|
|
||||||
&exit,
|
|
||||||
s_reader,
|
|
||||||
recycler.clone(),
|
|
||||||
"bench-streamer-test",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let t_producer1 = producer(&addr, exit.clone());
|
let t_producer1 = producer(&addr, exit.clone());
|
||||||
|
4
bench-tps/.gitignore
vendored
4
bench-tps/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
/target/
|
|
||||||
/config/
|
|
||||||
/config-local/
|
|
||||||
/farf/
|
|
@@ -2,36 +2,21 @@
|
|||||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
name = "solana-bench-tps"
|
name = "solana-bench-tps"
|
||||||
version = "1.0.8"
|
version = "0.12.1"
|
||||||
repository = "https://github.com/solana-labs/solana"
|
repository = "https://github.com/solana-labs/solana"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
homepage = "https://solana.com/"
|
homepage = "https://solana.com/"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bincode = "1.2.1"
|
clap = "2.32.0"
|
||||||
clap = "2.33.0"
|
rayon = "1.0.3"
|
||||||
log = "0.4.8"
|
serde_json = "1.0.39"
|
||||||
rayon = "1.2.0"
|
solana = { path = "../core", version = "0.12.1" }
|
||||||
serde_json = "1.0.46"
|
solana-client = { path = "../client", version = "0.12.1" }
|
||||||
serde_yaml = "0.8.11"
|
solana-drone = { path = "../drone", version = "0.12.1" }
|
||||||
solana-clap-utils = { path = "../clap-utils", version = "1.0.8" }
|
solana-logger = { path = "../logger", version = "0.12.1" }
|
||||||
solana-core = { path = "../core", version = "1.0.8" }
|
solana-metrics = { path = "../metrics", version = "0.12.1" }
|
||||||
solana-genesis = { path = "../genesis", version = "1.0.8" }
|
solana-sdk = { path = "../sdk", version = "0.12.1" }
|
||||||
solana-client = { path = "../client", version = "1.0.8" }
|
|
||||||
solana-faucet = { path = "../faucet", version = "1.0.8" }
|
|
||||||
solana-librapay = { path = "../programs/librapay", version = "1.0.8", optional = true }
|
|
||||||
solana-logger = { path = "../logger", version = "1.0.8" }
|
|
||||||
solana-metrics = { path = "../metrics", version = "1.0.8" }
|
|
||||||
solana-measure = { path = "../measure", version = "1.0.8" }
|
|
||||||
solana-net-utils = { path = "../net-utils", version = "1.0.8" }
|
|
||||||
solana-runtime = { path = "../runtime", version = "1.0.8" }
|
|
||||||
solana-sdk = { path = "../sdk", version = "1.0.8" }
|
|
||||||
solana-move-loader-program = { path = "../programs/move_loader", version = "1.0.8", optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
serial_test = "0.3.2"
|
|
||||||
serial_test_derive = "0.4.0"
|
|
||||||
solana-local-cluster = { path = "../local-cluster", version = "1.0.8" }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
move = ["solana-librapay", "solana-move-loader-program"]
|
cuda = ["solana/cuda"]
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,75 +1,63 @@
|
|||||||
use clap::{crate_description, crate_name, App, Arg, ArgMatches};
|
use std::net::SocketAddr;
|
||||||
use solana_faucet::faucet::FAUCET_PORT;
|
use std::process::exit;
|
||||||
use solana_sdk::fee_calculator::FeeRateGovernor;
|
use std::time::Duration;
|
||||||
use solana_sdk::signature::{read_keypair_file, Keypair};
|
|
||||||
use std::{net::SocketAddr, process::exit, time::Duration};
|
|
||||||
|
|
||||||
const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = solana_sdk::native_token::LAMPORTS_PER_SOL;
|
use clap::{crate_version, App, Arg, ArgMatches};
|
||||||
|
use solana_drone::drone::DRONE_PORT;
|
||||||
|
use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
|
||||||
|
|
||||||
/// Holds the configuration for a single run of the benchmark
|
/// Holds the configuration for a single run of the benchmark
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub entrypoint_addr: SocketAddr,
|
pub network_addr: SocketAddr,
|
||||||
pub faucet_addr: SocketAddr,
|
pub drone_addr: SocketAddr,
|
||||||
pub id: Keypair,
|
pub id: Keypair,
|
||||||
pub threads: usize,
|
pub threads: usize,
|
||||||
pub num_nodes: usize,
|
pub num_nodes: usize,
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
pub tx_count: usize,
|
pub tx_count: usize,
|
||||||
pub keypair_multiplier: usize,
|
|
||||||
pub thread_batch_sleep_ms: usize,
|
pub thread_batch_sleep_ms: usize,
|
||||||
pub sustained: bool,
|
pub sustained: bool,
|
||||||
pub client_ids_and_stake_file: String,
|
pub reject_extra_nodes: bool,
|
||||||
pub write_to_client_file: bool,
|
pub converge_only: bool,
|
||||||
pub read_from_client_file: bool,
|
|
||||||
pub target_lamports_per_signature: u64,
|
|
||||||
pub multi_client: bool,
|
|
||||||
pub use_move: bool,
|
|
||||||
pub num_lamports_per_account: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Config {
|
fn default() -> Config {
|
||||||
Config {
|
Config {
|
||||||
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
network_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||||
faucet_addr: SocketAddr::from(([127, 0, 0, 1], FAUCET_PORT)),
|
drone_addr: SocketAddr::from(([127, 0, 0, 1], DRONE_PORT)),
|
||||||
id: Keypair::new(),
|
id: Keypair::new(),
|
||||||
threads: 4,
|
threads: 4,
|
||||||
num_nodes: 1,
|
num_nodes: 1,
|
||||||
duration: Duration::new(std::u64::MAX, 0),
|
duration: Duration::new(std::u64::MAX, 0),
|
||||||
tx_count: 50_000,
|
tx_count: 500_000,
|
||||||
keypair_multiplier: 8,
|
thread_batch_sleep_ms: 0,
|
||||||
thread_batch_sleep_ms: 1000,
|
|
||||||
sustained: false,
|
sustained: false,
|
||||||
client_ids_and_stake_file: String::new(),
|
reject_extra_nodes: false,
|
||||||
write_to_client_file: false,
|
converge_only: false,
|
||||||
read_from_client_file: false,
|
|
||||||
target_lamports_per_signature: FeeRateGovernor::default().target_lamports_per_signature,
|
|
||||||
multi_client: true,
|
|
||||||
use_move: false,
|
|
||||||
num_lamports_per_account: NUM_LAMPORTS_PER_ACCOUNT_DEFAULT,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Defines and builds the CLI args for a run of the benchmark
|
/// Defines and builds the CLI args for a run of the benchmark
|
||||||
pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
pub fn build_args<'a, 'b>() -> App<'a, 'b> {
|
||||||
App::new(crate_name!()).about(crate_description!())
|
App::new("solana-bench-tps")
|
||||||
.version(version)
|
.version(crate_version!())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("entrypoint")
|
Arg::with_name("network")
|
||||||
.short("n")
|
.short("n")
|
||||||
.long("entrypoint")
|
.long("network")
|
||||||
.value_name("HOST:PORT")
|
.value_name("HOST:PORT")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Rendezvous with the cluster at this entry point; defaults to 127.0.0.1:8001"),
|
.help("Rendezvous with the network at this gossip entry point; defaults to 127.0.0.1:8001"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("faucet")
|
Arg::with_name("drone")
|
||||||
.short("d")
|
.short("d")
|
||||||
.long("faucet")
|
.long("drone")
|
||||||
.value_name("HOST:PORT")
|
.value_name("HOST:PORT")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Location of the faucet; defaults to entrypoint:FAUCET_PORT"),
|
.help("Location of the drone; defaults to network:DRONE_PORT"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("identity")
|
Arg::with_name("identity")
|
||||||
@@ -87,6 +75,11 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
|||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Wait for NUM nodes to converge"),
|
.help("Wait for NUM nodes to converge"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("reject-extra-nodes")
|
||||||
|
.long("reject-extra-nodes")
|
||||||
|
.help("Require exactly `num-nodes` on convergence. Appropriate only for internal networks"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("threads")
|
Arg::with_name("threads")
|
||||||
.short("t")
|
.short("t")
|
||||||
@@ -102,21 +95,16 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
|||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Seconds to run benchmark, then exit; default is forever"),
|
.help("Seconds to run benchmark, then exit; default is forever"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("converge-only")
|
||||||
|
.long("converge-only")
|
||||||
|
.help("Exit immediately after converging"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("sustained")
|
Arg::with_name("sustained")
|
||||||
.long("sustained")
|
.long("sustained")
|
||||||
.help("Use sustained performance mode vs. peak mode. This overlaps the tx generation with transfers."),
|
.help("Use sustained performance mode vs. peak mode. This overlaps the tx generation with transfers."),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::with_name("use-move")
|
|
||||||
.long("use-move")
|
|
||||||
.help("Use Move language transactions to perform transfers."),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("no-multi-client")
|
|
||||||
.long("no-multi-client")
|
|
||||||
.help("Disable multi-client support, only transact with the entrypoint."),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("tx_count")
|
Arg::with_name("tx_count")
|
||||||
.long("tx_count")
|
.long("tx_count")
|
||||||
@@ -124,13 +112,6 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
|||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Number of transactions to send per batch")
|
.help("Number of transactions to send per batch")
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::with_name("keypair_multiplier")
|
|
||||||
.long("keypair-multiplier")
|
|
||||||
.value_name("NUM")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Multiply by transaction count to determine number of keypairs to create")
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("thread-batch-sleep-ms")
|
Arg::with_name("thread-batch-sleep-ms")
|
||||||
.short("z")
|
.short("z")
|
||||||
@@ -139,39 +120,6 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
|||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Per-thread-per-iteration sleep in ms"),
|
.help("Per-thread-per-iteration sleep in ms"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::with_name("write-client-keys")
|
|
||||||
.long("write-client-keys")
|
|
||||||
.value_name("FILENAME")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Generate client keys and stakes and write the list to YAML file"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("read-client-keys")
|
|
||||||
.long("read-client-keys")
|
|
||||||
.value_name("FILENAME")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Read client keys and stakes from the YAML file"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("target_lamports_per_signature")
|
|
||||||
.long("target-lamports-per-signature")
|
|
||||||
.value_name("LAMPORTS")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
"The cost in lamports that the cluster will charge for signature \
|
|
||||||
verification when the cluster is operating at target-signatures-per-slot",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("num_lamports_per_account")
|
|
||||||
.long("num-lamports-per-account")
|
|
||||||
.value_name("LAMPORTS")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
"Number of lamports per account.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a clap `ArgMatches` structure into a `Config`
|
/// Parses a clap `ArgMatches` structure into a `Config`
|
||||||
@@ -182,22 +130,22 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
|||||||
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
||||||
let mut args = Config::default();
|
let mut args = Config::default();
|
||||||
|
|
||||||
if let Some(addr) = matches.value_of("entrypoint") {
|
if let Some(addr) = matches.value_of("network") {
|
||||||
args.entrypoint_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| {
|
args.network_addr = addr.parse().unwrap_or_else(|e| {
|
||||||
eprintln!("failed to parse entrypoint address: {}", e);
|
eprintln!("failed to parse network: {}", e);
|
||||||
exit(1)
|
exit(1)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(addr) = matches.value_of("faucet") {
|
if let Some(addr) = matches.value_of("drone") {
|
||||||
args.faucet_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| {
|
args.drone_addr = addr.parse().unwrap_or_else(|e| {
|
||||||
eprintln!("failed to parse faucet address: {}", e);
|
eprintln!("failed to parse drone address: {}", e);
|
||||||
exit(1)
|
exit(1)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches.is_present("identity") {
|
if matches.is_present("identity") {
|
||||||
args.id = read_keypair_file(matches.value_of("identity").unwrap())
|
args.id = read_keypair(matches.value_of("identity").unwrap())
|
||||||
.expect("can't read client identity");
|
.expect("can't read client identity");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,15 +165,7 @@ pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(s) = matches.value_of("tx_count") {
|
if let Some(s) = matches.value_of("tx_count") {
|
||||||
args.tx_count = s.to_string().parse().expect("can't parse tx_count");
|
args.tx_count = s.to_string().parse().expect("can't parse tx_account");
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(s) = matches.value_of("keypair_multiplier") {
|
|
||||||
args.keypair_multiplier = s
|
|
||||||
.to_string()
|
|
||||||
.parse()
|
|
||||||
.expect("can't parse keypair-multiplier");
|
|
||||||
assert!(args.keypair_multiplier >= 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(t) = matches.value_of("thread-batch-sleep-ms") {
|
if let Some(t) = matches.value_of("thread-batch-sleep-ms") {
|
||||||
@@ -236,28 +176,8 @@ pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
args.sustained = matches.is_present("sustained");
|
args.sustained = matches.is_present("sustained");
|
||||||
|
args.converge_only = matches.is_present("converge-only");
|
||||||
if let Some(s) = matches.value_of("write-client-keys") {
|
args.reject_extra_nodes = matches.is_present("reject-extra-nodes");
|
||||||
args.write_to_client_file = true;
|
|
||||||
args.client_ids_and_stake_file = s.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(s) = matches.value_of("read-client-keys") {
|
|
||||||
assert!(!args.write_to_client_file);
|
|
||||||
args.read_from_client_file = true;
|
|
||||||
args.client_ids_and_stake_file = s.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = matches.value_of("target_lamports_per_signature") {
|
|
||||||
args.target_lamports_per_signature = v.to_string().parse().expect("can't parse lamports");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.use_move = matches.is_present("use-move");
|
|
||||||
args.multi_client = !matches.is_present("no-multi-client");
|
|
||||||
|
|
||||||
if let Some(v) = matches.value_of("num_lamports_per_account") {
|
|
||||||
args.num_lamports_per_account = v.to_string().parse().expect("can't parse lamports");
|
|
||||||
}
|
|
||||||
|
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
pub mod bench;
|
|
||||||
pub mod cli;
|
|
@@ -1,137 +1,251 @@
|
|||||||
use log::*;
|
mod bench;
|
||||||
use solana_bench_tps::bench::{do_bench_tps, generate_and_fund_keypairs, generate_keypairs};
|
mod cli;
|
||||||
use solana_bench_tps::cli;
|
|
||||||
use solana_core::gossip_service::{discover_cluster, get_client, get_multi_client};
|
|
||||||
use solana_genesis::Base64Account;
|
|
||||||
use solana_sdk::fee_calculator::FeeRateGovernor;
|
|
||||||
use solana_sdk::signature::{Keypair, Signer};
|
|
||||||
use solana_sdk::system_program;
|
|
||||||
use std::{collections::HashMap, fs::File, io::prelude::*, path::Path, process::exit, sync::Arc};
|
|
||||||
|
|
||||||
/// Number of signatures for all transactions in ~1 week at ~100K TPS
|
use crate::bench::*;
|
||||||
pub const NUM_SIGNATURES_FOR_TXS: u64 = 100_000 * 60 * 60 * 24 * 7;
|
use solana::cluster_info::FULLNODE_PORT_RANGE;
|
||||||
|
use solana::gen_keys::GenKeys;
|
||||||
|
use solana::gossip_service::discover;
|
||||||
|
use solana_client::client::create_client;
|
||||||
|
use solana_metrics;
|
||||||
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::process::exit;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::thread::Builder;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
solana_logger::setup_with_default("solana=info");
|
solana_logger::setup();
|
||||||
solana_metrics::set_panic_hook("bench-tps");
|
solana_metrics::set_panic_hook("bench-tps");
|
||||||
|
|
||||||
let matches = cli::build_args(solana_clap_utils::version!()).get_matches();
|
let matches = cli::build_args().get_matches();
|
||||||
let cli_config = cli::extract_args(&matches);
|
|
||||||
|
let cfg = cli::extract_args(&matches);
|
||||||
|
|
||||||
let cli::Config {
|
let cli::Config {
|
||||||
entrypoint_addr,
|
network_addr: network,
|
||||||
faucet_addr,
|
drone_addr,
|
||||||
id,
|
id,
|
||||||
|
threads,
|
||||||
|
thread_batch_sleep_ms,
|
||||||
num_nodes,
|
num_nodes,
|
||||||
|
duration,
|
||||||
tx_count,
|
tx_count,
|
||||||
keypair_multiplier,
|
sustained,
|
||||||
client_ids_and_stake_file,
|
reject_extra_nodes,
|
||||||
write_to_client_file,
|
converge_only,
|
||||||
read_from_client_file,
|
} = cfg;
|
||||||
target_lamports_per_signature,
|
|
||||||
use_move,
|
|
||||||
multi_client,
|
|
||||||
num_lamports_per_account,
|
|
||||||
..
|
|
||||||
} = &cli_config;
|
|
||||||
|
|
||||||
let keypair_count = *tx_count * keypair_multiplier;
|
let nodes = discover(&network, num_nodes).unwrap_or_else(|err| {
|
||||||
if *write_to_client_file {
|
|
||||||
info!("Generating {} keypairs", keypair_count);
|
|
||||||
let (keypairs, _) = generate_keypairs(&id, keypair_count as u64);
|
|
||||||
let num_accounts = keypairs.len() as u64;
|
|
||||||
let max_fee =
|
|
||||||
FeeRateGovernor::new(*target_lamports_per_signature, 0).max_lamports_per_signature;
|
|
||||||
let num_lamports_per_account = (num_accounts - 1 + NUM_SIGNATURES_FOR_TXS * max_fee)
|
|
||||||
/ num_accounts
|
|
||||||
+ num_lamports_per_account;
|
|
||||||
let mut accounts = HashMap::new();
|
|
||||||
keypairs.iter().for_each(|keypair| {
|
|
||||||
accounts.insert(
|
|
||||||
serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(),
|
|
||||||
Base64Account {
|
|
||||||
balance: num_lamports_per_account,
|
|
||||||
executable: false,
|
|
||||||
owner: system_program::id().to_string(),
|
|
||||||
data: String::new(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
info!("Writing {}", client_ids_and_stake_file);
|
|
||||||
let serialized = serde_yaml::to_string(&accounts).unwrap();
|
|
||||||
let path = Path::new(&client_ids_and_stake_file);
|
|
||||||
let mut file = File::create(path).unwrap();
|
|
||||||
file.write_all(&serialized.into_bytes()).unwrap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Connecting to the cluster");
|
|
||||||
let (nodes, _archivers) =
|
|
||||||
discover_cluster(&entrypoint_addr, *num_nodes).unwrap_or_else(|err| {
|
|
||||||
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
||||||
exit(1);
|
exit(1);
|
||||||
});
|
});
|
||||||
|
if nodes.len() < num_nodes {
|
||||||
let client = if *multi_client {
|
|
||||||
let (client, num_clients) = get_multi_client(&nodes);
|
|
||||||
if nodes.len() < num_clients {
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Error: Insufficient nodes discovered. Expecting {} or more",
|
"Error: Insufficient nodes discovered. Expecting {} or more",
|
||||||
num_nodes
|
num_nodes
|
||||||
);
|
);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
Arc::new(client)
|
if reject_extra_nodes && nodes.len() > num_nodes {
|
||||||
} else {
|
|
||||||
Arc::new(get_client(&nodes))
|
|
||||||
};
|
|
||||||
|
|
||||||
let (keypairs, move_keypairs) = if *read_from_client_file && !use_move {
|
|
||||||
let path = Path::new(&client_ids_and_stake_file);
|
|
||||||
let file = File::open(path).unwrap();
|
|
||||||
|
|
||||||
info!("Reading {}", client_ids_and_stake_file);
|
|
||||||
let accounts: HashMap<String, Base64Account> = serde_yaml::from_reader(file).unwrap();
|
|
||||||
let mut keypairs = vec![];
|
|
||||||
let mut last_balance = 0;
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|(keypair, primordial_account)| {
|
|
||||||
let bytes: Vec<u8> = serde_json::from_str(keypair.as_str()).unwrap();
|
|
||||||
keypairs.push(Keypair::from_bytes(&bytes).unwrap());
|
|
||||||
last_balance = primordial_account.balance;
|
|
||||||
});
|
|
||||||
|
|
||||||
if keypairs.len() < keypair_count {
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Expected {} accounts in {}, only received {} (--tx_count mismatch?)",
|
"Error: Extra nodes discovered. Expecting exactly {}",
|
||||||
keypair_count,
|
num_nodes
|
||||||
client_ids_and_stake_file,
|
|
||||||
keypairs.len(),
|
|
||||||
);
|
);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
// Sort keypairs so that do_bench_tps() uses the same subset of accounts for each run.
|
|
||||||
// This prevents the amount of storage needed for bench-tps accounts from creeping up
|
|
||||||
// across multiple runs.
|
|
||||||
keypairs.sort_by(|x, y| x.pubkey().to_string().cmp(&y.pubkey().to_string()));
|
|
||||||
(keypairs, None)
|
|
||||||
} else {
|
|
||||||
generate_and_fund_keypairs(
|
|
||||||
client.clone(),
|
|
||||||
Some(*faucet_addr),
|
|
||||||
&id,
|
|
||||||
keypair_count,
|
|
||||||
*num_lamports_per_account,
|
|
||||||
*use_move,
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
eprintln!("Error could not fund keys: {:?}", e);
|
|
||||||
exit(1);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
do_bench_tps(client, cli_config, keypairs, move_keypairs);
|
if converge_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cluster_entrypoint = nodes[0].clone(); // Pick the first node, why not?
|
||||||
|
|
||||||
|
let mut client = create_client(cluster_entrypoint.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||||
|
let mut barrier_client =
|
||||||
|
create_client(cluster_entrypoint.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||||
|
|
||||||
|
let mut seed = [0u8; 32];
|
||||||
|
seed.copy_from_slice(&id.public_key_bytes()[..32]);
|
||||||
|
let mut rnd = GenKeys::new(seed);
|
||||||
|
|
||||||
|
println!("Creating {} keypairs...", tx_count * 2);
|
||||||
|
let mut total_keys = 0;
|
||||||
|
let mut target = tx_count * 2;
|
||||||
|
while target > 0 {
|
||||||
|
total_keys += target;
|
||||||
|
target /= MAX_SPENDS_PER_TX;
|
||||||
|
}
|
||||||
|
let gen_keypairs = rnd.gen_n_keypairs(total_keys as u64);
|
||||||
|
let barrier_source_keypair = Keypair::new();
|
||||||
|
let barrier_dest_id = Keypair::new().pubkey();
|
||||||
|
|
||||||
|
println!("Get lamports...");
|
||||||
|
let num_lamports_per_account = 20;
|
||||||
|
|
||||||
|
// Sample the first keypair, see if it has lamports, if so then resume
|
||||||
|
// to avoid lamport loss
|
||||||
|
let keypair0_balance = client
|
||||||
|
.poll_get_balance(&gen_keypairs.last().unwrap().pubkey())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if num_lamports_per_account > keypair0_balance {
|
||||||
|
let extra = num_lamports_per_account - keypair0_balance;
|
||||||
|
let total = extra * (gen_keypairs.len() as u64);
|
||||||
|
airdrop_lamports(&mut client, &drone_addr, &id, total);
|
||||||
|
println!("adding more lamports {}", extra);
|
||||||
|
fund_keys(&mut client, &id, &gen_keypairs, extra);
|
||||||
|
}
|
||||||
|
let start = gen_keypairs.len() - (tx_count * 2) as usize;
|
||||||
|
let keypairs = &gen_keypairs[start..];
|
||||||
|
airdrop_lamports(&mut barrier_client, &drone_addr, &barrier_source_keypair, 1);
|
||||||
|
|
||||||
|
println!("Get last ID...");
|
||||||
|
let mut blockhash = client.get_recent_blockhash();
|
||||||
|
println!("Got last ID {:?}", blockhash);
|
||||||
|
|
||||||
|
let first_tx_count = client.transaction_count();
|
||||||
|
println!("Initial transaction count {}", first_tx_count);
|
||||||
|
|
||||||
|
let exit_signal = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
// Setup a thread per validator to sample every period
|
||||||
|
// collect the max transaction rate and total tx count seen
|
||||||
|
let maxes = Arc::new(RwLock::new(Vec::new()));
|
||||||
|
let sample_period = 1; // in seconds
|
||||||
|
println!("Sampling TPS every {} second...", sample_period);
|
||||||
|
let v_threads: Vec<_> = nodes
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| {
|
||||||
|
let exit_signal = exit_signal.clone();
|
||||||
|
let maxes = maxes.clone();
|
||||||
|
Builder::new()
|
||||||
|
.name("solana-client-sample".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
sample_tx_count(&exit_signal, &maxes, first_tx_count, &v, sample_period);
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
|
||||||
|
|
||||||
|
let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0));
|
||||||
|
let total_tx_sent_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
|
let s_threads: Vec<_> = (0..threads)
|
||||||
|
.map(|_| {
|
||||||
|
let exit_signal = exit_signal.clone();
|
||||||
|
let shared_txs = shared_txs.clone();
|
||||||
|
let cluster_entrypoint = cluster_entrypoint.clone();
|
||||||
|
let shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
|
||||||
|
let total_tx_sent_count = total_tx_sent_count.clone();
|
||||||
|
Builder::new()
|
||||||
|
.name("solana-client-sender".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
do_tx_transfers(
|
||||||
|
&exit_signal,
|
||||||
|
&shared_txs,
|
||||||
|
&cluster_entrypoint,
|
||||||
|
&shared_tx_active_thread_count,
|
||||||
|
&total_tx_sent_count,
|
||||||
|
thread_batch_sleep_ms,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// generate and send transactions for the specified duration
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut reclaim_lamports_back_to_source_account = false;
|
||||||
|
let mut i = keypair0_balance;
|
||||||
|
while start.elapsed() < duration {
|
||||||
|
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
||||||
|
metrics_submit_lamport_balance(balance);
|
||||||
|
|
||||||
|
// ping-pong between source and destination accounts for each loop iteration
|
||||||
|
// this seems to be faster than trying to determine the balance of individual
|
||||||
|
// accounts
|
||||||
|
let len = tx_count as usize;
|
||||||
|
generate_txs(
|
||||||
|
&shared_txs,
|
||||||
|
&keypairs[..len],
|
||||||
|
&keypairs[len..],
|
||||||
|
threads,
|
||||||
|
reclaim_lamports_back_to_source_account,
|
||||||
|
&cluster_entrypoint,
|
||||||
|
);
|
||||||
|
// In sustained mode overlap the transfers with generation
|
||||||
|
// this has higher average performance but lower peak performance
|
||||||
|
// in tested environments.
|
||||||
|
if !sustained {
|
||||||
|
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
|
||||||
|
sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// It's not feasible (would take too much time) to confirm each of the `tx_count / 2`
|
||||||
|
// transactions sent by `generate_txs()` so instead send and confirm a single transaction
|
||||||
|
// to validate the network is still functional.
|
||||||
|
send_barrier_transaction(
|
||||||
|
&mut barrier_client,
|
||||||
|
&mut blockhash,
|
||||||
|
&barrier_source_keypair,
|
||||||
|
&barrier_dest_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
if should_switch_directions(num_lamports_per_account, i) {
|
||||||
|
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the sampling threads so it will collect the stats
|
||||||
|
exit_signal.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
|
println!("Waiting for validator threads...");
|
||||||
|
for t in v_threads {
|
||||||
|
if let Err(err) = t.join() {
|
||||||
|
println!(" join() failed with: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// join the tx send threads
|
||||||
|
println!("Waiting for transmit threads...");
|
||||||
|
for t in s_threads {
|
||||||
|
if let Err(err) = t.join() {
|
||||||
|
println!(" join() failed with: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
|
||||||
|
metrics_submit_lamport_balance(balance);
|
||||||
|
|
||||||
|
compute_and_report_stats(
|
||||||
|
&maxes,
|
||||||
|
sample_period,
|
||||||
|
&start.elapsed(),
|
||||||
|
total_tx_sent_count.load(Ordering::Relaxed),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_switch_directions() {
|
||||||
|
assert_eq!(should_switch_directions(20, 0), false);
|
||||||
|
assert_eq!(should_switch_directions(20, 1), false);
|
||||||
|
assert_eq!(should_switch_directions(20, 14), false);
|
||||||
|
assert_eq!(should_switch_directions(20, 15), true);
|
||||||
|
assert_eq!(should_switch_directions(20, 16), false);
|
||||||
|
assert_eq!(should_switch_directions(20, 19), false);
|
||||||
|
assert_eq!(should_switch_directions(20, 20), true);
|
||||||
|
assert_eq!(should_switch_directions(20, 21), false);
|
||||||
|
assert_eq!(should_switch_directions(20, 99), false);
|
||||||
|
assert_eq!(should_switch_directions(20, 100), true);
|
||||||
|
assert_eq!(should_switch_directions(20, 101), false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,86 +0,0 @@
|
|||||||
use serial_test_derive::serial;
|
|
||||||
use solana_bench_tps::bench::{do_bench_tps, generate_and_fund_keypairs};
|
|
||||||
use solana_bench_tps::cli::Config;
|
|
||||||
use solana_client::thin_client::create_client;
|
|
||||||
use solana_core::cluster_info::VALIDATOR_PORT_RANGE;
|
|
||||||
use solana_core::validator::ValidatorConfig;
|
|
||||||
use solana_faucet::faucet::run_local_faucet;
|
|
||||||
use solana_local_cluster::local_cluster::{ClusterConfig, LocalCluster};
|
|
||||||
#[cfg(feature = "move")]
|
|
||||||
use solana_sdk::move_loader::solana_move_loader_program;
|
|
||||||
use solana_sdk::signature::{Keypair, Signer};
|
|
||||||
use std::sync::{mpsc::channel, Arc};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
fn test_bench_tps_local_cluster(config: Config) {
|
|
||||||
#[cfg(feature = "move")]
|
|
||||||
let native_instruction_processors = vec![solana_move_loader_program()];
|
|
||||||
|
|
||||||
#[cfg(not(feature = "move"))]
|
|
||||||
let native_instruction_processors = vec![];
|
|
||||||
|
|
||||||
solana_logger::setup();
|
|
||||||
const NUM_NODES: usize = 1;
|
|
||||||
let cluster = LocalCluster::new(&ClusterConfig {
|
|
||||||
node_stakes: vec![999_990; NUM_NODES],
|
|
||||||
cluster_lamports: 200_000_000,
|
|
||||||
validator_configs: vec![ValidatorConfig::default(); NUM_NODES],
|
|
||||||
native_instruction_processors,
|
|
||||||
..ClusterConfig::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let faucet_keypair = Keypair::new();
|
|
||||||
cluster.transfer(
|
|
||||||
&cluster.funding_keypair,
|
|
||||||
&faucet_keypair.pubkey(),
|
|
||||||
100_000_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
let client = Arc::new(create_client(
|
|
||||||
(cluster.entry_point_info.rpc, cluster.entry_point_info.tpu),
|
|
||||||
VALIDATOR_PORT_RANGE,
|
|
||||||
));
|
|
||||||
|
|
||||||
let (addr_sender, addr_receiver) = channel();
|
|
||||||
run_local_faucet(faucet_keypair, addr_sender, None);
|
|
||||||
let faucet_addr = addr_receiver.recv_timeout(Duration::from_secs(2)).unwrap();
|
|
||||||
|
|
||||||
let lamports_per_account = 100;
|
|
||||||
|
|
||||||
let keypair_count = config.tx_count * config.keypair_multiplier;
|
|
||||||
let (keypairs, move_keypairs) = generate_and_fund_keypairs(
|
|
||||||
client.clone(),
|
|
||||||
Some(faucet_addr),
|
|
||||||
&config.id,
|
|
||||||
keypair_count,
|
|
||||||
lamports_per_account,
|
|
||||||
config.use_move,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let _total = do_bench_tps(client, config, keypairs, move_keypairs);
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
assert!(_total > 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_bench_tps_local_cluster_solana() {
|
|
||||||
let mut config = Config::default();
|
|
||||||
config.tx_count = 100;
|
|
||||||
config.duration = Duration::from_secs(10);
|
|
||||||
|
|
||||||
test_bench_tps_local_cluster(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_bench_tps_local_cluster_move() {
|
|
||||||
let mut config = Config::default();
|
|
||||||
config.tx_count = 100;
|
|
||||||
config.duration = Duration::from_secs(10);
|
|
||||||
config.use_move = true;
|
|
||||||
|
|
||||||
test_bench_tps_local_cluster(config);
|
|
||||||
}
|
|
248
benches/append_vec.rs
Normal file
248
benches/append_vec.rs
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate rand;
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use bincode::{deserialize, serialize_into, serialized_size};
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
use solana_runtime::append_vec::{
|
||||||
|
deserialize_account, get_serialized_size, serialize_account, AppendVec,
|
||||||
|
};
|
||||||
|
use solana_sdk::account::Account;
|
||||||
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
|
use std::env;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread::spawn;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
const START_SIZE: u64 = 4 * 1024 * 1024;
|
||||||
|
const INC_SIZE: u64 = 1 * 1024 * 1024;
|
||||||
|
|
||||||
|
macro_rules! align_up {
|
||||||
|
($addr: expr, $align: expr) => {
|
||||||
|
($addr + ($align - 1)) & !($align - 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_append_vec_bench_path(path: &str) -> PathBuf {
|
||||||
|
let out_dir = env::var("OUT_DIR").unwrap_or_else(|_| "target".to_string());
|
||||||
|
let mut buf = PathBuf::new();
|
||||||
|
buf.push(&format!("{}/{}", out_dir, path));
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn append_vec_atomic_append(bencher: &mut Bencher) {
|
||||||
|
let path = get_append_vec_bench_path("bench_append");
|
||||||
|
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||||
|
bencher.iter(|| {
|
||||||
|
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||||
|
assert!(vec.grow_file().is_ok());
|
||||||
|
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn append_vec_atomic_random_access(bencher: &mut Bencher) {
|
||||||
|
let path = get_append_vec_bench_path("bench_ra");
|
||||||
|
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||||
|
let size = 1_000_000;
|
||||||
|
for _ in 0..size {
|
||||||
|
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||||
|
assert!(vec.grow_file().is_ok());
|
||||||
|
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bencher.iter(|| {
|
||||||
|
let index = thread_rng().gen_range(0, size as u64);
|
||||||
|
vec.get(index * std::mem::size_of::<AtomicUsize>() as u64);
|
||||||
|
});
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn append_vec_atomic_random_change(bencher: &mut Bencher) {
|
||||||
|
let path = get_append_vec_bench_path("bench_rax");
|
||||||
|
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||||
|
let size = 1_000_000;
|
||||||
|
for k in 0..size {
|
||||||
|
if vec.append(AtomicUsize::new(k)).is_none() {
|
||||||
|
assert!(vec.grow_file().is_ok());
|
||||||
|
assert!(vec.append(AtomicUsize::new(k)).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bencher.iter(|| {
|
||||||
|
let index = thread_rng().gen_range(0, size as u64);
|
||||||
|
let atomic1 = vec.get(index * std::mem::size_of::<AtomicUsize>() as u64);
|
||||||
|
let current1 = atomic1.load(Ordering::Relaxed);
|
||||||
|
assert_eq!(current1, index as usize);
|
||||||
|
let next = current1 + 1;
|
||||||
|
let mut index = vec.append(AtomicUsize::new(next));
|
||||||
|
if index.is_none() {
|
||||||
|
assert!(vec.grow_file().is_ok());
|
||||||
|
index = vec.append(AtomicUsize::new(next));
|
||||||
|
}
|
||||||
|
let atomic2 = vec.get(index.unwrap());
|
||||||
|
let current2 = atomic2.load(Ordering::Relaxed);
|
||||||
|
assert_eq!(current2, next);
|
||||||
|
});
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn append_vec_atomic_random_read(bencher: &mut Bencher) {
|
||||||
|
let path = get_append_vec_bench_path("bench_read");
|
||||||
|
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
|
||||||
|
let size = 1_000_000;
|
||||||
|
for _ in 0..size {
|
||||||
|
if vec.append(AtomicUsize::new(0)).is_none() {
|
||||||
|
assert!(vec.grow_file().is_ok());
|
||||||
|
assert!(vec.append(AtomicUsize::new(0)).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bencher.iter(|| {
|
||||||
|
let index = thread_rng().gen_range(0, size);
|
||||||
|
let atomic1 = vec.get((index * std::mem::size_of::<AtomicUsize>()) as u64);
|
||||||
|
let current1 = atomic1.load(Ordering::Relaxed);
|
||||||
|
assert_eq!(current1, 0);
|
||||||
|
});
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn append_vec_concurrent_lock_append(bencher: &mut Bencher) {
|
||||||
|
let path = get_append_vec_bench_path("bench_lock_append");
|
||||||
|
let vec = Arc::new(RwLock::new(AppendVec::<AtomicUsize>::new(
|
||||||
|
&path, true, START_SIZE, INC_SIZE,
|
||||||
|
)));
|
||||||
|
let vec1 = vec.clone();
|
||||||
|
let size = 1_000_000;
|
||||||
|
let count = Arc::new(AtomicUsize::new(0));
|
||||||
|
let count1 = count.clone();
|
||||||
|
spawn(move || loop {
|
||||||
|
let mut len = count.load(Ordering::Relaxed);
|
||||||
|
{
|
||||||
|
let rlock = vec1.read().unwrap();
|
||||||
|
loop {
|
||||||
|
if rlock.append(AtomicUsize::new(0)).is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
len = count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if len >= size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut wlock = vec1.write().unwrap();
|
||||||
|
if len >= size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert!(wlock.grow_file().is_ok());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bencher.iter(|| {
|
||||||
|
let _rlock = vec.read().unwrap();
|
||||||
|
let len = count1.load(Ordering::Relaxed);
|
||||||
|
assert!(len < size * 2);
|
||||||
|
});
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn append_vec_concurrent_get_append(bencher: &mut Bencher) {
|
||||||
|
let path = get_append_vec_bench_path("bench_get_append");
|
||||||
|
let vec = Arc::new(RwLock::new(AppendVec::<AtomicUsize>::new(
|
||||||
|
&path, true, START_SIZE, INC_SIZE,
|
||||||
|
)));
|
||||||
|
let vec1 = vec.clone();
|
||||||
|
let size = 1_000_000;
|
||||||
|
let count = Arc::new(AtomicUsize::new(0));
|
||||||
|
let count1 = count.clone();
|
||||||
|
spawn(move || loop {
|
||||||
|
let mut len = count.load(Ordering::Relaxed);
|
||||||
|
{
|
||||||
|
let rlock = vec1.read().unwrap();
|
||||||
|
loop {
|
||||||
|
if rlock.append(AtomicUsize::new(0)).is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
len = count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if len >= size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut wlock = vec1.write().unwrap();
|
||||||
|
if len >= size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert!(wlock.grow_file().is_ok());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bencher.iter(|| {
|
||||||
|
let rlock = vec.read().unwrap();
|
||||||
|
let len = count1.load(Ordering::Relaxed);
|
||||||
|
if len > 0 {
|
||||||
|
let index = thread_rng().gen_range(0, len);
|
||||||
|
rlock.get((index * std::mem::size_of::<AtomicUsize>()) as u64);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn bench_account_serialize(bencher: &mut Bencher) {
|
||||||
|
let num: usize = 1000;
|
||||||
|
let account = Account::new(2, 100, &Keypair::new().pubkey());
|
||||||
|
let len = get_serialized_size(&account);
|
||||||
|
let ser_len = align_up!(len + std::mem::size_of::<u64>(), std::mem::size_of::<u64>());
|
||||||
|
let mut memory = vec![0; num * ser_len];
|
||||||
|
bencher.iter(|| {
|
||||||
|
for i in 0..num {
|
||||||
|
let start = i * ser_len;
|
||||||
|
serialize_account(&mut memory[start..start + ser_len], &account, len);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// make sure compiler doesn't delete the code.
|
||||||
|
let index = thread_rng().gen_range(0, num);
|
||||||
|
if memory[index] != 0 {
|
||||||
|
println!("memory: {}", memory[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = index * ser_len;
|
||||||
|
let new_account = deserialize_account(&memory[start..start + ser_len], 0, num * len).unwrap();
|
||||||
|
assert_eq!(new_account, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn bench_account_serialize_bincode(bencher: &mut Bencher) {
|
||||||
|
let num: usize = 1000;
|
||||||
|
let account = Account::new(2, 100, &Keypair::new().pubkey());
|
||||||
|
let len = serialized_size(&account).unwrap() as usize;
|
||||||
|
let mut memory = vec![0u8; num * len];
|
||||||
|
bencher.iter(|| {
|
||||||
|
for i in 0..num {
|
||||||
|
let start = i * len;
|
||||||
|
let cursor = Cursor::new(&mut memory[start..start + len]);
|
||||||
|
serialize_into(cursor, &account).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// make sure compiler doesn't delete the code.
|
||||||
|
let index = thread_rng().gen_range(0, len);
|
||||||
|
if memory[index] != 0 {
|
||||||
|
println!("memory: {}", memory[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = index * len;
|
||||||
|
let new_account: Account = deserialize(&memory[start..start + len]).unwrap();
|
||||||
|
assert_eq!(new_account, account);
|
||||||
|
}
|
241
benches/banking_stage.rs
Normal file
241
benches/banking_stage.rs
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use solana::banking_stage::{create_test_recorder, BankingStage};
|
||||||
|
use solana::cluster_info::ClusterInfo;
|
||||||
|
use solana::cluster_info::Node;
|
||||||
|
use solana::packet::to_packets_chunked;
|
||||||
|
use solana::poh_recorder::WorkingBankEntries;
|
||||||
|
use solana::service::Service;
|
||||||
|
use solana_runtime::bank::Bank;
|
||||||
|
use solana_sdk::genesis_block::GenesisBlock;
|
||||||
|
use solana_sdk::hash::hash;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
use solana_sdk::signature::{KeypairUtil, Signature};
|
||||||
|
use solana_sdk::system_transaction::SystemTransaction;
|
||||||
|
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, MAX_RECENT_BLOCKHASHES};
|
||||||
|
use std::iter;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::mpsc::{channel, Receiver};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
fn check_txs(receiver: &Receiver<WorkingBankEntries>, ref_tx_count: usize) {
|
||||||
|
let mut total = 0;
|
||||||
|
loop {
|
||||||
|
let entries = receiver.recv_timeout(Duration::new(1, 0));
|
||||||
|
if let Ok((_, entries)) = entries {
|
||||||
|
for (entry, _) in &entries {
|
||||||
|
total += entry.transactions.len();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if total >= ref_tx_count {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(total, ref_tx_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||||
|
let num_threads = 4;
|
||||||
|
// a multiple of packet chunk 2X duplicates to avoid races
|
||||||
|
let txes = 192 * 50 * num_threads * 2;
|
||||||
|
let mint_total = 1_000_000_000_000;
|
||||||
|
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
||||||
|
|
||||||
|
let (verified_sender, verified_receiver) = channel();
|
||||||
|
let bank = Arc::new(Bank::new(&genesis_block));
|
||||||
|
let dummy = SystemTransaction::new_move(
|
||||||
|
&mint_keypair,
|
||||||
|
&mint_keypair.pubkey(),
|
||||||
|
1,
|
||||||
|
genesis_block.hash(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
let transactions: Vec<_> = (0..txes)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|_| {
|
||||||
|
let mut new = dummy.clone();
|
||||||
|
let from: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||||
|
let to: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||||
|
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||||
|
new.account_keys[0] = Pubkey::new(&from[0..32]);
|
||||||
|
new.account_keys[1] = Pubkey::new(&to[0..32]);
|
||||||
|
new.signatures = vec![Signature::new(&sig[0..64])];
|
||||||
|
new
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// fund all the accounts
|
||||||
|
transactions.iter().for_each(|tx| {
|
||||||
|
let fund = SystemTransaction::new_move(
|
||||||
|
&mint_keypair,
|
||||||
|
&tx.account_keys[0],
|
||||||
|
mint_total / txes as u64,
|
||||||
|
genesis_block.hash(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
let x = bank.process_transaction(&fund);
|
||||||
|
x.unwrap();
|
||||||
|
});
|
||||||
|
//sanity check, make sure all the transactions can execute sequentially
|
||||||
|
transactions.iter().for_each(|tx| {
|
||||||
|
let res = bank.process_transaction(&tx);
|
||||||
|
assert!(res.is_ok(), "sanity test transactions");
|
||||||
|
});
|
||||||
|
bank.clear_signatures();
|
||||||
|
//sanity check, make sure all the transactions can execute in parallel
|
||||||
|
let res = bank.process_transactions(&transactions);
|
||||||
|
for r in res {
|
||||||
|
assert!(r.is_ok(), "sanity parallel execution");
|
||||||
|
}
|
||||||
|
bank.clear_signatures();
|
||||||
|
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 192)
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| {
|
||||||
|
let len = x.read().unwrap().packets.len();
|
||||||
|
(x, iter::repeat(1).take(len).collect())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(&bank);
|
||||||
|
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||||
|
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||||
|
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
|
||||||
|
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||||
|
|
||||||
|
let mut id = genesis_block.hash();
|
||||||
|
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
|
||||||
|
id = hash(&id.as_ref());
|
||||||
|
bank.register_tick(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let half_len = verified.len() / 2;
|
||||||
|
let mut start = 0;
|
||||||
|
bencher.iter(move || {
|
||||||
|
// make sure the transactions are still valid
|
||||||
|
bank.register_tick(&genesis_block.hash());
|
||||||
|
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||||
|
verified_sender.send(v.to_vec()).unwrap();
|
||||||
|
}
|
||||||
|
check_txs(&signal_receiver, txes / 2);
|
||||||
|
bank.clear_signatures();
|
||||||
|
start += half_len;
|
||||||
|
start %= verified.len();
|
||||||
|
});
|
||||||
|
exit.store(true, Ordering::Relaxed);
|
||||||
|
poh_service.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||||
|
let progs = 4;
|
||||||
|
let num_threads = 4;
|
||||||
|
// a multiple of packet chunk 2X duplicates to avoid races
|
||||||
|
let txes = 96 * 100 * num_threads * 2;
|
||||||
|
let mint_total = 1_000_000_000_000;
|
||||||
|
let (genesis_block, mint_keypair) = GenesisBlock::new(mint_total);
|
||||||
|
|
||||||
|
let (verified_sender, verified_receiver) = channel();
|
||||||
|
let bank = Arc::new(Bank::new(&genesis_block));
|
||||||
|
let dummy = SystemTransaction::new_move(
|
||||||
|
&mint_keypair,
|
||||||
|
&mint_keypair.pubkey(),
|
||||||
|
1,
|
||||||
|
genesis_block.hash(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
let transactions: Vec<_> = (0..txes)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|_| {
|
||||||
|
let mut new = dummy.clone();
|
||||||
|
let from: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||||
|
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||||
|
let to: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||||
|
new.account_keys[0] = Pubkey::new(&from[0..32]);
|
||||||
|
new.account_keys[1] = Pubkey::new(&to[0..32]);
|
||||||
|
let prog = new.instructions[0].clone();
|
||||||
|
for i in 1..progs {
|
||||||
|
//generate programs that spend to random keys
|
||||||
|
let to: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
|
||||||
|
let to_key = Pubkey::new(&to[0..32]);
|
||||||
|
new.account_keys.push(to_key);
|
||||||
|
assert_eq!(new.account_keys.len(), i + 2);
|
||||||
|
new.instructions.push(prog.clone());
|
||||||
|
assert_eq!(new.instructions.len(), i + 1);
|
||||||
|
new.instructions[i].accounts[1] = 1 + i as u8;
|
||||||
|
assert_eq!(new.key(i, 1), Some(&to_key));
|
||||||
|
assert_eq!(
|
||||||
|
new.account_keys[new.instructions[i].accounts[1] as usize],
|
||||||
|
to_key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(new.instructions.len(), progs);
|
||||||
|
new.signatures = vec![Signature::new(&sig[0..64])];
|
||||||
|
new
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
transactions.iter().for_each(|tx| {
|
||||||
|
let fund = SystemTransaction::new_move(
|
||||||
|
&mint_keypair,
|
||||||
|
&tx.account_keys[0],
|
||||||
|
mint_total / txes as u64,
|
||||||
|
genesis_block.hash(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
bank.process_transaction(&fund).unwrap();
|
||||||
|
});
|
||||||
|
//sanity check, make sure all the transactions can execute sequentially
|
||||||
|
transactions.iter().for_each(|tx| {
|
||||||
|
let res = bank.process_transaction(&tx);
|
||||||
|
assert!(res.is_ok(), "sanity test transactions");
|
||||||
|
});
|
||||||
|
bank.clear_signatures();
|
||||||
|
//sanity check, make sure all the transactions can execute in parallel
|
||||||
|
let res = bank.process_transactions(&transactions);
|
||||||
|
for r in res {
|
||||||
|
assert!(r.is_ok(), "sanity parallel execution");
|
||||||
|
}
|
||||||
|
bank.clear_signatures();
|
||||||
|
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 96)
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| {
|
||||||
|
let len = x.read().unwrap().packets.len();
|
||||||
|
(x, iter::repeat(1).take(len).collect())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(&bank);
|
||||||
|
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
|
||||||
|
let cluster_info = Arc::new(RwLock::new(cluster_info));
|
||||||
|
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
|
||||||
|
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||||
|
|
||||||
|
let mut id = genesis_block.hash();
|
||||||
|
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
|
||||||
|
id = hash(&id.as_ref());
|
||||||
|
bank.register_tick(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let half_len = verified.len() / 2;
|
||||||
|
let mut start = 0;
|
||||||
|
bencher.iter(move || {
|
||||||
|
// make sure the transactions are still valid
|
||||||
|
bank.register_tick(&genesis_block.hash());
|
||||||
|
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
|
||||||
|
verified_sender.send(v.to_vec()).unwrap();
|
||||||
|
}
|
||||||
|
check_txs(&signal_receiver, txes / 2);
|
||||||
|
bank.clear_signatures();
|
||||||
|
start += half_len;
|
||||||
|
start %= verified.len();
|
||||||
|
});
|
||||||
|
exit.store(true, Ordering::Relaxed);
|
||||||
|
poh_service.join().unwrap();
|
||||||
|
}
|
194
benches/blocktree.rs
Normal file
194
benches/blocktree.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
use rand;
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate solana;
|
||||||
|
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
use solana::blocktree::{get_tmp_ledger_path, Blocktree};
|
||||||
|
use solana::entry::{make_large_test_entries, make_tiny_test_entries, EntrySlice};
|
||||||
|
use solana::packet::{Blob, BLOB_HEADER_SIZE};
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
// Given some blobs and a ledger at ledger_path, benchmark writing the blobs to the ledger
|
||||||
|
fn bench_write_blobs(bench: &mut Bencher, blobs: &mut Vec<Blob>, ledger_path: &str) {
|
||||||
|
let blocktree =
|
||||||
|
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||||
|
|
||||||
|
let num_blobs = blobs.len();
|
||||||
|
|
||||||
|
bench.iter(move || {
|
||||||
|
for blob in blobs.iter_mut() {
|
||||||
|
let index = blob.index();
|
||||||
|
|
||||||
|
blocktree
|
||||||
|
.put_data_blob_bytes(
|
||||||
|
blob.slot(),
|
||||||
|
index,
|
||||||
|
&blob.data[..BLOB_HEADER_SIZE + blob.size()],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
blob.set_index(index + num_blobs as u64);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert some blobs into the ledger in preparation for read benchmarks
|
||||||
|
fn setup_read_bench(
|
||||||
|
blocktree: &mut Blocktree,
|
||||||
|
num_small_blobs: u64,
|
||||||
|
num_large_blobs: u64,
|
||||||
|
slot: u64,
|
||||||
|
) {
|
||||||
|
// Make some big and small entries
|
||||||
|
let mut entries = make_large_test_entries(num_large_blobs as usize);
|
||||||
|
entries.extend(make_tiny_test_entries(num_small_blobs as usize));
|
||||||
|
|
||||||
|
// Convert the entries to blobs, write the blobs to the ledger
|
||||||
|
let mut blobs = entries.to_blobs();
|
||||||
|
for (index, b) in blobs.iter_mut().enumerate() {
|
||||||
|
b.set_index(index as u64);
|
||||||
|
b.set_slot(slot);
|
||||||
|
}
|
||||||
|
blocktree
|
||||||
|
.write_blobs(&blobs)
|
||||||
|
.expect("Expectd successful insertion of blobs into ledger");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write small blobs to the ledger
|
||||||
|
#[bench]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_write_small(bench: &mut Bencher) {
|
||||||
|
let ledger_path = get_tmp_ledger_path!();
|
||||||
|
let num_entries = 32 * 1024;
|
||||||
|
let entries = make_tiny_test_entries(num_entries);
|
||||||
|
let mut blobs = entries.to_blobs();
|
||||||
|
for (index, b) in blobs.iter_mut().enumerate() {
|
||||||
|
b.set_index(index as u64);
|
||||||
|
}
|
||||||
|
bench_write_blobs(bench, &mut blobs, &ledger_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write big blobs to the ledger
|
||||||
|
#[bench]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_write_big(bench: &mut Bencher) {
|
||||||
|
let ledger_path = get_tmp_ledger_path!();
|
||||||
|
let num_entries = 32 * 1024;
|
||||||
|
let entries = make_large_test_entries(num_entries);
|
||||||
|
let mut blobs = entries.to_blobs();
|
||||||
|
for (index, b) in blobs.iter_mut().enumerate() {
|
||||||
|
b.set_index(index as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
bench_write_blobs(bench, &mut blobs, &ledger_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_read_sequential(bench: &mut Bencher) {
|
||||||
|
let ledger_path = get_tmp_ledger_path!();
|
||||||
|
let mut blocktree =
|
||||||
|
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||||
|
|
||||||
|
// Insert some big and small blobs into the ledger
|
||||||
|
let num_small_blobs = 32 * 1024;
|
||||||
|
let num_large_blobs = 32 * 1024;
|
||||||
|
let total_blobs = num_small_blobs + num_large_blobs;
|
||||||
|
let slot = 0;
|
||||||
|
setup_read_bench(&mut blocktree, num_small_blobs, num_large_blobs, slot);
|
||||||
|
|
||||||
|
let num_reads = total_blobs / 15;
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
bench.iter(move || {
|
||||||
|
// Generate random starting point in the range [0, total_blobs - 1], read num_reads blobs sequentially
|
||||||
|
let start_index = rng.gen_range(0, num_small_blobs + num_large_blobs);
|
||||||
|
for i in start_index..start_index + num_reads {
|
||||||
|
let _ = blocktree.get_data_blob(slot, i as u64 % total_blobs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_read_random(bench: &mut Bencher) {
|
||||||
|
let ledger_path = get_tmp_ledger_path!();
|
||||||
|
let mut blocktree =
|
||||||
|
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||||
|
|
||||||
|
// Insert some big and small blobs into the ledger
|
||||||
|
let num_small_blobs = 32 * 1024;
|
||||||
|
let num_large_blobs = 32 * 1024;
|
||||||
|
let total_blobs = num_small_blobs + num_large_blobs;
|
||||||
|
let slot = 0;
|
||||||
|
setup_read_bench(&mut blocktree, num_small_blobs, num_large_blobs, slot);
|
||||||
|
|
||||||
|
let num_reads = total_blobs / 15;
|
||||||
|
|
||||||
|
// Generate a num_reads sized random sample of indexes in range [0, total_blobs - 1],
|
||||||
|
// simulating random reads
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let indexes: Vec<usize> = (0..num_reads)
|
||||||
|
.map(|_| rng.gen_range(0, total_blobs) as usize)
|
||||||
|
.collect();
|
||||||
|
bench.iter(move || {
|
||||||
|
for i in indexes.iter() {
|
||||||
|
let _ = blocktree.get_data_blob(slot, *i as u64);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_insert_data_blob_small(bench: &mut Bencher) {
|
||||||
|
let ledger_path = get_tmp_ledger_path!();
|
||||||
|
let blocktree =
|
||||||
|
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||||
|
let num_entries = 32 * 1024;
|
||||||
|
let entries = make_tiny_test_entries(num_entries);
|
||||||
|
let mut blobs = entries.to_blobs();
|
||||||
|
|
||||||
|
blobs.shuffle(&mut thread_rng());
|
||||||
|
|
||||||
|
bench.iter(move || {
|
||||||
|
for blob in blobs.iter_mut() {
|
||||||
|
let index = blob.index();
|
||||||
|
blob.set_index(index + num_entries as u64);
|
||||||
|
}
|
||||||
|
blocktree.write_blobs(&blobs).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_insert_data_blob_big(bench: &mut Bencher) {
|
||||||
|
let ledger_path = get_tmp_ledger_path!();
|
||||||
|
let blocktree =
|
||||||
|
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger");
|
||||||
|
let num_entries = 32 * 1024;
|
||||||
|
let entries = make_large_test_entries(num_entries);
|
||||||
|
let mut shared_blobs = entries.to_shared_blobs();
|
||||||
|
shared_blobs.shuffle(&mut thread_rng());
|
||||||
|
|
||||||
|
bench.iter(move || {
|
||||||
|
for blob in shared_blobs.iter_mut() {
|
||||||
|
let index = blob.read().unwrap().index();
|
||||||
|
blocktree.write_shared_blobs(vec![blob.clone()]).unwrap();
|
||||||
|
blob.write().unwrap().set_index(index + num_entries as u64);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
|
||||||
|
}
|
@@ -1,9 +1,9 @@
|
|||||||
//#![feature(test)]
|
//#![feature(test)]
|
||||||
//
|
//
|
||||||
//extern crate solana_core;
|
//extern crate solana;
|
||||||
//extern crate test;
|
//extern crate test;
|
||||||
//
|
//
|
||||||
//use solana_core::chacha::chacha_cbc_encrypt_files;
|
//use solana::chacha::chacha_cbc_encrypt_files;
|
||||||
//use std::fs::remove_file;
|
//use std::fs::remove_file;
|
||||||
//use std::fs::File;
|
//use std::fs::File;
|
||||||
//use std::io::Write;
|
//use std::io::Write;
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
use solana_core::gen_keys::GenKeys;
|
use solana::gen_keys::GenKeys;
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
24
benches/ledger.rs
Normal file
24
benches/ledger.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use solana::entry::{next_entries, reconstruct_entries_from_blobs, EntrySlice};
|
||||||
|
use solana_sdk::hash::{hash, Hash};
|
||||||
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
|
use solana_sdk::system_transaction::SystemTransaction;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn bench_block_to_blobs_to_block(bencher: &mut Bencher) {
|
||||||
|
let zero = Hash::default();
|
||||||
|
let one = hash(&zero.as_ref());
|
||||||
|
let keypair = Keypair::new();
|
||||||
|
let tx0 = SystemTransaction::new_move(&keypair, &keypair.pubkey(), 1, one, 0);
|
||||||
|
let transactions = vec![tx0; 10];
|
||||||
|
let entries = next_entries(&zero, 1, transactions);
|
||||||
|
|
||||||
|
bencher.iter(|| {
|
||||||
|
let blobs = entries.to_blobs();
|
||||||
|
assert_eq!(reconstruct_entries_from_blobs(blobs).unwrap().0, entries);
|
||||||
|
});
|
||||||
|
}
|
21
benches/sigverify.rs
Normal file
21
benches/sigverify.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use solana::packet::to_packets;
|
||||||
|
use solana::sigverify;
|
||||||
|
use solana::test_tx::test_tx;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn bench_sigverify(bencher: &mut Bencher) {
|
||||||
|
let tx = test_tx();
|
||||||
|
|
||||||
|
// generate packet vector
|
||||||
|
let batches = to_packets(&vec![tx; 128]);
|
||||||
|
|
||||||
|
// verify packets
|
||||||
|
bencher.iter(|| {
|
||||||
|
let _ans = sigverify::ed25519_verify(&batches);
|
||||||
|
})
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
Building the Solana book
|
Building the Solana book
|
||||||
---
|
---
|
||||||
|
|
||||||
Install dependencies, build, and test the docs:
|
Install the book's dependnecies, build, and test the book:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ./build.sh
|
$ ./build.sh
|
||||||
@@ -19,7 +19,7 @@ Render markdown as HTML:
|
|||||||
$ make build
|
$ make build
|
||||||
```
|
```
|
||||||
|
|
||||||
Render and view the docs:
|
Render and view the book:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ make open
|
$ make open
|
15
book/art/consensus.msc
Normal file
15
book/art/consensus.msc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
msc {
|
||||||
|
client,leader,verifier_a,verifier_b,verifier_c;
|
||||||
|
|
||||||
|
client=>leader [ label = "SUBMIT" ] ;
|
||||||
|
leader=>client [ label = "CONFIRMED" ] ;
|
||||||
|
leader=>verifier_a [ label = "CONFIRMED" ] ;
|
||||||
|
leader=>verifier_b [ label = "CONFIRMED" ] ;
|
||||||
|
leader=>verifier_c [ label = "CONFIRMED" ] ;
|
||||||
|
verifier_a=>leader [ label = "VERIFIED" ] ;
|
||||||
|
verifier_b=>leader [ label = "VERIFIED" ] ;
|
||||||
|
leader=>client [ label = "FINALIZED" ] ;
|
||||||
|
leader=>verifier_a [ label = "FINALIZED" ] ;
|
||||||
|
leader=>verifier_b [ label = "FINALIZED" ] ;
|
||||||
|
leader=>verifier_c [ label = "FINALIZED" ] ;
|
||||||
|
}
|
28
book/art/data-plane.bob
Normal file
28
book/art/data-plane.bob
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
+--------------+
|
||||||
|
| |
|
||||||
|
+------------+ Leader +------------+
|
||||||
|
| | | |
|
||||||
|
| +--------------+ |
|
||||||
|
v v
|
||||||
|
+--------+--------+ +--------+--------+
|
||||||
|
| +--------------------->+ |
|
||||||
|
+-----------------+ Validator 1 | | Validator 2 +-------------+
|
||||||
|
| | +<---------------------+ | |
|
||||||
|
| +------+-+-+------+ +---+-+-+---------+ |
|
||||||
|
| | | | | | | |
|
||||||
|
| | | | | | | |
|
||||||
|
| +---------------------------------------------+ | | |
|
||||||
|
| | | | | | | |
|
||||||
|
| | | | | +----------------------+ | |
|
||||||
|
| | | | | | | |
|
||||||
|
| | | | +--------------------------------------------+ |
|
||||||
|
| | | | | | | |
|
||||||
|
| | | +----------------------+ | | |
|
||||||
|
| | | | | | | |
|
||||||
|
v v v v v v v v
|
||||||
|
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
||||||
|
| | | | | | | |
|
||||||
|
| Neighborhood 1 | | Neighborhood 2 | | Neighborhood 3 | | Neighborhood 4 |
|
||||||
|
| | | | | | | |
|
||||||
|
+--------------------+ +--------------------+ +--------------------+ +--------------------+
|
30
book/art/fullnode.bob
Normal file
30
book/art/fullnode.bob
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.--------------------------------------.
|
||||||
|
| Fullnode |
|
||||||
|
| |
|
||||||
|
.--------. | .-------------------. |
|
||||||
|
| |---->| | |
|
||||||
|
| Client | | | JSON RPC Service | |
|
||||||
|
| |<----| | |
|
||||||
|
`----+---` | `-------------------` |
|
||||||
|
| | ^ |
|
||||||
|
| | | .----------------. | .------------------.
|
||||||
|
| | | | Gossip Service |<----------| Validators |
|
||||||
|
| | | `----------------` | | |
|
||||||
|
| | | ^ | | |
|
||||||
|
| | | | | | .------------. |
|
||||||
|
| | .---+---. .----+---. .-----------. | | | | |
|
||||||
|
| | | Bank |<-+ Replay | | BlobFetch |<------+ Upstream | |
|
||||||
|
| | | Forks | | Stage | | Stage | | | | Validators | |
|
||||||
|
| | `-------` `--------` `--+--------` | | | | |
|
||||||
|
| | ^ ^ | | | `------------` |
|
||||||
|
| | | | v | | |
|
||||||
|
| | | .--+--------. | | |
|
||||||
|
| | | | Blocktree | | | |
|
||||||
|
| | | `-----------` | | .------------. |
|
||||||
|
| | | ^ | | | | |
|
||||||
|
| | | | | | | Downstream | |
|
||||||
|
| | .--+--. .-------+---. | | | Validators | |
|
||||||
|
`-------->| TPU +---->| Broadcast +--------------->| | |
|
||||||
|
| `-----` | Service | | | `------------` |
|
||||||
|
| `-----------` | `------------------`
|
||||||
|
`--------------------------------------`
|
18
book/art/tpu.bob
Normal file
18
book/art/tpu.bob
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.-------------------------------------------.
|
||||||
|
| TPU .-------------. |
|
||||||
|
| | PoH Service | |
|
||||||
|
| `--------+----` |
|
||||||
|
| ^ | |
|
||||||
|
| | v |
|
||||||
|
| .-------. .-----------. .-+-------. | .------------.
|
||||||
|
.---------. | | Fetch | | SigVerify | | Banking | | | Broadcast |
|
||||||
|
| Clients |--->| Stage |->| Stage |->| Stage |------>| Service |
|
||||||
|
`---------` | | | | | | | | | |
|
||||||
|
| `-------` `-----------` `----+----` | `------------`
|
||||||
|
| | |
|
||||||
|
`---------------------------------|---------`
|
||||||
|
|
|
||||||
|
v
|
||||||
|
.------.
|
||||||
|
| Bank |
|
||||||
|
`------`
|
@@ -7,7 +7,7 @@
|
|||||||
| TVU | |
|
| TVU | |
|
||||||
| | |
|
| | |
|
||||||
| .-------. .------------. .----+---. .---------. |
|
| .-------. .------------. .----+---. .---------. |
|
||||||
.------------. | | Shred | | Retransmit | | Replay | | Storage | |
|
.------------. | | Blob | | Retransmit | | Replay | | Storage | |
|
||||||
| Upstream +----->| Fetch +-->| Stage +-->| Stage +-->| Stage | |
|
| Upstream +----->| Fetch +-->| Stage +-->| Stage +-->| Stage | |
|
||||||
| Validators | | | Stage | | | | | | | |
|
| Validators | | | Stage | | | | | | | |
|
||||||
`------------` | `-------` `----+-------` `----+---` `---------` |
|
`------------` | `-------` `----+-------` `----+---` `---------` |
|
18
book/build.sh
Executable file
18
book/build.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
cargo_install_unless() {
|
||||||
|
declare crate=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
"$@" > /dev/null 2>&1 || \
|
||||||
|
cargo install "$crate"
|
||||||
|
}
|
||||||
|
|
||||||
|
export PATH=$CARGO_HOME/bin:$PATH
|
||||||
|
cargo_install_unless mdbook mdbook --help
|
||||||
|
cargo_install_unless svgbob_cli svgbob --help
|
||||||
|
|
||||||
|
make -j"$(nproc)"
|
33
book/makefile
Normal file
33
book/makefile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
BOB_SRCS=$(wildcard art/*.bob)
|
||||||
|
MD_SRCS=$(wildcard src/*.md)
|
||||||
|
|
||||||
|
SVG_IMGS=$(BOB_SRCS:art/%.bob=src/img/%.svg)
|
||||||
|
|
||||||
|
all: html/index.html
|
||||||
|
|
||||||
|
test: src/tests.ok
|
||||||
|
|
||||||
|
open: all
|
||||||
|
mdbook build --open
|
||||||
|
|
||||||
|
watch: $(SVG_IMGS)
|
||||||
|
mdbook watch
|
||||||
|
|
||||||
|
src/img/%.svg: art/%.bob
|
||||||
|
@mkdir -p $(@D)
|
||||||
|
svgbob < $< > $@
|
||||||
|
|
||||||
|
src/%.md: %.md
|
||||||
|
@mkdir -p $(@D)
|
||||||
|
@cp $< $@
|
||||||
|
|
||||||
|
src/tests.ok: $(SVG_IMGS) $(MD_SRCS)
|
||||||
|
mdbook test
|
||||||
|
touch $@
|
||||||
|
|
||||||
|
html/index.html: src/tests.ok
|
||||||
|
mdbook build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(SVG_IMGS) src/tests.ok
|
||||||
|
rm -rf html
|
59
book/src/SUMMARY.md
Normal file
59
book/src/SUMMARY.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Solana Architecture
|
||||||
|
|
||||||
|
- [Introduction](introduction.md)
|
||||||
|
|
||||||
|
- [Terminology](terminology.md)
|
||||||
|
|
||||||
|
- [Getting Started](getting-started.md)
|
||||||
|
- [Example: Web Wallet](webwallet.md)
|
||||||
|
|
||||||
|
- [Programming Model](programs.md)
|
||||||
|
- [Example: Tic-Tac-Toe](tictactoe.md)
|
||||||
|
- [Drones](drones.md)
|
||||||
|
|
||||||
|
- [A Solana Cluster](cluster.md)
|
||||||
|
- [Synchronization](synchronization.md)
|
||||||
|
- [Leader Rotation](leader-rotation.md)
|
||||||
|
- [Fork Generation](fork-generation.md)
|
||||||
|
- [Managing Forks](managing-forks.md)
|
||||||
|
- [Data Plane Fanout](data-plane-fanout.md)
|
||||||
|
- [Ledger Replication](ledger-replication.md)
|
||||||
|
- [Secure Vote Signing](vote-signing.md)
|
||||||
|
- [Staking Delegation and Rewards](stake-delegation-and-rewards.md)
|
||||||
|
|
||||||
|
- [Anatomy of a Fullnode](fullnode.md)
|
||||||
|
- [TPU](tpu.md)
|
||||||
|
- [TVU](tvu.md)
|
||||||
|
- [Blocktree](blocktree.md)
|
||||||
|
- [Gossip Service](gossip.md)
|
||||||
|
- [The Runtime](runtime.md)
|
||||||
|
|
||||||
|
- [API Reference](api-reference.md)
|
||||||
|
- [Blockstreamer](blockstreamer.md)
|
||||||
|
- [JSON RPC API](jsonrpc-api.md)
|
||||||
|
- [JavaScript API](javascript-api.md)
|
||||||
|
- [solana-wallet CLI](wallet.md)
|
||||||
|
|
||||||
|
- [Proposed Architectural Changes](proposals.md)
|
||||||
|
- [Ledger Replication](ledger-replication-to-implement.md)
|
||||||
|
- [Secure Vote Signing](vote-signing-to-implement.md)
|
||||||
|
- [Staking Rewards](staking-rewards.md)
|
||||||
|
- [Fork Selection](fork-selection.md)
|
||||||
|
- [Reliable Vote Transmission](reliable-vote-transmission.md)
|
||||||
|
- [Persistent Account Storage](persistent-account-storage.md)
|
||||||
|
- [Leader to Leader Transition](leader-leader-transition.md)
|
||||||
|
- [Cluster Economics](ed_overview.md)
|
||||||
|
- [Validation-client Economics](ed_validation_client_economics.md)
|
||||||
|
- [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md)
|
||||||
|
- [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md)
|
||||||
|
- [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md)
|
||||||
|
- [Validation Stake Delegation](ed_vce_validation_stake_delegation.md)
|
||||||
|
- [Replication-client Economics](ed_replication_client_economics.md)
|
||||||
|
- [Storage-replication Rewards](ed_rce_storage_replication_rewards.md)
|
||||||
|
- [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md)
|
||||||
|
- [Economic Sustainability](ed_economic_sustainability.md)
|
||||||
|
- [Attack Vectors](ed_attack_vectors.md)
|
||||||
|
- [References](ed_references.md)
|
||||||
|
- [Leader-to-Validator Transition](leader-validator-transition.md)
|
||||||
|
- [Cluster Test Framework](cluster-test-framework.md)
|
||||||
|
- [Testing Programs](testing-programs.md)
|
4
book/src/api-reference.md
Normal file
4
book/src/api-reference.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# API Reference
|
||||||
|
|
||||||
|
The following sections contain API references material you may find useful
|
||||||
|
when developing applications utilizing a Solana cluster.
|
@@ -4,7 +4,7 @@ A validator votes on a PoH hash for two purposes. First, the vote indicates it
|
|||||||
believes the ledger is valid up until that point in time. Second, since many
|
believes the ledger is valid up until that point in time. Second, since many
|
||||||
valid forks may exist at a given height, the vote also indicates exclusive
|
valid forks may exist at a given height, the vote also indicates exclusive
|
||||||
support for the fork. This document describes only the former. The latter is
|
support for the fork. This document describes only the former. The latter is
|
||||||
described in [Tower BFT](tower-bft.md).
|
described in [fork selection](fork-selection.md).
|
||||||
|
|
||||||
## Current Design
|
## Current Design
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ height of the block it is voting on. The account stores the 32 highest heights.
|
|||||||
* Only the validator knows how to find its own votes directly.
|
* Only the validator knows how to find its own votes directly.
|
||||||
|
|
||||||
Other components, such as the one that calculates confirmation time, needs to
|
Other components, such as the one that calculates confirmation time, needs to
|
||||||
be baked into the validator code. The validator code queries the bank for all
|
be baked into the fullnode code. The fullnode code queries the bank for all
|
||||||
accounts owned by the vote program.
|
accounts owned by the vote program.
|
||||||
|
|
||||||
* Voting ballots do not contain a PoH hash. The validator is only voting that
|
* Voting ballots do not contain a PoH hash. The validator is only voting that
|
||||||
@@ -50,11 +50,12 @@ log the time since the NewBlock transaction was submitted.
|
|||||||
|
|
||||||
### Finality and Payouts
|
### Finality and Payouts
|
||||||
|
|
||||||
[Tower BFT](tower-bft.md) is the proposed fork selection algorithm. It proposes
|
Locktower is the proposed [fork selection](fork-selection.md) algorithm. It
|
||||||
that payment to miners be postponed until the *stack* of validator votes reaches
|
proposes that payment to miners be postponed until the *stack* of validator
|
||||||
a certain depth, at which point rollback is not economically feasible. The vote
|
votes reaches a certain depth, at which point rollback is not economically
|
||||||
program may therefore implement Tower BFT. Vote instructions would need to
|
feasible. The vote program may therefore implement locktower. Vote instructions
|
||||||
reference a global Tower account so that it can track cross-block state.
|
would need to reference a global locktower account so that it can track
|
||||||
|
cross-block state.
|
||||||
|
|
||||||
## Challenges
|
## Challenges
|
||||||
|
|
37
book/src/blockstreamer.md
Normal file
37
book/src/blockstreamer.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Blockstreamer
|
||||||
|
|
||||||
|
Solana supports a node type called an *blockstreamer*. This fullnode variation
|
||||||
|
is intended for applications that need to observe the data plane without
|
||||||
|
participating in transaction validation or ledger replication.
|
||||||
|
|
||||||
|
A blockstreamer runs without a vote signer, and can optionally stream ledger
|
||||||
|
entries out to a Unix domain socket as they are processed. The JSON-RPC service
|
||||||
|
still functions as on any other node.
|
||||||
|
|
||||||
|
To run a blockstreamer, include the argument `no-signer` and (optional)
|
||||||
|
`blockstream` socket location:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./multinode-demo/fullnode-x.sh --no-signer --blockstream <SOCKET>
|
||||||
|
```
|
||||||
|
|
||||||
|
The stream will output a series of JSON objects:
|
||||||
|
- An Entry event JSON object is sent when each ledger entry is processed, with
|
||||||
|
the following fields:
|
||||||
|
|
||||||
|
* `dt`, the system datetime, as RFC3339-formatted string
|
||||||
|
* `t`, the event type, always "entry"
|
||||||
|
* `s`, the slot height, as unsigned 64-bit integer
|
||||||
|
* `h`, the tick height, as unsigned 64-bit integer
|
||||||
|
* `entry`, the entry, as JSON object
|
||||||
|
|
||||||
|
|
||||||
|
- A Block event JSON object is sent when a block is complete, with the
|
||||||
|
following fields:
|
||||||
|
|
||||||
|
* `dt`, the system datetime, as RFC3339-formatted string
|
||||||
|
* `t`, the event type, always "block"
|
||||||
|
* `s`, the slot height, as unsigned 64-bit integer
|
||||||
|
* `h`, the tick height, as unsigned 64-bit integer
|
||||||
|
* `l`, the slot leader id, as base-58 encoded string
|
||||||
|
* `id`, the block id, as base-58 encoded string
|
102
book/src/blocktree.md
Normal file
102
book/src/blocktree.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Blocktree
|
||||||
|
|
||||||
|
After a block reaches finality, all blocks from that one on down
|
||||||
|
to the genesis block form a linear chain with the familiar name
|
||||||
|
blockchain. Until that point, however, the validator must maintain all
|
||||||
|
potentially valid chains, called *forks*. The process by which forks
|
||||||
|
naturally form as a result of leader rotation is described in
|
||||||
|
[fork generation](fork-generation.md). The *blocktree* data structure
|
||||||
|
described here is how a validator copes with those forks until blocks
|
||||||
|
are finalized.
|
||||||
|
|
||||||
|
The blocktree allows a validator to record every blob it observes
|
||||||
|
on the network, in any order, as long as the blob is signed by the expected
|
||||||
|
leader for a given slot.
|
||||||
|
|
||||||
|
Blobs are moved to a fork-able key space the tuple of `leader slot` + `blob
|
||||||
|
index` (within the slot). This permits the skip-list structure of the Solana
|
||||||
|
protocol to be stored in its entirety, without a-priori choosing which fork to
|
||||||
|
follow, which Entries to persist or when to persist them.
|
||||||
|
|
||||||
|
Repair requests for recent blobs are served out of RAM or recent files and out
|
||||||
|
of deeper storage for less recent blobs, as implemented by the store backing
|
||||||
|
Blocktree.
|
||||||
|
|
||||||
|
### Functionalities of Blocktree
|
||||||
|
|
||||||
|
1. Persistence: the Blocktree lives in the front of the nodes verification
|
||||||
|
pipeline, right behind network receive and signature verification. If the
|
||||||
|
blob received is consistent with the leader schedule (i.e. was signed by the
|
||||||
|
leader for the indicated slot), it is immediately stored.
|
||||||
|
2. Repair: repair is the same as window repair above, but able to serve any
|
||||||
|
blob that's been received. Blocktree stores blobs with signatures,
|
||||||
|
preserving the chain of origination.
|
||||||
|
3. Forks: Blocktree supports random access of blobs, so can support a
|
||||||
|
validator's need to rollback and replay from a Bank checkpoint.
|
||||||
|
4. Restart: with proper pruning/culling, the Blocktree can be replayed by
|
||||||
|
ordered enumeration of entries from slot 0. The logic of the replay stage
|
||||||
|
(i.e. dealing with forks) will have to be used for the most recent entries in
|
||||||
|
the Blocktree.
|
||||||
|
|
||||||
|
### Blocktree Design
|
||||||
|
|
||||||
|
1. Entries in the Blocktree are stored as key-value pairs, where the key is the concatenated
|
||||||
|
slot index and blob index for an entry, and the value is the entry data. Note blob indexes are zero-based for each slot (i.e. they're slot-relative).
|
||||||
|
|
||||||
|
2. The Blocktree maintains metadata for each slot, in the `SlotMeta` struct containing:
|
||||||
|
* `slot_index` - The index of this slot
|
||||||
|
* `num_blocks` - The number of blocks in the slot (used for chaining to a previous slot)
|
||||||
|
* `consumed` - The highest blob index `n`, such that for all `m < n`, there exists a blob in this slot with blob index equal to `n` (i.e. the highest consecutive blob index).
|
||||||
|
* `received` - The highest received blob index for the slot
|
||||||
|
* `next_slots` - A list of future slots this slot could chain to. Used when rebuilding
|
||||||
|
the ledger to find possible fork points.
|
||||||
|
* `last_index` - The index of the blob that is flagged as the last blob for this slot. This flag on a blob will be set by the leader for a slot when they are transmitting the last blob for a slot.
|
||||||
|
* `is_rooted` - True iff every block from 0...slot forms a full sequence without any holes. We can derive is_rooted for each slot with the following rules. Let slot(n) be the slot with index `n`, and slot(n).is_full() is true if the slot with index `n` has all the ticks expected for that slot. Let is_rooted(n) be the statement that "the slot(n).is_rooted is true". Then:
|
||||||
|
|
||||||
|
is_rooted(0)
|
||||||
|
is_rooted(n+1) iff (is_rooted(n) and slot(n).is_full()
|
||||||
|
|
||||||
|
3. Chaining - When a blob for a new slot `x` arrives, we check the number of blocks (`num_blocks`) for that new slot (this information is encoded in the blob). We then know that this new slot chains to slot `x - num_blocks`.
|
||||||
|
|
||||||
|
4. Subscriptions - The Blocktree records a set of slots that have been "subscribed" to. This means entries that chain to these slots will be sent on the Blocktree channel for consumption by the ReplayStage. See the `Blocktree APIs` for details.
|
||||||
|
|
||||||
|
5. Update notifications - The Blocktree notifies listeners when slot(n).is_rooted is flipped from false to true for any `n`.
|
||||||
|
|
||||||
|
### Blocktree APIs
|
||||||
|
|
||||||
|
The Blocktree offers a subscription based API that ReplayStage uses to ask for entries it's interested in. The entries will be sent on a channel exposed by the Blocktree. These subscription API's are as follows:
|
||||||
|
1. `fn get_slots_since(slot_indexes: &[u64]) -> Vec<SlotMeta>`: Returns new slots connecting to any element of the list `slot_indexes`.
|
||||||
|
|
||||||
|
2. `fn get_slot_entries(slot_index: u64, entry_start_index: usize, max_entries: Option<u64>) -> Vec<Entry>`: Returns the entry vector for the slot starting with `entry_start_index`, capping the result at `max` if `max_entries == Some(max)`, otherwise, no upper limit on the length of the return vector is imposed.
|
||||||
|
|
||||||
|
Note: Cumulatively, this means that the replay stage will now have to know when a slot is finished, and subscribe to the next slot it's interested in to get the next set of entries. Previously, the burden of chaining slots fell on the Blocktree.
|
||||||
|
|
||||||
|
### Interfacing with Bank
|
||||||
|
|
||||||
|
The bank exposes to replay stage:
|
||||||
|
|
||||||
|
1. `prev_hash`: which PoH chain it's working on as indicated by the hash of the last
|
||||||
|
entry it processed
|
||||||
|
2. `tick_height`: the ticks in the PoH chain currently being verified by this
|
||||||
|
bank
|
||||||
|
3. `votes`: a stack of records that contain:
|
||||||
|
|
||||||
|
1. `prev_hashes`: what anything after this vote must chain to in PoH
|
||||||
|
2. `tick_height`: the tick height at which this vote was cast
|
||||||
|
3. `lockout period`: how long a chain must be observed to be in the ledger to
|
||||||
|
be able to be chained below this vote
|
||||||
|
|
||||||
|
Replay stage uses Blocktree APIs to find the longest chain of entries it can
|
||||||
|
hang off a previous vote. If that chain of entries does not hang off the
|
||||||
|
latest vote, the replay stage rolls back the bank to that vote and replays the
|
||||||
|
chain from there.
|
||||||
|
|
||||||
|
### Pruning Blocktree
|
||||||
|
|
||||||
|
Once Blocktree entries are old enough, representing all the possible forks
|
||||||
|
becomes less useful, perhaps even problematic for replay upon restart. Once a
|
||||||
|
validator's votes have reached max lockout, however, any Blocktree contents
|
||||||
|
that are not on the PoH chain for that vote for can be pruned, expunged.
|
||||||
|
|
||||||
|
Replicator nodes will be responsible for storing really old ledger contents,
|
||||||
|
and validators need only persist their bank periodically.
|
122
book/src/cluster-test-framework.md
Normal file
122
book/src/cluster-test-framework.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Cluster Test Framework
|
||||||
|
|
||||||
|
This document proposes the Cluster Test Framework (CTF). CTF is a test harness
|
||||||
|
that allows tests to execute against a local, in-process cluster or a
|
||||||
|
deployed cluster.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
The goal of CTF is to provide a framework for writing tests independent of where
|
||||||
|
and how the cluster is deployed. Regressions can be captured in these tests and
|
||||||
|
the tests can be run against deployed clusters to verify the deployment. The
|
||||||
|
focus of these tests should be on cluster stability, consensus, fault tolerance,
|
||||||
|
API stability.
|
||||||
|
|
||||||
|
Tests should verify a single bug or scenario, and should be written with the
|
||||||
|
least amount of internal plumbing exposed to the test.
|
||||||
|
|
||||||
|
## Design Overview
|
||||||
|
|
||||||
|
Tests are provided an entry point, which is a `contact_info::ContactInfo`
|
||||||
|
structure, and a keypair that has already been funded.
|
||||||
|
|
||||||
|
Each node in the cluster is configured with a `fullnode::FullnodeConfig` at boot
|
||||||
|
time. At boot time this configuration specifies any extra cluster configuration
|
||||||
|
required for the test. The cluster should boot with the configuration when it
|
||||||
|
is run in-process or in a data center.
|
||||||
|
|
||||||
|
Once booted, the test will discover the cluster through a gossip entry point and
|
||||||
|
configure any runtime behaviors via fullnode RPC.
|
||||||
|
|
||||||
|
## Test Interface
|
||||||
|
|
||||||
|
Each CTF test starts with an opaque entry point and a funded keypair. The test
|
||||||
|
should not depend on how the cluster is deployed, and should be able to exercise
|
||||||
|
all the cluster functionality through the publicly available interfaces.
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use crate::contact_info::ContactInfo;
|
||||||
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
|
pub fn test_this_behavior(
|
||||||
|
entry_point_info: &ContactInfo,
|
||||||
|
funding_keypair: &Keypair,
|
||||||
|
num_nodes: usize,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Cluster Discovery
|
||||||
|
|
||||||
|
At test start, the cluster has already been established and is fully connected.
|
||||||
|
The test can discover most of the available nodes over a few second.
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use crate::gossip_service::discover;
|
||||||
|
|
||||||
|
// Discover the cluster over a few seconds.
|
||||||
|
let cluster_nodes = discover(&entry_point_info, num_nodes);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cluster Configuration
|
||||||
|
|
||||||
|
To enable specific scenarios, the cluster needs to be booted with special
|
||||||
|
configurations. These configurations can be captured in
|
||||||
|
`fullnode::FullnodeConfig`.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
let mut fullnode_config = FullnodeConfig::default();
|
||||||
|
fullnode_config.rpc_config.enable_fullnode_exit = true;
|
||||||
|
let local = LocalCluster::new_with_config(
|
||||||
|
num_nodes,
|
||||||
|
10_000,
|
||||||
|
100,
|
||||||
|
&fullnode_config
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to design a new test
|
||||||
|
|
||||||
|
For example, there is a bug that shows that the cluster fails when it is flooded
|
||||||
|
with invalid advertised gossip nodes. Our gossip library and protocol may
|
||||||
|
change, but the cluster still needs to stay resilient to floods of invalid
|
||||||
|
advertised gossip nodes.
|
||||||
|
|
||||||
|
Configure the RPC service:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
let mut fullnode_config = FullnodeConfig::default();
|
||||||
|
fullnode_config.rpc_config.enable_rpc_gossip_push = true;
|
||||||
|
fullnode_config.rpc_config.enable_rpc_gossip_refresh_active_set = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire the RPCs and write a new test:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
pub fn test_large_invalid_gossip_nodes(
|
||||||
|
entry_point_info: &ContactInfo,
|
||||||
|
funding_keypair: &Keypair,
|
||||||
|
num_nodes: usize,
|
||||||
|
) {
|
||||||
|
let cluster = discover(&entry_point_info, num_nodes);
|
||||||
|
|
||||||
|
// Poison the cluster.
|
||||||
|
let mut client = create_client(entry_point_info.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||||
|
for _ in 0..(num_nodes * 100) {
|
||||||
|
client.gossip_push(
|
||||||
|
cluster_info::invalid_contact_info()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sleep(Durration::from_millis(1000));
|
||||||
|
|
||||||
|
// Force refresh of the active set.
|
||||||
|
for node in &cluster {
|
||||||
|
let mut client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
|
||||||
|
client.gossip_refresh_active_set();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that spends still work.
|
||||||
|
verify_spends(&cluster);
|
||||||
|
}
|
||||||
|
```
|
100
book/src/cluster.md
Normal file
100
book/src/cluster.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# A Solana Cluster
|
||||||
|
|
||||||
|
A Solana cluster is a set of fullnodes working together to serve client
|
||||||
|
transactions and maintain the integrity of the ledger. Many clusters may
|
||||||
|
coexist. When two clusters share a common genesis block, they attempt to
|
||||||
|
converge. Otherwise, they simply ignore the existence of the other.
|
||||||
|
Transactions sent to the wrong one are quietly rejected. In this chapter, we'll
|
||||||
|
discuss how a cluster is created, how nodes join the cluster, how they share
|
||||||
|
the ledger, how they ensure the ledger is replicated, and how they cope with
|
||||||
|
buggy and malicious nodes.
|
||||||
|
|
||||||
|
## Creating a Cluster
|
||||||
|
|
||||||
|
Before starting any fullnodes, one first needs to create a *genesis block*.
|
||||||
|
The block contains entries referencing two public keys, a *mint* and a
|
||||||
|
*bootstrap leader*. The fullnode holding the bootstrap leader's secret key is
|
||||||
|
responsible for appending the first entries to the ledger. It initializes its
|
||||||
|
internal state with the mint's account. That account will hold the number of
|
||||||
|
native tokens defined by the genesis block. The second fullnode then contacts
|
||||||
|
the bootstrap leader to register as a *validator* or *replicator*. Additional
|
||||||
|
fullnodes then register with any registered member of the cluster.
|
||||||
|
|
||||||
|
A validator receives all entries from the leader and submits votes confirming
|
||||||
|
those entries are valid. After voting, the validator is expected to store those
|
||||||
|
entries until replicator nodes submit proofs that they have stored copies of
|
||||||
|
it. Once the validator observes a sufficient number of copies exist, it deletes
|
||||||
|
its copy.
|
||||||
|
|
||||||
|
## Joining a Cluster
|
||||||
|
|
||||||
|
Fullnodes and replicators enter the cluster via registration messages sent to
|
||||||
|
its *control plane*. The control plane is implemented using a *gossip*
|
||||||
|
protocol, meaning that a node may register with any existing node, and expect
|
||||||
|
its registration to propagate to all nodes in the cluster. The time it takes
|
||||||
|
for all nodes to synchronize is proportional to the square of the number of
|
||||||
|
nodes participating in the cluster. Algorithmically, that's considered very
|
||||||
|
slow, but in exchange for that time, a node is assured that it eventually has
|
||||||
|
all the same information as every other node, and that that information cannot
|
||||||
|
be censored by any one node.
|
||||||
|
|
||||||
|
## Sending Transactions to a Cluster
|
||||||
|
|
||||||
|
Clients send transactions to any fullnode's Transaction Processing Unit (TPU)
|
||||||
|
port. If the node is in the validator role, it forwards the transaction to the
|
||||||
|
designated leader. If in the leader role, the node bundles incoming
|
||||||
|
transactions, timestamps them creating an *entry*, and pushes them onto the
|
||||||
|
cluster's *data plane*. Once on the data plane, the transactions are validated
|
||||||
|
by validator nodes and replicated by replicator nodes, effectively appending
|
||||||
|
them to the ledger.
|
||||||
|
|
||||||
|
## Confirming Transactions
|
||||||
|
|
||||||
|
A Solana cluster is capable of subsecond *confirmation* for up to 150 nodes
|
||||||
|
with plans to scale up to hundreds of thousands of nodes. Once fully
|
||||||
|
implemented, confirmation times are expected to increase only with the
|
||||||
|
logarithm of the number of validators, where the logarithm's base is very high.
|
||||||
|
If the base is one thousand, for example, it means that for the first thousand
|
||||||
|
nodes, confirmation will be the duration of three network hops plus the time it
|
||||||
|
takes the slowest validator of a supermajority to vote. For the next million
|
||||||
|
nodes, confirmation increases by only one network hop.
|
||||||
|
|
||||||
|
Solana defines confirmation as the duration of time from when the leader
|
||||||
|
timestamps a new entry to the moment when it recognizes a supermajority of
|
||||||
|
ledger votes.
|
||||||
|
|
||||||
|
A gossip network is much too slow to achieve subsecond confirmation once the
|
||||||
|
network grows beyond a certain size. The time it takes to send messages to all
|
||||||
|
nodes is proportional to the square of the number of nodes. If a blockchain
|
||||||
|
wants to achieve low confirmation and attempts to do it using a gossip network,
|
||||||
|
it will be forced to centralize to just a handful of nodes.
|
||||||
|
|
||||||
|
Scalable confirmation can be achieved using the follow combination of
|
||||||
|
techniques:
|
||||||
|
|
||||||
|
1. Timestamp transactions with a VDF sample and sign the timestamp.
|
||||||
|
2. Split the transactions into batches, send each to separate nodes and have
|
||||||
|
each node share its batch with its peers.
|
||||||
|
3. Repeat the previous step recursively until all nodes have all batches.
|
||||||
|
|
||||||
|
Solana rotates leaders at fixed intervals, called *slots*. Each leader may only
|
||||||
|
produce entries during its allotted slot. The leader therefore timestamps
|
||||||
|
transactions so that validators may lookup the public key of the designated
|
||||||
|
leader. The leader then signs the timestamp so that a validator may verify the
|
||||||
|
signature, proving the signer is owner of the designated leader's public key.
|
||||||
|
|
||||||
|
Next, transactions are broken into batches so that a node can send transactions
|
||||||
|
to multiple parties without making multiple copies. If, for example, the leader
|
||||||
|
needed to send 60 transactions to 6 nodes, it would break that collection of 60
|
||||||
|
into batches of 10 transactions and send one to each node. This allows the
|
||||||
|
leader to put 60 transactions on the wire, not 60 transactions for each node.
|
||||||
|
Each node then shares its batch with its peers. Once the node has collected all
|
||||||
|
6 batches, it reconstructs the original set of 60 transactions.
|
||||||
|
|
||||||
|
A batch of transactions can only be split so many times before it is so small
|
||||||
|
that header information becomes the primary consumer of network bandwidth. At
|
||||||
|
the time of this writing, the approach is scaling well up to about 150
|
||||||
|
validators. To scale up to hundreds of thousands of validators, each node can
|
||||||
|
apply the same technique as the leader node to another set of nodes of equal
|
||||||
|
size. We call the technique *data plane fanout*; learn more in the [data plan
|
||||||
|
fanout](data-plane-fanout.md) section.
|
84
book/src/data-plane-fanout.md
Normal file
84
book/src/data-plane-fanout.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Data Plane Fanout
|
||||||
|
|
||||||
|
A Solana cluster uses a multi-layer mechanism called *data plane fanout* to
|
||||||
|
broadcast transaction blobs to all nodes in a very quick and efficient manner.
|
||||||
|
In order to establish the fanout, the cluster divides itself into small
|
||||||
|
collections of nodes, called *neighborhoods*. Each node is responsible for
|
||||||
|
sharing any data it receives with the other nodes in its neighborhood, as well
|
||||||
|
as propagating the data on to a small set of nodes in other neighborhoods.
|
||||||
|
|
||||||
|
During its slot, the leader node distributes blobs between the validator nodes
|
||||||
|
in one neighborhood (layer 1). Each validator shares its data within its
|
||||||
|
neighborhood, but also retransmits the blobs to one node in each of multiple
|
||||||
|
neighborhoods in the next layer (layer 2). The layer-2 nodes each share their
|
||||||
|
data with their neighborhood peers, and retransmit to nodes in the next layer,
|
||||||
|
etc, until all nodes in the cluster have received all the blobs.
|
||||||
|
|
||||||
|
<img alt="Two layer cluster" src="img/data-plane.svg" class="center"/>
|
||||||
|
|
||||||
|
## Neighborhood Assignment - Weighted Selection
|
||||||
|
|
||||||
|
In order for data plane fanout to work, the entire cluster must agree on how the
|
||||||
|
cluster is divided into neighborhoods. To achieve this, all the recognized
|
||||||
|
validator nodes (the TVU peers) are sorted by stake and stored in a list. This
|
||||||
|
list is then indexed in different ways to figure out neighborhood boundaries and
|
||||||
|
retransmit peers. For example, the leader will simply select the first nodes to
|
||||||
|
make up layer 1. These will automatically be the highest stake holders, allowing
|
||||||
|
the heaviest votes to come back to the leader first. Layer-1 and lower-layer
|
||||||
|
nodes use the same logic to find their neighbors and lower layer peers.
|
||||||
|
|
||||||
|
## Layer and Neighborhood Structure
|
||||||
|
|
||||||
|
The current leader makes its initial broadcasts to at most `DATA_PLANE_FANOUT`
|
||||||
|
nodes. If this layer 1 is smaller than the number of nodes in the cluster, then
|
||||||
|
the data plane fanout mechanism adds layers below. Subsequent layers follow
|
||||||
|
these constraints to determine layer-capacity: Each neighborhood contains
|
||||||
|
`NEIGHBORHOOD_SIZE` nodes and each layer may have up to `DATA_PLANE_FANOUT/2`
|
||||||
|
neighborhoods.
|
||||||
|
|
||||||
|
As mentioned above, each node in a layer only has to broadcast its blobs to its
|
||||||
|
neighbors and to exactly 1 node in each next-layer neighborhood, instead of to
|
||||||
|
every TVU peer in the cluster. In the default mode, each layer contains
|
||||||
|
`DATA_PLANE_FANOUT/2` neighborhoods. The retransmit mechanism also supports a
|
||||||
|
second, `grow`, mode of operation that squares the number of neighborhoods
|
||||||
|
allowed each layer. This dramatically reduces the number of layers needed to
|
||||||
|
support a large cluster, but can also have a negative impact on the network
|
||||||
|
pressure on each node in the lower layers. A good way to think of the default
|
||||||
|
mode (when `grow` is disabled) is to imagine it as chain of layers, where the
|
||||||
|
leader sends blobs to layer-1 and then layer-1 to layer-2 and so on, the `layer
|
||||||
|
capacities` remain constant, so all layers past layer-2 will have the same
|
||||||
|
number of nodes until the whole cluster is covered. When `grow` is enabled, this
|
||||||
|
becomes a traditional fanout where layer-3 will have the square of the number of
|
||||||
|
nodes in layer-2 and so on.
|
||||||
|
|
||||||
|
#### Configuration Values
|
||||||
|
|
||||||
|
`DATA_PLANE_FANOUT` - Determines the size of layer 1. Subsequent
|
||||||
|
layers have `DATA_PLANE_FANOUT/2` neighborhoods when `grow` is inactive.
|
||||||
|
|
||||||
|
`NEIGHBORHOOD_SIZE` - The number of nodes allowed in a neighborhood.
|
||||||
|
Neighborhoods will fill to capacity before new ones are added, i.e if a
|
||||||
|
neighborhood isn't full, it _must_ be the last one.
|
||||||
|
|
||||||
|
`GROW_LAYER_CAPACITY` - Whether or not retransmit should be behave like a
|
||||||
|
_traditional fanout_, i.e if each additional layer should have growing
|
||||||
|
capacities. When this mode is disabled (default), all layers after layer 1 have
|
||||||
|
the same capacity, keeping the network pressure on all nodes equal.
|
||||||
|
|
||||||
|
Currently, configuration is set when the cluster is launched. In the future,
|
||||||
|
these parameters may be hosted on-chain, allowing modification on the fly as the
|
||||||
|
cluster sizes change.
|
||||||
|
|
||||||
|
## Neighborhoods
|
||||||
|
|
||||||
|
The following diagram shows how two neighborhoods in different layers interact.
|
||||||
|
What this diagram doesn't capture is that each neighbor actually receives
|
||||||
|
blobs from one validator per neighborhood above it. This means that, to
|
||||||
|
cripple a neighborhood, enough nodes (erasure codes +1 per neighborhood) from
|
||||||
|
the layer above need to fail. Since multiple neighborhoods exist in the upper
|
||||||
|
layer and a node will receive blobs from a node in each of those neighborhoods,
|
||||||
|
we'd need a big network failure in the upper layers to end up with incomplete
|
||||||
|
data.
|
||||||
|
|
||||||
|
<img alt="Inner workings of a neighborhood"
|
||||||
|
src="img/data-plane-neighborhood.svg" class="center"/>
|
86
book/src/drones.md
Normal file
86
book/src/drones.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Creating Signing Services with Drones
|
||||||
|
|
||||||
|
This chapter defines an off-chain service called a *drone*, which acts as
|
||||||
|
custodian of a user's private key. In its simplest form, it can be used to
|
||||||
|
create *airdrop* transactions, a token transfer from the drone's account to a
|
||||||
|
client's account.
|
||||||
|
|
||||||
|
## Signing Service
|
||||||
|
|
||||||
|
A drone is a simple signing service. It listens for requests to sign
|
||||||
|
*transaction data*. Once received, the drone validates the request however it
|
||||||
|
sees fit. It may, for example, only accept transaction data with a
|
||||||
|
`SystemInstruction::Move` instruction transferring only up to a certain amount
|
||||||
|
of tokens. If the drone accepts the transaction, it returns an `Ok(Signature)`
|
||||||
|
where `Signature` is a signature of the transaction data using the drone's
|
||||||
|
private key. If it rejects the transaction data, it returns a `DroneError`
|
||||||
|
describing why.
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Granting access to an on-chain game
|
||||||
|
|
||||||
|
Creator of on-chain game tic-tac-toe hosts a drone that responds to airdrop
|
||||||
|
requests containing an `InitGame` instruction. The drone signs the transaction
|
||||||
|
data in the request and returns it, thereby authorizing its account to pay the
|
||||||
|
transaction fee and as well as seeding the game's account with enough tokens to
|
||||||
|
play it. The user then creates a transaction for its transaction data and the
|
||||||
|
drones signature and submits it to the Solana cluster. Each time the user
|
||||||
|
interacts with the game, the game pays the user enough tokens to pay the next
|
||||||
|
transaction fee to advance the game. At that point, the user may choose to keep
|
||||||
|
the tokens instead of advancing the game. If the creator wants to defend
|
||||||
|
against that case, they could require the user to return to the drone to sign
|
||||||
|
each instruction.
|
||||||
|
|
||||||
|
### Worldwide airdrop of a new token
|
||||||
|
|
||||||
|
Creator of a new on-chain token (ERC-20 interface), may wish to do a worldwide
|
||||||
|
airdrop to distribute its tokens to millions of users over just a few seconds.
|
||||||
|
That drone cannot spend resources interacting with the Solana cluster. Instead,
|
||||||
|
the drone should only verify the client is unique and human, and then return
|
||||||
|
the signature. It may also want to listen to the Solana cluster for recent
|
||||||
|
entry IDs to support client retries and to ensure the airdrop is targeting the
|
||||||
|
desired cluster.
|
||||||
|
|
||||||
|
|
||||||
|
## Attack vectors
|
||||||
|
|
||||||
|
### Invalid recent_blockhash
|
||||||
|
|
||||||
|
The drone may prefer its airdrops only target a particular Solana cluster. To
|
||||||
|
do that, it listens to the cluster for new entry IDs and ensure any requests
|
||||||
|
reference a recent one.
|
||||||
|
|
||||||
|
Note: to listen for new entry IDs assumes the drone is either a fullnode or a
|
||||||
|
*light* client. At the time of this writing, light clients have not been
|
||||||
|
implemented and no proposal describes them. This document assumes one of the
|
||||||
|
following approaches be taken:
|
||||||
|
|
||||||
|
1. Define and implement a light client
|
||||||
|
2. Embed a fullnode
|
||||||
|
3. Query the jsonrpc API for the latest last id at a rate slightly faster than
|
||||||
|
ticks are produced.
|
||||||
|
|
||||||
|
### Double spends
|
||||||
|
|
||||||
|
A client may request multiple airdrops before the first has been submitted to
|
||||||
|
the ledger. The client may do this maliciously or simply because it thinks the
|
||||||
|
first request was dropped. The drone should not simply query the cluster to
|
||||||
|
ensure the client has not already received an airdrop. Instead, it should use
|
||||||
|
`recent_blockhash` to ensure the previous request is expired before signing another.
|
||||||
|
Note that the Solana cluster will reject any transaction with a `recent_blockhash`
|
||||||
|
beyond a certain *age*.
|
||||||
|
|
||||||
|
### Denial of Service
|
||||||
|
|
||||||
|
If the transaction data size is smaller than the size of the returned signature
|
||||||
|
(or descriptive error), a single client can flood the network. Considering
|
||||||
|
that a simple `Move` operation requires two public keys (each 32 bytes) and a
|
||||||
|
`fee` field, and that the returned signature is 64 bytes (and a byte to
|
||||||
|
indicate `Ok`), consideration for this attack may not be required.
|
||||||
|
|
||||||
|
In the current design, the drone accepts TCP connections. This allows clients
|
||||||
|
to DoS the service by simply opening lots of idle connections. Switching to UDP
|
||||||
|
may be preferred. The transaction data will be smaller than a UDP packet since
|
||||||
|
the transaction sent to the Solana cluster is already pinned to using UDP.
|
11
book/src/ed_attack_vectors.md
Normal file
11
book/src/ed_attack_vectors.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
## Attack Vectors
|
||||||
|
|
||||||
|
### Colluding validation and replication clients
|
||||||
|
|
||||||
|
A colluding validation-client, may take the strategy to mark PoReps from non-colluding replicator nodes as invalid as an attempt to maximize the rewards for the colluding replicator nodes. In this case, it isn’t feasible for the offended-against replicator nodes to petition the network for resolution as this would result in a network-wide vote on each offending PoRep and create too much overhead for the network to progress adequately. Also, this mitigation attempt would still be vulnerable to a >= 51% staked colluder.
|
||||||
|
|
||||||
|
Alternatively, transaction fees from submitted PoReps are pooled and distributed across validation-clients in proportion to the number of valid PoReps discounted by the number of invalid PoReps as voted by each validator-client. Thus invalid votes are directly dis-incentivized through this reward channel. Invalid votes that are revealed by replicator nodes as fishing PoReps, will not be discounted from the payout PoRep count.
|
||||||
|
|
||||||
|
Another collusion attack involves a validator-client who may take the strategy to ignore invalid PoReps from colluding replicator and vote them as valid. In this case, colluding replicator-clients would not have to store the data while still receiving rewards for validated PoReps. Additionally, colluding validator nodes would also receive rewards for validating these PoReps. To mitigate this attack, validators must randomly sample PoReps corresponding to the ledger block they are validating and because of this, there will be multiple validators that will receive the colluding replicator’s invalid submissions. These non-colluding validators will be incentivized to mark these PoReps as invalid as they have no way to determine whether the proposed invalid PoRep is actually a fishing PoRep, for which a confirmation vote would result in the validator’s stake being slashed.
|
||||||
|
|
||||||
|
In this case, the proportion of time a colluding pair will be successful has an upper limit determined by the % of stake of the network claimed by the colluding validator. This also sets bounds to the value of such an attack. For example, if a colluding validator controls 10% of the total validator stake, transaction fees will be lost (likely sent to mining pool) by the colluding replicator 90% of the time and so the attack vector is only profitable if the per-PoRep reward at least 90% higher than the average PoRep transaction fee. While, probabilistically, some colluding replicator-client PoReps will find their way to colluding validation-clients, the network can also monitor rates of paired (validator + replicator) discrepancies in voting patterns and censor identified colluders in these cases.
|
18
book/src/ed_economic_sustainability.md
Normal file
18
book/src/ed_economic_sustainability.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## Economic Sustainability
|
||||||
|
|
||||||
|
Long term economic sustainability is one of the guiding principles of Solana’s economic design. While it is impossible to predict how decentralized economies will develop over time, especially economies with flexible decentralized governances, we can arrange economic components such that, under certain conditions, a sustainable economy may take shape in the long term. In the case of Solana’s network, these components take the form of the remittances and deposits into and out of the reserve ‘mining pool’.
|
||||||
|
|
||||||
|
The dominant remittances from the Solana mining pool are validator and replicator rewards. The deposit mechanism is a flat, protocol-specified and adjusted, % of each transaction fee.
|
||||||
|
|
||||||
|
The Replicator rewards are to be delivered to replicators from the mining pool after successful PoRep validation. The per-PoRep reward amount is determined as a function of the total network storage redundancy at the time of the PoRep validation and the network goal redundancy. This function is likely to take the form of a discount from a base reward to be delivered when the network has achieved and maintained its goal redundancy. An example of such a reward function is shown in **Figure 3**
|
||||||
|
|
||||||
|
<!--  -->
|
||||||
|
<p style="text-align:center;"><img src="img/porep_reward.png" alt="==PoRep Reward Curve ==" width="800"/></p>
|
||||||
|
|
||||||
|
**Figure 3**: Example PoRep reward design as a function of global network storage redundancy.
|
||||||
|
|
||||||
|
In the example shown in Figure 1, multiple per PoRep base rewards are explored (as a % of Tx Fee) to be delivered when the global ledger replication redundancy meets 10X. When the global ledger replication redundancy is less than 10X, the base reward is discounted as a function of the square of the ratio of the actual ledger replication redundancy to the goal redundancy (i.e. 10X).
|
||||||
|
|
||||||
|
The other protocol-based remittance goes to validation-clients as a reward distributed in proportion to stake-weight for voting to validate the ledger state. The functional issuance of this reward is described in [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md) and is designed to reduce over time until validators are incentivized solely through collection of transaction fees. Therefore, in the long-run, protocol-based rewards to replication-nodes will be the only remittances from the mining pool, and will have to be countered by the portion of each non-PoRep transaction fee that is directed back into the mining pool. I.e. for a long-term self-sustaining economy, replicator-client rewards must be subsidized through a minimum fee on each non-PoRep transaction pre-allocated to the mining pool. Through this constraint, we can write the following inequality:
|
||||||
|
|
||||||
|
**== WIP [here](https://docs.google.com/document/d/1HBDasdkjS4Ja9wC_tIUsZPVcxGAWTuYOq9zf6xoQNps/edit?usp=sharing) ==**
|
16
book/src/ed_overview.md
Normal file
16
book/src/ed_overview.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
## Economic Design Overview
|
||||||
|
|
||||||
|
Solana’s crypto-economic system is designed to promote a healthy, long term self-sustaining economy with participant incentives aligned to the security and decentralization of the network. The main participants in this economy are validation-clients and replication-clients. Their contributions to the network, state validation and data storage respectively, and their requisite remittance mechanisms are discussed below.
|
||||||
|
|
||||||
|
The main channels of participant remittances are referred to as protocol-based rewards and transaction fees. Protocol-based rewards are protocol-derived issuances from a network-controlled reserve of tokens (sometimes referred to as the ‘mining pool’). These rewards will constitute the total reward delivered to replication clients and a portion of the total rewards for validation clients, the remaining sourced from transaction fees. In the early days of the network, it is likely that protocol-based rewards, deployed based on predefined issuance schedule, will drive the majority of participant incentives to join the network.
|
||||||
|
|
||||||
|
These protocol-based rewards, to be distributed to participating validation and replication clients, are to be specified as annual interest rates calculated per, real-time, Solana epoch [DEFINITION]. As discussed further below, the issuance rates are determined as a function of total network validator staked percentage and total replication provided by replicators in each previous epoch. The choice for validator and replicator client rewards to be based on participation rates, rather than a global fixed inflation or interest rate, emphasizes a protocol priority of overall economic security, rather than monetary supply predictability. Due to Solana’s hard total supply cap of 1B tokens and the bounds of client participant rates in the protocol, we believe that global interest, and supply issuance, scenarios should be able to be modeled with reasonable uncertainties.
|
||||||
|
|
||||||
|
Transaction fees are market-based participant-to-participant transfers, attached to network interactions as a necessary motivation and compensation for the inclusion and execution of a proposed transaction (be it a state execution or proof-of-replication verification). A mechanism for continuous and long-term funding of the mining pool through a pre-dedicated portion of transaction fees is also discussed below.
|
||||||
|
|
||||||
|
A high-level schematic of Solana’s crypto-economic design is shown below in **Figure 1**. The specifics of validation-client economics are described in sections: [Validation-client Economics](ed_validation_client_economics.md), [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md), [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md) and [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md). Also, the chapter titled [Validation Stake Delegation](ed_vce_validation_stake_delegation.md) closes with a discussion of validator delegation opportunties and marketplace. The [Replication-client Economics](ed_replication_client_economics.md) chapter will review the Solana network design for global ledger storage/redundancy and replicator-client economics ([Storage-replication rewards](ed_rce_storage_replication_rewards.md)) along with a replicator-to-validator delegation mechanism designed to aide participant on-boarding into the Solana economy discussed in [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md). The [Economic Sustainability](ed_economic_sustainability.md) section dives deeper into Solana’s design for long-term economic sustainability and outlines the constraints and conditions for a self-sustaining economy. Finally, in chapter [Attack Vectors](ed_attack_vectors.md), various attack vectors will be described and potential vulnerabilities explored and parameterized.
|
||||||
|
|
||||||
|
<!--  -->
|
||||||
|
<p style="text-align:center;"><img src="img/solana_economic_design.png" alt="== Solana Economic Design Diagram ==" width="800"/></p>
|
||||||
|
|
||||||
|
**Figure 1**: Schematic overview of Solana economic incentive design.
|
@@ -0,0 +1,5 @@
|
|||||||
|
### Replication-client Reward Auto-delegation
|
||||||
|
|
||||||
|
The ability for Solana network participant’s to earn rewards by providing storage service is a unique on-boarding path that requires little hardware overhead and minimal upfront capital. It offers an avenue for individuals with extra-storage space on their home laptops or PCs to contribute to the security of the network and become integrated into the Solana economy.
|
||||||
|
|
||||||
|
To enhance this on-boarding ramp and facilitate further participation and investment in the Solana economy, replication-clients have the opportunity to auto-delegate their rewards to validation-clients of their choice. Much like the automatic reinvestment of stock dividends, in this scenario, a replicator-client can earn Solana tokens by providing some storage capacity to the network (i.e. via submitting valid PoReps), have the protocol-based rewards automatically assigned as delegation to a staked validator node and therefore earning interest in the validation-client reward pool.
|
5
book/src/ed_rce_storage_replication_rewards.md
Normal file
5
book/src/ed_rce_storage_replication_rewards.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
### Storage-replication Rewards
|
||||||
|
|
||||||
|
Replicator-clients download, encrypt and submit PoReps for ledger block sections.3 PoReps submitted to the PoH stream, and subsequently validated, function as evidence that the submitting replicator client is indeed storing the assigned ledger block sections on local hard drive space as a service to the network. Therefore, replicator clients should earn protocol rewards proportional to the amount of storage, and the number of successfully validated PoReps, that they are verifiably providing to the network.
|
||||||
|
|
||||||
|
Additionally, replicator clients have the opportunity to capture a portion of slashed bounties [TBD] of dishonest validator clients. This can be accomplished by a replicator client submitting a verifiably false PoRep for which a dishonest validator client receives and signs as a valid PoRep. This reward incentive is to prevent lazy validators and minimize validator-replicator collusion attacks, more on this below.
|
@@ -1,6 +1,7 @@
|
|||||||
# References
|
## References
|
||||||
|
|
||||||
1. [https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/](https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/)
|
1. [https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/](https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/)
|
||||||
2. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281)
|
|
||||||
3. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281)
|
|
||||||
|
|
||||||
|
2. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281)
|
||||||
|
|
||||||
|
3. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281)
|
3
book/src/ed_replication_client_economics.md
Normal file
3
book/src/ed_replication_client_economics.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## Replication-client economics
|
||||||
|
|
||||||
|
Replication-clients should be rewarded for providing the network with storage space. Incentivization of the set of replicators provides data security through redundancy of the historical ledger. Replication nodes are rewarded in proportion to the amount of ledger data storage provided. These rewards are captured by generating and entering Proofs of Replication (PoReps) into the PoH stream which can be validated by Validation nodes as described above in the [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md) chapter.
|
3
book/src/ed_validation_client_economics.md
Normal file
3
book/src/ed_validation_client_economics.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## Validation-client Economics
|
||||||
|
|
||||||
|
Validator-clients are eligible to receive protocol-based (i.e. via mining pool) rewards issued via stake-based annual interest rates by providing compute (CPU+GPU) resources to validate and vote on a given PoH state. These protocol-based rewards are determined through an algorithmic schedule as a function of total amount of Solana tokens staked in the system and duration since network launch (genesis block). Additionally, these clients may earn revenue through two types of transaction fees: state-validation transaction fees and pooled Proof-of-Replication (PoRep) transaction fees. The distribution of these two types of transaction fees to the participating validation set are designed independently as economic goals and attack vectors are unique between the state- generation/validation mechanism and the ledger replication/validation mechanism. For clarity, we separately describe the design and motivation of the three types of potential revenue streams for validation-clients below: state-validation protocol-based rewards, state-validation transaction fees and PoRep-validation transaction fees.
|
@@ -0,0 +1,9 @@
|
|||||||
|
### Replication-validation Transaction Fees
|
||||||
|
|
||||||
|
As previously mentioned, validator-clients will also be responsible for validating PoReps submitted into the PoH stream by replicator-clients. In this case, validators are providing compute (CPU/GPU) and light storage resources to confirm that these replication proofs could only be generated by a client that is storing the referenced PoH leger block.2
|
||||||
|
|
||||||
|
While replication-clients are incentivized and rewarded through protocol-based rewards schedule (see [Replication-client Economics](ed_replication_client_economics.md)), validator-clients will be incentivized to include and validate PoReps in PoH through the distribution of the transaction fees associated with the submitted PoRep. As will be described in detail in the Section 3.1, replication-client rewards are protocol-based and designed to reward based on a global data redundancy factor. I.e. the protocol will incentivize replication-client participation through rewards based on a target ledger redundancy (e.g. 10x data redundancy). It was chosen not to include a distribution of these rewards to PoRep validators, and to rely only on the collection of PoRep attached transaction fees, due to the fact that the confluence of two participation incentive modes (state-validation inflation rate via global staked % and replication-validation rewards based on global redundancy factor) on the incentives of a single network participant (a validator-client) potentially opened up a significant incentive-driven attack surface area.
|
||||||
|
|
||||||
|
The validation of PoReps by validation-clients is computationally more expensive than state-validation (detail in the [Economic Sustainability](ed_economic_sustainability.md) chapter), thus the transaction fees are expected to be proportionally higher. However, because replication-client rewards are distributed in proportion to and only after submitted PoReps are validated, they are uniquely motivated for the inclusion and validation of their proofs. This pressure is expected to generate an adequate market economy between replication-clients and validation-clients. Additionally, transaction fees submitted with PoReps have no minimum amount pre-allocated to the mining pool, as do state-validation transaction fees.
|
||||||
|
|
||||||
|
There are various attack vectors available for colluding validation and replication clients, as described in detail below in [Economic Sustainability](ed_economic_sustainability). To protect against various collusion attack vectors, for a given epoch, PoRep transaction fees are pooled, and redistributed across participating validation-clients in proportion to the number of validated PoReps in the epoch less the number of invalidated PoReps [DIAGRAM]. This design rewards validators proportional to the number of PoReps they process and validate, while providing negative pressure for validation-clients to submit lazy or malicious invalid votes on submitted PoReps (note that it is computationally prohibitive to determine whether a validator-client has marked a valid PoRep as invalid).
|
46
book/src/ed_vce_state_validation_protocol_based_rewards.md
Normal file
46
book/src/ed_vce_state_validation_protocol_based_rewards.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
### State-validation protocol-based rewards
|
||||||
|
|
||||||
|
Validator-clients have two functional roles in the Solana network
|
||||||
|
|
||||||
|
* Validate (vote) the current global state of that PoH along with any Proofs-of-Replication (see [Replication Client Economics](ed_replication_client_economics.md)) that they are eligible to validate
|
||||||
|
|
||||||
|
* Be elected as ‘leader’ on a stake-weighted round-robin schedule during which time they are responsible for collecting outstanding transactions and Proofs-of-Replication and incorporating them into the PoH, thus updating the global state of the network and providing chain continuity.
|
||||||
|
|
||||||
|
Validator-client rewards for these services are to be distributed at the end of each Solana epoch. Compensation for validator-clients is provided via a protocol-based annual interest rate dispersed in proportion to the stake-weight of each validator (see below) along with leader-claimed transaction fees available during each leader rotation. I.e. during the time a given validator-client is elected as leader, it has the opportunity to keep a portion of each non-PoRep transaction fee, less a protocol-specified amount that is returned to the mining pool (see [Validation-client State Transaction Fees](ed_vce_state_validation_transaction_fees.md)). PoRep transaction fees are not collected directly by the leader client but pooled and returned to the validator set in proportion to the number of successfully validated PoReps. (see [Replication-client Transaction Fees](ed_vce_replication_validation_transaction_fees.md))
|
||||||
|
|
||||||
|
|
||||||
|
The protocol-based annual interest-rate (%) per epoch to be distributed to validation-clients is to be a function of:
|
||||||
|
|
||||||
|
* the current fraction of staked SOLs out of the current total circulating supply,
|
||||||
|
|
||||||
|
* the global time since the genesis block instantiation
|
||||||
|
|
||||||
|
* the up-time/participation [% of available slots/blocks that validator had opportunity to vote on?] of a given validator over the previous epoch.
|
||||||
|
|
||||||
|
The first two factors are protocol parameters only (i.e. independent of validator behavior in a given epoch) and describe a global validation reward schedule designed to both incentivize early participation and optimal security in the network. This schedule sets a maximum annual validator-client interest rate per epoch.
|
||||||
|
|
||||||
|
At any given point in time, this interest rate is pegged to a defined value given a specific % staked SOL out of the circulating supply (e.g. 10% interest rate when 66% of circulating SOL is staked). The interest rate adjusts as the square-root [TBD] of the % staked, leading to higher validation-client interest rates as the % staked drops below the targeted goal, thus incentivizing more participation leading to more security in the network. An example of such a schedule, for a specified point in time (e.g. network launch) is shown in **Table 1**.
|
||||||
|
|
||||||
|
| Percentage circulating supply staked [%] | Annual validator-client interest rate [%] |
|
||||||
|
| ---: | ---: |
|
||||||
|
| 5 | 13.87 |
|
||||||
|
| 15 | 13.31 |
|
||||||
|
| 25 | 12.73 |
|
||||||
|
| 35 | 12.12 |
|
||||||
|
| 45 | 11.48 |
|
||||||
|
| 55 | 10.80 |
|
||||||
|
| **66** | **10.00** |
|
||||||
|
| 75 | 9.29 |
|
||||||
|
| 85 | 8.44 |
|
||||||
|
|
||||||
|
**Table 1:** Example interest rate schedule based on % SOL staked out of circulating supply. In this case, interest rates are fixed at 10% for 66% of staked circulating supply
|
||||||
|
|
||||||
|
Over time, the interest rate, at any network staked percentage, will drop as described by an algorithmic schedule. Validation-client interest rates are designed to be higher in the early days of the network to incentivize participation and jumpstart the network economy. This mining-pool provided interest rate will reduce over time until a network-chosen baseline value is reached. This is a fixed, long-term, interest rate to be provided to validator-clients. This value does not represent the total interest available to validator-clients as transaction fees for both state-validation and ledger storage replication (PoReps) are not accounted for here. A validation-client interest rate schedule as a function of % network staked and time is shown in** Figure 2**.
|
||||||
|
|
||||||
|
<!--  -->
|
||||||
|
|
||||||
|
<p style="text-align:center;"><img src="img/validation_client_interest_rates.png" alt="drawing" width="800"/></p>
|
||||||
|
|
||||||
|
**Figure 2:** In this example schedule, the annual interest rate [%] reduces at around 16.7% per year, until it reaches the long-term, fixed, 4% rate.
|
||||||
|
|
||||||
|
This epoch-specific protocol-defined interest rate sets an upper limit of *protocol-generated* annual interest rate (not absolute total interest rate) possible to be delivered to any validator-client per epoch. The distributed interest rate per epoch is then discounted from this value based on the participation of the validator-client during the previous epoch. Each epoch is comprised of XXX slots. The protocol-defined interest rate is then discounted by the log [TBD] of the % of slots a given validator submitted a vote on a PoH branch during that epoch, see **Figure XX**
|
20
book/src/ed_vce_state_validation_transaction_fees.md
Normal file
20
book/src/ed_vce_state_validation_transaction_fees.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
### State-validation Transaction Fees
|
||||||
|
|
||||||
|
Each message sent through the network, to be processed by the current leader validation-client and confirmed as a global state transaction, must contain a transaction fee. Transaction fees offer many benefits in the Solana economic design, for example they:
|
||||||
|
|
||||||
|
* provide unit compensation to the validator network for the CPU/GPU resources necessary to process the state transaction,
|
||||||
|
|
||||||
|
* reduce network spam by introducing real cost to transactions,
|
||||||
|
|
||||||
|
* open avenues for a transaction market to incentivize validation-client to collect and process submitted transactions in their function as leader,
|
||||||
|
|
||||||
|
* and provide potential long-term economic stability of the network through a protocol-captured minimum fee amount per transaction, as described below.
|
||||||
|
|
||||||
|
Many current blockchain economies (e.g. Bitcoin, Ethereum), rely on protocol-based rewards to support the economy in the short term, with the assumption that the revenue generated through transaction fees will support the economy in the long term, when the protocol derived rewards expire. In an attempt to create a sustainable economy through protocol-based rewards and transaction fees, a fixed portion of each transaction fee is sent to the mining pool, with the resulting fee going to the current leader processing the transaction. These pooled fees, then re-enter the system through rewards distributed to validation-clients, through the process described above, and replication-clients, as discussed below.
|
||||||
|
|
||||||
|
The intent of this design is to retain leader incentive to include as many transactions as possible within the leader-slot time, while providing a redistribution avenue that protects against "tax evasion" attacks (i.e. side-channel fee payments)<sup>[1](ed_referenced.md)</sup>. Constraints on the fixed portion of transaction fees going to the mining pool, to establish long-term economic sustainability, are established and discussed in detail in the [Economic Sustainability](ed_economic_sustainability.md) section.
|
||||||
|
|
||||||
|
This minimum, protocol-earmarked, portion of each transaction fee can be dynamically adjusted depending on historical gas usage. In this way, the protocol can use the minimum fee to target a desired hardware utilisation. By monitoring a protocol specified gas usage with respect to a desired, target usage amount (e.g. 50% of a block's capacity), the minimum fee can be raised/lowered which should, in turn, lower/raise the actual gas usage per block until it reaches the target amount. This adjustment process can be thought of as similar to the difficulty adjustment algorithm in the Bitcoin protocol, however in this case it is adjusting the minimum transaction fee to guide the transaction processing hardware usage to a desired level.
|
||||||
|
|
||||||
|
Additionally, the minimum protocol captured fee can be a consideration in fork selection. In the case of a PoH fork with a malicious, censoring leader, we would expect the total procotol captured fee to be less than a comparable honest fork, due to the fees lost from censoring. If the censoring leader is to compensate for these lost protocol fees, they would have to replace the fees on their fork themselves, thus potentially reducing the incentive to censor in the first place.
|
||||||
|
|
@@ -1,32 +1,29 @@
|
|||||||
# Validation Stake Delegation
|
### Validation Stake Delegation
|
||||||
|
|
||||||
**Subject to change.**
|
|
||||||
|
|
||||||
Running a Solana validation-client required relatively modest upfront hardware capital investment. **Table 2** provides an example hardware configuration to support ~1M tx/s with estimated ‘off-the-shelf’ costs:
|
Running a Solana validation-client required relatively modest upfront hardware capital investment. **Table 2** provides an example hardware configuration to support ~1M tx/s with estimated ‘off-the-shelf’ costs:
|
||||||
|
|
||||||
| Component | Example | Estimated Cost |
|
|Component|Example|Estimated Cost|
|
||||||
| :--- | :--- | :--- |
|
|--- |--- |--- |
|
||||||
| GPU | 2x 2080 Ti | $2500 |
|
|GPU|2x 2080 Ti|$2500|
|
||||||
| or | 4x 1080 Ti | $2800 |
|
|or|4x 1080 Ti|$2800|
|
||||||
| OS/Ledger Storage | Samsung 860 Evo 2TB | $370 |
|
|OS/Ledger Storage|Samsung 860 Evo 2TB|$370|
|
||||||
| Accounts storage | 2x Samsung 970 Pro M.2 512GB | $340 |
|
|Accounts storage|2x Samsung 970 Pro M.2 512GB|$340|
|
||||||
| RAM | 32 Gb | $300 |
|
|RAM|32 Gb|$300|
|
||||||
| Motherboard | AMD x399 | $400 |
|
|Motherboard|AMD x399|$400|
|
||||||
| CPU | AMD Threadripper 2920x | $650 |
|
|CPU|AMD Threadripper 2920x|$650|
|
||||||
| Case | | $100 |
|
|Case||$100|
|
||||||
| Power supply | EVGA 1600W | $300 |
|
|Power supply|EVGA 1600W|$300|
|
||||||
| Network | > 500 mbps | |
|
|Network|> 500 mbps||
|
||||||
| Network \(1\) | Google webpass business bay area 1gbps unlimited | $5500/mo |
|
|Network (1)|Google webpass business bay area 1gbps unlimited|$5500/mo|
|
||||||
| Network \(2\) | Hurricane Electric bay area colo 1gbps | $500/mo |
|
|Network (2)|Hurricane Electric bay area colo 1gbps|$500/mo|
|
||||||
|
|
||||||
**Table 2** example high-end hardware setup for running a Solana client.
|
**Table 2** example high-end hardware setup for running a Solana client.
|
||||||
|
|
||||||
Despite the low-barrier to entry as a validation-client, from a capital investment perspective, as in any developing economy, there will be much opportunity and need for trusted validation services as evidenced by node reliability, UX/UI, APIs and other software accessibility tools. Additionally, although Solana’s validator node startup costs are nominal when compared to similar networks, they may still be somewhat restrictive for some potential participants. In the spirit of developing a true decentralized, permissionless network, these interested parties still have two options to become involved in the Solana network/economy:
|
Despite the low-barrier to entry as a validation-client, from a capital investment perspective, as in any developing economy, there will be much opportunity and need for trusted validation services as evidenced by node reliability, UX/UI, APIs and other software accessibility tools. Additionally, although Solana’s validator node startup costs are nominal when compared to similar networks, they may still be somewhat restrictive for some potential participants. In the spirit of developing a true decentralized, permissionless network, these interested parties still have two options to become involved in the Solana network/economy:
|
||||||
|
|
||||||
1. Delegation of previously acquired tokens with a reliable validation node to earn a portion of interest generated
|
1. Delegation of previously acquired tokens with a reliable validation node to earn a portion of interest generated
|
||||||
2. Provide local storage space as a replication-client and receive rewards by submitting Proof-of-Replication \(see [Replication-client Economics](../ed_replication_client_economics/)\).
|
|
||||||
|
|
||||||
a. This participant has the additional option to directly delegate their earned storage rewards \([Replication-client Reward Auto-delegation](../ed_replication_client_economics/ed_rce_replication_client_reward_auto_delegation.md)\)
|
2. Provide local storage space as a replication-client and receive rewards by submitting Proof-of-Replication (see [Replication-client Economics](ed_replication_client_economics.md)).
|
||||||
|
|
||||||
Delegation of tokens to validation-clients, via option 1, provides a way for passive Solana token holders to become part of the active Solana economy and earn interest rates proportional to the interest rate generated by the delegated validation-client. Additionally, this feature intends to create a healthy validation-client market, with potential validation-client nodes competing to build reliable, transparent and profitable delegation services.
|
a. This participant has the additional option to directly delegate their earned storage rewards ([Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md))
|
||||||
|
|
||||||
|
Delegation of tokens to validation-clients, via option 1, provides a way for passive Solana token holders to become part of the active Solana economy and earn interest rates proportional to the interest rate generated by the delegated validation-client. Additionally, this feature creates a healthy validation-client market, with potential validation-client nodes competing to build reliable, transparent and profitable delegation services.
|
104
book/src/fork-generation.md
Normal file
104
book/src/fork-generation.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Fork Generation
|
||||||
|
|
||||||
|
The chapter describes how forks naturally occur as a consequence of [leader
|
||||||
|
rotation](leader-rotation.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Nodes take turns being leader and generating the PoH that encodes state
|
||||||
|
changes. The cluster can tolerate loss of connection to any leader by
|
||||||
|
synthesizing what the leader ***would*** have generated had it been connected
|
||||||
|
but not ingesting any state changes. The possible number of forks is thereby
|
||||||
|
limited to a "there/not-there" skip list of forks that may arise on leader
|
||||||
|
rotation slot boundaries. At any given slot, only a single leader's
|
||||||
|
transactions will be accepted.
|
||||||
|
|
||||||
|
## Message Flow
|
||||||
|
|
||||||
|
1. Transactions are ingested by the current leader.
|
||||||
|
2. Leader filters valid transactions.
|
||||||
|
3. Leader executes valid transactions updating its state.
|
||||||
|
4. Leader packages transactions into entries based off its current PoH slot.
|
||||||
|
5. Leader transmits the entries to validator nodes (in signed blobs)
|
||||||
|
1. The PoH stream includes ticks; empty entries that indicate liveness of
|
||||||
|
the leader and the passage of time on the cluster.
|
||||||
|
2. A leader's stream begins with the tick entries necessary complete the PoH
|
||||||
|
back to the leaders most recently observed prior leader slot.
|
||||||
|
6. Validators retransmit entries to peers in their set and to further
|
||||||
|
downstream nodes.
|
||||||
|
7. Validators validate the transactions and execute them on their state.
|
||||||
|
8. Validators compute the hash of the state.
|
||||||
|
9. At specific times, i.e. specific PoH tick counts, validators transmit votes
|
||||||
|
to the leader.
|
||||||
|
1. Votes are signatures of the hash of the computed state at that PoH tick
|
||||||
|
count
|
||||||
|
2. Votes are also propagated via gossip
|
||||||
|
10. Leader executes the votes as any other transaction and broadcasts them to
|
||||||
|
the cluster.
|
||||||
|
11. Validators observe their votes and all the votes from the cluster.
|
||||||
|
|
||||||
|
## Partitions, Forks
|
||||||
|
|
||||||
|
Forks can arise at PoH tick counts that correspond to a vote. The next leader
|
||||||
|
may not have observed the last vote slot and may start their slot with
|
||||||
|
generated virtual PoH entries. These empty ticks are generated by all nodes in
|
||||||
|
the cluster at a cluster-configured rate for hashes/per/tick `Z`.
|
||||||
|
|
||||||
|
There are only two possible versions of the PoH during a voting slot: PoH with
|
||||||
|
`T` ticks and entries generated by the current leader, or PoH with just ticks.
|
||||||
|
The "just ticks" version of the PoH can be thought of as a virtual ledger, one
|
||||||
|
that all nodes in the cluster can derive from the last tick in the previous
|
||||||
|
slot.
|
||||||
|
|
||||||
|
Validators can ignore forks at other points (e.g. from the wrong leader), or
|
||||||
|
slash the leader responsible for the fork.
|
||||||
|
|
||||||
|
Validators vote based on a greedy choice to maximize their reward described in
|
||||||
|
[forks selection](fork-selection.md).
|
||||||
|
|
||||||
|
### Validator's View
|
||||||
|
|
||||||
|
#### Time Progression
|
||||||
|
|
||||||
|
The diagram below represents a validator's view of the
|
||||||
|
PoH stream with possible forks over time. L1, L2, etc. are leader slots, and
|
||||||
|
`E`s represent entries from that leader during that leader's slot. The `x`s
|
||||||
|
represent ticks only, and time flows downwards in the diagram.
|
||||||
|
|
||||||
|
|
||||||
|
<img alt="Fork generation" src="img/fork-generation.svg" class="center"/>
|
||||||
|
|
||||||
|
Note that an `E` appearing on 2 forks at the same slot is a slashable
|
||||||
|
condition, so a validator observing `E3` and `E3'` can slash L3 and safely
|
||||||
|
choose `x` for that slot. Once a validator commits to a forks, other forks can
|
||||||
|
be discarded below that tick count. For any slot, validators need only
|
||||||
|
consider a single "has entries" chain or a "ticks only" chain to be proposed by
|
||||||
|
a leader. But multiple virtual entries may overlap as they link back to the a
|
||||||
|
previous slot.
|
||||||
|
|
||||||
|
#### Time Division
|
||||||
|
|
||||||
|
It's useful to consider leader rotation over PoH tick count as time division of
|
||||||
|
the job of encoding state for the cluster. The following table presents the
|
||||||
|
above tree of forks as a time-divided ledger.
|
||||||
|
|
||||||
|
leader slot | L1 | L2 | L3 | L4 | L5
|
||||||
|
-------|----|----|----|----|----
|
||||||
|
data | E1| E2 | E3 | E4 | E5
|
||||||
|
ticks since prev | | | | x | xx
|
||||||
|
|
||||||
|
Note that only data from leader L3 will be accepted during leader slot L3.
|
||||||
|
Data from L3 may include "catchup" ticks back to a slot other than L2 if L3 did
|
||||||
|
not observe L2's data. L4 and L5's transmissions include the "ticks to prev"
|
||||||
|
PoH entries.
|
||||||
|
|
||||||
|
This arrangement of the network data streams permits nodes to save exactly this
|
||||||
|
to the ledger for replay, restart, and checkpoints.
|
||||||
|
|
||||||
|
### Leader's View
|
||||||
|
|
||||||
|
When a new leader begins a slot, it must first transmit any PoH (ticks)
|
||||||
|
required to link the new slot with the most recently observed and voted slot.
|
||||||
|
The fork the leader proposes would link the current slot to a previous fork
|
||||||
|
that the leader has voted on with virtual ticks.
|
233
book/src/fork-selection.md
Normal file
233
book/src/fork-selection.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# Fork Selection
|
||||||
|
|
||||||
|
This design describes a *Fork Selection* algorithm. It addresses the following
|
||||||
|
problems:
|
||||||
|
|
||||||
|
* Some forks may not end up accepted by the super-majority of the cluster, and
|
||||||
|
voters need to recover from voting on such forks.
|
||||||
|
|
||||||
|
* Many forks may be votable by different voters, and each voter may see a
|
||||||
|
different set of votable forks. The selected forks should eventually converge
|
||||||
|
for the cluster.
|
||||||
|
|
||||||
|
* Reward based votes have an associated risk. Voters should have the ability to
|
||||||
|
configure how much risk they take on.
|
||||||
|
|
||||||
|
* The [cost of rollback](#cost-of-rollback) needs to be computable. It is
|
||||||
|
important to clients that rely on some measurable form of Consistency. The
|
||||||
|
costs to break consistency need to be computable, and increase super-linearly
|
||||||
|
for older votes.
|
||||||
|
|
||||||
|
* ASIC speeds are different between nodes, and attackers could employ Proof of
|
||||||
|
History ASICS that are much faster than the rest of the cluster. Consensus
|
||||||
|
needs to be resistant to attacks that exploit the variability in Proof of
|
||||||
|
History ASIC speed.
|
||||||
|
|
||||||
|
For brevity this design assumes that a single voter with a stake is deployed as
|
||||||
|
an individual validator in the cluster.
|
||||||
|
|
||||||
|
## Time
|
||||||
|
|
||||||
|
The Solana cluster generates a source of time via a Verifiable Delay Function we
|
||||||
|
are calling [Proof of History](book/src/synchronization.md).
|
||||||
|
|
||||||
|
Proof of History is used to create a deterministic round robin schedule for all
|
||||||
|
the active leaders. At any given time only 1 leader, which can be computed from
|
||||||
|
the ledger itself, can propose a fork. For more details, see [fork
|
||||||
|
generation](fork-generation.md) and [leader rotation](leader-rotation.md).
|
||||||
|
|
||||||
|
## Lockouts
|
||||||
|
|
||||||
|
The purpose of the lockout is to force a validator to commit opportunity cost to
|
||||||
|
a specific fork. Lockouts are measured in slots, and therefor represent a
|
||||||
|
real-time forced delay that a validator needs to wait before breaking the
|
||||||
|
commitment to a fork.
|
||||||
|
|
||||||
|
Validators that violate the lockouts and vote for a diverging fork within the
|
||||||
|
lockout should be punished. The proposed punishment is to slash the validator
|
||||||
|
stake if a concurrent vote within a lockout for a non-descendant fork can be
|
||||||
|
proven to the cluster.
|
||||||
|
|
||||||
|
## Algorithm
|
||||||
|
|
||||||
|
The basic idea to this approach is to stack consensus votes and double lockouts.
|
||||||
|
Each vote in the stack is a confirmation of a fork. Each confirmed fork is an
|
||||||
|
ancestor of the fork above it. Each vote has a `lockout` in units of slots
|
||||||
|
before the validator can submit a vote that does not contain the confirmed fork
|
||||||
|
as an ancestor.
|
||||||
|
|
||||||
|
When a vote is added to the stack, the lockouts of all the previous votes in the
|
||||||
|
stack are doubled (more on this in [Rollback](#Rollback)). With each new vote,
|
||||||
|
a validator commits the previous votes to an ever-increasing lockout. At 32
|
||||||
|
votes we can consider the vote to be at `max lockout` any votes with a lockout
|
||||||
|
equal to or above `1<<32` are dequeued (FIFO). Dequeuing a vote is the trigger
|
||||||
|
for a reward. If a vote expires before it is dequeued, it and all the votes
|
||||||
|
above it are popped (LIFO) from the vote stack. The validator needs to start
|
||||||
|
rebuilding the stack from that point.
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
|
||||||
|
Before a vote is pushed to the stack, all the votes leading up to vote with a
|
||||||
|
lower lock time than the new vote are popped. After rollback lockouts are not
|
||||||
|
doubled until the validator catches up to the rollback height of votes.
|
||||||
|
|
||||||
|
For example, a vote stack with the following state:
|
||||||
|
|
||||||
|
| vote | vote time | lockout | lock expiration time |
|
||||||
|
|-----:|----------:|--------:|---------------------:|
|
||||||
|
| 4 | 4 | 2 | 6 |
|
||||||
|
| 3 | 3 | 4 | 7 |
|
||||||
|
| 2 | 2 | 8 | 10 |
|
||||||
|
| 1 | 1 | 16 | 17 |
|
||||||
|
|
||||||
|
*Vote 5* is at time 9, and the resulting state is
|
||||||
|
|
||||||
|
| vote | vote time | lockout | lock expiration time |
|
||||||
|
|-----:|----------:|--------:|---------------------:|
|
||||||
|
| 5 | 9 | 2 | 11 |
|
||||||
|
| 2 | 2 | 8 | 10 |
|
||||||
|
| 1 | 1 | 16 | 17 |
|
||||||
|
|
||||||
|
*Vote 6* is at time 10
|
||||||
|
|
||||||
|
| vote | vote time | lockout | lock expiration time |
|
||||||
|
|-----:|----------:|--------:|---------------------:|
|
||||||
|
| 6 | 10 | 2 | 12 |
|
||||||
|
| 5 | 9 | 4 | 13 |
|
||||||
|
| 2 | 2 | 8 | 10 |
|
||||||
|
| 1 | 1 | 16 | 17 |
|
||||||
|
|
||||||
|
At time 10 the new votes caught up to the previous votes. But *vote 2* expires
|
||||||
|
at 10, so the when *vote 7* at time 11 is applied the votes including and above
|
||||||
|
*vote 2* will be popped.
|
||||||
|
|
||||||
|
| vote | vote time | lockout | lock expiration time |
|
||||||
|
|-----:|----------:|--------:|---------------------:|
|
||||||
|
| 7 | 11 | 2 | 13 |
|
||||||
|
| 1 | 1 | 16 | 17 |
|
||||||
|
|
||||||
|
The lockout for vote 1 will not increase from 16 until the stack contains 5
|
||||||
|
votes.
|
||||||
|
|
||||||
|
### Slashing and Rewards
|
||||||
|
|
||||||
|
Validators should be rewarded for selecting the fork that the rest of the
|
||||||
|
cluster selected as often as possible. This is well-aligned with generating a
|
||||||
|
reward when the vote stack is full and the oldest vote needs to be dequeued.
|
||||||
|
Thus a reward should be generated for each successful dequeue.
|
||||||
|
|
||||||
|
### Cost of Rollback
|
||||||
|
|
||||||
|
Cost of rollback of *fork A* is defined as the cost in terms of lockout time to
|
||||||
|
the validator to confirm any other fork that does not include *fork A* as an
|
||||||
|
ancestor.
|
||||||
|
|
||||||
|
The **Economic Finality** of *fork A* can be calculated as the loss of all the
|
||||||
|
rewards from rollback of *fork A* and its descendants, plus the opportunity cost
|
||||||
|
of reward due to the exponentially growing lockout of the votes that have
|
||||||
|
confirmed *fork A*.
|
||||||
|
|
||||||
|
### Thresholds
|
||||||
|
|
||||||
|
Each validator can independently set a threshold of cluster commitment to a fork
|
||||||
|
before that validator commits to a fork. For example, at vote stack index 7,
|
||||||
|
the lockout is 256 time units. A validator may withhold votes and let votes 0-7
|
||||||
|
expire unless the vote at index 7 has at greater than 50% commitment in the
|
||||||
|
cluster. This allows each validator to independently control how much risk to
|
||||||
|
commit to a fork. Committing to forks at a higher frequency would allow the
|
||||||
|
validator to earn more rewards.
|
||||||
|
|
||||||
|
### Algorithm parameters
|
||||||
|
|
||||||
|
The following parameters need to be tuned:
|
||||||
|
|
||||||
|
* Number of votes in the stack before dequeue occurs (32).
|
||||||
|
|
||||||
|
* Rate of growth for lockouts in the stack (2x).
|
||||||
|
|
||||||
|
* Starting default lockout (2).
|
||||||
|
|
||||||
|
* Threshold depth for minimum cluster commitment before committing to the fork
|
||||||
|
(8).
|
||||||
|
|
||||||
|
* Minimum cluster commitment size at threshold depth (50%+).
|
||||||
|
|
||||||
|
### Free Choice
|
||||||
|
|
||||||
|
A "Free Choice" is an unenforcible validator action. There is no way for the
|
||||||
|
protocol to encode and enforce these actions since each validator can modify the
|
||||||
|
code and adjust the algorithm. A validator that maximizes self-reward over all
|
||||||
|
possible futures should behave in such a way that the system is stable, and the
|
||||||
|
local greedy choice should result in a greedy choice over all possible futures.
|
||||||
|
A set of validator that are engaging in choices to disrupt the protocol should
|
||||||
|
be bound by their stake weight to the denial of service. Two options exits for
|
||||||
|
validator:
|
||||||
|
|
||||||
|
* a validator can outrun previous validator in virtual generation and submit a
|
||||||
|
concurrent fork
|
||||||
|
|
||||||
|
* a validator can withhold a vote to observe multiple forks before voting
|
||||||
|
|
||||||
|
In both cases, the validator in the cluster have several forks to pick from
|
||||||
|
concurrently, even though each fork represents a different height. In both
|
||||||
|
cases it is impossible for the protocol to detect if the validator behavior is
|
||||||
|
intentional or not.
|
||||||
|
|
||||||
|
### Greedy Choice for Concurrent Forks
|
||||||
|
|
||||||
|
When evaluating multiple forks, each validator should use the following rules:
|
||||||
|
|
||||||
|
1. Forks must satisfy the *Threshold* rule.
|
||||||
|
|
||||||
|
2. Pick the fork that maximizes the total cluster lockout time for all the
|
||||||
|
ancestor forks.
|
||||||
|
|
||||||
|
3. Pick the fork that has the greatest amount of cluster transaction fees.
|
||||||
|
|
||||||
|
4. Pick the latest fork in terms of PoH.
|
||||||
|
|
||||||
|
Cluster transaction fees are fees that are deposited to the mining pool as
|
||||||
|
described in the [Staking Rewards](book/src/staking-rewards.md) section.
|
||||||
|
|
||||||
|
## PoH ASIC Resistance
|
||||||
|
|
||||||
|
Votes and lockouts grow exponentially while ASIC speed up is linear. There are
|
||||||
|
two possible attack vectors involving a faster ASIC.
|
||||||
|
|
||||||
|
### ASIC censorship
|
||||||
|
|
||||||
|
An attacker generates a concurrent fork that outruns previous leaders in an
|
||||||
|
effort to censor them. A fork proposed by this attacker will be available
|
||||||
|
concurrently with the next available leader. For nodes to pick this fork it
|
||||||
|
must satisfy the *Greedy Choice* rule.
|
||||||
|
|
||||||
|
1. Fork must have equal number of votes for the ancestor fork.
|
||||||
|
|
||||||
|
2. Fork cannot be so far a head as to cause expired votes.
|
||||||
|
|
||||||
|
3. Fork must have a greater amount of cluster transaction fees.
|
||||||
|
|
||||||
|
This attack is then limited to censoring the previous leaders fees, and
|
||||||
|
individual transactions. But it cannot halt the cluster, or reduce the
|
||||||
|
validator set compared to the concurrent fork. Fee censorship is limited to
|
||||||
|
access fees going to the leaders but not the validators.
|
||||||
|
|
||||||
|
### ASIC Rollback
|
||||||
|
|
||||||
|
An attacker generates a concurrent fork from an older block to try to rollback
|
||||||
|
the cluster. In this attack the concurrent fork is competing with forks that
|
||||||
|
have already been voted on. This attack is limited by the exponential growth of
|
||||||
|
the lockouts.
|
||||||
|
|
||||||
|
* 1 vote has a lockout of 2 slots. Concurrent fork must be at least 2 slots
|
||||||
|
ahead, and be produced in 1 slot. Therefore requires an ASIC 2x faster.
|
||||||
|
|
||||||
|
* 2 votes have a lockout of 4 slots. Concurrent fork must be at least 4 slots
|
||||||
|
ahead and produced in 2 slots. Therefore requires an ASIC 2x faster.
|
||||||
|
|
||||||
|
* 3 votes have a lockout of 8 slots. Concurrent fork must be at least 8 slots
|
||||||
|
ahead and produced in 3 slots. Therefore requires an ASIC 2.6x faster.
|
||||||
|
|
||||||
|
* 10 votes have a lockout of 1024 slots. 1024/10, or 102.4x faster ASIC.
|
||||||
|
|
||||||
|
* 20 votes have a lockout of 2^20 slots. 2^20/20, or 52,428.8x faster ASIC.
|
29
book/src/fullnode.md
Normal file
29
book/src/fullnode.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Anatomy of a Fullnode
|
||||||
|
|
||||||
|
<img alt="Fullnode block diagrams" src="img/fullnode.svg" class="center"/>
|
||||||
|
|
||||||
|
## Pipelining
|
||||||
|
|
||||||
|
The fullnodes make extensive use of an optimization common in CPU design,
|
||||||
|
called *pipelining*. Pipelining is the right tool for the job when there's a
|
||||||
|
stream of input data that needs to be processed by a sequence of steps, and
|
||||||
|
there's different hardware responsible for each. The quintessential example is
|
||||||
|
using a washer and dryer to wash/dry/fold several loads of laundry. Washing
|
||||||
|
must occur before drying and drying before folding, but each of the three
|
||||||
|
operations is performed by a separate unit. To maximize efficiency, one creates
|
||||||
|
a pipeline of *stages*. We'll call the washer one stage, the dryer another, and
|
||||||
|
the folding process a third. To run the pipeline, one adds a second load of
|
||||||
|
laundry to the washer just after the first load is added to the dryer.
|
||||||
|
Likewise, the third load is added to the washer after the second is in the
|
||||||
|
dryer and the first is being folded. In this way, one can make progress on
|
||||||
|
three loads of laundry simultaneously. Given infinite loads, the pipeline will
|
||||||
|
consistently complete a load at the rate of the slowest stage in the pipeline.
|
||||||
|
|
||||||
|
## Pipelining in the Fullnode
|
||||||
|
|
||||||
|
The fullnode contains two pipelined processes, one used in leader mode called
|
||||||
|
the TPU and one used in validator mode called the TVU. In both cases, the
|
||||||
|
hardware being pipelined is the same, the network input, the GPU cards, the CPU
|
||||||
|
cores, writes to disk, and the network output. What it does with that hardware
|
||||||
|
is different. The TPU exists to create ledger entries whereas the TVU exists
|
||||||
|
to validate them.
|
168
book/src/getting-started.md
Normal file
168
book/src/getting-started.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
The Solana git repository contains all the scripts you might need to spin up your
|
||||||
|
own local testnet. Depending on what you're looking to achieve, you may want to
|
||||||
|
run a different variation, as the full-fledged, performance-enhanced
|
||||||
|
multinode testnet is considerably more complex to set up than a Rust-only,
|
||||||
|
singlenode testnode. If you are looking to develop high-level features, such
|
||||||
|
as experimenting with smart contracts, save yourself some setup headaches and
|
||||||
|
stick to the Rust-only singlenode demo. If you're doing performance optimization
|
||||||
|
of the transaction pipeline, consider the enhanced singlenode demo. If you're
|
||||||
|
doing consensus work, you'll need at least a Rust-only multinode demo. If you want
|
||||||
|
to reproduce our TPS metrics, run the enhanced multinode demo.
|
||||||
|
|
||||||
|
For all four variations, you'd need the latest Rust toolchain and the Solana
|
||||||
|
source code:
|
||||||
|
|
||||||
|
First, install Rust's package manager Cargo.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl https://sh.rustup.rs -sSf | sh
|
||||||
|
$ source $HOME/.cargo/env
|
||||||
|
```
|
||||||
|
|
||||||
|
Now checkout the code from github:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone https://github.com/solana-labs/solana.git
|
||||||
|
$ cd solana
|
||||||
|
```
|
||||||
|
|
||||||
|
The demo code is sometimes broken between releases as we add new low-level
|
||||||
|
features, so if this is your first time running the demo, you'll improve
|
||||||
|
your odds of success if you check out the
|
||||||
|
[latest release](https://github.com/solana-labs/solana/releases)
|
||||||
|
before proceeding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||||
|
$ git checkout $TAG
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Setup
|
||||||
|
|
||||||
|
Ensure important programs such as the vote program are built before any
|
||||||
|
nodes are started
|
||||||
|
```bash
|
||||||
|
$ cargo build --all
|
||||||
|
```
|
||||||
|
|
||||||
|
The network is initialized with a genesis ledger and fullnode configuration files.
|
||||||
|
These files can be generated by running the following script.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./multinode-demo/setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drone
|
||||||
|
|
||||||
|
In order for the fullnodes and clients to work, we'll need to
|
||||||
|
spin up a drone to give out some test tokens. The drone delivers Milton
|
||||||
|
Friedman-style "air drops" (free tokens to requesting clients) to be used in
|
||||||
|
test transactions.
|
||||||
|
|
||||||
|
Start the drone with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./multinode-demo/drone.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Singlenode Testnet
|
||||||
|
|
||||||
|
Before you start a fullnode, make sure you know the IP address of the machine you
|
||||||
|
want to be the bootstrap leader for the demo, and make sure that udp ports 8000-10000 are
|
||||||
|
open on all the machines you want to test with.
|
||||||
|
|
||||||
|
Now start the bootstrap leader in a separate shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./multinode-demo/bootstrap-leader.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait a few seconds for the server to initialize. It will print "leader ready..." when it's ready to
|
||||||
|
receive transactions. The leader will request some tokens from the drone if it doesn't have any.
|
||||||
|
The drone does not need to be running for subsequent leader starts.
|
||||||
|
|
||||||
|
### Multinode Testnet
|
||||||
|
|
||||||
|
To run a multinode testnet, after starting a leader node, spin up some
|
||||||
|
additional full nodes in separate shells:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./multinode-demo/fullnode-x.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
To run a performance-enhanced full node on Linux,
|
||||||
|
[CUDA 10.0](https://developer.nvidia.com/cuda-downloads) must be installed on
|
||||||
|
your system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./fetch-perf-libs.sh
|
||||||
|
$ SOLANA_CUDA=1 ./multinode-demo/bootstrap-leader.sh
|
||||||
|
$ SOLANA_CUDA=1 ./multinode-demo/fullnode-x.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testnet Client Demo
|
||||||
|
|
||||||
|
Now that your singlenode or multinode testnet is up and running let's send it
|
||||||
|
some transactions!
|
||||||
|
|
||||||
|
In a separate shell start the client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./multinode-demo/client.sh # runs against localhost by default
|
||||||
|
```
|
||||||
|
|
||||||
|
What just happened? The client demo spins up several threads to send 500,000 transactions
|
||||||
|
to the testnet as quickly as it can. The client then pings the testnet periodically to see
|
||||||
|
how many transactions it processed in that time. Take note that the demo intentionally
|
||||||
|
floods the network with UDP packets, such that the network will almost certainly drop a
|
||||||
|
bunch of them. This ensures the testnet has an opportunity to reach 710k TPS. The client
|
||||||
|
demo completes after it has convinced itself the testnet won't process any additional
|
||||||
|
transactions. You should see several TPS measurements printed to the screen. In the
|
||||||
|
multinode variation, you'll see TPS measurements for each validator node as well.
|
||||||
|
|
||||||
|
### Testnet Debugging
|
||||||
|
|
||||||
|
There are some useful debug messages in the code, you can enable them on a per-module and per-level
|
||||||
|
basis. Before running a leader or validator set the normal RUST\_LOG environment variable.
|
||||||
|
|
||||||
|
For example
|
||||||
|
|
||||||
|
* To enable `info` everywhere and `debug` only in the solana::banking_stage module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ export RUST_LOG=solana=info,solana::banking_stage=debug
|
||||||
|
```
|
||||||
|
|
||||||
|
* To enable BPF program logging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ export RUST_LOG=solana_bpf_loader=trace
|
||||||
|
```
|
||||||
|
|
||||||
|
Generally we are using `debug` for infrequent debug messages, `trace` for potentially frequent
|
||||||
|
messages and `info` for performance-related logging.
|
||||||
|
|
||||||
|
You can also attach to a running process with GDB. The leader's process is named
|
||||||
|
_solana-fullnode_:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo gdb
|
||||||
|
attach <PID>
|
||||||
|
set logging on
|
||||||
|
thread apply all bt
|
||||||
|
```
|
||||||
|
|
||||||
|
This will dump all the threads stack traces into gdb.txt
|
||||||
|
|
||||||
|
## Public Testnet
|
||||||
|
|
||||||
|
In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./multinode-demo/client.sh --network $(dig +short testnet.solana.com):8001 --duration 60
|
||||||
|
```
|
||||||
|
|
||||||
|
You can observe the effects of your client's transactions on our [dashboard](https://metrics.solana.com:3000/d/testnet/testnet-hud?orgId=2&from=now-30m&to=now&refresh=5s&var-testnet=testnet)
|
||||||
|
|
128
book/src/gossip.md
Normal file
128
book/src/gossip.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Gossip Service
|
||||||
|
|
||||||
|
The Gossip Service acts as a gateway to nodes in the control plane. Fullnodes
|
||||||
|
use the service to ensure information is available to all other nodes in a cluster.
|
||||||
|
The service broadcasts information using a gossip protocol.
|
||||||
|
|
||||||
|
## Gossip Overview
|
||||||
|
|
||||||
|
Nodes continuously share signed data objects among themselves in order to
|
||||||
|
manage a cluster. For example, they share their contact information, ledger
|
||||||
|
height, and votes.
|
||||||
|
|
||||||
|
Every tenth of a second, each node sends a "push" message and/or a "pull"
|
||||||
|
message. Push and pull messages may elicit responses, and push messages may be
|
||||||
|
forwarded on to others in the cluster.
|
||||||
|
|
||||||
|
Gossip runs on a well-known UDP/IP port or a port in a well-known range. Once
|
||||||
|
a cluster is bootstrapped, nodes advertise to each other where to find their
|
||||||
|
gossip endpoint (a socket address).
|
||||||
|
|
||||||
|
## Gossip Records
|
||||||
|
|
||||||
|
Records shared over gossip are arbitrary, but signed and versioned (with a
|
||||||
|
timestamp) as needed to make sense to the node receiving them. If a node
|
||||||
|
recieves two records from the same source, it it updates its own copy with the
|
||||||
|
record with the most recent timestamp.
|
||||||
|
|
||||||
|
## Gossip Service Interface
|
||||||
|
|
||||||
|
### Push Message
|
||||||
|
|
||||||
|
A node sends a push message to tells the cluster it has information to share.
|
||||||
|
Nodes send push messages to `PUSH_FANOUT` push peers.
|
||||||
|
|
||||||
|
Upon receiving a push message, a node examines the message for:
|
||||||
|
|
||||||
|
1. Duplication: if the message has been seen before, the node responds with
|
||||||
|
`PushMessagePrune` and drops the message
|
||||||
|
|
||||||
|
2. New data: if the message is new to the node
|
||||||
|
* Stores the new information with an updated version in its cluster info and
|
||||||
|
purges any previous older value
|
||||||
|
* Stores the message in `pushed_once` (used for detecting duplicates,
|
||||||
|
purged after `PUSH_MSG_TIMEOUT * 5` ms)
|
||||||
|
* Retransmits the messages to its own push peers
|
||||||
|
|
||||||
|
3. Expiration: nodes drop push messages that are older than `PUSH_MSG_TIMEOUT`
|
||||||
|
|
||||||
|
### Push Peers, Prune Message
|
||||||
|
|
||||||
|
A nodes selects its push peers at random from the active set of known peers.
|
||||||
|
The node keeps this selection for a relatively long time. When a prune message
|
||||||
|
is received, the node drops the push peer that sent the prune. Prune is an
|
||||||
|
indication that there is another, faster path to that node than direct push.
|
||||||
|
|
||||||
|
The set of push peers is kept fresh by rotating a new node into the set every
|
||||||
|
`PUSH_MSG_TIMEOUT/2` milliseconds.
|
||||||
|
|
||||||
|
### Pull Message
|
||||||
|
|
||||||
|
A node sends a pull message to ask the cluster if there is any new information.
|
||||||
|
A pull message is sent to a single peer at random and comprises a Bloom filter
|
||||||
|
that represents things it already has. A node receiving a pull message
|
||||||
|
iterates over its values and constructs a pull response of things that miss the
|
||||||
|
filter and would fit in a message.
|
||||||
|
|
||||||
|
A node constructs the pull Bloom filter by iterating over current values and
|
||||||
|
recently purged values.
|
||||||
|
|
||||||
|
A node handles items in a pull response the same way it handles new data in a
|
||||||
|
push message.
|
||||||
|
|
||||||
|
|
||||||
|
## Purging
|
||||||
|
|
||||||
|
Nodes retain prior versions of values (those updated by a pull or push) and
|
||||||
|
expired values (those older than `GOSSIP_PULL_CRDS_TIMEOUT_MS`) in
|
||||||
|
`purged_values` (things I recently had). Nodes purge `purged_values` that are
|
||||||
|
older than `5 * GOSSIP_PULL_CRDS_TIMEOUT_MS`.
|
||||||
|
|
||||||
|
## Eclipse Attacks
|
||||||
|
|
||||||
|
An eclipse attack is an attempt to take over the set of node connections with
|
||||||
|
adversarial endpoints.
|
||||||
|
|
||||||
|
This is relevant to our implementation in the following ways.
|
||||||
|
|
||||||
|
* Pull messages select a random node from the network. An eclipse attack on
|
||||||
|
*pull* would require an attacker to influence the random selection in such a way
|
||||||
|
that only adversarial nodes are selected for pull.
|
||||||
|
|
||||||
|
* Push messages maintain an active set of nodes and select a random fanout for
|
||||||
|
every push message. An eclipse attack on *push* would influence the active set
|
||||||
|
selection, or the random fanout selection.
|
||||||
|
|
||||||
|
### Time and Stake based weights
|
||||||
|
|
||||||
|
Weights are calculated based on `time since last picked` and the `natural log` of the `stake weight`.
|
||||||
|
|
||||||
|
Taking the `ln` of the stake weight allows giving all nodes a fairer chance of network
|
||||||
|
coverage in a reasonable amount of time. It helps normalize the large possible `stake weight` differences between nodes.
|
||||||
|
This way a node with low `stake weight`, compared to a node with large `stake weight` will only have to wait a
|
||||||
|
few multiples of ln(`stake`) seconds before it gets picked.
|
||||||
|
|
||||||
|
There is no way for an adversary to influence these parameters.
|
||||||
|
|
||||||
|
### Pull Message
|
||||||
|
|
||||||
|
A node is selected as a pull target based on the weights described above.
|
||||||
|
|
||||||
|
### Push Message
|
||||||
|
|
||||||
|
A prune message can only remove an adversary from a potential connection.
|
||||||
|
|
||||||
|
Just like *pull message*, nodes are selected into the active set based on weights.
|
||||||
|
|
||||||
|
## Notable differences from PlumTree
|
||||||
|
|
||||||
|
The active push protocol described here is based on (Plum
|
||||||
|
Tree)[https://haslab.uminho.pt/jop/files/lpr07a.pdf]. The main differences are:
|
||||||
|
|
||||||
|
* Push messages have a wallclock that is signed by the originator. Once the
|
||||||
|
wallclock expires the message is dropped. A hop limit is difficult to implement
|
||||||
|
in an adversarial setting.
|
||||||
|
|
||||||
|
* Lazy Push is not implemented because its not obvious how to prevent an
|
||||||
|
adversary from forging the message fingerprint. A naive approach would allow an
|
||||||
|
adversary to be prioritized for pull based on their input.
|
Before Width: | Height: | Size: 372 KiB After Width: | Height: | Size: 372 KiB |
BIN
book/src/img/solana_economic_design.png
Normal file
BIN
book/src/img/solana_economic_design.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
BIN
book/src/img/validation_client_interest_rates.png
Normal file
BIN
book/src/img/validation_client_interest_rates.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 401 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user