Compare commits

..

105 Commits

Author SHA1 Message Date
cd3ff5c335 V0.12.3, cherry pick 3523 and 3529 (#3531)
* validator confirmation

* validator confirmaiton

* remove leader confirmaiton

* hang out on progress until fork is confirmed

* use the right id for delegate id

* fixup! hang out on progress until fork is confirmed

* fixup! use the right id for delegate id

* version bump
2019-03-28 05:59:42 -07:00
e55249e63f propagate TESTNET_DB_HOST env variable to next step in buildkite 2019-03-26 15:00:33 -07:00
10bc0c6ee2 Add provisions to specify a database server in testnet manager buildkite 2019-03-26 15:00:33 -07:00
ed14b78d81 also check the delegate_id 2019-03-26 13:44:53 -07:00
7f404941bb remove status_cache.freeze (#3509) 2019-03-26 12:10:46 -07:00
6d45ac1bc7 Record the current nodes locktower votes from the bank (#3502)
* observed_locktower_stats

* fixup! observed_locktower_stats
2019-03-26 11:45:59 -07:00
fabb6d2092 delay freeze of status_cache until squash (#3503) 2019-03-26 11:37:38 -07:00
93cea4c86c Remove rewards crate from publishing script 2019-03-25 21:34:54 -07:00
5fb35f79c3 Added stats for locktower in testnet dashboard 2019-03-25 21:11:37 -07:00
da11274b63 Add support for influx cloud 2019-03-25 21:11:37 -07:00
5d70e2efa9 0.12.2 2019-03-25 20:38:48 -07:00
8f181b4350 keep track of locktower slots and stakes 2019-03-25 16:36:19 -07:00
48844924e5 Setup staking (#3480) 2019-03-25 14:19:14 -07:00
f84593ad5f Revert "Disable accounts squash call from bank"
This reverts commit 7685ba2805.
2019-03-25 12:21:32 -07:00
0469dc52ac Ensure accounts are unlocked (#3458) 2019-03-25 12:21:32 -07:00
4cf418f33f Fix wrong keypair 2019-03-23 16:33:50 -07:00
6c46fcfa4e Restart node test (#3459)
* Add test to local_cluster for restarting a node

* fix so that we don't hit end of epoch - leader not found before trying to transfer
2019-03-23 15:00:23 -07:00
12ec5304f2 Revert "fix so that we don't hit end of epoch - leader not found before trying to transfer"
Revert "Add test to local_cluster for restarting a node"
2019-03-22 21:46:08 -07:00
e32f798d5f fix so that we don't hit end of epoch - leader not found before trying to transfer 2019-03-22 20:47:32 -07:00
68a8b955bc Add test to local_cluster for restarting a node 2019-03-22 19:30:14 -07:00
f479021c0f Update leader slot in poh recorder if we skipped it (#3451)
* reset poh recorder with the original start slot
2019-03-22 17:35:54 -07:00
b91afb7079 Remove attempt to update the cluster, just restart it (v0.12 is not ready for update) 2019-03-22 16:51:53 -07:00
e189c429d5 Refrain from trying to configure a staking account that was previously configured 2019-03-22 16:51:53 -07:00
6a1904664c Demote log level 2019-03-22 16:51:53 -07:00
3285cf8047 Retry more for a new blockhash 2019-03-22 10:56:59 -07:00
bdee3a25f2 Add --poll-for-new-genesis-block flag 2019-03-22 00:44:31 -07:00
8655df0520 Use same gossip port for all testnet nodes 2019-03-21 23:56:23 -07:00
c43eecb8ca Include multinode-demo scripts in release tarball 2019-03-21 22:12:07 -07:00
18f45ebc2c Use installed binaries if not within the cargo workspace 2019-03-21 22:12:07 -07:00
fd28642603 Run a drone on blockstreamer nodes 2019-03-21 22:12:07 -07:00
038583b466 Kill all node processes (blockexplorer) 2019-03-21 22:12:07 -07:00
ed138d392d Fixup ledger path 2019-03-21 17:06:05 -07:00
58f1f0a28b solana-install doesn't exist on v0.12 2019-03-21 16:49:41 -07:00
330d9330b0 Ensure current crate versions match the tag before publishing to crates.io 2019-03-21 16:27:44 -07:00
d626a89c88 / 2019-03-21 16:27:06 -07:00
db5d22e532 Upload tarball as a github release asset 2019-03-21 16:27:06 -07:00
aa8759744e Add script to upload github release assets 2019-03-21 16:27:06 -07:00
060db36c34 Add GITHUB_TOKEN 2019-03-21 16:27:06 -07:00
fa1ea1c458 Switch version file from .txt to .yaml; add target tuple to version.yml 2019-03-21 16:27:06 -07:00
7685ba2805 Disable accounts squash call from bank
- It's asserting and killing testnet
- temporary solution for beacons
2019-03-21 16:01:43 -07:00
a0d940acf0 allow empty ancestors 2019-03-21 16:01:43 -07:00
f4c914a630 Clear progress map on squash (#3377) 2019-03-21 16:01:43 -07:00
eede274cfe fix is_locked_out logic 2019-03-21 16:01:43 -07:00
4df79b653b PR comments 2019-03-21 16:01:43 -07:00
a2c1fa7cb4 Modify bank_forks to support squashing/filtering new root and also don't remove parents from bank_forks when inserting, otherwise we lose potential fork points when querying blocktree for child slots 2019-03-21 16:01:43 -07:00
95cead91a5 Decendent is not a word 2019-03-21 16:01:43 -07:00
89c42ecd3f Implement locktower voting (#3251)
* locktower components and tests

* integrate locktower into replay stage

* track locktower duration

* make sure threshold is checked after simulating the vote

* check vote lockouts using the VoteState program

* duplicate vote test

* epoch stakes

* disable impossible to verify tests
2019-03-21 16:01:43 -07:00
f93c9f052f Ensure genesis ledger directory is populated on all validator nodes
This allows all nodes to serve the genesis ledger over rsync instead of
just the bootstrap leader
2019-03-21 15:55:12 -07:00
e2871053bd Get client-id.json out of the genesis ledger directory 2019-03-21 15:55:08 -07:00
351c9c33d2 change num threads in banking stage bench 2019-03-21 15:00:30 -07:00
59f2a478b7 v0.12 specific stability changes 2019-03-21 15:00:30 -07:00
3f7cd4adc4 Ignore broken tests that are fixed on master
- ignoring, as cherry picking from master will bring in other
  unnecessary dependent changes
2019-03-21 13:45:41 -07:00
4318854a64 ignore broken test 2019-03-21 13:45:41 -07:00
430740b691 use ticks per slot to check if the current tick is in the leader slot 2019-03-21 13:45:41 -07:00
797603a0fe address review comments 2019-03-21 13:45:41 -07:00
f402139991 change pubkey to ref 2019-03-21 13:45:41 -07:00
4db72d85d7 find next leader slot before resetting working bank in Poh recorder 2019-03-21 13:45:41 -07:00
007e17c290 Check if poh recorder has over stepped the leader slot 2019-03-21 13:45:41 -07:00
ad7e727938 Use same VM type for validators as leader, if CUDA is enabled (#3253)
- Since all nodes are created equal
2019-03-21 13:45:41 -07:00
3d5eeab6d9 stop copying Blooms (#3379)
* stop copying Blooms

* fixup

* clippy
2019-03-21 13:45:41 -07:00
8278585545 Avoid panic on duplicate account indices 2019-03-19 16:06:50 -07:00
061d6ec8fd fix formatting 2019-03-19 11:21:00 -07:00
000cc27e53 Schedule node for consecutive slots as leader (#3353)
* Also tweak epoch and slot duration

* new test for leader schedule
2019-03-19 11:21:00 -07:00
9b3092b965 Report how many grace ticks were afforded to previous leader (#3350) 2019-03-19 11:21:00 -07:00
ca819fc4fb Fix leader rotation counter 2019-03-19 11:21:00 -07:00
5ff8f57c0e Remove dangling thin_client 2019-03-18 22:20:14 -07:00
4798612560 Reduce log level for periodic debug messages 2019-03-15 16:02:52 -07:00
9760cb2e6a add support for finding the next slot a node will be leader (#3298) 2019-03-15 15:02:20 -07:00
46b3b3a1c6 Give last leader some grace ticks to catch up (#3299)
* Wait for last leader for some ticks

* New tests and fixed existing tests
2019-03-15 15:02:20 -07:00
1e70f85e83 [v0.12] Reduce ticks per second (#3287)
* Reduce ticks per second

- It's improving TPS. Temp fix for beacons timeframe

* Fix confirmation test
2019-03-15 14:15:54 -07:00
b2d6681762 Bump log level for better CI logs 2019-03-15 07:48:57 -07:00
1b51cba778 Avoid stray '' when rust version is not specified 2019-03-14 21:32:25 -07:00
19ab7333aa cloud_DeleteInstances() now waits for the instances to be terminated 2019-03-14 21:17:36 -07:00
b0e6604b9a Revert "Block until instances are confirmed to be deleted"
This reverts commit 5e40a5bfc1.
2019-03-14 21:17:30 -07:00
9ce1d5e990 Upgrade nightly rust version 2019-03-14 20:37:44 -07:00
facc47cb62 Preserve original nightly name 2019-03-14 20:37:44 -07:00
3dba8b7952 Overhaul cargo/rustc version management 2019-03-14 20:37:44 -07:00
5e40a5bfc1 Block until instances are confirmed to be deleted 2019-03-14 16:20:35 -07:00
c60baf99f3 Rename userdata to data (#3282)
* Rename userdata to data

Instead of saying "userdata", which is ambiguous and imprecise,
say "instruction data" or "account data".

Also, add `ProgramError::InvalidInstructionData`

Fixes #2761
2019-03-14 13:04:42 -07:00
de04884c1b Fix flag to disable leader-rotation (#3243) 2019-03-14 12:08:53 -07:00
e666509409 Don't vote for empty leader transmissions (#3248)
* Don't vote for empty leader transmissions

* Add is_delta flag to bank to detect empty leader transmissions

* Plumb new is_votable flag through replay stage

* Fix PohRecorder tests

* Change is_delta to AtomicBool to avoid making Bank references mutable

* Reset start slot in poh_recorder when working bank is cleared, so that connsecutive TPU's will start from the correct place

* Use proper max tick height calculation

* Test for not voting on empty transmission

* tests for is_votable
2019-03-13 14:32:04 -07:00
28aff96d21 Replace stale --no-signer usage with --no-voting 2019-03-13 13:56:57 -07:00
242975f8cd Remove duplicate --rpc-drone-address 2019-03-13 13:23:18 -07:00
c6ba6cac83 Revert "Add case for --rpc-drone-address"
This reverts commit dc67dd3357.
2019-03-13 13:15:49 -07:00
dc67dd3357 Add case for --rpc-drone-address 2019-03-13 13:03:54 -07:00
733c2a0b07 Enable rpc for all testnet nodes 2019-03-13 10:51:49 -07:00
07d6212d18 Drop socat for iptables 2019-03-13 10:16:28 -07:00
c20d60e4cf Run socat in the background 2019-03-13 08:18:10 -07:00
7147f03efe tell blockexplorer to run on port 8080 (#3237)
* tell blockexplorer to run on port 8080

* forward port 80 to 5000 for a blockexplorer node
2019-03-13 07:37:28 -07:00
6740cb5b02 Replay Stage start_leader() can use wrong parent fork() (#3238)
*  Make sure start_leader starts on the last voted block, not necessarily the biggest indexed bank in frozen_slots()

* Fix tvu test
2019-03-13 03:16:13 -07:00
1e8e99cc3e Move and rename cluster_client 2019-03-12 23:07:48 -06:00
ef7f30e09f Update publish script 2019-03-12 23:07:48 -06:00
ca8e0ec7ae Move thin client tests to integration test suite 2019-03-12 23:07:48 -06:00
2a4f4b3e53 Update crate references 2019-03-12 23:07:48 -06:00
7cecd3851a Add solana-client crate 2019-03-12 23:07:48 -06:00
4d189f2c38 Cargo.lock 2019-03-12 23:07:48 -06:00
9a232475a7 0.12.1 2019-03-12 13:42:47 -07:00
09c9897591 Adjust crate list 2019-03-12 13:36:18 -07:00
06d7573478 Adjust readme path 2019-03-12 13:36:13 -07:00
0b55ffa368 Move programs/system into runtime/ 2019-03-12 12:25:47 -05:00
ae750bb16b Filter vote accounts with no delegate from being selected in Rotation (#3224) 2019-03-11 21:32:19 -07:00
80b2f2f6b7 Update current leader information in metrics and dashboard 2019-03-11 18:47:27 -07:00
6684d84fbc Provide drone's host address while setting up staking account 2019-03-11 18:20:27 -07:00
dc02abae3c Keep stable dashboard on stable channel at all times 2019-03-11 16:19:35 -07:00
6caec655d3 Move testnet/testnet-perf to the stable channel 2019-03-11 16:15:47 -07:00
345 changed files with 17956 additions and 27470 deletions

View File

@ -46,17 +46,10 @@ and longer descriptions detailing what problem it solves and how it solves it.
Draft Pull Requests
---
If you want early feedback on your PR, use GitHub's "Draft Pull Request"
mechanism. Draft PRs are a convenient way to collaborate with the Solana
maintainers without triggering notifications as you make changes. When you feel
your PR is ready for a broader audience, you can transition your draft PR to a
standard PR with the click of a button.
Do not add reviewers to draft PRs. GitHub doesn't automatically clear approvals
when you click "Ready for Review", so a review that meant "I approve of the
direction" suddenly has the appearance of "I approve of these changes." Instead,
add a comment that mentions the usernames that you would like a review from. Ask
explicitly what you would like feedback on.
If you want early feedback on your PR, use GitHub's "Draft Pull Request" mechanism. Draft
PRs are a convenient way to collaborate with the Solana maintainers without triggering
notifications as you make changes. When you feel your PR is ready for a broader audience,
you can transition your draft PR to a standard PR with the click of a button.
Rust coding conventions
---
@ -96,23 +89,24 @@ understood. Avoid introducing new 3-letter terms, which can be confused with 3-l
[Terms currently in use](book/src/terminology.md)
Design Proposals
Proposing architectural changes
---
Solana's architecture is described by a book generated from markdown files in
the `book/src/` directory, maintained by an *editor* (currently @garious). To
add a design proposal, you'll need to at least propose a change the content
under the [Accepted Design
Proposals](https://solana-labs.github.io/book-edge/proposals.html) chapter.
Here's the full process:
change the architecture, you'll need to at least propose a change the content
under the [Proposed
Changes](https://solana-labs.github.io/book-edge/proposals.html) chapter. Here's
the full process:
1. Propose a design by creating a PR that adds a markdown document to the
directory `book/src/` and references it from the [table of
contents](book/src/SUMMARY.md). Add any relevant *maintainers* to the PR review.
1. Propose to a change to the architecture by creating a PR that adds a
markdown document to the directory `book/src/` and references it from the
[table of contents](book/src/SUMMARY.md). Add the editor and any relevant
*maintainers* to the PR review.
2. The PR being merged indicates your proposed change was accepted and that the
maintainers support your plan of attack.
editor and maintainers support your plan of attack.
3. Submit PRs that implement the proposal. When the implementation reveals the
need for tweaks to the proposal, be sure to update the proposal and have
need for tweaks to the architecture, be sure to update the proposal and have
that change reviewed by the same people as in step 1.
4. Once the implementation is complete, submit a PR that moves the link from
the Accepted Proposals to the Implemented Proposals section.
4. Once the implementation is complete, the editor will then work to integrate
the document into the book.

1814
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,88 @@
[package]
name = "solana-workspace"
description = "Blockchain, Rebuilt for Scale"
version = "0.12.3"
documentation = "https://docs.rs/solana"
homepage = "https://solana.com/"
readme = "README.md"
repository = "https://github.com/solana-labs/solana"
authors = ["Solana Maintainers <maintainers@solana.com>"]
license = "Apache-2.0"
edition = "2018"
[badges]
codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
[features]
chacha = ["solana/chacha"]
cuda = ["solana/cuda"]
erasure = ["solana/erasure"]
[dev-dependencies]
bincode = "1.1.2"
bs58 = "0.2.0"
hashbrown = "0.1.8"
log = "0.4.2"
rand = "0.6.5"
rayon = "1.0.0"
reqwest = "0.9.11"
serde_json = "1.0.39"
solana = { path = "core", version = "0.12.3" }
solana-budget-program = { path = "programs/budget", version = "0.12.3" }
solana-client = { path = "client", version = "0.12.3" }
solana-logger = { path = "logger", version = "0.12.3" }
solana-netutil = { path = "netutil", version = "0.12.3" }
solana-runtime = { path = "runtime", version = "0.12.3" }
solana-sdk = { path = "sdk", version = "0.12.3" }
solana-vote-api = { path = "programs/vote_api", version = "0.12.3" }
sys-info = "0.5.6"
[[bench]]
name = "banking_stage"
[[bench]]
name = "blocktree"
[[bench]]
name = "ledger"
[[bench]]
name = "gen_keys"
[[bench]]
name = "sigverify"
[[bench]]
required-features = ["chacha"]
name = "chacha"
[workspace]
members = [
".",
"bench-streamer",
"bench-tps",
"core",
"drone",
"fullnode",
"genesis",
"gossip",
"install",
"keygen",
"kvstore",
"ledger-tool",
"logger",
"metrics",
"programs/bpf",
"programs/bpf_loader",
"programs/budget",
"programs/budget_api",
"programs/budget_program",
"programs/config_api",
"programs/config_program",
"programs/exchange_api",
"programs/exchange_program",
"programs/token",
"programs/token_api",
"programs/token_program",
"programs/failure_program",
"programs/noop_program",
"programs/stake_api",
"programs/stake_program",
"programs/failure",
"programs/noop",
"programs/rewards",
"programs/rewards_api",
"programs/storage",
"programs/storage_api",
"programs/storage_program",
"programs/vote",
"programs/vote_api",
"programs/vote_program",
"replicator",
"sdk",
"upload-perf",

View File

@ -44,7 +44,7 @@ $ source $HOME/.cargo/env
$ rustup component add rustfmt-preview
```
If your rustc version is lower than 1.34.0, please update it:
If your rustc version is lower than 1.31.0, please update it:
```bash
$ rustup update
@ -83,6 +83,12 @@ Run the test suite:
$ cargo test --all
```
To emulate all the tests that will run on a Pull Request, run:
```bash
$ ./ci/run-local.sh
```
Local Testnet
---
@ -125,47 +131,6 @@ can run your own testnet using the scripts in the `net/` directory.
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
---

View File

@ -63,25 +63,20 @@ There are three release channels that map to branches as follows:
### Changing channels
#### Create the new branch
When cutting a new channel branch these pre-steps are required:
1. Pick your branch point for release on master.
1. Create the branch. The name should be "v" + the first 2 "version" fields
from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies
the next branch name is "v0.9".
1. Note the Cargo.toml in the repo root directory does not contain a version. Look at any other Cargo.toml file.
1. Create a new branch and push this branch to the solana repository.
1. `git checkout -b <branchname>`
1. `git push -u origin <branchname>`
#### Update master with the next version
1. After the new branch has been created and pushed, update Cargo.toml on **master** to the next semantic version (e.g. 0.9.0 -> 0.10.0)
by running `./scripts/increment-cargo-version.sh`, then rebuild with
`cargo build` to cause a refresh of `Cargo.lock`.
1. Push the new branch to the solana repository
1. Update Cargo.toml on master to the next semantic version (e.g. 0.9.0 -> 0.10.0)
by running `./scripts/increment-cargo-version.sh`, then rebuild with a
`cargo build --all` to cause a refresh of `Cargo.lock`.
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
master branch
At this point, `ci/channel-info.sh` should show your freshly cut release branch as
At this point, ci/channel-info.sh should show your freshly cut release branch as
"BETA_CHANNEL" and the previous release branch as "STABLE_CHANNEL".
### Updating channels (i.e. "making a release")
@ -91,8 +86,7 @@ We use [github's Releases UI](https://github.com/solana-labs/solana/releases) fo
1. Go [there ;)](https://github.com/solana-labs/solana/releases).
1. Click "Draft new release". The release tag must exactly match the `version`
field in `/Cargo.toml` prefixed by `v` (ie, `<branchname>.X`).
1. If the Cargo.toml verion field is **0.12.3**, then the release tag must be **v0.12.3**
1. If this is the first release on the branch (e.g. v0.13.**0**), paste in [this
1. If the first major release on the branch (e.g. v0.8.0), paste in [this
template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md)
and fill it in.
1. Test the release by generating a tag using semver's rules. First try at a
@ -103,9 +97,9 @@ We use [github's Releases UI](https://github.com/solana-labs/solana/releases) fo
1. After testnet deployment, verify that testnets are running correct software.
http://metrics.solana.com should show testnet running on a hash from your
newly created branch.
1. Once the release has been made, update Cargo.toml on the release branch to the next
1. Once the release has been made, update Cargo.toml on release to the next
semantic version (e.g. 0.9.0 -> 0.9.1) by running
`./scripts/increment-cargo-version.sh patch`, then rebuild with `cargo
build` to cause a refresh of `Cargo.lock`.
`./scripts/increment-cargo-version.sh patch`, then rebuild with a `cargo
build --all` to cause a refresh of `Cargo.lock`.
1. Push your Cargo.toml change and the autogenerated Cargo.lock changes to the
release branch.
release branch

View File

@ -2,17 +2,16 @@
authors = ["Solana Maintainers <maintainers@solana.com>"]
edition = "2018"
name = "solana-bench-streamer"
version = "0.13.2"
version = "0.12.3"
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
[dependencies]
clap = "2.33.0"
solana = { path = "../core", version = "0.13.2" }
solana-logger = { path = "../logger", version = "0.13.2" }
solana-netutil = { path = "../netutil", version = "0.13.2" }
clap = "2.32.0"
solana = { path = "../core", version = "0.12.3" }
solana-logger = { path = "../logger", version = "0.12.3" }
solana-netutil = { path = "../netutil", version = "0.12.3" }
[features]
cuda = ["solana/cuda"]
erasure = []

View File

@ -1,4 +1,4 @@
use clap::{crate_description, crate_name, crate_version, App, Arg};
use clap::{App, Arg};
use solana::packet::{Packet, SharedPackets, BLOB_SIZE, PACKET_DATA_SIZE};
use solana::result::Result;
use solana::streamer::{receiver, PacketReceiver};
@ -51,9 +51,7 @@ fn sink(exit: Arc<AtomicBool>, rvs: Arc<AtomicUsize>, r: PacketReceiver) -> Join
fn main() -> Result<()> {
let mut num_sockets = 1usize;
let matches = App::new(crate_name!())
.about(crate_description!())
.version(crate_version!())
let matches = App::new("solana-bench-streamer")
.arg(
Arg::with_name("num-recv-sockets")
.long("num-recv-sockets")

View File

@ -2,23 +2,21 @@
authors = ["Solana Maintainers <maintainers@solana.com>"]
edition = "2018"
name = "solana-bench-tps"
version = "0.13.2"
version = "0.12.3"
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
[dependencies]
clap = "2.33.0"
clap = "2.32.0"
rayon = "1.0.3"
serde_json = "1.0.39"
solana = { path = "../core", version = "0.13.2" }
solana-client = { path = "../client", version = "0.13.2" }
solana-drone = { path = "../drone", version = "0.13.2" }
solana-logger = { path = "../logger", version = "0.13.2" }
solana-metrics = { path = "../metrics", version = "0.13.2" }
solana-netutil = { path = "../netutil", version = "0.13.2" }
solana-sdk = { path = "../sdk", version = "0.13.2" }
solana = { path = "../core", version = "0.12.3" }
solana-client = { path = "../client", version = "0.12.3" }
solana-drone = { path = "../drone", version = "0.12.3" }
solana-logger = { path = "../logger", version = "0.12.3" }
solana-metrics = { path = "../metrics", version = "0.12.3" }
solana-sdk = { path = "../sdk", version = "0.12.3" }
[features]
cuda = ["solana/cuda"]
erasure = []

View File

@ -1,21 +1,16 @@
use solana_metrics;
use crate::cli::Config;
use rayon::prelude::*;
use solana::cluster_info::FULLNODE_PORT_RANGE;
use solana::contact_info::ContactInfo;
use solana::gen_keys::GenKeys;
use solana::gossip_service::discover_nodes;
use solana_client::thin_client::create_client;
use solana_client::client::create_client;
use solana_client::thin_client::ThinClient;
use solana_drone::drone::request_airdrop_transaction;
use solana_metrics::influxdb;
use solana_sdk::client::{AsyncClient, SyncClient};
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_instruction;
use solana_sdk::system_transaction;
use solana_sdk::system_transaction::SystemTransaction;
use solana_sdk::timing::timestamp;
use solana_sdk::timing::{duration_as_ms, duration_as_s};
use solana_sdk::transaction::Transaction;
@ -26,7 +21,6 @@ use std::process::exit;
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use std::thread::sleep;
use std::thread::Builder;
use std::time::Duration;
use std::time::Instant;
@ -41,201 +35,7 @@ pub const MAX_SPENDS_PER_TX: usize = 4;
pub type SharedTransactions = Arc<RwLock<VecDeque<Vec<(Transaction, u64)>>>>;
pub fn do_bench_tps(config: Config) {
let Config {
network_addr: network,
drone_addr,
id,
threads,
thread_batch_sleep_ms,
num_nodes,
duration,
tx_count,
sustained,
} = config;
let nodes = discover_nodes(&network, num_nodes).unwrap_or_else(|err| {
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
exit(1);
});
if nodes.len() < num_nodes {
eprintln!(
"Error: Insufficient nodes discovered. Expecting {} or more",
num_nodes
);
exit(1);
}
let cluster_entrypoint = nodes[0].clone(); // Pick the first node, why not?
let client = create_client(cluster_entrypoint.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut barrier_client =
create_client(cluster_entrypoint.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut seed = [0u8; 32];
seed.copy_from_slice(&id.public_key_bytes()[..32]);
let mut rnd = GenKeys::new(seed);
println!("Creating {} keypairs...", tx_count * 2);
let mut total_keys = 0;
let mut target = tx_count * 2;
while target > 0 {
total_keys += target;
target /= MAX_SPENDS_PER_TX;
}
let gen_keypairs = rnd.gen_n_keypairs(total_keys as u64);
let barrier_source_keypair = Keypair::new();
let barrier_dest_id = Pubkey::new_rand();
println!("Get lamports...");
let num_lamports_per_account = 20;
// Sample the first keypair, see if it has lamports, if so then resume
// to avoid lamport loss
let keypair0_balance = client
.poll_get_balance(&gen_keypairs.last().unwrap().pubkey())
.unwrap_or(0);
if num_lamports_per_account > keypair0_balance {
let extra = num_lamports_per_account - keypair0_balance;
let total = extra * (gen_keypairs.len() as u64);
airdrop_lamports(&client, &drone_addr, &id, total);
println!("adding more lamports {}", extra);
fund_keys(&client, &id, &gen_keypairs, extra);
}
let start = gen_keypairs.len() - (tx_count * 2) as usize;
let keypairs = &gen_keypairs[start..];
airdrop_lamports(&barrier_client, &drone_addr, &barrier_source_keypair, 1);
println!("Get last ID...");
let mut blockhash = client.get_recent_blockhash().unwrap();
println!("Got last ID {:?}", blockhash);
let first_tx_count = client.get_transaction_count().expect("transaction count");
println!("Initial transaction count {}", first_tx_count);
let exit_signal = Arc::new(AtomicBool::new(false));
// Setup a thread per validator to sample every period
// collect the max transaction rate and total tx count seen
let maxes = Arc::new(RwLock::new(Vec::new()));
let sample_period = 1; // in seconds
println!("Sampling TPS every {} second...", sample_period);
let v_threads: Vec<_> = nodes
.into_iter()
.map(|v| {
let exit_signal = exit_signal.clone();
let maxes = maxes.clone();
Builder::new()
.name("solana-client-sample".to_string())
.spawn(move || {
sample_tx_count(&exit_signal, &maxes, first_tx_count, &v, sample_period);
})
.unwrap()
})
.collect();
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0));
let total_tx_sent_count = Arc::new(AtomicUsize::new(0));
let s_threads: Vec<_> = (0..threads)
.map(|_| {
let exit_signal = exit_signal.clone();
let shared_txs = shared_txs.clone();
let cluster_entrypoint = cluster_entrypoint.clone();
let shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
let total_tx_sent_count = total_tx_sent_count.clone();
Builder::new()
.name("solana-client-sender".to_string())
.spawn(move || {
do_tx_transfers(
&exit_signal,
&shared_txs,
&cluster_entrypoint,
&shared_tx_active_thread_count,
&total_tx_sent_count,
thread_batch_sleep_ms,
);
})
.unwrap()
})
.collect();
// generate and send transactions for the specified duration
let start = Instant::now();
let mut reclaim_lamports_back_to_source_account = false;
let mut i = keypair0_balance;
while start.elapsed() < duration {
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
metrics_submit_lamport_balance(balance);
// ping-pong between source and destination accounts for each loop iteration
// this seems to be faster than trying to determine the balance of individual
// accounts
let len = tx_count as usize;
generate_txs(
&shared_txs,
&keypairs[..len],
&keypairs[len..],
threads,
reclaim_lamports_back_to_source_account,
&cluster_entrypoint,
);
// In sustained mode overlap the transfers with generation
// this has higher average performance but lower peak performance
// in tested environments.
if !sustained {
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
sleep(Duration::from_millis(100));
}
}
// It's not feasible (would take too much time) to confirm each of the `tx_count / 2`
// transactions sent by `generate_txs()` so instead send and confirm a single transaction
// to validate the network is still functional.
send_barrier_transaction(
&mut barrier_client,
&mut blockhash,
&barrier_source_keypair,
&barrier_dest_id,
);
i += 1;
if should_switch_directions(num_lamports_per_account, i) {
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
}
}
// Stop the sampling threads so it will collect the stats
exit_signal.store(true, Ordering::Relaxed);
println!("Waiting for validator threads...");
for t in v_threads {
if let Err(err) = t.join() {
println!(" join() failed with: {:?}", err);
}
}
// join the tx send threads
println!("Waiting for transmit threads...");
for t in s_threads {
if let Err(err) = t.join() {
println!(" join() failed with: {:?}", err);
}
}
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
metrics_submit_lamport_balance(balance);
compute_and_report_stats(
&maxes,
sample_period,
&start.elapsed(),
total_tx_sent_count.load(Ordering::Relaxed),
);
}
fn metrics_submit_lamport_balance(lamport_balance: u64) {
pub fn metrics_submit_lamport_balance(lamport_balance: u64) {
println!("Token balance: {}", lamport_balance);
solana_metrics::submit(
influxdb::Point::new("bench-tps")
@ -245,23 +45,23 @@ fn metrics_submit_lamport_balance(lamport_balance: u64) {
);
}
fn sample_tx_count(
pub fn sample_tx_count(
exit_signal: &Arc<AtomicBool>,
maxes: &Arc<RwLock<Vec<(SocketAddr, NodeStats)>>>,
first_tx_count: u64,
v: &ContactInfo,
sample_period: u64,
) {
let client = create_client(v.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut client = create_client(v.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut now = Instant::now();
let mut initial_tx_count = client.get_transaction_count().expect("transaction count");
let mut initial_tx_count = client.transaction_count();
let mut max_tps = 0.0;
let mut total;
let log_prefix = format!("{:21}:", v.tpu.to_string());
loop {
let tx_count = client.get_transaction_count().expect("transaction count");
let tx_count = client.transaction_count();
assert!(
tx_count >= initial_tx_count,
"expected tx_count({}) >= initial_tx_count({})",
@ -302,7 +102,7 @@ fn sample_tx_count(
}
/// Send loopback payment of 0 lamports and confirm the network processed it
fn send_barrier_transaction(
pub fn send_barrier_transaction(
barrier_client: &mut ThinClient,
blockhash: &mut Hash,
source_keypair: &Keypair,
@ -319,12 +119,9 @@ fn send_barrier_transaction(
);
}
*blockhash = barrier_client.get_recent_blockhash().unwrap();
let transaction =
system_transaction::create_user_account(&source_keypair, dest_id, 0, *blockhash, 0);
*blockhash = barrier_client.get_recent_blockhash();
let signature = barrier_client
.async_send_transaction(transaction)
.transfer(0, &source_keypair, dest_id, blockhash)
.expect("Unable to send barrier transaction");
let confirmatiom = barrier_client.poll_for_signature(&signature);
@ -364,7 +161,7 @@ fn send_barrier_transaction(
exit(1);
}
let new_blockhash = barrier_client.get_recent_blockhash().unwrap();
let new_blockhash = barrier_client.get_recent_blockhash();
if new_blockhash == *blockhash {
if poll_count > 0 && poll_count % 8 == 0 {
println!("blockhash is not advancing, still at {:?}", *blockhash);
@ -377,7 +174,7 @@ fn send_barrier_transaction(
}
}
fn generate_txs(
pub fn generate_txs(
shared_txs: &SharedTransactions,
source: &[Keypair],
dest: &[Keypair],
@ -385,8 +182,8 @@ fn generate_txs(
reclaim: bool,
contact_info: &ContactInfo,
) {
let client = create_client(contact_info.client_facing_addr(), FULLNODE_PORT_RANGE);
let blockhash = client.get_recent_blockhash().unwrap();
let mut client = create_client(contact_info.client_facing_addr(), FULLNODE_PORT_RANGE);
let blockhash = client.get_recent_blockhash();
let tx_count = source.len();
println!("Signing transactions... {} (reclaim={})", tx_count, reclaim);
let signing_start = Instant::now();
@ -400,7 +197,7 @@ fn generate_txs(
.par_iter()
.map(|(id, keypair)| {
(
system_transaction::create_user_account(id, &keypair.pubkey(), 1, blockhash, 0),
SystemTransaction::new_account(id, &keypair.pubkey(), 1, blockhash, 0),
timestamp(),
)
})
@ -437,7 +234,7 @@ fn generate_txs(
}
}
fn do_tx_transfers(
pub fn do_tx_transfers(
exit_signal: &Arc<AtomicBool>,
shared_txs: &SharedTransactions,
contact_info: &ContactInfo,
@ -469,7 +266,7 @@ fn do_tx_transfers(
if now > tx.1 && now - tx.1 > 1000 * 30 {
continue;
}
client.async_send_transaction(tx.0).unwrap();
client.transfer_signed(&tx.0).unwrap();
}
shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed);
total_tx_sent_count.fetch_add(tx_len, Ordering::Relaxed);
@ -495,8 +292,8 @@ fn do_tx_transfers(
}
}
fn verify_funding_transfer(client: &ThinClient, tx: &Transaction, amount: u64) -> bool {
for a in &tx.message().account_keys[1..] {
pub fn verify_funding_transfer(client: &mut ThinClient, tx: &Transaction, amount: u64) -> bool {
for a in &tx.account_keys[1..] {
if client.get_balance(a).unwrap_or(0) >= amount {
return true;
}
@ -508,7 +305,7 @@ fn verify_funding_transfer(client: &ThinClient, tx: &Transaction, amount: u64) -
/// fund the dests keys by spending all of the source keys into MAX_SPENDS_PER_TX
/// on every iteration. This allows us to replay the transfers because the source is either empty,
/// or full
fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports: u64) {
pub fn fund_keys(client: &mut ThinClient, source: &Keypair, dests: &[Keypair], lamports: u64) {
let total = lamports * dests.len() as u64;
let mut funded: Vec<(&Keypair, u64)> = vec![(source, total)];
let mut notfunded: Vec<&Keypair> = dests.iter().collect();
@ -552,10 +349,7 @@ fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports:
.map(|(k, m)| {
(
k.clone(),
Transaction::new_unsigned_instructions(system_instruction::transfer_many(
&k.pubkey(),
&m,
)),
SystemTransaction::new_move_many(k, &m, Hash::default(), 0),
)
})
.collect();
@ -565,7 +359,7 @@ fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports:
while !to_fund_txs.is_empty() {
let receivers = to_fund_txs
.iter()
.fold(0, |len, (_, tx)| len + tx.message().instructions.len());
.fold(0, |len, (_, tx)| len + tx.instructions.len());
println!(
"{} {} to {} in {} txs",
@ -579,7 +373,7 @@ fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports:
to_fund_txs.len(),
);
let blockhash = client.get_recent_blockhash().unwrap();
let blockhash = client.get_recent_blockhash();
// re-sign retained to_fund_txes with updated blockhash
to_fund_txs.par_iter_mut().for_each(|(k, tx)| {
@ -587,19 +381,13 @@ fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports:
});
to_fund_txs.iter().for_each(|(_, tx)| {
client.async_send_transaction(tx.clone()).expect("transfer");
client.transfer_signed(&tx).expect("transfer");
});
// retry anything that seems to have dropped through cracks
// again since these txs are all or nothing, they're fine to
// retry
for _ in 0..10 {
to_fund_txs.retain(|(_, tx)| !verify_funding_transfer(client, &tx, amount));
if to_fund_txs.is_empty() {
break;
}
sleep(Duration::from_millis(100));
}
to_fund_txs.retain(|(_, tx)| !verify_funding_transfer(client, &tx, amount));
tries += 1;
}
@ -610,7 +398,12 @@ fn fund_keys(client: &ThinClient, source: &Keypair, dests: &[Keypair], lamports:
}
}
fn airdrop_lamports(client: &ThinClient, drone_addr: &SocketAddr, id: &Keypair, tx_count: u64) {
pub fn airdrop_lamports(
client: &mut ThinClient,
drone_addr: &SocketAddr,
id: &Keypair,
tx_count: u64,
) {
let starting_balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
metrics_submit_lamport_balance(starting_balance);
println!("starting balance {}", starting_balance);
@ -624,10 +417,10 @@ fn airdrop_lamports(client: &ThinClient, drone_addr: &SocketAddr, id: &Keypair,
id.pubkey(),
);
let blockhash = client.get_recent_blockhash().unwrap();
let blockhash = client.get_recent_blockhash();
match request_airdrop_transaction(&drone_addr, &id.pubkey(), airdrop_amount, blockhash) {
Ok(transaction) => {
let signature = client.async_send_transaction(transaction).unwrap();
let signature = client.transfer_signed(&transaction).unwrap();
client.poll_for_signature(&signature).unwrap();
}
Err(err) => {
@ -657,7 +450,7 @@ fn airdrop_lamports(client: &ThinClient, drone_addr: &SocketAddr, id: &Keypair,
}
}
fn compute_and_report_stats(
pub fn compute_and_report_stats(
maxes: &Arc<RwLock<Vec<(SocketAddr, NodeStats)>>>,
sample_period: u64,
tx_send_elapsed: &Duration,
@ -724,18 +517,13 @@ fn compute_and_report_stats(
// First transfer 3/4 of the lamports to the dest accounts
// then ping-pong 1/4 of the lamports back to the other account
// this leaves 1/4 lamport buffer in each account
fn should_switch_directions(num_lamports_per_account: u64, i: u64) -> bool {
pub fn should_switch_directions(num_lamports_per_account: u64, i: u64) -> bool {
i % (num_lamports_per_account / 4) == 0 && (i >= (3 * num_lamports_per_account) / 4)
}
#[cfg(test)]
mod tests {
use super::*;
use solana::fullnode::FullnodeConfig;
use solana::local_cluster::{ClusterConfig, LocalCluster};
use solana_drone::drone::run_local_drone;
use std::sync::mpsc::channel;
#[test]
fn test_switch_directions() {
assert_eq!(should_switch_directions(20, 0), false);
@ -750,33 +538,4 @@ mod tests {
assert_eq!(should_switch_directions(20, 100), true);
assert_eq!(should_switch_directions(20, 101), false);
}
#[test]
#[ignore]
fn test_bench_tps() {
let fullnode_config = FullnodeConfig::default();
const NUM_NODES: usize = 1;
let cluster = LocalCluster::new(&ClusterConfig {
node_stakes: vec![999_990; NUM_NODES],
cluster_lamports: 2_000_000,
fullnode_config,
..ClusterConfig::default()
});
let drone_keypair = Keypair::new();
cluster.transfer(&cluster.funding_keypair, &drone_keypair.pubkey(), 1_000_000);
let (addr_sender, addr_receiver) = channel();
run_local_drone(drone_keypair, addr_sender, None);
let drone_addr = addr_receiver.recv_timeout(Duration::from_secs(2)).unwrap();
let mut cfg = Config::default();
cfg.network_addr = cluster.entry_point_info.gossip;
cfg.drone_addr = drone_addr;
cfg.tx_count = 100;
cfg.duration = Duration::from_secs(5);
cfg.num_nodes = NUM_NODES;
do_bench_tps(cfg);
}
}

View File

@ -2,7 +2,7 @@ use std::net::SocketAddr;
use std::process::exit;
use std::time::Duration;
use clap::{crate_description, crate_name, crate_version, App, Arg, ArgMatches};
use clap::{crate_version, App, Arg, ArgMatches};
use solana_drone::drone::DRONE_PORT;
use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil};
@ -17,6 +17,8 @@ pub struct Config {
pub tx_count: usize,
pub thread_batch_sleep_ms: usize,
pub sustained: bool,
pub reject_extra_nodes: bool,
pub converge_only: bool,
}
impl Default for Config {
@ -31,13 +33,15 @@ impl Default for Config {
tx_count: 500_000,
thread_batch_sleep_ms: 0,
sustained: false,
reject_extra_nodes: false,
converge_only: false,
}
}
}
/// Defines and builds the CLI args for a run of the benchmark
pub fn build_args<'a, 'b>() -> App<'a, 'b> {
App::new(crate_name!()).about(crate_description!())
App::new("solana-bench-tps")
.version(crate_version!())
.arg(
Arg::with_name("network")
@ -71,6 +75,11 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
.takes_value(true)
.help("Wait for NUM nodes to converge"),
)
.arg(
Arg::with_name("reject-extra-nodes")
.long("reject-extra-nodes")
.help("Require exactly `num-nodes` on convergence. Appropriate only for internal networks"),
)
.arg(
Arg::with_name("threads")
.short("t")
@ -86,6 +95,11 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> {
.takes_value(true)
.help("Seconds to run benchmark, then exit; default is forever"),
)
.arg(
Arg::with_name("converge-only")
.long("converge-only")
.help("Exit immediately after converging"),
)
.arg(
Arg::with_name("sustained")
.long("sustained")
@ -117,14 +131,14 @@ pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
let mut args = Config::default();
if let Some(addr) = matches.value_of("network") {
args.network_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
eprintln!("failed to parse network address: {}", e);
args.network_addr = addr.parse().unwrap_or_else(|e| {
eprintln!("failed to parse network: {}", e);
exit(1)
});
}
if let Some(addr) = matches.value_of("drone") {
args.drone_addr = solana_netutil::parse_host_port(addr).unwrap_or_else(|e| {
args.drone_addr = addr.parse().unwrap_or_else(|e| {
eprintln!("failed to parse drone address: {}", e);
exit(1)
});
@ -162,6 +176,8 @@ pub fn extract_args<'a>(matches: &ArgMatches<'a>) -> Config {
}
args.sustained = matches.is_present("sustained");
args.converge_only = matches.is_present("converge-only");
args.reject_extra_nodes = matches.is_present("reject-extra-nodes");
args
}

View File

@ -1,7 +1,21 @@
mod bench;
mod cli;
use crate::bench::do_bench_tps;
use crate::bench::*;
use solana::cluster_info::FULLNODE_PORT_RANGE;
use solana::gen_keys::GenKeys;
use solana::gossip_service::discover;
use solana_client::client::create_client;
use solana_metrics;
use solana_sdk::signature::{Keypair, KeypairUtil};
use std::collections::VecDeque;
use std::process::exit;
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use std::thread::sleep;
use std::thread::Builder;
use std::time::Duration;
use std::time::Instant;
fn main() {
solana_logger::setup();
@ -11,5 +25,227 @@ fn main() {
let cfg = cli::extract_args(&matches);
do_bench_tps(cfg);
let cli::Config {
network_addr: network,
drone_addr,
id,
threads,
thread_batch_sleep_ms,
num_nodes,
duration,
tx_count,
sustained,
reject_extra_nodes,
converge_only,
} = cfg;
let nodes = discover(&network, num_nodes).unwrap_or_else(|err| {
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
exit(1);
});
if nodes.len() < num_nodes {
eprintln!(
"Error: Insufficient nodes discovered. Expecting {} or more",
num_nodes
);
exit(1);
}
if reject_extra_nodes && nodes.len() > num_nodes {
eprintln!(
"Error: Extra nodes discovered. Expecting exactly {}",
num_nodes
);
exit(1);
}
if converge_only {
return;
}
let cluster_entrypoint = nodes[0].clone(); // Pick the first node, why not?
let mut client = create_client(cluster_entrypoint.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut barrier_client =
create_client(cluster_entrypoint.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut seed = [0u8; 32];
seed.copy_from_slice(&id.public_key_bytes()[..32]);
let mut rnd = GenKeys::new(seed);
println!("Creating {} keypairs...", tx_count * 2);
let mut total_keys = 0;
let mut target = tx_count * 2;
while target > 0 {
total_keys += target;
target /= MAX_SPENDS_PER_TX;
}
let gen_keypairs = rnd.gen_n_keypairs(total_keys as u64);
let barrier_source_keypair = Keypair::new();
let barrier_dest_id = Keypair::new().pubkey();
println!("Get lamports...");
let num_lamports_per_account = 20;
// Sample the first keypair, see if it has lamports, if so then resume
// to avoid lamport loss
let keypair0_balance = client
.poll_get_balance(&gen_keypairs.last().unwrap().pubkey())
.unwrap_or(0);
if num_lamports_per_account > keypair0_balance {
let extra = num_lamports_per_account - keypair0_balance;
let total = extra * (gen_keypairs.len() as u64);
airdrop_lamports(&mut client, &drone_addr, &id, total);
println!("adding more lamports {}", extra);
fund_keys(&mut client, &id, &gen_keypairs, extra);
}
let start = gen_keypairs.len() - (tx_count * 2) as usize;
let keypairs = &gen_keypairs[start..];
airdrop_lamports(&mut barrier_client, &drone_addr, &barrier_source_keypair, 1);
println!("Get last ID...");
let mut blockhash = client.get_recent_blockhash();
println!("Got last ID {:?}", blockhash);
let first_tx_count = client.transaction_count();
println!("Initial transaction count {}", first_tx_count);
let exit_signal = Arc::new(AtomicBool::new(false));
// Setup a thread per validator to sample every period
// collect the max transaction rate and total tx count seen
let maxes = Arc::new(RwLock::new(Vec::new()));
let sample_period = 1; // in seconds
println!("Sampling TPS every {} second...", sample_period);
let v_threads: Vec<_> = nodes
.into_iter()
.map(|v| {
let exit_signal = exit_signal.clone();
let maxes = maxes.clone();
Builder::new()
.name("solana-client-sample".to_string())
.spawn(move || {
sample_tx_count(&exit_signal, &maxes, first_tx_count, &v, sample_period);
})
.unwrap()
})
.collect();
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0));
let total_tx_sent_count = Arc::new(AtomicUsize::new(0));
let s_threads: Vec<_> = (0..threads)
.map(|_| {
let exit_signal = exit_signal.clone();
let shared_txs = shared_txs.clone();
let cluster_entrypoint = cluster_entrypoint.clone();
let shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
let total_tx_sent_count = total_tx_sent_count.clone();
Builder::new()
.name("solana-client-sender".to_string())
.spawn(move || {
do_tx_transfers(
&exit_signal,
&shared_txs,
&cluster_entrypoint,
&shared_tx_active_thread_count,
&total_tx_sent_count,
thread_batch_sleep_ms,
);
})
.unwrap()
})
.collect();
// generate and send transactions for the specified duration
let start = Instant::now();
let mut reclaim_lamports_back_to_source_account = false;
let mut i = keypair0_balance;
while start.elapsed() < duration {
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
metrics_submit_lamport_balance(balance);
// ping-pong between source and destination accounts for each loop iteration
// this seems to be faster than trying to determine the balance of individual
// accounts
let len = tx_count as usize;
generate_txs(
&shared_txs,
&keypairs[..len],
&keypairs[len..],
threads,
reclaim_lamports_back_to_source_account,
&cluster_entrypoint,
);
// In sustained mode overlap the transfers with generation
// this has higher average performance but lower peak performance
// in tested environments.
if !sustained {
while shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 {
sleep(Duration::from_millis(100));
}
}
// It's not feasible (would take too much time) to confirm each of the `tx_count / 2`
// transactions sent by `generate_txs()` so instead send and confirm a single transaction
// to validate the network is still functional.
send_barrier_transaction(
&mut barrier_client,
&mut blockhash,
&barrier_source_keypair,
&barrier_dest_id,
);
i += 1;
if should_switch_directions(num_lamports_per_account, i) {
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
}
}
// Stop the sampling threads so it will collect the stats
exit_signal.store(true, Ordering::Relaxed);
println!("Waiting for validator threads...");
for t in v_threads {
if let Err(err) = t.join() {
println!(" join() failed with: {:?}", err);
}
}
// join the tx send threads
println!("Waiting for transmit threads...");
for t in s_threads {
if let Err(err) = t.join() {
println!(" join() failed with: {:?}", err);
}
}
let balance = client.poll_get_balance(&id.pubkey()).unwrap_or(0);
metrics_submit_lamport_balance(balance);
compute_and_report_stats(
&maxes,
sample_period,
&start.elapsed(),
total_tx_sent_count.load(Ordering::Relaxed),
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_switch_directions() {
assert_eq!(should_switch_directions(20, 0), false);
assert_eq!(should_switch_directions(20, 1), false);
assert_eq!(should_switch_directions(20, 14), false);
assert_eq!(should_switch_directions(20, 15), true);
assert_eq!(should_switch_directions(20, 16), false);
assert_eq!(should_switch_directions(20, 19), false);
assert_eq!(should_switch_directions(20, 20), true);
assert_eq!(should_switch_directions(20, 21), false);
assert_eq!(should_switch_directions(20, 99), false);
assert_eq!(should_switch_directions(20, 100), true);
assert_eq!(should_switch_directions(20, 101), false);
}
}

248
benches/append_vec.rs Normal file
View File

@ -0,0 +1,248 @@
#![feature(test)]
extern crate rand;
extern crate test;
use bincode::{deserialize, serialize_into, serialized_size};
use rand::{thread_rng, Rng};
use solana_runtime::append_vec::{
deserialize_account, get_serialized_size, serialize_account, AppendVec,
};
use solana_sdk::account::Account;
use solana_sdk::signature::{Keypair, KeypairUtil};
use std::env;
use std::io::Cursor;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use std::thread::spawn;
use test::Bencher;
const START_SIZE: u64 = 4 * 1024 * 1024;
const INC_SIZE: u64 = 1 * 1024 * 1024;
macro_rules! align_up {
($addr: expr, $align: expr) => {
($addr + ($align - 1)) & !($align - 1)
};
}
fn get_append_vec_bench_path(path: &str) -> PathBuf {
let out_dir = env::var("OUT_DIR").unwrap_or_else(|_| "target".to_string());
let mut buf = PathBuf::new();
buf.push(&format!("{}/{}", out_dir, path));
buf
}
#[bench]
fn append_vec_atomic_append(bencher: &mut Bencher) {
let path = get_append_vec_bench_path("bench_append");
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
bencher.iter(|| {
if vec.append(AtomicUsize::new(0)).is_none() {
assert!(vec.grow_file().is_ok());
assert!(vec.append(AtomicUsize::new(0)).is_some());
}
});
std::fs::remove_file(path).unwrap();
}
#[bench]
fn append_vec_atomic_random_access(bencher: &mut Bencher) {
let path = get_append_vec_bench_path("bench_ra");
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
let size = 1_000_000;
for _ in 0..size {
if vec.append(AtomicUsize::new(0)).is_none() {
assert!(vec.grow_file().is_ok());
assert!(vec.append(AtomicUsize::new(0)).is_some());
}
}
bencher.iter(|| {
let index = thread_rng().gen_range(0, size as u64);
vec.get(index * std::mem::size_of::<AtomicUsize>() as u64);
});
std::fs::remove_file(path).unwrap();
}
#[bench]
fn append_vec_atomic_random_change(bencher: &mut Bencher) {
let path = get_append_vec_bench_path("bench_rax");
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
let size = 1_000_000;
for k in 0..size {
if vec.append(AtomicUsize::new(k)).is_none() {
assert!(vec.grow_file().is_ok());
assert!(vec.append(AtomicUsize::new(k)).is_some());
}
}
bencher.iter(|| {
let index = thread_rng().gen_range(0, size as u64);
let atomic1 = vec.get(index * std::mem::size_of::<AtomicUsize>() as u64);
let current1 = atomic1.load(Ordering::Relaxed);
assert_eq!(current1, index as usize);
let next = current1 + 1;
let mut index = vec.append(AtomicUsize::new(next));
if index.is_none() {
assert!(vec.grow_file().is_ok());
index = vec.append(AtomicUsize::new(next));
}
let atomic2 = vec.get(index.unwrap());
let current2 = atomic2.load(Ordering::Relaxed);
assert_eq!(current2, next);
});
std::fs::remove_file(path).unwrap();
}
#[bench]
fn append_vec_atomic_random_read(bencher: &mut Bencher) {
let path = get_append_vec_bench_path("bench_read");
let mut vec = AppendVec::<AtomicUsize>::new(&path, true, START_SIZE, INC_SIZE);
let size = 1_000_000;
for _ in 0..size {
if vec.append(AtomicUsize::new(0)).is_none() {
assert!(vec.grow_file().is_ok());
assert!(vec.append(AtomicUsize::new(0)).is_some());
}
}
bencher.iter(|| {
let index = thread_rng().gen_range(0, size);
let atomic1 = vec.get((index * std::mem::size_of::<AtomicUsize>()) as u64);
let current1 = atomic1.load(Ordering::Relaxed);
assert_eq!(current1, 0);
});
std::fs::remove_file(path).unwrap();
}
#[bench]
fn append_vec_concurrent_lock_append(bencher: &mut Bencher) {
let path = get_append_vec_bench_path("bench_lock_append");
let vec = Arc::new(RwLock::new(AppendVec::<AtomicUsize>::new(
&path, true, START_SIZE, INC_SIZE,
)));
let vec1 = vec.clone();
let size = 1_000_000;
let count = Arc::new(AtomicUsize::new(0));
let count1 = count.clone();
spawn(move || loop {
let mut len = count.load(Ordering::Relaxed);
{
let rlock = vec1.read().unwrap();
loop {
if rlock.append(AtomicUsize::new(0)).is_none() {
break;
}
len = count.fetch_add(1, Ordering::Relaxed);
}
if len >= size {
break;
}
}
{
let mut wlock = vec1.write().unwrap();
if len >= size {
break;
}
assert!(wlock.grow_file().is_ok());
}
});
bencher.iter(|| {
let _rlock = vec.read().unwrap();
let len = count1.load(Ordering::Relaxed);
assert!(len < size * 2);
});
std::fs::remove_file(path).unwrap();
}
#[bench]
fn append_vec_concurrent_get_append(bencher: &mut Bencher) {
let path = get_append_vec_bench_path("bench_get_append");
let vec = Arc::new(RwLock::new(AppendVec::<AtomicUsize>::new(
&path, true, START_SIZE, INC_SIZE,
)));
let vec1 = vec.clone();
let size = 1_000_000;
let count = Arc::new(AtomicUsize::new(0));
let count1 = count.clone();
spawn(move || loop {
let mut len = count.load(Ordering::Relaxed);
{
let rlock = vec1.read().unwrap();
loop {
if rlock.append(AtomicUsize::new(0)).is_none() {
break;
}
len = count.fetch_add(1, Ordering::Relaxed);
}
if len >= size {
break;
}
}
{
let mut wlock = vec1.write().unwrap();
if len >= size {
break;
}
assert!(wlock.grow_file().is_ok());
}
});
bencher.iter(|| {
let rlock = vec.read().unwrap();
let len = count1.load(Ordering::Relaxed);
if len > 0 {
let index = thread_rng().gen_range(0, len);
rlock.get((index * std::mem::size_of::<AtomicUsize>()) as u64);
}
});
std::fs::remove_file(path).unwrap();
}
#[bench]
fn bench_account_serialize(bencher: &mut Bencher) {
let num: usize = 1000;
let account = Account::new(2, 100, &Keypair::new().pubkey());
let len = get_serialized_size(&account);
let ser_len = align_up!(len + std::mem::size_of::<u64>(), std::mem::size_of::<u64>());
let mut memory = vec![0; num * ser_len];
bencher.iter(|| {
for i in 0..num {
let start = i * ser_len;
serialize_account(&mut memory[start..start + ser_len], &account, len);
}
});
// make sure compiler doesn't delete the code.
let index = thread_rng().gen_range(0, num);
if memory[index] != 0 {
println!("memory: {}", memory[index]);
}
let start = index * ser_len;
let new_account = deserialize_account(&memory[start..start + ser_len], 0, num * len).unwrap();
assert_eq!(new_account, account);
}
#[bench]
fn bench_account_serialize_bincode(bencher: &mut Bencher) {
let num: usize = 1000;
let account = Account::new(2, 100, &Keypair::new().pubkey());
let len = serialized_size(&account).unwrap() as usize;
let mut memory = vec![0u8; num * len];
bencher.iter(|| {
for i in 0..num {
let start = i * len;
let cursor = Cursor::new(&mut memory[start..start + len]);
serialize_into(cursor, &account).unwrap();
}
});
// make sure compiler doesn't delete the code.
let index = thread_rng().gen_range(0, len);
if memory[index] != 0 {
println!("memory: {}", memory[index]);
}
let start = index * len;
let new_account: Account = deserialize(&memory[start..start + len]).unwrap();
assert_eq!(new_account, account);
}

View File

@ -1,13 +1,10 @@
#![feature(test)]
extern crate test;
#[macro_use]
extern crate solana;
use rand::{thread_rng, Rng};
use rayon::prelude::*;
use solana::banking_stage::{create_test_recorder, BankingStage};
use solana::blocktree::{get_tmp_ledger_path, Blocktree};
use solana::cluster_info::ClusterInfo;
use solana::cluster_info::Node;
use solana::packet::to_packets_chunked;
@ -18,7 +15,7 @@ use solana_sdk::genesis_block::GenesisBlock;
use solana_sdk::hash::hash;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{KeypairUtil, Signature};
use solana_sdk::system_transaction;
use solana_sdk::system_transaction::SystemTransaction;
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, MAX_RECENT_BLOCKHASHES};
use std::iter;
use std::sync::atomic::Ordering;
@ -48,7 +45,7 @@ fn check_txs(receiver: &Receiver<WorkingBankEntries>, ref_tx_count: usize) {
#[bench]
#[ignore]
fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
let num_threads = BankingStage::num_threads() as usize;
let num_threads = 4;
// a multiple of packet chunk 2X duplicates to avoid races
let txes = 192 * 50 * num_threads * 2;
let mint_total = 1_000_000_000_000;
@ -56,7 +53,7 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
let (verified_sender, verified_receiver) = channel();
let bank = Arc::new(Bank::new(&genesis_block));
let dummy = system_transaction::transfer(
let dummy = SystemTransaction::new_move(
&mint_keypair,
&mint_keypair.pubkey(),
1,
@ -70,17 +67,17 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
let from: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
let to: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
new.message.account_keys[0] = Pubkey::new(&from[0..32]);
new.message.account_keys[1] = Pubkey::new(&to[0..32]);
new.account_keys[0] = Pubkey::new(&from[0..32]);
new.account_keys[1] = Pubkey::new(&to[0..32]);
new.signatures = vec![Signature::new(&sig[0..64])];
new
})
.collect();
// fund all the accounts
transactions.iter().for_each(|tx| {
let fund = system_transaction::transfer(
let fund = SystemTransaction::new_move(
&mint_keypair,
&tx.message.account_keys[0],
&tx.account_keys[0],
mint_total / txes as u64,
genesis_block.hash(),
0,
@ -107,48 +104,40 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
(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);
poh_recorder.lock().unwrap().set_bank(&bank);
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(&bank);
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
let cluster_info = Arc::new(RwLock::new(cluster_info));
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
poh_recorder.lock().unwrap().set_bank(&bank);
let mut id = genesis_block.hash();
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
id = hash(&id.as_ref());
bank.register_tick(&id);
}
let half_len = verified.len() / 2;
let mut start = 0;
bencher.iter(move || {
// make sure the transactions are still valid
bank.register_tick(&genesis_block.hash());
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
verified_sender.send(v.to_vec()).unwrap();
}
check_txs(&signal_receiver, txes / 2);
bank.clear_signatures();
start += half_len;
start %= verified.len();
});
exit.store(true, Ordering::Relaxed);
poh_service.join().unwrap();
let mut id = genesis_block.hash();
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
id = hash(&id.as_ref());
bank.register_tick(&id);
}
Blocktree::destroy(&ledger_path).unwrap();
let half_len = verified.len() / 2;
let mut start = 0;
bencher.iter(move || {
// make sure the transactions are still valid
bank.register_tick(&genesis_block.hash());
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
verified_sender.send(v.to_vec()).unwrap();
}
check_txs(&signal_receiver, txes / 2);
bank.clear_signatures();
start += half_len;
start %= verified.len();
});
exit.store(true, Ordering::Relaxed);
poh_service.join().unwrap();
}
#[bench]
#[ignore]
fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
let progs = 4;
let num_threads = BankingStage::num_threads() as usize;
let num_threads = 4;
// a multiple of packet chunk 2X duplicates to avoid races
let txes = 96 * 100 * num_threads * 2;
let mint_total = 1_000_000_000_000;
@ -156,7 +145,7 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
let (verified_sender, verified_receiver) = channel();
let bank = Arc::new(Bank::new(&genesis_block));
let dummy = system_transaction::transfer(
let dummy = SystemTransaction::new_move(
&mint_keypair,
&mint_keypair.pubkey(),
1,
@ -170,33 +159,33 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
let from: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
let to: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
new.message.account_keys[0] = Pubkey::new(&from[0..32]);
new.message.account_keys[1] = Pubkey::new(&to[0..32]);
let prog = new.message.instructions[0].clone();
new.account_keys[0] = Pubkey::new(&from[0..32]);
new.account_keys[1] = Pubkey::new(&to[0..32]);
let prog = new.instructions[0].clone();
for i in 1..progs {
//generate programs that spend to random keys
let to: Vec<u8> = (0..32).map(|_| thread_rng().gen()).collect();
let to_key = Pubkey::new(&to[0..32]);
new.message.account_keys.push(to_key);
assert_eq!(new.message.account_keys.len(), i + 2);
new.message.instructions.push(prog.clone());
assert_eq!(new.message.instructions.len(), i + 1);
new.message.instructions[i].accounts[1] = 1 + i as u8;
new.account_keys.push(to_key);
assert_eq!(new.account_keys.len(), i + 2);
new.instructions.push(prog.clone());
assert_eq!(new.instructions.len(), i + 1);
new.instructions[i].accounts[1] = 1 + i as u8;
assert_eq!(new.key(i, 1), Some(&to_key));
assert_eq!(
new.message.account_keys[new.message.instructions[i].accounts[1] as usize],
new.account_keys[new.instructions[i].accounts[1] as usize],
to_key
);
}
assert_eq!(new.message.instructions.len(), progs);
assert_eq!(new.instructions.len(), progs);
new.signatures = vec![Signature::new(&sig[0..64])];
new
})
.collect();
transactions.iter().for_each(|tx| {
let fund = system_transaction::transfer(
let fund = SystemTransaction::new_move(
&mint_keypair,
&tx.message.account_keys[0],
&tx.account_keys[0],
mint_total / txes as u64,
genesis_block.hash(),
0,
@ -222,40 +211,31 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
(x, iter::repeat(1).take(len).collect())
})
.collect();
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(&bank);
let cluster_info = ClusterInfo::new_with_invalid_keypair(Node::new_localhost().info);
let cluster_info = Arc::new(RwLock::new(cluster_info));
let _banking_stage = BankingStage::new(&cluster_info, &poh_recorder, verified_receiver);
poh_recorder.lock().unwrap().set_bank(&bank);
let 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);
poh_recorder.lock().unwrap().set_bank(&bank);
let mut id = genesis_block.hash();
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
id = hash(&id.as_ref());
bank.register_tick(&id);
}
let half_len = verified.len() / 2;
let mut start = 0;
bencher.iter(move || {
// make sure the transactions are still valid
bank.register_tick(&genesis_block.hash());
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
verified_sender.send(v.to_vec()).unwrap();
}
check_txs(&signal_receiver, txes / 2);
bank.clear_signatures();
start += half_len;
start %= verified.len();
});
exit.store(true, Ordering::Relaxed);
poh_service.join().unwrap();
let mut id = genesis_block.hash();
for _ in 0..(MAX_RECENT_BLOCKHASHES * DEFAULT_TICKS_PER_SLOT as usize) {
id = hash(&id.as_ref());
bank.register_tick(&id);
}
Blocktree::destroy(&ledger_path).unwrap();
let half_len = verified.len() / 2;
let mut start = 0;
bencher.iter(move || {
// make sure the transactions are still valid
bank.register_tick(&genesis_block.hash());
for v in verified[start..start + half_len].chunks(verified.len() / num_threads) {
verified_sender.send(v.to_vec()).unwrap();
}
check_txs(&signal_receiver, txes / 2);
bank.clear_signatures();
start += half_len;
start %= verified.len();
});
exit.store(true, Ordering::Relaxed);
poh_service.join().unwrap();
}

View File

@ -5,7 +5,7 @@ extern crate test;
use solana::entry::{next_entries, reconstruct_entries_from_blobs, EntrySlice};
use solana_sdk::hash::{hash, Hash};
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction;
use solana_sdk::system_transaction::SystemTransaction;
use test::Bencher;
#[bench]
@ -13,7 +13,7 @@ fn bench_block_to_blobs_to_block(bencher: &mut Bencher) {
let zero = Hash::default();
let one = hash(&zero.as_ref());
let keypair = Keypair::new();
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, one, 0);
let tx0 = SystemTransaction::new_move(&keypair, &keypair.pubkey(), 1, one, 0);
let transactions = vec![tx0; 10];
let entries = next_entries(&zero, 1, transactions);

1
book/.gitattributes vendored
View File

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

View File

@ -25,6 +25,6 @@
| | | | | | | Downstream | |
| | .--+--. .-------+---. | | | Validators | |
`-------->| TPU +---->| Broadcast +--------------->| | |
| `-----` | Stage | | | `------------` |
| `-----` | Service | | | `------------` |
| `-----------` | `------------------`
`--------------------------------------`

View File

@ -1,30 +0,0 @@
msc {
hscale="2.2";
VoteSigner,
Validator,
Cluster,
StakerX,
StakerY;
|||;
Validator box Validator [label="boot.."];
VoteSigner <:> Validator [label="register\n\n(optional)"];
Validator => Cluster [label="VoteState::Initialize(VoteSigner)"];
StakerX => Cluster [label="StakeState::Delegate(Validator)"];
StakerY => Cluster [label="StakeState::Delegate(Validator)"];
|||;
Validator box Cluster [label="\nvalidate\n"];
Validator => VoteSigner [label="sign(vote)"];
VoteSigner >> Validator [label="signed vote"];
Validator => Cluster [label="gossip(vote)"];
...;
... ;
Validator abox Validator [label="\nmax\nlockout\n"];
|||;
StakerX => Cluster [label="StakeState::RedeemCredits()"];
StakerY => Cluster [label="StakeState::RedeemCredits()"] ;
}

View File

@ -1,17 +1,16 @@
.-------------.
| PoH Service |
`--------+----`
^ |
.------------------------------|----|--------------------.
| TPU | v |
| .-------. .-----------. .-+-------. .-----------. | .------------.
.---------. | | Fetch | | SigVerify | | Banking | | Broadcast | | | Downstream |
| Clients |--->| Stage |->| Stage |->| Stage |->| Stage |---->| Validators |
`---------` | | | | | | | | | | | |
| `-------` `-----------` `----+----` `-----------` | `------------`
| | |
`---------------------------------|----------------------`
.-------------------------------------------.
| TPU .-------------. |
| | PoH Service | |
| `--------+----` |
| ^ | |
| | v |
| .-------. .-----------. .-+-------. | .------------.
.---------. | | Fetch | | SigVerify | | Banking | | | Broadcast |
| Clients |--->| Stage |->| Stage |->| Stage |------>| Service |
`---------` | | | | | | | | | |
| `-------` `-----------` `----+----` | `------------`
| | |
`---------------------------------|---------`
|
v
.------.

View File

@ -34,13 +34,14 @@
- [JavaScript API](javascript-api.md)
- [solana-wallet CLI](wallet.md)
- [Accepted Design Proposals](proposals.md)
- [Proposed Architectural Changes](proposals.md)
- [Ledger Replication](ledger-replication-to-implement.md)
- [Secure Vote Signing](vote-signing-to-implement.md)
- [Staking Rewards](staking-rewards.md)
- [Passive Stake Delegation and Rewards](passive-stake-delegation-and-rewards.md)
- [Fork Selection](fork-selection.md)
- [Reliable Vote Transmission](reliable-vote-transmission.md)
- [Persistent Account Storage](persistent-account-storage.md)
- [Leader to Leader Transition](leader-leader-transition.md)
- [Cluster Economics](ed_overview.md)
- [Validation-client Economics](ed_validation_client_economics.md)
- [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md)
@ -52,16 +53,7 @@
- [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md)
- [Economic Sustainability](ed_economic_sustainability.md)
- [Attack Vectors](ed_attack_vectors.md)
- [Economic Design MVP](ed_mvp.md)
- [References](ed_references.md)
- [Leader-to-Validator Transition](leader-validator-transition.md)
- [Cluster Test Framework](cluster-test-framework.md)
- [Testing Programs](testing-programs.md)
- [Credit-only Accounts](credit-only-credit-debit-accounts.md)
- [Cluster Software Installation and Updates](installer.md)
- [Deterministic Transaction Fees](transaction-fees.md)
- [Implemented Design Proposals](implemented-proposals.md)
- [Fork Selection](fork-selection.md)
- [Leader-to-Leader Transition](leader-leader-transition.md)
- [Leader-to-Validator Transition](leader-validator-transition.md)
- [Testnet Participation](testnet-participation.md)

View File

@ -51,10 +51,10 @@ At test start, the cluster has already been established and is fully connected.
The test can discover most of the available nodes over a few second.
```rust,ignore
use crate::gossip_service::discover_nodes;
use crate::gossip_service::discover;
// Discover the cluster over a few seconds.
let cluster_nodes = discover_nodes(&entry_point_info, num_nodes);
let cluster_nodes = discover(&entry_point_info, num_nodes);
```
## Cluster Configuration
@ -99,10 +99,10 @@ pub fn test_large_invalid_gossip_nodes(
funding_keypair: &Keypair,
num_nodes: usize,
) {
let cluster = discover_nodes(&entry_point_info, num_nodes);
let cluster = discover(&entry_point_info, num_nodes);
// Poison the cluster.
let client = create_client(entry_point_info.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut client = create_client(entry_point_info.client_facing_addr(), FULLNODE_PORT_RANGE);
for _ in 0..(num_nodes * 100) {
client.gossip_push(
cluster_info::invalid_contact_info()
@ -112,7 +112,7 @@ pub fn test_large_invalid_gossip_nodes(
// Force refresh of the active set.
for node in &cluster {
let client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
client.gossip_refresh_active_set();
}

View File

@ -1,140 +0,0 @@
# Credit-Only Accounts
This design covers the handling of credit-only and credit-debit accounts in the
[runtime](runtime.md). Accounts already distinguish themselves as credit-only or
credit-debit based on the program ID specified by the transaction's instruction.
Programs must treat accounts that are not owned by them as credit-only.
To identify credit-only accounts by program id would require the account to be
fetched and loaded from disk. This operation is expensive, and while it is
occurring, the runtime would have to reject any transactions referencing the same
account.
The proposal introduces a `num_readonly_accounts` field to the transaction
structure, and removes the `program_ids` dedicated vector for program accounts.
This design doesn't change the runtime transaction processing rules.
Programs still can't write or spend accounts that they do not own, but it
allows the runtime to optimistically take the correct lock for each account
specified in the transaction before loading the accounts from storage.
Accounts selected as credit-debit by the transaction can still be treated as
credit-only by the instructions.
## Runtime handling
credit-only accounts have the following properties:
* Can be deposited into: Deposits can be implemented as a simple `atomic_add`.
* read-only access to account data.
Instructions that debit or modify the credit-only account data will fail.
## Account Lock Optimizations
The Accounts module keeps track of current locked accounts in the runtime,
which separates credit-only accounts from the credit-debit accounts. The credit-only
accounts can be cached in memory and shared between all the threads executing
transactions.
The current runtime can't predict whether an account is credit-only or credit-debit when
the transaction account keys are locked at the start of the transaction
processing pipeline. Accounts referenced by the transaction have not been
loaded from the disk yet.
An ideal design would cache the credit-only accounts while they are referenced by
any transaction moving through the runtime, and release the cache when the last
transaction exits the runtime.
## Credit-only accounts and read-only account data
Credit-only account data can be treated as read-only. Credit-debit
account data is treated as read-write.
## Transaction changes
To enable the possibility of caching accounts only while they are in the
runtime, the Transaction structure should be changed in the following way:
* `program_ids: Vec<Pubkey>` - This vector is removed. Program keys can be
placed at the end of the `account_keys` vector within the `num_readonly_accounts`
number set to the number of programs.
* `num_readonly_accounts: u8` - The number of keys from the **end** of the
transaction's `account_keys` array that is credit-only.
The following possible accounts are present in an transaction:
* paying account
* RW accounts
* R accounts
* Program IDs
The paying account must be credit-debit, and program IDs must be credit-only. The
first account in the `account_keys` array is always the account that pays for
the transaction fee, therefore it cannot be credit-only. For these reasons the
credit-only accounts are all grouped together at the end of the `account_keys`
vector. Counting credit-only accounts from the end allow for the default `0`
value to still be functionally correct, since a transaction will succeed with
all credit-debit accounts.
Since accounts can only appear once in the transaction's `account_keys` array,
an account can only be credit-only or credit-debit in a single transaction, not
both. The runtime treats a transaction as one atomic unit of execution. If any
instruction needs credit-debit access to an account, a copy needs to be made. The
write lock is held for the entire time the transaction is being processed by
the runtime.
## Starvation
Read locks for credit-only accounts can keep the runtime from executing
transactions requesting a write lock to a credit-debit account.
When a request for a write lock is made while a read lock is open, the
transaction requesting the write lock should be cached. Upon closing the read
lock, the pending transactions can be pushed through the runtime.
While a pending write transaction exists, any additional read lock requests for
that account should fail. It follows that any other write lock requests will also
fail. Currently, clients must retransmit when a transaction fails because of
a pending transaction. This approach would mimic that behavior as closely as
possible while preventing write starvation.
## Program execution with credit-only accounts
Before handing off the accounts to program execution, the runtime can mark each
account in each instruction as a credit-only account. The credit-only accounts can
be passed as references without an extra copy. The transaction will abort on a
write to credit-only.
An alternative is to detect writes to credit-only accounts and fail the
transactions before commit.
## Alternative design
This design attempts to cache a credit-only account after loading without the use
of a transaction-specified credit-only accounts list. Instead, the credit-only
accounts are held in a reference-counted table inside the runtime as the
transactions are processed.
1. Transaction accounts are locked.
a. If the account is present in the credit-only' table, the TX does not fail.
The pending state for this TX is marked NeedReadLock.
2. Transaction accounts are loaded.
a. Transaction accounts that are credit-only increase their reference
count in the `credit-only` table.
b. Transaction accounts that need a write lock and are present in the
`credit-only` table fail.
3. Transaction accounts are unlocked.
a. Decrement the `credit-only` lock table reference count; remove if its 0
b. Remove from the `lock` set if the account is not in the `credit-only`
table.
The downside with this approach is that if the `lock` set mutex is released
between lock and load to allow better pipelining of transactions, a request for
a credit-only account may fail. Therefore, this approach is not suitable for
treating programs as credit-only accounts.
Holding the accounts lock mutex while fetching the account from disk would
potentially have a significant performance hit on the runtime. Fetching from
disk is expected to be slow, but can be parallelized between multiple disks.

View File

@ -1,12 +0,0 @@
## Proposed MVP of Economic Design
The preceeding sections, outlined in the [Economic Design Overview](ed_overview.md), describe a long-term vision of a sustainable Solana economy. Of course, we don't expect the final implementation to perfectly match what has been described above. We intend to fully engage with network stakeholders throughout the implementation phases (i.e. pre-testnet, testnet, mainnet) to ensure the system supports, and is representative of, the various network participants' interests. The first step toward this goal, however, is outlining a some desired MVP economic features to be available for early pre-testnet and testnet participants. Below is a rough sketch outlining basic economic functionality from which a more complete and functional system can be developed.
### MVP Economic Features
* Faucet to deliver testnet SOLs to validators for staking and dapp development.
* Mechanism by which validators are rewarded in proportion to their stake. Interest rate mechansism (i.e. to be determined by total % staked) to come later.
* Ability to delegate tokens to validator nodes.
* Replicators to receive fixed, arbitrary reward for submitting validated PoReps. Reward size mechanism (i.e. PoRep reward as a function of total ledger redundancy) to come later.
* Pooling of replicator PoRep transaction fees and weighted distribution to validators based on PoRep verification (see [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md). It will be useful to test this protection against attacks on testnet.
* Nice-to-have: auto-delegation of replicator rewards to validator.

View File

@ -8,7 +8,7 @@ These protocol-based rewards, to be distributed to participating validation and
Transaction fees are market-based participant-to-participant transfers, attached to network interactions as a necessary motivation and compensation for the inclusion and execution of a proposed transaction (be it a state execution or proof-of-replication verification). A mechanism for continuous and long-term funding of the mining pool through a pre-dedicated portion of transaction fees is also discussed below.
A high-level schematic of Solanas crypto-economic design is shown below in **Figure 1**. The specifics of validation-client economics are described in sections: [Validation-client Economics](ed_validation_client_economics.md), [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md), [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md) and [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md). Also, the chapter titled [Validation Stake Delegation](ed_vce_validation_stake_delegation.md) closes with a discussion of validator delegation opportunties and marketplace. The [Replication-client Economics](ed_replication_client_economics.md) chapter will review the Solana network design for global ledger storage/redundancy and replicator-client economics ([Storage-replication rewards](ed_rce_storage_replication_rewards.md)) along with a replicator-to-validator delegation mechanism designed to aide participant on-boarding into the Solana economy discussed in [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md). The [Economic Sustainability](ed_economic_sustainability.md) section dives deeper into Solanas design for long-term economic sustainability and outlines the constraints and conditions for a self-sustaining economy. An outline of features for an MVP economic design is discussed in the [Economic Design MVP](ed_mvp.md) section. Finally, in chapter [Attack Vectors](ed_attack_vectors.md), various attack vectors will be described and potential vulnerabilities explored and parameterized.
A high-level schematic of Solanas crypto-economic design is shown below in **Figure 1**. The specifics of validation-client economics are described in sections: [Validation-client Economics](ed_validation_client_economics.md), [State-validation Protocol-based Rewards](ed_vce_state_validation_protocol_based_rewards.md), [State-validation Transaction Fees](ed_vce_state_validation_transaction_fees.md) and [Replication-validation Transaction Fees](ed_vce_replication_validation_transaction_fees.md). Also, the chapter titled [Validation Stake Delegation](ed_vce_validation_stake_delegation.md) closes with a discussion of validator delegation opportunties and marketplace. The [Replication-client Economics](ed_replication_client_economics.md) chapter will review the Solana network design for global ledger storage/redundancy and replicator-client economics ([Storage-replication rewards](ed_rce_storage_replication_rewards.md)) along with a replicator-to-validator delegation mechanism designed to aide participant on-boarding into the Solana economy discussed in [Replication-client Reward Auto-delegation](ed_rce_replication_client_reward_auto_delegation.md). The [Economic Sustainability](ed_economic_sustainability.md) section dives deeper into Solanas design for long-term economic sustainability and outlines the constraints and conditions for a self-sustaining economy. Finally, in chapter [Attack Vectors](ed_attack_vectors.md), various attack vectors will be described and potential vulnerabilities explored and parameterized.
<!-- ![img alt text](solana_economic_design.png) -->
<p style="text-align:center;"><img src="img/solana_economic_design.png" alt="== Solana Economic Design Diagram ==" width="800"/></p>

View File

@ -161,7 +161,7 @@ This will dump all the threads stack traces into gdb.txt
In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`.
```bash
$ ./multinode-demo/client.sh --network testnet.solana.com:8001 --duration 60
$ ./multinode-demo/client.sh --network $(dig +short testnet.solana.com):8001 --duration 60
```
You can observe the effects of your client's transactions on our [dashboard](https://metrics.solana.com:3000/d/testnet/testnet-hud?orgId=2&from=now-30m&to=now&refresh=5s&var-testnet=testnet)

View File

@ -1,3 +0,0 @@
# Implemented Design Proposals
The following design proposals are fully implemented.

View File

@ -1,213 +0,0 @@
## Cluster Software Installation and Updates
Currently users are required to build the solana cluster software themselves
from the git repository and manually update it, which is error prone and
inconvenient.
This document proposes an easy to use software install and updater that can be
used to deploy pre-built binaries for supported platforms. Users may elect to
use binaries supplied by Solana or any other party they trust. Deployment of
updates is managed using an on-chain update manifest program.
### Motivating Examples
#### Fetch and run a pre-built installer using a bootstrap curl/shell script
The easiest install method for supported platforms:
```bash
$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.13.0/install/solana-install-init.sh | sh
```
This script will check github for the latest tagged release and download and run the
`solana-install` binary from there.
If additional arguments need to be specified during the installation, the
following shell syntax is used:
```bash
$ init_args=.... # arguments for `solana-installer init ...`
$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.13.0/install/solana-install-init.sh | sh -s - ${init_args}
```
#### Fetch and run a pre-built installer from a Github release
With a well-known release URL, a pre-built binary can be obtained for supported
platforms:
```bash
$ curl -o solana-install https://github.com/solana-labs/solana/releases/download/v0.13.0/solana-install-x86_64-apple-darwin
$ chmod +x ./solana-install
$ ./solana-install --help
```
#### Build and run the installer from source
If a pre-built binary is not available for a given platform, building the
installer from source is always an option:
```bash
$ git clone https://github.com/solana-labs/solana.git
$ cd solana/install
$ cargo run -- --help
```
#### Deploy a new update to a cluster
Given a solana release tarball (as created by `ci/publish-tarball.sh`) that has already been uploaded to a publicly accessible URL,
the following commands will deploy the update:
```bash
$ solana-keygen -o update-manifest.json # <-- only generated once, the public key is shared with users
$ solana-install deploy http://example.com/path/to/solana-release.tar.bz2 update-manifest.json
```
#### Run a validator node that auto updates itself
```bash
$ solana-install init --pubkey 92DMonmBYXwEMHJ99c9ceRSpAmk9v6i3RdvDdXaVcrfj # <-- pubkey is obtained from whoever is deploying the updates
$ export PATH=~/.local/share/solana-install/bin:$PATH
$ solana-keygen ... # <-- runs the latest solana-keygen
$ solana-install run solana-fullnode ... # <-- runs a fullnode, restarting it as necesary when an update is applied
```
### On-chain Update Manifest
An update manifest is used to advertise the deployment of new release tarballs
on a solana cluster. The update manifest is stored using the `config` program,
and each update manifest account describes a logical update channel for a given
target triple (eg, `x86_64-apple-darwin`). The account public key is well-known
between the entity deploying new updates and users consuming those updates.
The update tarball itself is hosted elsewhere, off-chain and can be fetched from
the specified `download_url`.
```rust,ignore
use solana_sdk::signature::Signature;
/// Information required to download and apply a given update
pub struct UpdateManifest {
pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH
pub download_url: String, // Download URL to the release tar.bz2
pub download_sha256: String, // SHA256 digest of the release tar.bz2 file
}
/// Userdata of an Update Manifest program Account.
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
pub struct SignedUpdateManifest {
pub manifest: UpdateManifest,
pub manifest_signature: Signature,
}
```
Note that the `manifest` field itself contains a corresponding signature
(`manifest_signature`) to guard against man-in-the-middle attacks between the
`solana-install` tool and the solana cluster RPC API.
To guard against rollback attacks, `solana-install` will refuse to install an
update with an older `timestamp_secs` than what is currently installed.
### Release Archive Contents
A release archive is expected to be a tar file compressed with
bzip2 with the following internal structure:
* `/version.yml` - a simple YAML file containing the field `"target"` - the
target tuple. Any additional fields are ignored.
* `/bin/` -- directory containing available programs in the release.
`solana-install` will symlink this directory to
`~/.local/share/solana-install/bin` for use by the `PATH` environment
variable.
* `...` -- any additional files and directories are permitted
### solana-install Tool
The `solana-install` tool is used by the user to install and update their cluster software.
It manages the following files and directories in the user's home directory:
* `~/.config/solana/install/config.yml` - user configuration and information about currently installed software version
* `~/.local/share/solana/install/bin` - a symlink to the current release. eg, `~/.local/share/solana-update/<update-pubkey>-<manifest_signature>/bin`
* `~/.local/share/solana/install/releases/<download_sha256>/` - contents of a release
#### Command-line Interface
```manpage
solana-install 0.13.0
The solana cluster software installer
USAGE:
solana-install [OPTIONS] <SUBCOMMAND>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-c, --config <PATH> Configuration file to use [default: /Users/mvines/Library/Preferences/solana/install.yml]
SUBCOMMANDS:
deploy deploys a new update
help Prints this message or the help of the given subcommand(s)
info displays information about the current installation
init initializes a new installation
run Runs a program while periodically checking and applying software updates
update checks for an update, and if available downloads and applies it
```
```manpage
solana-install-init
initializes a new installation
USAGE:
solana-install init [OPTIONS]
FLAGS:
-h, --help Prints help information
OPTIONS:
-d, --data_dir <PATH> Directory to store install data [default: /Users/mvines/Library/Application Support/solana]
-u, --url <URL> JSON RPC URL for the solana cluster [default: https://api.testnet.solana.com/]
-p, --pubkey <PUBKEY> Public key of the update manifest [default: 9XX329sPuskWhH4DQh6k16c87dHKhXLBZTL3Gxmve8Gp]
```
```manpage
solana-install-info
displays information about the current installation
USAGE:
solana-install info [FLAGS]
FLAGS:
-h, --help Prints help information
-l, --local only display local information, don't check the cluster for new updates
```
```manpage
solana-install-deploy
deploys a new update
USAGE:
solana-install deploy <download_url> <update_manifest_keypair>
FLAGS:
-h, --help Prints help information
ARGS:
<download_url> URL to the solana release archive
<update_manifest_keypair> Keypair file for the update manifest (/path/to/keypair.json)
```
```manpage
solana-install-update
checks for an update, and if available downloads and applies it
USAGE:
solana-install update
FLAGS:
-h, --help Prints help information
```
```manpage
solana-install-run
Runs a program while periodically checking and applying software updates
USAGE:
solana-install run <program_name> [program_arguments]...
FLAGS:
-h, --help Prints help information
ARGS:
<program_name> program to run
<program_arguments>... arguments to supply to the program
The program will be restarted upon a successful software update
```

View File

@ -24,11 +24,8 @@ Methods
* [confirmTransaction](#confirmtransaction)
* [getAccountInfo](#getaccountinfo)
* [getBalance](#getbalance)
* [getClusterNodes](#getclusternodes)
* [getRecentBlockhash](#getrecentblockhash)
* [getSignatureStatus](#getsignaturestatus)
* [getSlotLeader](#getslotleader)
* [getNumBlocksSinceSignatureConfirmation](#getnumblockssincesignatureconfirmation)
* [getTransactionCount](#gettransactioncount)
* [requestAirdrop](#requestairdrop)
* [sendTransaction](#sendtransaction)
@ -116,30 +113,6 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "
---
### getClusterNodes
Returns information about all the nodes participating in the cluster
##### Parameters:
None
##### Results:
The result field will be an array of JSON objects, each with the following sub fields:
* `id` - Node identifier, as base-58 encoded string
* `gossip` - Gossip network address for the node
* `tpu` - TPU network address for the node
* `rpc` - JSON RPC network address for the node, or `null` if the JSON RPC service is not enabled
##### Example:
```bash
// Request
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getClusterNodes"}' http://localhost:8899
// Result
{"jsonrpc":"2.0","result":[{"gossip":"10.239.6.48:8001","id":"9QzsJf7LPLj8GkXbYT3LFDKqsj2hHG7TA3xinJHu8epQ","rpc":"10.239.6.48:8899","tpu":"10.239.6.48:8856"}],"id":1}
```
---
### getAccountInfo
Returns all information associated with the account of provided Pubkey
@ -195,10 +168,12 @@ events.
* `string` - Signature of Transaction to confirm, as base-58 encoded string
##### Results:
* `null` - Unknown transaction
* `object` - Transaction status:
* `"Ok": null` - Transaction was successful
* `"Err": <ERR>` - Transaction failed with TransactionError <ERR> [TransactionError definitions](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction.rs#L14)
* `string` - Transaction status:
* `Confirmed` - Transaction was successful
* `SignatureNotFound` - Unknown transaction
* `ProgramRuntimeError` - An error occurred in the program that processed this Transaction
* `AccountInUse` - Another Transaction had a write lock one of the Accounts specified in this Transaction. The Transaction may succeed if retried
* `GenericFailure` - Some other error occurred. **Note**: In the future new Transaction statuses may be added to this list. It's safe to assume that all new statuses will be more specific error conditions that previously presented as `GenericFailure`
##### Example:
```bash
@ -209,48 +184,7 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "
{"jsonrpc":"2.0","result":"SignatureNotFound","id":1}
```
-----
### getSlotLeader
Returns the current slot leader
##### Parameters:
None
##### Results:
* `string` - Node Id as base-58 encoded string
##### Example:
```bash
// Request
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getSlotLeader"}' http://localhost:8899
// Result
{"jsonrpc":"2.0","result":"ENvAW7JScgYq6o4zKZwewtkzzJgDzuJAFxYasvmEQdpS","id":1}
```
-----
### getNumBlocksSinceSignatureConfirmation
Returns the current number of blocks since signature has been confirmed.
##### Parameters:
* `string` - Signature of Transaction to confirm, as base-58 encoded string
##### Results:
* `integer` - count, as unsigned 64-bit integer
##### Example:
```bash
// Request
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getNumBlocksSinceSignatureConfirmation", "params":["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW"]}' http://localhost:8899
// Result
{"jsonrpc":"2.0","result":8,"id":1}
```
---
### getTransactionCount
Returns the current Transaction count from the ledger

View File

@ -1,212 +0,0 @@
# Stake Delegation and Reward
This design proposal focuses on the software architecture for the on-chain
voting and staking programs. Incentives for staking is covered in [staking
rewards](staking-rewards.md).
The current architecture requires a vote for each delegated stake from the
validator, and therefore does not scale to allow replicator clients to
automatically delegate their rewards.
The design proposes a new set of programs for voting and stake delegation, The
proposed programs allow many stake accounts to passively earn rewards with a
single validator vote without permission or active involvement from the
validator.
## Current Design Problems
In the current design each staker creates their own VoteState, and assigns a
**delegate** in the VoteState that can submit votes. Since the validator has to
actively vote for each stake delegated to it, validators can censor stakes by
not voting for them.
The number of votes is equal to the number of stakers, and not the number of
validators. Replicator clients are expected to delegate their replication
rewards as they are earned, and therefore the number of stakes is expected to be
large compared to the number of validators in a long running cluster.
## Proposed changes to the current design.
The general idea is that instead of the staker, the validator will own the
VoteState program. In this proposal the VoteState program is there to track
validator votes, count validator generated credits and to provide any
additional validator specific state. The VoteState program is not aware of any
stakes delegated to it, and has no staking weight.
The rewards generated are proportional to the amount of lamports staked. In
this proposal stake state is stored as part of the StakeState program. This
program is owned by the staker only. Lamports stored in this program are the
stake. Unlike the current design, this program contains a new field to indicate
which VoteState program the stake is delegated to.
### VoteState
VoteState is the current state of all the votes the **delegate** has submitted
to the bank. VoteState contains the following state information:
* votes - The submitted votes data structure.
* credits - The total number of rewards this vote program has generated over its
lifetime.
* root\_slot - The last slot to reach the full lockout commitment necessary for
rewards.
* commission - The commission taken by this VoteState for any rewards claimed by
staker's StakeState accounts. This is the percentage ceiling of the reward.
* Account::lamports - The accumulated lamports from the commission. These do not
count as stakes.
* `authorized_vote_signer` - Only this identity is authorized to submit votes, and
this field can only modified by this entity
### VoteInstruction::Initialize
* `account[0]` - RW - The VoteState
`VoteState::authorized_vote_signer` is initialized to `account[0]`
other VoteState members defaulted
### VoteInstruction::AuthorizeVoteSigner(Pubkey)
* `account[0]` - RW - The VoteState
`VoteState::authorized_vote_signer` is set to to `Pubkey`, instruction must by
signed by Pubkey
### StakeState
A StakeState takes one of two forms, StakeState::Delegate and StakeState::MiningPool.
### StakeState::Delegate
StakeState is the current delegation preference of the **staker**. StakeState
contains the following state information:
* Account::lamports - The staked lamports.
* `voter_id` - The pubkey of the VoteState instance the lamports are
delegated to.
* `credits_observed` - The total credits claimed over the lifetime of the
program.
### StakeState::MiningPool
There are two approaches to the mining pool. The bank could allow the
StakeState program to bypass the token balance check, or a program representing
the mining pool could run on the network. To avoid a single network wide lock,
the pool can be split into several mining pools. This design focuses on using a
StakeState::MiningPool as the cluster wide mining pools.
* 256 StakeState::MiningPool are initialized, each with 1/256 number of mining pool
tokens stored as `Account::lamports`.
The stakes and the MiningPool are accounts that are owned by the same `Stake`
program.
### StakeInstruction::Initialize
* `account[0]` - RW - The StakeState::Delegate instance.
`StakeState::Delegate::credits_observed` is initialized to `VoteState::credits`.
`StakeState::Delegate::voter_id` is initialized to `account[1]`
* `account[1]` - R - The VoteState instance.
### StakeInstruction::RedeemVoteCredits
The VoteState program and the StakeState programs maintain a lifetime counter
of total rewards generated and claimed. Therefore an explicit `Clear`
instruction is not necessary. When claiming rewards, the total lamports
deposited into the StakeState and as validator commission is proportional to
`VoteState::credits - StakeState::credits_observed`.
* `account[0]` - RW - The StakeState::MiningPool instance that will fulfill the
reward.
* `account[1]` - RW - The StakeState::Delegate instance that is redeeming votes
credits.
* `account[2]` - R - The VoteState instance, must be the same as
`StakeState::voter_id`
Reward is payed out for the difference between `VoteState::credits` to
`StakeState::Delgate.credits_observed`, and `credits_observed` is updated to
`VoteState::credits`. The commission is deposited into the `VoteState` token
balance, and the reward is deposited to the `StakeState::Delegate` token balance. The
reward and the commission is weighted by the `StakeState::lamports` divided by total lamports staked.
The Staker or the owner of the Stake program sends a transaction with this
instruction to claim the reward.
Any random MiningPool can be used to redeem the credits.
```rust,ignore
let credits_to_claim = vote_state.credits - stake_state.credits_observed;
stake_state.credits_observed = vote_state.credits;
```
`credits_to_claim` is used to compute the reward and commission, and
`StakeState::Delegate::credits_observed` is updated to the latest
`VoteState::credits` value.
### Collecting network fees into the MiningPool
At the end of the block, before the bank is frozen, but after it processed all
the transactions for the block, a virtual instruction is executed to collect
the transaction fees.
* A portion of the fees are deposited into the leader's account.
* A portion of the fees are deposited into the smallest StakeState::MiningPool
account.
### Benefits
* Single vote for all the stakers.
* Clearing of the credit variable is not necessary for claiming rewards.
* Each delegated stake can claim its rewards independently.
* Commission for the work is deposited when a reward is claimed by the delegated
stake.
This proposal would benefit from the `read-only` accounts proposal to allow for
many rewards to be claimed concurrently.
## Passive Delegation
Any number of instances of StakeState::Delegate programs can delegate to a single
VoteState program without an interactive action from the identity controlling
the VoteState program or submitting votes to the program.
The total stake allocated to a VoteState program can be calculated by the sum of
all the StakeState programs that have the VoteState pubkey as the
`StakeState::Delegate::voter_id`.
## Example Callflow
<img alt="Passive Staking Callflow" src="img/passive-staking-callflow.svg" class="center"/>
## Future work
Validators may want to split the stake delegated to them amongst many validator
nodes since stake is used as weight in the network control and data planes. One
way to implement this would be for the StakeState to delegate to a pool of
validators instead of a single one.
Instead of a single `vote_id` and `credits_observed` entry in the StakeState
program, the program can be initialized with a vector of tuples.
```rust,ignore
Voter {
voter_id: Pubkey,
credits_observed: u64,
weight: u8,
}
```
* voters: Vec<Voter> - Array of VoteState accounts that are voting rewards with
this stake.
A StakeState program would claim a fraction of the reward from each voter in
the `voters` array, and each voter would be delegated a fraction of the stake.

View File

@ -25,7 +25,7 @@ When Futures 0.3.0 is released, the Transact trait may look like this:
```rust,ignore
trait Transact {
async fn send_transactions(txs: &[Transaction]) -> Vec<Result<(), TransactionError>>;
async fn send_transactions(txs: &[Transaction]) -> Vec<Result<(), BankError>>;
}
```

View File

@ -1,141 +0,0 @@
## Testnet Participation
This document describes how to participate in the beta testnet as a
validator node.
Please note some of the information and instructions described here may change
in future releases.
### Beta Testnet Overview
The beta testnet features a validator running at beta.testnet.solana.com, which
serves as the entrypoint to the cluster for your validator.
Additionally there is a blockexplorer available at
[http://beta.testnet.solana.com/](http://beta.testnet.solana.com/).
The beta testnet is configured to reset the ledger every 24hours, or sooner
should an hourly automated sanity test fail.
### Machine Requirements
Since the beta testnet is not intended for stress testing of max transaction
throughput, a higher-end machine with a GPU is not necessary to participate.
However ensure the machine used is not behind a residential NAT to avoid NAT
traversal issues. A cloud-hosted machine works best. Ensure that IP ports
8000 through 10000 are not blocked for Internet traffic.
Prebuilt binaries are available for Linux x86_64 (Ubuntu 18.04 recommended).
MacOS or WSL users may build from source.
#### Confirm The Testnet Is Reachable
Before attaching a validator node, sanity check that the cluster is accessible
to your machine by running some simple wallet commands. If any of these
commands fail, please retry 5-10 minutes later to confirm the testnet is not
just restarting itself before debugging further.
Fetch the current testnet transaction count over JSON RPC:
```bash
$ curl -X POST -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1, "method":"getTransactionCount"}' http://beta.testnet.solana.com:8899
```
Inspect the blockexplorer at [http://beta.testnet.solana.com/](http://beta.testnet.solana.com/) for activity.
Run the following command to join the gossip network and view all the other nodes in the cluster:
```bash
$ solana-gossip --network beta.testnet.solana.com:8001
```
View the [metrics dashboard](https://metrics.solana.com:3000/d/testnet-edge/testnet-monitor-edge?var-testnet=testnet-beta)
for more detail on cluster activity.
### Validator Setup
#### Obtaining The Software
##### Bootstrap with `solana-install`
The `solana-install` tool can be used to easily install and upgrade the cluster
software on Linux x86_64 systems.
Install the latest release with a single shell command:
```bash
$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v0.13.0/install/solana-install-init.sh | \
sh -s - --url https://api.beta.testnet.solana.com
```
Alternatively build the `solana-install` program from source and run the
following command to obtain the same result:
```bash
$ solana-install init --url https://api.beta.testnet.solana.com
```
After a successful install, `solana-install update` may be used to easily update the cluster
software to a newer version.
##### Download Prebuilt Binaries
Binaries are available for Linux x86_64 systems.
Download the binaries by navigating to https://github.com/solana-labs/solana/releases/latest, download
**solana-release-x86_64-unknown-linux-gnu.tar.bz2**, then extract the archive:
```bash
$ tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2
$ cd solana-release/
$ export PATH=$PWD/bin:$PATH
```
##### Build From Source
If you are unable to use the prebuilt binaries or prefer to build it yourself from source, navigate to:
> https://github.com/solana-labs/solana/releases/latest
Download the source code tarball (solana-*[release]*.tar.gz) from our latest release tag. Extract the code and build the binaries with:
```bash
$ ./scripts/cargo-install-all.sh .
$ export PATH=$PWD/bin:$PATH
```
### Starting The Validator
Sanity check that you are able to interact with the cluster by receiving a small
airdrop of lamports from the testnet drone:
```bash
$ solana-wallet -n beta.testnet.solana.com airdrop 123
$ solana-wallet -n beta.testnet.solana.com balance
```
Then the following command will start a new validator node.
If this is a `solana-install`-installation:
```bash
$ fullnode-x.sh --public-address --poll-for-new-genesis-block beta.testnet.solana.com
```
Alternatively, the `solana-install run` command can be used to run the validator
node while periodically checking for and applying software updates:
```bash
$ solana-install run fullnode-x.sh -- --public-address --poll-for-new-genesis-block beta.testnet.solana.com
```
When not using `solana-install`:
```bash
$ USE_INSTALL=1 ./multinode-demo/fullnode-x.sh --public-address --poll-for-new-genesis-block beta.testnet.solana.com
```
Then from another console, confirm the IP address if your node is now visible in
the gossip network by running:
```bash
$ solana-gossip --network beta.testnet.solana.com:8001
```
Congratulations, you're now participating in the testnet cluster!
#### Controlling local network port allocation
By default the validator will dynamically select available network ports in the
8000-10000 range, and may be overridden with `--dynamic-port-range`. For
example, `fullnode-x.sh --dynamic-port-range 11000-11010 ...` will restrict the
validator to ports 11000-11011.
### Sharing Metrics From Your Validator
If you'd like to share metrics perform the following steps before starting the
validator node:
```bash
export u="username obtained from the Solana maintainers"
export p="password obtained from the Solana maintainers"
export SOLANA_METRICS_CONFIG="db=testnet-beta,u=${u:?},p=${p:?}"
source scripts/configure-metrics.sh
```

View File

@ -1,59 +0,0 @@
# Deterministic Transaction Fees
Transactions currently include a fee field that indicates the maximum fee field
a slot leader is permitted to charge to process a transaction. The cluster, on
the other hand, agrees on a minimum fee. If the network is congested, the slot
leader may prioritize the transactions offering higher fees. That means the
client won't know how much was collected until the transaction is confirmed by
the cluster and the remaining balance is checked. It smells of exactly what we
dislike about Ethereum's "gas", non-determinism.
## Implementation Status
This design is not yet implemented, but is written as though it has been. Once
implemented, delete this comment.
### Congestion-driven fees
Each validator uses *signatures per slot* (SPS) to estimate network congestion
and *SPS target* to estimate the desired processing capacity of the cluster.
The validator learns the SPS target from the genesis block, whereas it
calculates SPS from the ledger data in the previous epoch.
### Calculating fees
The client uses the JSON RPC API to query the cluster for the current fee
parameters. Those parameters are tagged with a blockhash and remain valid
until that blockhash is old enough to be rejected by the slot leader.
Before sending a transaction to the cluster, a client may submit the
transaction and fee account data to an SDK module called the *fee calculator*.
So long as the client's SDK version matches the slot leader's version, the
client is assured that its account will be changed exactly the same number of
lamports as returned by the fee calculator.
### Fee Parameters
In the first implementation of this design, the only fee parameter is
`lamports_per_signature`. The more signatures the cluster needs to verify, the
higher the fee. The exact number of lamports is determined by the ratio of SPS
to the SPS target. The cluster lowers `lamports_per_signature` when SPS is
below the target and raises it when at or above the target.
Future parameters might include:
* `lamports_per_pubkey` - cost to load an account
* `lamports_per_slot_distance` - higher cost to load very old accounts
* `lamports_per_byte` - cost per size of account loaded
* `lamports_per_bpf_instruction` - cost to run a program
### Attacks
#### Hijacking the SPS Target
A group of validators can centralize the cluster if they can convince it to
raise the SPS Target above a point where the rest of the validators can keep
up. Raising the target will cause fees to drop, presumably creating more demand
and therefore higher TPS. If the validator doesn't have hardware that can
process that many transactions that fast, its confirmation votes will
eventually get so long that the cluster will be forced to boot it.

View File

@ -41,7 +41,7 @@ $ solana-wallet balance
$ solana-wallet confirm <TX_SIGNATURE>
// Return
"Confirmed" / "Not found" / "Transaction failed with error <ERR>"
"Confirmed" / "Not found"
```
#### Deploy program
@ -352,3 +352,4 @@ ARGS:
<PUBKEY> The pubkey of recipient
<PROCESS_ID> The process id of the transfer to unlock
```

View File

@ -9,7 +9,6 @@ steps:
- command: "ci/test-stable-perf.sh"
name: "stable-perf"
timeout_in_minutes: 20
artifact_paths: "log-*.txt"
agents:
- "queue=cuda"
- command: "ci/test-bench.sh"
@ -17,8 +16,7 @@ steps:
timeout_in_minutes: 20
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_stable_docker_image ci/test-stable.sh"
name: "stable"
timeout_in_minutes: 25
artifact_paths: "log-*.txt"
timeout_in_minutes: 20
- command: ". ci/rust-version.sh; ci/docker-run.sh $$rust_nightly_docker_image ci/test-coverage.sh"
name: "coverage"
timeout_in_minutes: 20

View File

@ -1,26 +1,17 @@
#!/usr/bin/env bash
#
# Outputs the current crate version from a given Cargo.toml
# Outputs the current crate version
#
set -e
Cargo_toml=$1
[[ -n $Cargo_toml ]] || {
echo "Usage: $0 path/to/Cargo.toml"
exit 0
}
[[ -r $Cargo_toml ]] || {
echo "Error: unable to read $Cargo_toml"
exit 1
}
cd "$(dirname "$0")"/..
while read -r name equals value _; do
if [[ $name = version && $equals = = ]]; then
echo "${value//\"/}"
exit 0
fi
done < <(cat "$Cargo_toml")
done < <(cat Cargo.toml)
echo Unable to locate version in Cargo.toml 1>&2
exit 1

View File

@ -1,6 +1,6 @@
# Note: when the rust version is changed also modify
# ci/buildkite.yml to pick up the new image tag
FROM rust:1.34.0
FROM rust:1.32.0
RUN set -x \
&& apt update \
@ -17,7 +17,6 @@ RUN set -x \
lcov \
libclang-common-7-dev \
llvm-7 \
mscgen \
rsync \
sudo \
\

View File

@ -8,4 +8,5 @@ docker build -t solanalabs/rust .
read -r rustc version _ < <(docker run solanalabs/rust rustc --version)
[[ $rustc = rustc ]]
docker tag solanalabs/rust:latest solanalabs/rust:"$version"
docker push solanalabs/rust:"$version"
docker push solanalabs/rust

View File

@ -55,7 +55,7 @@ while getopts "ch?i:k:brxR" opt; do
restartInterval=$OPTARG
;;
b)
maybeNoLeaderRotation="--stake 0"
maybeNoLeaderRotation="--only-bootstrap-stake"
;;
x)
extraNodes=$((extraNodes + 1))
@ -78,6 +78,7 @@ source scripts/configure-metrics.sh
nodes=(
"multinode-demo/drone.sh"
"multinode-demo/bootstrap-leader.sh \
$maybeNoLeaderRotation \
--enable-rpc-exit \
--init-complete-file init-complete-node1.log"
"multinode-demo/fullnode.sh \
@ -306,8 +307,11 @@ while [[ $iteration -le $iterations ]]; do
set -x
client_id=/tmp/client-id.json-$$
$solana_keygen -o $client_id || exit $?
$solana_gossip \
--num-nodes-exactly $numNodes || exit $?
$solana_bench_tps \
--identity $client_id \
--num-nodes $numNodes \
--reject-extra-nodes \
--converge-only || exit $?
rm -rf $client_id
) || flag_error

View File

@ -2,7 +2,15 @@
set -e
cd "$(dirname "$0")/.."
eval "$(ci/channel-info.sh)"
if [[ $BUILDKITE_BRANCH = "$STABLE_CHANNEL" ]]; then
CHANNEL=stable
elif [[ $BUILDKITE_BRANCH = "$EDGE_CHANNEL" ]]; then
CHANNEL=edge
elif [[ $BUILDKITE_BRANCH = "$BETA_CHANNEL" ]]; then
CHANNEL=beta
fi
echo --- Creating tarball
(

View File

@ -11,7 +11,6 @@ source ci/semver_bash/semver.sh
# here. (TODO: figure the crate ordering dynamically)
#
CRATES=(
kvstore
logger
netutil
sdk
@ -19,20 +18,18 @@ CRATES=(
metrics
client
drone
programs/{budget_api,config_api,stake_api,storage_api,token_api,vote_api,exchange_api}
programs/{vote_program,budget_program,bpf_loader,config_program,exchange_program,failure_program}
programs/{noop_program,stake_program,storage_program,token_program}
programs/{budget_api,storage_api,token_api,vote_api}
runtime
programs/{budget,bpf_loader,storage,token,vote}
vote-signer
core
fullnode
genesis
gossip
ledger-tool
wallet
install
)
# Only package/publish if this is a tagged release
[[ -n $TRIGGERED_BUILDKITE_TAG ]] || {
echo TRIGGERED_BUILDKITE_TAG unset, skipped
@ -55,7 +52,7 @@ for crate in "${CRATES[@]}"; do
exit 1
fi
echo "-- $crate"
grep -q "^version = \"$expectedCrateVersion\"$" "$crate"/Cargo.toml || {
grep -q "^version = \"$expectedCrateVersion\"$" Cargo.toml || {
echo "Error: $crate/Cargo.toml version is not $expectedCrateVersion"
exit 1
}

View File

@ -66,30 +66,7 @@ echo --- Creating tarball
cp solana-release-cuda/bin/solana-fullnode solana-release/bin/solana-fullnode-cuda
cp -a scripts multinode-demo solana-release/
# Add a wrapper script for fullnode.sh
# TODO: Remove multinode/... from tarball
cat > solana-release/bin/fullnode.sh <<'EOF'
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"/..
export USE_INSTALL=1
exec multinode-demo/fullnode.sh "$@"
EOF
chmod +x solana-release/bin/fullnode.sh
# Add a wrapper script for fullnode-x.sh
# TODO: Remove multinode/... from tarball
cat > solana-release/bin/fullnode-x.sh <<'EOF'
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"/..
export USE_INSTALL=1
exec multinode-demo/fullnode-x.sh "$@"
EOF
chmod +x solana-release/bin/fullnode-x.sh
tar jvcf solana-release-$TARGET.tar.bz2 solana-release/
cp solana-release/bin/solana-install solana-install-$TARGET
)
echo --- Saving build artifacts
@ -101,25 +78,24 @@ if [[ -n $DO_NOT_PUBLISH_TAR ]]; then
exit 0
fi
for file in solana-release-$TARGET.tar.bz2 solana-install-$TARGET; do
echo --- AWS S3 Store: $file
(
set -x
$DRYRUN docker run \
--rm \
--env AWS_ACCESS_KEY_ID \
--env AWS_SECRET_ACCESS_KEY \
--volume "$PWD:/solana" \
eremite/aws-cli:2018.12.18 \
/usr/bin/s3cmd --acl-public put /solana/"$file" s3://solana-release/"$CHANNEL_OR_TAG"/"$file"
file=solana-release-$TARGET.tar.bz2
echo --- AWS S3 Store: $file
(
set -x
$DRYRUN docker run \
--rm \
--env AWS_ACCESS_KEY_ID \
--env AWS_SECRET_ACCESS_KEY \
--volume "$PWD:/solana" \
eremite/aws-cli:2018.12.18 \
/usr/bin/s3cmd --acl-public put /solana/"$file" s3://solana-release/"$CHANNEL_OR_TAG"/"$file"
echo Published to:
$DRYRUN ci/format-url.sh http://solana-release.s3.amazonaws.com/"$CHANNEL_OR_TAG"/"$file"
)
echo Published to:
$DRYRUN ci/format-url.sh http://solana-release.s3.amazonaws.com/"$CHANNEL_OR_TAG"/"$file"
)
if [[ -n $TAG ]]; then
ci/upload-github-release-asset.sh $file
fi
done
if [[ -n $TAG ]]; then
ci/upload-github-release-asset.sh $file
fi
echo --- ok

20
ci/run-local.sh Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
#
# Run the entire buildkite CI pipeline locally for pre-testing before sending a
# Github pull request
#
set -e
cd "$(dirname "$0")/.."
BKRUN=ci/node_modules/.bin/bkrun
if [[ ! -x $BKRUN ]]; then
(
set -x
cd ci/
npm install bkrun
)
fi
set -x
exec ./ci/node_modules/.bin/bkrun ci/buildkite.yml

View File

@ -13,8 +13,8 @@
# $ source ci/rust-version.sh
#
export rust_stable=1.34.0
export rust_stable_docker_image=solanalabs/rust:1.34.0
export rust_stable=1.32.0
export rust_stable_docker_image=solanalabs/rust:1.32.0
export rust_nightly=nightly-2019-03-14
export rust_nightly_docker_image=solanalabs/rust-nightly:2019-03-14

View File

@ -39,26 +39,17 @@ fi
BENCH_FILE=bench_output.log
BENCH_ARTIFACT=current_bench_results.log
# First remove "BENCH_FILE", if it exists so that the following commands can append
rm -f "$BENCH_FILE"
# Run sdk benches
_ cargo +$rust_nightly bench --manifest-path sdk/Cargo.toml ${V:+--verbose} \
-- -Z unstable-options --format=json | tee -a "$BENCH_FILE"
# Run runtime benches
_ cargo +$rust_nightly bench --manifest-path runtime/Cargo.toml ${V:+--verbose} \
-- -Z unstable-options --format=json | tee -a "$BENCH_FILE"
# Run core benches
_ cargo +$rust_nightly bench --manifest-path core/Cargo.toml ${V:+--verbose} \
-- -Z unstable-options --format=json | tee -a "$BENCH_FILE"
_ cargo +$rust_nightly bench ${V:+--verbose} \
-- -Z unstable-options --format=json | tee "$BENCH_FILE"
# Run bpf benches
_ cargo +$rust_nightly bench --manifest-path programs/bpf/Cargo.toml ${V:+--verbose} --features=bpf_c \
-- -Z unstable-options --format=json --nocapture | tee -a "$BENCH_FILE"
echo --- program/bpf
(
set -x
cd programs/bpf
cargo +$rust_nightly bench ${V:+--verbose} --features=bpf_c \
-- -Z unstable-options --format=json --nocapture | tee -a ../../../"$BENCH_FILE"
)
_ cargo +$rust_nightly run --release --package solana-upload-perf \
-- "$BENCH_FILE" "$TARGET_BRANCH" "$UPLOAD_METRICS" > "$BENCH_ARTIFACT"

View File

@ -27,6 +27,7 @@ test-stable)
_ cargo +"$rust_stable" build --all ${V:+--verbose}
_ cargo +"$rust_stable" test --all ${V:+--verbose} -- --nocapture --test-threads=1
_ cargo +"$rust_stable" test --manifest-path runtime/Cargo.toml
;;
test-stable-perf)
echo "Executing $testName"
@ -70,7 +71,19 @@ test-stable-perf)
# Run root package library tests
_ cargo +"$rust_stable" build --all ${V:+--verbose} --features="$ROOT_FEATURES"
_ cargo +"$rust_stable" test --manifest-path=core/Cargo.toml ${V:+--verbose} --features="$ROOT_FEATURES" -- --nocapture --test-threads=1
_ cargo +"$rust_stable" test --all --lib ${V:+--verbose} --features="$ROOT_FEATURES" -- --nocapture --test-threads=1
_ cargo +"$rust_stable" test --manifest-path runtime/Cargo.toml
# Run root package integration tests
for test in tests/*.rs; do
test=${test##*/} # basename x
test=${test%.rs} # basename x .rs
(
export RUST_LOG="$test"=trace,$RUST_LOG
_ cargo +"$rust_stable" test --all ${V:+--verbose} --features="$ROOT_FEATURES" --test="$test" \
-- --test-threads=1 --nocapture
)
done
;;
*)
echo "Error: Unknown test: $testName"

View File

@ -17,22 +17,18 @@ source ci/upload-ci-artifact.sh
LEADER_CPU_MACHINE_TYPE="n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100"
[[ -n $CLIENT_COUNT ]] || CLIENT_COUNT=2
[[ -n $TESTNET_TAG ]] || TESTNET_TAG=testnet-automation
[[ -n $TESTNET_ZONES ]] || TESTNET_ZONES="us-west1-b"
[[ -n $TESTNET_ZONE ]] || TESTNET_ZONE=us-west1-b
[[ -n $CHANNEL ]] || CHANNEL=beta
[[ -n $ADDITIONAL_FLAGS ]] || ADDITIONAL_FLAGS=""
TESTNET_CLOUD_ZONES=(); while read -r -d, ; do TESTNET_CLOUD_ZONES+=( "$REPLY" ); done <<< "${TESTNET_ZONES},"
launchTestnet() {
declare nodeCount=$1
echo --- setup "$nodeCount" node test
# shellcheck disable=SC2068
net/gce.sh create \
-b \
-d pd-ssd \
-n "$nodeCount" -c "$CLIENT_COUNT" \
-G "$LEADER_CPU_MACHINE_TYPE" \
-p "$TESTNET_TAG" ${TESTNET_CLOUD_ZONES[@]/#/-z } "$ADDITIONAL_FLAGS"
-p "$TESTNET_TAG" -z "$TESTNET_ZONE"
echo --- configure database
net/init-metrics.sh -e
@ -47,39 +43,37 @@ launchTestnet() {
echo --- wait "$ITERATION_WAIT" seconds to complete test
sleep "$ITERATION_WAIT"
set -x
declare q_mean_tps='
SELECT round(mean("sum_count")) AS "mean_tps" FROM (
SELECT sum("count") AS "sum_count"
FROM "testnet-automation"."autogen"."counter-bank-process_transactions-txs"
FROM "testnet-automation"."autogen"."counter-banking_stage-process_transactions"
WHERE time > now() - 300s GROUP BY time(1s)
)'
declare q_max_tps='
SELECT round(max("sum_count")) AS "max_tps" FROM (
SELECT sum("count") AS "sum_count"
FROM "testnet-automation"."autogen"."counter-bank-process_transactions-txs"
FROM "testnet-automation"."autogen"."counter-banking_stage-process_transactions"
WHERE time > now() - 300s GROUP BY time(1s)
)'
declare q_mean_confirmation='
SELECT round(mean("duration_ms")) as "mean_confirmation"
FROM "testnet-automation"."autogen"."validator-confirmation"
FROM "testnet-automation"."autogen"."leader-confirmation"
WHERE time > now() - 300s'
declare q_max_confirmation='
SELECT round(max("duration_ms")) as "max_confirmation"
FROM "testnet-automation"."autogen"."validator-confirmation"
FROM "testnet-automation"."autogen"."leader-confirmation"
WHERE time > now() - 300s'
declare q_99th_confirmation='
SELECT round(percentile("duration_ms", 99)) as "99th_confirmation"
FROM "testnet-automation"."autogen"."validator-confirmation"
FROM "testnet-automation"."autogen"."leader-confirmation"
WHERE time > now() - 300s'
curl -G "${INFLUX_HOST}/query?u=ro&p=topsecret" \
--data-urlencode "db=testnet-automation" \
curl -G "https://metrics.solana.com:8086/query?u=${INFLUX_USERNAME}&p=${INFLUX_PASSWORD}" \
--data-urlencode "db=$INFLUX_DATABASE" \
--data-urlencode "q=$q_mean_tps;$q_max_tps;$q_mean_confirmation;$q_max_confirmation;$q_99th_confirmation" |
python ci/testnet-automation-json-parser.py >>TPS"$nodeCount".log

View File

@ -11,8 +11,6 @@ clientNodeCount=0
additionalFullNodeCount=10
publicNetwork=false
skipSetup=false
skipStart=false
externalNode=false
tarChannelOrTag=edge
delete=false
enableGpu=false
@ -27,14 +25,13 @@ usage() {
echo "Error: $*"
fi
cat <<EOF
usage: $0 -p network-name -C cloud -z zone1 [-z zone2] ... [-z zoneN] [options...]
usage: $0 [name] [cloud] [zone] [options...]
Deploys a CD testnet
mandatory arguments:
-p [network-name] - name of the network
-C [cloud] - cloud provider to use (gce, ec2)
-z [zone] - cloud provider zone to deploy the network into. Must specify at least one zone
name - name of the network
cloud - cloud provider to use (gce, ec2)
zone - cloud provider zone to deploy the network into
options:
-t edge|beta|stable|vX.Y.Z - Deploy the latest tarball release for the
@ -60,22 +57,19 @@ EOF
exit $exitcode
}
zone=()
netName=$1
cloudProvider=$2
zone=$3
[[ -n $netName ]] || usage
[[ -n $cloudProvider ]] || usage "Cloud provider not specified"
[[ -n $zone ]] || usage "Zone not specified"
shift 3
while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:" opt; do
while getopts "h?p:Pn:c:t:gG:a:Dbd:ru" opt; do
case $opt in
h | \?)
usage
;;
p)
netName=$OPTARG
;;
C)
cloudProvider=$OPTARG
;;
z)
zone+=("$OPTARG")
;;
P)
publicNetwork=true
;;
@ -117,12 +111,6 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:" opt; do
r)
skipSetup=true
;;
s)
skipStart=true
;;
x)
externalNode=true
;;
u)
blockstreamer=true
;;
@ -132,10 +120,6 @@ while getopts "h?p:Pn:c:t:gG:a:Dbd:rusxz:p:C:" opt; do
esac
done
[[ -n $netName ]] || usage
[[ -n $cloudProvider ]] || usage "Cloud provider not specified"
[[ -n ${zone[*]} ]] || usage "At least one zone must be specified"
shutdown() {
exitcode=$?
@ -156,16 +140,9 @@ trap shutdown EXIT INT
set -x
# Build a string to pass zone opts to $cloudProvider.sh: "-z zone1 -z zone2 ..."
zone_args=()
for val in "${zone[@]}"; do
zone_args+=("-z $val")
done
if ! $skipSetup; then
echo "--- $cloudProvider.sh delete"
# shellcheck disable=SC2068
time net/"$cloudProvider".sh delete ${zone_args[@]} -p "$netName" ${externalNode:+-x}
time net/"$cloudProvider".sh delete -z "$zone" -p "$netName"
if $delete; then
exit 0
fi
@ -173,12 +150,11 @@ if ! $skipSetup; then
echo "--- $cloudProvider.sh create"
create_args=(
-p "$netName"
-z "$zone"
-a "$bootstrapFullNodeAddress"
-c "$clientNodeCount"
-n "$additionalFullNodeCount"
)
# shellcheck disable=SC2206
create_args+=(${zone_args[@]})
if $blockstreamer; then
create_args+=(-u)
@ -204,18 +180,13 @@ if ! $skipSetup; then
create_args+=(-P)
fi
if $externalNode; then
create_args+=(-x)
fi
time net/"$cloudProvider".sh create "${create_args[@]}"
else
echo "--- $cloudProvider.sh config"
config_args=(
-p "$netName"
-z "$zone"
)
# shellcheck disable=SC2206
config_args+=(${zone_args[@]})
if $publicNetwork; then
config_args+=(-P)
fi
@ -247,33 +218,19 @@ if $skipSetup; then
fi
ok=true
if ! $skipStart; then
(
if $skipSetup; then
# TODO: Enable rolling updates
#op=update
op=restart
else
op=start
fi
(
if $skipSetup; then
# TODO: Enable rolling updates
#op=update
op=restart
else
op=start
fi
maybeUpdateManifestKeypairFile=
# shellcheck disable=SC2154 # SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu comes from .buildkite/env/
if [[ -n $SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu ]]; then
echo "$SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu" > update_manifest_keypair.json
maybeUpdateManifestKeypairFile="-i update_manifest_keypair.json"
fi
# shellcheck disable=SC2086 # Don't want to double quote the $maybeXYZ variables
time net/net.sh $op -t "$tarChannelOrTag" \
$maybeUpdateManifestKeypairFile \
$maybeSkipSetup \
$maybeRejectExtraNodes \
$maybeNoValidatorSanity \
$maybeNoLedgerVerify
) || ok=false
net/net.sh logs
fi
# shellcheck disable=SC2086 # Don't want to double quote maybeRejectExtraNodes
time net/net.sh $op -t "$tarChannelOrTag" \
$maybeSkipSetup $maybeRejectExtraNodes $maybeNoValidatorSanity $maybeNoLedgerVerify
) || ok=false
net/net.sh logs
$ok

View File

@ -64,24 +64,25 @@ EOF
exit 0
fi
if [[ -n $TESTNET_DB_HOST ]]; then
SOLANA_METRICS_PARTIAL_CONFIG="host=$TESTNET_DB_HOST,$SOLANA_METRICS_PARTIAL_CONFIG"
fi
export SOLANA_METRICS_CONFIG="db=$TESTNET,$SOLANA_METRICS_PARTIAL_CONFIG"
echo "SOLANA_METRICS_CONFIG: $SOLANA_METRICS_CONFIG"
source scripts/configure-metrics.sh
ci/channel-info.sh
eval "$(ci/channel-info.sh)"
EC2_ZONES=(us-west-1a sa-east-1a ap-northeast-2a eu-central-1a ca-central-1a)
GCE_ZONES=(us-west1-b asia-east2-a europe-west4-a southamerica-east1-b us-east4-c)
case $TESTNET in
testnet-edge|testnet-edge-perf)
CHANNEL_OR_TAG=edge
CHANNEL_BRANCH=$EDGE_CHANNEL
: "${TESTNET_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
;;
testnet-beta|testnet-beta-perf)
CHANNEL_OR_TAG=beta
CHANNEL_BRANCH=$BETA_CHANNEL
: "${TESTNET_DB_HOST:=https://clocktower-f1d56615.influxcloud.net:8086}"
: "${EC2_NODE_COUNT:=10}"
: "${GCE_NODE_COUNT:=}"
;;
testnet|testnet-perf)
CHANNEL_OR_TAG=$STABLE_CHANNEL_LATEST_TAG
@ -93,20 +94,9 @@ testnet|testnet-perf)
;;
esac
if [[ -n $TESTNET_DB_HOST ]]; then
SOLANA_METRICS_PARTIAL_CONFIG="host=$TESTNET_DB_HOST,$SOLANA_METRICS_PARTIAL_CONFIG"
fi
export SOLANA_METRICS_CONFIG="db=$TESTNET,$SOLANA_METRICS_PARTIAL_CONFIG"
echo "SOLANA_METRICS_CONFIG: $SOLANA_METRICS_CONFIG"
source scripts/configure-metrics.sh
if [[ -n $TESTNET_TAG ]]; then
CHANNEL_OR_TAG=$TESTNET_TAG
else
if [[ $BUILDKITE_BRANCH != "$CHANNEL_BRANCH" ]]; then
(
cat <<EOF
if [[ $BUILDKITE_BRANCH != "$CHANNEL_BRANCH" ]]; then
(
cat <<EOF
steps:
- trigger: "$BUILDKITE_PIPELINE_SLUG"
async: true
@ -117,22 +107,19 @@ steps:
TESTNET: "$TESTNET"
TESTNET_OP: "$TESTNET_OP"
TESTNET_DB_HOST: "$TESTNET_DB_HOST"
EC2_NODE_COUNT: "$EC2_NODE_COUNT"
GCE_NODE_COUNT: "$GCE_NODE_COUNT"
EOF
) | buildkite-agent pipeline upload
exit 0
fi
) | buildkite-agent pipeline upload
exit 0
fi
sanity() {
echo "--- sanity $TESTNET"
case $TESTNET in
testnet-edge)
(
set -x
NO_LEDGER_VERIFY=1 \
ci/testnet-sanity.sh edge-testnet-solana-com ec2 us-west-1a
ci/testnet-sanity.sh edge-testnet-solana-com ec2 us-west-1a
)
;;
testnet-edge-perf)
@ -147,22 +134,7 @@ sanity() {
testnet-beta)
(
set -x
ok=true
if [[ -n $EC2_NODE_COUNT ]]; then
NO_LEDGER_VERIFY=1 \
ci/testnet-sanity.sh beta-testnet-solana-com ec2 "${EC2_ZONES[0]}" || ok=false
fi
if $ok && [[ -n $GCE_NODE_COUNT ]]; then
if [[ -n $EC2_NODE_COUNT ]]; then
echo "TODO: Fix testnet-sanity.sh to work with a multinode testnet. It needs an '-x'-like argument"
exit 1
fi
NO_LEDGER_VERIFY=1 \
ci/testnet-sanity.sh beta-testnet-solana-com gce "${GCE_ZONES[0]}" || ok=false
fi
$ok
ci/testnet-sanity.sh beta-testnet-solana-com ec2 us-west-1a
)
;;
testnet-beta-perf)
@ -177,8 +149,7 @@ sanity() {
testnet)
(
set -x
NO_LEDGER_VERIFY=1 \
ci/testnet-sanity.sh testnet-solana-com ec2 us-west-1a
ci/testnet-sanity.sh testnet-solana-com ec2 us-west-1a
#ci/testnet-sanity.sh testnet-solana-com gce us-east1-c
)
;;
@ -213,10 +184,12 @@ start() {
testnet-edge)
(
set -x
ci/testnet-deploy.sh -p edge-testnet-solana-com -C ec2 -z us-west-1a \
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0ccd4f2239886fa94 \
${maybeReuseLedger:+-r} \
${maybeDelete:+-D}
NO_VALIDATOR_SANITY=1 \
RUST_LOG=solana=info \
ci/testnet-deploy.sh edge-testnet-solana-com ec2 us-west-1a \
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0ccd4f2239886fa94 \
${maybeReuseLedger:+-r} \
${maybeDelete:+-D}
)
;;
testnet-edge-perf)
@ -224,8 +197,7 @@ start() {
set -x
NO_LEDGER_VERIFY=1 \
NO_VALIDATOR_SANITY=1 \
RUST_LOG=solana=warn \
ci/testnet-deploy.sh -p edge-perf-testnet-solana-com -C ec2 -z us-west-2b \
ci/testnet-deploy.sh edge-perf-testnet-solana-com ec2 us-west-2b \
-g -t "$CHANNEL_OR_TAG" -c 2 \
-b \
${maybeReuseLedger:+-r} \
@ -235,35 +207,13 @@ start() {
testnet-beta)
(
set -x
# Build an array to pass as opts to testnet-deploy.sh: "-z zone1 -z zone2 ..."
GCE_ZONE_ARGS=()
for val in "${GCE_ZONES[@]}"; do
GCE_ZONE_ARGS+=("-z $val")
done
EC2_ZONE_ARGS=()
for val in "${EC2_ZONES[@]}"; do
EC2_ZONE_ARGS+=("-z $val")
done
if [[ -n $EC2_NODE_COUNT ]]; then
# shellcheck disable=SC2068
ci/testnet-deploy.sh -p beta-testnet-solana-com -C ec2 ${EC2_ZONE_ARGS[@]} \
-t "$CHANNEL_OR_TAG" -n "$EC2_NODE_COUNT" -c 0 -u -P -a eipalloc-0f286cf8a0771ce35 \
NO_VALIDATOR_SANITY=1 \
RUST_LOG=solana=info \
ci/testnet-deploy.sh beta-testnet-solana-com ec2 us-west-1a \
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0f286cf8a0771ce35 \
-b \
${maybeReuseLedger:+-r} \
${maybeDelete:+-D} \
${GCE_NODE_COUNT:+-s}
fi
if [[ -n $GCE_NODE_COUNT ]]; then
# shellcheck disable=SC2068
ci/testnet-deploy.sh -p beta-testnet-solana-com -C gce ${GCE_ZONE_ARGS[@]} \
-t "$CHANNEL_OR_TAG" -n "$GCE_NODE_COUNT" -c 0 -P \
${maybeReuseLedger:+-r} \
${maybeDelete:+-D} \
${EC2_NODE_COUNT:+-x}
fi
${maybeDelete:+-D}
)
;;
testnet-beta-perf)
@ -271,8 +221,7 @@ start() {
set -x
NO_LEDGER_VERIFY=1 \
NO_VALIDATOR_SANITY=1 \
RUST_LOG=solana=warn \
ci/testnet-deploy.sh -p beta-perf-testnet-solana-com -C ec2 -z us-west-2b \
ci/testnet-deploy.sh beta-perf-testnet-solana-com ec2 us-west-2b \
-g -t "$CHANNEL_OR_TAG" -c 2 \
-b \
${maybeReuseLedger:+-r} \
@ -283,12 +232,13 @@ start() {
(
set -x
NO_VALIDATOR_SANITY=1 \
ci/testnet-deploy.sh -p testnet-solana-com -C ec2 -z us-west-1a \
RUST_LOG=solana=info \
ci/testnet-deploy.sh testnet-solana-com ec2 us-west-1a \
-t "$CHANNEL_OR_TAG" -n 3 -c 0 -u -P -a eipalloc-0fa502bf95f6f18b2 \
-b \
${maybeReuseLedger:+-r} \
${maybeDelete:+-D}
#ci/testnet-deploy.sh -p testnet-solana-com -C gce -z us-east1-c \
#ci/testnet-deploy.sh testnet-solana-com gce us-east1-c \
# -t "$CHANNEL_OR_TAG" -n 3 -c 0 -P -a testnet-solana-com \
# ${maybeReuseLedger:+-r} \
# ${maybeDelete:+-D}
@ -299,15 +249,14 @@ start() {
set -x
NO_LEDGER_VERIFY=1 \
NO_VALIDATOR_SANITY=1 \
RUST_LOG=solana=warn \
ci/testnet-deploy.sh -p perf-testnet-solana-com -C gce -z us-west1-b \
ci/testnet-deploy.sh perf-testnet-solana-com gce us-west1-b \
-G "n1-standard-16 --accelerator count=2,type=nvidia-tesla-v100" \
-t "$CHANNEL_OR_TAG" -c 2 \
-b \
-d pd-ssd \
${maybeReuseLedger:+-r} \
${maybeDelete:+-D}
#ci/testnet-deploy.sh -p perf-testnet-solana-com -C ec2 -z us-east-1a \
#ci/testnet-deploy.sh perf-testnet-solana-com ec2 us-east-1a \
# -g \
# -t "$CHANNEL_OR_TAG" -c 2 \
# ${maybeReuseLedger:+-r} \
@ -336,34 +285,16 @@ stop)
stop
;;
update-or-restart)
if start "" update; then
echo Update successful
else
echo "+++ Update failed, restarting the network"
$metricsWriteDatapoint "testnet-manager update-failure=1"
start
fi
echo "+++ Restarting the network"
start
;;
sanity-or-restart)
if sanity; then
echo Pass
else
echo "+++ Sanity failed, updating the network"
echo "+++ Sanity failed, restarting the network"
$metricsWriteDatapoint "testnet-manager sanity-failure=1"
# TODO: Restore attempt to restart the cluster before recreating it
# See https://github.com/solana-labs/solana/issues/3774
if false; then
if start "" update; then
echo Update successful
else
echo "+++ Update failed, restarting the network"
$metricsWriteDatapoint "testnet-manager update-failure=1"
start
fi
else
start
fi
start
fi
;;
esac

View File

@ -11,13 +11,13 @@ usage() {
echo "Error: $*"
fi
cat <<EOF
usage: $0 [name] [cloud] [zone1] ... [zoneN]
usage: $0 [name] [cloud] [zone]
Sanity check a testnet
Sanity check a CD testnet
name - name of the network
cloud - cloud provider to use (gce, ec2)
zone1 .. zoneN - cloud provider zones to check
name - name of the network
cloud - cloud provider to use (gce, ec2)
zone - cloud provider zone of the network
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
metrics
@ -27,10 +27,10 @@ EOF
netName=$1
cloudProvider=$2
zone=$3
[[ -n $netName ]] || usage ""
[[ -n $cloudProvider ]] || usage "Cloud provider not specified"
shift 2
[[ -n $1 ]] || usage "zone1 not specified"
[[ -n $zone ]] || usage "Zone not specified"
shutdown() {
exitcode=$?
@ -48,24 +48,20 @@ shutdown() {
exit $exitcode
}
rm -rf net/{log,-sanity}
rm -f net/config/config
trap shutdown EXIT INT
set -x
for zone in "$@"; do
echo "--- $cloudProvider config [$zone]"
timeout 5m net/"$cloudProvider".sh config -p "$netName" -z "$zone"
net/init-metrics.sh -e
echo "+++ $cloudProvider.sh info"
net/"$cloudProvider".sh info
echo "--- net.sh sanity [$cloudProvider:$zone]"
ok=true
timeout 5m net/net.sh sanity \
${NO_LEDGER_VERIFY:+-o noLedgerVerify} \
${NO_VALIDATOR_SANITY:+-o noValidatorSanity} \
${REJECT_EXTRA_NODES:+-o rejectExtraNodes} \
$zone || ok=false
echo "--- $cloudProvider.sh config"
timeout 5m net/"$cloudProvider".sh config -p "$netName" -z "$zone"
net/init-metrics.sh -e
echo "+++ $cloudProvider.sh info"
net/"$cloudProvider".sh info
echo --- net.sh sanity
ok=true
timeout 5m net/net.sh sanity \
${NO_LEDGER_VERIFY:+-o noLedgerVerify} \
${NO_VALIDATOR_SANITY:+-o noValidatorSanity} \
${REJECT_EXTRA_NODES:+-o rejectExtraNodes} || ok=false
net/net.sh logs
$ok
done
net/net.sh logs
$ok

View File

@ -1,6 +1,6 @@
[package]
name = "solana-client"
version = "0.13.2"
version = "0.12.3"
description = "Solana Client"
authors = ["Solana Maintainers <maintainers@solana.com>"]
repository = "https://github.com/solana-labs/solana"
@ -12,15 +12,13 @@ edition = "2018"
bincode = "1.1.2"
bs58 = "0.2.0"
log = "0.4.2"
jsonrpc-core = "10.1.0"
reqwest = "0.9.11"
serde = "1.0.89"
serde_derive = "1.0.88"
serde_json = "1.0.39"
solana-netutil = { path = "../netutil", version = "0.13.2" }
solana-sdk = { path = "../sdk", version = "0.13.2" }
solana-metrics = { path = "../metrics", version = "0.12.3" }
solana-netutil = { path = "../netutil", version = "0.12.3" }
solana-sdk = { path = "../sdk", version = "0.12.3" }
[dev-dependencies]
jsonrpc-core = "10.1.0"
jsonrpc-http-server = "10.1.0"
solana-logger = { path = "../logger", version = "0.13.2" }
solana-logger = { path = "../logger", version = "0.12.3" }

17
client/src/client.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::thin_client::ThinClient;
use std::net::SocketAddr;
use std::time::Duration;
pub fn create_client((rpc, tpu): (SocketAddr, SocketAddr), range: (u16, u16)) -> ThinClient {
let (_, transactions_socket) = solana_netutil::bind_in_range(range).unwrap();
ThinClient::new(rpc, tpu, transactions_socket)
}
pub fn create_client_with_timeout(
(rpc, tpu): (SocketAddr, SocketAddr),
range: (u16, u16),
timeout: Duration,
) -> ThinClient {
let (_, transactions_socket) = solana_netutil::bind_in_range(range).unwrap();
ThinClient::new_with_timeout(rpc, tpu, transactions_socket, timeout)
}

View File

@ -1,10 +0,0 @@
use crate::rpc_request::RpcRequest;
pub(crate) trait GenericRpcClientRequest {
fn send(
&self,
request: &RpcRequest,
params: Option<serde_json::Value>,
retries: usize,
) -> Result<serde_json::Value, Box<dyn std::error::Error>>;
}

View File

@ -1,6 +1,4 @@
mod generic_rpc_client_request;
pub mod mock_rpc_client_request;
pub mod rpc_client;
pub mod rpc_client_request;
pub mod client;
pub mod rpc_mock;
pub mod rpc_request;
pub mod thin_client;

View File

@ -1,63 +0,0 @@
use crate::generic_rpc_client_request::GenericRpcClientRequest;
use crate::rpc_request::RpcRequest;
use serde_json::{Number, Value};
use solana_sdk::transaction::{self, TransactionError};
pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
pub const SIGNATURE: &str =
"43yNSFC6fYTuPgTNFFhF4axw7AfWxB2BPdurme8yrsWEYwm8299xh8n6TAHjGymiSub1XtyxTNyd9GBfY2hxoBw8";
pub struct MockRpcClientRequest {
url: String,
}
impl MockRpcClientRequest {
pub fn new(url: String) -> Self {
Self { url }
}
}
impl GenericRpcClientRequest for MockRpcClientRequest {
fn send(
&self,
request: &RpcRequest,
params: Option<serde_json::Value>,
_retries: usize,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
if self.url == "fails" {
return Ok(Value::Null);
}
let val = match request {
RpcRequest::ConfirmTransaction => {
if let Some(Value::Array(param_array)) = params {
if let Value::String(param_string) = &param_array[0] {
Value::Bool(param_string == SIGNATURE)
} else {
Value::Null
}
} else {
Value::Null
}
}
RpcRequest::GetBalance => {
let n = if self.url == "airdrop" { 0 } else { 50 };
Value::Number(Number::from(n))
}
RpcRequest::GetRecentBlockhash => Value::String(PUBKEY.to_string()),
RpcRequest::GetSignatureStatus => {
let response: Option<transaction::Result<()>> = if self.url == "account_in_use" {
Some(Err(TransactionError::AccountInUse))
} else if self.url == "sig_not_found" {
None
} else {
Some(Ok(()))
};
serde_json::to_value(response).unwrap()
}
RpcRequest::GetTransactionCount => Value::Number(Number::from(1234)),
RpcRequest::SendTransaction => Value::String(SIGNATURE.to_string()),
_ => Value::Null,
};
Ok(val)
}
}

View File

@ -1,746 +0,0 @@
use crate::generic_rpc_client_request::GenericRpcClientRequest;
use crate::mock_rpc_client_request::MockRpcClientRequest;
use crate::rpc_client_request::RpcClientRequest;
use crate::rpc_request::RpcRequest;
use bincode::serialize;
use bs58;
use log::*;
use serde_json::{json, Value};
use solana_sdk::account::Account;
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
use solana_sdk::transaction::{self, Transaction, TransactionError};
use std::error;
use std::io;
use std::net::SocketAddr;
use std::thread::sleep;
use std::time::{Duration, Instant};
pub struct RpcClient {
client: Box<GenericRpcClientRequest>,
}
impl RpcClient {
pub fn new(url: String) -> Self {
Self {
client: Box::new(RpcClientRequest::new(url)),
}
}
pub fn new_mock(url: String) -> Self {
Self {
client: Box::new(MockRpcClientRequest::new(url)),
}
}
pub fn new_socket(addr: SocketAddr) -> Self {
Self::new(get_rpc_request_str(addr, false))
}
pub fn new_socket_with_timeout(addr: SocketAddr, timeout: Duration) -> Self {
let url = get_rpc_request_str(addr, false);
Self {
client: Box::new(RpcClientRequest::new_with_timeout(url, timeout)),
}
}
pub fn send_transaction(
&self,
transaction: &Transaction,
) -> Result<String, Box<dyn error::Error>> {
let serialized = serialize(transaction).unwrap();
let params = json!([serialized]);
let signature = self
.client
.send(&RpcRequest::SendTransaction, Some(params), 5)?;
if signature.as_str().is_none() {
Err(io::Error::new(
io::ErrorKind::Other,
"Received result of an unexpected type",
))?;
}
Ok(signature.as_str().unwrap().to_string())
}
pub fn get_signature_status(
&self,
signature: &str,
) -> Result<Option<transaction::Result<()>>, Box<dyn error::Error>> {
let params = json!([signature.to_string()]);
let signature_status =
self.client
.send(&RpcRequest::GetSignatureStatus, Some(params), 5)?;
let result: Option<transaction::Result<()>> =
serde_json::from_value(signature_status).unwrap();
Ok(result)
}
pub fn send_and_confirm_transaction<T: KeypairUtil>(
&self,
transaction: &mut Transaction,
signer: &T,
) -> Result<String, Box<dyn error::Error>> {
let mut send_retries = 5;
loop {
let mut status_retries = 4;
let signature_str = self.send_transaction(transaction)?;
let status = loop {
let status = self.get_signature_status(&signature_str)?;
if status.is_none() {
status_retries -= 1;
if status_retries == 0 {
break status;
}
} else {
break status;
}
if cfg!(not(test)) {
// Retry ~twice during a slot
sleep(Duration::from_millis(
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
));
}
};
send_retries = if let Some(result) = status.clone() {
match result {
Ok(_) => return Ok(signature_str),
Err(TransactionError::AccountInUse) => {
// Fetch a new blockhash and re-sign the transaction before sending it again
self.resign_transaction(transaction, signer)?;
send_retries - 1
}
Err(_) => 0,
}
} else {
send_retries - 1
};
if send_retries == 0 {
Err(io::Error::new(
io::ErrorKind::Other,
format!("Transaction {:?} failed: {:?}", signature_str, status),
))?;
}
}
}
pub fn send_and_confirm_transactions(
&self,
mut transactions: Vec<Transaction>,
signer: &Keypair,
) -> Result<(), Box<dyn error::Error>> {
let mut send_retries = 5;
loop {
let mut status_retries = 4;
// Send all transactions
let mut transactions_signatures = vec![];
for transaction in transactions {
if cfg!(not(test)) {
// Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors
// when all the write transactions modify the same program account (eg, deploying a
// new program)
sleep(Duration::from_millis(1000 / NUM_TICKS_PER_SECOND));
}
let signature = self.send_transaction(&transaction).ok();
transactions_signatures.push((transaction, signature))
}
// Collect statuses for all the transactions, drop those that are confirmed
while status_retries > 0 {
status_retries -= 1;
if cfg!(not(test)) {
// Retry ~twice during a slot
sleep(Duration::from_millis(
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
));
}
transactions_signatures = transactions_signatures
.into_iter()
.filter(|(_transaction, signature)| {
if let Some(signature) = signature {
if let Ok(status) = self.get_signature_status(&signature) {
if status.is_none() {
return false;
}
return status.unwrap().is_err();
}
}
true
})
.collect();
if transactions_signatures.is_empty() {
return Ok(());
}
}
if send_retries == 0 {
Err(io::Error::new(io::ErrorKind::Other, "Transactions failed"))?;
}
send_retries -= 1;
// Re-sign any failed transactions with a new blockhash and retry
let blockhash =
self.get_new_blockhash(&transactions_signatures[0].0.message().recent_blockhash)?;
transactions = transactions_signatures
.into_iter()
.map(|(mut transaction, _)| {
transaction.sign(&[signer], blockhash);
transaction
})
.collect();
}
}
pub fn resign_transaction<T: KeypairUtil>(
&self,
tx: &mut Transaction,
signer_key: &T,
) -> Result<(), Box<dyn error::Error>> {
let blockhash = self.get_new_blockhash(&tx.message().recent_blockhash)?;
tx.sign(&[signer_key], blockhash);
Ok(())
}
pub fn retry_get_balance(
&self,
pubkey: &Pubkey,
retries: usize,
) -> Result<Option<u64>, Box<dyn error::Error>> {
let params = json!([format!("{}", pubkey)]);
let res = self
.client
.send(&RpcRequest::GetBalance, Some(params), retries)?
.as_u64();
Ok(res)
}
pub fn get_account_data(&self, pubkey: &Pubkey) -> io::Result<Vec<u8>> {
let params = json!([format!("{}", pubkey)]);
let response = self
.client
.send(&RpcRequest::GetAccountInfo, Some(params), 0);
match response {
Ok(account_json) => {
let account: Account =
serde_json::from_value(account_json).expect("deserialize account");
Ok(account.data)
}
Err(error) => {
debug!("get_account_data failed: {:?}", error);
Err(io::Error::new(
io::ErrorKind::Other,
"get_account_data failed",
))
}
}
}
/// Request the balance of the user holding `pubkey`. This method blocks
/// until the server sends a response. If the response packet is dropped
/// by the network, this method will hang indefinitely.
pub fn get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
let params = json!([format!("{}", pubkey)]);
let response = self
.client
.send(&RpcRequest::GetAccountInfo, Some(params), 0);
response
.and_then(|account_json| {
let account: Account =
serde_json::from_value(account_json).expect("deserialize account");
trace!("Response account {:?} {:?}", pubkey, account);
trace!("get_balance {:?}", account.lamports);
Ok(account.lamports)
})
.map_err(|error| {
debug!("Response account {}: None (error: {:?})", pubkey, error);
io::Error::new(io::ErrorKind::Other, "AccountNotFound")
})
}
/// Request the transaction count. If the response packet is dropped by the network,
/// this method will try again 5 times.
pub fn get_transaction_count(&self) -> io::Result<u64> {
debug!("get_transaction_count");
let mut num_retries = 5;
while num_retries > 0 {
let response = self.client.send(&RpcRequest::GetTransactionCount, None, 0);
match response {
Ok(value) => {
debug!("transaction_count response: {:?}", value);
if let Some(transaction_count) = value.as_u64() {
return Ok(transaction_count);
}
}
Err(err) => {
debug!("transaction_count failed: {:?}", err);
}
}
num_retries -= 1;
}
Err(io::Error::new(
io::ErrorKind::Other,
"Unable to get transaction count, too many retries",
))?
}
pub fn get_recent_blockhash(&self) -> io::Result<Hash> {
let mut num_retries = 5;
while num_retries > 0 {
match self.client.send(&RpcRequest::GetRecentBlockhash, None, 0) {
Ok(value) => {
if let Some(blockhash_str) = value.as_str() {
let blockhash_vec = bs58::decode(blockhash_str)
.into_vec()
.expect("bs58::decode");
return Ok(Hash::new(&blockhash_vec));
}
}
Err(err) => {
debug!("retry_get_recent_blockhash failed: {:?}", err);
}
}
num_retries -= 1;
}
Err(io::Error::new(
io::ErrorKind::Other,
"Unable to get recent blockhash, too many retries",
))
}
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<Hash> {
let mut num_retries = 10;
while num_retries > 0 {
if let Ok(new_blockhash) = self.get_recent_blockhash() {
if new_blockhash != *blockhash {
return Ok(new_blockhash);
}
}
debug!("Got same blockhash ({:?}), will retry...", blockhash);
// Retry ~twice during a slot
sleep(Duration::from_millis(
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
));
num_retries -= 1;
}
Err(io::Error::new(
io::ErrorKind::Other,
"Unable to get new blockhash, too many retries",
))
}
pub fn poll_balance_with_timeout(
&self,
pubkey: &Pubkey,
polling_frequency: &Duration,
timeout: &Duration,
) -> io::Result<u64> {
let now = Instant::now();
loop {
match self.get_balance(&pubkey) {
Ok(bal) => {
return Ok(bal);
}
Err(e) => {
sleep(*polling_frequency);
if now.elapsed() > *timeout {
return Err(e);
}
}
};
}
}
pub fn poll_get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
self.poll_balance_with_timeout(pubkey, &Duration::from_millis(100), &Duration::from_secs(1))
}
pub fn wait_for_balance(&self, pubkey: &Pubkey, expected_balance: Option<u64>) -> Option<u64> {
const LAST: usize = 30;
for run in 0..LAST {
let balance_result = self.poll_get_balance(pubkey);
if expected_balance.is_none() {
return balance_result.ok();
}
trace!(
"retry_get_balance[{}] {:?} {:?}",
run,
balance_result,
expected_balance
);
if let (Some(expected_balance), Ok(balance_result)) = (expected_balance, balance_result)
{
if expected_balance == balance_result {
return Some(balance_result);
}
}
}
None
}
/// Poll the server to confirm a transaction.
pub fn poll_for_signature(&self, signature: &Signature) -> io::Result<()> {
let now = Instant::now();
while !self.check_signature(signature) {
if now.elapsed().as_secs() > 15 {
// TODO: Return a better error.
return Err(io::Error::new(io::ErrorKind::Other, "signature not found"));
}
sleep(Duration::from_millis(250));
}
Ok(())
}
/// Check a signature in the bank.
pub fn check_signature(&self, signature: &Signature) -> bool {
trace!("check_signature: {:?}", signature);
let params = json!([format!("{}", signature)]);
for _ in 0..30 {
let response =
self.client
.send(&RpcRequest::ConfirmTransaction, Some(params.clone()), 0);
match response {
Ok(confirmation) => {
let signature_status = confirmation.as_bool().unwrap();
if signature_status {
trace!("Response found signature");
} else {
trace!("Response signature not found");
}
return signature_status;
}
Err(err) => {
debug!("check_signature request failed: {:?}", err);
}
};
sleep(Duration::from_millis(250));
}
panic!("Couldn't check signature of {}", signature);
}
/// Poll the server to confirm a transaction.
pub fn poll_for_signature_confirmation(
&self,
signature: &Signature,
min_confirmed_blocks: usize,
) -> io::Result<()> {
let mut now = Instant::now();
let mut confirmed_blocks = 0;
loop {
let response = self.get_num_blocks_since_signature_confirmation(signature);
match response {
Ok(count) => {
if confirmed_blocks != count {
info!(
"signature {} confirmed {} out of {}",
signature, count, min_confirmed_blocks
);
now = Instant::now();
confirmed_blocks = count;
}
if count >= min_confirmed_blocks {
break;
}
}
Err(err) => {
debug!("check_confirmations request failed: {:?}", err);
}
};
if now.elapsed().as_secs() > 15 {
// TODO: Return a better error.
return Err(io::Error::new(io::ErrorKind::Other, "signature not found"));
}
sleep(Duration::from_millis(250));
}
Ok(())
}
pub fn get_num_blocks_since_signature_confirmation(
&self,
sig: &Signature,
) -> io::Result<usize> {
let params = json!([format!("{}", sig)]);
let response = self
.client
.send(
&RpcRequest::GetNumBlocksSinceSignatureConfirmation,
Some(params.clone()),
1,
)
.map_err(|error| {
debug!(
"Response get_num_blocks_since_signature_confirmation: {}",
error
);
io::Error::new(
io::ErrorKind::Other,
"GetNumBlocksSinceSignatureConfirmation request failure",
)
})?;
serde_json::from_value(response).map_err(|error| {
debug!(
"ParseError: get_num_blocks_since_signature_confirmation: {}",
error
);
io::Error::new(
io::ErrorKind::Other,
"GetNumBlocksSinceSignatureConfirmation parse failure",
)
})
}
pub fn fullnode_exit(&self) -> io::Result<bool> {
let response = self
.client
.send(&RpcRequest::FullnodeExit, None, 0)
.map_err(|err| {
io::Error::new(
io::ErrorKind::Other,
format!("FullnodeExit request failure: {:?}", err),
)
})?;
serde_json::from_value(response).map_err(|err| {
io::Error::new(
io::ErrorKind::Other,
format!("FullnodeExit parse failure: {:?}", err),
)
})
}
// TODO: Remove
pub fn retry_make_rpc_request(
&self,
request: &RpcRequest,
params: Option<Value>,
retries: usize,
) -> Result<Value, Box<dyn error::Error>> {
self.client.send(request, params, retries)
}
}
pub fn get_rpc_request_str(rpc_addr: SocketAddr, tls: bool) -> String {
if tls {
format!("https://{}", rpc_addr)
} else {
format!("http://{}", rpc_addr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock_rpc_client_request::{PUBKEY, SIGNATURE};
use jsonrpc_core::{Error, IoHandler, Params};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use serde_json::Number;
use solana_logger;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction;
use solana_sdk::transaction::TransactionError;
use std::sync::mpsc::channel;
use std::thread;
#[test]
fn test_make_rpc_request() {
let (sender, receiver) = channel();
thread::spawn(move || {
let rpc_addr = "0.0.0.0:0".parse().unwrap();
let mut io = IoHandler::default();
// Successful request
io.add_method("getBalance", |_params: Params| {
Ok(Value::Number(Number::from(50)))
});
// Failed request
io.add_method("getRecentBlockhash", |params: Params| {
if params != Params::None {
Err(Error::invalid_request())
} else {
Ok(Value::String(
"deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx".to_string(),
))
}
});
let server = ServerBuilder::new(io)
.threads(1)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Any,
]))
.start_http(&rpc_addr)
.expect("Unable to start RPC server");
sender.send(*server.address()).unwrap();
server.wait();
});
let rpc_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(rpc_addr);
let balance = rpc_client.retry_make_rpc_request(
&RpcRequest::GetBalance,
Some(json!(["deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx"])),
0,
);
assert_eq!(balance.unwrap().as_u64().unwrap(), 50);
let blockhash = rpc_client.retry_make_rpc_request(&RpcRequest::GetRecentBlockhash, None, 0);
assert_eq!(
blockhash.unwrap().as_str().unwrap(),
"deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx"
);
// Send erroneous parameter
let blockhash = rpc_client.retry_make_rpc_request(
&RpcRequest::GetRecentBlockhash,
Some(json!("paramter")),
0,
);
assert_eq!(blockhash.is_err(), true);
}
#[test]
fn test_retry_make_rpc_request() {
solana_logger::setup();
let (sender, receiver) = channel();
thread::spawn(move || {
// 1. Pick a random port
// 2. Tell the client to start using it
// 3. Delay for 1.5 seconds before starting the server to ensure the client will fail
// and need to retry
let rpc_addr: SocketAddr = "0.0.0.0:4242".parse().unwrap();
sender.send(rpc_addr.clone()).unwrap();
sleep(Duration::from_millis(1500));
let mut io = IoHandler::default();
io.add_method("getBalance", move |_params: Params| {
Ok(Value::Number(Number::from(5)))
});
let server = ServerBuilder::new(io)
.threads(1)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Any,
]))
.start_http(&rpc_addr)
.expect("Unable to start RPC server");
server.wait();
});
let rpc_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(rpc_addr);
let balance = rpc_client.retry_make_rpc_request(
&RpcRequest::GetBalance,
Some(json!(["deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhw"])),
10,
);
assert_eq!(balance.unwrap().as_u64().unwrap(), 5);
}
#[test]
fn test_send_transaction() {
let rpc_client = RpcClient::new_mock("succeeds".to_string());
let key = Keypair::new();
let to = Pubkey::new_rand();
let blockhash = Hash::default();
let tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
let signature = rpc_client.send_transaction(&tx);
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
let rpc_client = RpcClient::new_mock("fails".to_string());
let signature = rpc_client.send_transaction(&tx);
assert!(signature.is_err());
}
#[test]
fn test_get_recent_blockhash() {
let rpc_client = RpcClient::new_mock("succeeds".to_string());
let vec = bs58::decode(PUBKEY).into_vec().unwrap();
let expected_blockhash = Hash::new(&vec);
let blockhash = dbg!(rpc_client.get_recent_blockhash()).expect("blockhash ok");
assert_eq!(blockhash, expected_blockhash);
let rpc_client = RpcClient::new_mock("fails".to_string());
let blockhash = dbg!(rpc_client.get_recent_blockhash());
assert!(blockhash.is_err());
}
#[test]
fn test_get_signature_status() {
let rpc_client = RpcClient::new_mock("succeeds".to_string());
let signature = "good_signature";
let status = rpc_client.get_signature_status(&signature).unwrap();
assert_eq!(status, Some(Ok(())));
let rpc_client = RpcClient::new_mock("sig_not_found".to_string());
let signature = "sig_not_found";
let status = rpc_client.get_signature_status(&signature).unwrap();
assert_eq!(status, None);
let rpc_client = RpcClient::new_mock("account_in_use".to_string());
let signature = "account_in_use";
let status = rpc_client.get_signature_status(&signature).unwrap();
assert_eq!(status, Some(Err(TransactionError::AccountInUse)));
}
#[test]
fn test_send_and_confirm_transaction() {
let rpc_client = RpcClient::new_mock("succeeds".to_string());
let key = Keypair::new();
let to = Pubkey::new_rand();
let blockhash = Hash::default();
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
result.unwrap();
let rpc_client = RpcClient::new_mock("account_in_use".to_string());
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
assert!(result.is_err());
let rpc_client = RpcClient::new_mock("fails".to_string());
let result = rpc_client.send_and_confirm_transaction(&mut tx, &key);
assert!(result.is_err());
}
#[test]
fn test_resign_transaction() {
let rpc_client = RpcClient::new_mock("succeeds".to_string());
let key = Keypair::new();
let to = Pubkey::new_rand();
let vec = bs58::decode("HUu3LwEzGRsUkuJS121jzkPJW39Kq62pXCTmTa1F9jDL")
.into_vec()
.unwrap();
let blockhash = Hash::new(&vec);
let prev_tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
let mut tx = system_transaction::create_user_account(&key, &to, 50, blockhash, 0);
rpc_client.resign_transaction(&mut tx, &key).unwrap();
assert_ne!(prev_tx, tx);
assert_ne!(prev_tx.signatures, tx.signatures);
assert_ne!(
prev_tx.message().recent_blockhash,
tx.message().recent_blockhash
);
}
}

View File

@ -1,81 +0,0 @@
use crate::generic_rpc_client_request::GenericRpcClientRequest;
use crate::rpc_request::{RpcError, RpcRequest};
use log::*;
use reqwest;
use reqwest::header::CONTENT_TYPE;
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
use std::thread::sleep;
use std::time::Duration;
pub struct RpcClientRequest {
client: reqwest::Client,
url: String,
}
impl RpcClientRequest {
pub fn new(url: String) -> Self {
Self {
client: reqwest::Client::new(),
url,
}
}
pub fn new_with_timeout(url: String, timeout: Duration) -> Self {
let client = reqwest::Client::builder()
.timeout(timeout)
.build()
.expect("build rpc client");
Self { client, url }
}
}
impl GenericRpcClientRequest for RpcClientRequest {
fn send(
&self,
request: &RpcRequest,
params: Option<serde_json::Value>,
mut retries: usize,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
// Concurrent requests are not supported so reuse the same request id for all requests
let request_id = 1;
let request_json = request.build_request_json(request_id, params);
loop {
match self
.client
.post(&self.url)
.header(CONTENT_TYPE, "application/json")
.body(request_json.to_string())
.send()
{
Ok(mut response) => {
let json: serde_json::Value = serde_json::from_str(&response.text()?)?;
if json["error"].is_object() {
Err(RpcError::RpcRequestError(format!(
"RPC Error response: {}",
serde_json::to_string(&json["error"]).unwrap()
)))?
}
return Ok(json["result"].clone());
}
Err(e) => {
info!(
"make_rpc_request() failed, {} retries left: {:?}",
retries, e
);
if retries == 0 {
Err(e)?;
}
retries -= 1;
// Sleep for approximately half a slot
sleep(Duration::from_millis(
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
));
}
}
}
}
}

111
client/src/rpc_mock.rs Normal file
View File

@ -0,0 +1,111 @@
// Implementation of RpcRequestHandler trait for testing Rpc requests without i/o
use crate::rpc_request::{RpcRequest, RpcRequestHandler};
use serde_json::{json, Number, Value};
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction::SystemTransaction;
use solana_sdk::transaction::Transaction;
use std::error;
use std::io::{Error, ErrorKind};
use std::net::SocketAddr;
pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
pub const SIGNATURE: &str =
"43yNSFC6fYTuPgTNFFhF4axw7AfWxB2BPdurme8yrsWEYwm8299xh8n6TAHjGymiSub1XtyxTNyd9GBfY2hxoBw8";
#[derive(Clone)]
pub struct MockRpcClient {
pub addr: String,
}
impl MockRpcClient {
pub fn new(addr: String) -> Self {
MockRpcClient { addr }
}
pub fn retry_get_balance(
&self,
id: u64,
pubkey: &Pubkey,
retries: usize,
) -> Result<Option<u64>, Box<dyn error::Error>> {
let params = json!([format!("{}", pubkey)]);
let res = self
.retry_make_rpc_request(id, &RpcRequest::GetBalance, Some(params), retries)?
.as_u64();
Ok(res)
}
pub fn retry_make_rpc_request(
&self,
_id: u64,
request: &RpcRequest,
params: Option<Value>,
mut _retries: usize,
) -> Result<Value, Box<dyn error::Error>> {
if self.addr == "fails" {
return Ok(Value::Null);
}
let val = match request {
RpcRequest::ConfirmTransaction => {
if let Some(Value::Array(param_array)) = params {
if let Value::String(param_string) = &param_array[0] {
Value::Bool(param_string == SIGNATURE)
} else {
Value::Null
}
} else {
Value::Null
}
}
RpcRequest::GetBalance => {
let n = if self.addr == "airdrop" { 0 } else { 50 };
Value::Number(Number::from(n))
}
RpcRequest::GetRecentBlockhash => Value::String(PUBKEY.to_string()),
RpcRequest::GetSignatureStatus => {
let str = if self.addr == "account_in_use" {
"AccountInUse"
} else if self.addr == "bad_sig_status" {
"Nonexistent"
} else {
"Confirmed"
};
Value::String(str.to_string())
}
RpcRequest::GetTransactionCount => Value::Number(Number::from(1234)),
RpcRequest::SendTransaction => Value::String(SIGNATURE.to_string()),
_ => Value::Null,
};
Ok(val)
}
}
impl RpcRequestHandler for MockRpcClient {
fn make_rpc_request(
&self,
id: u64,
request: RpcRequest,
params: Option<Value>,
) -> Result<Value, Box<dyn error::Error>> {
self.retry_make_rpc_request(id, &request, params, 0)
}
}
pub fn request_airdrop_transaction(
_drone_addr: &SocketAddr,
_id: &Pubkey,
lamports: u64,
_blockhash: Hash,
) -> Result<Transaction, Error> {
if lamports == 0 {
Err(Error::new(ErrorKind::Other, "Airdrop failed"))?
}
let key = Keypair::new();
let to = Keypair::new().pubkey();
let blockhash = Hash::default();
let tx = SystemTransaction::new_account(&key, &to, lamports, blockhash, 0);
Ok(tx)
}

View File

@ -1,6 +1,130 @@
use log::*;
use reqwest;
use reqwest::header::CONTENT_TYPE;
use serde_json::{json, Value};
use solana_sdk::timing::{DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND};
use std::net::SocketAddr;
use std::thread::sleep;
use std::time::Duration;
use std::{error, fmt};
use solana_sdk::pubkey::Pubkey;
#[derive(Clone)]
pub struct RpcClient {
pub client: reqwest::Client,
pub addr: String,
}
impl RpcClient {
pub fn new(addr: String) -> Self {
RpcClient {
client: reqwest::Client::new(),
addr,
}
}
pub fn new_with_timeout(addr: SocketAddr, timeout: Duration) -> Self {
let addr = get_rpc_request_str(addr, false);
let client = reqwest::Client::builder()
.timeout(timeout)
.build()
.expect("build rpc client");
RpcClient { client, addr }
}
pub fn new_from_socket(addr: SocketAddr) -> Self {
Self::new(get_rpc_request_str(addr, false))
}
pub fn retry_get_balance(
&self,
id: u64,
pubkey: &Pubkey,
retries: usize,
) -> Result<Option<u64>, Box<dyn error::Error>> {
let params = json!([format!("{}", pubkey)]);
let res = self
.retry_make_rpc_request(id, &RpcRequest::GetBalance, Some(params), retries)?
.as_u64();
Ok(res)
}
pub fn retry_make_rpc_request(
&self,
id: u64,
request: &RpcRequest,
params: Option<Value>,
mut retries: usize,
) -> Result<Value, Box<dyn error::Error>> {
let request_json = request.build_request_json(id, params);
loop {
match self
.client
.post(&self.addr)
.header(CONTENT_TYPE, "application/json")
.body(request_json.to_string())
.send()
{
Ok(mut response) => {
let json: Value = serde_json::from_str(&response.text()?)?;
if json["error"].is_object() {
Err(RpcError::RpcRequestError(format!(
"RPC Error response: {}",
serde_json::to_string(&json["error"]).unwrap()
)))?
}
return Ok(json["result"].clone());
}
Err(e) => {
info!(
"make_rpc_request() failed, {} retries left: {:?}",
retries, e
);
if retries == 0 {
Err(e)?;
}
retries -= 1;
// Sleep for approximately half a slot
sleep(Duration::from_millis(
500 * DEFAULT_TICKS_PER_SLOT / NUM_TICKS_PER_SECOND,
));
}
}
}
}
}
pub fn get_rpc_request_str(rpc_addr: SocketAddr, tls: bool) -> String {
if tls {
format!("https://{}", rpc_addr)
} else {
format!("http://{}", rpc_addr)
}
}
pub trait RpcRequestHandler {
fn make_rpc_request(
&self,
id: u64,
request: RpcRequest,
params: Option<Value>,
) -> Result<Value, Box<dyn error::Error>>;
}
impl RpcRequestHandler for RpcClient {
fn make_rpc_request(
&self,
id: u64,
request: RpcRequest,
params: Option<Value>,
) -> Result<Value, Box<dyn error::Error>> {
self.retry_make_rpc_request(id, &request, params, 0)
}
}
#[derive(Debug, PartialEq)]
pub enum RpcRequest {
ConfirmTransaction,
@ -18,11 +142,10 @@ pub enum RpcRequest {
GetStorageEntryHeight,
GetStoragePubkeysForEntryHeight,
FullnodeExit,
GetNumBlocksSinceSignatureConfirmation,
}
impl RpcRequest {
pub(crate) fn build_request_json(&self, id: u64, params: Option<Value>) -> Value {
fn build_request_json(&self, id: u64, params: Option<Value>) -> Value {
let jsonrpc = "2.0";
let method = match self {
RpcRequest::ConfirmTransaction => "confirmTransaction",
@ -40,9 +163,6 @@ impl RpcRequest {
RpcRequest::GetStorageEntryHeight => "getStorageEntryHeight",
RpcRequest::GetStoragePubkeysForEntryHeight => "getStoragePubkeysForEntryHeight",
RpcRequest::FullnodeExit => "fullnodeExit",
RpcRequest::GetNumBlocksSinceSignatureConfirmation => {
"getNumBlocksSinceSignatureConfirmation"
}
};
let mut request = json!({
"jsonrpc": jsonrpc,
@ -81,6 +201,12 @@ impl error::Error for RpcError {
#[cfg(test)]
mod tests {
use super::*;
use jsonrpc_core::{Error, IoHandler, Params};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use serde_json::Number;
use solana_logger;
use std::sync::mpsc::channel;
use std::thread;
#[test]
fn test_build_request_json() {
@ -110,4 +236,96 @@ mod tests {
let request = test_request.build_request_json(1, None);
assert_eq!(request["method"], "sendTransaction");
}
#[test]
fn test_make_rpc_request() {
let (sender, receiver) = channel();
thread::spawn(move || {
let rpc_addr = "0.0.0.0:0".parse().unwrap();
let mut io = IoHandler::default();
// Successful request
io.add_method("getBalance", |_params: Params| {
Ok(Value::Number(Number::from(50)))
});
// Failed request
io.add_method("getRecentBlockhash", |params: Params| {
if params != Params::None {
Err(Error::invalid_request())
} else {
Ok(Value::String(
"deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx".to_string(),
))
}
});
let server = ServerBuilder::new(io)
.threads(1)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Any,
]))
.start_http(&rpc_addr)
.expect("Unable to start RPC server");
sender.send(*server.address()).unwrap();
server.wait();
});
let rpc_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_from_socket(rpc_addr);
let balance = rpc_client.make_rpc_request(
1,
RpcRequest::GetBalance,
Some(json!(["deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx"])),
);
assert_eq!(balance.unwrap().as_u64().unwrap(), 50);
let blockhash = rpc_client.make_rpc_request(2, RpcRequest::GetRecentBlockhash, None);
assert_eq!(
blockhash.unwrap().as_str().unwrap(),
"deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhx"
);
// Send erroneous parameter
let blockhash =
rpc_client.make_rpc_request(3, RpcRequest::GetRecentBlockhash, Some(json!("paramter")));
assert_eq!(blockhash.is_err(), true);
}
#[test]
fn test_retry_make_rpc_request() {
solana_logger::setup();
let (sender, receiver) = channel();
thread::spawn(move || {
// 1. Pick a random port
// 2. Tell the client to start using it
// 3. Delay for 1.5 seconds before starting the server to ensure the client will fail
// and need to retry
let rpc_addr: SocketAddr = "0.0.0.0:4242".parse().unwrap();
sender.send(rpc_addr.clone()).unwrap();
sleep(Duration::from_millis(1500));
let mut io = IoHandler::default();
io.add_method("getBalance", move |_params: Params| {
Ok(Value::Number(Number::from(5)))
});
let server = ServerBuilder::new(io)
.threads(1)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Any,
]))
.start_http(&rpc_addr)
.expect("Unable to start RPC server");
server.wait();
});
let rpc_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_from_socket(rpc_addr);
let balance = rpc_client.retry_make_rpc_request(
1,
&RpcRequest::GetBalance,
Some(json!(["deadbeefXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNHhw"])),
10,
);
assert_eq!(balance.unwrap().as_u64().unwrap(), 5);
}
}

View File

@ -3,25 +3,31 @@
//! messages to the network directly. The binary encoding of its messages are
//! unstable and may change in future releases.
use crate::rpc_client::RpcClient;
use bincode::{serialize_into, serialized_size};
use crate::rpc_request::{RpcClient, RpcRequest, RpcRequestHandler};
use bincode::serialize_into;
use bs58;
use log::*;
use solana_sdk::client::{AsyncClient, Client, SyncClient};
use serde_json::json;
use solana_metrics;
use solana_metrics::influxdb;
use solana_sdk::account::Account;
use solana_sdk::hash::Hash;
use solana_sdk::instruction::Instruction;
use solana_sdk::message::Message;
use solana_sdk::packet::PACKET_DATA_SIZE;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
use solana_sdk::system_instruction;
use solana_sdk::transaction::{self, Transaction};
use solana_sdk::transport::Result as TransportResult;
use solana_sdk::system_transaction::SystemTransaction;
use solana_sdk::timing;
use solana_sdk::transaction::Transaction;
use std;
use std::io;
use std::net::{SocketAddr, UdpSocket};
use std::thread::sleep;
use std::time::Duration;
use std::time::Instant;
/// An object for querying and sending transactions to the network.
pub struct ThinClient {
rpc_addr: SocketAddr,
transactions_addr: SocketAddr,
transactions_socket: UdpSocket,
rpc_client: RpcClient,
@ -36,207 +42,41 @@ impl ThinClient {
transactions_socket: UdpSocket,
) -> Self {
Self::new_from_client(
rpc_addr,
transactions_addr,
transactions_socket,
RpcClient::new_socket(rpc_addr),
RpcClient::new_from_socket(rpc_addr),
)
}
pub fn new_socket_with_timeout(
pub fn new_with_timeout(
rpc_addr: SocketAddr,
transactions_addr: SocketAddr,
transactions_socket: UdpSocket,
timeout: Duration,
) -> Self {
let rpc_client = RpcClient::new_socket_with_timeout(rpc_addr, timeout);
Self::new_from_client(transactions_addr, transactions_socket, rpc_client)
let rpc_client = RpcClient::new_with_timeout(rpc_addr, timeout);
Self::new_from_client(rpc_addr, transactions_addr, transactions_socket, rpc_client)
}
fn new_from_client(
rpc_addr: SocketAddr,
transactions_addr: SocketAddr,
transactions_socket: UdpSocket,
rpc_client: RpcClient,
) -> Self {
Self {
ThinClient {
rpc_client,
rpc_addr,
transactions_addr,
transactions_socket,
}
}
/// Retry a sending a signed Transaction to the server for processing.
pub fn retry_transfer_until_confirmed(
&self,
keypair: &Keypair,
transaction: &mut Transaction,
tries: usize,
min_confirmed_blocks: usize,
) -> io::Result<Signature> {
self.send_and_confirm_transaction(&[keypair], transaction, tries, min_confirmed_blocks)
}
/// Retry sending a signed Transaction with one signing Keypair to the server for processing.
pub fn retry_transfer(
&self,
keypair: &Keypair,
transaction: &mut Transaction,
tries: usize,
) -> io::Result<Signature> {
self.send_and_confirm_transaction(&[keypair], transaction, tries, 0)
}
/// Retry sending a signed Transaction to the server for processing
pub fn send_and_confirm_transaction(
&self,
keypairs: &[&Keypair],
transaction: &mut Transaction,
tries: usize,
min_confirmed_blocks: usize,
) -> io::Result<Signature> {
for x in 0..tries {
let mut buf = vec![0; serialized_size(&transaction).unwrap() as usize];
let mut wr = std::io::Cursor::new(&mut buf[..]);
serialize_into(&mut wr, &transaction)
.expect("serialize Transaction in pub fn transfer_signed");
self.transactions_socket
.send_to(&buf[..], &self.transactions_addr)?;
if self
.poll_for_signature_confirmation(&transaction.signatures[0], min_confirmed_blocks)
.is_ok()
{
return Ok(transaction.signatures[0]);
}
info!("{} tries failed transfer to {}", x, self.transactions_addr);
transaction.sign(keypairs, self.rpc_client.get_recent_blockhash()?);
}
Err(io::Error::new(
io::ErrorKind::Other,
format!("retry_transfer failed in {} retries", tries),
))
}
pub fn get_new_blockhash(&self, blockhash: &Hash) -> io::Result<Hash> {
self.rpc_client.get_new_blockhash(blockhash)
}
pub fn poll_balance_with_timeout(
&self,
pubkey: &Pubkey,
polling_frequency: &Duration,
timeout: &Duration,
) -> io::Result<u64> {
self.rpc_client
.poll_balance_with_timeout(pubkey, polling_frequency, timeout)
}
pub fn poll_get_balance(&self, pubkey: &Pubkey) -> io::Result<u64> {
self.rpc_client.poll_get_balance(pubkey)
}
pub fn wait_for_balance(&self, pubkey: &Pubkey, expected_balance: Option<u64>) -> Option<u64> {
self.rpc_client.wait_for_balance(pubkey, expected_balance)
}
pub fn poll_for_signature(&self, signature: &Signature) -> io::Result<()> {
self.rpc_client.poll_for_signature(signature)
}
/// Poll the server until the signature has been confirmed by at least `min_confirmed_blocks`
pub fn poll_for_signature_confirmation(
&self,
signature: &Signature,
min_confirmed_blocks: usize,
) -> io::Result<()> {
self.rpc_client
.poll_for_signature_confirmation(signature, min_confirmed_blocks)
}
/// Check a signature in the bank. This method blocks
/// until the server sends a response.
pub fn check_signature(&self, signature: &Signature) -> bool {
self.rpc_client.check_signature(signature)
}
pub fn fullnode_exit(&self) -> io::Result<bool> {
self.rpc_client.fullnode_exit()
}
pub fn get_num_blocks_since_signature_confirmation(
&mut self,
sig: &Signature,
) -> io::Result<usize> {
self.rpc_client
.get_num_blocks_since_signature_confirmation(sig)
}
}
impl Client for ThinClient {}
impl SyncClient for ThinClient {
fn send_message(&self, keypairs: &[&Keypair], message: Message) -> TransportResult<Signature> {
let blockhash = self.get_recent_blockhash()?;
let mut transaction = Transaction::new(&keypairs, message, blockhash);
let signature = self.send_and_confirm_transaction(keypairs, &mut transaction, 5, 0)?;
Ok(signature)
}
fn send_instruction(
&self,
keypair: &Keypair,
instruction: Instruction,
) -> TransportResult<Signature> {
let message = Message::new(vec![instruction]);
self.send_message(&[keypair], message)
}
fn transfer(
&self,
lamports: u64,
keypair: &Keypair,
pubkey: &Pubkey,
) -> TransportResult<Signature> {
let transfer_instruction =
system_instruction::transfer(&keypair.pubkey(), pubkey, lamports);
self.send_instruction(keypair, transfer_instruction)
}
fn get_account_data(&self, pubkey: &Pubkey) -> TransportResult<Option<Vec<u8>>> {
Ok(self.rpc_client.get_account_data(pubkey).ok())
}
fn get_balance(&self, pubkey: &Pubkey) -> TransportResult<u64> {
let balance = self.rpc_client.get_balance(pubkey)?;
Ok(balance)
}
fn get_signature_status(
&self,
signature: &Signature,
) -> TransportResult<Option<transaction::Result<()>>> {
let status = self
.rpc_client
.get_signature_status(&signature.to_string())
.map_err(|err| {
io::Error::new(
io::ErrorKind::Other,
format!("send_transaction failed with error {}", err),
)
})?;
Ok(status)
}
fn get_recent_blockhash(&self) -> TransportResult<Hash> {
let recent_blockhash = self.rpc_client.get_recent_blockhash()?;
Ok(recent_blockhash)
}
fn get_transaction_count(&self) -> TransportResult<u64> {
let transaction_count = self.rpc_client.get_transaction_count()?;
Ok(transaction_count)
}
}
impl AsyncClient for ThinClient {
fn async_send_transaction(&self, transaction: Transaction) -> io::Result<Signature> {
let mut buf = vec![0; serialized_size(&transaction).unwrap() as usize];
/// Send a signed Transaction to the server for processing. This method
/// does not wait for a response.
pub fn transfer_signed(&self, transaction: &Transaction) -> io::Result<Signature> {
let mut buf = vec![0; transaction.serialized_size().unwrap() as usize];
let mut wr = std::io::Cursor::new(&mut buf[..]);
serialize_into(&mut wr, &transaction)
.expect("serialize Transaction in pub fn transfer_signed");
@ -245,47 +85,327 @@ impl AsyncClient for ThinClient {
.send_to(&buf[..], &self.transactions_addr)?;
Ok(transaction.signatures[0])
}
fn async_send_message(
&self,
keypairs: &[&Keypair],
message: Message,
recent_blockhash: Hash,
) -> io::Result<Signature> {
let transaction = Transaction::new(&keypairs, message, recent_blockhash);
self.async_send_transaction(transaction)
}
fn async_send_instruction(
&self,
/// Retry a sending a signed Transaction to the server for processing.
pub fn retry_transfer(
&mut self,
keypair: &Keypair,
instruction: Instruction,
recent_blockhash: Hash,
transaction: &mut Transaction,
tries: usize,
) -> io::Result<Signature> {
let message = Message::new(vec![instruction]);
self.async_send_message(&[keypair], message, recent_blockhash)
for x in 0..tries {
transaction.sign(&[keypair], self.get_recent_blockhash());
let mut buf = vec![0; transaction.serialized_size().unwrap() as usize];
let mut wr = std::io::Cursor::new(&mut buf[..]);
serialize_into(&mut wr, &transaction)
.expect("serialize Transaction in pub fn transfer_signed");
self.transactions_socket
.send_to(&buf[..], &self.transactions_addr)?;
if self.poll_for_signature(&transaction.signatures[0]).is_ok() {
return Ok(transaction.signatures[0]);
}
info!("{} tries failed transfer to {}", x, self.transactions_addr);
}
Err(io::Error::new(
io::ErrorKind::Other,
"retry_transfer failed",
))
}
fn async_transfer(
/// Creates, signs, and processes a Transaction. Useful for writing unit-tests.
pub fn transfer(
&self,
lamports: u64,
keypair: &Keypair,
pubkey: &Pubkey,
recent_blockhash: Hash,
to: &Pubkey,
blockhash: &Hash,
) -> io::Result<Signature> {
let transfer_instruction =
system_instruction::transfer(&keypair.pubkey(), pubkey, lamports);
self.async_send_instruction(keypair, transfer_instruction, recent_blockhash)
debug!(
"transfer: lamports={} from={:?} to={:?} blockhash={:?}",
lamports,
keypair.pubkey(),
to,
blockhash
);
let now = Instant::now();
let transaction = SystemTransaction::new_account(keypair, to, lamports, *blockhash, 0);
let result = self.transfer_signed(&transaction);
solana_metrics::submit(
influxdb::Point::new("thinclient")
.add_tag("op", influxdb::Value::String("transfer".to_string()))
.add_field(
"duration_ms",
influxdb::Value::Integer(timing::duration_as_ms(&now.elapsed()) as i64),
)
.to_owned(),
);
result
}
pub fn get_account_data(&mut self, pubkey: &Pubkey) -> io::Result<Option<Vec<u8>>> {
let params = json!([format!("{}", pubkey)]);
let response =
self.rpc_client
.make_rpc_request(1, RpcRequest::GetAccountInfo, Some(params));
match response {
Ok(account_json) => {
let account: Account =
serde_json::from_value(account_json).expect("deserialize account");
Ok(Some(account.data))
}
Err(error) => {
debug!("get_account_data failed: {:?}", error);
Err(io::Error::new(
io::ErrorKind::Other,
"get_account_data failed",
))
}
}
}
/// Request the balance of the user holding `pubkey`. This method blocks
/// until the server sends a response. If the response packet is dropped
/// by the network, this method will hang indefinitely.
pub fn get_balance(&mut self, pubkey: &Pubkey) -> io::Result<u64> {
trace!("get_balance sending request to {}", self.rpc_addr);
let params = json!([format!("{}", pubkey)]);
let response =
self.rpc_client
.make_rpc_request(1, RpcRequest::GetAccountInfo, Some(params));
response
.and_then(|account_json| {
let account: Account =
serde_json::from_value(account_json).expect("deserialize account");
trace!("Response account {:?} {:?}", pubkey, account);
trace!("get_balance {:?}", account.lamports);
Ok(account.lamports)
})
.map_err(|error| {
debug!("Response account {}: None (error: {:?})", pubkey, error);
io::Error::new(io::ErrorKind::Other, "AccountNotFound")
})
}
/// Request the transaction count. If the response packet is dropped by the network,
/// this method will try again 5 times.
pub fn transaction_count(&mut self) -> u64 {
debug!("transaction_count");
for _tries in 0..5 {
let response =
self.rpc_client
.make_rpc_request(1, RpcRequest::GetTransactionCount, None);
match response {
Ok(value) => {
debug!("transaction_count response: {:?}", value);
let transaction_count = value.as_u64().unwrap();
return transaction_count;
}
Err(error) => {
debug!("transaction_count failed: {:?}", error);
}
};
}
0
}
/// Request the last Entry ID from the server without blocking.
/// Returns the blockhash Hash or None if there was no response from the server.
pub fn try_get_recent_blockhash(&mut self, mut num_retries: u64) -> Option<Hash> {
loop {
trace!("try_get_recent_blockhash send_to {}", &self.rpc_addr);
let response =
self.rpc_client
.make_rpc_request(1, RpcRequest::GetRecentBlockhash, None);
match response {
Ok(value) => {
let blockhash_str = value.as_str().unwrap();
let blockhash_vec = bs58::decode(blockhash_str).into_vec().unwrap();
return Some(Hash::new(&blockhash_vec));
}
Err(error) => {
debug!("thin_client get_recent_blockhash error: {:?}", error);
num_retries -= 1;
if num_retries == 0 {
return None;
}
}
}
}
}
/// Request the last Entry ID from the server. This method blocks
/// until the server sends a response.
pub fn get_recent_blockhash(&mut self) -> Hash {
loop {
trace!("get_recent_blockhash send_to {}", &self.rpc_addr);
if let Some(hash) = self.try_get_recent_blockhash(10) {
return hash;
}
}
}
/// Request a new last Entry ID from the server. This method blocks
/// until the server sends a response.
pub fn get_next_blockhash(&mut self, previous_blockhash: &Hash) -> Hash {
self.get_next_blockhash_ext(previous_blockhash, &|| {
sleep(Duration::from_millis(100));
})
}
pub fn get_next_blockhash_ext(&mut self, previous_blockhash: &Hash, func: &Fn()) -> Hash {
loop {
let blockhash = self.get_recent_blockhash();
if blockhash != *previous_blockhash {
break blockhash;
}
debug!("Got same blockhash ({:?}), will retry...", blockhash);
func()
}
}
pub fn submit_poll_balance_metrics(elapsed: &Duration) {
solana_metrics::submit(
influxdb::Point::new("thinclient")
.add_tag("op", influxdb::Value::String("get_balance".to_string()))
.add_field(
"duration_ms",
influxdb::Value::Integer(timing::duration_as_ms(elapsed) as i64),
)
.to_owned(),
);
}
pub fn poll_balance_with_timeout(
&mut self,
pubkey: &Pubkey,
polling_frequency: &Duration,
timeout: &Duration,
) -> io::Result<u64> {
let now = Instant::now();
loop {
match self.get_balance(&pubkey) {
Ok(bal) => {
ThinClient::submit_poll_balance_metrics(&now.elapsed());
return Ok(bal);
}
Err(e) => {
sleep(*polling_frequency);
if now.elapsed() > *timeout {
ThinClient::submit_poll_balance_metrics(&now.elapsed());
return Err(e);
}
}
};
}
}
pub fn poll_get_balance(&mut self, pubkey: &Pubkey) -> io::Result<u64> {
self.poll_balance_with_timeout(pubkey, &Duration::from_millis(100), &Duration::from_secs(1))
}
/// Poll the server to confirm a transaction.
pub fn poll_for_signature(&mut self, signature: &Signature) -> io::Result<()> {
let now = Instant::now();
while !self.check_signature(signature) {
if now.elapsed().as_secs() > 15 {
// TODO: Return a better error.
return Err(io::Error::new(io::ErrorKind::Other, "signature not found"));
}
sleep(Duration::from_millis(250));
}
Ok(())
}
/// Check a signature in the bank. This method blocks
/// until the server sends a response.
pub fn check_signature(&mut self, signature: &Signature) -> bool {
trace!("check_signature: {:?}", signature);
let params = json!([format!("{}", signature)]);
let now = Instant::now();
loop {
let response = self.rpc_client.make_rpc_request(
1,
RpcRequest::ConfirmTransaction,
Some(params.clone()),
);
match response {
Ok(confirmation) => {
let signature_status = confirmation.as_bool().unwrap();
if signature_status {
trace!("Response found signature");
} else {
trace!("Response signature not found");
}
solana_metrics::submit(
influxdb::Point::new("thinclient")
.add_tag("op", influxdb::Value::String("check_signature".to_string()))
.add_field(
"duration_ms",
influxdb::Value::Integer(
timing::duration_as_ms(&now.elapsed()) as i64
),
)
.to_owned(),
);
return signature_status;
}
Err(err) => {
debug!("check_signature request failed: {:?}", err);
}
};
}
}
pub fn fullnode_exit(&mut self) -> io::Result<bool> {
trace!("fullnode_exit sending request to {}", self.rpc_addr);
let response = self
.rpc_client
.make_rpc_request(1, RpcRequest::FullnodeExit, None)
.map_err(|error| {
debug!("Response from {} fullndoe_exit: {}", self.rpc_addr, error);
io::Error::new(io::ErrorKind::Other, "FullodeExit request failure")
})?;
serde_json::from_value(response).map_err(|error| {
debug!(
"ParseError: from {} fullndoe_exit: {}",
self.rpc_addr, error
);
io::Error::new(io::ErrorKind::Other, "FullodeExit parse failure")
})
}
}
pub fn create_client((rpc, tpu): (SocketAddr, SocketAddr), range: (u16, u16)) -> ThinClient {
let (_, transactions_socket) = solana_netutil::bind_in_range(range).unwrap();
ThinClient::new(rpc, tpu, transactions_socket)
impl Drop for ThinClient {
fn drop(&mut self) {
solana_metrics::flush();
}
}
pub fn create_client_with_timeout(
(rpc, tpu): (SocketAddr, SocketAddr),
range: (u16, u16),
timeout: Duration,
) -> ThinClient {
let (_, transactions_socket) = solana_netutil::bind_in_range(range).unwrap();
ThinClient::new_socket_with_timeout(rpc, tpu, transactions_socket, timeout)
pub fn retry_get_balance(
client: &mut ThinClient,
bob_pubkey: &Pubkey,
expected_balance: Option<u64>,
) -> Option<u64> {
const LAST: usize = 30;
for run in 0..LAST {
let balance_result = client.poll_get_balance(bob_pubkey);
if expected_balance.is_none() {
return balance_result.ok();
}
trace!(
"retry_get_balance[{}] {:?} {:?}",
run,
balance_result,
expected_balance
);
if let (Some(expected_balance), Ok(balance_result)) = (expected_balance, balance_result) {
if expected_balance == balance_result {
return Some(balance_result);
}
}
}
None
}

View File

@ -1,7 +1,7 @@
[package]
name = "solana"
description = "Blockchain, Rebuilt for Scale"
version = "0.13.2"
version = "0.12.3"
documentation = "https://docs.rs/solana"
homepage = "https://solana.com/"
readme = "../README.md"
@ -17,22 +17,21 @@ codecov = { repository = "solana-labs/solana", branch = "master", service = "git
chacha = []
cuda = []
erasure = []
kvstore = ["solana-kvstore"]
kvstore = ["memmap"]
[dependencies]
bincode = "1.1.2"
bs58 = "0.2.0"
byteorder = "1.3.1"
chrono = { version = "0.4.0", features = ["serde"] }
crc = { version = "1.8.1", optional = true }
hashbrown = "0.2.0"
hashbrown = "0.1.8"
indexmap = "1.0"
itertools = "0.8.0"
jsonrpc-core = "11.0.0"
jsonrpc-derive = "11.0.0"
jsonrpc-http-server = "11.0.0"
jsonrpc-pubsub = "11.0.0"
jsonrpc-ws-server = "11.0.0"
jsonrpc-core = "10.1.0"
jsonrpc-derive = "10.1.0"
jsonrpc-http-server = "10.1.0"
jsonrpc-pubsub = "10.1.0"
jsonrpc-ws-server = "10.1.0"
libc = "0.2.50"
log = "0.4.2"
memmap = { version = "0.7.0", optional = true }
@ -40,50 +39,29 @@ nix = "0.13.0"
rand = "0.6.5"
rand_chacha = "0.1.1"
rayon = "1.0.0"
reqwest = "0.9.11"
ring = "0.13.2"
rocksdb = "0.11.0"
serde = "1.0.89"
serde_derive = "1.0.88"
serde_json = "1.0.39"
solana-budget-api = { path = "../programs/budget_api", version = "0.13.2" }
solana-client = { path = "../client", version = "0.13.2" }
solana-drone = { path = "../drone", version = "0.13.2" }
solana-kvstore = { path = "../kvstore", version = "0.13.2", optional = true }
solana-logger = { path = "../logger", version = "0.13.2" }
solana-metrics = { path = "../metrics", version = "0.13.2" }
solana-netutil = { path = "../netutil", version = "0.13.2" }
solana-runtime = { path = "../runtime", version = "0.13.2" }
solana-sdk = { path = "../sdk", version = "0.13.2" }
solana-storage-api = { path = "../programs/storage_api", version = "0.13.2" }
solana-vote-api = { path = "../programs/vote_api", version = "0.13.2" }
solana-vote-signer = { path = "../vote-signer", version = "0.13.2" }
solana-budget-api = { path = "../programs/budget_api", version = "0.12.3" }
solana-client = { path = "../client", version = "0.12.3" }
solana-drone = { path = "../drone", version = "0.12.3" }
solana-logger = { path = "../logger", version = "0.12.3" }
solana-metrics = { path = "../metrics", version = "0.12.3" }
solana-netutil = { path = "../netutil", version = "0.12.3" }
solana-runtime = { path = "../runtime", version = "0.12.3" }
solana-sdk = { path = "../sdk", version = "0.12.3" }
solana-storage-api = { path = "../programs/storage_api", version = "0.12.3" }
solana-vote-api = { path = "../programs/vote_api", version = "0.12.3" }
solana-vote-signer = { path = "../vote-signer", version = "0.12.3" }
sys-info = "0.5.6"
tokio = "0.1"
tokio-codec = "0.1"
untrusted = "0.6.2"
[dev-dependencies]
hex-literal = "0.1.4"
hex-literal = "0.1.3"
matches = "0.1.6"
solana-vote-program = { path = "../programs/vote_program", version = "0.13.2" }
solana-budget-program = { path = "../programs/budget_program", version = "0.13.2" }
[[bench]]
name = "banking_stage"
[[bench]]
name = "blocktree"
[[bench]]
name = "ledger"
[[bench]]
name = "gen_keys"
[[bench]]
name = "sigverify"
[[bench]]
required-features = ["chacha"]
name = "chacha"
solana-vote-program = { path = "../programs/vote", version = "0.12.3" }
solana-budget-program = { path = "../programs/budget", version = "0.12.3" }

View File

@ -1,14 +1,15 @@
#![cfg(feature = "kvstore")]
#![feature(test)]
extern crate test;
use std::fs;
use std::path::{Path, PathBuf};
use rand::{self, Rng};
use rand::{self, thread_rng, Rng};
use test::Bencher;
use solana_kvstore::{test::gen, Config, Key, KvStore};
use solana::kvstore::{Config, Key, KvStore};
const SMALL_SIZE: usize = 512;
const LARGE_SIZE: usize = 32 * 1024;
@ -44,7 +45,7 @@ fn bench_write_partitioned(bench: &mut Bencher, rows: &[(Key, Vec<u8>)], ledger_
fn bench_write_small(bench: &mut Bencher) {
let ledger_path = setup("bench_write_small");
let num_entries = 32 * 1024;
let rows = gen::pairs(SMALL_SIZE).take(num_entries).collect::<Vec<_>>();
let rows = gen_pairs(SMALL_SIZE).take(num_entries).collect::<Vec<_>>();
bench_write(bench, &rows, &ledger_path.to_string_lossy());
}
@ -53,7 +54,7 @@ fn bench_write_small(bench: &mut Bencher) {
fn bench_write_small_partitioned(bench: &mut Bencher) {
let ledger_path = setup("bench_write_small_partitioned");
let num_entries = 32 * 1024;
let rows = gen::pairs(SMALL_SIZE).take(num_entries).collect::<Vec<_>>();
let rows = gen_pairs(SMALL_SIZE).take(num_entries).collect::<Vec<_>>();
bench_write_partitioned(bench, &rows, &ledger_path.to_string_lossy());
}
@ -62,7 +63,7 @@ fn bench_write_small_partitioned(bench: &mut Bencher) {
fn bench_write_large(bench: &mut Bencher) {
let ledger_path = setup("bench_write_large");
let num_entries = 32 * 1024;
let rows = gen::pairs(LARGE_SIZE).take(num_entries).collect::<Vec<_>>();
let rows = gen_pairs(LARGE_SIZE).take(num_entries).collect::<Vec<_>>();
bench_write(bench, &rows, &ledger_path.to_string_lossy());
}
@ -71,7 +72,7 @@ fn bench_write_large(bench: &mut Bencher) {
fn bench_write_huge(bench: &mut Bencher) {
let ledger_path = setup("bench_write_huge");
let num_entries = 32 * 1024;
let rows = gen::pairs(HUGE_SIZE).take(num_entries).collect::<Vec<_>>();
let rows = gen_pairs(HUGE_SIZE).take(num_entries).collect::<Vec<_>>();
bench_write(bench, &rows, &ledger_path.to_string_lossy());
}
@ -86,8 +87,8 @@ fn bench_read_sequential(bench: &mut Bencher) {
let num_large_blobs = 32 * 1024;
let total_blobs = num_small_blobs + num_large_blobs;
let small = gen::data(SMALL_SIZE).take(num_small_blobs);
let large = gen::data(LARGE_SIZE).take(num_large_blobs);
let small = gen_data(SMALL_SIZE).take(num_small_blobs);
let large = gen_data(LARGE_SIZE).take(num_large_blobs);
let rows = gen_seq_keys().zip(small.chain(large));
let _ = store.put_many(rows);
@ -119,8 +120,8 @@ fn bench_read_random(bench: &mut Bencher) {
let num_large_blobs = 32 * 1024;
let total_blobs = num_small_blobs + num_large_blobs;
let small = gen::data(SMALL_SIZE).take(num_small_blobs);
let large = gen::data(LARGE_SIZE).take(num_large_blobs);
let small = gen_data(SMALL_SIZE).take(num_small_blobs);
let large = gen_data(LARGE_SIZE).take(num_large_blobs);
let rows = gen_seq_keys().zip(small.chain(large));
let _ = store.put_many(rows);
@ -165,6 +166,24 @@ fn gen_seq_keys() -> impl Iterator<Item = Key> {
})
}
fn gen_keys() -> impl Iterator<Item = Key> {
let mut rng = thread_rng();
std::iter::repeat_with(move || {
let buf = rng.gen();
Key(buf)
})
}
fn gen_data(size: usize) -> impl Iterator<Item = Vec<u8>> {
std::iter::repeat(vec![1u8; size])
}
fn gen_pairs(data_size: usize) -> impl Iterator<Item = (Key, Vec<u8>)> {
gen_keys().zip(gen_data(data_size))
}
fn teardown<P: AsRef<Path>>(p: P) {
KvStore::destroy(p).expect("Expect successful store destruction");
}

View File

@ -1,12 +1,9 @@
//! The `bank_forks` module implments BankForks a DAG of checkpointed Banks
use hashbrown::{HashMap, HashSet};
use solana_metrics::counter::Counter;
use solana_runtime::bank::Bank;
use solana_sdk::timing;
use std::ops::Index;
use std::sync::Arc;
use std::time::Instant;
pub struct BankForks {
banks: HashMap<u64, Arc<Bank>>,
@ -102,17 +99,12 @@ impl BankForks {
}
pub fn set_root(&mut self, root: u64) {
let set_root_start = Instant::now();
let root_bank = self
.banks
.get(&root)
.expect("root bank didn't exist in bank_forks");
root_bank.squash();
self.prune_non_root(root);
inc_new_counter_info!(
"bank-forks_set_root_ms",
timing::duration_as_ms(&set_root_start.elapsed()) as usize
);
}
fn prune_non_root(&mut self, root: u64) {

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,7 @@
use crate::entry::Entry;
use crate::result::Result;
use bincode::serialize;
use chrono::{SecondsFormat, Utc};
use serde_json::json;
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
use std::cell::RefCell;
@ -93,13 +91,7 @@ where
leader_id: &Pubkey,
entry: &Entry,
) -> Result<()> {
let transactions: Vec<Vec<u8>> = serialize_transactions(entry);
let stream_entry = json!({
"num_hashes": entry.num_hashes,
"hash": entry.hash,
"transactions": transactions
});
let json_entry = serde_json::to_string(&stream_entry)?;
let json_entry = serde_json::to_string(&entry)?;
let payload = format!(
r#"{{"dt":"{}","t":"entry","s":{},"h":{},"l":"{:?}","entry":{}}}"#,
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true),
@ -156,14 +148,6 @@ impl MockBlockstream {
}
}
fn serialize_transactions(entry: &Entry) -> Vec<Vec<u8>> {
entry
.transactions
.iter()
.map(|tx| serialize(&tx).unwrap())
.collect()
}
#[cfg(test)]
mod test {
use super::*;
@ -172,30 +156,8 @@ mod test {
use serde_json::Value;
use solana_sdk::hash::Hash;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction;
use std::collections::HashSet;
#[test]
fn test_serialize_transactions() {
let entry = Entry::new(&Hash::default(), 1, vec![]);
let empty_vec: Vec<Vec<u8>> = vec![];
assert_eq!(serialize_transactions(&entry), empty_vec);
let keypair0 = Keypair::new();
let keypair1 = Keypair::new();
let tx0 =
system_transaction::transfer(&keypair0, &keypair1.pubkey(), 1, Hash::default(), 0);
let tx1 =
system_transaction::transfer(&keypair1, &keypair0.pubkey(), 2, Hash::default(), 0);
let serialized_tx0 = serialize(&tx0).unwrap();
let serialized_tx1 = serialize(&tx1).unwrap();
let entry = Entry::new(&Hash::default(), 1, vec![tx0, tx1]);
assert_eq!(
serialize_transactions(&entry),
vec![serialized_tx0, serialized_tx1]
);
}
#[test]
fn test_blockstream() -> () {
let blockstream = MockBlockstream::new("test_stream".to_string());
@ -208,7 +170,7 @@ mod test {
let tick_height_initial = 0;
let tick_height_final = tick_height_initial + ticks_per_slot + 2;
let mut curr_slot = 0;
let leader_id = Pubkey::new_rand();
let leader_id = Keypair::new().pubkey();
for tick_height in tick_height_initial..=tick_height_final {
if tick_height == 5 {

View File

@ -109,26 +109,25 @@ mod test {
use super::*;
use crate::blocktree::create_new_tmp_ledger;
use crate::entry::{create_ticks, Entry};
use bincode::{deserialize, serialize};
use chrono::{DateTime, FixedOffset};
use serde_json::Value;
use solana_sdk::genesis_block::GenesisBlock;
use solana_sdk::hash::Hash;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction;
use solana_sdk::system_transaction::SystemTransaction;
use std::sync::mpsc::channel;
#[test]
fn test_blockstream_service_process_entries() {
let ticks_per_slot = 5;
let leader_id = Pubkey::new_rand();
let leader_id = Keypair::new().pubkey();
// Set up genesis block and blocktree
let (mut genesis_block, _mint_keypair) = GenesisBlock::new(1000);
genesis_block.ticks_per_slot = ticks_per_slot;
let (ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block);
let blocktree = Blocktree::open(&ledger_path).unwrap();
let blocktree = Blocktree::open_config(&ledger_path, ticks_per_slot).unwrap();
// Set up blockstream
let mut blockstream = Blockstream::new("test_stream".to_string());
@ -141,13 +140,7 @@ mod test {
let keypair = Keypair::new();
let mut blockhash = entries[3].hash;
let tx = system_transaction::create_user_account(
&keypair,
&keypair.pubkey(),
1,
Hash::default(),
0,
);
let tx = SystemTransaction::new_account(&keypair, &keypair.pubkey(), 1, Hash::default(), 0);
let entry = Entry::new(&mut blockhash, 1, vec![tx]);
blockhash = entry.hash;
entries.push(entry);
@ -157,9 +150,7 @@ mod test {
let expected_entries = entries.clone();
let expected_tick_heights = [5, 6, 7, 8, 8, 9];
blocktree
.write_entries(1, 0, 0, ticks_per_slot, &entries)
.unwrap();
blocktree.write_entries(1, 0, 0, &entries).unwrap();
slot_full_sender.send((1, leader_id)).unwrap();
BlockstreamService::process_entries(
@ -189,34 +180,13 @@ mod test {
assert_eq!(height, expected_tick_heights[i]);
let entry_obj = json["entry"].clone();
let tx = entry_obj["transactions"].as_array().unwrap();
let entry: Entry;
if tx.len() == 0 {
entry = serde_json::from_value(entry_obj).unwrap();
} else {
let entry_json = entry_obj.as_object().unwrap();
entry = Entry {
num_hashes: entry_json.get("num_hashes").unwrap().as_u64().unwrap(),
hash: serde_json::from_value(entry_json.get("hash").unwrap().clone()).unwrap(),
transactions: entry_json
.get("transactions")
.unwrap()
.as_array()
.unwrap()
.into_iter()
.enumerate()
.map(|(j, tx)| {
let tx_vec: Vec<u8> = serde_json::from_value(tx.clone()).unwrap();
// Check explicitly that transaction matches bincode-serialized format
assert_eq!(
tx_vec,
serialize(&expected_entries[i].transactions[j]).unwrap()
);
deserialize(&tx_vec).unwrap()
})
.collect(),
};
// TODO: There is a bug in Transaction deserialize methods such that
// `serde_json::from_str` does not work for populated Entries.
// Remove this `if` when fixed.
let entry: Entry = serde_json::from_value(entry_obj).unwrap();
assert_eq!(entry, expected_entries[i]);
}
assert_eq!(entry, expected_entries[i]);
}
for json in block_events {
let slot = json["s"].as_u64().unwrap();

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
use crate::entry::Entry;
use crate::result::{Error, Result};
use bincode::{deserialize, serialize};
@ -6,58 +7,24 @@ use serde::de::DeserializeOwned;
use serde::Serialize;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::path::Path;
use std::sync::Arc;
pub mod columns {
#[derive(Debug)]
/// SlotMeta Column
pub struct SlotMeta;
#[derive(Debug)]
/// Orphans Column
pub struct Orphans;
#[derive(Debug)]
/// Erasure Column
pub struct Coding;
#[derive(Debug)]
/// Data Column
pub struct Data;
#[cfg(feature = "erasure")]
#[derive(Debug)]
/// The erasure meta column
pub struct ErasureMeta;
}
pub trait Backend: Sized + Send + Sync {
type Key: ?Sized + ToOwned<Owned = Self::OwnedKey>;
type OwnedKey: Borrow<Self::Key>;
type ColumnFamily: Clone;
type Cursor: DbCursor<Self>;
type Iter: Iterator<Item = (Box<Self::Key>, Box<[u8]>)>;
type WriteBatch: IWriteBatch<Self>;
pub trait Database: Sized + Send + Sync {
type Error: Into<Error>;
type Key: Borrow<Self::KeyRef>;
type KeyRef: ?Sized;
type ColumnFamily;
type Cursor: Cursor<Self>;
type EntryIter: Iterator<Item = Entry>;
type WriteBatch: IWriteBatch<Self>;
fn open(path: &Path) -> Result<Self>;
fn cf_handle(&self, cf: &str) -> Option<Self::ColumnFamily>;
fn columns(&self) -> Vec<&'static str>;
fn get_cf(&self, cf: Self::ColumnFamily, key: &Self::KeyRef) -> Result<Option<Vec<u8>>>;
fn destroy(path: &Path) -> Result<()>;
fn put_cf(&self, cf: Self::ColumnFamily, key: &Self::KeyRef, data: &[u8]) -> Result<()>;
fn cf_handle(&self, cf: &str) -> Self::ColumnFamily;
fn get_cf(&self, cf: Self::ColumnFamily, key: &Self::Key) -> Result<Option<Vec<u8>>>;
fn put_cf(&self, cf: Self::ColumnFamily, key: &Self::Key, value: &[u8]) -> Result<()>;
fn delete_cf(&self, cf: Self::ColumnFamily, key: &Self::Key) -> Result<()>;
fn iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Iter>;
fn delete_cf(&self, cf: Self::ColumnFamily, key: &Self::KeyRef) -> Result<()>;
fn raw_iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Cursor>;
@ -66,343 +33,163 @@ pub trait Backend: Sized + Send + Sync {
fn batch(&self) -> Result<Self::WriteBatch>;
}
pub trait Column<B>
where
B: Backend,
{
const NAME: &'static str;
type Index;
fn key(index: Self::Index) -> B::OwnedKey;
fn index(key: &B::Key) -> Self::Index;
}
pub trait DbCursor<B>
where
B: Backend,
{
pub trait Cursor<D: Database> {
fn valid(&self) -> bool;
fn seek(&mut self, key: &B::Key);
fn seek(&mut self, key: &D::KeyRef);
fn seek_to_first(&mut self);
fn next(&mut self);
fn key(&self) -> Option<B::OwnedKey>;
fn key(&self) -> Option<D::Key>;
fn value(&self) -> Option<Vec<u8>>;
}
pub trait IWriteBatch<B>
where
B: Backend,
{
fn put_cf(&mut self, cf: B::ColumnFamily, key: &B::Key, value: &[u8]) -> Result<()>;
fn delete_cf(&mut self, cf: B::ColumnFamily, key: &B::Key) -> Result<()>;
pub trait IWriteBatch<D: Database> {
fn put_cf(&mut self, cf: D::ColumnFamily, key: &D::KeyRef, data: &[u8]) -> Result<()>;
}
pub trait TypedColumn<B>: Column<B>
where
B: Backend,
{
type Type: Serialize + DeserializeOwned;
}
pub trait IDataCf<D: Database>: LedgerColumnFamilyRaw<D> {
fn new(db: Arc<D>) -> Self;
#[derive(Debug, Clone)]
pub struct Database<B>
where
B: Backend,
{
backend: B,
}
#[derive(Debug, Clone)]
pub struct Cursor<B, C>
where
B: Backend,
C: Column<B>,
{
db_cursor: B::Cursor,
column: PhantomData<C>,
backend: PhantomData<B>,
}
#[derive(Debug, Clone)]
pub struct LedgerColumn<B, C>
where
B: Backend,
C: Column<B>,
{
pub db: Arc<Database<B>>,
column: PhantomData<C>,
}
#[derive(Debug)]
pub struct WriteBatch<B>
where
B: Backend,
{
write_batch: B::WriteBatch,
backend: PhantomData<B>,
map: HashMap<&'static str, B::ColumnFamily>,
}
impl<B> Database<B>
where
B: Backend,
{
pub fn open(path: &Path) -> Result<Self> {
let backend = B::open(path)?;
Ok(Database { backend })
fn get_by_slot_index(&self, slot: u64, index: u64) -> Result<Option<Vec<u8>>> {
let key = Self::key(slot, index);
self.get(key.borrow())
}
pub fn destroy(path: &Path) -> Result<()> {
B::destroy(path)?;
Ok(())
fn delete_by_slot_index(&self, slot: u64, index: u64) -> Result<()> {
let key = Self::key(slot, index);
self.delete(&key.borrow())
}
pub fn get_bytes<C>(&self, key: C::Index) -> Result<Option<Vec<u8>>>
where
C: Column<B>,
{
self.backend
.get_cf(self.cf_handle::<C>(), C::key(key).borrow())
fn put_by_slot_index(&self, slot: u64, index: u64, serialized_value: &[u8]) -> Result<()> {
let key = Self::key(slot, index);
self.put(key.borrow(), serialized_value)
}
pub fn put_bytes<C>(&self, key: C::Index, data: &[u8]) -> Result<()>
where
C: Column<B>,
{
self.backend
.put_cf(self.cf_handle::<C>(), C::key(key).borrow(), data)
fn key(slot: u64, index: u64) -> D::Key;
fn slot_from_key(key: &D::KeyRef) -> Result<u64>;
fn index_from_key(key: &D::KeyRef) -> Result<u64>;
}
pub trait IErasureCf<D: Database>: LedgerColumnFamilyRaw<D> {
fn new(db: Arc<D>) -> Self;
fn delete_by_slot_index(&self, slot: u64, index: u64) -> Result<()> {
let key = Self::key(slot, index);
self.delete(key.borrow())
}
pub fn delete<C>(&self, key: C::Index) -> Result<()>
where
C: Column<B>,
{
self.backend
.delete_cf(self.cf_handle::<C>(), C::key(key).borrow())
fn get_by_slot_index(&self, slot: u64, index: u64) -> Result<Option<Vec<u8>>> {
let key = Self::key(slot, index);
self.get(key.borrow())
}
pub fn get<C>(&self, key: C::Index) -> Result<Option<C::Type>>
where
C: TypedColumn<B>,
{
if let Some(serialized_value) = self
.backend
.get_cf(self.cf_handle::<C>(), C::key(key).borrow())?
{
let value = deserialize(&serialized_value)?;
fn put_by_slot_index(&self, slot: u64, index: u64, serialized_value: &[u8]) -> Result<()> {
let key = Self::key(slot, index);
self.put(key.borrow(), serialized_value)
}
Ok(Some(value))
fn key(slot: u64, index: u64) -> D::Key;
fn slot_from_key(key: &D::KeyRef) -> Result<u64>;
fn index_from_key(key: &D::KeyRef) -> Result<u64>;
}
pub trait IMetaCf<D: Database>: LedgerColumnFamily<D, ValueType = super::SlotMeta> {
fn new(db: Arc<D>) -> Self;
fn key(slot: u64) -> D::Key;
fn get_slot_meta(&self, slot: u64) -> Result<Option<super::SlotMeta>> {
let key = Self::key(slot);
self.get(key.borrow())
}
fn put_slot_meta(&self, slot: u64, slot_meta: &super::SlotMeta) -> Result<()> {
let key = Self::key(slot);
self.put(key.borrow(), slot_meta)
}
fn index_from_key(key: &D::KeyRef) -> Result<u64>;
}
pub trait LedgerColumnFamily<D: Database> {
type ValueType: DeserializeOwned + Serialize;
fn get(&self, key: &D::KeyRef) -> Result<Option<Self::ValueType>> {
let db = self.db();
let data_bytes = db.get_cf(self.handle(), key)?;
if let Some(raw) = data_bytes {
let result: Self::ValueType = deserialize(&raw)?;
Ok(Some(result))
} else {
Ok(None)
}
}
pub fn put<C>(&self, key: C::Index, value: &C::Type) -> Result<()>
where
C: TypedColumn<B>,
{
let serialized_value = serialize(value)?;
self.backend.put_cf(
self.cf_handle::<C>(),
C::key(key).borrow(),
&serialized_value,
)
fn get_bytes(&self, key: &D::KeyRef) -> Result<Option<Vec<u8>>> {
let db = self.db();
let data_bytes = db.get_cf(self.handle(), key)?;
Ok(data_bytes.map(|x| x.to_vec()))
}
pub fn cursor<C>(&self) -> Result<Cursor<B, C>>
where
C: Column<B>,
{
let db_cursor = self.backend.raw_iterator_cf(self.cf_handle::<C>())?;
Ok(Cursor {
db_cursor,
column: PhantomData,
backend: PhantomData,
})
fn put_bytes(&self, key: &D::KeyRef, serialized_value: &[u8]) -> Result<()> {
let db = self.db();
db.put_cf(self.handle(), key, &serialized_value)?;
Ok(())
}
pub fn iter<C>(&self) -> Result<impl Iterator<Item = (C::Index, Vec<u8>)>>
where
C: Column<B>,
{
let iter = self
.backend
.iterator_cf(self.cf_handle::<C>())?
.map(|(key, value)| (C::index(&key), value.into()));
Ok(iter)
fn put(&self, key: &D::KeyRef, value: &Self::ValueType) -> Result<()> {
let db = self.db();
let serialized = serialize(value)?;
db.put_cf(self.handle(), key, &serialized)?;
Ok(())
}
pub fn batch(&self) -> Result<WriteBatch<B>> {
let db_write_batch = self.backend.batch()?;
let map = self
.backend
.columns()
.into_iter()
.map(|desc| (desc, self.backend.cf_handle(desc)))
.collect();
Ok(WriteBatch {
write_batch: db_write_batch,
backend: PhantomData,
map,
})
fn delete(&self, key: &D::KeyRef) -> Result<()> {
let db = self.db();
db.delete_cf(self.handle(), key)?;
Ok(())
}
pub fn write(&self, batch: WriteBatch<B>) -> Result<()> {
self.backend.write(batch.write_batch)
}
fn db(&self) -> &Arc<D>;
#[inline]
pub fn cf_handle<C>(&self) -> B::ColumnFamily
where
C: Column<B>,
{
self.backend.cf_handle(C::NAME).clone()
}
fn handle(&self) -> D::ColumnFamily;
}
impl<B, C> Cursor<B, C>
where
B: Backend,
C: Column<B>,
{
pub fn valid(&self) -> bool {
self.db_cursor.valid()
pub trait LedgerColumnFamilyRaw<D: Database> {
fn get(&self, key: &D::KeyRef) -> Result<Option<Vec<u8>>> {
let db = self.db();
let data_bytes = db.get_cf(self.handle(), key)?;
Ok(data_bytes.map(|x| x.to_vec()))
}
pub fn seek(&mut self, key: C::Index) {
self.db_cursor.seek(C::key(key).borrow());
fn put(&self, key: &D::KeyRef, serialized_value: &[u8]) -> Result<()> {
let db = self.db();
db.put_cf(self.handle(), &key, &serialized_value)?;
Ok(())
}
pub fn seek_to_first(&mut self) {
self.db_cursor.seek_to_first();
fn delete(&self, key: &D::KeyRef) -> Result<()> {
let db = self.db();
db.delete_cf(self.handle(), &key)?;
Ok(())
}
pub fn next(&mut self) {
self.db_cursor.next();
fn raw_iterator(&self) -> D::Cursor {
let db = self.db();
db.raw_iterator_cf(self.handle())
.expect("Expected to be able to open database iterator")
}
pub fn key(&self) -> Option<C::Index> {
if let Some(key) = self.db_cursor.key() {
Some(C::index(key.borrow()))
} else {
None
}
}
fn handle(&self) -> D::ColumnFamily;
pub fn value_bytes(&self) -> Option<Vec<u8>> {
self.db_cursor.value()
}
}
impl<B, C> Cursor<B, C>
where
B: Backend,
C: TypedColumn<B>,
{
pub fn value(&self) -> Option<C::Type> {
if let Some(bytes) = self.db_cursor.value() {
let value = deserialize(&bytes).ok()?;
Some(value)
} else {
None
}
}
}
impl<B, C> LedgerColumn<B, C>
where
B: Backend,
C: Column<B>,
{
pub fn new(db: &Arc<Database<B>>) -> Self {
LedgerColumn {
db: Arc::clone(db),
column: PhantomData,
}
}
pub fn put_bytes(&self, key: C::Index, value: &[u8]) -> Result<()> {
self.db
.backend
.put_cf(self.handle(), C::key(key).borrow(), value)
}
pub fn get_bytes(&self, key: C::Index) -> Result<Option<Vec<u8>>> {
self.db.backend.get_cf(self.handle(), C::key(key).borrow())
}
pub fn delete(&self, key: C::Index) -> Result<()> {
self.db
.backend
.delete_cf(self.handle(), C::key(key).borrow())
}
pub fn cursor(&self) -> Result<Cursor<B, C>> {
self.db.cursor()
}
pub fn iter(&self) -> Result<impl Iterator<Item = (C::Index, Vec<u8>)>> {
self.db.iter::<C>()
}
pub fn handle(&self) -> B::ColumnFamily {
self.db.cf_handle::<C>()
}
pub fn is_empty(&self) -> Result<bool> {
let mut cursor = self.cursor()?;
cursor.seek_to_first();
Ok(!cursor.valid())
}
}
impl<B, C> LedgerColumn<B, C>
where
B: Backend,
C: TypedColumn<B>,
{
pub fn put(&self, key: C::Index, value: &C::Type) -> Result<()> {
self.db.put::<C>(key, value)
}
pub fn get(&self, key: C::Index) -> Result<Option<C::Type>> {
self.db.get::<C>(key)
}
}
impl<B> WriteBatch<B>
where
B: Backend,
{
pub fn put_bytes<C: Column<B>>(&mut self, key: C::Index, bytes: &[u8]) -> Result<()> {
self.write_batch
.put_cf(self.get_cf::<C>(), C::key(key).borrow(), bytes)
}
pub fn delete<C: Column<B>>(&mut self, key: C::Index) -> Result<()> {
self.write_batch
.delete_cf(self.get_cf::<C>(), C::key(key).borrow())
}
pub fn put<C: TypedColumn<B>>(&mut self, key: C::Index, value: &C::Type) -> Result<()> {
let serialized_value = serialize(&value)?;
self.write_batch
.put_cf(self.get_cf::<C>(), C::key(key).borrow(), &serialized_value)
}
#[inline]
fn get_cf<C: Column<B>>(&self) -> B::ColumnFamily {
self.map[C::NAME].clone()
}
fn db(&self) -> &Arc<D>;
}

View File

@ -1,42 +1,80 @@
use crate::blocktree::db::columns as cf;
use crate::blocktree::db::{Backend, Column, DbCursor, IWriteBatch, TypedColumn};
use crate::blocktree::BlocktreeError;
use crate::entry::Entry;
use crate::kvstore::{self, Key};
use crate::packet::Blob;
use crate::result::{Error, Result};
use byteorder::{BigEndian, ByteOrder};
use solana_kvstore::{self as kvstore, Key, KvStore};
use std::path::Path;
type ColumnFamily = u64;
use std::sync::Arc;
use super::db::{
Cursor, Database, IDataCf, IErasureCf, IMetaCf, IWriteBatch, LedgerColumnFamily,
LedgerColumnFamilyRaw,
};
use super::{Blocktree, BlocktreeError};
#[derive(Debug)]
pub struct Kvs(KvStore);
pub struct Kvs(());
/// Dummy struct for now
#[derive(Debug, Clone, Copy)]
pub struct Dummy;
/// The metadata column family
#[derive(Debug)]
pub struct MetaCf {
db: Arc<Kvs>,
}
impl Backend for Kvs {
type Key = Key;
type OwnedKey = Key;
type ColumnFamily = ColumnFamily;
type Cursor = Dummy;
type Iter = Dummy;
type WriteBatch = Dummy;
/// The data column family
#[derive(Debug)]
pub struct DataCf {
db: Arc<Kvs>,
}
/// The erasure column family
#[derive(Debug)]
pub struct ErasureCf {
db: Arc<Kvs>,
}
/// Dummy struct to get things compiling
/// TODO: all this goes away with Blocktree
pub struct EntryIterator(i32);
/// Dummy struct to get things compiling
pub struct KvsCursor;
/// Dummy struct to get things compiling
pub struct ColumnFamily;
/// Dummy struct to get things compiling
pub struct KvsWriteBatch;
impl Blocktree {
/// Opens a Ledger in directory, provides "infinite" window of blobs
pub fn open(_ledger_path: &str) -> Result<Blocktree> {
unimplemented!()
}
#[allow(unreachable_code)]
pub fn read_ledger_blobs(&self) -> impl Iterator<Item = Blob> {
unimplemented!();
self.read_ledger().unwrap().map(|_| Blob::new(&[]))
}
/// Return an iterator for all the entries in the given file.
#[allow(unreachable_code)]
pub fn read_ledger(&self) -> Result<impl Iterator<Item = Entry>> {
Ok(EntryIterator(unimplemented!()))
}
pub fn destroy(_ledger_path: &str) -> Result<()> {
unimplemented!()
}
}
impl Database for Kvs {
type Error = kvstore::Error;
type Key = Key;
type KeyRef = Key;
type ColumnFamily = ColumnFamily;
type Cursor = KvsCursor;
type EntryIter = EntryIterator;
type WriteBatch = KvsWriteBatch;
fn open(_path: &Path) -> Result<Kvs> {
unimplemented!()
}
fn columns(&self) -> Vec<&'static str> {
unimplemented!()
}
fn destroy(_path: &Path) -> Result<()> {
unimplemented!()
}
fn cf_handle(&self, _cf: &str) -> ColumnFamily {
fn cf_handle(&self, _cf: &str) -> Option<ColumnFamily> {
unimplemented!()
}
@ -44,125 +82,28 @@ impl Backend for Kvs {
unimplemented!()
}
fn put_cf(&self, _cf: ColumnFamily, _key: &Key, _value: &[u8]) -> Result<()> {
fn put_cf(&self, _cf: ColumnFamily, _key: &Key, _data: &[u8]) -> Result<()> {
unimplemented!()
}
fn delete_cf(&self, _cf: ColumnFamily, _key: &Key) -> Result<()> {
fn delete_cf(&self, _cf: Self::ColumnFamily, _key: &Key) -> Result<()> {
unimplemented!()
}
fn iterator_cf(&self, _cf: ColumnFamily) -> Result<Dummy> {
fn raw_iterator_cf(&self, _cf: Self::ColumnFamily) -> Result<Self::Cursor> {
unimplemented!()
}
fn raw_iterator_cf(&self, _cf: ColumnFamily) -> Result<Dummy> {
fn write(&self, _batch: Self::WriteBatch) -> Result<()> {
unimplemented!()
}
fn batch(&self) -> Result<Dummy> {
unimplemented!()
}
fn write(&self, _batch: Dummy) -> Result<()> {
fn batch(&self) -> Result<Self::WriteBatch> {
unimplemented!()
}
}
impl Column<Kvs> for cf::Coding {
const NAME: &'static str = super::ERASURE_CF;
type Index = (u64, u64);
fn key(index: (u64, u64)) -> Key {
cf::Data::key(index)
}
fn index(key: &Key) -> (u64, u64) {
cf::Data::index(key)
}
}
impl Column<Kvs> for cf::Data {
const NAME: &'static str = super::DATA_CF;
type Index = (u64, u64);
fn key((slot, index): (u64, u64)) -> Key {
let mut key = Key::default();
BigEndian::write_u64(&mut key.0[8..16], slot);
BigEndian::write_u64(&mut key.0[16..], index);
key
}
fn index(key: &Key) -> (u64, u64) {
let slot = BigEndian::read_u64(&key.0[8..16]);
let index = BigEndian::read_u64(&key.0[16..]);
(slot, index)
}
}
impl Column<Kvs> for cf::Orphans {
const NAME: &'static str = super::ORPHANS_CF;
type Index = u64;
fn key(slot: u64) -> Key {
let mut key = Key::default();
BigEndian::write_u64(&mut key.0[8..16], slot);
key
}
fn index(key: &Key) -> u64 {
BigEndian::read_u64(&key.0[8..16])
}
}
impl TypedColumn<Kvs> for cf::Orphans {
type Type = bool;
}
impl Column<Kvs> for cf::SlotMeta {
const NAME: &'static str = super::META_CF;
type Index = u64;
fn key(slot: u64) -> Key {
let mut key = Key::default();
BigEndian::write_u64(&mut key.0[8..16], slot);
key
}
fn index(key: &Key) -> u64 {
BigEndian::read_u64(&key.0[8..16])
}
}
impl TypedColumn<Kvs> for cf::SlotMeta {
type Type = super::SlotMeta;
}
#[cfg(feature = "erasure")]
impl Column<Kvs> for cf::ErasureMeta {
const NAME: &'static str = super::ERASURE_META_CF;
type Index = (u64, u64);
fn key((slot, set_index): (u64, u64)) -> Key {
let mut key = Key::default();
BigEndian::write_u64(&mut key.0[8..16], slot);
BigEndian::write_u64(&mut key.0[16..], set_index);
key
}
fn index(key: &Key) -> (u64, u64) {
let slot = BigEndian::read_u64(&key.0[8..16]);
let set_index = BigEndian::read_u64(&key.0[16..]);
(slot, set_index)
}
}
#[cfg(feature = "erasure")]
impl TypedColumn<Kvs> for cf::ErasureMeta {
type Type = super::ErasureMeta;
}
impl DbCursor<Kvs> for Dummy {
impl Cursor<Kvs> for KvsCursor {
fn valid(&self) -> bool {
unimplemented!()
}
@ -188,22 +129,124 @@ impl DbCursor<Kvs> for Dummy {
}
}
impl IWriteBatch<Kvs> for Dummy {
fn put_cf(&mut self, _cf: ColumnFamily, _key: &Key, _value: &[u8]) -> Result<()> {
unimplemented!()
}
fn delete_cf(&mut self, _cf: ColumnFamily, _key: &Key) -> Result<()> {
impl IWriteBatch<Kvs> for KvsWriteBatch {
fn put_cf(&mut self, _cf: ColumnFamily, _key: &Key, _data: &[u8]) -> Result<()> {
unimplemented!()
}
}
impl Iterator for Dummy {
type Item = (Box<Key>, Box<[u8]>);
impl IDataCf<Kvs> for DataCf {
fn new(db: Arc<Kvs>) -> Self {
DataCf { db }
}
fn next(&mut self) -> Option<Self::Item> {
fn get_by_slot_index(&self, _slot: u64, _index: u64) -> Result<Option<Vec<u8>>> {
unimplemented!()
}
fn delete_by_slot_index(&self, _slot: u64, _index: u64) -> Result<()> {
unimplemented!()
}
fn put_by_slot_index(&self, _slot: u64, _index: u64, _serialized_value: &[u8]) -> Result<()> {
unimplemented!()
}
fn key(_slot: u64, _index: u64) -> Key {
unimplemented!()
}
fn slot_from_key(_key: &Key) -> Result<u64> {
unimplemented!()
}
fn index_from_key(_key: &Key) -> Result<u64> {
unimplemented!()
}
}
impl IErasureCf<Kvs> for ErasureCf {
fn new(db: Arc<Kvs>) -> Self {
ErasureCf { db }
}
fn delete_by_slot_index(&self, _slot: u64, _index: u64) -> Result<()> {
unimplemented!()
}
fn get_by_slot_index(&self, _slot: u64, _index: u64) -> Result<Option<Vec<u8>>> {
unimplemented!()
}
fn put_by_slot_index(&self, _slot: u64, _index: u64, _serialized_value: &[u8]) -> Result<()> {
unimplemented!()
}
fn key(slot: u64, index: u64) -> Key {
DataCf::key(slot, index)
}
fn slot_from_key(key: &Key) -> Result<u64> {
DataCf::slot_from_key(key)
}
fn index_from_key(key: &Key) -> Result<u64> {
DataCf::index_from_key(key)
}
}
impl IMetaCf<Kvs> for MetaCf {
fn new(db: Arc<Kvs>) -> Self {
MetaCf { db }
}
fn key(_slot: u64) -> Key {
unimplemented!()
}
fn get_slot_meta(&self, _slot: u64) -> Result<Option<super::SlotMeta>> {
unimplemented!()
}
fn put_slot_meta(&self, _slot: u64, _slot_meta: &super::SlotMeta) -> Result<()> {
unimplemented!()
}
fn index_from_key(_key: &Key) -> Result<u64> {
unimplemented!()
}
}
impl LedgerColumnFamilyRaw<Kvs> for DataCf {
fn db(&self) -> &Arc<Kvs> {
&self.db
}
fn handle(&self) -> ColumnFamily {
self.db.cf_handle(super::DATA_CF).unwrap()
}
}
impl LedgerColumnFamilyRaw<Kvs> for ErasureCf {
fn db(&self) -> &Arc<Kvs> {
&self.db
}
fn handle(&self) -> ColumnFamily {
self.db.cf_handle(super::ERASURE_CF).unwrap()
}
}
impl LedgerColumnFamily<Kvs> for MetaCf {
type ValueType = super::SlotMeta;
fn db(&self) -> &Arc<Kvs> {
&self.db
}
fn handle(&self) -> ColumnFamily {
self.db.cf_handle(super::META_CF).unwrap()
}
}
impl std::convert::From<kvstore::Error> for Error {
@ -211,3 +254,12 @@ impl std::convert::From<kvstore::Error> for Error {
Error::BlocktreeError(BlocktreeError::KvsDb(e))
}
}
/// TODO: all this goes away with Blocktree
impl Iterator for EntryIterator {
type Item = Entry;
fn next(&mut self) -> Option<Entry> {
unimplemented!()
}
}

View File

View File

@ -1,213 +0,0 @@
#[cfg(feature = "erasure")]
use crate::erasure::{NUM_CODING, NUM_DATA};
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
// The Meta column family
pub struct SlotMeta {
// The number of slots above the root (the genesis block). The first
// slot has slot 0.
pub slot: u64,
// The total number of consecutive blobs starting from index 0
// we have received for this slot.
pub consumed: u64,
// The index *plus one* of the highest blob received for this slot. Useful
// for checking if the slot has received any blobs yet, and to calculate the
// range where there is one or more holes: `(consumed..received)`.
pub received: u64,
// The index of the blob that is flagged as the last blob for this slot.
pub last_index: u64,
// The slot height of the block this one derives from.
pub parent_slot: u64,
// The list of slot heights, each of which contains a block that derives
// from this one.
pub next_slots: Vec<u64>,
// True if this slot is full (consumed == last_index + 1) and if every
// slot that is a parent of this slot is also connected.
pub is_connected: bool,
// True if this slot is a root
pub is_root: bool,
}
impl SlotMeta {
pub fn is_full(&self) -> bool {
// last_index is std::u64::MAX when it has no information about how
// many blobs will fill this slot.
// Note: A full slot with zero blobs is not possible.
if self.last_index == std::u64::MAX {
return false;
}
assert!(self.consumed <= self.last_index + 1);
self.consumed == self.last_index + 1
}
pub fn is_parent_set(&self) -> bool {
self.parent_slot != std::u64::MAX
}
pub(in crate::blocktree) fn new(slot: u64, parent_slot: u64) -> Self {
SlotMeta {
slot,
consumed: 0,
received: 0,
parent_slot,
next_slots: vec![],
is_connected: slot == 0,
is_root: false,
last_index: std::u64::MAX,
}
}
}
#[cfg(feature = "erasure")]
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
/// Erasure coding information
pub struct ErasureMeta {
/// Which erasure set in the slot this is
pub set_index: u64,
/// Bitfield representing presence/absence of data blobs
pub data: u64,
/// Bitfield representing presence/absence of coding blobs
pub coding: u64,
}
#[cfg(feature = "erasure")]
impl ErasureMeta {
pub fn new(set_index: u64) -> ErasureMeta {
ErasureMeta {
set_index,
data: 0,
coding: 0,
}
}
pub fn can_recover(&self) -> bool {
let (data_missing, coding_missing) = (
NUM_DATA - self.data.count_ones() as usize,
NUM_CODING - self.coding.count_ones() as usize,
);
data_missing > 0 && data_missing + coding_missing <= NUM_CODING
}
pub fn is_coding_present(&self, index: u64) -> bool {
let set_index = Self::set_index_for(index);
let position = index - self.start_index();
set_index == self.set_index && self.coding & (1 << position) != 0
}
pub fn set_coding_present(&mut self, index: u64) {
let set_index = Self::set_index_for(index);
if set_index as u64 == self.set_index {
let position = index - self.start_index();
self.coding |= 1 << position;
}
}
pub fn is_data_present(&self, index: u64) -> bool {
let set_index = Self::set_index_for(index);
let position = index - self.start_index();
set_index == self.set_index && self.data & (1 << position) != 0
}
pub fn set_data_present(&mut self, index: u64) {
let set_index = Self::set_index_for(index);
if set_index as u64 == self.set_index {
let position = index - self.start_index();
self.data |= 1 << position;
}
}
pub fn set_index_for(index: u64) -> u64 {
index / NUM_DATA as u64
}
pub fn start_index(&self) -> u64 {
self.set_index * NUM_DATA as u64
}
/// returns a tuple of (data_end, coding_end)
pub fn end_indexes(&self) -> (u64, u64) {
let start = self.start_index();
(start + NUM_DATA as u64, start + NUM_CODING as u64)
}
}
#[cfg(feature = "erasure")]
#[test]
fn test_meta_coding_present() {
let set_index = 0;
let mut e_meta = ErasureMeta {
set_index,
data: 0,
coding: 0,
};
for i in 0..NUM_CODING as u64 {
e_meta.set_coding_present(i);
assert_eq!(e_meta.is_coding_present(i), true);
}
for i in NUM_CODING as u64..NUM_DATA as u64 {
assert_eq!(e_meta.is_coding_present(i), false);
}
e_meta.set_index = ErasureMeta::set_index_for((NUM_DATA * 17) as u64);
for i in (NUM_DATA * 17) as u64..((NUM_DATA * 17) + NUM_CODING) as u64 {
e_meta.set_coding_present(i);
assert_eq!(e_meta.is_coding_present(i), true);
}
for i in (NUM_DATA * 17 + NUM_CODING) as u64..((NUM_DATA * 17) + NUM_DATA) as u64 {
assert_eq!(e_meta.is_coding_present(i), false);
}
}
#[cfg(feature = "erasure")]
#[test]
fn test_can_recover() {
let set_index = 0;
let mut e_meta = ErasureMeta {
set_index,
data: 0,
coding: 0,
};
assert!(!e_meta.can_recover());
e_meta.data = 0b1111_1111_1111_1111;
e_meta.coding = 0x00;
assert!(!e_meta.can_recover());
e_meta.coding = 0x0e;
assert_eq!(0x0fu8, 0b0000_1111u8);
assert!(!e_meta.can_recover());
e_meta.data = 0b0111_1111_1111_1111;
assert!(e_meta.can_recover());
e_meta.data = 0b0111_1111_1111_1110;
assert!(e_meta.can_recover());
e_meta.data = 0b0111_1111_1011_1110;
assert!(e_meta.can_recover());
e_meta.data = 0b0111_1011_1011_1110;
assert!(!e_meta.can_recover());
e_meta.data = 0b0111_1011_1011_1110;
assert!(!e_meta.can_recover());
e_meta.coding = 0b0000_1110;
e_meta.data = 0b1111_1111_1111_1100;
assert!(e_meta.can_recover());
e_meta.data = 0b1111_1111_1111_1000;
assert!(e_meta.can_recover());
}

View File

@ -1,17 +1,29 @@
use crate::blocktree::db::columns as cf;
use crate::blocktree::db::{Backend, Column, DbCursor, IWriteBatch, TypedColumn};
use crate::blocktree::BlocktreeError;
use crate::entry::Entry;
use crate::packet::{Blob, BLOB_HEADER_SIZE};
use crate::result::{Error, Result};
use byteorder::{BigEndian, ByteOrder};
use bincode::deserialize;
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
use rocksdb::{
self, ColumnFamily, ColumnFamilyDescriptor, DBIterator, DBRawIterator, IteratorMode, Options,
self, ColumnFamily, ColumnFamilyDescriptor, DBRawIterator, IteratorMode, Options,
WriteBatch as RWriteBatch, DB,
};
use solana_sdk::hash::Hash;
use solana_sdk::timing::DEFAULT_TICKS_PER_SLOT;
use std::fs;
use std::io;
use std::path::Path;
use std::sync::Arc;
use super::db::{
Cursor, Database, IDataCf, IErasureCf, IMetaCf, IWriteBatch, LedgerColumnFamily,
LedgerColumnFamilyRaw,
};
use super::{Blocktree, BlocktreeError};
// A good value for this is the number of cores on the machine
const TOTAL_THREADS: i32 = 8;
@ -20,222 +32,194 @@ const MAX_WRITE_BUFFER_SIZE: usize = 512 * 1024 * 1024;
#[derive(Debug)]
pub struct Rocks(rocksdb::DB);
impl Backend for Rocks {
type Key = [u8];
type OwnedKey = Vec<u8>;
type ColumnFamily = ColumnFamily;
type Cursor = DBRawIterator;
type Iter = DBIterator;
type WriteBatch = RWriteBatch;
type Error = rocksdb::Error;
/// The metadata column family
#[derive(Debug)]
pub struct MetaCf {
db: Arc<Rocks>,
}
fn open(path: &Path) -> Result<Rocks> {
#[cfg(feature = "erasure")]
use crate::blocktree::db::columns::ErasureMeta;
use crate::blocktree::db::columns::{Coding, Data, Orphans, SlotMeta};
/// The data column family
#[derive(Debug)]
pub struct DataCf {
db: Arc<Rocks>,
}
fs::create_dir_all(&path)?;
/// The erasure column family
#[derive(Debug)]
pub struct ErasureCf {
db: Arc<Rocks>,
}
/// TODO: all this goes away with Blocktree
pub struct EntryIterator {
db_iterator: DBRawIterator,
// TODO: remove me when replay_stage is iterating by block (Blocktree)
// this verification is duplicating that of replay_stage, which
// can do this in parallel
blockhash: Option<Hash>,
// https://github.com/rust-rocksdb/rust-rocksdb/issues/234
// rocksdb issue: the _blocktree member must be lower in the struct to prevent a crash
// when the db_iterator member above is dropped.
// _blocktree is unused, but dropping _blocktree results in a broken db_iterator
// you have to hold the database open in order to iterate over it, and in order
// for db_iterator to be able to run Drop
// _blocktree: Blocktree,
}
impl Blocktree {
/// Opens a Ledger in directory, provides "infinite" window of blobs
pub fn open(ledger_path: &str) -> Result<Blocktree> {
fs::create_dir_all(&ledger_path)?;
let ledger_path = Path::new(ledger_path).join(super::BLOCKTREE_DIRECTORY);
// Use default database options
let db_options = get_db_options();
let db_options = Blocktree::get_db_options();
// Column family names
let meta_cf_descriptor = ColumnFamilyDescriptor::new(SlotMeta::NAME, get_cf_options());
let data_cf_descriptor = ColumnFamilyDescriptor::new(Data::NAME, get_cf_options());
let erasure_cf_descriptor = ColumnFamilyDescriptor::new(Coding::NAME, get_cf_options());
#[cfg(feature = "erasure")]
let erasure_meta_cf_descriptor =
ColumnFamilyDescriptor::new(ErasureMeta::NAME, get_cf_options());
let orphans_cf_descriptor = ColumnFamilyDescriptor::new(Orphans::NAME, get_cf_options());
let meta_cf_descriptor =
ColumnFamilyDescriptor::new(super::META_CF, Blocktree::get_cf_options());
let data_cf_descriptor =
ColumnFamilyDescriptor::new(super::DATA_CF, Blocktree::get_cf_options());
let erasure_cf_descriptor =
ColumnFamilyDescriptor::new(super::ERASURE_CF, Blocktree::get_cf_options());
let cfs = vec![
meta_cf_descriptor,
data_cf_descriptor,
erasure_cf_descriptor,
#[cfg(feature = "erasure")]
erasure_meta_cf_descriptor,
orphans_cf_descriptor,
];
// Open the database
let db = Rocks(DB::open_cf_descriptors(&db_options, path, cfs)?);
let db = Arc::new(Rocks(DB::open_cf_descriptors(
&db_options,
ledger_path,
cfs,
)?));
Ok(db)
// Create the metadata column family
let meta_cf = MetaCf::new(db.clone());
// Create the data column family
let data_cf = DataCf::new(db.clone());
// Create the erasure column family
let erasure_cf = ErasureCf::new(db.clone());
let ticks_per_slot = DEFAULT_TICKS_PER_SLOT;
Ok(Blocktree {
db,
meta_cf,
data_cf,
erasure_cf,
new_blobs_signals: vec![],
ticks_per_slot,
})
}
fn columns(&self) -> Vec<&'static str> {
#[cfg(feature = "erasure")]
use crate::blocktree::db::columns::ErasureMeta;
use crate::blocktree::db::columns::{Coding, Data, Orphans, SlotMeta};
vec![
Coding::NAME,
#[cfg(feature = "erasure")]
ErasureMeta::NAME,
Data::NAME,
Orphans::NAME,
SlotMeta::NAME,
]
pub fn read_ledger_blobs(&self) -> impl Iterator<Item = Blob> {
self.db
.0
.iterator_cf(self.data_cf.handle(), IteratorMode::Start)
.unwrap()
.map(|(_, blob_data)| Blob::new(&blob_data))
}
fn destroy(path: &Path) -> Result<()> {
DB::destroy(&Options::default(), path)?;
/// Return an iterator for all the entries in the given file.
pub fn read_ledger(&self) -> Result<impl Iterator<Item = Entry>> {
let mut db_iterator = self.db.raw_iterator_cf(self.data_cf.handle())?;
db_iterator.seek_to_first();
Ok(EntryIterator {
db_iterator,
blockhash: None,
})
}
pub fn destroy(ledger_path: &str) -> Result<()> {
// DB::destroy() fails if `ledger_path` doesn't exist
fs::create_dir_all(&ledger_path)?;
let ledger_path = Path::new(ledger_path).join(super::BLOCKTREE_DIRECTORY);
DB::destroy(&Options::default(), &ledger_path)?;
Ok(())
}
fn cf_handle(&self, cf: &str) -> ColumnFamily {
self.0
.cf_handle(cf)
.expect("should never get an unknown column")
fn get_cf_options() -> Options {
let mut options = Options::default();
options.set_max_write_buffer_number(32);
options.set_write_buffer_size(MAX_WRITE_BUFFER_SIZE);
options.set_max_bytes_for_level_base(MAX_WRITE_BUFFER_SIZE as u64);
options
}
fn get_db_options() -> Options {
let mut options = Options::default();
options.create_if_missing(true);
options.create_missing_column_families(true);
options.increase_parallelism(TOTAL_THREADS);
options.set_max_background_flushes(4);
options.set_max_background_compactions(4);
options.set_max_write_buffer_number(32);
options.set_write_buffer_size(MAX_WRITE_BUFFER_SIZE);
options.set_max_bytes_for_level_base(MAX_WRITE_BUFFER_SIZE as u64);
options
}
}
impl Database for Rocks {
type Error = rocksdb::Error;
type Key = Vec<u8>;
type KeyRef = [u8];
type ColumnFamily = ColumnFamily;
type Cursor = DBRawIterator;
type EntryIter = EntryIterator;
type WriteBatch = RWriteBatch;
fn cf_handle(&self, cf: &str) -> Option<ColumnFamily> {
self.0.cf_handle(cf)
}
fn get_cf(&self, cf: ColumnFamily, key: &[u8]) -> Result<Option<Vec<u8>>> {
let opt = self.0.get_cf(cf, key)?.map(|db_vec| db_vec.to_vec());
Ok(opt)
let opt = self.0.get_cf(cf, key)?;
Ok(opt.map(|dbvec| dbvec.to_vec()))
}
fn put_cf(&self, cf: ColumnFamily, key: &[u8], value: &[u8]) -> Result<()> {
self.0.put_cf(cf, key, value)?;
fn put_cf(&self, cf: ColumnFamily, key: &[u8], data: &[u8]) -> Result<()> {
self.0.put_cf(cf, key, data)?;
Ok(())
}
fn delete_cf(&self, cf: ColumnFamily, key: &[u8]) -> Result<()> {
self.0.delete_cf(cf, key)?;
Ok(())
fn delete_cf(&self, cf: Self::ColumnFamily, key: &[u8]) -> Result<()> {
self.0.delete_cf(cf, key).map_err(From::from)
}
fn iterator_cf(&self, cf: ColumnFamily) -> Result<DBIterator> {
let raw_iter = self.0.iterator_cf(cf, IteratorMode::Start)?;
Ok(raw_iter)
fn raw_iterator_cf(&self, cf: Self::ColumnFamily) -> Result<Self::Cursor> {
Ok(self.0.raw_iterator_cf(cf)?)
}
fn raw_iterator_cf(&self, cf: ColumnFamily) -> Result<DBRawIterator> {
let raw_iter = self.0.raw_iterator_cf(cf)?;
Ok(raw_iter)
fn write(&self, batch: Self::WriteBatch) -> Result<()> {
self.0.write(batch).map_err(From::from)
}
fn batch(&self) -> Result<RWriteBatch> {
fn batch(&self) -> Result<Self::WriteBatch> {
Ok(RWriteBatch::default())
}
fn write(&self, batch: RWriteBatch) -> Result<()> {
self.0.write(batch)?;
Ok(())
}
}
impl Column<Rocks> for cf::Coding {
const NAME: &'static str = super::ERASURE_CF;
type Index = (u64, u64);
fn key(index: (u64, u64)) -> Vec<u8> {
cf::Data::key(index)
}
fn index(key: &[u8]) -> (u64, u64) {
cf::Data::index(key)
}
}
impl Column<Rocks> for cf::Data {
const NAME: &'static str = super::DATA_CF;
type Index = (u64, u64);
fn key((slot, index): (u64, u64)) -> Vec<u8> {
let mut key = vec![0; 16];
BigEndian::write_u64(&mut key[..8], slot);
BigEndian::write_u64(&mut key[8..16], index);
key
}
fn index(key: &[u8]) -> (u64, u64) {
let slot = BigEndian::read_u64(&key[..8]);
let index = BigEndian::read_u64(&key[8..16]);
(slot, index)
}
}
impl Column<Rocks> for cf::Orphans {
const NAME: &'static str = super::ORPHANS_CF;
type Index = u64;
fn key(slot: u64) -> Vec<u8> {
let mut key = vec![0; 8];
BigEndian::write_u64(&mut key[..], slot);
key
}
fn index(key: &[u8]) -> u64 {
BigEndian::read_u64(&key[..8])
}
}
impl TypedColumn<Rocks> for cf::Orphans {
type Type = bool;
}
impl Column<Rocks> for cf::SlotMeta {
const NAME: &'static str = super::META_CF;
type Index = u64;
fn key(slot: u64) -> Vec<u8> {
let mut key = vec![0; 8];
BigEndian::write_u64(&mut key[..], slot);
key
}
fn index(key: &[u8]) -> u64 {
BigEndian::read_u64(&key[..8])
}
}
impl TypedColumn<Rocks> for cf::SlotMeta {
type Type = super::SlotMeta;
}
#[cfg(feature = "erasure")]
impl Column<Rocks> for cf::ErasureMeta {
const NAME: &'static str = super::ERASURE_META_CF;
type Index = (u64, u64);
fn index(key: &[u8]) -> (u64, u64) {
let slot = BigEndian::read_u64(&key[..8]);
let set_index = BigEndian::read_u64(&key[8..]);
(slot, set_index)
}
fn key((slot, set_index): (u64, u64)) -> Vec<u8> {
let mut key = vec![0; 16];
BigEndian::write_u64(&mut key[..8], slot);
BigEndian::write_u64(&mut key[8..], set_index);
key
}
}
#[cfg(feature = "erasure")]
impl TypedColumn<Rocks> for cf::ErasureMeta {
type Type = super::ErasureMeta;
}
impl DbCursor<Rocks> for DBRawIterator {
impl Cursor<Rocks> for DBRawIterator {
fn valid(&self) -> bool {
DBRawIterator::valid(self)
}
fn seek(&mut self, key: &[u8]) {
DBRawIterator::seek(self, key);
DBRawIterator::seek(self, key)
}
fn seek_to_first(&mut self) {
DBRawIterator::seek_to_first(self);
DBRawIterator::seek_to_first(self)
}
fn next(&mut self) {
DBRawIterator::next(self);
DBRawIterator::next(self)
}
fn key(&self) -> Option<Vec<u8>> {
@ -248,14 +232,141 @@ impl DbCursor<Rocks> for DBRawIterator {
}
impl IWriteBatch<Rocks> for RWriteBatch {
fn put_cf(&mut self, cf: ColumnFamily, key: &[u8], value: &[u8]) -> Result<()> {
RWriteBatch::put_cf(self, cf, key, value)?;
fn put_cf(&mut self, cf: ColumnFamily, key: &[u8], data: &[u8]) -> Result<()> {
RWriteBatch::put_cf(self, cf, key, data)?;
Ok(())
}
}
fn delete_cf(&mut self, cf: ColumnFamily, key: &[u8]) -> Result<()> {
RWriteBatch::delete_cf(self, cf, key)?;
Ok(())
impl IDataCf<Rocks> for DataCf {
fn new(db: Arc<Rocks>) -> Self {
DataCf { db }
}
fn get_by_slot_index(&self, slot: u64, index: u64) -> Result<Option<Vec<u8>>> {
let key = Self::key(slot, index);
self.get(&key)
}
fn delete_by_slot_index(&self, slot: u64, index: u64) -> Result<()> {
let key = Self::key(slot, index);
self.delete(&key)
}
fn put_by_slot_index(&self, slot: u64, index: u64, serialized_value: &[u8]) -> Result<()> {
let key = Self::key(slot, index);
self.put(&key, serialized_value)
}
fn key(slot: u64, index: u64) -> Vec<u8> {
let mut key = vec![0u8; 16];
BigEndian::write_u64(&mut key[0..8], slot);
BigEndian::write_u64(&mut key[8..16], index);
key
}
fn slot_from_key(key: &[u8]) -> Result<u64> {
let mut rdr = io::Cursor::new(&key[0..8]);
let height = rdr.read_u64::<BigEndian>()?;
Ok(height)
}
fn index_from_key(key: &[u8]) -> Result<u64> {
let mut rdr = io::Cursor::new(&key[8..16]);
let index = rdr.read_u64::<BigEndian>()?;
Ok(index)
}
}
impl IErasureCf<Rocks> for ErasureCf {
fn new(db: Arc<Rocks>) -> Self {
ErasureCf { db }
}
fn delete_by_slot_index(&self, slot: u64, index: u64) -> Result<()> {
let key = Self::key(slot, index);
self.delete(&key)
}
fn get_by_slot_index(&self, slot: u64, index: u64) -> Result<Option<Vec<u8>>> {
let key = Self::key(slot, index);
self.get(&key)
}
fn put_by_slot_index(&self, slot: u64, index: u64, serialized_value: &[u8]) -> Result<()> {
let key = Self::key(slot, index);
self.put(&key, serialized_value)
}
fn key(slot: u64, index: u64) -> Vec<u8> {
DataCf::key(slot, index)
}
fn slot_from_key(key: &[u8]) -> Result<u64> {
DataCf::slot_from_key(key)
}
fn index_from_key(key: &[u8]) -> Result<u64> {
DataCf::index_from_key(key)
}
}
impl IMetaCf<Rocks> for MetaCf {
fn new(db: Arc<Rocks>) -> Self {
MetaCf { db }
}
fn key(slot: u64) -> Vec<u8> {
let mut key = vec![0u8; 8];
BigEndian::write_u64(&mut key[0..8], slot);
key
}
fn get_slot_meta(&self, slot: u64) -> Result<Option<super::SlotMeta>> {
let key = Self::key(slot);
self.get(&key)
}
fn put_slot_meta(&self, slot: u64, slot_meta: &super::SlotMeta) -> Result<()> {
let key = Self::key(slot);
self.put(&key, slot_meta)
}
fn index_from_key(key: &[u8]) -> Result<u64> {
let mut rdr = io::Cursor::new(&key[..]);
let index = rdr.read_u64::<BigEndian>()?;
Ok(index)
}
}
impl LedgerColumnFamilyRaw<Rocks> for DataCf {
fn db(&self) -> &Arc<Rocks> {
&self.db
}
fn handle(&self) -> ColumnFamily {
self.db.cf_handle(super::DATA_CF).unwrap()
}
}
impl LedgerColumnFamilyRaw<Rocks> for ErasureCf {
fn db(&self) -> &Arc<Rocks> {
&self.db
}
fn handle(&self) -> ColumnFamily {
self.db.cf_handle(super::ERASURE_CF).unwrap()
}
}
impl LedgerColumnFamily<Rocks> for MetaCf {
type ValueType = super::SlotMeta;
fn db(&self) -> &Arc<Rocks> {
&self.db
}
fn handle(&self) -> ColumnFamily {
self.db.cf_handle(super::META_CF).unwrap()
}
}
@ -265,23 +376,25 @@ impl std::convert::From<rocksdb::Error> for Error {
}
}
fn get_cf_options() -> Options {
let mut options = Options::default();
options.set_max_write_buffer_number(32);
options.set_write_buffer_size(MAX_WRITE_BUFFER_SIZE);
options.set_max_bytes_for_level_base(MAX_WRITE_BUFFER_SIZE as u64);
options
}
/// TODO: all this goes away with Blocktree
impl Iterator for EntryIterator {
type Item = Entry;
fn get_db_options() -> Options {
let mut options = Options::default();
options.create_if_missing(true);
options.create_missing_column_families(true);
options.increase_parallelism(TOTAL_THREADS);
options.set_max_background_flushes(4);
options.set_max_background_compactions(4);
options.set_max_write_buffer_number(32);
options.set_write_buffer_size(MAX_WRITE_BUFFER_SIZE);
options.set_max_bytes_for_level_base(MAX_WRITE_BUFFER_SIZE as u64);
options
fn next(&mut self) -> Option<Entry> {
if self.db_iterator.valid() {
if let Some(value) = self.db_iterator.value() {
if let Ok(entry) = deserialize::<Entry>(&value[BLOB_HEADER_SIZE..]) {
if let Some(blockhash) = self.blockhash {
if !entry.verify(&blockhash) {
return None;
}
}
self.db_iterator.next();
self.blockhash = Some(entry.hash);
return Some(entry);
}
}
}
None
}
}

View File

@ -4,16 +4,22 @@ use crate::entry::{Entry, EntrySlice};
use crate::leader_schedule_utils;
use rayon::prelude::*;
use solana_metrics::counter::Counter;
use solana_runtime::bank::Bank;
use solana_runtime::locked_accounts_results::LockedAccountsResults;
use solana_runtime::bank::{Bank, BankError, Result};
use solana_sdk::genesis_block::GenesisBlock;
use solana_sdk::timing::duration_as_ms;
use solana_sdk::timing::MAX_RECENT_BLOCKHASHES;
use solana_sdk::transaction::{Result, TransactionError};
use std::result;
use std::sync::Arc;
use std::time::Instant;
pub fn process_entry(bank: &Bank, entry: &Entry) -> Result<()> {
if !entry.is_tick() {
first_err(&bank.process_transactions(&entry.transactions))?;
} else {
bank.register_tick(&entry.hash);
}
Ok(())
}
fn first_err(results: &[Result<()>]) -> Result<()> {
for r in results {
r.clone()?;
@ -21,55 +27,30 @@ fn first_err(results: &[Result<()>]) -> Result<()> {
Ok(())
}
fn is_unexpected_validator_error(r: &Result<()>) -> bool {
match r {
Err(TransactionError::DuplicateSignature) => true,
_ => false,
}
}
fn par_execute_entries(bank: &Bank, entries: &[(&Entry, LockedAccountsResults)]) -> Result<()> {
fn par_execute_entries(bank: &Bank, entries: &[(&Entry, Vec<Result<()>>)]) -> Result<()> {
inc_new_counter_info!("bank-par_execute_entries-count", entries.len());
let results: Vec<Result<()>> = entries
.into_par_iter()
.map(|(e, locked_accounts)| {
.map(|(e, lock_results)| {
let results = bank.load_execute_and_commit_transactions(
&e.transactions,
locked_accounts,
lock_results.to_vec(),
MAX_RECENT_BLOCKHASHES,
);
let mut first_err = None;
for r in results {
if let Err(ref e) = r {
if first_err.is_none() {
first_err = Some(r.clone());
}
if is_unexpected_validator_error(&r) {
warn!("Unexpected validator error: {:?}", e);
solana_metrics::submit(
solana_metrics::influxdb::Point::new("validator_process_entry_error")
.add_field(
"error",
solana_metrics::influxdb::Value::String(format!("{:?}", e)),
)
.to_owned(),
)
}
}
}
first_err.unwrap_or(Ok(()))
bank.unlock_accounts(&e.transactions, &results);
first_err(&results)
})
.collect();
first_err(&results)
}
/// Process an ordered list of entries in parallel
/// process entries in parallel
/// 1. In order lock accounts for each entry while the lock succeeds, up to a Tick entry
/// 2. Process the locked group in parallel
/// 3. Register the `Tick` if it's available
/// 4. Update the leader scheduler, goto 1
pub fn process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
fn par_process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
// accumulator for entries that can be processed in parallel
let mut mt_group = vec![];
for entry in entries {
@ -84,12 +65,11 @@ pub fn process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
let lock_results = bank.lock_accounts(&entry.transactions);
// if any of the locks error out
// execute the current group
if first_err(lock_results.locked_accounts_results()).is_err() {
if first_err(&lock_results).is_err() {
par_execute_entries(bank, &mt_group)?;
// Drop all the locks on accounts by clearing the LockedAccountsFinalizer's in the
// mt_group
mt_group = vec![];
drop(lock_results);
//reset the lock and push the entry
bank.unlock_accounts(&entry.transactions, &lock_results);
let lock_results = bank.lock_accounts(&entry.transactions);
mt_group.push((entry, lock_results));
} else {
@ -101,22 +81,22 @@ pub fn process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
Ok(())
}
/// Process an ordered list of entries.
pub fn process_entries(bank: &Bank, entries: &[Entry]) -> Result<()> {
par_process_entries(bank, entries)
}
#[derive(Debug, PartialEq)]
pub struct BankForksInfo {
pub bank_slot: u64,
pub entry_height: u64,
}
#[derive(Debug)]
pub enum BlocktreeProcessorError {
LedgerVerificationFailed,
}
pub fn process_blocktree(
genesis_block: &GenesisBlock,
blocktree: &Blocktree,
account_paths: Option<String>,
) -> result::Result<(BankForks, Vec<BankForksInfo>), BlocktreeProcessorError> {
) -> Result<(BankForks, Vec<BankForksInfo>)> {
let now = Instant::now();
info!("processing ledger...");
@ -132,7 +112,7 @@ pub fn process_blocktree(
.meta(slot)
.map_err(|err| {
warn!("Failed to load meta for slot {}: {:?}", slot, err);
BlocktreeProcessorError::LedgerVerificationFailed
BankError::LedgerVerificationFailed
})?
.unwrap();
@ -147,7 +127,7 @@ pub fn process_blocktree(
// Fetch all entries for this slot
let mut entries = blocktree.get_slot_entries(slot, 0, None).map_err(|err| {
warn!("Failed to load entries for slot {}: {:?}", slot, err);
BlocktreeProcessorError::LedgerVerificationFailed
BankError::LedgerVerificationFailed
})?;
if slot == 0 {
@ -156,12 +136,12 @@ pub fn process_blocktree(
// processed by the bank, skip over it.
if entries.is_empty() {
warn!("entry0 not present");
return Err(BlocktreeProcessorError::LedgerVerificationFailed);
return Err(BankError::LedgerVerificationFailed);
}
let entry0 = entries.remove(0);
if !(entry0.is_tick() && entry0.verify(&last_entry_hash)) {
warn!("Ledger proof of history failed at entry0");
return Err(BlocktreeProcessorError::LedgerVerificationFailed);
return Err(BankError::LedgerVerificationFailed);
}
last_entry_hash = entry0.hash;
entry_height += 1;
@ -173,23 +153,20 @@ pub fn process_blocktree(
"Ledger proof of history failed at slot: {}, entry: {}",
slot, entry_height
);
return Err(BlocktreeProcessorError::LedgerVerificationFailed);
return Err(BankError::LedgerVerificationFailed);
}
process_entries(&bank, &entries).map_err(|err| {
warn!("Failed to process entries for slot {}: {:?}", slot, err);
BlocktreeProcessorError::LedgerVerificationFailed
BankError::LedgerVerificationFailed
})?;
last_entry_hash = entries.last().unwrap().hash;
entry_height += entries.len() as u64;
}
bank.freeze(); // all banks handled by this routine are created from complete slots
if blocktree.is_root(slot) {
bank.squash();
}
// TODO merge with locktower, voting, bank.vote_accounts()...
bank.squash();
if meta.next_slots.is_empty() {
// Reached the end of this fork. Record the final entry height and last entry.hash
@ -207,7 +184,7 @@ pub fn process_blocktree(
.meta(next_slot)
.map_err(|err| {
warn!("Failed to load meta for slot {}: {:?}", slot, err);
BlocktreeProcessorError::LedgerVerificationFailed
BankError::LedgerVerificationFailed
})?
.unwrap();
@ -261,11 +238,8 @@ mod tests {
use crate::entry::{create_ticks, next_entry, Entry};
use solana_sdk::genesis_block::GenesisBlock;
use solana_sdk::hash::Hash;
use solana_sdk::instruction::InstructionError;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction;
use solana_sdk::transaction::TransactionError;
use solana_sdk::system_transaction::SystemTransaction;
fn fill_blocktree_slot_with_ticks(
blocktree: &Blocktree,
@ -306,8 +280,8 @@ mod tests {
let (ledger_path, mut blockhash) = create_new_tmp_ledger!(&genesis_block);
debug!("ledger_path: {:?}", ledger_path);
let blocktree =
Blocktree::open(&ledger_path).expect("Expected to successfully open database ledger");
let blocktree = Blocktree::open_config(&ledger_path, ticks_per_slot)
.expect("Expected to successfully open database ledger");
// Write slot 1
// slot 1, points at slot 0. Missing one tick
@ -357,7 +331,7 @@ mod tests {
slot 0
|
slot 1 <-- set_root(true)
slot 1
/ \
slot 2 |
/ |
@ -366,8 +340,8 @@ mod tests {
slot 4
*/
let blocktree =
Blocktree::open(&ledger_path).expect("Expected to successfully open database ledger");
let blocktree = Blocktree::open_config(&ledger_path, ticks_per_slot)
.expect("Expected to successfully open database ledger");
// Fork 1, ending at slot 3
let last_slot1_entry_hash =
@ -384,9 +358,6 @@ mod tests {
info!("last_fork1_entry.hash: {:?}", last_fork1_entry_hash);
info!("last_fork2_entry.hash: {:?}", last_fork2_entry_hash);
blocktree.set_root(0).unwrap();
blocktree.set_root(1).unwrap();
let (bank_forks, bank_forks_info) =
process_blocktree(&genesis_block, &blocktree, None).unwrap();
@ -398,14 +369,6 @@ mod tests {
entry_height: ticks_per_slot * 4,
}
);
assert_eq!(
&bank_forks[3]
.parents()
.iter()
.map(|bank| bank.slot())
.collect::<Vec<_>>(),
&[2, 1]
);
assert_eq!(
bank_forks_info[1],
BankForksInfo {
@ -413,16 +376,9 @@ mod tests {
entry_height: ticks_per_slot * 3,
}
);
assert_eq!(
&bank_forks[4]
.parents()
.iter()
.map(|bank| bank.slot())
.collect::<Vec<_>>(),
&[1]
);
// Ensure bank_forks holds the right banks
// Ensure bank_forks holds the right banks, and that everything's
// frozen
for info in bank_forks_info {
assert_eq!(bank_forks[info.bank_slot].slot(), info.bank_slot);
assert!(bank_forks[info.bank_slot].is_frozen());
@ -433,32 +389,32 @@ mod tests {
fn test_first_err() {
assert_eq!(first_err(&[Ok(())]), Ok(()));
assert_eq!(
first_err(&[Ok(()), Err(TransactionError::DuplicateSignature)]),
Err(TransactionError::DuplicateSignature)
first_err(&[Ok(()), Err(BankError::DuplicateSignature)]),
Err(BankError::DuplicateSignature)
);
assert_eq!(
first_err(&[
Ok(()),
Err(TransactionError::DuplicateSignature),
Err(TransactionError::AccountInUse)
Err(BankError::DuplicateSignature),
Err(BankError::AccountInUse)
]),
Err(TransactionError::DuplicateSignature)
Err(BankError::DuplicateSignature)
);
assert_eq!(
first_err(&[
Ok(()),
Err(TransactionError::AccountInUse),
Err(TransactionError::DuplicateSignature)
Err(BankError::AccountInUse),
Err(BankError::DuplicateSignature)
]),
Err(TransactionError::AccountInUse)
Err(BankError::AccountInUse)
);
assert_eq!(
first_err(&[
Err(TransactionError::AccountInUse),
Err(BankError::AccountInUse),
Ok(()),
Err(TransactionError::DuplicateSignature)
Err(BankError::DuplicateSignature)
]),
Err(TransactionError::AccountInUse)
Err(BankError::AccountInUse)
);
}
@ -470,7 +426,7 @@ mod tests {
let bank = Bank::new(&genesis_block);
let keypair = Keypair::new();
let slot_entries = create_ticks(genesis_block.ticks_per_slot - 1, genesis_block.hash());
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&mint_keypair,
&keypair.pubkey(),
1,
@ -481,18 +437,18 @@ mod tests {
// First, ensure the TX is rejected because of the unregistered last ID
assert_eq!(
bank.process_transaction(&tx),
Err(TransactionError::BlockhashNotFound)
Err(BankError::BlockhashNotFound)
);
// Now ensure the TX is accepted despite pointing to the ID of an empty entry.
process_entries(&bank, &slot_entries).unwrap();
par_process_entries(&bank, &slot_entries).unwrap();
assert_eq!(bank.process_transaction(&tx), Ok(()));
}
#[test]
fn test_process_ledger_simple() {
solana_logger::setup();
let leader_pubkey = Pubkey::new_rand();
let leader_pubkey = Keypair::new().pubkey();
let (genesis_block, mint_keypair) = GenesisBlock::new_with_leader(100, &leader_pubkey, 50);
let (ledger_path, mut last_entry_hash) = create_new_tmp_ledger!(&genesis_block);
debug!("ledger_path: {:?}", ledger_path);
@ -502,27 +458,16 @@ mod tests {
for _ in 0..3 {
// Transfer one token from the mint to a random account
let keypair = Keypair::new();
let tx = system_transaction::create_user_account(
&mint_keypair,
&keypair.pubkey(),
1,
blockhash,
0,
);
let tx =
SystemTransaction::new_account(&mint_keypair, &keypair.pubkey(), 1, blockhash, 0);
let entry = Entry::new(&last_entry_hash, 1, vec![tx]);
last_entry_hash = entry.hash;
entries.push(entry);
// Add a second Transaction that will produce a
// InstructionError<0, ResultWithNegativeLamports> error when processed
// ProgramError<0, ResultWithNegativeLamports> error when processed
let keypair2 = Keypair::new();
let tx = system_transaction::create_user_account(
&keypair,
&keypair2.pubkey(),
42,
blockhash,
0,
);
let tx = SystemTransaction::new_account(&keypair, &keypair2.pubkey(), 42, blockhash, 0);
let entry = Entry::new(&last_entry_hash, 1, vec![tx]);
last_entry_hash = entry.hash;
entries.push(entry);
@ -533,9 +478,7 @@ mod tests {
let blocktree =
Blocktree::open(&ledger_path).expect("Expected to successfully open database ledger");
blocktree
.write_entries(1, 0, 0, genesis_block.ticks_per_slot, &entries)
.unwrap();
blocktree.write_entries(1, 0, 0, &entries).unwrap();
let entry_height = genesis_block.ticks_per_slot + entries.len() as u64;
let (bank_forks, bank_forks_info) =
process_blocktree(&genesis_block, &blocktree, None).unwrap();
@ -578,19 +521,19 @@ mod tests {
}
#[test]
fn test_process_entries_tick() {
fn test_par_process_entries_tick() {
let (genesis_block, _mint_keypair) = GenesisBlock::new(1000);
let bank = Bank::new(&genesis_block);
// ensure bank can process a tick
assert_eq!(bank.tick_height(), 0);
let tick = next_entry(&genesis_block.hash(), 1, vec![]);
assert_eq!(process_entries(&bank, &[tick.clone()]), Ok(()));
assert_eq!(par_process_entries(&bank, &[tick.clone()]), Ok(()));
assert_eq!(bank.tick_height(), 1);
}
#[test]
fn test_process_entries_2_entries_collision() {
fn test_par_process_entries_2_entries_collision() {
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
let bank = Bank::new(&genesis_block);
let keypair1 = Keypair::new();
@ -599,7 +542,7 @@ mod tests {
let blockhash = bank.last_blockhash();
// ensure bank can process 2 entries that have a common account and no tick is registered
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&mint_keypair,
&keypair1.pubkey(),
2,
@ -607,7 +550,7 @@ mod tests {
0,
);
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&mint_keypair,
&keypair2.pubkey(),
2,
@ -615,14 +558,14 @@ mod tests {
0,
);
let entry_2 = next_entry(&entry_1.hash, 1, vec![tx]);
assert_eq!(process_entries(&bank, &[entry_1, entry_2]), Ok(()));
assert_eq!(par_process_entries(&bank, &[entry_1, entry_2]), Ok(()));
assert_eq!(bank.get_balance(&keypair1.pubkey()), 2);
assert_eq!(bank.get_balance(&keypair2.pubkey()), 2);
assert_eq!(bank.last_blockhash(), blockhash);
}
#[test]
fn test_process_entries_2_txes_collision() {
fn test_par_process_entries_2_txes_collision() {
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
let bank = Bank::new(&genesis_block);
let keypair1 = Keypair::new();
@ -630,14 +573,20 @@ mod tests {
let keypair3 = Keypair::new();
// fund: put 4 in each of 1 and 2
assert_matches!(bank.transfer(4, &mint_keypair, &keypair1.pubkey()), Ok(_));
assert_matches!(bank.transfer(4, &mint_keypair, &keypair2.pubkey()), Ok(_));
assert_matches!(
bank.transfer(4, &mint_keypair, &keypair1.pubkey(), bank.last_blockhash()),
Ok(_)
);
assert_matches!(
bank.transfer(4, &mint_keypair, &keypair2.pubkey(), bank.last_blockhash()),
Ok(_)
);
// construct an Entry whose 2nd transaction would cause a lock conflict with previous entry
let entry_1_to_mint = next_entry(
&bank.last_blockhash(),
1,
vec![system_transaction::create_user_account(
vec![SystemTransaction::new_account(
&keypair1,
&mint_keypair.pubkey(),
1,
@ -650,14 +599,14 @@ mod tests {
&entry_1_to_mint.hash,
1,
vec![
system_transaction::create_user_account(
SystemTransaction::new_account(
&keypair2,
&keypair3.pubkey(),
2,
bank.last_blockhash(),
0,
), // should be fine
system_transaction::create_user_account(
SystemTransaction::new_account(
&keypair1,
&mint_keypair.pubkey(),
2,
@ -668,7 +617,7 @@ mod tests {
);
assert_eq!(
process_entries(&bank, &[entry_1_to_mint, entry_2_to_3_mint_to_1]),
par_process_entries(&bank, &[entry_1_to_mint, entry_2_to_3_mint_to_1]),
Ok(())
);
@ -678,89 +627,7 @@ mod tests {
}
#[test]
fn test_process_entries_2_txes_collision_and_error() {
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
let bank = Bank::new(&genesis_block);
let keypair1 = Keypair::new();
let keypair2 = Keypair::new();
let keypair3 = Keypair::new();
let keypair4 = Keypair::new();
// fund: put 4 in each of 1 and 2
assert_matches!(bank.transfer(4, &mint_keypair, &keypair1.pubkey()), Ok(_));
assert_matches!(bank.transfer(4, &mint_keypair, &keypair2.pubkey()), Ok(_));
assert_matches!(bank.transfer(4, &mint_keypair, &keypair4.pubkey()), Ok(_));
// construct an Entry whose 2nd transaction would cause a lock conflict with previous entry
let entry_1_to_mint = next_entry(
&bank.last_blockhash(),
1,
vec![
system_transaction::create_user_account(
&keypair1,
&mint_keypair.pubkey(),
1,
bank.last_blockhash(),
0,
),
system_transaction::transfer(
&keypair4,
&keypair4.pubkey(),
1,
Hash::default(), // Should cause a transaction failure with BlockhashNotFound
0,
),
],
);
let entry_2_to_3_mint_to_1 = next_entry(
&entry_1_to_mint.hash,
1,
vec![
system_transaction::create_user_account(
&keypair2,
&keypair3.pubkey(),
2,
bank.last_blockhash(),
0,
), // should be fine
system_transaction::create_user_account(
&keypair1,
&mint_keypair.pubkey(),
2,
bank.last_blockhash(),
0,
), // will collide
],
);
assert!(process_entries(
&bank,
&[entry_1_to_mint.clone(), entry_2_to_3_mint_to_1.clone()]
)
.is_err());
// First transaction in first entry succeeded, so keypair1 lost 1 lamport
assert_eq!(bank.get_balance(&keypair1.pubkey()), 3);
assert_eq!(bank.get_balance(&keypair2.pubkey()), 4);
// Check all accounts are unlocked
let txs1 = &entry_1_to_mint.transactions[..];
let txs2 = &entry_2_to_3_mint_to_1.transactions[..];
let locked_accounts1 = bank.lock_accounts(txs1);
for result in locked_accounts1.locked_accounts_results() {
assert!(result.is_ok());
}
// txs1 and txs2 have accounts that conflict, so we must drop txs1 first
drop(locked_accounts1);
let locked_accounts2 = bank.lock_accounts(txs2);
for result in locked_accounts2.locked_accounts_results() {
assert!(result.is_ok());
}
}
#[test]
fn test_process_entries_2_entries_par() {
fn test_par_process_entries_2_entries_par() {
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
let bank = Bank::new(&genesis_block);
let keypair1 = Keypair::new();
@ -769,7 +636,7 @@ mod tests {
let keypair4 = Keypair::new();
//load accounts
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&mint_keypair,
&keypair1.pubkey(),
1,
@ -777,7 +644,7 @@ mod tests {
0,
);
assert_eq!(bank.process_transaction(&tx), Ok(()));
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&mint_keypair,
&keypair2.pubkey(),
1,
@ -788,7 +655,7 @@ mod tests {
// ensure bank can process 2 entries that do not have a common account and no tick is registered
let blockhash = bank.last_blockhash();
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&keypair1,
&keypair3.pubkey(),
1,
@ -796,7 +663,7 @@ mod tests {
0,
);
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&keypair2,
&keypair4.pubkey(),
1,
@ -804,14 +671,14 @@ mod tests {
0,
);
let entry_2 = next_entry(&entry_1.hash, 1, vec![tx]);
assert_eq!(process_entries(&bank, &[entry_1, entry_2]), Ok(()));
assert_eq!(par_process_entries(&bank, &[entry_1, entry_2]), Ok(()));
assert_eq!(bank.get_balance(&keypair3.pubkey()), 1);
assert_eq!(bank.get_balance(&keypair4.pubkey()), 1);
assert_eq!(bank.last_blockhash(), blockhash);
}
#[test]
fn test_process_entries_2_entries_tick() {
fn test_par_process_entries_2_entries_tick() {
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
let bank = Bank::new(&genesis_block);
let keypair1 = Keypair::new();
@ -820,7 +687,7 @@ mod tests {
let keypair4 = Keypair::new();
//load accounts
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&mint_keypair,
&keypair1.pubkey(),
1,
@ -828,7 +695,7 @@ mod tests {
0,
);
assert_eq!(bank.process_transaction(&tx), Ok(()));
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&mint_keypair,
&keypair2.pubkey(),
1,
@ -843,11 +710,10 @@ mod tests {
}
// ensure bank can process 2 entries that do not have a common account and tick is registered
let tx =
system_transaction::create_user_account(&keypair2, &keypair3.pubkey(), 1, blockhash, 0);
let tx = SystemTransaction::new_account(&keypair2, &keypair3.pubkey(), 1, blockhash, 0);
let entry_1 = next_entry(&blockhash, 1, vec![tx]);
let tick = next_entry(&entry_1.hash, 1, vec![]);
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&keypair1,
&keypair4.pubkey(),
1,
@ -856,14 +722,14 @@ mod tests {
);
let entry_2 = next_entry(&tick.hash, 1, vec![tx]);
assert_eq!(
process_entries(&bank, &[entry_1.clone(), tick.clone(), entry_2.clone()]),
par_process_entries(&bank, &[entry_1.clone(), tick.clone(), entry_2.clone()]),
Ok(())
);
assert_eq!(bank.get_balance(&keypair3.pubkey()), 1);
assert_eq!(bank.get_balance(&keypair4.pubkey()), 1);
// ensure that an error is returned for an empty account (keypair2)
let tx = system_transaction::create_user_account(
let tx = SystemTransaction::new_account(
&keypair2,
&keypair3.pubkey(),
1,
@ -872,91 +738,8 @@ mod tests {
);
let entry_3 = next_entry(&entry_2.hash, 1, vec![tx]);
assert_eq!(
process_entries(&bank, &[entry_3]),
Err(TransactionError::AccountNotFound)
par_process_entries(&bank, &[entry_3]),
Err(BankError::AccountNotFound)
);
}
#[test]
fn test_update_transaction_statuses() {
// Make sure instruction errors still update the signature cache
let (genesis_block, mint_keypair) = GenesisBlock::new(11_000);
let bank = Bank::new(&genesis_block);
let pubkey = Pubkey::new_rand();
bank.transfer(1_000, &mint_keypair, &pubkey).unwrap();
assert_eq!(bank.transaction_count(), 1);
assert_eq!(bank.get_balance(&pubkey), 1_000);
assert_eq!(
bank.transfer(10_001, &mint_keypair, &pubkey),
Err(TransactionError::InstructionError(
0,
InstructionError::new_result_with_negative_lamports(),
))
);
assert_eq!(
bank.transfer(10_001, &mint_keypair, &pubkey),
Err(TransactionError::DuplicateSignature)
);
// Make sure other errors don't update the signature cache
let tx = system_transaction::create_user_account(
&mint_keypair,
&pubkey,
1000,
Hash::default(),
0,
);
let signature = tx.signatures[0];
// Should fail with blockhash not found
assert_eq!(
bank.process_transaction(&tx).map(|_| signature),
Err(TransactionError::BlockhashNotFound)
);
// Should fail again with blockhash not found
assert_eq!(
bank.process_transaction(&tx).map(|_| signature),
Err(TransactionError::BlockhashNotFound)
);
}
#[test]
fn test_update_transaction_statuses_fail() {
let (genesis_block, mint_keypair) = GenesisBlock::new(11_000);
let bank = Bank::new(&genesis_block);
let keypair1 = Keypair::new();
let keypair2 = Keypair::new();
let success_tx = system_transaction::create_user_account(
&mint_keypair,
&keypair1.pubkey(),
1,
bank.last_blockhash(),
0,
);
let fail_tx = system_transaction::create_user_account(
&mint_keypair,
&keypair2.pubkey(),
2,
bank.last_blockhash(),
0,
);
let entry_1_to_mint = next_entry(
&bank.last_blockhash(),
1,
vec![
success_tx,
fail_tx.clone(), // will collide
],
);
assert_eq!(
process_entries(&bank, &[entry_1_to_mint]),
Err(TransactionError::AccountInUse)
);
// Should not see duplicate signature error
assert_eq!(bank.process_transaction(&fail_tx), Ok(()));
}
}

View File

@ -2,10 +2,10 @@
//!
use crate::blocktree::Blocktree;
use crate::cluster_info::{ClusterInfo, ClusterInfoError, DATA_PLANE_FANOUT};
use crate::entry::{EntrySender, EntrySlice};
use crate::entry::EntrySlice;
#[cfg(feature = "erasure")]
use crate::erasure::CodingGenerator;
use crate::packet::index_blobs_with_genesis;
use crate::packet::index_blobs;
use crate::poh_recorder::WorkingBankEntries;
use crate::result::{Error, Result};
use crate::service::Service;
@ -13,7 +13,6 @@ use crate::staking_utils;
use rayon::prelude::*;
use solana_metrics::counter::Counter;
use solana_metrics::{influxdb, submit};
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::timing::duration_as_ms;
use std::net::UdpSocket;
@ -42,12 +41,10 @@ impl Broadcast {
receiver: &Receiver<WorkingBankEntries>,
sock: &UdpSocket,
blocktree: &Arc<Blocktree>,
storage_entry_sender: &EntrySender,
genesis_blockhash: &Hash,
) -> Result<()> {
let timer = Duration::new(1, 0);
let (mut bank, entries) = receiver.recv_timeout(timer)?;
let mut max_tick_height = bank.max_tick_height();
let mut max_tick_height = (bank.slot() + 1) * bank.ticks_per_slot() - 1;
let now = Instant::now();
let mut num_entries = entries.len();
@ -55,7 +52,7 @@ impl Broadcast {
let mut last_tick = entries.last().map(|v| v.1).unwrap_or(0);
ventries.push(entries);
assert!(last_tick <= max_tick_height);
assert!(last_tick <= max_tick_height,);
if last_tick != max_tick_height {
while let Ok((same_bank, entries)) = receiver.try_recv() {
// If the bank changed, that implies the previous slot was interrupted and we do not have to
@ -64,7 +61,7 @@ impl Broadcast {
num_entries = 0;
ventries.clear();
bank = same_bank.clone();
max_tick_height = bank.max_tick_height();
max_tick_height = (bank.slot() + 1) * bank.ticks_per_slot() - 1;
}
num_entries += entries.len();
last_tick = entries.last().map(|v| v.1).unwrap_or(0);
@ -90,13 +87,10 @@ impl Broadcast {
let blobs: Vec<_> = ventries
.into_par_iter()
.map_with(storage_entry_sender.clone(), |s, p| {
.flat_map(|p| {
let entries: Vec<_> = p.into_iter().map(|e| e.0).collect();
let blobs = entries.to_shared_blobs();
let _ignored = s.send(entries);
blobs
entries.to_shared_blobs()
})
.flatten()
.collect();
let blob_index = blocktree
@ -105,10 +99,9 @@ impl Broadcast {
.map(|meta| meta.consumed)
.unwrap_or(0);
index_blobs_with_genesis(
index_blobs(
&blobs,
&self.id,
genesis_blockhash,
blob_index,
bank.slot(),
bank.parent().map_or(0, |parent| parent.slot()),
@ -122,9 +115,6 @@ impl Broadcast {
blocktree.write_shared_blobs(&blobs)?;
#[cfg(feature = "erasure")]
let coding = self.coding_generator.next(&blobs);
let to_blobs_elapsed = duration_as_ms(&to_blobs_start.elapsed());
let broadcast_start = Instant::now();
@ -132,14 +122,16 @@ impl Broadcast {
// Send out data
ClusterInfo::broadcast(&self.id, contains_last_tick, &broadcast_table, sock, &blobs)?;
#[cfg(feature = "erasure")]
ClusterInfo::broadcast(&self.id, false, &broadcast_table, sock, &coding)?;
inc_new_counter_info!("streamer-broadcast-sent", blobs.len());
// generate and transmit any erasure coding blobs. if erasure isn't supported, just send everything again
#[cfg(not(feature = "erasure"))]
ClusterInfo::broadcast(&self.id, contains_last_tick, &broadcast_table, sock, &blobs)?;
// Fill in the coding blob data from the window data blobs
#[cfg(feature = "erasure")]
{
let coding = self.coding_generator.next(&blobs)?;
// send out erasures
ClusterInfo::broadcast(&self.id, false, &broadcast_table, sock, &coding)?;
}
let broadcast_elapsed = duration_as_ms(&broadcast_start.elapsed());
@ -194,8 +186,6 @@ impl BroadcastStage {
cluster_info: &Arc<RwLock<ClusterInfo>>,
receiver: &Receiver<WorkingBankEntries>,
blocktree: &Arc<Blocktree>,
storage_entry_sender: EntrySender,
genesis_blockhash: &Hash,
) -> BroadcastStageReturnType {
let me = cluster_info.read().unwrap().my_data().clone();
@ -206,14 +196,7 @@ impl BroadcastStage {
};
loop {
if let Err(e) = broadcast.run(
&cluster_info,
receiver,
sock,
blocktree,
&storage_entry_sender,
genesis_blockhash,
) {
if let Err(e) = broadcast.run(&cluster_info, receiver, sock, blocktree) {
match e {
Error::RecvTimeoutError(RecvTimeoutError::Disconnected) | Error::SendError => {
return BroadcastStageReturnType::ChannelDisconnected;
@ -251,24 +234,14 @@ impl BroadcastStage {
receiver: Receiver<WorkingBankEntries>,
exit_sender: &Arc<AtomicBool>,
blocktree: &Arc<Blocktree>,
storage_entry_sender: EntrySender,
genesis_blockhash: &Hash,
) -> Self {
let blocktree = blocktree.clone();
let exit_sender = exit_sender.clone();
let genesis_blockhash = *genesis_blockhash;
let thread_hdl = Builder::new()
.name("solana-broadcaster".to_string())
.spawn(move || {
let _finalizer = Finalizer::new(exit_sender);
Self::run(
&sock,
&cluster_info,
&receiver,
&blocktree,
storage_entry_sender,
&genesis_blockhash,
)
Self::run(&sock, &cluster_info, &receiver, &blocktree)
})
.unwrap();
@ -328,7 +301,6 @@ mod test {
let cluster_info = Arc::new(RwLock::new(cluster_info));
let exit_sender = Arc::new(AtomicBool::new(false));
let (storage_sender, _receiver) = channel();
let bank = Arc::new(Bank::default());
// Start up the broadcast stage
@ -338,8 +310,6 @@ mod test {
entry_receiver,
&exit_sender,
&blocktree,
storage_sender,
&Hash::default(),
);
MockBroadcastStage {

View File

@ -97,7 +97,7 @@ mod tests {
use ring::signature::Ed25519KeyPair;
use solana_sdk::hash::{hash, Hash, Hasher};
use solana_sdk::signature::KeypairUtil;
use solana_sdk::system_transaction;
use solana_sdk::system_transaction::SystemTransaction;
use std::fs::remove_file;
use std::fs::File;
use std::io::Read;
@ -124,7 +124,7 @@ mod tests {
Entry::new_mut(
&mut id,
&mut num_hashes,
vec![system_transaction::create_user_account(
vec![SystemTransaction::new_account(
&keypair,
&keypair.pubkey(),
1,
@ -142,13 +142,11 @@ mod tests {
let ledger_dir = "chacha_test_encrypt_file";
let ledger_path = get_tmp_ledger_path(ledger_dir);
let ticks_per_slot = 16;
let blocktree = Arc::new(Blocktree::open(&ledger_path).unwrap());
let blocktree = Arc::new(Blocktree::open_config(&ledger_path, ticks_per_slot).unwrap());
let out_path = Path::new("test_chacha_encrypt_file_output.txt.enc");
let entries = make_tiny_deterministic_test_entries(32);
blocktree
.write_entries(0, 0, 0, ticks_per_slot, &entries)
.unwrap();
blocktree.write_entries(0, 0, 0, &entries).unwrap();
let mut key = hex!(
"abcd1234abcd1234abcd1234abcd1234 abcd1234abcd1234abcd1234abcd1234
@ -164,7 +162,7 @@ mod tests {
use bs58;
// golden needs to be updated if blob stuff changes....
let golden = Hash::new(
&bs58::decode("F5zEvoozmV87cgS1sfG226nYSJoupRjKA93QUQiWd31E")
&bs58::decode("B33zQ8Kc3Wr3vZAbB6GcWaB3sSGeG98nvm4QB9URpJhR")
.into_vec()
.unwrap(),
);

View File

@ -127,11 +127,9 @@ mod tests {
let ledger_dir = "test_encrypt_file_many_keys_single";
let ledger_path = get_tmp_ledger_path(ledger_dir);
let ticks_per_slot = 16;
let blocktree = Arc::new(Blocktree::open(&ledger_path).unwrap());
let blocktree = Arc::new(Blocktree::open_config(&ledger_path, ticks_per_slot).unwrap());
blocktree
.write_entries(0, 0, 0, ticks_per_slot, &entries)
.unwrap();
blocktree.write_entries(0, 0, 0, &entries).unwrap();
let out_path = Path::new("test_chacha_encrypt_file_many_keys_single_output.txt.enc");
@ -163,10 +161,8 @@ mod tests {
let ledger_dir = "test_encrypt_file_many_keys_multiple";
let ledger_path = get_tmp_ledger_path(ledger_dir);
let ticks_per_slot = 16;
let blocktree = Arc::new(Blocktree::open(&ledger_path).unwrap());
blocktree
.write_entries(0, 0, 0, ticks_per_slot, &entries)
.unwrap();
let blocktree = Arc::new(Blocktree::open_config(&ledger_path, ticks_per_slot).unwrap());
blocktree.write_entries(0, 0, 0, &entries).unwrap();
let out_path = Path::new("test_chacha_encrypt_file_many_keys_multiple_output.txt.enc");

View File

@ -20,8 +20,8 @@ use crate::crds_gossip_error::CrdsGossipError;
use crate::crds_gossip_pull::CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS;
use crate::crds_value::{CrdsValue, CrdsValueLabel, Vote};
use crate::packet::{to_shared_blob, Blob, SharedBlob, BLOB_SIZE};
use crate::repair_service::RepairType;
use crate::result::Result;
use crate::rpc_service::RPC_PORT;
use crate::staking_utils;
use crate::streamer::{BlobReceiver, BlobSender};
use bincode::{deserialize, serialize};
@ -31,9 +31,7 @@ use rand::{thread_rng, Rng};
use rayon::prelude::*;
use solana_metrics::counter::Counter;
use solana_metrics::{influxdb, submit};
use solana_netutil::{
bind_in_range, bind_to, find_available_port_in_range, multi_bind_in_range, PortRange,
};
use solana_netutil::{bind_in_range, bind_to, find_available_port_in_range, multi_bind_in_range};
use solana_runtime::bloom::Bloom;
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
@ -49,7 +47,7 @@ use std::sync::{Arc, RwLock};
use std::thread::{sleep, Builder, JoinHandle};
use std::time::{Duration, Instant};
pub const FULLNODE_PORT_RANGE: PortRange = (8000, 10_000);
pub const FULLNODE_PORT_RANGE: (u16, u16) = (8000, 10_000);
/// The fanout for Ledger Replication
pub const DATA_PLANE_FANOUT: usize = 200;
@ -60,9 +58,6 @@ pub const GROW_LAYER_CAPACITY: bool = false;
/// milliseconds we sleep for between gossip requests
pub const GOSSIP_SLEEP_MILLIS: u64 = 100;
/// the number of slots to respond with when responding to `Orphan` requests
pub const MAX_ORPHAN_REPAIR_RESPONSES: usize = 10;
#[derive(Debug, PartialEq, Eq)]
pub enum ClusterInfoError {
NoPeers,
@ -166,7 +161,6 @@ enum Protocol {
/// TODO: move this message to a different module
RequestWindowIndex(ContactInfo, u64, u64),
RequestHighestWindowIndex(ContactInfo, u64, u64),
RequestOrphan(ContactInfo, u64),
}
impl ClusterInfo {
@ -227,7 +221,7 @@ impl ClusterInfo {
self.gossip
.crds
.lookup(&entry)
.and_then(CrdsValue::contact_info)
.and_then(|x| x.contact_info())
}
pub fn my_data(&self) -> ContactInfo {
@ -246,7 +240,7 @@ impl ClusterInfo {
pub fn contact_info_trace(&self) -> String {
let leader_id = self.gossip_leader_id;
let nodes: Vec<_> = self
.tvu_peers()
.rpc_peers()
.into_iter()
.map(|node| {
let mut annotation = String::new();
@ -262,11 +256,7 @@ impl ClusterInfo {
node.id,
annotation,
node.tpu.to_string(),
if ContactInfo::is_valid_address(&node.rpc) {
node.rpc.to_string()
} else {
"none".to_string()
}
node.rpc.to_string()
)
})
.collect();
@ -292,28 +282,28 @@ impl ClusterInfo {
pub fn push_vote(&mut self, vote: Transaction) {
let now = timestamp();
let vote = Vote::new(&self.id(), vote, now);
let vote = Vote::new(vote, now);
let mut entry = CrdsValue::Vote(vote);
entry.sign(&self.keypair);
self.gossip.process_push_message(&[entry], now);
}
/// Get votes in the crds
/// * since - The timestamp of when the vote inserted must be greater than
/// * since - The local timestamp when the vote was updated or inserted must be greater then
/// since. This allows the bank to query for new votes only.
///
/// * return - The votes, and the max timestamp from the new set.
/// * return - The votes, and the max local timestamp from the new set.
pub fn get_votes(&self, since: u64) -> (Vec<Transaction>, u64) {
let votes: Vec<_> = self
.gossip
.crds
.table
.values()
.filter(|x| x.insert_timestamp > since)
.filter(|x| x.local_timestamp > since)
.filter_map(|x| {
x.value
.vote()
.map(|v| (x.insert_timestamp, v.transaction.clone()))
.map(|v| (x.local_timestamp, v.transaction.clone()))
})
.collect();
let max_ts = votes.iter().map(|x| x.0).max().unwrap_or(since);
@ -338,17 +328,6 @@ impl ClusterInfo {
.collect()
}
// All nodes in gossip, including spy nodes
pub(crate) fn all_peers(&self) -> Vec<ContactInfo> {
self.gossip
.crds
.table
.values()
.filter_map(|x| x.value.contact_info())
.cloned()
.collect()
}
pub fn gossip_peers(&self) -> Vec<ContactInfo> {
let me = self.my_data().id;
self.gossip
@ -767,13 +746,12 @@ impl ClusterInfo {
Ok(out)
}
fn orphan_bytes(&self, slot: u64) -> Result<Vec<u8>> {
let req = Protocol::RequestOrphan(self.my_data().clone(), slot);
let out = serialize(&req)?;
Ok(out)
}
pub fn repair_request(&self, repair_request: &RepairType) -> Result<(SocketAddr, Vec<u8>)> {
pub fn window_index_request(
&self,
slot: u64,
blob_index: u64,
get_highest: bool,
) -> Result<(SocketAddr, Vec<u8>)> {
// find a peer that appears to be accepting replication, as indicated
// by a valid tvu port location
let valid: Vec<_> = self.repair_peers();
@ -783,42 +761,19 @@ impl ClusterInfo {
let n = thread_rng().gen::<usize>() % valid.len();
let addr = valid[n].gossip; // send the request to the peer's gossip port
let out = {
match repair_request {
RepairType::Blob(slot, blob_index) => {
submit(
influxdb::Point::new("cluster_info-repair")
.add_field("repair-slot", influxdb::Value::Integer(*slot as i64))
.add_field("repair-ix", influxdb::Value::Integer(*blob_index as i64))
.to_owned(),
);
self.window_index_request_bytes(*slot, *blob_index)?
}
RepairType::HighestBlob(slot, blob_index) => {
submit(
influxdb::Point::new("cluster_info-repair_highest")
.add_field(
"repair-highest-slot",
influxdb::Value::Integer(*slot as i64),
)
.add_field(
"repair-highest-ix",
influxdb::Value::Integer(*blob_index as i64),
)
.to_owned(),
);
self.window_highest_index_request_bytes(*slot, *blob_index)?
}
RepairType::Orphan(slot) => {
submit(
influxdb::Point::new("cluster_info-repair_orphan")
.add_field("repair-orphan", influxdb::Value::Integer(*slot as i64))
.to_owned(),
);
self.orphan_bytes(*slot)?
}
if get_highest {
self.window_highest_index_request_bytes(slot, blob_index)?
} else {
self.window_index_request_bytes(slot, blob_index)?
}
};
submit(
influxdb::Point::new("cluster-info")
.add_field("repair-ix", influxdb::Value::Integer(blob_index as i64))
.to_owned(),
);
Ok((addr, out))
}
// If the network entrypoint hasn't been discovered yet, add it to the crds table
@ -858,12 +813,14 @@ impl ClusterInfo {
self.gossip
.crds
.lookup(&peer_label)
.and_then(CrdsValue::contact_info)
.and_then(|v| v.contact_info())
.map(|peer_info| (peer, filter, peer_info.gossip, self_info))
})
.collect();
if pr.is_empty() {
self.add_entrypoint(&mut pr);
} else {
self.entrypoint = None;
}
pr.into_iter()
.map(|(peer, filter, gossip, self_info)| {
@ -882,7 +839,7 @@ impl ClusterInfo {
self.gossip
.crds
.lookup(&peer_label)
.and_then(CrdsValue::contact_info)
.and_then(|v| v.contact_info())
.map(|p| p.gossip)
})
.map(|peer| (peer, Protocol::PushMessage(self_id, msgs.clone())))
@ -1009,35 +966,6 @@ impl ClusterInfo {
vec![]
}
fn run_orphan(
from_addr: &SocketAddr,
blocktree: Option<&Arc<Blocktree>>,
mut slot: u64,
max_responses: usize,
) -> Vec<SharedBlob> {
let mut res = vec![];
if let Some(blocktree) = blocktree {
// Try to find the next "n" parent slots of the input slot
while let Ok(Some(meta)) = blocktree.meta(slot) {
if meta.received == 0 {
break;
}
let blob = blocktree.get_data_blob(slot, meta.received - 1);
if let Ok(Some(mut blob)) = blob {
blob.meta.set_addr(from_addr);
res.push(Arc::new(RwLock::new(blob)));
}
if meta.is_parent_set() && res.len() <= max_responses {
slot = meta.parent_slot;
} else {
break;
}
}
}
res
}
//TODO we should first coalesce all the requests
fn handle_blob(
obj: &Arc<RwLock<Self>>,
@ -1080,16 +1008,21 @@ impl ClusterInfo {
.process_pull_request(caller, filter, now);
let len = data.len();
trace!("get updates since response {}", len);
let rsp = Protocol::PullResponse(self_id, data);
// The remote node may not know its public IP:PORT. Record what it looks like to us.
// This may or may not be correct for everybody, but it's better than leaving the remote with
// an unspecified address in our table
if from.gossip.ip().is_unspecified() {
inc_new_counter_info!("cluster_info-window-request-updates-unspec-gossip", 1);
from.gossip = *from_addr;
if data.is_empty() {
trace!("no updates me {}", self_id);
vec![]
} else {
let rsp = Protocol::PullResponse(self_id, data);
// the remote side may not know his public IP:PORT, record what he looks like to us
// this may or may not be correct for everybody but it's better than leaving him with
// an unspecified address in our table
if from.gossip.ip().is_unspecified() {
inc_new_counter_info!("cluster_info-window-request-updates-unspec-gossip", 1);
from.gossip = *from_addr;
}
inc_new_counter_info!("cluster_info-pull_request-rsp", len);
to_shared_blob(rsp, from.gossip).ok().into_iter().collect()
}
inc_new_counter_info!("cluster_info-pull_request-rsp", len);
to_shared_blob(rsp, from.gossip).ok().into_iter().collect()
}
fn handle_pull_response(me: &Arc<RwLock<Self>>, from: &Pubkey, data: Vec<CrdsValue>) {
let len = data.len();
@ -1149,21 +1082,14 @@ impl ClusterInfo {
vec![]
}
}
fn get_repair_sender(request: &Protocol) -> &ContactInfo {
match request {
Protocol::RequestWindowIndex(ref from, _, _) => from,
Protocol::RequestHighestWindowIndex(ref from, _, _) => from,
Protocol::RequestOrphan(ref from, _) => from,
_ => panic!("Not a repair request"),
}
}
fn handle_repair(
fn handle_request_window_index(
me: &Arc<RwLock<Self>>,
from_addr: &SocketAddr,
from: &ContactInfo,
blocktree: Option<&Arc<Blocktree>>,
request: Protocol,
slot: u64,
blob_index: u64,
from_addr: &SocketAddr,
is_get_highest: bool,
) -> Vec<SharedBlob> {
let now = Instant::now();
@ -1172,13 +1098,12 @@ impl ClusterInfo {
//TODO verify from is signed
let self_id = me.read().unwrap().gossip.id;
let from = Self::get_repair_sender(&request);
if from.id == me.read().unwrap().gossip.id {
warn!(
"{}: Ignored received repair request from ME {}",
self_id, from.id,
"{}: Ignored received RequestWindowIndex from ME {} {} {} ",
self_id, from.id, slot, blob_index,
);
inc_new_counter_info!("cluster_info-handle-repair--eq", 1);
inc_new_counter_info!("cluster_info-window-request-address-eq", 1);
return vec![];
}
@ -1188,49 +1113,26 @@ impl ClusterInfo {
.crds
.update_record_timestamp(&from.id, timestamp());
let my_info = me.read().unwrap().my_data().clone();
let (res, label) = {
match &request {
Protocol::RequestWindowIndex(from, slot, blob_index) => {
inc_new_counter_info!("cluster_info-request-window-index", 1);
(
Self::run_window_request(
from,
&from_addr,
blocktree,
&my_info,
*slot,
*blob_index,
),
"RequestWindowIndex",
)
}
Protocol::RequestHighestWindowIndex(_, slot, highest_index) => {
inc_new_counter_info!("cluster_info-request-highest-window-index", 1);
(
Self::run_highest_window_request(
&from_addr,
blocktree,
*slot,
*highest_index,
),
"RequestHighestWindowIndex",
)
}
Protocol::RequestOrphan(_, slot) => {
inc_new_counter_info!("cluster_info-request-orphan", 1);
(
Self::run_orphan(&from_addr, blocktree, *slot, MAX_ORPHAN_REPAIR_RESPONSES),
"RequestOrphan",
)
}
_ => panic!("Not a repair request"),
inc_new_counter_info!("cluster_info-window-request-recv", 1);
trace!(
"{}: received RequestWindowIndex from: {} slot: {}, blob_index: {}",
self_id,
from.id,
slot,
blob_index,
);
let res = {
if is_get_highest {
Self::run_highest_window_request(&from_addr, blocktree, slot, blob_index)
} else {
Self::run_window_request(&from, &from_addr, blocktree, &my_info, slot, blob_index)
}
};
trace!("{}: received repair request: {:?}", self_id, request);
report_time_spent(label, &now.elapsed(), "");
report_time_spent(
"RequestWindowIndex",
&now.elapsed(),
&format!("slot {}, blob_index: {}", slot, blob_index),
);
res
}
@ -1296,7 +1198,22 @@ impl ClusterInfo {
}
vec![]
}
_ => Self::handle_repair(me, from_addr, blocktree, request),
Protocol::RequestWindowIndex(from, slot, blob_index) => {
Self::handle_request_window_index(
me, &from, blocktree, slot, blob_index, from_addr, false,
)
}
Protocol::RequestHighestWindowIndex(from, slot, highest_index) => {
Self::handle_request_window_index(
me,
&from,
blocktree,
slot,
highest_index,
from_addr,
true,
)
}
}
}
@ -1430,7 +1347,6 @@ pub struct Sockets {
pub broadcast: UdpSocket,
pub repair: UdpSocket,
pub retransmit: UdpSocket,
pub storage: Option<UdpSocket>,
}
#[derive(Debug)]
@ -1441,44 +1357,9 @@ pub struct Node {
impl Node {
pub fn new_localhost() -> Self {
let pubkey = Pubkey::new_rand();
let pubkey = Keypair::new().pubkey();
Self::new_localhost_with_pubkey(&pubkey)
}
pub fn new_localhost_replicator(pubkey: &Pubkey) -> Self {
let gossip = UdpSocket::bind("127.0.0.1:0").unwrap();
let tvu = UdpSocket::bind("127.0.0.1:0").unwrap();
let storage = UdpSocket::bind("127.0.0.1:0").unwrap();
let empty = "0.0.0.0:0".parse().unwrap();
let repair = UdpSocket::bind("127.0.0.1:0").unwrap();
let broadcast = UdpSocket::bind("0.0.0.0:0").unwrap();
let retransmit = UdpSocket::bind("0.0.0.0:0").unwrap();
let info = ContactInfo::new(
pubkey,
gossip.local_addr().unwrap(),
tvu.local_addr().unwrap(),
empty,
empty,
storage.local_addr().unwrap(),
empty,
empty,
timestamp(),
);
Node {
info,
sockets: Sockets {
gossip,
tvu: vec![tvu],
tpu: vec![],
tpu_via_blobs: vec![],
broadcast,
repair,
retransmit,
storage: Some(storage),
},
}
}
pub fn new_localhost_with_pubkey(pubkey: &Pubkey) -> Self {
let tpu = UdpSocket::bind("127.0.0.1:0").unwrap();
let gossip = UdpSocket::bind("127.0.0.1:0").unwrap();
@ -1515,12 +1396,15 @@ impl Node {
broadcast,
repair,
retransmit,
storage: None,
},
}
}
fn get_gossip_port(gossip_addr: &SocketAddr, port_range: PortRange) -> (u16, UdpSocket) {
if gossip_addr.port() != 0 {
pub fn new_with_external_ip(pubkey: &Pubkey, gossip_addr: &SocketAddr) -> Node {
fn bind() -> (u16, UdpSocket) {
bind_in_range(FULLNODE_PORT_RANGE).expect("Failed to bind")
};
let (gossip_port, gossip) = if gossip_addr.port() != 0 {
(
gossip_addr.port(),
bind_to(gossip_addr.port(), false).unwrap_or_else(|e| {
@ -1528,29 +1412,22 @@ impl Node {
}),
)
} else {
Self::bind(port_range)
}
}
fn bind(port_range: PortRange) -> (u16, UdpSocket) {
bind_in_range(port_range).expect("Failed to bind")
}
pub fn new_with_external_ip(
pubkey: &Pubkey,
gossip_addr: &SocketAddr,
port_range: PortRange,
) -> Node {
let (gossip_port, gossip) = Self::get_gossip_port(gossip_addr, port_range);
bind()
};
let (tvu_port, tvu_sockets) = multi_bind_in_range(port_range, 8).expect("tvu multi_bind");
let (tvu_port, tvu_sockets) =
multi_bind_in_range(FULLNODE_PORT_RANGE, 8).expect("tvu multi_bind");
let (tpu_port, tpu_sockets) = multi_bind_in_range(port_range, 32).expect("tpu multi_bind");
let (tpu_port, tpu_sockets) =
multi_bind_in_range(FULLNODE_PORT_RANGE, 32).expect("tpu multi_bind");
let (tpu_via_blobs_port, tpu_via_blobs_sockets) =
multi_bind_in_range(port_range, 8).expect("tpu multi_bind");
multi_bind_in_range(FULLNODE_PORT_RANGE, 8).expect("tpu multi_bind");
let (_, repair) = Self::bind(port_range);
let (_, broadcast) = Self::bind(port_range);
let (_, retransmit) = Self::bind(port_range);
let (_, repair) = bind();
let (_, broadcast) = bind();
let (_, retransmit) = bind();
let (storage_port, _) = bind();
let info = ContactInfo::new(
pubkey,
@ -1558,9 +1435,9 @@ impl Node {
SocketAddr::new(gossip_addr.ip(), tvu_port),
SocketAddr::new(gossip_addr.ip(), tpu_port),
SocketAddr::new(gossip_addr.ip(), tpu_via_blobs_port),
socketaddr_any!(),
socketaddr_any!(),
socketaddr_any!(),
SocketAddr::new(gossip_addr.ip(), storage_port),
SocketAddr::new(gossip_addr.ip(), RPC_PORT),
SocketAddr::new(gossip_addr.ip(), RPC_PORT + 1),
0,
);
trace!("new ContactInfo: {:?}", info);
@ -1575,29 +1452,9 @@ impl Node {
broadcast,
repair,
retransmit,
storage: None,
},
}
}
pub fn new_replicator_with_external_ip(
pubkey: &Pubkey,
gossip_addr: &SocketAddr,
port_range: PortRange,
) -> Node {
let mut new = Self::new_with_external_ip(pubkey, gossip_addr, port_range);
let (storage_port, storage_socket) = Self::bind(port_range);
new.info.storage_addr = SocketAddr::new(gossip_addr.ip(), storage_port);
new.sockets.storage = Some(storage_socket);
let empty = socketaddr_any!();
new.info.tpu = empty;
new.info.tpu_via_blobs = empty;
new.sockets.tpu = vec![];
new.sockets.tpu_via_blobs = vec![];
new
}
}
fn report_time_spent(label: &str, time: &Duration, extra: &str) {
@ -1611,11 +1468,9 @@ fn report_time_spent(label: &str, time: &Duration, extra: &str) {
mod tests {
use super::*;
use crate::blocktree::get_tmp_ledger_path;
use crate::blocktree::tests::make_many_slot_entries;
use crate::blocktree::Blocktree;
use crate::crds_value::CrdsValueLabel;
use crate::packet::BLOB_HEADER_SIZE;
use crate::repair_service::RepairType;
use crate::result::Error;
use crate::test_tx::test_tx;
use solana_sdk::signature::{Keypair, KeypairUtil};
@ -1627,7 +1482,7 @@ mod tests {
fn test_cluster_spy_gossip() {
//check that gossip doesn't try to push to invalid addresses
let node = Node::new_localhost();
let (spy, _) = ClusterInfo::spy_node(&Pubkey::new_rand());
let (spy, _) = ClusterInfo::spy_node(&Keypair::new().pubkey());
let cluster_info = Arc::new(RwLock::new(ClusterInfo::new_with_invalid_keypair(
node.info,
)));
@ -1651,43 +1506,43 @@ mod tests {
#[test]
fn test_cluster_info_new() {
let d = ContactInfo::new_localhost(&Pubkey::new_rand(), timestamp());
let d = ContactInfo::new_localhost(&Keypair::new().pubkey(), timestamp());
let cluster_info = ClusterInfo::new_with_invalid_keypair(d.clone());
assert_eq!(d.id, cluster_info.my_data().id);
}
#[test]
fn insert_info_test() {
let d = ContactInfo::new_localhost(&Pubkey::new_rand(), timestamp());
let d = ContactInfo::new_localhost(&Keypair::new().pubkey(), timestamp());
let mut cluster_info = ClusterInfo::new_with_invalid_keypair(d);
let d = ContactInfo::new_localhost(&Pubkey::new_rand(), timestamp());
let d = ContactInfo::new_localhost(&Keypair::new().pubkey(), timestamp());
let label = CrdsValueLabel::ContactInfo(d.id);
cluster_info.insert_info(d);
assert!(cluster_info.gossip.crds.lookup(&label).is_some());
}
#[test]
fn test_insert_self() {
let d = ContactInfo::new_localhost(&Pubkey::new_rand(), timestamp());
let d = ContactInfo::new_localhost(&Keypair::new().pubkey(), timestamp());
let mut cluster_info = ClusterInfo::new_with_invalid_keypair(d.clone());
let entry_label = CrdsValueLabel::ContactInfo(cluster_info.id());
assert!(cluster_info.gossip.crds.lookup(&entry_label).is_some());
// inserting something else shouldn't work
let d = ContactInfo::new_localhost(&Pubkey::new_rand(), timestamp());
let d = ContactInfo::new_localhost(&Keypair::new().pubkey(), timestamp());
cluster_info.insert_self(d.clone());
let label = CrdsValueLabel::ContactInfo(d.id);
assert!(cluster_info.gossip.crds.lookup(&label).is_none());
}
#[test]
fn window_index_request() {
let me = ContactInfo::new_localhost(&Pubkey::new_rand(), timestamp());
let me = ContactInfo::new_localhost(&Keypair::new().pubkey(), timestamp());
let mut cluster_info = ClusterInfo::new_with_invalid_keypair(me);
let rv = cluster_info.repair_request(&RepairType::Blob(0, 0));
let rv = cluster_info.window_index_request(0, 0, false);
assert_matches!(rv, Err(Error::ClusterInfoError(ClusterInfoError::NoPeers)));
let gossip_addr = socketaddr!([127, 0, 0, 1], 1234);
let nxt = ContactInfo::new(
&Pubkey::new_rand(),
&Keypair::new().pubkey(),
gossip_addr,
socketaddr!([127, 0, 0, 1], 1235),
socketaddr!([127, 0, 0, 1], 1236),
@ -1698,15 +1553,13 @@ mod tests {
0,
);
cluster_info.insert_info(nxt.clone());
let rv = cluster_info
.repair_request(&RepairType::Blob(0, 0))
.unwrap();
let rv = cluster_info.window_index_request(0, 0, false).unwrap();
assert_eq!(nxt.gossip, gossip_addr);
assert_eq!(rv.0, nxt.gossip);
let gossip_addr2 = socketaddr!([127, 0, 0, 2], 1234);
let nxt = ContactInfo::new(
&Pubkey::new_rand(),
&Keypair::new().pubkey(),
gossip_addr2,
socketaddr!([127, 0, 0, 1], 1235),
socketaddr!([127, 0, 0, 1], 1236),
@ -1721,9 +1574,7 @@ mod tests {
let mut two = false;
while !one || !two {
//this randomly picks an option, so eventually it should pick both
let rv = cluster_info
.repair_request(&RepairType::Blob(0, 0))
.unwrap();
let rv = cluster_info.window_index_request(0, 0, false).unwrap();
if rv.0 == gossip_addr {
one = true;
}
@ -1742,7 +1593,7 @@ mod tests {
{
let blocktree = Arc::new(Blocktree::open(&ledger_path).unwrap());
let me = ContactInfo::new(
&Pubkey::new_rand(),
&Keypair::new().pubkey(),
socketaddr!("127.0.0.1:1234"),
socketaddr!("127.0.0.1:1235"),
socketaddr!("127.0.0.1:1236"),
@ -1841,46 +1692,10 @@ mod tests {
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
}
#[test]
fn run_orphan() {
solana_logger::setup();
let ledger_path = get_tmp_ledger_path!();
{
let blocktree = Arc::new(Blocktree::open(&ledger_path).unwrap());
let rv = ClusterInfo::run_orphan(&socketaddr_any!(), Some(&blocktree), 2, 0);
assert!(rv.is_empty());
// Create slots 1, 2, 3 with 5 blobs apiece
let (blobs, _) = make_many_slot_entries(1, 3, 5);
blocktree
.write_blobs(&blobs)
.expect("Expect successful ledger write");
// We don't have slot 4, so we don't know how to service this requeset
let rv = ClusterInfo::run_orphan(&socketaddr_any!(), Some(&blocktree), 4, 5);
assert!(rv.is_empty());
// For slot 3, we should return the highest blobs from slots 3, 2, 1 respectively
// for this request
let rv: Vec<_> = ClusterInfo::run_orphan(&socketaddr_any!(), Some(&blocktree), 3, 5)
.iter()
.map(|b| b.read().unwrap().clone())
.collect();
let expected: Vec<_> = (1..=3)
.rev()
.map(|slot| blocktree.get_data_blob(slot, 4).unwrap().unwrap())
.collect();
assert_eq!(rv, expected)
}
Blocktree::destroy(&ledger_path).expect("Expected successful database destruction");
}
#[test]
fn test_default_leader() {
solana_logger::setup();
let contact_info = ContactInfo::new_localhost(&Pubkey::new_rand(), 0);
let contact_info = ContactInfo::new_localhost(&Keypair::new().pubkey(), 0);
let mut cluster_info = ClusterInfo::new_with_invalid_keypair(contact_info);
let network_entry_point =
ContactInfo::new_gossip_entry_point(&socketaddr!("127.0.0.1:1239"));
@ -1888,80 +1703,69 @@ mod tests {
assert!(cluster_info.leader_data().is_none());
}
fn assert_in_range(x: u16, range: (u16, u16)) {
assert!(x >= range.0);
assert!(x < range.1);
}
fn check_sockets(sockets: &Vec<UdpSocket>, ip: IpAddr, range: (u16, u16)) {
assert!(sockets.len() > 1);
let port = sockets[0].local_addr().unwrap().port();
for socket in sockets.iter() {
check_socket(socket, ip, range);
assert_eq!(socket.local_addr().unwrap().port(), port);
}
}
fn check_socket(socket: &UdpSocket, ip: IpAddr, range: (u16, u16)) {
let local_addr = socket.local_addr().unwrap();
assert_eq!(local_addr.ip(), ip);
assert_in_range(local_addr.port(), range);
}
fn check_node_sockets(node: &Node, ip: IpAddr, range: (u16, u16)) {
check_socket(&node.sockets.gossip, ip, range);
check_socket(&node.sockets.repair, ip, range);
check_sockets(&node.sockets.tvu, ip, range);
check_sockets(&node.sockets.tpu, ip, range);
}
#[test]
fn new_with_external_ip_test_random() {
let ip = Ipv4Addr::from(0);
let node = Node::new_with_external_ip(
&Pubkey::new_rand(),
&socketaddr!(ip, 0),
FULLNODE_PORT_RANGE,
);
let node = Node::new_with_external_ip(&Keypair::new().pubkey(), &socketaddr!(ip, 0));
assert_eq!(node.sockets.gossip.local_addr().unwrap().ip(), ip);
assert!(node.sockets.tvu.len() > 1);
for tx_socket in node.sockets.tvu.iter() {
assert_eq!(tx_socket.local_addr().unwrap().ip(), ip);
}
assert!(node.sockets.tpu.len() > 1);
for tx_socket in node.sockets.tpu.iter() {
assert_eq!(tx_socket.local_addr().unwrap().ip(), ip);
}
assert_eq!(node.sockets.repair.local_addr().unwrap().ip(), ip);
check_node_sockets(&node, IpAddr::V4(ip), FULLNODE_PORT_RANGE);
assert!(node.sockets.gossip.local_addr().unwrap().port() >= FULLNODE_PORT_RANGE.0);
assert!(node.sockets.gossip.local_addr().unwrap().port() < FULLNODE_PORT_RANGE.1);
let tx_port = node.sockets.tvu[0].local_addr().unwrap().port();
assert!(tx_port >= FULLNODE_PORT_RANGE.0);
assert!(tx_port < FULLNODE_PORT_RANGE.1);
for tx_socket in node.sockets.tvu.iter() {
assert_eq!(tx_socket.local_addr().unwrap().port(), tx_port);
}
let tx_port = node.sockets.tpu[0].local_addr().unwrap().port();
assert!(tx_port >= FULLNODE_PORT_RANGE.0);
assert!(tx_port < FULLNODE_PORT_RANGE.1);
for tx_socket in node.sockets.tpu.iter() {
assert_eq!(tx_socket.local_addr().unwrap().port(), tx_port);
}
assert!(node.sockets.repair.local_addr().unwrap().port() >= FULLNODE_PORT_RANGE.0);
assert!(node.sockets.repair.local_addr().unwrap().port() < FULLNODE_PORT_RANGE.1);
}
#[test]
fn new_with_external_ip_test_gossip() {
let ip = IpAddr::V4(Ipv4Addr::from(0));
let port = {
bind_in_range(FULLNODE_PORT_RANGE)
.expect("Failed to bind")
.0
};
let node = Node::new_with_external_ip(
&Pubkey::new_rand(),
&socketaddr!(0, port),
FULLNODE_PORT_RANGE,
);
let node = Node::new_with_external_ip(&Keypair::new().pubkey(), &socketaddr!(0, 8050));
assert_eq!(node.sockets.gossip.local_addr().unwrap().ip(), ip);
assert!(node.sockets.tvu.len() > 1);
for tx_socket in node.sockets.tvu.iter() {
assert_eq!(tx_socket.local_addr().unwrap().ip(), ip);
}
assert!(node.sockets.tpu.len() > 1);
for tx_socket in node.sockets.tpu.iter() {
assert_eq!(tx_socket.local_addr().unwrap().ip(), ip);
}
assert_eq!(node.sockets.repair.local_addr().unwrap().ip(), ip);
check_node_sockets(&node, ip, FULLNODE_PORT_RANGE);
assert_eq!(node.sockets.gossip.local_addr().unwrap().port(), port);
}
#[test]
fn new_replicator_external_ip_test() {
let ip = Ipv4Addr::from(0);
let node = Node::new_replicator_with_external_ip(
&Pubkey::new_rand(),
&socketaddr!(ip, 0),
FULLNODE_PORT_RANGE,
);
let ip = IpAddr::V4(ip);
check_socket(&node.sockets.storage.unwrap(), ip, FULLNODE_PORT_RANGE);
check_socket(&node.sockets.gossip, ip, FULLNODE_PORT_RANGE);
check_socket(&node.sockets.repair, ip, FULLNODE_PORT_RANGE);
check_sockets(&node.sockets.tvu, ip, FULLNODE_PORT_RANGE);
assert_eq!(node.sockets.gossip.local_addr().unwrap().port(), 8050);
let tx_port = node.sockets.tvu[0].local_addr().unwrap().port();
assert!(tx_port >= FULLNODE_PORT_RANGE.0);
assert!(tx_port < FULLNODE_PORT_RANGE.1);
for tx_socket in node.sockets.tvu.iter() {
assert_eq!(tx_socket.local_addr().unwrap().port(), tx_port);
}
let tx_port = node.sockets.tpu[0].local_addr().unwrap().port();
assert!(tx_port >= FULLNODE_PORT_RANGE.0);
assert!(tx_port < FULLNODE_PORT_RANGE.1);
for tx_socket in node.sockets.tpu.iter() {
assert_eq!(tx_socket.local_addr().unwrap().port(), tx_port);
}
assert!(node.sockets.repair.local_addr().unwrap().port() >= FULLNODE_PORT_RANGE.0);
assert!(node.sockets.repair.local_addr().unwrap().port() < FULLNODE_PORT_RANGE.1);
}
//test that all cluster_info objects only generate signed messages
@ -2176,7 +1980,7 @@ fn test_add_entrypoint() {
ContactInfo::new_localhost(&node_keypair.pubkey(), timestamp()),
node_keypair,
);
let entrypoint_id = Pubkey::new_rand();
let entrypoint_id = Keypair::new().pubkey();
let entrypoint = ContactInfo::new_localhost(&entrypoint_id, timestamp());
cluster_info.set_entrypoint(entrypoint.clone());
let pulls = cluster_info.new_pull_requests(&HashMap::new());
@ -2204,5 +2008,5 @@ fn test_add_entrypoint() {
.unwrap()
.new_pull_requests(&HashMap::new());
assert_eq!(1, pulls.len());
assert_eq!(cluster_info.read().unwrap().entrypoint, Some(entrypoint));
assert_eq!(cluster_info.read().unwrap().entrypoint, None);
}

View File

@ -1,13 +1,11 @@
use crate::cluster_info::{ClusterInfo, GOSSIP_SLEEP_MILLIS};
use crate::poh_recorder::PohRecorder;
use crate::packet;
use crate::result::Result;
use crate::service::Service;
use crate::sigverify_stage::VerifiedPackets;
use crate::{packet, sigverify};
use crate::streamer::PacketSender;
use solana_metrics::counter::Counter;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex, RwLock};
use std::sync::{Arc, RwLock};
use std::thread::{self, sleep, Builder, JoinHandle};
use std::time::Duration;
@ -19,22 +17,13 @@ impl ClusterInfoVoteListener {
pub fn new(
exit: &Arc<AtomicBool>,
cluster_info: Arc<RwLock<ClusterInfo>>,
sigverify_disabled: bool,
sender: Sender<VerifiedPackets>,
poh_recorder: &Arc<Mutex<PohRecorder>>,
sender: PacketSender,
) -> Self {
let exit = exit.clone();
let poh_recorder = poh_recorder.clone();
let thread = Builder::new()
.name("solana-cluster_info_vote_listener".to_string())
.spawn(move || {
let _ = Self::recv_loop(
exit,
&cluster_info,
sigverify_disabled,
&sender,
poh_recorder,
);
let _ = Self::recv_loop(exit, &cluster_info, &sender);
})
.unwrap();
Self {
@ -44,9 +33,7 @@ impl ClusterInfoVoteListener {
fn recv_loop(
exit: Arc<AtomicBool>,
cluster_info: &Arc<RwLock<ClusterInfo>>,
sigverify_disabled: bool,
sender: &Sender<VerifiedPackets>,
poh_recorder: Arc<Mutex<PohRecorder>>,
sender: &PacketSender,
) -> Result<()> {
let mut last_ts = 0;
loop {
@ -54,16 +41,11 @@ impl ClusterInfoVoteListener {
return Ok(());
}
let (votes, new_ts) = cluster_info.read().unwrap().get_votes(last_ts);
if poh_recorder.lock().unwrap().bank().is_some() {
last_ts = new_ts;
inc_new_counter_info!("cluster_info_vote_listener-recv_count", votes.len());
let msgs = packet::to_packets(&votes);
let r = if sigverify_disabled {
sigverify::ed25519_verify_disabled(&msgs)
} else {
sigverify::ed25519_verify_cpu(&msgs)
};
sender.send(msgs.into_iter().zip(r).collect())?;
last_ts = new_ts;
inc_new_counter_info!("cluster_info_vote_listener-recv_count", votes.len());
let msgs = packet::to_packets(&votes);
for m in msgs {
sender.send(m)?;
}
sleep(Duration::from_millis(GOSSIP_SLEEP_MILLIS));
}

View File

@ -6,18 +6,15 @@ use crate::blocktree::Blocktree;
use crate::cluster_info::FULLNODE_PORT_RANGE;
use crate::contact_info::ContactInfo;
use crate::entry::{Entry, EntrySlice};
use crate::gossip_service::discover_nodes;
use crate::locktower::VOTE_THRESHOLD_DEPTH;
use crate::gossip_service::discover;
use crate::poh_service::PohServiceConfig;
use solana_client::thin_client::create_client;
use solana_sdk::client::SyncClient;
use solana_client::client::create_client;
use solana_sdk::hash::Hash;
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
use solana_sdk::system_transaction;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction::SystemTransaction;
use solana_sdk::timing::{
duration_as_ms, DEFAULT_TICKS_PER_SLOT, NUM_CONSECUTIVE_LEADER_SLOTS, NUM_TICKS_PER_SECOND,
duration_as_ms, DEFAULT_SLOTS_PER_EPOCH, DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND,
};
use std::io;
use std::thread::sleep;
use std::time::Duration;
@ -29,46 +26,45 @@ pub fn spend_and_verify_all_nodes(
funding_keypair: &Keypair,
nodes: usize,
) {
let cluster_nodes = discover_nodes(&entry_point_info.gossip, nodes).unwrap();
let cluster_nodes = discover(&entry_point_info.gossip, nodes).unwrap();
assert!(cluster_nodes.len() >= nodes);
for ingress_node in &cluster_nodes {
let random_keypair = Keypair::new();
let client = create_client(ingress_node.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut client = create_client(ingress_node.client_facing_addr(), FULLNODE_PORT_RANGE);
let bal = client
.poll_get_balance(&funding_keypair.pubkey())
.expect("balance in source");
assert!(bal > 0);
let mut transaction = system_transaction::transfer(
let mut transaction = SystemTransaction::new_move(
&funding_keypair,
&random_keypair.pubkey(),
1,
client.get_recent_blockhash().unwrap(),
client.get_recent_blockhash(),
0,
);
let confs = VOTE_THRESHOLD_DEPTH + 1;
let sig = client
.retry_transfer_until_confirmed(&funding_keypair, &mut transaction, 5, confs)
.retry_transfer(&funding_keypair, &mut transaction, 5)
.unwrap();
for validator in &cluster_nodes {
let client = create_client(validator.client_facing_addr(), FULLNODE_PORT_RANGE);
client.poll_for_signature_confirmation(&sig, confs).unwrap();
let mut client = create_client(validator.client_facing_addr(), FULLNODE_PORT_RANGE);
client.poll_for_signature(&sig).unwrap();
}
}
}
pub fn send_many_transactions(node: &ContactInfo, funding_keypair: &Keypair, num_txs: u64) {
let client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
for _ in 0..num_txs {
let random_keypair = Keypair::new();
let bal = client
.poll_get_balance(&funding_keypair.pubkey())
.expect("balance in source");
assert!(bal > 0);
let mut transaction = system_transaction::transfer(
let mut transaction = SystemTransaction::new_move(
&funding_keypair,
&random_keypair.pubkey(),
1,
client.get_recent_blockhash().unwrap(),
client.get_recent_blockhash(),
0,
);
client
@ -78,15 +74,15 @@ pub fn send_many_transactions(node: &ContactInfo, funding_keypair: &Keypair, num
}
pub fn fullnode_exit(entry_point_info: &ContactInfo, nodes: usize) {
let cluster_nodes = discover_nodes(&entry_point_info.gossip, nodes).unwrap();
let cluster_nodes = discover(&entry_point_info.gossip, nodes).unwrap();
assert!(cluster_nodes.len() >= nodes);
for node in &cluster_nodes {
let client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
assert!(client.fullnode_exit().unwrap());
}
sleep(Duration::from_millis(SLOT_MILLIS));
for node in &cluster_nodes {
let client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
let mut client = create_client(node.client_facing_addr(), FULLNODE_PORT_RANGE);
assert!(client.fullnode_exit().is_err());
}
}
@ -149,96 +145,47 @@ pub fn kill_entry_and_spend_and_verify_rest(
nodes: usize,
) {
solana_logger::setup();
let cluster_nodes = discover_nodes(&entry_point_info.gossip, nodes).unwrap();
let cluster_nodes = discover(&entry_point_info.gossip, nodes).unwrap();
assert!(cluster_nodes.len() >= nodes);
let client = create_client(entry_point_info.client_facing_addr(), FULLNODE_PORT_RANGE);
info!("sleeping for 2 leader fortnights");
sleep(Duration::from_millis(
SLOT_MILLIS * NUM_CONSECUTIVE_LEADER_SLOTS * 2,
));
info!("done sleeping for 2 fortnights");
let mut client = create_client(entry_point_info.client_facing_addr(), FULLNODE_PORT_RANGE);
info!("sleeping for an epoch");
sleep(Duration::from_millis(SLOT_MILLIS * DEFAULT_SLOTS_PER_EPOCH));
info!("done sleeping for an epoch");
info!("killing entry point");
assert!(client.fullnode_exit().unwrap());
info!("sleeping for 2 leader fortnights");
sleep(Duration::from_millis(
SLOT_MILLIS * NUM_CONSECUTIVE_LEADER_SLOTS,
));
info!("done sleeping for 2 fortnights");
info!("sleeping for a slot");
sleep(Duration::from_millis(SLOT_MILLIS));
info!("done sleeping for a slot");
for ingress_node in &cluster_nodes {
if ingress_node.id == entry_point_info.id {
continue;
}
let client = create_client(ingress_node.client_facing_addr(), FULLNODE_PORT_RANGE);
let random_keypair = Keypair::new();
let mut client = create_client(ingress_node.client_facing_addr(), FULLNODE_PORT_RANGE);
let bal = client
.poll_get_balance(&funding_keypair.pubkey())
.expect("balance in source");
assert!(bal > 0);
let mut result = Ok(());
let mut retries = 0;
loop {
retries += 1;
if retries > 5 {
result.unwrap();
}
let random_keypair = Keypair::new();
let mut transaction = system_transaction::transfer(
&funding_keypair,
&random_keypair.pubkey(),
1,
client.get_recent_blockhash().unwrap(),
0,
);
let confs = VOTE_THRESHOLD_DEPTH + 1;
let sig = {
let sig = client.retry_transfer_until_confirmed(
&funding_keypair,
&mut transaction,
5,
confs,
);
match sig {
Err(e) => {
result = Err(e);
continue;
}
Ok(sig) => sig,
}
};
match poll_all_nodes_for_signature(&entry_point_info, &cluster_nodes, &sig, confs) {
Err(e) => {
result = Err(e);
}
Ok(()) => {
break;
}
let mut transaction = SystemTransaction::new_move(
&funding_keypair,
&random_keypair.pubkey(),
1,
client.get_recent_blockhash(),
0,
);
let sig = client
.retry_transfer(&funding_keypair, &mut transaction, 5)
.unwrap();
for validator in &cluster_nodes {
if validator.id == entry_point_info.id {
continue;
}
let mut client = create_client(validator.client_facing_addr(), FULLNODE_PORT_RANGE);
client.poll_for_signature(&sig).unwrap();
}
}
}
fn poll_all_nodes_for_signature(
entry_point_info: &ContactInfo,
cluster_nodes: &[ContactInfo],
sig: &Signature,
confs: usize,
) -> io::Result<()> {
for validator in cluster_nodes {
if validator.id == entry_point_info.id {
continue;
}
let client = create_client(validator.client_facing_addr(), FULLNODE_PORT_RANGE);
client.poll_for_signature_confirmation(&sig, confs)?;
}
Ok(())
}
fn get_and_verify_slot_entries(blocktree: &Blocktree, slot: u64, last_entry: &Hash) -> Vec<Entry> {
let entries = blocktree.get_slot_entries(slot, 0, None).unwrap();
assert!(entries.verify(last_entry));

View File

@ -1,10 +1,7 @@
use crate::rpc_service::RPC_PORT;
use bincode::serialize;
use solana_sdk::pubkey::Pubkey;
#[cfg(test)]
use solana_sdk::rpc_port;
#[cfg(test)]
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::signature::{Signable, Signature};
use solana_sdk::signature::{Keypair, KeypairUtil, Signable, Signature};
use solana_sdk::timing::timestamp;
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
@ -133,7 +130,7 @@ impl ContactInfo {
let addr = socketaddr!("224.0.1.255:1000");
assert!(addr.ip().is_multicast());
Self::new(
&Pubkey::new_rand(),
&Keypair::new().pubkey(),
addr,
addr,
addr,
@ -144,21 +141,18 @@ impl ContactInfo {
0,
)
}
#[cfg(test)]
fn next_port(addr: &SocketAddr, nxt: u16) -> SocketAddr {
let mut nxt_addr = *addr;
nxt_addr.set_port(addr.port() + nxt);
nxt_addr
}
fn new_with_pubkey_socketaddr(pubkey: &Pubkey, bind_addr: &SocketAddr) -> Self {
fn next_port(addr: &SocketAddr, nxt: u16) -> SocketAddr {
let mut nxt_addr = *addr;
nxt_addr.set_port(addr.port() + nxt);
nxt_addr
}
let tpu_addr = *bind_addr;
let gossip_addr = next_port(&bind_addr, 1);
let tvu_addr = next_port(&bind_addr, 2);
let tpu_via_blobs_addr = next_port(&bind_addr, 3);
let rpc_addr = SocketAddr::new(bind_addr.ip(), rpc_port::DEFAULT_RPC_PORT);
let rpc_pubsub_addr = SocketAddr::new(bind_addr.ip(), rpc_port::DEFAULT_RPC_PUBSUB_PORT);
let gossip_addr = Self::next_port(&bind_addr, 1);
let tvu_addr = Self::next_port(&bind_addr, 2);
let tpu_via_blobs_addr = Self::next_port(&bind_addr, 3);
let rpc_addr = SocketAddr::new(bind_addr.ip(), RPC_PORT);
let rpc_pubsub_addr = SocketAddr::new(bind_addr.ip(), RPC_PORT + 1);
Self::new(
pubkey,
gossip_addr,
@ -171,9 +165,7 @@ impl ContactInfo {
timestamp(),
)
}
#[cfg(test)]
pub(crate) fn new_with_socketaddr(bind_addr: &SocketAddr) -> Self {
pub fn new_with_socketaddr(bind_addr: &SocketAddr) -> Self {
let keypair = Keypair::new();
Self::new_with_pubkey_socketaddr(&keypair.pubkey(), bind_addr)
}
@ -193,13 +185,11 @@ impl ContactInfo {
timestamp(),
)
}
fn is_valid_ip(addr: IpAddr) -> bool {
!(addr.is_unspecified() || addr.is_multicast())
// || (addr.is_loopback() && !cfg_test))
// TODO: boot loopback in production networks
}
/// port must not be 0
/// ip must be specified and not mulitcast
/// loopback ip is only allowed in tests
@ -224,7 +214,6 @@ impl Signable for ContactInfo {
gossip: SocketAddr,
tvu: SocketAddr,
tpu: SocketAddr,
tpu_via_blobs: SocketAddr,
storage_addr: SocketAddr,
rpc: SocketAddr,
rpc_pubsub: SocketAddr,
@ -238,7 +227,6 @@ impl Signable for ContactInfo {
tvu: me.tvu,
tpu: me.tpu,
storage_addr: me.storage_addr,
tpu_via_blobs: me.tpu_via_blobs,
rpc: me.rpc,
rpc_pubsub: me.rpc_pubsub,
wallclock: me.wallclock,

View File

@ -165,6 +165,7 @@ impl Crds {
mod test {
use super::*;
use crate::contact_info::ContactInfo;
use solana_sdk::signature::{Keypair, KeypairUtil};
#[test]
fn test_insert() {
@ -301,11 +302,11 @@ mod test {
fn test_label_order() {
let v1 = VersionedCrdsValue::new(
1,
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0)),
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0)),
);
let v2 = VersionedCrdsValue::new(
1,
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0)),
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0)),
);
assert_ne!(v1, v2);
assert!(!(v1 == v2));

View File

@ -209,16 +209,18 @@ impl CrdsGossipPull {
mod test {
use super::*;
use crate::contact_info::ContactInfo;
use solana_sdk::signature::{Keypair, KeypairUtil};
#[test]
fn test_new_pull_with_stakes() {
let mut crds = Crds::default();
let mut stakes = HashMap::new();
let node = CrdsGossipPull::default();
let me = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let me = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
crds.insert(me.clone(), 0).unwrap();
for i in 1..=30 {
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let entry =
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
let id = entry.label().pubkey();
crds.insert(entry.clone(), 0).unwrap();
stakes.insert(id, i * 100);
@ -237,7 +239,7 @@ mod test {
#[test]
fn test_new_pull_request() {
let mut crds = Crds::default();
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
let id = entry.label().pubkey();
let node = CrdsGossipPull::default();
assert_eq!(
@ -251,7 +253,7 @@ mod test {
Err(CrdsGossipError::NoPeers)
);
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
crds.insert(new.clone(), 0).unwrap();
let req = node.new_pull_request(&crds, &id, 0, &HashMap::new());
let (to, _, self_info) = req.unwrap();
@ -262,13 +264,13 @@ mod test {
#[test]
fn test_new_mark_creation_time() {
let mut crds = Crds::default();
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
let node_id = entry.label().pubkey();
let mut node = CrdsGossipPull::default();
crds.insert(entry.clone(), 0).unwrap();
let old = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let old = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
crds.insert(old.clone(), 0).unwrap();
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
crds.insert(new.clone(), 0).unwrap();
// set request creation time to max_value
@ -286,11 +288,11 @@ mod test {
#[test]
fn test_process_pull_request() {
let mut node_crds = Crds::default();
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
let node_id = entry.label().pubkey();
let node = CrdsGossipPull::default();
node_crds.insert(entry.clone(), 0).unwrap();
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
node_crds.insert(new.clone(), 0).unwrap();
let req = node.new_pull_request(&node_crds, &node_id, 0, &HashMap::new());
@ -318,17 +320,17 @@ mod test {
#[test]
fn test_process_pull_request_response() {
let mut node_crds = Crds::default();
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
let node_id = entry.label().pubkey();
let mut node = CrdsGossipPull::default();
node_crds.insert(entry.clone(), 0).unwrap();
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
node_crds.insert(new.clone(), 0).unwrap();
let mut dest = CrdsGossipPull::default();
let mut dest_crds = Crds::default();
let new_id = Pubkey::new_rand();
let new_id = Keypair::new().pubkey();
let new = CrdsValue::ContactInfo(ContactInfo::new_localhost(&new_id, 1));
dest_crds.insert(new.clone(), 0).unwrap();
@ -382,12 +384,12 @@ mod test {
#[test]
fn test_gossip_purge() {
let mut node_crds = Crds::default();
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let entry = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
let node_label = entry.label();
let node_id = node_label.pubkey();
let mut node = CrdsGossipPull::default();
node_crds.insert(entry.clone(), 0).unwrap();
let old = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let old = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
node_crds.insert(old.clone(), 0).unwrap();
let value_hash = node_crds.lookup_versioned(&old.label()).unwrap().value_hash;

View File

@ -266,12 +266,13 @@ impl CrdsGossipPush {
mod test {
use super::*;
use crate::contact_info::ContactInfo;
use solana_sdk::signature::{Keypair, KeypairUtil};
#[test]
fn test_process_push() {
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let value = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let value = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
let label = value.label();
// push a new message
assert_eq!(
@ -290,7 +291,7 @@ mod test {
fn test_process_push_old_version() {
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let mut ci = ContactInfo::new_localhost(&Pubkey::new_rand(), 0);
let mut ci = ContactInfo::new_localhost(&Keypair::new().pubkey(), 0);
ci.wallclock = 1;
let value = CrdsValue::ContactInfo(ci.clone());
@ -310,7 +311,7 @@ mod test {
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let timeout = push.msg_timeout;
let mut ci = ContactInfo::new_localhost(&Pubkey::new_rand(), 0);
let mut ci = ContactInfo::new_localhost(&Keypair::new().pubkey(), 0);
// push a version to far in the future
ci.wallclock = timeout + 1;
@ -332,7 +333,7 @@ mod test {
fn test_process_push_update() {
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let mut ci = ContactInfo::new_localhost(&Pubkey::new_rand(), 0);
let mut ci = ContactInfo::new_localhost(&Keypair::new().pubkey(), 0);
ci.wallclock = 0;
let value_old = CrdsValue::ContactInfo(ci.clone());
@ -365,13 +366,15 @@ mod test {
solana_logger::setup();
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let value1 = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let value1 =
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
assert_eq!(crds.insert(value1.clone(), 0), Ok(None));
push.refresh_push_active_set(&crds, &HashMap::new(), &Pubkey::default(), 1, 1);
assert!(push.active_set.get(&value1.label().pubkey()).is_some());
let value2 = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let value2 =
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
assert!(push.active_set.get(&value2.label().pubkey()).is_none());
assert_eq!(crds.insert(value2.clone(), 0), Ok(None));
for _ in 0..30 {
@ -383,7 +386,8 @@ mod test {
assert!(push.active_set.get(&value2.label().pubkey()).is_some());
for _ in 0..push.num_active {
let value2 = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let value2 =
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
assert_eq!(crds.insert(value2.clone(), 0), Ok(None));
}
push.refresh_push_active_set(&crds, &HashMap::new(), &Pubkey::default(), 1, 1);
@ -397,7 +401,7 @@ mod test {
let mut stakes = HashMap::new();
for i in 1..=100 {
let peer =
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), time));
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), time));
let id = peer.label().pubkey();
crds.insert(peer.clone(), time).unwrap();
stakes.insert(id, i * 100);
@ -415,11 +419,12 @@ mod test {
fn test_new_push_messages() {
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let peer = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let peer = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
assert_eq!(crds.insert(peer.clone(), 0), Ok(None));
push.refresh_push_active_set(&crds, &HashMap::new(), &Pubkey::default(), 1, 1);
let new_msg = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let new_msg =
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
assert_eq!(
push.process_push_message(&mut crds, new_msg.clone(), 0),
Ok(None)
@ -434,11 +439,12 @@ mod test {
fn test_process_prune() {
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let peer = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let peer = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
assert_eq!(crds.insert(peer.clone(), 0), Ok(None));
push.refresh_push_active_set(&crds, &HashMap::new(), &Pubkey::default(), 1, 1);
let new_msg = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let new_msg =
CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
assert_eq!(
push.process_push_message(&mut crds, new_msg.clone(), 0),
Ok(None)
@ -453,11 +459,11 @@ mod test {
fn test_purge_old_pending_push_messages() {
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let peer = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Pubkey::new_rand(), 0));
let peer = CrdsValue::ContactInfo(ContactInfo::new_localhost(&Keypair::new().pubkey(), 0));
assert_eq!(crds.insert(peer.clone(), 0), Ok(None));
push.refresh_push_active_set(&crds, &HashMap::new(), &Pubkey::default(), 1, 1);
let mut ci = ContactInfo::new_localhost(&Pubkey::new_rand(), 0);
let mut ci = ContactInfo::new_localhost(&Keypair::new().pubkey(), 0);
ci.wallclock = 1;
let new_msg = CrdsValue::ContactInfo(ci.clone());
assert_eq!(
@ -475,7 +481,7 @@ mod test {
fn test_purge_old_pushed_once_messages() {
let mut crds = Crds::default();
let mut push = CrdsGossipPush::default();
let mut ci = ContactInfo::new_localhost(&Pubkey::new_rand(), 0);
let mut ci = ContactInfo::new_localhost(&Keypair::new().pubkey(), 0);
ci.wallclock = 0;
let value = CrdsValue::ContactInfo(ci.clone());
let label = value.label();

View File

@ -1,5 +1,4 @@
use crate::contact_info::ContactInfo;
use bincode::serialize;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, Signable, Signature};
use solana_sdk::transaction::Transaction;
@ -16,37 +15,30 @@ pub enum CrdsValue {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Vote {
pub from: Pubkey,
pub transaction: Transaction,
pub signature: Signature,
pub wallclock: u64,
}
impl Signable for Vote {
fn sign(&mut self, _keypair: &Keypair) {}
fn verify(&self) -> bool {
self.transaction.verify_signature()
}
fn pubkey(&self) -> Pubkey {
self.from
self.transaction.account_keys[0]
}
fn signable_data(&self) -> Vec<u8> {
#[derive(Serialize)]
struct SignData {
transaction: Transaction,
wallclock: u64,
}
let data = SignData {
transaction: self.transaction.clone(),
wallclock: self.wallclock,
};
serialize(&data).expect("unable to serialize Vote")
vec![]
}
fn get_signature(&self) -> Signature {
self.signature
Signature::default()
}
fn set_signature(&mut self, signature: Signature) {
self.signature = signature
}
fn set_signature(&mut self, _signature: Signature) {}
}
/// Type of the replicated value
@ -76,11 +68,10 @@ impl CrdsValueLabel {
}
impl Vote {
pub fn new(from: &Pubkey, transaction: Transaction, wallclock: u64) -> Self {
// TODO: it might make sense for the transaction to encode the wallclock in the data
pub fn new(transaction: Transaction, wallclock: u64) -> Self {
Vote {
from: *from,
transaction,
signature: Signature::default(),
wallclock,
}
}
@ -186,31 +177,21 @@ mod test {
let key = v.clone().contact_info().unwrap().id;
assert_eq!(v.label(), CrdsValueLabel::ContactInfo(key));
let v = CrdsValue::Vote(Vote::new(&Pubkey::default(), test_tx(), 0));
let v = CrdsValue::Vote(Vote::new(test_tx(), 0));
assert_eq!(v.wallclock(), 0);
let key = v.clone().vote().unwrap().from;
let key = v.clone().vote().unwrap().transaction.account_keys[0];
assert_eq!(v.label(), CrdsValueLabel::Vote(key));
}
#[test]
fn test_signature() {
let keypair = Keypair::new();
let wrong_keypair = Keypair::new();
let fake_keypair = Keypair::new();
let mut v =
CrdsValue::ContactInfo(ContactInfo::new_localhost(&keypair.pubkey(), timestamp()));
verify_signatures(&mut v, &keypair, &wrong_keypair);
v = CrdsValue::Vote(Vote::new(&keypair.pubkey(), test_tx(), timestamp()));
verify_signatures(&mut v, &keypair, &wrong_keypair);
v.sign(&keypair);
assert!(v.verify());
v.sign(&fake_keypair);
assert!(!v.verify());
}
fn verify_signatures(
value: &mut CrdsValue,
correct_keypair: &Keypair,
wrong_keypair: &Keypair,
) {
assert!(!value.verify());
value.sign(&correct_keypair);
assert!(value.verify());
value.sign(&wrong_keypair);
assert!(!value.verify());
}
}

474
core/src/db_window.rs Normal file
View File

@ -0,0 +1,474 @@
//! Set of functions for emulating windowing functions from a database ledger implementation
use crate::blocktree::*;
#[cfg(feature = "erasure")]
use crate::erasure;
use crate::packet::{SharedBlob, BLOB_HEADER_SIZE};
use crate::result::Result;
use crate::streamer::BlobSender;
use solana_metrics::counter::Counter;
use solana_sdk::pubkey::Pubkey;
use std::borrow::Borrow;
use std::sync::Arc;
pub const MAX_REPAIR_LENGTH: usize = 128;
pub fn retransmit_blobs(dq: &[SharedBlob], retransmit: &BlobSender, id: &Pubkey) -> Result<()> {
let mut retransmit_queue: Vec<SharedBlob> = Vec::new();
for b in dq {
// Don't add blobs generated by this node to the retransmit queue
if b.read().unwrap().id() != *id {
retransmit_queue.push(b.clone());
}
}
if !retransmit_queue.is_empty() {
inc_new_counter_info!("streamer-recv_window-retransmit", retransmit_queue.len());
retransmit.send(retransmit_queue)?;
}
Ok(())
}
/// Process a blob: Add blob to the ledger window.
pub fn process_blob(blocktree: &Arc<Blocktree>, blob: &SharedBlob) -> Result<()> {
let is_coding = blob.read().unwrap().is_coding();
// Check if the blob is in the range of our known leaders. If not, we return.
let (slot, pix) = {
let r_blob = blob.read().unwrap();
(r_blob.slot(), r_blob.index())
};
// TODO: Once the original leader signature is added to the blob, make sure that
// the blob was originally generated by the expected leader for this slot
// Insert the new blob into block tree
if is_coding {
let blob = &blob.read().unwrap();
blocktree.put_coding_blob_bytes(slot, pix, &blob.data[..BLOB_HEADER_SIZE + blob.size()])?;
} else {
blocktree.insert_data_blobs(vec![(*blob.read().unwrap()).borrow()])?;
}
#[cfg(feature = "erasure")]
{
// TODO: Support per-slot erasure. Issue: https://github.com/solana-labs/solana/issues/2441
if let Err(e) = try_erasure(blocktree, 0) {
trace!(
"erasure::recover failed to write recovered coding blobs. Err: {:?}",
e
);
}
}
Ok(())
}
#[cfg(feature = "erasure")]
fn try_erasure(blocktree: &Arc<Blocktree>, slot_index: u64) -> Result<()> {
let meta = blocktree.meta(slot_index)?;
if let Some(meta) = meta {
let (data, coding) = erasure::recover(blocktree, slot_index, meta.consumed)?;
for c in coding {
let c = c.read().unwrap();
blocktree.put_coding_blob_bytes(
0,
c.index(),
&c.data[..BLOB_HEADER_SIZE + c.size()],
)?;
}
blocktree.write_shared_blobs(data)
} else {
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::blocktree::get_tmp_ledger_path;
#[cfg(all(feature = "erasure", test))]
use crate::entry::reconstruct_entries_from_blobs;
use crate::entry::{make_tiny_test_entries, EntrySlice};
#[cfg(all(feature = "erasure", test))]
use crate::erasure::test::{generate_blocktree_from_window, setup_window_ledger};
#[cfg(all(feature = "erasure", test))]
use crate::erasure::{NUM_CODING, NUM_DATA};
use crate::packet::{index_blobs, Blob, Packet, Packets, SharedBlob, PACKET_DATA_SIZE};
use crate::streamer::{receiver, responder, PacketReceiver};
use solana_sdk::signature::{Keypair, KeypairUtil};
use std::io;
use std::io::Write;
use std::net::UdpSocket;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::time::Duration;
fn get_msgs(r: PacketReceiver, num: &mut usize) {
for _t in 0..5 {
let timer = Duration::new(1, 0);
match r.recv_timeout(timer) {
Ok(m) => *num += m.read().unwrap().packets.len(),
e => info!("error {:?}", e),
}
if *num == 10 {
break;
}
}
}
#[test]
pub fn streamer_debug() {
write!(io::sink(), "{:?}", Packet::default()).unwrap();
write!(io::sink(), "{:?}", Packets::default()).unwrap();
write!(io::sink(), "{:?}", Blob::default()).unwrap();
}
#[test]
pub fn streamer_send_test() {
let read = UdpSocket::bind("127.0.0.1:0").expect("bind");
read.set_read_timeout(Some(Duration::new(1, 0))).unwrap();
let addr = read.local_addr().unwrap();
let send = UdpSocket::bind("127.0.0.1:0").expect("bind");
let exit = Arc::new(AtomicBool::new(false));
let (s_reader, r_reader) = channel();
let t_receiver = receiver(Arc::new(read), &exit, s_reader, "window-streamer-test");
let t_responder = {
let (s_responder, r_responder) = channel();
let t_responder = responder("streamer_send_test", Arc::new(send), r_responder);
let mut msgs = Vec::new();
for i in 0..10 {
let b = SharedBlob::default();
{
let mut w = b.write().unwrap();
w.data[0] = i as u8;
w.meta.size = PACKET_DATA_SIZE;
w.meta.set_addr(&addr);
}
msgs.push(b);
}
s_responder.send(msgs).expect("send");
t_responder
};
let mut num = 0;
get_msgs(r_reader, &mut num);
assert_eq!(num, 10);
exit.store(true, Ordering::Relaxed);
t_receiver.join().expect("join");
t_responder.join().expect("join");
}
#[test]
pub fn test_find_missing_data_indexes_sanity() {
let slot = 0;
let blocktree_path = get_tmp_ledger_path!();
let blocktree = Blocktree::open(&blocktree_path).unwrap();
// Early exit conditions
let empty: Vec<u64> = vec![];
assert_eq!(blocktree.find_missing_data_indexes(slot, 0, 0, 1), empty);
assert_eq!(blocktree.find_missing_data_indexes(slot, 5, 5, 1), empty);
assert_eq!(blocktree.find_missing_data_indexes(slot, 4, 3, 1), empty);
assert_eq!(blocktree.find_missing_data_indexes(slot, 1, 2, 0), empty);
let mut blobs = make_tiny_test_entries(2).to_blobs();
const ONE: u64 = 1;
const OTHER: u64 = 4;
blobs[0].set_index(ONE);
blobs[1].set_index(OTHER);
// Insert one blob at index = first_index
blocktree.write_blobs(&blobs).unwrap();
const STARTS: u64 = OTHER * 2;
const END: u64 = OTHER * 3;
const MAX: usize = 10;
// The first blob has index = first_index. Thus, for i < first_index,
// given the input range of [i, first_index], the missing indexes should be
// [i, first_index - 1]
for start in 0..STARTS {
let result = blocktree.find_missing_data_indexes(
slot, start, // start
END, //end
MAX, //max
);
let expected: Vec<u64> = (start..END).filter(|i| *i != ONE && *i != OTHER).collect();
assert_eq!(result, expected);
}
drop(blocktree);
Blocktree::destroy(&blocktree_path).expect("Expected successful database destruction");
}
#[test]
pub fn test_find_missing_data_indexes() {
let slot = 0;
let blocktree_path = get_tmp_ledger_path!();
let blocktree = Blocktree::open(&blocktree_path).unwrap();
// Write entries
let gap = 10;
assert!(gap > 3);
let num_entries = 10;
let mut blobs = make_tiny_test_entries(num_entries).to_blobs();
for (i, b) in blobs.iter_mut().enumerate() {
b.set_index(i as u64 * gap);
b.set_slot(slot);
}
blocktree.write_blobs(&blobs).unwrap();
// Index of the first blob is 0
// Index of the second blob is "gap"
// Thus, the missing indexes should then be [1, gap - 1] for the input index
// range of [0, gap)
let expected: Vec<u64> = (1..gap).collect();
assert_eq!(
blocktree.find_missing_data_indexes(slot, 0, gap, gap as usize),
expected
);
assert_eq!(
blocktree.find_missing_data_indexes(slot, 1, gap, (gap - 1) as usize),
expected,
);
assert_eq!(
blocktree.find_missing_data_indexes(slot, 0, gap - 1, (gap - 1) as usize),
&expected[..expected.len() - 1],
);
assert_eq!(
blocktree.find_missing_data_indexes(slot, gap - 2, gap, gap as usize),
vec![gap - 2, gap - 1],
);
assert_eq!(
blocktree.find_missing_data_indexes(slot, gap - 2, gap, 1),
vec![gap - 2],
);
assert_eq!(
blocktree.find_missing_data_indexes(slot, 0, gap, 1),
vec![1],
);
// Test with end indexes that are greater than the last item in the ledger
let mut expected: Vec<u64> = (1..gap).collect();
expected.push(gap + 1);
assert_eq!(
blocktree.find_missing_data_indexes(slot, 0, gap + 2, (gap + 2) as usize),
expected,
);
assert_eq!(
blocktree.find_missing_data_indexes(slot, 0, gap + 2, (gap - 1) as usize),
&expected[..expected.len() - 1],
);
for i in 0..num_entries as u64 {
for j in 0..i {
let expected: Vec<u64> = (j..i)
.flat_map(|k| {
let begin = k * gap + 1;
let end = (k + 1) * gap;
(begin..end)
})
.collect();
assert_eq!(
blocktree.find_missing_data_indexes(
slot,
j * gap,
i * gap,
((i - j) * gap) as usize
),
expected,
);
}
}
drop(blocktree);
Blocktree::destroy(&blocktree_path).expect("Expected successful database destruction");
}
#[test]
pub fn test_find_missing_data_indexes_slots() {
let blocktree_path = get_tmp_ledger_path!();
let blocktree = Blocktree::open(&blocktree_path).unwrap();
let num_entries_per_slot = 10;
let num_slots = 2;
let mut blobs = make_tiny_test_entries(num_slots * num_entries_per_slot).to_blobs();
// Insert every nth entry for each slot
let nth = 3;
for (i, b) in blobs.iter_mut().enumerate() {
b.set_index(((i % num_entries_per_slot) * nth) as u64);
b.set_slot((i / num_entries_per_slot) as u64);
}
blocktree.write_blobs(&blobs).unwrap();
let mut expected: Vec<u64> = (0..num_entries_per_slot)
.flat_map(|x| ((nth * x + 1) as u64..(nth * x + nth) as u64))
.collect();
// For each slot, find all missing indexes in the range [0, num_entries_per_slot * nth]
for slot in 0..num_slots {
assert_eq!(
blocktree.find_missing_data_indexes(
slot as u64,
0,
(num_entries_per_slot * nth) as u64,
num_entries_per_slot * nth as usize
),
expected,
);
}
// Test with a limit on the number of returned entries
for slot in 0..num_slots {
assert_eq!(
blocktree.find_missing_data_indexes(
slot as u64,
0,
(num_entries_per_slot * nth) as u64,
num_entries_per_slot * (nth - 1)
)[..],
expected[..num_entries_per_slot * (nth - 1)],
);
}
// Try to find entries in the range [num_entries_per_slot * nth..num_entries_per_slot * (nth + 1)
// that don't exist in the ledger.
let extra_entries =
(num_entries_per_slot * nth) as u64..(num_entries_per_slot * (nth + 1)) as u64;
expected.extend(extra_entries);
// For each slot, find all missing indexes in the range [0, num_entries_per_slot * nth]
for slot in 0..num_slots {
assert_eq!(
blocktree.find_missing_data_indexes(
slot as u64,
0,
(num_entries_per_slot * (nth + 1)) as u64,
num_entries_per_slot * (nth + 1),
),
expected,
);
}
}
#[test]
pub fn test_no_missing_blob_indexes() {
let slot = 0;
let blocktree_path = get_tmp_ledger_path!();
let blocktree = Blocktree::open(&blocktree_path).unwrap();
// Write entries
let num_entries = 10;
let shared_blobs = make_tiny_test_entries(num_entries).to_shared_blobs();
index_blobs(&shared_blobs, &Keypair::new().pubkey(), 0, slot, 0);
let blob_locks: Vec<_> = shared_blobs.iter().map(|b| b.read().unwrap()).collect();
let blobs: Vec<&Blob> = blob_locks.iter().map(|b| &**b).collect();
blocktree.write_blobs(blobs).unwrap();
let empty: Vec<u64> = vec![];
for i in 0..num_entries as u64 {
for j in 0..i {
assert_eq!(
blocktree.find_missing_data_indexes(slot, j, i, (i - j) as usize),
empty
);
}
}
drop(blocktree);
Blocktree::destroy(&blocktree_path).expect("Expected successful database destruction");
}
#[cfg(all(feature = "erasure", test))]
#[test]
pub fn test_try_erasure() {
// Setup the window
let offset = 0;
let num_blobs = NUM_DATA + 2;
let slot = 0;
let mut window = setup_window_ledger(offset, num_blobs, false, slot);
let end_index = (offset + num_blobs) % window.len();
// Test erasing a data block and an erasure block
let coding_start = offset - (offset % NUM_DATA) + (NUM_DATA - NUM_CODING);
let erased_index = coding_start % window.len();
// Create a hole in the window
let erased_data = window[erased_index].data.clone();
let erased_coding = window[erased_index].coding.clone().unwrap();
window[erased_index].data = None;
window[erased_index].coding = None;
// Generate the blocktree from the window
let ledger_path = get_tmp_ledger_path!();
let blocktree = Arc::new(generate_blocktree_from_window(&ledger_path, &window, false));
try_erasure(&blocktree, 0).expect("Expected successful erasure attempt");
window[erased_index].data = erased_data;
{
let data_blobs: Vec<_> = window[erased_index..end_index]
.iter()
.map(|entry| entry.data.clone().unwrap())
.collect();
let locks: Vec<_> = data_blobs.iter().map(|blob| blob.read().unwrap()).collect();
let locked_data: Vec<&Blob> = locks.iter().map(|lock| &**lock).collect();
let (expected, _) = reconstruct_entries_from_blobs(locked_data).unwrap();
assert_eq!(
blocktree
.get_slot_entries(
0,
erased_index as u64,
Some((end_index - erased_index) as u64)
)
.unwrap(),
expected
);
}
let erased_coding_l = erased_coding.read().unwrap();
assert_eq!(
&blocktree
.get_coding_blob_bytes(slot, erased_index as u64)
.unwrap()
.unwrap()[BLOB_HEADER_SIZE..],
&erased_coding_l.data()[..erased_coding_l.size() as usize],
);
}
#[test]
fn test_process_blob() {
let blocktree_path = get_tmp_ledger_path!();
let blocktree = Arc::new(Blocktree::open(&blocktree_path).unwrap());
let num_entries = 10;
let original_entries = make_tiny_test_entries(num_entries);
let shared_blobs = original_entries.clone().to_shared_blobs();
index_blobs(&shared_blobs, &Keypair::new().pubkey(), 0, 0, 0);
for blob in shared_blobs.iter().rev() {
process_blob(&blocktree, blob).expect("Expect successful processing of blob");
}
assert_eq!(
blocktree.get_slot_entries(0, 0, None).unwrap(),
original_entries
);
drop(blocktree);
Blocktree::destroy(&blocktree_path).expect("Expected successful database destruction");
}
}

View File

@ -5,14 +5,18 @@
use crate::packet::{Blob, SharedBlob, BLOB_DATA_SIZE};
use crate::poh::Poh;
use crate::result::Result;
use bincode::{deserialize, serialized_size};
use bincode::{deserialize, serialize_into, serialized_size};
use chrono::prelude::Utc;
use rayon::prelude::*;
use solana_budget_api::budget_instruction;
use solana_sdk::hash::{Hash, Hasher};
use solana_budget_api::budget_transaction::BudgetTransaction;
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::transaction::Transaction;
use solana_vote_api::vote_instruction::Vote;
use solana_vote_api::vote_transaction::VoteTransaction;
use std::borrow::Borrow;
use std::io::Cursor;
use std::mem::size_of;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, RwLock};
@ -98,19 +102,62 @@ impl Entry {
}
pub fn to_blob(&self) -> Blob {
Blob::from_serializable(&vec![&self])
let mut blob = Blob::default();
let pos = {
let mut out = Cursor::new(blob.data_mut());
serialize_into(&mut out, &self).expect("failed to serialize output");
out.position() as usize
};
blob.set_size(pos);
blob
}
/// Estimate serialized_size of Entry without creating an Entry.
pub fn serialized_size(transactions: &[Transaction]) -> u64 {
let txs_size: u64 = transactions
.iter()
.map(|tx| serialized_size(tx).unwrap())
.map(|tx| tx.serialized_size().unwrap())
.sum();
// num_hashes + hash + txs
(2 * size_of::<u64>() + size_of::<Hash>()) as u64 + txs_size
}
pub fn num_will_fit(transactions: &[Transaction]) -> usize {
if transactions.is_empty() {
return 0;
}
let mut num = transactions.len();
let mut upper = transactions.len();
let mut lower = 1; // if one won't fit, we have a lot of TODOs
let mut next = transactions.len(); // optimistic
loop {
debug!(
"num {}, upper {} lower {} next {} transactions.len() {}",
num,
upper,
lower,
next,
transactions.len()
);
if Self::serialized_size(&transactions[..num]) <= BLOB_DATA_SIZE as u64 {
next = (upper + num) / 2;
lower = num;
debug!("num {} fits, maybe too well? trying {}", num, next);
} else {
next = (lower + num) / 2;
upper = num;
debug!("num {} doesn't fit! trying {}", num, next);
}
// same as last time
if next == num {
debug!("converged on num {}", num);
break;
}
num = next;
}
num
}
/// Creates the next Tick Entry `num_hashes` after `start_hash`.
pub fn new_mut(
start_hash: &mut Hash,
@ -155,17 +202,6 @@ impl Entry {
}
}
pub fn hash_transactions(transactions: &[Transaction]) -> Hash {
// a hash of a slice of transactions only needs to hash the signatures
let mut hasher = Hasher::default();
transactions.iter().for_each(|tx| {
if !tx.signatures.is_empty() {
hasher.hash(&tx.signatures[0].as_ref());
}
});
hasher.result()
}
/// Creates the hash `num_hashes` after `start_hash`. If the transaction contains
/// a signature, the final hash will be a hash of both the previous ID and
/// the signature. If num_hashes is zero and there's no transaction data,
@ -184,7 +220,7 @@ fn next_hash(start_hash: &Hash, num_hashes: u64, transactions: &[Transaction]) -
if transactions.is_empty() {
poh.tick().hash
} else {
poh.record(hash_transactions(transactions)).hash
poh.record(Transaction::hash(transactions)).hash
}
}
@ -197,14 +233,15 @@ where
let mut num_ticks = 0;
for blob in blobs.into_iter() {
let new_entries: Vec<Entry> = {
let entry: Entry = {
let msg_size = blob.borrow().size();
deserialize(&blob.borrow().data()[..msg_size])?
};
let num_new_ticks: u64 = new_entries.iter().map(|entry| entry.is_tick() as u64).sum();
num_ticks += num_new_ticks;
entries.extend(new_entries)
if entry.is_tick() {
num_ticks += 1
}
entries.push(entry)
}
Ok((entries, num_ticks))
}
@ -215,8 +252,7 @@ pub trait EntrySlice {
fn verify(&self, start_hash: &Hash) -> bool;
fn to_shared_blobs(&self) -> Vec<SharedBlob>;
fn to_blobs(&self) -> Vec<Blob>;
fn to_single_entry_blobs(&self) -> Vec<Blob>;
fn to_single_entry_shared_blobs(&self) -> Vec<SharedBlob>;
fn votes(&self) -> Vec<(Pubkey, Vote, Hash)>;
}
impl EntrySlice for [Entry] {
@ -242,31 +278,23 @@ impl EntrySlice for [Entry] {
}
fn to_blobs(&self) -> Vec<Blob> {
split_serializable_chunks(
&self,
BLOB_DATA_SIZE as u64,
&|s| bincode::serialized_size(&s).unwrap(),
&mut |entries: &[Entry]| Blob::from_serializable(entries),
)
self.iter().map(|entry| entry.to_blob()).collect()
}
fn to_shared_blobs(&self) -> Vec<SharedBlob> {
self.to_blobs()
.into_iter()
.map(|b| Arc::new(RwLock::new(b)))
.collect()
self.iter().map(|entry| entry.to_shared_blob()).collect()
}
fn to_single_entry_shared_blobs(&self) -> Vec<SharedBlob> {
self.to_single_entry_blobs()
.into_iter()
.map(|b| Arc::new(RwLock::new(b)))
fn votes(&self) -> Vec<(Pubkey, Vote, Hash)> {
self.iter()
.flat_map(|entry| {
entry
.transactions
.iter()
.flat_map(VoteTransaction::get_votes)
})
.collect()
}
fn to_single_entry_blobs(&self) -> Vec<Blob> {
self.iter().map(Entry::to_blob).collect()
}
}
pub fn next_entry_mut(start: &mut Hash, num_hashes: u64, transactions: Vec<Transaction>) -> Entry {
@ -275,67 +303,6 @@ pub fn next_entry_mut(start: &mut Hash, num_hashes: u64, transactions: Vec<Trans
entry
}
pub fn num_will_fit<T, F>(serializables: &[T], max_size: u64, serialized_size: &F) -> usize
where
F: Fn(&[T]) -> u64,
{
if serializables.is_empty() {
return 0;
}
let mut num = serializables.len();
let mut upper = serializables.len();
let mut lower = 1; // if one won't fit, we have a lot of TODOs
let mut next = serializables.len(); // optimistic
loop {
debug!(
"num {}, upper {} lower {} next {} serializables.len() {}",
num,
upper,
lower,
next,
serializables.len()
);
if serialized_size(&serializables[..num]) <= max_size {
next = (upper + num) / 2;
lower = num;
debug!("num {} fits, maybe too well? trying {}", num, next);
} else {
next = (lower + num) / 2;
upper = num;
debug!("num {} doesn't fit! trying {}", num, next);
}
// same as last time
if next == num {
debug!("converged on num {}", num);
break;
}
num = next;
}
num
}
pub fn split_serializable_chunks<T, R, F1, F2>(
serializables: &[T],
max_size: u64,
serialized_size: &F1,
converter: &mut F2,
) -> Vec<R>
where
F1: Fn(&[T]) -> u64,
F2: FnMut(&[T]) -> R,
{
let mut result = vec![];
let mut chunk_start = 0;
while chunk_start < serializables.len() {
let chunk_end =
chunk_start + num_will_fit(&serializables[chunk_start..], max_size, serialized_size);
result.push(converter(&serializables[chunk_start..chunk_end]));
chunk_start = chunk_end;
}
result
}
/// Creates the next entries for given transactions, outputs
/// updates start_hash to hash of last Entry, sets num_hashes to 0
pub fn next_entries_mut(
@ -343,12 +310,61 @@ pub fn next_entries_mut(
num_hashes: &mut u64,
transactions: Vec<Transaction>,
) -> Vec<Entry> {
split_serializable_chunks(
&transactions[..],
BLOB_DATA_SIZE as u64,
&Entry::serialized_size,
&mut |txs: &[Transaction]| Entry::new_mut(start_hash, num_hashes, txs.to_vec()),
)
// TODO: ?? find a number that works better than |?
// V
if transactions.is_empty() || transactions.len() == 1 {
vec![Entry::new_mut(start_hash, num_hashes, transactions)]
} else {
let mut chunk_start = 0;
let mut entries = Vec::new();
while chunk_start < transactions.len() {
let mut chunk_end = transactions.len();
let mut upper = chunk_end;
let mut lower = chunk_start;
let mut next = chunk_end; // be optimistic that all will fit
// binary search for how many transactions will fit in an Entry (i.e. a BLOB)
loop {
debug!(
"chunk_end {}, upper {} lower {} next {} transactions.len() {}",
chunk_end,
upper,
lower,
next,
transactions.len()
);
if Entry::serialized_size(&transactions[chunk_start..chunk_end])
<= BLOB_DATA_SIZE as u64
{
next = (upper + chunk_end) / 2;
lower = chunk_end;
debug!(
"chunk_end {} fits, maybe too well? trying {}",
chunk_end, next
);
} else {
next = (lower + chunk_end) / 2;
upper = chunk_end;
debug!("chunk_end {} doesn't fit! trying {}", chunk_end, next);
}
// same as last time
if next == chunk_end {
debug!("converged on chunk_end {}", chunk_end);
break;
}
chunk_end = next;
}
entries.push(Entry::new_mut(
start_hash,
num_hashes,
transactions[chunk_start..chunk_end].to_vec(),
));
chunk_start = chunk_end;
}
entries
}
}
/// Creates the next Entries for given transactions
@ -374,15 +390,22 @@ pub fn create_ticks(num_ticks: u64, mut hash: Hash) -> Vec<Entry> {
pub fn make_tiny_test_entries_from_hash(start: &Hash, num: usize) -> Vec<Entry> {
let keypair = Keypair::new();
let pubkey = keypair.pubkey();
let mut hash = *start;
let mut num_hashes = 0;
(0..num)
.map(|_| {
let ix = budget_instruction::apply_timestamp(&pubkey, &pubkey, &pubkey, Utc::now());
let tx = Transaction::new_signed_instructions(&[&keypair], vec![ix], *start);
Entry::new_mut(&mut hash, &mut num_hashes, vec![tx])
Entry::new_mut(
&mut hash,
&mut num_hashes,
vec![BudgetTransaction::new_timestamp(
&keypair,
&keypair.pubkey(),
&keypair.pubkey(),
Utc::now(),
*start,
)],
)
})
.collect()
}
@ -397,12 +420,16 @@ pub fn make_large_test_entries(num_entries: usize) -> Vec<Entry> {
let zero = Hash::default();
let one = solana_sdk::hash::hash(&zero.as_ref());
let keypair = Keypair::new();
let pubkey = keypair.pubkey();
let ix = budget_instruction::apply_timestamp(&pubkey, &pubkey, &pubkey, Utc::now());
let tx = Transaction::new_signed_instructions(&[&keypair], vec![ix], one);
let tx = BudgetTransaction::new_timestamp(
&keypair,
&keypair.pubkey(),
&keypair.pubkey(),
Utc::now(),
one,
);
let serialized_size = serialized_size(&tx).unwrap();
let serialized_size = tx.serialized_size().unwrap();
let num_txs = BLOB_DATA_SIZE / serialized_size as usize;
let txs = vec![tx; num_txs];
let entry = next_entries(&one, 1, txs)[0].clone();
@ -411,7 +438,7 @@ pub fn make_large_test_entries(num_entries: usize) -> Vec<Entry> {
#[cfg(test)]
pub fn make_consecutive_blobs(
id: &solana_sdk::pubkey::Pubkey,
id: &Pubkey,
num_blobs_to_make: u64,
start_height: u64,
start_hash: Hash,
@ -419,7 +446,7 @@ pub fn make_consecutive_blobs(
) -> Vec<SharedBlob> {
let entries = create_ticks(num_blobs_to_make, start_hash);
let blobs = entries.to_single_entry_shared_blobs();
let blobs = entries.to_shared_blobs();
let mut index = start_height;
for blob in &blobs {
let mut blob = blob.write().unwrap();
@ -450,35 +477,9 @@ mod tests {
use crate::packet::{to_blobs, BLOB_DATA_SIZE, PACKET_DATA_SIZE};
use solana_sdk::hash::hash;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction;
use solana_vote_api::vote_instruction;
use solana_vote_api::vote_state::Vote;
use solana_sdk::system_transaction::SystemTransaction;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
fn create_sample_payment(keypair: &Keypair, hash: Hash) -> Transaction {
let pubkey = keypair.pubkey();
let ixs = budget_instruction::payment(&pubkey, &pubkey, 1);
Transaction::new_signed_instructions(&[keypair], ixs, hash)
}
fn create_sample_timestamp(keypair: &Keypair, hash: Hash) -> Transaction {
let pubkey = keypair.pubkey();
let ix = budget_instruction::apply_timestamp(&pubkey, &pubkey, &pubkey, Utc::now());
Transaction::new_signed_instructions(&[keypair], vec![ix], hash)
}
fn create_sample_signature(keypair: &Keypair, hash: Hash) -> Transaction {
let pubkey = keypair.pubkey();
let ix = budget_instruction::apply_signature(&pubkey, &pubkey, &pubkey);
Transaction::new_signed_instructions(&[keypair], vec![ix], hash)
}
fn create_sample_vote(keypair: &Keypair, hash: Hash) -> Transaction {
let pubkey = keypair.pubkey();
let ix = vote_instruction::vote(&pubkey, vec![Vote::new(1)]);
Transaction::new_signed_instructions(&[keypair], vec![ix], hash)
}
#[test]
fn test_entry_verify() {
let zero = Hash::default();
@ -495,8 +496,8 @@ mod tests {
// First, verify entries
let keypair = Keypair::new();
let tx0 = system_transaction::create_user_account(&keypair, &keypair.pubkey(), 0, zero, 0);
let tx1 = system_transaction::create_user_account(&keypair, &keypair.pubkey(), 1, zero, 0);
let tx0 = SystemTransaction::new_account(&keypair, &keypair.pubkey(), 0, zero, 0);
let tx1 = SystemTransaction::new_account(&keypair, &keypair.pubkey(), 1, zero, 0);
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()]);
assert!(e0.verify(&zero));
@ -512,8 +513,15 @@ mod tests {
// First, verify entries
let keypair = Keypair::new();
let tx0 = create_sample_timestamp(&keypair, zero);
let tx1 = create_sample_signature(&keypair, zero);
let tx0 = BudgetTransaction::new_timestamp(
&keypair,
&keypair.pubkey(),
&keypair.pubkey(),
Utc::now(),
zero,
);
let tx1 =
BudgetTransaction::new_signature(&keypair, &keypair.pubkey(), &keypair.pubkey(), zero);
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()]);
assert!(e0.verify(&zero));
@ -535,7 +543,13 @@ mod tests {
assert_eq!(tick.hash, zero);
let keypair = Keypair::new();
let tx0 = create_sample_timestamp(&keypair, zero);
let tx0 = BudgetTransaction::new_timestamp(
&keypair,
&keypair.pubkey(),
&keypair.pubkey(),
Utc::now(),
zero,
);
let entry0 = next_entry(&zero, 1, vec![tx0.clone()]);
assert_eq!(entry0.num_hashes, 1);
assert_eq!(entry0.hash, next_hash(&zero, 1, &vec![tx0]));
@ -546,7 +560,7 @@ mod tests {
fn test_next_entry_panic() {
let zero = Hash::default();
let keypair = Keypair::new();
let tx = system_transaction::create_user_account(&keypair, &keypair.pubkey(), 0, zero, 0);
let tx = SystemTransaction::new_account(&keypair, &keypair.pubkey(), 0, zero, 0);
next_entry(&zero, 0, vec![tx]);
}
@ -554,7 +568,7 @@ mod tests {
fn test_serialized_size() {
let zero = Hash::default();
let keypair = Keypair::new();
let tx = system_transaction::create_user_account(&keypair, &keypair.pubkey(), 0, zero, 0);
let tx = SystemTransaction::new_account(&keypair, &keypair.pubkey(), 0, zero, 0);
let entry = next_entry(&zero, 1, vec![tx.clone()]);
assert_eq!(
Entry::serialized_size(&[tx]),
@ -582,8 +596,14 @@ mod tests {
let one = hash(&zero.as_ref());
let keypair = Keypair::new();
let vote_account = Keypair::new();
let tx0 = create_sample_vote(&vote_account, one);
let tx1 = create_sample_timestamp(&keypair, one);
let tx0 = VoteTransaction::new_vote(&vote_account.pubkey(), &vote_account, 1, one, 1);
let tx1 = BudgetTransaction::new_timestamp(
&keypair,
&keypair.pubkey(),
&keypair.pubkey(),
Utc::now(),
one,
);
//
// TODO: this magic number and the mix of transaction types
// is designed to fill up a Blob more or less exactly,
@ -600,7 +620,7 @@ mod tests {
}
#[test]
fn test_entries_to_blobs() {
fn test_entries_to_shared_blobs() {
solana_logger::setup();
let entries = make_test_entries();
@ -609,23 +629,6 @@ mod tests {
assert_eq!(reconstruct_entries_from_blobs(blob_q).unwrap().0, entries);
}
#[test]
fn test_multiple_entries_to_blobs() {
solana_logger::setup();
let num_blobs = 10;
let serialized_size =
bincode::serialized_size(&make_tiny_test_entries_from_hash(&Hash::default(), 1))
.unwrap();
let num_entries = (num_blobs * BLOB_DATA_SIZE as u64) / serialized_size;
let entries = make_tiny_test_entries_from_hash(&Hash::default(), num_entries as usize);
let blob_q = entries.to_blobs();
assert_eq!(blob_q.len() as u64, num_blobs);
assert_eq!(reconstruct_entries_from_blobs(blob_q).unwrap().0, entries);
}
#[test]
fn test_bad_blobs_attack() {
solana_logger::setup();
@ -641,11 +644,12 @@ mod tests {
let next_hash = solana_sdk::hash::hash(&hash.as_ref());
let keypair = Keypair::new();
let vote_account = Keypair::new();
let tx_small = create_sample_vote(&vote_account, next_hash);
let tx_large = create_sample_payment(&keypair, next_hash);
let tx_small =
VoteTransaction::new_vote(&vote_account.pubkey(), &vote_account, 1, next_hash, 2);
let tx_large = BudgetTransaction::new_payment(&keypair, &keypair.pubkey(), 1, next_hash, 0);
let tx_small_size = serialized_size(&tx_small).unwrap() as usize;
let tx_large_size = serialized_size(&tx_large).unwrap() as usize;
let tx_small_size = tx_small.serialized_size().unwrap() as usize;
let tx_large_size = tx_large.serialized_size().unwrap() as usize;
let entry_size = serialized_size(&Entry {
num_hashes: 0,
hash: Hash::default(),
@ -681,40 +685,4 @@ mod tests {
assert!(entries0.verify(&hash));
}
#[test]
fn test_num_will_fit_empty() {
let serializables: Vec<u32> = vec![];
let result = num_will_fit(&serializables[..], 8, &|_| 4);
assert_eq!(result, 0);
}
#[test]
fn test_num_fit() {
let serializables_vec: Vec<u8> = (0..10).map(|_| 1).collect();
let serializables = &serializables_vec[..];
let sum = |i: &[u8]| (0..i.len()).into_iter().sum::<usize>() as u64;
// sum[0] is = 0, but sum[0..1] > 0, so result contains 1 item
let result = num_will_fit(serializables, 0, &sum);
assert_eq!(result, 1);
// sum[0..3] is <= 8, but sum[0..4] > 8, so result contains 3 items
let result = num_will_fit(serializables, 8, &sum);
assert_eq!(result, 4);
// sum[0..1] is = 1, but sum[0..2] > 0, so result contains 2 items
let result = num_will_fit(serializables, 1, &sum);
assert_eq!(result, 2);
// sum[0..9] = 45, so contains all items
let result = num_will_fit(serializables, 45, &sum);
assert_eq!(result, 10);
// sum[0..8] <= 44, but sum[0..9] = 45, so contains all but last item
let result = num_will_fit(serializables, 44, &sum);
assert_eq!(result, 9);
// sum[0..9] <= 46, but contains all items
let result = num_will_fit(serializables, 46, &sum);
assert_eq!(result, 10);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -25,18 +25,19 @@ use solana_sdk::genesis_block::GenesisBlock;
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction;
use solana_sdk::system_transaction::SystemTransaction;
use solana_sdk::timing::timestamp;
use solana_sdk::transaction::Transaction;
use solana_vote_api::vote_instruction;
use solana_vote_api::vote_state::Vote;
use solana_vote_api::vote_transaction::VoteTransaction;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, Receiver};
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex, RwLock};
use std::thread::Result;
use std::thread::sleep;
use std::thread::JoinHandle;
use std::thread::{spawn, Result};
use std::time::Duration;
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct FullnodeConfig {
pub sigverify_disabled: bool,
pub voting_disabled: bool,
@ -69,6 +70,7 @@ pub struct Fullnode {
exit: Arc<AtomicBool>,
rpc_service: Option<JsonRpcService>,
rpc_pubsub_service: Option<PubSubService>,
rpc_working_bank_handle: JoinHandle<()>,
gossip_service: GossipService,
poh_recorder: Arc<Mutex<PohRecorder>>,
poh_service: PohService,
@ -100,26 +102,24 @@ impl Fullnode {
let exit = Arc::new(AtomicBool::new(false));
let bank_info = &bank_forks_info[0];
let bank = bank_forks[bank_info.bank_slot].clone();
let genesis_blockhash = bank.last_blockhash();
info!(
"starting PoH... {} {}",
bank.tick_height(),
bank.last_blockhash(),
);
let blocktree = Arc::new(blocktree);
let (poh_recorder, entry_receiver) = PohRecorder::new_with_clear_signal(
let (poh_recorder, entry_receiver) = PohRecorder::new(
bank.tick_height(),
bank.last_blockhash(),
bank.slot(),
leader_schedule_utils::next_leader_slot(&id, bank.slot(), &bank, Some(&blocktree)),
leader_schedule_utils::next_leader_slot(&id, bank.slot(), &bank),
bank.ticks_per_slot(),
&id,
&blocktree,
blocktree.new_blobs_signals.first().cloned(),
);
let poh_recorder = Arc::new(Mutex::new(poh_recorder));
let poh_service = PohService::new(poh_recorder.clone(), &config.tick_config, &exit);
poh_recorder.lock().unwrap().clear_bank_signal =
blocktree.new_blobs_signals.first().cloned();
assert_eq!(
blocktree.new_blobs_signals.len(),
1,
@ -133,6 +133,7 @@ impl Fullnode {
node.sockets.gossip.local_addr().unwrap()
);
let blocktree = Arc::new(blocktree);
let bank_forks = Arc::new(RwLock::new(bank_forks));
node.info.wallclock = timestamp();
@ -143,32 +144,23 @@ impl Fullnode {
let storage_state = StorageState::new();
let rpc_service = if node.info.rpc.port() == 0 {
None
} else {
Some(JsonRpcService::new(
&cluster_info,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), node.info.rpc.port()),
storage_state.clone(),
config.rpc_config.clone(),
bank_forks.clone(),
&exit,
))
};
let rpc_service = JsonRpcService::new(
&cluster_info,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), node.info.rpc.port()),
storage_state.clone(),
config.rpc_config.clone(),
&exit,
);
let subscriptions = Arc::new(RpcSubscriptions::default());
let rpc_pubsub_service = if node.info.rpc_pubsub.port() == 0 {
None
} else {
Some(PubSubService::new(
&subscriptions,
SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
node.info.rpc_pubsub.port(),
),
&exit,
))
};
let rpc_pubsub_service = PubSubService::new(
&subscriptions,
SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
node.info.rpc_pubsub.port(),
),
&exit,
);
let gossip_service = GossipService::new(
&cluster_info,
@ -213,9 +205,7 @@ impl Fullnode {
Some(Arc::new(voting_keypair))
};
// Setup channel for sending entries to storage stage
let (sender, receiver) = channel();
// Setup channel for rotation indications
let tvu = Tvu::new(
vote_account,
voting_keypair,
@ -230,10 +220,7 @@ impl Fullnode {
ledger_signal_receiver,
&subscriptions,
&poh_recorder,
sender.clone(),
receiver,
&exit,
&genesis_blockhash,
);
let tpu = Tpu::new(
&id,
@ -245,17 +232,29 @@ impl Fullnode {
node.sockets.broadcast,
config.sigverify_disabled,
&blocktree,
sender,
&exit,
&genesis_blockhash,
);
let exit_ = exit.clone();
let bank_forks_ = bank_forks.clone();
let rpc_service_rp = rpc_service.request_processor.clone();
let rpc_working_bank_handle = spawn(move || loop {
if exit_.load(Ordering::Relaxed) {
break;
}
let bank = bank_forks_.read().unwrap().working_bank();
trace!("rpc working bank {} {}", bank.slot(), bank.last_blockhash());
rpc_service_rp.write().unwrap().set_bank(&bank);
let timer = Duration::from_millis(100);
sleep(timer);
});
inc_new_counter_info!("fullnode-new", 1);
Self {
id,
gossip_service,
rpc_service,
rpc_pubsub_service,
rpc_service: Some(rpc_service),
rpc_pubsub_service: Some(rpc_pubsub_service),
rpc_working_bank_handle,
tpu,
tvu,
exit,
@ -282,8 +281,9 @@ pub fn new_banks_from_blocktree(
let genesis_block =
GenesisBlock::load(blocktree_path).expect("Expected to successfully open genesis block");
let (blocktree, ledger_signal_receiver) = Blocktree::open_with_signal(blocktree_path)
.expect("Expected to successfully open database ledger");
let (blocktree, ledger_signal_receiver) =
Blocktree::open_with_config_signal(blocktree_path, genesis_block.ticks_per_slot)
.expect("Expected to successfully open database ledger");
let (bank_forks, bank_forks_info) =
blocktree_processor::process_blocktree(&genesis_block, &blocktree, account_paths)
@ -310,6 +310,7 @@ impl Service for Fullnode {
rpc_pubsub_service.join()?;
}
self.rpc_working_bank_handle.join()?;
self.gossip_service.join()?;
self.tpu.join()?;
self.tvu.join()?;
@ -329,7 +330,7 @@ pub fn make_active_set_entries(
num_ending_ticks: u64,
) -> (Vec<Entry>, Keypair) {
// 1) Assume the active_keypair node has no lamports staked
let transfer_tx = system_transaction::create_user_account(
let transfer_tx = SystemTransaction::new_account(
&lamport_source,
&active_keypair.pubkey(),
stake,
@ -343,25 +344,23 @@ pub fn make_active_set_entries(
let voting_keypair = Keypair::new();
let vote_account_id = voting_keypair.pubkey();
let new_vote_account_ixs = vote_instruction::create_account(
&active_keypair.pubkey(),
let new_vote_account_tx = VoteTransaction::new_account(
active_keypair,
&vote_account_id,
&active_keypair.pubkey(),
0,
stake.saturating_sub(2),
);
let new_vote_account_tx = Transaction::new_signed_instructions(
&[active_keypair.as_ref()],
new_vote_account_ixs,
*blockhash,
stake.saturating_sub(2),
1,
);
let new_vote_account_entry = next_entry_mut(&mut last_entry_hash, 1, vec![new_vote_account_tx]);
// 3) Create vote entry
let vote_ix =
vote_instruction::vote(&voting_keypair.pubkey(), vec![Vote::new(slot_to_vote_on)]);
let vote_tx =
Transaction::new_signed_instructions(&[&voting_keypair], vec![vote_ix], *blockhash);
let vote_tx = VoteTransaction::new_vote(
&voting_keypair.pubkey(),
&voting_keypair,
slot_to_vote_on,
*blockhash,
0,
);
let vote_entry = next_entry_mut(&mut last_entry_hash, 1, vec![vote_tx]);
// 4) Create `num_ending_ticks` empty ticks
@ -380,12 +379,7 @@ pub fn new_fullnode_for_tests() -> (Fullnode, ContactInfo, Keypair, String) {
let node = Node::new_localhost_with_pubkey(&node_keypair.pubkey());
let contact_info = node.info.clone();
let (mut genesis_block, mint_keypair) =
GenesisBlock::new_with_leader(10_000, &contact_info.id, 42);
genesis_block
.native_instruction_processors
.push(("solana_budget_program".to_string(), solana_budget_api::id()));
let (genesis_block, mint_keypair) = GenesisBlock::new_with_leader(10_000, &contact_info.id, 42);
let (ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_block);
let voting_keypair = Keypair::new();

View File

@ -6,7 +6,6 @@ use crate::cluster_info::ClusterInfo;
use crate::contact_info::ContactInfo;
use crate::service::Service;
use crate::streamer;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use std::net::SocketAddr;
use std::net::UdpSocket;
@ -52,96 +51,31 @@ impl GossipService {
}
}
pub fn discover_nodes(
gossip_addr: &SocketAddr,
num_nodes: usize,
) -> std::io::Result<Vec<ContactInfo>> {
discover(gossip_addr, Some(num_nodes), Some(30), None)
}
pub fn discover(
gossip_addr: &SocketAddr,
num_nodes: Option<usize>,
timeout: Option<u64>,
find_node: Option<Pubkey>,
) -> std::io::Result<Vec<ContactInfo>> {
pub fn discover(gossip_addr: &SocketAddr, num_nodes: usize) -> std::io::Result<Vec<ContactInfo>> {
let exit = Arc::new(AtomicBool::new(false));
let (gossip_service, spy_ref) = make_spy_node(gossip_addr, &exit);
let id = spy_ref.read().unwrap().keypair.pubkey();
info!("Gossip entry point: {:?}", gossip_addr);
trace!(
"discover: spy_node {} looking for at least {:?} nodes",
"discover: spy_node {} looking for at least {} nodes",
id,
num_nodes
);
let (met_criteria, secs, tvu_peers) = spy(spy_ref.clone(), num_nodes, timeout, find_node);
exit.store(true, Ordering::Relaxed);
gossip_service.join().unwrap();
if met_criteria {
info!(
"discover success in {}s...\n{}",
secs,
spy_ref.read().unwrap().contact_info_trace()
);
return Ok(tvu_peers);
}
if !tvu_peers.is_empty() {
info!(
"discover failed to match criteria by timeout...\n{}",
spy_ref.read().unwrap().contact_info_trace()
);
return Ok(tvu_peers);
}
info!(
"discover failed...\n{}",
spy_ref.read().unwrap().contact_info_trace()
);
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to converge",
))
}
fn spy(
spy_ref: Arc<RwLock<ClusterInfo>>,
num_nodes: Option<usize>,
timeout: Option<u64>,
find_node: Option<Pubkey>,
) -> (bool, u64, Vec<ContactInfo>) {
// Wait for the cluster to converge
let now = Instant::now();
let mut met_criteria = false;
let mut tvu_peers: Vec<ContactInfo> = Vec::new();
let mut i = 0;
loop {
if let Some(secs) = timeout {
if now.elapsed() >= Duration::from_secs(secs) {
break;
}
}
tvu_peers = spy_ref.read().unwrap().tvu_peers();
if let Some(num) = num_nodes {
if tvu_peers.len() >= num {
if let Some(pubkey) = find_node {
if tvu_peers.iter().any(|x| x.id == pubkey) {
met_criteria = true;
break;
}
} else {
met_criteria = true;
break;
}
}
}
if let Some(pubkey) = find_node {
if num_nodes.is_none() && tvu_peers.iter().any(|x| x.id == pubkey) {
met_criteria = true;
break;
}
while now.elapsed() < Duration::from_secs(30) {
let rpc_peers = spy_ref.read().unwrap().rpc_peers();
if rpc_peers.len() >= num_nodes {
info!(
"discover success in {}s...\n{}",
now.elapsed().as_secs(),
spy_ref.read().unwrap().contact_info_trace()
);
exit.store(true, Ordering::Relaxed);
gossip_service.join().unwrap();
return Ok(rpc_peers);
}
if i % 20 == 0 {
info!(
@ -154,7 +88,17 @@ fn spy(
));
i += 1;
}
(met_criteria, now.elapsed().as_secs(), tvu_peers)
exit.store(true, Ordering::Relaxed);
gossip_service.join().unwrap();
info!(
"discover failed...\n{}",
spy_ref.read().unwrap().contact_info_trace()
);
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to converge",
))
}
fn make_spy_node(
@ -201,44 +145,4 @@ mod tests {
exit.store(true, Ordering::Relaxed);
d.join().unwrap();
}
#[test]
fn test_gossip_services_spy() {
let keypair = Keypair::new();
let peer0 = Pubkey::new_rand();
let peer1 = Pubkey::new_rand();
let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), 0);
let peer0_info = ContactInfo::new_localhost(&peer0, 0);
let peer1_info = ContactInfo::new_localhost(&peer1, 0);
let mut cluster_info = ClusterInfo::new(contact_info.clone(), Arc::new(keypair));
cluster_info.insert_info(peer0_info);
cluster_info.insert_info(peer1_info);
let spy_ref = Arc::new(RwLock::new(cluster_info));
let (met_criteria, secs, tvu_peers) = spy(spy_ref.clone(), None, Some(1), None);
assert_eq!(met_criteria, false);
assert_eq!(secs, 1);
assert_eq!(tvu_peers, spy_ref.read().unwrap().tvu_peers());
// Find num_nodes
let (met_criteria, _, _) = spy(spy_ref.clone(), Some(1), None, None);
assert_eq!(met_criteria, true);
let (met_criteria, _, _) = spy(spy_ref.clone(), Some(2), None, None);
assert_eq!(met_criteria, true);
// Find specific node by pubkey
let (met_criteria, _, _) = spy(spy_ref.clone(), None, None, Some(peer0));
assert_eq!(met_criteria, true);
let (met_criteria, _, _) = spy(spy_ref.clone(), None, Some(0), Some(Pubkey::new_rand()));
assert_eq!(met_criteria, false);
// Find num_nodes *and* specific node by pubkey
let (met_criteria, _, _) = spy(spy_ref.clone(), Some(1), None, Some(peer0));
assert_eq!(met_criteria, true);
let (met_criteria, _, _) = spy(spy_ref.clone(), Some(3), Some(0), Some(peer0));
assert_eq!(met_criteria, false);
let (met_criteria, _, _) = spy(spy_ref.clone(), Some(1), Some(0), Some(Pubkey::new_rand()));
assert_eq!(met_criteria, false);
}
}

345
core/src/kvstore.rs Normal file
View File

@ -0,0 +1,345 @@
use crate::kvstore::mapper::{Disk, Mapper, Memory};
use crate::kvstore::sstable::SSTable;
use crate::kvstore::storage::WriteState;
use crate::kvstore::writelog::WriteLog;
use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::ops::RangeInclusive;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, RwLock};
use std::thread::JoinHandle;
mod compactor;
mod error;
mod io_utils;
mod mapper;
mod readtx;
mod sstable;
mod storage;
mod writelog;
mod writetx;
pub use self::error::{Error, Result};
pub use self::readtx::ReadTx as Snapshot;
pub use self::sstable::Key;
pub use self::writetx::WriteTx;
const TABLES_FILE: &str = "tables.meta";
const LOG_FILE: &str = "mem-log";
const DEFAULT_TABLE_SIZE: usize = 64 * 1024 * 1024;
const DEFAULT_MEM_SIZE: usize = 64 * 1024 * 1024;
const DEFAULT_MAX_PAGES: usize = 10;
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct Config {
pub max_mem: usize,
pub max_tables: usize,
pub page_size: usize,
pub in_memory: bool,
}
#[derive(Debug)]
pub struct KvStore {
write: RwLock<WriteState>,
tables: RwLock<Vec<BTreeMap<Key, SSTable>>>,
config: Config,
root: PathBuf,
mapper: Arc<dyn Mapper>,
req_tx: RwLock<Sender<compactor::Req>>,
resp_rx: RwLock<Receiver<compactor::Resp>>,
compactor_handle: JoinHandle<()>,
}
impl KvStore {
pub fn open_default<P>(root: P) -> Result<Self>
where
P: AsRef<Path>,
{
let mapper = Disk::single(root.as_ref());
open(root.as_ref(), Arc::new(mapper), Config::default())
}
pub fn open<P>(root: P, config: Config) -> Result<Self>
where
P: AsRef<Path>,
{
let mapper: Arc<dyn Mapper> = if config.in_memory {
Arc::new(Memory::new())
} else {
Arc::new(Disk::single(root.as_ref()))
};
open(root.as_ref(), mapper, config)
}
pub fn partitioned<P, P2>(root: P, storage_dirs: &[P2], config: Config) -> Result<Self>
where
P: AsRef<Path>,
P2: AsRef<Path>,
{
let mapper = Disk::new(storage_dirs);
open(root.as_ref(), Arc::new(mapper), config)
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn put(&self, key: &Key, data: &[u8]) -> Result<()> {
self.ensure_mem()?;
let mut write = self.write.write().unwrap();
write.put(key, data)?;
write.commit += 1;
Ok(())
}
pub fn put_many<Iter, Tup, K, V>(&self, rows: Iter) -> Result<()>
where
Iter: Iterator<Item = Tup>,
Tup: std::borrow::Borrow<(K, V)>,
K: std::borrow::Borrow<Key>,
V: std::borrow::Borrow<[u8]>,
{
{
let mut write = self.write.write().unwrap();
for pair in rows {
let tup = pair.borrow();
let (key, data) = (tup.0.borrow(), tup.1.borrow());
write.put(key, data)?;
}
write.commit += 1;
}
self.ensure_mem()?;
Ok(())
}
pub fn get(&self, key: &Key) -> Result<Option<Vec<u8>>> {
self.query_compactor()?;
let (write_state, tables) = (self.write.read().unwrap(), self.tables.read().unwrap());
storage::get(&write_state.values, &*tables, key)
}
pub fn delete(&self, key: &Key) -> Result<()> {
self.query_compactor()?;
{
let mut write = self.write.write().unwrap();
write.delete(key)?;
write.commit += 1;
}
self.ensure_mem()?;
Ok(())
}
pub fn delete_many<Iter, K>(&self, rows: Iter) -> Result<()>
where
Iter: Iterator<Item = K>,
K: std::borrow::Borrow<Key>,
{
self.query_compactor()?;
{
let mut write = self.write.write().unwrap();
for k in rows {
let key = k.borrow();
write.delete(key)?;
}
write.commit += 1;
}
self.ensure_mem()?;
Ok(())
}
pub fn transaction(&self) -> Result<WriteTx> {
unimplemented!()
}
pub fn commit(&self, _txn: WriteTx) -> Result<()> {
unimplemented!()
}
pub fn snapshot(&self) -> Snapshot {
let (state, tables) = (self.write.read().unwrap(), self.tables.read().unwrap());
Snapshot::new(state.values.clone(), tables.clone())
}
pub fn range(
&self,
range: RangeInclusive<Key>,
) -> Result<impl Iterator<Item = (Key, Vec<u8>)>> {
self.query_compactor()?;
let (write_state, tables) = (self.write.read().unwrap(), self.tables.read().unwrap());
storage::range(&write_state.values, &*tables, range)
}
pub fn destroy<P>(path: P) -> Result<()>
where
P: AsRef<Path>,
{
let path = path.as_ref();
if !path.exists() {
return Ok(());
}
fs::remove_dir_all(path)?;
Ok(())
}
fn query_compactor(&self) -> Result<()> {
if let (Ok(mut req_tx), Ok(mut resp_rx), Ok(mut tables)) = (
self.req_tx.try_write(),
self.resp_rx.try_write(),
self.tables.try_write(),
) {
query_compactor(
&self.root,
&*self.mapper,
&mut *tables,
&mut *resp_rx,
&mut *req_tx,
)?;
}
Ok(())
}
fn ensure_mem(&self) -> Result<()> {
let trigger_compact = {
let mut write_rw = self.write.write().unwrap();
if write_rw.mem_size < self.config.max_mem {
return Ok(());
}
let mut tables = self.tables.write().unwrap();
storage::flush_table(&write_rw.values, &*self.mapper, &mut *tables)?;
write_rw.reset()?;
write_rw.commit += 1;
is_lvl0_full(&tables, &self.config)
};
dump_tables(&self.root, &*self.mapper).unwrap();
if trigger_compact {
let tables_path = self.root.join(TABLES_FILE);
self.req_tx
.write()
.unwrap()
.send(compactor::Req::Start(tables_path))
.expect("compactor thread dead");
}
Ok(())
}
}
impl Default for Config {
fn default() -> Config {
Config {
max_mem: DEFAULT_MEM_SIZE,
max_tables: DEFAULT_MAX_PAGES,
page_size: DEFAULT_TABLE_SIZE,
in_memory: false,
}
}
}
fn open(root: &Path, mapper: Arc<dyn Mapper>, config: Config) -> Result<KvStore> {
let root = root.to_path_buf();
let log_path = root.join(LOG_FILE);
if !root.exists() {
fs::create_dir(&root)?;
}
let write_log = WriteLog::open(&log_path, config.max_mem)?;
let mem = write_log.materialize()?;
let write = RwLock::new(WriteState::new(write_log, mem));
let tables = load_tables(&root, &*mapper)?;
let tables = RwLock::new(tables);
let cfg = compactor::Config {
max_pages: config.max_tables,
page_size: config.page_size,
};
let (req_tx, resp_rx, compactor_handle) = compactor::spawn_compactor(Arc::clone(&mapper), cfg)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let (req_tx, resp_rx) = (RwLock::new(req_tx), RwLock::new(resp_rx));
Ok(KvStore {
write,
tables,
config,
mapper,
root,
req_tx,
resp_rx,
compactor_handle,
})
}
fn load_tables(root: &Path, mapper: &dyn Mapper) -> Result<Vec<BTreeMap<Key, SSTable>>> {
let mut tables = Vec::new();
let meta_path = root.join(TABLES_FILE);
if meta_path.exists() {
mapper.load_state_from(&meta_path)?;
tables = SSTable::sorted_tables(&mapper.active_set()?);
}
Ok(tables)
}
fn dump_tables(root: &Path, mapper: &Mapper) -> Result<()> {
mapper.serialize_state_to(&root.join(TABLES_FILE))?;
Ok(())
}
fn query_compactor(
root: &Path,
mapper: &dyn Mapper,
tables: &mut Vec<BTreeMap<Key, SSTable>>,
resp_rx: &mut Receiver<compactor::Resp>,
req_tx: &mut Sender<compactor::Req>,
) -> Result<()> {
match resp_rx.try_recv() {
Ok(compactor::Resp::Done(new_tables)) => {
std::mem::replace(tables, new_tables);
dump_tables(root, mapper)?;
req_tx.send(compactor::Req::Gc).unwrap();
}
Ok(compactor::Resp::Failed(e)) => {
return Err(e);
}
// Nothing available, do nothing
_ => {}
}
Ok(())
}
#[inline]
fn is_lvl0_full(tables: &[BTreeMap<Key, SSTable>], config: &Config) -> bool {
if tables.is_empty() {
false
} else {
tables[0].len() > config.max_tables
}
}

View File

@ -1,6 +1,6 @@
use crate::error::{Error, Result};
use crate::mapper::{Kind, Mapper};
use crate::sstable::{Key, Merged, SSTable};
use crate::kvstore::error::{Error, Result};
use crate::kvstore::mapper::{Kind, Mapper};
use crate::kvstore::sstable::{Key, Merged, SSTable};
use std::collections::BTreeMap;
use std::path::PathBuf;

View File

@ -12,7 +12,6 @@ pub enum Error {
Corrupted(bincode::Error),
Channel(Box<dyn StdErr + Sync + Send>),
Missing,
WriteBatchFull(usize),
}
impl fmt::Display for Error {
@ -22,7 +21,6 @@ impl fmt::Display for Error {
Error::Channel(e) => write!(f, "Internal communication error: {}", e),
Error::Io(e) => write!(f, "I/O error: {}", e),
Error::Missing => write!(f, "Item not present in ledger"),
Error::WriteBatchFull(capacity) => write!(f, "WriteBatch capacity {} full", capacity),
}
}
}
@ -34,7 +32,6 @@ impl StdErr for Error {
Error::Corrupted(ref e) => Some(e),
Error::Channel(e) => Some(e.as_ref()),
Error::Missing => None,
Error::WriteBatchFull(_) => None,
}
}
}

View File

@ -0,0 +1,131 @@
use memmap::Mmap;
use std::fs::File;
use std::io::{self, BufWriter, Seek, SeekFrom, Write};
use std::ops::Deref;
use std::sync::{Arc, RwLock};
const BACKING_ERR: &str = "In-memory table lock poisoned; concurrency error";
#[derive(Debug)]
pub enum MemMap {
Disk(Mmap),
Mem(Arc<RwLock<Vec<u8>>>),
}
#[derive(Debug)]
pub enum Writer {
Disk(BufWriter<File>),
Mem(SharedWriter),
}
#[derive(Debug)]
pub struct SharedWriter {
buf: Arc<RwLock<Vec<u8>>>,
pos: u64,
}
impl SharedWriter {
pub fn new(buf: Arc<RwLock<Vec<u8>>>) -> SharedWriter {
SharedWriter { buf, pos: 0 }
}
}
impl Deref for MemMap {
type Target = [u8];
fn deref(&self) -> &[u8] {
match self {
MemMap::Disk(mmap) => mmap.deref(),
MemMap::Mem(vec) => {
let buf = vec.read().expect(BACKING_ERR);
let slice = buf.as_slice();
// transmute lifetime. Relying on the RwLock + immutability for safety
unsafe { std::mem::transmute(slice) }
}
}
}
}
impl Write for SharedWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
use std::cmp;
let mut vec = self.buf.write().expect(BACKING_ERR);
// Calc ranges
let space_remaining = vec.len() - self.pos as usize;
let copy_len = cmp::min(buf.len(), space_remaining);
let copy_src_range = 0..copy_len;
let append_src_range = copy_len..buf.len();
let copy_dest_range = self.pos as usize..(self.pos as usize + copy_len);
// Copy then append
(&mut vec[copy_dest_range]).copy_from_slice(&buf[copy_src_range]);
vec.extend_from_slice(&buf[append_src_range]);
let written = buf.len();
self.pos += written as u64;
Ok(written)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
let _written = self.write(buf)?;
Ok(())
}
}
impl Seek for SharedWriter {
fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
self.pos = match to {
SeekFrom::Start(new_pos) => new_pos,
SeekFrom::Current(diff) => (self.pos as i64 + diff) as u64,
SeekFrom::End(rpos) => (self.buf.read().expect(BACKING_ERR).len() as i64 + rpos) as u64,
};
Ok(self.pos)
}
}
impl Write for Writer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Writer::Disk(ref mut wtr) => wtr.write(buf),
Writer::Mem(ref mut wtr) => wtr.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Writer::Disk(ref mut wtr) => {
wtr.flush()?;
wtr.get_mut().sync_data()?;
Ok(())
}
Writer::Mem(ref mut wtr) => wtr.flush(),
}
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
match self {
Writer::Disk(ref mut wtr) => wtr.write_all(buf),
Writer::Mem(ref mut wtr) => wtr.write_all(buf),
}
}
}
impl Seek for Writer {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match self {
Writer::Disk(ref mut wtr) => wtr.seek(pos),
Writer::Mem(ref mut wtr) => wtr.seek(pos),
}
}
}

View File

@ -1,6 +1,6 @@
use crate::io_utils::Writer;
use crate::sstable::SSTable;
use crate::Result;
use crate::kvstore::io_utils::Writer;
use crate::kvstore::sstable::SSTable;
use crate::kvstore::Result;
use std::path::Path;
use std::sync::RwLock;

View File

@ -1,7 +1,7 @@
use crate::io_utils::{MemMap, Writer};
use crate::mapper::{Kind, Mapper, RwLockExt};
use crate::sstable::SSTable;
use crate::Result;
use crate::kvstore::io_utils::{MemMap, Writer};
use crate::kvstore::mapper::{Kind, Mapper, RwLockExt};
use crate::kvstore::sstable::SSTable;
use crate::kvstore::Result;
use memmap::Mmap;
@ -50,7 +50,7 @@ impl Disk {
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PathInfo {
pub data: PathBuf,
pub index: PathBuf,
@ -213,124 +213,3 @@ fn next_id(kind: Kind) -> Id {
kind,
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::mapper::Kind;
use crate::sstable::{Key, Value};
use crate::test::gen;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::thread;
use tempfile::tempdir;
const DATA_SIZE: usize = 128;
#[test]
fn test_table_management() {
let tempdir = tempdir().unwrap();
let mapper = Arc::new(Disk::single(tempdir.path()));
let records: BTreeMap<_, _> = gen_records().take(1024).collect();
let mut threads = vec![];
let mut number_of_tables = 4;
for kind in [Kind::Active, Kind::Garbage, Kind::Compaction].iter() {
let records = records.clone();
let mapper = Arc::clone(&mapper);
let child = thread::spawn(move || {
for _ in 0..number_of_tables {
mapper
.make_table(*kind, &mut |mut data_writer, mut index_writer| {
SSTable::create(
&mut records.iter(),
0,
&mut data_writer,
&mut index_writer,
);
})
.unwrap();
}
});
number_of_tables *= 2;
threads.push(child);
}
threads.into_iter().for_each(|child| child.join().unwrap());
let count_kind = |kind, mapper: &Disk| {
mapper
.mappings
.read()
.unwrap()
.keys()
.filter(|id| id.kind == kind)
.count()
};
assert_eq!(count_kind(Kind::Active, &mapper), 4);
assert_eq!(count_kind(Kind::Garbage, &mapper), 8);
assert_eq!(count_kind(Kind::Compaction, &mapper), 16);
mapper.empty_trash().unwrap();
assert_eq!(count_kind(Kind::Garbage, &mapper), 0);
mapper.rotate_tables().unwrap();
assert_eq!(count_kind(Kind::Active, &mapper), 16);
assert_eq!(count_kind(Kind::Garbage, &mapper), 4);
assert_eq!(count_kind(Kind::Compaction, &mapper), 0);
let active_set = mapper.active_set().unwrap();
assert_eq!(active_set.len(), 16);
}
#[test]
fn test_state() {
let tempdir = tempdir().unwrap();
let dirs_1: Vec<_> = (0..4).map(|i| tempdir.path().join(i.to_string())).collect();
let dirs_2: Vec<_> = (4..8).map(|i| tempdir.path().join(i.to_string())).collect();
let mapper_1 = Arc::new(Disk::new(&dirs_1));
let records: BTreeMap<_, _> = gen_records().take(1024).collect();
for (i, &kind) in [Kind::Active, Kind::Compaction, Kind::Garbage]
.iter()
.enumerate()
{
for _ in 0..(i * 3) {
mapper_1
.make_table(kind, &mut |mut data_writer, mut index_writer| {
SSTable::create(
&mut records.iter(),
0,
&mut data_writer,
&mut index_writer,
);
})
.unwrap();
}
}
let state_path = tempdir.path().join("state");
mapper_1.serialize_state_to(&state_path).unwrap();
assert!(state_path.exists());
let mapper_2 = Arc::new(Disk::new(&dirs_2));
mapper_2.load_state_from(&state_path).unwrap();
assert_eq!(
&*mapper_1.mappings.read().unwrap(),
&*mapper_2.mappings.read().unwrap()
);
assert_eq!(
&*mapper_1.storage_dirs.read().unwrap(),
&*mapper_2.storage_dirs.read().unwrap()
);
}
fn gen_records() -> impl Iterator<Item = (Key, Value)> {
gen::pairs(DATA_SIZE).map(|(key, data)| (key, Value::new(0, Some(data))))
}
}

View File

@ -1,7 +1,7 @@
use crate::io_utils::{MemMap, SharedWriter, Writer};
use crate::mapper::{Kind, Mapper, RwLockExt};
use crate::sstable::SSTable;
use crate::Result;
use crate::kvstore::io_utils::{MemMap, SharedWriter, Writer};
use crate::kvstore::mapper::{Kind, Mapper, RwLockExt};
use crate::kvstore::sstable::SSTable;
use crate::kvstore::Result;
use rand::{rngs::SmallRng, FromEntropy, Rng};
@ -62,16 +62,18 @@ impl Mapper for Memory {
}
fn rotate_tables(&self) -> Result<()> {
let (mut active, mut compaction, mut garbage) = (
use std::mem::swap;
let (mut active, mut compact, mut garbage) = (
self.tables.write().expect(BACKING_ERR_MSG),
self.compaction.write().expect(BACKING_ERR_MSG),
self.garbage.write().expect(BACKING_ERR_MSG),
);
// compacted tables => active set
swap(&mut active, &mut compact);
// old active set => garbage
garbage.extend(active.drain());
// compacted tables => new active set
active.extend(compaction.drain());
garbage.extend(compact.drain());
Ok(())
}
@ -140,87 +142,3 @@ fn get_table(id: Id, map: &TableMap) -> Result<SSTable> {
fn next_id() -> Id {
rand::thread_rng().gen()
}
#[cfg(test)]
mod test {
use super::*;
use crate::mapper::Kind;
use crate::sstable::{Key, Value};
use crate::test::gen;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::thread;
const DATA_SIZE: usize = 128;
#[test]
fn test_table_management() {
let mapper = Arc::new(Memory::new());
let records: BTreeMap<_, _> = gen_records().take(1024).collect();
let mut threads = vec![];
let mut number_of_tables = 4;
for kind in [Kind::Active, Kind::Garbage, Kind::Compaction].iter() {
let records = records.clone();
let mapper = Arc::clone(&mapper);
let child = thread::spawn(move || {
for _ in 0..number_of_tables {
mapper
.make_table(*kind, &mut |mut data_writer, mut index_writer| {
SSTable::create(
&mut records.iter(),
0,
&mut data_writer,
&mut index_writer,
);
})
.unwrap();
}
});
number_of_tables *= 2;
threads.push(child);
}
threads.into_iter().for_each(|child| child.join().unwrap());
assert_eq!(mapper.tables.read().unwrap().len(), 4);
assert_eq!(mapper.garbage.read().unwrap().len(), 8);
assert_eq!(mapper.compaction.read().unwrap().len(), 16);
mapper.empty_trash().unwrap();
assert_eq!(mapper.garbage.read().unwrap().len(), 0);
mapper.rotate_tables().unwrap();
assert_eq!(mapper.tables.read().unwrap().len(), 16);
assert_eq!(mapper.garbage.read().unwrap().len(), 4);
assert!(mapper.compaction.read().unwrap().is_empty());
let active_set = mapper.active_set().unwrap();
assert_eq!(active_set.len(), 16);
}
#[test]
fn test_no_state() {
let tempdir = tempfile::tempdir().unwrap();
let mapper = Arc::new(Memory::new());
let records: BTreeMap<_, _> = gen_records().take(1024).collect();
mapper
.make_table(Kind::Active, &mut |mut data_writer, mut index_writer| {
SSTable::create(&mut records.iter(), 0, &mut data_writer, &mut index_writer);
})
.unwrap();
let state_path = tempdir.path().join("state");
mapper.serialize_state_to(&state_path).unwrap();
mapper.load_state_from(&state_path).unwrap();
assert!(!state_path.exists());
}
fn gen_records() -> impl Iterator<Item = (Key, Value)> {
gen::pairs(DATA_SIZE).map(|(key, data)| (key, Value::new(0, Some(data))))
}
}

View File

@ -1,6 +1,6 @@
use crate::error::Result;
use crate::sstable::{Key, SSTable, Value};
use crate::storage;
use crate::kvstore::error::Result;
use crate::kvstore::sstable::{Key, SSTable, Value};
use crate::kvstore::storage;
use std::collections::BTreeMap;
use std::ops::RangeInclusive;

476
core/src/kvstore/sstable.rs Normal file
View File

@ -0,0 +1,476 @@
use crate::kvstore::error::Result;
use crate::kvstore::io_utils::{MemMap, Writer};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use std::borrow::Borrow;
use std::collections::{BTreeMap, HashMap};
use std::io::{prelude::*, Cursor, Seek, SeekFrom};
use std::ops::RangeInclusive;
use std::sync::Arc;
use std::u64;
// ___________________________________________
// | start_key | end_key | level | data_size |
// -------------------------------------------
const IDX_META_SIZE: usize = KEY_LEN + KEY_LEN + 1 + 8;
const KEY_LEN: usize = 3 * 8;
// _________________
// | offset | size |
// -----------------
const PTR_SIZE: usize = 2 * 8;
// __________________________________________
// | key | timestamp | pointer OR tombstone |
// ------------------------------------------
const INDEX_ENTRY_SIZE: usize = KEY_LEN + 8 + PTR_SIZE;
// Represented by zero offset and size
const TOMBSTONE: [u8; PTR_SIZE] = [0u8; PTR_SIZE];
#[derive(Clone, Debug)]
pub struct SSTable {
data: Arc<MemMap>,
index: Arc<MemMap>,
meta: IndexMeta,
}
#[derive(Debug, PartialEq, Clone)]
pub struct IndexMeta {
pub level: u8,
pub data_size: u64,
pub start: Key,
pub end: Key,
}
#[derive(Debug, Default, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Hash)]
pub struct Key(pub [u8; 24]);
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Copy, Clone)]
pub struct Index {
pub offset: u64,
pub size: u64,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Value {
pub ts: i64,
pub val: Option<Vec<u8>>,
}
/// An iterator that produces logical view over a set of SSTables
pub struct Merged<I> {
sources: Vec<I>,
heads: BTreeMap<(Key, usize), Value>,
seen: HashMap<Key, i64>,
}
impl SSTable {
pub fn meta(&self) -> &IndexMeta {
&self.meta
}
#[allow(dead_code)]
pub fn num_keys(&self) -> u64 {
((self.index.len() - IDX_META_SIZE) / INDEX_ENTRY_SIZE) as u64
}
pub fn get(&self, key: &Key) -> Result<Option<Value>> {
let range = *key..=*key;
let found_opt = self.range(&range)?.find(|(k, _)| k == key).map(|(_, v)| v);
Ok(found_opt)
}
pub fn range(&self, range: &RangeInclusive<Key>) -> Result<impl Iterator<Item = (Key, Value)>> {
Ok(Scan::new(
range.clone(),
Arc::clone(&self.data),
Arc::clone(&self.index),
))
}
pub fn create_capped<I, K, V>(
rows: &mut I,
level: u8,
max_table_size: u64,
data_wtr: &mut Writer,
index_wtr: &mut Writer,
) where
I: Iterator<Item = (K, V)>,
K: Borrow<Key>,
V: Borrow<Value>,
{
const DATA_ERR: &str = "Error writing table data";
const INDEX_ERR: &str = "Error writing index data";
let (data_size, index) =
flush_mem_table_capped(rows, data_wtr, max_table_size).expect(DATA_ERR);
data_wtr.flush().expect(DATA_ERR);
let (&start, &end) = (
index.keys().next().unwrap(),
index.keys().next_back().unwrap(),
);
let meta = IndexMeta {
start,
end,
level,
data_size,
};
flush_index(&index, &meta, index_wtr).expect(INDEX_ERR);
index_wtr.flush().expect(INDEX_ERR);
}
pub fn create<I, K, V>(rows: &mut I, level: u8, data_wtr: &mut Writer, index_wtr: &mut Writer)
where
I: Iterator<Item = (K, V)>,
K: Borrow<Key>,
V: Borrow<Value>,
{
SSTable::create_capped(rows, level, u64::MAX, data_wtr, index_wtr);
}
pub fn from_parts(data: Arc<MemMap>, index: Arc<MemMap>) -> Result<Self> {
sst_from_parts(data, index)
}
pub fn could_contain(&self, key: &Key) -> bool {
self.meta.start <= *key && *key <= self.meta.end
}
pub fn is_overlap(&self, range: &RangeInclusive<Key>) -> bool {
let r = self.meta.start..=self.meta.end;
overlapping(&r, range)
}
pub fn sorted_tables(tables: &[SSTable]) -> Vec<BTreeMap<Key, SSTable>> {
let mut sorted = Vec::new();
for sst in tables {
let (key, level) = {
let meta = sst.meta();
(meta.start, meta.level)
};
while level as usize >= tables.len() {
sorted.push(BTreeMap::new());
}
sorted[level as usize].insert(key, sst.clone());
}
sorted
}
}
impl Key {
pub const MIN: Key = Key([0u8; KEY_LEN as usize]);
pub const MAX: Key = Key([255u8; KEY_LEN as usize]);
pub const ALL_INCLUSIVE: RangeInclusive<Key> = RangeInclusive::new(Key::MIN, Key::MAX);
pub fn write<W: Write>(&self, wtr: &mut W) -> Result<()> {
wtr.write_all(&self.0)?;
Ok(())
}
pub fn read(bytes: &[u8]) -> Key {
let mut key = Key::default();
key.0.copy_from_slice(bytes);
key
}
}
struct Scan {
bounds: RangeInclusive<Key>,
data: Arc<MemMap>,
index: Arc<MemMap>,
index_pos: usize,
}
impl Scan {
fn new(bounds: RangeInclusive<Key>, data: Arc<MemMap>, index: Arc<MemMap>) -> Self {
Scan {
bounds,
data,
index,
index_pos: IDX_META_SIZE as usize,
}
}
fn step(&mut self) -> Result<Option<(Key, Value)>> {
while self.index_pos < self.index.len() {
let pos = self.index_pos as usize;
let end = pos + INDEX_ENTRY_SIZE;
let (key, ts, idx) = read_index_rec(&self.index[pos..end]);
if key < *self.bounds.start() {
self.index_pos = end;
continue;
}
if *self.bounds.end() < key {
self.index_pos = std::usize::MAX;
return Ok(None);
}
let bytes_opt = idx.map(|ptr| get_val(&self.data, ptr).to_vec());
let val = Value { ts, val: bytes_opt };
self.index_pos = end;
return Ok(Some((key, val)));
}
Ok(None)
}
}
impl From<(u64, u64, u64)> for Key {
fn from((k0, k1, k2): (u64, u64, u64)) -> Self {
let mut buf = [0u8; KEY_LEN as usize];
BigEndian::write_u64(&mut buf[..8], k0);
BigEndian::write_u64(&mut buf[8..16], k1);
BigEndian::write_u64(&mut buf[16..], k2);
Key(buf)
}
}
impl Index {
fn write<W: Write>(&self, wtr: &mut W) -> Result<()> {
wtr.write_u64::<BigEndian>(self.offset)?;
wtr.write_u64::<BigEndian>(self.size)?;
Ok(())
}
#[inline]
fn read(bytes: &[u8]) -> Index {
let offset = BigEndian::read_u64(&bytes[..8]);
let size = BigEndian::read_u64(&bytes[8..16]);
Index { offset, size }
}
}
impl IndexMeta {
fn write<W: Write>(&self, wtr: &mut W) -> Result<()> {
self.start.write(wtr)?;
self.end.write(wtr)?;
wtr.write_u8(self.level)?;
wtr.write_u64::<BigEndian>(self.data_size)?;
Ok(())
}
fn read(data: &[u8]) -> Self {
let start = Key::read(&data[..24]);
let end = Key::read(&data[24..48]);
let level = data[48];
let data_size = BigEndian::read_u64(&data[49..57]);
IndexMeta {
start,
end,
level,
data_size,
}
}
}
impl<I> Merged<I>
where
I: Iterator<Item = (Key, Value)>,
{
pub fn new(mut sources: Vec<I>) -> Self {
let mut heads = BTreeMap::new();
for (source_idx, source) in sources.iter_mut().enumerate() {
if let Some((k, v)) = source.next() {
heads.insert((k, source_idx), v);
}
}
Merged {
sources,
heads,
seen: HashMap::new(),
}
}
}
impl<I> Iterator for Merged<I>
where
I: Iterator<Item = (Key, Value)>,
{
type Item = (Key, Value);
fn next(&mut self) -> Option<Self::Item> {
while !self.heads.is_empty() {
let (key, source_idx) = *self.heads.keys().next().unwrap();
let val = self.heads.remove(&(key, source_idx)).unwrap();
// replace
if let Some((k, v)) = self.sources[source_idx].next() {
self.heads.insert((k, source_idx), v);
}
// merge logic
// if deleted, remember
let (deleted, stale) = match self.seen.get(&key) {
Some(&seen_ts) if seen_ts < val.ts => {
// fresh val
self.seen.insert(key, val.ts);
(val.val.is_none(), false)
}
Some(_) => (val.val.is_none(), true),
None => {
self.seen.insert(key, val.ts);
(val.val.is_none(), false)
}
};
if !(stale || deleted) {
return Some((key, val));
}
}
None
}
}
impl Iterator for Scan {
type Item = (Key, Value);
fn next(&mut self) -> Option<Self::Item> {
if self.index_pos as usize >= self.index.len() {
return None;
}
match self.step() {
Ok(opt) => opt,
Err(_) => {
self.index_pos = std::usize::MAX;
None
}
}
}
}
fn sst_from_parts(data: Arc<MemMap>, index: Arc<MemMap>) -> Result<SSTable> {
let len = index.len() as usize;
assert!(len > IDX_META_SIZE);
assert_eq!((len - IDX_META_SIZE) % INDEX_ENTRY_SIZE, 0);
let mut rdr = Cursor::new(&**index);
let mut idx_buf = [0; IDX_META_SIZE];
rdr.read_exact(&mut idx_buf)?;
let meta = IndexMeta::read(&idx_buf);
Ok(SSTable { data, index, meta })
}
fn flush_index(
index: &BTreeMap<Key, (i64, Option<Index>)>,
meta: &IndexMeta,
wtr: &mut Writer,
) -> Result<()> {
meta.write(wtr)?;
for (&key, &(ts, idx)) in index.iter() {
write_index_rec(wtr, (key, ts, idx))?;
}
Ok(())
}
#[allow(clippy::type_complexity)]
fn flush_mem_table_capped<I, K, V>(
rows: &mut I,
wtr: &mut Writer,
max_table_size: u64,
) -> Result<(u64, BTreeMap<Key, (i64, Option<Index>)>)>
where
I: Iterator<Item = (K, V)>,
K: Borrow<Key>,
V: Borrow<Value>,
{
let mut ssi = BTreeMap::new();
let mut size = 0;
for (key, val) in rows {
let (key, val) = (key.borrow(), val.borrow());
let ts = val.ts;
let (index, item_size) = match val.val {
Some(ref bytes) => (Some(write_val(wtr, bytes)?), bytes.len()),
None => (None, 0),
};
size += item_size as u64;
ssi.insert(*key, (ts, index));
if size >= max_table_size {
break;
}
}
Ok((size, ssi))
}
#[inline]
fn overlapping<T: Ord + Eq>(r1: &RangeInclusive<T>, r2: &RangeInclusive<T>) -> bool {
r1.start() <= r2.end() && r2.start() <= r1.end()
}
#[inline]
fn write_val<W: Write + Seek>(wtr: &mut W, val: &[u8]) -> Result<Index> {
let offset = wtr.seek(SeekFrom::Current(0))?;
let size = val.len() as u64;
wtr.write_all(val)?;
Ok(Index { offset, size })
}
#[inline]
fn get_val(mmap: &MemMap, idx: Index) -> &[u8] {
let row = &mmap[idx.offset as usize..(idx.offset + idx.size) as usize];
assert_eq!(row.len(), idx.size as usize);
row
}
#[inline]
fn write_index_rec<W: Write>(wtr: &mut W, (key, ts, ptr): (Key, i64, Option<Index>)) -> Result<()> {
key.write(wtr)?;
wtr.write_i64::<BigEndian>(ts)?;
match ptr {
Some(idx) => idx.write(wtr)?,
None => wtr.write_all(&TOMBSTONE)?,
};
Ok(())
}
#[inline]
fn read_index_rec(bytes: &[u8]) -> (Key, i64, Option<Index>) {
assert_eq!(bytes.len(), INDEX_ENTRY_SIZE);
const TS_END: usize = KEY_LEN + 8;
let mut key_buf = [0; KEY_LEN as usize];
key_buf.copy_from_slice(&bytes[..KEY_LEN as usize]);
let key = Key(key_buf);
let ts = BigEndian::read_i64(&bytes[KEY_LEN..TS_END]);
let idx_slice = &bytes[TS_END..INDEX_ENTRY_SIZE];
let idx = if idx_slice == TOMBSTONE {
None
} else {
Some(Index::read(idx_slice))
};
(key, ts, idx)
}

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