Compare commits

...

305 Commits

Author SHA1 Message Date
5d8b2f899a Fix wallet doc 2018-07-02 19:21:03 -07:00
490205ab84 Fix sanity check
...that my last PR broke
2018-07-02 19:21:03 -07:00
2c0e704c82 Confirm the payment 2018-07-02 17:59:50 -07:00
253048f72d Only tune networking for leader/validator 2018-07-02 17:59:50 -07:00
e09b8430ce Add |wallet reset| command 2018-07-02 17:59:50 -07:00
9ae283dc3a Expose wallet.sh as a Snap program temporarily 2018-07-02 17:59:50 -07:00
f95a79d145 Default to using testnet.s.c when running as a Snap 2018-07-02 17:59:50 -07:00
0dabdfd48e Use zero to represent a nonexistent account
This also fixes a bug in the thin client where a nonexistent account
would have triggered a panic because we were using `balances[k]` instead
of `balances.get(key)`.

Fixes #534
2018-07-02 18:48:40 -06:00
d2bb4dc14a Purge empty accounts 2018-07-02 18:48:40 -06:00
b4dc180592 More quotes to pacify shellcheck 2018-07-02 16:41:22 -07:00
263577773f Set client config directory correctly in a Snap 2018-07-02 16:41:22 -07:00
7d708be121 Drone now grabs mint.json locally 2018-07-02 16:41:22 -07:00
feb1669d39 Correct locate rsync when running as a Snap 2018-07-02 15:57:30 -07:00
2cbfe41422 Abort nicer on drone connection failure 2018-07-02 15:57:30 -07:00
b7653865b1 Support testnet.solana.com as first argument 2018-07-02 15:57:30 -07:00
c72dced8fa Report error when an invalid confirmation signature or public key is provided 2018-07-02 15:57:30 -07:00
6feed5fd56 rebased 2018-07-02 16:34:49 -06:00
b8fe5ae076 rename server to fullnode 2018-07-02 16:34:49 -06:00
7e657d65f3 merged f2ab08c65e 2018-07-02 16:34:49 -06:00
a166bb816e wtfr 2018-07-02 16:34:49 -06:00
2952027d04 wtfr 2018-07-02 16:34:49 -06:00
430d9d9314 fixup! 2018-07-02 16:34:49 -06:00
fa247196c0 fullnode lib 2018-07-02 16:34:49 -06:00
5d17c2b58f Return output receivers from each stage
Reaching into the stages' structs for their receivers is, in hindsight,
more awkward than returning multiple values from constructors. By
returning the receiver, the caller can name the receiver whatever it
wants (as you would with any return value), and doesn't need to
reach into the struct for the field (which is super awkward in
combination with move semantics).
2018-07-02 16:18:32 -06:00
6ee45d282e some auto-detect of wallet commands 2018-07-02 15:51:12 -06:00
cfc3bd0696 Add manual wallet sanity test 2018-07-02 14:38:01 -07:00
3e0e09555a Undo UPnP UDP port binding 2018-07-02 14:38:01 -07:00
1d8bb5144e Drop -demo suffix 2018-07-02 14:38:01 -07:00
67e0100866 Bind to 0.0.0.0 2018-07-02 14:38:01 -07:00
f2ab08c65e Reuse request UDP port for responses 2018-07-02 14:38:01 -07:00
04a93050e7 No need to share a write lock across single-threaded methods 2018-07-02 15:25:16 -06:00
03401041db Correct signature argument name 2018-07-02 11:24:13 -07:00
6eac744a05 Only rsync leader.json once 2018-07-02 10:59:09 -07:00
ae29e2085f Init env_logger 2018-07-02 10:59:09 -07:00
7ce0b58af8 Document pkg-config dependency 2018-07-02 10:42:56 -07:00
ea5663c0da Demote log 2018-07-02 10:28:43 -07:00
a61bfae8a4 Document libssl-dev dependency 2018-07-02 10:28:43 -07:00
5716898216 setup.sh can now be more picky about the kind of config it creates 2018-07-02 09:22:26 -07:00
c0f9e452f2 mint.json is now private 2018-07-02 09:22:26 -07:00
4e3526394e Use IntoInterator to simplify write_entries() usage 2018-07-02 09:51:39 -06:00
6806a14a3f Use Cursor instead of tempfile.
Faster and one less dependency.
2018-07-02 09:51:39 -06:00
ec7e50b37d Consolidate ledger serialization code
The new read_entries() works, but is overly-contrained. Not
using that function yet, but adding it here in the hopes some
Rust guru will tell us how to get that lifetime constraint out
of there.

Fixes #517
2018-07-02 09:51:39 -06:00
e7b7dfebf5 Add tests for process_ledger() 2018-07-02 09:51:39 -06:00
a9e0b27772 Speed up snap build
1. Use pre-installed host rust toolchain
2. Build reference/performance fullnode in same part to avoid rebuilding libraries
3. Merge scripts into same part
2018-07-01 17:47:51 -07:00
669164bada Boot EntryWriter's Mutex
Finally!
2018-07-01 17:29:24 -06:00
4f3a291391 Move the writer into EntryWriter 2018-07-01 17:29:24 -06:00
56e37ad2f4 Limit sticky mutex to WriteStage 2018-07-01 17:29:24 -06:00
17de79a83a Remove dead code 2018-07-01 17:29:24 -06:00
09e9139855 Move channel code to write stage 2018-07-01 17:29:24 -06:00
76fc5822c9 Send Vec<Entry> between stages instead of Entry
Might see a performance boost here.
2018-07-01 17:29:24 -06:00
c767a854ed Remove useless Arc 2018-07-01 11:35:32 -07:00
b60802ddff Refactor such that genesis can use entry_writer 2018-07-01 11:35:32 -07:00
1c35d59f26 Receive entries first, then write 2018-07-01 11:35:32 -07:00
adcaf715c6 Cleanup write_entries 2018-07-01 11:35:32 -07:00
1f9494221b Make space for a write_entry() that only writes entries 2018-07-01 11:35:32 -07:00
466d6f76b9 Don't hide error in write_entry() 2018-07-01 11:35:32 -07:00
b05e6ce3db Cleanup solana-genesis 2018-07-01 11:35:32 -07:00
1d812e78d5 Use hard linking to speed up target cache save/restore 2018-07-01 08:59:42 -07:00
fba494343f Save/restore target/ directory between builds 2018-06-30 22:30:57 -07:00
0b878eccf8 Map HOME to grant docker containers access to the ~/.cargo registry cache 2018-06-30 21:50:15 -07:00
98772b16d6 Generalize solana-snap build trigger 2018-06-30 21:50:15 -07:00
bb82ff0c80 Don't wanna wait 2018-06-30 20:05:27 -07:00
71af03dc98 Skip snap build for PRs if nothing under snap/ is modified
Additionally relegate push snap build to a secondary CI pipeline
2018-06-30 20:05:27 -07:00
5671da4a0a Generate a client-specific mint.json 2018-06-30 15:28:17 -07:00
d63493a852 Grant the snap build more time 2018-06-30 12:20:22 -07:00
c06582ba40 Wallet no longer uses global mint.json 2018-06-29 22:26:42 -07:00
450f271cf7 Move public IP address detection out of bash 2018-06-29 21:12:05 -07:00
a31889f129 Readme version bump 2018-06-29 21:39:41 -06:00
ba6a6f5227 Use clap crate for wallet CLI subcommands and arguments 2018-06-29 21:30:20 -06:00
9a38d61048 Version bump 2018-06-29 21:23:50 -06:00
903ec27754 Add BROKEN_NAT env variable to select Udp sender port workaround 2018-06-29 20:02:28 -07:00
0b56d603c2 Client NAT traversal 0.1
UPnP is now used to request a port on the NAT be forwarded to the local machine.
This obviously only works for NATs that support UPnP, and thus is not a panacea
for all NAT-related connectivity issues.

Notable hacks in this patch include a transmit/receive UDP socket pair to work
around current protocol limitations whereby the full node assumes its peer can
receive on the same UDP port it transmitted from.
2018-06-29 17:36:26 -07:00
4ffb5d157a Disable coverage until issue #433 is resolved 2018-06-29 17:36:26 -07:00
816246ebee Add doc 2018-06-29 17:28:12 -06:00
a9881aee05 Add base58-encoded addresses 2018-06-29 17:28:12 -06:00
7b5b989cfe Print usage is a command is not provided 2018-06-29 17:28:12 -06:00
c4b62e19f2 Do Proof of History verification before appending entries to the bank
Note: replicate_stage is still using `process_entries()` because
changing it to `process_blocks()` causes the `test_replicate` test to
fail.
2018-06-29 15:35:39 -06:00
79a97ada04 Fix more shellchecks
Also, stops current nodes before pushing updates
2018-06-29 15:19:28 -06:00
da215d1a21 Fix failed shellchecks 2018-06-29 15:19:28 -06:00
9ffc50bead Address review comments 2018-06-29 15:19:28 -06:00
f8352bac2f Address review comments
* Only public IP address in the list
* formatting and other comments
2018-06-29 15:19:28 -06:00
27c1410fdc Script to deploy multiple nodes (one as leader, others as validators)
* The built code is loaded to the nodes
* ssh_keys can be copied to the nodes for internode comm
* The nodes are started with their respective roles
* The client demo is started on the last node
2018-06-29 15:19:28 -06:00
9a4733bde7 Remove interactive behavior from wallet 2018-06-29 13:22:20 -06:00
f3df5df52c add validator catchup to multi-node test 2018-06-29 10:39:41 -07:00
517d08c637 Cleanup 2018-06-29 09:51:13 -07:00
90dd794ae5 cargo fmt
rustfmt 0.6.1-stable (49279d71 2018-05-08)
2018-06-29 09:51:13 -07:00
e0dbbba8a3 fmt 2018-06-29 09:51:13 -07:00
705df55a7f Fix program name 2018-06-29 09:51:13 -07:00
d354e85a9a Return bool on signature check 2018-06-29 09:51:13 -07:00
e4e1f8ec1e Missing -m parameter handling 2018-06-29 09:51:13 -07:00
0112a24179 Add confirm command to wallet, and update RPU to check bank for a signature 2018-06-29 09:51:13 -07:00
d680f6b3a5 Fix bash scripts:
* Use wallet name everywhere
* Update drone to use mint.json
2018-06-29 09:51:13 -07:00
47e732717f more notes 2018-06-29 10:18:36 -06:00
ec56abfccb Correct setup.sh args 2018-06-29 07:59:16 -07:00
e7cdb402fb highlight 2018-06-29 07:16:03 -06:00
a3fe1965fb spelling 2018-06-29 07:16:03 -06:00
5256e6833e update 2018-06-29 07:16:03 -06:00
051cd2e1ff more examples 2018-06-29 07:16:03 -06:00
51929e7df8 rfcs 2018-06-29 07:16:03 -06:00
a094507bb8 Lower default benchmarking numbers to make CI timeout 2018-06-29 07:14:47 -06:00
8effa4e3e0 Clear old blobs before putting in the new one
Otherwise we will just warn about overrun and not insert new blob
Also, break if the index we find is less than consumed otherwise
we can infinite loop
2018-06-29 07:14:47 -06:00
1c9e7dbc45 Don't recycle in the replicate stage
Windowing stage owns all the blobs now
2018-06-29 07:14:47 -06:00
799b249f02 Don't null blob window until we have to 2018-06-29 07:14:47 -06:00
7b4a378c92 Add public-ip option to snap validator with cuda 2018-06-28 21:14:29 -06:00
47917d00d1 Always bind to 0.0.0.0 regardless of what's being advertised to other nodes 2018-06-28 19:13:36 -07:00
a4c49af859 Add public-ip argument to setup.sh 2018-06-28 19:13:36 -07:00
1c1d7d1e0e Log get_last_id errors 2018-06-28 19:13:36 -07:00
d28536d76e Fix spelling of signature 2018-06-28 16:31:33 -07:00
63cfbb9497 Only register last entry after a split 2018-06-28 16:54:06 -06:00
231040b93e Add tests 2018-06-28 12:28:43 -07:00
7c74afc35a Relax recycler
Instead of asserting ref count is 1 before recycling, allow users
to recycle items early. If it turns out that was too early, and
allocate() wants to return it, then boot it and take a memory
allocation performance hit instead.
2018-06-28 12:28:43 -07:00
7878a011eb Use a Mint to configure the wallet
* Send transactions from the mint's private key
* By default, send full balance to oneself
* By default, request the mint's number of tokens for airdrops
2018-06-27 17:35:50 -06:00
c05416e27d Turn simple-client-demo into a simpler wallet 2018-06-27 17:35:50 -06:00
ee200d8fa0 Add DEBUG= flag to select debug binaries 2018-06-27 15:34:31 -07:00
2f42658cd4 ... 2018-06-27 14:51:18 -07:00
d95e8030fc ... 2018-06-27 14:51:18 -07:00
4aedd3f1b6 Cleanup type aliases and imports 2018-06-27 15:06:18 -06:00
bb89d6f54d Get back to 500k transactions 2018-06-27 13:50:27 -07:00
ed10841e3d No longer spin up accounts for client-demo
Now that the Bank is single-threaded again, we can spin up new
accounts on the fly without concern of thread contention. Likewise,
we can send all transactions from a single account, which was also
problematic in the multi-threaded bank. Sending from one account will
also make client-demo straightforward to port to solana-drone.
2018-06-27 13:50:27 -07:00
6dac87f2a7 Add entry to snapscraft yaml; cleanup bash header 2018-06-27 13:01:29 -06:00
a167d0d331 CI cleanup 2018-06-27 13:01:29 -06:00
eed37820b5 Comments 2018-06-27 13:01:29 -06:00
124e1fa350 Bash scripts to go with simple-client-demo 2018-06-27 13:01:29 -06:00
ac40434cdf Initial simple client demo commit 2018-06-27 13:01:29 -06:00
39354c06f8 take multiple log files, allow restart of leader, validator 2018-06-27 11:41:25 -07:00
faedb88de0 s/local/declare/g 2018-06-26 19:11:31 -07:00
5cd1fb486f Automatically add rsync:// prefix to URLs that need it 2018-06-26 17:45:53 -07:00
5b5df49e6c make client.sh behave like the others, i.e. no tee to a log 2018-06-26 17:02:24 -07:00
86f9277e2d Add USE_SNAP flag 2018-06-26 16:32:55 -07:00
56b09bf0ac cargo fmt 2018-06-26 16:51:07 -06:00
f4c4b9df9c Only free in replicate if we did not hold the reference in window stage
And then free when we are consuming blobs
2018-06-26 16:51:07 -06:00
6e568c69a7 Preemptive strike
Should that blob have been passed to a recycler, it would have
had too high a reference count.
2018-06-26 16:51:07 -06:00
14d624ee40 Fix benchmarks too
This change will make these benchmarks way slower, because its now
cloning the transaction vector each iteration instead of the ref
counts. We need to rethink these.
2018-06-26 16:51:07 -06:00
d5c0557891 Fix test_replicate too 2018-06-26 16:51:07 -06:00
1691060a22 Assert recycler is given last reference to data
This patch likely fixes the sporadic failures in the following tests:

```
test server::tests::validator_exit ... FAILED
test streamer::test::streamer_send_test ... FAILED
test thin_client::tests::test_bad_sig ... FAILED
test drone::tests::test_send_airdrop ... FAILED
test thin_client::tests::test_thin_client ... FAILED
```
2018-06-26 16:51:07 -06:00
a5ce578c72 ... 2018-06-26 16:23:41 -06:00
05edfad13a Fix compiler warnings 2018-06-26 15:03:15 -07:00
136b43f461 Fix whitespace
TODO: Why didn't "cargo fmt" fail the build.
2018-06-26 15:03:15 -07:00
ac40c1818f .. 2018-06-26 13:57:10 -07:00
eb63dbcd2a an Entry needs to be multiple of 4 bytes long 2018-06-26 13:57:10 -07:00
4e2f1a519e whack next_entries_batched 2018-06-26 13:57:10 -07:00
55ec7f9fe9 add entry.has_more
* quick fix for really big genesis
 * longer term fix for possible parallel verification over multiple
      Blobs/Entries
2018-06-26 13:57:10 -07:00
b7ddefdbf9 Empty plug array is not accepted by the snap store 2018-06-26 12:49:40 -07:00
ce361c2cdc Add Snap fullnode daemon 2018-06-26 12:32:33 -07:00
ed6ba55261 Add snap/ README 2018-06-26 12:32:33 -07:00
ec333d2bd6 Revert "-v was renamed to -t"
This reverts commit 8f4ce1e8d0.
2018-06-26 12:32:33 -07:00
551f639259 Some pull request fixes(linting + documentation) 2018-06-26 12:31:04 -06:00
da3bb6fb93 ran linter 2018-06-26 12:31:04 -06:00
08bcb62016 added remote table to update respones 2018-06-26 12:31:04 -06:00
8f4ce1e8d0 -v was renamed to -t 2018-06-25 20:48:26 -07:00
4a534d6abb Don't clone() Arc before recycling
This might fix an awful bug where the streamer reuses a Blob
before the current user is done with it. Recycler should probably
assert ref count is one?

* Also don't collect() an iterator before iterating over it.
2018-06-25 17:33:07 -06:00
b48a8c0555 Chunk blobs into window size to avoid window overrun
Fixes #447
2018-06-25 17:33:07 -06:00
1919ec247b add a clock to validator windows (part 3 of #309) (#448)
* count entries processed by Bank
 * initialize windows with initial height of Entries
2018-06-25 15:07:48 -07:00
3966eb5374 support MacOS bash and ifconfig properly 2018-06-25 13:14:36 -06:00
c22ef50cae Client fixes, poll for unique last id and cache clients
So we don't keep running up the port range
2018-06-25 10:02:29 -06:00
be5f2ef9b9 Consolidate CI jobs 2018-06-24 22:28:24 -07:00
adfcb79387 Force install cargo-cov 2018-06-24 15:34:30 -06:00
73c4c0ac5f Revert "cargo-cov installed by default in nightly?"
This reverts commit 6fc601f696.
2018-06-24 15:34:30 -06:00
6fc601f696 cargo-cov installed by default in nightly? 2018-06-24 12:17:42 -06:00
07111fb7bb Use llvm-cov instead of gcov
@marco-c called this a hack, but since grcov isn't working
out-of-the-box (panics on call to gcov), we'll take a stab at using
llvm-cov.
2018-06-24 12:17:42 -06:00
a06d2170b0 No need for rustfmt on nightly 2018-06-24 12:17:42 -06:00
7f53ea3bf3 Generate coverage with Rust nightly
Fixes #177

Thanks @marco-c!
2018-06-24 12:17:42 -06:00
b2accd1c2a Run snap build sooner to better mask the delay 2018-06-24 10:24:32 -07:00
8ef8a8dea7 borrow checker 2018-06-24 11:17:55 -06:00
e929404676 comments 2018-06-24 11:17:55 -06:00
c2258bedae fixed! 2018-06-24 11:17:55 -06:00
215fdbb7ed nits 2018-06-24 11:17:55 -06:00
ee998f6882 fix docs 2018-06-24 11:17:55 -06:00
826e95afca fix logs 2018-06-24 11:17:55 -06:00
47583d48e7 get rid of dummy test 2018-06-24 11:17:55 -06:00
e759cdf061 tests 2018-06-24 11:17:55 -06:00
88503c2a09 generic array fail case 2018-06-24 11:17:55 -06:00
d5be23dffe fmt 2018-06-24 10:44:17 -06:00
80c01dc085 Use leader.json or ReplicatedData to get ports for drone 2018-06-24 10:44:17 -06:00
45b2549fa9 Reset bad TestNode edit 2018-06-24 10:44:17 -06:00
c7ce454188 Use pnet_datalink instead of all of pnet
pnet_transport takes a long time to build. It's been especially
painful from within a docker container for reasons I don't care
to understand. pnet_datalink is the only part of pnet we're using
so booting the rest.
2018-06-24 10:39:59 -06:00
7059ea42d6 comments 2018-06-24 09:19:05 -06:00
8ea1c29c9b more notes 2018-06-24 09:19:05 -06:00
33bbfdbc9b Retry flaky coverage/cuda builds on initial failure 2018-06-23 16:17:25 -07:00
5de54f8853 Make cuda/erasure build logs public 2018-06-23 16:17:25 -07:00
a1ac41218a Document CUDA version 2018-06-23 16:17:25 -07:00
55fc647568 fix more shellcheck 2018-06-23 16:00:17 -07:00
e83e898eed fix shellcheck's concerns 2018-06-23 16:00:17 -07:00
eb07e4588b remove IPADDR, which was making Rob feel ill
IPADDR is simple, but not exactly what we need for testnet, where NAT'd
  folks need to join in, need to advertize themselves as on the interweb.

  myip() helps, but there's some TODOs: fullnode-config probably needs to
  be told where it lives in the real world (machine interfaces tell us dick),
  or incorporate something like the "ifconfig.co" code in myip.sh
2018-06-23 16:00:17 -07:00
563f834c96 Document how to update the snap 2018-06-23 15:29:22 -07:00
183178681d Simply fetching perf libs 2018-06-23 12:54:38 -07:00
8dba53e494 debit undo (#423) 2018-06-23 06:14:52 -07:00
e4782b19a3 Document GCP setup 2018-06-23 02:12:20 -07:00
ec86b1dffa Adapt to GCP-based CI 2018-06-23 02:12:20 -07:00
6cb8266c7b cleanup (#419) 2018-06-22 23:26:42 -07:00
9c50302a39 Update rfc-001-smart-contracts-engine.md 2018-06-22 22:45:22 -07:00
3313c69898 remove ccal 2018-06-22 22:43:54 -07:00
530c6ca7ec a bunch of updates 2018-06-22 22:43:54 -07:00
07ed2fb523 cleanup 2018-06-22 22:43:54 -07:00
d9ec380a15 cleanup 2018-06-22 22:43:54 -07:00
b60eb3a899 edits 2018-06-22 22:43:54 -07:00
b4df69791b cleanup 2018-06-22 22:43:54 -07:00
c21b8a22b9 update 2018-06-22 22:43:54 -07:00
475a76e656 wip 2018-06-22 22:43:54 -07:00
7ba5d5ef86 first! 2018-06-22 22:43:54 -07:00
737dc1ddde Per rustc 1.27.0, we can ensure nested results are used 2018-06-22 22:42:47 -07:00
164bf19b36 Update LICENSE 2018-06-22 22:41:04 -07:00
25976771d9 Version bump 2018-06-22 22:38:18 -07:00
f2198c2e9a cargo fmt
rustc 1.27.0

```
$ cargo fmt --version
rustfmt 0.6.1-stable (49279d71 2018-05-08)
```
2018-06-22 22:23:55 -07:00
eec19c6d2c move genesis to new Entry generation 2018-06-22 17:46:45 -07:00
30e03feb5f Add initial CI subsystem documentation 2018-06-22 15:30:29 -07:00
58cd3bde9f Add drone to snap package 2018-06-22 15:27:25 -07:00
662bfb7b88 fmt 2018-06-22 14:52:36 -07:00
5f3e3a17d3 Fix test_send_airdrop cap; add helpful panic msgs 2018-06-22 14:52:36 -07:00
feba2d9975 Set request cap to a reasonable number, based on 30min reset noted in issue #341 2018-06-22 14:52:36 -07:00
e3e3a1c457 Better drone request cap handling 2018-06-22 14:52:36 -07:00
90628f3c8d Edit TestNode port logic to be consistent with new_leader (fixes hanging test_send_airdrop) 2018-06-22 14:52:36 -07:00
f6bcadb79d Make airdrop amount variable 2018-06-22 14:52:36 -07:00
d4ac16773c fmt 2018-06-22 14:52:36 -07:00
96f044d2bf Clean up; add new_from_server_addr routine 2018-06-22 14:52:36 -07:00
f31868b913 Rename drone bin; fix usage statement 2018-06-22 14:52:36 -07:00
73b0ff5b55 Add request-count check and tests; fmt 2018-06-22 14:52:36 -07:00
64cf69045a Add request-count check; Clean up solana-drone and fmt 2018-06-22 14:52:36 -07:00
e57dae0f31 Update config and dependencies for solana-drone 2018-06-22 14:52:36 -07:00
6386e7d5cf Leave some tokens in the mint for solana-drone 2018-06-22 14:52:36 -07:00
4bad103da9 Add solana-drone CLI 2018-06-22 14:52:36 -07:00
30a26adb7c Add solana-drone module to library 2018-06-22 14:52:36 -07:00
8be4adfc0a Rename tr => tx and add back comments 2018-06-22 14:34:46 -07:00
fed4cc3965 Remove commented code/imports 2018-06-22 14:34:46 -07:00
7d1e074683 bump last_ids 2018-06-22 14:34:46 -07:00
00516e50a1 last_ids opt 2018-06-22 14:34:46 -07:00
e83d76fbd9 Remove mutexes 2018-06-22 14:34:46 -07:00
304f152315 rwlock balances table 2018-06-22 14:34:46 -07:00
3a82ebf7fd Add multiple source accounts for benchmark 2018-06-22 14:34:46 -07:00
0253d34467 Address review comments 2018-06-22 14:18:45 -07:00
9209f9acde Run multiple instances from same workspace
* Support running leader and validators from multiple machines
  using the same NFS mounted workspace.
* Changes to setup, leader and validator scripts
2018-06-22 14:18:45 -07:00
3dbbb398df use next_entries() in recorder, recycle blobs in reconstruct_from_blobs 2018-06-22 14:17:36 -07:00
17e8ad110f Temporarily disable failing CI to get back to green 2018-06-22 11:29:31 -07:00
5e91d31ed3 issue 309 part 1
* limit the number of Tntries per Blob to at most one
* limit the number of Transactions per Entry such that an Entry will
    always fit in a Blob

With a one-to-one map of Entries to Blobs, recovery of a validator
  is a simple fast-forward from the end of the initial genesis.log
  and tx-*.logs Entries.

TODO: initialize validators' blob index with initial # of Entries.
2018-06-22 09:58:51 -07:00
fad9d20820 Add assertion for now next_entry must be called 2018-06-21 21:24:32 -07:00
fe9a1c8580 Fix comment 2018-06-21 21:24:32 -07:00
cd6d7d5198 Remove redundant clones 2018-06-21 21:24:32 -07:00
771478bc68 Add simple CUDA version check, warn on mismatch 2018-06-21 13:42:06 -07:00
c4a59896f8 Run test-erasure in a container 2018-06-21 13:00:40 -07:00
3eb1608403 Skip --user if SOLANA_DOCKER_RUN_NOSETUID is set 2018-06-21 12:24:52 -07:00
8fde70d4dc Erasure tests do not require a CUDA agent 2018-06-21 11:42:37 -07:00
5a047833ed Run snap publishing directly on CUDA agent
This is necessary until we build a docker image that also contains a CUDA
installation
2018-06-21 11:42:37 -07:00
f6c28e6be1 Update snapcraft docker image contain snapcraft 2.42.1 2018-06-21 11:42:37 -07:00
0ebf10d19d Snap cuda fullnode 2018-06-21 11:42:37 -07:00
d3005d3ef3 Updated setup and leader scripts
* Setup will us -b to set validator ports
* Leader script fixed to append .log to the log file
* Updated readme file
2018-06-20 19:05:38 -07:00
effcef2184 fixed sleep bug 2018-06-20 16:58:10 -07:00
89fc0ad7a9 Add convenience script to download performance libraries 2018-06-20 16:48:32 -07:00
410272ee1d Update generic_array
Warning: this may have performance implications.
2018-06-20 11:41:54 -07:00
1c97bf50b6 Fix nightly
No longer ignore failures in the nightly build.
2018-06-19 17:38:04 -07:00
4ecd2c9d0b update demo scripts
* add setup to combine init steps, configurable initial mint
  * bash -e -> bash and be explicit about errors with || exit $?
  * feed transaction logs to validator, too
2018-06-19 17:04:44 -07:00
e592243a09 De-double quote 2018-06-19 13:20:47 -07:00
2f4a92e352 Cleanup test 2018-06-19 12:36:02 -07:00
ceafc29040 fix linting errors, add retransmission fix to repair requests 2018-06-19 12:36:02 -07:00
b20efabfd2 added retransmission of repair messages 2018-06-19 12:36:02 -07:00
85b6e7293c Add cleanup script to manage build agent disk space 2018-06-19 12:22:45 -07:00
6aced927ad improve ledger initialization for fullnode
* use a line iterator on stdin instead of a line iterator on a buffer
 * move some unwrap() to expect(), documenting failures
 * bind entry type earlier (for kicks)
2018-06-19 09:28:35 -07:00
75997e6c08 Allow BUILDKITE_BRANCH in containers 2018-06-18 22:51:30 -07:00
9040d00110 Package solana as a snap 2018-06-18 17:36:03 -07:00
8ebc5c6b07 Suggest different validator port by default to coexist with leader port on the same machine 2018-06-18 17:36:03 -07:00
d4807790ff Add snapcraft login credentials
This file was created as follows:
$ snapcraft export-login --snaps solana --channels beta,edge snapcraft.credentials
$ openssl aes-256-cbc -e -in snapcraft.credentials -out snapcraft.credentials.enc
2018-06-18 17:36:03 -07:00
0de5e7a285 attempt to understand entry 2018-06-18 16:48:59 -07:00
c40000aeda Fix compiler warning 2018-06-18 15:49:41 -07:00
31198bc105 Fix cargo bench nightly 2018-06-18 13:20:39 -07:00
92599acfca Abort when -l is not present or unreadable 2018-06-16 09:55:03 -07:00
f6e70779fe Don't panic if sent a bad packet 2018-06-16 09:51:45 -06:00
3017bde686 Update README.md 2018-06-16 09:43:23 -06:00
9d84ec4bb3 Delete TODO
That comment predates the separation of RPU and TPU.
2018-06-16 08:59:30 -06:00
586141adb2 Cleanup TVU docs 2018-06-15 22:45:35 -06:00
3f763f99e2 Fail fast in CI when |cargo fmt| says no 2018-06-15 17:10:00 -07:00
15c7f36ea3 Improve error reporting 2018-06-15 17:10:00 -07:00
04d1a083fa Skip |sudo sysctl ...| on macOS 2018-06-15 17:10:00 -07:00
327ee1dae8 Apply feedback from @aeyakovenko 2018-06-15 17:01:38 -06:00
22885c3e64 Add TVU ASCII art 2018-06-15 17:01:38 -06:00
94ededb54c Add comments and limit digits for tps prints 2018-06-15 11:54:01 -06:00
af6a07697a Change client-demo to run continuosly for some amount of time
Also retry for get_last_id/transaction_count if dropped.
2018-06-15 11:54:01 -06:00
5f1d8c95eb Fix blob data size 2018-06-15 11:54:01 -06:00
7d9e032407 make sure we test large tables 2018-06-15 06:56:35 -06:00
bc918a5ad5 purger 2018-06-15 06:56:35 -06:00
ee54ce4727 min table size before purge 2018-06-15 06:56:35 -06:00
e85bf2f2d5 tests pass 2018-06-15 06:56:35 -06:00
a7460ffbd1 purge validators we havent seen for a long time 2018-06-15 06:56:35 -06:00
7fe1fd2f95 clean up fullnode cmdline
* fix documentation, other opt parameters
 * add support for a named output file, remove hardcoded "leader.log"
 * resurrect stdout as the default output
2018-06-15 00:41:07 -07:00
d30670e92e clean up demo bash scripts
* allow other level of RUST logging
 * avoid "echo" in favor of printf (builtin)
 * single quotes for literals, double quotes for variables
2018-06-14 23:12:11 -06:00
9b202c6e1e No longer flood log with emtpy entries 2018-06-14 18:04:36 -06:00
87946eafd5 Lower processing transaction message to debug by default 2018-06-14 17:08:11 -06:00
7575d3c726 Add timestamp to log messages
Upgraded env_logger and now we have timestamps and colorful messages.

Fixes #318
2018-06-14 17:07:58 -06:00
8b9713a934 Skip link_local v4 addresses and v6 address when v6 is not enabled 2018-06-14 16:10:31 -06:00
ec713c18c4 Revert client.sh script to use cargo 2018-06-14 11:56:36 -06:00
c24b0a1a3f TVU rework (#352)
Refactored TVU, into stages
* blob fetch stage for blobs
* window stage for maintaining the blob window
* pulled out NCP out of the TVU so they can be separate units
TVU is now just the fetch -> window -> request and bank processing
2018-06-13 21:52:23 -07:00
34e0cb0092 cargo fmt 2018-06-13 19:17:21 -07:00
7b7c7cba21 changed atty library 2018-06-13 19:17:21 -07:00
c45343dd30 comments 2018-06-13 16:11:44 -06:00
b7f6603c1f fix coverage build 2018-06-13 16:11:44 -06:00
2d3b052dea allow for insertion of dummy entry points into the local table (#346)
* Needed for #341. Create a dummy entry with public key 0..., but with a valid gossip address that we can ask for updates. This will allow validators to discover the full network by just knowing a single node's gossip address without knowing anything else about their identity.
* once we start removing dead validators this entry should get purged since we will never see a message from public key 0, #344
2018-06-13 11:42:30 -07:00
dcb6234771 Fix relative link to client demo 2018-06-12 23:29:04 -06:00
e44d423e83 Make version syntax consistent
Using no symbol implies its a symver caret requirement.

https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
2018-06-11 15:51:25 -06:00
5435bb734c Upgrade rand 2018-06-11 15:51:25 -06:00
13f59adf61 Update build status badge link to publicly available build log 2018-06-10 22:12:09 -07:00
0fce3368d3 Fix json perf counter print and add script to generate a chart 2018-06-09 10:55:22 -07:00
1ee5c81267 Fix benchmarking banking_stage 2018-06-08 15:50:36 -07:00
3bb9d5eb50 Use timing::timestamp for counters 2018-06-08 15:50:36 -07:00
efb23f7cf9 Ensure stuck builds eventually timeout 2018-06-07 19:08:03 -07:00
013f4674de Target cuda agents 2018-06-07 19:08:03 -07:00
6966b25d9c Don't mark a build as failed if line coverage drops
It's not always a problem if line coverage drops. For example,
coverage will drop if you make well-tested code more succinct.
It just means the uncovered code is just a larger percentage of
the codebase.
2018-06-07 19:09:25 -06:00
d513f56c8c Readme version bump 2018-06-07 17:32:07 -06:00
95 changed files with 5639 additions and 1655 deletions

14
.buildkite/hooks/post-command Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash -e
[[ -n "$CARGO_TARGET_CACHE_NAME" ]] || exit 0
#
# Save target/ for the next CI build on this machine
#
(
d=$HOME/cargo-target-cache/"$CARGO_TARGET_CACHE_NAME"
mkdir -p $d
set -x
rsync -a --delete --link-dest=$PWD target $d
du -hs $d
)

13
.buildkite/hooks/pre-command Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash -e
[[ -n "$CARGO_TARGET_CACHE_NAME" ]] || exit 0
#
# Restore target/ from the previous CI build on this machine
#
(
d=$HOME/cargo-target-cache/"$CARGO_TARGET_CACHE_NAME"
mkdir -p $d/target
set -x
rsync -a --delete --link-dest=$d $d/target .
)

View File

@ -1,2 +1,5 @@
ignore:
- "src/bin"
coverage:
status:
patch: off

7
.gitignore vendored
View File

@ -2,3 +2,10 @@ Cargo.lock
/target/
**/*.rs.bk
.cargo
# node configuration files
/config/
/config-private/
/config-drone/
/config-validator/
/config-client/

View File

@ -1,7 +1,7 @@
[package]
name = "solana"
description = "Blockchain, Rebuilt for Scale"
version = "0.6.1"
version = "0.7.0-beta"
documentation = "https://docs.rs/solana"
homepage = "http://solana.com/"
readme = "README.md"
@ -17,6 +17,10 @@ license = "Apache-2.0"
name = "solana-client-demo"
path = "src/bin/client-demo.rs"
[[bin]]
name = "solana-wallet"
path = "src/bin/wallet.rs"
[[bin]]
name = "solana-fullnode"
path = "src/bin/fullnode.rs"
@ -29,17 +33,13 @@ path = "src/bin/fullnode-config.rs"
name = "solana-genesis"
path = "src/bin/genesis.rs"
[[bin]]
name = "solana-genesis-demo"
path = "src/bin/genesis-demo.rs"
[[bin]]
name = "solana-mint"
path = "src/bin/mint.rs"
[[bin]]
name = "solana-mint-demo"
path = "src/bin/mint-demo.rs"
name = "solana-drone"
path = "src/bin/drone.rs"
[badges]
codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
@ -53,7 +53,7 @@ erasure = []
[dependencies]
rayon = "1.0.0"
sha2 = "0.7.0"
generic-array = { version = "0.9.0", default-features = false, features = ["serde"] }
generic-array = { version = "0.11.1", default-features = false, features = ["serde"] }
serde = "1.0.27"
serde_derive = "1.0.27"
serde_json = "1.0.10"
@ -61,12 +61,22 @@ ring = "0.12.1"
untrusted = "0.5.1"
bincode = "1.0.0"
chrono = { version = "0.4.0", features = ["serde"] }
log = "^0.4.1"
env_logger = "^0.4.1"
matches = "^0.1.6"
byteorder = "^1.2.1"
libc = "^0.2.1"
getopts = "^0.2"
isatty = "0.1"
rand = "0.4.2"
pnet = "^0.21.0"
log = "0.4.2"
env_logger = "0.5.10"
matches = "0.1.6"
byteorder = "1.2.1"
libc = "0.2.1"
getopts = "0.2"
atty = "0.2"
rand = "0.5.1"
pnet_datalink = "0.21.0"
tokio = "0.1"
tokio-codec = "0.1"
tokio-core = "0.1.17"
tokio-io = "0.1"
itertools = "0.7.8"
bs58 = "0.2.0"
p2p = "0.5.2"
futures = "0.1.21"
clap = "2.31"
reqwest = "0.8.6"

View File

@ -1,4 +1,4 @@
Copyright 2018 Anatoly Yakovenko, Greg Fitzgerald and Stephen Akridge
Copyright 2018 Solana Labs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

166
README.md
View File

@ -1,6 +1,6 @@
[![Solana crate](https://img.shields.io/crates/v/solana.svg)](https://crates.io/crates/solana)
[![Solana documentation](https://docs.rs/solana/badge.svg)](https://docs.rs/solana)
[![Build status](https://badge.buildkite.com/d4c4d7da9154e3a8fb7199325f430ccdb05be5fc1e92777e51.svg?branch=master)](https://buildkite.com/solana-labs/solana)
[![Build status](https://badge.buildkite.com/d4c4d7da9154e3a8fb7199325f430ccdb05be5fc1e92777e51.svg?branch=master)](https://solana-ci-gate.herokuapp.com/buildkite_public_log?https://buildkite.com/solana-labs/solana/builds/latest/master)
[![codecov](https://codecov.io/gh/solana-labs/solana/branch/master/graph/badge.svg)](https://codecov.io/gh/solana-labs/solana)
Blockchain, Rebuilt for Scale
@ -58,45 +58,38 @@ your odds of success if you check out the
before proceeding:
```bash
$ git checkout v0.6.0
$ git checkout v0.7.0-beta
```
Configuration Setup
---
The network is initialized with a genesis ledger and leader/validator configuration files.
These files can be generated by running the following script.
```bash
$ ./multinode-demo/setup.sh
```
Singlenode Testnet
---
The fullnode server is initialized with a ledger from stdin and
generates new ledger entries on stdout. To create the input ledger, we'll need
to create *the mint* and use it to generate a *genesis ledger*. It's done in
two steps because the mint-demo.json file contains private keys that will be
used later in this demo.
```bash
$ echo 1000000000 | cargo run --release --bin solana-mint-demo > mint-demo.json
$ cat mint-demo.json | cargo run --release --bin solana-genesis-demo > genesis.log
```
Before you start a fullnode, make sure you know the IP address of the machine you
want to be the leader for the demo, and make sure that udp ports 8000-10000 are
open on all the machines you want to test with.
Generate a leader configuration file with:
```bash
cargo run --release --bin solana-fullnode-config -- -d > leader.json
```
Now start the server:
```bash
$ ./multinode-demo/leader.sh > leader-txs.log
$ ./multinode-demo/leader.sh
```
To run a performance-enhanced fullnode on Linux, download `libcuda_verify_ed25519.a`. Enable
it by adding `--features=cuda` to the line that runs `solana-fullnode` in `leader.sh`.
To run a performance-enhanced fullnode on Linux,
[CUDA 9.2](https://developer.nvidia.com/cuda-downloads) must be installed on
your system:
```bash
$ wget https://solana-build-artifacts.s3.amazonaws.com/v0.5.0/libcuda_verify_ed25519.a
cargo run --release --features=cuda --bin solana-fullnode -- -l leader.json < genesis.log
$ ./fetch-perf-libs.sh
$ SOLANA_CUDA=1 ./multinode-demo/leader.sh
```
Wait a few seconds for the server to initialize. It will print "Ready." when it's ready to
@ -107,21 +100,16 @@ Multinode Testnet
To run a multinode testnet, after starting a leader node, spin up some validator nodes:
Generate the validator's configuration file:
```bash
cargo run --release --bin solana-fullnode-config -- -d > validator.json
$ ./multinode-demo/validator.sh ubuntu@10.0.1.51:~/solana 10.0.1.51
```
To run a performance-enhanced fullnode on Linux,
[CUDA 9.2](https://developer.nvidia.com/cuda-downloads) must be installed on
your system:
```bash
$ ./multinode-demo/validator.sh ubuntu@10.0.1.51:~/solana > validator-txs.log #The leader machine
```
As with the leader node, you can run a performance-enhanced validator fullnode by adding
`--features=cuda` to the line that runs `solana-fullnode` in `validator.sh`.
```bash
cargo run --release --features=cuda --bin solana-fullnode -- -l validator.json -v leader.json < genesis.log
$ ./fetch-perf-libs.sh
$ SOLANA_CUDA=1 ./multinode-demo/leader.sh ubuntu@10.0.1.51:~/solana 10.0.1.51
```
@ -144,6 +132,92 @@ demo completes after it has convinced itself the testnet won't process any addit
transactions. You should see several TPS measurements printed to the screen. In the
multinode variation, you'll see TPS measurements for each validator node as well.
Linux Snap
---
A Linux [Snap](https://snapcraft.io/) is available, which can be used to
easily get Solana running on supported Linux systems without building anything
from source. The `edge` Snap channel is updated daily with the latest
development from the `master` branch. To install:
```bash
$ sudo snap install solana --edge --devmode
```
(`--devmode` flag is required only for `solana.fullnode-cuda`)
Once installed the usual Solana programs will be available as `solona.*` instead
of `solana-*`. For example, `solana.fullnode` instead of `solana-fullnode`.
Update to the latest version at any time with
```bash
$ snap info solana
$ sudo snap refresh solana --devmode
```
### Daemon support
The snap supports running a leader, validator or leader+drone node as a system
daemon.
Run `sudo snap get solana` to view the current daemon configuration, and
`sudo snap logs -f solana` to view the daemon logs.
Disable the daemon at any time by running:
```bash
$ sudo snap set solana mode=
```
Runtime configuration files for the daemon can be found in
`/var/snap/solana/current/config`.
#### Leader daemon
```bash
$ sudo snap set solana mode=leader
```
If CUDA is available:
```bash
$ sudo snap set solana mode=leader enable-cuda=1
```
`rsync` must be configured and running on the leader.
1. Ensure rsync is installed with `sudo apt-get -y install rsync`
2. Edit `/etc/rsyncd.conf` to include the following
```
[config]
path = /var/snap/solana/current/config
hosts allow = *
read only = true
```
3. Run `sudo systemctl enable rsync; sudo systemctl start rsync`
4. Test by running `rsync -Pzravv rsync://<ip-address-of-leader>/config
solana-config` from another machine. **If the leader is running on a cloud
provider it may be necessary to configure the Firewall rules to permit ingress
to port tcp:873, tcp:9900 and the port range udp:8000-udp:10000**
To run both the Leader and Drone:
```bash
$ sudo snap set solana mode=leader+drone
```
#### Validator daemon
```bash
$ sudo snap set solana mode=validator
```
If CUDA is available:
```bash
$ sudo snap set solana mode=validator enable-cuda=1
```
By default the validator will connect to **testnet.solana.com**, override
the leader IP address by running:
```bash
$ sudo snap set solana mode=validator leader-address=127.0.0.1 #<-- change IP address
```
It's assumed that the leader will be running `rsync` configured as described in
the previous **Leader daemon** section.
Developing
===
@ -164,6 +238,11 @@ If your rustc version is lower than 1.26.1, please update it:
$ rustup update
```
On Linux systems you may need to install libssl-dev and pkg-config. On Ubuntu:
```bash
$ sudo apt-get install libssl-dev pkg-config
```
Download the source code:
```bash
@ -226,12 +305,21 @@ $ cargo +nightly bench --features="unstable"
Code coverage
---
To generate code coverage statistics, run kcov via Docker:
To generate code coverage statistics, install cargo-cov. Note: the tool currently only works
in Rust nightly.
```bash
$ ./ci/coverage.sh
$ cargo +nightly install cargo-cov
```
The coverage report will be written to `./target/cov/index.html`
Run cargo-cov and generate a report:
```bash
$ cargo +nightly cov test
$ cargo +nightly cov report --open
```
The coverage report will be written to `./target/cov/report/index.html`
Why coverage? While most see coverage as a code quality metric, we see it primarily as a developer

View File

@ -11,5 +11,6 @@ fn main() {
}
if !env::var("CARGO_FEATURE_ERASURE").is_err() {
println!("cargo:rustc-link-lib=dylib=Jerasure");
println!("cargo:rustc-link-lib=dylib=gf_complete");
}
}

1
ci/.gitignore vendored
View File

@ -1,2 +1,3 @@
/node_modules/
/package-lock.json
/snapcraft.credentials

88
ci/README.md Normal file
View File

@ -0,0 +1,88 @@
Our CI infrastructure is built around [BuildKite](https://buildkite.com) with some
additional GitHub integration provided by https://github.com/mvines/ci-gate
## Agent Queues
We define two [Agent Queues](https://buildkite.com/docs/agent/v3/queues):
`queue=default` and `queue=cuda`. The `default` queue should be favored and
runs on lower-cost CPU instances. The `cuda` queue is only necessary for
running **tests** that depend on GPU (via CUDA) access -- CUDA builds may still
be run on the `default` queue, and the [buildkite artifact
system](https://buildkite.com/docs/builds/artifacts) used to transfer build
products over to a GPU instance for testing.
## Buildkite Agent Management
### Buildkite GCP Setup
CI runs on Google Cloud Platform via two Compute Engine Instance groups:
`ci-default` and `ci-cuda`. Autoscaling is currently disabled and the number of
VM Instances in each group is manually adjusted.
#### Updating a CI Disk Image
Each Instance group has its own disk image, `ci-default-vX` and
`ci-cuda-vY`, where *X* and *Y* are incremented each time the image is changed.
The process to update a disk image is as follows (TODO: make this less manual):
1. Create a new VM Instance using the disk image to modify.
2. Once the VM boots, ssh to it and modify the disk as desired.
3. Stop the VM Instance running the modified disk. Remember the name of the VM disk
4. From another machine, `gcloud auth login`, then create a new Disk Image based
off the modified VM Instance:
```
$ gcloud compute images create ci-default-v5 --source-disk xxx --source-disk-zone us-east1-b
```
or
```
$ gcloud compute images create ci-cuda-v5 --source-disk xxx --source-disk-zone us-east1-b
```
5. Delete the new VM instance.
6. Go to the Instance templates tab, find the existing template named
`ci-default-vX` or `ci-cuda-vY` and select it. Use the "Copy" button to create
a new Instance template called `ci-default-vX+1` or `ci-cuda-vY+1` with the
newly created Disk image.
7. Go to the Instance Groups tag and find the applicable group, `ci-default` or
`ci-cuda`. Edit the Instance Group in two steps: (a) Set the number of
instances to 0 and wait for them all to terminate, (b) Update the Instance
template and restore the number of instances to the original value.
8. Clean up the previous version by deleting it from Instance Templates and
Images.
## Reference
### Buildkite AWS CloudFormation Setup
**AWS CloudFormation is currently inactive, although it may be restored in the
future**
AWS CloudFormation can be used to scale machines up and down based on the
current CI load. If no machine is currently running it can take up to 60
seconds to spin up a new instance, please remain calm during this time.
#### AMI
We use a custom AWS AMI built via https://github.com/solana-labs/elastic-ci-stack-for-aws/tree/solana/cuda.
Use the following process to update this AMI as dependencies change:
```bash
$ export AWS_ACCESS_KEY_ID=my_access_key
$ export AWS_SECRET_ACCESS_KEY=my_secret_access_key
$ git clone https://github.com/solana-labs/elastic-ci-stack-for-aws.git -b solana/cuda
$ cd elastic-ci-stack-for-aws/
$ make build
$ make build-ami
```
Watch for the *"amazon-ebs: AMI:"* log message to extract the name of the new
AMI. For example:
```
amazon-ebs: AMI: ami-07118545e8b4ce6dc
```
The new AMI should also now be visible in your EC2 Dashboard. Go to the desired
AWS CloudFormation stack, update the **ImageId** field to the new AMI id, and
*apply* the stack changes.

4
ci/buildkite-snap.yml Normal file
View File

@ -0,0 +1,4 @@
steps:
- command: "ci/snap.sh"
timeout_in_minutes: 20
name: "snap [public]"

View File

@ -1,18 +1,38 @@
steps:
- command: "ci/coverage.sh"
name: "coverage [public]"
- command: "ci/docker-run.sh rust ci/test-stable.sh"
name: "stable [public]"
- command: "ci/docker-run.sh rustlang/rust:nightly ci/test-nightly.sh || true"
name: "nightly - FAILURES IGNORED [public]"
- command: "ci/docker-run.sh rust ci/test-ignored.sh"
name: "ignored [public]"
- command: "ci/test-cuda.sh"
name: "cuda"
env:
CARGO_TARGET_CACHE_NAME: "stable"
timeout_in_minutes: 20
- command: "ci/shellcheck.sh"
name: "shellcheck [public]"
- command: "ci/test-erasure.sh"
name: "erasure"
timeout_in_minutes: 20
- command: "ci/docker-run.sh rustlang/rust:nightly ci/test-nightly.sh"
name: "nightly [public]"
env:
CARGO_TARGET_CACHE_NAME: "nightly"
timeout_in_minutes: 30
- command: "ci/test-stable-perf.sh"
name: "stable-perf [public]"
env:
CARGO_TARGET_CACHE_NAME: "stable-perf"
timeout_in_minutes: 20
retry:
automatic:
- exit_status: "*"
limit: 2
agents:
- "queue=cuda"
- command: "ci/pr-snap.sh"
timeout_in_minutes: 20
name: "snap [public]"
- wait
- command: "ci/publish.sh"
name: "publish release artifacts"
- command: "ci/publish-crate.sh"
timeout_in_minutes: 20
name: "publish crate [public]"
- command: "ci/hoover.sh"
timeout_in_minutes: 20
name: "clean agent [public]"
- trigger: "solana-snap"
branches: "!pull/*"
async: true

View File

@ -1,21 +0,0 @@
#!/bin/bash -e
cd "$(dirname "$0")/.."
ci/docker-run.sh evilmachines/rust-cargo-kcov \
bash -exc "\
export RUST_BACKTRACE=1; \
cargo build --verbose; \
cargo kcov --lib --verbose; \
"
echo Coverage report:
ls -l target/cov/index.html
if [[ -z "$CODECOV_TOKEN" ]]; then
echo CODECOV_TOKEN undefined
else
bash <(curl -s https://codecov.io/bash)
fi
exit 0

View File

@ -19,23 +19,31 @@ fi
docker pull "$IMAGE"
shift
ARGS=(--workdir /solana --volume "$PWD:/solana" --rm)
ARGS+=(--env "CARGO_HOME=/solana/.cargo")
ARGS=(
--workdir /solana
--volume "$PWD:/solana"
--volume "$HOME:/home"
--env "CARGO_HOME=/home/.cargo"
--rm
)
# kcov tries to set the personality of the binary which docker
# doesn't allow by default.
ARGS+=(--security-opt "seccomp=unconfined")
# Ensure files are created with the current host uid/gid
ARGS+=(--user "$(id -u):$(id -g)")
if [[ -z "$SOLANA_DOCKER_RUN_NOSETUID" ]]; then
ARGS+=(--user "$(id -u):$(id -g)")
fi
# Environment variables to propagate into the container
ARGS+=(
--env BUILDKITE_BRANCH
--env BUILDKITE_TAG
--env CODECOV_TOKEN
--env CRATES_IO_TOKEN
--env SNAPCRAFT_CREDENTIALS_KEY
)
set -x
docker run "${ARGS[@]}" "$IMAGE" "$@"
exec docker run "${ARGS[@]}" "$IMAGE" "$@"

View File

@ -0,0 +1,7 @@
FROM snapcraft/xenial-amd64
# Update snapcraft to latest version
RUN apt-get update -qq \
&& apt-get install -y snapcraft \
&& rm -rf /var/lib/apt/lists/* \
&& snapcraft --version

6
ci/docker-snapcraft/build.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash -ex
cd "$(dirname "$0")"
docker build -t solanalabs/snapcraft .
docker push solanalabs/snapcraft

57
ci/hoover.sh Executable file
View File

@ -0,0 +1,57 @@
#!/bin/bash
#
# Regular maintenance performed on a buildkite agent to control disk usage
#
echo --- Delete all exited containers first
(
set -x
exited=$(docker ps -aq --no-trunc --filter "status=exited")
if [[ -n "$exited" ]]; then
# shellcheck disable=SC2086 # Don't want to double quote "$exited"
docker rm $exited
fi
)
echo --- Delete untagged images
(
set -x
untagged=$(docker images | grep '<none>'| awk '{ print $3 }')
if [[ -n "$untagged" ]]; then
# shellcheck disable=SC2086 # Don't want to double quote "$untagged"
docker rmi $untagged
fi
)
echo --- Delete all dangling images
(
set -x
dangling=$(docker images --filter 'dangling=true' -q --no-trunc | sort | uniq)
if [[ -n "$dangling" ]]; then
# shellcheck disable=SC2086 # Don't want to double quote "$dangling"
docker rmi $dangling
fi
)
echo --- Remove unused docker networks
(
set -x
docker network prune -f
)
echo "--- Delete /tmp files older than 1 day owned by $(whoami)"
(
set -x
find /tmp -maxdepth 1 -user "$(whoami)" -mtime +1 -print0 | xargs -0 rm -rf
)
echo --- System Status
(
set -x
docker images
docker ps
docker network ls
df -h
)
exit 0

8
ci/is-pr.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash -e
#
# The standard BUILDKITE_PULL_REQUEST environment variable is always "false" due
# to how solana-ci-gate is used to trigger PR builds rather than using the
# standard Buildkite PR trigger.
#
[[ $BUILDKITE_BRANCH =~ pull/* ]]

18
ci/pr-snap.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash -e
#
# Only run snap.sh for pull requests that modify files under /snap
#
cd "$(dirname "$0")"
if ./is-pr.sh; then
affected_files="$(buildkite-agent meta-data get affected_files)"
echo "Affected files in this PR: $affected_files"
if [[ ! ":$affected_files:" =~ :snap/ ]]; then
echo "Skipping snap build as no files under /snap were modified"
exit 0
fi
exec ./snap.sh
else
echo "Skipping snap build as this is not a pull request"
fi

View File

@ -16,4 +16,4 @@ if [[ ! -x $BKRUN ]]; then
fi
set -x
./ci/node_modules/.bin/bkrun ci/buildkite.yml
exec ./ci/node_modules/.bin/bkrun ci/buildkite.yml

43
ci/snap.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash -e
cd "$(dirname "$0")/.."
DRYRUN=
if [[ -z $BUILDKITE_BRANCH ]] || ./ci/is-pr.sh; then
DRYRUN="echo"
fi
if [[ -z "$BUILDKITE_TAG" ]]; then
SNAP_CHANNEL=edge
else
SNAP_CHANNEL=beta
fi
if [[ -z $DRYRUN ]]; then
[[ -n $SNAPCRAFT_CREDENTIALS_KEY ]] || {
echo SNAPCRAFT_CREDENTIALS_KEY not defined
exit 1;
}
(
openssl aes-256-cbc -d \
-in ci/snapcraft.credentials.enc \
-out ci/snapcraft.credentials \
-k "$SNAPCRAFT_CREDENTIALS_KEY"
snapcraft login --with ci/snapcraft.credentials
) || {
rm -f ci/snapcraft.credentials;
exit 1
}
fi
set -x
echo --- build
snapcraft
source ci/upload_ci_artifact.sh
upload_ci_artifact solana_*.snap
echo --- publish
$DRYRUN snapcraft push solana_*.snap --release $SNAP_CHANNEL

Binary file not shown.

View File

@ -1,22 +0,0 @@
#!/bin/bash -e
cd "$(dirname "$0")/.."
LIB=libcuda_verify_ed25519.a
if [[ ! -r $LIB ]]; then
if [[ -z "${libcuda_verify_ed25519_URL:-}" ]]; then
echo "$0 skipped. Unable to locate $LIB"
exit 0
fi
export LD_LIBRARY_PATH=/usr/local/cuda/lib64
export PATH=$PATH:/usr/local/cuda/bin
curl -X GET -o $LIB "$libcuda_verify_ed25519_URL"
fi
# shellcheck disable=SC1090 # <-- shellcheck can't follow ~
source ~/.cargo/env
export RUST_BACKTRACE=1
cargo test --features=cuda
exit 0

View File

@ -1,29 +0,0 @@
#!/bin/bash -e
set -o xtrace
cd "$(dirname "$0")/.."
if [[ -z "${libgf_complete_URL:-}" ]]; then
echo libgf_complete_URL undefined
exit 1
fi
if [[ -z "${libJerasure_URL:-}" ]]; then
echo libJerasure_URL undefined
exit 1
fi
curl -X GET -o libJerasure.so "$libJerasure_URL"
curl -X GET -o libgf_complete.so "$libgf_complete_URL"
ln -s libJerasure.so libJerasure.so.2
ln -s libJerasure.so libJerasure.so.2.0.0
ln -s libgf_complete.so libgf_complete.so.1.0.0
export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH
# shellcheck disable=SC1090 # <-- shellcheck can't follow ~
source ~/.cargo/env
cargo test --features="erasure"
exit 0

View File

@ -1,9 +0,0 @@
#!/bin/bash -e
cd "$(dirname "$0")/.."
rustc --version
cargo --version
export RUST_BACKTRACE=1
cargo test -- --ignored

View File

@ -2,13 +2,33 @@
cd "$(dirname "$0")/.."
export RUST_BACKTRACE=1
rustc --version
cargo --version
export RUST_BACKTRACE=1
rustup component add rustfmt-preview
cargo build --verbose --features unstable
cargo test --verbose --features unstable
cargo bench --verbose --features unstable
_() {
echo "--- $*"
"$@"
}
_ cargo build --verbose --features unstable
_ cargo test --verbose --features unstable
_ cargo bench --verbose --features unstable
exit 0
# Coverage disabled (see issue #433)
_ cargo install --force cargo-cov
_ cargo cov test
_ cargo cov report
echo --- Coverage report:
ls -l target/cov/report/index.html
if [[ -z "$CODECOV_TOKEN" ]]; then
echo CODECOV_TOKEN undefined
else
bash <(curl -s https://codecov.io/bash) -x 'llvm-cov gcov'
fi

12
ci/test-stable-perf.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash -e
cd "$(dirname "$0")/.."
./fetch-perf-libs.sh
export LD_LIBRARY_PATH=$PWD:/usr/local/cuda/lib64
export PATH=$PATH:/usr/local/cuda/bin
export RUST_BACKTRACE=1
set -x
exec cargo test --features=cuda,erasure

View File

@ -2,13 +2,17 @@
cd "$(dirname "$0")/.."
export RUST_BACKTRACE=1
rustc --version
cargo --version
export RUST_BACKTRACE=1
rustup component add rustfmt-preview
cargo fmt -- --write-mode=diff
cargo build --verbose
cargo test --verbose
_() {
echo "--- $*"
"$@"
}
exit 0
_ rustup component add rustfmt-preview
_ cargo fmt -- --write-mode=check
_ cargo build --verbose
_ cargo test --verbose
_ cargo test -- --ignored

18
ci/upload_ci_artifact.sh Normal file
View File

@ -0,0 +1,18 @@
# |source| me
upload_ci_artifact() {
echo "--- artifact: $1"
if [[ -r "$1" ]]; then
ls -l "$1"
if ${BUILDKITE:-false}; then
(
set -x
buildkite-agent artifact upload "$1"
)
fi
else
echo ^^^ +++
echo "$1 not found"
fi
}

37
fetch-perf-libs.sh Executable file
View File

@ -0,0 +1,37 @@
#!/bin/bash -e
if [[ $(uname) != Linux ]]; then
echo Performance libraries are only available for Linux
exit 1
fi
if [[ $(uname -m) != x86_64 ]]; then
echo Performance libraries are only available for x86_64 architecture
exit 1
fi
(
set -x
curl -o solana-perf.tgz \
https://solana-perf.s3.amazonaws.com/master/x86_64-unknown-linux-gnu/solana-perf.tgz
tar zxvf solana-perf.tgz
)
if [[ -r /usr/local/cuda/version.txt && -r cuda-version.txt ]]; then
if ! diff /usr/local/cuda/version.txt cuda-version.txt > /dev/null; then
echo ==============================================
echo Warning: possible CUDA version mismatch
echo
echo "Expected version: $(cat cuda-version.txt)"
echo "Detected version: $(cat /usr/local/cuda/version.txt)"
echo ==============================================
fi
else
echo ==============================================
echo Warning: unable to validate CUDA version
echo ==============================================
fi
echo "Downloaded solana-perf version: $(cat solana-perf-HEAD.txt)"
exit 0

View File

@ -1,20 +1,23 @@
#!/bin/bash -e
#!/bin/bash
#
# usage: $0 <rsync network path to solana repo on leader machine> <number of nodes in the network>"
#
if [[ -z "$1" ]]; then
echo "usage: $0 [network path to solana repo on leader machine] [number of nodes in the network if greater then 1]"
exit 1
fi
here=$(dirname "$0")
# shellcheck source=multinode-demo/common.sh
source "$here"/common.sh
LEADER="$1"
COUNT="$2"
if [[ -z "$2" ]]; then
COUNT=1
fi
leader=${1:-${here}/..} # Default to local solana repo
count=${2:-1}
set -x
export RUST_LOG=solana=info
rsync -v -e ssh "$LEADER/leader.json" .
rsync -v -e ssh "$LEADER/mint-demo.json" .
rsync_leader_url=$(rsync_url "$leader")
cargo run --release --bin solana-client-demo -- \
-l leader.json -n $COUNT -d < mint-demo.json 2>&1 | tee client.log
set -ex
mkdir -p "$SOLANA_CONFIG_CLIENT_DIR"
$rsync -vPz "$rsync_leader_url"/config/leader.json "$SOLANA_CONFIG_CLIENT_DIR"/
$rsync -vPz "$rsync_leader_url"/config-private/mint.json "$SOLANA_CONFIG_CLIENT_DIR"/
# shellcheck disable=SC2086 # $solana_client_demo should not be quoted
exec $solana_client_demo \
-n "$count" -l "$SOLANA_CONFIG_CLIENT_DIR"/leader.json \
< "$SOLANA_CONFIG_CLIENT_DIR"/mint.json

85
multinode-demo/common.sh Normal file
View File

@ -0,0 +1,85 @@
# |source| this file
#
# Disable complaints about unused variables in this file:
# shellcheck disable=2034
rsync=rsync
if [[ -d "$SNAP" ]]; then # Running inside a Linux Snap?
solana_program() {
declare program="$1"
if [[ "$program" = wallet ]]; then
# TODO: Merge wallet.sh functionality into solana-wallet proper and
# remove this special case
printf "%s/bin/solana-%s" "$SNAP" "$program"
else
printf "%s/command-%s.wrapper" "$SNAP" "$program"
fi
}
rsync="$SNAP"/bin/rsync
SOLANA_CUDA="$(snapctl get enable-cuda)"
elif [[ -n "$USE_SNAP" ]]; then # Use the Linux Snap binaries
solana_program() {
declare program="$1"
printf "solana.%s" "$program"
}
elif [[ -n "$USE_INSTALL" ]]; then # Assume |cargo install| was run
solana_program() {
declare program="$1"
printf "solana-%s" "$program"
}
# CUDA was/wasn't selected at build time, can't affect CUDA state here
unset SOLANA_CUDA
else
solana_program() {
declare program="$1"
declare features=""
if [[ "$program" =~ ^(.*)-cuda$ ]]; then
program=${BASH_REMATCH[1]}
features="--features=cuda,erasure"
fi
if [[ -z "$DEBUG" ]]; then
maybe_release=--release
fi
printf "cargo run $maybe_release --bin solana-%s %s -- " "$program" "$features"
}
fi
solana_client_demo=$(solana_program client-demo)
solana_wallet=$(solana_program wallet)
solana_drone=$(solana_program drone)
solana_fullnode=$(solana_program fullnode)
solana_fullnode_config=$(solana_program fullnode-config)
solana_fullnode_cuda=$(solana_program fullnode-cuda)
solana_genesis=$(solana_program genesis)
solana_mint=$(solana_program mint)
export RUST_LOG=${RUST_LOG:-solana=info} # if RUST_LOG is unset, default to info
export RUST_BACKTRACE=1
tune_networking() {
[[ $(uname) = Linux ]] && (set -x; sudo sysctl -w net.core.rmem_max=26214400 1>/dev/null 2>/dev/null)
}
SOLANA_CONFIG_DIR=${SNAP_DATA:-$PWD}/config
SOLANA_CONFIG_PRIVATE_DIR=${SNAP_DATA:-$PWD}/config-private
SOLANA_CONFIG_CLIENT_DIR=${SNAP_USER_DATA:-$PWD}/config-client-client
rsync_url() { # adds the 'rsync://` prefix to URLs that need it
declare url="$1"
if [[ "$url" =~ ^.*:.*$ ]]; then
# assume remote-shell transport when colon is present, use $url unmodified
echo "$url"
return
fi
if [[ -d "$url" ]]; then
# assume local directory if $url is a valid directory, use $url unmodified
echo "$url"
return
fi
# Default to rsync:// URL
echo "rsync://$url"
}

41
multinode-demo/drone.sh Executable file
View File

@ -0,0 +1,41 @@
#!/bin/bash
#
# usage: $0 <rsync network path to solana repo on leader machine>
#
here=$(dirname "$0")
# shellcheck source=multinode-demo/common.sh
source "$here"/common.sh
SOLANA_CONFIG_DIR="$SOLANA_CONFIG_DIR"-drone
if [[ -d "$SNAP" ]]; then
# Exit if mode is not yet configured
# (typically the case after the Snap is first installed)
[[ -n "$(snapctl get mode)" ]] || exit 0
# Select leader from the Snap configuration
leader_address="$(snapctl get leader-address)"
if [[ -z "$leader_address" ]]; then
# Assume drone is running on the same node as the leader by default
leader_address="localhost"
fi
leader="$leader_address"
else
leader=${1:-${here}/..} # Default to local solana repo
fi
[[ -f "$SOLANA_CONFIG_PRIVATE_DIR"/mint.json ]] || {
echo "$SOLANA_CONFIG_PRIVATE_DIR/mint.json not found, create it by running:"
echo
echo " ${here}/setup.sh -t leader"
exit 1
}
rsync_leader_url=$(rsync_url "$leader")
set -ex
mkdir -p "$SOLANA_CONFIG_DIR"
$rsync -vPz "$rsync_leader_url"/config/leader.json "$SOLANA_CONFIG_DIR"/
# shellcheck disable=SC2086 # $solana_drone should not be quoted
exec $solana_drone \
-l "$SOLANA_CONFIG_DIR"/leader.json < "$SOLANA_CONFIG_PRIVATE_DIR"/mint.json

View File

@ -1,4 +1,33 @@
#!/bin/bash
export RUST_LOG=solana=info
sudo sysctl -w net.core.rmem_max=26214400
cargo run --release --bin solana-fullnode -- -l leader.json < genesis.log
here=$(dirname "$0")
# shellcheck source=multinode-demo/common.sh
source "$here"/common.sh
if [[ -d "$SNAP" ]]; then
# Exit if mode is not yet configured
# (typically the case after the Snap is first installed)
[[ -n "$(snapctl get mode)" ]] || exit 0
fi
[[ -f "$SOLANA_CONFIG_DIR"/leader.json ]] || {
echo "$SOLANA_CONFIG_DIR/leader.json not found, create it by running:"
echo
echo " ${here}/setup.sh -t leader"
exit 1
}
if [[ -n "$SOLANA_CUDA" ]]; then
program="$solana_fullnode_cuda"
else
program="$solana_fullnode"
fi
tune_networking
# shellcheck disable=SC2086 # $program should not be quoted
exec $program \
-l "$SOLANA_CONFIG_DIR"/leader.json \
< <(shopt -s nullglob && cat "$SOLANA_CONFIG_DIR"/genesis.log \
"$SOLANA_CONFIG_DIR"/tx-*.log) \
> "$SOLANA_CONFIG_DIR"/tx-"$(date -u +%Y%m%d%H%M%S%N)".log

59
multinode-demo/myip.sh Executable file
View File

@ -0,0 +1,59 @@
#!/bin/bash
function myip()
{
# shellcheck disable=SC2207
declare ipaddrs=(
# query interwebs
$(curl -s ifconfig.co)
# machine's interfaces
$(ifconfig |
awk '/inet addr:/ {gsub("addr:","",$2); print $2; next}
/inet6 addr:/ {gsub("/.*", "", $3); print $3; next}
/inet(6)? / {print $2}'
)
)
if (( ! ${#ipaddrs[*]} ))
then
echo "
myip: error: I'm having trouble determining what our IP address is...
Are we connected to a network?
"
return 1
fi
declare prompt="
Please choose the IP address you want to advertise to the network:
0) ${ipaddrs[0]} <====== this one was returned by the interwebs...
"
for ((i=1; i < ${#ipaddrs[*]}; i++))
do
prompt+=" $i) ${ipaddrs[i]}
"
done
while read -r -p "${prompt}
please enter a number [0 for default]: " which
do
[[ -z ${which} ]] && break;
[[ ${which} =~ [0-9]+ ]] && (( which < ${#ipaddrs[*]} )) && break;
echo "Ug. invalid entry \"${which}\"...
"
sleep 1
done
which=${which:-0}
echo "${ipaddrs[which]}"
}
if [[ ${0} == "${BASH_SOURCE[0]}" ]]
then
myip "$@"
fi

105
multinode-demo/setup.sh Executable file
View File

@ -0,0 +1,105 @@
#!/bin/bash
here=$(dirname "$0")
# shellcheck source=multinode-demo/common.sh
source "$here"/common.sh
usage () {
exitcode=0
if [[ -n "$1" ]]; then
exitcode=1
echo "Error: $*"
fi
cat <<EOF
usage: $0 [-n num_tokens] [-l] [-p] [-t node_type]
Creates a fullnode configuration
-n num_tokens - Number of tokens to create
-l - Detect network address from local machine configuration, which
may be a private IP address unaccessible on the Intenet (default)
-p - Detect public address using public Internet servers
-t node_type - Create configuration files only for this kind of node. Valid
options are validator or leader. Creates configuration files
for both by default
EOF
exit $exitcode
}
ip_address_arg=-l
num_tokens=1000000000
node_type_leader=true
node_type_validator=true
while getopts "h?n:lpt:" opt; do
case $opt in
h|\?)
usage
exit 0
;;
l)
ip_address_arg=-l
;;
p)
ip_address_arg=-p
;;
n)
num_tokens="$OPTARG"
;;
t)
node_type="$OPTARG"
case $OPTARG in
leader)
node_type_leader=true
node_type_validator=false
;;
validator)
node_type_leader=false
node_type_validator=true
;;
*)
usage "Error: unknown node type: $node_type"
;;
esac
;;
*)
usage "Error: unhandled option: $opt"
;;
esac
done
leader_address_args=("$ip_address_arg")
validator_address_args=("$ip_address_arg" -b 9000)
set -e
echo "Cleaning $SOLANA_CONFIG_DIR"
rm -rvf "$SOLANA_CONFIG_DIR"
mkdir -p "$SOLANA_CONFIG_DIR"
if $node_type_leader; then
rm -rvf "$SOLANA_CONFIG_PRIVATE_DIR"
mkdir -p "$SOLANA_CONFIG_PRIVATE_DIR"
echo "Creating $SOLANA_CONFIG_DIR/mint.json with $num_tokens tokens"
$solana_mint <<<"$num_tokens" > "$SOLANA_CONFIG_PRIVATE_DIR"/mint.json
echo "Creating $SOLANA_CONFIG_DIR/genesis.log"
$solana_genesis < "$SOLANA_CONFIG_PRIVATE_DIR"/mint.json > "$SOLANA_CONFIG_DIR"/genesis.log
echo "Creating $SOLANA_CONFIG_DIR/leader.json"
$solana_fullnode_config "${leader_address_args[@]}" > "$SOLANA_CONFIG_DIR"/leader.json
fi
if $node_type_validator; then
echo "Creating $SOLANA_CONFIG_DIR/validator.json"
$solana_fullnode_config "${validator_address_args[@]}" > "$SOLANA_CONFIG_DIR"/validator.json
fi
ls -lh "$SOLANA_CONFIG_DIR"/
if $node_type_leader; then
ls -lh "$SOLANA_CONFIG_PRIVATE_DIR"
fi

80
multinode-demo/start_nodes.sh Executable file
View File

@ -0,0 +1,80 @@
#!/bin/bash
ip_addr_file=$1
remote_user=$2
ssh_keys=$3
usage() {
echo -e "\\tUsage: $0 <IP Address array> <username> [path to ssh keys]\\n"
echo -e "\\t <IP Address array>: A bash script that exports an array of IP addresses, ip_addr_array. Elements of the array are public IP address of remote nodes."
echo -e "\\t <username>: The username for logging into remote nodes."
echo -e "\\t [path to ssh keys]: The public/private key pair that remote nodes can use to perform rsync and ssh among themselves. Must contain pub, priv and authorized_keys.\\n"
}
# Sample IP Address array file contents
# ip_addr_array=(192.168.1.1 192.168.1.5 192.168.2.2)
if [[ -z "$ip_addr_file" ]]; then
usage
exit 1
fi
if [[ -z "$remote_user" ]]; then
usage
exit 1
fi
# Build and install locally
PATH="$HOME"/.cargo/bin:"$PATH"
cargo install --force
ip_addr_array=()
# Get IP address array
# shellcheck source=/dev/null
source "$ip_addr_file"
# shellcheck disable=SC2089,SC2016
ssh_command_prefix='export PATH="$HOME/.cargo/bin:$PATH"; cd solana; USE_INSTALL=1 ./multinode-demo/'
count=0
leader=
for ip_addr in "${ip_addr_array[@]}"; do
echo "$ip_addr"
# Deploy build and scripts to remote node
rsync -r -av ~/.cargo/bin "$remote_user"@"$ip_addr":~/.cargo
rsync -r -av ./multinode-demo "$remote_user"@"$ip_addr":~/solana/
# If provided, deploy SSH keys
if [[ -z $ssh_keys ]]; then
echo "skip copying the ssh keys"
else
rsync -r -av "$ssh_keys"/* "$remote_user"@"$ip_addr":~/.ssh/
fi
# Stop current nodes
ssh "$remote_user"@"$ip_addr" 'pkill -9 solana-fullnode'
ssh "$remote_user"@"$ip_addr" 'pkill -9 solana-client-demo'
# Run setup
ssh "$remote_user"@"$ip_addr" "$ssh_command_prefix"'setup.sh -p "$ip_addr"'
if (( !count )); then
# Start the leader on the first node
echo "Starting leader node $ip_addr"
ssh -n -f "$remote_user"@"$ip_addr" "$ssh_command_prefix"'leader.sh > leader.log 2>&1'
leader=${ip_addr_array[0]}
else
# Start validator on all other nodes
echo "Starting validator node $ip_addr"
ssh -n -f "$remote_user"@"$ip_addr" "$ssh_command_prefix""validator.sh $remote_user@$leader:~/solana $leader > validator.log 2>&1"
fi
(( count++ ))
if (( count == ${#ip_addr_array[@]} )); then
# Launch client demo on the last node
echo "Starting client demo on $ip_addr"
ssh -n -f "$remote_user"@"$ip_addr" "$ssh_command_prefix""client.sh $remote_user@$leader:~/solana $count > client.log 2>&1"
fi
done

View File

@ -0,0 +1,41 @@
#!/bin/bash -e
#
# Wallet sanity test
#
here=$(dirname "$0")
cd "$here"
wallet="../wallet.sh $1"
# Tokens transferred to this address are lost forever...
garbage_address=vS3ngn1TfQmpsW1Z4NkLuqNAQFF3dYQw8UZ6TCx9bmq
check_balance_output() {
declare expected_output="$1"
exec 42>&1
output=$($wallet balance | tee >(cat - >&42))
if [[ ! "$output" =~ $expected_output ]]; then
echo "Balance is incorrect. Expected: $expected_output"
exit 1
fi
}
pay_and_confirm() {
exec 42>&1
signature=$($wallet pay "$@" | tee >(cat - >&42))
$wallet confirm "$signature"
}
$wallet reset
$wallet address
check_balance_output "Your balance is: 0"
$wallet airdrop --tokens 60
check_balance_output "Your balance is: 60"
$wallet airdrop --tokens 40
check_balance_output "Your balance is: 100"
pay_and_confirm --to $garbage_address --tokens 99
check_balance_output "Your balance is: 1"
echo PASS
exit 0

View File

@ -1,21 +1,80 @@
#!/bin/bash -e
#!/bin/bash
here=$(dirname "$0")
# shellcheck source=multinode-demo/common.sh
source "$here"/common.sh
if [[ -z "$1" ]]; then
echo "usage: $0 [network path to solana repo on leader machine]"
usage() {
if [[ -n "$1" ]]; then
echo "$*"
echo
fi
echo "usage: $0 [rsync network path to solana repo on leader machine] [network ip address of leader]"
exit 1
}
if [[ "$1" = "-h" || -n "$3" ]]; then
usage
fi
LEADER="$1"
if [[ -d "$SNAP" ]]; then
# Exit if mode is not yet configured
# (typically the case after the Snap is first installed)
[[ -n "$(snapctl get mode)" ]] || exit 0
set -x
# Select leader from the Snap configuration
leader_address="$(snapctl get leader-address)"
if [[ -z "$leader_address" ]]; then
# Assume public testnet by default
leader_address=35.230.65.68 # testnet.solana.com
fi
leader="$leader_address"
else
if [[ -n "$3" ]]; then
usage
fi
rsync -v -e ssh "$LEADER/mint-demo.json" .
rsync -v -e ssh "$LEADER/leader.json" .
rsync -v -e ssh "$LEADER/genesis.log" .
if [[ -z "$1" ]]; then
leader=${1:-${here}/..} # Default to local solana repo
leader_address=${2:-127.0.0.1} # Default to local leader
elif [[ -z "$2" ]]; then
leader="$1"
leader_address=$(dig +short "$1" | head -n1)
if [[ -z "$leader_address" ]]; then
usage "Error: unable to resolve IP address for $leader"
fi
else
leader="$1"
leader_address="$2"
fi
fi
leader_port=8001
export RUST_LOG=solana=info
if [[ -n "$SOLANA_CUDA" ]]; then
program="$solana_fullnode_cuda"
else
program="$solana_fullnode"
fi
sudo sysctl -w net.core.rmem_max=26214400
cargo run --release --bin solana-fullnode -- \
-l validator.json -v leader.json < genesis.log
[[ -f "$SOLANA_CONFIG_DIR"/validator.json ]] || {
echo "$SOLANA_CONFIG_DIR/validator.json not found, create it by running:"
echo
echo " ${here}/setup.sh -t validator"
exit 1
}
rsync_leader_url=$(rsync_url "$leader")
set -ex
SOLANA_LEADER_CONFIG_DIR="$SOLANA_CONFIG_DIR"/leader-config
rm -rf "$SOLANA_LEADER_CONFIG_DIR"
$rsync -vPrz "$rsync_leader_url"/config/ "$SOLANA_LEADER_CONFIG_DIR"
ls -lh "$SOLANA_LEADER_CONFIG_DIR"
tune_networking
# shellcheck disable=SC2086 # $program should not be quoted
exec $program \
-l "$SOLANA_CONFIG_DIR"/validator.json -t "$leader_address:$leader_port" \
< <(shopt -s nullglob && cat "$SOLANA_LEADER_CONFIG_DIR"/genesis.log \
"$SOLANA_LEADER_CONFIG_DIR"/tx-*.log)

47
multinode-demo/wallet.sh Executable file
View File

@ -0,0 +1,47 @@
#!/bin/bash
#
# usage: $0 <rsync network path to solana repo on leader machine>"
#
here=$(dirname "$0")
# shellcheck source=multinode-demo/common.sh
source "$here"/common.sh
# if $1 isn't host:path, something.com, or a valid local path
if [[ ${1%:} != "$1" || "$1" =~ [^.]\.[^.] || -d $1 ]]; then
leader=$1 # interpret
shift
else
if [[ -d "$SNAP" ]]; then
leader=testnet.solana.com # Default to testnet when running as a Snap
else
leader=$here/.. # Default to local solana repo
fi
fi
if [[ "$1" = "reset" ]]; then
echo Wallet resetting
rm -rf "$SOLANA_CONFIG_CLIENT_DIR"
exit 0
fi
rsync_leader_url=$(rsync_url "$leader")
set -e
mkdir -p "$SOLANA_CONFIG_CLIENT_DIR"
if [[ ! -r "$SOLANA_CONFIG_CLIENT_DIR"/leader.json ]]; then
(
set -x
$rsync -vPz "$rsync_leader_url"/config/leader.json "$SOLANA_CONFIG_CLIENT_DIR"/
)
fi
client_json="$SOLANA_CONFIG_CLIENT_DIR"/client.json
if [[ ! -r $client_json ]]; then
$solana_mint <<<0 > "$client_json"
fi
set -x
# shellcheck disable=SC2086 # $solana_wallet should not be quoted
exec $solana_wallet \
-l "$SOLANA_CONFIG_CLIENT_DIR"/leader.json -m "$client_json" "$@"

View File

@ -0,0 +1,190 @@
# Smart Contracts Engine
The goal of this RFC is to define a set of constraints for APIs and runtime such that we can execute our smart contracts safely on massively parallel hardware such as a GPU. Our runtime is built around an OS *syscall* primitive. The difference in blockchain is that now the OS does a cryptographic check of memory region ownership before accessing the memory in the Solana kernel.
## Version
version 0.1
## Toolchain Stack
+---------------------+ +---------------------+
| | | |
| +------------+ | | +------------+ |
| | | | | | | |
| | frontend | | | | verifier | |
| | | | | | | |
| +-----+------+ | | +-----+------+ |
| | | | | |
| | | | | |
| +-----+------+ | | +-----+------+ |
| | | | | | | |
| | llvm | | | | loader | |
| | | +------>+ | | |
| +-----+------+ | | +-----+------+ |
| | | | | |
| | | | | |
| +-----+------+ | | +-----+------+ |
| | | | | | | |
| | ELF | | | | runtime | |
| | | | | | | |
| +------------+ | | +------------+ |
| | | |
| client | | solana |
+---------------------+ +---------------------+
[Figure 1. Smart Contracts Stack]
In Figure 1 an untrusted client, creates a program in the front-end language of her choice, (like C/C++/Rust/Lua), and compiles it with LLVM to a position independent shared object ELF, targeting BPF bytecode. Solana will safely load and execute the ELF.
## Bytecode
Our bytecode is based on Berkley Packet Filter. The requirements for BPF overlap almost exactly with the requirements we have:
1. Deterministic amount of time to execute the code
2. Bytecode that is portable between machine instruction sets
3. Verified memory accesses
4. Fast to load the object, verify the bytecode and JIT to local machine instruction set
For 1, that means that loops are unrolled, and for any jumps back we can guard them with a check against the number of instruction that have been executed at this point. If the limit is reached, the program yields its execution. This involves saving the stack and current instruction index.
For 2, the BPF bytecode already easily maps to x8664, arm64 and other instruction sets. 
For 3, every load and store that is relative can be checked to be within the expected memory that is passed into the ELF. Dynamic load and stores can do a runtime check against available memory, these will be slow and should be avoided.
For 4, Fully linked PIC ELF with just a single RX segment. Effectively we are linking a shared object with `-fpic -target bpf` and with a linker script to collect everything into a single RX segment. Writable globals are not supported.
### Address Checks
The interface to the module takes a `&mut Vec<Vec<u8>>` in rust, or a `int sz, void* data[sz], int szs[sz]` in `C`. Given the module's bytecode, for each method, we need to analyze the bounds on load and stores into each buffer the module uses. This check needs to be done `on chain`, and after those bounds are computed we can verify that the user supplied array of buffers will not cause a memory fault. For load and stores that we cannot analyze, we can replace with a `safe_load` and `safe_store` instruction that will check the table for access.
## Loader
The loader is our first smart contract. The job of this contract is to load the actual program with its own instance data. The loader will verify the bytecode and that the object implements the expected entry points.
Since there is only one RX segment, the context for the contract instance is passed into each entry point as well as the event data for that entry point.
A client will create a transaction to create a new loader instance:
`Solana_NewLoader(Loader Instance PubKey, proof of key ownership, space I need for my elf)`
A client will then do a bunch of transactions to load its elf into the loader instance they created:
`Loader_UploadElf(Loader Instance PubKey, proof of key ownership, pos start, pos end, data)`
At this point the client can create a new instance of the module with its own instance address:
`Loader_NewInstance(Loader Instance PubKey, proof of key ownership, Instance PubKey, proof of key ownership)`
Once the instance has been created, the client may need to upload more user data to solana to configure this instance:
`Instance_UploadModuleData(Instance PubKey, proof of key ownership, pos start, pos end, data)`
Now clients can `start` the instance:
`Instance_Start(Instance PubKey, proof of key ownership)`
## Runtime
Our goal with the runtime is to have a general purpose execution environment that is highly parallelizable and doesn't require dynamic resource management. We want to execute as many contracts as we can in parallel, and have them pass or fail without a destructive state change.
### State and Entry Point
State is addressed by an account which is at the moment simply the PubKey. Our goal is to eliminate dynamic memory allocation in the smart contract itself, so the contract is a function that takes a mapping of [(PubKey,State)] and returns [(PubKey, State')]. The output of keys is a subset of the input. Three basic kinds of state exist:
* Instance State
* Participant State
* Caller State
There isn't any difference in how each is implemented, but conceptually Participant State is memory that is allocated for each participant in the contract. Instance State is memory that is allocated for the contract itself, and Caller State is memory that the transactions caller has allocated.
### Call
```
void call(
const struct instance_data *data,
const uint8_t kind[], //instance|participant|caller|read|write
const uint8_t *keys[],
uint8_t *data[],
int num,
uint8_t dirty[], //dirty memory bits
uint8_t *userdata, //current transaction data
);
```
To call this operation, the transaction that is destined to the contract instance specifies what keyed state it should present to the `call` function. To allocate the state memory or a call context, the client has to first call a function on the contract with the designed address that will own the state.
At its core, this is a system call that requires cryptographic proof of ownership of memory regions instead of an OS that checks page tables for access rights.
* `Instance_AllocateContext(Instance PubKey, My PubKey, Proof of key ownership)`
Any transaction can then call `call` on the contract with a set of keys. It's up to the contract itself to manage ownership:
* `Instance_Call(Instance PubKey, [Context PubKeys], proofs of ownership, userdata...)`
Contracts should be able to read any state that is part of solana, but only write to state that the contract allocated.
#### Caller State
Caller `state` is memory allocated for the `call` that belongs to the public key that is issuing the `call`. This is the caller's context.
#### Instance State
Instance `state` is memory that belongs to this contract instance. We may also need module-wide `state` as well.
#### Participant State
Participant `state` is any other memory. In some cases it may make sense to have these allocated as part of the call by the caller.
### Reduce
Some operations on the contract will require iteration over all the keys. To make this parallelizable the iteration is broken up into reduce calls which are combined.
```
void reduce_m(
const struct instance_data *data,
const uint8_t *keys[],
const uint8_t *data[],
int num,
uint8_t *reduce_data,
);
void reduce_r(
const struct instance_data *data,
const uint8_t *reduce_data[],
int num,
uint8_t *reduce_data,
);
```
### Execution
Transactions are batched and processed in parallel at each stage.
```
+-----------+ +--------------+ +-----------+ +---------------+
| sigverify |-+->| debit commit |---+->| execution |-+->| memory commit |
+-----------+ | +--------------+ | +-----------+ | +---------------+
| | |
| +---------------+ | | +--------------+
|->| memory verify |->+ +->| debit undo |
+---------------+ | +--------------+
|
| +---------------+
+->| credit commit |
+---------------+
```
The `debit verify` stage is very similar to `memory verify`. Proof of key ownership is used to check if the callers key has some state allocated with the contract, then the memory is loaded and executed. After execution stage, the dirty pages are written back by the contract. Because know all the memory accesses during execution, we can batch transactions that do not interfere with each other. We can also apply the `debit undo` and `credit commit` stages of the transaction. `debit undo` is run in case of an exception during contract execution, only transfers may be reversed, fees are commited to solana.
### GPU execution
A single contract can read and write to separate key pairs without interference. These separate calls to the same contract can execute on the same GPU thread over different memory using different SIMD lanes.
## Notes
1. There is no dynamic memory allocation.
2. Persistant Memory is allocated to a Key with ownership
3. Contracts can `call` to update key owned state
4. Contracts can `reduce` over the memory to aggregate state
5. `call` is just a *syscall* that does a cryptographic check of memory owndershp

122
rfcs/rfc-002-consensus.md Normal file
View File

@ -0,0 +1,122 @@
# Consensus
VERY WIP
The goal of this RFC is to define the consensus algorithm used in solana. This proposal covers a Proof of Stake algorithm that leverages Proof of History. PoH is a permissionless clock for blockchain that is available before consensus. This PoS approach leverages PoH to make strong assumptions about time between partitions.
## Version
version 0.1
## Message Flow
1. Transactions are ingested at the leader.
2. Leader filters for valid transactions
3. Leader executes valid transactions on its state
4. Leader packages transactions into blobs
5. Leader transmits blobs to validator nodes.
a. The set of supermajority + `M` by stake weight of nodes is rotated in round robin fashion.
6. Validators retransmit blobs to peers in their set and to further downstream nodes.
7. Validators validate the transactions and execute them on their state.
8. Validators compute the hash of the state.
9. Validators transmit votes to the leader.
a. Votes are signatures of the hash of the computed state.
10. Leader executes the votes as any other transaction and broadcasts them out to the network
11. Validators observe their votes, and all the votes from the network.
12. Validators continue voting if the supermajority of stake is observed in the vote for the same hash.
Supermajority is defined as `2/3rds + 1` vote of the PoS stakes.
## Staking
Validators `stake` some of their spendable sol into a staking account. The stakes are not spendable and can only be used for voting.
```
CreateStake(
PoH count,
PoH hash,
source public key,
amount,
destination public key,
proof of ownership of destination public key,
signature of the message with the source keypair
)
```
Creating the stake has a warmup period of TBD. Unstaking requires the node to miss a certain amount of validation votes.
## Validation Votes
```
Validate(
PoH count,
PoH hash,
stake public key,
signature of the state,
signature of the message with the stake keypair
)
```
## Validator Slashing
Validators `stake` some of their spendable sol into a staking account. The stakes are not spendable and can only be used for voting.
```
Slash(Validate(
PoH count,
PoH hash,
stake public key,
...
signature of the message with the stake keypair
))
```
When the `Slash` vote is processed, validators should lookup `PoH hash` at `PoH count` and compare it with the message. If they do not match, the stake at `stake public key` should be set to `0`.
## Leader Slashing
TBD. The goal of this is to discourage leaders from generating multiple PoH streams.
## Validation Vote Contract
The goal of this contract is to simulate economic cost of mining on a shorter branch.
1. With my signature I am certifying that I computed `state hash` at `PoH count` and `PoH hash`.
2. I will not vote on a branch that doesn't contain this message for at least `N` counts, or until `PoH count` + `N` is reached by the PoH stream.
3. I will not vote for any other branch below `PoH count`.
a. if there are other votes not present in this PoH history the validator may need to `cancel` them before creating this vote.
## Leader Seed Generation
Leader selection is decided via a random seed. The process is as follows:
1. Periodically at a specific `PoH count` select the first vote signatures that create a supermajority from the previous round.
2. append them together
3. hash the string for `N` counts via a similar process as PoH itself.
4. The resulting hash is the random seed for `M` counts, where M > N
## Leader Ranking and Rotation
Leader's transmit for a count of `T`. When `T` is reached all the validators should switch to the next ranked leader. To rank leaders, the supermajority + `M` nodes are shuffled with the using the above calculated random seed.
TBD: define a ranking for critical partitions without a node from supermajority + `M` set.
## Partition selection
Validators should select the first branch to reach finality, or the highest ranking leader.
## Examples
### Small Partition
1. Network partition M occurs for 10% of the nodes
2. The larger partition K, with 90% of the stake weight continues to operate as normal
3. M cycles through the ranks until one of them is leader.
4. M validators observe 10% of the vote pool, finality is not reached
5. M and K re-connect.
6. M validators cancel their votes on K which are below K's `PoH count`
### Leader Timeout
1. Next rank node observes a timeout.
2. Nodes receiving both PoH streams pick the higher rank node.
3. 2, causes a partition, since nodes can only vote for 1 leader.
4. Partition is resolved just like in the [Small Partition](#small-parition)

54
rfcs/rfc-003-storage.md Normal file
View File

@ -0,0 +1,54 @@
# Storage
The goal of this RFC is to define a protocol for storing a very large ledger over a p2p network that is verified by solana validators. At full capacity on a 1gbps network solana will generate 4 petabytes of data per year. To prevent the network from centralizing around full nodes that have to store the full data set this protocol proposes a way for mining nodes to provide storage capacity for pieces of the network.
# Version
version 0.1
# Background
The basic idea to Proof of Replication is encrypting a dataset with a public symmetric key using CBC encryption, then hash the encrypted dataset. The main problem with the naive approach is that a dishonest storage node can stream the encryption and delete the data as its hashed. The simple solution is to force the hash to be done on the reverse of the encryption, or perhaps with a random order. This ensures that all the data is present during the generation of the proof and it also requires the validator to have the entirety of the encrypted data present for verification of every proof of every identity. So the space required to validate is `(Number of Proofs)*(data size)`
# Optimization with PoH
Our improvement on this approach is to randomly sample the encrypted blocks faster than it takes to encrypt, and record the hash of those samples into the PoH ledger. Thus the blocks stay in the exact same order for every PoRep and verification can stream the data and verify all the proofs in a single batch. This way we can verify multiple proofs concurrently, each one on its own CUDA core. With the current generation of graphics cards our network can support up to 14k replication identities or symmetric keys. The total space required for verification is `(2 CBC blocks) * (Number of Identities)`, with core count of equal to (Number of Identities). A CBC block is expected to be 1MB in size.
# Network
Validators for PoRep are the same validators that are verifying transactions. They have some stake that they have put up as collateral that ensures that their work is honest. If you can prove that a validator verified a fake PoRep, then the validators stake can be slashed.
Replicators are specialized thin clients. They download a part of the ledger and store it, and provide PoReps of storing the ledger. For each verified PoRep replicators earn a reward of sol from the mining pool.
# Constraints
We have the following constraints:
* At most 14k replication identities can be used, because thats how many CUDA cores we can fit in a $5k box at the moment.
* Verification requires generating the CBC blocks. That requires space of 2 blocks per identity, and 1 CUDA core per identity for the same dataset. So as many identities at once should be batched with as many proofs for those identities verified concurrently for the same dataset.
# Validation and Replication Protocol
1. Network sets the replication target number, lets say 1k. 1k PoRep identities are created from signatures of a PoH hash. So they are tied to a specific PoH hash. It doesn't matter who creates them, or simply the last 1k validation signatures we saw for the ledger at that count. This maybe just the initial batch of identities, because we want to stagger identity rotation.
2. Any client can use any of these identities to create PoRep proofs. Replicator identities are the CBC encryption keys.
3. Periodically at a specific PoH count, replicator that want to create PoRep proofs sign the PoH hash at that count. That signature is the seed used to pick the block and identity to replicate. A block is 1TB of ledger.
4. Periodically at a specific PoH count, replicator submits PoRep proofs for their selected block. A signature of the PoH hash at that count is the seed used to sample the 1TB encrypted block, and hash it. This is done faster than it takes to encrypt the 1TB block with the original identity.
5. Replicators must submit some number of fake proofs, which they can prove to be fake by providing the seed for the hash result.
6. Periodically at a specific PoH count, validators sign the hash and use the signature to select the 1TB block that they need to validate. They batch all the identities and proofs and submit approval for all the verified ones.
7. After #6, replicator client submit the proofs of fake proofs.
For any random seed, we force everyone to use a signature that is derived from a PoH hash. Everyone must use the same count, so the same PoH hash is signed by every participant. The signatures are then each cryptographically tied to the keypair, which prevents a leader from grinding on the resulting value for more than 1 identity.
We need to stagger the rotation of the identity keys. Once this gets going, the next identity could be generated by hashing itself with a PoH hash, or via some other process based on the validation signatures.
Since there are many more client identities then encryption identities, we need to split the reward for multiple clients, and prevent Sybil attacks from generating many clients to acquire the same block of data. To remain BFT we want to avoid a single human entity from storing all the replications of a single chunk of the ledger.
Our solution to this is to force the clients to continue using the same identity. If the first round is used to acquire the same block for many client identities, the second round for the same client identities will force a redistribution of the signatures, and therefore PoRep identities and blocks. Thus to get a reward for storage clients need to store the first block for free and the network can reward long lived client identities more than new ones.
# Notes
* We can reduce the costs of verification of PoRep by using PoH, and actually make it feasible to verify a large number of proofs for a global dataset.
* We can eliminate grinding by forcing everyone to sign the same PoH hash and use the signatures as the seed
* The game between validators and replicators is over random blocks and random encryption identities and random data samples. The goal of randomization is to prevent colluding groups from having overlap on data or validation.
* Replicator clients fish for lazy validators by submitting fake proofs that they can prove are fake.
* Replication identities are just symmetric encryption keys, the number of them on the network is our storage replication target. Many more client identities can exist than replicator identities, so unlimited number of clients can provide proofs of the same replicator identity.
* To defend against Sybil client identities that try to store the same block we force the clients to store for multiple rounds before receiving a reward.

43
scripts/perf-plot.py Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env python
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import json
import sys
stages_to_counters = {}
stages_to_time = {}
if len(sys.argv) != 2:
print("USAGE: {} <input file>".format(sys.argv[0]))
sys.exit(1)
with open(sys.argv[1]) as fh:
for line in fh.readlines():
if "COUNTER" in line:
json_part = line[line.find("{"):]
x = json.loads(json_part)
counter = x['name']
if not (counter in stages_to_counters):
stages_to_counters[counter] = []
stages_to_time[counter] = []
stages_to_counters[counter].append(x['counts'])
stages_to_time[counter].append(x['now'])
fig, ax = plt.subplots()
for stage in stages_to_counters.keys():
plt.plot(stages_to_time[stage], stages_to_counters[stage], label=stage)
plt.xlabel('ms')
plt.ylabel('count')
plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3,
ncol=2, mode="expand", borderaxespad=0.)
plt.locator_params(axis='x', nbins=10)
plt.grid(True)
plt.savefig("perf.pdf")

17
snap/README.md Normal file
View File

@ -0,0 +1,17 @@
## Development
If you're running Ubuntu 16.04 and already have `snapcraft` installed, simply
run:
```
$ snapcraft
```
For other systems we provide a docker image that can be used for snap
development:
```
$ ./ci/docker-run.sh solanalabs/snapcraft snapcraft -d
```
## Reference
* https://docs.snapcraft.io/

34
snap/hooks/configure vendored Executable file
View File

@ -0,0 +1,34 @@
#!/bin/bash -e
echo Stopping daemons
snapctl stop --disable solana.daemon-drone
snapctl stop --disable solana.daemon-leader
snapctl stop --disable solana.daemon-validator
mode="$(snapctl get mode)"
if [[ -z "$mode" ]]; then
exit 0
fi
ip_address_arg=-p # Use public IP address (TODO: make this configurable?)
num_tokens="$(snapctl get num-tokens)"
case $mode in
leader+drone)
$SNAP/bin/setup.sh ${num_tokens:+-n $num_tokens} ${ip_address_arg} -t leader
snapctl start --enable solana.daemon-leader
snapctl start --enable solana.daemon-drone
;;
leader)
$SNAP/bin/setup.sh ${num_tokens:+-n $num_tokens} ${ip_address_arg} -t leader
snapctl start --enable solana.daemon-leader
;;
validator)
$SNAP/bin/setup.sh ${ip_address_arg} -t validator
snapctl start --enable solana.daemon-validator
;;
*)
echo "Error: Unknown mode: $mode"
exit 1
;;
esac

92
snap/snapcraft.yaml Normal file
View File

@ -0,0 +1,92 @@
name: solana
version: git
summary: Blockchain, Rebuilt for Scale
description: |
710,000 tx/s with off-the-shelf hardware and no sharding.
Scales with Moore's Law.
grade: devel
# TODO: solana-perf-fullnode does not yet run with 'strict' confinement due to the
# CUDA dependency, so use 'devmode' confinement for now
confinement: devmode
hooks:
configure:
plugs: [network]
apps:
drone:
command: solana-drone
plugs:
- network
- network-bind
fullnode:
command: solana-fullnode
plugs:
- network
- network-bind
- home
fullnode-cuda:
command: solana-fullnode-cuda
plugs:
- network
- network-bind
- home
fullnode-config:
command: solana-fullnode-config
plugs:
- network
- network-bind
genesis:
command: solana-genesis
mint:
command: solana-mint
client-demo:
command: solana-client-demo
wallet:
# TODO: Merge wallet.sh functionality into solana-wallet proper
command: wallet.sh
#command: solana-wallet
daemon-validator:
daemon: simple
command: validator.sh
daemon-leader:
daemon: simple
command: leader.sh
daemon-drone:
daemon: simple
command: drone.sh
parts:
solana:
plugin: nil
prime:
- bin
- usr/lib/libgf_complete.so.1
- usr/lib/libJerasure.so.2
override-build: |
# Build/install solana-fullnode-cuda
./fetch-perf-libs.sh
cargo install --features=cuda,erasure --root $SNAPCRAFT_PART_INSTALL --bin solana-fullnode
mv $SNAPCRAFT_PART_INSTALL/bin/solana-fullnode $SNAPCRAFT_PART_INSTALL
rm -rf $SNAPCRAFT_PART_INSTALL/bin/*
mv $SNAPCRAFT_PART_INSTALL/solana-fullnode $SNAPCRAFT_PART_INSTALL/bin/solana-fullnode-cuda
mkdir -p $SNAPCRAFT_PART_INSTALL/usr/lib/
cp -f libJerasure.so $SNAPCRAFT_PART_INSTALL/usr/lib/libJerasure.so.2
cp -f libgf_complete.so $SNAPCRAFT_PART_INSTALL/usr/lib/libgf_complete.so.1
# Build/install all other programs
cargo install --root $SNAPCRAFT_PART_INSTALL --bins
# Install multinode scripts
mkdir -p $SNAPCRAFT_PART_INSTALL/bin
cp -av multinode-demo/* $SNAPCRAFT_PART_INSTALL/bin/
# TODO: build rsync from source instead of sneaking it in from the host
# system...
set -x
mkdir -p $SNAPCRAFT_PART_INSTALL/bin
cp -av /usr/bin/rsync $SNAPCRAFT_PART_INSTALL/bin/

View File

@ -8,15 +8,18 @@ extern crate libc;
use chrono::prelude::*;
use entry::Entry;
use hash::Hash;
use itertools::Itertools;
use ledger::Block;
use mint::Mint;
use payment_plan::{Payment, PaymentPlan, Witness};
use rayon::prelude::*;
use signature::{KeyPair, PublicKey, Signature};
use std::collections::hash_map::Entry::Occupied;
use std::collections::{HashMap, HashSet, VecDeque};
use std::result;
use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::RwLock;
use std::time::Instant;
use timing::duration_as_us;
use transaction::{Instruction, Plan, Transaction};
/// The number of most recent `last_id` values that the bank will track the signatures
@ -25,7 +28,9 @@ use transaction::{Instruction, Plan, Transaction};
/// but requires clients to update its `last_id` more frequently. Raising the value
/// lengthens the time a client must wait to be certain a missing transaction will
/// not be processed by the network.
pub const MAX_ENTRY_IDS: usize = 1024 * 4;
pub const MAX_ENTRY_IDS: usize = 1024 * 16;
pub const VERIFY_BLOCK_SIZE: usize = 16;
/// Reasons a transaction might be rejected.
#[derive(Debug, PartialEq, Eq)]
@ -41,7 +46,7 @@ pub enum BankError {
/// The bank has seen `Signature` before. This can occur under normal operation
/// when a UDP packet is duplicated, as a user error from a client not updating
/// its `last_id`, or as a double-spend attack.
DuplicateSiganture(Signature),
DuplicateSignature(Signature),
/// The bank has not seen the given `last_id` or the transaction is too old and
/// the `last_id` has been discarded.
@ -50,6 +55,9 @@ pub enum BankError {
/// The transaction is invalid and has requested a debit or credit of negative
/// tokens.
NegativeTokens,
/// Proof of History verification failed.
LedgerVerificationFailed,
}
pub type Result<T> = result::Result<T, BankError>;
@ -57,7 +65,7 @@ pub type Result<T> = result::Result<T, BankError>;
/// The state of all accounts and contracts after processing its entries.
pub struct Bank {
/// A map of account public keys to the balance in that account.
balances: RwLock<HashMap<PublicKey, AtomicIsize>>,
balances: RwLock<HashMap<PublicKey, i64>>,
/// A map of smart contract transaction signatures to what remains of its payment
/// plan. Each transaction that targets the plan should cause it to be reduced.
@ -65,10 +73,13 @@ pub struct Bank {
pending: RwLock<HashMap<Signature, Plan>>,
/// A FIFO queue of `last_id` items, where each item is a set of signatures
/// that have been processed using that `last_id`. The bank uses this data to
/// reject transactions with signatures its seen before as well as `last_id`
/// values that are so old that its `last_id` has been pulled out of the queue.
last_ids: RwLock<VecDeque<(Hash, RwLock<HashSet<Signature>>)>>,
/// that have been processed using that `last_id`. Rejected `last_id`
/// values are so old that the `last_id` has been pulled out of the queue.
last_ids: RwLock<VecDeque<Hash>>,
// Mapping of hashes to signature sets. The bank uses this data to
/// reject transactions with signatures its seen before
last_ids_sigs: RwLock<HashMap<Hash, HashSet<Signature>>>,
/// The set of trusted timekeepers. A Timestamp transaction from a `PublicKey`
/// outside this set will be discarded. Note that if validators do not have the
@ -85,17 +96,24 @@ pub struct Bank {
transaction_count: AtomicUsize,
}
impl Bank {
/// Create an Bank using a deposit.
pub fn new_from_deposit(deposit: &Payment) -> Self {
let bank = Bank {
impl Default for Bank {
fn default() -> Self {
Bank {
balances: RwLock::new(HashMap::new()),
pending: RwLock::new(HashMap::new()),
last_ids: RwLock::new(VecDeque::new()),
last_ids_sigs: RwLock::new(HashMap::new()),
time_sources: RwLock::new(HashSet::new()),
last_time: RwLock::new(Utc.timestamp(0, 0)),
transaction_count: AtomicUsize::new(0),
};
}
}
}
impl Bank {
/// Create an Bank using a deposit.
pub fn new_from_deposit(deposit: &Payment) -> Self {
let bank = Self::default();
bank.apply_payment(deposit);
bank
}
@ -113,79 +131,56 @@ impl Bank {
/// Commit funds to the `payment.to` party.
fn apply_payment(&self, payment: &Payment) {
// First we check balances with a read lock to maximize potential parallelization.
if self.balances
.read()
.expect("'balances' read lock in apply_payment")
.contains_key(&payment.to)
{
let bals = self.balances.read().expect("'balances' read lock");
bals[&payment.to].fetch_add(payment.tokens as isize, Ordering::Relaxed);
let mut balances = self.balances.write().unwrap();
if balances.contains_key(&payment.to) {
*balances.get_mut(&payment.to).unwrap() += payment.tokens;
} else {
// Now we know the key wasn't present a nanosecond ago, but it might be there
// by the time we aquire a write lock, so we'll have to check again.
let mut bals = self.balances.write().expect("'balances' write lock");
if bals.contains_key(&payment.to) {
bals[&payment.to].fetch_add(payment.tokens as isize, Ordering::Relaxed);
} else {
bals.insert(payment.to, AtomicIsize::new(payment.tokens as isize));
}
balances.insert(payment.to, payment.tokens);
}
}
/// Return the last entry ID registered.
pub fn last_id(&self) -> Hash {
let last_ids = self.last_ids.read().expect("'last_ids' read lock");
let last_item = last_ids.iter().last().expect("empty 'last_ids' list");
last_item.0
let last_item = last_ids
.iter()
.last()
.expect("get last item from 'last_ids' list");
*last_item
}
/// Store the given signature. The bank will reject any transaction with the same signature.
fn reserve_signature(signatures: &RwLock<HashSet<Signature>>, sig: &Signature) -> Result<()> {
if signatures
.read()
.expect("'signatures' read lock")
.contains(sig)
{
return Err(BankError::DuplicateSiganture(*sig));
fn reserve_signature(signatures: &mut HashSet<Signature>, sig: &Signature) -> Result<()> {
if let Some(sig) = signatures.get(sig) {
return Err(BankError::DuplicateSignature(*sig));
}
signatures
.write()
.expect("'signatures' write lock")
.insert(*sig);
signatures.insert(*sig);
Ok(())
}
/// Forget the given `signature` because its transaction was rejected.
fn forget_signature(signatures: &RwLock<HashSet<Signature>>, signature: &Signature) {
signatures
.write()
.expect("'signatures' write lock in forget_signature")
.remove(signature);
fn forget_signature(signatures: &mut HashSet<Signature>, signature: &Signature) {
signatures.remove(signature);
}
/// Forget the given `signature` with `last_id` because the transaction was rejected.
fn forget_signature_with_last_id(&self, signature: &Signature, last_id: &Hash) {
if let Some(entry) = self.last_ids
.read()
if let Some(entry) = self.last_ids_sigs
.write()
.expect("'last_ids' read lock in forget_signature_with_last_id")
.iter()
.rev()
.find(|x| x.0 == *last_id)
.get_mut(last_id)
{
Self::forget_signature(&entry.1, signature);
Self::forget_signature(entry, signature);
}
}
fn reserve_signature_with_last_id(&self, signature: &Signature, last_id: &Hash) -> Result<()> {
if let Some(entry) = self.last_ids
.read()
if let Some(entry) = self.last_ids_sigs
.write()
.expect("'last_ids' read lock in reserve_signature_with_last_id")
.iter()
.rev()
.find(|x| x.0 == *last_id)
.get_mut(last_id)
{
return Self::reserve_signature(&entry.1, signature);
return Self::reserve_signature(entry, signature);
}
Err(BankError::LastIdNotFound(*last_id))
}
@ -198,60 +193,52 @@ impl Bank {
let mut last_ids = self.last_ids
.write()
.expect("'last_ids' write lock in register_entry_id");
let mut last_ids_sigs = self.last_ids_sigs
.write()
.expect("last_ids_sigs write lock");
if last_ids.len() >= MAX_ENTRY_IDS {
last_ids.pop_front();
let id = last_ids.pop_front().unwrap();
last_ids_sigs.remove(&id);
}
last_ids.push_back((*last_id, RwLock::new(HashSet::new())));
last_ids_sigs.insert(*last_id, HashSet::new());
last_ids.push_back(*last_id);
}
/// Deduct tokens from the 'from' address the account has sufficient
/// funds and isn't a duplicate.
fn apply_debits(&self, tx: &Transaction) -> Result<()> {
if let Instruction::NewContract(contract) = &tx.instruction {
trace!("Transaction {}", contract.tokens);
if contract.tokens < 0 {
return Err(BankError::NegativeTokens);
let mut bals = self.balances.write().unwrap();
let mut purge = false;
{
let option = bals.get_mut(&tx.from);
if option.is_none() {
return Err(BankError::AccountNotFound(tx.from));
}
}
let bals = self.balances
.read()
.expect("'balances' read lock in apply_debits");
let option = bals.get(&tx.from);
let bal = option.unwrap();
if option.is_none() {
return Err(BankError::AccountNotFound(tx.from));
}
self.reserve_signature_with_last_id(&tx.sig, &tx.last_id)?;
self.reserve_signature_with_last_id(&tx.sig, &tx.last_id)?;
if let Instruction::NewContract(contract) = &tx.instruction {
if contract.tokens < 0 {
return Err(BankError::NegativeTokens);
}
loop {
let result = if let Instruction::NewContract(contract) = &tx.instruction {
let bal = option.expect("assignment of option to bal");
let current = bal.load(Ordering::Relaxed) as i64;
if current < contract.tokens {
if *bal < contract.tokens {
self.forget_signature_with_last_id(&tx.sig, &tx.last_id);
return Err(BankError::InsufficientFunds(tx.from));
} else if *bal == contract.tokens {
purge = true;
} else {
*bal -= contract.tokens;
}
bal.compare_exchange(
current as isize,
(current - contract.tokens) as isize,
Ordering::Relaxed,
Ordering::Relaxed,
)
} else {
Ok(0)
};
match result {
Ok(_) => {
self.transaction_count.fetch_add(1, Ordering::Relaxed);
return Ok(());
}
Err(_) => continue,
};
}
if purge {
bals.remove(&tx.from);
}
Ok(())
}
/// Apply only a transaction's credits. Credits from multiple transactions
@ -287,40 +274,126 @@ impl Bank {
fn process_transaction(&self, tx: &Transaction) -> Result<()> {
self.apply_debits(tx)?;
self.apply_credits(tx);
self.transaction_count.fetch_add(1, Ordering::Relaxed);
Ok(())
}
/// Process a batch of transactions. It runs all debits first to filter out any
/// transactions that can't be processed in parallel deterministically.
/// Process a batch of transactions.
#[must_use]
pub fn process_transactions(&self, txs: Vec<Transaction>) -> Vec<Result<Transaction>> {
info!("processing Transactions {}", txs.len());
let results: Vec<_> = txs.into_par_iter()
debug!("processing Transactions {}", txs.len());
let txs_len = txs.len();
let now = Instant::now();
let results: Vec<_> = txs.into_iter()
.map(|tx| self.apply_debits(&tx).map(|_| tx))
.collect(); // Calling collect() here forces all debits to complete before moving on.
results
.into_par_iter()
let debits = now.elapsed();
let now = Instant::now();
let res: Vec<_> = results
.into_iter()
.map(|result| {
result.map(|tx| {
self.apply_credits(&tx);
tx
})
})
.collect()
.collect();
debug!(
"debits: {} us credits: {:?} us tx: {}",
duration_as_us(&debits),
duration_as_us(&now.elapsed()),
txs_len
);
let mut tx_count = 0;
for r in &res {
if r.is_ok() {
tx_count += 1;
} else {
info!("tx error: {:?}", r);
}
}
self.transaction_count
.fetch_add(tx_count, Ordering::Relaxed);
res
}
/// Process an ordered list of entries.
pub fn process_entries<I>(&self, entries: I) -> Result<()>
pub fn process_entries<I>(&self, entries: I) -> Result<u64>
where
I: IntoIterator<Item = Entry>,
{
let mut entry_count = 0;
for entry in entries {
for result in self.process_transactions(entry.transactions) {
result?;
entry_count += 1;
if !entry.transactions.is_empty() {
for result in self.process_transactions(entry.transactions) {
result?;
}
}
// TODO: verify this is ok in cases like:
// 1. an untrusted genesis or tx-<DATE>.log
// 2. a crazy leader..
if !entry.has_more {
self.register_entry_id(&entry.id);
}
self.register_entry_id(&entry.id);
}
Ok(())
Ok(entry_count)
}
/// Append entry blocks to the ledger, verifying them along the way.
pub fn process_blocks<I>(&self, entries: I) -> Result<u64>
where
I: IntoIterator<Item = Entry>,
{
// Ledger verification needs to be parallelized, but we can't pull the whole
// thing into memory. We therefore chunk it.
let mut entry_count = 0;
for block in &entries.into_iter().chunks(VERIFY_BLOCK_SIZE) {
let block: Vec<_> = block.collect();
if !block.verify(&self.last_id()) {
return Err(BankError::LedgerVerificationFailed);
}
entry_count += self.process_entries(block)?;
}
Ok(entry_count)
}
/// Process a full ledger.
pub fn process_ledger<I>(&self, entries: I) -> Result<u64>
where
I: IntoIterator<Item = Entry>,
{
let mut entries = entries.into_iter();
// The first item in the ledger is required to be an entry with zero num_hashes,
// which implies its id can be used as the ledger's seed.
let entry0 = entries.next().expect("invalid ledger: empty");
// The second item in the ledger is a special transaction where the to and from
// fields are the same. That entry should be treated as a deposit, not a
// transfer to oneself.
let entry1 = entries
.next()
.expect("invalid ledger: need at least 2 entries");
let tx = &entry1.transactions[0];
let deposit = if let Instruction::NewContract(contract) = &tx.instruction {
contract.plan.final_payment()
} else {
None
}.expect("invalid ledger, needs to start with a contract");
self.apply_payment(&deposit);
self.register_entry_id(&entry0.id);
self.register_entry_id(&entry1.id);
let mut entry_count = 2;
entry_count += self.process_blocks(entries)?;
Ok(entry_count)
}
/// Process a Witness Signature. Any payment plans waiting on this signature
@ -424,24 +497,40 @@ impl Bank {
self.process_transaction(&tx).map(|_| sig)
}
pub fn get_balance(&self, pubkey: &PublicKey) -> Option<i64> {
pub fn get_balance(&self, pubkey: &PublicKey) -> i64 {
let bals = self.balances
.read()
.expect("'balances' read lock in get_balance");
bals.get(pubkey).map(|x| x.load(Ordering::Relaxed) as i64)
bals.get(pubkey).map(|x| *x).unwrap_or(0)
}
pub fn transaction_count(&self) -> usize {
self.transaction_count.load(Ordering::Relaxed)
}
pub fn has_signature(&self, signature: &Signature) -> bool {
let last_ids_sigs = self.last_ids_sigs
.read()
.expect("'last_ids_sigs' read lock");
for (_hash, signatures) in last_ids_sigs.iter() {
if signatures.contains(signature) {
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use bincode::serialize;
use entry::next_entry;
use entry_writer::{self, EntryWriter};
use hash::hash;
use ledger::next_entries;
use signature::KeyPairUtil;
use std::io::{BufRead, BufReader, Cursor, Seek, SeekFrom};
#[test]
fn test_two_payments_to_one_party() {
@ -452,11 +541,11 @@ mod tests {
bank.transfer(1_000, &mint.keypair(), pubkey, mint.last_id())
.unwrap();
assert_eq!(bank.get_balance(&pubkey).unwrap(), 1_000);
assert_eq!(bank.get_balance(&pubkey), 1_000);
bank.transfer(500, &mint.keypair(), pubkey, mint.last_id())
.unwrap();
assert_eq!(bank.get_balance(&pubkey).unwrap(), 1_500);
assert_eq!(bank.get_balance(&pubkey), 1_500);
assert_eq!(bank.transaction_count(), 2);
}
@ -499,8 +588,8 @@ mod tests {
assert_eq!(bank.transaction_count(), 1);
let mint_pubkey = mint.keypair().pubkey();
assert_eq!(bank.get_balance(&mint_pubkey).unwrap(), 10_000);
assert_eq!(bank.get_balance(&pubkey).unwrap(), 1_000);
assert_eq!(bank.get_balance(&mint_pubkey), 10_000);
assert_eq!(bank.get_balance(&pubkey), 1_000);
}
#[test]
@ -510,7 +599,7 @@ mod tests {
let pubkey = KeyPair::new().pubkey();
bank.transfer(500, &mint.keypair(), pubkey, mint.last_id())
.unwrap();
assert_eq!(bank.get_balance(&pubkey).unwrap(), 500);
assert_eq!(bank.get_balance(&pubkey), 500);
}
#[test]
@ -523,26 +612,26 @@ mod tests {
.unwrap();
// Mint's balance will be zero because all funds are locked up.
assert_eq!(bank.get_balance(&mint.pubkey()), Some(0));
assert_eq!(bank.get_balance(&mint.pubkey()), 0);
// tx count is 1, because debits were applied.
assert_eq!(bank.transaction_count(), 1);
// pubkey's balance will be None because the funds have not been
// sent.
assert_eq!(bank.get_balance(&pubkey), None);
assert_eq!(bank.get_balance(&pubkey), 0);
// Now, acknowledge the time in the condition occurred and
// that pubkey's funds are now available.
bank.apply_timestamp(mint.pubkey(), dt).unwrap();
assert_eq!(bank.get_balance(&pubkey), Some(1));
assert_eq!(bank.get_balance(&pubkey), 1);
// tx count is still 1, because we chose not to count timestamp transactions
// tx count.
assert_eq!(bank.transaction_count(), 1);
bank.apply_timestamp(mint.pubkey(), dt).unwrap(); // <-- Attack! Attempt to process completed transaction.
assert_ne!(bank.get_balance(&pubkey), Some(2));
assert_ne!(bank.get_balance(&pubkey), 2);
}
#[test]
@ -557,8 +646,8 @@ mod tests {
bank.transfer_on_date(1, &mint.keypair(), pubkey, dt, mint.last_id())
.unwrap();
assert_eq!(bank.get_balance(&mint.pubkey()), Some(0));
assert_eq!(bank.get_balance(&pubkey), Some(1));
assert_eq!(bank.get_balance(&mint.pubkey()), 0);
assert_eq!(bank.get_balance(&pubkey), 1);
}
#[test]
@ -574,22 +663,22 @@ mod tests {
assert_eq!(bank.transaction_count(), 1);
// Mint's balance will be zero because all funds are locked up.
assert_eq!(bank.get_balance(&mint.pubkey()), Some(0));
assert_eq!(bank.get_balance(&mint.pubkey()), 0);
// pubkey's balance will be None because the funds have not been
// sent.
assert_eq!(bank.get_balance(&pubkey), None);
assert_eq!(bank.get_balance(&pubkey), 0);
// Now, cancel the trancaction. Mint gets her funds back, pubkey never sees them.
bank.apply_signature(mint.pubkey(), sig).unwrap();
assert_eq!(bank.get_balance(&mint.pubkey()), Some(1));
assert_eq!(bank.get_balance(&pubkey), None);
assert_eq!(bank.get_balance(&mint.pubkey()), 1);
assert_eq!(bank.get_balance(&pubkey), 0);
// Assert cancel doesn't cause count to go backward.
assert_eq!(bank.transaction_count(), 1);
bank.apply_signature(mint.pubkey(), sig).unwrap(); // <-- Attack! Attempt to cancel completed transaction.
assert_ne!(bank.get_balance(&mint.pubkey()), Some(2));
assert_ne!(bank.get_balance(&mint.pubkey()), 2);
}
#[test]
@ -603,7 +692,7 @@ mod tests {
);
assert_eq!(
bank.reserve_signature_with_last_id(&sig, &mint.last_id()),
Err(BankError::DuplicateSiganture(sig))
Err(BankError::DuplicateSignature(sig))
);
}
@ -621,6 +710,16 @@ mod tests {
);
}
#[test]
fn test_has_signature() {
let mint = Mint::new(1);
let bank = Bank::new(&mint);
let sig = Signature::default();
bank.reserve_signature_with_last_id(&sig, &mint.last_id())
.expect("reserve signature");
assert!(bank.has_signature(&sig));
}
#[test]
fn test_reject_old_last_id() {
let mint = Mint::new(1);
@ -651,6 +750,88 @@ mod tests {
// Assert bad transactions aren't counted.
assert_eq!(bank.transaction_count(), 1);
}
#[test]
fn test_process_empty_entry_is_registered() {
let mint = Mint::new(1);
let bank = Bank::new(&mint);
let keypair = KeyPair::new();
let entry = next_entry(&mint.last_id(), 1, vec![]);
let tx = Transaction::new(&mint.keypair(), keypair.pubkey(), 1, entry.id);
// First, ensure the TX is rejected because of the unregistered last ID
assert_eq!(
bank.process_transaction(&tx),
Err(BankError::LastIdNotFound(entry.id))
);
// Now ensure the TX is accepted despite pointing to the ID of an empty entry.
bank.process_entries(vec![entry]).unwrap();
assert!(bank.process_transaction(&tx).is_ok());
}
#[test]
fn test_process_genesis() {
let mint = Mint::new(1);
let genesis = mint.create_entries();
let bank = Bank::default();
bank.process_ledger(genesis).unwrap();
assert_eq!(bank.get_balance(&mint.pubkey()), 1);
}
fn create_sample_block(mint: &Mint) -> impl Iterator<Item = Entry> {
let keypair = KeyPair::new();
let tx = Transaction::new(&mint.keypair(), keypair.pubkey(), 1, mint.last_id());
next_entries(&mint.last_id(), 0, vec![tx]).into_iter()
}
fn create_sample_ledger() -> (impl Iterator<Item = Entry>, PublicKey) {
let mint = Mint::new(2);
let genesis = mint.create_entries();
let block = create_sample_block(&mint);
(genesis.into_iter().chain(block), mint.pubkey())
}
#[test]
fn test_process_ledger() {
let (ledger, pubkey) = create_sample_ledger();
let bank = Bank::default();
bank.process_ledger(ledger).unwrap();
assert_eq!(bank.get_balance(&pubkey), 1);
}
// Write the given entries to a file and then return a file iterator to them.
fn to_file_iter(entries: impl Iterator<Item = Entry>) -> impl Iterator<Item = Entry> {
let mut file = Cursor::new(vec![]);
EntryWriter::write_entries(&mut file, entries).unwrap();
file.seek(SeekFrom::Start(0)).unwrap();
let reader = BufReader::new(file);
reader
.lines()
.map(|line| entry_writer::read_entry(line.unwrap()).unwrap())
}
#[test]
fn test_process_ledger_from_file() {
let (ledger, pubkey) = create_sample_ledger();
let ledger = to_file_iter(ledger);
let bank = Bank::default();
bank.process_ledger(ledger).unwrap();
assert_eq!(bank.get_balance(&pubkey), 1);
}
#[test]
fn test_process_ledger_from_files() {
let mint = Mint::new(2);
let genesis = to_file_iter(mint.create_entries().into_iter());
let block = to_file_iter(create_sample_block(&mint));
let bank = Bank::default();
bank.process_ledger(genesis.chain(block)).unwrap();
assert_eq!(bank.get_balance(&mint.pubkey()), 1);
}
}
#[cfg(all(feature = "unstable", test))]
@ -660,6 +841,7 @@ mod bench {
use bank::*;
use bincode::serialize;
use hash::hash;
use rayon::prelude::*;
use signature::KeyPairUtil;
#[bench]
@ -689,8 +871,8 @@ mod bench {
.collect();
bencher.iter(|| {
// Since benchmarker runs this multiple times, we need to clear the signatures.
for sigs in bank.last_ids.read().unwrap().iter() {
sigs.1.write().unwrap().clear();
for (_, sigs) in bank.last_ids_sigs.write().unwrap().iter_mut() {
sigs.clear();
}
assert!(

View File

@ -5,8 +5,7 @@
use bank::Bank;
use bincode::deserialize;
use counter::Counter;
use packet;
use packet::SharedPackets;
use packet::{PacketRecycler, Packets, SharedPackets};
use rayon::prelude::*;
use record_stage::Signal;
use result::Result;
@ -24,9 +23,6 @@ use transaction::Transaction;
pub struct BankingStage {
/// Handle to the stage's thread.
pub thread_hdl: JoinHandle<()>,
/// Output receiver for the following stage.
pub signal_receiver: Receiver<Signal>,
}
impl BankingStage {
@ -38,8 +34,8 @@ impl BankingStage {
bank: Arc<Bank>,
exit: Arc<AtomicBool>,
verified_receiver: Receiver<Vec<(SharedPackets, Vec<u8>)>>,
packet_recycler: packet::PacketRecycler,
) -> Self {
packet_recycler: PacketRecycler,
) -> (Self, Receiver<Signal>) {
let (signal_sender, signal_receiver) = channel();
let thread_hdl = Builder::new()
.name("solana-banking-stage".to_string())
@ -57,15 +53,12 @@ impl BankingStage {
}
})
.unwrap();
BankingStage {
thread_hdl,
signal_receiver,
}
(BankingStage { thread_hdl }, signal_receiver)
}
/// Convert the transactions from a blob of binary data to a vector of transactions and
/// an unused `SocketAddr` that could be used to send a response.
fn deserialize_transactions(p: &packet::Packets) -> Vec<Option<(Transaction, SocketAddr)>> {
fn deserialize_transactions(p: &Packets) -> Vec<Option<(Transaction, SocketAddr)>> {
p.packets
.par_iter()
.map(|x| {
@ -82,7 +75,7 @@ impl BankingStage {
bank: Arc<Bank>,
verified_receiver: &Receiver<Vec<(SharedPackets, Vec<u8>)>>,
signal_sender: &Sender<Signal>,
packet_recycler: &packet::PacketRecycler,
packet_recycler: &PacketRecycler,
) -> Result<()> {
let timer = Duration::new(1, 0);
let recv_start = Instant::now();
@ -271,51 +264,165 @@ mod bench {
use self::test::Bencher;
use bank::*;
use banking_stage::BankingStage;
use logger;
use mint::Mint;
use packet::{to_packets, PacketRecycler};
use packet::{to_packets_chunked, PacketRecycler};
use rayon::prelude::*;
use record_stage::Signal;
use signature::{KeyPair, KeyPairUtil};
use std::iter;
use std::sync::mpsc::channel;
use std::sync::mpsc::{channel, Receiver};
use std::sync::Arc;
use transaction::Transaction;
fn check_txs(batches: usize, receiver: &Receiver<Signal>, ref_tx_count: usize) {
let mut total = 0;
for _ in 0..batches {
let signal = receiver.recv().unwrap();
if let Signal::Transactions(transactions) = signal {
total += transactions.len();
} else {
assert!(false);
}
}
assert_eq!(total, ref_tx_count);
}
#[bench]
fn bench_stage(bencher: &mut Bencher) {
let tx = 100_usize;
let mint = Mint::new(1_000_000_000);
let pubkey = KeyPair::new().pubkey();
fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
logger::setup();
let tx = 10_000_usize;
let mint_total = 1_000_000_000_000;
let mint = Mint::new(mint_total);
let num_dst_accounts = 8 * 1024;
let num_src_accounts = 8 * 1024;
let srckeys: Vec<_> = (0..num_src_accounts).map(|_| KeyPair::new()).collect();
let dstkeys: Vec<_> = (0..num_dst_accounts)
.map(|_| KeyPair::new().pubkey())
.collect();
info!("created keys src: {} dst: {}", srckeys.len(), dstkeys.len());
let transactions: Vec<_> = (0..tx)
.map(|i| Transaction::new(&mint.keypair(), pubkey, i as i64, mint.last_id()))
.map(|i| {
Transaction::new(
&srckeys[i % num_src_accounts],
dstkeys[i % num_dst_accounts],
i as i64,
mint.last_id(),
)
})
.collect();
info!("created transactions");
let (verified_sender, verified_receiver) = channel();
let (signal_sender, signal_receiver) = channel();
let packet_recycler = PacketRecycler::default();
let verified: Vec<_> = to_packets(&packet_recycler, transactions)
.into_iter()
.map(|x| {
let len = (*x).read().unwrap().packets.len();
(x, iter::repeat(1).take(len).collect())
let setup_transactions: Vec<_> = (0..num_src_accounts)
.map(|i| {
Transaction::new(
&mint.keypair(),
srckeys[i].pubkey(),
mint_total / num_src_accounts as i64,
mint.last_id(),
)
})
.collect();
bencher.iter(move || {
let bank = Arc::new(Bank::new(&mint));
verified_sender.send(verified.clone()).unwrap();
let verified_setup: Vec<_> =
to_packets_chunked(&packet_recycler, setup_transactions.clone(), tx)
.into_iter()
.map(|x| {
let len = (*x).read().unwrap().packets.len();
(x, iter::repeat(1).take(len).collect())
})
.collect();
let verified_setup_len = verified_setup.len();
verified_sender.send(verified_setup).unwrap();
BankingStage::process_packets(
bank.clone(),
&verified_receiver,
&signal_sender,
&packet_recycler,
).unwrap();
let signal = signal_receiver.recv().unwrap();
if let Signal::Transactions(transactions) = signal {
assert_eq!(transactions.len(), tx);
} else {
assert!(false);
}
check_txs(verified_setup_len, &signal_receiver, num_src_accounts);
let verified: Vec<_> = to_packets_chunked(&packet_recycler, transactions.clone(), 192)
.into_iter()
.map(|x| {
let len = (*x).read().unwrap().packets.len();
(x, iter::repeat(1).take(len).collect())
})
.collect();
let verified_len = verified.len();
verified_sender.send(verified).unwrap();
BankingStage::process_packets(
bank.clone(),
&verified_receiver,
&signal_sender,
&packet_recycler,
).unwrap();
check_txs(verified_len, &signal_receiver, tx);
});
}
#[bench]
fn bench_banking_stage_single_from(bencher: &mut Bencher) {
logger::setup();
let tx = 10_000_usize;
let mint = Mint::new(1_000_000_000_000);
let mut pubkeys = Vec::new();
let num_keys = 8;
for _ in 0..num_keys {
pubkeys.push(KeyPair::new().pubkey());
}
let transactions: Vec<_> = (0..tx)
.into_par_iter()
.map(|i| {
Transaction::new(
&mint.keypair(),
pubkeys[i % num_keys],
i as i64,
mint.last_id(),
)
})
.collect();
let (verified_sender, verified_receiver) = channel();
let (signal_sender, signal_receiver) = channel();
let packet_recycler = PacketRecycler::default();
bencher.iter(move || {
let bank = Arc::new(Bank::new(&mint));
let verified: Vec<_> = to_packets_chunked(&packet_recycler, transactions.clone(), tx)
.into_iter()
.map(|x| {
let len = (*x).read().unwrap().packets.len();
(x, iter::repeat(1).take(len).collect())
})
.collect();
let verified_len = verified.len();
verified_sender.send(verified).unwrap();
BankingStage::process_packets(
bank.clone(),
&verified_receiver,
&signal_sender,
&packet_recycler,
).unwrap();
check_txs(verified_len, &signal_receiver, tx);
});
}
}

View File

@ -1,21 +1,22 @@
extern crate atty;
extern crate env_logger;
extern crate getopts;
extern crate isatty;
extern crate pnet;
extern crate rayon;
extern crate serde_json;
extern crate solana;
use atty::{is, Stream};
use getopts::Options;
use isatty::stdin_isatty;
use pnet::datalink;
use rayon::prelude::*;
use solana::crdt::{Crdt, ReplicatedData};
use solana::mint::MintDemo;
use solana::hash::Hash;
use solana::mint::Mint;
use solana::nat::udp_public_bind;
use solana::ncp::Ncp;
use solana::signature::{GenKeys, KeyPair, KeyPairUtil};
use solana::streamer::default_window;
use solana::thin_client::ThinClient;
use solana::timing::{duration_as_ms, duration_as_s};
use solana::transaction::Transaction;
use std::env;
use std::fs::File;
@ -25,6 +26,7 @@ use std::process::exit;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::thread::sleep;
use std::thread::Builder;
use std::thread::JoinHandle;
use std::time::Duration;
use std::time::Instant;
@ -38,27 +40,122 @@ fn print_usage(program: &str, opts: Options) {
print!("{}", opts.usage(&brief));
}
fn get_ip_addr() -> Option<IpAddr> {
for iface in datalink::interfaces() {
for p in iface.ips {
if !p.ip().is_loopback() && !p.ip().is_multicast() {
return Some(p.ip());
}
fn sample_tx_count(
exit: Arc<AtomicBool>,
maxes: Arc<RwLock<Vec<(f64, u64)>>>,
first_count: u64,
v: ReplicatedData,
sample_period: u64,
) {
let mut client = mk_client(&v);
let mut now = Instant::now();
let mut initial_tx_count = client.transaction_count();
let mut max_tps = 0.0;
let mut total;
loop {
let tx_count = client.transaction_count();
let duration = now.elapsed();
now = Instant::now();
let sample = tx_count - initial_tx_count;
initial_tx_count = tx_count;
println!("{}: Transactions processed {}", v.transactions_addr, sample);
let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos());
let tps = (sample * 1_000_000_000) as f64 / ns as f64;
if tps > max_tps {
max_tps = tps;
}
println!("{}: {:.2} tps", v.transactions_addr, tps);
total = tx_count - first_count;
println!(
"{}: Total Transactions processed {}",
v.transactions_addr, total
);
sleep(Duration::new(sample_period, 0));
if exit.load(Ordering::Relaxed) {
println!("exiting validator thread");
maxes.write().unwrap().push((max_tps, total));
break;
}
}
None
}
fn generate_and_send_txs(
client: &mut ThinClient,
tx_clients: &Vec<ThinClient>,
mint: &Mint,
keypairs: &Vec<KeyPair>,
leader: &ReplicatedData,
txs: i64,
last_id: &mut Hash,
threads: usize,
) {
println!("Signing transactions... {}", keypairs.len(),);
let signing_start = Instant::now();
let transactions: Vec<_> = keypairs
.par_iter()
.map(|keypair| Transaction::new(&mint.keypair(), keypair.pubkey(), 1, *last_id))
.collect();
let duration = signing_start.elapsed();
let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos());
let bsps = txs as f64 / ns as f64;
let nsps = ns as f64 / txs as f64;
println!(
"Done. {:.2} thousand signatures per second, {:.2} us per signature, {} ms total time",
bsps * 1_000_000_f64,
nsps / 1_000_f64,
duration_as_ms(&duration),
);
println!("Transfering {} transactions in {} batches", txs, threads);
let transfer_start = Instant::now();
let sz = transactions.len() / threads;
let chunks: Vec<_> = transactions.chunks(sz).collect();
chunks
.into_par_iter()
.zip(tx_clients)
.for_each(|(txs, client)| {
println!(
"Transferring 1 unit {} times... to {:?}",
txs.len(),
leader.transactions_addr
);
for tx in txs {
client.transfer_signed(tx.clone()).unwrap();
}
});
println!(
"Transfer done. {:?} ms {} tps",
duration_as_ms(&transfer_start.elapsed()),
txs as f32 / (duration_as_s(&transfer_start.elapsed()))
);
loop {
let new_id = client.get_last_id();
if *last_id != new_id {
*last_id = new_id;
break;
}
sleep(Duration::from_millis(100));
}
}
fn main() {
env_logger::init().unwrap();
env_logger::init();
let mut threads = 4usize;
let mut num_nodes = 1usize;
let mut time_sec = 60;
let mut opts = Options::new();
opts.optopt("l", "", "leader", "leader.json");
opts.optopt("c", "", "client port", "port");
opts.optopt("t", "", "number of threads", &format!("{}", threads));
opts.optflag("d", "dyn", "detect network address dynamically");
opts.optopt(
"s",
"",
"send transactions for this many seconds",
&format!("{}", time_sec),
);
opts.optopt(
"n",
"",
@ -80,21 +177,15 @@ fn main() {
print_usage(&program, opts);
return;
}
let mut addr: SocketAddr = "0.0.0.0:8100".parse().unwrap();
if matches.opt_present("c") {
let port = matches.opt_str("c").unwrap().parse().unwrap();
addr.set_port(port);
}
if matches.opt_present("d") {
addr.set_ip(get_ip_addr().unwrap());
}
let client_addr: Arc<RwLock<SocketAddr>> = Arc::new(RwLock::new(addr));
if matches.opt_present("t") {
threads = matches.opt_str("t").unwrap().parse().expect("integer");
}
if matches.opt_present("n") {
num_nodes = matches.opt_str("n").unwrap().parse().expect("integer");
}
if matches.opt_present("s") {
time_sec = matches.opt_str("s").unwrap().parse().expect("integer");
}
let leader = if matches.opt_present("l") {
read_leader(matches.opt_str("l").unwrap())
@ -105,16 +196,10 @@ fn main() {
let signal = Arc::new(AtomicBool::new(false));
let mut c_threads = vec![];
let validators = converge(
&client_addr,
&leader,
signal.clone(),
num_nodes,
&mut c_threads,
);
let validators = converge(&leader, signal.clone(), num_nodes, &mut c_threads);
assert_eq!(validators.len(), num_nodes);
if stdin_isatty() {
if is(Stream::Stdin) {
eprintln!("nothing found on stdin, expected a json file");
exit(1);
}
@ -127,127 +212,102 @@ fn main() {
}
println!("Parsing stdin...");
let demo: MintDemo = serde_json::from_str(&buffer).unwrap_or_else(|e| {
let mint: Mint = serde_json::from_str(&buffer).unwrap_or_else(|e| {
eprintln!("failed to parse json: {}", e);
exit(1);
});
let mut client = mk_client(&client_addr, &leader);
let mut client = mk_client(&leader);
println!("Get last ID...");
let last_id = client.get_last_id();
let mut last_id = client.get_last_id();
println!("Got last ID {:?}", last_id);
let rnd = GenKeys::new(demo.mint.keypair().public_key_bytes());
let mut seed = [0u8; 32];
seed.copy_from_slice(&mint.keypair().public_key_bytes()[..32]);
let rnd = GenKeys::new(seed);
println!("Creating keypairs...");
let txs = demo.num_accounts / 2;
let keypairs = rnd.gen_n_keypairs(demo.num_accounts);
let keypair_pairs: Vec<_> = keypairs.chunks(2).collect();
println!("Signing transactions...");
let now = Instant::now();
let transactions: Vec<_> = keypair_pairs
.into_par_iter()
.map(|chunk| Transaction::new(&chunk[0], chunk[1].pubkey(), 1, last_id))
.collect();
let duration = now.elapsed();
let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos());
let bsps = txs as f64 / ns as f64;
let nsps = ns as f64 / txs as f64;
println!(
"Done. {} thousand signatures per second, {}us per signature",
bsps * 1_000_000_f64,
nsps / 1_000_f64
);
let txs = 500_000;
let keypairs = rnd.gen_n_keypairs(txs);
let first_count = client.transaction_count();
println!("initial count {}", first_count);
println!("Transfering {} transactions in {} batches", txs, threads);
let sz = transactions.len() / threads;
let chunks: Vec<_> = transactions.chunks(sz).collect();
chunks.into_par_iter().for_each(|txs| {
println!(
"Transferring 1 unit {} times... to {:?}",
txs.len(),
leader.transactions_addr
);
let client = mk_client(&client_addr, &leader);
for tx in txs {
client.transfer_signed(tx.clone()).unwrap();
}
});
let sample_period = 1; // in seconds
println!("Sampling tps every second...",);
let maxes: Vec<_> = validators
.into_par_iter()
.map(|val| {
let mut client = mk_client(&client_addr, &val);
let mut now = Instant::now();
let mut initial_tx_count = client.transaction_count();
let mut max_tps = 0.0;
let mut total = 0;
for i in 0..100 {
let tx_count = client.transaction_count();
let duration = now.elapsed();
now = Instant::now();
let sample = tx_count - initial_tx_count;
initial_tx_count = tx_count;
println!(
"{}: Transactions processed {}",
val.transactions_addr, sample
);
let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos());
let tps = (sample * 1_000_000_000) as f64 / ns as f64;
if tps > max_tps {
max_tps = tps;
}
println!("{}: {} tps", val.transactions_addr, tps);
total = tx_count - first_count;
println!(
"{}: Total Transactions processed {}",
val.transactions_addr, total
);
if total == transactions.len() as u64 {
break;
}
if i > 20 && sample == 0 {
break;
}
sleep(Duration::new(sample_period, 0));
}
(max_tps, total)
// 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
let v_threads: Vec<_> = validators
.into_iter()
.map(|v| {
let exit = signal.clone();
let maxes = maxes.clone();
Builder::new()
.name("solana-client-sample".to_string())
.spawn(move || {
sample_tx_count(exit, maxes, first_count, v, sample_period);
})
.unwrap()
})
.collect();
let clients = (0..threads).map(|_| mk_client(&leader)).collect();
// generate and send transactions for the specified duration
let time = Duration::new(time_sec, 0);
let now = Instant::now();
while now.elapsed() < time {
generate_and_send_txs(
&mut client,
&clients,
&mint,
&keypairs,
&leader,
txs,
&mut last_id,
threads,
);
}
// Stop the sampling threads so it will collect the stats
signal.store(true, Ordering::Relaxed);
for t in v_threads {
t.join().unwrap();
}
// Compute/report stats
let mut max_of_maxes = 0.0;
let mut total_txs = 0;
for (max, txs) in &maxes {
for (max, txs) in maxes.read().unwrap().iter() {
if *max > max_of_maxes {
max_of_maxes = *max;
}
total_txs += *txs;
}
println!(
"\nHighest TPS: {} sampling period {}s total transactions: {} clients: {}",
"\nHighest TPS: {:.2} sampling period {}s total transactions: {} clients: {}",
max_of_maxes,
sample_period,
total_txs,
maxes.len()
maxes.read().unwrap().len()
);
signal.store(true, Ordering::Relaxed);
// join the crdt client threads
for t in c_threads {
t.join().unwrap();
}
}
fn mk_client(locked_addr: &Arc<RwLock<SocketAddr>>, r: &ReplicatedData) -> ThinClient {
let mut addr = locked_addr.write().unwrap();
let port = addr.port();
let transactions_socket = UdpSocket::bind(addr.clone()).unwrap();
addr.set_port(port + 1);
let requests_socket = UdpSocket::bind(addr.clone()).unwrap();
addr.set_port(port + 2);
fn mk_client(r: &ReplicatedData) -> ThinClient {
let requests_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
requests_socket
.set_read_timeout(Some(Duration::new(1, 0)))
.unwrap();
ThinClient::new(
r.requests_addr,
requests_socket,
@ -256,26 +316,23 @@ fn mk_client(locked_addr: &Arc<RwLock<SocketAddr>>, r: &ReplicatedData) -> ThinC
)
}
fn spy_node(client_addr: &Arc<RwLock<SocketAddr>>) -> (ReplicatedData, UdpSocket) {
let mut addr = client_addr.write().unwrap();
let port = addr.port();
let gossip = UdpSocket::bind(addr.clone()).unwrap();
addr.set_port(port + 1);
let daddr = "0.0.0.0:0".parse().unwrap();
fn spy_node() -> (ReplicatedData, UdpSocket) {
let gossip_socket_pair = udp_public_bind("gossip");
let pubkey = KeyPair::new().pubkey();
let daddr = "0.0.0.0:0".parse().unwrap();
let node = ReplicatedData::new(
pubkey,
gossip.local_addr().unwrap(),
//gossip.local_addr().unwrap(),
gossip_socket_pair.addr,
daddr,
daddr,
daddr,
daddr,
);
(node, gossip)
(node, gossip_socket_pair.receiver)
}
fn converge(
client_addr: &Arc<RwLock<SocketAddr>>,
leader: &ReplicatedData,
exit: Arc<AtomicBool>,
num_nodes: usize,
@ -283,7 +340,7 @@ fn converge(
) -> Vec<ReplicatedData> {
//lets spy on the network
let daddr = "0.0.0.0:0".parse().unwrap();
let (spy, spy_gossip) = spy_node(client_addr);
let (spy, spy_gossip) = spy_node();
let mut spy_crdt = Crdt::new(spy);
spy_crdt.insert(&leader);
spy_crdt.set_leader(leader.id);
@ -321,6 +378,6 @@ fn converge(
}
fn read_leader(path: String) -> ReplicatedData {
let file = File::open(path).expect("file");
serde_json::from_reader(file).expect("parse")
let file = File::open(path.clone()).expect(&format!("file not found: {}", path));
serde_json::from_reader(file).expect(&format!("failed to parse {}", path))
}

167
src/bin/drone.rs Normal file
View File

@ -0,0 +1,167 @@
extern crate atty;
extern crate bincode;
extern crate env_logger;
extern crate getopts;
extern crate serde_json;
extern crate solana;
extern crate tokio;
extern crate tokio_codec;
extern crate tokio_io;
use atty::{is, Stream as atty_stream};
use bincode::deserialize;
use getopts::Options;
use solana::crdt::ReplicatedData;
use solana::drone::{Drone, DroneRequest};
use solana::mint::Mint;
use std::env;
use std::fs::File;
use std::io::{stdin, Read};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::process::exit;
use std::sync::{Arc, Mutex};
use std::thread;
use tokio::net::TcpListener;
use tokio::prelude::*;
use tokio_codec::{BytesCodec, Decoder};
fn print_usage(program: &str, opts: Options) {
let mut brief = format!("Usage: cat <mint.json> | {} [options]\n\n", program);
brief += " Run a Solana Drone to act as the custodian of the mint's remaining tokens\n";
print!("{}", opts.usage(&brief));
}
fn main() {
env_logger::init();
let mut opts = Options::new();
opts.optopt(
"t",
"",
"time",
"time slice over which to limit token requests to drone",
);
opts.optopt("c", "", "cap", "request limit for time slice");
opts.optopt("l", "", "leader", "leader.json");
opts.optflag("h", "help", "print help");
let args: Vec<String> = env::args().collect();
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(e) => {
eprintln!("{}", e);
exit(1);
}
};
if matches.opt_present("h") {
let program = args[0].clone();
print_usage(&program, opts);
return;
}
let time_slice: Option<u64>;
if matches.opt_present("t") {
time_slice = matches
.opt_str("t")
.expect("unexpected string from input")
.parse()
.ok();
} else {
time_slice = None;
}
let request_cap: Option<u64>;
if matches.opt_present("c") {
request_cap = matches
.opt_str("c")
.expect("unexpected string from input")
.parse()
.ok();
} else {
request_cap = None;
}
let leader = if matches.opt_present("l") {
read_leader(matches.opt_str("l").unwrap())
} else {
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000);
ReplicatedData::new_leader(&server_addr)
};
if is(atty_stream::Stdin) {
eprintln!("nothing found on stdin, expected a json file");
exit(1);
}
let mut buffer = String::new();
let num_bytes = stdin().read_to_string(&mut buffer).unwrap();
if num_bytes == 0 {
eprintln!("empty file on stdin, expected a json file");
exit(1);
}
let mint: Mint = serde_json::from_str(&buffer).unwrap_or_else(|e| {
eprintln!("failed to parse json: {}", e);
exit(1);
});
let mint_keypair = mint.keypair();
let drone_addr: SocketAddr = "0.0.0.0:9900".parse().unwrap();
let drone = Arc::new(Mutex::new(Drone::new(
mint_keypair,
drone_addr,
leader.transactions_addr,
leader.requests_addr,
time_slice,
request_cap,
)));
let drone1 = drone.clone();
thread::spawn(move || loop {
let time = drone1.lock().unwrap().time_slice;
thread::sleep(time);
drone1.lock().unwrap().clear_request_count();
});
let socket = TcpListener::bind(&drone_addr).unwrap();
println!("Drone started. Listening on: {}", drone_addr);
let done = socket
.incoming()
.map_err(|e| println!("failed to accept socket; error = {:?}", e))
.for_each(move |socket| {
let drone2 = drone.clone();
// let client_ip = socket.peer_addr().expect("drone peer_addr").ip();
let framed = BytesCodec::new().framed(socket);
let (_writer, reader) = framed.split();
let processor = reader
.for_each(move |bytes| {
let req: DroneRequest =
deserialize(&bytes).expect("deserialize packet in drone");
println!("Airdrop requested...");
// let res = drone2.lock().unwrap().check_rate_limit(client_ip);
let res1 = drone2.lock().unwrap().send_airdrop(req);
match res1 {
Ok(_) => println!("Airdrop sent!"),
Err(_) => println!("Request limit reached for this time slice"),
}
Ok(())
})
.and_then(|()| {
println!("Socket received FIN packet and closed connection");
Ok(())
})
.or_else(|err| {
println!("Socket closed with error: {:?}", err);
Err(err)
})
.then(|result| {
println!("Socket closed with result: {:?}", result);
Ok(())
});
tokio::spawn(processor)
});
tokio::run(done);
}
fn read_leader(path: String) -> ReplicatedData {
let file = File::open(path.clone()).expect(&format!("file not found: {}", path));
serde_json::from_reader(file).expect(&format!("failed to parse {}", path))
}

View File

@ -4,6 +4,7 @@ extern crate solana;
use getopts::Options;
use solana::crdt::{get_ip_addr, parse_port_or_addr, ReplicatedData};
use solana::nat::get_public_ip_addr;
use std::env;
use std::io;
use std::net::SocketAddr;
@ -19,7 +20,16 @@ fn print_usage(program: &str, opts: Options) {
fn main() {
let mut opts = Options::new();
opts.optopt("b", "", "bind", "bind to port or address");
opts.optflag("d", "dyn", "detect network address dynamically");
opts.optflag(
"p",
"",
"detect public network address using public servers",
);
opts.optflag(
"l",
"",
"detect network address from local machine configuration",
);
opts.optflag("h", "help", "print help");
let args: Vec<String> = env::args().collect();
let matches = match opts.parse(&args[1..]) {
@ -37,10 +47,14 @@ fn main() {
let bind_addr: SocketAddr = {
let mut bind_addr = parse_port_or_addr(matches.opt_str("b"));
if matches.opt_present("d") {
if matches.opt_present("l") {
let ip = get_ip_addr().unwrap();
bind_addr.set_ip(ip);
}
if matches.opt_present("p") {
let ip = get_public_ip_addr().unwrap();
bind_addr.set_ip(ip);
}
bind_addr
};

View File

@ -1,23 +1,17 @@
extern crate atty;
extern crate env_logger;
extern crate getopts;
extern crate isatty;
extern crate log;
extern crate serde_json;
extern crate solana;
#[macro_use]
extern crate log;
use atty::{is, Stream};
use getopts::Options;
use isatty::stdin_isatty;
use solana::bank::Bank;
use solana::crdt::ReplicatedData;
use solana::entry::Entry;
use solana::payment_plan::PaymentPlan;
use solana::server::Server;
use solana::transaction::Instruction;
use solana::crdt::{ReplicatedData, TestNode};
use solana::fullnode::FullNode;
use std::env;
use std::fs::File;
use std::io::{stdin, Read};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::process::exit;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@ -32,17 +26,24 @@ fn print_usage(program: &str, opts: Options) {
print!("{}", opts.usage(&brief));
}
fn main() {
env_logger::init().unwrap();
fn main() -> () {
env_logger::init();
let mut opts = Options::new();
opts.optopt("l", "", "load", "load my identity to path.json");
opts.optflag("h", "help", "print help");
opts.optopt("l", "", "run with the identity found in FILE", "FILE");
opts.optopt(
"v",
"t",
"",
"validator",
"run as replicate with path to leader.json",
"testnet; connect to the network at this gossip entry point",
"HOST:PORT",
);
opts.optopt(
"o",
"",
"output log to FILE, defaults to stdout (ignored by validators)",
"FILE",
);
let args: Vec<String> = env::args().collect();
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
@ -56,55 +57,11 @@ fn main() {
print_usage(&program, opts);
return;
}
if stdin_isatty() {
if is(Stream::Stdin) {
eprintln!("nothing found on stdin, expected a log file");
exit(1);
}
let mut buffer = String::new();
let num_bytes = stdin().read_to_string(&mut buffer).unwrap();
if num_bytes == 0 {
eprintln!("empty file on stdin, expected a log file");
exit(1);
}
eprintln!("Initializing...");
let mut entries = buffer.lines().map(|line| {
serde_json::from_str(&line).unwrap_or_else(|e| {
eprintln!("failed to parse json: {}", e);
exit(1);
})
});
eprintln!("done parsing...");
// The first item in the ledger is required to be an entry with zero num_hashes,
// which implies its id can be used as the ledger's seed.
let entry0 = entries.next().unwrap();
// The second item in the ledger is a special transaction where the to and from
// fields are the same. That entry should be treated as a deposit, not a
// transfer to oneself.
let entry1: Entry = entries.next().unwrap();
let tx = &entry1.transactions[0];
let deposit = if let Instruction::NewContract(contract) = &tx.instruction {
contract.plan.final_payment()
} else {
None
};
eprintln!("creating bank...");
let bank = Bank::new_from_deposit(&deposit.unwrap());
bank.register_entry_id(&entry0.id);
bank.register_entry_id(&entry1.id);
eprintln!("processing entries...");
bank.process_entries(entries).expect("process_entries");
eprintln!("creating networking stack...");
let exit = Arc::new(AtomicBool::new(false));
let bind_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000);
let mut repl_data = ReplicatedData::new_leader(&bind_addr);
if matches.opt_present("l") {
@ -113,49 +70,27 @@ fn main() {
if let Ok(data) = serde_json::from_reader(file) {
repl_data = data;
} else {
warn!("failed to parse leader {}, generating new identity", path);
eprintln!("failed to parse {}", path);
exit(1);
}
} else {
eprintln!("failed to read {}", path);
exit(1);
}
}
let threads = if matches.opt_present("v") {
eprintln!("starting validator... {}", repl_data.requests_addr);
let path = matches.opt_str("v").unwrap();
let file = File::open(path).expect("file");
let leader = serde_json::from_reader(file).expect("parse");
let s = Server::new_validator(
bank,
repl_data.clone(),
UdpSocket::bind(repl_data.requests_addr).unwrap(),
UdpSocket::bind("0.0.0.0:0").unwrap(),
UdpSocket::bind(repl_data.replicate_addr).unwrap(),
UdpSocket::bind(repl_data.gossip_addr).unwrap(),
UdpSocket::bind(repl_data.repair_addr).unwrap(),
leader,
exit.clone(),
);
s.thread_hdls
let mut node = TestNode::new_with_bind_addr(repl_data, bind_addr);
let exit = Arc::new(AtomicBool::new(false));
let fullnode = if matches.opt_present("t") {
let testnet_address_string = matches.opt_str("t").unwrap();
let testnet_addr = testnet_address_string.parse().unwrap();
FullNode::new(node, false, None, Some(testnet_addr), None, exit)
} else {
eprintln!("starting leader... {}", repl_data.requests_addr);
repl_data.current_leader_id = repl_data.id.clone();
let file = File::create("leader.log").expect("leader.log create");
let server = Server::new_leader(
bank,
//Some(Duration::from_millis(1000)),
None,
repl_data.clone(),
UdpSocket::bind(repl_data.requests_addr).unwrap(),
UdpSocket::bind(repl_data.transactions_addr).unwrap(),
UdpSocket::bind("0.0.0.0:0").unwrap(),
UdpSocket::bind("0.0.0.0:0").unwrap(),
UdpSocket::bind(repl_data.gossip_addr).unwrap(),
exit.clone(),
file,
);
server.thread_hdls
};
eprintln!("Ready. Listening on {}", repl_data.transactions_addr);
node.data.current_leader_id = node.data.id.clone();
for t in threads {
let outfile = matches.opt_str("o");
FullNode::new(node, true, None, None, outfile, exit)
};
for t in fullnode.thread_hdls {
t.join().expect("join");
}
}

View File

@ -1,77 +0,0 @@
extern crate isatty;
extern crate rayon;
extern crate serde_json;
extern crate solana;
use isatty::stdin_isatty;
use rayon::prelude::*;
use solana::bank::MAX_ENTRY_IDS;
use solana::entry::{next_entry, Entry};
use solana::mint::MintDemo;
use solana::signature::{GenKeys, KeyPairUtil};
use solana::transaction::Transaction;
use std::io::{stdin, Read};
use std::process::exit;
// Generate a ledger with lots and lots of accounts.
fn main() {
if stdin_isatty() {
eprintln!("nothing found on stdin, expected a json file");
exit(1);
}
let mut buffer = String::new();
let num_bytes = stdin().read_to_string(&mut buffer).unwrap();
if num_bytes == 0 {
eprintln!("empty file on stdin, expected a json file");
exit(1);
}
let demo: MintDemo = serde_json::from_str(&buffer).unwrap_or_else(|e| {
eprintln!("failed to parse json: {}", e);
exit(1);
});
let rnd = GenKeys::new(demo.mint.keypair().public_key_bytes());
let num_accounts = demo.num_accounts;
let tokens_per_user = 1_000;
let keypairs = rnd.gen_n_keypairs(num_accounts);
let mint_keypair = demo.mint.keypair();
let last_id = demo.mint.last_id();
for entry in demo.mint.create_entries() {
println!("{}", serde_json::to_string(&entry).unwrap());
}
eprintln!("Creating {} empty entries...", MAX_ENTRY_IDS);
// Offer client lots of entry IDs to use for each transaction's last_id.
let mut last_id = last_id;
let mut last_ids = vec![];
for _ in 0..MAX_ENTRY_IDS {
let entry = next_entry(&last_id, 1, vec![]);
last_id = entry.id;
last_ids.push(last_id);
let serialized = serde_json::to_string(&entry).unwrap_or_else(|e| {
eprintln!("failed to serialize: {}", e);
exit(1);
});
println!("{}", serialized);
}
eprintln!("Creating {} transactions...", num_accounts);
let transactions: Vec<_> = keypairs
.into_par_iter()
.enumerate()
.map(|(i, rando)| {
let last_id = last_ids[i % MAX_ENTRY_IDS];
Transaction::new(&mint_keypair, rando.pubkey(), tokens_per_user, last_id)
})
.collect();
eprintln!("Logging the creation of {} accounts...", num_accounts);
let entry = Entry::new(&last_id, 0, transactions);
println!("{}", serde_json::to_string(&entry).unwrap());
}

View File

@ -1,36 +1,31 @@
//! A command-line executable for generating the chain's genesis block.
extern crate isatty;
extern crate atty;
extern crate serde_json;
extern crate solana;
use isatty::stdin_isatty;
use atty::{is, Stream};
use solana::entry_writer::EntryWriter;
use solana::mint::Mint;
use std::io::{stdin, Read};
use std::error;
use std::io::{stdin, stdout, Read};
use std::process::exit;
fn main() {
if stdin_isatty() {
fn main() -> Result<(), Box<error::Error>> {
if is(Stream::Stdin) {
eprintln!("nothing found on stdin, expected a json file");
exit(1);
}
let mut buffer = String::new();
let num_bytes = stdin().read_to_string(&mut buffer).unwrap();
let num_bytes = stdin().read_to_string(&mut buffer)?;
if num_bytes == 0 {
eprintln!("empty file on stdin, expected a json file");
exit(1);
}
let mint: Mint = serde_json::from_str(&buffer).unwrap_or_else(|e| {
eprintln!("failed to parse json: {}", e);
exit(1);
});
for x in mint.create_entries() {
let serialized = serde_json::to_string(&x).unwrap_or_else(|e| {
eprintln!("failed to serialize: {}", e);
exit(1);
});
println!("{}", serialized);
}
let mint: Mint = serde_json::from_str(&buffer)?;
let mut writer = stdout();
EntryWriter::write_entries(&mut writer, mint.create_entries())?;
Ok(())
}

View File

@ -1,21 +0,0 @@
extern crate rayon;
extern crate ring;
extern crate serde_json;
extern crate solana;
use solana::mint::{Mint, MintDemo};
use std::io;
fn main() {
let mut input_text = String::new();
io::stdin().read_line(&mut input_text).unwrap();
let trimmed = input_text.trim();
let tokens = trimmed.parse::<i64>().unwrap();
let mint = Mint::new(tokens);
let tokens_per_user = 1_000;
let num_accounts = tokens / tokens_per_user;
let demo = MintDemo { mint, num_accounts };
println!("{}", serde_json::to_string(&demo).unwrap());
}

View File

@ -1,15 +1,15 @@
extern crate isatty;
extern crate atty;
extern crate serde_json;
extern crate solana;
use isatty::stdin_isatty;
use atty::{is, Stream};
use solana::mint::Mint;
use std::io;
use std::process::exit;
fn main() {
let mut input_text = String::new();
if stdin_isatty() {
if is(Stream::Stdin) {
eprintln!("nothing found on stdin, expected a token number");
exit(1);
}

337
src/bin/wallet.rs Normal file
View File

@ -0,0 +1,337 @@
extern crate atty;
extern crate bincode;
extern crate bs58;
extern crate clap;
extern crate env_logger;
extern crate getopts;
extern crate serde_json;
extern crate solana;
use bincode::serialize;
use clap::{App, Arg, SubCommand};
use solana::crdt::ReplicatedData;
use solana::drone::DroneRequest;
use solana::mint::Mint;
use solana::signature::{PublicKey, Signature};
use solana::thin_client::ThinClient;
use std::error;
use std::fmt;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream, UdpSocket};
use std::process::exit;
use std::thread::sleep;
use std::time::Duration;
enum WalletCommand {
Address,
Balance,
AirDrop(i64),
Pay(i64, PublicKey),
Confirm(Signature),
}
#[derive(Debug, Clone)]
enum WalletError {
CommandNotRecognized(String),
BadParameter(String),
}
impl fmt::Display for WalletError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid")
}
}
impl error::Error for WalletError {
fn description(&self) -> &str {
"invalid"
}
fn cause(&self) -> Option<&error::Error> {
// Generic error, underlying cause isn't tracked.
None
}
}
struct WalletConfig {
leader: ReplicatedData,
id: Mint,
drone_addr: SocketAddr,
command: WalletCommand,
}
impl Default for WalletConfig {
fn default() -> WalletConfig {
let default_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000);
WalletConfig {
leader: ReplicatedData::new_leader(&default_addr.clone()),
id: Mint::new(0),
drone_addr: default_addr.clone(),
command: WalletCommand::Balance,
}
}
}
fn parse_args() -> Result<WalletConfig, Box<error::Error>> {
let matches = App::new("solana-wallet")
.arg(
Arg::with_name("leader")
.short("l")
.long("leader")
.value_name("PATH")
.takes_value(true)
.help("/path/to/leader.json"),
)
.arg(
Arg::with_name("mint")
.short("m")
.long("mint")
.value_name("PATH")
.takes_value(true)
.help("/path/to/mint.json"),
)
.subcommand(
SubCommand::with_name("airdrop")
.about("Request a batch of tokens")
.arg(
Arg::with_name("tokens")
// .index(1)
.long("tokens")
.value_name("NUMBER")
.takes_value(true)
.help("The number of tokens to request"),
),
)
.subcommand(
SubCommand::with_name("pay")
.about("Send a payment")
.arg(
Arg::with_name("tokens")
// .index(2)
.long("tokens")
.value_name("NUMBER")
.takes_value(true)
.required(true)
.help("the number of tokens to send"),
)
.arg(
Arg::with_name("to")
// .index(1)
.long("to")
.value_name("PUBKEY")
.takes_value(true)
.required(true)
.help("The pubkey of recipient"),
),
)
.subcommand(
SubCommand::with_name("confirm")
.about("Confirm your payment by signature")
.arg(
Arg::with_name("signature")
.index(1)
.value_name("SIGNATURE")
.required(true)
.help("The transaction signature to confirm"),
),
)
.subcommand(SubCommand::with_name("balance").about("Get your balance"))
.subcommand(SubCommand::with_name("address").about("Get your public key"))
.get_matches();
let leader: ReplicatedData;
if let Some(l) = matches.value_of("leader") {
leader = read_leader(l.to_string());
} else {
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000);
leader = ReplicatedData::new_leader(&server_addr);
};
let id: Mint;
if let Some(m) = matches.value_of("mint") {
id = read_mint(m.to_string())?;
} else {
eprintln!("No mint found!");
exit(1);
};
let mut drone_addr = leader.transactions_addr.clone();
drone_addr.set_port(9900);
let command = match matches.subcommand() {
("airdrop", Some(airdrop_matches)) => {
let mut tokens: i64 = id.tokens;
if airdrop_matches.is_present("tokens") {
tokens = airdrop_matches.value_of("tokens").unwrap().parse()?;
}
Ok(WalletCommand::AirDrop(tokens))
}
("pay", Some(pay_matches)) => {
let to: PublicKey;
if pay_matches.is_present("to") {
let pubkey_vec = bs58::decode(pay_matches.value_of("to").unwrap())
.into_vec()
.expect("base58-encoded public key");
if pubkey_vec.len() != std::mem::size_of::<PublicKey>() {
display_actions();
Err(WalletError::BadParameter("Invalid public key".to_string()))?;
}
to = PublicKey::clone_from_slice(&pubkey_vec);
} else {
to = id.pubkey();
}
let mut tokens: i64 = id.tokens;
if pay_matches.is_present("tokens") {
tokens = pay_matches.value_of("tokens").unwrap().parse()?;
}
Ok(WalletCommand::Pay(tokens, to))
}
("confirm", Some(confirm_matches)) => {
let sig_vec = bs58::decode(confirm_matches.value_of("signature").unwrap())
.into_vec()
.expect("base58-encoded signature");
if sig_vec.len() == std::mem::size_of::<Signature>() {
let sig = Signature::clone_from_slice(&sig_vec);
Ok(WalletCommand::Confirm(sig))
} else {
display_actions();
Err(WalletError::BadParameter("Invalid signature".to_string()))
}
}
("balance", Some(_balance_matches)) => Ok(WalletCommand::Balance),
("address", Some(_address_matches)) => Ok(WalletCommand::Address),
("", None) => {
display_actions();
Err(WalletError::CommandNotRecognized(
"no subcommand given".to_string(),
))
}
_ => unreachable!(),
}?;
Ok(WalletConfig {
leader,
id,
drone_addr, // TODO: Add an option for this.
command,
})
}
fn process_command(
config: &WalletConfig,
client: &mut ThinClient,
) -> Result<(), Box<error::Error>> {
match config.command {
// Check client balance
WalletCommand::Address => {
println!("{}", bs58::encode(config.id.pubkey()).into_string());
}
WalletCommand::Balance => {
println!("Balance requested...");
let balance = client.poll_get_balance(&config.id.pubkey());
match balance {
Ok(balance) => {
println!("Your balance is: {:?}", balance);
}
Err(ref e) if e.kind() == std::io::ErrorKind::Other => {
println!("No account found! Request an airdrop to get started.");
}
Err(error) => {
println!("An error occurred: {:?}", error);
}
}
}
// Request an airdrop from Solana Drone;
// Request amount is set in request_airdrop function
WalletCommand::AirDrop(tokens) => {
println!("Airdrop requested...");
println!("Airdropping {:?} tokens", tokens);
let _airdrop = request_airdrop(&config.drone_addr, &config.id, tokens as u64)?;
// TODO: return airdrop Result from Drone
sleep(Duration::from_millis(100));
println!(
"Your balance is: {:?}",
client.poll_get_balance(&config.id.pubkey()).unwrap()
);
}
// If client has positive balance, spend tokens in {balance} number of transactions
WalletCommand::Pay(tokens, to) => {
let last_id = client.get_last_id();
let sig = client.transfer(tokens, &config.id.keypair(), to, &last_id)?;
println!("{}", bs58::encode(sig).into_string());
}
// Confirm the last client transaction by signature
WalletCommand::Confirm(sig) => {
if client.check_signature(&sig) {
println!("Confirmed");
} else {
println!("Not found");
}
}
}
Ok(())
}
fn display_actions() {
println!("");
println!("Commands:");
println!(" address Get your public key");
println!(" balance Get your account balance");
println!(" airdrop Request a batch of tokens");
println!(" pay Send tokens to a public key");
println!(" confirm Confirm your last payment by signature");
println!("");
}
fn read_leader(path: String) -> ReplicatedData {
let file = File::open(path.clone()).expect(&format!("file not found: {}", path));
serde_json::from_reader(file).expect(&format!("failed to parse {}", path))
}
fn read_mint(path: String) -> Result<Mint, Box<error::Error>> {
let file = File::open(path.clone())?;
let mint = serde_json::from_reader(file)?;
Ok(mint)
}
fn mk_client(r: &ReplicatedData) -> io::Result<ThinClient> {
let requests_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
requests_socket
.set_read_timeout(Some(Duration::new(1, 0)))
.unwrap();
Ok(ThinClient::new(
r.requests_addr,
requests_socket,
r.transactions_addr,
transactions_socket,
))
}
fn request_airdrop(
drone_addr: &SocketAddr,
id: &Mint,
tokens: u64,
) -> Result<(), Box<error::Error>> {
let mut stream = TcpStream::connect(drone_addr)?;
let req = DroneRequest::GetAirdrop {
airdrop_request_amount: tokens,
client_public_key: id.pubkey(),
};
let tx = serialize(&req).expect("serialize drone request");
stream.write_all(&tx).unwrap();
// TODO: add timeout to this function, in case of unresponsive drone
Ok(())
}
fn main() -> Result<(), Box<error::Error>> {
env_logger::init();
let config = parse_args()?;
let mut client = mk_client(&config.leader)?;
process_command(&config, &mut client)
}

43
src/blob_fetch_stage.rs Normal file
View File

@ -0,0 +1,43 @@
//! The `blob_fetch_stage` pulls blobs from UDP sockets and sends it to a channel.
use packet::BlobRecycler;
use std::net::UdpSocket;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::thread::JoinHandle;
use streamer::{self, BlobReceiver};
pub struct BlobFetchStage {
pub thread_hdls: Vec<JoinHandle<()>>,
}
impl BlobFetchStage {
pub fn new(
socket: UdpSocket,
exit: Arc<AtomicBool>,
blob_recycler: BlobRecycler,
) -> (Self, BlobReceiver) {
Self::new_multi_socket(vec![socket], exit, blob_recycler)
}
pub fn new_multi_socket(
sockets: Vec<UdpSocket>,
exit: Arc<AtomicBool>,
blob_recycler: BlobRecycler,
) -> (Self, BlobReceiver) {
let (blob_sender, blob_receiver) = channel();
let thread_hdls: Vec<_> = sockets
.into_iter()
.map(|socket| {
streamer::blob_receiver(
exit.clone(),
blob_recycler.clone(),
socket,
blob_sender.clone(),
).expect("blob receiver init")
})
.collect();
(BlobFetchStage { thread_hdls }, blob_receiver)
}
}

View File

@ -0,0 +1,333 @@
use crdt::ReplicatedData;
use rand::distributions::{Distribution, Weighted, WeightedChoice};
use rand::thread_rng;
use result::{Error, Result};
use signature::PublicKey;
use std;
use std::collections::HashMap;
pub const DEFAULT_WEIGHT: u32 = 1;
pub trait ChooseGossipPeerStrategy {
fn choose_peer<'a>(&self, options: Vec<&'a ReplicatedData>) -> Result<&'a ReplicatedData>;
}
pub struct ChooseRandomPeerStrategy<'a> {
random: &'a Fn() -> u64,
}
// Given a source of randomness "random", this strategy will randomly pick a validator
// from the input options. This strategy works in isolation, but doesn't leverage any
// rumors from the rest of the gossip network to make more informed decisions about
// which validators have more/less updates
impl<'a, 'b> ChooseRandomPeerStrategy<'a> {
pub fn new(random: &'a Fn() -> u64) -> Self {
ChooseRandomPeerStrategy { random }
}
}
impl<'a> ChooseGossipPeerStrategy for ChooseRandomPeerStrategy<'a> {
fn choose_peer<'b>(&self, options: Vec<&'b ReplicatedData>) -> Result<&'b ReplicatedData> {
if options.is_empty() {
return Err(Error::CrdtTooSmall);
}
let n = ((self.random)() as usize) % options.len();
Ok(options[n])
}
}
// This strategy uses rumors accumulated from the rest of the network to weight
// the importance of communicating with a particular validator based on cumulative network
// perceiption of the number of updates the validator has to offer. A validator is randomly
// picked based on a weighted sample from the pool of viable choices. The "weight", w, of a
// particular validator "v" is calculated as follows:
//
// w = [Sum for all i in I_v: (rumor_v(i) - observed(v)) * stake(i)] /
// [Sum for all i in I_v: Sum(stake(i))]
//
// where I_v is the set of all validators that returned a rumor about the update_index of
// validator "v", stake(i) is the size of the stake of validator "i", observed(v) is the
// observed update_index from the last direct communication validator "v", and
// rumor_v(i) is the rumored update_index of validator "v" propagated by fellow validator "i".
// This could be a problem if there are validators with large stakes lying about their
// observed updates. There could also be a problem in network partitions, or even just
// when certain validators are disproportionately active, where we hear more rumors about
// certain clusters of nodes that then propagate more rumros about each other. Hopefully
// this can be resolved with a good baseline DEFAULT_WEIGHT, or by implementing lockout
// periods for very active validators in the future.
pub struct ChooseWeightedPeerStrategy<'a> {
// The map of last directly observed update_index for each active validator.
// This is how we get observed(v) from the formula above.
remote: &'a HashMap<PublicKey, u64>,
// The map of rumored update_index for each active validator. Using the formula above,
// to find rumor_v(i), we would first look up "v" in the outer map, then look up
// "i" in the inner map, i.e. look up external_liveness[v][i]
external_liveness: &'a HashMap<PublicKey, HashMap<PublicKey, u64>>,
// A function returning the size of the stake for a particular validator, corresponds
// to stake(i) in the formula above.
get_stake: &'a Fn(PublicKey) -> f64,
}
impl<'a> ChooseWeightedPeerStrategy<'a> {
pub fn new(
remote: &'a HashMap<PublicKey, u64>,
external_liveness: &'a HashMap<PublicKey, HashMap<PublicKey, u64>>,
get_stake: &'a Fn(PublicKey) -> f64,
) -> Self {
ChooseWeightedPeerStrategy {
remote,
external_liveness,
get_stake,
}
}
fn calculate_weighted_remote_index(&self, peer_id: PublicKey) -> u32 {
let mut last_seen_index = 0;
// If the peer is not in our remote table, then we leave last_seen_index as zero.
// Only happens when a peer appears in our crdt.table but not in our crdt.remote,
// which means a validator was directly injected into our crdt.table
if let Some(index) = self.remote.get(&peer_id) {
last_seen_index = *index;
}
let liveness_entry = self.external_liveness.get(&peer_id);
if liveness_entry.is_none() {
return DEFAULT_WEIGHT;
}
let votes = liveness_entry.unwrap();
if votes.is_empty() {
return DEFAULT_WEIGHT;
}
// Calculate the weighted average of the rumors
let mut relevant_votes = vec![];
let total_stake = votes.iter().fold(0.0, |total_stake, (&id, &vote)| {
let stake = (self.get_stake)(id);
// If the total stake is going to overflow u64, pick
// the larger of either the current total_stake, or the
// new stake, this way we are guaranteed to get at least u64/2
// sample of stake in our weighted calculation
if std::f64::MAX - total_stake < stake {
if stake > total_stake {
relevant_votes = vec![(stake, vote)];
stake
} else {
total_stake
}
} else {
relevant_votes.push((stake, vote));
total_stake + stake
}
});
let weighted_vote = relevant_votes.iter().fold(0.0, |sum, &(stake, vote)| {
if vote < last_seen_index {
// This should never happen because we maintain the invariant that the indexes
// in the external_liveness table are always greater than the corresponding
// indexes in the remote table, if the index exists in the remote table at all.
// Case 1: Attempt to insert bigger index into the "external_liveness" table
// happens after an insertion into the "remote" table. In this case,
// (see apply_updates()) function, we prevent the insertion if the entry
// in the remote table >= the atempted insertion into the "external" liveness
// table.
// Case 2: Bigger index in the "external_liveness" table inserted before
// a smaller insertion into the "remote" table. We clear the corresponding
// "external_liveness" table entry on all insertions into the "remote" table
// See apply_updates() function.
warn!("weighted peer index was smaller than local entry in remote table");
return sum;
}
let vote_difference = (vote - last_seen_index) as f64;
let new_weight = vote_difference * (stake / total_stake);
if std::f64::MAX - sum < new_weight {
return f64::max(new_weight, sum);
}
sum + new_weight
});
// Return u32 b/c the weighted sampling API from rand::distributions
// only takes u32 for weights
if weighted_vote >= std::u32::MAX as f64 {
return std::u32::MAX;
}
// If the weighted rumors we've heard about aren't any greater than
// what we've directly learned from the last time we communicated with the
// peer (i.e. weighted_vote == 0), then return a weight of 1.
// Otherwise, return the calculated weight.
weighted_vote as u32 + DEFAULT_WEIGHT
}
}
impl<'a> ChooseGossipPeerStrategy for ChooseWeightedPeerStrategy<'a> {
fn choose_peer<'b>(&self, options: Vec<&'b ReplicatedData>) -> Result<&'b ReplicatedData> {
if options.len() < 1 {
return Err(Error::CrdtTooSmall);
}
let mut weighted_peers = vec![];
for peer in options {
let weight = self.calculate_weighted_remote_index(peer.id);
weighted_peers.push(Weighted { weight, item: peer });
}
let mut rng = thread_rng();
Ok(WeightedChoice::new(&mut weighted_peers).sample(&mut rng))
}
}
#[cfg(test)]
mod tests {
use choose_gossip_peer_strategy::{ChooseWeightedPeerStrategy, DEFAULT_WEIGHT};
use logger;
use signature::{KeyPair, KeyPairUtil, PublicKey};
use std;
use std::collections::HashMap;
fn get_stake(_id: PublicKey) -> f64 {
1.0
}
#[test]
fn test_default() {
logger::setup();
// Initialize the filler keys
let key1 = KeyPair::new().pubkey();
let remote: HashMap<PublicKey, u64> = HashMap::new();
let external_liveness: HashMap<PublicKey, HashMap<PublicKey, u64>> = HashMap::new();
let weighted_strategy =
ChooseWeightedPeerStrategy::new(&remote, &external_liveness, &get_stake);
// If external_liveness table doesn't contain this entry,
// return the default weight
let result = weighted_strategy.calculate_weighted_remote_index(key1);
assert_eq!(result, DEFAULT_WEIGHT);
}
#[test]
fn test_only_external_liveness() {
logger::setup();
// Initialize the filler keys
let key1 = KeyPair::new().pubkey();
let key2 = KeyPair::new().pubkey();
let remote: HashMap<PublicKey, u64> = HashMap::new();
let mut external_liveness: HashMap<PublicKey, HashMap<PublicKey, u64>> = HashMap::new();
// If only the liveness table contains the entry, should return the
// weighted liveness entries
let test_value: u32 = 5;
let mut rumors: HashMap<PublicKey, u64> = HashMap::new();
rumors.insert(key2, test_value as u64);
external_liveness.insert(key1, rumors);
let weighted_strategy =
ChooseWeightedPeerStrategy::new(&remote, &external_liveness, &get_stake);
let result = weighted_strategy.calculate_weighted_remote_index(key1);
assert_eq!(result, test_value + DEFAULT_WEIGHT);
}
#[test]
fn test_overflow_votes() {
logger::setup();
// Initialize the filler keys
let key1 = KeyPair::new().pubkey();
let key2 = KeyPair::new().pubkey();
let remote: HashMap<PublicKey, u64> = HashMap::new();
let mut external_liveness: HashMap<PublicKey, HashMap<PublicKey, u64>> = HashMap::new();
// If the vote index is greater than u32::MAX, default to u32::MAX
let test_value = (std::u32::MAX as u64) + 10;
let mut rumors: HashMap<PublicKey, u64> = HashMap::new();
rumors.insert(key2, test_value);
external_liveness.insert(key1, rumors);
let weighted_strategy =
ChooseWeightedPeerStrategy::new(&remote, &external_liveness, &get_stake);
let result = weighted_strategy.calculate_weighted_remote_index(key1);
assert_eq!(result, std::u32::MAX);
}
#[test]
fn test_many_validators() {
logger::setup();
// Initialize the filler keys
let key1 = KeyPair::new().pubkey();
let mut remote: HashMap<PublicKey, u64> = HashMap::new();
let mut external_liveness: HashMap<PublicKey, HashMap<PublicKey, u64>> = HashMap::new();
// Test many validators' rumors in external_liveness
let num_peers = 10;
let mut rumors: HashMap<PublicKey, u64> = HashMap::new();
remote.insert(key1, 0);
for i in 0..num_peers {
let pk = KeyPair::new().pubkey();
rumors.insert(pk, i);
}
external_liveness.insert(key1, rumors);
let weighted_strategy =
ChooseWeightedPeerStrategy::new(&remote, &external_liveness, &get_stake);
let result = weighted_strategy.calculate_weighted_remote_index(key1);
assert_eq!(result, (num_peers / 2) as u32);
}
#[test]
fn test_many_validators2() {
logger::setup();
// Initialize the filler keys
let key1 = KeyPair::new().pubkey();
let mut remote: HashMap<PublicKey, u64> = HashMap::new();
let mut external_liveness: HashMap<PublicKey, HashMap<PublicKey, u64>> = HashMap::new();
// Test many validators' rumors in external_liveness
let num_peers = 10;
let old_index = 20;
let mut rumors: HashMap<PublicKey, u64> = HashMap::new();
remote.insert(key1, old_index);
for _i in 0..num_peers {
let pk = KeyPair::new().pubkey();
rumors.insert(pk, old_index);
}
external_liveness.insert(key1, rumors);
let weighted_strategy =
ChooseWeightedPeerStrategy::new(&remote, &external_liveness, &get_stake);
let result = weighted_strategy.calculate_weighted_remote_index(key1);
// If nobody has seen a newer update then revert to default
assert_eq!(result, DEFAULT_WEIGHT);
}
}

View File

@ -1,5 +1,6 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::time::Duration;
use timing;
pub struct Counter {
pub name: &'static str,
@ -34,16 +35,14 @@ impl Counter {
let nanos = self.nanos.fetch_add(total as usize, Ordering::Relaxed);
let times = self.times.fetch_add(1, Ordering::Relaxed);
if times % self.lograte == 0 && times > 0 {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let now_ms = now.as_secs() * 1_000 + now.subsec_nanos() as u64 / 1_000_000;
info!(
"COUNTER:{{\"name:\":\"{}\", \"counts\": {}, \"nanos\": {}, \"samples\": {} \"rate\": {}, \"now\": {}}}",
"COUNTER:{{\"name\": \"{}\", \"counts\": {}, \"nanos\": {}, \"samples\": {}, \"rate\": {}, \"now\": {}}}",
self.name,
counts,
nanos,
times,
counts as f64 * 1e9 / nanos as f64,
now_ms,
timing::timestamp(),
);
}
}

View File

@ -15,14 +15,14 @@
use bincode::{deserialize, serialize};
use byteorder::{LittleEndian, ReadBytesExt};
use choose_gossip_peer_strategy::{ChooseGossipPeerStrategy, ChooseWeightedPeerStrategy};
use hash::Hash;
use packet::{to_blob, Blob, BlobRecycler, SharedBlob, BLOB_SIZE};
use pnet::datalink;
use pnet_datalink as datalink;
use rayon::prelude::*;
use result::{Error, Result};
use ring::rand::{SecureRandom, SystemRandom};
use signature::{KeyPair, KeyPairUtil};
use signature::{PublicKey, Signature};
use signature::{KeyPair, KeyPairUtil, PublicKey, Signature};
use std;
use std::collections::HashMap;
use std::collections::VecDeque;
@ -32,7 +32,15 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::thread::{sleep, Builder, JoinHandle};
use std::time::Duration;
use streamer::{BlobReceiver, BlobSender};
use streamer::{BlobReceiver, BlobSender, Window};
use timing::timestamp;
/// milliseconds we sleep for between gossip requests
const GOSSIP_SLEEP_MILLIS: u64 = 100;
//const GOSSIP_MIN_PURGE_MILLIS: u64 = 15000;
/// minimum membership table size before we start purging dead nodes
const MIN_TABLE_SIZE: usize = 2;
pub fn parse_port_or_addr(optstr: Option<String>) -> SocketAddr {
let daddr: SocketAddr = "0.0.0.0:8000".parse().expect("default socket address");
@ -55,7 +63,20 @@ pub fn get_ip_addr() -> Option<IpAddr> {
for iface in datalink::interfaces() {
for p in iface.ips {
if !p.ip().is_loopback() && !p.ip().is_multicast() {
return Some(p.ip());
match p.ip() {
IpAddr::V4(addr) => {
if !addr.is_link_local() {
return Some(p.ip());
}
}
IpAddr::V6(_addr) => {
// Select an ipv6 address if the config is selected
#[cfg(feature = "ipv6")]
{
return Some(p.ip());
}
}
}
}
}
}
@ -134,6 +155,17 @@ impl ReplicatedData {
repair_addr,
)
}
pub fn new_entry_point(gossip_addr: SocketAddr) -> Self {
let daddr: SocketAddr = "0.0.0.0:0".parse().unwrap();
ReplicatedData::new(
PublicKey::default(),
gossip_addr,
daddr.clone(),
daddr.clone(),
daddr.clone(),
daddr,
)
}
}
/// `Crdt` structure keeps a table of `ReplicatedData` structs
@ -157,8 +189,10 @@ pub struct Crdt {
/// The value of the remote update index that I have last seen
/// This Node will ask external nodes for updates since the value in this list
pub remote: HashMap<PublicKey, u64>,
pub alive: HashMap<PublicKey, u64>,
pub update_index: u64,
pub me: PublicKey,
external_liveness: HashMap<PublicKey, HashMap<PublicKey, u64>>,
}
// TODO These messages should be signed, and go through the gpu pipeline for spam filtering
#[derive(Serialize, Deserialize, Debug)]
@ -169,7 +203,7 @@ enum Protocol {
RequestUpdates(u64, ReplicatedData),
//TODO might need a since?
/// from id, form's last update index, ReplicatedData
ReceiveUpdates(PublicKey, u64, Vec<ReplicatedData>),
ReceiveUpdates(PublicKey, u64, Vec<ReplicatedData>, Vec<(PublicKey, u64)>),
/// ask for a missing index
RequestWindowIndex(ReplicatedData, u64),
}
@ -181,6 +215,8 @@ impl Crdt {
table: HashMap::new(),
local: HashMap::new(),
remote: HashMap::new(),
alive: HashMap::new(),
external_liveness: HashMap::new(),
me: me.id,
update_index: 1,
};
@ -191,17 +227,22 @@ impl Crdt {
pub fn my_data(&self) -> &ReplicatedData {
&self.table[&self.me]
}
pub fn leader_data(&self) -> &ReplicatedData {
&self.table[&self.table[&self.me].current_leader_id]
pub fn leader_data(&self) -> Option<&ReplicatedData> {
self.table.get(&(self.table[&self.me].current_leader_id))
}
pub fn set_leader(&mut self, key: PublicKey) -> () {
let mut me = self.my_data().clone();
me.current_leader_id = key;
me.version += 1;
info!("setting leader to {:?}", &key[..4]);
self.insert(&me);
}
pub fn get_external_liveness_entry(&self, key: &PublicKey) -> Option<&HashMap<PublicKey, u64>> {
self.external_liveness.get(key)
}
pub fn insert(&mut self, v: &ReplicatedData) {
// TODO check that last_verified types are always increasing
if self.table.get(&v.id).is_none() || (v.version > self.table[&v.id].version) {
@ -225,6 +266,48 @@ impl Crdt {
self.table[&v.id].version
);
}
//update the liveness table
let now = timestamp();
*self.alive.entry(v.id).or_insert(now) = now;
}
/// purge old validators
/// TODO: we need a robust membership protocol
/// http://asc.di.fct.unl.pt/~jleitao/pdf/dsn07-leitao.pdf
/// challenging part is that we are on a permissionless network
pub fn purge(&mut self, now: u64) {
if self.table.len() <= MIN_TABLE_SIZE {
return;
}
//wait for 4x as long as it would randomly take to reach our node
//assuming everyone is waiting the same amount of time as this node
let limit = self.table.len() as u64 * GOSSIP_SLEEP_MILLIS * 4;
//let limit = std::cmp::max(limit, GOSSIP_MIN_PURGE_MILLIS);
let dead_ids: Vec<PublicKey> = self.alive
.iter()
.filter_map(|(&k, v)| {
if k != self.me && (now - v) > limit {
info!("purge {:?} {}", &k[..4], now - v);
Some(k)
} else {
trace!("purge skipped {:?} {} {}", &k[..4], now - v, limit);
None
}
})
.collect();
for id in dead_ids.iter() {
self.alive.remove(id);
self.table.remove(id);
self.remote.remove(id);
self.local.remove(id);
self.external_liveness.remove(id);
for map in self.external_liveness.values_mut() {
map.remove(id);
}
}
}
pub fn index_blobs(
@ -255,7 +338,7 @@ impl Crdt {
/// We need to avoid having obj locked while doing any io, such as the `send_to`
pub fn broadcast(
obj: &Arc<RwLock<Self>>,
window: &Arc<RwLock<Vec<Option<SharedBlob>>>>,
window: &Window,
s: &UdpSocket,
transmit_index: &mut u64,
received_index: u64,
@ -407,11 +490,17 @@ impl Crdt {
rdr.read_u64::<LittleEndian>()
.expect("rdr.read_u64 in fn random")
}
// TODO: fill in with real implmentation once staking is implemented
fn get_stake(_id: PublicKey) -> f64 {
1.0
}
fn get_updates_since(&self, v: u64) -> (PublicKey, u64, Vec<ReplicatedData>) {
//trace!("get updates since {}", v);
let data = self.table
.values()
.filter(|x| self.local[&x.id] > v)
.filter(|x| x.id != PublicKey::default() && self.local[&x.id] > v)
.cloned()
.collect();
let id = self.me;
@ -442,16 +531,28 @@ impl Crdt {
/// * B - RequestUpdates protocol message
fn gossip_request(&self) -> Result<(SocketAddr, Protocol)> {
let options: Vec<_> = self.table.values().filter(|v| v.id != self.me).collect();
if options.len() < 1 {
trace!(
"crdt too small for gossip {:?} {}",
&self.me[..4],
self.table.len()
);
return Err(Error::CrdtTooSmall);
}
let n = (Self::random() as usize) % options.len();
let v = options[n].clone();
let choose_peer_strategy = ChooseWeightedPeerStrategy::new(
&self.remote,
&self.external_liveness,
&Self::get_stake,
);
let choose_peer_result = choose_peer_strategy.choose_peer(options);
let v = match choose_peer_result {
Ok(peer) => peer,
Err(Error::CrdtTooSmall) => {
trace!(
"crdt too small for gossip {:?} {}",
&self.me[..4],
self.table.len()
);
return Err(Error::CrdtTooSmall);
}
Err(e) => return Err(e),
};
let remote_update_index = *self.remote.get(&v.id).unwrap_or(&0);
let req = Protocol::RequestUpdates(remote_update_index, self.table[&self.me].clone());
trace!(
@ -460,6 +561,7 @@ impl Crdt {
&v.id[..4],
v.gossip_addr
);
Ok((v.gossip_addr, req))
}
@ -477,6 +579,7 @@ impl Crdt {
let (remote_gossip_addr, req) = obj.read()
.expect("'obj' read lock in fn run_gossip")
.gossip_request()?;
// TODO this will get chatty, so we need to first ask for number of updates since
// then only ask for specific data that we dont have
let blob = to_blob(req, remote_gossip_addr, blob_recycler)?;
@ -485,20 +588,78 @@ impl Crdt {
blob_sender.send(q)?;
Ok(())
}
/// TODO: This is obviously the wrong way to do this. Need to implement leader selection
fn top_leader(&self) -> Option<PublicKey> {
let mut table = HashMap::new();
let def = PublicKey::default();
let cur = self.table.values().filter(|x| x.current_leader_id != def);
for v in cur {
let cnt = table.entry(&v.current_leader_id).or_insert(0);
*cnt += 1;
trace!("leader {:?} {}", &v.current_leader_id[..4], *cnt);
}
let mut sorted: Vec<(&PublicKey, usize)> = table.into_iter().collect();
if sorted.len() > 0 {
info!("sorted leaders {:?}", sorted);
}
sorted.sort_by_key(|a| a.1);
sorted.last().map(|a| *a.0)
}
/// TODO: This is obviously the wrong way to do this. Need to implement leader selection
/// A t-shirt for the first person to actually use this bad behavior to attack the alpha testnet
fn update_leader(&mut self) {
if let Some(leader_id) = self.top_leader() {
if self.my_data().current_leader_id != leader_id {
if self.table.get(&leader_id).is_some() {
self.set_leader(leader_id);
}
}
}
}
/// Apply updates that we received from the identity `from`
/// # Arguments
/// * `from` - identity of the sender of the updates
/// * `update_index` - the number of updates that `from` has completed and this set of `data` represents
/// * `data` - the update data
fn apply_updates(&mut self, from: PublicKey, update_index: u64, data: &[ReplicatedData]) {
fn apply_updates(
&mut self,
from: PublicKey,
update_index: u64,
data: &[ReplicatedData],
external_liveness: &[(PublicKey, u64)],
) {
trace!("got updates {}", data.len());
// TODO we need to punish/spam resist here
// sig verify the whole update and slash anyone who sends a bad update
for v in data {
self.insert(&v);
}
for (pk, external_remote_index) in external_liveness {
let remote_entry = if let Some(v) = self.remote.get(pk) {
*v
} else {
0
};
if remote_entry >= *external_remote_index {
continue;
}
let liveness_entry = self.external_liveness.entry(*pk).or_insert(HashMap::new());
let peer_index = *liveness_entry.entry(from).or_insert(*external_remote_index);
if *external_remote_index > peer_index {
liveness_entry.insert(from, *external_remote_index);
}
}
*self.remote.entry(from).or_insert(update_index) = update_index;
// Clear the remote liveness table for this node, b/c we've heard directly from them
// so we don't need to rely on rumors
self.external_liveness.remove(&from);
}
/// randomly pick a node and ask them for updates asynchronously
@ -511,68 +672,100 @@ impl Crdt {
Builder::new()
.name("solana-gossip".to_string())
.spawn(move || loop {
let start = timestamp();
let _ = Self::run_gossip(&obj, &blob_sender, &blob_recycler);
obj.write().unwrap().purge(timestamp());
if exit.load(Ordering::Relaxed) {
return;
}
//TODO: possibly tune this parameter
//we saw a deadlock passing an obj.read().unwrap().timeout into sleep
sleep(Duration::from_millis(100));
let _ = obj.write().unwrap().update_leader();
let elapsed = timestamp() - start;
if GOSSIP_SLEEP_MILLIS > elapsed {
let time_left = GOSSIP_SLEEP_MILLIS - elapsed;
sleep(Duration::from_millis(time_left));
}
})
.unwrap()
}
fn run_window_request(
window: &Arc<RwLock<Vec<Option<SharedBlob>>>>,
window: &Window,
me: &ReplicatedData,
from: &ReplicatedData,
ix: u64,
blob_recycler: &BlobRecycler,
) -> Option<SharedBlob> {
let pos = (ix as usize) % window.read().unwrap().len();
if let Some(blob) = &window.read().unwrap()[pos] {
let rblob = blob.read().unwrap();
let blob_ix = rblob.get_index().expect("run_window_request get_index");
let mut wblob = blob.write().unwrap();
let blob_ix = wblob.get_index().expect("run_window_request get_index");
if blob_ix == ix {
let num_retransmits = wblob.meta.num_retransmits;
wblob.meta.num_retransmits += 1;
// Setting the sender id to the requester id
// prevents the requester from retransmitting this response
// to other peers
let mut sender_id = from.id;
// Allow retransmission of this response if the node
// is the leader and the number of repair requests equals
// a power of two
if me.current_leader_id == me.id
&& (num_retransmits == 0 || num_retransmits.is_power_of_two())
{
sender_id = me.id
}
let out = blob_recycler.allocate();
// copy to avoid doing IO inside the lock
{
let mut outblob = out.write().unwrap();
let sz = rblob.meta.size;
let sz = wblob.meta.size;
outblob.meta.size = sz;
outblob.data[..sz].copy_from_slice(&rblob.data[..sz]);
outblob.data[..sz].copy_from_slice(&wblob.data[..sz]);
outblob.meta.set_addr(&from.repair_addr);
//TODO, set the sender id to the requester so we dont retransmit
//come up with a cleaner solution for this when sender signatures are checked
outblob.set_id(from.id).expect("blob set_id");
outblob.set_id(sender_id).expect("blob set_id");
}
return Some(out);
}
} else {
assert!(window.read().unwrap()[pos].is_none());
info!("failed RequestWindowIndex {} {}", ix, from.repair_addr);
info!(
"failed RequestWindowIndex {} {} {}",
ix, pos, from.repair_addr
);
}
None
}
//TODO we should first coalesce all the requests
fn handle_blob(
obj: &Arc<RwLock<Self>>,
window: &Arc<RwLock<Vec<Option<SharedBlob>>>>,
window: &Window,
blob_recycler: &BlobRecycler,
blob: &Blob,
) -> Option<SharedBlob> {
match deserialize(&blob.data[..blob.meta.size]) {
// TODO sigverify these
Ok(Protocol::RequestUpdates(v, reqdata)) => {
Ok(Protocol::RequestUpdates(v, from_rd)) => {
trace!("RequestUpdates {}", v);
let addr = reqdata.gossip_addr;
// only lock for this call, dont lock during IO `sock.send_to` or `sock.recv_from`
let (from, ups, data) = obj.read()
.expect("'obj' read lock in RequestUpdates")
.get_updates_since(v);
let addr = from_rd.gossip_addr;
let me = obj.read().unwrap();
// only lock for these two calls, dont lock during IO `sock.send_to` or `sock.recv_from`
let (from, ups, data) = me.get_updates_since(v);
let external_liveness = me.remote
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
drop(me);
trace!("get updates since response {} {}", v, data.len());
let len = data.len();
let rsp = Protocol::ReceiveUpdates(from, ups, data);
obj.write().unwrap().insert(&reqdata);
let rsp = Protocol::ReceiveUpdates(from, ups, data, external_liveness);
obj.write().unwrap().insert(&from_rd);
if len < 1 {
let me = obj.read().unwrap();
trace!(
@ -587,7 +780,7 @@ impl Crdt {
"sending updates me {:?} len {} to {:?} {}",
&obj.read().unwrap().me[..4],
len,
&reqdata.id[..4],
&from_rd.id[..4],
addr,
);
Some(r)
@ -596,11 +789,11 @@ impl Crdt {
None
}
}
Ok(Protocol::ReceiveUpdates(from, ups, data)) => {
Ok(Protocol::ReceiveUpdates(from, ups, data, external_liveness)) => {
trace!("ReceivedUpdates {:?} {} {}", &from[0..4], ups, data.len());
obj.write()
.expect("'obj' write lock in ReceiveUpdates")
.apply_updates(from, ups, &data);
.apply_updates(from, ups, &data, &external_liveness);
None
}
Ok(Protocol::RequestWindowIndex(from, ix)) => {
@ -616,7 +809,7 @@ impl Crdt {
me.repair_addr
);
assert_ne!(from.repair_addr, me.repair_addr);
Self::run_window_request(&window, &from, ix, blob_recycler)
Self::run_window_request(&window, &me, &from, ix, blob_recycler)
}
Err(_) => {
warn!("deserialize crdt packet failed");
@ -628,7 +821,7 @@ impl Crdt {
/// Process messages from the network
fn run_listen(
obj: &Arc<RwLock<Self>>,
window: &Arc<RwLock<Vec<Option<SharedBlob>>>>,
window: &Window,
blob_recycler: &BlobRecycler,
requests_receiver: &BlobReceiver,
response_sender: &BlobSender,
@ -650,7 +843,7 @@ impl Crdt {
}
pub fn listen(
obj: Arc<RwLock<Self>>,
window: Arc<RwLock<Vec<Option<SharedBlob>>>>,
window: Window,
blob_recycler: BlobRecycler,
requests_receiver: BlobReceiver,
response_sender: BlobSender,
@ -689,6 +882,7 @@ pub struct Sockets {
pub respond: UdpSocket,
pub broadcast: UdpSocket,
pub repair: UdpSocket,
pub retransmit: UdpSocket,
}
pub struct TestNode {
@ -698,14 +892,15 @@ pub struct TestNode {
impl TestNode {
pub fn new() -> TestNode {
let gossip = UdpSocket::bind("0.0.0.0:0").unwrap();
let gossip_send = UdpSocket::bind("0.0.0.0:0").unwrap();
let requests = UdpSocket::bind("0.0.0.0:0").unwrap();
let transaction = UdpSocket::bind("0.0.0.0:0").unwrap();
let gossip = UdpSocket::bind("0.0.0.0:0").unwrap();
let replicate = UdpSocket::bind("0.0.0.0:0").unwrap();
let requests = UdpSocket::bind("0.0.0.0:0").unwrap();
let repair = UdpSocket::bind("0.0.0.0:0").unwrap();
let gossip_send = UdpSocket::bind("0.0.0.0:0").unwrap();
let respond = UdpSocket::bind("0.0.0.0:0").unwrap();
let broadcast = UdpSocket::bind("0.0.0.0:0").unwrap();
let repair = UdpSocket::bind("0.0.0.0:0").unwrap();
let retransmit = UdpSocket::bind("0.0.0.0:0").unwrap();
let pubkey = KeyPair::new().pubkey();
let data = ReplicatedData::new(
pubkey,
@ -726,6 +921,52 @@ impl TestNode {
respond,
broadcast,
repair,
retransmit,
},
}
}
pub fn new_with_bind_addr(data: ReplicatedData, bind_addr: SocketAddr) -> TestNode {
let mut local_gossip_addr = bind_addr.clone();
local_gossip_addr.set_port(data.gossip_addr.port());
let mut local_replicate_addr = bind_addr.clone();
local_replicate_addr.set_port(data.replicate_addr.port());
let mut local_requests_addr = bind_addr.clone();
local_requests_addr.set_port(data.requests_addr.port());
let mut local_transactions_addr = bind_addr.clone();
local_transactions_addr.set_port(data.transactions_addr.port());
let mut local_repair_addr = bind_addr.clone();
local_repair_addr.set_port(data.repair_addr.port());
let transaction = UdpSocket::bind(local_transactions_addr).unwrap();
let gossip = UdpSocket::bind(local_gossip_addr).unwrap();
let replicate = UdpSocket::bind(local_replicate_addr).unwrap();
let repair = UdpSocket::bind(local_repair_addr).unwrap();
let requests = UdpSocket::bind(local_requests_addr).unwrap();
// Responses are sent from the same Udp port as requests are received
// from, in hopes that a NAT sitting in the middle will route the
// response Udp packet correctly back to the requester.
let respond = requests.try_clone().unwrap();
let gossip_send = UdpSocket::bind("0.0.0.0:0").unwrap();
let broadcast = UdpSocket::bind("0.0.0.0:0").unwrap();
let retransmit = UdpSocket::bind("0.0.0.0:0").unwrap();
TestNode {
data: data,
sockets: Sockets {
gossip,
gossip_send,
requests,
replicate,
transaction,
respond,
broadcast,
repair,
retransmit,
},
}
}
@ -733,13 +974,15 @@ impl TestNode {
#[cfg(test)]
mod tests {
use crdt::{parse_port_or_addr, Crdt, ReplicatedData};
use crdt::{parse_port_or_addr, Crdt, ReplicatedData, GOSSIP_SLEEP_MILLIS, MIN_TABLE_SIZE};
use logger;
use packet::BlobRecycler;
use result::Error;
use signature::{KeyPair, KeyPairUtil};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock};
use std::thread::sleep;
use std::time::Duration;
use streamer::default_window;
@ -829,14 +1072,21 @@ mod tests {
assert_eq!(key, d1.id);
assert_eq!(ix, 3);
assert_eq!(ups.len(), 3);
assert_eq!(sorted(&ups), sorted(&vec![d2.clone(), d1, d3]));
assert_eq!(
sorted(&ups),
sorted(&vec![d1.clone(), d2.clone(), d3.clone()])
);
let mut crdt2 = Crdt::new(d2.clone());
crdt2.apply_updates(key, ix, &ups);
crdt2.apply_updates(key, ix, &ups, &vec![]);
assert_eq!(crdt2.table.values().len(), 3);
assert_eq!(
sorted(&crdt2.table.values().map(|x| x.clone()).collect()),
sorted(&crdt.table.values().map(|x| x.clone()).collect())
);
let d4 = ReplicatedData::new_entry_point("127.0.0.4:1234".parse().unwrap());
crdt.insert(&d4);
let (_key, _ix, ups) = crdt.get_updates_since(0);
assert_eq!(sorted(&ups), sorted(&vec![d2.clone(), d1, d3]));
}
#[test]
fn window_index_request() {
@ -927,14 +1177,7 @@ mod tests {
let rv = crdt.gossip_request().unwrap();
assert_eq!(rv.0, nxt1.gossip_addr);
let nxt2 = ReplicatedData::new(
KeyPair::new().pubkey(),
"127.0.0.3:1234".parse().unwrap(),
"127.0.0.1:1235".parse().unwrap(),
"127.0.0.1:1236".parse().unwrap(),
"127.0.0.1:1237".parse().unwrap(),
"127.0.0.1:1238".parse().unwrap(),
);
let nxt2 = ReplicatedData::new_entry_point("127.0.0.3:1234".parse().unwrap());
crdt.insert(&nxt2);
// check that the service works
// and that it eventually produces a request for both nodes
@ -972,6 +1215,47 @@ mod tests {
assert!(one && two);
}
#[test]
fn purge_test() {
let me = ReplicatedData::new_leader(&"127.0.0.1:1234".parse().unwrap());
let mut crdt = Crdt::new(me.clone());
let nxt = ReplicatedData::new_leader(&"127.0.0.2:1234".parse().unwrap());
assert_ne!(me.id, nxt.id);
crdt.insert(&nxt);
let rv = crdt.gossip_request().unwrap();
assert_eq!(rv.0, nxt.gossip_addr);
let now = crdt.alive[&nxt.id];
let len = crdt.table.len() as u64;
crdt.purge(now);
let rv = crdt.gossip_request().unwrap();
assert_eq!(rv.0, nxt.gossip_addr);
crdt.purge(now + len * GOSSIP_SLEEP_MILLIS * 4);
let rv = crdt.gossip_request().unwrap();
assert_eq!(rv.0, nxt.gossip_addr);
crdt.purge(now + len * GOSSIP_SLEEP_MILLIS * 4 + 1);
let rv = crdt.gossip_request().unwrap();
assert_eq!(rv.0, nxt.gossip_addr);
let nxt2 = ReplicatedData::new_leader(&"127.0.0.2:1234".parse().unwrap());
assert_ne!(me.id, nxt2.id);
assert_ne!(nxt.id, nxt2.id);
crdt.insert(&nxt2);
while now == crdt.alive[&nxt2.id] {
sleep(Duration::from_millis(GOSSIP_SLEEP_MILLIS));
crdt.insert(&nxt2);
}
let len = crdt.table.len() as u64;
assert!((MIN_TABLE_SIZE as u64) < len);
crdt.purge(now + len * GOSSIP_SLEEP_MILLIS * 4);
assert_eq!(len as usize, crdt.table.len());
crdt.purge(now + len * GOSSIP_SLEEP_MILLIS * 4 + 1);
let rv = crdt.gossip_request().unwrap();
assert_eq!(rv.0, nxt.gossip_addr);
assert_eq!(2, crdt.table.len());
}
/// test window requests respond with the right blob, and do not overrun
#[test]
fn run_window_request() {
@ -985,18 +1269,81 @@ mod tests {
"127.0.0.1:1238".parse().unwrap(),
);
let recycler = BlobRecycler::default();
let rv = Crdt::run_window_request(&window, &me, 0, &recycler);
let rv = Crdt::run_window_request(&window, &me, &me, 0, &recycler);
assert!(rv.is_none());
let out = recycler.allocate();
out.write().unwrap().meta.size = 200;
window.write().unwrap()[0] = Some(out);
let rv = Crdt::run_window_request(&window, &me, 0, &recycler);
let rv = Crdt::run_window_request(&window, &me, &me, 0, &recycler);
assert!(rv.is_some());
let v = rv.unwrap();
//test we copied the blob
assert_eq!(v.read().unwrap().meta.size, 200);
let len = window.read().unwrap().len() as u64;
let rv = Crdt::run_window_request(&window, &me, len, &recycler);
let rv = Crdt::run_window_request(&window, &me, &me, len, &recycler);
assert!(rv.is_none());
}
/// test window requests respond with the right blob, and do not overrun
#[test]
fn run_window_request_with_backoff() {
let window = default_window();
let mut me = ReplicatedData::new_leader(&"127.0.0.1:1234".parse().unwrap());
me.current_leader_id = me.id;
let mock_peer = ReplicatedData::new_leader(&"127.0.0.1:1234".parse().unwrap());
let recycler = BlobRecycler::default();
// Simulate handling a repair request from mock_peer
let rv = Crdt::run_window_request(&window, &me, &mock_peer, 0, &recycler);
assert!(rv.is_none());
let blob = recycler.allocate();
let blob_size = 200;
blob.write().unwrap().meta.size = blob_size;
window.write().unwrap()[0] = Some(blob);
let num_requests: u32 = 64;
for i in 0..num_requests {
let shared_blob =
Crdt::run_window_request(&window, &me, &mock_peer, 0, &recycler).unwrap();
let blob = shared_blob.read().unwrap();
// Test we copied the blob
assert_eq!(blob.meta.size, blob_size);
let id = if i == 0 || i.is_power_of_two() {
me.id
} else {
mock_peer.id
};
assert_eq!(blob.get_id().unwrap(), id);
}
}
/// TODO: This is obviously the wrong way to do this. Need to implement leader selection,
/// delete this test after leader selection is correctly implemented
#[test]
fn test_update_leader() {
logger::setup();
let me = ReplicatedData::new_leader(&"127.0.0.1:1234".parse().unwrap());
let leader0 = ReplicatedData::new_leader(&"127.0.0.1:1234".parse().unwrap());
let leader1 = ReplicatedData::new_leader(&"127.0.0.1:1234".parse().unwrap());
let mut crdt = Crdt::new(me.clone());
assert_eq!(crdt.top_leader(), None);
crdt.set_leader(leader0.id);
assert_eq!(crdt.top_leader().unwrap(), leader0.id);
//add a bunch of nodes with a new leader
for _ in 0..10 {
let mut dum = ReplicatedData::new_entry_point("127.0.0.1:1234".parse().unwrap());
dum.id = KeyPair::new().pubkey();
dum.current_leader_id = leader1.id;
crdt.insert(&dum);
}
assert_eq!(crdt.top_leader().unwrap(), leader1.id);
crdt.update_leader();
assert_eq!(crdt.my_data().current_leader_id, leader0.id);
crdt.insert(&leader1);
crdt.update_leader();
assert_eq!(crdt.my_data().current_leader_id, leader1.id);
}
}

313
src/drone.rs Normal file
View File

@ -0,0 +1,313 @@
//! The `drone` module provides an object for launching a Solana Drone,
//! which is the custodian of any remaining tokens in a mint.
//! The Solana Drone builds and send airdrop transactions,
//! checking requests against a request cap for a given time time_slice
//! and (to come) an IP rate limit.
use signature::{KeyPair, PublicKey};
use std::io;
use std::io::{Error, ErrorKind};
use std::net::{IpAddr, SocketAddr, UdpSocket};
use std::time::Duration;
use thin_client::ThinClient;
use transaction::Transaction;
pub const TIME_SLICE: u64 = 60;
pub const REQUEST_CAP: u64 = 150_000;
#[derive(Serialize, Deserialize, Debug)]
pub enum DroneRequest {
GetAirdrop {
airdrop_request_amount: u64,
client_public_key: PublicKey,
},
}
pub struct Drone {
mint_keypair: KeyPair,
ip_cache: Vec<IpAddr>,
_airdrop_addr: SocketAddr,
transactions_addr: SocketAddr,
requests_addr: SocketAddr,
pub time_slice: Duration,
request_cap: u64,
pub request_current: u64,
}
impl Drone {
pub fn new(
mint_keypair: KeyPair,
_airdrop_addr: SocketAddr,
transactions_addr: SocketAddr,
requests_addr: SocketAddr,
time_input: Option<u64>,
request_cap_input: Option<u64>,
) -> Drone {
let time_slice = match time_input {
Some(time) => Duration::new(time, 0),
None => Duration::new(TIME_SLICE, 0),
};
let request_cap = match request_cap_input {
Some(cap) => cap,
None => REQUEST_CAP,
};
Drone {
mint_keypair,
ip_cache: Vec::new(),
_airdrop_addr,
transactions_addr,
requests_addr,
time_slice,
request_cap,
request_current: 0,
}
}
pub fn check_request_limit(&mut self, request_amount: u64) -> bool {
(self.request_current + request_amount) <= self.request_cap
}
pub fn clear_request_count(&mut self) {
self.request_current = 0;
}
pub fn add_ip_to_cache(&mut self, ip: IpAddr) {
self.ip_cache.push(ip);
}
pub fn clear_ip_cache(&mut self) {
self.ip_cache.clear();
}
pub fn check_rate_limit(&mut self, ip: IpAddr) -> Result<IpAddr, IpAddr> {
// [WIP] This is placeholder code for a proper rate limiter.
// Right now it will only allow one total drone request per IP
if self.ip_cache.contains(&ip) {
// Add proper error handling here
Err(ip)
} else {
self.add_ip_to_cache(ip);
Ok(ip)
}
}
pub fn send_airdrop(&mut self, req: DroneRequest) -> Result<usize, io::Error> {
let tx: Transaction;
let request_amount: u64;
let requests_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let mut client = ThinClient::new(
self.requests_addr,
requests_socket,
self.transactions_addr,
transactions_socket,
);
let last_id = client.get_last_id();
match req {
DroneRequest::GetAirdrop {
airdrop_request_amount,
client_public_key,
} => {
request_amount = airdrop_request_amount.clone();
tx = Transaction::new(
&self.mint_keypair,
client_public_key,
airdrop_request_amount as i64,
last_id,
);
}
}
if self.check_request_limit(request_amount) {
self.request_current += request_amount;
client.transfer_signed(tx)
} else {
Err(Error::new(ErrorKind::Other, "token limit reached"))
}
}
}
#[cfg(test)]
mod tests {
use bank::Bank;
use crdt::{get_ip_addr, TestNode};
use drone::{Drone, DroneRequest, REQUEST_CAP, TIME_SLICE};
use fullnode::FullNode;
use logger;
use mint::Mint;
use signature::{KeyPair, KeyPairUtil};
use std::io::sink;
use std::net::{SocketAddr, UdpSocket};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use thin_client::ThinClient;
#[test]
fn test_check_request_limit() {
let keypair = KeyPair::new();
let mut addr: SocketAddr = "0.0.0.0:9900".parse().unwrap();
addr.set_ip(get_ip_addr().unwrap());
let transactions_addr = "0.0.0.0:0".parse().unwrap();
let requests_addr = "0.0.0.0:0".parse().unwrap();
let mut drone = Drone::new(
keypair,
addr,
transactions_addr,
requests_addr,
None,
Some(3),
);
assert!(drone.check_request_limit(1));
drone.request_current = 3;
assert!(!drone.check_request_limit(1));
}
#[test]
fn test_clear_request_count() {
let keypair = KeyPair::new();
let mut addr: SocketAddr = "0.0.0.0:9900".parse().unwrap();
addr.set_ip(get_ip_addr().unwrap());
let transactions_addr = "0.0.0.0:0".parse().unwrap();
let requests_addr = "0.0.0.0:0".parse().unwrap();
let mut drone = Drone::new(keypair, addr, transactions_addr, requests_addr, None, None);
drone.request_current = drone.request_current + 256;
assert_eq!(drone.request_current, 256);
drone.clear_request_count();
assert_eq!(drone.request_current, 0);
}
#[test]
fn test_add_ip_to_cache() {
let keypair = KeyPair::new();
let mut addr: SocketAddr = "0.0.0.0:9900".parse().unwrap();
addr.set_ip(get_ip_addr().unwrap());
let transactions_addr = "0.0.0.0:0".parse().unwrap();
let requests_addr = "0.0.0.0:0".parse().unwrap();
let mut drone = Drone::new(keypair, addr, transactions_addr, requests_addr, None, None);
let ip = "127.0.0.1".parse().expect("create IpAddr from string");
assert_eq!(drone.ip_cache.len(), 0);
drone.add_ip_to_cache(ip);
assert_eq!(drone.ip_cache.len(), 1);
assert!(drone.ip_cache.contains(&ip));
}
#[test]
fn test_clear_ip_cache() {
let keypair = KeyPair::new();
let mut addr: SocketAddr = "0.0.0.0:9900".parse().unwrap();
addr.set_ip(get_ip_addr().unwrap());
let transactions_addr = "0.0.0.0:0".parse().unwrap();
let requests_addr = "0.0.0.0:0".parse().unwrap();
let mut drone = Drone::new(keypair, addr, transactions_addr, requests_addr, None, None);
let ip = "127.0.0.1".parse().expect("create IpAddr from string");
assert_eq!(drone.ip_cache.len(), 0);
drone.add_ip_to_cache(ip);
assert_eq!(drone.ip_cache.len(), 1);
drone.clear_ip_cache();
assert_eq!(drone.ip_cache.len(), 0);
assert!(drone.ip_cache.is_empty());
}
#[test]
fn test_drone_default_init() {
let keypair = KeyPair::new();
let mut addr: SocketAddr = "0.0.0.0:9900".parse().unwrap();
addr.set_ip(get_ip_addr().unwrap());
let transactions_addr = "0.0.0.0:0".parse().unwrap();
let requests_addr = "0.0.0.0:0".parse().unwrap();
let time_slice: Option<u64> = None;
let request_cap: Option<u64> = None;
let drone = Drone::new(
keypair,
addr,
transactions_addr,
requests_addr,
time_slice,
request_cap,
);
assert_eq!(drone.time_slice, Duration::new(TIME_SLICE, 0));
assert_eq!(drone.request_cap, REQUEST_CAP);
}
#[test]
fn test_send_airdrop() {
const SMALL_BATCH: i64 = 50;
const TPS_BATCH: i64 = 5_000_000;
logger::setup();
let leader = TestNode::new();
let alice = Mint::new(10_000_000);
let bank = Bank::new(&alice);
let bob_pubkey = KeyPair::new().pubkey();
let carlos_pubkey = KeyPair::new().pubkey();
let exit = Arc::new(AtomicBool::new(false));
let server = FullNode::new_leader(
bank,
0,
Some(Duration::from_millis(30)),
leader.data.clone(),
leader.sockets.requests,
leader.sockets.transaction,
leader.sockets.broadcast,
leader.sockets.respond,
leader.sockets.gossip,
exit.clone(),
sink(),
);
sleep(Duration::from_millis(900));
let mut addr: SocketAddr = "0.0.0.0:9900".parse().expect("bind to drone socket");
addr.set_ip(get_ip_addr().expect("drone get_ip_addr"));
let mut drone = Drone::new(
alice.keypair(),
addr,
leader.data.transactions_addr,
leader.data.requests_addr,
None,
Some(5_000_050),
);
let bob_req = DroneRequest::GetAirdrop {
airdrop_request_amount: 50,
client_public_key: bob_pubkey,
};
let bob_result = drone.send_airdrop(bob_req).expect("send airdrop test");
assert!(bob_result > 0);
let carlos_req = DroneRequest::GetAirdrop {
airdrop_request_amount: 5_000_000,
client_public_key: carlos_pubkey,
};
let carlos_result = drone.send_airdrop(carlos_req).expect("send airdrop test");
assert!(carlos_result > 0);
let requests_socket = UdpSocket::bind("0.0.0.0:0").expect("drone bind to requests socket");
let transactions_socket =
UdpSocket::bind("0.0.0.0:0").expect("drone bind to transactions socket");
let mut client = ThinClient::new(
leader.data.requests_addr,
requests_socket,
leader.data.transactions_addr,
transactions_socket,
);
let bob_balance = client.poll_get_balance(&bob_pubkey);
info!("Small request balance: {:?}", bob_balance);
assert_eq!(bob_balance.unwrap(), SMALL_BATCH);
let carlos_balance = client.poll_get_balance(&carlos_pubkey);
info!("TPS request balance: {:?}", carlos_balance);
assert_eq!(carlos_balance.unwrap(), TPS_BATCH);
exit.store(true, Ordering::Relaxed);
for t in server.thread_hdls {
t.join().unwrap();
}
}
}

View File

@ -2,7 +2,9 @@
//! unique ID that is the hash of the Entry before it, plus the hash of the
//! transactions within it. Entries cannot be reordered, and its field `num_hashes`
//! represents an approximate amount of time since the last Entry was created.
use bincode::serialized_size;
use hash::{extend_and_hash, hash, Hash};
use packet::BLOB_DATA_SIZE;
use rayon::prelude::*;
use transaction::Transaction;
@ -33,18 +35,47 @@ pub struct Entry {
/// generated. The may have been observed before a previous Entry ID but were
/// pushed back into this list to ensure deterministic interpretation of the ledger.
pub transactions: Vec<Transaction>,
/// Indication that:
/// 1. the next Entry in the ledger has transactions that can potentially
/// be verified in parallel with these transactions
/// 2. this Entry can be left out of the bank's entry_id cache for
/// purposes of duplicate rejection
pub has_more: bool,
/// Erasure requires that Entry be a multiple of 4 bytes in size
pad: [u8; 3],
}
impl Entry {
/// Creates the next Entry `num_hashes` after `start_hash`.
pub fn new(start_hash: &Hash, cur_hashes: u64, transactions: Vec<Transaction>) -> Self {
pub fn new(
start_hash: &Hash,
cur_hashes: u64,
transactions: Vec<Transaction>,
has_more: bool,
) -> Self {
let num_hashes = cur_hashes + if transactions.is_empty() { 0 } else { 1 };
let id = next_hash(start_hash, 0, &transactions);
Entry {
let entry = Entry {
num_hashes,
id,
transactions,
}
has_more,
pad: [0, 0, 0],
};
assert!(serialized_size(&entry).unwrap() <= BLOB_DATA_SIZE as u64);
entry
}
pub fn will_fit(transactions: Vec<Transaction>) -> bool {
serialized_size(&Entry {
num_hashes: 0,
id: Hash::default(),
transactions,
has_more: false,
pad: [0, 0, 0],
}).unwrap() <= BLOB_DATA_SIZE as u64
}
/// Creates the next Tick Entry `num_hashes` after `start_hash`.
@ -52,10 +83,12 @@ impl Entry {
start_hash: &mut Hash,
cur_hashes: &mut u64,
transactions: Vec<Transaction>,
has_more: bool,
) -> Self {
let entry = Self::new(start_hash, *cur_hashes, transactions);
let entry = Self::new(start_hash, *cur_hashes, transactions, has_more);
*start_hash = entry.id;
*cur_hashes = 0;
assert!(serialized_size(&entry).unwrap() <= BLOB_DATA_SIZE as u64);
entry
}
@ -66,6 +99,8 @@ impl Entry {
num_hashes,
id: *id,
transactions: vec![],
has_more: false,
pad: [0, 0, 0],
}
}
@ -84,8 +119,9 @@ fn add_transaction_data(hash_data: &mut Vec<u8>, tx: &Transaction) {
/// 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.
pub fn next_hash(start_hash: &Hash, num_hashes: u64, transactions: &[Transaction]) -> Hash {
/// the signature. If num_hashes is zero and there's no transaction data,
/// start_hash is returned.
fn next_hash(start_hash: &Hash, num_hashes: u64, transactions: &[Transaction]) -> Hash {
let mut id = *start_hash;
for _ in 1..num_hashes {
id = hash(&id);
@ -108,10 +144,13 @@ pub fn next_hash(start_hash: &Hash, num_hashes: u64, transactions: &[Transaction
/// Creates the next Tick or Transaction Entry `num_hashes` after `start_hash`.
pub fn next_entry(start_hash: &Hash, num_hashes: u64, transactions: Vec<Transaction>) -> Entry {
assert!(num_hashes > 0 || transactions.len() == 0);
Entry {
num_hashes,
id: next_hash(start_hash, num_hashes, &transactions),
transactions,
has_more: false,
pad: [0, 0, 0],
}
}
@ -142,7 +181,7 @@ mod tests {
let keypair = KeyPair::new();
let tx0 = Transaction::new(&keypair, keypair.pubkey(), 0, zero);
let tx1 = Transaction::new(&keypair, keypair.pubkey(), 1, zero);
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()]);
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()], false);
assert!(e0.verify(&zero));
// Next, swap two transactions and ensure verification fails.
@ -159,7 +198,7 @@ mod tests {
let keypair = KeyPair::new();
let tx0 = Transaction::new_timestamp(&keypair, Utc::now(), zero);
let tx1 = Transaction::new_signature(&keypair, Default::default(), zero);
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()]);
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()], false);
assert!(e0.verify(&zero));
// Next, swap two witness transactions and ensure verification fails.
@ -174,5 +213,24 @@ mod tests {
let tick = next_entry(&zero, 1, vec![]);
assert_eq!(tick.num_hashes, 1);
assert_ne!(tick.id, zero);
let tick = next_entry(&zero, 0, vec![]);
assert_eq!(tick.num_hashes, 0);
assert_eq!(tick.id, zero);
let keypair = KeyPair::new();
let tx0 = Transaction::new_timestamp(&keypair, Utc::now(), zero);
let entry0 = next_entry(&zero, 1, vec![tx0.clone()]);
assert_eq!(entry0.num_hashes, 1);
assert_eq!(entry0.id, next_hash(&zero, 1, &vec![tx0]));
}
#[test]
#[should_panic]
fn test_next_entry_panic() {
let zero = Hash::default();
let keypair = KeyPair::new();
let tx = Transaction::new(&keypair, keypair.pubkey(), 0, zero);
next_entry(&zero, 0, vec![tx]);
}
}

View File

@ -4,79 +4,98 @@
use bank::Bank;
use entry::Entry;
use ledger::Block;
use packet;
use result::Result;
use serde_json;
use std::collections::VecDeque;
use std::io::sink;
use std::io::Write;
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use streamer;
use std::io::{self, BufRead, Error, ErrorKind, Write};
pub struct EntryWriter<'a> {
pub struct EntryWriter<'a, W> {
bank: &'a Bank,
writer: W,
}
impl<'a> EntryWriter<'a> {
impl<'a, W: Write> EntryWriter<'a, W> {
/// Create a new Tpu that wraps the given Bank.
pub fn new(bank: &'a Bank) -> Self {
EntryWriter { bank }
pub fn new(bank: &'a Bank, writer: W) -> Self {
EntryWriter { bank, writer }
}
fn write_entry<W: Write>(&self, writer: &Mutex<W>, entry: &Entry) {
trace!("write_entry entry");
self.bank.register_entry_id(&entry.id);
writeln!(
writer.lock().expect("'writer' lock in fn fn write_entry"),
"{}",
serde_json::to_string(&entry).expect("'entry' to_strong in fn write_entry")
).expect("writeln! in fn write_entry");
fn write_entry(writer: &mut W, entry: &Entry) -> io::Result<()> {
let serialized = serde_json::to_string(entry).unwrap();
writeln!(writer, "{}", serialized)
}
fn write_entries<W: Write>(
&self,
writer: &Mutex<W>,
entry_receiver: &Receiver<Entry>,
) -> Result<Vec<Entry>> {
//TODO implement a serialize for channel that does this without allocations
let mut l = vec![];
let entry = entry_receiver.recv_timeout(Duration::new(1, 0))?;
self.write_entry(writer, &entry);
l.push(entry);
while let Ok(entry) = entry_receiver.try_recv() {
self.write_entry(writer, &entry);
l.push(entry);
}
Ok(l)
}
/// Process any Entry items that have been published by the Historian.
/// continuosly broadcast blobs of entries out
pub fn write_and_send_entries<W: Write>(
&self,
broadcast: &streamer::BlobSender,
blob_recycler: &packet::BlobRecycler,
writer: &Mutex<W>,
entry_receiver: &Receiver<Entry>,
) -> Result<()> {
let mut q = VecDeque::new();
let list = self.write_entries(writer, entry_receiver)?;
trace!("New blobs? {}", list.len());
list.to_blobs(blob_recycler, &mut q);
if !q.is_empty() {
trace!("broadcasting {}", q.len());
broadcast.send(q)?;
pub fn write_entries<I>(writer: &mut W, entries: I) -> io::Result<()>
where
I: IntoIterator<Item = Entry>,
{
for entry in entries {
Self::write_entry(writer, &entry)?;
}
Ok(())
}
/// Process any Entry items that have been published by the Historian.
/// continuosly broadcast blobs of entries out
pub fn drain_entries(&self, entry_receiver: &Receiver<Entry>) -> Result<()> {
self.write_entries(&Arc::new(Mutex::new(sink())), entry_receiver)?;
fn write_and_register_entry(&mut self, entry: &Entry) -> io::Result<()> {
trace!("write_and_register_entry entry");
if !entry.has_more {
self.bank.register_entry_id(&entry.id);
}
Self::write_entry(&mut self.writer, entry)
}
pub fn write_and_register_entries(&mut self, entries: &[Entry]) -> io::Result<()> {
for entry in entries {
self.write_and_register_entry(&entry)?;
}
Ok(())
}
}
pub fn read_entry(s: String) -> io::Result<Entry> {
serde_json::from_str(&s).map_err(|e| Error::new(ErrorKind::Other, e.to_string()))
}
// TODO: How to implement this without attaching the input's lifetime to the output?
pub fn read_entries<'a, R: BufRead>(
reader: &'a mut R,
) -> impl Iterator<Item = io::Result<Entry>> + 'a {
reader.lines().map(|s| read_entry(s?))
}
#[cfg(test)]
mod tests {
use super::*;
use ledger;
use mint::Mint;
use packet::BLOB_DATA_SIZE;
use signature::{KeyPair, KeyPairUtil};
use transaction::Transaction;
#[test]
fn test_dont_register_partial_entries() {
let mint = Mint::new(1);
let bank = Bank::new(&mint);
let writer = io::sink();
let mut entry_writer = EntryWriter::new(&bank, writer);
let keypair = KeyPair::new();
let tx = Transaction::new(&mint.keypair(), keypair.pubkey(), 1, mint.last_id());
// NOTE: if Entry grows to larger than a transaction, the code below falls over
let threshold = (BLOB_DATA_SIZE / 256) - 1; // 256 is transaction size
// Verify large entries are split up and the first sets has_more.
let txs = vec![tx.clone(); threshold * 2];
let entries = ledger::next_entries(&mint.last_id(), 0, txs);
assert_eq!(entries.len(), 2);
assert!(entries[0].has_more);
assert!(!entries[1].has_more);
// Verify that write_and_register_entry doesn't register the first entries after a split.
assert_eq!(bank.last_id(), mint.last_id());
entry_writer.write_and_register_entry(&entries[0]).unwrap();
assert_eq!(bank.last_id(), mint.last_id());
// Verify that write_and_register_entry registers the final entry after a split.
entry_writer.write_and_register_entry(&entries[1]).unwrap();
assert_eq!(bank.last_id(), entries[1].id);
}
}

View File

@ -1,31 +1,43 @@
//! The `fetch_stage` batches input from a UDP socket and sends it to a channel.
use packet;
use packet::PacketRecycler;
use std::net::UdpSocket;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::thread::JoinHandle;
use streamer;
use streamer::{self, PacketReceiver};
pub struct FetchStage {
pub packet_receiver: streamer::PacketReceiver,
pub thread_hdl: JoinHandle<()>,
pub thread_hdls: Vec<JoinHandle<()>>,
}
impl FetchStage {
pub fn new(
socket: UdpSocket,
exit: Arc<AtomicBool>,
packet_recycler: packet::PacketRecycler,
) -> Self {
packet_recycler: PacketRecycler,
) -> (Self, PacketReceiver) {
Self::new_multi_socket(vec![socket], exit, packet_recycler)
}
pub fn new_multi_socket(
sockets: Vec<UdpSocket>,
exit: Arc<AtomicBool>,
packet_recycler: PacketRecycler,
) -> (Self, PacketReceiver) {
let (packet_sender, packet_receiver) = channel();
let thread_hdl =
streamer::receiver(socket, exit.clone(), packet_recycler.clone(), packet_sender);
let thread_hdls: Vec<_> = sockets
.into_iter()
.map(|socket| {
streamer::receiver(
socket,
exit.clone(),
packet_recycler.clone(),
packet_sender.clone(),
)
})
.collect();
FetchStage {
packet_receiver,
thread_hdl,
}
(FetchStage { thread_hdls }, packet_receiver)
}
}

312
src/fullnode.rs Normal file
View File

@ -0,0 +1,312 @@
//! The `fullnode` module hosts all the fullnode microservices.
use bank::Bank;
use crdt::{Crdt, ReplicatedData, TestNode};
use entry_writer;
use ncp::Ncp;
use packet::BlobRecycler;
use rpu::Rpu;
use std::fs::File;
use std::io::Write;
use std::io::{stdin, stdout, BufReader};
use std::net::SocketAddr;
use std::net::UdpSocket;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, RwLock};
use std::thread::JoinHandle;
use std::time::Duration;
use streamer;
use tpu::Tpu;
use tvu::Tvu;
//use std::time::Duration;
pub struct FullNode {
pub thread_hdls: Vec<JoinHandle<()>>,
}
impl FullNode {
pub fn new(
mut node: TestNode,
leader: bool,
infile: Option<String>,
network_entry_for_validator: Option<SocketAddr>,
outfile_for_leader: Option<String>,
exit: Arc<AtomicBool>,
) -> FullNode {
info!("creating bank...");
let bank = Bank::default();
let entry_height = if let Some(path) = infile {
let f = File::open(path).unwrap();
let mut r = BufReader::new(f);
let entries =
entry_writer::read_entries(&mut r).map(|e| e.expect("failed to parse entry"));
info!("processing ledger...");
bank.process_ledger(entries).expect("process_ledger")
} else {
let mut r = BufReader::new(stdin());
let entries =
entry_writer::read_entries(&mut r).map(|e| e.expect("failed to parse entry"));
info!("processing ledger...");
bank.process_ledger(entries).expect("process_ledger")
};
// entry_height is the network-wide agreed height of the ledger.
// initialize it from the input ledger
info!("processed {} ledger...", entry_height);
info!("creating networking stack...");
let local_gossip_addr = node.sockets.gossip.local_addr().unwrap();
let local_requests_addr = node.sockets.requests.local_addr().unwrap();
info!(
"starting... local gossip address: {} (advertising {})",
local_gossip_addr, node.data.gossip_addr
);
if !leader {
let testnet_addr = network_entry_for_validator.expect("validator requires entry");
let network_entry_point = ReplicatedData::new_entry_point(testnet_addr);
let server = FullNode::new_validator(
bank,
entry_height,
node.data.clone(),
node.sockets.requests,
node.sockets.respond,
node.sockets.replicate,
node.sockets.gossip,
node.sockets.repair,
network_entry_point,
exit.clone(),
);
info!(
"validator ready... local request address: {} (advertising {}) connected to: {}",
local_requests_addr, node.data.requests_addr, testnet_addr
);
server
} else {
node.data.current_leader_id = node.data.id.clone();
let server = if let Some(file) = outfile_for_leader {
FullNode::new_leader(
bank,
entry_height,
//Some(Duration::from_millis(1000)),
None,
node.data.clone(),
node.sockets.requests,
node.sockets.transaction,
node.sockets.broadcast,
node.sockets.respond,
node.sockets.gossip,
exit.clone(),
File::create(file).expect("opening ledger file"),
)
} else {
FullNode::new_leader(
bank,
entry_height,
//Some(Duration::from_millis(1000)),
None,
node.data.clone(),
node.sockets.requests,
node.sockets.transaction,
node.sockets.broadcast,
node.sockets.respond,
node.sockets.gossip,
exit.clone(),
stdout(),
)
};
info!(
"leader ready... local request address: {} (advertising {})",
local_requests_addr, node.data.requests_addr
);
server
}
}
/// Create a server instance acting as a leader.
///
/// ```text
/// .---------------------.
/// | Leader |
/// | |
/// .--------. | .-----. |
/// | |---->| | |
/// | Client | | | RPU | |
/// | |<----| | |
/// `----+---` | `-----` |
/// | | ^ |
/// | | | |
/// | | .--+---. |
/// | | | Bank | |
/// | | `------` |
/// | | ^ |
/// | | | | .------------.
/// | | .--+--. .-----. | | |
/// `-------->| TPU +-->| NCP +------>| Validators |
/// | `-----` `-----` | | |
/// | | `------------`
/// `---------------------`
/// ```
pub fn new_leader<W: Write + Send + 'static>(
bank: Bank,
entry_height: u64,
tick_duration: Option<Duration>,
me: ReplicatedData,
requests_socket: UdpSocket,
transactions_socket: UdpSocket,
broadcast_socket: UdpSocket,
respond_socket: UdpSocket,
gossip_socket: UdpSocket,
exit: Arc<AtomicBool>,
writer: W,
) -> Self {
let bank = Arc::new(bank);
let mut thread_hdls = vec![];
let rpu = Rpu::new(bank.clone(), requests_socket, respond_socket, exit.clone());
thread_hdls.extend(rpu.thread_hdls);
let blob_recycler = BlobRecycler::default();
let (tpu, blob_receiver) = Tpu::new(
bank.clone(),
tick_duration,
transactions_socket,
blob_recycler.clone(),
exit.clone(),
writer,
);
thread_hdls.extend(tpu.thread_hdls);
let crdt = Arc::new(RwLock::new(Crdt::new(me)));
let window = streamer::default_window();
let gossip_send_socket = UdpSocket::bind("0.0.0.0:0").expect("bind 0");
let ncp = Ncp::new(
crdt.clone(),
window.clone(),
gossip_socket,
gossip_send_socket,
exit.clone(),
).expect("Ncp::new");
thread_hdls.extend(ncp.thread_hdls);
let t_broadcast = streamer::broadcaster(
broadcast_socket,
exit.clone(),
crdt,
window,
entry_height,
blob_recycler.clone(),
blob_receiver,
);
thread_hdls.extend(vec![t_broadcast]);
FullNode { thread_hdls }
}
/// Create a server instance acting as a validator.
///
/// ```text
/// .-------------------------------.
/// | Validator |
/// | |
/// .--------. | .-----. |
/// | |-------------->| | |
/// | Client | | | RPU | |
/// | |<--------------| | |
/// `--------` | `-----` |
/// | ^ |
/// | | |
/// | .--+---. |
/// | | Bank | |
/// | `------` |
/// | ^ |
/// .--------. | | | .------------.
/// | | | .--+--. | | |
/// | Leader |<------------->| TVU +<--------------->| |
/// | | | `-----` | | Validators |
/// | | | ^ | | |
/// | | | | | | |
/// | | | .--+--. | | |
/// | |<------------->| NCP +<--------------->| |
/// | | | `-----` | | |
/// `--------` | | `------------`
/// `-------------------------------`
/// ```
pub fn new_validator(
bank: Bank,
entry_height: u64,
me: ReplicatedData,
requests_socket: UdpSocket,
respond_socket: UdpSocket,
replicate_socket: UdpSocket,
gossip_listen_socket: UdpSocket,
repair_socket: UdpSocket,
entry_point: ReplicatedData,
exit: Arc<AtomicBool>,
) -> Self {
let bank = Arc::new(bank);
let mut thread_hdls = vec![];
let rpu = Rpu::new(bank.clone(), requests_socket, respond_socket, exit.clone());
thread_hdls.extend(rpu.thread_hdls);
let crdt = Arc::new(RwLock::new(Crdt::new(me)));
crdt.write()
.expect("'crdt' write lock before insert() in pub fn replicate")
.insert(&entry_point);
let window = streamer::default_window();
let gossip_send_socket = UdpSocket::bind("0.0.0.0:0").expect("bind 0");
let retransmit_socket = UdpSocket::bind("0.0.0.0:0").expect("bind 0");
let ncp = Ncp::new(
crdt.clone(),
window.clone(),
gossip_listen_socket,
gossip_send_socket,
exit.clone(),
).expect("Ncp::new");
let tvu = Tvu::new(
bank.clone(),
entry_height,
crdt.clone(),
window.clone(),
replicate_socket,
repair_socket,
retransmit_socket,
exit.clone(),
);
thread_hdls.extend(tvu.thread_hdls);
thread_hdls.extend(ncp.thread_hdls);
FullNode { thread_hdls }
}
}
#[cfg(test)]
mod tests {
use bank::Bank;
use crdt::TestNode;
use fullnode::FullNode;
use mint::Mint;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[test]
fn validator_exit() {
let tn = TestNode::new();
let alice = Mint::new(10_000);
let bank = Bank::new(&alice);
let exit = Arc::new(AtomicBool::new(false));
let v = FullNode::new_validator(
bank,
0,
tn.data.clone(),
tn.sockets.requests,
tn.sockets.respond,
tn.sockets.replicate,
tn.sockets.gossip,
tn.sockets.repair,
tn.data,
exit.clone(),
);
exit.store(true, Ordering::Relaxed);
for t in v.thread_hdls {
t.join().unwrap();
}
}
}

View File

@ -10,7 +10,10 @@ pub type Hash = GenericArray<u8, U32>;
pub fn hash(val: &[u8]) -> Hash {
let mut hasher = Sha256::default();
hasher.input(val);
hasher.result()
// At the time of this writing, the sha2 library is stuck on an old version
// of generic_array (0.9.0). Decouple ourselves with a clone to our version.
GenericArray::clone_from_slice(hasher.result().as_slice())
}
/// Return the hash of the given hash extended with the given value.

View File

@ -1,18 +1,17 @@
//! The `ledger` module provides functions for parallel verification of the
//! Proof of History ledger.
use bincode::{deserialize, serialize_into};
use entry::{next_entry, Entry};
use bincode::{self, deserialize, serialize_into};
use entry::Entry;
use hash::Hash;
use packet;
use packet::{SharedBlob, BLOB_DATA_SIZE, BLOB_SIZE};
use packet::{self, SharedBlob, BLOB_SIZE};
use rayon::prelude::*;
use std::cmp::min;
use std::collections::VecDeque;
use std::io::Cursor;
use std::mem::size_of;
use transaction::Transaction;
// a Block is a slice of Entries
pub trait Block {
/// Verifies the hashes and counts of a slice of transactions are all consistent.
fn verify(&self, start_hash: &Hash) -> bool;
@ -27,110 +26,98 @@ impl Block for [Entry] {
}
fn to_blobs(&self, blob_recycler: &packet::BlobRecycler, q: &mut VecDeque<SharedBlob>) {
let mut start = 0;
let mut end = 0;
while start < self.len() {
let mut entries: Vec<Vec<Entry>> = Vec::new();
let mut total = 0;
for i in &self[start..] {
total += size_of::<Transaction>() * i.transactions.len();
total += size_of::<Entry>();
if total >= BLOB_DATA_SIZE {
break;
}
end += 1;
}
// See if we need to split the transactions
if end <= start {
let mut transaction_start = 0;
let num_transactions_per_blob = BLOB_DATA_SIZE / size_of::<Transaction>();
let total_entry_chunks = (self[end].transactions.len() + num_transactions_per_blob
- 1) / num_transactions_per_blob;
trace!(
"splitting transactions end: {} total_chunks: {}",
end,
total_entry_chunks
);
for _ in 0..total_entry_chunks {
let transaction_end = min(
transaction_start + num_transactions_per_blob,
self[end].transactions.len(),
);
let mut entry = Entry {
num_hashes: self[end].num_hashes,
id: self[end].id,
transactions: self[end].transactions[transaction_start..transaction_end]
.to_vec(),
};
entries.push(vec![entry]);
transaction_start = transaction_end;
}
end += 1;
} else {
entries.push(self[start..end].to_vec());
}
for entry in entries {
let b = blob_recycler.allocate();
let pos = {
let mut bd = b.write().unwrap();
let mut out = Cursor::new(bd.data_mut());
serialize_into(&mut out, &entry).expect("failed to serialize output");
out.position() as usize
};
assert!(pos < BLOB_SIZE);
b.write().unwrap().set_size(pos);
q.push_back(b);
}
start = end;
for entry in self {
let blob = blob_recycler.allocate();
let pos = {
let mut bd = blob.write().unwrap();
let mut out = Cursor::new(bd.data_mut());
serialize_into(&mut out, &entry).expect("failed to serialize output");
out.position() as usize
};
assert!(pos < BLOB_SIZE);
blob.write().unwrap().set_size(pos);
q.push_back(blob);
}
}
}
/// Create a vector of Entries of length `transaction_batches.len()` from `start_hash` hash, `num_hashes`, and `transaction_batches`.
pub fn reconstruct_entries_from_blobs(blobs: VecDeque<SharedBlob>) -> bincode::Result<Vec<Entry>> {
let mut entries: Vec<Entry> = Vec::with_capacity(blobs.len());
for blob in blobs {
let entry = {
let msg = blob.read().unwrap();
deserialize(&msg.data()[..msg.meta.size])
};
match entry {
Ok(entry) => entries.push(entry),
Err(err) => {
trace!("reconstruct_entry_from_blobs: {}", err);
return Err(err);
}
}
}
Ok(entries)
}
/// Creates the next entries for given transactions, outputs
/// updates start_hash to id of last Entry, sets cur_hashes to 0
pub fn next_entries_mut(
start_hash: &mut Hash,
cur_hashes: &mut u64,
transactions: Vec<Transaction>,
) -> Vec<Entry> {
if transactions.is_empty() {
vec![Entry::new_mut(start_hash, cur_hashes, transactions, false)]
} else {
let mut chunk_len = transactions.len();
// check for fit, make sure they can be serialized
while !Entry::will_fit(transactions[0..chunk_len].to_vec()) {
chunk_len /= 2;
}
let mut num_chunks = if transactions.len() % chunk_len == 0 {
transactions.len() / chunk_len
} else {
transactions.len() / chunk_len + 1
};
let mut entries = Vec::with_capacity(num_chunks);
for chunk in transactions.chunks(chunk_len) {
num_chunks -= 1;
entries.push(Entry::new_mut(
start_hash,
cur_hashes,
chunk.to_vec(),
num_chunks > 0,
));
}
entries
}
}
/// Creates the next Entries for given transactions
pub fn next_entries(
start_hash: &Hash,
num_hashes: u64,
transaction_batches: Vec<Vec<Transaction>>,
cur_hashes: u64,
transactions: Vec<Transaction>,
) -> Vec<Entry> {
let mut id = *start_hash;
let mut entries = vec![];
for transactions in &transaction_batches {
let transactions = transactions.clone();
let entry = next_entry(&id, num_hashes, transactions);
id = entry.id;
entries.push(entry);
}
entries
}
pub fn reconstruct_entries_from_blobs(blobs: &VecDeque<SharedBlob>) -> Vec<Entry> {
let mut entries_to_apply: Vec<Entry> = Vec::new();
let mut last_id = Hash::default();
for msgs in blobs {
let blob = msgs.read().unwrap();
let entries: Vec<Entry> = deserialize(&blob.data()[..blob.meta.size]).unwrap();
for entry in entries {
if entry.id == last_id {
if let Some(last_entry) = entries_to_apply.last_mut() {
last_entry.transactions.extend(entry.transactions);
}
} else {
last_id = entry.id;
entries_to_apply.push(entry);
}
}
//TODO respond back to leader with hash of the state
}
entries_to_apply
let mut num_hashes = cur_hashes;
next_entries_mut(&mut id, &mut num_hashes, transactions)
}
#[cfg(test)]
mod tests {
use super::*;
use entry::{next_entry, Entry};
use hash::hash;
use packet::BlobRecycler;
use packet::{BlobRecycler, BLOB_DATA_SIZE};
use signature::{KeyPair, KeyPairUtil};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use transaction::Transaction;
#[test]
@ -140,49 +127,73 @@ mod tests {
assert!(vec![][..].verify(&zero)); // base case
assert!(vec![Entry::new_tick(0, &zero)][..].verify(&zero)); // singleton case 1
assert!(!vec![Entry::new_tick(0, &zero)][..].verify(&one)); // singleton case 2, bad
assert!(next_entries(&zero, 0, vec![vec![]; 2])[..].verify(&zero)); // inductive step
assert!(vec![next_entry(&zero, 0, vec![]); 2][..].verify(&zero)); // inductive step
let mut bad_ticks = next_entries(&zero, 0, vec![vec![]; 2]);
let mut bad_ticks = vec![next_entry(&zero, 0, vec![]); 2];
bad_ticks[1].id = one;
assert!(!bad_ticks.verify(&zero)); // inductive step, bad
}
#[test]
fn test_entry_to_blobs() {
fn test_entries_to_blobs() {
let zero = Hash::default();
let one = hash(&zero);
let keypair = KeyPair::new();
let tx0 = Transaction::new(&keypair, keypair.pubkey(), 1, one);
let transactions = vec![tx0.clone(); 10000];
let e0 = Entry::new(&zero, 0, transactions);
let transactions = vec![tx0; 10_000];
let entries = next_entries(&zero, 0, transactions);
let entries = vec![e0.clone(); 1];
let blob_recycler = BlobRecycler::default();
let mut blob_q = VecDeque::new();
entries.to_blobs(&blob_recycler, &mut blob_q);
assert_eq!(reconstruct_entries_from_blobs(&blob_q), entries);
assert_eq!(reconstruct_entries_from_blobs(blob_q).unwrap(), entries);
}
#[test]
fn test_bad_blobs_attack() {
let blob_recycler = BlobRecycler::default();
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000);
let blobs_q = packet::to_blobs(vec![(0, addr)], &blob_recycler).unwrap(); // <-- attack!
assert!(reconstruct_entries_from_blobs(blobs_q).is_err());
}
#[test]
fn test_next_entries() {
let mut id = Hash::default();
let id = Hash::default();
let next_id = hash(&id);
let keypair = KeyPair::new();
let tx0 = Transaction::new(&keypair, keypair.pubkey(), 1, next_id);
let transactions = vec![tx0.clone(); 5];
let transaction_batches = vec![transactions.clone(); 5];
let entries0 = next_entries(&id, 0, transaction_batches);
assert_eq!(entries0.len(), 5);
// NOTE: if Entry grows to larger than a transaction, the code below falls over
let threshold = (BLOB_DATA_SIZE / 256) - 1; // 256 is transaction size
let mut entries1 = vec![];
for _ in 0..5 {
let entry = next_entry(&id, 0, transactions.clone());
id = entry.id;
entries1.push(entry);
}
assert_eq!(entries0, entries1);
// verify no split
let transactions = vec![tx0.clone(); threshold];
let entries0 = next_entries(&id, 0, transactions.clone());
assert_eq!(entries0.len(), 1);
assert!(entries0.verify(&id));
// verify the split
let transactions = vec![tx0.clone(); threshold * 2];
let entries0 = next_entries(&id, 0, transactions.clone());
assert_eq!(entries0.len(), 2);
assert!(entries0[0].has_more);
assert!(!entries0[entries0.len() - 1].has_more);
assert!(entries0.verify(&id));
// test hand-construction... brittle, changes if split method changes... ?
// let mut entries1 = vec![];
// entries1.push(Entry::new(&id, 1, transactions[..threshold].to_vec(), true));
// id = entries1[0].id;
// entries1.push(Entry::new(
// &id,
// 1,
// transactions[threshold..].to_vec(),
// false,
// ));
//
// assert_eq!(entries0, entries1);
}
}
@ -190,14 +201,27 @@ mod tests {
mod bench {
extern crate test;
use self::test::Bencher;
use hash::hash;
use ledger::*;
use packet::BlobRecycler;
use signature::{KeyPair, KeyPairUtil};
use transaction::Transaction;
#[bench]
fn bench_next_entries(bencher: &mut Bencher) {
let start_hash = Hash::default();
let entries = next_entries(&start_hash, 10_000, vec![vec![]; 8]);
fn bench_block_to_blobs_to_block(bencher: &mut Bencher) {
let zero = Hash::default();
let one = hash(&zero);
let keypair = KeyPair::new();
let tx0 = Transaction::new(&keypair, keypair.pubkey(), 1, one);
let transactions = vec![tx0; 10];
let entries = next_entries(&zero, 1, transactions);
let blob_recycler = BlobRecycler::default();
bencher.iter(|| {
assert!(entries.verify(&start_hash));
let mut blob_q = VecDeque::new();
entries.to_blobs(&blob_recycler, &mut blob_q);
assert_eq!(reconstruct_entries_from_blobs(blob_q).unwrap(), entries);
});
}
}

View File

@ -11,17 +11,22 @@
pub mod counter;
pub mod bank;
pub mod banking_stage;
pub mod blob_fetch_stage;
pub mod budget;
pub mod choose_gossip_peer_strategy;
pub mod crdt;
pub mod drone;
pub mod entry;
pub mod entry_writer;
#[cfg(feature = "erasure")]
pub mod erasure;
pub mod fetch_stage;
pub mod fullnode;
pub mod hash;
pub mod ledger;
pub mod logger;
pub mod mint;
pub mod nat;
pub mod ncp;
pub mod packet;
pub mod payment_plan;
@ -33,7 +38,6 @@ pub mod request_processor;
pub mod request_stage;
pub mod result;
pub mod rpu;
pub mod server;
pub mod signature;
pub mod sigverify;
pub mod sigverify_stage;
@ -43,11 +47,13 @@ pub mod timing;
pub mod tpu;
pub mod transaction;
pub mod tvu;
pub mod window_stage;
pub mod write_stage;
extern crate bincode;
extern crate byteorder;
extern crate chrono;
extern crate generic_array;
extern crate itertools;
extern crate libc;
#[macro_use]
extern crate log;
@ -56,7 +62,7 @@ extern crate ring;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate pnet;
extern crate pnet_datalink;
extern crate serde_json;
extern crate sha2;
extern crate untrusted;

View File

@ -53,18 +53,12 @@ impl Mint {
}
pub fn create_entries(&self) -> Vec<Entry> {
let e0 = Entry::new(&self.seed(), 0, vec![]);
let e1 = Entry::new(&e0.id, 0, self.create_transactions());
let e0 = Entry::new(&self.seed(), 0, vec![], false);
let e1 = Entry::new(&e0.id, 0, self.create_transactions(), false);
vec![e0, e1]
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MintDemo {
pub mint: Mint,
pub num_accounts: i64,
}
#[cfg(test)]
mod tests {
use super::*;

97
src/nat.rs Normal file
View File

@ -0,0 +1,97 @@
//! The `nat` module assists with NAT traversal
extern crate futures;
extern crate p2p;
extern crate reqwest;
extern crate tokio_core;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use self::futures::Future;
use self::p2p::UdpSocketExt;
use std::env;
use std::str;
/// A data type representing a public Udp socket
pub struct UdpSocketPair {
pub addr: SocketAddr, // Public address of the socket
pub receiver: UdpSocket, // Locally bound socket that can receive from the public address
pub sender: UdpSocket, // Locally bound socket to send via public address
}
/// Tries to determine the public IP address of this machine
pub fn get_public_ip_addr() -> Result<IpAddr, String> {
let body = reqwest::get("http://ifconfig.co/ip")
.map_err(|err| err.to_string())?
.text()
.map_err(|err| err.to_string())?;
match body.lines().next() {
Some(ip) => Result::Ok(ip.parse().unwrap()),
None => Result::Err("Empty response body".to_string()),
}
}
/// Binds a private Udp address to a public address using UPnP if possible
pub fn udp_public_bind(label: &str) -> UdpSocketPair {
let private_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
let mut core = tokio_core::reactor::Core::new().unwrap();
let handle = core.handle();
let mc = p2p::P2p::default();
let res = core.run({
tokio_core::net::UdpSocket::bind_public(&private_addr, &handle, &mc)
.map_err(|e| {
info!("Failed to bind public socket for {}: {}", label, e);
})
.and_then(|(socket, public_addr)| Ok((public_addr, socket.local_addr().unwrap())))
});
match res {
Ok((public_addr, local_addr)) => {
info!(
"Using local address {} mapped to UPnP public address {} for {}",
local_addr, public_addr, label
);
// NAT should now be forwarding inbound packets directed at
// |public_addr| to the local |receiver| socket...
let receiver = UdpSocket::bind(local_addr).unwrap();
// TODO: try to autodetect a broken NAT (issue #496)
let sender = if env::var("BROKEN_NAT").is_err() {
receiver.try_clone().unwrap()
} else {
// ... however for outbound packets, some NATs *will not* rewrite the
// source port from |receiver.local_addr().port()| to |public_addr.port()|.
// This is currently a problem when talking with a fullnode as it
// assumes it can send UDP packets back at the source. This hits the
// NAT as a datagram for |receiver.local_addr().port()| on the NAT's public
// IP, which the NAT promptly discards. As a short term hack, create a
// local UDP socket, |sender|, with the same port as |public_addr.port()|.
//
// TODO: Remove the |sender| socket and deal with the downstream changes to
// the UDP signalling
let mut local_addr_sender = local_addr.clone();
local_addr_sender.set_port(public_addr.port());
UdpSocket::bind(local_addr_sender).unwrap()
};
UdpSocketPair {
addr: public_addr,
receiver,
sender,
}
}
Err(_) => {
let sender = UdpSocket::bind(private_addr).unwrap();
let local_addr = sender.local_addr().unwrap();
info!("Using local address {} for {}", local_addr, label);
UdpSocketPair {
addr: private_addr,
receiver: sender.try_clone().unwrap(),
sender,
}
}
}
}

View File

@ -1,7 +1,7 @@
//! The `ncp` module implements the network control plane.
use crdt;
use packet;
use crdt::Crdt;
use packet::{BlobRecycler, SharedBlob};
use result::Result;
use std::net::UdpSocket;
use std::sync::atomic::AtomicBool;
@ -16,13 +16,13 @@ pub struct Ncp {
impl Ncp {
pub fn new(
crdt: Arc<RwLock<crdt::Crdt>>,
window: Arc<RwLock<Vec<Option<packet::SharedBlob>>>>,
crdt: Arc<RwLock<Crdt>>,
window: Arc<RwLock<Vec<Option<SharedBlob>>>>,
gossip_listen_socket: UdpSocket,
gossip_send_socket: UdpSocket,
exit: Arc<AtomicBool>,
) -> Result<Ncp> {
let blob_recycler = packet::BlobRecycler::default();
let blob_recycler = BlobRecycler::default();
let (request_sender, request_receiver) = channel();
trace!(
"Ncp: id: {:?}, listening on: {:?}",
@ -42,7 +42,7 @@ impl Ncp {
blob_recycler.clone(),
response_receiver,
);
let t_listen = crdt::Crdt::listen(
let t_listen = Crdt::listen(
crdt.clone(),
window,
blob_recycler.clone(),
@ -50,7 +50,7 @@ impl Ncp {
response_sender.clone(),
exit.clone(),
);
let t_gossip = crdt::Crdt::gossip(crdt.clone(), blob_recycler, response_sender, exit);
let t_gossip = Crdt::gossip(crdt.clone(), blob_recycler, response_sender, exit);
let thread_hdls = vec![t_receiver, t_responder, t_listen, t_gossip];
Ok(Ncp { thread_hdls })
}

View File

@ -16,12 +16,13 @@ use std::time::Instant;
pub type SharedPackets = Arc<RwLock<Packets>>;
pub type SharedBlob = Arc<RwLock<Blob>>;
pub type SharedBlobs = VecDeque<SharedBlob>;
pub type PacketRecycler = Recycler<Packets>;
pub type BlobRecycler = Recycler<Blob>;
pub const NUM_PACKETS: usize = 1024 * 8;
pub const BLOB_SIZE: usize = 64 * 1024;
pub const BLOB_DATA_SIZE: usize = BLOB_SIZE - BLOB_ID_END;
pub const BLOB_DATA_SIZE: usize = BLOB_SIZE - BLOB_HEADER_SIZE;
pub const PACKET_DATA_SIZE: usize = 256;
pub const NUM_BLOBS: usize = (NUM_PACKETS * PACKET_DATA_SIZE) / BLOB_SIZE;
@ -29,6 +30,7 @@ pub const NUM_BLOBS: usize = (NUM_PACKETS * PACKET_DATA_SIZE) / BLOB_SIZE;
#[repr(C)]
pub struct Meta {
pub size: usize,
pub num_retransmits: u64,
pub addr: [u16; 8],
pub port: u16,
pub v6: bool,
@ -161,12 +163,26 @@ impl<T: Default> Clone for Recycler<T> {
impl<T: Default> Recycler<T> {
pub fn allocate(&self) -> Arc<RwLock<T>> {
let mut gc = self.gc.lock().expect("recycler lock in pb fn allocate");
gc.pop()
.unwrap_or_else(|| Arc::new(RwLock::new(Default::default())))
let x = gc.pop()
.unwrap_or_else(|| Arc::new(RwLock::new(Default::default())));
// Only return the item if this recycler is the last reference to it.
// Remove this check once `T` holds a Weak reference back to this
// recycler and implements `Drop`. At the time of this writing, Weak can't
// be passed across threads ('alloc' is a nightly-only API), and so our
// reference-counted recyclables are awkwardly being recycled by hand,
// which allows this race condition to exist.
if Arc::strong_count(&x) > 1 {
warn!("Recycled item still in use. Booting it.");
drop(gc);
self.allocate()
} else {
x
}
}
pub fn recycle(&self, msgs: Arc<RwLock<T>>) {
pub fn recycle(&self, x: Arc<RwLock<T>>) {
let mut gc = self.gc.lock().expect("recycler lock in pub fn recycle");
gc.push(msgs);
gc.push(x);
}
}
@ -224,9 +240,13 @@ impl Packets {
}
}
pub fn to_packets<T: Serialize>(r: &PacketRecycler, xs: Vec<T>) -> Vec<SharedPackets> {
pub fn to_packets_chunked<T: Serialize>(
r: &PacketRecycler,
xs: Vec<T>,
chunks: usize,
) -> Vec<SharedPackets> {
let mut out = vec![];
for x in xs.chunks(NUM_PACKETS) {
for x in xs.chunks(chunks) {
let p = r.allocate();
p.write()
.unwrap()
@ -243,6 +263,10 @@ pub fn to_packets<T: Serialize>(r: &PacketRecycler, xs: Vec<T>) -> Vec<SharedPac
return out;
}
pub fn to_packets<T: Serialize>(r: &PacketRecycler, xs: Vec<T>) -> Vec<SharedPackets> {
to_packets_chunked(r, xs, NUM_PACKETS)
}
pub fn to_blob<T: Serialize>(
resp: T,
rsp_addr: SocketAddr,
@ -253,10 +277,6 @@ pub fn to_blob<T: Serialize>(
let mut b = blob.write().unwrap();
let v = serialize(&resp)?;
let len = v.len();
// TODO: we are not using .data_mut() method here because
// the raw bytes are being serialized and sent, this isn't the
// right interface, and we should create a separate path for
// sending request responses in the RPU
assert!(len < BLOB_SIZE);
b.data[..len].copy_from_slice(&v);
b.meta.size = len;
@ -268,7 +288,7 @@ pub fn to_blob<T: Serialize>(
pub fn to_blobs<T: Serialize>(
rsps: Vec<(T, SocketAddr)>,
blob_recycler: &BlobRecycler,
) -> Result<VecDeque<SharedBlob>> {
) -> Result<SharedBlobs> {
let mut blobs = VecDeque::new();
for (resp, rsp_addr) in rsps {
blobs.push_back(to_blob(resp, rsp_addr, blob_recycler)?);
@ -361,7 +381,7 @@ impl Blob {
self.meta.size = new_size;
self.set_data_size(new_size as u64).unwrap();
}
pub fn recv_from(re: &BlobRecycler, socket: &UdpSocket) -> Result<VecDeque<SharedBlob>> {
pub fn recv_from(re: &BlobRecycler, socket: &UdpSocket) -> Result<SharedBlobs> {
let mut v = VecDeque::new();
//DOCUMENTED SIDE-EFFECT
//Performance out of the IO without poll
@ -399,11 +419,7 @@ impl Blob {
}
Ok(v)
}
pub fn send_to(
re: &BlobRecycler,
socket: &UdpSocket,
v: &mut VecDeque<SharedBlob>,
) -> Result<()> {
pub fn send_to(re: &BlobRecycler, socket: &UdpSocket, v: &mut SharedBlobs) -> Result<()> {
while let Some(r) = v.pop_front() {
{
let p = r.read().expect("'r' read lock in pub fn send_to");
@ -417,13 +433,16 @@ impl Blob {
}
#[cfg(test)]
mod test {
use packet::{to_packets, Blob, BlobRecycler, Packet, PacketRecycler, Packets, NUM_PACKETS};
mod tests {
use packet::{
to_packets, Blob, BlobRecycler, Packet, PacketRecycler, Packets, Recycler, NUM_PACKETS,
};
use request::Request;
use std::collections::VecDeque;
use std::io;
use std::io::Write;
use std::net::UdpSocket;
use std::sync::Arc;
#[test]
pub fn packet_recycler_test() {
@ -434,6 +453,37 @@ mod test {
let _ = r.allocate();
assert_eq!(r.gc.lock().unwrap().len(), 0);
}
#[test]
pub fn test_leaked_recyclable() {
// Ensure that the recycler won't return an item
// that is still referenced outside the recycler.
let r = Recycler::<u8>::default();
let x0 = r.allocate();
r.recycle(x0.clone());
assert_eq!(Arc::strong_count(&x0), 2);
assert_eq!(r.gc.lock().unwrap().len(), 1);
let x1 = r.allocate();
assert_eq!(Arc::strong_count(&x1), 1);
assert_eq!(r.gc.lock().unwrap().len(), 0);
}
#[test]
pub fn test_leaked_recyclable_recursion() {
// In the case of a leaked recyclable, ensure the recycler drops its lock before recursing.
let r = Recycler::<u8>::default();
let x0 = r.allocate();
let x1 = r.allocate();
r.recycle(x0); // <-- allocate() of this will require locking the recycler's stack.
r.recycle(x1.clone()); // <-- allocate() of this will cause it to be dropped and recurse.
assert_eq!(Arc::strong_count(&x1), 2);
assert_eq!(r.gc.lock().unwrap().len(), 2);
r.allocate(); // Ensure lock is released before recursing.
assert_eq!(r.gc.lock().unwrap().len(), 0);
}
#[test]
pub fn blob_recycler_test() {
let r = BlobRecycler::default();

View File

@ -20,14 +20,16 @@ pub enum Signal {
}
pub struct RecordStage {
pub entry_receiver: Receiver<Entry>,
pub thread_hdl: JoinHandle<()>,
}
impl RecordStage {
/// A background thread that will continue tagging received Transaction messages and
/// sending back Entry messages until either the receiver or sender channel is closed.
pub fn new(signal_receiver: Receiver<Signal>, start_hash: &Hash) -> Self {
pub fn new(
signal_receiver: Receiver<Signal>,
start_hash: &Hash,
) -> (Self, Receiver<Vec<Entry>>) {
let (entry_sender, entry_receiver) = channel();
let start_hash = start_hash.clone();
@ -39,10 +41,7 @@ impl RecordStage {
})
.unwrap();
RecordStage {
entry_receiver,
thread_hdl,
}
(RecordStage { thread_hdl }, entry_receiver)
}
/// Same as `RecordStage::new`, but will automatically produce entries every `tick_duration`.
@ -50,7 +49,7 @@ impl RecordStage {
signal_receiver: Receiver<Signal>,
start_hash: &Hash,
tick_duration: Duration,
) -> Self {
) -> (Self, Receiver<Vec<Entry>>) {
let (entry_sender, entry_receiver) = channel();
let start_hash = start_hash.clone();
@ -74,30 +73,28 @@ impl RecordStage {
})
.unwrap();
RecordStage {
entry_receiver,
thread_hdl,
}
(RecordStage { thread_hdl }, entry_receiver)
}
fn process_signal(
signal: Signal,
recorder: &mut Recorder,
sender: &Sender<Entry>,
sender: &Sender<Vec<Entry>>,
) -> Result<(), ()> {
let txs = if let Signal::Transactions(txs) = signal {
txs
} else {
vec![]
};
let entry = recorder.record(txs);
sender.send(entry).map_err(|_| ())
let entries = recorder.record(txs);
sender.send(entries).or(Err(()))?;
Ok(())
}
fn process_signals(
recorder: &mut Recorder,
receiver: &Receiver<Signal>,
sender: &Sender<Entry>,
sender: &Sender<Vec<Entry>>,
) -> Result<(), ()> {
loop {
match receiver.recv() {
@ -112,11 +109,11 @@ impl RecordStage {
start_time: Instant,
tick_duration: Duration,
receiver: &Receiver<Signal>,
sender: &Sender<Entry>,
sender: &Sender<Vec<Entry>>,
) -> Result<(), ()> {
loop {
if let Some(entry) = recorder.tick(start_time, tick_duration) {
sender.send(entry).or(Err(()))?;
sender.send(vec![entry]).or(Err(()))?;
}
match receiver.try_recv() {
Ok(signal) => Self::process_signal(signal, recorder, sender)?,
@ -139,7 +136,7 @@ mod tests {
fn test_historian() {
let (tx_sender, tx_receiver) = channel();
let zero = Hash::default();
let record_stage = RecordStage::new(tx_receiver, &zero);
let (record_stage, entry_receiver) = RecordStage::new(tx_receiver, &zero);
tx_sender.send(Signal::Tick).unwrap();
sleep(Duration::new(0, 1_000_000));
@ -147,9 +144,9 @@ mod tests {
sleep(Duration::new(0, 1_000_000));
tx_sender.send(Signal::Tick).unwrap();
let entry0 = record_stage.entry_receiver.recv().unwrap();
let entry1 = record_stage.entry_receiver.recv().unwrap();
let entry2 = record_stage.entry_receiver.recv().unwrap();
let entry0 = entry_receiver.recv().unwrap()[0].clone();
let entry1 = entry_receiver.recv().unwrap()[0].clone();
let entry2 = entry_receiver.recv().unwrap()[0].clone();
assert_eq!(entry0.num_hashes, 0);
assert_eq!(entry1.num_hashes, 0);
@ -165,8 +162,8 @@ mod tests {
fn test_historian_closed_sender() {
let (tx_sender, tx_receiver) = channel();
let zero = Hash::default();
let record_stage = RecordStage::new(tx_receiver, &zero);
drop(record_stage.entry_receiver);
let (record_stage, entry_receiver) = RecordStage::new(tx_receiver, &zero);
drop(entry_receiver);
tx_sender.send(Signal::Tick).unwrap();
assert_eq!(record_stage.thread_hdl.join().unwrap(), ());
}
@ -175,7 +172,7 @@ mod tests {
fn test_transactions() {
let (tx_sender, signal_receiver) = channel();
let zero = Hash::default();
let record_stage = RecordStage::new(signal_receiver, &zero);
let (_record_stage, entry_receiver) = RecordStage::new(signal_receiver, &zero);
let alice_keypair = KeyPair::new();
let bob_pubkey = KeyPair::new().pubkey();
let tx0 = Transaction::new(&alice_keypair, bob_pubkey, 1, zero);
@ -184,7 +181,7 @@ mod tests {
.send(Signal::Transactions(vec![tx0, tx1]))
.unwrap();
drop(tx_sender);
let entries: Vec<_> = record_stage.entry_receiver.iter().collect();
let entries: Vec<_> = entry_receiver.iter().collect();
assert_eq!(entries.len(), 1);
}
@ -192,12 +189,12 @@ mod tests {
fn test_clock() {
let (tx_sender, tx_receiver) = channel();
let zero = Hash::default();
let record_stage =
let (_record_stage, entry_receiver) =
RecordStage::new_with_clock(tx_receiver, &zero, Duration::from_millis(20));
sleep(Duration::from_millis(900));
tx_sender.send(Signal::Tick).unwrap();
drop(tx_sender);
let entries: Vec<Entry> = record_stage.entry_receiver.iter().collect();
let entries: Vec<_> = entry_receiver.iter().flat_map(|x| x).collect();
assert!(entries.len() > 1);
// Ensure the ID is not the seed.

View File

@ -3,6 +3,7 @@
use entry::Entry;
use hash::{hash, Hash};
use ledger;
use std::time::{Duration, Instant};
use transaction::Transaction;
@ -26,15 +27,20 @@ impl Recorder {
self.num_hashes += 1;
}
pub fn record(&mut self, transactions: Vec<Transaction>) -> Entry {
Entry::new_mut(&mut self.last_hash, &mut self.num_hashes, transactions)
pub fn record(&mut self, transactions: Vec<Transaction>) -> Vec<Entry> {
ledger::next_entries_mut(&mut self.last_hash, &mut self.num_hashes, transactions)
}
pub fn tick(&mut self, start_time: Instant, tick_duration: Duration) -> Option<Entry> {
if start_time.elapsed() > tick_duration * (self.num_ticks + 1) {
// TODO: don't let this overflow u32
self.num_ticks += 1;
Some(self.record(vec![]))
Some(Entry::new_mut(
&mut self.last_hash,
&mut self.num_hashes,
vec![],
false,
))
} else {
None
}

View File

@ -2,49 +2,37 @@
use bank::Bank;
use ledger;
use packet;
use result::Result;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{Builder, JoinHandle};
use std::time::Duration;
use streamer;
use streamer::BlobReceiver;
pub struct ReplicateStage {
pub thread_hdl: JoinHandle<()>,
}
impl ReplicateStage {
/// Process verified blobs, already in order
fn replicate_requests(
bank: &Arc<Bank>,
verified_receiver: &streamer::BlobReceiver,
blob_recycler: &packet::BlobRecycler,
) -> Result<()> {
/// Process entry blobs, already in order
fn replicate_requests(bank: &Arc<Bank>, blob_receiver: &BlobReceiver) -> Result<()> {
let timer = Duration::new(1, 0);
let blobs = verified_receiver.recv_timeout(timer)?;
let entries = ledger::reconstruct_entries_from_blobs(&blobs);
let blobs = blob_receiver.recv_timeout(timer)?;
let blobs_len = blobs.len();
let entries = ledger::reconstruct_entries_from_blobs(blobs)?;
let res = bank.process_entries(entries);
if res.is_err() {
error!("process_entries {} {:?}", blobs.len(), res);
error!("process_entries {} {:?}", blobs_len, res);
}
res?;
for blob in blobs {
blob_recycler.recycle(blob);
}
Ok(())
}
pub fn new(
bank: Arc<Bank>,
exit: Arc<AtomicBool>,
window_receiver: streamer::BlobReceiver,
blob_recycler: packet::BlobRecycler,
) -> Self {
pub fn new(bank: Arc<Bank>, exit: Arc<AtomicBool>, window_receiver: BlobReceiver) -> Self {
let thread_hdl = Builder::new()
.name("solana-replicate-stage".to_string())
.spawn(move || loop {
let e = Self::replicate_requests(&bank, &window_receiver, &blob_recycler);
let e = Self::replicate_requests(&bank, &window_receiver);
if e.is_err() && exit.load(Ordering::Relaxed) {
break;
}

View File

@ -1,7 +1,7 @@
//! The `request` module defines the messages for the thin client.
use hash::Hash;
use signature::PublicKey;
use signature::{PublicKey, Signature};
#[cfg_attr(feature = "cargo-clippy", allow(large_enum_variant))]
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -9,6 +9,7 @@ pub enum Request {
GetBalance { key: PublicKey },
GetLastId,
GetTransactionCount,
GetSignature { signature: Signature },
}
impl Request {
@ -20,7 +21,8 @@ impl Request {
#[derive(Serialize, Deserialize, Debug)]
pub enum Response {
Balance { key: PublicKey, val: Option<i64> },
Balance { key: PublicKey, val: i64 },
LastId { id: Hash },
TransactionCount { transaction_count: u64 },
SignatureStatus { signature_status: bool },
}

View File

@ -40,6 +40,12 @@ impl RequestProcessor {
info!("Response::TransactionCount {:?}", rsp);
Some(rsp)
}
Request::GetSignature { signature } => {
let signature_status = self.bank.has_signature(&signature);
let rsp = (Response::SignatureStatus { signature_status }, rsp_addr);
info!("Response::Signature {:?}", rsp);
Some(rsp)
}
}
}

View File

@ -1,8 +1,7 @@
//! The `request_stage` processes thin client Request messages.
use bincode::deserialize;
use packet;
use packet::SharedPackets;
use packet::{to_blobs, BlobRecycler, PacketRecycler, Packets, SharedPackets};
use rayon::prelude::*;
use request::Request;
use request_processor::RequestProcessor;
@ -13,17 +12,16 @@ use std::sync::mpsc::{channel, Receiver};
use std::sync::Arc;
use std::thread::{Builder, JoinHandle};
use std::time::Instant;
use streamer;
use streamer::{self, BlobReceiver, BlobSender};
use timing;
pub struct RequestStage {
pub thread_hdl: JoinHandle<()>,
pub blob_receiver: streamer::BlobReceiver,
pub request_processor: Arc<RequestProcessor>,
}
impl RequestStage {
pub fn deserialize_requests(p: &packet::Packets) -> Vec<Option<(Request, SocketAddr)>> {
pub fn deserialize_requests(p: &Packets) -> Vec<Option<(Request, SocketAddr)>> {
p.packets
.par_iter()
.map(|x| {
@ -37,9 +35,9 @@ impl RequestStage {
pub fn process_request_packets(
request_processor: &RequestProcessor,
packet_receiver: &Receiver<SharedPackets>,
blob_sender: &streamer::BlobSender,
packet_recycler: &packet::PacketRecycler,
blob_recycler: &packet::BlobRecycler,
blob_sender: &BlobSender,
packet_recycler: &PacketRecycler,
blob_recycler: &BlobRecycler,
) -> Result<()> {
let (batch, batch_len) = streamer::recv_batch(packet_receiver)?;
@ -60,7 +58,7 @@ impl RequestStage {
let rsps = request_processor.process_requests(reqs);
let blobs = packet::to_blobs(rsps, blob_recycler)?;
let blobs = to_blobs(rsps, blob_recycler)?;
if !blobs.is_empty() {
info!("process: sending blobs: {}", blobs.len());
//don't wake up the other side if there is nothing
@ -84,9 +82,9 @@ impl RequestStage {
request_processor: RequestProcessor,
exit: Arc<AtomicBool>,
packet_receiver: Receiver<SharedPackets>,
packet_recycler: packet::PacketRecycler,
blob_recycler: packet::BlobRecycler,
) -> Self {
packet_recycler: PacketRecycler,
blob_recycler: BlobRecycler,
) -> (Self, BlobReceiver) {
let request_processor = Arc::new(request_processor);
let request_processor_ = request_processor.clone();
let (blob_sender, blob_receiver) = channel();
@ -107,10 +105,12 @@ impl RequestStage {
}
})
.unwrap();
RequestStage {
thread_hdl,
(
RequestStage {
thread_hdl,
request_processor,
},
blob_receiver,
request_processor,
}
)
}
}

View File

@ -24,7 +24,7 @@
//! ```
use bank::Bank;
use packet;
use packet::{BlobRecycler, PacketRecycler};
use request_processor::RequestProcessor;
use request_stage::RequestStage;
use std::net::UdpSocket;
@ -45,7 +45,7 @@ impl Rpu {
respond_socket: UdpSocket,
exit: Arc<AtomicBool>,
) -> Self {
let packet_recycler = packet::PacketRecycler::default();
let packet_recycler = PacketRecycler::default();
let (packet_sender, packet_receiver) = channel();
let t_receiver = streamer::receiver(
requests_socket,
@ -54,9 +54,9 @@ impl Rpu {
packet_sender,
);
let blob_recycler = packet::BlobRecycler::default();
let blob_recycler = BlobRecycler::default();
let request_processor = RequestProcessor::new(bank.clone());
let request_stage = RequestStage::new(
let (request_stage, blob_receiver) = RequestStage::new(
request_processor,
exit.clone(),
packet_receiver,
@ -68,7 +68,7 @@ impl Rpu {
respond_socket,
exit.clone(),
blob_recycler.clone(),
request_stage.blob_receiver,
blob_receiver,
);
let thread_hdls = vec![t_receiver, t_responder, request_stage.thread_hdl];

View File

@ -1,182 +0,0 @@
//! The `server` module hosts all the server microservices.
use bank::Bank;
use crdt::{Crdt, ReplicatedData};
use ncp::Ncp;
use packet;
use rpu::Rpu;
use std::io::Write;
use std::net::UdpSocket;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, RwLock};
use std::thread::JoinHandle;
use std::time::Duration;
use streamer;
use tpu::Tpu;
use tvu::Tvu;
pub struct Server {
pub thread_hdls: Vec<JoinHandle<()>>,
}
impl Server {
/// Create a server instance acting as a leader.
///
/// ```text
/// .---------------------.
/// | Leader |
/// | |
/// .--------. | .-----. |
/// | |---->| | |
/// | Client | | | RPU | |
/// | |<----| | |
/// `----+---` | `-----` |
/// | | ^ |
/// | | | |
/// | | .--+---. |
/// | | | Bank | |
/// | | `------` |
/// | | ^ |
/// | | | | .------------.
/// | | .--+--. .-----. | | |
/// `-------->| TPU +-->| NCP +------>| Validators |
/// | `-----` `-----` | | |
/// | | `------------`
/// `---------------------`
/// ```
pub fn new_leader<W: Write + Send + 'static>(
bank: Bank,
tick_duration: Option<Duration>,
me: ReplicatedData,
requests_socket: UdpSocket,
transactions_socket: UdpSocket,
broadcast_socket: UdpSocket,
respond_socket: UdpSocket,
gossip_socket: UdpSocket,
exit: Arc<AtomicBool>,
writer: W,
) -> Self {
let bank = Arc::new(bank);
let mut thread_hdls = vec![];
let rpu = Rpu::new(bank.clone(), requests_socket, respond_socket, exit.clone());
thread_hdls.extend(rpu.thread_hdls);
let blob_recycler = packet::BlobRecycler::default();
let tpu = Tpu::new(
bank.clone(),
tick_duration,
transactions_socket,
blob_recycler.clone(),
exit.clone(),
writer,
);
thread_hdls.extend(tpu.thread_hdls);
let crdt = Arc::new(RwLock::new(Crdt::new(me)));
let window = streamer::default_window();
let gossip_send_socket = UdpSocket::bind("0.0.0.0:0").expect("bind 0");
let ncp = Ncp::new(
crdt.clone(),
window.clone(),
gossip_socket,
gossip_send_socket,
exit.clone(),
).expect("Ncp::new");
thread_hdls.extend(ncp.thread_hdls);
let t_broadcast = streamer::broadcaster(
broadcast_socket,
exit.clone(),
crdt,
window,
blob_recycler.clone(),
tpu.blob_receiver,
);
thread_hdls.extend(vec![t_broadcast]);
Server { thread_hdls }
}
/// Create a server instance acting as a validator.
///
/// ```text
/// .-------------------------------.
/// | Validator |
/// | |
/// .--------. | .-----. |
/// | |-------------->| | |
/// | Client | | | RPU | |
/// | |<--------------| | |
/// `--------` | `-----` |
/// | ^ |
/// | | |
/// | .--+---. |
/// | | Bank | |
/// | `------` |
/// | ^ |
/// .--------. | | | .------------.
/// | | | .-----. .--+--. .-----. | | |
/// | Leader |--->| NCP +-->| TVU +-->| NCP +------>| Validators |
/// | | | `-----` `-----` `-----` | | |
/// `--------` | | `------------`
/// `-------------------------------`
/// ```
pub fn new_validator(
bank: Bank,
me: ReplicatedData,
requests_socket: UdpSocket,
respond_socket: UdpSocket,
replicate_socket: UdpSocket,
gossip_socket: UdpSocket,
repair_socket: UdpSocket,
leader_repl_data: ReplicatedData,
exit: Arc<AtomicBool>,
) -> Self {
let bank = Arc::new(bank);
let mut thread_hdls = vec![];
let rpu = Rpu::new(bank.clone(), requests_socket, respond_socket, exit.clone());
thread_hdls.extend(rpu.thread_hdls);
let tvu = Tvu::new(
bank.clone(),
me,
gossip_socket,
replicate_socket,
repair_socket,
leader_repl_data,
exit.clone(),
);
thread_hdls.extend(tvu.thread_hdls);
Server { thread_hdls }
}
}
#[cfg(test)]
mod tests {
use bank::Bank;
use crdt::TestNode;
use mint::Mint;
use server::Server;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[test]
fn validator_exit() {
let tn = TestNode::new();
let alice = Mint::new(10_000);
let bank = Bank::new(&alice);
let exit = Arc::new(AtomicBool::new(false));
let v = Server::new_validator(
bank,
tn.data.clone(),
tn.sockets.requests,
tn.sockets.respond,
tn.sockets.replicate,
tn.sockets.gossip,
tn.sockets.repair,
tn.data,
exit.clone(),
);
exit.store(true, Ordering::Relaxed);
for t in v.thread_hdls {
t.join().unwrap();
}
}
}

View File

@ -56,9 +56,8 @@ pub struct GenKeys {
}
impl GenKeys {
pub fn new(seed: &[u8]) -> GenKeys {
let seed32: Vec<_> = seed.iter().map(|&x| x as u32).collect();
let rng = ChaChaRng::from_seed(&seed32);
pub fn new(seed: [u8; 32]) -> GenKeys {
let rng = ChaChaRng::from_seed(seed);
GenKeys {
generator: RefCell::new(rng),
}
@ -68,7 +67,7 @@ impl GenKeys {
KeyPair::generate_pkcs8(self).unwrap().to_vec()
}
pub fn gen_n_seeds(&self, n: i64) -> Vec<[u8; 16]> {
pub fn gen_n_seeds(&self, n: i64) -> Vec<[u8; 32]> {
let mut rng = self.generator.borrow_mut();
(0..n).map(|_| rng.gen()).collect()
}
@ -77,7 +76,7 @@ impl GenKeys {
self.gen_n_seeds(n)
.into_par_iter()
.map(|seed| {
let pkcs8 = GenKeys::new(&seed).new_key();
let pkcs8 = GenKeys::new(seed).new_key();
KeyPair::from_pkcs8(untrusted::Input::from(&pkcs8)).unwrap()
})
.collect()
@ -87,7 +86,7 @@ impl GenKeys {
impl SecureRandom for GenKeys {
fn fill(&self, dest: &mut [u8]) -> Result<(), Unspecified> {
let mut rng = self.generator.borrow_mut();
rng.fill_bytes(dest);
rng.fill(dest);
Ok(())
}
}
@ -99,17 +98,17 @@ mod tests {
#[test]
fn test_new_key_is_deterministic() {
let seed = [1, 2, 3, 4];
let rng0 = GenKeys::new(&seed);
let rng1 = GenKeys::new(&seed);
let seed = [0u8; 32];
let rng0 = GenKeys::new(seed);
let rng1 = GenKeys::new(seed);
for _ in 0..100 {
assert_eq!(rng0.new_key(), rng1.new_key());
}
}
fn gen_n_pubkeys(seed: &[u8], n: i64) -> HashSet<PublicKey> {
GenKeys::new(&seed)
fn gen_n_pubkeys(seed: [u8; 32], n: i64) -> HashSet<PublicKey> {
GenKeys::new(seed)
.gen_n_keypairs(n)
.into_iter()
.map(|x| x.pubkey())
@ -118,8 +117,8 @@ mod tests {
#[test]
fn test_gen_n_pubkeys_deterministic() {
let seed = [1, 2, 3, 4];
assert_eq!(gen_n_pubkeys(&seed, 50), gen_n_pubkeys(&seed, 50));
let seed = [0u8; 32];
assert_eq!(gen_n_pubkeys(seed, 50), gen_n_pubkeys(seed, 50));
}
}
@ -132,8 +131,7 @@ mod bench {
#[bench]
fn bench_gen_keys(b: &mut Bencher) {
let seed: &[_] = &[1, 2, 3, 4];
let rnd = GenKeys::new(seed);
let rnd = GenKeys::new([0u8; 32]);
b.iter(|| rnd.gen_n_keypairs(1000));
}
}

View File

@ -14,22 +14,21 @@ use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread::{spawn, JoinHandle};
use std::time::Instant;
use streamer;
use streamer::{self, PacketReceiver};
use timing;
pub struct SigVerifyStage {
pub verified_receiver: Receiver<Vec<(SharedPackets, Vec<u8>)>>,
pub thread_hdls: Vec<JoinHandle<()>>,
}
impl SigVerifyStage {
pub fn new(exit: Arc<AtomicBool>, packet_receiver: Receiver<SharedPackets>) -> Self {
pub fn new(
exit: Arc<AtomicBool>,
packet_receiver: Receiver<SharedPackets>,
) -> (Self, Receiver<Vec<(SharedPackets, Vec<u8>)>>) {
let (verified_sender, verified_receiver) = channel();
let thread_hdls = Self::verifier_services(exit, packet_receiver, verified_sender);
SigVerifyStage {
thread_hdls,
verified_receiver,
}
(SigVerifyStage { thread_hdls }, verified_receiver)
}
fn verify_batch(batch: Vec<SharedPackets>) -> Vec<(SharedPackets, Vec<u8>)> {
@ -38,7 +37,7 @@ impl SigVerifyStage {
}
fn verifier(
recvr: &Arc<Mutex<streamer::PacketReceiver>>,
recvr: &Arc<Mutex<PacketReceiver>>,
sendr: &Arc<Mutex<Sender<Vec<(SharedPackets, Vec<u8>)>>>>,
) -> Result<()> {
let (batch, len) =
@ -76,7 +75,7 @@ impl SigVerifyStage {
fn verifier_service(
exit: Arc<AtomicBool>,
packet_receiver: Arc<Mutex<streamer::PacketReceiver>>,
packet_receiver: Arc<Mutex<PacketReceiver>>,
verified_sender: Arc<Mutex<Sender<Vec<(SharedPackets, Vec<u8>)>>>>,
) -> JoinHandle<()> {
spawn(move || loop {
@ -89,7 +88,7 @@ impl SigVerifyStage {
fn verifier_services(
exit: Arc<AtomicBool>,
packet_receiver: streamer::PacketReceiver,
packet_receiver: PacketReceiver,
verified_sender: Sender<Vec<(SharedPackets, Vec<u8>)>>,
) -> Vec<JoinHandle<()>> {
let sender = Arc::new(Mutex::new(verified_sender));

View File

@ -3,21 +3,25 @@
use crdt::Crdt;
#[cfg(feature = "erasure")]
use erasure;
use packet::{Blob, BlobRecycler, PacketRecycler, SharedBlob, SharedPackets, BLOB_SIZE};
use packet::{
Blob, BlobRecycler, PacketRecycler, SharedBlob, SharedBlobs, SharedPackets, BLOB_SIZE,
};
use result::{Error, Result};
use std::collections::VecDeque;
use std::mem;
use std::net::{SocketAddr, UdpSocket};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, RwLock};
use std::thread::{Builder, JoinHandle};
use std::time::Duration;
pub const WINDOW_SIZE: usize = 2 * 1024;
pub type PacketReceiver = mpsc::Receiver<SharedPackets>;
pub type PacketSender = mpsc::Sender<SharedPackets>;
pub type BlobSender = mpsc::Sender<VecDeque<SharedBlob>>;
pub type BlobReceiver = mpsc::Receiver<VecDeque<SharedBlob>>;
pub const WINDOW_SIZE: u64 = 2 * 1024;
pub type PacketReceiver = Receiver<SharedPackets>;
pub type PacketSender = Sender<SharedPackets>;
pub type BlobSender = Sender<SharedBlobs>;
pub type BlobReceiver = Receiver<SharedBlobs>;
pub type Window = Arc<RwLock<Vec<Option<SharedBlob>>>>;
fn recv_loop(
sock: &UdpSocket,
@ -27,19 +31,18 @@ fn recv_loop(
) -> Result<()> {
loop {
let msgs = re.allocate();
let msgs_ = msgs.clone();
loop {
match msgs.write()
let result = msgs.write()
.expect("write lock in fn recv_loop")
.recv_from(sock)
{
.recv_from(sock);
match result {
Ok(()) => {
channel.send(msgs_)?;
channel.send(msgs)?;
break;
}
Err(_) => {
if exit.load(Ordering::Relaxed) {
re.recycle(msgs_);
re.recycle(msgs);
return Ok(());
}
}
@ -143,10 +146,10 @@ pub fn blob_receiver(
}
fn find_next_missing(
locked_window: &Arc<RwLock<Vec<Option<SharedBlob>>>>,
locked_window: &Window,
crdt: &Arc<RwLock<Crdt>>,
consumed: &mut usize,
received: &mut usize,
consumed: &mut u64,
received: &mut u64,
) -> Result<Vec<(SocketAddr, Vec<u8>)>> {
if *received <= *consumed {
return Err(Error::GenericError);
@ -154,7 +157,7 @@ fn find_next_missing(
let window = locked_window.read().unwrap();
let reqs: Vec<_> = (*consumed..*received)
.filter_map(|pix| {
let i = pix % WINDOW_SIZE;
let i = (pix % WINDOW_SIZE) as usize;
if let &None = &window[i] {
let val = crdt.read().unwrap().window_index_request(pix as u64);
if let Ok((to, req)) = val {
@ -168,21 +171,21 @@ fn find_next_missing(
}
fn repair_window(
locked_window: &Arc<RwLock<Vec<Option<SharedBlob>>>>,
locked_window: &Window,
crdt: &Arc<RwLock<Crdt>>,
_recycler: &BlobRecycler,
last: &mut usize,
last: &mut u64,
times: &mut usize,
consumed: &mut usize,
received: &mut usize,
consumed: &mut u64,
received: &mut u64,
) -> Result<()> {
#[cfg(feature = "erasure")]
{
if erasure::recover(
_recycler,
&mut locked_window.write().unwrap(),
*consumed,
*received,
*consumed as usize,
*received as usize,
).is_err()
{
trace!("erasure::recover failed");
@ -211,11 +214,11 @@ fn repair_window(
}
fn recv_window(
locked_window: &Arc<RwLock<Vec<Option<SharedBlob>>>>,
locked_window: &Window,
crdt: &Arc<RwLock<Crdt>>,
recycler: &BlobRecycler,
consumed: &mut usize,
received: &mut usize,
consumed: &mut u64,
received: &mut u64,
r: &BlobReceiver,
s: &BlobSender,
retransmit: &BlobSender,
@ -225,6 +228,7 @@ fn recv_window(
let leader_id = crdt.read()
.expect("'crdt' read lock in fn recv_window")
.leader_data()
.expect("leader not ready")
.id;
while let Ok(mut nq) = r.try_recv() {
dq.append(&mut nq)
@ -268,9 +272,10 @@ fn recv_window(
//send a contiguous set of blocks
let mut contq = VecDeque::new();
while let Some(b) = dq.pop_front() {
let b_ = b.clone();
let p = b.write().expect("'b' write lock in fn recv_window");
let pix = p.get_index()? as usize;
let (pix, meta_size) = {
let p = b.write().expect("'b' write lock in fn recv_window");
(p.get_index()?, p.meta.size)
};
if pix > *received {
*received = pix;
}
@ -283,16 +288,32 @@ fn recv_window(
);
continue;
}
let w = pix % WINDOW_SIZE;
let w = (pix % WINDOW_SIZE) as usize;
//TODO, after the block are authenticated
//if we get different blocks at the same index
//that is a network failure/attack
trace!("window w: {} size: {}", w, p.meta.size);
drop(p);
trace!("window w: {} size: {}", w, meta_size);
{
let mut window = locked_window.write().unwrap();
// Search the window for old blobs in the window
// of consumed to received and clear any old ones
for ix in *consumed..(pix + 1) {
let k = (ix % WINDOW_SIZE) as usize;
if let Some(b) = &mut window[k] {
if b.read().unwrap().get_index().unwrap() >= *consumed as u64 {
continue;
}
}
if let Some(b) = mem::replace(&mut window[k], None) {
recycler.recycle(b);
}
}
// Insert the new blob into the window
// spot should be free because we cleared it above
if window[w].is_none() {
window[w] = Some(b_);
window[w] = Some(b);
} else if let Some(cblob) = &window[w] {
if cblob.read().unwrap().get_index().unwrap() != pix as u64 {
warn!("overrun blob at index {:}", w);
@ -301,40 +322,45 @@ fn recv_window(
}
}
loop {
let k = *consumed % WINDOW_SIZE;
let k = (*consumed % WINDOW_SIZE) as usize;
trace!("k: {} consumed: {}", k, *consumed);
if window[k].is_none() {
break;
}
let mut is_coding = false;
if let &Some(ref cblob) = &window[k] {
if cblob
let cblob_r = cblob
.read()
.expect("blob read lock for flags streamer::window")
.is_coding()
{
.expect("blob read lock for flogs streamer::window");
if cblob_r.get_index().unwrap() < *consumed {
break;
}
if cblob_r.is_coding() {
is_coding = true;
}
}
if !is_coding {
contq.push_back(window[k].clone().expect("clone in fn recv_window"));
*consumed += 1;
#[cfg(not(feature = "erasure"))]
{
window[k] = None;
}
} else {
#[cfg(feature = "erasure")]
{
let block_start = *consumed - (*consumed % erasure::NUM_CODED);
let coding_end = block_start + erasure::NUM_CODED;
let block_start = *consumed - (*consumed % erasure::NUM_CODED as u64);
let coding_end = block_start + erasure::NUM_CODED as u64;
// We've received all this block's data blobs, go and null out the window now
for j in block_start..coding_end {
window[j % WINDOW_SIZE] = None;
for j in block_start..*consumed {
if let Some(b) =
mem::replace(&mut window[(j % WINDOW_SIZE) as usize], None)
{
recycler.recycle(b);
}
}
for j in *consumed..coding_end {
window[(j % WINDOW_SIZE) as usize] = None;
}
*consumed += erasure::MAX_MISSING;
*consumed += erasure::MAX_MISSING as u64;
debug!(
"skipping processing coding blob k: {} consumed: {}",
k, *consumed
@ -353,7 +379,7 @@ fn recv_window(
Ok(())
}
fn print_window(locked_window: &Arc<RwLock<Vec<Option<SharedBlob>>>>, consumed: usize) {
fn print_window(locked_window: &Window, consumed: u64) {
{
let buf: Vec<_> = locked_window
.read()
@ -361,7 +387,7 @@ fn print_window(locked_window: &Arc<RwLock<Vec<Option<SharedBlob>>>>, consumed:
.iter()
.enumerate()
.map(|(i, v)| {
if i == (consumed % WINDOW_SIZE) {
if i == (consumed % WINDOW_SIZE) as usize {
"_"
} else if v.is_none() {
"0"
@ -382,14 +408,15 @@ fn print_window(locked_window: &Arc<RwLock<Vec<Option<SharedBlob>>>>, consumed:
}
}
pub fn default_window() -> Arc<RwLock<Vec<Option<SharedBlob>>>> {
Arc::new(RwLock::new(vec![None; WINDOW_SIZE]))
pub fn default_window() -> Window {
Arc::new(RwLock::new(vec![None; WINDOW_SIZE as usize]))
}
pub fn window(
exit: Arc<AtomicBool>,
crdt: Arc<RwLock<Crdt>>,
window: Arc<RwLock<Vec<Option<SharedBlob>>>>,
window: Window,
entry_height: u64,
recycler: BlobRecycler,
r: BlobReceiver,
s: BlobSender,
@ -398,9 +425,9 @@ pub fn window(
Builder::new()
.name("solana-window".to_string())
.spawn(move || {
let mut consumed = 0;
let mut received = 0;
let mut last = 0;
let mut consumed = entry_height;
let mut received = entry_height;
let mut last = entry_height;
let mut times = 0;
loop {
if exit.load(Ordering::Relaxed) {
@ -432,7 +459,7 @@ pub fn window(
fn broadcast(
crdt: &Arc<RwLock<Crdt>>,
window: &Arc<RwLock<Vec<Option<SharedBlob>>>>,
window: &Window,
recycler: &BlobRecycler,
r: &BlobReceiver,
sock: &UdpSocket,
@ -444,63 +471,67 @@ fn broadcast(
while let Ok(mut nq) = r.try_recv() {
dq.append(&mut nq);
}
let mut blobs: Vec<_> = dq.into_iter().collect();
print_window(window, *receive_index as usize);
// flatten deque to vec
let blobs_vec: Vec<_> = dq.into_iter().collect();
// Insert the coding blobs into the blob stream
#[cfg(feature = "erasure")]
erasure::add_coding_blobs(recycler, &mut blobs, *receive_index);
// We could receive more blobs than window slots so
// break them up into window-sized chunks to process
let blobs_chunked = blobs_vec.chunks(WINDOW_SIZE as usize).map(|x| x.to_vec());
let blobs_len = blobs.len();
info!("broadcast blobs.len: {}", blobs_len);
print_window(window, *receive_index);
// Index the blobs
Crdt::index_blobs(crdt, &blobs, receive_index)?;
// keep the cache of blobs that are broadcast
{
let mut win = window.write().unwrap();
for b in &blobs {
let ix = b.read().unwrap().get_index().expect("blob index");
let pos = (ix as usize) % WINDOW_SIZE;
if let Some(x) = &win[pos] {
trace!(
"popped {} at {}",
x.read().unwrap().get_index().unwrap(),
pos
);
recycler.recycle(x.clone());
}
trace!("null {}", pos);
win[pos] = None;
assert!(win[pos].is_none());
}
while let Some(b) = blobs.pop() {
let ix = b.read().unwrap().get_index().expect("blob index");
let pos = (ix as usize) % WINDOW_SIZE;
trace!("caching {} at {}", ix, pos);
assert!(win[pos].is_none());
win[pos] = Some(b);
}
}
for mut blobs in blobs_chunked {
// Insert the coding blobs into the blob stream
#[cfg(feature = "erasure")]
erasure::add_coding_blobs(recycler, &mut blobs, *receive_index);
// Fill in the coding blob data from the window data blobs
#[cfg(feature = "erasure")]
{
if erasure::generate_coding(
&mut window.write().unwrap(),
*receive_index as usize,
blobs_len,
).is_err()
let blobs_len = blobs.len();
debug!("broadcast blobs.len: {}", blobs_len);
// Index the blobs
Crdt::index_blobs(crdt, &blobs, receive_index)?;
// keep the cache of blobs that are broadcast
{
return Err(Error::GenericError);
let mut win = window.write().unwrap();
assert!(blobs.len() <= win.len());
for b in &blobs {
let ix = b.read().unwrap().get_index().expect("blob index");
let pos = (ix % WINDOW_SIZE) as usize;
if let Some(x) = mem::replace(&mut win[pos], None) {
trace!(
"popped {} at {}",
x.read().unwrap().get_index().unwrap(),
pos
);
recycler.recycle(x);
}
trace!("null {}", pos);
}
while let Some(b) = blobs.pop() {
let ix = b.read().unwrap().get_index().expect("blob index");
let pos = (ix % WINDOW_SIZE) as usize;
trace!("caching {} at {}", ix, pos);
assert!(win[pos].is_none());
win[pos] = Some(b);
}
}
// Fill in the coding blob data from the window data blobs
#[cfg(feature = "erasure")]
{
erasure::generate_coding(
&mut window.write().unwrap(),
*receive_index as usize,
blobs_len,
).map_err(|_| Error::GenericError)?;
}
*receive_index += blobs_len as u64;
// Send blobs out from the window
Crdt::broadcast(crdt, &window, &sock, transmit_index, *receive_index)?;
}
*receive_index += blobs_len as u64;
// Send blobs out from the window
Crdt::broadcast(crdt, &window, &sock, transmit_index, *receive_index)?;
Ok(())
}
@ -517,15 +548,16 @@ pub fn broadcaster(
sock: UdpSocket,
exit: Arc<AtomicBool>,
crdt: Arc<RwLock<Crdt>>,
window: Arc<RwLock<Vec<Option<SharedBlob>>>>,
window: Window,
entry_height: u64,
recycler: BlobRecycler,
r: BlobReceiver,
) -> JoinHandle<()> {
Builder::new()
.name("solana-broadcaster".to_string())
.spawn(move || {
let mut transmit_index = 0;
let mut receive_index = 0;
let mut transmit_index = entry_height;
let mut receive_index = entry_height;
loop {
if exit.load(Ordering::Relaxed) {
break;
@ -654,9 +686,8 @@ mod bench {
let timer = Duration::new(1, 0);
match r.recv_timeout(timer) {
Ok(msgs) => {
let msgs_ = msgs.clone();
*rvs.lock().unwrap() += msgs.read().unwrap().packets.len();
recycler.recycle(msgs_);
recycler.recycle(msgs);
}
_ => (),
}
@ -753,12 +784,13 @@ mod test {
let mut msgs = VecDeque::new();
for i in 0..10 {
let b = resp_recycler.allocate();
let b_ = b.clone();
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_back(b_);
{
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_back(b);
}
s_responder.send(msgs).expect("send");
let mut num = 0;
@ -811,6 +843,7 @@ mod test {
exit.clone(),
subs,
win,
0,
resp_recycler.clone(),
r_reader,
s_window,
@ -827,14 +860,15 @@ mod test {
for v in 0..10 {
let i = 9 - v;
let b = resp_recycler.allocate();
let b_ = b.clone();
let mut w = b.write().unwrap();
w.set_index(i).unwrap();
w.set_id(me_id).unwrap();
assert_eq!(i, w.get_index().unwrap());
w.meta.size = PACKET_DATA_SIZE;
w.meta.set_addr(&tn.data.gossip_addr);
msgs.push_back(b_);
{
let mut w = b.write().unwrap();
w.set_index(i).unwrap();
w.set_id(me_id).unwrap();
assert_eq!(i, w.get_index().unwrap());
w.meta.size = PACKET_DATA_SIZE;
w.meta.set_addr(&tn.data.gossip_addr);
}
msgs.push_back(b);
}
s_responder.send(msgs).expect("send");
let mut num = 0;

View File

@ -20,7 +20,8 @@ pub struct ThinClient {
transactions_socket: UdpSocket,
last_id: Option<Hash>,
transaction_count: u64,
balances: HashMap<PublicKey, Option<i64>>,
balances: HashMap<PublicKey, i64>,
signature_status: bool,
}
impl ThinClient {
@ -41,6 +42,7 @@ impl ThinClient {
last_id: None,
transaction_count: 0,
balances: HashMap::new(),
signature_status: false,
};
client
}
@ -61,13 +63,24 @@ impl ThinClient {
self.balances.insert(key, val);
}
Response::LastId { id } => {
info!("Response last_id {:?}", id);
trace!("Response last_id {:?}", id);
self.last_id = Some(id);
}
Response::TransactionCount { transaction_count } => {
info!("Response transaction count {:?}", transaction_count);
trace!("Response transaction count {:?}", transaction_count);
self.transaction_count = transaction_count;
}
Response::SignatureStatus { signature_status } => {
self.signature_status = signature_status;
match signature_status {
true => {
trace!("Response found signature");
}
false => {
trace!("Response signature not found");
}
}
}
}
}
@ -111,7 +124,10 @@ impl ThinClient {
}
self.process_response(resp);
}
self.balances[pubkey].ok_or(io::Error::new(io::ErrorKind::Other, "nokey"))
self.balances
.get(pubkey)
.map(|x| *x)
.ok_or(io::Error::new(io::ErrorKind::Other, "nokey"))
}
/// Request the transaction count. If the response packet is dropped by the network,
@ -121,17 +137,19 @@ impl ThinClient {
let req = Request::GetTransactionCount;
let data =
serialize(&req).expect("serialize GetTransactionCount in pub fn transaction_count");
self.requests_socket
.send_to(&data, &self.requests_addr)
.expect("buffer error in pub fn transaction_count");
let mut done = false;
while !done {
let resp = self.recv_response().expect("transaction count dropped");
info!("recv_response {:?}", resp);
if let &Response::TransactionCount { .. } = &resp {
done = true;
self.requests_socket
.send_to(&data, &self.requests_addr)
.expect("buffer error in pub fn transaction_count");
if let Ok(resp) = self.recv_response() {
info!("recv_response {:?}", resp);
if let &Response::TransactionCount { .. } = &resp {
done = true;
}
self.process_response(resp);
}
self.process_response(resp);
}
self.transaction_count
}
@ -139,19 +157,27 @@ impl ThinClient {
/// Request the last Entry ID from the server. This method blocks
/// until the server sends a response.
pub fn get_last_id(&mut self) -> Hash {
info!("get_last_id");
trace!("get_last_id");
let req = Request::GetLastId;
let data = serialize(&req).expect("serialize GetLastId in pub fn get_last_id");
self.requests_socket
.send_to(&data, &self.requests_addr)
.expect("buffer error in pub fn get_last_id");
let mut done = false;
while !done {
let resp = self.recv_response().expect("get_last_id response");
if let &Response::LastId { .. } = &resp {
done = true;
debug!("get_last_id send_to {}", &self.requests_addr);
self.requests_socket
.send_to(&data, &self.requests_addr)
.expect("buffer error in pub fn get_last_id");
match self.recv_response() {
Ok(resp) => {
if let &Response::LastId { .. } = &resp {
done = true;
}
self.process_response(resp);
}
Err(e) => {
debug!("thin_client get_last_id error: {}", e);
}
}
self.process_response(resp);
}
self.last_id.expect("some last_id")
}
@ -163,13 +189,35 @@ impl ThinClient {
let now = Instant::now();
loop {
balance = self.get_balance(pubkey);
if balance.is_ok() || now.elapsed().as_secs() > 1 {
if balance.is_ok() && *balance.as_ref().unwrap() != 0 || now.elapsed().as_secs() > 1 {
break;
}
}
balance
}
/// Check a signature in the bank. This method blocks
/// until the server sends a response.
pub fn check_signature(&mut self, sig: &Signature) -> bool {
trace!("check_signature");
let req = Request::GetSignature { signature: *sig };
let data = serialize(&req).expect("serialize GetSignature in pub fn check_signature");
let mut done = false;
while !done {
self.requests_socket
.send_to(&data, &self.requests_addr)
.expect("buffer error in pub fn get_last_id");
if let Ok(resp) = self.recv_response() {
if let &Response::SignatureStatus { .. } = &resp {
done = true;
}
self.process_response(resp);
}
}
self.signature_status
}
}
#[cfg(test)]
@ -178,9 +226,9 @@ mod tests {
use bank::Bank;
use budget::Budget;
use crdt::TestNode;
use fullnode::FullNode;
use logger;
use mint::Mint;
use server::Server;
use signature::{KeyPair, KeyPairUtil};
use std::io::sink;
use std::sync::atomic::{AtomicBool, Ordering};
@ -199,8 +247,9 @@ mod tests {
let bob_pubkey = KeyPair::new().pubkey();
let exit = Arc::new(AtomicBool::new(false));
let server = Server::new_leader(
let server = FullNode::new_leader(
bank,
0,
Some(Duration::from_millis(30)),
leader.data.clone(),
leader.sockets.requests,
@ -243,8 +292,9 @@ mod tests {
let bob_pubkey = KeyPair::new().pubkey();
let exit = Arc::new(AtomicBool::new(false));
let server = Server::new_leader(
let server = FullNode::new_leader(
bank,
0,
Some(Duration::from_millis(30)),
leader.data.clone(),
leader.sockets.requests,
@ -290,4 +340,53 @@ mod tests {
t.join().unwrap();
}
}
#[test]
fn test_client_check_signature() {
logger::setup();
let leader = TestNode::new();
let alice = Mint::new(10_000);
let bank = Bank::new(&alice);
let bob_pubkey = KeyPair::new().pubkey();
let exit = Arc::new(AtomicBool::new(false));
let server = FullNode::new_leader(
bank,
0,
Some(Duration::from_millis(30)),
leader.data.clone(),
leader.sockets.requests,
leader.sockets.transaction,
leader.sockets.broadcast,
leader.sockets.respond,
leader.sockets.gossip,
exit.clone(),
sink(),
);
sleep(Duration::from_millis(300));
let requests_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
requests_socket
.set_read_timeout(Some(Duration::new(5, 0)))
.unwrap();
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let mut client = ThinClient::new(
leader.data.requests_addr,
requests_socket,
leader.data.transactions_addr,
transactions_socket,
);
let last_id = client.get_last_id();
let sig = client
.transfer(500, &alice.keypair(), bob_pubkey, &last_id)
.unwrap();
sleep(Duration::from_millis(100));
assert!(client.check_signature(&sig));
exit.store(true, Ordering::Relaxed);
for t in server.thread_hdls {
t.join().unwrap();
}
}
}

View File

@ -2,6 +2,10 @@
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn duration_as_us(d: &Duration) -> u64 {
return (d.as_secs() * 1000 * 1000) + (d.subsec_nanos() as u64 / 1_000);
}
pub fn duration_as_ms(d: &Duration) -> u64 {
return (d.as_secs() * 1000) + (d.subsec_nanos() as u64 / 1_000_000);
}

View File

@ -34,14 +34,13 @@ use sigverify_stage::SigVerifyStage;
use std::io::Write;
use std::net::UdpSocket;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::Duration;
use streamer::BlobReceiver;
use write_stage::WriteStage;
pub struct Tpu {
pub blob_receiver: BlobReceiver,
pub thread_hdls: Vec<JoinHandle<()>>,
}
@ -53,47 +52,43 @@ impl Tpu {
blob_recycler: BlobRecycler,
exit: Arc<AtomicBool>,
writer: W,
) -> Self {
) -> (Self, BlobReceiver) {
let packet_recycler = PacketRecycler::default();
let fetch_stage =
let (fetch_stage, packet_receiver) =
FetchStage::new(transactions_socket, exit.clone(), packet_recycler.clone());
let sigverify_stage = SigVerifyStage::new(exit.clone(), fetch_stage.packet_receiver);
let (sigverify_stage, verified_receiver) =
SigVerifyStage::new(exit.clone(), packet_receiver);
let banking_stage = BankingStage::new(
let (banking_stage, signal_receiver) = BankingStage::new(
bank.clone(),
exit.clone(),
sigverify_stage.verified_receiver,
verified_receiver,
packet_recycler.clone(),
);
let record_stage = match tick_duration {
Some(tick_duration) => RecordStage::new_with_clock(
banking_stage.signal_receiver,
&bank.last_id(),
tick_duration,
),
None => RecordStage::new(banking_stage.signal_receiver, &bank.last_id()),
let (record_stage, entry_receiver) = match tick_duration {
Some(tick_duration) => {
RecordStage::new_with_clock(signal_receiver, &bank.last_id(), tick_duration)
}
None => RecordStage::new(signal_receiver, &bank.last_id()),
};
let write_stage = WriteStage::new(
let (write_stage, blob_receiver) = WriteStage::new(
bank.clone(),
exit.clone(),
blob_recycler.clone(),
Mutex::new(writer),
record_stage.entry_receiver,
writer,
entry_receiver,
);
let mut thread_hdls = vec![
fetch_stage.thread_hdl,
banking_stage.thread_hdl,
record_stage.thread_hdl,
write_stage.thread_hdl,
];
thread_hdls.extend(fetch_stage.thread_hdls.into_iter());
thread_hdls.extend(sigverify_stage.thread_hdls.into_iter());
Tpu {
blob_receiver: write_stage.blob_receiver,
thread_hdls,
}
(Tpu { thread_hdls }, blob_receiver)
}
}

View File

@ -180,7 +180,8 @@ impl Transaction {
/// Verify only the payment plan.
pub fn verify_plan(&self) -> bool {
if let Instruction::NewContract(contract) = &self.instruction {
self.fee >= 0 && self.fee <= contract.tokens
self.fee >= 0
&& self.fee <= contract.tokens
&& contract.plan.verify(contract.tokens - self.fee)
} else {
true

View File

@ -1,36 +1,50 @@
//! The `tvu` module implements the Transaction Validation Unit, a
//! 5-stage transaction validation pipeline in software.
//! 1. streamer
//! - Incoming blobs are picked up from the replicate socket.
//! 2. verifier
//! - TODO Blobs are sent to the GPU, and while the memory is there the PoH stream is verified
//! along with the ecdsa signature for the blob and each signature in all the transactions. Blobs
//! with errors are dropped, or marked for slashing.
//! 3.a retransmit
//! - Blobs originating from the parent (leader, at the moment, is the only parent), are retransmit to all the
//! peers in the crdt. Peers is everyone who is not me or the leader that has a known replicate
//! address.
//! 3.b window
//! - Verified blobs are placed into a window, indexed by the counter set by the leader.sockets. This could
//! be the PoH counter if its monotonically increasing in each blob. Erasure coding is used to
//! recover any missing packets, and requests are made at random to peers and parents to retransmit
//! a missing packet.
//! 4. accountant
//! - Contigous blobs are sent to the accountant for processing transactions
//! 5. validator
//! - TODO Validation messages are sent back to the leader
//! 3-stage transaction validation pipeline in software.
//!
//! ```text
//! .------------------------------------------.
//! | TVU |
//! | |
//! | | .------------.
//! | .------------------------>| Validators |
//! | .-------. | | `------------`
//! .--------. | | | .----+---. .-----------. |
//! | Leader |--------->| Blob | | Window | | Replicate | |
//! `--------` | | Fetch |-->| Stage |-->| Stage | |
//! .------------. | | Stage | | | | | |
//! | Validators |----->| | `--------` `----+------` |
//! `------------` | `-------` | |
//! | | |
//! | | |
//! | | |
//! `--------------------------------|---------`
//! |
//! v
//! .------.
//! | Bank |
//! `------`
//! ```
//!
//! 1. Fetch Stage
//! - Incoming blobs are picked up from the replicate socket and repair socket.
//! 2. Window Stage
//! - Blobs are windowed until a contiguous chunk is available. This stage also repairs and
//! retransmits blobs that are in the queue.
//! 3. Replicate Stage
//! - Transactions in blobs are processed and applied to the bank.
//! - TODO We need to verify the signatures in the blobs.
use bank::Bank;
use crdt::{Crdt, ReplicatedData};
use ncp::Ncp;
use packet;
use blob_fetch_stage::BlobFetchStage;
use crdt::Crdt;
use packet::BlobRecycler;
use replicate_stage::ReplicateStage;
use std::net::UdpSocket;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock};
use std::thread::JoinHandle;
use streamer;
use streamer::Window;
use window_stage::WindowStage;
pub struct Tvu {
pub thread_hdls: Vec<JoinHandle<()>>,
@ -41,98 +55,47 @@ impl Tvu {
/// on the bank state.
/// # Arguments
/// * `bank` - The bank state.
/// * `me` - my configuration
/// * `gossip` - my gossisp socket
/// * `replicate` - my replicate socket
/// * `leader` - leader configuration
/// * `entry_height` - Initial ledger height, passed to replicate stage
/// * `crdt` - The crdt state.
/// * `window` - The window state.
/// * `replicate_socket` - my replicate socket
/// * `repair_socket` - my repair socket
/// * `retransmit_socket` - my retransmit socket
/// * `exit` - The exit signal.
pub fn new(
bank: Arc<Bank>,
me: ReplicatedData,
gossip_listen_socket: UdpSocket,
replicate: UdpSocket,
entry_height: u64,
crdt: Arc<RwLock<Crdt>>,
window: Window,
replicate_socket: UdpSocket,
repair_socket: UdpSocket,
leader: ReplicatedData,
retransmit_socket: UdpSocket,
exit: Arc<AtomicBool>,
) -> Self {
//replicate pipeline
let crdt = Arc::new(RwLock::new(Crdt::new(me)));
crdt.write()
.expect("'crdt' write lock in pub fn replicate")
.set_leader(leader.id);
crdt.write()
.expect("'crdt' write lock before insert() in pub fn replicate")
.insert(&leader);
let window = streamer::default_window();
let gossip_send_socket = UdpSocket::bind("0.0.0.0:0").expect("bind 0");
let ncp = Ncp::new(
crdt.clone(),
window.clone(),
gossip_listen_socket,
gossip_send_socket,
exit.clone(),
).expect("Ncp::new");
// TODO pull this socket out through the public interface
// make sure we are on the same interface
let mut local = replicate.local_addr().expect("tvu: get local address");
local.set_port(0);
let write = UdpSocket::bind(local).expect("tvu: bind to local socket");
let blob_recycler = packet::BlobRecycler::default();
let (blob_sender, blob_receiver) = channel();
let t_blob_receiver = streamer::blob_receiver(
let blob_recycler = BlobRecycler::default();
let (fetch_stage, blob_receiver) = BlobFetchStage::new_multi_socket(
vec![replicate_socket, repair_socket],
exit.clone(),
blob_recycler.clone(),
replicate,
blob_sender.clone(),
).expect("tvu: blob receiver creation");
let (window_sender, window_receiver) = channel();
let (retransmit_sender, retransmit_receiver) = channel();
let t_retransmit = streamer::retransmitter(
write,
exit.clone(),
crdt.clone(),
blob_recycler.clone(),
retransmit_receiver,
);
let t_repair_receiver = streamer::blob_receiver(
exit.clone(),
blob_recycler.clone(),
repair_socket,
blob_sender.clone(),
).expect("tvu: blob repair receiver fail");
//TODO
//the packets coming out of blob_receiver need to be sent to the GPU and verified
//then sent to the window, which does the erasure coding reconstruction
let t_window = streamer::window(
exit.clone(),
crdt.clone(),
let (window_stage, blob_receiver) = WindowStage::new(
crdt,
window,
entry_height,
retransmit_socket,
exit.clone(),
blob_recycler.clone(),
blob_receiver,
window_sender,
retransmit_sender,
);
let replicate_stage = ReplicateStage::new(
bank.clone(),
exit.clone(),
window_receiver,
blob_recycler.clone(),
);
let replicate_stage = ReplicateStage::new(bank, exit, blob_receiver);
let mut threads = vec![
//replicate threads
t_blob_receiver,
t_retransmit,
t_window,
t_repair_receiver,
replicate_stage.thread_hdl,
];
threads.extend(ncp.thread_hdls.into_iter());
let mut threads = vec![replicate_stage.thread_hdl];
threads.extend(fetch_stage.thread_hdls.into_iter());
threads.extend(window_stage.thread_hdls.into_iter());
Tvu {
thread_hdls: threads,
}
@ -158,18 +121,19 @@ pub mod tests {
use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use streamer;
use streamer::{self, Window};
use transaction::Transaction;
use tvu::Tvu;
fn new_replicator(
fn new_ncp(
crdt: Arc<RwLock<Crdt>>,
listen: UdpSocket,
exit: Arc<AtomicBool>,
) -> Result<Ncp> {
) -> Result<(Ncp, Window)> {
let window = streamer::default_window();
let send_sock = UdpSocket::bind("0.0.0.0:0").expect("bind 0");
Ncp::new(crdt, window, listen, send_sock, exit)
let ncp = Ncp::new(crdt, window.clone(), listen, send_sock, exit)?;
Ok((ncp, window))
}
/// Test that message sent from leader to target1 and replicated to target2
#[test]
@ -185,7 +149,7 @@ pub mod tests {
crdt_l.set_leader(leader.data.id);
let cref_l = Arc::new(RwLock::new(crdt_l));
let dr_l = new_replicator(cref_l, leader.sockets.gossip, exit.clone()).unwrap();
let dr_l = new_ncp(cref_l, leader.sockets.gossip, exit.clone()).unwrap();
//start crdt2
let mut crdt2 = Crdt::new(target2.data.clone());
@ -193,7 +157,7 @@ pub mod tests {
crdt2.set_leader(leader.data.id);
let leader_id = leader.data.id;
let cref2 = Arc::new(RwLock::new(crdt2));
let dr_2 = new_replicator(cref2, target2.sockets.gossip, exit.clone()).unwrap();
let dr_2 = new_ncp(cref2, target2.sockets.gossip, exit.clone()).unwrap();
// setup some blob services to send blobs into the socket
// to simulate the source peer and get blobs out of the socket to
@ -221,30 +185,34 @@ pub mod tests {
let mint = Mint::new(starting_balance);
let replicate_addr = target1.data.replicate_addr;
let bank = Arc::new(Bank::new(&mint));
//start crdt1
let mut crdt1 = Crdt::new(target1.data.clone());
crdt1.insert(&leader.data);
crdt1.set_leader(leader.data.id);
let cref1 = Arc::new(RwLock::new(crdt1));
let dr_1 = new_ncp(cref1.clone(), target1.sockets.gossip, exit.clone()).unwrap();
let tvu = Tvu::new(
bank.clone(),
target1.data,
target1.sockets.gossip,
0,
cref1,
dr_1.1,
target1.sockets.replicate,
target1.sockets.repair,
leader.data,
target1.sockets.retransmit,
exit.clone(),
);
let mut alice_ref_balance = starting_balance;
let mut msgs = VecDeque::new();
let mut cur_hash = Hash::default();
let num_blobs = 10;
let mut blob_id = 0;
let num_transfers = 10;
let transfer_amount = 501;
let bob_keypair = KeyPair::new();
for i in 0..num_blobs {
let b = resp_recycler.allocate();
let b_ = b.clone();
let mut w = b.write().unwrap();
w.set_index(i).unwrap();
w.set_id(leader_id).unwrap();
let entry0 = Entry::new(&cur_hash, i, vec![]);
for i in 0..num_transfers {
let entry0 = Entry::new(&cur_hash, i, vec![], false);
bank.register_entry_id(&cur_hash);
cur_hash = hash(&cur_hash);
@ -256,19 +224,28 @@ pub mod tests {
);
bank.register_entry_id(&cur_hash);
cur_hash = hash(&cur_hash);
let entry1 = Entry::new(&cur_hash, i + num_blobs, vec![tx0]);
let entry1 = Entry::new(&cur_hash, i + num_transfers, vec![tx0], false);
bank.register_entry_id(&cur_hash);
cur_hash = hash(&cur_hash);
alice_ref_balance -= transfer_amount;
let serialized_entry = serialize(&vec![entry0, entry1]).unwrap();
for entry in vec![entry0, entry1] {
let b = resp_recycler.allocate();
{
let mut w = b.write().unwrap();
w.set_index(blob_id).unwrap();
blob_id += 1;
w.set_id(leader_id).unwrap();
w.data_mut()[..serialized_entry.len()].copy_from_slice(&serialized_entry);
w.set_size(serialized_entry.len());
w.meta.set_addr(&replicate_addr);
drop(w);
msgs.push_back(b_);
let serialized_entry = serialize(&entry).unwrap();
w.data_mut()[..serialized_entry.len()].copy_from_slice(&serialized_entry);
w.set_size(serialized_entry.len());
w.meta.set_addr(&replicate_addr);
}
msgs.push_back(b);
}
}
// send the blobs into the socket
@ -276,26 +253,27 @@ pub mod tests {
// receive retransmitted messages
let timer = Duration::new(1, 0);
let mut msgs: Vec<_> = Vec::new();
while let Ok(msg) = r_reader.recv_timeout(timer) {
trace!("msg: {:?}", msg);
msgs.push(msg);
}
let alice_balance = bank.get_balance(&mint.keypair().pubkey()).unwrap();
let alice_balance = bank.get_balance(&mint.keypair().pubkey());
assert_eq!(alice_balance, alice_ref_balance);
let bob_balance = bank.get_balance(&bob_keypair.pubkey()).unwrap();
let bob_balance = bank.get_balance(&bob_keypair.pubkey());
assert_eq!(bob_balance, starting_balance - alice_ref_balance);
exit.store(true, Ordering::Relaxed);
for t in tvu.thread_hdls {
t.join().expect("join");
}
for t in dr_l.thread_hdls {
for t in dr_l.0.thread_hdls {
t.join().expect("join");
}
for t in dr_2.thread_hdls {
for t in dr_2.0.thread_hdls {
t.join().expect("join");
}
for t in dr_1.0.thread_hdls {
t.join().expect("join");
}
t_receiver.join().expect("join");

50
src/window_stage.rs Normal file
View File

@ -0,0 +1,50 @@
//! The `window_stage` maintains the blob window
use crdt::Crdt;
use packet::BlobRecycler;
use std::net::UdpSocket;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock};
use std::thread::JoinHandle;
use streamer::{self, BlobReceiver, Window};
pub struct WindowStage {
pub thread_hdls: Vec<JoinHandle<()>>,
}
impl WindowStage {
pub fn new(
crdt: Arc<RwLock<Crdt>>,
window: Window,
entry_height: u64,
retransmit_socket: UdpSocket,
exit: Arc<AtomicBool>,
blob_recycler: BlobRecycler,
fetch_stage_receiver: BlobReceiver,
) -> (Self, BlobReceiver) {
let (retransmit_sender, retransmit_receiver) = channel();
let t_retransmit = streamer::retransmitter(
retransmit_socket,
exit.clone(),
crdt.clone(),
blob_recycler.clone(),
retransmit_receiver,
);
let (blob_sender, blob_receiver) = channel();
let t_window = streamer::window(
exit.clone(),
crdt.clone(),
window,
entry_height,
blob_recycler.clone(),
fetch_stage_receiver,
blob_sender,
retransmit_sender,
);
let thread_hdls = vec![t_retransmit, t_window];
(WindowStage { thread_hdls }, blob_receiver)
}
}

View File

@ -5,75 +5,71 @@
use bank::Bank;
use entry::Entry;
use entry_writer::EntryWriter;
use packet;
use ledger::Block;
use packet::BlobRecycler;
use result::Result;
use std::collections::VecDeque;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, Receiver};
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use std::thread::{Builder, JoinHandle};
use streamer;
use std::time::Duration;
use streamer::{BlobReceiver, BlobSender};
pub struct WriteStage {
pub thread_hdl: JoinHandle<()>,
pub blob_receiver: streamer::BlobReceiver,
}
impl WriteStage {
/// Process any Entry items that have been published by the Historian.
/// continuosly broadcast blobs of entries out
pub fn write_and_send_entries<W: Write>(
entry_writer: &mut EntryWriter<W>,
blob_sender: &BlobSender,
blob_recycler: &BlobRecycler,
entry_receiver: &Receiver<Vec<Entry>>,
) -> Result<()> {
let entries = entry_receiver.recv_timeout(Duration::new(1, 0))?;
entry_writer.write_and_register_entries(&entries)?;
trace!("New blobs? {}", entries.len());
let mut blobs = VecDeque::new();
entries.to_blobs(blob_recycler, &mut blobs);
if !blobs.is_empty() {
trace!("broadcasting {}", blobs.len());
blob_sender.send(blobs)?;
}
Ok(())
}
/// Create a new Rpu that wraps the given Bank.
pub fn new<W: Write + Send + 'static>(
bank: Arc<Bank>,
exit: Arc<AtomicBool>,
blob_recycler: packet::BlobRecycler,
writer: Mutex<W>,
entry_receiver: Receiver<Entry>,
) -> Self {
blob_recycler: BlobRecycler,
writer: W,
entry_receiver: Receiver<Vec<Entry>>,
) -> (Self, BlobReceiver) {
let (blob_sender, blob_receiver) = channel();
let thread_hdl = Builder::new()
.name("solana-writer".to_string())
.spawn(move || loop {
let entry_writer = EntryWriter::new(&bank);
let _ = entry_writer.write_and_send_entries(
&blob_sender,
&blob_recycler,
&writer,
&entry_receiver,
);
if exit.load(Ordering::Relaxed) {
info!("broadcat_service exiting");
break;
}
})
.unwrap();
WriteStage {
thread_hdl,
blob_receiver,
}
}
pub fn new_drain(
bank: Arc<Bank>,
exit: Arc<AtomicBool>,
entry_receiver: Receiver<Entry>,
) -> Self {
let (_blob_sender, blob_receiver) = channel();
let thread_hdl = Builder::new()
.name("solana-drain".to_string())
.spawn(move || {
let entry_writer = EntryWriter::new(&bank);
let mut entry_writer = EntryWriter::new(&bank, writer);
loop {
let _ = entry_writer.drain_entries(&entry_receiver);
let _ = Self::write_and_send_entries(
&mut entry_writer,
&blob_sender,
&blob_recycler,
&entry_receiver,
);
if exit.load(Ordering::Relaxed) {
info!("drain_service exiting");
info!("broadcat_service exiting");
break;
}
}
})
.unwrap();
WriteStage {
thread_hdl,
blob_receiver,
}
(WriteStage { thread_hdl }, blob_receiver)
}
}

View File

@ -150,7 +150,8 @@ pub fn crdt_retransmit() {
trace!("waiting to converge:");
let mut done = false;
for _ in 0..30 {
done = c1.read().unwrap().table.len() == 3 && c2.read().unwrap().table.len() == 3
done = c1.read().unwrap().table.len() == 3
&& c2.read().unwrap().table.len() == 3
&& c3.read().unwrap().table.len() == 3;
if done {
break;
@ -182,3 +183,109 @@ pub fn crdt_retransmit() {
t.join().unwrap();
}
}
#[test]
fn test_external_liveness_table() {
logger::setup();
let c1_c4_exit = Arc::new(AtomicBool::new(false));
let c2_c3_exit = Arc::new(AtomicBool::new(false));
trace!("c1:");
let (c1, dr1, _) = test_node(c1_c4_exit.clone());
trace!("c2:");
let (c2, dr2, _) = test_node(c2_c3_exit.clone());
trace!("c3:");
let (c3, dr3, _) = test_node(c2_c3_exit.clone());
trace!("c4:");
let (c4, dr4, _) = test_node(c1_c4_exit.clone());
let c1_data = c1.read().unwrap().my_data().clone();
c1.write().unwrap().set_leader(c1_data.id);
let c2_id = c2.read().unwrap().me;
let c3_id = c3.read().unwrap().me;
let c4_id = c4.read().unwrap().me;
// Insert the remote data about c4
let c2_index_for_c4 = 10;
c2.write().unwrap().remote.insert(c4_id, c2_index_for_c4);
let c3_index_for_c4 = 20;
c3.write().unwrap().remote.insert(c4_id, c3_index_for_c4);
// Set up the initial network topology
c2.write().unwrap().insert(&c1_data);
c3.write().unwrap().insert(&c1_data);
c2.write().unwrap().set_leader(c1_data.id);
c3.write().unwrap().set_leader(c1_data.id);
// Wait to converge
trace!("waiting to converge:");
let mut done = false;
for _ in 0..30 {
done = c1.read().unwrap().table.len() == 3
&& c2.read().unwrap().table.len() == 3
&& c3.read().unwrap().table.len() == 3;
if done {
break;
}
sleep(Duration::new(1, 0));
}
assert!(done);
// Validate c1's external liveness table, then release lock rc1
{
let rc1 = c1.read().unwrap();
let el = rc1.get_external_liveness_entry(&c4.read().unwrap().me);
// Make sure liveness table entry for c4 exists on node c1
assert!(el.is_some());
let liveness_map = el.unwrap();
// Make sure liveness table entry contains correct result for c2
let c2_index_result_for_c4 = liveness_map.get(&c2_id);
assert!(c2_index_result_for_c4.is_some());
assert_eq!(*(c2_index_result_for_c4.unwrap()), c2_index_for_c4);
// Make sure liveness table entry contains correct result for c3
let c3_index_result_for_c4 = liveness_map.get(&c3_id);
assert!(c3_index_result_for_c4.is_some());
assert_eq!(*(c3_index_result_for_c4.unwrap()), c3_index_for_c4);
}
// Shutdown validators c2 and c3
c2_c3_exit.store(true, Ordering::Relaxed);
let mut threads = vec![];
threads.extend(dr2.thread_hdls.into_iter());
threads.extend(dr3.thread_hdls.into_iter());
for t in threads {
t.join().unwrap();
}
// Allow communication between c1 and c4, make sure that c1's external_liveness table
// entry for c4 gets cleared
c4.write().unwrap().insert(&c1_data);
c4.write().unwrap().set_leader(c1_data.id);
for _ in 0..30 {
done = c1.read()
.unwrap()
.get_external_liveness_entry(&c4_id)
.is_none();
if done {
break;
}
sleep(Duration::new(1, 0));
}
assert!(done);
// Shutdown validators c1 and c4
c1_c4_exit.store(true, Ordering::Relaxed);
let mut threads = vec![];
threads.extend(dr1.thread_hdls.into_iter());
threads.extend(dr4.thread_hdls.into_iter());
for t in threads {
t.join().unwrap();
}
}

View File

@ -6,10 +6,10 @@ extern crate solana;
use solana::bank::Bank;
use solana::crdt::TestNode;
use solana::crdt::{Crdt, ReplicatedData};
use solana::fullnode::FullNode;
use solana::logger;
use solana::mint::Mint;
use solana::ncp::Ncp;
use solana::server::Server;
use solana::signature::{KeyPair, KeyPairUtil, PublicKey};
use solana::streamer::default_window;
use solana::thin_client::ThinClient;
@ -30,8 +30,9 @@ fn validator(
) {
let validator = TestNode::new();
let replicant_bank = Bank::new(&alice);
let mut ts = Server::new_validator(
let mut ts = FullNode::new_validator(
replicant_bank,
0,
validator.data.clone(),
validator.sockets.requests,
validator.sockets.respond,
@ -49,7 +50,7 @@ fn converge(
exit: Arc<AtomicBool>,
num_nodes: usize,
threads: &mut Vec<JoinHandle<()>>,
) -> Vec<ReplicatedData> {
) -> (Vec<ReplicatedData>, PublicKey) {
//lets spy on the network
let mut spy = TestNode::new();
let daddr = "0.0.0.0:0".parse().unwrap();
@ -89,22 +90,23 @@ fn converge(
.filter(|x| x.id != me)
.map(|x| x.clone())
.collect();
v.clone()
(v.clone(), me)
}
#[test]
fn test_multi_node() {
fn test_multi_node_validator_catchup_from_zero() {
logger::setup();
const N: usize = 5;
trace!("test_multi_accountant_stub");
trace!("test_multi_node_validator_catchup_from_zero");
let leader = TestNode::new();
let alice = Mint::new(10_000);
let bob_pubkey = KeyPair::new().pubkey();
let exit = Arc::new(AtomicBool::new(false));
let leader_bank = Bank::new(&alice);
let server = Server::new_leader(
let server = FullNode::new_leader(
leader_bank,
0,
None,
leader.data.clone(),
leader.sockets.requests,
@ -120,7 +122,97 @@ fn test_multi_node() {
for _ in 0..N {
validator(&leader.data, exit.clone(), &alice, &mut threads);
}
let servers = converge(&leader.data, exit.clone(), N + 2, &mut threads);
let (servers, spy_id0) = converge(&leader.data, exit.clone(), N + 2, &mut threads);
//contains the leader addr as well
assert_eq!(servers.len(), N + 1);
//verify leader can do transfer
let leader_balance = tx_and_retry_get_balance(&leader.data, &alice, &bob_pubkey).unwrap();
assert_eq!(leader_balance, 500);
//verify validator has the same balance
let mut success = 0usize;
for server in servers.iter() {
info!("0server: {:?}", server.id[0]);
let mut client = mk_client(server);
if let Ok(bal) = client.poll_get_balance(&bob_pubkey) {
info!("validator balance {}", bal);
if bal == leader_balance {
success += 1;
}
}
}
assert_eq!(success, servers.len());
success = 0;
// start up another validator, converge and then check everyone's balances
validator(&leader.data, exit.clone(), &alice, &mut threads);
let (servers, _) = converge(&leader.data, exit.clone(), N + 4, &mut threads);
let mut leader_balance = tx_and_retry_get_balance(&leader.data, &alice, &bob_pubkey).unwrap();
info!("leader balance {}", leader_balance);
loop {
let mut client = mk_client(&leader.data);
leader_balance = client.poll_get_balance(&bob_pubkey).unwrap();
if leader_balance == 1000 {
break;
}
sleep(Duration::from_millis(300));
}
assert_eq!(leader_balance, 1000);
for server in servers.iter() {
if server.id != spy_id0 {
let mut client = mk_client(server);
info!("1server: {:?}", server.id[0]);
for _ in 0..10 {
if let Ok(bal) = client.poll_get_balance(&bob_pubkey) {
info!("validator balance {}", bal);
if bal == leader_balance {
success += 1;
break;
}
}
sleep(Duration::from_millis(500));
}
}
}
assert_eq!(success, (servers.len() - 1));
exit.store(true, Ordering::Relaxed);
for t in threads {
t.join().unwrap();
}
}
#[test]
fn test_multi_node_basic() {
logger::setup();
const N: usize = 5;
trace!("test_multi_node_basic");
let leader = TestNode::new();
let alice = Mint::new(10_000);
let bob_pubkey = KeyPair::new().pubkey();
let exit = Arc::new(AtomicBool::new(false));
let leader_bank = Bank::new(&alice);
let server = FullNode::new_leader(
leader_bank,
0,
None,
leader.data.clone(),
leader.sockets.requests,
leader.sockets.transaction,
leader.sockets.broadcast,
leader.sockets.respond,
leader.sockets.gossip,
exit.clone(),
sink(),
);
let mut threads = server.thread_hdls;
for _ in 0..N {
validator(&leader.data, exit.clone(), &alice, &mut threads);
}
let (servers, _) = converge(&leader.data, exit.clone(), N + 2, &mut threads);
//contains the leader addr as well
assert_eq!(servers.len(), N + 1);
//verify leader can do transfer
@ -138,6 +230,7 @@ fn test_multi_node() {
}
}
assert_eq!(success, servers.len());
exit.store(true, Ordering::Relaxed);
for t in threads {
t.join().unwrap();
@ -167,7 +260,7 @@ fn tx_and_retry_get_balance(
let mut client = mk_client(leader);
trace!("getting leader last_id");
let last_id = client.get_last_id();
info!("executing leader transer");
info!("executing leader transfer");
let _sig = client
.transfer(500, &alice.keypair(), *bob_pubkey, &last_id)
.unwrap();