Compare commits

..

73 Commits

Author SHA1 Message Date
mergify[bot]
66d9c56a77 Add buildkite-agent key for colo access (bp #6205) (#6206)
* Add buildkite-agent key for colo access (#6205)

(cherry picked from commit 58139ce5ae)

# Conflicts:
#	net/scripts/solana-user-authorized_keys.sh

* Update solana-user-authorized_keys.sh
2019-10-15 11:53:33 -04:00
Michael Vines
a77ca870c2 Disable cargo audit, not going to cherry-pick audit fixes into v0.19 2019-10-13 21:28:38 -07:00
Michael Vines
15686e7879 Skip deploy attempt on sanity failure 2019-10-12 22:21:12 -07:00
Greg Fitzgerald
068f9a7e60 GitBook: [v0.19] 13 pages and 55 assets modified 2019-10-08 23:35:36 +00:00
mergify[bot]
6d9560717a Bench tps: improve fund_keys (#6225) (#6237)
automerge
2019-10-04 01:45:25 -07:00
Michael Vines
d44b6016d1 Keep the build green when there's nowhere to publish 2019-10-03 14:55:18 -07:00
Michael Vines
4b0577baff Increase testnets to 4 validator nodes to avoid the need for 100% consensus 2019-10-03 09:53:17 -07:00
Michael Vines
f200862230 Cargo.lock 2019-10-03 00:23:12 -07:00
Michael Vines
e2b954fd78 Revert "GitBook: [v0.19] 14 pages and 19 assets modified"
This reverts commit 88950d2fb4.
2019-10-02 23:53:43 -07:00
Michael Vines
88950d2fb4 GitBook: [v0.19] 14 pages and 19 assets modified 2019-10-03 06:41:04 +00:00
Michael Vines
252a55da37 Fix crate metadata 2019-10-02 23:20:59 -07:00
Michael Vines
e419b3cf9f Add description tag 2019-10-02 23:12:55 -07:00
Michael Vines
713818b5eb Add description tag 2019-10-02 23:00:54 -07:00
Michael Vines
882068424d Enable patch branches 2019-10-02 22:48:04 -07:00
Michael Vines
10df95e36f Enable patch branches 2019-10-02 22:24:09 -07:00
Michael Vines
c43f57912c Bump version to 0.19.2 2019-10-02 22:16:16 -07:00
mergify[bot]
8f0cd4e456 Switch to solana-reed-solomon-erasure temporarily to fix windows build (#6220)
automerge
2019-10-02 19:39:45 -07:00
mergify[bot]
382663aef6 Bench-tps: flush tx queue when too old (#6201) (#6208)
automerge
2019-10-01 15:09:53 -07:00
mergify[bot]
ff79693568 Use built-in solana-gossip timeout for better error messages (#6189) (#6203)
automerge
2019-10-01 13:23:10 -07:00
mergify[bot]
517cebf7f0 Add native_token module to sdk (bp #6192) (#6196)
automerge
2019-10-01 13:02:43 -07:00
mergify[bot]
fcae91d7fa Rename bank height to block_height and expose method (#6199) (#6202)
automerge
2019-10-01 12:52:09 -07:00
Justin Starry
c44d3139fd Expose current stake accounts of a bank for use in cli tooling (#6184) (#6190)
automerge
2019-10-01 10:05:49 -07:00
mergify[bot]
effd2fd835 Cleanly error when trying to delegate-stake an existing stake account (#6158) (#6160)
(cherry picked from commit 284273a73f)
2019-10-01 09:02:28 -07:00
mergify[bot]
2cd832521a Clear download progress bar to avoid flicker during archive extraction (#6162) (#6176)
(cherry picked from commit 11fc684f3c)
2019-09-29 18:56:50 -07:00
mergify[bot]
77a4354b93 Don't try signging coding shred if fec rate is 0 (#6171) (#6172)
automerge

(cherry picked from commit 5637f88aff)
2019-09-28 19:38:00 -07:00
mergify[bot]
31f86158e8 Add get-epoch-info command (#6161) (#6173)
automerge
2019-09-27 22:53:47 -07:00
Jack May
29c892ee00 Validator-info doc update (#6152) (#6157) 2019-09-27 14:27:41 -07:00
mergify[bot]
06e34ef1cf Add ability to manually create a db (#6151) (#6154)
automerge
2019-09-27 12:42:17 -07:00
Ryan Shea
11bc11f0b7 Fix URLs for images (#6148) 2019-09-27 12:00:46 -07:00
mergify[bot]
8f27240211 get_new_blockhash() now retries longer (5s instead of 2s) (#6143) (#6150)
automerge
2019-09-27 11:27:43 -07:00
mergify[bot]
b8f680d1e7 doc: update validator-info publish arguments (bp #6146) (#6153)
* doc: update validator-info publish arguments (#6146)

(cherry picked from commit bf199a2ebc)

# Conflicts:
#	book/src/running-validator/validator-info.md

* Update validator-info.md
2019-09-27 11:25:57 -07:00
Michael Vines
2d4a081c3c Create vote account with 1 lamport instead of 1 SOL 2019-09-27 08:14:35 -07:00
mergify[bot]
be5f879e42 Avoid storing epoch 0 credits if no credits where earned in epoch 0 (#6132) (#6138)
automerge
2019-09-26 21:27:29 -07:00
mergify[bot]
8b353da83c Prevent subtract overflow panic when slot < MAX_LOCKOUT_HISTORY (#6135) (#6136)
automerge
2019-09-26 19:08:20 -07:00
Tyera Eulberg
61930c0dd3 Cherry-pick vote and stake authority changes (#6127)
* add authorized parameters to vote api (#6072)

* add authorized parameters to vote api

* code review

* add authorities to stake init (#6104)

* add authorities to stake init

* fixups

* code review
2019-09-26 17:18:31 -06:00
Michael Vines
232d2b3899 Remove CUDA feature (#6094) (#6126)
automerge
2019-09-26 14:38:30 -07:00
mergify[bot]
0f3a8314ae Enable SOL or lamports for create-vote-account, show-{stake,vote}-account commands (#6114) (#6116)
automerge
2019-09-26 11:35:14 -07:00
mergify[bot]
562034efe3 Remove serializing all ForkHashes (#6110) (#6111)
automerge
2019-09-26 02:43:15 -07:00
mergify[bot]
01c98c5e53 Remove some AccountStorage Serialization (#6047) (#6108)
automerge
2019-09-25 19:14:08 -07:00
mergify[bot]
a4128e886a Fix Bench-tps being too strict (#6105) (#6107)
automerge
2019-09-25 18:50:19 -07:00
mergify[bot]
2589816245 Change formula used in erasure statistics graph (#6102) (#6103)
automerge
2019-09-25 15:24:48 -07:00
mergify[bot]
0245e4cf94 Move status cache serialization to the Snapshot Packager service (#6081) (#6101)
automerge
2019-09-25 14:40:18 -07:00
Dan Albert
2293ad20d5 GitBook: [v0.19] 10 pages and 59 assets modified 2019-09-25 20:58:38 +00:00
mergify[bot]
c0195a2121 Prune fork_hashes with dead forks (#6085) (#6095)
automerge
2019-09-25 12:06:47 -07:00
mergify[bot]
d203adc3d9 Remove brace expansion in arg list (#6091) (#6092)
automerge
2019-09-25 10:28:19 -07:00
Dan Albert
0249d53a14 Update cargo files to 0.19.1 (#6093) 2019-09-25 13:12:00 -04:00
mergify[bot]
1db0fbc77e Add erasure recovery stats to dashboard (#6079) (#6087)
automerge
2019-09-24 22:34:12 -07:00
mergify[bot]
82153f934f Fix staker / voter credit redemption (#6074) (#6086)
automerge
2019-09-24 22:05:41 -07:00
mergify[bot]
4f3f662cfe Remove local_cluster tests from stable-perf job, removee other tests from local-cluster job (#6067) (#6084)
automerge
2019-09-24 21:00:10 -07:00
mergify[bot]
1942c47d73 Avoid accidential tx_count mismatches when using an accounts file (#6069) (#6082)
automerge
2019-09-24 20:37:22 -07:00
Tyera Eulberg
a90281ad5c Reduce poll sleep (#6068) (#6073)
* Reduce sleep in poll_for_signature_confirmations

* Unignore test_repairman_catchup
2019-09-24 20:01:04 -06:00
mergify[bot]
6360bac5e8 Fix using temp file for archive (#6058) (#6071)
* Fix using temp file for archive

* Rename the temp archive instead of hardlinking it

(cherry picked from commit 374b776a3e)
2019-09-24 18:29:11 -07:00
mergify[bot]
c54b23b9d5 Additional tests for should_retransmit_and_persist (#6062) (#6070)
automerge
2019-09-24 17:48:39 -07:00
mergify[bot]
04aaa714e6 Revert back to reqwest, using rustls feature (bp #6041) (#6064)
automerge
2019-09-24 15:35:07 -07:00
Michael Vines
2cc0ab2c5f Tweak Bank Slot Distance graph 2019-09-24 14:52:50 -07:00
mergify[bot]
aac0d7f2f5 Window service is filtering out coding shreds (#6052) (#6059)
automerge
2019-09-24 13:48:13 -07:00
mergify[bot]
9fd29b0575 Fix vote metrics (#6038) (#6040)
automerge
2019-09-24 13:20:27 -07:00
mergify[bot]
6abe3c2804 Fix race between observing tick height being set to last tick and blockhash being observed on a bank (#6013) (#6056)
automerge
2019-09-24 12:20:46 -07:00
mergify[bot]
01ce5beb19 Support primordial accounts with no data (#6053) (#6055)
automerge
2019-09-24 11:52:57 -07:00
mergify[bot]
2bd72318f6 Remove dead code from cluster_info (#6051) (#6054)
automerge
2019-09-24 11:29:52 -07:00
carllin
68a9224604 Fix race between observing tick height being set to last tick and blockhash being observed on a bank (#6013) 2019-09-24 11:16:24 -07:00
Rob Walker
181cad233c rename balance (#5984) 2019-09-24 11:15:15 -07:00
Justin Starry
0b66529f11 Fix BPF program static linking (#5992) (#6049)
automerge
2019-09-24 09:40:33 -07:00
Michael Vines
e20e4180a9 Flip order of arg to ensure -t sticks 2019-09-23 22:21:12 -07:00
mergify[bot]
de4309a905 Avoid hardlinking as that confuses tar (#6042) (#6045)
(cherry picked from commit 7fa809c16d)
2019-09-23 21:40:09 -07:00
mergify[bot]
d5248c936f Skip considering banks older than the latest vote slot (#6037) (#6043)
automerge
2019-09-23 20:44:47 -07:00
mergify[bot]
40bc37266d Don't recover coding shreds (#6034) (#6039)
automerge
2019-09-23 18:35:50 -07:00
mergify[bot]
1819f263f1 ' => " (#6035) (#6036)
automerge
2019-09-23 17:31:05 -07:00
Pankaj Garg
24a055e490 Upgrade to ReedSolomon 4.0 release (#6026) (#6030)
automerge
2019-09-23 15:29:52 -07:00
mergify[bot]
dfbd77f18d Fix really old banks triggering log spam (#6025) (#6029)
automerge
2019-09-23 15:27:38 -07:00
mergify[bot]
7c5557d69b Dump tar stdout/err on failure for better debug (#6024) (#6027)
automerge
2019-09-23 13:57:39 -07:00
mergify[bot]
d180eedd17 Remove the _/deps symlink, just copy instead (#6020) (#6021)
automerge
2019-09-23 09:50:07 -07:00
Michael Vines
3b274ca8db GitBook: [v0.19] 79 pages and 12 assets modified 2019-09-23 03:38:36 +00:00
2758 changed files with 125123 additions and 372715 deletions

42
.appveyor.yml Normal file
View File

@@ -0,0 +1,42 @@
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

View File

@@ -1,7 +1,14 @@
{ {
"_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f", "_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f",
"_comment": "These credentials are encrypted and pose no risk",
"environment": { "environment": {
"CODECOV_TOKEN": "EJ[1:Z7OneT3RdJJ0DipCHQ7rC84snQ+FPbgHwZADQiz54wk=:3K68mE38LJ2RB98VWmjuNLFBNn1XTGR4:cR4r05/TOZQKmEZp1v4CSgUJtC6QJiOaL85QjXW0qZ061fMnsBA8AtAPMDoDq4WCGOZM1A==]" "CODECOV_TOKEN": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:ks2/ElgxwgxqgmFcxTHANNLmj23YH74h:U4uzRONRfiQyqy6HrPQ/e7OnBUY4HkW37R0iekkF3KJ9UGnHqT1UvwgVbDqLahtDIJ4rWw==]",
"CRATES_IO_TOKEN": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:lKMh3aLW+jyRrfS/c7yvkpB+TaPhXqLq:j0v27EbaPgwRdHZAbsM0FlAnt3r9ScQrFbWJYOAZtM3qestEiByTlKpZ0eyF/823]",
"GITHUB_TOKEN": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:Ll78c3jGpYqnTwR7HJq3mNNUC7pOv9Lu:GrInO2r8MjmP5c54szkyygdsrW5KQYkDgJQUVyFEPyG8SWfchyM9Gur8RV0a+cdwuxNkHLi4U2M=]",
"INFLUX_DATABASE": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:IlH/ZLTXv3SwlY3TVyAPCX2KzLRY6iG3:gGmUGSU/kCfR/mTwKONaUC/X]",
"INFLUX_PASSWORD": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:o2qm95GU4VrrcC4OU06jjPvCwKZy/CZF:OW2ga3kLOQJvaDEdGRJ+gn3L2ckFm8AJZtv9wj/GeUIKDH2A4uBPTHsAH9PMe6zujpuHGk3qbeg=]",
"INFLUX_USERNAME": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:yDWW/uIHsJqOTDYskZoSx3pzoB1vztWY:2z31oTA3g0Xs9fCczGNJRcx8xf/hFCed]",
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:RqRaHlYUvGPNFJa6gmciaYM3tRJTURUH:q78/3GTHCN3Uqx9z4nOBjPZcO1lOazNoB/mdhGRDFsnAqVd2hU8zbKkqLrZfLlGqyD8WQOFuw5oTJR9qWg6L9LcOyj3pGL8jWF2yjgZxdtNMXnkbSrCWLooWBBLT61jYQnEwg73gT8ld3Q8EVv3T+MeSMu6FnPz+0+bqQCAGgfqksP4hsUAJGzgZu+i0tNOdlT7fxnh5KJK/yFM/CKgN2sRwEjukA9hXsffyB61g2zqzTDJxCUDLbCVrCkA/bfUk7Of/t0W5t0nK1H3oyGZEc/lRMauCknDBka3Gz11dVss2QT19WQNh0u7bHVaT/U4lepX1j9Zv]",
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_apple_darwin": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:wFDl3INEnA3EQDHRX40avqGe1OMoJxyy:6ncCRVRTIRuYI5o/gayeuWCudWvmKNYr8KEHAWeTq34a5bdcKInBdKhjmjX+wLHqsEwQ5gcyhcxy4Ri2mbuN6AHazfZOZlubQkGlyUOAIYO5D5jkbyIh40DAtjVzo1MD/0HsW9zdGOzqUKp5xJJeDsbR4F153jbxa7fvwF90Q4UQjYFTKAtExEmHtDGSJG48ToVwTabTV/OnISMIggDZBviIv2QWHvXgK07b2mUj34rHJywEDGN1nj5rITTDdUeRcB1x4BAMOe94kTFPSTaj/OszvYlGECt8rkKFqbm092qL+XLfiBaImqe/WJHRCnAj6Don]",
"SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_pc_windows_msvc": "EJ[1:8iZ6baJB4fbBV+XDsrUooyGAnGL/8Ol+4Qd0zKh5YjI=:wAh+dBuZopv6vruVOYegUcq/aBnbksT1:qIJfCfDvDWiqicMOkmbJs/0n7UJLKNmgMQaKzeQ8J7Q60YpXbtWzKVW3tS6lzlgf64m3MrPXyo1C+mWh6jkjsb18T/OfggZy1ZHM4AcsOC6/ldUkV5YtuxUQuAmd5jCuV/R7iuYY8Z66AcfAevlb+bnLpgIifdA8fh/IktOo58nZUQwZDdppAacmftsLc6Frn5Er6A6+EXpxK1nmnlmLJ4AJztqlh6X0r+JvE2O7qeoZUXrIegnkxo7Aay7I/dd8zdYpp7ICSiTEtfVN/xNIu/5QmTRU7gWoz7cPl9epq4aiEALzPOzb6KVOiRcsOg+TlFvLQ71Ik5o=]"
} }
} }

View File

@@ -3,19 +3,16 @@
# #
# Save target/ for the next CI build on this machine # Save target/ for the next CI build on this machine
# #
if [[ -z $CARGO_TARGET_CACHE ]]; then (
echo "+++ CARGO_TARGET_CACHE not defined" # pre-command should have defined it set -x
else d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
( mkdir -p "$d"
set -x set -x
mkdir -p "$CARGO_TARGET_CACHE" rsync -a --delete --link-dest="$PWD" target "$d"
set -x du -hs "$d"
rsync -a --delete --link-dest="$PWD" target "$CARGO_TARGET_CACHE" read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$d")
du -hs "$CARGO_TARGET_CACHE" echo "--- ${cacheSizeInGB}GB: $d"
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$CARGO_TARGET_CACHE") )
echo "--- ${cacheSizeInGB}GB: $CARGO_TARGET_CACHE"
)
fi
# #
# Add job_stats data point # Add job_stats data point

View File

@@ -11,29 +11,23 @@ export PS4="++"
# #
# Restore target/ from the previous CI build on this machine # Restore target/ from the previous CI build on this machine
# #
eval "$(ci/channel-info.sh)"
export CARGO_TARGET_CACHE=$HOME/cargo-target-cache/"$CHANNEL"-"$BUILDKITE_LABEL"
( (
set -x set -x
d=$HOME/cargo-target-cache/"$BUILDKITE_LABEL"
MAX_CACHE_SIZE=18 # gigabytes MAX_CACHE_SIZE=18 # gigabytes
if [[ -d $CARGO_TARGET_CACHE ]]; then if [[ -d $d ]]; then
du -hs "$CARGO_TARGET_CACHE" du -hs "$d"
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$CARGO_TARGET_CACHE") read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$d")
echo "--- ${cacheSizeInGB}GB: $CARGO_TARGET_CACHE" echo "--- ${cacheSizeInGB}GB: $d"
if [[ $cacheSizeInGB -gt $MAX_CACHE_SIZE ]]; then if [[ $cacheSizeInGB -gt $MAX_CACHE_SIZE ]]; then
echo "--- $CARGO_TARGET_CACHE is too large, removing it" echo "--- $d is too large, removing it"
rm -rf "$CARGO_TARGET_CACHE" rm -rf "$d"
fi fi
else else
echo "--- $CARGO_TARGET_CACHE not present" echo "--- $d not present"
fi fi
mkdir -p "$CARGO_TARGET_CACHE"/target mkdir -p "$d"/target
rsync -a --delete --link-dest="$CARGO_TARGET_CACHE" "$CARGO_TARGET_CACHE"/target . rsync -a --delete --link-dest="$d" "$d"/target .
# Don't reuse BPF target build artifacts due to incremental build issues with
# `std:
# "found possibly newer version of crate `std` which `xyz` depends on
rm -rf target/bpfel-unknown-unknown
) )

View File

@@ -9,10 +9,23 @@
set -e set -e
cd "$(dirname "$0")"/.. cd "$(dirname "$0")"/..
source ci/_
_ ci/buildkite-pipeline.sh pipeline.yml if [[ -n $BUILDKITE_TAG ]]; then
echo +++ pipeline buildkite-agent annotate --style info --context release-tag \
cat pipeline.yml "https://github.com/solana-labs/solana/releases/$BUILDKITE_TAG"
buildkite-agent pipeline upload ci/buildkite-release.yml
else
if [[ $BUILDKITE_BRANCH =~ ^pull ]]; then
# Add helpful link back to the corresponding Github Pull Request
buildkite-agent annotate --style info --context pr-backlink \
"Github Pull Request: https://github.com/solana-labs/solana/$BUILDKITE_BRANCH"
fi
_ buildkite-agent pipeline upload pipeline.yml 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

5
.gitbook.yaml Normal file
View File

@@ -0,0 +1,5 @@
root: ./book/src
structure:
readme: introduction.md
summary: SUMMARY.md

View File

@@ -1,41 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: cargo
directory: "/"
schedule:
interval: daily
time: "01:00"
timezone: America/Los_Angeles
#labels:
# - "automerge"
open-pull-requests-limit: 3
- package-ecosystem: npm
directory: "/web3.js"
schedule:
interval: daily
time: "01:00"
timezone: America/Los_Angeles
labels:
- "automerge"
commit-message:
prefix: "chore:"
open-pull-requests-limit: 3
- package-ecosystem: npm
directory: "/explorer"
schedule:
interval: daily
time: "01:00"
timezone: America/Los_Angeles
labels:
- "automerge"
commit-message:
prefix: "chore:"
include: "scope"
open-pull-requests-limit: 3

47
.github/stale.yml vendored
View File

@@ -1,39 +1,24 @@
only: pulls
# Number of days of inactivity before a pull request becomes stale
daysUntilStale: 30
# Number of days of inactivity before a stale pull request is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- security - security
- blocked
# Label to use when marking a pull request as stale # Label to use when marking a pull request as stale
staleLabel: stale staleLabel: stale
pulls: # Comment to post when marking a pull request as stale. Set to `false` to disable
# Number of days of inactivity before a pull request becomes stale markComment: >
daysUntilStale: 7 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.
# Number of days of inactivity before a stale pull request is closed # Comment to post when closing a stale pull request. Set to `false` to disable
daysUntilClose: 7 closeComment: >
# Comment to post when marking a pull request as stale. Set to `false` to disable This stale pull request has been automatically closed.
markComment: > Thank you for your contributions.
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.
issues:
# Number of days of inactivity before a issue becomes stale
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Comment to post when marking a issue as stale. Set to `false` to disable
markComment: >
This issue 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 issue. Set to `false` to disable
closeComment: >
This stale issue has been automatically closed.
Thank you for your contributions.

15
.gitignore vendored
View File

@@ -1,7 +1,5 @@
/docs/html/ /book/html/
/docs/src/tests.ok /book/src/tests.ok
/docs/src/cli/usage.md
/docs/src/.gitbook/assets/*.svg
/farf/ /farf/
/solana-release/ /solana-release/
/solana-release.tar.bz2 /solana-release.tar.bz2
@@ -14,20 +12,11 @@
/config/ /config/
.cache
# log files # log files
*.log *.log
log-*.txt log-*.txt
log-*/
# intellij files # intellij files
/.idea/ /.idea/
/solana.iml /solana.iml
/.vscode/ /.vscode/
# fetch-spl.sh artifacts
/spl-genesis-args.sh
/spl_*.so
.DS_Store

View File

@@ -1,40 +1,9 @@
# Validate your changes with: # Validate your changes with:
# #
# $ curl -F 'data=@.mergify.yml' https://gh.mergify.io/validate/ # $ curl -F 'data=@.mergify.yml' https://gh.mergify.io/validate
# #
# https://doc.mergify.io/ # https://doc.mergify.io/
pull_request_rules: pull_request_rules:
- name: automatic merge (squash) on CI success
conditions:
- status-success=buildkite/solana
- status-success=Travis CI - Pull Request
- status-success=ci-gate
- label=automerge
- author≠@dont-squash-my-commits
actions:
merge:
method: squash
# Join the dont-squash-my-commits group if you won't like your commits squashed
- name: automatic merge (rebase) on CI success
conditions:
- status-success=buildkite/solana
- status-success=Travis CI - Pull Request
- status-success=ci-gate
- label=automerge
- author=@dont-squash-my-commits
actions:
merge:
method: rebase
- name: remove automerge label on CI failure
conditions:
- label=automerge
- "#status-failure!=0"
actions:
label:
remove:
- automerge
comment:
message: automerge label removed due to a CI failure
- name: remove outdated reviews - name: remove outdated reviews
conditions: conditions:
- base=master - base=master
@@ -50,35 +19,59 @@ pull_request_rules:
label: label:
add: add:
- automerge - automerge
- name: v1.3 backport - name: v0.16 backport
conditions: conditions:
- label=v1.3 - base=master
- label=v0.16
actions: actions:
backport: backport:
ignore_conflicts: true
branches: branches:
- v1.3 - v0.16
- name: v1.4 backport - name: v0.17 backport
conditions: conditions:
- label=v1.4 - base=master
- label=v0.17
actions: actions:
backport: backport:
ignore_conflicts: true
branches: branches:
- v1.4 - v0.17
- name: v1.5 backport - name: v0.18 backport
conditions: conditions:
- label=v1.5 - base=master
- label=v0.18
actions: actions:
backport: backport:
ignore_conflicts: true
branches: branches:
- v1.5 - v0.18
- name: v1.6 backport - name: v0.19 backport
conditions: conditions:
- label=v1.6 - base=master
- label=v0.19
actions: actions:
backport: backport:
ignore_conflicts: true
branches: branches:
- v1.6 - v0.19
- name: v0.20 backport
conditions:
- base=master
- label=v0.20
actions:
backport:
branches:
- v0.20
- name: v0.21 backport
conditions:
- base=master
- label=v0.21
actions:
backport:
branches:
- v0.21
- name: v0.22 backport
conditions:
- base=master
- label=v0.22
actions:
backport:
branches:
- v0.22

View File

@@ -1,136 +1,43 @@
os:
- osx
language: rust
rust:
- 1.37.0
install:
- source ci/rust-version.sh
- test $rust_stable = $TRAVIS_RUST_VERSION # Update .travis.yml rust version above when this fails
script:
- source ci/env.sh
- ci/publish-tarball.sh
branches: branches:
only: only:
- master - master
- /^v\d+\.\d+/ - /^v\d+\.\d+/
notifications: notifications:
email: false
slack: slack:
on_success: change on_success: change
if: NOT type = pull_request
secure: F4IjOE05MyaMOdPRL+r8qhs7jBvv4yDM3RmFKE1zNXnfUOqV4X38oQM1EI+YVsgpMQLj/pxnEB7wcTE4Bf86N6moLssEULCpvAuMVoXj4QbWdomLX+01WbFa6fLVeNQIg45NHrz2XzVBhoKOrMNnl+QI5mbR2AlS5oqsudHsXDnyLzZtd4Y5SDMdYG1zVWM01+oNNjgNfjcCGmOE/K0CnOMl6GPi3X9C34tJ19P2XT7MTDsz1/IfEF7fro2Q8DHEYL9dchJMoisXSkem5z7IDQkGzXsWdWT4NnndUvmd1MlTCE9qgoXDqRf95Qh8sB1Dz08HtvgfaosP2XjtNTfDI9BBYS15Ibw9y7PchAJE1luteNjF35EOy6OgmCLw/YpnweqfuNViBZz+yOPWXVC0kxnPIXKZ1wyH9ibeH6E4hr7a8o9SV/6SiWIlbYF+IR9jPXyTCLP/cc3sYljPWxDnhWFwFdRVIi3PbVAhVu7uWtVUO17Oc9gtGPgs/GrhOMkJfwQPXaudRJDpVZowxTX4x9kefNotlMAMRgq+Drbmgt4eEBiCNp0ITWgh17BiE1U09WS3myuduhoct85+FoVeaUkp1sxzHVtGsNQH0hcz7WcpZyOM+AwistJA/qzeEDQao5zi1eKWPbO2xAhi2rV1bDH6bPf/4lDBwLRqSiwvlWU= secure: F4IjOE05MyaMOdPRL+r8qhs7jBvv4yDM3RmFKE1zNXnfUOqV4X38oQM1EI+YVsgpMQLj/pxnEB7wcTE4Bf86N6moLssEULCpvAuMVoXj4QbWdomLX+01WbFa6fLVeNQIg45NHrz2XzVBhoKOrMNnl+QI5mbR2AlS5oqsudHsXDnyLzZtd4Y5SDMdYG1zVWM01+oNNjgNfjcCGmOE/K0CnOMl6GPi3X9C34tJ19P2XT7MTDsz1/IfEF7fro2Q8DHEYL9dchJMoisXSkem5z7IDQkGzXsWdWT4NnndUvmd1MlTCE9qgoXDqRf95Qh8sB1Dz08HtvgfaosP2XjtNTfDI9BBYS15Ibw9y7PchAJE1luteNjF35EOy6OgmCLw/YpnweqfuNViBZz+yOPWXVC0kxnPIXKZ1wyH9ibeH6E4hr7a8o9SV/6SiWIlbYF+IR9jPXyTCLP/cc3sYljPWxDnhWFwFdRVIi3PbVAhVu7uWtVUO17Oc9gtGPgs/GrhOMkJfwQPXaudRJDpVZowxTX4x9kefNotlMAMRgq+Drbmgt4eEBiCNp0ITWgh17BiE1U09WS3myuduhoct85+FoVeaUkp1sxzHVtGsNQH0hcz7WcpZyOM+AwistJA/qzeEDQao5zi1eKWPbO2xAhi2rV1bDH6bPf/4lDBwLRqSiwvlWU=
os: linux deploy:
dist: bionic - provider: s3
language: minimal access_key_id: $AWS_ACCESS_KEY_ID
secret_access_key: $AWS_SECRET_ACCESS_KEY
jobs: bucket: release.solana.com
include: region: us-west-1
- name: "Export Github Repositories" skip_cleanup: true
if: type IN (push, cron) AND branch = master acl: public_read
language: python local_dir: travis-s3-upload
git: on:
depth: false all_branches: true
script: - provider: releases
- .travis/export-github-repo.sh web3.js/ solana-web3.js api_key: $GITHUB_TOKEN
- .travis/export-github-repo.sh explorer/ explorer skip_cleanup: true
file_glob: true
- &release-artifacts file: travis-release-upload/*
if: type IN (api, cron) OR tag IS present on:
name: "macOS release artifacts" tags: true
os: osx
language: rust
rust:
- stable
install:
- source ci/rust-version.sh
- PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
- readlink -f .
script:
- source ci/env.sh
- ci/publish-tarball.sh
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
token: $GITHUB_TOKEN
skip_cleanup: true
file_glob: true
file: travis-release-upload/*
on:
tags: true
- <<: *release-artifacts
name: "Windows release artifacts"
os: windows
# Linux release artifacts are still built by ci/buildkite-secondary.yml
#- <<: *release-artifacts
# name: "Linux release artifacts"
# os: linux
# before_install:
# - sudo apt-get install libssl-dev libudev-dev
# explorer pull request
- name: "explorer"
if: type = pull_request AND branch = master
language: node_js
node_js:
- "node"
cache:
directories:
- ~/.npm
before_install:
- .travis/affects.sh explorer/ .travis || travis_terminate 0
- cd explorer
script:
- npm run build
- npm run format
# web3.js pull request
- name: "web3.js"
if: type = pull_request AND branch = master
language: node_js
node_js:
- "lts/*"
services:
- docker
cache:
directories:
- ~/.npm
before_install:
- .travis/affects.sh web3.js/ .travis || travis_terminate 0
- cd web3.js/
- source .travis/before_install.sh
script:
- ../.travis/commitlint.sh
- source .travis/script.sh
# docs pull request
- name: "docs"
if: type IN (push, pull_request) OR tag IS present
language: node_js
node_js:
- "node"
services:
- docker
cache:
directories:
- ~/.npm
before_install:
- source ci/env.sh
- .travis/channel_restriction.sh edge beta || travis_terminate 0
- .travis/affects.sh docs/ .travis || travis_terminate 0
- cd docs/
- source .travis/before_install.sh
script:
- source .travis/script.sh

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env bash
#
# Check if files in the commit range match one or more prefixes
#
# Always run the job if we are on a tagged release
if [[ -n "$TRAVIS_TAG" ]]; then
exit 0
fi
(
set -x
git diff --name-only "$TRAVIS_COMMIT_RANGE"
)
for file in $(git diff --name-only "$TRAVIS_COMMIT_RANGE"); do
for prefix in "$@"; do
if [[ $file =~ ^"$prefix" ]]; then
exit 0
fi
done
done
echo "No modifications to $*"
exit 1

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
#
# Only proceed if we are on one of the channels passed in, or a tag build
#
set -ex
[[ -n $CI_TAG ]] && exit 0
eval "$(ci/channel-info.sh)"
for acceptable_channel in "$@"; do
if [[ "$CHANNEL" == "$acceptable_channel" ]]; then
exit 0
fi
done
echo "Not running from one of the following channels: $*"
exit 1

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env bash
#
# Runs commitlint in the provided subdirectory
#
set -e
basedir=$1
if [[ -z "$basedir" ]]; then
basedir=.
fi
if [[ ! -d "$basedir" ]]; then
echo "Error: not a directory: $basedir"
exit 1
fi
if [[ ! -f "$basedir"/commitlint.config.js ]]; then
echo "Error: No commitlint configuration found"
exit 1
fi
if [[ -z $TRAVIS_COMMIT_RANGE ]]; then
echo "Error: TRAVIS_COMMIT_RANGE not defined"
exit 1
fi
cd "$basedir"
echo "Checking commits in TRAVIS_COMMIT_RANGE: $TRAVIS_COMMIT_RANGE"
while IFS= read -r line; do
echo "$line" | npx commitlint
done < <(git log "$TRAVIS_COMMIT_RANGE" --format=%s -- .)

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
#
# Exports a subdirectory into another github repository
#
set -e
if [[ -z $GITHUB_TOKEN ]]; then
echo GITHUB_TOKEN not defined
exit 1
fi
cd "$(dirname "$0")/.."
pip3 install git-filter-repo
declare subdir=$1
declare repo_name=$2
[[ -n "$subdir" ]] || {
echo "Error: subdir not specified"
exit 1
}
[[ -n "$repo_name" ]] || {
echo "Error: repo_name not specified"
exit 1
}
echo "Exporting $subdir"
set -x
rm -rf .github_export/"$repo_name"
git clone https://"$GITHUB_TOKEN"@github.com/solana-labs/"$repo_name" .github_export/"$repo_name"
git filter-repo --subdirectory-filter "$subdir" --target .github_export/"$repo_name"
git -C .github_export/"$repo_name" push https://"$GITHUB_TOKEN"@github.com/solana-labs/"$repo_name"

View File

@@ -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,137 +25,26 @@ $ 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 1,000 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
fix" using a thumb's up emoji. If you're confident your fix is exactly as
prescribed, add a reply "Fixed in COMMIT\_HASH" and mark the comment as
resolved. If you're not sure, reply "Is this what you had in mind?
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?
Recall that once your PR is opened, a notification is sent every time you push
a commit. After a reviewer adds feedback, they won't be checking on the status
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
If you want early feedback on your PR, use GitHub's "Draft Pull Request" If you want early feedback on your PR, use GitHub's "Draft Pull Request"
mechanism. Draft PRs are a convenient way to collaborate with the Solana mechanism. Draft PRs are a convenient way to collaborate with the Solana
@@ -181,68 +52,67 @@ 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 your PR is ready for a broader audience, you can transition your draft PR to a
standard PR with the click of a button. standard PR with the click of a button.
Do not add reviewers to draft PRs. GitHub doesn't automatically clear Do not add reviewers to draft PRs. GitHub doesn't automatically clear approvals
approvals when you click "Ready for Review", so a review that meant "I approve when you click "Ready for Review", so a review that meant "I approve of the
of the direction" suddenly has the appearance of "I approve of these changes." direction" suddenly has the appearance of "I approve of these changes." Instead,
Instead, add a comment that mentions the usernames that you would like a review add a comment that mentions the usernames that you would like a review from. Ask
from. Ask explicitly what you would like feedback on. explicitly what you would like feedback on.
## Rust coding conventions Rust coding conventions
---
* All Rust code is formatted using the latest version of `rustfmt`. Once * All Rust code is formatted using the latest version of `rustfmt`. Once installed, it will be
installed, it will be updated automatically when you update the compiler with updated automatically when you update the compiler with `rustup`.
`rustup`.
* All Rust code is linted with Clippy. If you'd prefer to ignore its advice, do * All Rust code is linted with Clippy. If you'd prefer to ignore its advice, do so explicitly:
so explicitly:
```rust #[allow(clippy::too_many_arguments)] ``` ```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 Design Proposals
---
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 add a design proposal, you'll need to at least propose a change the content
[Accepted Design Proposals](https://docs.solana.com/proposals/accepted-design-proposals) under the [Accepted Design
section of the Solana docs. Here's the full process: Proposals](https://docs.solana.com/book/v/master/proposals) chapter.
Here's the full process:
1. Propose a design by creating a PR that adds a markdown document to the 1. Propose a design by creating a PR that adds a markdown document to the
`docs/src/proposals` directory and references it from the [table of directory `book/src/` and references it from the [table of
contents](docs/src/SUMMARY.md). Add any relevant *maintainers* to the PR contents](book/src/SUMMARY.md). Add any relevant *maintainers* to the PR review.
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. 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 proposal, 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, submit a PR that moves the link from
the Accepted Proposals to the Implemented Proposals section. the Accepted Proposals to the Implemented Proposals section.

8261
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,73 +3,60 @@ members = [
"bench-exchange", "bench-exchange",
"bench-streamer", "bench-streamer",
"bench-tps", "bench-tps",
"accounts-bench", "banking_bench",
"banking-bench", "chacha-sys",
"banks-client",
"banks-interface",
"banks-server",
"clap-utils",
"cli-config",
"cli-output",
"client", "client",
"core", "core",
"dos", "drone",
"download-utils",
"faucet",
"frozen-abi",
"perf",
"validator", "validator",
"genesis", "genesis",
"genesis_programs",
"gossip", "gossip",
"install", "install",
"keygen", "keygen",
"ledger", "kvstore",
"ledger-tool", "ledger-tool",
"local-cluster", "local_cluster",
"logger", "logger",
"log-analyzer",
"merkle-tree", "merkle-tree",
"stake-o-matic",
"storage-bigtable",
"storage-proto",
"streamer",
"measure", "measure",
"metrics", "metrics",
"net-shaper", "programs/bpf_loader_api",
"notifier", "programs/bpf_loader_program",
"poh-bench", "programs/budget_api",
"program-test", "programs/budget_program",
"programs/secp256k1", "programs/btc_spv_program",
"programs/bpf_loader", "programs/btc_spv_api",
"programs/budget", "programs/btc_spv_bin",
"programs/config", "programs/config_api",
"programs/exchange", "programs/config_program",
"programs/failure", "programs/config_tests",
"programs/noop", "programs/exchange_api",
"programs/ownable", "programs/exchange_program",
"programs/stake", "programs/failure_program",
"programs/vest", "programs/move_loader_api",
"programs/vote", "programs/move_loader_program",
"remote-wallet", "programs/librapay_api",
"ramp-tps", "programs/noop_program",
"programs/stake_api",
"programs/stake_program",
"programs/stake_tests",
"programs/storage_api",
"programs/storage_program",
"programs/token_api",
"programs/token_program",
"programs/vote_api",
"programs/vote_program",
"replicator",
"runtime", "runtime",
"runtime/store-tool",
"sdk", "sdk",
"sdk/cargo-build-bpf", "sdk-c",
"sdk/cargo-test-bpf",
"scripts",
"stake-accounts",
"stake-monitor",
"sys-tuner",
"tokens",
"transaction-status",
"account-decoder",
"upload-perf", "upload-perf",
"net-utils", "netutil",
"version", "fixed-buf",
"vote-signer",
"cli", "cli",
"rayon-threadlimit", "rayon-threadlimit",
"watchtower",
] ]
exclude = [ exclude = [

View File

@@ -1,4 +1,4 @@
Copyright 2020 Solana Foundation. Copyright 2018 Solana Labs, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

192
README.md
View File

@@ -1,17 +1,76 @@
<p align="center">
<a href="https://solana.com">
<img alt="Solana" src="https://i.imgur.com/OMnvVEz.png" width="250" />
</a>
</p>
[![Solana crate](https://img.shields.io/crates/v/solana-core.svg)](https://crates.io/crates/solana-core) [![Solana crate](https://img.shields.io/crates/v/solana-core.svg)](https://crates.io/crates/solana-core)
[![Solana documentation](https://docs.rs/solana-core/badge.svg)](https://docs.rs/solana-core) [![Solana documentation](https://docs.rs/solana-core/badge.svg)](https://docs.rs/solana-core)
[![Build status](https://badge.buildkite.com/8cc350de251d61483db98bdfc895b9ea0ac8ffa4a32ee850ed.svg?branch=master)](https://buildkite.com/solana-labs/solana/builds?branch=master) [![Build status](https://badge.buildkite.com/8cc350de251d61483db98bdfc895b9ea0ac8ffa4a32ee850ed.svg?branch=master)](https://buildkite.com/solana-labs/solana/builds?branch=master)
[![codecov](https://codecov.io/gh/solana-labs/solana/branch/master/graph/badge.svg)](https://codecov.io/gh/solana-labs/solana) [![codecov](https://codecov.io/gh/solana-labs/solana/branch/master/graph/badge.svg)](https://codecov.io/gh/solana-labs/solana)
# Building Blockchain Rebuilt for Scale
===
## **1. Install rustc, cargo and rustfmt.** Solana&trade; is a new blockchain architecture built from the ground up for scale. The architecture supports
up to 710 thousand transactions per second on a gigabit network.
Disclaimer
===
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.
Introduction
===
It's possible for a centralized database to process 710,000 transactions per second on a standard gigabit network if the transactions are, on average, no more than 176 bytes. A centralized database can also replicate itself and maintain high availability without significantly compromising that transaction rate using the distributed system technique known as Optimistic Concurrency Control [\[H.T.Kung, J.T.Robinson (1981)\]](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.65.4735). At Solana, we're demonstrating that these same theoretical limits apply just as well to blockchain on an adversarial network. The key ingredient? Finding a way to share time when nodes can't trust one-another. Once nodes can trust time, suddenly ~40 years of distributed systems research becomes applicable to blockchain!
> Perhaps the most striking difference between algorithms obtained by our method and ones based upon timeout is that using timeout produces a traditional distributed algorithm in which the processes operate asynchronously, while our method produces a globally synchronous one in which every process does the same thing at (approximately) the same time. Our method seems to contradict the whole purpose of distributed processing, which is to permit different processes to operate independently and perform different functions. However, if a distributed system is really a single system, then the processes must be synchronized in some way. Conceptually, the easiest way to synchronize processes is to get them all to do the same thing at the same time. Therefore, our method is used to implement a kernel that performs the necessary synchronization--for example, making sure that two different processes do not try to modify a file at the same time. Processes might spend only a small fraction of their time executing the synchronizing kernel; the rest of the time, they can operate independently--e.g., accessing different files. This is an approach we have advocated even when fault-tolerance is not required. The method's basic simplicity makes it easier to understand the precise properties of a system, which is crucial if one is to know just how fault-tolerant the system is. [\[L.Lamport (1984)\]](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.1078)
Furthermore, and much to our surprise, it can be implemented using a mechanism that has existed in Bitcoin since day one. The Bitcoin feature is called nLocktime and it can be used to postdate transactions using block height instead of a timestamp. As a Bitcoin client, you'd use block height instead of a timestamp if you don't trust the network. Block height turns out to be an instance of what's being called a Verifiable Delay Function in cryptography circles. It's a cryptographically secure way to say time has passed. In Solana, we use a far more granular verifiable delay function, a SHA 256 hash chain, to checkpoint the ledger and coordinate consensus. With it, we implement Optimistic Concurrency Control and are now well en route towards that theoretical limit of 710,000 transactions per second.
Architecture
===
Before you jump into the code, review the online book [Solana: Blockchain Rebuilt for Scale](https://docs.solana.com/book/).
(The _latest_ development version of the online book is also [available here](https://docs.solana.com/book/v/master/).)
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
beta channels. Note that these pre-release binaries may be less stable than an
official release.
### Edge channel
#### 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
#### Linux (x86_64-unknown-linux-gnu)
* [solana.tar.bz2](http://release.solana.com/beta/solana-release-x86_64-unknown-linux-gnu.tar.bz2)
* [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)
* [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)
* [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
===
Building
---
Install rustc, cargo and rustfmt:
```bash ```bash
$ curl https://sh.rustup.rs -sSf | sh $ curl https://sh.rustup.rs -sSf | sh
@@ -19,7 +78,7 @@ $ source $HOME/.cargo/env
$ rustup component add rustfmt $ rustup component add rustfmt
``` ```
Please sure you are always using the latest stable rust version by running: If your rustc version is lower than 1.37.0, please update it:
```bash ```bash
$ rustup update $ rustup update
@@ -28,44 +87,118 @@ $ 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 make
``` ```
## **2. Download the source code.** Download the source code:
```bash ```bash
$ git clone https://github.com/solana-labs/solana.git $ git clone https://github.com/solana-labs/solana.git
$ cd solana $ cd solana
``` ```
## **3. Build.** Build
```bash ```bash
$ cargo build $ cargo build
``` ```
## **4. Run a minimal local cluster.** Then to run a minimal local cluster
```bash ```bash
$ ./run.sh $ ./run.sh
``` ```
# Testing Testing
---
**Run the test suite:** Run the test suite:
```bash ```bash
$ cargo test $ cargo test
``` ```
### Starting a local testnet Local Testnet
Start your own testnet locally, instructions are in the [online docs](https://docs.solana.com/cluster/bench-tps). ---
### Accessing the remote development cluster Start your own testnet locally, instructions are in the book [Solana: Blockchain Rebuild for Scale: Getting Started](https://docs.solana.com/book/getting-started).
* `devnet` - stable public cluster for development accessible via
devnet.solana.com. Runs 24/7. Learn more about the [public clusters](https://docs.solana.com/clusters)
# Benchmarking Remote Testnets
---
We maintain several testnets:
* `testnet` - public stable testnet accessible via testnet.solana.com. Runs 24/7
* `testnet-beta` - public beta channel testnet accessible via beta.testnet.solana.com. Runs 24/7
* `testnet-edge` - public edge channel testnet accessible via edge.testnet.solana.com. Runs 24/7
## Deploy process
They are deployed with the `ci/testnet-manager.sh` script through a list of [scheduled
buildkite jobs](https://buildkite.com/solana-labs/testnet-management/settings/schedules).
Each testnet can be manually manipulated from buildkite as well.
## How do I reset the testnet?
Manually trigger the [testnet-management](https://buildkite.com/solana-labs/testnet-management) pipeline
and when prompted select the desired testnet
## How can I scale the tx generation rate?
Increase the TX rate by increasing the number of cores on the client machine which is running
`bench-tps` or run multiple clients. Decrease by lowering cores or using the rayon env
variable `RAYON_NUM_THREADS=<xx>`
## How can I test a change on the testnet?
Currently, a merged PR is the only way to test a change on the testnet. But you
can run your own testnet using the scripts in the `net/` directory.
## Adjusting the number of clients or validators on the testnet
Edit `ci/testnet-manager.sh`
## 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
---
First install the nightly build of rustc. `cargo bench` requires use of the First install the nightly build of rustc. `cargo bench` requires use of the
unstable features only available in the nightly build. unstable features only available in the nightly build.
@@ -80,11 +213,13 @@ Run the benchmarks:
$ cargo +nightly bench $ cargo +nightly bench
``` ```
# Release Process Release Process
---
The release process for this project is described [here](RELEASE.md). The release process for this project is described [here](RELEASE.md).
# Code coverage
Code coverage
---
To generate code coverage statistics: To generate code coverage statistics:
@@ -93,6 +228,7 @@ $ scripts/coverage.sh
$ open target/cov/lcov-local/index.html $ open target/cov/lcov-local/index.html
``` ```
Why coverage? While most see coverage as a code quality metric, we see it primarily as a developer Why coverage? While most see coverage as a code quality metric, we see it primarily as a developer
productivity metric. When a developer makes a change to the codebase, presumably it's a *solution* to productivity metric. When a developer makes a change to the codebase, presumably it's a *solution* to
some problem. Our unit-test suite is how we encode the set of *problems* the codebase solves. Running some problem. Our unit-test suite is how we encode the set of *problems* the codebase solves. Running
@@ -104,9 +240,3 @@ problem is solved by this code?" On the other hand, if a test does fail and you
better way to solve the same problem, a Pull Request with your solution would most certainly be better way to solve the same problem, a Pull Request with your solution would most certainly be
welcome! Likewise, if rewriting a test can better communicate what code it's protecting, please 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.
Any content produced by Solana, or developer resources that Solana provides, are for educational and inspiration purposes only. Solana does not encourage, induce or sanction the deployment of any such applications in violation of applicable laws or regulations.

View File

@@ -76,20 +76,21 @@ There are three release channels that map to branches as follows:
git push -u origin <branchname> git push -u origin <branchname>
``` ```
Alternatively use the Github UI. ### Update master branch with the next version
### Update master branch to the next release minor version
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: 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:
``` ```
$ scripts/increment-cargo-version.sh minor scripts/increment-cargo-version.sh minor
$ ./scripts/cargo-for-all-lock-files.sh update
``` ```
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: 1. Push all the changed Cargo.toml and Cargo.lock files to the `master` branch with something like:
``` ```
git co -b version_update git co -b version_update
git ls-files -m | xargs git add git ls-files -m | xargs git add
git commit -m 'Bump version to X.Y+1.0' git commit -m 'Update Cargo.toml versions from X.Y to X.Y+1'
git push -u origin version_update 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`: 1. Confirm that your freshly cut release branch is shown as `BETA_CHANNEL` and the previous release branch as `STABLE_CHANNEL`:
@@ -101,56 +102,103 @@ Alternatively use the Github UI.
### Create the Release Tag on GitHub ### Create the Release Tag on GitHub
1. Go to [GitHub Releases](https://github.com/solana-labs/solana/releases) for tagging a release. 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`.
1. If the Cargo.toml version field is **0.12.3**, then the release tag must be **v0.12.3** 1. If the Cargo.toml verion field is **0.12.3**, then the release tag must be **v0.12.3**
1. Make sure the Target Branch field matches the branch you want to make a release on. 1. Make sure the Target Branch field matches the branch you want to make a release on.
1. If you want to release v0.12.0, the target branch must be v0.12 1. If you want to release v0.12.0, the target branch must be v0.12
1. Fill the release notes. 1. If this is the first release on the branch (e.g. v0.13.**0**), paste in [this
1. If this is the first release on the branch (e.g. v0.13.**0**), paste in [this
template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md). Engineering Lead can provide summary contents for release notes if needed. template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md). Engineering Lead can provide summary contents for release notes if needed.
1. If this is a patch release, review all the commits since the previous release on this branch and add details as needed. 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. Click "Save Draft", then confirm the release notes look good and the tag name and branch are correct.
1. Ensure all desired commits (usually backports) are landed on the branch by now.
1. Ensure the release is marked **"This is a pre-release"**. This flag will need to be be removed manually after confirming the the Linux binary artifacts appear at a later step.
1. Go back into edit the release and click "Publish release" while being marked as a pre-release.
1. Confirm there is new git tag with intended version number at the intended revision after running `git fetch` locally.
### Update release branch with the next patch version ### Update release branch with the next patch version
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: 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:
``` ```
$ scripts/increment-cargo-version.sh patch scripts/increment-cargo-version.sh patch
$ ./scripts/cargo-for-all-lock-files.sh tree
``` ```
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 **release branch** with something like: 1. Push all the changed Cargo.toml and Cargo.lock files to the **release branch** with something like:
``` ```
git co -b version_update origin/vX.Y git co -b version_update
git add -u git ls-files -m | xargs git add
git commit -m 'Bump version to X.Y.Z+1' git commit -m 'Update Cargo.toml versions from X.Y.Z to X.Y.Z+1'
git push -u <user-remote> version_update git push -u origin version_update
``` ```
1. Open a PR against origin/vX.Y and then merge the PR after passing CI.
### Prepare for the next release
1. Go to [GitHub Releases](https://github.com/solana-labs/solana/releases) and create a new draft release for `X.Y.Z+1` with empty release notes. This allows people to incrementally add new release notes until it's time for the next release
1. Also, point the branch field to the same branch and mark the relese as **"This is a pre-release"**.
1. Go to the [Github Milestones](https://github.com/solana-labs/solana/milestones). Create a new milestone for the `X.Y.Z+1`, move over
unresolved issues still in the `X.Y.Z` milestone, then close the `X.Y.Z` milestone.
### Verify release automation success ### Verify release automation success
Go to [Solana Releases](https://github.com/solana-labs/solana/releases) and click on the latest release that you just published. 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.
Verify that all of the build artifacts are present, then the uncheck **"This is a pre-release"** for the release. 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.
Build artifacts can take up to 60 minutes after creating the tag before ### Update documentation
appearing. To check for progress: TODO: Documentation update procedure is WIP as we move to gitbook
* The `solana-secondary` Buildkite pipeline handles creating the Linux release artifacts and updated crates. Look for a job under the tag name of the release: https://buildkite.com/solana-labs/solana-secondary.
* The macOS and Windows release artifacts are produced by Travis CI: https://travis-ci.com/github/solana-labs/solana/branches
[Crates.io](https://crates.io/crates/solana) should have an updated Solana version. This can take 2-3 hours, and sometimes fails in the `solana-secondary` job. Document the new recommended version by updating `book/src/running-replicator.md` and `book/src/validator-testnet.md` on the release (beta) branch to point at the `solana-install` for the upcoming release version.
If this happens and the error is non-fatal, click "Retry" on the "publish crate" job
### Update software on devnet.solana.com/testnet.solana.com/mainnet-beta.solana.com #### Publish updated Book
See the documentation at https://github.com/solana-labs/cluster-ops/ We maintain three copies of the "book" as official documentation:
1) "Book" is the documentation for the latest official release. This should get manually updated whenever a new release is made. It is published here:
https://solana-labs.github.io/book/
2) "Book-edge" tracks the tip of the master branch and updates automatically.
https://solana-labs.github.io/book-edge/
3) "Book-beta" tracks the tip of the beta branch and updates automatically.
https://solana-labs.github.io/book-beta/
To manually trigger an update of the "Book", create a new job of the manual-update-book pipeline.
Set the tag of the latest release as the PUBLISH_BOOK_TAG environment variable.
```bash
PUBLISH_BOOK_TAG=v0.16.6
```
https://buildkite.com/solana-labs/manual-update-book
### Update software on testnet.solana.com
The testnet running on testnet.solana.com is set to use a fixed release tag
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
testnet.solana.com is available

View File

@@ -1,30 +0,0 @@
[package]
name = "solana-account-decoder"
version = "1.5.1"
description = "Solana account decoder"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana"
homepage = "https://solana.com/"
license = "Apache-2.0"
edition = "2018"
[dependencies]
base64 = "0.12.3"
bincode = "1.3.1"
bs58 = "0.3.1"
bv = "0.11.1"
Inflector = "0.11.4"
lazy_static = "1.4.0"
serde = "1.0.112"
serde_derive = "1.0.103"
serde_json = "1.0.56"
solana-config-program = { path = "../programs/config", version = "1.5.1" }
solana-sdk = { path = "../sdk", version = "1.5.1" }
solana-stake-program = { path = "../programs/stake", version = "1.5.1" }
solana-vote-program = { path = "../programs/vote", version = "1.5.1" }
spl-token-v2-0 = { package = "spl-token", version = "=3.0.1", features = ["no-entrypoint"] }
thiserror = "1.0"
zstd = "0.5.1"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@@ -1,233 +0,0 @@
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate serde_derive;
pub mod parse_account_data;
pub mod parse_config;
pub mod parse_nonce;
pub mod parse_stake;
pub mod parse_sysvar;
pub mod parse_token;
pub mod parse_vote;
pub mod validator_info;
use {
crate::parse_account_data::{parse_account_data, AccountAdditionalData, ParsedAccount},
solana_sdk::{account::Account, clock::Epoch, fee_calculator::FeeCalculator, pubkey::Pubkey},
std::{
io::{Read, Write},
str::FromStr,
},
};
pub type StringAmount = String;
/// A duplicate representation of an Account for pretty JSON serialization
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UiAccount {
pub lamports: u64,
pub data: UiAccountData,
pub owner: String,
pub executable: bool,
pub rent_epoch: Epoch,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", untagged)]
pub enum UiAccountData {
LegacyBinary(String), // Legacy. Retained for RPC backwards compatibility
Json(ParsedAccount),
Binary(String, UiAccountEncoding),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum UiAccountEncoding {
Binary, // Legacy. Retained for RPC backwards compatibility
Base58,
Base64,
JsonParsed,
#[serde(rename = "base64+zstd")]
Base64Zstd,
}
impl UiAccount {
pub fn encode(
pubkey: &Pubkey,
account: Account,
encoding: UiAccountEncoding,
additional_data: Option<AccountAdditionalData>,
data_slice_config: Option<UiDataSliceConfig>,
) -> Self {
let data = match encoding {
UiAccountEncoding::Binary => UiAccountData::LegacyBinary(
bs58::encode(slice_data(&account.data, data_slice_config)).into_string(),
),
UiAccountEncoding::Base58 => UiAccountData::Binary(
bs58::encode(slice_data(&account.data, data_slice_config)).into_string(),
encoding,
),
UiAccountEncoding::Base64 => UiAccountData::Binary(
base64::encode(slice_data(&account.data, data_slice_config)),
encoding,
),
UiAccountEncoding::Base64Zstd => {
let mut encoder = zstd::stream::write::Encoder::new(Vec::new(), 0).unwrap();
match encoder
.write_all(slice_data(&account.data, data_slice_config))
.and_then(|()| encoder.finish())
{
Ok(zstd_data) => UiAccountData::Binary(base64::encode(zstd_data), encoding),
Err(_) => UiAccountData::Binary(
base64::encode(slice_data(&account.data, data_slice_config)),
UiAccountEncoding::Base64,
),
}
}
UiAccountEncoding::JsonParsed => {
if let Ok(parsed_data) =
parse_account_data(pubkey, &account.owner, &account.data, additional_data)
{
UiAccountData::Json(parsed_data)
} else {
UiAccountData::Binary(base64::encode(&account.data), UiAccountEncoding::Base64)
}
}
};
UiAccount {
lamports: account.lamports,
data,
owner: account.owner.to_string(),
executable: account.executable,
rent_epoch: account.rent_epoch,
}
}
pub fn decode(&self) -> Option<Account> {
let data = match &self.data {
UiAccountData::Json(_) => None,
UiAccountData::LegacyBinary(blob) => bs58::decode(blob).into_vec().ok(),
UiAccountData::Binary(blob, encoding) => match encoding {
UiAccountEncoding::Base58 => bs58::decode(blob).into_vec().ok(),
UiAccountEncoding::Base64 => base64::decode(blob).ok(),
UiAccountEncoding::Base64Zstd => base64::decode(blob)
.ok()
.map(|zstd_data| {
let mut data = vec![];
zstd::stream::read::Decoder::new(zstd_data.as_slice())
.and_then(|mut reader| reader.read_to_end(&mut data))
.map(|_| data)
.ok()
})
.flatten(),
UiAccountEncoding::Binary | UiAccountEncoding::JsonParsed => None,
},
}?;
Some(Account {
lamports: self.lamports,
data,
owner: Pubkey::from_str(&self.owner).ok()?,
executable: self.executable,
rent_epoch: self.rent_epoch,
})
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiFeeCalculator {
pub lamports_per_signature: StringAmount,
}
impl From<FeeCalculator> for UiFeeCalculator {
fn from(fee_calculator: FeeCalculator) -> Self {
Self {
lamports_per_signature: fee_calculator.lamports_per_signature.to_string(),
}
}
}
impl Default for UiFeeCalculator {
fn default() -> Self {
Self {
lamports_per_signature: "0".to_string(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UiDataSliceConfig {
pub offset: usize,
pub length: usize,
}
fn slice_data(data: &[u8], data_slice_config: Option<UiDataSliceConfig>) -> &[u8] {
if let Some(UiDataSliceConfig { offset, length }) = data_slice_config {
if offset >= data.len() {
&[]
} else if length > data.len() - offset {
&data[offset..]
} else {
&data[offset..offset + length]
}
} else {
data
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_slice_data() {
let data = vec![1, 2, 3, 4, 5];
let slice_config = Some(UiDataSliceConfig {
offset: 0,
length: 5,
});
assert_eq!(slice_data(&data, slice_config), &data[..]);
let slice_config = Some(UiDataSliceConfig {
offset: 0,
length: 10,
});
assert_eq!(slice_data(&data, slice_config), &data[..]);
let slice_config = Some(UiDataSliceConfig {
offset: 1,
length: 2,
});
assert_eq!(slice_data(&data, slice_config), &data[1..3]);
let slice_config = Some(UiDataSliceConfig {
offset: 10,
length: 2,
});
assert_eq!(slice_data(&data, slice_config), &[] as &[u8]);
}
#[test]
fn test_base64_zstd() {
let encoded_account = UiAccount::encode(
&Pubkey::default(),
Account {
data: vec![0; 1024],
..Account::default()
},
UiAccountEncoding::Base64Zstd,
None,
None,
);
assert!(matches!(
encoded_account.data,
UiAccountData::Binary(_, UiAccountEncoding::Base64Zstd)
));
let decoded_account = encoded_account.decode().unwrap();
assert_eq!(decoded_account.data, vec![0; 1024]);
}
}

View File

@@ -1,145 +0,0 @@
use crate::{
parse_config::parse_config,
parse_nonce::parse_nonce,
parse_stake::parse_stake,
parse_sysvar::parse_sysvar,
parse_token::{parse_token, spl_token_id_v2_0},
parse_vote::parse_vote,
};
use inflector::Inflector;
use serde_json::Value;
use solana_sdk::{instruction::InstructionError, pubkey::Pubkey, system_program, sysvar};
use std::collections::HashMap;
use thiserror::Error;
lazy_static! {
static ref CONFIG_PROGRAM_ID: Pubkey = solana_config_program::id();
static ref STAKE_PROGRAM_ID: Pubkey = solana_stake_program::id();
static ref SYSTEM_PROGRAM_ID: Pubkey = system_program::id();
static ref SYSVAR_PROGRAM_ID: Pubkey = sysvar::id();
static ref TOKEN_PROGRAM_ID: Pubkey = spl_token_id_v2_0();
static ref VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id();
pub static ref PARSABLE_PROGRAM_IDS: HashMap<Pubkey, ParsableAccount> = {
let mut m = HashMap::new();
m.insert(*CONFIG_PROGRAM_ID, ParsableAccount::Config);
m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce);
m.insert(*TOKEN_PROGRAM_ID, ParsableAccount::SplToken);
m.insert(*STAKE_PROGRAM_ID, ParsableAccount::Stake);
m.insert(*SYSVAR_PROGRAM_ID, ParsableAccount::Sysvar);
m.insert(*VOTE_PROGRAM_ID, ParsableAccount::Vote);
m
};
}
#[derive(Error, Debug)]
pub enum ParseAccountError {
#[error("{0:?} account not parsable")]
AccountNotParsable(ParsableAccount),
#[error("Program not parsable")]
ProgramNotParsable,
#[error("Additional data required to parse: {0}")]
AdditionalDataMissing(String),
#[error("Instruction error")]
InstructionError(#[from] InstructionError),
#[error("Serde json error")]
SerdeJsonError(#[from] serde_json::error::Error),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ParsedAccount {
pub program: String,
pub parsed: Value,
pub space: u64,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ParsableAccount {
Config,
Nonce,
SplToken,
Stake,
Sysvar,
Vote,
}
#[derive(Default)]
pub struct AccountAdditionalData {
pub spl_token_decimals: Option<u8>,
}
pub fn parse_account_data(
pubkey: &Pubkey,
program_id: &Pubkey,
data: &[u8],
additional_data: Option<AccountAdditionalData>,
) -> Result<ParsedAccount, ParseAccountError> {
let program_name = PARSABLE_PROGRAM_IDS
.get(program_id)
.ok_or(ParseAccountError::ProgramNotParsable)?;
let additional_data = additional_data.unwrap_or_default();
let parsed_json = match program_name {
ParsableAccount::Config => serde_json::to_value(parse_config(data, pubkey)?)?,
ParsableAccount::Nonce => serde_json::to_value(parse_nonce(data)?)?,
ParsableAccount::SplToken => {
serde_json::to_value(parse_token(data, additional_data.spl_token_decimals)?)?
}
ParsableAccount::Stake => serde_json::to_value(parse_stake(data)?)?,
ParsableAccount::Sysvar => serde_json::to_value(parse_sysvar(data, pubkey)?)?,
ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?,
};
Ok(ParsedAccount {
program: format!("{:?}", program_name).to_kebab_case(),
parsed: parsed_json,
space: data.len() as u64,
})
}
#[cfg(test)]
mod test {
use super::*;
use solana_sdk::nonce::{
state::{Data, Versions},
State,
};
use solana_vote_program::vote_state::{VoteState, VoteStateVersions};
#[test]
fn test_parse_account_data() {
let account_pubkey = solana_sdk::pubkey::new_rand();
let other_program = solana_sdk::pubkey::new_rand();
let data = vec![0; 4];
assert!(parse_account_data(&account_pubkey, &other_program, &data, None).is_err());
let vote_state = VoteState::default();
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
let versioned = VoteStateVersions::new_current(vote_state);
VoteState::serialize(&versioned, &mut vote_account_data).unwrap();
let parsed = parse_account_data(
&account_pubkey,
&solana_vote_program::id(),
&vote_account_data,
None,
)
.unwrap();
assert_eq!(parsed.program, "vote".to_string());
assert_eq!(parsed.space, VoteState::size_of() as u64);
let nonce_data = Versions::new_current(State::Initialized(Data::default()));
let nonce_account_data = bincode::serialize(&nonce_data).unwrap();
let parsed = parse_account_data(
&account_pubkey,
&system_program::id(),
&nonce_account_data,
None,
)
.unwrap();
assert_eq!(parsed.program, "nonce".to_string());
assert_eq!(parsed.space, State::size() as u64);
}
}

View File

@@ -1,146 +0,0 @@
use crate::{
parse_account_data::{ParsableAccount, ParseAccountError},
validator_info,
};
use bincode::deserialize;
use serde_json::Value;
use solana_config_program::{get_config_data, ConfigKeys};
use solana_sdk::pubkey::Pubkey;
use solana_stake_program::config::Config as StakeConfig;
pub fn parse_config(data: &[u8], pubkey: &Pubkey) -> Result<ConfigAccountType, ParseAccountError> {
let parsed_account = if pubkey == &solana_stake_program::config::id() {
get_config_data(data)
.ok()
.and_then(|data| deserialize::<StakeConfig>(data).ok())
.map(|config| ConfigAccountType::StakeConfig(config.into()))
} else {
deserialize::<ConfigKeys>(data).ok().and_then(|key_list| {
if !key_list.keys.is_empty() && key_list.keys[0].0 == validator_info::id() {
parse_config_data::<String>(data, key_list.keys).and_then(|validator_info| {
Some(ConfigAccountType::ValidatorInfo(UiConfig {
keys: validator_info.keys,
config_data: serde_json::from_str(&validator_info.config_data).ok()?,
}))
})
} else {
None
}
})
};
parsed_account.ok_or(ParseAccountError::AccountNotParsable(
ParsableAccount::Config,
))
}
fn parse_config_data<T>(data: &[u8], keys: Vec<(Pubkey, bool)>) -> Option<UiConfig<T>>
where
T: serde::de::DeserializeOwned,
{
let config_data: T = deserialize(&get_config_data(data).ok()?).ok()?;
let keys = keys
.iter()
.map(|key| UiConfigKey {
pubkey: key.0.to_string(),
signer: key.1,
})
.collect();
Some(UiConfig { keys, config_data })
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
pub enum ConfigAccountType {
StakeConfig(UiStakeConfig),
ValidatorInfo(UiConfig<Value>),
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiConfigKey {
pub pubkey: String,
pub signer: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiStakeConfig {
pub warmup_cooldown_rate: f64,
pub slash_penalty: u8,
}
impl From<StakeConfig> for UiStakeConfig {
fn from(config: StakeConfig) -> Self {
Self {
warmup_cooldown_rate: config.warmup_cooldown_rate,
slash_penalty: config.slash_penalty,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiConfig<T> {
pub keys: Vec<UiConfigKey>,
pub config_data: T,
}
#[cfg(test)]
mod test {
use super::*;
use crate::validator_info::ValidatorInfo;
use serde_json::json;
use solana_config_program::create_config_account;
#[test]
fn test_parse_config() {
let stake_config = StakeConfig {
warmup_cooldown_rate: 0.25,
slash_penalty: 50,
};
let stake_config_account = create_config_account(vec![], &stake_config, 10);
assert_eq!(
parse_config(
&stake_config_account.data,
&solana_stake_program::config::id()
)
.unwrap(),
ConfigAccountType::StakeConfig(UiStakeConfig {
warmup_cooldown_rate: 0.25,
slash_penalty: 50,
}),
);
let validator_info = ValidatorInfo {
info: serde_json::to_string(&json!({
"name": "Solana",
}))
.unwrap(),
};
let info_pubkey = solana_sdk::pubkey::new_rand();
let validator_info_config_account = create_config_account(
vec![(validator_info::id(), false), (info_pubkey, true)],
&validator_info,
10,
);
assert_eq!(
parse_config(&validator_info_config_account.data, &info_pubkey).unwrap(),
ConfigAccountType::ValidatorInfo(UiConfig {
keys: vec![
UiConfigKey {
pubkey: validator_info::id().to_string(),
signer: false,
},
UiConfigKey {
pubkey: info_pubkey.to_string(),
signer: true,
}
],
config_data: serde_json::from_str(r#"{"name":"Solana"}"#).unwrap(),
}),
);
let bad_data = vec![0; 4];
assert!(parse_config(&bad_data, &info_pubkey).is_err());
}
}

View File

@@ -1,67 +0,0 @@
use crate::{parse_account_data::ParseAccountError, UiFeeCalculator};
use solana_sdk::{
instruction::InstructionError,
nonce::{state::Versions, State},
};
pub fn parse_nonce(data: &[u8]) -> Result<UiNonceState, ParseAccountError> {
let nonce_state: Versions = bincode::deserialize(data)
.map_err(|_| ParseAccountError::from(InstructionError::InvalidAccountData))?;
let nonce_state = nonce_state.convert_to_current();
match nonce_state {
State::Uninitialized => Ok(UiNonceState::Uninitialized),
State::Initialized(data) => Ok(UiNonceState::Initialized(UiNonceData {
authority: data.authority.to_string(),
blockhash: data.blockhash.to_string(),
fee_calculator: data.fee_calculator.into(),
})),
}
}
/// A duplicate representation of NonceState for pretty JSON serialization
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
pub enum UiNonceState {
Uninitialized,
Initialized(UiNonceData),
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiNonceData {
pub authority: String,
pub blockhash: String,
pub fee_calculator: UiFeeCalculator,
}
#[cfg(test)]
mod test {
use super::*;
use solana_sdk::{
hash::Hash,
nonce::{
state::{Data, Versions},
State,
},
pubkey::Pubkey,
};
#[test]
fn test_parse_nonce() {
let nonce_data = Versions::new_current(State::Initialized(Data::default()));
let nonce_account_data = bincode::serialize(&nonce_data).unwrap();
assert_eq!(
parse_nonce(&nonce_account_data).unwrap(),
UiNonceState::Initialized(UiNonceData {
authority: Pubkey::default().to_string(),
blockhash: Hash::default().to_string(),
fee_calculator: UiFeeCalculator {
lamports_per_signature: 0.to_string(),
},
}),
);
let bad_data = vec![0; 4];
assert!(parse_nonce(&bad_data).is_err());
}
}

View File

@@ -1,234 +0,0 @@
use crate::{
parse_account_data::{ParsableAccount, ParseAccountError},
StringAmount,
};
use bincode::deserialize;
use solana_sdk::clock::{Epoch, UnixTimestamp};
use solana_stake_program::stake_state::{Authorized, Delegation, Lockup, Meta, Stake, StakeState};
pub fn parse_stake(data: &[u8]) -> Result<StakeAccountType, ParseAccountError> {
let stake_state: StakeState = deserialize(data)
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Stake))?;
let parsed_account = match stake_state {
StakeState::Uninitialized => StakeAccountType::Uninitialized,
StakeState::Initialized(meta) => StakeAccountType::Initialized(UiStakeAccount {
meta: meta.into(),
stake: None,
}),
StakeState::Stake(meta, stake) => StakeAccountType::Delegated(UiStakeAccount {
meta: meta.into(),
stake: Some(stake.into()),
}),
StakeState::RewardsPool => StakeAccountType::RewardsPool,
};
Ok(parsed_account)
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
pub enum StakeAccountType {
Uninitialized,
Initialized(UiStakeAccount),
Delegated(UiStakeAccount),
RewardsPool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiStakeAccount {
pub meta: UiMeta,
pub stake: Option<UiStake>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiMeta {
pub rent_exempt_reserve: StringAmount,
pub authorized: UiAuthorized,
pub lockup: UiLockup,
}
impl From<Meta> for UiMeta {
fn from(meta: Meta) -> Self {
Self {
rent_exempt_reserve: meta.rent_exempt_reserve.to_string(),
authorized: meta.authorized.into(),
lockup: meta.lockup.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiLockup {
pub unix_timestamp: UnixTimestamp,
pub epoch: Epoch,
pub custodian: String,
}
impl From<Lockup> for UiLockup {
fn from(lockup: Lockup) -> Self {
Self {
unix_timestamp: lockup.unix_timestamp,
epoch: lockup.epoch,
custodian: lockup.custodian.to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiAuthorized {
pub staker: String,
pub withdrawer: String,
}
impl From<Authorized> for UiAuthorized {
fn from(authorized: Authorized) -> Self {
Self {
staker: authorized.staker.to_string(),
withdrawer: authorized.withdrawer.to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiStake {
pub delegation: UiDelegation,
pub credits_observed: u64,
}
impl From<Stake> for UiStake {
fn from(stake: Stake) -> Self {
Self {
delegation: stake.delegation.into(),
credits_observed: stake.credits_observed,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiDelegation {
pub voter: String,
pub stake: StringAmount,
pub activation_epoch: StringAmount,
pub deactivation_epoch: StringAmount,
pub warmup_cooldown_rate: f64,
}
impl From<Delegation> for UiDelegation {
fn from(delegation: Delegation) -> Self {
Self {
voter: delegation.voter_pubkey.to_string(),
stake: delegation.stake.to_string(),
activation_epoch: delegation.activation_epoch.to_string(),
deactivation_epoch: delegation.deactivation_epoch.to_string(),
warmup_cooldown_rate: delegation.warmup_cooldown_rate,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use bincode::serialize;
#[test]
fn test_parse_stake() {
let stake_state = StakeState::Uninitialized;
let stake_data = serialize(&stake_state).unwrap();
assert_eq!(
parse_stake(&stake_data).unwrap(),
StakeAccountType::Uninitialized
);
let pubkey = solana_sdk::pubkey::new_rand();
let custodian = solana_sdk::pubkey::new_rand();
let authorized = Authorized::auto(&pubkey);
let lockup = Lockup {
unix_timestamp: 0,
epoch: 1,
custodian,
};
let meta = Meta {
rent_exempt_reserve: 42,
authorized,
lockup,
};
let stake_state = StakeState::Initialized(meta);
let stake_data = serialize(&stake_state).unwrap();
assert_eq!(
parse_stake(&stake_data).unwrap(),
StakeAccountType::Initialized(UiStakeAccount {
meta: UiMeta {
rent_exempt_reserve: 42.to_string(),
authorized: UiAuthorized {
staker: pubkey.to_string(),
withdrawer: pubkey.to_string(),
},
lockup: UiLockup {
unix_timestamp: 0,
epoch: 1,
custodian: custodian.to_string(),
}
},
stake: None,
})
);
let voter_pubkey = solana_sdk::pubkey::new_rand();
let stake = Stake {
delegation: Delegation {
voter_pubkey,
stake: 20,
activation_epoch: 2,
deactivation_epoch: std::u64::MAX,
warmup_cooldown_rate: 0.25,
},
credits_observed: 10,
};
let stake_state = StakeState::Stake(meta, stake);
let stake_data = serialize(&stake_state).unwrap();
assert_eq!(
parse_stake(&stake_data).unwrap(),
StakeAccountType::Delegated(UiStakeAccount {
meta: UiMeta {
rent_exempt_reserve: 42.to_string(),
authorized: UiAuthorized {
staker: pubkey.to_string(),
withdrawer: pubkey.to_string(),
},
lockup: UiLockup {
unix_timestamp: 0,
epoch: 1,
custodian: custodian.to_string(),
}
},
stake: Some(UiStake {
delegation: UiDelegation {
voter: voter_pubkey.to_string(),
stake: 20.to_string(),
activation_epoch: 2.to_string(),
deactivation_epoch: std::u64::MAX.to_string(),
warmup_cooldown_rate: 0.25,
},
credits_observed: 10,
})
})
);
let stake_state = StakeState::RewardsPool;
let stake_data = serialize(&stake_state).unwrap();
assert_eq!(
parse_stake(&stake_data).unwrap(),
StakeAccountType::RewardsPool
);
let bad_data = vec![1, 2, 3, 4];
assert!(parse_stake(&bad_data).is_err());
}
}

View File

@@ -1,329 +0,0 @@
use crate::{
parse_account_data::{ParsableAccount, ParseAccountError},
StringAmount, UiFeeCalculator,
};
use bincode::deserialize;
use bv::BitVec;
use solana_sdk::{
clock::{Clock, Epoch, Slot, UnixTimestamp},
epoch_schedule::EpochSchedule,
pubkey::Pubkey,
rent::Rent,
slot_hashes::SlotHashes,
slot_history::{self, SlotHistory},
stake_history::{StakeHistory, StakeHistoryEntry},
sysvar::{self, fees::Fees, recent_blockhashes::RecentBlockhashes, rewards::Rewards},
};
pub fn parse_sysvar(data: &[u8], pubkey: &Pubkey) -> Result<SysvarAccountType, ParseAccountError> {
let parsed_account = {
if pubkey == &sysvar::clock::id() {
deserialize::<Clock>(data)
.ok()
.map(|clock| SysvarAccountType::Clock(clock.into()))
} else if pubkey == &sysvar::epoch_schedule::id() {
deserialize(data).ok().map(SysvarAccountType::EpochSchedule)
} else if pubkey == &sysvar::fees::id() {
deserialize::<Fees>(data)
.ok()
.map(|fees| SysvarAccountType::Fees(fees.into()))
} else if pubkey == &sysvar::recent_blockhashes::id() {
deserialize::<RecentBlockhashes>(data)
.ok()
.map(|recent_blockhashes| {
let recent_blockhashes = recent_blockhashes
.iter()
.map(|entry| UiRecentBlockhashesEntry {
blockhash: entry.blockhash.to_string(),
fee_calculator: entry.fee_calculator.clone().into(),
})
.collect();
SysvarAccountType::RecentBlockhashes(recent_blockhashes)
})
} else if pubkey == &sysvar::rent::id() {
deserialize::<Rent>(data)
.ok()
.map(|rent| SysvarAccountType::Rent(rent.into()))
} else if pubkey == &sysvar::rewards::id() {
deserialize::<Rewards>(data)
.ok()
.map(|rewards| SysvarAccountType::Rewards(rewards.into()))
} else if pubkey == &sysvar::slot_hashes::id() {
deserialize::<SlotHashes>(data).ok().map(|slot_hashes| {
let slot_hashes = slot_hashes
.iter()
.map(|slot_hash| UiSlotHashEntry {
slot: slot_hash.0,
hash: slot_hash.1.to_string(),
})
.collect();
SysvarAccountType::SlotHashes(slot_hashes)
})
} else if pubkey == &sysvar::slot_history::id() {
deserialize::<SlotHistory>(data).ok().map(|slot_history| {
SysvarAccountType::SlotHistory(UiSlotHistory {
next_slot: slot_history.next_slot,
bits: format!("{:?}", SlotHistoryBits(slot_history.bits)),
})
})
} else if pubkey == &sysvar::stake_history::id() {
deserialize::<StakeHistory>(data).ok().map(|stake_history| {
let stake_history = stake_history
.iter()
.map(|entry| UiStakeHistoryEntry {
epoch: entry.0,
stake_history: entry.1.clone(),
})
.collect();
SysvarAccountType::StakeHistory(stake_history)
})
} else {
None
}
};
parsed_account.ok_or(ParseAccountError::AccountNotParsable(
ParsableAccount::Sysvar,
))
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
pub enum SysvarAccountType {
Clock(UiClock),
EpochSchedule(EpochSchedule),
Fees(UiFees),
RecentBlockhashes(Vec<UiRecentBlockhashesEntry>),
Rent(UiRent),
Rewards(UiRewards),
SlotHashes(Vec<UiSlotHashEntry>),
SlotHistory(UiSlotHistory),
StakeHistory(Vec<UiStakeHistoryEntry>),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct UiClock {
pub slot: Slot,
pub epoch: Epoch,
pub epoch_start_timestamp: UnixTimestamp,
pub leader_schedule_epoch: Epoch,
pub unix_timestamp: UnixTimestamp,
}
impl From<Clock> for UiClock {
fn from(clock: Clock) -> Self {
Self {
slot: clock.slot,
epoch: clock.epoch,
epoch_start_timestamp: clock.epoch_start_timestamp,
leader_schedule_epoch: clock.leader_schedule_epoch,
unix_timestamp: clock.unix_timestamp,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct UiFees {
pub fee_calculator: UiFeeCalculator,
}
impl From<Fees> for UiFees {
fn from(fees: Fees) -> Self {
Self {
fee_calculator: fees.fee_calculator.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct UiRent {
pub lamports_per_byte_year: StringAmount,
pub exemption_threshold: f64,
pub burn_percent: u8,
}
impl From<Rent> for UiRent {
fn from(rent: Rent) -> Self {
Self {
lamports_per_byte_year: rent.lamports_per_byte_year.to_string(),
exemption_threshold: rent.exemption_threshold,
burn_percent: rent.burn_percent,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct UiRewards {
pub validator_point_value: f64,
}
impl From<Rewards> for UiRewards {
fn from(rewards: Rewards) -> Self {
Self {
validator_point_value: rewards.validator_point_value,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiRecentBlockhashesEntry {
pub blockhash: String,
pub fee_calculator: UiFeeCalculator,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiSlotHashEntry {
pub slot: Slot,
pub hash: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiSlotHistory {
pub next_slot: Slot,
pub bits: String,
}
struct SlotHistoryBits(BitVec<u64>);
impl std::fmt::Debug for SlotHistoryBits {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for i in 0..slot_history::MAX_ENTRIES {
if self.0.get(i) {
write!(f, "1")?;
} else {
write!(f, "0")?;
}
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiStakeHistoryEntry {
pub epoch: Epoch,
pub stake_history: StakeHistoryEntry,
}
#[cfg(test)]
mod test {
use super::*;
use solana_sdk::{
account::create_account, fee_calculator::FeeCalculator, hash::Hash,
sysvar::recent_blockhashes::IterItem,
};
#[test]
fn test_parse_sysvars() {
let clock_sysvar = create_account(&Clock::default(), 1);
assert_eq!(
parse_sysvar(&clock_sysvar.data, &sysvar::clock::id()).unwrap(),
SysvarAccountType::Clock(UiClock::default()),
);
let epoch_schedule = EpochSchedule {
slots_per_epoch: 12,
leader_schedule_slot_offset: 0,
warmup: false,
first_normal_epoch: 1,
first_normal_slot: 12,
};
let epoch_schedule_sysvar = create_account(&epoch_schedule, 1);
assert_eq!(
parse_sysvar(&epoch_schedule_sysvar.data, &sysvar::epoch_schedule::id()).unwrap(),
SysvarAccountType::EpochSchedule(epoch_schedule),
);
let fees_sysvar = create_account(&Fees::default(), 1);
assert_eq!(
parse_sysvar(&fees_sysvar.data, &sysvar::fees::id()).unwrap(),
SysvarAccountType::Fees(UiFees::default()),
);
let hash = Hash::new(&[1; 32]);
let fee_calculator = FeeCalculator {
lamports_per_signature: 10,
};
let recent_blockhashes: RecentBlockhashes = vec![IterItem(0, &hash, &fee_calculator)]
.into_iter()
.collect();
let recent_blockhashes_sysvar = create_account(&recent_blockhashes, 1);
assert_eq!(
parse_sysvar(
&recent_blockhashes_sysvar.data,
&sysvar::recent_blockhashes::id()
)
.unwrap(),
SysvarAccountType::RecentBlockhashes(vec![UiRecentBlockhashesEntry {
blockhash: hash.to_string(),
fee_calculator: fee_calculator.into(),
}]),
);
let rent = Rent {
lamports_per_byte_year: 10,
exemption_threshold: 2.0,
burn_percent: 5,
};
let rent_sysvar = create_account(&rent, 1);
assert_eq!(
parse_sysvar(&rent_sysvar.data, &sysvar::rent::id()).unwrap(),
SysvarAccountType::Rent(rent.into()),
);
let rewards_sysvar = create_account(&Rewards::default(), 1);
assert_eq!(
parse_sysvar(&rewards_sysvar.data, &sysvar::rewards::id()).unwrap(),
SysvarAccountType::Rewards(UiRewards::default()),
);
let mut slot_hashes = SlotHashes::default();
slot_hashes.add(1, hash);
let slot_hashes_sysvar = create_account(&slot_hashes, 1);
assert_eq!(
parse_sysvar(&slot_hashes_sysvar.data, &sysvar::slot_hashes::id()).unwrap(),
SysvarAccountType::SlotHashes(vec![UiSlotHashEntry {
slot: 1,
hash: hash.to_string(),
}]),
);
let mut slot_history = SlotHistory::default();
slot_history.add(42);
let slot_history_sysvar = create_account(&slot_history, 1);
assert_eq!(
parse_sysvar(&slot_history_sysvar.data, &sysvar::slot_history::id()).unwrap(),
SysvarAccountType::SlotHistory(UiSlotHistory {
next_slot: slot_history.next_slot,
bits: format!("{:?}", SlotHistoryBits(slot_history.bits)),
}),
);
let mut stake_history = StakeHistory::default();
let stake_history_entry = StakeHistoryEntry {
effective: 10,
activating: 2,
deactivating: 3,
};
stake_history.add(1, stake_history_entry.clone());
let stake_history_sysvar = create_account(&stake_history, 1);
assert_eq!(
parse_sysvar(&stake_history_sysvar.data, &sysvar::stake_history::id()).unwrap(),
SysvarAccountType::StakeHistory(vec![UiStakeHistoryEntry {
epoch: 1,
stake_history: stake_history_entry,
}]),
);
let bad_pubkey = solana_sdk::pubkey::new_rand();
assert!(parse_sysvar(&stake_history_sysvar.data, &bad_pubkey).is_err());
let bad_data = vec![0; 4];
assert!(parse_sysvar(&bad_data, &sysvar::stake_history::id()).is_err());
}
}

View File

@@ -1,352 +0,0 @@
use crate::{
parse_account_data::{ParsableAccount, ParseAccountError},
StringAmount,
};
use solana_sdk::pubkey::Pubkey;
use spl_token_v2_0::{
solana_program::{
program_option::COption, program_pack::Pack, pubkey::Pubkey as SplTokenPubkey,
},
state::{Account, AccountState, Mint, Multisig},
};
use std::str::FromStr;
// A helper function to convert spl_token_v2_0::id() as spl_sdk::pubkey::Pubkey to
// solana_sdk::pubkey::Pubkey
pub fn spl_token_id_v2_0() -> Pubkey {
Pubkey::from_str(&spl_token_v2_0::id().to_string()).unwrap()
}
// A helper function to convert spl_token_v2_0::native_mint::id() as spl_sdk::pubkey::Pubkey to
// solana_sdk::pubkey::Pubkey
pub fn spl_token_v2_0_native_mint() -> Pubkey {
Pubkey::from_str(&spl_token_v2_0::native_mint::id().to_string()).unwrap()
}
// A helper function to convert a solana_sdk::pubkey::Pubkey to spl_sdk::pubkey::Pubkey
pub fn spl_token_v2_0_pubkey(pubkey: &Pubkey) -> SplTokenPubkey {
SplTokenPubkey::from_str(&pubkey.to_string()).unwrap()
}
// A helper function to convert a spl_sdk::pubkey::Pubkey to solana_sdk::pubkey::Pubkey
pub fn pubkey_from_spl_token_v2_0(pubkey: &SplTokenPubkey) -> Pubkey {
Pubkey::from_str(&pubkey.to_string()).unwrap()
}
pub fn parse_token(
data: &[u8],
mint_decimals: Option<u8>,
) -> Result<TokenAccountType, ParseAccountError> {
if data.len() == Account::get_packed_len() {
let account = Account::unpack(data)
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
let decimals = mint_decimals.ok_or_else(|| {
ParseAccountError::AdditionalDataMissing(
"no mint_decimals provided to parse spl-token account".to_string(),
)
})?;
Ok(TokenAccountType::Account(UiTokenAccount {
mint: account.mint.to_string(),
owner: account.owner.to_string(),
token_amount: token_amount_to_ui_amount(account.amount, decimals),
delegate: match account.delegate {
COption::Some(pubkey) => Some(pubkey.to_string()),
COption::None => None,
},
state: account.state.into(),
is_native: account.is_native(),
rent_exempt_reserve: match account.is_native {
COption::Some(reserve) => Some(token_amount_to_ui_amount(reserve, decimals)),
COption::None => None,
},
delegated_amount: if account.delegate.is_none() {
None
} else {
Some(token_amount_to_ui_amount(
account.delegated_amount,
decimals,
))
},
close_authority: match account.close_authority {
COption::Some(pubkey) => Some(pubkey.to_string()),
COption::None => None,
},
}))
} else if data.len() == Mint::get_packed_len() {
let mint = Mint::unpack(data)
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
Ok(TokenAccountType::Mint(UiMint {
mint_authority: match mint.mint_authority {
COption::Some(pubkey) => Some(pubkey.to_string()),
COption::None => None,
},
supply: mint.supply.to_string(),
decimals: mint.decimals,
is_initialized: mint.is_initialized,
freeze_authority: match mint.freeze_authority {
COption::Some(pubkey) => Some(pubkey.to_string()),
COption::None => None,
},
}))
} else if data.len() == Multisig::get_packed_len() {
let multisig = Multisig::unpack(data)
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
Ok(TokenAccountType::Multisig(UiMultisig {
num_required_signers: multisig.m,
num_valid_signers: multisig.n,
is_initialized: multisig.is_initialized,
signers: multisig
.signers
.iter()
.filter_map(|pubkey| {
if pubkey != &SplTokenPubkey::default() {
Some(pubkey.to_string())
} else {
None
}
})
.collect(),
}))
} else {
Err(ParseAccountError::AccountNotParsable(
ParsableAccount::SplToken,
))
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
pub enum TokenAccountType {
Account(UiTokenAccount),
Mint(UiMint),
Multisig(UiMultisig),
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiTokenAccount {
pub mint: String,
pub owner: String,
pub token_amount: UiTokenAmount,
#[serde(skip_serializing_if = "Option::is_none")]
pub delegate: Option<String>,
pub state: UiAccountState,
pub is_native: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub rent_exempt_reserve: Option<UiTokenAmount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delegated_amount: Option<UiTokenAmount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub close_authority: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum UiAccountState {
Uninitialized,
Initialized,
Frozen,
}
impl From<AccountState> for UiAccountState {
fn from(state: AccountState) -> Self {
match state {
AccountState::Uninitialized => UiAccountState::Uninitialized,
AccountState::Initialized => UiAccountState::Initialized,
AccountState::Frozen => UiAccountState::Frozen,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiTokenAmount {
pub ui_amount: f64,
pub decimals: u8,
pub amount: StringAmount,
}
impl UiTokenAmount {
pub fn real_number_string(&self) -> String {
let decimals = self.decimals as usize;
if decimals > 0 {
let amount = u64::from_str(&self.amount).unwrap_or(0);
// Left-pad zeros to decimals + 1, so we at least have an integer zero
let mut s = format!("{:01$}", amount, decimals + 1);
// Add the decimal point (Sorry, "," locales!)
s.insert(s.len() - decimals, '.');
s
} else {
self.amount.clone()
}
}
pub fn real_number_string_trimmed(&self) -> String {
let s = self.real_number_string();
let zeros_trimmed = s.trim_end_matches('0');
let decimal_trimmed = zeros_trimmed.trim_end_matches('.');
decimal_trimmed.to_string()
}
}
pub fn token_amount_to_ui_amount(amount: u64, decimals: u8) -> UiTokenAmount {
// Use `amount_to_ui_amount()` once spl_token is bumped to a version that supports it: https://github.com/solana-labs/solana-program-library/pull/211
let amount_decimals = amount as f64 / 10_usize.pow(decimals as u32) as f64;
UiTokenAmount {
ui_amount: amount_decimals,
decimals,
amount: amount.to_string(),
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiMint {
pub mint_authority: Option<String>,
pub supply: StringAmount,
pub decimals: u8,
pub is_initialized: bool,
pub freeze_authority: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiMultisig {
pub num_required_signers: u8,
pub num_valid_signers: u8,
pub is_initialized: bool,
pub signers: Vec<String>,
}
pub fn get_token_account_mint(data: &[u8]) -> Option<Pubkey> {
if data.len() == Account::get_packed_len() {
Some(Pubkey::new(&data[0..32]))
} else {
None
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_token() {
let mint_pubkey = SplTokenPubkey::new(&[2; 32]);
let owner_pubkey = SplTokenPubkey::new(&[3; 32]);
let mut account_data = vec![0; Account::get_packed_len()];
let mut account = Account::unpack_unchecked(&account_data).unwrap();
account.mint = mint_pubkey;
account.owner = owner_pubkey;
account.amount = 42;
account.state = AccountState::Initialized;
account.is_native = COption::None;
account.close_authority = COption::Some(owner_pubkey);
Account::pack(account, &mut account_data).unwrap();
assert!(parse_token(&account_data, None).is_err());
assert_eq!(
parse_token(&account_data, Some(2)).unwrap(),
TokenAccountType::Account(UiTokenAccount {
mint: mint_pubkey.to_string(),
owner: owner_pubkey.to_string(),
token_amount: UiTokenAmount {
ui_amount: 0.42,
decimals: 2,
amount: "42".to_string()
},
delegate: None,
state: UiAccountState::Initialized,
is_native: false,
rent_exempt_reserve: None,
delegated_amount: None,
close_authority: Some(owner_pubkey.to_string()),
}),
);
let mut mint_data = vec![0; Mint::get_packed_len()];
let mut mint = Mint::unpack_unchecked(&mint_data).unwrap();
mint.mint_authority = COption::Some(owner_pubkey);
mint.supply = 42;
mint.decimals = 3;
mint.is_initialized = true;
mint.freeze_authority = COption::Some(owner_pubkey);
Mint::pack(mint, &mut mint_data).unwrap();
assert_eq!(
parse_token(&mint_data, None).unwrap(),
TokenAccountType::Mint(UiMint {
mint_authority: Some(owner_pubkey.to_string()),
supply: 42.to_string(),
decimals: 3,
is_initialized: true,
freeze_authority: Some(owner_pubkey.to_string()),
}),
);
let signer1 = SplTokenPubkey::new(&[1; 32]);
let signer2 = SplTokenPubkey::new(&[2; 32]);
let signer3 = SplTokenPubkey::new(&[3; 32]);
let mut multisig_data = vec![0; Multisig::get_packed_len()];
let mut signers = [SplTokenPubkey::default(); 11];
signers[0] = signer1;
signers[1] = signer2;
signers[2] = signer3;
let mut multisig = Multisig::unpack_unchecked(&multisig_data).unwrap();
multisig.m = 2;
multisig.n = 3;
multisig.is_initialized = true;
multisig.signers = signers;
Multisig::pack(multisig, &mut multisig_data).unwrap();
assert_eq!(
parse_token(&multisig_data, None).unwrap(),
TokenAccountType::Multisig(UiMultisig {
num_required_signers: 2,
num_valid_signers: 3,
is_initialized: true,
signers: vec![
signer1.to_string(),
signer2.to_string(),
signer3.to_string()
],
}),
);
let bad_data = vec![0; 4];
assert!(parse_token(&bad_data, None).is_err());
}
#[test]
fn test_get_token_account_mint() {
let mint_pubkey = SplTokenPubkey::new(&[2; 32]);
let mut account_data = vec![0; Account::get_packed_len()];
let mut account = Account::unpack_unchecked(&account_data).unwrap();
account.mint = mint_pubkey;
Account::pack(account, &mut account_data).unwrap();
let expected_mint_pubkey = Pubkey::new(&[2; 32]);
assert_eq!(
get_token_account_mint(&account_data),
Some(expected_mint_pubkey)
);
}
#[test]
fn test_ui_token_amount_real_string() {
let token_amount = token_amount_to_ui_amount(1, 0);
assert_eq!(&token_amount.real_number_string(), "1");
assert_eq!(&token_amount.real_number_string_trimmed(), "1");
let token_amount = token_amount_to_ui_amount(1, 9);
assert_eq!(&token_amount.real_number_string(), "0.000000001");
assert_eq!(&token_amount.real_number_string_trimmed(), "0.000000001");
let token_amount = token_amount_to_ui_amount(1_000_000_000, 9);
assert_eq!(&token_amount.real_number_string(), "1.000000000");
assert_eq!(&token_amount.real_number_string_trimmed(), "1");
let token_amount = token_amount_to_ui_amount(1_234_567_890, 3);
assert_eq!(&token_amount.real_number_string(), "1234567.890");
assert_eq!(&token_amount.real_number_string_trimmed(), "1234567.89");
}
}

View File

@@ -1,146 +0,0 @@
use crate::{parse_account_data::ParseAccountError, StringAmount};
use solana_sdk::{
clock::{Epoch, Slot},
pubkey::Pubkey,
};
use solana_vote_program::vote_state::{BlockTimestamp, Lockout, VoteState};
pub fn parse_vote(data: &[u8]) -> Result<VoteAccountType, ParseAccountError> {
let mut vote_state = VoteState::deserialize(data).map_err(ParseAccountError::from)?;
let epoch_credits = vote_state
.epoch_credits()
.iter()
.map(|(epoch, credits, previous_credits)| UiEpochCredits {
epoch: *epoch,
credits: credits.to_string(),
previous_credits: previous_credits.to_string(),
})
.collect();
let votes = vote_state
.votes
.iter()
.map(|lockout| UiLockout {
slot: lockout.slot,
confirmation_count: lockout.confirmation_count,
})
.collect();
let authorized_voters = vote_state
.authorized_voters()
.iter()
.map(|(epoch, authorized_voter)| UiAuthorizedVoters {
epoch: *epoch,
authorized_voter: authorized_voter.to_string(),
})
.collect();
let prior_voters = vote_state
.prior_voters()
.buf()
.iter()
.filter(|(pubkey, _, _)| pubkey != &Pubkey::default())
.map(
|(authorized_pubkey, epoch_of_last_authorized_switch, target_epoch)| UiPriorVoters {
authorized_pubkey: authorized_pubkey.to_string(),
epoch_of_last_authorized_switch: *epoch_of_last_authorized_switch,
target_epoch: *target_epoch,
},
)
.collect();
Ok(VoteAccountType::Vote(UiVoteState {
node_pubkey: vote_state.node_pubkey.to_string(),
authorized_withdrawer: vote_state.authorized_withdrawer.to_string(),
commission: vote_state.commission,
votes,
root_slot: vote_state.root_slot,
authorized_voters,
prior_voters,
epoch_credits,
last_timestamp: vote_state.last_timestamp,
}))
}
/// A wrapper enum for consistency across programs
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
pub enum VoteAccountType {
Vote(UiVoteState),
}
/// A duplicate representation of VoteState for pretty JSON serialization
#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UiVoteState {
node_pubkey: String,
authorized_withdrawer: String,
commission: u8,
votes: Vec<UiLockout>,
root_slot: Option<Slot>,
authorized_voters: Vec<UiAuthorizedVoters>,
prior_voters: Vec<UiPriorVoters>,
epoch_credits: Vec<UiEpochCredits>,
last_timestamp: BlockTimestamp,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct UiLockout {
slot: Slot,
confirmation_count: u32,
}
impl From<&Lockout> for UiLockout {
fn from(lockout: &Lockout) -> Self {
Self {
slot: lockout.slot,
confirmation_count: lockout.confirmation_count,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct UiAuthorizedVoters {
epoch: Epoch,
authorized_voter: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct UiPriorVoters {
authorized_pubkey: String,
epoch_of_last_authorized_switch: Epoch,
target_epoch: Epoch,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct UiEpochCredits {
epoch: Epoch,
credits: StringAmount,
previous_credits: StringAmount,
}
#[cfg(test)]
mod test {
use super::*;
use solana_vote_program::vote_state::VoteStateVersions;
#[test]
fn test_parse_vote() {
let vote_state = VoteState::default();
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
let versioned = VoteStateVersions::new_current(vote_state);
VoteState::serialize(&versioned, &mut vote_account_data).unwrap();
let expected_vote_state = UiVoteState {
node_pubkey: Pubkey::default().to_string(),
authorized_withdrawer: Pubkey::default().to_string(),
..UiVoteState::default()
};
assert_eq!(
parse_vote(&vote_account_data).unwrap(),
VoteAccountType::Vote(expected_vote_state)
);
let bad_data = vec![0; 4];
assert!(parse_vote(&bad_data).is_err());
}
}

View File

@@ -1,18 +0,0 @@
use solana_config_program::ConfigState;
pub const MAX_SHORT_FIELD_LENGTH: usize = 70;
pub const MAX_LONG_FIELD_LENGTH: usize = 300;
pub const MAX_VALIDATOR_INFO: u64 = 576;
solana_sdk::declare_id!("Va1idator1nfo111111111111111111111111111111");
#[derive(Debug, Deserialize, PartialEq, Serialize, Default)]
pub struct ValidatorInfo {
pub info: String,
}
impl ConfigState for ValidatorInfo {
fn max_space() -> u64 {
MAX_VALIDATOR_INFO
}
}

View File

@@ -1,24 +0,0 @@
[package]
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
edition = "2018"
name = "solana-accounts-bench"
version = "1.5.1"
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
publish = false
[dependencies]
log = "0.4.11"
rayon = "1.4.0"
solana-logger = { path = "../logger", version = "1.5.1" }
solana-runtime = { path = "../runtime", version = "1.5.1" }
solana-measure = { path = "../measure", version = "1.5.1" }
solana-sdk = { path = "../sdk", version = "1.5.1" }
solana-version = { path = "../version", version = "1.5.1" }
rand = "0.7.0"
clap = "2.33.1"
crossbeam-channel = "0.4"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@@ -1,110 +0,0 @@
use clap::{crate_description, crate_name, value_t, App, Arg};
use rayon::prelude::*;
use solana_measure::measure::Measure;
use solana_runtime::{
accounts::{create_test_accounts, update_accounts, Accounts},
accounts_index::Ancestors,
};
use solana_sdk::{genesis_config::ClusterType, pubkey::Pubkey};
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
solana_logger::setup();
let matches = App::new(crate_name!())
.about(crate_description!())
.version(solana_version::version!())
.arg(
Arg::with_name("num_slots")
.long("num_slots")
.takes_value(true)
.value_name("SLOTS")
.help("Number of slots to store to."),
)
.arg(
Arg::with_name("num_accounts")
.long("num_accounts")
.takes_value(true)
.value_name("NUM_ACCOUNTS")
.help("Total number of accounts"),
)
.arg(
Arg::with_name("iterations")
.long("iterations")
.takes_value(true)
.value_name("ITERATIONS")
.help("Number of bench iterations"),
)
.arg(
Arg::with_name("clean")
.long("clean")
.takes_value(false)
.help("Run clean"),
)
.get_matches();
let num_slots = value_t!(matches, "num_slots", usize).unwrap_or(4);
let num_accounts = value_t!(matches, "num_accounts", usize).unwrap_or(10_000);
let iterations = value_t!(matches, "iterations", usize).unwrap_or(20);
let clean = matches.is_present("clean");
println!("clean: {:?}", clean);
let path = PathBuf::from(env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_owned()))
.join("accounts-bench");
if fs::remove_dir_all(path.clone()).is_err() {
println!("Warning: Couldn't remove {:?}", path);
}
let accounts = Accounts::new(vec![path], &ClusterType::Testnet);
println!("Creating {} accounts", num_accounts);
let mut create_time = Measure::start("create accounts");
let pubkeys: Vec<_> = (0..num_slots)
.into_par_iter()
.map(|slot| {
let mut pubkeys: Vec<Pubkey> = vec![];
create_test_accounts(
&accounts,
&mut pubkeys,
num_accounts / num_slots,
slot as u64,
);
pubkeys
})
.collect();
let pubkeys: Vec<_> = pubkeys.into_iter().flatten().collect();
create_time.stop();
println!(
"created {} accounts in {} slots {}",
(num_accounts / num_slots) * num_slots,
num_slots,
create_time
);
let mut ancestors: Ancestors = vec![(0, 0)].into_iter().collect();
for i in 1..num_slots {
ancestors.insert(i as u64, i - 1);
accounts.add_root(i as u64);
}
for x in 0..iterations {
if clean {
let mut time = Measure::start("clean");
accounts.accounts_db.clean_accounts(None);
time.stop();
println!("{}", time);
for slot in 0..num_slots {
update_accounts(&accounts, &pubkeys, ((x + 1) * num_slots + slot) as u64);
accounts.add_root((x * num_slots + slot) as u64);
}
} else {
let mut pubkeys: Vec<Pubkey> = vec![];
let mut time = Measure::start("hash");
let hash = accounts
.accounts_db
.update_accounts_hash(0, &ancestors, true)
.0;
time.stop();
println!("hash: {} {}", hash, time);
create_test_accounts(&accounts, &mut pubkeys, 1, 0);
}
}
}

View File

@@ -1,29 +0,0 @@
[package]
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
edition = "2018"
name = "solana-banking-bench"
version = "1.5.1"
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
publish = false
[dependencies]
clap = "2.33.1"
crossbeam-channel = "0.4"
log = "0.4.11"
rand = "0.7.0"
rayon = "1.4.0"
solana-core = { path = "../core", version = "1.5.1" }
solana-clap-utils = { path = "../clap-utils", version = "1.5.1" }
solana-streamer = { path = "../streamer", version = "1.5.1" }
solana-perf = { path = "../perf", version = "1.5.1" }
solana-ledger = { path = "../ledger", version = "1.5.1" }
solana-logger = { path = "../logger", version = "1.5.1" }
solana-runtime = { path = "../runtime", version = "1.5.1" }
solana-measure = { path = "../measure", version = "1.5.1" }
solana-sdk = { path = "../sdk", version = "1.5.1" }
solana-version = { path = "../version", version = "1.5.1" }
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@@ -1,391 +0,0 @@
use clap::{crate_description, crate_name, value_t, App, Arg};
use crossbeam_channel::unbounded;
use log::*;
use rand::{thread_rng, Rng};
use rayon::prelude::*;
use solana_core::{
banking_stage::{create_test_recorder, BankingStage},
cluster_info::ClusterInfo,
cluster_info::Node,
poh_recorder::PohRecorder,
poh_recorder::WorkingBankEntry,
};
use solana_ledger::{
blockstore::Blockstore,
genesis_utils::{create_genesis_config, GenesisConfigInfo},
get_tmp_ledger_path,
};
use solana_measure::measure::Measure;
use solana_perf::packet::to_packets_chunked;
use solana_runtime::{
accounts_background_service::ABSRequestSender, bank::Bank, bank_forks::BankForks,
};
use solana_sdk::{
hash::Hash,
signature::Keypair,
signature::Signature,
system_transaction,
timing::{duration_as_us, timestamp},
transaction::Transaction,
};
use std::{
sync::{atomic::Ordering, mpsc::Receiver, Arc, Mutex},
thread::sleep,
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(
total_num_transactions: usize,
hash: Hash,
same_payer: bool,
) -> Vec<Transaction> {
let to_pubkey = solana_sdk::pubkey::new_rand();
let payer_key = Keypair::new();
let dummy = system_transaction::transfer(&payer_key, &to_pubkey, 1, hash);
(0..total_num_transactions)
.into_par_iter()
.map(|_| {
let mut new = dummy.clone();
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
if !same_payer {
new.message.account_keys[0] = solana_sdk::pubkey::new_rand();
}
new.message.account_keys[1] = solana_sdk::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
}
#[allow(clippy::cognitive_complexity)]
fn main() {
solana_logger::setup();
let matches = App::new(crate_name!())
.about(crate_description!())
.version(solana_version::version!())
.arg(
Arg::with_name("num_chunks")
.long("num-chunks")
.takes_value(true)
.value_name("SIZE")
.help("Number of transaction chunks."),
)
.arg(
Arg::with_name("packets_per_chunk")
.long("packets-per-chunk")
.takes_value(true)
.value_name("SIZE")
.help("Packets per chunk"),
)
.arg(
Arg::with_name("skip_sanity")
.long("skip-sanity")
.takes_value(false)
.help("Skip transaction sanity execution"),
)
.arg(
Arg::with_name("same_payer")
.long("same-payer")
.takes_value(false)
.help("Use the same payer for transfers"),
)
.arg(
Arg::with_name("iterations")
.long("iterations")
.takes_value(true)
.help("Number of iterations"),
)
.arg(
Arg::with_name("num_threads")
.long("num-threads")
.takes_value(true)
.help("Number of iterations"),
)
.get_matches();
let num_threads =
value_t!(matches, "num_threads", usize).unwrap_or(BankingStage::num_threads() as usize);
// a multiple of packet chunk duplicates to avoid races
let num_chunks = value_t!(matches, "num_chunks", usize).unwrap_or(16);
let packets_per_chunk = value_t!(matches, "packets_per_chunk", usize).unwrap_or(192);
let iterations = value_t!(matches, "iterations", usize).unwrap_or(1000);
let total_num_transactions = num_chunks * num_threads * packets_per_chunk;
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 (replay_vote_sender, _replay_vote_receiver) = unbounded();
let bank0 = Bank::new(&genesis_config);
let mut bank_forks = BankForks::new(bank0);
let mut bank = bank_forks.working_bank();
info!("threads: {} txs: {}", num_threads, total_num_transactions);
let same_payer = matches.is_present("same_payer");
let mut transactions =
make_accounts_txs(total_num_transactions, genesis_config.hash(), same_payer);
// fund all the accounts
transactions.iter().for_each(|tx| {
let mut fund = system_transaction::transfer(
&mint_keypair,
&tx.message.account_keys[0],
mint_total / total_num_transactions as u64,
genesis_config.hash(),
);
// Ignore any pesky duplicate signature errors in the case we are using single-payer
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
fund.signatures = vec![Signature::new(&sig[0..64])];
let x = bank.process_transaction(&fund);
x.unwrap();
});
let skip_sanity = matches.is_present("skip_sanity");
if !skip_sanity {
//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 error: {:?}", res);
});
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 error: {:?}", r);
}
bank.clear_signatures();
}
let mut verified: Vec<_> = to_packets_chunked(&transactions, packets_per_chunk);
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(cluster_info);
let banking_stage = BankingStage::new(
&cluster_info,
&poh_recorder,
verified_receiver,
vote_receiver,
None,
replay_vote_sender,
);
poh_recorder.lock().unwrap().set_bank(&bank);
let chunk_len = verified.len() / num_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 base_tx_count = bank.transaction_count();
let mut txs_processed = 0;
let mut root = 1;
let collector = solana_sdk::pubkey::new_rand();
let config = Config {
packets_per_batch: packets_per_chunk,
chunk_len,
num_threads,
};
let mut total_sent = 0;
for _ in 0..iterations {
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,
total_num_transactions / num_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, &ABSRequestSender::default(), 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()),
total_num_transactions / num_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_chunk);
}
start += chunk_len;
start %= verified.len();
}
let txs_processed = bank_forks.working_bank().transaction_count();
debug!("processed: {} base: {}", txs_processed, base_tx_count);
eprintln!(
"{{'name': 'banking_bench_total', 'median': '{:.2}'}}",
(1000.0 * 1000.0 * total_sent as f64) / (total_us as f64),
);
eprintln!(
"{{'name': 'banking_bench_tx_total', 'median': '{:.2}'}}",
(1000.0 * 1000.0 * total_sent as f64) / (tx_total_us as f64),
);
eprintln!(
"{{'name': 'banking_bench_success_tx_total', 'median': '{:.2}'}}",
(1000.0 * 1000.0 * (txs_processed - base_tx_count) as f64) / (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);
}

19
banking_bench/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
authors = ["Solana Maintainers <maintainers@solana.com>"]
edition = "2018"
name = "solana-banking-bench"
version = "0.19.2"
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 = "0.19.2" }
solana-logger = { path = "../logger", version = "0.19.2" }
solana-runtime = { path = "../runtime", version = "0.19.2" }
solana-measure = { path = "../measure", version = "0.19.2" }
solana-sdk = { path = "../sdk", version = "0.19.2" }
rand = "0.6.5"
crossbeam-channel = "0.3"

319
banking_bench/src/main.rs Normal file
View File

@@ -0,0 +1,319 @@
#[macro_use]
extern crate solana_core;
extern crate crossbeam_channel;
use crossbeam_channel::unbounded;
use log::*;
use rand::{thread_rng, Rng};
use rayon::prelude::*;
use solana_core::bank_forks::BankForks;
use solana_core::banking_stage::{create_test_recorder, BankingStage};
use solana_core::blocktree::{get_tmp_ledger_path, Blocktree};
use solana_core::cluster_info::ClusterInfo;
use solana_core::cluster_info::Node;
use solana_core::genesis_utils::{create_genesis_block, GenesisBlockInfo};
use solana_core::packet::to_packets_chunked;
use solana_core::poh_recorder::PohRecorder;
use solana_core::poh_recorder::WorkingBankEntry;
use solana_core::service::Service;
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::iter;
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_count))) = 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 GenesisBlockInfo {
genesis_block,
mint_keypair,
..
} = create_genesis_block(mint_total);
let (verified_sender, verified_receiver) = unbounded();
let (vote_sender, vote_receiver) = unbounded();
let bank0 = Bank::new(&genesis_block);
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_block.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_block.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)
.into_iter()
.map(|x| {
let len = x.packets.len();
(x, iter::repeat(1).take(len).collect())
})
.collect();
let ledger_path = get_tmp_ledger_path!();
{
let blocktree = Arc::new(
Blocktree::open(&ledger_path).expect("Expected to be able to open database ledger"),
);
let (exit, poh_recorder, poh_service, signal_receiver) =
create_test_recorder(&bank, &blocktree);
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
let cluster_info = Arc::new(RwLock::new(cluster_info));
let _banking_stage = BankingStage::new(
&cluster_info,
&poh_recorder,
verified_receiver,
vote_receiver,
);
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 signal_receiver2 = signal_receiver.clone();
let mut total = 0;
let mut tx_total = 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,
};
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.0.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_receiver2, 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 += 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 += duration_as_us(&now.elapsed());
}
// This signature clear may not actually clear the signatures
// in this chunk, but since we rotate between CHUNKS then
// we should clear them by the time we come around again to re-use that chunk.
bank.clear_signatures();
total += duration_as_us(&now.elapsed());
debug!(
"time: {} us checked: {} sent: {}",
duration_as_us(&now.elapsed()),
txes / CHUNKS,
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)
.into_iter()
.map(|x| {
let len = x.packets.len();
(x, iter::repeat(1).take(len).collect())
})
.collect();
}
start += chunk_len;
start %= verified.len();
}
eprintln!(
"{{'name': 'banking_bench_total', 'median': '{}'}}",
total / ITERS as u64,
);
eprintln!(
"{{'name': 'banking_bench_tx_total', 'median': '{}'}}",
tx_total / ITERS as u64,
);
drop(vote_sender);
exit.store(true, Ordering::Relaxed);
poh_service.join().unwrap();
sleep(Duration::from_secs(1));
debug!("waited for poh_service");
}
let _unused = Blocktree::destroy(&ledger_path);
}

View File

@@ -1,30 +0,0 @@
[package]
name = "solana-banks-client"
version = "1.5.1"
description = "Solana banks client"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
edition = "2018"
[dependencies]
bincode = "1.3.1"
futures = "0.3"
mio = "0.7.6"
solana-banks-interface = { path = "../banks-interface", version = "1.5.1" }
solana-sdk = { path = "../sdk", version = "1.5.1" }
tarpc = { version = "0.23.0", features = ["full"] }
tokio = { version = "0.3.5", features = ["full"] }
tokio-serde = { version = "0.6", features = ["bincode"] }
[dev-dependencies]
solana-runtime = { path = "../runtime", version = "1.5.1" }
solana-banks-server = { path = "../banks-server", version = "1.5.1" }
[lib]
crate-type = ["lib"]
name = "solana_banks_client"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@@ -1,371 +0,0 @@
//! A client for the ledger state, from the perspective of an arbitrary validator.
//!
//! Use start_tcp_client() to create a client and then import BanksClientExt to
//! access its methods. Additional "*_with_context" methods are also available,
//! but they are undocumented, may change over time, and are generally more
//! cumbersome to use.
use futures::{future::join_all, Future, FutureExt};
pub use solana_banks_interface::{BanksClient as TarpcClient, TransactionStatus};
use solana_banks_interface::{BanksRequest, BanksResponse};
use solana_sdk::{
account::{from_account, Account},
clock::Slot,
commitment_config::CommitmentLevel,
fee_calculator::FeeCalculator,
hash::Hash,
pubkey::Pubkey,
rent::Rent,
signature::Signature,
sysvar,
transaction::{self, Transaction},
transport,
};
use std::io::{self, Error, ErrorKind};
use tarpc::{
client::{self, channel::RequestDispatch, NewClient},
context::{self, Context},
rpc::{ClientMessage, Response},
serde_transport::tcp,
Transport,
};
use tokio::{net::ToSocketAddrs, time::Duration};
use tokio_serde::formats::Bincode;
// This exists only for backward compatibility
pub trait BanksClientExt {}
#[derive(Clone)]
pub struct BanksClient {
inner: TarpcClient,
}
impl BanksClient {
#[allow(clippy::new_ret_no_self)]
pub fn new<C>(
config: client::Config,
transport: C,
) -> NewClient<TarpcClient, RequestDispatch<BanksRequest, BanksResponse, C>>
where
C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>>,
{
TarpcClient::new(config, transport)
}
pub fn send_transaction_with_context(
&mut self,
ctx: Context,
transaction: Transaction,
) -> impl Future<Output = io::Result<()>> + '_ {
self.inner.send_transaction_with_context(ctx, transaction)
}
pub fn get_fees_with_commitment_and_context(
&mut self,
ctx: Context,
commitment: CommitmentLevel,
) -> impl Future<Output = io::Result<(FeeCalculator, Hash, Slot)>> + '_ {
self.inner
.get_fees_with_commitment_and_context(ctx, commitment)
}
pub fn get_transaction_status_with_context(
&mut self,
ctx: Context,
signature: Signature,
) -> impl Future<Output = io::Result<Option<TransactionStatus>>> + '_ {
self.inner
.get_transaction_status_with_context(ctx, signature)
}
pub fn get_slot_with_context(
&mut self,
ctx: Context,
commitment: CommitmentLevel,
) -> impl Future<Output = io::Result<Slot>> + '_ {
self.inner.get_slot_with_context(ctx, commitment)
}
pub fn process_transaction_with_commitment_and_context(
&mut self,
ctx: Context,
transaction: Transaction,
commitment: CommitmentLevel,
) -> impl Future<Output = io::Result<Option<transaction::Result<()>>>> + '_ {
self.inner
.process_transaction_with_commitment_and_context(ctx, transaction, commitment)
}
pub fn get_account_with_commitment_and_context(
&mut self,
ctx: Context,
address: Pubkey,
commitment: CommitmentLevel,
) -> impl Future<Output = io::Result<Option<Account>>> + '_ {
self.inner
.get_account_with_commitment_and_context(ctx, address, commitment)
}
/// Send a transaction and return immediately. The server will resend the
/// transaction until either it is accepted by the cluster or the transaction's
/// blockhash expires.
pub fn send_transaction(
&mut self,
transaction: Transaction,
) -> impl Future<Output = io::Result<()>> + '_ {
self.send_transaction_with_context(context::current(), transaction)
}
/// Return the fee parameters associated with a recent, rooted blockhash. The cluster
/// will use the transaction's blockhash to look up these same fee parameters and
/// use them to calculate the transaction fee.
pub fn get_fees(
&mut self,
) -> impl Future<Output = io::Result<(FeeCalculator, Hash, Slot)>> + '_ {
self.get_fees_with_commitment_and_context(context::current(), CommitmentLevel::Root)
}
/// Return the cluster rent
pub fn get_rent(&mut self) -> impl Future<Output = io::Result<Rent>> + '_ {
self.get_account(sysvar::rent::id()).map(|result| {
let rent_sysvar = result?
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Rent sysvar not present"))?;
from_account::<Rent>(&rent_sysvar).ok_or_else(|| {
io::Error::new(io::ErrorKind::Other, "Failed to deserialize Rent sysvar")
})
})
}
/// Return a recent, rooted blockhash from the server. The cluster will only accept
/// transactions with a blockhash that has not yet expired. Use the `get_fees`
/// method to get both a blockhash and the blockhash's last valid slot.
pub fn get_recent_blockhash(&mut self) -> impl Future<Output = io::Result<Hash>> + '_ {
self.get_fees().map(|result| Ok(result?.1))
}
/// Send a transaction and return after the transaction has been rejected or
/// reached the given level of commitment.
pub fn process_transaction_with_commitment(
&mut self,
transaction: Transaction,
commitment: CommitmentLevel,
) -> impl Future<Output = transport::Result<()>> + '_ {
let mut ctx = context::current();
ctx.deadline += Duration::from_secs(50);
self.process_transaction_with_commitment_and_context(ctx, transaction, commitment)
.map(|result| match result? {
None => {
Err(Error::new(ErrorKind::TimedOut, "invalid blockhash or fee-payer").into())
}
Some(transaction_result) => Ok(transaction_result?),
})
}
/// Send a transaction and return until the transaction has been finalized or rejected.
pub fn process_transaction(
&mut self,
transaction: Transaction,
) -> impl Future<Output = transport::Result<()>> + '_ {
self.process_transaction_with_commitment(transaction, CommitmentLevel::default())
}
pub async fn process_transactions_with_commitment(
&mut self,
transactions: Vec<Transaction>,
commitment: CommitmentLevel,
) -> transport::Result<()> {
let mut clients: Vec<_> = transactions.iter().map(|_| self.clone()).collect();
let futures = clients
.iter_mut()
.zip(transactions)
.map(|(client, transaction)| {
client.process_transaction_with_commitment(transaction, commitment)
});
let statuses = join_all(futures).await;
statuses.into_iter().collect() // Convert Vec<Result<_, _>> to Result<Vec<_>>
}
/// Send transactions and return until the transaction has been finalized or rejected.
pub fn process_transactions(
&mut self,
transactions: Vec<Transaction>,
) -> impl Future<Output = transport::Result<()>> + '_ {
self.process_transactions_with_commitment(transactions, CommitmentLevel::default())
}
/// Return the most recent rooted slot height. All transactions at or below this height
/// are said to be finalized. The cluster will not fork to a higher slot height.
pub fn get_root_slot(&mut self) -> impl Future<Output = io::Result<Slot>> + '_ {
self.get_slot_with_context(context::current(), CommitmentLevel::Root)
}
/// Return the account at the given address at the slot corresponding to the given
/// commitment level. If the account is not found, None is returned.
pub fn get_account_with_commitment(
&mut self,
address: Pubkey,
commitment: CommitmentLevel,
) -> impl Future<Output = io::Result<Option<Account>>> + '_ {
self.get_account_with_commitment_and_context(context::current(), address, commitment)
}
/// Return the account at the given address at the time of the most recent root slot.
/// If the account is not found, None is returned.
pub fn get_account(
&mut self,
address: Pubkey,
) -> impl Future<Output = io::Result<Option<Account>>> + '_ {
self.get_account_with_commitment(address, CommitmentLevel::default())
}
/// Return the balance in lamports of an account at the given address at the slot
/// corresponding to the given commitment level.
pub fn get_balance_with_commitment(
&mut self,
address: Pubkey,
commitment: CommitmentLevel,
) -> impl Future<Output = io::Result<u64>> + '_ {
self.get_account_with_commitment_and_context(context::current(), address, commitment)
.map(|result| Ok(result?.map(|x| x.lamports).unwrap_or(0)))
}
/// Return the balance in lamports of an account at the given address at the time
/// of the most recent root slot.
pub fn get_balance(&mut self, address: Pubkey) -> impl Future<Output = io::Result<u64>> + '_ {
self.get_balance_with_commitment(address, CommitmentLevel::default())
}
/// Return the status of a transaction with a signature matching the transaction's first
/// signature. Return None if the transaction is not found, which may be because the
/// blockhash was expired or the fee-paying account had insufficient funds to pay the
/// transaction fee. Note that servers rarely store the full transaction history. This
/// method may return None if the transaction status has been discarded.
pub fn get_transaction_status(
&mut self,
signature: Signature,
) -> impl Future<Output = io::Result<Option<TransactionStatus>>> + '_ {
self.get_transaction_status_with_context(context::current(), signature)
}
/// Same as get_transaction_status, but for multiple transactions.
pub async fn get_transaction_statuses(
&mut self,
signatures: Vec<Signature>,
) -> io::Result<Vec<Option<TransactionStatus>>> {
// tarpc futures oddly hold a mutable reference back to the client so clone the client upfront
let mut clients_and_signatures: Vec<_> = signatures
.into_iter()
.map(|signature| (self.clone(), signature))
.collect();
let futs = clients_and_signatures
.iter_mut()
.map(|(client, signature)| client.get_transaction_status(*signature));
let statuses = join_all(futs).await;
// Convert Vec<Result<_, _>> to Result<Vec<_>>
statuses.into_iter().collect()
}
}
pub async fn start_client<C>(transport: C) -> io::Result<BanksClient>
where
C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>> + Send + 'static,
{
Ok(BanksClient {
inner: TarpcClient::new(client::Config::default(), transport).spawn()?,
})
}
pub async fn start_tcp_client<T: ToSocketAddrs>(addr: T) -> io::Result<BanksClient> {
let transport = tcp::connect(addr, Bincode::default).await?;
Ok(BanksClient {
inner: TarpcClient::new(client::Config::default(), transport).spawn()?,
})
}
#[cfg(test)]
mod tests {
use super::*;
use solana_banks_server::banks_server::start_local_server;
use solana_runtime::{bank::Bank, bank_forks::BankForks, genesis_utils::create_genesis_config};
use solana_sdk::{message::Message, signature::Signer, system_instruction};
use std::sync::{Arc, RwLock};
use tarpc::transport;
use tokio::{runtime::Runtime, time::sleep};
#[test]
fn test_banks_client_new() {
let (client_transport, _server_transport) = transport::channel::unbounded();
BanksClient::new(client::Config::default(), client_transport);
}
#[test]
fn test_banks_server_transfer_via_server() -> io::Result<()> {
// This test shows the preferred way to interact with BanksServer.
// It creates a runtime explicitly (no globals via tokio macros) and calls
// `runtime.block_on()` just once, to run all the async code.
let genesis = create_genesis_config(10);
let bank_forks = Arc::new(RwLock::new(BankForks::new(Bank::new(
&genesis.genesis_config,
))));
let bob_pubkey = solana_sdk::pubkey::new_rand();
let mint_pubkey = genesis.mint_keypair.pubkey();
let instruction = system_instruction::transfer(&mint_pubkey, &bob_pubkey, 1);
let message = Message::new(&[instruction], Some(&mint_pubkey));
Runtime::new()?.block_on(async {
let client_transport = start_local_server(&bank_forks).await;
let mut banks_client = start_client(client_transport).await?;
let recent_blockhash = banks_client.get_recent_blockhash().await?;
let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
Ok(())
})
}
#[test]
fn test_banks_server_transfer_via_client() -> io::Result<()> {
// The caller may not want to hold the connection open until the transaction
// is processed (or blockhash expires). In this test, we verify the
// server-side functionality is available to the client.
let genesis = create_genesis_config(10);
let bank_forks = Arc::new(RwLock::new(BankForks::new(Bank::new(
&genesis.genesis_config,
))));
let mint_pubkey = &genesis.mint_keypair.pubkey();
let bob_pubkey = solana_sdk::pubkey::new_rand();
let instruction = system_instruction::transfer(&mint_pubkey, &bob_pubkey, 1);
let message = Message::new(&[instruction], Some(&mint_pubkey));
Runtime::new()?.block_on(async {
let client_transport = start_local_server(&bank_forks).await;
let mut banks_client = start_client(client_transport).await?;
let (_, recent_blockhash, last_valid_slot) = banks_client.get_fees().await?;
let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
let signature = transaction.signatures[0];
banks_client.send_transaction(transaction).await?;
let mut status = banks_client.get_transaction_status(signature).await?;
while status.is_none() {
let root_slot = banks_client.get_root_slot().await?;
if root_slot > last_valid_slot {
break;
}
sleep(Duration::from_millis(100)).await;
status = banks_client.get_transaction_status(signature).await?;
}
assert!(status.unwrap().err.is_none());
assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
Ok(())
})
}
}

View File

@@ -1,25 +0,0 @@
[package]
name = "solana-banks-interface"
version = "1.5.1"
description = "Solana banks RPC interface"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
edition = "2018"
[dependencies]
mio = "0.7.6"
serde = { version = "1.0.112", features = ["derive"] }
solana-sdk = { path = "../sdk", version = "1.5.1" }
tarpc = { version = "0.23.0", features = ["full"] }
[dev-dependencies]
tokio = { version = "0.3.5", features = ["full"] }
[lib]
crate-type = ["lib"]
name = "solana_banks_interface"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@@ -1,49 +0,0 @@
use serde::{Deserialize, Serialize};
use solana_sdk::{
account::Account,
clock::Slot,
commitment_config::CommitmentLevel,
fee_calculator::FeeCalculator,
hash::Hash,
pubkey::Pubkey,
signature::Signature,
transaction::{self, Transaction, TransactionError},
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TransactionStatus {
pub slot: Slot,
pub confirmations: Option<usize>, // None = rooted
pub err: Option<TransactionError>,
}
#[tarpc::service]
pub trait Banks {
async fn send_transaction_with_context(transaction: Transaction);
async fn get_fees_with_commitment_and_context(
commitment: CommitmentLevel,
) -> (FeeCalculator, Hash, Slot);
async fn get_transaction_status_with_context(signature: Signature)
-> Option<TransactionStatus>;
async fn get_slot_with_context(commitment: CommitmentLevel) -> Slot;
async fn process_transaction_with_commitment_and_context(
transaction: Transaction,
commitment: CommitmentLevel,
) -> Option<transaction::Result<()>>;
async fn get_account_with_commitment_and_context(
address: Pubkey,
commitment: CommitmentLevel,
) -> Option<Account>;
}
#[cfg(test)]
mod tests {
use super::*;
use tarpc::{client, transport};
#[test]
fn test_banks_client_new() {
let (client_transport, _server_transport) = transport::channel::unbounded();
BanksClient::new(client::Config::default(), client_transport);
}
}

View File

@@ -1,29 +0,0 @@
[package]
name = "solana-banks-server"
version = "1.5.1"
description = "Solana banks server"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
edition = "2018"
[dependencies]
bincode = "1.3.1"
futures = "0.3"
log = "0.4.11"
mio = "0.7.6"
solana-banks-interface = { path = "../banks-interface", version = "1.5.1" }
solana-runtime = { path = "../runtime", version = "1.5.1" }
solana-sdk = { path = "../sdk", version = "1.5.1" }
solana-metrics = { path = "../metrics", version = "1.5.1" }
tarpc = { version = "0.23.0", features = ["full"] }
tokio = { version = "0.3", features = ["full"] }
tokio-serde = { version = "0.6", features = ["bincode"] }
[lib]
crate-type = ["lib"]
name = "solana_banks_server"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@@ -1,275 +0,0 @@
use crate::send_transaction_service::{SendTransactionService, TransactionInfo};
use bincode::{deserialize, serialize};
use futures::{
future,
prelude::stream::{self, StreamExt},
};
use solana_banks_interface::{Banks, BanksRequest, BanksResponse, TransactionStatus};
use solana_runtime::{bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache};
use solana_sdk::{
account::Account,
clock::Slot,
commitment_config::CommitmentLevel,
fee_calculator::FeeCalculator,
hash::Hash,
pubkey::Pubkey,
signature::Signature,
transaction::{self, Transaction},
};
use std::{
io,
net::{Ipv4Addr, SocketAddr},
sync::{
mpsc::{channel, Receiver, Sender},
Arc, RwLock,
},
thread::Builder,
time::Duration,
};
use tarpc::{
context::Context,
rpc::{transport::channel::UnboundedChannel, ClientMessage, Response},
serde_transport::tcp,
server::{self, Channel, Handler},
transport,
};
use tokio::time::sleep;
use tokio_serde::formats::Bincode;
#[derive(Clone)]
struct BanksServer {
bank_forks: Arc<RwLock<BankForks>>,
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
transaction_sender: Sender<TransactionInfo>,
}
impl BanksServer {
/// Return a BanksServer that forwards transactions to the
/// given sender. If unit-testing, those transactions can go to
/// a bank in the given BankForks. Otherwise, the receiver should
/// forward them to a validator in the leader schedule.
fn new(
bank_forks: Arc<RwLock<BankForks>>,
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
transaction_sender: Sender<TransactionInfo>,
) -> Self {
Self {
bank_forks,
block_commitment_cache,
transaction_sender,
}
}
fn run(bank: &Bank, transaction_receiver: Receiver<TransactionInfo>) {
while let Ok(info) = transaction_receiver.recv() {
let mut transaction_infos = vec![info];
while let Ok(info) = transaction_receiver.try_recv() {
transaction_infos.push(info);
}
let transactions: Vec<_> = transaction_infos
.into_iter()
.map(|info| deserialize(&info.wire_transaction).unwrap())
.collect();
let _ = bank.process_transactions(&transactions);
}
}
/// Useful for unit-testing
fn new_loopback(bank_forks: Arc<RwLock<BankForks>>) -> Self {
let (transaction_sender, transaction_receiver) = channel();
let bank = bank_forks.read().unwrap().working_bank();
let slot = bank.slot();
let block_commitment_cache = Arc::new(RwLock::new(
BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
));
Builder::new()
.name("solana-bank-forks-client".to_string())
.spawn(move || Self::run(&bank, transaction_receiver))
.unwrap();
Self::new(bank_forks, block_commitment_cache, transaction_sender)
}
fn slot(&self, commitment: CommitmentLevel) -> Slot {
self.block_commitment_cache
.read()
.unwrap()
.slot_with_commitment(commitment)
}
fn bank(&self, commitment: CommitmentLevel) -> Arc<Bank> {
self.bank_forks.read().unwrap()[self.slot(commitment)].clone()
}
async fn poll_signature_status(
self,
signature: &Signature,
blockhash: &Hash,
last_valid_slot: Slot,
commitment: CommitmentLevel,
) -> Option<transaction::Result<()>> {
let mut status = self
.bank(commitment)
.get_signature_status_with_blockhash(signature, blockhash);
while status.is_none() {
sleep(Duration::from_millis(200)).await;
let bank = self.bank(commitment);
if bank.slot() > last_valid_slot {
break;
}
status = bank.get_signature_status_with_blockhash(signature, blockhash);
}
status
}
}
fn verify_transaction(transaction: &Transaction) -> transaction::Result<()> {
if let Err(err) = transaction.verify() {
Err(err)
} else if let Err(err) = transaction.verify_precompiles() {
Err(err)
} else {
Ok(())
}
}
#[tarpc::server]
impl Banks for BanksServer {
async fn send_transaction_with_context(self, _: Context, transaction: Transaction) {
let blockhash = &transaction.message.recent_blockhash;
let last_valid_slot = self
.bank_forks
.read()
.unwrap()
.root_bank()
.get_blockhash_last_valid_slot(&blockhash)
.unwrap();
let signature = transaction.signatures.get(0).cloned().unwrap_or_default();
let info =
TransactionInfo::new(signature, serialize(&transaction).unwrap(), last_valid_slot);
self.transaction_sender.send(info).unwrap();
}
async fn get_fees_with_commitment_and_context(
self,
_: Context,
commitment: CommitmentLevel,
) -> (FeeCalculator, Hash, Slot) {
let bank = self.bank(commitment);
let (blockhash, fee_calculator) = bank.last_blockhash_with_fee_calculator();
let last_valid_slot = bank.get_blockhash_last_valid_slot(&blockhash).unwrap();
(fee_calculator, blockhash, last_valid_slot)
}
async fn get_transaction_status_with_context(
self,
_: Context,
signature: Signature,
) -> Option<TransactionStatus> {
let bank = self.bank(CommitmentLevel::Recent);
let (slot, status) = bank.get_signature_status_slot(&signature)?;
let r_block_commitment_cache = self.block_commitment_cache.read().unwrap();
let confirmations = if r_block_commitment_cache.root() >= slot {
None
} else {
r_block_commitment_cache
.get_confirmation_count(slot)
.or(Some(0))
};
Some(TransactionStatus {
slot,
confirmations,
err: status.err(),
})
}
async fn get_slot_with_context(self, _: Context, commitment: CommitmentLevel) -> Slot {
self.slot(commitment)
}
async fn process_transaction_with_commitment_and_context(
self,
_: Context,
transaction: Transaction,
commitment: CommitmentLevel,
) -> Option<transaction::Result<()>> {
if let Err(err) = verify_transaction(&transaction) {
return Some(Err(err));
}
let blockhash = &transaction.message.recent_blockhash;
let last_valid_slot = self
.bank_forks
.read()
.unwrap()
.root_bank()
.get_blockhash_last_valid_slot(blockhash)
.unwrap();
let signature = transaction.signatures.get(0).cloned().unwrap_or_default();
let info =
TransactionInfo::new(signature, serialize(&transaction).unwrap(), last_valid_slot);
self.transaction_sender.send(info).unwrap();
self.poll_signature_status(&signature, blockhash, last_valid_slot, commitment)
.await
}
async fn get_account_with_commitment_and_context(
self,
_: Context,
address: Pubkey,
commitment: CommitmentLevel,
) -> Option<Account> {
let bank = self.bank(commitment);
bank.get_account(&address)
}
}
pub async fn start_local_server(
bank_forks: &Arc<RwLock<BankForks>>,
) -> UnboundedChannel<Response<BanksResponse>, ClientMessage<BanksRequest>> {
let banks_server = BanksServer::new_loopback(bank_forks.clone());
let (client_transport, server_transport) = transport::channel::unbounded();
let server = server::new(server::Config::default())
.incoming(stream::once(future::ready(server_transport)))
.respond_with(banks_server.serve());
tokio::spawn(server);
client_transport
}
pub async fn start_tcp_server(
listen_addr: SocketAddr,
tpu_addr: SocketAddr,
bank_forks: Arc<RwLock<BankForks>>,
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
) -> io::Result<()> {
// Note: These settings are copied straight from the tarpc example.
let server = tcp::listen(listen_addr, Bincode::default)
.await?
// Ignore accept errors.
.filter_map(|r| future::ready(r.ok()))
.map(server::BaseChannel::with_defaults)
// Limit channels to 1 per IP.
.max_channels_per_key(1, |t| {
t.as_ref()
.peer_addr()
.map(|x| x.ip())
.unwrap_or_else(|_| Ipv4Addr::new(0, 0, 0, 0).into())
})
// serve is generated by the service attribute. It takes as input any type implementing
// the generated Banks trait.
.map(move |chan| {
let (sender, receiver) = channel();
SendTransactionService::new(tpu_addr, &bank_forks, receiver);
let server =
BanksServer::new(bank_forks.clone(), block_commitment_cache.clone(), sender);
chan.respond_with(server.serve()).execute()
})
// Max 10 channels.
.buffer_unordered(10)
.for_each(|_| async {});
server.await;
Ok(())
}

View File

@@ -1,6 +0,0 @@
pub mod banks_server;
pub mod rpc_banks_service;
pub mod send_transaction_service;
#[macro_use]
extern crate solana_metrics;

View File

@@ -1,116 +0,0 @@
//! The `rpc_banks_service` module implements the Solana Banks RPC API.
use crate::banks_server::start_tcp_server;
use futures::{future::FutureExt, pin_mut, prelude::stream::StreamExt, select};
use solana_runtime::{bank_forks::BankForks, commitment::BlockCommitmentCache};
use std::{
net::SocketAddr,
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
},
thread::{self, Builder, JoinHandle},
};
use tokio::{
runtime::Runtime,
time::{self, Duration},
};
pub struct RpcBanksService {
thread_hdl: JoinHandle<()>,
}
/// Run the TCP service until `exit` is set to true
async fn start_abortable_tcp_server(
listen_addr: SocketAddr,
tpu_addr: SocketAddr,
bank_forks: Arc<RwLock<BankForks>>,
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
exit: Arc<AtomicBool>,
) {
let server = start_tcp_server(
listen_addr,
tpu_addr,
bank_forks.clone(),
block_commitment_cache.clone(),
)
.fuse();
let interval = time::interval(Duration::from_millis(100)).fuse();
pin_mut!(server, interval);
loop {
select! {
_ = server => {},
_ = interval.select_next_some() => {
if exit.load(Ordering::Relaxed) {
break;
}
}
}
}
}
impl RpcBanksService {
fn run(
listen_addr: SocketAddr,
tpu_addr: SocketAddr,
bank_forks: Arc<RwLock<BankForks>>,
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
exit: Arc<AtomicBool>,
) {
let server = start_abortable_tcp_server(
listen_addr,
tpu_addr,
bank_forks,
block_commitment_cache,
exit,
);
Runtime::new().unwrap().block_on(server);
}
pub fn new(
listen_addr: SocketAddr,
tpu_addr: SocketAddr,
bank_forks: &Arc<RwLock<BankForks>>,
block_commitment_cache: &Arc<RwLock<BlockCommitmentCache>>,
exit: &Arc<AtomicBool>,
) -> Self {
let bank_forks = bank_forks.clone();
let block_commitment_cache = block_commitment_cache.clone();
let exit = exit.clone();
let thread_hdl = Builder::new()
.name("solana-rpc-banks".to_string())
.spawn(move || {
Self::run(
listen_addr,
tpu_addr,
bank_forks,
block_commitment_cache,
exit,
)
})
.unwrap();
Self { thread_hdl }
}
pub fn join(self) -> thread::Result<()> {
self.thread_hdl.join()
}
}
#[cfg(test)]
mod tests {
use super::*;
use solana_runtime::bank::Bank;
#[test]
fn test_rpc_banks_server_exit() {
let bank_forks = Arc::new(RwLock::new(BankForks::new(Bank::default())));
let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default()));
let exit = Arc::new(AtomicBool::new(false));
let addr = "127.0.0.1:0".parse().unwrap();
let service = RpcBanksService::new(addr, addr, &bank_forks, &block_commitment_cache, &exit);
exit.store(true, Ordering::Relaxed);
service.join().unwrap();
}
}

View File

@@ -1,343 +0,0 @@
// TODO: Merge this implementation with the one at `core/src/send_transaction_service.rs`
use log::*;
use solana_metrics::{datapoint_warn, inc_new_counter_info};
use solana_runtime::{bank::Bank, bank_forks::BankForks};
use solana_sdk::{clock::Slot, signature::Signature};
use std::{
collections::HashMap,
net::{SocketAddr, UdpSocket},
sync::{
mpsc::{Receiver, RecvTimeoutError},
Arc, RwLock,
},
thread::{self, Builder, JoinHandle},
time::{Duration, Instant},
};
/// Maximum size of the transaction queue
const MAX_TRANSACTION_QUEUE_SIZE: usize = 10_000; // This seems like a lot but maybe it needs to be bigger one day
pub struct SendTransactionService {
thread: JoinHandle<()>,
}
pub struct TransactionInfo {
pub signature: Signature,
pub wire_transaction: Vec<u8>,
pub last_valid_slot: Slot,
}
impl TransactionInfo {
pub fn new(signature: Signature, wire_transaction: Vec<u8>, last_valid_slot: Slot) -> Self {
Self {
signature,
wire_transaction,
last_valid_slot,
}
}
}
#[derive(Default, Debug, PartialEq)]
struct ProcessTransactionsResult {
rooted: u64,
expired: u64,
retried: u64,
failed: u64,
retained: u64,
}
impl SendTransactionService {
pub fn new(
tpu_address: SocketAddr,
bank_forks: &Arc<RwLock<BankForks>>,
receiver: Receiver<TransactionInfo>,
) -> Self {
let thread = Self::retry_thread(receiver, bank_forks.clone(), tpu_address);
Self { thread }
}
fn retry_thread(
receiver: Receiver<TransactionInfo>,
bank_forks: Arc<RwLock<BankForks>>,
tpu_address: SocketAddr,
) -> JoinHandle<()> {
let mut last_status_check = Instant::now();
let mut transactions = HashMap::new();
let send_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
Builder::new()
.name("send-tx-svc".to_string())
.spawn(move || loop {
match receiver.recv_timeout(Duration::from_secs(1)) {
Err(RecvTimeoutError::Disconnected) => break,
Err(RecvTimeoutError::Timeout) => {}
Ok(transaction_info) => {
Self::send_transaction(
&send_socket,
&tpu_address,
&transaction_info.wire_transaction,
);
if transactions.len() < MAX_TRANSACTION_QUEUE_SIZE {
transactions.insert(transaction_info.signature, transaction_info);
} else {
datapoint_warn!("send_transaction_service-queue-overflow");
}
}
}
if Instant::now().duration_since(last_status_check).as_secs() >= 5 {
if !transactions.is_empty() {
datapoint_info!(
"send_transaction_service-queue-size",
("len", transactions.len(), i64)
);
let bank_forks = bank_forks.read().unwrap();
let root_bank = bank_forks.root_bank();
let working_bank = bank_forks.working_bank();
let _result = Self::process_transactions(
&working_bank,
&root_bank,
&send_socket,
&tpu_address,
&mut transactions,
);
}
last_status_check = Instant::now();
}
})
.unwrap()
}
fn process_transactions(
working_bank: &Arc<Bank>,
root_bank: &Arc<Bank>,
send_socket: &UdpSocket,
tpu_address: &SocketAddr,
transactions: &mut HashMap<Signature, TransactionInfo>,
) -> ProcessTransactionsResult {
let mut result = ProcessTransactionsResult::default();
transactions.retain(|signature, transaction_info| {
if root_bank.has_signature(signature) {
info!("Transaction is rooted: {}", signature);
result.rooted += 1;
inc_new_counter_info!("send_transaction_service-rooted", 1);
false
} else if transaction_info.last_valid_slot < root_bank.slot() {
info!("Dropping expired transaction: {}", signature);
result.expired += 1;
inc_new_counter_info!("send_transaction_service-expired", 1);
false
} else {
match working_bank.get_signature_status_slot(signature) {
None => {
// Transaction is unknown to the working bank, it might have been
// dropped or landed in another fork. Re-send it
info!("Retrying transaction: {}", signature);
result.retried += 1;
inc_new_counter_info!("send_transaction_service-retry", 1);
Self::send_transaction(
&send_socket,
&tpu_address,
&transaction_info.wire_transaction,
);
true
}
Some((_slot, status)) => {
if status.is_err() {
info!("Dropping failed transaction: {}", signature);
result.failed += 1;
inc_new_counter_info!("send_transaction_service-failed", 1);
false
} else {
result.retained += 1;
true
}
}
}
}
});
result
}
fn send_transaction(
send_socket: &UdpSocket,
tpu_address: &SocketAddr,
wire_transaction: &[u8],
) {
if let Err(err) = send_socket.send_to(wire_transaction, tpu_address) {
warn!("Failed to send transaction to {}: {:?}", tpu_address, err);
}
}
pub fn join(self) -> thread::Result<()> {
self.thread.join()
}
}
#[cfg(test)]
mod test {
use super::*;
use solana_sdk::{
genesis_config::create_genesis_config, pubkey::Pubkey, signature::Signer,
system_transaction,
};
use std::sync::mpsc::channel;
#[test]
fn service_exit() {
let tpu_address = "127.0.0.1:0".parse().unwrap();
let bank = Bank::default();
let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
let (sender, receiver) = channel();
let send_tranaction_service =
SendTransactionService::new(tpu_address, &bank_forks, receiver);
drop(sender);
send_tranaction_service.join().unwrap();
}
#[test]
fn process_transactions() {
let (genesis_config, mint_keypair) = create_genesis_config(4);
let bank = Bank::new(&genesis_config);
let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
let send_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let tpu_address = "127.0.0.1:0".parse().unwrap();
let root_bank = Arc::new(Bank::new_from_parent(
&bank_forks.read().unwrap().working_bank(),
&Pubkey::default(),
1,
));
let rooted_signature = root_bank
.transfer(1, &mint_keypair, &mint_keypair.pubkey())
.unwrap();
let working_bank = Arc::new(Bank::new_from_parent(&root_bank, &Pubkey::default(), 2));
let non_rooted_signature = working_bank
.transfer(2, &mint_keypair, &mint_keypair.pubkey())
.unwrap();
let failed_signature = {
let blockhash = working_bank.last_blockhash();
let transaction =
system_transaction::transfer(&mint_keypair, &Pubkey::default(), 1, blockhash);
let signature = transaction.signatures[0];
working_bank.process_transaction(&transaction).unwrap_err();
signature
};
let mut transactions = HashMap::new();
info!("Expired transactions are dropped..");
transactions.insert(
Signature::default(),
TransactionInfo::new(Signature::default(), vec![], root_bank.slot() - 1),
);
let result = SendTransactionService::process_transactions(
&working_bank,
&root_bank,
&send_socket,
&tpu_address,
&mut transactions,
);
assert!(transactions.is_empty());
assert_eq!(
result,
ProcessTransactionsResult {
expired: 1,
..ProcessTransactionsResult::default()
}
);
info!("Rooted transactions are dropped...");
transactions.insert(
rooted_signature,
TransactionInfo::new(rooted_signature, vec![], working_bank.slot()),
);
let result = SendTransactionService::process_transactions(
&working_bank,
&root_bank,
&send_socket,
&tpu_address,
&mut transactions,
);
assert!(transactions.is_empty());
assert_eq!(
result,
ProcessTransactionsResult {
rooted: 1,
..ProcessTransactionsResult::default()
}
);
info!("Failed transactions are dropped...");
transactions.insert(
failed_signature,
TransactionInfo::new(failed_signature, vec![], working_bank.slot()),
);
let result = SendTransactionService::process_transactions(
&working_bank,
&root_bank,
&send_socket,
&tpu_address,
&mut transactions,
);
assert!(transactions.is_empty());
assert_eq!(
result,
ProcessTransactionsResult {
failed: 1,
..ProcessTransactionsResult::default()
}
);
info!("Non-rooted transactions are kept...");
transactions.insert(
non_rooted_signature,
TransactionInfo::new(non_rooted_signature, vec![], working_bank.slot()),
);
let result = SendTransactionService::process_transactions(
&working_bank,
&root_bank,
&send_socket,
&tpu_address,
&mut transactions,
);
assert_eq!(transactions.len(), 1);
assert_eq!(
result,
ProcessTransactionsResult {
retained: 1,
..ProcessTransactionsResult::default()
}
);
transactions.clear();
info!("Unknown transactions are retried...");
transactions.insert(
Signature::default(),
TransactionInfo::new(Signature::default(), vec![], working_bank.slot()),
);
let result = SendTransactionService::process_transactions(
&working_bank,
&root_bank,
&send_socket,
&tpu_address,
&mut transactions,
);
assert_eq!(transactions.len(), 1);
assert_eq!(
result,
ProcessTransactionsResult {
retried: 1,
..ProcessTransactionsResult::default()
}
);
}
}

View File

@@ -1,38 +1,39 @@
[package] [package]
authors = ["Solana Maintainers <maintainers@solana.foundation>"] authors = ["Solana Maintainers <maintainers@solana.com>"]
edition = "2018" edition = "2018"
name = "solana-bench-exchange" name = "solana-bench-exchange"
version = "1.5.1" version = "0.19.2"
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/"
publish = false publish = false
[dependencies] [dependencies]
clap = "2.33.1" bincode = "1.1.4"
itertools = "0.9.0" bs58 = "0.3.0"
log = "0.4.11" clap = "2.32.0"
num-derive = "0.3" env_logger = "0.6.2"
itertools = "0.8.0"
log = "0.4.8"
num-derive = "0.2"
num-traits = "0.2" num-traits = "0.2"
rand = "0.7.0" rand = "0.6.5"
rayon = "1.4.0" rayon = "1.2.0"
serde_json = "1.0.56" serde = "1.0.101"
serde_yaml = "0.8.13" serde_derive = "1.0.101"
solana-clap-utils = { path = "../clap-utils", version = "1.5.1" } serde_json = "1.0.40"
solana-core = { path = "../core", version = "1.5.1" } serde_yaml = "0.8.9"
solana-genesis = { path = "../genesis", version = "1.5.1" } # solana-runtime = { path = "../solana/runtime"}
solana-client = { path = "../client", version = "1.5.1" } solana-core = { path = "../core", version = "0.19.2" }
solana-faucet = { path = "../faucet", version = "1.5.1" } solana-genesis = { path = "../genesis", version = "0.19.2" }
solana-exchange-program = { path = "../programs/exchange", version = "1.5.1" } solana-client = { path = "../client", version = "0.19.2" }
solana-logger = { path = "../logger", version = "1.5.1" } solana-drone = { path = "../drone", version = "0.19.2" }
solana-metrics = { path = "../metrics", version = "1.5.1" } solana-exchange-api = { path = "../programs/exchange_api", version = "0.19.2" }
solana-net-utils = { path = "../net-utils", version = "1.5.1" } solana-exchange-program = { path = "../programs/exchange_program", version = "0.19.2" }
solana-runtime = { path = "../runtime", version = "1.5.1" } solana-logger = { path = "../logger", version = "0.19.2" }
solana-sdk = { path = "../sdk", version = "1.5.1" } solana-metrics = { path = "../metrics", version = "0.19.2" }
solana-version = { path = "../version", version = "1.5.1" } solana-netutil = { path = "../netutil", version = "0.19.2" }
solana-runtime = { path = "../runtime", version = "0.19.2" }
[dev-dependencies] solana-sdk = { path = "../sdk", version = "0.19.2" }
solana-local-cluster = { path = "../local-cluster", version = "1.5.1" } untrusted = "0.7.0"
ws = "0.9.0"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@@ -360,7 +360,7 @@ The Matcher would initiate the following last swap:
- Row 1, To: Investor 1 trades 2 A token to 12 B tokens - 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 - Row 1, From: Investor 2 trades 2 A token from 12 B tokens
- Matcher takes 2 B tokens as profit - Matcher takes 4 B tokens as profit
Table becomes: Table becomes:

View File

@@ -7,37 +7,32 @@ use rand::{thread_rng, Rng};
use rayon::prelude::*; use rayon::prelude::*;
use solana_client::perf_utils::{sample_txs, SampleStats}; use solana_client::perf_utils::{sample_txs, SampleStats};
use solana_core::gen_keys::GenKeys; use solana_core::gen_keys::GenKeys;
use solana_exchange_program::{exchange_instruction, exchange_state::*, id}; use solana_drone::drone::request_airdrop_transaction;
use solana_faucet::faucet::request_airdrop_transaction; use solana_exchange_api::exchange_instruction;
use solana_genesis::Base64Account; use solana_exchange_api::exchange_state::*;
use solana_exchange_api::id;
use solana_genesis::PrimordialAccountDetails;
use solana_metrics::datapoint_info; use solana_metrics::datapoint_info;
use solana_sdk::{ use solana_sdk::client::Client;
client::{Client, SyncClient}, use solana_sdk::client::SyncClient;
commitment_config::CommitmentConfig, use solana_sdk::pubkey::Pubkey;
message::Message, use solana_sdk::signature::{Keypair, KeypairUtil};
pubkey::Pubkey, use solana_sdk::timing::{duration_as_ms, duration_as_s};
signature::{Keypair, Signer}, use solana_sdk::transaction::Transaction;
timing::{duration_as_ms, duration_as_s}, use solana_sdk::{system_instruction, system_program};
transaction::Transaction, use std::cmp;
{system_instruction, system_program}, use std::collections::{HashMap, VecDeque};
}; use std::fs::File;
use std::{ use std::io::prelude::*;
cmp, use std::mem;
collections::{HashMap, VecDeque}, use std::net::SocketAddr;
fs::File, use std::path::Path;
io::prelude::*, use std::process::exit;
mem, use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
net::SocketAddr, use std::sync::mpsc::{channel, Receiver, Sender};
path::Path, use std::sync::{Arc, RwLock};
process::exit, use std::thread::{sleep, Builder};
sync::{ use std::time::{Duration, Instant};
atomic::{AtomicBool, AtomicUsize, Ordering},
mpsc::{channel, Receiver, Sender},
Arc, RwLock,
},
thread::{sleep, Builder},
time::{Duration, Instant},
};
// TODO Chunk length as specified results in a bunch of failures, divide by 10 helps... // TODO Chunk length as specified results in a bunch of failures, divide by 10 helps...
// Assume 4MB network buffers, and 512 byte packets // Assume 4MB network buffers, and 512 byte packets
@@ -94,7 +89,7 @@ pub fn create_client_accounts_file(
keypairs.iter().for_each(|keypair| { keypairs.iter().for_each(|keypair| {
accounts.insert( accounts.insert(
serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(), serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(),
Base64Account { PrimordialAccountDetails {
balance: fund_amount, balance: fund_amount,
executable: false, executable: false,
owner: system_program::id().to_string(), owner: system_program::id().to_string(),
@@ -145,7 +140,8 @@ where
let path = Path::new(&client_ids_and_stake_file); let path = Path::new(&client_ids_and_stake_file);
let file = File::open(path).unwrap(); let file = File::open(path).unwrap();
let accounts: HashMap<String, Base64Account> = serde_yaml::from_reader(file).unwrap(); let accounts: HashMap<String, PrimordialAccountDetails> =
serde_yaml::from_reader(file).unwrap();
accounts accounts
.into_iter() .into_iter()
.map(|(keypair, _)| { .map(|(keypair, _)| {
@@ -179,22 +175,19 @@ where
info!("Generating {:?} account keys", total_keys); info!("Generating {:?} account keys", total_keys);
let mut account_keypairs = generate_keypairs(total_keys); let mut account_keypairs = generate_keypairs(total_keys);
let src_keypairs: Vec<_> = account_keypairs.drain(0..accounts_in_groups).collect(); let src_pubkeys: Vec<_> = account_keypairs
let src_pubkeys: Vec<Pubkey> = src_keypairs .drain(0..accounts_in_groups)
.iter()
.map(|keypair| keypair.pubkey()) .map(|keypair| keypair.pubkey())
.collect(); .collect();
let profit_pubkeys: Vec<_> = account_keypairs
let profit_keypairs: Vec<_> = account_keypairs.drain(0..accounts_in_groups).collect(); .drain(0..accounts_in_groups)
let profit_pubkeys: Vec<Pubkey> = profit_keypairs
.iter()
.map(|keypair| keypair.pubkey()) .map(|keypair| keypair.pubkey())
.collect(); .collect();
info!("Create {:?} source token accounts", src_pubkeys.len()); info!("Create {:?} source token accounts", src_pubkeys.len());
create_token_accounts(client, &trader_signers, &src_keypairs); create_token_accounts(client, &trader_signers, &src_pubkeys);
info!("Create {:?} profit token accounts", profit_pubkeys.len()); info!("Create {:?} profit token accounts", profit_pubkeys.len());
create_token_accounts(client, &swapper_signers, &profit_keypairs); create_token_accounts(client, &swapper_signers, &profit_pubkeys);
// Collect the max transaction rate and total tx count seen (single node only) // Collect the max transaction rate and total tx count seen (single node only)
let sample_stats = Arc::new(RwLock::new(Vec::new())); let sample_stats = Arc::new(RwLock::new(Vec::new()));
@@ -251,7 +244,7 @@ where
trace!("Start trader thread"); trace!("Start trader thread");
let trader_thread = { let trader_thread = {
let exit_signal = exit_signal.clone(); let exit_signal = exit_signal.clone();
let shared_txs = shared_txs.clone();
let client = clients[0].clone(); let client = clients[0].clone();
Builder::new() Builder::new()
.name("solana-exchange-trader".to_string()) .name("solana-exchange-trader".to_string())
@@ -388,10 +381,7 @@ fn swapper<T>(
let mut tries = 0; let mut tries = 0;
let mut trade_index = 0; let mut trade_index = 0;
while client while client
.get_balance_with_commitment( .get_balance(&trade_infos[trade_index].trade_account)
&trade_infos[trade_index].trade_account,
CommitmentConfig::recent(),
)
.unwrap_or(0) .unwrap_or(0)
== 0 == 0
{ {
@@ -444,22 +434,24 @@ fn swapper<T>(
} }
account_group = (account_group + 1) % account_groups as usize; account_group = (account_group + 1) % account_groups as usize;
let (blockhash, _fee_calculator, _last_valid_slot) = client let (blockhash, _fee_calculator) = client
.get_recent_blockhash_with_commitment(CommitmentConfig::recent()) .get_recent_blockhash()
.expect("Failed to get blockhash"); .expect("Failed to get blockhash");
let to_swap_txs: Vec<_> = to_swap let to_swap_txs: Vec<_> = to_swap
.par_iter() .par_iter()
.map(|(signer, swap, profit)| { .map(|(signer, swap, profit)| {
let s: &Keypair = &signer; let s: &Keypair = &signer;
let owner = &signer.pubkey(); let owner = &signer.pubkey();
let instruction = exchange_instruction::swap_request( Transaction::new_signed_instructions(
owner, &[s],
&swap.0.pubkey, vec![exchange_instruction::swap_request(
&swap.1.pubkey, owner,
&profit, &swap.0.pubkey,
); &swap.1.pubkey,
let message = Message::new(&[instruction], Some(&s.pubkey())); &profit,
Transaction::new(&[s], message, blockhash) )],
blockhash,
)
}) })
.collect(); .collect();
@@ -566,41 +558,31 @@ fn trader<T>(
trade_account: trade.pubkey(), trade_account: trade.pubkey(),
order_info, order_info,
}); });
trades.push((signer, trade, side, src)); trades.push((signer, trade.pubkey(), side, src));
} }
account_group = (account_group + 1) % account_groups as usize; account_group = (account_group + 1) % account_groups as usize;
let (blockhash, _fee_calculator, _last_valid_slot) = client let (blockhash, _fee_calculator) = client
.get_recent_blockhash_with_commitment(CommitmentConfig::recent()) .get_recent_blockhash()
.expect("Failed to get blockhash"); .expect("Failed to get blockhash");
trades.chunks(chunk_size).for_each(|chunk| { trades.chunks(chunk_size).for_each(|chunk| {
let trades_txs: Vec<_> = chunk let trades_txs: Vec<_> = chunk
.par_iter() .par_iter()
.map(|(owner, trade, side, src)| { .map(|(signer, trade, side, src)| {
let owner_pubkey = &owner.pubkey(); let s: &Keypair = &signer;
let trade_pubkey = &trade.pubkey(); let owner = &signer.pubkey();
let space = mem::size_of::<ExchangeState>() as u64; let space = mem::size_of::<ExchangeState>() as u64;
let instructions = [ Transaction::new_signed_instructions(
system_instruction::create_account( &[s],
owner_pubkey, vec![
trade_pubkey, system_instruction::create_account(owner, trade, 1, space, &id()),
1, exchange_instruction::trade_request(
space, owner, trade, *side, pair, tokens, price, src,
&id(), ),
), ],
exchange_instruction::trade_request( blockhash,
owner_pubkey, )
trade_pubkey,
*side,
pair,
tokens,
price,
src,
),
];
let message = Message::new(&instructions, Some(&owner_pubkey));
Transaction::new(&[owner.as_ref(), trade], message, blockhash)
}) })
.collect(); .collect();
@@ -652,14 +634,12 @@ fn trader<T>(
} }
} }
fn verify_transaction<T>(sync_client: &T, tx: &Transaction) -> bool fn verify_transfer<T>(sync_client: &T, tx: &Transaction) -> bool
where where
T: SyncClient + ?Sized, T: SyncClient + ?Sized,
{ {
for s in &tx.signatures { for s in &tx.signatures {
if let Ok(Some(r)) = if let Ok(Some(r)) = sync_client.get_signature_status(s) {
sync_client.get_signature_status_with_commitment(s, CommitmentConfig::recent())
{
match r { match r {
Ok(_) => { Ok(_) => {
return true; return true;
@@ -678,21 +658,16 @@ fn verify_funding_transfer<T: SyncClient + ?Sized>(
tx: &Transaction, tx: &Transaction,
amount: u64, amount: u64,
) -> bool { ) -> bool {
if verify_transaction(client, tx) { for a in &tx.message().account_keys[1..] {
for a in &tx.message().account_keys[1..] { if client.get_balance(a).unwrap_or(0) >= amount {
if client return true;
.get_balance_with_commitment(a, CommitmentConfig::recent())
.unwrap_or(0)
>= amount
{
return true;
}
} }
} }
false false
} }
pub fn fund_keys<T: Client>(client: &T, source: &Keypair, dests: &[Arc<Keypair>], lamports: u64) { pub fn fund_keys(client: &dyn Client, source: &Keypair, dests: &[Arc<Keypair>], lamports: u64) {
let total = lamports * (dests.len() as u64 + 1); let total = lamports * (dests.len() as u64 + 1);
let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)]; let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)];
let mut notfunded: Vec<&Arc<Keypair>> = dests.iter().collect(); let mut notfunded: Vec<&Arc<Keypair>> = dests.iter().collect();
@@ -738,19 +713,22 @@ pub fn fund_keys<T: Client>(client: &T, source: &Keypair, dests: &[Arc<Keypair>]
let mut to_fund_txs: Vec<_> = chunk let mut to_fund_txs: Vec<_> = chunk
.par_iter() .par_iter()
.map(|(k, m)| { .map(|(k, m)| {
let instructions = system_instruction::transfer_many(&k.pubkey(), &m); (
let message = Message::new(&instructions, Some(&k.pubkey())); k.clone(),
(k.clone(), Transaction::new_unsigned(message)) Transaction::new_unsigned_instructions(system_instruction::transfer_many(
&k.pubkey(),
&m,
)),
)
}) })
.collect(); .collect();
let mut retries = 0; let mut retries = 0;
let amount = chunk[0].1[0].1; let amount = chunk[0].1[0].1;
while !to_fund_txs.is_empty() { while !to_fund_txs.is_empty() {
let receivers: usize = to_fund_txs let receivers = to_fund_txs
.iter() .iter()
.map(|(_, tx)| tx.message().instructions.len()) .fold(0, |len, (_, tx)| len + tx.message().instructions.len());
.sum();
debug!( debug!(
" {} to {} in {} txs", " {} to {} in {} txs",
@@ -763,9 +741,8 @@ pub fn fund_keys<T: Client>(client: &T, source: &Keypair, dests: &[Arc<Keypair>]
to_fund_txs.len(), to_fund_txs.len(),
); );
let (blockhash, _fee_calculator, _last_valid_slot) = client let (blockhash, _fee_calculator) =
.get_recent_blockhash_with_commitment(CommitmentConfig::recent()) client.get_recent_blockhash().expect("blockhash");
.expect("blockhash");
to_fund_txs.par_iter_mut().for_each(|(k, tx)| { to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
tx.sign(&[*k], blockhash); tx.sign(&[*k], blockhash);
}); });
@@ -802,51 +779,35 @@ pub fn fund_keys<T: Client>(client: &T, source: &Keypair, dests: &[Arc<Keypair>]
}); });
funded.append(&mut new_funded); funded.append(&mut new_funded);
funded.retain(|(k, b)| { funded.retain(|(k, b)| {
client client.get_balance(&k.pubkey()).unwrap_or(0) > lamports && *b > lamports
.get_balance_with_commitment(&k.pubkey(), CommitmentConfig::recent())
.unwrap_or(0)
> lamports
&& *b > lamports
}); });
debug!(" Funded: {} left: {}", funded.len(), notfunded.len()); debug!(" Funded: {} left: {}", funded.len(), notfunded.len());
} }
} }
pub fn create_token_accounts<T: Client>( pub fn create_token_accounts(client: &dyn Client, signers: &[Arc<Keypair>], accounts: &[Pubkey]) {
client: &T, let mut notfunded: Vec<(&Arc<Keypair>, &Pubkey)> = signers.iter().zip(accounts).collect();
signers: &[Arc<Keypair>],
accounts: &[Keypair],
) {
let mut notfunded: Vec<(&Arc<Keypair>, &Keypair)> = signers.iter().zip(accounts).collect();
while !notfunded.is_empty() { while !notfunded.is_empty() {
notfunded.chunks(FUND_CHUNK_LEN).for_each(|chunk| { notfunded.chunks(FUND_CHUNK_LEN).for_each(|chunk| {
let mut to_create_txs: Vec<_> = chunk let mut to_create_txs: Vec<_> = chunk
.par_iter() .par_iter()
.map(|(from_keypair, new_keypair)| { .map(|(signer, new)| {
let owner_pubkey = &from_keypair.pubkey(); let owner_pubkey = &signer.pubkey();
let space = mem::size_of::<ExchangeState>() as u64; let space = mem::size_of::<ExchangeState>() as u64;
let create_ix = system_instruction::create_account( let create_ix =
owner_pubkey, system_instruction::create_account(owner_pubkey, new, 1, space, &id());
&new_keypair.pubkey(), let request_ix = exchange_instruction::account_request(owner_pubkey, new);
1,
space,
&id(),
);
let request_ix =
exchange_instruction::account_request(owner_pubkey, &new_keypair.pubkey());
let message = Message::new(&[create_ix, request_ix], Some(&owner_pubkey));
( (
(from_keypair, new_keypair), signer,
Transaction::new_unsigned(message), Transaction::new_unsigned_instructions(vec![create_ix, request_ix]),
) )
}) })
.collect(); .collect();
let accounts: usize = to_create_txs let accounts = to_create_txs
.iter() .iter()
.map(|(_, tx)| tx.message().instructions.len() / 2) .fold(0, |len, (_, tx)| len + tx.message().instructions.len() / 2);
.sum();
debug!( debug!(
" Creating {} accounts in {} txs", " Creating {} accounts in {} txs",
@@ -856,14 +817,13 @@ pub fn create_token_accounts<T: Client>(
let mut retries = 0; let mut retries = 0;
while !to_create_txs.is_empty() { while !to_create_txs.is_empty() {
let (blockhash, _fee_calculator, _last_valid_slot) = client let (blockhash, _fee_calculator) = client
.get_recent_blockhash_with_commitment(CommitmentConfig::recent()) .get_recent_blockhash()
.expect("Failed to get blockhash"); .expect("Failed to get blockhash");
to_create_txs to_create_txs.par_iter_mut().for_each(|(k, tx)| {
.par_iter_mut() let kp: &Keypair = k;
.for_each(|((from_keypair, to_keypair), tx)| { tx.sign(&[kp], blockhash);
tx.sign(&[from_keypair.as_ref(), to_keypair], blockhash); });
});
to_create_txs.iter().for_each(|(_, tx)| { to_create_txs.iter().for_each(|(_, tx)| {
client.async_send_transaction(tx.clone()).expect("transfer"); client.async_send_transaction(tx.clone()).expect("transfer");
}); });
@@ -871,11 +831,11 @@ pub fn create_token_accounts<T: Client>(
let mut waits = 0; let mut waits = 0;
while !to_create_txs.is_empty() { while !to_create_txs.is_empty() {
sleep(Duration::from_millis(200)); sleep(Duration::from_millis(200));
to_create_txs.retain(|(_, tx)| !verify_transaction(client, &tx)); to_create_txs.retain(|(_, tx)| !verify_transfer(client, &tx));
if to_create_txs.is_empty() { if to_create_txs.is_empty() {
break; break;
} }
info!( debug!(
" {} transactions outstanding, waits {:?}", " {} transactions outstanding, waits {:?}",
to_create_txs.len(), to_create_txs.len(),
waits waits
@@ -888,7 +848,7 @@ pub fn create_token_accounts<T: Client>(
if !to_create_txs.is_empty() { if !to_create_txs.is_empty() {
retries += 1; retries += 1;
info!(" Retry {:?} {} txes left", retries, to_create_txs.len()); debug!(" Retry {:?}", retries);
if retries >= 20 { if retries >= 20 {
error!( error!(
"create_token_accounts: Too many retries ({}), give up", "create_token_accounts: Too many retries ({}), give up",
@@ -900,13 +860,9 @@ pub fn create_token_accounts<T: Client>(
} }
}); });
let mut new_notfunded: Vec<(&Arc<Keypair>, &Keypair)> = vec![]; let mut new_notfunded: Vec<(&Arc<Keypair>, &Pubkey)> = vec![];
for f in &notfunded { for f in &notfunded {
if client if client.get_balance(&f.1).unwrap_or(0) == 0 {
.get_balance_with_commitment(&f.1.pubkey(), CommitmentConfig::recent())
.unwrap_or(0)
== 0
{
new_notfunded.push(*f) new_notfunded.push(*f)
} }
} }
@@ -962,13 +918,8 @@ fn generate_keypairs(num: u64) -> Vec<Keypair> {
rnd.gen_n_keypairs(num) rnd.gen_n_keypairs(num)
} }
pub fn airdrop_lamports<T: Client>( pub fn airdrop_lamports(client: &dyn Client, drone_addr: &SocketAddr, id: &Keypair, amount: u64) {
client: &T, let balance = client.get_balance(&id.pubkey());
faucet_addr: &SocketAddr,
id: &Keypair,
amount: u64,
) {
let balance = client.get_balance_with_commitment(&id.pubkey(), CommitmentConfig::recent());
let balance = balance.unwrap_or(0); let balance = balance.unwrap_or(0);
if balance >= amount { if balance >= amount {
return; return;
@@ -979,40 +930,33 @@ pub fn airdrop_lamports<T: Client>(
info!( info!(
"Airdropping {:?} lamports from {} for {}", "Airdropping {:?} lamports from {} for {}",
amount_to_drop, amount_to_drop,
faucet_addr, drone_addr,
id.pubkey(), id.pubkey(),
); );
let mut tries = 0; let mut tries = 0;
loop { loop {
let (blockhash, _fee_calculator, _last_valid_slot) = client let (blockhash, _fee_calculator) = client
.get_recent_blockhash_with_commitment(CommitmentConfig::recent()) .get_recent_blockhash()
.expect("Failed to get blockhash"); .expect("Failed to get blockhash");
match request_airdrop_transaction(&faucet_addr, &id.pubkey(), amount_to_drop, blockhash) { match request_airdrop_transaction(&drone_addr, &id.pubkey(), amount_to_drop, blockhash) {
Ok(transaction) => { Ok(transaction) => {
let signature = client.async_send_transaction(transaction).unwrap(); let signature = client.async_send_transaction(transaction).unwrap();
for _ in 0..30 { for _ in 0..30 {
if let Ok(Some(_)) = client.get_signature_status_with_commitment( if let Ok(Some(_)) = client.get_signature_status(&signature) {
&signature,
CommitmentConfig::recent(),
) {
break; break;
} }
sleep(Duration::from_millis(100)); sleep(Duration::from_millis(100));
} }
if client if client.get_balance(&id.pubkey()).unwrap_or(0) >= amount {
.get_balance_with_commitment(&id.pubkey(), CommitmentConfig::recent())
.unwrap_or(0)
>= amount
{
break; break;
} }
} }
Err(err) => { Err(err) => {
panic!( panic!(
"Error requesting airdrop: {:?} to addr: {:?} amount: {}", "Error requesting airdrop: {:?} to addr: {:?} amount: {}",
err, faucet_addr, amount err, drone_addr, amount
); );
} }
}; };

View File

@@ -1,14 +1,14 @@
use clap::{crate_description, crate_name, value_t, App, Arg, ArgMatches}; use clap::{crate_description, crate_name, crate_version, value_t, App, Arg, ArgMatches};
use solana_core::gen_keys::GenKeys; use solana_core::gen_keys::GenKeys;
use solana_faucet::faucet::FAUCET_PORT; use solana_drone::drone::DRONE_PORT;
use solana_sdk::signature::{read_keypair_file, Keypair}; use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::process::exit; use std::process::exit;
use std::time::Duration; use std::time::Duration;
pub struct Config { pub struct Config {
pub entrypoint_addr: SocketAddr, pub entrypoint_addr: SocketAddr,
pub faucet_addr: SocketAddr, pub drone_addr: SocketAddr,
pub identity: Keypair, pub identity: Keypair,
pub threads: usize, pub threads: usize,
pub num_nodes: usize, pub num_nodes: usize,
@@ -27,7 +27,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)), entrypoint_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)),
identity: Keypair::new(), identity: Keypair::new(),
num_nodes: 1, num_nodes: 1,
threads: 4, threads: 4,
@@ -44,10 +44,10 @@ impl Default for Config {
} }
} }
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!()) App::new(crate_name!())
.about(crate_description!()) .about(crate_description!())
.version(version) .version(crate_version!())
.arg( .arg(
Arg::with_name("entrypoint") Arg::with_name("entrypoint")
.short("n") .short("n")
@@ -59,14 +59,14 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
.help("Cluster entry point; defaults to 127.0.0.1:8001"), .help("Cluster 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)
.required(false) .required(false)
.default_value("127.0.0.1:9900") .default_value("127.0.0.1:9900")
.help("Location of the faucet; defaults to 127.0.0.1:9900"), .help("Location of the drone; defaults to 127.0.0.1:9900"),
) )
.arg( .arg(
Arg::with_name("identity") Arg::with_name("identity")
@@ -163,26 +163,23 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
) )
} }
#[allow(clippy::field_reassign_with_default)] pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
pub fn extract_args(matches: &ArgMatches) -> Config {
let mut args = Config::default(); let mut args = Config::default();
args.entrypoint_addr = solana_net_utils::parse_host_port( args.entrypoint_addr = solana_netutil::parse_host_port(matches.value_of("entrypoint").unwrap())
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| { .unwrap_or_else(|e| {
eprintln!("failed to parse faucet address: {}", e); eprintln!("failed to parse entrypoint address: {}", e);
exit(1)
});
args.drone_addr = solana_netutil::parse_host_port(matches.value_of("drone").unwrap())
.unwrap_or_else(|e| {
eprintln!("failed to parse drone address: {}", e);
exit(1) exit(1)
}); });
if matches.is_present("identity") { if matches.is_present("identity") {
args.identity = read_keypair_file(matches.value_of("identity").unwrap()) args.identity = read_keypair(matches.value_of("identity").unwrap())
.expect("can't read client identity"); .expect("can't read client identity");
} else { } else {
args.identity = { args.identity = {

View File

@@ -5,18 +5,18 @@ pub mod order_book;
use crate::bench::{airdrop_lamports, create_client_accounts_file, do_bench_exchange, Config}; use crate::bench::{airdrop_lamports, create_client_accounts_file, do_bench_exchange, Config};
use log::*; use log::*;
use solana_core::gossip_service::{discover_cluster, get_multi_client}; use solana_core::gossip_service::{discover_cluster, get_multi_client};
use solana_sdk::signature::Signer; use solana_sdk::signature::KeypairUtil;
fn main() { fn main() {
solana_logger::setup(); solana_logger::setup();
solana_metrics::set_panic_hook("bench-exchange"); solana_metrics::set_panic_hook("bench-exchange");
let matches = cli::build_args(solana_version::version!()).get_matches(); let matches = cli::build_args().get_matches();
let cli_config = cli::extract_args(&matches); let cli_config = cli::extract_args(&matches);
let cli::Config { let cli::Config {
entrypoint_addr, entrypoint_addr,
faucet_addr, drone_addr,
identity, identity,
threads, threads,
num_nodes, num_nodes,
@@ -54,9 +54,10 @@ fn main() {
); );
} else { } else {
info!("Connecting to the cluster"); info!("Connecting to the cluster");
let nodes = discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|_| { let (nodes, _replicators) =
panic!("Failed to discover nodes"); discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|_| {
}); panic!("Failed to discover nodes");
});
let (client, num_clients) = get_multi_client(&nodes); let (client, num_clients) = get_multi_client(&nodes);
@@ -72,7 +73,7 @@ fn main() {
const NUM_SIGNERS: u64 = 2; const NUM_SIGNERS: u64 = 2;
airdrop_lamports( airdrop_lamports(
&client, &client,
&faucet_addr, &drone_addr,
&config.identity, &config.identity,
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS, fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
); );

View File

@@ -1,7 +1,7 @@
use itertools::EitherOrBoth::{Both, Left, Right}; use itertools::EitherOrBoth::{Both, Left, Right};
use itertools::Itertools; use itertools::Itertools;
use log::*; use log::*;
use solana_exchange_program::exchange_state::*; use solana_exchange_api::exchange_state::*;
use solana_sdk::pubkey::Pubkey; use solana_sdk::pubkey::Pubkey;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::BinaryHeap; use std::collections::BinaryHeap;

View File

@@ -1,108 +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 config = Config {
identity: Keypair::new(),
duration: Duration::from_secs(1),
fund_amount: 100_000,
threads: 1,
transfer_delay: 20, // 15
batch_size: 100, // 1000
chunk_size: 10, // 200
account_groups: 1, // 10
..Config::default()
};
let Config {
fund_amount,
batch_size,
account_groups,
..
} = config;
let accounts_in_groups = batch_size * account_groups;
let cluster = LocalCluster::new(&mut 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_builtin("exchange_program", id(), process_instruction);
let clients = vec![BankClient::new(bank)];
do_bench_exchange(
clients,
Config {
identity,
duration: Duration::from_secs(1),
fund_amount: 100_000,
threads: 1,
transfer_delay: 20, // 0;
batch_size: 100, // 1500;
chunk_size: 10, // 1500;
account_groups: 1, // 50;
..Config::default()
},
);
}

View File

@@ -1,20 +1,14 @@
[package] [package]
authors = ["Solana Maintainers <maintainers@solana.foundation>"] authors = ["Solana Maintainers <maintainers@solana.com>"]
edition = "2018" edition = "2018"
name = "solana-bench-streamer" name = "solana-bench-streamer"
version = "1.5.1" version = "0.19.2"
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/"
publish = false
[dependencies] [dependencies]
clap = "2.33.1" clap = "2.33.0"
solana-clap-utils = { path = "../clap-utils", version = "1.5.1" } solana-core = { path = "../core", version = "0.19.2" }
solana-streamer = { path = "../streamer", version = "1.5.1" } solana-logger = { path = "../logger", version = "0.19.2" }
solana-logger = { path = "../logger", version = "1.5.1" } solana-netutil = { path = "../netutil", version = "0.19.2" }
solana-net-utils = { path = "../net-utils", version = "1.5.1" }
solana-version = { path = "../version", version = "1.5.1" }
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@@ -1,13 +1,15 @@
use clap::{crate_description, crate_name, App, Arg}; use clap::{crate_description, crate_name, crate_version, App, Arg};
use solana_streamer::packet::{Packet, Packets, PacketsRecycler, PACKET_DATA_SIZE}; use solana_core::packet::PacketsRecycler;
use solana_streamer::streamer::{receiver, PacketReceiver}; use solana_core::packet::{Packet, Packets, BLOB_SIZE, PACKET_DATA_SIZE};
use solana_core::result::Result;
use solana_core::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;
@@ -27,7 +29,7 @@ fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
let mut num = 0; let mut num = 0;
for p in &msgs.packets { for p in &msgs.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;
} }
@@ -52,7 +54,7 @@ fn main() -> Result<()> {
let matches = App::new(crate_name!()) let matches = App::new(crate_name!())
.about(crate_description!()) .about(crate_description!())
.version(solana_version::version!()) .version(crate_version!())
.arg( .arg(
Arg::with_name("num-recv-sockets") Arg::with_name("num-recv-sockets")
.long("num-recv-sockets") .long("num-recv-sockets")
@@ -67,8 +69,7 @@ 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));
@@ -76,7 +77,7 @@ fn main() -> Result<()> {
let mut read_threads = Vec::new(); let mut read_threads = Vec::new();
let recycler = PacketsRecycler::default(); 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();

View File

@@ -1,37 +1,35 @@
[package] [package]
authors = ["Solana Maintainers <maintainers@solana.foundation>"] authors = ["Solana Maintainers <maintainers@solana.com>"]
edition = "2018" edition = "2018"
name = "solana-bench-tps" name = "solana-bench-tps"
version = "1.5.1" version = "0.19.2"
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/"
publish = false
[dependencies] [dependencies]
bincode = "1.3.1" bincode = "1.1.4"
clap = "2.33.1" clap = "2.33.0"
log = "0.4.11" log = "0.4.8"
rayon = "1.4.0" rayon = "1.2.0"
serde_json = "1.0.56" serde = "1.0.101"
serde_yaml = "0.8.13" serde_derive = "1.0.101"
solana-clap-utils = { path = "../clap-utils", version = "1.5.1" } serde_json = "1.0.40"
solana-core = { path = "../core", version = "1.5.1" } serde_yaml = "0.8.9"
solana-genesis = { path = "../genesis", version = "1.5.1" } solana-core = { path = "../core", version = "0.19.2" }
solana-client = { path = "../client", version = "1.5.1" } solana-genesis = { path = "../genesis", version = "0.19.2" }
solana-faucet = { path = "../faucet", version = "1.5.1" } solana-client = { path = "../client", version = "0.19.2" }
solana-logger = { path = "../logger", version = "1.5.1" } solana-drone = { path = "../drone", version = "0.19.2" }
solana-metrics = { path = "../metrics", version = "1.5.1" } solana-librapay-api = { path = "../programs/librapay_api", version = "0.19.2" }
solana-measure = { path = "../measure", version = "1.5.1" } solana-logger = { path = "../logger", version = "0.19.2" }
solana-net-utils = { path = "../net-utils", version = "1.5.1" } solana-metrics = { path = "../metrics", version = "0.19.2" }
solana-runtime = { path = "../runtime", version = "1.5.1" } solana-measure = { path = "../measure", version = "0.19.2" }
solana-sdk = { path = "../sdk", version = "1.5.1" } solana-netutil = { path = "../netutil", version = "0.19.2" }
solana-version = { path = "../version", version = "1.5.1" } solana-runtime = { path = "../runtime", version = "0.19.2" }
solana-sdk = { path = "../sdk", version = "0.19.2" }
solana-move-loader-program = { path = "../programs/move_loader_program", version = "0.19.2" }
solana-move-loader-api = { path = "../programs/move_loader_api", version = "0.19.2" }
[dev-dependencies] [dev-dependencies]
serial_test = "0.4.0" serial_test = "0.2.0"
serial_test_derive = "0.4.0" serial_test_derive = "0.2.0"
solana-local-cluster = { path = "../local-cluster", version = "1.5.1" }
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,56 @@
use clap::{crate_description, crate_name, App, Arg, ArgMatches}; use clap::{crate_description, crate_name, crate_version, App, Arg, ArgMatches};
use solana_faucet::faucet::FAUCET_PORT; use solana_drone::drone::DRONE_PORT;
use solana_sdk::fee_calculator::FeeRateGovernor; use solana_sdk::fee_calculator::FeeCalculator;
use solana_sdk::{ use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
pubkey::Pubkey,
signature::{read_keypair_file, Keypair},
};
use std::{net::SocketAddr, process::exit, time::Duration}; use std::{net::SocketAddr, process::exit, time::Duration};
const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = solana_sdk::native_token::LAMPORTS_PER_SOL; const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = 64 * 1024;
/// 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 entrypoint_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 client_ids_and_stake_file: String,
pub write_to_client_file: bool, pub write_to_client_file: bool,
pub read_from_client_file: bool, pub read_from_client_file: bool,
pub target_lamports_per_signature: u64, pub target_lamports_per_signature: u64,
pub multi_client: bool, pub use_move: bool,
pub num_lamports_per_account: u64, pub num_lamports_per_account: u64,
pub target_slots_per_epoch: u64,
pub target_node: Option<Pubkey>,
} }
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)), entrypoint_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(), client_ids_and_stake_file: String::new(),
write_to_client_file: false, write_to_client_file: false,
read_from_client_file: false, read_from_client_file: false,
target_lamports_per_signature: FeeRateGovernor::default().target_lamports_per_signature, target_lamports_per_signature: FeeCalculator::default().target_lamports_per_signature,
multi_client: true, use_move: false,
num_lamports_per_account: NUM_LAMPORTS_PER_ACCOUNT_DEFAULT, num_lamports_per_account: NUM_LAMPORTS_PER_ACCOUNT_DEFAULT,
target_slots_per_epoch: 0,
target_node: None,
} }
} }
} }
/// 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(crate_name!()).about(crate_description!())
.version(version) .version(crate_version!())
.arg( .arg(
Arg::with_name("entrypoint") Arg::with_name("entrypoint")
.short("n") .short("n")
@@ -69,12 +60,12 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
.help("Rendezvous with the cluster at this entry point; defaults to 127.0.0.1:8001"), .help("Rendezvous with the cluster at this entry point; defaults to 127.0.0.1:8001"),
) )
.arg( .arg(
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 entrypoint:DRONE_PORT"),
) )
.arg( .arg(
Arg::with_name("identity") Arg::with_name("identity")
@@ -113,17 +104,9 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
.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(
Arg::with_name("no-multi-client") Arg::with_name("use-move")
.long("no-multi-client") .long("use-move")
.help("Disable multi-client support, only transact with the entrypoint."), .help("Use Move language transactions to perform transfers."),
)
.arg(
Arg::with_name("target_node")
.long("target-node")
.requires("no-multi-client")
.takes_value(true)
.value_name("PUBKEY")
.help("Specify an exact node to send transactions to."),
) )
.arg( .arg(
Arg::with_name("tx_count") Arg::with_name("tx_count")
@@ -132,13 +115,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")
@@ -180,15 +156,6 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
"Number of lamports per account.", "Number of lamports per account.",
), ),
) )
.arg(
Arg::with_name("target_slots_per_epoch")
.long("target-slots-per-epoch")
.value_name("SLOTS")
.takes_value(true)
.help(
"Wait until epochs are this many slots long.",
),
)
} }
/// Parses a clap `ArgMatches` structure into a `Config` /// Parses a clap `ArgMatches` structure into a `Config`
@@ -196,25 +163,25 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
/// * `matches` - command line arguments parsed by clap /// * `matches` - command line arguments parsed by clap
/// # Panics /// # Panics
/// Panics if there is trouble parsing any of the arguments /// Panics if there is trouble parsing any of the arguments
pub fn extract_args(matches: &ArgMatches) -> 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("entrypoint") {
args.entrypoint_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| { args.entrypoint_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
eprintln!("failed to parse entrypoint address: {}", e); eprintln!("failed to parse entrypoint address: {}", 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 = solana_netutil::parse_host_port(addr).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");
} }
@@ -234,15 +201,7 @@ pub fn extract_args(matches: &ArgMatches) -> 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") {
@@ -269,21 +228,11 @@ pub fn extract_args(matches: &ArgMatches) -> Config {
args.target_lamports_per_signature = v.to_string().parse().expect("can't parse lamports"); args.target_lamports_per_signature = v.to_string().parse().expect("can't parse lamports");
} }
args.multi_client = !matches.is_present("no-multi-client"); args.use_move = matches.is_present("use-move");
args.target_node = matches
.value_of("target_node")
.map(|target_str| target_str.parse().unwrap());
if let Some(v) = matches.value_of("num_lamports_per_account") { 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.num_lamports_per_account = v.to_string().parse().expect("can't parse lamports");
} }
if let Some(t) = matches.value_of("target_slots_per_epoch") {
args.target_slots_per_epoch = t
.to_string()
.parse()
.expect("can't parse target slots per epoch");
}
args args
} }

View File

@@ -1,47 +1,43 @@
use log::*; use log::*;
use solana_bench_tps::bench::{do_bench_tps, generate_and_fund_keypairs, generate_keypairs}; use solana_bench_tps::bench::{do_bench_tps, generate_and_fund_keypairs, generate_keypairs};
use solana_bench_tps::cli; use solana_bench_tps::cli;
use solana_core::gossip_service::{discover_cluster, get_client, get_multi_client}; use solana_core::gossip_service::{discover_cluster, get_multi_client};
use solana_genesis::Base64Account; use solana_genesis::PrimordialAccountDetails;
use solana_sdk::fee_calculator::FeeRateGovernor; use solana_sdk::fee_calculator::FeeCalculator;
use solana_sdk::signature::{Keypair, Signer}; use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_program; use solana_sdk::system_program;
use std::{collections::HashMap, fs::File, io::prelude::*, path::Path, process::exit, sync::Arc}; use std::{collections::HashMap, fs::File, io::prelude::*, path::Path, process::exit};
/// Number of signatures for all transactions in ~1 week at ~100K TPS /// Number of signatures for all transactions in ~1 week at ~100K TPS
pub const NUM_SIGNATURES_FOR_TXS: u64 = 100_000 * 60 * 60 * 24 * 7; pub const NUM_SIGNATURES_FOR_TXS: u64 = 100_000 * 60 * 60 * 24 * 7;
fn main() { fn main() {
solana_logger::setup_with_default("solana=info"); solana_logger::setup_with_filter("solana=info");
solana_metrics::set_panic_hook("bench-tps"); solana_metrics::set_panic_hook("bench-tps");
let matches = cli::build_args(solana_version::version!()).get_matches(); let matches = cli::build_args().get_matches();
let cli_config = cli::extract_args(&matches); let cli_config = cli::extract_args(&matches);
let cli::Config { let cli::Config {
entrypoint_addr, entrypoint_addr,
faucet_addr, drone_addr,
id, id,
num_nodes, num_nodes,
tx_count, tx_count,
keypair_multiplier,
client_ids_and_stake_file, client_ids_and_stake_file,
write_to_client_file, write_to_client_file,
read_from_client_file, read_from_client_file,
target_lamports_per_signature, target_lamports_per_signature,
multi_client, use_move,
num_lamports_per_account, num_lamports_per_account,
target_node,
.. ..
} = &cli_config; } = &cli_config;
let keypair_count = *tx_count * keypair_multiplier;
if *write_to_client_file { if *write_to_client_file {
info!("Generating {} keypairs", keypair_count); info!("Generating {} keypairs", *tx_count * 2);
let (keypairs, _) = generate_keypairs(&id, keypair_count as u64); let (keypairs, _) = generate_keypairs(&id, *tx_count as u64 * 2);
let num_accounts = keypairs.len() as u64; let num_accounts = keypairs.len() as u64;
let max_fee = let max_fee = FeeCalculator::new(*target_lamports_per_signature).max_lamports_per_signature;
FeeRateGovernor::new(*target_lamports_per_signature, 0).max_lamports_per_signature;
let num_lamports_per_account = (num_accounts - 1 + NUM_SIGNATURES_FOR_TXS * max_fee) let num_lamports_per_account = (num_accounts - 1 + NUM_SIGNATURES_FOR_TXS * max_fee)
/ num_accounts / num_accounts
+ num_lamports_per_account; + num_lamports_per_account;
@@ -49,7 +45,7 @@ fn main() {
keypairs.iter().for_each(|keypair| { keypairs.iter().for_each(|keypair| {
accounts.insert( accounts.insert(
serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(), serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(),
Base64Account { PrimordialAccountDetails {
balance: num_lamports_per_account, balance: num_lamports_per_account,
executable: false, executable: false,
owner: system_program::id().to_string(), owner: system_program::id().to_string(),
@@ -67,44 +63,29 @@ fn main() {
} }
info!("Connecting to the cluster"); info!("Connecting to the cluster");
let nodes = discover_cluster(&entrypoint_addr, *num_nodes).unwrap_or_else(|err| { let (nodes, _replicators) =
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err); discover_cluster(&entrypoint_addr, *num_nodes).unwrap_or_else(|err| {
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
exit(1);
});
let (client, num_clients) = get_multi_client(&nodes);
if nodes.len() < num_clients {
eprintln!(
"Error: Insufficient nodes discovered. Expecting {} or more",
num_nodes
);
exit(1); exit(1);
}); }
let client = if *multi_client { let (keypairs, move_keypairs, keypair_balance) = if *read_from_client_file && !use_move {
let (client, num_clients) = get_multi_client(&nodes);
if nodes.len() < num_clients {
eprintln!(
"Error: Insufficient nodes discovered. Expecting {} or more",
num_nodes
);
exit(1);
}
Arc::new(client)
} else if let Some(target_node) = target_node {
info!("Searching for target_node: {:?}", target_node);
let mut target_client = None;
for node in nodes {
if node.id == *target_node {
target_client = Some(Arc::new(get_client(&[node])));
break;
}
}
target_client.unwrap_or_else(|| {
eprintln!("Target node {} not found", target_node);
exit(1);
})
} else {
Arc::new(get_client(&nodes))
};
let keypairs = if *read_from_client_file {
let path = Path::new(&client_ids_and_stake_file); let path = Path::new(&client_ids_and_stake_file);
let file = File::open(path).unwrap(); let file = File::open(path).unwrap();
info!("Reading {}", client_ids_and_stake_file); info!("Reading {}", client_ids_and_stake_file);
let accounts: HashMap<String, Base64Account> = serde_yaml::from_reader(file).unwrap(); let accounts: HashMap<String, PrimordialAccountDetails> =
serde_yaml::from_reader(file).unwrap();
let mut keypairs = vec![]; let mut keypairs = vec![];
let mut last_balance = 0; let mut last_balance = 0;
@@ -116,10 +97,10 @@ fn main() {
last_balance = primordial_account.balance; last_balance = primordial_account.balance;
}); });
if keypairs.len() < keypair_count { if keypairs.len() < tx_count * 2 {
eprintln!( eprintln!(
"Expected {} accounts in {}, only received {} (--tx_count mismatch?)", "Expected {} accounts in {}, only received {} (--tx_count mismatch?)",
keypair_count, tx_count * 2,
client_ids_and_stake_file, client_ids_and_stake_file,
keypairs.len(), keypairs.len(),
); );
@@ -128,15 +109,16 @@ fn main() {
// Sort keypairs so that do_bench_tps() uses the same subset of accounts for each run. // 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 // This prevents the amount of storage needed for bench-tps accounts from creeping up
// across multiple runs. // across multiple runs.
keypairs.sort_by_key(|x| x.pubkey().to_string()); keypairs.sort_by(|x, y| x.pubkey().to_string().cmp(&y.pubkey().to_string()));
keypairs (keypairs, None, last_balance)
} else { } else {
generate_and_fund_keypairs( generate_and_fund_keypairs(
client.clone(), &client,
Some(*faucet_addr), Some(*drone_addr),
&id, &id,
keypair_count, *tx_count,
*num_lamports_per_account, *num_lamports_per_account,
*use_move,
) )
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
eprintln!("Error could not fund keys: {:?}", e); eprintln!("Error could not fund keys: {:?}", e);
@@ -144,5 +126,11 @@ fn main() {
}) })
}; };
do_bench_tps(client, cli_config, keypairs); do_bench_tps(
vec![client],
cli_config,
keypairs,
keypair_balance,
move_keypairs,
);
} }

View File

@@ -1,68 +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};
use solana_sdk::signature::{Keypair, Signer};
use std::sync::{mpsc::channel, Arc};
use std::time::Duration;
fn test_bench_tps_local_cluster(config: Config) {
let native_instruction_processors = vec![];
solana_logger::setup();
const NUM_NODES: usize = 1;
let cluster = LocalCluster::new(&mut 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 = generate_and_fund_keypairs(
client.clone(),
Some(faucet_addr),
&config.id,
keypair_count,
lamports_per_account,
)
.unwrap();
let _total = do_bench_tps(client, config, keypairs);
#[cfg(not(debug_assertions))]
assert!(_total > 100);
}
#[test]
#[serial]
fn test_bench_tps_local_cluster_solana() {
test_bench_tps_local_cluster(Config {
tx_count: 100,
duration: Duration::from_secs(10),
..Config::default()
});
}

1
book/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
theme/highlight.js binary

26
book/README.md Normal file
View File

@@ -0,0 +1,26 @@
Building the Solana book
---
Install the book's dependnecies, build, and test the book:
```bash
$ ./build.sh
```
Run any Rust tests in the markdown:
```bash
$ make test
```
Render markdown as HTML:
```bash
$ make build
```
Render and view the book:
```bash
$ make open
```

15
book/art/consensus.msc Normal file
View 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" ] ;
}

View File

@@ -24,7 +24,7 @@ msc {
... ; ... ;
Validator abox Validator [label="\nmax\nlockout\n"]; Validator abox Validator [label="\nmax\nlockout\n"];
|||; |||;
Cluster box Cluster [label="credits redeemed (at epoch)"]; StakerX => Cluster [label="StakeState::RedeemCredits()"];
StakerY => Cluster [label="StakeState::RedeemCredits()"] ;
} }

20
book/art/sdk-tools.bob Normal file
View File

@@ -0,0 +1,20 @@
.----------------------------------------.
| Solana Runtime |
| |
| .------------. .------------. |
| | | | | |
.-------->| Verifier +-->| Accounts | |
| | | | | | |
.----------. | | `------------` `------------` |
| +--------` | ^ |
| Client | | LoadAccounts | |
| +--------. | .----------------` |
`----------` | | | |
| | .------+-----. .-------------. |
| | | | | | |
`-------->| Loader +-->| Interpreter | |
| | | | | |
| `------------` `-------------` |
| |
`----------------------------------------`

View File

@@ -0,0 +1,18 @@
+------------+
| Bank-Merkle|
+------------+
^ ^
/ \
+-----------------+ +-------------+
| Bank-Diff-Merkle| | Block-Merkle|
+-----------------+ +-------------+
^ ^
/ \
+------+ +--------------------------+
| Hash | | Previous Bank-Diff-Merkle|
+------+ +--------------------------+
^ ^
/ \
+---------------+ +---------------+
| Hash(Account1)| | Hash(Account2)|
+---------------+ +---------------+

22
book/art/tvu.bob Normal file
View File

@@ -0,0 +1,22 @@
.--------.
| Leader |
`--------`
^
|
.------------------------------------|--------------------.
| TVU | |
| | |
| .-------. .------------. .----+---. .---------. |
.------------. | | Blob | | Retransmit | | Replay | | Storage | |
| Upstream +----->| Fetch +-->| Stage +-->| Stage +-->| Stage | |
| Validators | | | Stage | | | | | | | |
`------------` | `-------` `----+-------` `----+---` `---------` |
| ^ | | |
| | | | |
`--------|----------|----------------|--------------------`
| | |
| V v
.+-----------. .------.
| Gossip | | Bank |
| Service | `------`
`------------`

30
book/art/validator.bob Normal file
View File

@@ -0,0 +1,30 @@
.--------------------------------------.
| Validator |
| |
.--------. | .-------------------. |
| |---->| | |
| Client | | | JSON RPC Service | |
| |<----| | |
`----+---` | `-------------------` |
| | ^ |
| | | .----------------. | .------------------.
| | | | Gossip Service |<----------| Validators |
| | | `----------------` | | |
| | | ^ | | |
| | | | | | .------------. |
| | .---+---. .----+---. .-----------. | | | | |
| | | Bank |<-+ Replay | | BlobFetch |<------+ Upstream | |
| | | Forks | | Stage | | Stage | | | | Validators | |
| | `-------` `--------` `--+--------` | | | | |
| | ^ ^ | | | `------------` |
| | | | v | | |
| | | .--+--------. | | |
| | | | Blocktree | | | |
| | | `-----------` | | .------------. |
| | | ^ | | | | |
| | | | | | | Downstream | |
| | .--+--. .-------+---. | | | Validators | |
`-------->| TPU +---->| Broadcast +--------------->| | |
| `-----` | Stage | | | `------------` |
| `-----------` | `------------------`
`--------------------------------------`

10
book/book.toml Normal file
View File

@@ -0,0 +1,10 @@
[book]
title = "Solana: Blockchain Rebuilt for Scale"
authors = ["The Solana Team"]
[build]
build-dir = "html"
create-missing = false
[output.html]
theme = "theme"

11
book/build-svg.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
make -j"$(nproc)" -B svg
if [[ -n $CI ]]; then
# In CI confirm that no svgs need to be built
git diff --exit-code
fi

6
book/build.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
make -j"$(nproc)" test

43
book/makefile Normal file
View File

@@ -0,0 +1,43 @@
BOB_SRCS=$(wildcard art/*.bob)
MSC_SRCS=$(wildcard art/*.msc)
MD_SRCS=$(wildcard src/*.md)
SVG_IMGS=$(BOB_SRCS:art/%.bob=src/.gitbook/assets/%.svg) $(MSC_SRCS:art/%.msc=src/.gitbook/assets/%.svg)
TARGET=html/index.html
TEST_STAMP=src/tests.ok
all: $(TARGET)
svg: $(SVG_IMGS)
test: $(TEST_STAMP)
open: $(TEST_STAMP)
mdbook build --open
watch: $(SVG_IMGS)
mdbook watch
src/.gitbook/assets/%.svg: art/%.bob
@mkdir -p $(@D)
svgbob < $< > $@
src/.gitbook/assets/%.svg: art/%.msc
@mkdir -p $(@D)
mscgen -T svg -i $< -o $@
src/%.md: %.md
@mkdir -p $(@D)
@cp $< $@
$(TEST_STAMP): $(TARGET)
mdbook test
touch $@
$(TARGET): $(SVG_IMGS) $(MD_SRCS)
mdbook build
clean:
rm -f $(SVG_IMGS) src/tests.ok
rm -rf html

View File

@@ -0,0 +1,192 @@
<svg class="bob" font-family="arial" font-size="14" height="288" width="736" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="288" width="736" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="216" y2="280"/>
<line x1="4" x2="156" y1="216" y2="216"/>
<line x1="4" x2="172" y1="280" y2="280"/>
<line x1="156" x2="172" y1="216" y2="216"/>
<line x1="172" x2="172" y1="216" y2="280"/>
</g>
<g>
<line x1="124" x2="124" y1="104" y2="168"/>
<line x1="124" x2="204" y1="104" y2="104"/>
<line x1="124" x2="156" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="156" x2="156" y1="168" y2="204"/>
<line x1="156" x2="204" y1="168" y2="168"/>
<line x1="204" x2="292" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="168" y2="204"/>
<line x1="204" x2="292" y1="168" y2="168"/>
<line x1="292" x2="292" y1="104" y2="168"/>
</g>
<g>
<line x1="188" x2="188" y1="216" y2="280"/>
<line x1="188" x2="204" y1="216" y2="216"/>
<line x1="188" x2="356" y1="280" y2="280"/>
<line x1="204" x2="356" y1="216" y2="216"/>
<line x1="356" x2="356" y1="216" y2="280"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="40" y2="92"/>
<line x1="204" x2="276" y1="40" y2="40"/>
<line x1="276" x2="276" y1="8" y2="40"/>
<line x1="276" x2="444" y1="8" y2="8"/>
<line x1="276" x2="276" y1="40" y2="72"/>
<line x1="276" x2="444" y1="72" y2="72"/>
<line x1="444" x2="444" y1="8" y2="40"/>
<line x1="444" x2="444" y1="40" y2="72"/>
<line x1="444" x2="532" y1="40" y2="40"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="40" y2="92"/>
</g>
<g>
<line x1="380" x2="380" y1="216" y2="280"/>
<line x1="380" x2="532" y1="216" y2="216"/>
<line x1="380" x2="548" y1="280" y2="280"/>
<line x1="532" x2="548" y1="216" y2="216"/>
<line x1="548" x2="548" y1="216" y2="280"/>
</g>
<g>
<line x1="444" x2="444" y1="104" y2="168"/>
<line x1="444" x2="532" y1="104" y2="104"/>
<line x1="444" x2="532" y1="168" y2="168"/>
<line x1="532" x2="612" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="168" y2="204"/>
<line x1="532" x2="580" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="580" x2="580" y1="168" y2="204"/>
<line x1="580" x2="612" y1="168" y2="168"/>
<line x1="612" x2="612" y1="104" y2="168"/>
</g>
<g>
<line x1="564" x2="564" y1="216" y2="280"/>
<line x1="564" x2="580" y1="216" y2="216"/>
<line x1="564" x2="732" y1="280" y2="280"/>
<line x1="580" x2="732" y1="216" y2="216"/>
<line x1="732" x2="732" y1="216" y2="280"/>
</g>
<g>
<text x="33" y="252">
Neighborhood
</text>
</g>
<g>
<text x="137" y="252">
3
</text>
</g>
<g>
<text x="153" y="140">
Neighborhood
</text>
</g>
<g>
<text x="217" y="252">
Neighborhood
</text>
</g>
<g>
<text x="257" y="140">
1
</text>
</g>
<g>
<text x="305" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="252">
4
</text>
</g>
<g>
<text x="409" y="44">
0
</text>
</g>
<g>
<text x="409" y="252">
Neighborhood
</text>
</g>
<g>
<text x="473" y="140">
Neighborhood
</text>
</g>
<g>
<text x="513" y="252">
5
</text>
</g>
<g>
<text x="577" y="140">
2
</text>
</g>
<g>
<text x="593" y="252">
Neighborhood
</text>
</g>
<g>
<text x="697" y="252">
6
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,192 @@
<svg class="bob" font-family="arial" font-size="14" height="288" width="736" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="288" width="736" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="216" y2="280"/>
<line x1="4" x2="156" y1="216" y2="216"/>
<line x1="4" x2="172" y1="280" y2="280"/>
<line x1="156" x2="172" y1="216" y2="216"/>
<line x1="172" x2="172" y1="216" y2="280"/>
</g>
<g>
<line x1="124" x2="124" y1="104" y2="168"/>
<line x1="124" x2="204" y1="104" y2="104"/>
<line x1="124" x2="156" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="156" x2="156" y1="168" y2="204"/>
<line x1="156" x2="204" y1="168" y2="168"/>
<line x1="204" x2="292" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="168" y2="204"/>
<line x1="204" x2="292" y1="168" y2="168"/>
<line x1="292" x2="292" y1="104" y2="168"/>
</g>
<g>
<line x1="188" x2="188" y1="216" y2="280"/>
<line x1="188" x2="204" y1="216" y2="216"/>
<line x1="188" x2="356" y1="280" y2="280"/>
<line x1="204" x2="356" y1="216" y2="216"/>
<line x1="356" x2="356" y1="216" y2="280"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="40" y2="92"/>
<line x1="204" x2="276" y1="40" y2="40"/>
<line x1="276" x2="276" y1="8" y2="40"/>
<line x1="276" x2="444" y1="8" y2="8"/>
<line x1="276" x2="276" y1="40" y2="72"/>
<line x1="276" x2="444" y1="72" y2="72"/>
<line x1="444" x2="444" y1="8" y2="40"/>
<line x1="444" x2="444" y1="40" y2="72"/>
<line x1="444" x2="532" y1="40" y2="40"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="40" y2="92"/>
</g>
<g>
<line x1="380" x2="380" y1="216" y2="280"/>
<line x1="380" x2="532" y1="216" y2="216"/>
<line x1="380" x2="548" y1="280" y2="280"/>
<line x1="532" x2="548" y1="216" y2="216"/>
<line x1="548" x2="548" y1="216" y2="280"/>
</g>
<g>
<line x1="444" x2="444" y1="104" y2="168"/>
<line x1="444" x2="532" y1="104" y2="104"/>
<line x1="444" x2="532" y1="168" y2="168"/>
<line x1="532" x2="612" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="168" y2="204"/>
<line x1="532" x2="580" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="580" x2="580" y1="168" y2="204"/>
<line x1="580" x2="612" y1="168" y2="168"/>
<line x1="612" x2="612" y1="104" y2="168"/>
</g>
<g>
<line x1="564" x2="564" y1="216" y2="280"/>
<line x1="564" x2="580" y1="216" y2="216"/>
<line x1="564" x2="732" y1="280" y2="280"/>
<line x1="580" x2="732" y1="216" y2="216"/>
<line x1="732" x2="732" y1="216" y2="280"/>
</g>
<g>
<text x="33" y="252">
Neighborhood
</text>
</g>
<g>
<text x="137" y="252">
3
</text>
</g>
<g>
<text x="153" y="140">
Neighborhood
</text>
</g>
<g>
<text x="217" y="252">
Neighborhood
</text>
</g>
<g>
<text x="257" y="140">
1
</text>
</g>
<g>
<text x="305" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="252">
4
</text>
</g>
<g>
<text x="409" y="44">
0
</text>
</g>
<g>
<text x="409" y="252">
Neighborhood
</text>
</g>
<g>
<text x="473" y="140">
Neighborhood
</text>
</g>
<g>
<text x="513" y="252">
5
</text>
</g>
<g>
<text x="577" y="140">
2
</text>
</g>
<g>
<text x="593" y="252">
Neighborhood
</text>
</g>
<g>
<text x="697" y="252">
6
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,192 @@
<svg class="bob" font-family="arial" font-size="14" height="288" width="736" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="288" width="736" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="216" y2="280"/>
<line x1="4" x2="156" y1="216" y2="216"/>
<line x1="4" x2="172" y1="280" y2="280"/>
<line x1="156" x2="172" y1="216" y2="216"/>
<line x1="172" x2="172" y1="216" y2="280"/>
</g>
<g>
<line x1="124" x2="124" y1="104" y2="168"/>
<line x1="124" x2="204" y1="104" y2="104"/>
<line x1="124" x2="156" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="156" x2="156" y1="168" y2="204"/>
<line x1="156" x2="204" y1="168" y2="168"/>
<line x1="204" x2="292" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="168" y2="204"/>
<line x1="204" x2="292" y1="168" y2="168"/>
<line x1="292" x2="292" y1="104" y2="168"/>
</g>
<g>
<line x1="188" x2="188" y1="216" y2="280"/>
<line x1="188" x2="204" y1="216" y2="216"/>
<line x1="188" x2="356" y1="280" y2="280"/>
<line x1="204" x2="356" y1="216" y2="216"/>
<line x1="356" x2="356" y1="216" y2="280"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="40" y2="92"/>
<line x1="204" x2="276" y1="40" y2="40"/>
<line x1="276" x2="276" y1="8" y2="40"/>
<line x1="276" x2="444" y1="8" y2="8"/>
<line x1="276" x2="276" y1="40" y2="72"/>
<line x1="276" x2="444" y1="72" y2="72"/>
<line x1="444" x2="444" y1="8" y2="40"/>
<line x1="444" x2="444" y1="40" y2="72"/>
<line x1="444" x2="532" y1="40" y2="40"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="40" y2="92"/>
</g>
<g>
<line x1="380" x2="380" y1="216" y2="280"/>
<line x1="380" x2="532" y1="216" y2="216"/>
<line x1="380" x2="548" y1="280" y2="280"/>
<line x1="532" x2="548" y1="216" y2="216"/>
<line x1="548" x2="548" y1="216" y2="280"/>
</g>
<g>
<line x1="444" x2="444" y1="104" y2="168"/>
<line x1="444" x2="532" y1="104" y2="104"/>
<line x1="444" x2="532" y1="168" y2="168"/>
<line x1="532" x2="612" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="168" y2="204"/>
<line x1="532" x2="580" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="580" x2="580" y1="168" y2="204"/>
<line x1="580" x2="612" y1="168" y2="168"/>
<line x1="612" x2="612" y1="104" y2="168"/>
</g>
<g>
<line x1="564" x2="564" y1="216" y2="280"/>
<line x1="564" x2="580" y1="216" y2="216"/>
<line x1="564" x2="732" y1="280" y2="280"/>
<line x1="580" x2="732" y1="216" y2="216"/>
<line x1="732" x2="732" y1="216" y2="280"/>
</g>
<g>
<text x="33" y="252">
Neighborhood
</text>
</g>
<g>
<text x="137" y="252">
3
</text>
</g>
<g>
<text x="153" y="140">
Neighborhood
</text>
</g>
<g>
<text x="217" y="252">
Neighborhood
</text>
</g>
<g>
<text x="257" y="140">
1
</text>
</g>
<g>
<text x="305" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="252">
4
</text>
</g>
<g>
<text x="409" y="44">
0
</text>
</g>
<g>
<text x="409" y="252">
Neighborhood
</text>
</g>
<g>
<text x="473" y="140">
Neighborhood
</text>
</g>
<g>
<text x="513" y="252">
5
</text>
</g>
<g>
<text x="577" y="140">
2
</text>
</g>
<g>
<text x="593" y="252">
Neighborhood
</text>
</g>
<g>
<text x="697" y="252">
6
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,192 @@
<svg class="bob" font-family="arial" font-size="14" height="288" width="736" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="288" width="736" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="216" y2="280"/>
<line x1="4" x2="156" y1="216" y2="216"/>
<line x1="4" x2="172" y1="280" y2="280"/>
<line x1="156" x2="172" y1="216" y2="216"/>
<line x1="172" x2="172" y1="216" y2="280"/>
</g>
<g>
<line x1="124" x2="124" y1="104" y2="168"/>
<line x1="124" x2="204" y1="104" y2="104"/>
<line x1="124" x2="156" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="156" x2="156" y1="168" y2="204"/>
<line x1="156" x2="204" y1="168" y2="168"/>
<line x1="204" x2="292" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="168" y2="204"/>
<line x1="204" x2="292" y1="168" y2="168"/>
<line x1="292" x2="292" y1="104" y2="168"/>
</g>
<g>
<line x1="188" x2="188" y1="216" y2="280"/>
<line x1="188" x2="204" y1="216" y2="216"/>
<line x1="188" x2="356" y1="280" y2="280"/>
<line x1="204" x2="356" y1="216" y2="216"/>
<line x1="356" x2="356" y1="216" y2="280"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="40" y2="92"/>
<line x1="204" x2="276" y1="40" y2="40"/>
<line x1="276" x2="276" y1="8" y2="40"/>
<line x1="276" x2="444" y1="8" y2="8"/>
<line x1="276" x2="276" y1="40" y2="72"/>
<line x1="276" x2="444" y1="72" y2="72"/>
<line x1="444" x2="444" y1="8" y2="40"/>
<line x1="444" x2="444" y1="40" y2="72"/>
<line x1="444" x2="532" y1="40" y2="40"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="40" y2="92"/>
</g>
<g>
<line x1="380" x2="380" y1="216" y2="280"/>
<line x1="380" x2="532" y1="216" y2="216"/>
<line x1="380" x2="548" y1="280" y2="280"/>
<line x1="532" x2="548" y1="216" y2="216"/>
<line x1="548" x2="548" y1="216" y2="280"/>
</g>
<g>
<line x1="444" x2="444" y1="104" y2="168"/>
<line x1="444" x2="532" y1="104" y2="104"/>
<line x1="444" x2="532" y1="168" y2="168"/>
<line x1="532" x2="612" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="168" y2="204"/>
<line x1="532" x2="580" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="580" x2="580" y1="168" y2="204"/>
<line x1="580" x2="612" y1="168" y2="168"/>
<line x1="612" x2="612" y1="104" y2="168"/>
</g>
<g>
<line x1="564" x2="564" y1="216" y2="280"/>
<line x1="564" x2="580" y1="216" y2="216"/>
<line x1="564" x2="732" y1="280" y2="280"/>
<line x1="580" x2="732" y1="216" y2="216"/>
<line x1="732" x2="732" y1="216" y2="280"/>
</g>
<g>
<text x="33" y="252">
Neighborhood
</text>
</g>
<g>
<text x="137" y="252">
3
</text>
</g>
<g>
<text x="153" y="140">
Neighborhood
</text>
</g>
<g>
<text x="217" y="252">
Neighborhood
</text>
</g>
<g>
<text x="257" y="140">
1
</text>
</g>
<g>
<text x="305" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="252">
4
</text>
</g>
<g>
<text x="409" y="44">
0
</text>
</g>
<g>
<text x="409" y="252">
Neighborhood
</text>
</g>
<g>
<text x="473" y="140">
Neighborhood
</text>
</g>
<g>
<text x="513" y="252">
5
</text>
</g>
<g>
<text x="577" y="140">
2
</text>
</g>
<g>
<text x="593" y="252">
Neighborhood
</text>
</g>
<g>
<text x="697" y="252">
6
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,210 @@
<svg class="bob" font-family="arial" font-size="14" height="288" width="736" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon class="fg_fill" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon class="bg_fill" points="2,2 2,12 18,7 2,2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle class="fg_fill" cx="10" cy="10" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect class="fg_fill" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle class="bg_fill" cx="10" cy="10" r="4"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle class="bg_fill" cx="20" cy="20" r="6"/>
</marker>
</defs>
<style type="text/css">
rect.backdrop {
fill: white;
}
text{
fill: black;
}
circle {
fill: none;
stroke: black;
stroke-width: 2;
}
line {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
path {
fill: none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
.fg_fill {
fill: black;
}
.bg_fill {
fill: white;
stroke: black;
stroke-width: 2;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect class="backdrop" height="288" width="736" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="216" y2="280"/>
<line x1="4" x2="156" y1="216" y2="216"/>
<line x1="4" x2="172" y1="280" y2="280"/>
<line x1="156" x2="172" y1="216" y2="216"/>
<line x1="172" x2="172" y1="216" y2="280"/>
</g>
<g>
<line x1="124" x2="124" y1="104" y2="168"/>
<line x1="124" x2="204" y1="104" y2="104"/>
<line x1="124" x2="156" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="156" x2="156" y1="168" y2="204"/>
<line x1="156" x2="204" y1="168" y2="168"/>
<line x1="204" x2="292" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="168" y2="204"/>
<line x1="204" x2="292" y1="168" y2="168"/>
<line x1="292" x2="292" y1="104" y2="168"/>
</g>
<g>
<line x1="188" x2="188" y1="216" y2="280"/>
<line x1="188" x2="204" y1="216" y2="216"/>
<line x1="188" x2="356" y1="280" y2="280"/>
<line x1="204" x2="356" y1="216" y2="216"/>
<line x1="356" x2="356" y1="216" y2="280"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="40" y2="92"/>
<line x1="204" x2="276" y1="40" y2="40"/>
<line x1="276" x2="276" y1="8" y2="40"/>
<line x1="276" x2="444" y1="8" y2="8"/>
<line x1="276" x2="276" y1="40" y2="72"/>
<line x1="276" x2="444" y1="72" y2="72"/>
<line x1="444" x2="444" y1="8" y2="40"/>
<line x1="444" x2="444" y1="40" y2="72"/>
<line x1="444" x2="532" y1="40" y2="40"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="40" y2="92"/>
</g>
<g>
<line x1="380" x2="380" y1="216" y2="280"/>
<line x1="380" x2="532" y1="216" y2="216"/>
<line x1="380" x2="548" y1="280" y2="280"/>
<line x1="532" x2="548" y1="216" y2="216"/>
<line x1="548" x2="548" y1="216" y2="280"/>
</g>
<g>
<line x1="444" x2="444" y1="104" y2="168"/>
<line x1="444" x2="532" y1="104" y2="104"/>
<line x1="444" x2="532" y1="168" y2="168"/>
<line x1="532" x2="612" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="168" y2="204"/>
<line x1="532" x2="580" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="580" x2="580" y1="168" y2="204"/>
<line x1="580" x2="612" y1="168" y2="168"/>
<line x1="612" x2="612" y1="104" y2="168"/>
</g>
<g>
<line x1="564" x2="564" y1="216" y2="280"/>
<line x1="564" x2="580" y1="216" y2="216"/>
<line x1="564" x2="732" y1="280" y2="280"/>
<line x1="580" x2="732" y1="216" y2="216"/>
<line x1="732" x2="732" y1="216" y2="280"/>
</g>
<g>
<text x="33" y="252">
Neighborhood
</text>
</g>
<g>
<text x="137" y="252">
3
</text>
</g>
<g>
<text x="153" y="140">
Neighborhood
</text>
</g>
<g>
<text x="217" y="252">
Neighborhood
</text>
</g>
<g>
<text x="257" y="140">
1
</text>
</g>
<g>
<text x="305" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="252">
4
</text>
</g>
<g>
<text x="409" y="44">
0
</text>
</g>
<g>
<text x="409" y="252">
Neighborhood
</text>
</g>
<g>
<text x="473" y="140">
Neighborhood
</text>
</g>
<g>
<text x="513" y="252">
5
</text>
</g>
<g>
<text x="577" y="140">
2
</text>
</g>
<g>
<text x="593" y="252">
Neighborhood
</text>
</g>
<g>
<text x="697" y="252">
6
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,192 @@
<svg class="bob" font-family="arial" font-size="14" height="288" width="736" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="288" width="736" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="216" y2="280"/>
<line x1="4" x2="156" y1="216" y2="216"/>
<line x1="4" x2="172" y1="280" y2="280"/>
<line x1="156" x2="172" y1="216" y2="216"/>
<line x1="172" x2="172" y1="216" y2="280"/>
</g>
<g>
<line x1="124" x2="124" y1="104" y2="168"/>
<line x1="124" x2="204" y1="104" y2="104"/>
<line x1="124" x2="156" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="156" x2="156" y1="168" y2="204"/>
<line x1="156" x2="204" y1="168" y2="168"/>
<line x1="204" x2="292" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="168" y2="204"/>
<line x1="204" x2="292" y1="168" y2="168"/>
<line x1="292" x2="292" y1="104" y2="168"/>
</g>
<g>
<line x1="188" x2="188" y1="216" y2="280"/>
<line x1="188" x2="204" y1="216" y2="216"/>
<line x1="188" x2="356" y1="280" y2="280"/>
<line x1="204" x2="356" y1="216" y2="216"/>
<line x1="356" x2="356" y1="216" y2="280"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="204" x2="204" y1="40" y2="92"/>
<line x1="204" x2="276" y1="40" y2="40"/>
<line x1="276" x2="276" y1="8" y2="40"/>
<line x1="276" x2="444" y1="8" y2="8"/>
<line x1="276" x2="276" y1="40" y2="72"/>
<line x1="276" x2="444" y1="72" y2="72"/>
<line x1="444" x2="444" y1="8" y2="40"/>
<line x1="444" x2="444" y1="40" y2="72"/>
<line x1="444" x2="532" y1="40" y2="40"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="40" y2="92"/>
</g>
<g>
<line x1="380" x2="380" y1="216" y2="280"/>
<line x1="380" x2="532" y1="216" y2="216"/>
<line x1="380" x2="548" y1="280" y2="280"/>
<line x1="532" x2="548" y1="216" y2="216"/>
<line x1="548" x2="548" y1="216" y2="280"/>
</g>
<g>
<line x1="444" x2="444" y1="104" y2="168"/>
<line x1="444" x2="532" y1="104" y2="104"/>
<line x1="444" x2="532" y1="168" y2="168"/>
<line x1="532" x2="612" y1="104" y2="104"/>
<line marker-end="url(#triangle)" x1="532" x2="532" y1="168" y2="204"/>
<line x1="532" x2="580" y1="168" y2="168"/>
<line marker-end="url(#triangle)" x1="580" x2="580" y1="168" y2="204"/>
<line x1="580" x2="612" y1="168" y2="168"/>
<line x1="612" x2="612" y1="104" y2="168"/>
</g>
<g>
<line x1="564" x2="564" y1="216" y2="280"/>
<line x1="564" x2="580" y1="216" y2="216"/>
<line x1="564" x2="732" y1="280" y2="280"/>
<line x1="580" x2="732" y1="216" y2="216"/>
<line x1="732" x2="732" y1="216" y2="280"/>
</g>
<g>
<text x="33" y="252">
Neighborhood
</text>
</g>
<g>
<text x="137" y="252">
3
</text>
</g>
<g>
<text x="153" y="140">
Neighborhood
</text>
</g>
<g>
<text x="217" y="252">
Neighborhood
</text>
</g>
<g>
<text x="257" y="140">
1
</text>
</g>
<g>
<text x="305" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="252">
4
</text>
</g>
<g>
<text x="409" y="44">
0
</text>
</g>
<g>
<text x="409" y="252">
Neighborhood
</text>
</g>
<g>
<text x="473" y="140">
Neighborhood
</text>
</g>
<g>
<text x="513" y="252">
5
</text>
</g>
<g>
<text x="577" y="140">
2
</text>
</g>
<g>
<text x="593" y="252">
Neighborhood
</text>
</g>
<g>
<text x="697" y="252">
6
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,183 @@
<svg class="bob" font-family="arial" font-size="14" height="304" width="544" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="304" width="544" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="184"/>
<line x1="4" x2="540" y1="8" y2="8"/>
<line x1="4" x2="540" y1="184" y2="184"/>
<line x1="540" x2="540" y1="8" y2="184"/>
</g>
<g>
<line x1="28" x2="28" y1="232" y2="296"/>
<line x1="28" x2="108" y1="232" y2="232"/>
<line x1="28" x2="196" y1="296" y2="296"/>
<line x1="108" x2="164" y1="232" y2="232"/>
<line x1="164" x2="196" y1="232" y2="232"/>
<line x1="196" x2="196" y1="232" y2="296"/>
</g>
<g>
<line x1="36" x2="36" y1="40" y2="104"/>
<line x1="36" x2="180" y1="40" y2="40"/>
<line x1="36" x2="108" y1="104" y2="104"/>
<line x1="108" x2="108" y1="104" y2="176"/>
<line x1="108" x2="124" y1="104" y2="104"/>
<line x1="124" x2="124" y1="104" y2="136"/>
<line x1="124" x2="180" y1="104" y2="104"/>
<line x1="124" x2="364" y1="136" y2="136"/>
<line x1="180" x2="180" y1="40" y2="56"/>
<line x1="180" x2="180" y1="56" y2="88"/>
<line marker-end="url(#triangle)" x1="180" x2="356" y1="56" y2="56"/>
<line x1="180" x2="180" y1="88" y2="104"/>
<line x1="180" x2="184" y1="88" y2="88"/>
<line x1="364" x2="364" y1="136" y2="152"/>
<line x1="364" x2="364" y1="152" y2="176"/>
<line x1="364" x2="420" y1="152" y2="152"/>
<line x1="420" x2="420" y1="104" y2="152"/>
<line x1="420" x2="436" y1="104" y2="104"/>
<line x1="436" x2="436" y1="104" y2="176"/>
<line x1="436" x2="508" y1="104" y2="104"/>
<line x1="508" x2="508" y1="40" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="108" x2="108" y1="192" y2="220"/>
</g>
<g>
<line x1="164" x2="164" y1="152" y2="176"/>
<line x1="164" x2="364" y1="152" y2="152"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="164" x2="164" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="192" x2="188" y1="88" y2="88"/>
<line x1="192" x2="364" y1="88" y2="88"/>
<line x1="364" x2="364" y1="56" y2="88"/>
<line x1="364" x2="364" y1="88" y2="104"/>
<line x1="364" x2="420" y1="104" y2="104"/>
</g>
<g>
<line x1="348" x2="348" y1="232" y2="296"/>
<line x1="348" x2="364" y1="232" y2="232"/>
<line x1="348" x2="516" y1="296" y2="296"/>
<line x1="364" x2="436" y1="232" y2="232"/>
<line x1="436" x2="516" y1="232" y2="232"/>
<line x1="516" x2="516" y1="232" y2="296"/>
</g>
<g>
<line x1="364" x2="364" y1="40" y2="56"/>
<line x1="364" x2="508" y1="40" y2="40"/>
<line x1="364" x2="360" y1="56" y2="56"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="364" x2="364" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="436" x2="436" y1="192" y2="220"/>
</g>
<g>
<text x="57" y="268">
Neighborhood
</text>
</g>
<g>
<text x="65" y="76">
Validator
</text>
</g>
<g>
<text x="145" y="76">
1
</text>
</g>
<g>
<text x="161" y="268">
1
</text>
</g>
<g>
<text x="217" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="44">
0
</text>
</g>
<g>
<text x="377" y="268">
Neighborhood
</text>
</g>
<g>
<text x="393" y="76">
Validator
</text>
</g>
<g>
<text x="473" y="76">
2
</text>
</g>
<g>
<text x="481" y="268">
2
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,201 @@
<svg class="bob" font-family="arial" font-size="14" height="304" width="544" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon class="fg_fill" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon class="bg_fill" points="2,2 2,12 18,7 2,2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle class="fg_fill" cx="10" cy="10" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect class="fg_fill" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle class="bg_fill" cx="10" cy="10" r="4"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle class="bg_fill" cx="20" cy="20" r="6"/>
</marker>
</defs>
<style type="text/css">
rect.backdrop {
fill: white;
}
text{
fill: black;
}
circle {
fill: none;
stroke: black;
stroke-width: 2;
}
line {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
path {
fill: none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
.fg_fill {
fill: black;
}
.bg_fill {
fill: white;
stroke: black;
stroke-width: 2;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect class="backdrop" height="304" width="544" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="184"/>
<line x1="4" x2="540" y1="8" y2="8"/>
<line x1="4" x2="540" y1="184" y2="184"/>
<line x1="540" x2="540" y1="8" y2="184"/>
</g>
<g>
<line x1="28" x2="28" y1="232" y2="296"/>
<line x1="28" x2="108" y1="232" y2="232"/>
<line x1="28" x2="196" y1="296" y2="296"/>
<line x1="108" x2="164" y1="232" y2="232"/>
<line x1="164" x2="196" y1="232" y2="232"/>
<line x1="196" x2="196" y1="232" y2="296"/>
</g>
<g>
<line x1="36" x2="36" y1="40" y2="104"/>
<line x1="36" x2="180" y1="40" y2="40"/>
<line x1="36" x2="108" y1="104" y2="104"/>
<line x1="108" x2="108" y1="104" y2="176"/>
<line x1="108" x2="124" y1="104" y2="104"/>
<line x1="124" x2="124" y1="104" y2="136"/>
<line x1="124" x2="180" y1="104" y2="104"/>
<line x1="124" x2="364" y1="136" y2="136"/>
<line x1="180" x2="180" y1="40" y2="56"/>
<line x1="180" x2="180" y1="56" y2="88"/>
<line marker-end="url(#triangle)" x1="180" x2="356" y1="56" y2="56"/>
<line x1="180" x2="180" y1="88" y2="104"/>
<line x1="180" x2="184" y1="88" y2="88"/>
<line x1="364" x2="364" y1="136" y2="152"/>
<line x1="364" x2="364" y1="152" y2="176"/>
<line x1="364" x2="420" y1="152" y2="152"/>
<line x1="420" x2="420" y1="104" y2="152"/>
<line x1="420" x2="436" y1="104" y2="104"/>
<line x1="436" x2="436" y1="104" y2="176"/>
<line x1="436" x2="508" y1="104" y2="104"/>
<line x1="508" x2="508" y1="40" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="108" x2="108" y1="192" y2="220"/>
</g>
<g>
<line x1="164" x2="164" y1="152" y2="176"/>
<line x1="164" x2="364" y1="152" y2="152"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="164" x2="164" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="192" x2="188" y1="88" y2="88"/>
<line x1="192" x2="364" y1="88" y2="88"/>
<line x1="364" x2="364" y1="56" y2="88"/>
<line x1="364" x2="364" y1="88" y2="104"/>
<line x1="364" x2="420" y1="104" y2="104"/>
</g>
<g>
<line x1="348" x2="348" y1="232" y2="296"/>
<line x1="348" x2="364" y1="232" y2="232"/>
<line x1="348" x2="516" y1="296" y2="296"/>
<line x1="364" x2="436" y1="232" y2="232"/>
<line x1="436" x2="516" y1="232" y2="232"/>
<line x1="516" x2="516" y1="232" y2="296"/>
</g>
<g>
<line x1="364" x2="364" y1="40" y2="56"/>
<line x1="364" x2="508" y1="40" y2="40"/>
<line x1="364" x2="360" y1="56" y2="56"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="364" x2="364" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="436" x2="436" y1="192" y2="220"/>
</g>
<g>
<text x="57" y="268">
Neighborhood
</text>
</g>
<g>
<text x="65" y="76">
Validator
</text>
</g>
<g>
<text x="145" y="76">
1
</text>
</g>
<g>
<text x="161" y="268">
1
</text>
</g>
<g>
<text x="217" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="44">
0
</text>
</g>
<g>
<text x="377" y="268">
Neighborhood
</text>
</g>
<g>
<text x="393" y="76">
Validator
</text>
</g>
<g>
<text x="473" y="76">
2
</text>
</g>
<g>
<text x="481" y="268">
2
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,183 @@
<svg class="bob" font-family="arial" font-size="14" height="304" width="544" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="304" width="544" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="184"/>
<line x1="4" x2="540" y1="8" y2="8"/>
<line x1="4" x2="540" y1="184" y2="184"/>
<line x1="540" x2="540" y1="8" y2="184"/>
</g>
<g>
<line x1="28" x2="28" y1="232" y2="296"/>
<line x1="28" x2="108" y1="232" y2="232"/>
<line x1="28" x2="196" y1="296" y2="296"/>
<line x1="108" x2="164" y1="232" y2="232"/>
<line x1="164" x2="196" y1="232" y2="232"/>
<line x1="196" x2="196" y1="232" y2="296"/>
</g>
<g>
<line x1="36" x2="36" y1="40" y2="104"/>
<line x1="36" x2="180" y1="40" y2="40"/>
<line x1="36" x2="108" y1="104" y2="104"/>
<line x1="108" x2="108" y1="104" y2="176"/>
<line x1="108" x2="124" y1="104" y2="104"/>
<line x1="124" x2="124" y1="104" y2="136"/>
<line x1="124" x2="180" y1="104" y2="104"/>
<line x1="124" x2="364" y1="136" y2="136"/>
<line x1="180" x2="180" y1="40" y2="56"/>
<line x1="180" x2="180" y1="56" y2="88"/>
<line marker-end="url(#triangle)" x1="180" x2="356" y1="56" y2="56"/>
<line x1="180" x2="180" y1="88" y2="104"/>
<line x1="180" x2="184" y1="88" y2="88"/>
<line x1="364" x2="364" y1="136" y2="152"/>
<line x1="364" x2="364" y1="152" y2="176"/>
<line x1="364" x2="420" y1="152" y2="152"/>
<line x1="420" x2="420" y1="104" y2="152"/>
<line x1="420" x2="436" y1="104" y2="104"/>
<line x1="436" x2="436" y1="104" y2="176"/>
<line x1="436" x2="508" y1="104" y2="104"/>
<line x1="508" x2="508" y1="40" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="108" x2="108" y1="192" y2="220"/>
</g>
<g>
<line x1="164" x2="164" y1="152" y2="176"/>
<line x1="164" x2="364" y1="152" y2="152"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="164" x2="164" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="192" x2="188" y1="88" y2="88"/>
<line x1="192" x2="364" y1="88" y2="88"/>
<line x1="364" x2="364" y1="56" y2="88"/>
<line x1="364" x2="364" y1="88" y2="104"/>
<line x1="364" x2="420" y1="104" y2="104"/>
</g>
<g>
<line x1="348" x2="348" y1="232" y2="296"/>
<line x1="348" x2="364" y1="232" y2="232"/>
<line x1="348" x2="516" y1="296" y2="296"/>
<line x1="364" x2="436" y1="232" y2="232"/>
<line x1="436" x2="516" y1="232" y2="232"/>
<line x1="516" x2="516" y1="232" y2="296"/>
</g>
<g>
<line x1="364" x2="364" y1="40" y2="56"/>
<line x1="364" x2="508" y1="40" y2="40"/>
<line x1="364" x2="360" y1="56" y2="56"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="364" x2="364" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="436" x2="436" y1="192" y2="220"/>
</g>
<g>
<text x="57" y="268">
Neighborhood
</text>
</g>
<g>
<text x="65" y="76">
Validator
</text>
</g>
<g>
<text x="145" y="76">
1
</text>
</g>
<g>
<text x="161" y="268">
1
</text>
</g>
<g>
<text x="217" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="44">
0
</text>
</g>
<g>
<text x="377" y="268">
Neighborhood
</text>
</g>
<g>
<text x="393" y="76">
Validator
</text>
</g>
<g>
<text x="473" y="76">
2
</text>
</g>
<g>
<text x="481" y="268">
2
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,183 @@
<svg class="bob" font-family="arial" font-size="14" height="304" width="544" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="304" width="544" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="184"/>
<line x1="4" x2="540" y1="8" y2="8"/>
<line x1="4" x2="540" y1="184" y2="184"/>
<line x1="540" x2="540" y1="8" y2="184"/>
</g>
<g>
<line x1="28" x2="28" y1="232" y2="296"/>
<line x1="28" x2="108" y1="232" y2="232"/>
<line x1="28" x2="196" y1="296" y2="296"/>
<line x1="108" x2="164" y1="232" y2="232"/>
<line x1="164" x2="196" y1="232" y2="232"/>
<line x1="196" x2="196" y1="232" y2="296"/>
</g>
<g>
<line x1="36" x2="36" y1="40" y2="104"/>
<line x1="36" x2="180" y1="40" y2="40"/>
<line x1="36" x2="108" y1="104" y2="104"/>
<line x1="108" x2="108" y1="104" y2="176"/>
<line x1="108" x2="124" y1="104" y2="104"/>
<line x1="124" x2="124" y1="104" y2="136"/>
<line x1="124" x2="180" y1="104" y2="104"/>
<line x1="124" x2="364" y1="136" y2="136"/>
<line x1="180" x2="180" y1="40" y2="56"/>
<line x1="180" x2="180" y1="56" y2="88"/>
<line marker-end="url(#triangle)" x1="180" x2="356" y1="56" y2="56"/>
<line x1="180" x2="180" y1="88" y2="104"/>
<line x1="180" x2="184" y1="88" y2="88"/>
<line x1="364" x2="364" y1="136" y2="152"/>
<line x1="364" x2="364" y1="152" y2="176"/>
<line x1="364" x2="420" y1="152" y2="152"/>
<line x1="420" x2="420" y1="104" y2="152"/>
<line x1="420" x2="436" y1="104" y2="104"/>
<line x1="436" x2="436" y1="104" y2="176"/>
<line x1="436" x2="508" y1="104" y2="104"/>
<line x1="508" x2="508" y1="40" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="108" x2="108" y1="192" y2="220"/>
</g>
<g>
<line x1="164" x2="164" y1="152" y2="176"/>
<line x1="164" x2="364" y1="152" y2="152"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="164" x2="164" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="192" x2="188" y1="88" y2="88"/>
<line x1="192" x2="364" y1="88" y2="88"/>
<line x1="364" x2="364" y1="56" y2="88"/>
<line x1="364" x2="364" y1="88" y2="104"/>
<line x1="364" x2="420" y1="104" y2="104"/>
</g>
<g>
<line x1="348" x2="348" y1="232" y2="296"/>
<line x1="348" x2="364" y1="232" y2="232"/>
<line x1="348" x2="516" y1="296" y2="296"/>
<line x1="364" x2="436" y1="232" y2="232"/>
<line x1="436" x2="516" y1="232" y2="232"/>
<line x1="516" x2="516" y1="232" y2="296"/>
</g>
<g>
<line x1="364" x2="364" y1="40" y2="56"/>
<line x1="364" x2="508" y1="40" y2="40"/>
<line x1="364" x2="360" y1="56" y2="56"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="364" x2="364" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="436" x2="436" y1="192" y2="220"/>
</g>
<g>
<text x="57" y="268">
Neighborhood
</text>
</g>
<g>
<text x="65" y="76">
Validator
</text>
</g>
<g>
<text x="145" y="76">
1
</text>
</g>
<g>
<text x="161" y="268">
1
</text>
</g>
<g>
<text x="217" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="44">
0
</text>
</g>
<g>
<text x="377" y="268">
Neighborhood
</text>
</g>
<g>
<text x="393" y="76">
Validator
</text>
</g>
<g>
<text x="473" y="76">
2
</text>
</g>
<g>
<text x="481" y="268">
2
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,183 @@
<svg class="bob" font-family="arial" font-size="14" height="304" width="544" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="304" width="544" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="184"/>
<line x1="4" x2="540" y1="8" y2="8"/>
<line x1="4" x2="540" y1="184" y2="184"/>
<line x1="540" x2="540" y1="8" y2="184"/>
</g>
<g>
<line x1="28" x2="28" y1="232" y2="296"/>
<line x1="28" x2="108" y1="232" y2="232"/>
<line x1="28" x2="196" y1="296" y2="296"/>
<line x1="108" x2="164" y1="232" y2="232"/>
<line x1="164" x2="196" y1="232" y2="232"/>
<line x1="196" x2="196" y1="232" y2="296"/>
</g>
<g>
<line x1="36" x2="36" y1="40" y2="104"/>
<line x1="36" x2="180" y1="40" y2="40"/>
<line x1="36" x2="108" y1="104" y2="104"/>
<line x1="108" x2="108" y1="104" y2="176"/>
<line x1="108" x2="124" y1="104" y2="104"/>
<line x1="124" x2="124" y1="104" y2="136"/>
<line x1="124" x2="180" y1="104" y2="104"/>
<line x1="124" x2="364" y1="136" y2="136"/>
<line x1="180" x2="180" y1="40" y2="56"/>
<line x1="180" x2="180" y1="56" y2="88"/>
<line marker-end="url(#triangle)" x1="180" x2="356" y1="56" y2="56"/>
<line x1="180" x2="180" y1="88" y2="104"/>
<line x1="180" x2="184" y1="88" y2="88"/>
<line x1="364" x2="364" y1="136" y2="152"/>
<line x1="364" x2="364" y1="152" y2="176"/>
<line x1="364" x2="420" y1="152" y2="152"/>
<line x1="420" x2="420" y1="104" y2="152"/>
<line x1="420" x2="436" y1="104" y2="104"/>
<line x1="436" x2="436" y1="104" y2="176"/>
<line x1="436" x2="508" y1="104" y2="104"/>
<line x1="508" x2="508" y1="40" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="108" x2="108" y1="192" y2="220"/>
</g>
<g>
<line x1="164" x2="164" y1="152" y2="176"/>
<line x1="164" x2="364" y1="152" y2="152"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="164" x2="164" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="192" x2="188" y1="88" y2="88"/>
<line x1="192" x2="364" y1="88" y2="88"/>
<line x1="364" x2="364" y1="56" y2="88"/>
<line x1="364" x2="364" y1="88" y2="104"/>
<line x1="364" x2="420" y1="104" y2="104"/>
</g>
<g>
<line x1="348" x2="348" y1="232" y2="296"/>
<line x1="348" x2="364" y1="232" y2="232"/>
<line x1="348" x2="516" y1="296" y2="296"/>
<line x1="364" x2="436" y1="232" y2="232"/>
<line x1="436" x2="516" y1="232" y2="232"/>
<line x1="516" x2="516" y1="232" y2="296"/>
</g>
<g>
<line x1="364" x2="364" y1="40" y2="56"/>
<line x1="364" x2="508" y1="40" y2="40"/>
<line x1="364" x2="360" y1="56" y2="56"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="364" x2="364" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="436" x2="436" y1="192" y2="220"/>
</g>
<g>
<text x="57" y="268">
Neighborhood
</text>
</g>
<g>
<text x="65" y="76">
Validator
</text>
</g>
<g>
<text x="145" y="76">
1
</text>
</g>
<g>
<text x="161" y="268">
1
</text>
</g>
<g>
<text x="217" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="44">
0
</text>
</g>
<g>
<text x="377" y="268">
Neighborhood
</text>
</g>
<g>
<text x="393" y="76">
Validator
</text>
</g>
<g>
<text x="473" y="76">
2
</text>
</g>
<g>
<text x="481" y="268">
2
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,183 @@
<svg class="bob" font-family="arial" font-size="14" height="304" width="544" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="304" width="544" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="184"/>
<line x1="4" x2="540" y1="8" y2="8"/>
<line x1="4" x2="540" y1="184" y2="184"/>
<line x1="540" x2="540" y1="8" y2="184"/>
</g>
<g>
<line x1="28" x2="28" y1="232" y2="296"/>
<line x1="28" x2="108" y1="232" y2="232"/>
<line x1="28" x2="196" y1="296" y2="296"/>
<line x1="108" x2="164" y1="232" y2="232"/>
<line x1="164" x2="196" y1="232" y2="232"/>
<line x1="196" x2="196" y1="232" y2="296"/>
</g>
<g>
<line x1="36" x2="36" y1="40" y2="104"/>
<line x1="36" x2="180" y1="40" y2="40"/>
<line x1="36" x2="108" y1="104" y2="104"/>
<line x1="108" x2="108" y1="104" y2="176"/>
<line x1="108" x2="124" y1="104" y2="104"/>
<line x1="124" x2="124" y1="104" y2="136"/>
<line x1="124" x2="180" y1="104" y2="104"/>
<line x1="124" x2="364" y1="136" y2="136"/>
<line x1="180" x2="180" y1="40" y2="56"/>
<line x1="180" x2="180" y1="56" y2="88"/>
<line marker-end="url(#triangle)" x1="180" x2="356" y1="56" y2="56"/>
<line x1="180" x2="180" y1="88" y2="104"/>
<line x1="180" x2="184" y1="88" y2="88"/>
<line x1="364" x2="364" y1="136" y2="152"/>
<line x1="364" x2="364" y1="152" y2="176"/>
<line x1="364" x2="420" y1="152" y2="152"/>
<line x1="420" x2="420" y1="104" y2="152"/>
<line x1="420" x2="436" y1="104" y2="104"/>
<line x1="436" x2="436" y1="104" y2="176"/>
<line x1="436" x2="508" y1="104" y2="104"/>
<line x1="508" x2="508" y1="40" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="108" x2="108" y1="192" y2="220"/>
</g>
<g>
<line x1="164" x2="164" y1="152" y2="176"/>
<line x1="164" x2="364" y1="152" y2="152"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="164" x2="164" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="192" x2="188" y1="88" y2="88"/>
<line x1="192" x2="364" y1="88" y2="88"/>
<line x1="364" x2="364" y1="56" y2="88"/>
<line x1="364" x2="364" y1="88" y2="104"/>
<line x1="364" x2="420" y1="104" y2="104"/>
</g>
<g>
<line x1="348" x2="348" y1="232" y2="296"/>
<line x1="348" x2="364" y1="232" y2="232"/>
<line x1="348" x2="516" y1="296" y2="296"/>
<line x1="364" x2="436" y1="232" y2="232"/>
<line x1="436" x2="516" y1="232" y2="232"/>
<line x1="516" x2="516" y1="232" y2="296"/>
</g>
<g>
<line x1="364" x2="364" y1="40" y2="56"/>
<line x1="364" x2="508" y1="40" y2="40"/>
<line x1="364" x2="360" y1="56" y2="56"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="364" x2="364" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="436" x2="436" y1="192" y2="220"/>
</g>
<g>
<text x="57" y="268">
Neighborhood
</text>
</g>
<g>
<text x="65" y="76">
Validator
</text>
</g>
<g>
<text x="145" y="76">
1
</text>
</g>
<g>
<text x="161" y="268">
1
</text>
</g>
<g>
<text x="217" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="44">
0
</text>
</g>
<g>
<text x="377" y="268">
Neighborhood
</text>
</g>
<g>
<text x="393" y="76">
Validator
</text>
</g>
<g>
<text x="473" y="76">
2
</text>
</g>
<g>
<text x="481" y="268">
2
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,183 @@
<svg class="bob" font-family="arial" font-size="14" height="304" width="544" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="304" width="544" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="184"/>
<line x1="4" x2="540" y1="8" y2="8"/>
<line x1="4" x2="540" y1="184" y2="184"/>
<line x1="540" x2="540" y1="8" y2="184"/>
</g>
<g>
<line x1="28" x2="28" y1="232" y2="296"/>
<line x1="28" x2="108" y1="232" y2="232"/>
<line x1="28" x2="196" y1="296" y2="296"/>
<line x1="108" x2="164" y1="232" y2="232"/>
<line x1="164" x2="196" y1="232" y2="232"/>
<line x1="196" x2="196" y1="232" y2="296"/>
</g>
<g>
<line x1="36" x2="36" y1="40" y2="104"/>
<line x1="36" x2="180" y1="40" y2="40"/>
<line x1="36" x2="108" y1="104" y2="104"/>
<line x1="108" x2="108" y1="104" y2="176"/>
<line x1="108" x2="124" y1="104" y2="104"/>
<line x1="124" x2="124" y1="104" y2="136"/>
<line x1="124" x2="180" y1="104" y2="104"/>
<line x1="124" x2="364" y1="136" y2="136"/>
<line x1="180" x2="180" y1="40" y2="56"/>
<line x1="180" x2="180" y1="56" y2="88"/>
<line marker-end="url(#triangle)" x1="180" x2="356" y1="56" y2="56"/>
<line x1="180" x2="180" y1="88" y2="104"/>
<line x1="180" x2="184" y1="88" y2="88"/>
<line x1="364" x2="364" y1="136" y2="152"/>
<line x1="364" x2="364" y1="152" y2="176"/>
<line x1="364" x2="420" y1="152" y2="152"/>
<line x1="420" x2="420" y1="104" y2="152"/>
<line x1="420" x2="436" y1="104" y2="104"/>
<line x1="436" x2="436" y1="104" y2="176"/>
<line x1="436" x2="508" y1="104" y2="104"/>
<line x1="508" x2="508" y1="40" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="108" x2="108" y1="192" y2="220"/>
</g>
<g>
<line x1="164" x2="164" y1="152" y2="176"/>
<line x1="164" x2="364" y1="152" y2="152"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="164" x2="164" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="192" x2="188" y1="88" y2="88"/>
<line x1="192" x2="364" y1="88" y2="88"/>
<line x1="364" x2="364" y1="56" y2="88"/>
<line x1="364" x2="364" y1="88" y2="104"/>
<line x1="364" x2="420" y1="104" y2="104"/>
</g>
<g>
<line x1="348" x2="348" y1="232" y2="296"/>
<line x1="348" x2="364" y1="232" y2="232"/>
<line x1="348" x2="516" y1="296" y2="296"/>
<line x1="364" x2="436" y1="232" y2="232"/>
<line x1="436" x2="516" y1="232" y2="232"/>
<line x1="516" x2="516" y1="232" y2="296"/>
</g>
<g>
<line x1="364" x2="364" y1="40" y2="56"/>
<line x1="364" x2="508" y1="40" y2="40"/>
<line x1="364" x2="360" y1="56" y2="56"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="364" x2="364" y1="192" y2="220"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="436" x2="436" y1="192" y2="220"/>
</g>
<g>
<text x="57" y="268">
Neighborhood
</text>
</g>
<g>
<text x="65" y="76">
Validator
</text>
</g>
<g>
<text x="145" y="76">
1
</text>
</g>
<g>
<text x="161" y="268">
1
</text>
</g>
<g>
<text x="217" y="44">
Neighborhood
</text>
</g>
<g>
<text x="321" y="44">
0
</text>
</g>
<g>
<text x="377" y="268">
Neighborhood
</text>
</g>
<g>
<text x="393" y="76">
Validator
</text>
</g>
<g>
<text x="473" y="76">
2
</text>
</g>
<g>
<text x="481" y="268">
2
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,340 @@
<svg class="bob" font-family="arial" font-size="14" height="400" width="856" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon class="fg_fill" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon class="bg_fill" points="2,2 2,12 18,7 2,2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle class="fg_fill" cx="10" cy="10" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect class="fg_fill" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle class="bg_fill" cx="10" cy="10" r="4"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle class="bg_fill" cx="20" cy="20" r="6"/>
</marker>
</defs>
<style type="text/css">
rect.backdrop {
fill: white;
}
text{
fill: black;
}
circle {
fill: none;
stroke: black;
stroke-width: 2;
}
line {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
path {
fill: none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
.fg_fill {
fill: black;
}
.bg_fill {
fill: white;
stroke: black;
stroke-width: 2;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect class="backdrop" height="400" width="856" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="152"/>
<line x1="4" x2="852" y1="8" y2="8"/>
<line x1="4" x2="852" y1="152" y2="152"/>
<line x1="852" x2="852" y1="8" y2="152"/>
</g>
<g>
<line x1="4" x2="4" y1="248" y2="392"/>
<line x1="4" x2="852" y1="248" y2="248"/>
<line x1="4" x2="852" y1="392" y2="392"/>
<line x1="852" x2="852" y1="248" y2="392"/>
</g>
<g>
<line x1="60" x2="60" y1="56" y2="120"/>
<line x1="60" x2="196" y1="56" y2="56"/>
<line x1="60" x2="84" y1="120" y2="120"/>
<line x1="84" x2="84" y1="120" y2="144"/>
<line x1="84" x2="196" y1="120" y2="120"/>
<line x1="196" x2="196" y1="56" y2="72"/>
<line x1="196" x2="196" y1="72" y2="104"/>
<line marker-end="url(#triangle)" x1="196" x2="252" y1="72" y2="72"/>
<line x1="196" x2="196" y1="104" y2="120"/>
<line x1="196" x2="200" y1="104" y2="104"/>
</g>
<g>
<line x1="60" x2="60" y1="296" y2="360"/>
<line x1="60" x2="84" y1="296" y2="296"/>
<line x1="60" x2="196" y1="360" y2="360"/>
<line x1="84" x2="196" y1="296" y2="296"/>
<line x1="196" x2="196" y1="296" y2="312"/>
<line x1="196" x2="196" y1="312" y2="344"/>
<line marker-end="url(#triangle)" x1="196" x2="252" y1="312" y2="312"/>
<line x1="196" x2="196" y1="344" y2="360"/>
<line x1="196" x2="200" y1="344" y2="344"/>
</g>
<g>
<line x1="84" x2="84" y1="160" y2="240"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="84" x2="84" y1="256" y2="284"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="208" x2="204" y1="104" y2="104"/>
<line x1="208" x2="260" y1="104" y2="104"/>
<line x1="260" x2="260" y1="72" y2="104"/>
<line x1="260" x2="260" y1="104" y2="120"/>
<line x1="260" x2="284" y1="120" y2="120"/>
<line x1="284" x2="284" y1="120" y2="144"/>
<line x1="284" x2="396" y1="120" y2="120"/>
<line x1="396" x2="396" y1="104" y2="120"/>
<line x1="396" x2="400" y1="104" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="208" x2="204" y1="344" y2="344"/>
<line x1="208" x2="260" y1="344" y2="344"/>
<line x1="260" x2="260" y1="312" y2="344"/>
<line x1="260" x2="260" y1="344" y2="360"/>
<line x1="260" x2="396" y1="360" y2="360"/>
<line x1="396" x2="396" y1="344" y2="360"/>
<line x1="396" x2="400" y1="344" y2="344"/>
</g>
<g>
<line x1="260" x2="260" y1="56" y2="72"/>
<line x1="260" x2="396" y1="56" y2="56"/>
<line x1="260" x2="256" y1="72" y2="72"/>
<line x1="396" x2="396" y1="56" y2="72"/>
<line x1="396" x2="396" y1="72" y2="104"/>
<line marker-end="url(#triangle)" x1="396" x2="452" y1="72" y2="72"/>
</g>
<g>
<line x1="260" x2="260" y1="296" y2="312"/>
<line x1="260" x2="284" y1="296" y2="296"/>
<line x1="260" x2="256" y1="312" y2="312"/>
<line x1="284" x2="396" y1="296" y2="296"/>
<line x1="396" x2="396" y1="296" y2="312"/>
<line x1="396" x2="396" y1="312" y2="344"/>
<line marker-end="url(#triangle)" x1="396" x2="452" y1="312" y2="312"/>
</g>
<g>
<line x1="284" x2="284" y1="160" y2="240"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="284" x2="284" y1="256" y2="284"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="408" x2="404" y1="104" y2="104"/>
<line x1="408" x2="460" y1="104" y2="104"/>
<line x1="460" x2="460" y1="72" y2="104"/>
<line x1="460" x2="460" y1="104" y2="120"/>
<line x1="460" x2="508" y1="120" y2="120"/>
<line x1="508" x2="508" y1="120" y2="144"/>
<line x1="508" x2="596" y1="120" y2="120"/>
<line x1="596" x2="596" y1="104" y2="120"/>
<line x1="596" x2="600" y1="104" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="408" x2="404" y1="344" y2="344"/>
<line x1="408" x2="460" y1="344" y2="344"/>
<line x1="460" x2="460" y1="312" y2="344"/>
<line x1="460" x2="460" y1="344" y2="360"/>
<line x1="460" x2="596" y1="360" y2="360"/>
<line x1="596" x2="596" y1="344" y2="360"/>
<line x1="596" x2="600" y1="344" y2="344"/>
</g>
<g>
<line x1="460" x2="460" y1="56" y2="72"/>
<line x1="460" x2="596" y1="56" y2="56"/>
<line x1="460" x2="456" y1="72" y2="72"/>
<line x1="596" x2="596" y1="56" y2="72"/>
<line x1="596" x2="596" y1="72" y2="104"/>
<line marker-end="url(#triangle)" x1="596" x2="652" y1="72" y2="72"/>
</g>
<g>
<line x1="460" x2="460" y1="296" y2="312"/>
<line x1="460" x2="508" y1="296" y2="296"/>
<line x1="460" x2="456" y1="312" y2="312"/>
<line x1="508" x2="596" y1="296" y2="296"/>
<line x1="596" x2="596" y1="296" y2="312"/>
<line x1="596" x2="596" y1="312" y2="344"/>
<line marker-end="url(#triangle)" x1="596" x2="652" y1="312" y2="312"/>
</g>
<g>
<line x1="508" x2="508" y1="160" y2="240"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="508" x2="508" y1="256" y2="284"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="608" x2="604" y1="104" y2="104"/>
<line x1="608" x2="660" y1="104" y2="104"/>
<line x1="660" x2="660" y1="72" y2="104"/>
<line x1="660" x2="660" y1="104" y2="120"/>
<line x1="660" x2="684" y1="120" y2="120"/>
<line x1="684" x2="684" y1="120" y2="144"/>
<line x1="684" x2="796" y1="120" y2="120"/>
<line x1="796" x2="796" y1="56" y2="120"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="608" x2="604" y1="344" y2="344"/>
<line x1="608" x2="660" y1="344" y2="344"/>
<line x1="660" x2="660" y1="312" y2="344"/>
<line x1="660" x2="660" y1="344" y2="360"/>
<line x1="660" x2="796" y1="360" y2="360"/>
<line x1="796" x2="796" y1="296" y2="360"/>
</g>
<g>
<line x1="660" x2="660" y1="56" y2="72"/>
<line x1="660" x2="796" y1="56" y2="56"/>
<line x1="660" x2="656" y1="72" y2="72"/>
</g>
<g>
<line x1="660" x2="660" y1="296" y2="312"/>
<line x1="660" x2="684" y1="296" y2="296"/>
<line x1="660" x2="656" y1="312" y2="312"/>
<line x1="684" x2="796" y1="296" y2="296"/>
</g>
<g>
<line x1="684" x2="684" y1="160" y2="240"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="684" x2="684" y1="256" y2="284"/>
</g>
<g>
<text x="89" y="92">
Neighbor
</text>
</g>
<g>
<text x="89" y="332">
Neighbor
</text>
</g>
<g>
<text x="161" y="92">
1
</text>
</g>
<g>
<text x="161" y="332">
1
</text>
</g>
<g>
<text x="289" y="92">
Neighbor
</text>
</g>
<g>
<text x="289" y="332">
Neighbor
</text>
</g>
<g>
<text x="353" y="28">
Neighborhood
</text>
</g>
<g>
<text x="353" y="268">
Neighborhood
</text>
</g>
<g>
<text x="361" y="92">
2
</text>
</g>
<g>
<text x="361" y="332">
2
</text>
</g>
<g>
<text x="457" y="28">
Above
</text>
</g>
<g>
<text x="457" y="268">
Below
</text>
</g>
<g>
<text x="489" y="92">
Neighbor
</text>
</g>
<g>
<text x="489" y="332">
Neighbor
</text>
</g>
<g>
<text x="561" y="92">
3
</text>
</g>
<g>
<text x="561" y="332">
3
</text>
</g>
<g>
<text x="689" y="92">
Neighbor
</text>
</g>
<g>
<text x="689" y="332">
Neighbor
</text>
</g>
<g>
<text x="761" y="92">
4
</text>
</g>
<g>
<text x="761" y="332">
4
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,322 @@
<svg class="bob" font-family="arial" font-size="14" height="400" width="856" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="triangle" markerHeight="8" markerWidth="8" orient="auto" refX="4" refY="2" viewBox="0 0 8 4">
<polygon fill="black" points="0,0 0,4 8,2 0,0"/>
</marker>
<marker id="clear_triangle" markerHeight="10" markerWidth="10" orient="auto" refX="1" refY="7" viewBox="0 0 20 14">
<polygon fill="none" points="2,2 2,12 18,7 2,2" stroke="black" stroke-width="2"/>
</marker>
<marker id="circle" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="black" r="8"/>
</marker>
<marker id="square" markerHeight="5" markerWidth="5" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<rect fill="black" height="20" width="20" x="0" y="0"/>
</marker>
<marker id="open_circle" markerHeight="10" markerWidth="10" orient="auto" refX="10" refY="10" viewBox="0 0 20 20">
<circle cx="10" cy="10" fill="white" r="4" stroke="black" stroke-width="2"/>
</marker>
<marker id="big_open_circle" markerHeight="20" markerWidth="20" orient="auto" refX="20" refY="20" viewBox="0 0 40 40">
<circle cx="20" cy="20" fill="white" r="6" stroke="black" stroke-width="2"/>
</marker>
</defs>
<style type="text/css">
line,path {
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
line.dashed {
stroke-dasharray: 5;
}
circle.solid {
fill:black;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
circle.open {
fill:none;
stroke: black;
stroke-width: 2;
stroke-opacity: 1;
fill-opacity: 1;
stroke-linecap: round;
stroke-linejoin: miter;
}
tspan.head{
fill: none;
stroke: none;
}
</style>
<rect fill="white" height="400" width="856" x="0" y="0"/>
<g>
<line x1="4" x2="4" y1="8" y2="152"/>
<line x1="4" x2="852" y1="8" y2="8"/>
<line x1="4" x2="852" y1="152" y2="152"/>
<line x1="852" x2="852" y1="8" y2="152"/>
</g>
<g>
<line x1="4" x2="4" y1="248" y2="392"/>
<line x1="4" x2="852" y1="248" y2="248"/>
<line x1="4" x2="852" y1="392" y2="392"/>
<line x1="852" x2="852" y1="248" y2="392"/>
</g>
<g>
<line x1="60" x2="60" y1="56" y2="120"/>
<line x1="60" x2="196" y1="56" y2="56"/>
<line x1="60" x2="84" y1="120" y2="120"/>
<line x1="84" x2="84" y1="120" y2="144"/>
<line x1="84" x2="196" y1="120" y2="120"/>
<line x1="196" x2="196" y1="56" y2="72"/>
<line x1="196" x2="196" y1="72" y2="104"/>
<line marker-end="url(#triangle)" x1="196" x2="252" y1="72" y2="72"/>
<line x1="196" x2="196" y1="104" y2="120"/>
<line x1="196" x2="200" y1="104" y2="104"/>
</g>
<g>
<line x1="60" x2="60" y1="296" y2="360"/>
<line x1="60" x2="84" y1="296" y2="296"/>
<line x1="60" x2="196" y1="360" y2="360"/>
<line x1="84" x2="196" y1="296" y2="296"/>
<line x1="196" x2="196" y1="296" y2="312"/>
<line x1="196" x2="196" y1="312" y2="344"/>
<line marker-end="url(#triangle)" x1="196" x2="252" y1="312" y2="312"/>
<line x1="196" x2="196" y1="344" y2="360"/>
<line x1="196" x2="200" y1="344" y2="344"/>
</g>
<g>
<line x1="84" x2="84" y1="160" y2="240"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="84" x2="84" y1="256" y2="284"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="208" x2="204" y1="104" y2="104"/>
<line x1="208" x2="260" y1="104" y2="104"/>
<line x1="260" x2="260" y1="72" y2="104"/>
<line x1="260" x2="260" y1="104" y2="120"/>
<line x1="260" x2="284" y1="120" y2="120"/>
<line x1="284" x2="284" y1="120" y2="144"/>
<line x1="284" x2="396" y1="120" y2="120"/>
<line x1="396" x2="396" y1="104" y2="120"/>
<line x1="396" x2="400" y1="104" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="208" x2="204" y1="344" y2="344"/>
<line x1="208" x2="260" y1="344" y2="344"/>
<line x1="260" x2="260" y1="312" y2="344"/>
<line x1="260" x2="260" y1="344" y2="360"/>
<line x1="260" x2="396" y1="360" y2="360"/>
<line x1="396" x2="396" y1="344" y2="360"/>
<line x1="396" x2="400" y1="344" y2="344"/>
</g>
<g>
<line x1="260" x2="260" y1="56" y2="72"/>
<line x1="260" x2="396" y1="56" y2="56"/>
<line x1="260" x2="256" y1="72" y2="72"/>
<line x1="396" x2="396" y1="56" y2="72"/>
<line x1="396" x2="396" y1="72" y2="104"/>
<line marker-end="url(#triangle)" x1="396" x2="452" y1="72" y2="72"/>
</g>
<g>
<line x1="260" x2="260" y1="296" y2="312"/>
<line x1="260" x2="284" y1="296" y2="296"/>
<line x1="260" x2="256" y1="312" y2="312"/>
<line x1="284" x2="396" y1="296" y2="296"/>
<line x1="396" x2="396" y1="296" y2="312"/>
<line x1="396" x2="396" y1="312" y2="344"/>
<line marker-end="url(#triangle)" x1="396" x2="452" y1="312" y2="312"/>
</g>
<g>
<line x1="284" x2="284" y1="160" y2="240"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="284" x2="284" y1="256" y2="284"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="408" x2="404" y1="104" y2="104"/>
<line x1="408" x2="460" y1="104" y2="104"/>
<line x1="460" x2="460" y1="72" y2="104"/>
<line x1="460" x2="460" y1="104" y2="120"/>
<line x1="460" x2="508" y1="120" y2="120"/>
<line x1="508" x2="508" y1="120" y2="144"/>
<line x1="508" x2="596" y1="120" y2="120"/>
<line x1="596" x2="596" y1="104" y2="120"/>
<line x1="596" x2="600" y1="104" y2="104"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="408" x2="404" y1="344" y2="344"/>
<line x1="408" x2="460" y1="344" y2="344"/>
<line x1="460" x2="460" y1="312" y2="344"/>
<line x1="460" x2="460" y1="344" y2="360"/>
<line x1="460" x2="596" y1="360" y2="360"/>
<line x1="596" x2="596" y1="344" y2="360"/>
<line x1="596" x2="600" y1="344" y2="344"/>
</g>
<g>
<line x1="460" x2="460" y1="56" y2="72"/>
<line x1="460" x2="596" y1="56" y2="56"/>
<line x1="460" x2="456" y1="72" y2="72"/>
<line x1="596" x2="596" y1="56" y2="72"/>
<line x1="596" x2="596" y1="72" y2="104"/>
<line marker-end="url(#triangle)" x1="596" x2="652" y1="72" y2="72"/>
</g>
<g>
<line x1="460" x2="460" y1="296" y2="312"/>
<line x1="460" x2="508" y1="296" y2="296"/>
<line x1="460" x2="456" y1="312" y2="312"/>
<line x1="508" x2="596" y1="296" y2="296"/>
<line x1="596" x2="596" y1="296" y2="312"/>
<line x1="596" x2="596" y1="312" y2="344"/>
<line marker-end="url(#triangle)" x1="596" x2="652" y1="312" y2="312"/>
</g>
<g>
<line x1="508" x2="508" y1="160" y2="240"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="508" x2="508" y1="256" y2="284"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="608" x2="604" y1="104" y2="104"/>
<line x1="608" x2="660" y1="104" y2="104"/>
<line x1="660" x2="660" y1="72" y2="104"/>
<line x1="660" x2="660" y1="104" y2="120"/>
<line x1="660" x2="684" y1="120" y2="120"/>
<line x1="684" x2="684" y1="120" y2="144"/>
<line x1="684" x2="796" y1="120" y2="120"/>
<line x1="796" x2="796" y1="56" y2="120"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="608" x2="604" y1="344" y2="344"/>
<line x1="608" x2="660" y1="344" y2="344"/>
<line x1="660" x2="660" y1="312" y2="344"/>
<line x1="660" x2="660" y1="344" y2="360"/>
<line x1="660" x2="796" y1="360" y2="360"/>
<line x1="796" x2="796" y1="296" y2="360"/>
</g>
<g>
<line x1="660" x2="660" y1="56" y2="72"/>
<line x1="660" x2="796" y1="56" y2="56"/>
<line x1="660" x2="656" y1="72" y2="72"/>
</g>
<g>
<line x1="660" x2="660" y1="296" y2="312"/>
<line x1="660" x2="684" y1="296" y2="296"/>
<line x1="660" x2="656" y1="312" y2="312"/>
<line x1="684" x2="796" y1="296" y2="296"/>
</g>
<g>
<line x1="684" x2="684" y1="160" y2="240"/>
</g>
<g>
<line marker-end="url(#triangle)" x1="684" x2="684" y1="256" y2="284"/>
</g>
<g>
<text x="89" y="92">
Neighbor
</text>
</g>
<g>
<text x="89" y="332">
Neighbor
</text>
</g>
<g>
<text x="161" y="92">
1
</text>
</g>
<g>
<text x="161" y="332">
1
</text>
</g>
<g>
<text x="289" y="92">
Neighbor
</text>
</g>
<g>
<text x="289" y="332">
Neighbor
</text>
</g>
<g>
<text x="353" y="28">
Neighborhood
</text>
</g>
<g>
<text x="353" y="268">
Neighborhood
</text>
</g>
<g>
<text x="361" y="92">
2
</text>
</g>
<g>
<text x="361" y="332">
2
</text>
</g>
<g>
<text x="457" y="28">
Above
</text>
</g>
<g>
<text x="457" y="268">
Below
</text>
</g>
<g>
<text x="489" y="92">
Neighbor
</text>
</g>
<g>
<text x="489" y="332">
Neighbor
</text>
</g>
<g>
<text x="561" y="92">
3
</text>
</g>
<g>
<text x="561" y="332">
3
</text>
</g>
<g>
<text x="689" y="92">
Neighbor
</text>
</g>
<g>
<text x="689" y="332">
Neighbor
</text>
</g>
<g>
<text x="761" y="92">
4
</text>
</g>
<g>
<text x="761" y="332">
4
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

Some files were not shown because too many files have changed in this diff Show More