Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
68ec814bf3 | |||
d7f283a00a | |||
609889cc58 | |||
83fa02ee44 | |||
d8fcb22000 | |||
6b1c90a8b5 | |||
67d72e709f | |||
f0d2870e0f | |||
02c47b48da | |||
d30a39cd38 | |||
9e57d0467e | |||
fb57d13c03 | |||
41f6e27bba | |||
aff5649b39 | |||
9fd6ffe83f | |||
ff805361a9 | |||
30d1b0b4bf | |||
5233cf1ca6 | |||
d2754fd702 | |||
ce9a0ae215 | |||
067adcdfa8 |
@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This script is used to upload the full buildkite pipeline. The steps defined
|
||||
# in the buildkite UI should simply be:
|
||||
#
|
||||
# steps:
|
||||
# - command: ".buildkite/pipeline-upload.sh"
|
||||
#
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
source ci/_
|
||||
sudo chmod 0777 ci/buildkite-pipeline-in-disk.sh
|
||||
|
||||
_ ci/buildkite-pipeline-in-disk.sh pipeline.yml
|
||||
echo +++ pipeline
|
||||
cat pipeline.yml
|
||||
|
||||
_ buildkite-agent pipeline upload pipeline.yml
|
1
.buildkite/env/.gitignore
vendored
1
.buildkite/env/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/secrets_unencrypted.ejson
|
31
.buildkite/env/README.md
vendored
31
.buildkite/env/README.md
vendored
@ -1,31 +0,0 @@
|
||||
|
||||
[ejson](https://github.com/Shopify/ejson) and
|
||||
[ejson2env](https://github.com/Shopify/ejson2env) are used to manage access
|
||||
tokens and other secrets required for CI.
|
||||
|
||||
#### Setup
|
||||
```bash
|
||||
$ sudo gem install ejson ejson2env
|
||||
```
|
||||
|
||||
then obtain the necessary keypair and place it in `/opt/ejson/keys/`.
|
||||
|
||||
#### Usage
|
||||
Run the following command to decrypt the secrets into the environment:
|
||||
```bash
|
||||
eval $(ejson2env secrets.ejson)
|
||||
```
|
||||
|
||||
#### Managing secrets.ejson
|
||||
To decrypt `secrets.ejson` for modification, run:
|
||||
```bash
|
||||
$ ejson decrypt secrets.ejson -o secrets_unencrypted.ejson
|
||||
```
|
||||
|
||||
Edit, then run the following to re-encrypt the file **BEFORE COMMITING YOUR
|
||||
CHANGES**:
|
||||
```bash
|
||||
$ ejson encrypt secrets_unencrypted.ejson
|
||||
$ mv secrets_unencrypted.ejson secrets.ejson
|
||||
```
|
||||
|
7
.buildkite/env/secrets.ejson
vendored
7
.buildkite/env/secrets.ejson
vendored
@ -1,7 +0,0 @@
|
||||
{
|
||||
"_public_key": "ae29f4f7ad2fc92de70d470e411c8426d5d48db8817c9e3dae574b122192335f",
|
||||
"_comment": "These credentials are encrypted and pose no risk",
|
||||
"environment": {
|
||||
"CODECOV_TOKEN": "EJ[1:KToenD1Sr3w82lHGxz1n+j3hwNlLk/1pYrjZHlvY6kE=:hN1Q25omtJ+4yYVn+qzIsPLKT3O6J9XN:DMLNLXi/pkWgvwF6gNIcNF222sgsRR9LnwLZYj0P0wGj7q6w8YQnd1Rskj+sRroI/z5pQg==]"
|
||||
}
|
||||
}
|
@ -1,42 +1,2 @@
|
||||
CI_BUILD_START=$(date +%s)
|
||||
export CI_BUILD_START
|
||||
|
||||
source ci/env.sh
|
||||
|
||||
#
|
||||
# Kill any running docker containers, which are potentially left over from the
|
||||
# previous CI job
|
||||
#
|
||||
(
|
||||
containers=$(docker ps -q)
|
||||
if [[ $(hostname) != metrics-solana-com && -n $containers ]]; then
|
||||
echo "+++ Killing stale docker containers"
|
||||
docker ps
|
||||
|
||||
# shellcheck disable=SC2086 # Don't want to double quote $containers
|
||||
docker kill $containers
|
||||
fi
|
||||
)
|
||||
|
||||
# Processes from previously aborted CI jobs seem to loiter, unclear why as one
|
||||
# would expect the buildkite-agent to clean up all child processes of the
|
||||
# aborted CI job.
|
||||
# But as a workaround for now manually kill some known loiterers. These
|
||||
# processes will all have the `init` process as their PPID:
|
||||
(
|
||||
victims=
|
||||
for name in bash cargo docker solana; do
|
||||
victims="$victims $(pgrep -u "$(id -u)" -P 1 -d \ $name)"
|
||||
done
|
||||
for victim in $victims; do
|
||||
echo "Killing pid $victim"
|
||||
kill -9 "$victim" || true
|
||||
done
|
||||
)
|
||||
|
||||
# HACK: These are in our docker images, need to be removed from CARGO_HOME
|
||||
# because we try to cache downloads across builds with CARGO_HOME
|
||||
# cargo lacks a facility for "system" tooling, always tries CARGO_HOME first
|
||||
cargo uninstall cargo-audit || true
|
||||
cargo uninstall svgbob_cli || true
|
||||
cargo uninstall mdbook || true
|
||||
|
@ -3,17 +3,13 @@
|
||||
#
|
||||
# Save target/ for the next CI build on this machine
|
||||
#
|
||||
if [[ -z $CARGO_TARGET_CACHE ]]; then
|
||||
echo "+++ CARGO_TARGET_CACHE not defined" # pre-command should have defined it
|
||||
else
|
||||
if [[ -n $CARGO_TARGET_CACHE_NAME ]]; then
|
||||
(
|
||||
d=$HOME/cargo-target-cache/"$CARGO_TARGET_CACHE_NAME"
|
||||
mkdir -p "$d"
|
||||
set -x
|
||||
mkdir -p "$CARGO_TARGET_CACHE"
|
||||
set -x
|
||||
rsync -a --delete --link-dest="$PWD" target "$CARGO_TARGET_CACHE"
|
||||
du -hs "$CARGO_TARGET_CACHE"
|
||||
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$CARGO_TARGET_CACHE")
|
||||
echo "--- ${cacheSizeInGB}GB: $CARGO_TARGET_CACHE"
|
||||
rsync -a --delete --link-dest="$PWD" target "$d"
|
||||
du -hs "$d"
|
||||
)
|
||||
fi
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
eval "$(ejson2env .buildkite/env/secrets.ejson)"
|
||||
#!/bin/bash -e
|
||||
|
||||
# Ensure the pattern "+++ ..." never occurs when |set -x| is set, as buildkite
|
||||
# interprets this as the start of a log group.
|
||||
@ -11,39 +8,20 @@ export PS4="++"
|
||||
#
|
||||
# Restore target/ from the previous CI build on this machine
|
||||
#
|
||||
eval "$(ci/channel-info.sh)"
|
||||
eval "$(ci/sbf-tools-info.sh)"
|
||||
source "ci/rust-version.sh"
|
||||
HOST_RUST_VERSION="$rust_stable"
|
||||
pattern='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
if [[ ${HOST_RUST_VERSION} =~ ${pattern} ]]; then
|
||||
HOST_RUST_VERSION="${rust_stable%.*}"
|
||||
fi
|
||||
export CARGO_TARGET_CACHE=$HOME/cargo-target-cache/"$CHANNEL"-"$BUILDKITE_LABEL"-"$SBF_TOOLS_VERSION"-"$HOST_RUST_VERSION"
|
||||
(
|
||||
set -x
|
||||
MAX_CACHE_SIZE=18 # gigabytes
|
||||
[[ -n "$CARGO_TARGET_CACHE_NAME" ]] || (
|
||||
d=$HOME/cargo-target-cache/"$CARGO_TARGET_CACHE_NAME"
|
||||
|
||||
if [[ -d $CARGO_TARGET_CACHE ]]; then
|
||||
du -hs "$CARGO_TARGET_CACHE"
|
||||
read -r cacheSizeInGB _ < <(du -s --block-size=1800000000 "$CARGO_TARGET_CACHE")
|
||||
echo "--- ${cacheSizeInGB}GB: $CARGO_TARGET_CACHE"
|
||||
if [[ $cacheSizeInGB -gt $MAX_CACHE_SIZE ]]; then
|
||||
echo "--- $CARGO_TARGET_CACHE is too large, removing it"
|
||||
rm -rf "$CARGO_TARGET_CACHE"
|
||||
if [[ -d $d ]]; then
|
||||
du -hs "$d"
|
||||
read -r cacheSizeInGB _ < <(du -s --block-size=1000000000 "$d")
|
||||
if [[ $cacheSizeInGB -gt 5 ]]; then
|
||||
echo "$d has gotten too large, removing it"
|
||||
rm -rf "$d"
|
||||
fi
|
||||
else
|
||||
echo "--- $CARGO_TARGET_CACHE not present"
|
||||
fi
|
||||
|
||||
mkdir -p "$CARGO_TARGET_CACHE"/target
|
||||
rsync -a --delete --link-dest="$CARGO_TARGET_CACHE" "$CARGO_TARGET_CACHE"/target .
|
||||
|
||||
# Don't reuse BPF target build artifacts due to incremental build issues with
|
||||
# `std:
|
||||
# "found possibly newer version of crate `std` which `xyz` depends on
|
||||
rm -rf target/bpfel-unknown-unknown
|
||||
if [[ $BUILDKITE_LABEL = "stable-perf" ]]; then
|
||||
rm -rf target/release
|
||||
fi
|
||||
mkdir -p "$d"/target
|
||||
set -x
|
||||
rsync -a --delete --link-dest="$d" "$d"/target .
|
||||
)
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This script is used to upload the full buildkite pipeline. The steps defined
|
||||
# in the buildkite UI should simply be:
|
||||
#
|
||||
# steps:
|
||||
# - command: ".buildkite/pipeline-upload.sh"
|
||||
#
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
source ci/_
|
||||
|
||||
_ ci/buildkite-pipeline.sh pipeline.yml
|
||||
echo +++ pipeline
|
||||
cat pipeline.yml
|
||||
|
||||
_ buildkite-agent pipeline upload pipeline.yml
|
11
.codecov.yml
11
.codecov.yml
@ -1,12 +1,5 @@
|
||||
ignore:
|
||||
- "src/bin"
|
||||
coverage:
|
||||
range: 50..100
|
||||
round: down
|
||||
precision: 1
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
|
||||
comment:
|
||||
layout: "diff"
|
||||
behavior: default
|
||||
require_changes: no
|
||||
|
12
.github/ISSUE_TEMPLATE/0-general.md
vendored
12
.github/ISSUE_TEMPLATE/0-general.md
vendored
@ -1,12 +0,0 @@
|
||||
---
|
||||
name: General Issue
|
||||
about: Create a report describing a problem and a proposed solution
|
||||
title: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
#### Problem
|
||||
|
||||
|
||||
|
||||
#### Proposed Solution
|
70
.github/ISSUE_TEMPLATE/1-feature-gate.yml
vendored
70
.github/ISSUE_TEMPLATE/1-feature-gate.yml
vendored
@ -1,70 +0,0 @@
|
||||
name: Feature Gate Tracker
|
||||
description: Track the development and status of an on-chain feature
|
||||
title: "Feature Gate: "
|
||||
labels: ["feature-gate"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Steps to add a new feature are outlined below. Note that these steps only cover
|
||||
the process of getting a feature into the core Solana code.
|
||||
|
||||
- For features that are unambiguously good (ie bug fixes), these steps are sufficient.
|
||||
|
||||
- For features that should go up for community vote (ie fee structure changes), more
|
||||
information on the additional steps to follow can be found at:
|
||||
<https://spl.solana.com/feature-proposal#feature-proposal-life-cycle>
|
||||
|
||||
1. Generate a new keypair with `solana-keygen new --outfile feature.json --no-passphrase`
|
||||
- Keypairs should be held by core contributors only. If you're a non-core contirbutor going
|
||||
through these steps, the PR process will facilitate a keypair holder being picked. That
|
||||
person will generate the keypair, provide pubkey for PR, and ultimately enable the feature.
|
||||
|
||||
2. Add a public module for the feature, specifying keypair pubkey as the id with
|
||||
`solana_sdk::declare_id!()` within the module. Additionally, add an entry to `FEATURE_NAMES` map.
|
||||
|
||||
3. Add desired logic to check for and switch on feature availability.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
placeholder: Describe why the new feature gate is needed and any necessary conditions for its activation
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: id
|
||||
attributes:
|
||||
label: Feature ID
|
||||
description: The public key of the feature account
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: activation-method
|
||||
attributes:
|
||||
label: Activation Method
|
||||
options:
|
||||
- Single Core Contributor
|
||||
- Staked Validator Vote
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: testnet
|
||||
attributes:
|
||||
label: Testnet Activation Epoch
|
||||
placeholder: Edit this response when feature is activated on this cluster
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: devnet
|
||||
attributes:
|
||||
label: Devnet Activation Epoch
|
||||
placeholder: Edit this response when feature is activated on this cluster
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: mainnet-beta
|
||||
attributes:
|
||||
label: Mainnet-Beta Activation Epoch
|
||||
placeholder: Edit this response when feature is activated on this cluster
|
||||
validations:
|
||||
required: false
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,9 +0,0 @@
|
||||
#### Problem
|
||||
|
||||
|
||||
#### Summary of Changes
|
||||
|
||||
|
||||
Fixes #
|
||||
<!-- OPTIONAL: Feature Gate Issue: # -->
|
||||
<!-- Don't forget to add the "feature-gate" label -->
|
41
.github/dependabot.yml
vendored
41
.github/dependabot.yml
vendored
@ -1,41 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "01:00"
|
||||
timezone: America/Los_Angeles
|
||||
#labels:
|
||||
# - "automerge"
|
||||
open-pull-requests-limit: 3
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: "/web3.js"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "01:00"
|
||||
timezone: America/Los_Angeles
|
||||
labels:
|
||||
- "automerge"
|
||||
commit-message:
|
||||
prefix: "chore:"
|
||||
open-pull-requests-limit: 3
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: "/explorer"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "01:00"
|
||||
timezone: America/Los_Angeles
|
||||
labels:
|
||||
- "automerge"
|
||||
commit-message:
|
||||
prefix: "chore:"
|
||||
include: "scope"
|
||||
open-pull-requests-limit: 3
|
39
.github/stale.yml
vendored
39
.github/stale.yml
vendored
@ -1,39 +0,0 @@
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- security
|
||||
- blocked
|
||||
|
||||
# Label to use when marking a pull request as stale
|
||||
staleLabel: stale
|
||||
|
||||
pulls:
|
||||
# Number of days of inactivity before a pull request becomes stale
|
||||
daysUntilStale: 7
|
||||
|
||||
# Number of days of inactivity before a stale pull request is closed
|
||||
daysUntilClose: 7
|
||||
# Comment to post when marking a pull request as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This pull request has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
|
||||
# Comment to post when closing a stale pull request. Set to `false` to disable
|
||||
closeComment: >
|
||||
This stale pull request has been automatically closed.
|
||||
Thank you for your contributions.
|
||||
|
||||
issues:
|
||||
# Number of days of inactivity before a issue becomes stale
|
||||
daysUntilStale: 365
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Comment to post when marking a issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
This stale issue has been automatically closed.
|
||||
Thank you for your contributions.
|
37
.github/workflows/autolock_bot_PR.txt
vendored
37
.github/workflows/autolock_bot_PR.txt
vendored
@ -1,37 +0,0 @@
|
||||
name: 'Autolock RitBot for for PR'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
|
||||
github-token: ${{ github.token }}
|
||||
pr-inactive-days: '14'
|
||||
exclude-pr-created-before: ''
|
||||
exclude-pr-created-after: ''
|
||||
exclude-pr-created-between: ''
|
||||
exclude-pr-closed-before: ''
|
||||
exclude-pr-closed-after: ''
|
||||
exclude-pr-closed-between: ''
|
||||
include-any-pr-labels: 'automerge'
|
||||
include-all-pr-labels: ''
|
||||
exclude-any-pr-labels: ''
|
||||
add-pr-labels: 'locked PR'
|
||||
remove-pr-labels: ''
|
||||
pr-comment: 'This PR has been automatically locked since there has not been any activity in past 14 days after it was merged.'
|
||||
pr-lock-reason: 'resolved'
|
||||
log-output: true
|
38
.github/workflows/autolock_bot_closed_issue.txt
vendored
38
.github/workflows/autolock_bot_closed_issue.txt
vendored
@ -1,38 +0,0 @@
|
||||
name: 'Autolock NaviBot for closed issue'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '7'
|
||||
exclude-issue-created-before: ''
|
||||
exclude-issue-created-after: ''
|
||||
exclude-issue-created-between: ''
|
||||
exclude-issue-closed-before: ''
|
||||
exclude-issue-closed-after: ''
|
||||
exclude-issue-closed-between: ''
|
||||
include-any-issue-labels: ''
|
||||
include-all-issue-labels: ''
|
||||
exclude-any-issue-labels: ''
|
||||
add-issue-labels: 'locked issue'
|
||||
remove-issue-labels: ''
|
||||
issue-comment: 'This issue has been automatically locked since there has not been any activity in past 7 days after it was closed. Please open a new issue for related bugs.'
|
||||
issue-lock-reason: 'resolved'
|
||||
process-only: 'issues'
|
||||
log-output: true
|
66
.github/workflows/client-targets.yml
vendored
66
.github/workflows/client-targets.yml
vendored
@ -1,66 +0,0 @@
|
||||
name: client_targets
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "client/**"
|
||||
- "sdk/**"
|
||||
- ".github/workflows/client-targets.yml"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check_compilation:
|
||||
name: Client compilation
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
target: [aarch64-apple-ios, x86_64-apple-ios, aarch64-apple-darwin, x86_64-apple-darwin, aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, x86_64-linux-android]
|
||||
include:
|
||||
- target: aarch64-apple-ios
|
||||
platform: ios
|
||||
os: macos-latest
|
||||
- target: x86_64-apple-ios
|
||||
platform: ios
|
||||
os: macos-latest
|
||||
- target: aarch64-apple-darwin
|
||||
platform: ios
|
||||
os: macos-latest
|
||||
- target: x86_64-apple-darwin
|
||||
platform: ios
|
||||
os: macos-latest
|
||||
- target: aarch64-linux-android
|
||||
platform: android
|
||||
os: ubuntu-latest
|
||||
- target: armv7-linux-androideabi
|
||||
platform: android
|
||||
os: ubuntu-latest
|
||||
- target: i686-linux-android
|
||||
platform: android
|
||||
os: ubuntu-latest
|
||||
- target: x86_64-linux-android
|
||||
platform: android
|
||||
os: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.target }}
|
||||
- name: Install cargo-ndk
|
||||
if: ${{ matrix.platform == 'android' }}
|
||||
run: cargo install cargo-ndk
|
||||
- uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.platform == 'android' }}
|
||||
with:
|
||||
command: ndk
|
||||
args: --target ${{ matrix.target }} build -p solana-client
|
||||
- uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.platform == 'ios' }}
|
||||
with:
|
||||
command: build
|
||||
args: -p solana-client --target ${{ matrix.target }}
|
28
.github/workflows/explorer.yml
vendored
28
.github/workflows/explorer.yml
vendored
@ -1,28 +0,0 @@
|
||||
name: Explorer_build&test_on_PR
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'explorer/**'
|
||||
jobs:
|
||||
check-explorer:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: explorer
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: explorer/package-lock.json
|
||||
- run: npm i -g npm@7
|
||||
- run: npm ci
|
||||
- run: npm run format
|
||||
- run: npm run build
|
||||
- run: npm run test
|
58
.github/workflows/explorer_preview.yml
vendored
58
.github/workflows/explorer_preview.yml
vendored
@ -1,58 +0,0 @@
|
||||
name : explorer_preview
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Explorer_build&test_on_PR"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
explorer_preview:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: amondnet/vercel-action@v20
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }} #Optional
|
||||
vercel-org-id: ${{ secrets.ORG_ID}} #Required
|
||||
vercel-project-id: ${{ secrets.PROJECT_ID}} #Required
|
||||
scope: ${{ secrets.TEAM_ID }}
|
||||
|
||||
- name: vercel url
|
||||
run : |
|
||||
touch vercelfile.txt
|
||||
vercel --token ${{secrets.VERCEL_TOKEN}} ls explorer --scope team_8A2WD7p4uR7tmKX9M68loHXI > vercelfile.txt
|
||||
touch vercelfile1.txt
|
||||
head -n 2 vercelfile.txt > vercelfile1.txt
|
||||
touch vercelfile2.txt
|
||||
tail -n 1 vercelfile1.txt > vercelfile2.txt
|
||||
filtered_url7=$(cut -f7 -d" " vercelfile2.txt)
|
||||
echo "filtered_url7 is: $filtered_url7"
|
||||
touch .env.preview1
|
||||
echo "$filtered_url7" > .env.preview1
|
||||
#filtered_url=$(cat vercelfile2.txt )
|
||||
#echo "$filtered_url" >> .env.preview1
|
||||
|
||||
- name: Fetching Vercel Preview Deployment Link
|
||||
uses: mathiasvr/command-output@v1
|
||||
id: test1
|
||||
with:
|
||||
run: |
|
||||
echo "$(cat .env.preview1)"
|
||||
- name: Fetching PR commit URL
|
||||
uses: mathiasvr/command-output@v1
|
||||
id: test2
|
||||
with:
|
||||
run: |
|
||||
HEAD_SHA=$(curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/solana-labs/solana/pulls | jq .[0] | jq -r .head.sha)
|
||||
USER_NAME=$(curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/solana-labs/solana/pulls | jq .[0] | jq -r .head.user.login)
|
||||
echo "github.com/$USER_NAME/solana/commit/$HEAD_SHA"
|
||||
|
||||
- name: Slack Notification4
|
||||
uses: rtCamp/action-slack-notify@master
|
||||
env:
|
||||
SLACK_MESSAGE: ' Vercel Link: ${{ steps.test1.outputs.stdout }} PR Commit: ${{steps.test2.outputs.stdout}}'
|
||||
SLACK_TITLE: Vercel "Explorer" Preview Deployment Link , PR Commit
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
46
.github/workflows/explorer_production.yml
vendored
46
.github/workflows/explorer_production.yml
vendored
@ -1,46 +0,0 @@
|
||||
name: Explorer_production_build&test
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'explorer/**'
|
||||
jobs:
|
||||
Explorer_production_build_test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: explorer
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: explorer/package-lock.json
|
||||
- run: npm i -g npm@7
|
||||
- run: npm ci
|
||||
- run: npm run format
|
||||
- run: npm run build
|
||||
- run: npm run test
|
||||
|
||||
Explorer_production_deploy:
|
||||
needs: Explorer_production_build_test
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: explorer
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: amondnet/vercel-action@v20
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
|
||||
github-token: ${{ secrets.PAT }} #Optional
|
||||
vercel-args: '--prod' #for production
|
||||
vercel-org-id: ${{ secrets.ORG_ID}} #Required
|
||||
vercel-project-id: ${{ secrets.PROJECT_ID}} #Required
|
||||
scope: ${{ secrets.TEAM_ID }}
|
208
.github/workflows/solana-action.yml.txt
vendored
208
.github/workflows/solana-action.yml.txt
vendored
@ -1,208 +0,0 @@
|
||||
name : minimal
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
Export_Github_Repositories:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERCEL_TOKEN: ${{secrets.VERCEL_TOKEN}}
|
||||
GITHUB_TOKEN: ${{secrets.PAT_ANM}}
|
||||
COMMIT_RANGE: ${{ github.event.before}}...${{ github.event.after}}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- run: echo "COMMIT_DIFF_RANGE=$(echo $COMMIT_RANGE)" >> $GITHUB_ENV
|
||||
# - run: echo "$COMMIT_DIFF_RANGE"
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
GITHUB_TOKEN: ${{secrets.PAT_ANM}}
|
||||
if: ${{ github.event_name == 'push' && 'cron'&& github.ref == 'refs/heads/master'}}
|
||||
|
||||
- name: cmd
|
||||
run : |
|
||||
.travis/export-github-repo.sh web3.js/ solana-web3.js
|
||||
|
||||
macos-artifacts:
|
||||
needs: [Export_Github_Repositories]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: macos-latest
|
||||
if : ${{ github.event_name == 'api' && 'cron' || 'push' || startsWith(github.ref, 'refs/tags/v')}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup | Rust
|
||||
uses: ATiltedTree/setup-rust@v1
|
||||
with:
|
||||
rust-version: stable
|
||||
- name: release artifact
|
||||
run: |
|
||||
source ci/rust-version.sh
|
||||
brew install coreutils
|
||||
export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
|
||||
greadlink -f .
|
||||
source ci/env.sh
|
||||
rustup set profile default
|
||||
ci/publish-tarball.sh
|
||||
shell: bash
|
||||
|
||||
- name: Cache modules
|
||||
uses: actions/cache@master
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
|
||||
# - To stop from uploading on the production
|
||||
# - uses: ochanje210/simple-s3-upload-action@master
|
||||
# with:
|
||||
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
# AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
# SOURCE_DIR: 'travis-s3-upload1'
|
||||
# DEST_DIR: 'giitsol'
|
||||
|
||||
# - uses: ochanje210/simple-s3-upload-action@master
|
||||
# with:
|
||||
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
# AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
# SOURCE_DIR: './docs/'
|
||||
# DEST_DIR: 'giitsol'
|
||||
|
||||
|
||||
windows-artifact:
|
||||
needs: [Export_Github_Repositories]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: windows-latest
|
||||
if : ${{ github.event_name == 'api' && 'cron' || 'push' || startsWith(github.ref, 'refs/tags/v')}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup | Rust
|
||||
uses: ATiltedTree/setup-rust@v1
|
||||
with:
|
||||
rust-version: stable
|
||||
release-artifact:
|
||||
needs: windows-artifact
|
||||
runs-on: windows-latest
|
||||
if : ${{ github.event_name == 'api' && 'cron' || github.ref == 'refs/heads/master'}}
|
||||
steps:
|
||||
- name: release artifact
|
||||
run: |
|
||||
git clone git://git.openssl.org/openssl.git
|
||||
cd openssl
|
||||
make
|
||||
make test
|
||||
make install
|
||||
openssl version
|
||||
# choco install openssl
|
||||
# vcpkg integrate install
|
||||
# refreshenv
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
- run: choco install msys2
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
openssl version
|
||||
bash ci/rust-version.sh
|
||||
readlink -f .
|
||||
bash ci/env.sh
|
||||
rustup set profile default
|
||||
bash ci/publish-tarball.sh
|
||||
shell: bash
|
||||
|
||||
- name: Cache modules
|
||||
uses: actions/cache@v1
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
# - To stop from uploading on the production
|
||||
# - name: Config. aws cred
|
||||
# uses: aws-actions/configure-aws-credentials@v1
|
||||
# with:
|
||||
# aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# aws-region: us-east-2
|
||||
# - name: Deploy
|
||||
# uses: shallwefootball/s3-upload-action@master
|
||||
# with:
|
||||
# folder: build
|
||||
# aws_bucket: ${{ secrets.AWS_S3_BUCKET }}
|
||||
# aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# destination_dir: /
|
||||
# bucket-region: us-east-2
|
||||
# delete-removed: true
|
||||
# no-cache: true
|
||||
# private: true
|
||||
|
||||
# Docs:
|
||||
# needs: [windows-artifact,release-artifact]
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{secrets.PAT_NEW}}
|
||||
# GITHUB_EVENT_BEFORE: ${{ github.event.before }}
|
||||
# GITHUB_EVENT_AFTER: ${{ github.event.after }}
|
||||
# COMMIT_RANGE: ${{ github.event.before}}...${{ github.event.after}}
|
||||
# steps:
|
||||
# - name: Checkout repo
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 2
|
||||
# - name: docs
|
||||
# if: ${{github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/v')}}
|
||||
# run: |
|
||||
# touch .env
|
||||
# echo "COMMIT_RANGE=($COMMIT_RANGE)" > .env
|
||||
# source ci/env.sh
|
||||
# .travis/channel_restriction.sh edge beta || exit 0
|
||||
# .travis/affects.sh docs/ .travis || exit 0
|
||||
# cd docs/
|
||||
# source .travis/before_install.sh
|
||||
# source .travis/script.sh
|
||||
# - name: setup-node
|
||||
# uses: actions/checkout@v2
|
||||
# - name: setup-node
|
||||
# uses: actions/setup-node@v2
|
||||
# with:
|
||||
# node-version: 'lts/*'
|
||||
# - name: Cache
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.OS }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.OS }}-npm-cache-2
|
||||
|
||||
# auto_bump:
|
||||
# needs: [windows-artifact,release-artifact,Docs]
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name : checkout repo
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: '0'
|
||||
# - name: Bump version and push tag
|
||||
# uses: anothrNick/github-tag-action@1.26.0
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# WITH_V: true
|
||||
# DEFAULT_BUMP: patch
|
71
.github/workflows/web3.yml
vendored
71
.github/workflows/web3.yml
vendored
@ -1,71 +0,0 @@
|
||||
name: Web3
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- "web3.js/**"
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- "web3.js/**"
|
||||
|
||||
jobs:
|
||||
# needed for grouping check-web3 strategies into one check for mergify
|
||||
all-web3-checks:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-web3
|
||||
steps:
|
||||
- run: echo "Done"
|
||||
|
||||
web3-commit-lint:
|
||||
runs-on: ubuntu-latest
|
||||
# Set to true in order to avoid cancelling other workflow jobs.
|
||||
# Mergify will still require web3-commit-lint for automerge
|
||||
continue-on-error: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web3.js
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
# maybe needed for base sha below
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web3.js/package-lock.json
|
||||
- run: npm ci
|
||||
- name: commit-lint
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: bash commitlint.sh
|
||||
env:
|
||||
COMMIT_RANGE: ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}
|
||||
|
||||
check-web3:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web3.js
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node: [ '12', '14', '16' ]
|
||||
|
||||
name: Node ${{ matrix.node }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web3.js/package-lock.json
|
||||
- run: |
|
||||
source .travis/before_install.sh
|
||||
npm install
|
||||
source .travis/script.sh
|
34
.gitignore
vendored
34
.gitignore
vendored
@ -1,32 +1,16 @@
|
||||
/farf/
|
||||
/solana-release/
|
||||
/solana-release.tar.bz2
|
||||
/solana-metrics/
|
||||
/solana-metrics.tar.bz2
|
||||
Cargo.lock
|
||||
/target/
|
||||
/test-ledger/
|
||||
|
||||
**/*.rs.bk
|
||||
.cargo
|
||||
|
||||
# node configuration files
|
||||
/config/
|
||||
/config-private/
|
||||
/config-drone/
|
||||
/config-validator/
|
||||
/config-client/
|
||||
/multinode-demo/test/config-client/
|
||||
|
||||
.cache
|
||||
|
||||
# log files
|
||||
*.log
|
||||
log-*.txt
|
||||
log-*/
|
||||
|
||||
# intellij files
|
||||
/.idea/
|
||||
/solana.iml
|
||||
/.vscode/
|
||||
|
||||
# fetch-spl.sh artifacts
|
||||
/spl-genesis-args.sh
|
||||
/spl_*.so
|
||||
|
||||
.DS_Store
|
||||
# scripts that may be generated by cargo *-bpf commands
|
||||
**/cargo-*-bpf-child-script-*.sh
|
||||
# test temp files, ledgers, etc.
|
||||
/farf/
|
||||
|
151
.mergify.yml
151
.mergify.yml
@ -1,151 +0,0 @@
|
||||
# Validate your changes with:
|
||||
#
|
||||
# $ curl -F 'data=@.mergify.yml' https://gh.mergify.io/validate/
|
||||
#
|
||||
# https://doc.mergify.io/
|
||||
pull_request_rules:
|
||||
- name: label changes from community
|
||||
conditions:
|
||||
- author≠@core-contributors
|
||||
- author≠mergify[bot]
|
||||
- author≠dependabot[bot]
|
||||
actions:
|
||||
label:
|
||||
add:
|
||||
- community
|
||||
- name: request review for community changes
|
||||
conditions:
|
||||
- author≠@core-contributors
|
||||
- author≠mergify[bot]
|
||||
- author≠dependabot[bot]
|
||||
# Only request reviews from the pr subscribers group if no one
|
||||
# has reviewed the community PR yet. These checks only match
|
||||
# reviewers with admin, write or maintain permission on the repository.
|
||||
- "#approved-reviews-by=0"
|
||||
- "#commented-reviews-by=0"
|
||||
- "#changes-requested-reviews-by=0"
|
||||
- "#review-requested=0"
|
||||
actions:
|
||||
request_reviews:
|
||||
teams:
|
||||
- "@solana-labs/community-pr-subscribers"
|
||||
- name: automatic merge (squash) on CI success
|
||||
conditions:
|
||||
- and:
|
||||
- status-success=buildkite/solana
|
||||
- status-success=ci-gate
|
||||
- label=automerge
|
||||
- label!=no-automerge
|
||||
- author≠@dont-squash-my-commits
|
||||
- or:
|
||||
# only require travis success if docs files changed
|
||||
- status-success=Travis CI - Pull Request
|
||||
- -files~=^docs/
|
||||
- or:
|
||||
# only require explorer checks if explorer files changed
|
||||
- status-success=check-explorer
|
||||
- -files~=^explorer/
|
||||
- or:
|
||||
- and:
|
||||
- status-success=all-web3-checks
|
||||
- status-success=web3-commit-lint
|
||||
# only require web3 checks if web3.js files changed
|
||||
- -files~=^web3.js/
|
||||
actions:
|
||||
merge:
|
||||
method: squash
|
||||
# Join the dont-squash-my-commits group if you won't like your commits squashed
|
||||
- name: automatic merge (rebase) on CI success
|
||||
conditions:
|
||||
- and:
|
||||
- status-success=buildkite/solana
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=ci-gate
|
||||
- label=automerge
|
||||
- label!=no-automerge
|
||||
- author=@dont-squash-my-commits
|
||||
- or:
|
||||
# only require explorer checks if explorer files changed
|
||||
- status-success=check-explorer
|
||||
- -files~=^explorer/
|
||||
- or:
|
||||
# only require web3 checks if web3.js files changed
|
||||
- status-success=all-web3-checks
|
||||
- -files~=^web3.js/
|
||||
actions:
|
||||
merge:
|
||||
method: rebase
|
||||
- name: remove automerge label on CI failure
|
||||
conditions:
|
||||
- label=automerge
|
||||
- "#status-failure!=0"
|
||||
actions:
|
||||
label:
|
||||
remove:
|
||||
- automerge
|
||||
comment:
|
||||
message: automerge label removed due to a CI failure
|
||||
- name: remove outdated reviews
|
||||
conditions:
|
||||
- base=master
|
||||
actions:
|
||||
dismiss_reviews:
|
||||
changes_requested: true
|
||||
- name: set automerge label on mergify backport PRs
|
||||
conditions:
|
||||
- author=mergify[bot]
|
||||
- head~=^mergify/bp/
|
||||
- "#status-failure=0"
|
||||
- "-merged"
|
||||
- label!=no-automerge
|
||||
actions:
|
||||
label:
|
||||
add:
|
||||
- automerge
|
||||
- name: v1.9 feature-gate backport
|
||||
conditions:
|
||||
- label=v1.9
|
||||
- label=feature-gate
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
labels:
|
||||
- feature-gate
|
||||
branches:
|
||||
- v1.9
|
||||
- name: v1.9 non-feature-gate backport
|
||||
conditions:
|
||||
- label=v1.9
|
||||
- label!=feature-gate
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
branches:
|
||||
- v1.9
|
||||
- name: v1.10 feature-gate backport
|
||||
conditions:
|
||||
- label=v1.10
|
||||
- label=feature-gate
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
labels:
|
||||
- feature-gate
|
||||
branches:
|
||||
- v1.10
|
||||
- name: v1.10 non-feature-gate backport
|
||||
conditions:
|
||||
- label=v1.10
|
||||
- label!=feature-gate
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
branches:
|
||||
- v1.10
|
||||
|
||||
commands_restrictions:
|
||||
# The author of copied PRs is the Mergify user.
|
||||
# Restrict `copy` access to Core Contributors
|
||||
copy:
|
||||
conditions:
|
||||
- author=@core-contributors
|
120
.travis.yml
120
.travis.yml
@ -1,120 +0,0 @@
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+/
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
slack:
|
||||
on_success: change
|
||||
if: NOT type = pull_request
|
||||
secure: F4IjOE05MyaMOdPRL+r8qhs7jBvv4yDM3RmFKE1zNXnfUOqV4X38oQM1EI+YVsgpMQLj/pxnEB7wcTE4Bf86N6moLssEULCpvAuMVoXj4QbWdomLX+01WbFa6fLVeNQIg45NHrz2XzVBhoKOrMNnl+QI5mbR2AlS5oqsudHsXDnyLzZtd4Y5SDMdYG1zVWM01+oNNjgNfjcCGmOE/K0CnOMl6GPi3X9C34tJ19P2XT7MTDsz1/IfEF7fro2Q8DHEYL9dchJMoisXSkem5z7IDQkGzXsWdWT4NnndUvmd1MlTCE9qgoXDqRf95Qh8sB1Dz08HtvgfaosP2XjtNTfDI9BBYS15Ibw9y7PchAJE1luteNjF35EOy6OgmCLw/YpnweqfuNViBZz+yOPWXVC0kxnPIXKZ1wyH9ibeH6E4hr7a8o9SV/6SiWIlbYF+IR9jPXyTCLP/cc3sYljPWxDnhWFwFdRVIi3PbVAhVu7uWtVUO17Oc9gtGPgs/GrhOMkJfwQPXaudRJDpVZowxTX4x9kefNotlMAMRgq+Drbmgt4eEBiCNp0ITWgh17BiE1U09WS3myuduhoct85+FoVeaUkp1sxzHVtGsNQH0hcz7WcpZyOM+AwistJA/qzeEDQao5zi1eKWPbO2xAhi2rV1bDH6bPf/4lDBwLRqSiwvlWU=
|
||||
|
||||
os: linux
|
||||
dist: bionic
|
||||
language: minimal
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- name: "Export Github Repositories"
|
||||
if: type IN (push, cron) AND branch = master
|
||||
language: python
|
||||
git:
|
||||
depth: false
|
||||
script:
|
||||
- .travis/export-github-repo.sh web3.js/ solana-web3.js
|
||||
|
||||
- &release-artifacts
|
||||
if: type IN (api, cron) OR tag IS present
|
||||
name: "macOS release artifacts"
|
||||
os: osx
|
||||
osx_image: xcode12
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
install:
|
||||
- source ci/rust-version.sh
|
||||
- PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
|
||||
- readlink -f .
|
||||
- brew install gnu-tar
|
||||
- PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH"
|
||||
- tar --version
|
||||
script:
|
||||
- source ci/env.sh
|
||||
- rustup set profile default
|
||||
- ci/publish-tarball.sh
|
||||
deploy:
|
||||
- provider: s3
|
||||
access_key_id: $AWS_ACCESS_KEY_ID
|
||||
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
||||
bucket: release.solana.com
|
||||
region: us-west-1
|
||||
skip_cleanup: true
|
||||
acl: public_read
|
||||
local_dir: travis-s3-upload
|
||||
on:
|
||||
all_branches: true
|
||||
- provider: releases
|
||||
token: $GITHUB_TOKEN
|
||||
skip_cleanup: true
|
||||
file_glob: true
|
||||
file: travis-release-upload/*
|
||||
on:
|
||||
tags: true
|
||||
- <<: *release-artifacts
|
||||
name: "Windows release artifacts"
|
||||
os: windows
|
||||
install:
|
||||
- choco install openssl
|
||||
- export OPENSSL_DIR="C:\Program Files\OpenSSL-Win64"
|
||||
- source ci/rust-version.sh
|
||||
- PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
|
||||
- readlink -f .
|
||||
# Linux release artifacts are still built by ci/buildkite-secondary.yml
|
||||
#- <<: *release-artifacts
|
||||
# name: "Linux release artifacts"
|
||||
# os: linux
|
||||
# before_install:
|
||||
# - sudo apt-get install libssl-dev libudev-dev
|
||||
|
||||
# docs pull request
|
||||
# - name: "explorer"
|
||||
# if: type = pull_request AND branch = master
|
||||
|
||||
# language: node_js
|
||||
# node_js:
|
||||
# - "lts/*"
|
||||
|
||||
# cache:
|
||||
# directories:
|
||||
# - ~/.npm
|
||||
|
||||
# before_install:
|
||||
# - .travis/affects.sh explorer/ .travis || travis_terminate 0
|
||||
# - cd explorer
|
||||
|
||||
# script:
|
||||
# - npm run build
|
||||
# - npm run format
|
||||
- name: "docs"
|
||||
if: type IN (push, pull_request) OR tag IS present
|
||||
language: node_js
|
||||
node_js:
|
||||
- "lts/*"
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- ~/.npm
|
||||
|
||||
before_install:
|
||||
- source ci/env.sh
|
||||
- .travis/channel_restriction.sh edge beta || travis_terminate 0
|
||||
- .travis/affects.sh docs/ .travis || travis_terminate 0
|
||||
- cd docs/
|
||||
- source .travis/before_install.sh
|
||||
|
||||
script:
|
||||
- source .travis/script.sh
|
@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Check if files in the commit range match one or more prefixes
|
||||
#
|
||||
|
||||
# Always run the job if we are on a tagged release
|
||||
if [[ -n "$TRAVIS_TAG" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
(
|
||||
set -x
|
||||
git diff --name-only "$TRAVIS_COMMIT_RANGE"
|
||||
)
|
||||
|
||||
for file in $(git diff --name-only "$TRAVIS_COMMIT_RANGE"); do
|
||||
for prefix in "$@"; do
|
||||
if [[ $file =~ ^"$prefix" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "No modifications to $*"
|
||||
exit 1
|
@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Only proceed if we are on one of the channels passed in, or a tag build
|
||||
#
|
||||
|
||||
set -ex
|
||||
|
||||
[[ -n $CI_TAG ]] && exit 0
|
||||
|
||||
eval "$(ci/channel-info.sh)"
|
||||
|
||||
for acceptable_channel in "$@"; do
|
||||
if [[ "$CHANNEL" == "$acceptable_channel" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Not running from one of the following channels: $*"
|
||||
exit 1
|
@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Exports a subdirectory into another github repository
|
||||
#
|
||||
|
||||
set -e
|
||||
if [[ -z $GITHUB_TOKEN ]]; then
|
||||
echo GITHUB_TOKEN not defined
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
pip3 install git-filter-repo
|
||||
|
||||
declare subdir=$1
|
||||
declare repo_name=$2
|
||||
|
||||
[[ -n "$subdir" ]] || {
|
||||
echo "Error: subdir not specified"
|
||||
exit 1
|
||||
}
|
||||
[[ -n "$repo_name" ]] || {
|
||||
echo "Error: repo_name not specified"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "Exporting $subdir"
|
||||
|
||||
set -x
|
||||
rm -rf .github_export/"$repo_name"
|
||||
git clone https://"$GITHUB_TOKEN"@github.com/solana-labs/"$repo_name" .github_export/"$repo_name"
|
||||
git filter-repo --subdirectory-filter "$subdir" --target .github_export/"$repo_name"
|
||||
git -C .github_export/"$repo_name" push https://"$GITHUB_TOKEN"@github.com/solana-labs/"$repo_name"
|
292
CONTRIBUTING.md
292
CONTRIBUTING.md
@ -1,273 +1,53 @@
|
||||
# Solana Coding Guidelines
|
||||
Solana Coding Guidelines
|
||||
===
|
||||
|
||||
The goal of these guidelines is to improve developer productivity by allowing
|
||||
developers to jump into any file in the codebase and not need to adapt to
|
||||
inconsistencies in how the code is written. The codebase should appear as if it
|
||||
had been authored by a single developer. If you don't agree with a convention,
|
||||
submit a PR patching this document and let's discuss! Once the PR is accepted,
|
||||
*all* code should be updated as soon as possible to reflect the new
|
||||
The goal of these guidelines is to improve developer productivity by allowing developers to
|
||||
jump any file in the codebase and not need to adapt to inconsistencies in how the code is
|
||||
written. The codebase should appear as if it had been authored by a single developer. If you
|
||||
don't agree with a convention, submit a PR patching this document and let's discuss! Once
|
||||
the PR is accepted, *all* code should be updated as soon as possible to reflect the new
|
||||
conventions.
|
||||
|
||||
## Pull Requests
|
||||
Rust coding conventions
|
||||
---
|
||||
|
||||
Small, frequent PRs are much preferred to large, infrequent ones. A large PR is
|
||||
difficult to review, can block others from making progress, and can quickly get
|
||||
its author into "rebase hell". A large PR oftentimes arises when one change
|
||||
requires another, which requires another, and then another. When you notice
|
||||
those dependencies, put the fix into a commit of its own, then checkout a new
|
||||
branch, and cherry-pick it.
|
||||
* All Rust code is formatted using the latest version of `rustfmt`. Once installed, it will be
|
||||
updated automatically when you update the compiler with `rustup`.
|
||||
|
||||
```bash
|
||||
$ git commit -am "Fix foo, needed by bar"
|
||||
$ git checkout master
|
||||
$ git checkout -b fix-foo
|
||||
$ git cherry-pick fix-bar
|
||||
$ git push --set-upstream origin fix-foo
|
||||
```
|
||||
* All Rust code is linted with Clippy. If you'd prefer to ignore its advice, do so explicitly:
|
||||
|
||||
Open a PR to start the review process and then jump back to your original
|
||||
branch to keep making progress. Consider rebasing to make your fix the first
|
||||
commit:
|
||||
|
||||
```bash
|
||||
$ git checkout fix-bar
|
||||
$ git rebase -i master <Move fix-foo to top>
|
||||
```
|
||||
|
||||
Once the commit is merged, rebase the original branch to purge the
|
||||
cherry-picked commit:
|
||||
|
||||
```bash
|
||||
$ git pull --rebase upstream master
|
||||
```
|
||||
|
||||
### How big is too big?
|
||||
|
||||
If there are no functional changes, PRs can be very large and that's no
|
||||
problem. If, however, your changes are making meaningful changes or additions,
|
||||
then about 1,000 lines of changes is about the most you should ask a Solana
|
||||
maintainer to review.
|
||||
|
||||
### Should I send small PRs as I develop large, new components?
|
||||
|
||||
Add only code to the codebase that is ready to be deployed. If you are building
|
||||
a large library, consider developing it in a separate git repository. When it
|
||||
is ready to be integrated, the Solana maintainers will work with you to decide
|
||||
on a path forward. Smaller libraries may be copied in whereas very large ones
|
||||
may be pulled in with a package manager.
|
||||
|
||||
## Getting Pull Requests Merged
|
||||
|
||||
There is no single person assigned to watching GitHub PR queue and ushering you
|
||||
through the process. Typically, you will ask the person that wrote a component
|
||||
to review changes to it. You can find the author using `git blame` or asking on
|
||||
Discord. When working to get your PR merged, it's most important to understand
|
||||
that changing the code is your priority and not necessarily a priority of the
|
||||
person you need an approval from. Also, while you may interact the most with
|
||||
the component author, you should aim to be inclusive of others. Providing a
|
||||
detailed problem description is the most effective means of engaging both the
|
||||
component author and other potentially interested parties.
|
||||
|
||||
Consider opening all PRs as Draft Pull Requests first. Using a draft PR allows
|
||||
you to kickstart the CI automation, which typically takes between 10 and 30
|
||||
minutes to execute. Use that time to write a detailed problem description. Once
|
||||
the description is written and CI succeeds, click the "Ready to Review" button
|
||||
and add reviewers. Adding reviewers before CI succeeds is a fast path to losing
|
||||
reviewer engagement. Not only will they be notified and see the PR is not yet
|
||||
ready for them, they will also be bombarded with additional notifications
|
||||
each time you push a commit to get past CI or until they "mute" the PR. Once
|
||||
muted, you'll need to reach out over some other medium, such as Discord, to
|
||||
request they have another look. When you use draft PRs, no notifications are
|
||||
sent when you push commits and edit the PR description. Use draft PRs
|
||||
liberally. Don't bug the humans until you have gotten past the bots.
|
||||
|
||||
### What should be in my PR description?
|
||||
|
||||
Reviewing code is hard work and generally involves an attempt to guess the
|
||||
author's intent at various levels. Please assume reviewer time is scarce and do
|
||||
what you can to make your PR as consumable as possible. Inspired by techniques
|
||||
for writing good whitepapers, the guidance here aims to maximize reviewer
|
||||
engagement.
|
||||
|
||||
Assume the reviewer will spend no more than a few seconds reading the PR title.
|
||||
If it doesn't describe a noteworthy change, don't expect the reviewer to click
|
||||
to see more.
|
||||
|
||||
Next, like the abstract of a whitepaper, the reviewer will spend ~30 seconds
|
||||
reading the PR problem description. If what is described there doesn't look
|
||||
more important than competing issues, don't expect the reviewer to read on.
|
||||
|
||||
Next, the reviewer will read the proposed changes. At this point, the reviewer
|
||||
needs to be convinced the proposed changes are a *good* solution to the problem
|
||||
described above. If the proposed changes, not the code changes, generates
|
||||
discussion, consider closing the PR and returning with a design proposal
|
||||
instead.
|
||||
|
||||
Finally, once the reviewer understands the problem and agrees with the approach
|
||||
to solving it, the reviewer will view the code changes. At this point, the
|
||||
reviewer is simply looking to see if the implementation actually implements
|
||||
what was proposed and if that implementation is maintainable. When a concise,
|
||||
readable test for each new code path is present, the reviewer can safely ignore
|
||||
the details of its implementation. When those tests are missing, expect to
|
||||
either lose engagement or get a pile of review comments as the reviewer
|
||||
attempts to consider every ambiguity in your implementation.
|
||||
|
||||
### The PR Title
|
||||
|
||||
The PR title should contain a brief summary of the change, from the perspective
|
||||
of the user. Examples of good titles:
|
||||
|
||||
* Add rent to accounts
|
||||
* Fix out-of-memory error in validator
|
||||
* Clean up `process_message()` in runtime
|
||||
|
||||
The conventions here are all the same as a good git commit title:
|
||||
|
||||
* First word capitalized and in the imperative mood, not past tense ("add", not
|
||||
"added")
|
||||
* No trailing period
|
||||
* What was done, whom it was done to, and in what context
|
||||
|
||||
### The PR Problem Statement
|
||||
|
||||
The git repo implements a product with various features. The problem statement
|
||||
should describe how the product is missing a feature, how a feature is
|
||||
incomplete, or how the implementation of a feature is somehow undesirable. If
|
||||
an issue being fixed already describes the problem, go ahead and copy-paste it.
|
||||
As mentioned above, reviewer time is scarce. Given a queue of PRs to review,
|
||||
the reviewer may ignore PRs that expect them to click through links to see if
|
||||
the PR warrants attention.
|
||||
|
||||
### The Proposed Changes
|
||||
|
||||
Typically the content under the "Proposed changes" section will be a bulleted
|
||||
list of steps taken to solve the problem. Oftentimes, the list is identical to
|
||||
the subject lines of the git commits contained in the PR. It's especially
|
||||
generous (and not expected) to rebase or reword commits such that each change
|
||||
matches the logical flow in your PR description.
|
||||
|
||||
### The PR / Issue Labels
|
||||
Labels make it easier to manage and track PRs / issues. Below some common labels
|
||||
that we use in Solana. For the complete list of labels, please refer to the
|
||||
[label page](https://github.com/solana-labs/solana/issues/labels):
|
||||
|
||||
* "feature-gate": when you add a new feature gate or modify the behavior of
|
||||
an existing feature gate, please add the "feature-gate" label to your PR.
|
||||
New feature gates should also always have a corresponding tracking issue
|
||||
(go to "New Issue" -> "Feature Gate Tracker [Get Started](https://github.com/solana-labs/solana/issues/new?assignees=&labels=feature-gate&template=1-feature-gate.yml&title=Feature+Gate%3A+)")
|
||||
and should be updated each time the feature is activated on a cluster.
|
||||
|
||||
* "automerge": When a PR is labelled with "automerge", the PR will be
|
||||
automically merged once CI passes. In general, this label should only
|
||||
be used for small hot-fix (fewer than 100 lines) or automatic generated
|
||||
PRs. If you're uncertain, it's usually the case that the PR is not
|
||||
qualified as "automerge".
|
||||
|
||||
* "good first issue": If you happen to find an issue that is non-urgent and
|
||||
self-contained with moderate scope, you might want to consider attaching
|
||||
"good first issue" to it as it might be a good practice for newcomers.
|
||||
|
||||
* "rust": this pull request updates Rust code.
|
||||
|
||||
* "javascript": this pull request updates Javascript code.
|
||||
|
||||
### When will my PR be reviewed?
|
||||
|
||||
PRs are typically reviewed and merged in under 7 days. If your PR has been open
|
||||
for longer, it's a strong indicator that the reviewers aren't confident the
|
||||
change meets the quality standards of the codebase. You might consider closing
|
||||
it and coming back with smaller PRs and longer descriptions detailing what
|
||||
problem it solves and how it solves it. Old PRs will be marked stale and then
|
||||
closed automatically 7 days later.
|
||||
|
||||
### How to manage review feedback?
|
||||
|
||||
After a reviewer provides feedback, you can quickly say "acknowledged, will
|
||||
fix" using a thumb's up emoji. If you're confident your fix is exactly as
|
||||
prescribed, add a reply "Fixed in COMMIT\_HASH" and mark the comment as
|
||||
resolved. If you're not sure, reply "Is this what you had in mind?
|
||||
COMMIT\_HASH" and if so, the reviewer will reply and mark the conversation as
|
||||
resolved. Marking conversations as resolved is an excellent way to engage more
|
||||
reviewers. Leaving conversations open may imply the PR is not yet ready for
|
||||
additional review.
|
||||
|
||||
### When will my PR be re-reviewed?
|
||||
|
||||
Recall that once your PR is opened, a notification is sent every time you push
|
||||
a commit. After a reviewer adds feedback, they won't be checking on the status
|
||||
of that feedback after every new commit. Instead, directly mention the reviewer
|
||||
when you feel your PR is ready for another pass.
|
||||
|
||||
## Draft Pull Requests
|
||||
|
||||
If you want early feedback on your PR, use GitHub's "Draft Pull Request"
|
||||
mechanism. Draft PRs are a convenient way to collaborate with the Solana
|
||||
maintainers without triggering notifications as you make changes. When you feel
|
||||
your PR is ready for a broader audience, you can transition your draft PR to a
|
||||
standard PR with the click of a button.
|
||||
|
||||
Do not add reviewers to draft PRs. GitHub doesn't automatically clear
|
||||
approvals when you click "Ready for Review", so a review that meant "I approve
|
||||
of the direction" suddenly has the appearance of "I approve of these changes."
|
||||
Instead, add a comment that mentions the usernames that you would like a review
|
||||
from. Ask explicitly what you would like feedback on.
|
||||
|
||||
## Rust coding conventions
|
||||
|
||||
* All Rust code is formatted using the latest version of `rustfmt`. Once
|
||||
installed, it will be updated automatically when you update the compiler with
|
||||
`rustup`.
|
||||
|
||||
* All Rust code is linted with Clippy. If you'd prefer to ignore its advice, do
|
||||
so explicitly:
|
||||
|
||||
```rust #[allow(clippy::too_many_arguments)] ```
|
||||
```rust
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))]
|
||||
```
|
||||
|
||||
Note: Clippy defaults can be overridden in the top-level file `.clippy.toml`.
|
||||
|
||||
* For variable names, when in doubt, spell it out. The mapping from type names
|
||||
to variable names is to lowercase the type name, putting an underscore before
|
||||
each capital letter. Variable names should *not* be abbreviated unless being
|
||||
used as closure arguments and the brevity improves readability. When a function
|
||||
has multiple instances of the same type, qualify each with a prefix and
|
||||
underscore (i.e. alice\_keypair) or a numeric suffix (i.e. tx0).
|
||||
* For variable names, when in doubt, spell it out. The mapping from type names to variable names
|
||||
is to lowercase the type name, putting an underscore before each capital letter. Variable names
|
||||
should *not* be abbreviated unless being used as closure arguments and the brevity improves
|
||||
readability. When a function has multiple instances of the same type, qualify each with a
|
||||
prefix and underscore (i.e. alice_keypair) or a numeric suffix (i.e. tx0).
|
||||
|
||||
* For function and method names, use `<verb>_<subject>`. For unit tests, that
|
||||
verb should always be `test` and for benchmarks the verb should always be
|
||||
`bench`. Avoid namespacing function names with some arbitrary word. Avoid
|
||||
abbreviating words in function names.
|
||||
* For function and method names, use `<verb>_<subject>`. For unit tests, that verb should
|
||||
always be `test` and for benchmarks the verb should always be `bench`. Avoid namespacing
|
||||
function names with some arbitrary word. Avoid abreviating words in function names.
|
||||
|
||||
* As they say, "When in Rome, do as the Romans do." A good patch should
|
||||
acknowledge the coding conventions of the code that surrounds it, even in the
|
||||
case where that code has not yet been updated to meet the conventions described
|
||||
here.
|
||||
* As they say, "When in Rome, do as the Romans do." A good patch should acknowledge the coding
|
||||
conventions of the code that surrounds it, even in the case where that code has not yet been
|
||||
updated to meet the conventions described here.
|
||||
|
||||
|
||||
## Terminology
|
||||
Terminology
|
||||
---
|
||||
|
||||
Inventing new terms is allowed, but should only be done when the term is widely
|
||||
used and understood. Avoid introducing new 3-letter terms, which can be
|
||||
confused with 3-letter acronyms.
|
||||
Inventing new terms is allowed, but should only be done when the term is widely used and
|
||||
understood. Avoid introducing new 3-letter terms, which can be confused with 3-letter acronyms.
|
||||
|
||||
[Terms currently in use](docs/src/terminology.md)
|
||||
Some terms we currently use regularly in the codebase:
|
||||
|
||||
* fullnode: n. A fully participating network node.
|
||||
* hash: n. A SHA-256 Hash.
|
||||
* keypair: n. A Ed25519 key-pair, containing a public and private key.
|
||||
* pubkey: n. The public key of a Ed25519 key-pair.
|
||||
* sigverify: v. To verify a Ed25519 digital signature.
|
||||
|
||||
## Design Proposals
|
||||
|
||||
Solana's architecture is described by docs generated from markdown files in
|
||||
the `docs/src/` directory, maintained by an *editor* (currently @garious). To
|
||||
add a design proposal, you'll need to include it in the
|
||||
[Accepted Design Proposals](https://docs.solana.com/proposals/accepted-design-proposals)
|
||||
section of the Solana docs. Here's the full process:
|
||||
|
||||
1. Propose a design by creating a PR that adds a markdown document to the
|
||||
`docs/src/proposals` directory and references it from the [table of
|
||||
contents](docs/src/SUMMARY.md). Add any relevant *maintainers* to the PR
|
||||
review.
|
||||
2. The PR being merged indicates your proposed change was accepted and that the
|
||||
maintainers support your plan of attack.
|
||||
3. Submit PRs that implement the proposal. When the implementation reveals the
|
||||
need for tweaks to the proposal, be sure to update the proposal and have that
|
||||
change reviewed by the same people as in step 1.
|
||||
4. Once the implementation is complete, submit a PR that moves the link from
|
||||
the Accepted Proposals to the Implemented Proposals section.
|
||||
|
7565
Cargo.lock
generated
7565
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
231
Cargo.toml
231
Cargo.toml
@ -1,91 +1,148 @@
|
||||
[package]
|
||||
name = "solana"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "0.9.0"
|
||||
documentation = "https://docs.rs/solana"
|
||||
homepage = "http://solana.com/"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
authors = [
|
||||
"Anatoly Yakovenko <anatoly@solana.com>",
|
||||
"Greg Fitzgerald <greg@solana.com>",
|
||||
"Stephen Akridge <stephen@solana.com>",
|
||||
"Michael Vines <mvines@solana.com>",
|
||||
"Rob Walker <rob@solana.com>",
|
||||
"Pankaj Garg <pankaj@solana.com>",
|
||||
"Tyera Eulberg <tyera@solana.com>",
|
||||
]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-upload-perf"
|
||||
path = "src/bin/upload-perf.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-bench-streamer"
|
||||
path = "src/bin/bench-streamer.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-bench-tps"
|
||||
path = "src/bin/bench-tps.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-drone"
|
||||
path = "src/bin/drone.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-replicator"
|
||||
path = "src/bin/replicator.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-fullnode"
|
||||
path = "src/bin/fullnode.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-fullnode-config"
|
||||
path = "src/bin/fullnode-config.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-genesis"
|
||||
path = "src/bin/genesis.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-ledger-tool"
|
||||
path = "src/bin/ledger-tool.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-keygen"
|
||||
path = "src/bin/keygen.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solana-wallet"
|
||||
path = "src/bin/wallet.rs"
|
||||
|
||||
[badges]
|
||||
codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
|
||||
|
||||
[features]
|
||||
unstable = []
|
||||
ipv6 = []
|
||||
cuda = []
|
||||
erasure = []
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
atty = "0.2"
|
||||
bincode = "1.0.0"
|
||||
bs58 = "0.2.0"
|
||||
byteorder = "1.2.1"
|
||||
bytes = "0.4"
|
||||
chrono = { version = "0.4.0", features = ["serde"] }
|
||||
clap = "2.31"
|
||||
dirs = "1.0.2"
|
||||
env_logger = "0.5.12"
|
||||
generic-array = { version = "0.12.0", default-features = false, features = ["serde"] }
|
||||
getopts = "0.2"
|
||||
influx_db_client = "0.3.4"
|
||||
solana-jsonrpc-core = "0.1"
|
||||
solana-jsonrpc-http-server = "0.1"
|
||||
solana-jsonrpc-macros = "0.1"
|
||||
ipnetwork = "0.12.7"
|
||||
itertools = "0.7.8"
|
||||
libc = "0.2.43"
|
||||
libloading = "0.5.0"
|
||||
log = "0.4.2"
|
||||
matches = "0.1.6"
|
||||
nix = "0.11.0"
|
||||
pnet_datalink = "0.21.0"
|
||||
rand = "0.5.1"
|
||||
rayon = "1.0.0"
|
||||
reqwest = "0.9.0"
|
||||
ring = "0.13.2"
|
||||
sha2 = "0.7.0"
|
||||
serde = "1.0.27"
|
||||
serde_cbor = "0.9.0"
|
||||
serde_derive = "1.0.27"
|
||||
serde_json = "1.0.10"
|
||||
socket2 = "0.3.8"
|
||||
solana_program_interface = { path = "common", version="0.1.0" }
|
||||
sys-info = "0.5.6"
|
||||
tokio = "0.1"
|
||||
tokio-codec = "0.1"
|
||||
untrusted = "0.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
noop = { path = "programs/noop", version="0.1.0" }
|
||||
print = { path = "programs/print", version="0.1.0" }
|
||||
move_funds = { path = "programs/move_funds", version="0.1.0" }
|
||||
|
||||
[[bench]]
|
||||
name = "bank"
|
||||
|
||||
[[bench]]
|
||||
name = "banking_stage"
|
||||
|
||||
[[bench]]
|
||||
name = "ledger"
|
||||
|
||||
[[bench]]
|
||||
name = "signature"
|
||||
|
||||
[[bench]]
|
||||
name = "sigverify"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"account-decoder",
|
||||
"accounts-bench",
|
||||
"accounts-cluster-bench",
|
||||
"banking-bench",
|
||||
"banks-client",
|
||||
"banks-interface",
|
||||
"banks-server",
|
||||
"bench-streamer",
|
||||
"bench-tps",
|
||||
"bloom",
|
||||
"bucket_map",
|
||||
"clap-utils",
|
||||
"cli",
|
||||
"cli-config",
|
||||
"cli-output",
|
||||
"client",
|
||||
"client-test",
|
||||
"core",
|
||||
"dos",
|
||||
"download-utils",
|
||||
"entry",
|
||||
"faucet",
|
||||
"frozen-abi",
|
||||
"genesis",
|
||||
"genesis-utils",
|
||||
"geyser-plugin-interface",
|
||||
"geyser-plugin-manager",
|
||||
"gossip",
|
||||
"install",
|
||||
"keygen",
|
||||
"ledger",
|
||||
"ledger-tool",
|
||||
"local-cluster",
|
||||
"log-analyzer",
|
||||
"logger",
|
||||
"measure",
|
||||
"merkle-root-bench",
|
||||
"merkle-tree",
|
||||
"metrics",
|
||||
"net-shaper",
|
||||
"net-utils",
|
||||
"notifier",
|
||||
"perf",
|
||||
"poh",
|
||||
"poh-bench",
|
||||
"program-test",
|
||||
"programs/address-lookup-table",
|
||||
"programs/address-lookup-table-tests",
|
||||
"programs/bpf_loader",
|
||||
"programs/bpf_loader/gen-syscall-list",
|
||||
"programs/compute-budget",
|
||||
"programs/config",
|
||||
"programs/ed25519-tests",
|
||||
"programs/stake",
|
||||
"programs/vote",
|
||||
"programs/zk-token-proof",
|
||||
"rayon-threadlimit",
|
||||
"rbpf-cli",
|
||||
"remote-wallet",
|
||||
"rpc",
|
||||
"rpc-test",
|
||||
"runtime",
|
||||
"runtime/store-tool",
|
||||
"sdk",
|
||||
"sdk/cargo-build-bpf",
|
||||
"sdk/cargo-test-bpf",
|
||||
"send-transaction-service",
|
||||
"stake-accounts",
|
||||
"storage-bigtable",
|
||||
"storage-proto",
|
||||
"streamer",
|
||||
"sys-tuner",
|
||||
"test-validator",
|
||||
"tokens",
|
||||
"transaction-dos",
|
||||
"transaction-status",
|
||||
"upload-perf",
|
||||
"validator",
|
||||
"version",
|
||||
"watchtower",
|
||||
"zk-token-sdk",
|
||||
".",
|
||||
"common",
|
||||
"programs/noop",
|
||||
"programs/print",
|
||||
"programs/move_funds",
|
||||
]
|
||||
|
||||
exclude = [
|
||||
"programs/bpf",
|
||||
default-members = [
|
||||
".",
|
||||
"common",
|
||||
"programs/noop",
|
||||
"programs/print",
|
||||
"programs/move_funds",
|
||||
]
|
||||
|
||||
# This prevents a Travis CI error when building for Windows.
|
||||
resolver = "2"
|
||||
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright 2022 Solana Foundation.
|
||||
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.
|
||||
|
426
README.md
426
README.md
@ -1,75 +1,346 @@
|
||||
<p align="center">
|
||||
<a href="https://solana.com">
|
||||
<img alt="Solana" src="https://i.imgur.com/IKyzQ6T.png" width="250" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](https://crates.io/crates/solana-core)
|
||||
[](https://docs.rs/solana-core)
|
||||
[](https://buildkite.com/solana-labs/solana/builds?branch=master)
|
||||
[](https://crates.io/crates/solana)
|
||||
[](https://docs.rs/solana)
|
||||
[](https://solana-ci-gate.herokuapp.com/buildkite_public_log?https://buildkite.com/solana-labs/solana/builds/latest/master)
|
||||
[](https://codecov.io/gh/solana-labs/solana)
|
||||
|
||||
# Building
|
||||
Blockchain, Rebuilt for Scale
|
||||
===
|
||||
|
||||
## **1. Install rustc, cargo and rustfmt.**
|
||||
Solana™ is a new blockchain architecture built from the ground up for scale. The architecture supports
|
||||
up to 710 thousand transactions per second on a gigabit network.
|
||||
|
||||
Disclaimer
|
||||
===
|
||||
|
||||
All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the author's best effort. It is up to the reader to check and validate their accuracy and truthfulness. Furthermore nothing in this project constitutes a solicitation for investment.
|
||||
|
||||
Introduction
|
||||
===
|
||||
|
||||
It's possible for a centralized database to process 710,000 transactions per second on a standard gigabit network if the transactions are, on average, no more than 176 bytes. A centralized database can also replicate itself and maintain high availability without significantly compromising that transaction rate using the distributed system technique known as Optimistic Concurrency Control [\[H.T.Kung, J.T.Robinson (1981)\]](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.65.4735). At Solana, we're demonstrating that these same theoretical limits apply just as well to blockchain on an adversarial network. The key ingredient? Finding a way to share time when nodes can't trust one-another. Once nodes can trust time, suddenly ~40 years of distributed systems research becomes applicable to blockchain!
|
||||
|
||||
> Perhaps the most striking difference between algorithms obtained by our method and ones based upon timeout is that using timeout produces a traditional distributed algorithm in which the processes operate asynchronously, while our method produces a globally synchronous one in which every process does the same thing at (approximately) the same time. Our method seems to contradict the whole purpose of distributed processing, which is to permit different processes to operate independently and perform different functions. However, if a distributed system is really a single system, then the processes must be synchronized in some way. Conceptually, the easiest way to synchronize processes is to get them all to do the same thing at the same time. Therefore, our method is used to implement a kernel that performs the necessary synchronization--for example, making sure that two different processes do not try to modify a file at the same time. Processes might spend only a small fraction of their time executing the synchronizing kernel; the rest of the time, they can operate independently--e.g., accessing different files. This is an approach we have advocated even when fault-tolerance is not required. The method's basic simplicity makes it easier to understand the precise properties of a system, which is crucial if one is to know just how fault-tolerant the system is. [\[L.Lamport (1984)\]](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.1078)
|
||||
|
||||
Furthermore, and much to our surprise, it can be implemented using a mechanism that has existed in Bitcoin since day one. The Bitcoin feature is called nLocktime and it can be used to postdate transactions using block height instead of a timestamp. As a Bitcoin client, you'd use block height instead of a timestamp if you don't trust the network. Block height turns out to be an instance of what's being called a Verifiable Delay Function in cryptography circles. It's a cryptographically secure way to say time has passed. In Solana, we use a far more granular verifiable delay function, a SHA 256 hash chain, to checkpoint the ledger and coordinate consensus. With it, we implement Optimistic Concurrency Control and are now well in route towards that theoretical limit of 710,000 transactions per second.
|
||||
|
||||
|
||||
Testnet Demos
|
||||
===
|
||||
|
||||
The Solana repo contains all the scripts you might need to spin up your own
|
||||
local testnet. Depending on what you're looking to achieve, you may want to
|
||||
run a different variation, as the full-fledged, performance-enhanced
|
||||
multinode testnet is considerably more complex to set up than a Rust-only,
|
||||
singlenode testnode. If you are looking to develop high-level features, such
|
||||
as experimenting with smart contracts, save yourself some setup headaches and
|
||||
stick to the Rust-only singlenode demo. If you're doing performance optimization
|
||||
of the transaction pipeline, consider the enhanced singlenode demo. If you're
|
||||
doing consensus work, you'll need at least a Rust-only multinode demo. If you want
|
||||
to reproduce our TPS metrics, run the enhanced multinode demo.
|
||||
|
||||
For all four variations, you'd need the latest Rust toolchain and the Solana
|
||||
source code:
|
||||
|
||||
First, install Rust's package manager Cargo.
|
||||
|
||||
```bash
|
||||
$ curl https://sh.rustup.rs -sSf | sh
|
||||
$ source $HOME/.cargo/env
|
||||
$ rustup component add rustfmt
|
||||
```
|
||||
|
||||
When building the master branch, please make sure you are using the latest stable rust version by running:
|
||||
|
||||
```bash
|
||||
$ rustup update
|
||||
```
|
||||
|
||||
When building a specific release branch, you should check the rust version in `ci/rust-version.sh` and if necessary, install that version by running:
|
||||
```bash
|
||||
$ rustup install VERSION
|
||||
```
|
||||
Note that if this is not the latest rust version on your machine, cargo commands may require an [override](https://rust-lang.github.io/rustup/overrides.html) in order to use the correct version.
|
||||
|
||||
On Linux systems you may need to install libssl-dev, pkg-config, zlib1g-dev, etc. On Ubuntu:
|
||||
|
||||
```bash
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang cmake make
|
||||
```
|
||||
|
||||
## **2. Download the source code.**
|
||||
Now checkout the code from github:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/solana-labs/solana.git
|
||||
$ cd solana
|
||||
```
|
||||
|
||||
## **3. Build.**
|
||||
The demo code is sometimes broken between releases as we add new low-level
|
||||
features, so if this is your first time running the demo, you'll improve
|
||||
your odds of success if you check out the
|
||||
[latest release](https://github.com/solana-labs/solana/releases)
|
||||
before proceeding:
|
||||
|
||||
```bash
|
||||
$ cargo build
|
||||
$ git checkout v0.8.0
|
||||
```
|
||||
|
||||
# Testing
|
||||
Configuration Setup
|
||||
---
|
||||
|
||||
**Run the test suite:**
|
||||
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
|
||||
```
|
||||
|
||||
Drone
|
||||
---
|
||||
|
||||
In order for the leader, client and validators to work, we'll need to
|
||||
spin up a drone to give out some test tokens. The drone delivers Milton
|
||||
Friedman-style "air drops" (free tokens to requesting clients) to be used in
|
||||
test transactions.
|
||||
|
||||
Start the drone on the leader node with:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/drone.sh
|
||||
```
|
||||
|
||||
Singlenode Testnet
|
||||
---
|
||||
|
||||
Before you start a fullnode, make sure you know the IP address of the machine you
|
||||
want to be the leader for the demo, and make sure that udp ports 8000-10000 are
|
||||
open on all the machines you want to test with.
|
||||
|
||||
Now start the server in a separate shell:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/leader.sh
|
||||
```
|
||||
|
||||
Wait a few seconds for the server to initialize. It will print "leader ready..." when it's ready to
|
||||
receive transactions. The leader will request some tokens from the drone if it doesn't have any.
|
||||
The drone does not need to be running for subsequent leader starts.
|
||||
|
||||
Multinode Testnet
|
||||
---
|
||||
|
||||
To run a multinode testnet, after starting a leader node, spin up some validator nodes in
|
||||
separate shells:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/validator.sh
|
||||
```
|
||||
|
||||
To run a performance-enhanced leader or validator (on Linux),
|
||||
[CUDA 9.2](https://developer.nvidia.com/cuda-downloads) must be installed on
|
||||
your system:
|
||||
|
||||
```bash
|
||||
$ ./fetch-perf-libs.sh
|
||||
$ SOLANA_CUDA=1 ./multinode-demo/leader.sh
|
||||
$ SOLANA_CUDA=1 ./multinode-demo/validator.sh
|
||||
```
|
||||
|
||||
|
||||
Testnet Client Demo
|
||||
---
|
||||
|
||||
Now that your singlenode or multinode testnet is up and running let's send it
|
||||
some transactions!
|
||||
|
||||
In a separate shell start the client:
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/client.sh # runs against localhost by default
|
||||
```
|
||||
|
||||
What just happened? The client demo spins up several threads to send 500,000 transactions
|
||||
to the testnet as quickly as it can. The client then pings the testnet periodically to see
|
||||
how many transactions it processed in that time. Take note that the demo intentionally
|
||||
floods the network with UDP packets, such that the network will almost certainly drop a
|
||||
bunch of them. This ensures the testnet has an opportunity to reach 710k TPS. The client
|
||||
demo completes after it has convinced itself the testnet won't process any additional
|
||||
transactions. You should see several TPS measurements printed to the screen. In the
|
||||
multinode variation, you'll see TPS measurements for each validator node as well.
|
||||
|
||||
Public Testnet
|
||||
--------------
|
||||
In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`.
|
||||
|
||||
```bash
|
||||
$ ./multinode-demo/client.sh --network $(dig +short testnet.solana.com):8001 --identity config-private/client-id.json --duration 60
|
||||
```
|
||||
|
||||
You can observe the effects of your client's transactions on our [dashboard](https://metrics.solana.com:3000/d/testnet/testnet-hud?orgId=2&from=now-30m&to=now&refresh=5s&var-testnet=testnet)
|
||||
|
||||
|
||||
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. To view
|
||||
daemon logs:
|
||||
1. Run `sudo snap logs -n=all solana` to view the daemon initialization log
|
||||
2. Runtime logging can be found under `/var/snap/solana/current/leader/`,
|
||||
`/var/snap/solana/current/validator/`, or `/var/snap/solana/current/drone/` depending
|
||||
on which `mode=` was selected. Within each log directory the file `current`
|
||||
contains the latest log, and the files `*.s` (if present) contain older rotated
|
||||
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
|
||||
===
|
||||
|
||||
Building
|
||||
---
|
||||
|
||||
Install rustc, cargo and rustfmt:
|
||||
|
||||
```bash
|
||||
$ curl https://sh.rustup.rs -sSf | sh
|
||||
$ source $HOME/.cargo/env
|
||||
$ rustup component add rustfmt-preview
|
||||
```
|
||||
|
||||
If your rustc version is lower than 1.26.1, please update it:
|
||||
|
||||
```bash
|
||||
$ rustup update
|
||||
```
|
||||
|
||||
On Linux systems you may need to install libssl-dev, pkg-config, zlib1g-dev, etc. On Ubuntu:
|
||||
|
||||
```bash
|
||||
$ sudo apt-get install libssl-dev pkg-config zlib1g-dev
|
||||
```
|
||||
|
||||
Download the source code:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/solana-labs/solana.git
|
||||
$ cd solana
|
||||
```
|
||||
|
||||
Testing
|
||||
---
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
$ cargo test
|
||||
```
|
||||
|
||||
### Starting a local testnet
|
||||
Start your own testnet locally, instructions are in the [online docs](https://docs.solana.com/cluster/bench-tps).
|
||||
To emulate all the tests that will run on a Pull Request, run:
|
||||
|
||||
### Accessing the remote development cluster
|
||||
* `devnet` - stable public cluster for development accessible via
|
||||
devnet.solana.com. Runs 24/7. Learn more about the [public clusters](https://docs.solana.com/clusters)
|
||||
```bash
|
||||
$ ./ci/run-local.sh
|
||||
```
|
||||
|
||||
# Benchmarking
|
||||
Debugging
|
||||
---
|
||||
|
||||
First, install the nightly build of rustc. `cargo bench` requires the use of the
|
||||
unstable features only available in the nightly build.
|
||||
There are some useful debug messages in the code, you can enable them on a per-module and per-level
|
||||
basis. Before running a leader or validator set the normal RUST\_LOG environment variable.
|
||||
|
||||
For example, to enable info everywhere and debug only in the solana::banking_stage module:
|
||||
|
||||
```bash
|
||||
$ export RUST_LOG=info,solana::banking_stage=debug
|
||||
```
|
||||
|
||||
Generally we are using debug for infrequent debug messages, trace for potentially frequent
|
||||
messages and info for performance-related logging.
|
||||
|
||||
You can also attach to a running process with GDB. The leader's process is named
|
||||
_solana-fullnode_:
|
||||
|
||||
```bash
|
||||
$ sudo gdb
|
||||
attach <PID>
|
||||
set logging on
|
||||
thread apply all bt
|
||||
```
|
||||
|
||||
This will dump all the threads stack traces into gdb.txt
|
||||
|
||||
Benchmarking
|
||||
---
|
||||
|
||||
First install the nightly build of rustc. `cargo bench` requires unstable features:
|
||||
|
||||
```bash
|
||||
$ rustup install nightly
|
||||
@ -78,22 +349,34 @@ $ rustup install nightly
|
||||
Run the benchmarks:
|
||||
|
||||
```bash
|
||||
$ cargo +nightly bench
|
||||
$ cargo +nightly bench --features="unstable"
|
||||
```
|
||||
|
||||
# Release Process
|
||||
Release Process
|
||||
---
|
||||
The release process for this project is described [here](rfcs/rfc-005-branches-tags-and-channels.md).
|
||||
|
||||
The release process for this project is described [here](RELEASE.md).
|
||||
|
||||
# Code coverage
|
||||
Code coverage
|
||||
---
|
||||
|
||||
To generate code coverage statistics:
|
||||
To generate code coverage statistics, install cargo-cov. Note: the tool currently only works
|
||||
in Rust nightly.
|
||||
|
||||
```bash
|
||||
$ scripts/coverage.sh
|
||||
$ open target/cov/lcov-local/index.html
|
||||
$ cargo +nightly install cargo-cov
|
||||
```
|
||||
|
||||
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
|
||||
productivity metric. When a developer makes a change to the codebase, presumably it's a *solution* to
|
||||
some problem. Our unit-test suite is how we encode the set of *problems* the codebase solves. Running
|
||||
@ -105,44 +388,3 @@ problem is solved by this code?" On the other hand, if a test does fail and you
|
||||
better way to solve the same problem, a Pull Request with your solution would most certainly be
|
||||
welcome! Likewise, if rewriting a test can better communicate what code it's protecting, please
|
||||
send us that patch!
|
||||
|
||||
# Disclaimer
|
||||
|
||||
All claims, content, designs, algorithms, estimates, roadmaps,
|
||||
specifications, and performance measurements described in this project
|
||||
are done with the Solana Foundation's ("SF") good faith efforts. It is up to
|
||||
the reader to check and validate their accuracy and truthfulness.
|
||||
Furthermore, nothing in this project constitutes a solicitation for
|
||||
investment.
|
||||
|
||||
Any content produced by SF or developer resources that SF provides are
|
||||
for educational and inspirational purposes only. SF does not encourage,
|
||||
induce or sanction the deployment, integration or use of any such
|
||||
applications (including the code comprising the Solana blockchain
|
||||
protocol) in violation of applicable laws or regulations and hereby
|
||||
prohibits any such deployment, integration or use. This includes the use of
|
||||
any such applications by the reader (a) in violation of export control
|
||||
or sanctions laws of the United States or any other applicable
|
||||
jurisdiction, (b) if the reader is located in or ordinarily resident in
|
||||
a country or territory subject to comprehensive sanctions administered
|
||||
by the U.S. Office of Foreign Assets Control (OFAC), or (c) if the
|
||||
reader is or is working on behalf of a Specially Designated National
|
||||
(SDN) or a person subject to similar blocking or denied party
|
||||
prohibitions.
|
||||
|
||||
The reader should be aware that U.S. export control and sanctions laws
|
||||
prohibit U.S. persons (and other persons that are subject to such laws)
|
||||
from transacting with persons in certain countries and territories or
|
||||
that are on the SDN list. As a project-based primarily on open-source
|
||||
software, it is possible that such sanctioned persons may nevertheless
|
||||
bypass prohibitions, obtain the code comprising the Solana blockchain
|
||||
protocol (or other project code or applications) and deploy, integrate,
|
||||
or otherwise use it. Accordingly, there is a risk to individuals that
|
||||
other persons using the Solana blockchain protocol may be sanctioned
|
||||
persons and that transactions with such persons would be a violation of
|
||||
U.S. export controls and sanctions law. This risk applies to
|
||||
individuals, organizations, and other ecosystem participants that
|
||||
deploy, integrate, or use the Solana blockchain protocol code directly
|
||||
(e.g., as a node operator), and individuals that transact on the Solana
|
||||
blockchain through light clients, third party interfaces, and/or wallet
|
||||
software.
|
||||
|
166
RELEASE.md
166
RELEASE.md
@ -1,156 +1,32 @@
|
||||
# Solana Release process
|
||||
|
||||
## Branches and Tags
|
||||
## Introduction
|
||||
|
||||
```
|
||||
========================= master branch (edge channel) =======================>
|
||||
\ \ \
|
||||
\___v0.7.0 tag \ \
|
||||
\ \ v0.9.0 tag__\
|
||||
\ v0.8.0 tag__\ \
|
||||
v0.7.1 tag__\ \ v0.9 branch (beta channel)
|
||||
\___v0.7.2 tag \___v0.8.1 tag
|
||||
\ \
|
||||
\ \
|
||||
v0.7 branch v0.8 branch (stable channel)
|
||||
Solana uses a channel-oriented, date-based branching process described [here](https://github.com/solana-labs/solana/blob/master/rfcs/rfc-005-branches-tags-and-channels.md).
|
||||
|
||||
```
|
||||
## Release Steps
|
||||
|
||||
### master branch
|
||||
All new development occurs on the `master` branch.
|
||||
### Changing channels
|
||||
|
||||
Bug fixes that affect a `vX.Y` branch are first made on `master`. This is to
|
||||
allow a fix some soak time on `master` before it is applied to one or more
|
||||
stabilization branches.
|
||||
When cutting a new channel branch these pre-steps are required:
|
||||
|
||||
Merging to `master` first also helps ensure that fixes applied to one release
|
||||
are present for future releases. (Sometimes the joy of landing a critical
|
||||
release blocker in a branch causes you to forget to propagate back to
|
||||
`master`!)"
|
||||
1. Pick your branch point for release on master.
|
||||
2. Create the branch. The name should be "v" + the first 2 "version" fields from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies the next branch name is "v0.9".
|
||||
3. Update Cargo.toml to the next semantic version (e.g. 0.9.0 -> 0.10.0).
|
||||
4. Push your new branch to solana.git
|
||||
5. Land your Carto.toml change as a master PR.
|
||||
|
||||
Once the bug fix lands on `master` it is cherry-picked into the `vX.Y` branch
|
||||
and potentially the `vX.Y-1` branch. The exception to this rule is when a bug
|
||||
fix for `vX.Y` doesn't apply to `master` or `vX.Y-1`.
|
||||
At this point, ci/channel-info.sh should show your freshly cut release branch as "BETA_CHANNEL" and the previous release branch as "STABLE_CHANNEL".
|
||||
|
||||
Immediately after a new stabilization branch is forged, the `Cargo.toml` minor
|
||||
version (*Y*) in the `master` branch is incremented by the release engineer.
|
||||
Incrementing the major version of the `master` branch is outside the scope of
|
||||
this document.
|
||||
### Updating channels (i.e. "making a release")
|
||||
|
||||
### v*X.Y* stabilization branches
|
||||
These are stabilization branches for a given milestone. They are created off
|
||||
the `master` branch as late as possible prior to the milestone release.
|
||||
We use [github's Releases UI](https://github.com/solana-labs/solana/releases) for tagging a release.
|
||||
|
||||
### v*X.Y.Z* release tag
|
||||
The release tags are created as desired by the owner of the given stabilization
|
||||
branch, and cause that *X.Y.Z* release to be shipped to https://crates.io
|
||||
|
||||
Immediately after a new v*X.Y.Z* branch tag has been created, the `Cargo.toml`
|
||||
patch version number (*Z*) of the stabilization branch is incremented by the
|
||||
release engineer.
|
||||
|
||||
## Channels
|
||||
Channels are used by end-users (humans and bots) to consume the branches
|
||||
described in the previous section, so they may automatically update to the most
|
||||
recent version matching their desired stability.
|
||||
|
||||
There are three release channels that map to branches as follows:
|
||||
* edge - tracks the `master` branch, least stable.
|
||||
* beta - tracks the largest (and latest) `vX.Y` stabilization branch, more stable.
|
||||
* stable - tracks the second largest `vX.Y` stabilization branch, most stable.
|
||||
|
||||
## Steps to Create a Branch
|
||||
|
||||
### Create the new branch
|
||||
1. Check out the latest commit on `master` branch:
|
||||
```
|
||||
git fetch --all
|
||||
git checkout upstream/master
|
||||
```
|
||||
1. Determine the new branch name. The name should be "v" + the first 2 version fields
|
||||
from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies
|
||||
the next branch name is "v0.9".
|
||||
1. Create the new branch and push this branch to the `solana` repository:
|
||||
```
|
||||
git checkout -b <branchname>
|
||||
git push -u origin <branchname>
|
||||
```
|
||||
|
||||
Alternatively use the Github UI.
|
||||
|
||||
### Update master branch to the next release minor version
|
||||
|
||||
1. After the new branch has been created and pushed, update the Cargo.toml files on **master** to the next semantic version (e.g. 0.9.0 -> 0.10.0) with:
|
||||
```
|
||||
$ scripts/increment-cargo-version.sh minor
|
||||
$ ./scripts/cargo-for-all-lock-files.sh update
|
||||
```
|
||||
1. Push all the changed Cargo.toml and Cargo.lock files to the `master` branch with something like:
|
||||
```
|
||||
git co -b version_update
|
||||
git ls-files -m | xargs git add
|
||||
git commit -m 'Bump version to X.Y+1.0'
|
||||
git push -u origin version_update
|
||||
```
|
||||
1. Confirm that your freshly cut release branch is shown as `BETA_CHANNEL` and the previous release branch as `STABLE_CHANNEL`:
|
||||
```
|
||||
ci/channel-info.sh
|
||||
```
|
||||
|
||||
## Steps to Create a Release
|
||||
|
||||
### Create the Release Tag on GitHub
|
||||
|
||||
1. Go to [GitHub Releases](https://github.com/solana-labs/solana/releases) for tagging a release.
|
||||
1. Click "Draft new release". The release tag must exactly match the `version`
|
||||
field in `/Cargo.toml` prefixed by `v`.
|
||||
1. If the Cargo.toml version field is **0.12.3**, then the release tag must be **v0.12.3**
|
||||
1. Make sure the Target Branch field matches the branch you want to make a release on.
|
||||
1. If you want to release v0.12.0, the target branch must be v0.12
|
||||
1. Fill the release notes.
|
||||
1. If this is the first release on the branch (e.g. v0.13.**0**), paste in [this
|
||||
template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md). Engineering Lead can provide summary contents for release notes if needed.
|
||||
1. If this is a patch release, review all the commits since the previous release on this branch and add details as needed.
|
||||
1. Click "Save Draft", then confirm the release notes look good and the tag name and branch are correct.
|
||||
1. Ensure all desired commits (usually backports) are landed on the branch by now.
|
||||
1. Ensure the release is marked **"This is a pre-release"**. This flag will need to be be removed manually after confirming the the Linux binary artifacts appear at a later step.
|
||||
1. Go back into edit the release and click "Publish release" while being marked as a pre-release.
|
||||
1. Confirm there is new git tag with intended version number at the intended revision after running `git fetch` locally.
|
||||
|
||||
|
||||
### Update release branch with the next patch version
|
||||
|
||||
1. After the new release has been tagged, update the Cargo.toml files on **release branch** to the next semantic version (e.g. 0.9.0 -> 0.9.1) with:
|
||||
```
|
||||
$ scripts/increment-cargo-version.sh patch
|
||||
$ ./scripts/cargo-for-all-lock-files.sh tree
|
||||
```
|
||||
1. Push all the changed Cargo.toml and Cargo.lock files to the **release branch** with something like:
|
||||
```
|
||||
git co -b version_update origin/vX.Y
|
||||
git add -u
|
||||
git commit -m 'Bump version to X.Y.Z+1'
|
||||
git push -u <user-remote> version_update
|
||||
```
|
||||
1. Open a PR against origin/vX.Y and then merge the PR after passing CI.
|
||||
|
||||
### Prepare for the next release
|
||||
1. Go to [GitHub Releases](https://github.com/solana-labs/solana/releases) and create a new draft release for `X.Y.Z+1` with empty release notes. This allows people to incrementally add new release notes until it's time for the next release
|
||||
1. Also, point the branch field to the same branch and mark the relese as **"This is a pre-release"**.
|
||||
1. Go to the [Github Milestones](https://github.com/solana-labs/solana/milestones). Create a new milestone for the `X.Y.Z+1`, move over
|
||||
unresolved issues still in the `X.Y.Z` milestone, then close the `X.Y.Z` milestone.
|
||||
|
||||
### Verify release automation success
|
||||
Go to [Solana Releases](https://github.com/solana-labs/solana/releases) and click on the latest release that you just published.
|
||||
Verify that all of the build artifacts are present, then the uncheck **"This is a pre-release"** for the release.
|
||||
|
||||
Build artifacts can take up to 60 minutes after creating the tag before
|
||||
appearing. To check for progress:
|
||||
* The `solana-secondary` Buildkite pipeline handles creating the Linux release artifacts and updated crates. Look for a job under the tag name of the release: https://buildkite.com/solana-labs/solana-secondary.
|
||||
* The macOS and Windows release artifacts are produced by Travis CI: https://travis-ci.com/github/solana-labs/solana/branches
|
||||
|
||||
[Crates.io](https://crates.io/crates/solana) should have an updated Solana version. This can take 2-3 hours, and sometimes fails in the `solana-secondary` job.
|
||||
If this happens and the error is non-fatal, click "Retry" on the "publish crate" job
|
||||
|
||||
### Update software on testnet.solana.com
|
||||
See the documentation at https://github.com/solana-labs/cluster-ops/. devnet.solana.com and mainnet-beta.solana.com run stable releases that have been tested on testnet. Do not update devnet or mainnet-beta with a beta release.
|
||||
1. Go [there ;)](https://github.com/solana-labs/solana/releases).
|
||||
2. Click "Draft new release".
|
||||
3. If the first major release on the branch (e.g. v0.8.0), paste in [this template](https://raw.githubusercontent.com/solana-labs/solana/master/.github/RELEASE_TEMPLATE.md) and fill it in.
|
||||
4. Test the release by generating a tag using semver's rules. First try at a release should be <branchname>.X-rc.0.
|
||||
5. Verify release automation:
|
||||
1. [Crates.io](https://crates.io/crates/solana) should have an updated Solana version.
|
||||
2. ...
|
||||
6. After testnet deployment, verify that testnets are running correct software. http://metrics.solana.com should show testnet running on a hash from your newly created branch.
|
||||
|
169
SECURITY.md
169
SECURITY.md
@ -1,169 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
1. [Reporting security problems](#reporting)
|
||||
4. [Security Bug Bounties](#bounty)
|
||||
2. [Incident Response Process](#process)
|
||||
|
||||
<a name="reporting"></a>
|
||||
## Reporting security problems to Solana
|
||||
|
||||
**DO NOT CREATE AN ISSUE** to report a security problem. Instead, please send an
|
||||
email to security@solana.com and provide your github username so we can add you
|
||||
to a new draft security advisory for further discussion.
|
||||
|
||||
Expect a response as fast as possible, typically within 72 hours.
|
||||
|
||||
<a name="bounty"></a>
|
||||
## Security Bug Bounties
|
||||
We offer bounties for critical security issues. Please see below for more details.
|
||||
|
||||
Loss of Funds:
|
||||
$2,000,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* Theft of funds without users signature from any account
|
||||
* Theft of funds without users interaction in system, token, stake, vote programs
|
||||
* Theft of funds that requires users signature - creating a vote program that drains the delegated stakes.
|
||||
|
||||
Consensus/Safety Violations:
|
||||
$1,000,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* Consensus safety violation
|
||||
* Tricking a validator to accept an optimistic confirmation or rooted slot without a double vote, etc..
|
||||
|
||||
Other Attacks:
|
||||
$400,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* Protocol liveness attacks,
|
||||
* Eclipse attacks,
|
||||
* Remote attacks that partition the network,
|
||||
|
||||
DoS Attacks:
|
||||
$100,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* Remote resource exaustion via Non-RPC protocols
|
||||
|
||||
RPC DoS/Crashes:
|
||||
$5,000 USD in locked SOL tokens (locked for 12 months)
|
||||
* RPC attacks
|
||||
|
||||
Out of Scope:
|
||||
The following components are out of scope for the bounty program
|
||||
* Metrics: `/metrics` in the monorepo as well as https://metrics.solana.com
|
||||
* Explorer: `/explorer` in the monorepo as well as https://explorer.solana.com
|
||||
* Any encrypted credentials, auth tokens, etc. checked into the repo
|
||||
* Bugs in dependencies. Please take them upstream!
|
||||
* Attacks that require social engineering
|
||||
|
||||
Eligibility:
|
||||
* The participant submitting the bug report shall follow the process outlined within this document
|
||||
* Valid exploits can be eligible even if they are not successfully executed on the cluster
|
||||
* Multiple submissions for the same class of exploit are still eligible for compensation, though may be compensated at a lower rate, however these will be assessed on a case-by-case basis
|
||||
* Participants must complete KYC and sign the participation agreement here when the registrations are open https://solana.com/validator-registration. Security exploits will still be assessed and open for submission at all times. This needs only be done prior to distribution of tokens.
|
||||
|
||||
Payment of Bug Bounties:
|
||||
* Payments for eligible bug reports are distributed monthly.
|
||||
* Bounties for all bug reports submitted in a given month are paid out in the middle of the
|
||||
following month.
|
||||
* The SOL/USD conversion rate used for payments is the market price at the end of
|
||||
the last day of the month for the month in which the bug was submitted.
|
||||
* The reference for this price is the Closing Price given by Coingecko.com on
|
||||
that date given here:
|
||||
https://www.coingecko.com/en/coins/solana/historical_data/usd#panel
|
||||
* For example, for all bugs submitted in March 2021, the SOL/USD price for bug
|
||||
payouts is the Close price on 2021-03-31 of $19.49. This applies to all bugs
|
||||
submitted in March 2021, to be paid in mid-April 2021.
|
||||
* Bug bounties are paid out in
|
||||
[stake accounts](https://solana.com/staking) with a
|
||||
[lockup](https://docs.solana.com/staking/stake-accounts#lockups)
|
||||
expiring 12 months from the last day of the month in which the bug was submitted.
|
||||
|
||||
<a name="process"></a>
|
||||
## Incident Response Process
|
||||
|
||||
In case an incident is discovered or reported, the following process will be
|
||||
followed to contain, respond and remediate:
|
||||
|
||||
### 1. Establish a new draft security advisory
|
||||
In response to an email to security@solana.com, a member of the `solana-labs/admins` group will
|
||||
1. Create a new draft security advisory for the incident at https://github.com/solana-labs/solana/security/advisories
|
||||
1. Add the reporter's github user and the `solana-labs/security-incident-response` group to the draft security advisory
|
||||
1. Create a private fork of the repository (grey button towards the bottom of the page)
|
||||
1. Respond to the reporter by email, sharing a link to the draft security advisory
|
||||
|
||||
### 2. Triage
|
||||
Within the draft security advisory, discuss and determine the severity of the
|
||||
issue. If necessary, members of the `solana-labs/security-incident-response`
|
||||
group may add other github users to the advisory to assist.
|
||||
|
||||
If it is determined that this not a critical network issue then the advisory
|
||||
should be closed and if more follow-up is required a normal Solana public github
|
||||
issue should be created.
|
||||
|
||||
### 3. Prepare Fixes
|
||||
For the affected branches, typically all three (edge, beta and stable), prepare
|
||||
a fix for the issue and push them to the corresponding branch in the private
|
||||
repository associated with the draft security advisory.
|
||||
|
||||
There is no CI available in the private repository so you must build from source
|
||||
and manually verify fixes.
|
||||
|
||||
Code review from the reporter is ideal, as well as from multiple members of the
|
||||
core development team.
|
||||
|
||||
### 4. Notify Security Group Validators
|
||||
Once an ETA is available for the fix, a member of the
|
||||
`solana-labs/security-incident-response` group should notify the validators so
|
||||
they can prepare for an update using the "Solana Red Alert" notification system.
|
||||
|
||||
The teams are all over the world and it's critical to provide actionable
|
||||
information at the right time. Don't be the person that wakes everybody up at
|
||||
2am when a fix won't be available for hours.
|
||||
|
||||
### 5. Ship the patch
|
||||
Once the fix is accepted, a member of the
|
||||
`solana-labs/security-incident-response` group should prepare a single patch
|
||||
file for each affected branch. The commit title for the patch should only
|
||||
contain the advisory id, and not disclose any further details about the
|
||||
incident.
|
||||
|
||||
Copy the patches to https://release.solana.com/ under a subdirectory named after
|
||||
the advisory id (example:
|
||||
https://release.solana.com/GHSA-hx59-f5g4-jghh/v1.4.patch). Contact a member of
|
||||
the `solana-labs/admins` group if you require access to release.solana.com
|
||||
|
||||
Using the "Solana Red Alert" channel:
|
||||
1. Notify validators that there's an issue and a patch will be provided in X minutes
|
||||
2. If X minutes expires and there's no patch, notify of the delay and provide a
|
||||
new ETA
|
||||
3. Provide links to patches of https://release.solana.com/ for each affected branch
|
||||
|
||||
Validators can be expected to build the patch from source against the latest
|
||||
release for the affected branch.
|
||||
|
||||
Since the software version will not change after the patch is applied, request
|
||||
that each validator notify in the existing channel once they've updated. Manually
|
||||
monitor the roll out until a sufficient amount of stake has updated - typically
|
||||
at least 33.3% or 66.6% depending on the issue.
|
||||
|
||||
### 6. Public Disclosure and Release
|
||||
Once the fix has been deployed to the security group validators, the patches from the security
|
||||
advisory may be merged into the main source repository. A new official release
|
||||
for each affected branch should be shipped and all validators requested to
|
||||
upgrade as quickly as possible.
|
||||
|
||||
### 7. Security Advisory Bounty Accounting and Cleanup
|
||||
|
||||
If this issue is eligible for a bounty, prefix the title of the security
|
||||
advisory with one of the following, depending on the severity:
|
||||
* `[Bounty Category: Critical: Loss of Funds]`
|
||||
* `[Bounty Category: Critical: Loss of Availability]`
|
||||
* `[Bounty Category: Critical: DoS]`
|
||||
* `[Bounty Category: Critical: Other]`
|
||||
* `[Bounty Category: Non-critical]`
|
||||
* `[Bounty Category: RPC]`
|
||||
|
||||
Confirm with the reporter that they agree with the severity assessment, and
|
||||
discuss as required to reach a conclusion.
|
||||
|
||||
We currently do not use the Github workflow to publish security advisories.
|
||||
Once the issue and fix have been disclosed, and a bounty category is assessed if
|
||||
appropriate, the GitHub security advisory is no longer needed and can be closed.
|
||||
|
||||
Bounties are currently awarded once a quarter (TODO: link to this process, or
|
||||
inline the workflow)
|
@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "solana-account-decoder"
|
||||
version = "1.11.0"
|
||||
description = "Solana account decoder"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-account-decoder"
|
||||
license = "Apache-2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
Inflector = "0.11.4"
|
||||
base64 = "0.13.0"
|
||||
bincode = "1.3.3"
|
||||
bs58 = "0.4.0"
|
||||
bv = "0.11.1"
|
||||
lazy_static = "1.4.0"
|
||||
serde = "1.0.136"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.79"
|
||||
solana-config-program = { path = "../programs/config", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "=1.11.0" }
|
||||
spl-token = { version = "=3.2.0", features = ["no-entrypoint"] }
|
||||
thiserror = "1.0"
|
||||
zstd = "0.11.1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,259 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
pub mod parse_account_data;
|
||||
pub mod parse_bpf_loader;
|
||||
pub mod parse_config;
|
||||
pub mod parse_nonce;
|
||||
pub mod parse_stake;
|
||||
pub mod parse_sysvar;
|
||||
pub mod parse_token;
|
||||
pub mod parse_vote;
|
||||
pub mod validator_info;
|
||||
|
||||
use {
|
||||
crate::parse_account_data::{parse_account_data, AccountAdditionalData, ParsedAccount},
|
||||
solana_sdk::{
|
||||
account::{ReadableAccount, WritableAccount},
|
||||
clock::Epoch,
|
||||
fee_calculator::FeeCalculator,
|
||||
pubkey::Pubkey,
|
||||
},
|
||||
std::{
|
||||
io::{Read, Write},
|
||||
str::FromStr,
|
||||
},
|
||||
};
|
||||
|
||||
pub type StringAmount = String;
|
||||
pub type StringDecimals = String;
|
||||
pub const MAX_BASE58_BYTES: usize = 128;
|
||||
|
||||
/// A duplicate representation of an Account for pretty JSON serialization
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiAccount {
|
||||
pub lamports: u64,
|
||||
pub data: UiAccountData,
|
||||
pub owner: String,
|
||||
pub executable: bool,
|
||||
pub rent_epoch: Epoch,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum UiAccountData {
|
||||
LegacyBinary(String), // Legacy. Retained for RPC backwards compatibility
|
||||
Json(ParsedAccount),
|
||||
Binary(String, UiAccountEncoding),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum UiAccountEncoding {
|
||||
Binary, // Legacy. Retained for RPC backwards compatibility
|
||||
Base58,
|
||||
Base64,
|
||||
JsonParsed,
|
||||
#[serde(rename = "base64+zstd")]
|
||||
Base64Zstd,
|
||||
}
|
||||
|
||||
impl UiAccount {
|
||||
fn encode_bs58<T: ReadableAccount>(
|
||||
account: &T,
|
||||
data_slice_config: Option<UiDataSliceConfig>,
|
||||
) -> String {
|
||||
if account.data().len() <= MAX_BASE58_BYTES {
|
||||
bs58::encode(slice_data(account.data(), data_slice_config)).into_string()
|
||||
} else {
|
||||
"error: data too large for bs58 encoding".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode<T: ReadableAccount>(
|
||||
pubkey: &Pubkey,
|
||||
account: &T,
|
||||
encoding: UiAccountEncoding,
|
||||
additional_data: Option<AccountAdditionalData>,
|
||||
data_slice_config: Option<UiDataSliceConfig>,
|
||||
) -> Self {
|
||||
let data = match encoding {
|
||||
UiAccountEncoding::Binary => {
|
||||
let data = Self::encode_bs58(account, data_slice_config);
|
||||
UiAccountData::LegacyBinary(data)
|
||||
}
|
||||
UiAccountEncoding::Base58 => {
|
||||
let data = Self::encode_bs58(account, data_slice_config);
|
||||
UiAccountData::Binary(data, encoding)
|
||||
}
|
||||
UiAccountEncoding::Base64 => UiAccountData::Binary(
|
||||
base64::encode(slice_data(account.data(), data_slice_config)),
|
||||
encoding,
|
||||
),
|
||||
UiAccountEncoding::Base64Zstd => {
|
||||
let mut encoder = zstd::stream::write::Encoder::new(Vec::new(), 0).unwrap();
|
||||
match encoder
|
||||
.write_all(slice_data(account.data(), data_slice_config))
|
||||
.and_then(|()| encoder.finish())
|
||||
{
|
||||
Ok(zstd_data) => UiAccountData::Binary(base64::encode(zstd_data), encoding),
|
||||
Err(_) => UiAccountData::Binary(
|
||||
base64::encode(slice_data(account.data(), data_slice_config)),
|
||||
UiAccountEncoding::Base64,
|
||||
),
|
||||
}
|
||||
}
|
||||
UiAccountEncoding::JsonParsed => {
|
||||
if let Ok(parsed_data) =
|
||||
parse_account_data(pubkey, account.owner(), account.data(), additional_data)
|
||||
{
|
||||
UiAccountData::Json(parsed_data)
|
||||
} else {
|
||||
UiAccountData::Binary(
|
||||
base64::encode(&account.data()),
|
||||
UiAccountEncoding::Base64,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
UiAccount {
|
||||
lamports: account.lamports(),
|
||||
data,
|
||||
owner: account.owner().to_string(),
|
||||
executable: account.executable(),
|
||||
rent_epoch: account.rent_epoch(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode<T: WritableAccount>(&self) -> Option<T> {
|
||||
let data = match &self.data {
|
||||
UiAccountData::Json(_) => None,
|
||||
UiAccountData::LegacyBinary(blob) => bs58::decode(blob).into_vec().ok(),
|
||||
UiAccountData::Binary(blob, encoding) => match encoding {
|
||||
UiAccountEncoding::Base58 => bs58::decode(blob).into_vec().ok(),
|
||||
UiAccountEncoding::Base64 => base64::decode(blob).ok(),
|
||||
UiAccountEncoding::Base64Zstd => base64::decode(blob).ok().and_then(|zstd_data| {
|
||||
let mut data = vec![];
|
||||
zstd::stream::read::Decoder::new(zstd_data.as_slice())
|
||||
.and_then(|mut reader| reader.read_to_end(&mut data))
|
||||
.map(|_| data)
|
||||
.ok()
|
||||
}),
|
||||
UiAccountEncoding::Binary | UiAccountEncoding::JsonParsed => None,
|
||||
},
|
||||
}?;
|
||||
Some(T::create(
|
||||
self.lamports,
|
||||
data,
|
||||
Pubkey::from_str(&self.owner).ok()?,
|
||||
self.executable,
|
||||
self.rent_epoch,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiFeeCalculator {
|
||||
pub lamports_per_signature: StringAmount,
|
||||
}
|
||||
|
||||
impl From<FeeCalculator> for UiFeeCalculator {
|
||||
fn from(fee_calculator: FeeCalculator) -> Self {
|
||||
Self {
|
||||
lamports_per_signature: fee_calculator.lamports_per_signature.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UiFeeCalculator {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lamports_per_signature: "0".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiDataSliceConfig {
|
||||
pub offset: usize,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
fn slice_data(data: &[u8], data_slice_config: Option<UiDataSliceConfig>) -> &[u8] {
|
||||
if let Some(UiDataSliceConfig { offset, length }) = data_slice_config {
|
||||
if offset >= data.len() {
|
||||
&[]
|
||||
} else if length > data.len() - offset {
|
||||
&data[offset..]
|
||||
} else {
|
||||
&data[offset..offset + length]
|
||||
}
|
||||
} else {
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {
|
||||
super::*,
|
||||
solana_sdk::account::{Account, AccountSharedData},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_slice_data() {
|
||||
let data = vec![1, 2, 3, 4, 5];
|
||||
let slice_config = Some(UiDataSliceConfig {
|
||||
offset: 0,
|
||||
length: 5,
|
||||
});
|
||||
assert_eq!(slice_data(&data, slice_config), &data[..]);
|
||||
|
||||
let slice_config = Some(UiDataSliceConfig {
|
||||
offset: 0,
|
||||
length: 10,
|
||||
});
|
||||
assert_eq!(slice_data(&data, slice_config), &data[..]);
|
||||
|
||||
let slice_config = Some(UiDataSliceConfig {
|
||||
offset: 1,
|
||||
length: 2,
|
||||
});
|
||||
assert_eq!(slice_data(&data, slice_config), &data[1..3]);
|
||||
|
||||
let slice_config = Some(UiDataSliceConfig {
|
||||
offset: 10,
|
||||
length: 2,
|
||||
});
|
||||
assert_eq!(slice_data(&data, slice_config), &[] as &[u8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base64_zstd() {
|
||||
let encoded_account = UiAccount::encode(
|
||||
&Pubkey::default(),
|
||||
&AccountSharedData::from(Account {
|
||||
data: vec![0; 1024],
|
||||
..Account::default()
|
||||
}),
|
||||
UiAccountEncoding::Base64Zstd,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(matches!(
|
||||
encoded_account.data,
|
||||
UiAccountData::Binary(_, UiAccountEncoding::Base64Zstd)
|
||||
));
|
||||
|
||||
let decoded_account = encoded_account.decode::<Account>().unwrap();
|
||||
assert_eq!(decoded_account.data(), &vec![0; 1024]);
|
||||
let decoded_account = encoded_account.decode::<AccountSharedData>().unwrap();
|
||||
assert_eq!(decoded_account.data(), &vec![0; 1024]);
|
||||
}
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
parse_bpf_loader::parse_bpf_upgradeable_loader,
|
||||
parse_config::parse_config,
|
||||
parse_nonce::parse_nonce,
|
||||
parse_stake::parse_stake,
|
||||
parse_sysvar::parse_sysvar,
|
||||
parse_token::{parse_token, spl_token_ids},
|
||||
parse_vote::parse_vote,
|
||||
},
|
||||
inflector::Inflector,
|
||||
serde_json::Value,
|
||||
solana_sdk::{instruction::InstructionError, pubkey::Pubkey, stake, system_program, sysvar},
|
||||
std::collections::HashMap,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref BPF_UPGRADEABLE_LOADER_PROGRAM_ID: Pubkey = solana_sdk::bpf_loader_upgradeable::id();
|
||||
static ref CONFIG_PROGRAM_ID: Pubkey = solana_config_program::id();
|
||||
static ref STAKE_PROGRAM_ID: Pubkey = stake::program::id();
|
||||
static ref SYSTEM_PROGRAM_ID: Pubkey = system_program::id();
|
||||
static ref SYSVAR_PROGRAM_ID: Pubkey = sysvar::id();
|
||||
static ref VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id();
|
||||
pub static ref PARSABLE_PROGRAM_IDS: HashMap<Pubkey, ParsableAccount> = {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(
|
||||
*BPF_UPGRADEABLE_LOADER_PROGRAM_ID,
|
||||
ParsableAccount::BpfUpgradeableLoader,
|
||||
);
|
||||
m.insert(*CONFIG_PROGRAM_ID, ParsableAccount::Config);
|
||||
m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce);
|
||||
for spl_token_id in spl_token_ids() {
|
||||
m.insert(spl_token_id, ParsableAccount::SplToken);
|
||||
}
|
||||
m.insert(*STAKE_PROGRAM_ID, ParsableAccount::Stake);
|
||||
m.insert(*SYSVAR_PROGRAM_ID, ParsableAccount::Sysvar);
|
||||
m.insert(*VOTE_PROGRAM_ID, ParsableAccount::Vote);
|
||||
m
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ParseAccountError {
|
||||
#[error("{0:?} account not parsable")]
|
||||
AccountNotParsable(ParsableAccount),
|
||||
|
||||
#[error("Program not parsable")]
|
||||
ProgramNotParsable,
|
||||
|
||||
#[error("Additional data required to parse: {0}")]
|
||||
AdditionalDataMissing(String),
|
||||
|
||||
#[error("Instruction error")]
|
||||
InstructionError(#[from] InstructionError),
|
||||
|
||||
#[error("Serde json error")]
|
||||
SerdeJsonError(#[from] serde_json::error::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ParsedAccount {
|
||||
pub program: String,
|
||||
pub parsed: Value,
|
||||
pub space: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ParsableAccount {
|
||||
BpfUpgradeableLoader,
|
||||
Config,
|
||||
Nonce,
|
||||
SplToken,
|
||||
Stake,
|
||||
Sysvar,
|
||||
Vote,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AccountAdditionalData {
|
||||
pub spl_token_decimals: Option<u8>,
|
||||
}
|
||||
|
||||
pub fn parse_account_data(
|
||||
pubkey: &Pubkey,
|
||||
program_id: &Pubkey,
|
||||
data: &[u8],
|
||||
additional_data: Option<AccountAdditionalData>,
|
||||
) -> Result<ParsedAccount, ParseAccountError> {
|
||||
let program_name = PARSABLE_PROGRAM_IDS
|
||||
.get(program_id)
|
||||
.ok_or(ParseAccountError::ProgramNotParsable)?;
|
||||
let additional_data = additional_data.unwrap_or_default();
|
||||
let parsed_json = match program_name {
|
||||
ParsableAccount::BpfUpgradeableLoader => {
|
||||
serde_json::to_value(parse_bpf_upgradeable_loader(data)?)?
|
||||
}
|
||||
ParsableAccount::Config => serde_json::to_value(parse_config(data, pubkey)?)?,
|
||||
ParsableAccount::Nonce => serde_json::to_value(parse_nonce(data)?)?,
|
||||
ParsableAccount::SplToken => {
|
||||
serde_json::to_value(parse_token(data, additional_data.spl_token_decimals)?)?
|
||||
}
|
||||
ParsableAccount::Stake => serde_json::to_value(parse_stake(data)?)?,
|
||||
ParsableAccount::Sysvar => serde_json::to_value(parse_sysvar(data, pubkey)?)?,
|
||||
ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?,
|
||||
};
|
||||
Ok(ParsedAccount {
|
||||
program: format!("{:?}", program_name).to_kebab_case(),
|
||||
parsed: parsed_json,
|
||||
space: data.len() as u64,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {
|
||||
super::*,
|
||||
solana_sdk::nonce::{
|
||||
state::{Data, Versions},
|
||||
State,
|
||||
},
|
||||
solana_vote_program::vote_state::{VoteState, VoteStateVersions},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_account_data() {
|
||||
let account_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let other_program = solana_sdk::pubkey::new_rand();
|
||||
let data = vec![0; 4];
|
||||
assert!(parse_account_data(&account_pubkey, &other_program, &data, None).is_err());
|
||||
|
||||
let vote_state = VoteState::default();
|
||||
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
|
||||
let versioned = VoteStateVersions::new_current(vote_state);
|
||||
VoteState::serialize(&versioned, &mut vote_account_data).unwrap();
|
||||
let parsed = parse_account_data(
|
||||
&account_pubkey,
|
||||
&solana_vote_program::id(),
|
||||
&vote_account_data,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parsed.program, "vote".to_string());
|
||||
assert_eq!(parsed.space, VoteState::size_of() as u64);
|
||||
|
||||
let nonce_data = Versions::new_current(State::Initialized(Data::default()));
|
||||
let nonce_account_data = bincode::serialize(&nonce_data).unwrap();
|
||||
let parsed = parse_account_data(
|
||||
&account_pubkey,
|
||||
&system_program::id(),
|
||||
&nonce_account_data,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parsed.program, "nonce".to_string());
|
||||
assert_eq!(parsed.space, State::size() as u64);
|
||||
}
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
parse_account_data::{ParsableAccount, ParseAccountError},
|
||||
UiAccountData, UiAccountEncoding,
|
||||
},
|
||||
bincode::{deserialize, serialized_size},
|
||||
solana_sdk::{bpf_loader_upgradeable::UpgradeableLoaderState, pubkey::Pubkey},
|
||||
};
|
||||
|
||||
pub fn parse_bpf_upgradeable_loader(
|
||||
data: &[u8],
|
||||
) -> Result<BpfUpgradeableLoaderAccountType, ParseAccountError> {
|
||||
let account_state: UpgradeableLoaderState = deserialize(data).map_err(|_| {
|
||||
ParseAccountError::AccountNotParsable(ParsableAccount::BpfUpgradeableLoader)
|
||||
})?;
|
||||
let parsed_account = match account_state {
|
||||
UpgradeableLoaderState::Uninitialized => BpfUpgradeableLoaderAccountType::Uninitialized,
|
||||
UpgradeableLoaderState::Buffer { authority_address } => {
|
||||
let offset = if authority_address.is_some() {
|
||||
UpgradeableLoaderState::buffer_data_offset().unwrap()
|
||||
} else {
|
||||
// This case included for code completeness; in practice, a Buffer account will
|
||||
// always have authority_address.is_some()
|
||||
UpgradeableLoaderState::buffer_data_offset().unwrap()
|
||||
- serialized_size(&Pubkey::default()).unwrap() as usize
|
||||
};
|
||||
BpfUpgradeableLoaderAccountType::Buffer(UiBuffer {
|
||||
authority: authority_address.map(|pubkey| pubkey.to_string()),
|
||||
data: UiAccountData::Binary(
|
||||
base64::encode(&data[offset as usize..]),
|
||||
UiAccountEncoding::Base64,
|
||||
),
|
||||
})
|
||||
}
|
||||
UpgradeableLoaderState::Program {
|
||||
programdata_address,
|
||||
} => BpfUpgradeableLoaderAccountType::Program(UiProgram {
|
||||
program_data: programdata_address.to_string(),
|
||||
}),
|
||||
UpgradeableLoaderState::ProgramData {
|
||||
slot,
|
||||
upgrade_authority_address,
|
||||
} => {
|
||||
let offset = if upgrade_authority_address.is_some() {
|
||||
UpgradeableLoaderState::programdata_data_offset().unwrap()
|
||||
} else {
|
||||
UpgradeableLoaderState::programdata_data_offset().unwrap()
|
||||
- serialized_size(&Pubkey::default()).unwrap() as usize
|
||||
};
|
||||
BpfUpgradeableLoaderAccountType::ProgramData(UiProgramData {
|
||||
slot,
|
||||
authority: upgrade_authority_address.map(|pubkey| pubkey.to_string()),
|
||||
data: UiAccountData::Binary(
|
||||
base64::encode(&data[offset as usize..]),
|
||||
UiAccountEncoding::Base64,
|
||||
),
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(parsed_account)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
|
||||
pub enum BpfUpgradeableLoaderAccountType {
|
||||
Uninitialized,
|
||||
Buffer(UiBuffer),
|
||||
Program(UiProgram),
|
||||
ProgramData(UiProgramData),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiBuffer {
|
||||
pub authority: Option<String>,
|
||||
pub data: UiAccountData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiProgram {
|
||||
pub program_data: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiProgramData {
|
||||
pub slot: u64,
|
||||
pub authority: Option<String>,
|
||||
pub data: UiAccountData,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {super::*, bincode::serialize, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
#[test]
|
||||
fn test_parse_bpf_upgradeable_loader_accounts() {
|
||||
let bpf_loader_state = UpgradeableLoaderState::Uninitialized;
|
||||
let account_data = serialize(&bpf_loader_state).unwrap();
|
||||
assert_eq!(
|
||||
parse_bpf_upgradeable_loader(&account_data).unwrap(),
|
||||
BpfUpgradeableLoaderAccountType::Uninitialized
|
||||
);
|
||||
|
||||
let program = vec![7u8; 64]; // Arbitrary program data
|
||||
|
||||
let authority = Pubkey::new_unique();
|
||||
let bpf_loader_state = UpgradeableLoaderState::Buffer {
|
||||
authority_address: Some(authority),
|
||||
};
|
||||
let mut account_data = serialize(&bpf_loader_state).unwrap();
|
||||
account_data.extend_from_slice(&program);
|
||||
assert_eq!(
|
||||
parse_bpf_upgradeable_loader(&account_data).unwrap(),
|
||||
BpfUpgradeableLoaderAccountType::Buffer(UiBuffer {
|
||||
authority: Some(authority.to_string()),
|
||||
data: UiAccountData::Binary(base64::encode(&program), UiAccountEncoding::Base64),
|
||||
})
|
||||
);
|
||||
|
||||
// This case included for code completeness; in practice, a Buffer account will always have
|
||||
// authority_address.is_some()
|
||||
let bpf_loader_state = UpgradeableLoaderState::Buffer {
|
||||
authority_address: None,
|
||||
};
|
||||
let mut account_data = serialize(&bpf_loader_state).unwrap();
|
||||
account_data.extend_from_slice(&program);
|
||||
assert_eq!(
|
||||
parse_bpf_upgradeable_loader(&account_data).unwrap(),
|
||||
BpfUpgradeableLoaderAccountType::Buffer(UiBuffer {
|
||||
authority: None,
|
||||
data: UiAccountData::Binary(base64::encode(&program), UiAccountEncoding::Base64),
|
||||
})
|
||||
);
|
||||
|
||||
let programdata_address = Pubkey::new_unique();
|
||||
let bpf_loader_state = UpgradeableLoaderState::Program {
|
||||
programdata_address,
|
||||
};
|
||||
let account_data = serialize(&bpf_loader_state).unwrap();
|
||||
assert_eq!(
|
||||
parse_bpf_upgradeable_loader(&account_data).unwrap(),
|
||||
BpfUpgradeableLoaderAccountType::Program(UiProgram {
|
||||
program_data: programdata_address.to_string(),
|
||||
})
|
||||
);
|
||||
|
||||
let authority = Pubkey::new_unique();
|
||||
let slot = 42;
|
||||
let bpf_loader_state = UpgradeableLoaderState::ProgramData {
|
||||
slot,
|
||||
upgrade_authority_address: Some(authority),
|
||||
};
|
||||
let mut account_data = serialize(&bpf_loader_state).unwrap();
|
||||
account_data.extend_from_slice(&program);
|
||||
assert_eq!(
|
||||
parse_bpf_upgradeable_loader(&account_data).unwrap(),
|
||||
BpfUpgradeableLoaderAccountType::ProgramData(UiProgramData {
|
||||
slot,
|
||||
authority: Some(authority.to_string()),
|
||||
data: UiAccountData::Binary(base64::encode(&program), UiAccountEncoding::Base64),
|
||||
})
|
||||
);
|
||||
|
||||
let bpf_loader_state = UpgradeableLoaderState::ProgramData {
|
||||
slot,
|
||||
upgrade_authority_address: None,
|
||||
};
|
||||
let mut account_data = serialize(&bpf_loader_state).unwrap();
|
||||
account_data.extend_from_slice(&program);
|
||||
assert_eq!(
|
||||
parse_bpf_upgradeable_loader(&account_data).unwrap(),
|
||||
BpfUpgradeableLoaderAccountType::ProgramData(UiProgramData {
|
||||
slot,
|
||||
authority: None,
|
||||
data: UiAccountData::Binary(base64::encode(&program), UiAccountEncoding::Base64),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
parse_account_data::{ParsableAccount, ParseAccountError},
|
||||
validator_info,
|
||||
},
|
||||
bincode::deserialize,
|
||||
serde_json::Value,
|
||||
solana_config_program::{get_config_data, ConfigKeys},
|
||||
solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
stake::config::{self as stake_config, Config as StakeConfig},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn parse_config(data: &[u8], pubkey: &Pubkey) -> Result<ConfigAccountType, ParseAccountError> {
|
||||
let parsed_account = if pubkey == &stake_config::id() {
|
||||
get_config_data(data)
|
||||
.ok()
|
||||
.and_then(|data| deserialize::<StakeConfig>(data).ok())
|
||||
.map(|config| ConfigAccountType::StakeConfig(config.into()))
|
||||
} else {
|
||||
deserialize::<ConfigKeys>(data).ok().and_then(|key_list| {
|
||||
if !key_list.keys.is_empty() && key_list.keys[0].0 == validator_info::id() {
|
||||
parse_config_data::<String>(data, key_list.keys).and_then(|validator_info| {
|
||||
Some(ConfigAccountType::ValidatorInfo(UiConfig {
|
||||
keys: validator_info.keys,
|
||||
config_data: serde_json::from_str(&validator_info.config_data).ok()?,
|
||||
}))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
parsed_account.ok_or(ParseAccountError::AccountNotParsable(
|
||||
ParsableAccount::Config,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_config_data<T>(data: &[u8], keys: Vec<(Pubkey, bool)>) -> Option<UiConfig<T>>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let config_data: T = deserialize(get_config_data(data).ok()?).ok()?;
|
||||
let keys = keys
|
||||
.iter()
|
||||
.map(|key| UiConfigKey {
|
||||
pubkey: key.0.to_string(),
|
||||
signer: key.1,
|
||||
})
|
||||
.collect();
|
||||
Some(UiConfig { keys, config_data })
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
|
||||
pub enum ConfigAccountType {
|
||||
StakeConfig(UiStakeConfig),
|
||||
ValidatorInfo(UiConfig<Value>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiConfigKey {
|
||||
pub pubkey: String,
|
||||
pub signer: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiStakeConfig {
|
||||
pub warmup_cooldown_rate: f64,
|
||||
pub slash_penalty: u8,
|
||||
}
|
||||
|
||||
impl From<StakeConfig> for UiStakeConfig {
|
||||
fn from(config: StakeConfig) -> Self {
|
||||
Self {
|
||||
warmup_cooldown_rate: config.warmup_cooldown_rate,
|
||||
slash_penalty: config.slash_penalty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiConfig<T> {
|
||||
pub keys: Vec<UiConfigKey>,
|
||||
pub config_data: T,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {
|
||||
super::*, crate::validator_info::ValidatorInfo, serde_json::json,
|
||||
solana_config_program::create_config_account, solana_sdk::account::ReadableAccount,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_config() {
|
||||
let stake_config = StakeConfig {
|
||||
warmup_cooldown_rate: 0.25,
|
||||
slash_penalty: 50,
|
||||
};
|
||||
let stake_config_account = create_config_account(vec![], &stake_config, 10);
|
||||
assert_eq!(
|
||||
parse_config(stake_config_account.data(), &stake_config::id()).unwrap(),
|
||||
ConfigAccountType::StakeConfig(UiStakeConfig {
|
||||
warmup_cooldown_rate: 0.25,
|
||||
slash_penalty: 50,
|
||||
}),
|
||||
);
|
||||
|
||||
let validator_info = ValidatorInfo {
|
||||
info: serde_json::to_string(&json!({
|
||||
"name": "Solana",
|
||||
}))
|
||||
.unwrap(),
|
||||
};
|
||||
let info_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let validator_info_config_account = create_config_account(
|
||||
vec![(validator_info::id(), false), (info_pubkey, true)],
|
||||
&validator_info,
|
||||
10,
|
||||
);
|
||||
assert_eq!(
|
||||
parse_config(validator_info_config_account.data(), &info_pubkey).unwrap(),
|
||||
ConfigAccountType::ValidatorInfo(UiConfig {
|
||||
keys: vec![
|
||||
UiConfigKey {
|
||||
pubkey: validator_info::id().to_string(),
|
||||
signer: false,
|
||||
},
|
||||
UiConfigKey {
|
||||
pubkey: info_pubkey.to_string(),
|
||||
signer: true,
|
||||
}
|
||||
],
|
||||
config_data: serde_json::from_str(r#"{"name":"Solana"}"#).unwrap(),
|
||||
}),
|
||||
);
|
||||
|
||||
let bad_data = vec![0; 4];
|
||||
assert!(parse_config(&bad_data, &info_pubkey).is_err());
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
use {
|
||||
crate::{parse_account_data::ParseAccountError, UiFeeCalculator},
|
||||
solana_sdk::{
|
||||
instruction::InstructionError,
|
||||
nonce::{state::Versions, State},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn parse_nonce(data: &[u8]) -> Result<UiNonceState, ParseAccountError> {
|
||||
let nonce_state: Versions = bincode::deserialize(data)
|
||||
.map_err(|_| ParseAccountError::from(InstructionError::InvalidAccountData))?;
|
||||
let nonce_state = nonce_state.convert_to_current();
|
||||
match nonce_state {
|
||||
// This prevents parsing an allocated System-owned account with empty data of any non-zero
|
||||
// length as `uninitialized` nonce. An empty account of the wrong length can never be
|
||||
// initialized as a nonce account, and an empty account of the correct length may not be an
|
||||
// uninitialized nonce account, since it can be assigned to another program.
|
||||
State::Uninitialized => Err(ParseAccountError::from(
|
||||
InstructionError::InvalidAccountData,
|
||||
)),
|
||||
State::Initialized(data) => Ok(UiNonceState::Initialized(UiNonceData {
|
||||
authority: data.authority.to_string(),
|
||||
blockhash: data.blockhash.to_string(),
|
||||
fee_calculator: data.fee_calculator.into(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// A duplicate representation of NonceState for pretty JSON serialization
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
|
||||
pub enum UiNonceState {
|
||||
Uninitialized,
|
||||
Initialized(UiNonceData),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiNonceData {
|
||||
pub authority: String,
|
||||
pub blockhash: String,
|
||||
pub fee_calculator: UiFeeCalculator,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {
|
||||
super::*,
|
||||
solana_sdk::{
|
||||
hash::Hash,
|
||||
nonce::{
|
||||
state::{Data, Versions},
|
||||
State,
|
||||
},
|
||||
pubkey::Pubkey,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_nonce() {
|
||||
let nonce_data = Versions::new_current(State::Initialized(Data::default()));
|
||||
let nonce_account_data = bincode::serialize(&nonce_data).unwrap();
|
||||
assert_eq!(
|
||||
parse_nonce(&nonce_account_data).unwrap(),
|
||||
UiNonceState::Initialized(UiNonceData {
|
||||
authority: Pubkey::default().to_string(),
|
||||
blockhash: Hash::default().to_string(),
|
||||
fee_calculator: UiFeeCalculator {
|
||||
lamports_per_signature: 0.to_string(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let bad_data = vec![0; 4];
|
||||
assert!(parse_nonce(&bad_data).is_err());
|
||||
}
|
||||
}
|
@ -1,237 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
parse_account_data::{ParsableAccount, ParseAccountError},
|
||||
StringAmount,
|
||||
},
|
||||
bincode::deserialize,
|
||||
solana_sdk::{
|
||||
clock::{Epoch, UnixTimestamp},
|
||||
stake::state::{Authorized, Delegation, Lockup, Meta, Stake, StakeState},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn parse_stake(data: &[u8]) -> Result<StakeAccountType, ParseAccountError> {
|
||||
let stake_state: StakeState = deserialize(data)
|
||||
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Stake))?;
|
||||
let parsed_account = match stake_state {
|
||||
StakeState::Uninitialized => StakeAccountType::Uninitialized,
|
||||
StakeState::Initialized(meta) => StakeAccountType::Initialized(UiStakeAccount {
|
||||
meta: meta.into(),
|
||||
stake: None,
|
||||
}),
|
||||
StakeState::Stake(meta, stake) => StakeAccountType::Delegated(UiStakeAccount {
|
||||
meta: meta.into(),
|
||||
stake: Some(stake.into()),
|
||||
}),
|
||||
StakeState::RewardsPool => StakeAccountType::RewardsPool,
|
||||
};
|
||||
Ok(parsed_account)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
|
||||
pub enum StakeAccountType {
|
||||
Uninitialized,
|
||||
Initialized(UiStakeAccount),
|
||||
Delegated(UiStakeAccount),
|
||||
RewardsPool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiStakeAccount {
|
||||
pub meta: UiMeta,
|
||||
pub stake: Option<UiStake>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiMeta {
|
||||
pub rent_exempt_reserve: StringAmount,
|
||||
pub authorized: UiAuthorized,
|
||||
pub lockup: UiLockup,
|
||||
}
|
||||
|
||||
impl From<Meta> for UiMeta {
|
||||
fn from(meta: Meta) -> Self {
|
||||
Self {
|
||||
rent_exempt_reserve: meta.rent_exempt_reserve.to_string(),
|
||||
authorized: meta.authorized.into(),
|
||||
lockup: meta.lockup.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiLockup {
|
||||
pub unix_timestamp: UnixTimestamp,
|
||||
pub epoch: Epoch,
|
||||
pub custodian: String,
|
||||
}
|
||||
|
||||
impl From<Lockup> for UiLockup {
|
||||
fn from(lockup: Lockup) -> Self {
|
||||
Self {
|
||||
unix_timestamp: lockup.unix_timestamp,
|
||||
epoch: lockup.epoch,
|
||||
custodian: lockup.custodian.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiAuthorized {
|
||||
pub staker: String,
|
||||
pub withdrawer: String,
|
||||
}
|
||||
|
||||
impl From<Authorized> for UiAuthorized {
|
||||
fn from(authorized: Authorized) -> Self {
|
||||
Self {
|
||||
staker: authorized.staker.to_string(),
|
||||
withdrawer: authorized.withdrawer.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiStake {
|
||||
pub delegation: UiDelegation,
|
||||
pub credits_observed: u64,
|
||||
}
|
||||
|
||||
impl From<Stake> for UiStake {
|
||||
fn from(stake: Stake) -> Self {
|
||||
Self {
|
||||
delegation: stake.delegation.into(),
|
||||
credits_observed: stake.credits_observed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiDelegation {
|
||||
pub voter: String,
|
||||
pub stake: StringAmount,
|
||||
pub activation_epoch: StringAmount,
|
||||
pub deactivation_epoch: StringAmount,
|
||||
pub warmup_cooldown_rate: f64,
|
||||
}
|
||||
|
||||
impl From<Delegation> for UiDelegation {
|
||||
fn from(delegation: Delegation) -> Self {
|
||||
Self {
|
||||
voter: delegation.voter_pubkey.to_string(),
|
||||
stake: delegation.stake.to_string(),
|
||||
activation_epoch: delegation.activation_epoch.to_string(),
|
||||
deactivation_epoch: delegation.deactivation_epoch.to_string(),
|
||||
warmup_cooldown_rate: delegation.warmup_cooldown_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {super::*, bincode::serialize};
|
||||
|
||||
#[test]
|
||||
fn test_parse_stake() {
|
||||
let stake_state = StakeState::Uninitialized;
|
||||
let stake_data = serialize(&stake_state).unwrap();
|
||||
assert_eq!(
|
||||
parse_stake(&stake_data).unwrap(),
|
||||
StakeAccountType::Uninitialized
|
||||
);
|
||||
|
||||
let pubkey = solana_sdk::pubkey::new_rand();
|
||||
let custodian = solana_sdk::pubkey::new_rand();
|
||||
let authorized = Authorized::auto(&pubkey);
|
||||
let lockup = Lockup {
|
||||
unix_timestamp: 0,
|
||||
epoch: 1,
|
||||
custodian,
|
||||
};
|
||||
let meta = Meta {
|
||||
rent_exempt_reserve: 42,
|
||||
authorized,
|
||||
lockup,
|
||||
};
|
||||
|
||||
let stake_state = StakeState::Initialized(meta);
|
||||
let stake_data = serialize(&stake_state).unwrap();
|
||||
assert_eq!(
|
||||
parse_stake(&stake_data).unwrap(),
|
||||
StakeAccountType::Initialized(UiStakeAccount {
|
||||
meta: UiMeta {
|
||||
rent_exempt_reserve: 42.to_string(),
|
||||
authorized: UiAuthorized {
|
||||
staker: pubkey.to_string(),
|
||||
withdrawer: pubkey.to_string(),
|
||||
},
|
||||
lockup: UiLockup {
|
||||
unix_timestamp: 0,
|
||||
epoch: 1,
|
||||
custodian: custodian.to_string(),
|
||||
}
|
||||
},
|
||||
stake: None,
|
||||
})
|
||||
);
|
||||
|
||||
let voter_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let stake = Stake {
|
||||
delegation: Delegation {
|
||||
voter_pubkey,
|
||||
stake: 20,
|
||||
activation_epoch: 2,
|
||||
deactivation_epoch: std::u64::MAX,
|
||||
warmup_cooldown_rate: 0.25,
|
||||
},
|
||||
credits_observed: 10,
|
||||
};
|
||||
|
||||
let stake_state = StakeState::Stake(meta, stake);
|
||||
let stake_data = serialize(&stake_state).unwrap();
|
||||
assert_eq!(
|
||||
parse_stake(&stake_data).unwrap(),
|
||||
StakeAccountType::Delegated(UiStakeAccount {
|
||||
meta: UiMeta {
|
||||
rent_exempt_reserve: 42.to_string(),
|
||||
authorized: UiAuthorized {
|
||||
staker: pubkey.to_string(),
|
||||
withdrawer: pubkey.to_string(),
|
||||
},
|
||||
lockup: UiLockup {
|
||||
unix_timestamp: 0,
|
||||
epoch: 1,
|
||||
custodian: custodian.to_string(),
|
||||
}
|
||||
},
|
||||
stake: Some(UiStake {
|
||||
delegation: UiDelegation {
|
||||
voter: voter_pubkey.to_string(),
|
||||
stake: 20.to_string(),
|
||||
activation_epoch: 2.to_string(),
|
||||
deactivation_epoch: std::u64::MAX.to_string(),
|
||||
warmup_cooldown_rate: 0.25,
|
||||
},
|
||||
credits_observed: 10,
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
let stake_state = StakeState::RewardsPool;
|
||||
let stake_data = serialize(&stake_state).unwrap();
|
||||
assert_eq!(
|
||||
parse_stake(&stake_data).unwrap(),
|
||||
StakeAccountType::RewardsPool
|
||||
);
|
||||
|
||||
let bad_data = vec![1, 2, 3, 4];
|
||||
assert!(parse_stake(&bad_data).is_err());
|
||||
}
|
||||
}
|
@ -1,338 +0,0 @@
|
||||
#[allow(deprecated)]
|
||||
use solana_sdk::sysvar::{fees::Fees, recent_blockhashes::RecentBlockhashes};
|
||||
use {
|
||||
crate::{
|
||||
parse_account_data::{ParsableAccount, ParseAccountError},
|
||||
StringAmount, UiFeeCalculator,
|
||||
},
|
||||
bincode::deserialize,
|
||||
bv::BitVec,
|
||||
solana_sdk::{
|
||||
clock::{Clock, Epoch, Slot, UnixTimestamp},
|
||||
epoch_schedule::EpochSchedule,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
slot_hashes::SlotHashes,
|
||||
slot_history::{self, SlotHistory},
|
||||
stake_history::{StakeHistory, StakeHistoryEntry},
|
||||
sysvar::{self, rewards::Rewards},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn parse_sysvar(data: &[u8], pubkey: &Pubkey) -> Result<SysvarAccountType, ParseAccountError> {
|
||||
#[allow(deprecated)]
|
||||
let parsed_account = {
|
||||
if pubkey == &sysvar::clock::id() {
|
||||
deserialize::<Clock>(data)
|
||||
.ok()
|
||||
.map(|clock| SysvarAccountType::Clock(clock.into()))
|
||||
} else if pubkey == &sysvar::epoch_schedule::id() {
|
||||
deserialize(data).ok().map(SysvarAccountType::EpochSchedule)
|
||||
} else if pubkey == &sysvar::fees::id() {
|
||||
deserialize::<Fees>(data)
|
||||
.ok()
|
||||
.map(|fees| SysvarAccountType::Fees(fees.into()))
|
||||
} else if pubkey == &sysvar::recent_blockhashes::id() {
|
||||
deserialize::<RecentBlockhashes>(data)
|
||||
.ok()
|
||||
.map(|recent_blockhashes| {
|
||||
let recent_blockhashes = recent_blockhashes
|
||||
.iter()
|
||||
.map(|entry| UiRecentBlockhashesEntry {
|
||||
blockhash: entry.blockhash.to_string(),
|
||||
fee_calculator: entry.fee_calculator.clone().into(),
|
||||
})
|
||||
.collect();
|
||||
SysvarAccountType::RecentBlockhashes(recent_blockhashes)
|
||||
})
|
||||
} else if pubkey == &sysvar::rent::id() {
|
||||
deserialize::<Rent>(data)
|
||||
.ok()
|
||||
.map(|rent| SysvarAccountType::Rent(rent.into()))
|
||||
} else if pubkey == &sysvar::rewards::id() {
|
||||
deserialize::<Rewards>(data)
|
||||
.ok()
|
||||
.map(|rewards| SysvarAccountType::Rewards(rewards.into()))
|
||||
} else if pubkey == &sysvar::slot_hashes::id() {
|
||||
deserialize::<SlotHashes>(data).ok().map(|slot_hashes| {
|
||||
let slot_hashes = slot_hashes
|
||||
.iter()
|
||||
.map(|slot_hash| UiSlotHashEntry {
|
||||
slot: slot_hash.0,
|
||||
hash: slot_hash.1.to_string(),
|
||||
})
|
||||
.collect();
|
||||
SysvarAccountType::SlotHashes(slot_hashes)
|
||||
})
|
||||
} else if pubkey == &sysvar::slot_history::id() {
|
||||
deserialize::<SlotHistory>(data).ok().map(|slot_history| {
|
||||
SysvarAccountType::SlotHistory(UiSlotHistory {
|
||||
next_slot: slot_history.next_slot,
|
||||
bits: format!("{:?}", SlotHistoryBits(slot_history.bits)),
|
||||
})
|
||||
})
|
||||
} else if pubkey == &sysvar::stake_history::id() {
|
||||
deserialize::<StakeHistory>(data).ok().map(|stake_history| {
|
||||
let stake_history = stake_history
|
||||
.iter()
|
||||
.map(|entry| UiStakeHistoryEntry {
|
||||
epoch: entry.0,
|
||||
stake_history: entry.1.clone(),
|
||||
})
|
||||
.collect();
|
||||
SysvarAccountType::StakeHistory(stake_history)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
parsed_account.ok_or(ParseAccountError::AccountNotParsable(
|
||||
ParsableAccount::Sysvar,
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
|
||||
pub enum SysvarAccountType {
|
||||
Clock(UiClock),
|
||||
EpochSchedule(EpochSchedule),
|
||||
#[allow(deprecated)]
|
||||
Fees(UiFees),
|
||||
#[allow(deprecated)]
|
||||
RecentBlockhashes(Vec<UiRecentBlockhashesEntry>),
|
||||
Rent(UiRent),
|
||||
Rewards(UiRewards),
|
||||
SlotHashes(Vec<UiSlotHashEntry>),
|
||||
SlotHistory(UiSlotHistory),
|
||||
StakeHistory(Vec<UiStakeHistoryEntry>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiClock {
|
||||
pub slot: Slot,
|
||||
pub epoch: Epoch,
|
||||
pub epoch_start_timestamp: UnixTimestamp,
|
||||
pub leader_schedule_epoch: Epoch,
|
||||
pub unix_timestamp: UnixTimestamp,
|
||||
}
|
||||
|
||||
impl From<Clock> for UiClock {
|
||||
fn from(clock: Clock) -> Self {
|
||||
Self {
|
||||
slot: clock.slot,
|
||||
epoch: clock.epoch,
|
||||
epoch_start_timestamp: clock.epoch_start_timestamp,
|
||||
leader_schedule_epoch: clock.leader_schedule_epoch,
|
||||
unix_timestamp: clock.unix_timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiFees {
|
||||
pub fee_calculator: UiFeeCalculator,
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
impl From<Fees> for UiFees {
|
||||
fn from(fees: Fees) -> Self {
|
||||
Self {
|
||||
fee_calculator: fees.fee_calculator.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiRent {
|
||||
pub lamports_per_byte_year: StringAmount,
|
||||
pub exemption_threshold: f64,
|
||||
pub burn_percent: u8,
|
||||
}
|
||||
|
||||
impl From<Rent> for UiRent {
|
||||
fn from(rent: Rent) -> Self {
|
||||
Self {
|
||||
lamports_per_byte_year: rent.lamports_per_byte_year.to_string(),
|
||||
exemption_threshold: rent.exemption_threshold,
|
||||
burn_percent: rent.burn_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiRewards {
|
||||
pub validator_point_value: f64,
|
||||
}
|
||||
|
||||
impl From<Rewards> for UiRewards {
|
||||
fn from(rewards: Rewards) -> Self {
|
||||
Self {
|
||||
validator_point_value: rewards.validator_point_value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiRecentBlockhashesEntry {
|
||||
pub blockhash: String,
|
||||
pub fee_calculator: UiFeeCalculator,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiSlotHashEntry {
|
||||
pub slot: Slot,
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiSlotHistory {
|
||||
pub next_slot: Slot,
|
||||
pub bits: String,
|
||||
}
|
||||
|
||||
struct SlotHistoryBits(BitVec<u64>);
|
||||
|
||||
impl std::fmt::Debug for SlotHistoryBits {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for i in 0..slot_history::MAX_ENTRIES {
|
||||
if self.0.get(i) {
|
||||
write!(f, "1")?;
|
||||
} else {
|
||||
write!(f, "0")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiStakeHistoryEntry {
|
||||
pub epoch: Epoch,
|
||||
pub stake_history: StakeHistoryEntry,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[allow(deprecated)]
|
||||
use solana_sdk::sysvar::recent_blockhashes::IterItem;
|
||||
use {
|
||||
super::*,
|
||||
solana_sdk::{account::create_account_for_test, fee_calculator::FeeCalculator, hash::Hash},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_sysvars() {
|
||||
let hash = Hash::new(&[1; 32]);
|
||||
|
||||
let clock_sysvar = create_account_for_test(&Clock::default());
|
||||
assert_eq!(
|
||||
parse_sysvar(&clock_sysvar.data, &sysvar::clock::id()).unwrap(),
|
||||
SysvarAccountType::Clock(UiClock::default()),
|
||||
);
|
||||
|
||||
let epoch_schedule = EpochSchedule {
|
||||
slots_per_epoch: 12,
|
||||
leader_schedule_slot_offset: 0,
|
||||
warmup: false,
|
||||
first_normal_epoch: 1,
|
||||
first_normal_slot: 12,
|
||||
};
|
||||
let epoch_schedule_sysvar = create_account_for_test(&epoch_schedule);
|
||||
assert_eq!(
|
||||
parse_sysvar(&epoch_schedule_sysvar.data, &sysvar::epoch_schedule::id()).unwrap(),
|
||||
SysvarAccountType::EpochSchedule(epoch_schedule),
|
||||
);
|
||||
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
let fees_sysvar = create_account_for_test(&Fees::default());
|
||||
assert_eq!(
|
||||
parse_sysvar(&fees_sysvar.data, &sysvar::fees::id()).unwrap(),
|
||||
SysvarAccountType::Fees(UiFees::default()),
|
||||
);
|
||||
|
||||
let recent_blockhashes: RecentBlockhashes =
|
||||
vec![IterItem(0, &hash, 10)].into_iter().collect();
|
||||
let recent_blockhashes_sysvar = create_account_for_test(&recent_blockhashes);
|
||||
assert_eq!(
|
||||
parse_sysvar(
|
||||
&recent_blockhashes_sysvar.data,
|
||||
&sysvar::recent_blockhashes::id()
|
||||
)
|
||||
.unwrap(),
|
||||
SysvarAccountType::RecentBlockhashes(vec![UiRecentBlockhashesEntry {
|
||||
blockhash: hash.to_string(),
|
||||
fee_calculator: FeeCalculator::new(10).into(),
|
||||
}]),
|
||||
);
|
||||
}
|
||||
|
||||
let rent = Rent {
|
||||
lamports_per_byte_year: 10,
|
||||
exemption_threshold: 2.0,
|
||||
burn_percent: 5,
|
||||
};
|
||||
let rent_sysvar = create_account_for_test(&rent);
|
||||
assert_eq!(
|
||||
parse_sysvar(&rent_sysvar.data, &sysvar::rent::id()).unwrap(),
|
||||
SysvarAccountType::Rent(rent.into()),
|
||||
);
|
||||
|
||||
let rewards_sysvar = create_account_for_test(&Rewards::default());
|
||||
assert_eq!(
|
||||
parse_sysvar(&rewards_sysvar.data, &sysvar::rewards::id()).unwrap(),
|
||||
SysvarAccountType::Rewards(UiRewards::default()),
|
||||
);
|
||||
|
||||
let mut slot_hashes = SlotHashes::default();
|
||||
slot_hashes.add(1, hash);
|
||||
let slot_hashes_sysvar = create_account_for_test(&slot_hashes);
|
||||
assert_eq!(
|
||||
parse_sysvar(&slot_hashes_sysvar.data, &sysvar::slot_hashes::id()).unwrap(),
|
||||
SysvarAccountType::SlotHashes(vec![UiSlotHashEntry {
|
||||
slot: 1,
|
||||
hash: hash.to_string(),
|
||||
}]),
|
||||
);
|
||||
|
||||
let mut slot_history = SlotHistory::default();
|
||||
slot_history.add(42);
|
||||
let slot_history_sysvar = create_account_for_test(&slot_history);
|
||||
assert_eq!(
|
||||
parse_sysvar(&slot_history_sysvar.data, &sysvar::slot_history::id()).unwrap(),
|
||||
SysvarAccountType::SlotHistory(UiSlotHistory {
|
||||
next_slot: slot_history.next_slot,
|
||||
bits: format!("{:?}", SlotHistoryBits(slot_history.bits)),
|
||||
}),
|
||||
);
|
||||
|
||||
let mut stake_history = StakeHistory::default();
|
||||
let stake_history_entry = StakeHistoryEntry {
|
||||
effective: 10,
|
||||
activating: 2,
|
||||
deactivating: 3,
|
||||
};
|
||||
stake_history.add(1, stake_history_entry.clone());
|
||||
let stake_history_sysvar = create_account_for_test(&stake_history);
|
||||
assert_eq!(
|
||||
parse_sysvar(&stake_history_sysvar.data, &sysvar::stake_history::id()).unwrap(),
|
||||
SysvarAccountType::StakeHistory(vec![UiStakeHistoryEntry {
|
||||
epoch: 1,
|
||||
stake_history: stake_history_entry,
|
||||
}]),
|
||||
);
|
||||
|
||||
let bad_pubkey = solana_sdk::pubkey::new_rand();
|
||||
assert!(parse_sysvar(&stake_history_sysvar.data, &bad_pubkey).is_err());
|
||||
|
||||
let bad_data = vec![0; 4];
|
||||
assert!(parse_sysvar(&bad_data, &sysvar::stake_history::id()).is_err());
|
||||
}
|
||||
}
|
@ -1,461 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
parse_account_data::{ParsableAccount, ParseAccountError},
|
||||
StringAmount, StringDecimals,
|
||||
},
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
spl_token::{
|
||||
solana_program::{
|
||||
program_option::COption, program_pack::Pack, pubkey::Pubkey as SplTokenPubkey,
|
||||
},
|
||||
state::{Account, AccountState, Mint, Multisig},
|
||||
},
|
||||
std::str::FromStr,
|
||||
};
|
||||
|
||||
// A helper function to convert spl_token::id() as spl_sdk::pubkey::Pubkey to
|
||||
// solana_sdk::pubkey::Pubkey
|
||||
fn spl_token_id() -> Pubkey {
|
||||
Pubkey::new_from_array(spl_token::id().to_bytes())
|
||||
}
|
||||
|
||||
// Returns all known SPL Token program ids
|
||||
pub fn spl_token_ids() -> Vec<Pubkey> {
|
||||
vec![spl_token_id()]
|
||||
}
|
||||
|
||||
// Check if the provided program id as a known SPL Token program id
|
||||
pub fn is_known_spl_token_id(program_id: &Pubkey) -> bool {
|
||||
*program_id == spl_token_id()
|
||||
}
|
||||
|
||||
// A helper function to convert spl_token::native_mint::id() as spl_sdk::pubkey::Pubkey to
|
||||
// solana_sdk::pubkey::Pubkey
|
||||
pub fn spl_token_native_mint() -> Pubkey {
|
||||
Pubkey::new_from_array(spl_token::native_mint::id().to_bytes())
|
||||
}
|
||||
|
||||
// The program id of the `spl_token_native_mint` account
|
||||
pub fn spl_token_native_mint_program_id() -> Pubkey {
|
||||
spl_token_id()
|
||||
}
|
||||
|
||||
// A helper function to convert a solana_sdk::pubkey::Pubkey to spl_sdk::pubkey::Pubkey
|
||||
pub fn spl_token_pubkey(pubkey: &Pubkey) -> SplTokenPubkey {
|
||||
SplTokenPubkey::new_from_array(pubkey.to_bytes())
|
||||
}
|
||||
|
||||
// A helper function to convert a spl_sdk::pubkey::Pubkey to solana_sdk::pubkey::Pubkey
|
||||
pub fn pubkey_from_spl_token(pubkey: &SplTokenPubkey) -> Pubkey {
|
||||
Pubkey::new_from_array(pubkey.to_bytes())
|
||||
}
|
||||
|
||||
pub fn parse_token(
|
||||
data: &[u8],
|
||||
mint_decimals: Option<u8>,
|
||||
) -> Result<TokenAccountType, ParseAccountError> {
|
||||
if data.len() == Account::get_packed_len() {
|
||||
let account = Account::unpack(data)
|
||||
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
|
||||
let decimals = mint_decimals.ok_or_else(|| {
|
||||
ParseAccountError::AdditionalDataMissing(
|
||||
"no mint_decimals provided to parse spl-token account".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(TokenAccountType::Account(UiTokenAccount {
|
||||
mint: account.mint.to_string(),
|
||||
owner: account.owner.to_string(),
|
||||
token_amount: token_amount_to_ui_amount(account.amount, decimals),
|
||||
delegate: match account.delegate {
|
||||
COption::Some(pubkey) => Some(pubkey.to_string()),
|
||||
COption::None => None,
|
||||
},
|
||||
state: account.state.into(),
|
||||
is_native: account.is_native(),
|
||||
rent_exempt_reserve: match account.is_native {
|
||||
COption::Some(reserve) => Some(token_amount_to_ui_amount(reserve, decimals)),
|
||||
COption::None => None,
|
||||
},
|
||||
delegated_amount: if account.delegate.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(token_amount_to_ui_amount(
|
||||
account.delegated_amount,
|
||||
decimals,
|
||||
))
|
||||
},
|
||||
close_authority: match account.close_authority {
|
||||
COption::Some(pubkey) => Some(pubkey.to_string()),
|
||||
COption::None => None,
|
||||
},
|
||||
}))
|
||||
} else if data.len() == Mint::get_packed_len() {
|
||||
let mint = Mint::unpack(data)
|
||||
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
|
||||
Ok(TokenAccountType::Mint(UiMint {
|
||||
mint_authority: match mint.mint_authority {
|
||||
COption::Some(pubkey) => Some(pubkey.to_string()),
|
||||
COption::None => None,
|
||||
},
|
||||
supply: mint.supply.to_string(),
|
||||
decimals: mint.decimals,
|
||||
is_initialized: mint.is_initialized,
|
||||
freeze_authority: match mint.freeze_authority {
|
||||
COption::Some(pubkey) => Some(pubkey.to_string()),
|
||||
COption::None => None,
|
||||
},
|
||||
}))
|
||||
} else if data.len() == Multisig::get_packed_len() {
|
||||
let multisig = Multisig::unpack(data)
|
||||
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
|
||||
Ok(TokenAccountType::Multisig(UiMultisig {
|
||||
num_required_signers: multisig.m,
|
||||
num_valid_signers: multisig.n,
|
||||
is_initialized: multisig.is_initialized,
|
||||
signers: multisig
|
||||
.signers
|
||||
.iter()
|
||||
.filter_map(|pubkey| {
|
||||
if pubkey != &SplTokenPubkey::default() {
|
||||
Some(pubkey.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
} else {
|
||||
Err(ParseAccountError::AccountNotParsable(
|
||||
ParsableAccount::SplToken,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TokenAccountType {
|
||||
Account(UiTokenAccount),
|
||||
Mint(UiMint),
|
||||
Multisig(UiMultisig),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiTokenAccount {
|
||||
pub mint: String,
|
||||
pub owner: String,
|
||||
pub token_amount: UiTokenAmount,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub delegate: Option<String>,
|
||||
pub state: UiAccountState,
|
||||
pub is_native: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rent_exempt_reserve: Option<UiTokenAmount>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub delegated_amount: Option<UiTokenAmount>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub close_authority: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum UiAccountState {
|
||||
Uninitialized,
|
||||
Initialized,
|
||||
Frozen,
|
||||
}
|
||||
|
||||
impl From<AccountState> for UiAccountState {
|
||||
fn from(state: AccountState) -> Self {
|
||||
match state {
|
||||
AccountState::Uninitialized => UiAccountState::Uninitialized,
|
||||
AccountState::Initialized => UiAccountState::Initialized,
|
||||
AccountState::Frozen => UiAccountState::Frozen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn real_number_string(amount: u64, decimals: u8) -> StringDecimals {
|
||||
let decimals = decimals as usize;
|
||||
if decimals > 0 {
|
||||
// Left-pad zeros to decimals + 1, so we at least have an integer zero
|
||||
let mut s = format!("{:01$}", amount, decimals + 1);
|
||||
// Add the decimal point (Sorry, "," locales!)
|
||||
s.insert(s.len() - decimals, '.');
|
||||
s
|
||||
} else {
|
||||
amount.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn real_number_string_trimmed(amount: u64, decimals: u8) -> StringDecimals {
|
||||
let mut s = real_number_string(amount, decimals);
|
||||
if decimals > 0 {
|
||||
let zeros_trimmed = s.trim_end_matches('0');
|
||||
s = zeros_trimmed.trim_end_matches('.').to_string();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiTokenAmount {
|
||||
pub ui_amount: Option<f64>,
|
||||
pub decimals: u8,
|
||||
pub amount: StringAmount,
|
||||
pub ui_amount_string: StringDecimals,
|
||||
}
|
||||
|
||||
impl UiTokenAmount {
|
||||
pub fn real_number_string(&self) -> String {
|
||||
real_number_string(
|
||||
u64::from_str(&self.amount).unwrap_or_default(),
|
||||
self.decimals as u8,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn real_number_string_trimmed(&self) -> String {
|
||||
if !self.ui_amount_string.is_empty() {
|
||||
self.ui_amount_string.clone()
|
||||
} else {
|
||||
real_number_string_trimmed(
|
||||
u64::from_str(&self.amount).unwrap_or_default(),
|
||||
self.decimals as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_amount_to_ui_amount(amount: u64, decimals: u8) -> UiTokenAmount {
|
||||
let amount_decimals = 10_usize
|
||||
.checked_pow(decimals as u32)
|
||||
.map(|dividend| amount as f64 / dividend as f64);
|
||||
UiTokenAmount {
|
||||
ui_amount: amount_decimals,
|
||||
decimals,
|
||||
amount: amount.to_string(),
|
||||
ui_amount_string: real_number_string_trimmed(amount, decimals),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiMint {
|
||||
pub mint_authority: Option<String>,
|
||||
pub supply: StringAmount,
|
||||
pub decimals: u8,
|
||||
pub is_initialized: bool,
|
||||
pub freeze_authority: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiMultisig {
|
||||
pub num_required_signers: u8,
|
||||
pub num_valid_signers: u8,
|
||||
pub is_initialized: bool,
|
||||
pub signers: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn get_token_account_mint(data: &[u8]) -> Option<Pubkey> {
|
||||
if data.len() == Account::get_packed_len() {
|
||||
Some(Pubkey::new(&data[0..32]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_token() {
|
||||
let mint_pubkey = SplTokenPubkey::new(&[2; 32]);
|
||||
let owner_pubkey = SplTokenPubkey::new(&[3; 32]);
|
||||
let mut account_data = vec![0; Account::get_packed_len()];
|
||||
let mut account = Account::unpack_unchecked(&account_data).unwrap();
|
||||
account.mint = mint_pubkey;
|
||||
account.owner = owner_pubkey;
|
||||
account.amount = 42;
|
||||
account.state = AccountState::Initialized;
|
||||
account.is_native = COption::None;
|
||||
account.close_authority = COption::Some(owner_pubkey);
|
||||
Account::pack(account, &mut account_data).unwrap();
|
||||
|
||||
assert!(parse_token(&account_data, None).is_err());
|
||||
assert_eq!(
|
||||
parse_token(&account_data, Some(2)).unwrap(),
|
||||
TokenAccountType::Account(UiTokenAccount {
|
||||
mint: mint_pubkey.to_string(),
|
||||
owner: owner_pubkey.to_string(),
|
||||
token_amount: UiTokenAmount {
|
||||
ui_amount: Some(0.42),
|
||||
decimals: 2,
|
||||
amount: "42".to_string(),
|
||||
ui_amount_string: "0.42".to_string()
|
||||
},
|
||||
delegate: None,
|
||||
state: UiAccountState::Initialized,
|
||||
is_native: false,
|
||||
rent_exempt_reserve: None,
|
||||
delegated_amount: None,
|
||||
close_authority: Some(owner_pubkey.to_string()),
|
||||
}),
|
||||
);
|
||||
|
||||
let mut mint_data = vec![0; Mint::get_packed_len()];
|
||||
let mut mint = Mint::unpack_unchecked(&mint_data).unwrap();
|
||||
mint.mint_authority = COption::Some(owner_pubkey);
|
||||
mint.supply = 42;
|
||||
mint.decimals = 3;
|
||||
mint.is_initialized = true;
|
||||
mint.freeze_authority = COption::Some(owner_pubkey);
|
||||
Mint::pack(mint, &mut mint_data).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parse_token(&mint_data, None).unwrap(),
|
||||
TokenAccountType::Mint(UiMint {
|
||||
mint_authority: Some(owner_pubkey.to_string()),
|
||||
supply: 42.to_string(),
|
||||
decimals: 3,
|
||||
is_initialized: true,
|
||||
freeze_authority: Some(owner_pubkey.to_string()),
|
||||
}),
|
||||
);
|
||||
|
||||
let signer1 = SplTokenPubkey::new(&[1; 32]);
|
||||
let signer2 = SplTokenPubkey::new(&[2; 32]);
|
||||
let signer3 = SplTokenPubkey::new(&[3; 32]);
|
||||
let mut multisig_data = vec![0; Multisig::get_packed_len()];
|
||||
let mut signers = [SplTokenPubkey::default(); 11];
|
||||
signers[0] = signer1;
|
||||
signers[1] = signer2;
|
||||
signers[2] = signer3;
|
||||
let mut multisig = Multisig::unpack_unchecked(&multisig_data).unwrap();
|
||||
multisig.m = 2;
|
||||
multisig.n = 3;
|
||||
multisig.is_initialized = true;
|
||||
multisig.signers = signers;
|
||||
Multisig::pack(multisig, &mut multisig_data).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parse_token(&multisig_data, None).unwrap(),
|
||||
TokenAccountType::Multisig(UiMultisig {
|
||||
num_required_signers: 2,
|
||||
num_valid_signers: 3,
|
||||
is_initialized: true,
|
||||
signers: vec![
|
||||
signer1.to_string(),
|
||||
signer2.to_string(),
|
||||
signer3.to_string()
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
let bad_data = vec![0; 4];
|
||||
assert!(parse_token(&bad_data, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_token_account_mint() {
|
||||
let mint_pubkey = SplTokenPubkey::new(&[2; 32]);
|
||||
let mut account_data = vec![0; Account::get_packed_len()];
|
||||
let mut account = Account::unpack_unchecked(&account_data).unwrap();
|
||||
account.mint = mint_pubkey;
|
||||
Account::pack(account, &mut account_data).unwrap();
|
||||
|
||||
let expected_mint_pubkey = Pubkey::new(&[2; 32]);
|
||||
assert_eq!(
|
||||
get_token_account_mint(&account_data),
|
||||
Some(expected_mint_pubkey)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_token_amount_real_string() {
|
||||
assert_eq!(&real_number_string(1, 0), "1");
|
||||
assert_eq!(&real_number_string_trimmed(1, 0), "1");
|
||||
let token_amount = token_amount_to_ui_amount(1, 0);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(1, 0)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, Some(1.0));
|
||||
assert_eq!(&real_number_string(10, 0), "10");
|
||||
assert_eq!(&real_number_string_trimmed(10, 0), "10");
|
||||
let token_amount = token_amount_to_ui_amount(10, 0);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(10, 0)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, Some(10.0));
|
||||
assert_eq!(&real_number_string(1, 9), "0.000000001");
|
||||
assert_eq!(&real_number_string_trimmed(1, 9), "0.000000001");
|
||||
let token_amount = token_amount_to_ui_amount(1, 9);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(1, 9)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, Some(0.000000001));
|
||||
assert_eq!(&real_number_string(1_000_000_000, 9), "1.000000000");
|
||||
assert_eq!(&real_number_string_trimmed(1_000_000_000, 9), "1");
|
||||
let token_amount = token_amount_to_ui_amount(1_000_000_000, 9);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(1_000_000_000, 9)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, Some(1.0));
|
||||
assert_eq!(&real_number_string(1_234_567_890, 3), "1234567.890");
|
||||
assert_eq!(&real_number_string_trimmed(1_234_567_890, 3), "1234567.89");
|
||||
let token_amount = token_amount_to_ui_amount(1_234_567_890, 3);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(1_234_567_890, 3)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, Some(1234567.89));
|
||||
assert_eq!(
|
||||
&real_number_string(1_234_567_890, 25),
|
||||
"0.0000000000000001234567890"
|
||||
);
|
||||
assert_eq!(
|
||||
&real_number_string_trimmed(1_234_567_890, 25),
|
||||
"0.000000000000000123456789"
|
||||
);
|
||||
let token_amount = token_amount_to_ui_amount(1_234_567_890, 20);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(1_234_567_890, 20)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_token_amount_real_string_zero() {
|
||||
assert_eq!(&real_number_string(0, 0), "0");
|
||||
assert_eq!(&real_number_string_trimmed(0, 0), "0");
|
||||
let token_amount = token_amount_to_ui_amount(0, 0);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(0, 0)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, Some(0.0));
|
||||
assert_eq!(&real_number_string(0, 9), "0.000000000");
|
||||
assert_eq!(&real_number_string_trimmed(0, 9), "0");
|
||||
let token_amount = token_amount_to_ui_amount(0, 9);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(0, 9)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, Some(0.0));
|
||||
assert_eq!(&real_number_string(0, 25), "0.0000000000000000000000000");
|
||||
assert_eq!(&real_number_string_trimmed(0, 25), "0");
|
||||
let token_amount = token_amount_to_ui_amount(0, 20);
|
||||
assert_eq!(
|
||||
token_amount.ui_amount_string,
|
||||
real_number_string_trimmed(0, 20)
|
||||
);
|
||||
assert_eq!(token_amount.ui_amount, None);
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
use {
|
||||
crate::{parse_account_data::ParseAccountError, StringAmount},
|
||||
solana_sdk::{
|
||||
clock::{Epoch, Slot},
|
||||
pubkey::Pubkey,
|
||||
},
|
||||
solana_vote_program::vote_state::{BlockTimestamp, Lockout, VoteState},
|
||||
};
|
||||
|
||||
pub fn parse_vote(data: &[u8]) -> Result<VoteAccountType, ParseAccountError> {
|
||||
let mut vote_state = VoteState::deserialize(data).map_err(ParseAccountError::from)?;
|
||||
let epoch_credits = vote_state
|
||||
.epoch_credits()
|
||||
.iter()
|
||||
.map(|(epoch, credits, previous_credits)| UiEpochCredits {
|
||||
epoch: *epoch,
|
||||
credits: credits.to_string(),
|
||||
previous_credits: previous_credits.to_string(),
|
||||
})
|
||||
.collect();
|
||||
let votes = vote_state
|
||||
.votes
|
||||
.iter()
|
||||
.map(|lockout| UiLockout {
|
||||
slot: lockout.slot,
|
||||
confirmation_count: lockout.confirmation_count,
|
||||
})
|
||||
.collect();
|
||||
let authorized_voters = vote_state
|
||||
.authorized_voters()
|
||||
.iter()
|
||||
.map(|(epoch, authorized_voter)| UiAuthorizedVoters {
|
||||
epoch: *epoch,
|
||||
authorized_voter: authorized_voter.to_string(),
|
||||
})
|
||||
.collect();
|
||||
let prior_voters = vote_state
|
||||
.prior_voters()
|
||||
.buf()
|
||||
.iter()
|
||||
.filter(|(pubkey, _, _)| pubkey != &Pubkey::default())
|
||||
.map(
|
||||
|(authorized_pubkey, epoch_of_last_authorized_switch, target_epoch)| UiPriorVoters {
|
||||
authorized_pubkey: authorized_pubkey.to_string(),
|
||||
epoch_of_last_authorized_switch: *epoch_of_last_authorized_switch,
|
||||
target_epoch: *target_epoch,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
Ok(VoteAccountType::Vote(UiVoteState {
|
||||
node_pubkey: vote_state.node_pubkey.to_string(),
|
||||
authorized_withdrawer: vote_state.authorized_withdrawer.to_string(),
|
||||
commission: vote_state.commission,
|
||||
votes,
|
||||
root_slot: vote_state.root_slot,
|
||||
authorized_voters,
|
||||
prior_voters,
|
||||
epoch_credits,
|
||||
last_timestamp: vote_state.last_timestamp,
|
||||
}))
|
||||
}
|
||||
|
||||
/// A wrapper enum for consistency across programs
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "info")]
|
||||
pub enum VoteAccountType {
|
||||
Vote(UiVoteState),
|
||||
}
|
||||
|
||||
/// A duplicate representation of VoteState for pretty JSON serialization
|
||||
#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UiVoteState {
|
||||
node_pubkey: String,
|
||||
authorized_withdrawer: String,
|
||||
commission: u8,
|
||||
votes: Vec<UiLockout>,
|
||||
root_slot: Option<Slot>,
|
||||
authorized_voters: Vec<UiAuthorizedVoters>,
|
||||
prior_voters: Vec<UiPriorVoters>,
|
||||
epoch_credits: Vec<UiEpochCredits>,
|
||||
last_timestamp: BlockTimestamp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UiLockout {
|
||||
slot: Slot,
|
||||
confirmation_count: u32,
|
||||
}
|
||||
|
||||
impl From<&Lockout> for UiLockout {
|
||||
fn from(lockout: &Lockout) -> Self {
|
||||
Self {
|
||||
slot: lockout.slot,
|
||||
confirmation_count: lockout.confirmation_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UiAuthorizedVoters {
|
||||
epoch: Epoch,
|
||||
authorized_voter: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UiPriorVoters {
|
||||
authorized_pubkey: String,
|
||||
epoch_of_last_authorized_switch: Epoch,
|
||||
target_epoch: Epoch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UiEpochCredits {
|
||||
epoch: Epoch,
|
||||
credits: StringAmount,
|
||||
previous_credits: StringAmount,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {super::*, solana_vote_program::vote_state::VoteStateVersions};
|
||||
|
||||
#[test]
|
||||
fn test_parse_vote() {
|
||||
let vote_state = VoteState::default();
|
||||
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
|
||||
let versioned = VoteStateVersions::new_current(vote_state);
|
||||
VoteState::serialize(&versioned, &mut vote_account_data).unwrap();
|
||||
let expected_vote_state = UiVoteState {
|
||||
node_pubkey: Pubkey::default().to_string(),
|
||||
authorized_withdrawer: Pubkey::default().to_string(),
|
||||
..UiVoteState::default()
|
||||
};
|
||||
assert_eq!(
|
||||
parse_vote(&vote_account_data).unwrap(),
|
||||
VoteAccountType::Vote(expected_vote_state)
|
||||
);
|
||||
|
||||
let bad_data = vec![0; 4];
|
||||
assert!(parse_vote(&bad_data).is_err());
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
use solana_config_program::ConfigState;
|
||||
|
||||
pub const MAX_SHORT_FIELD_LENGTH: usize = 70;
|
||||
pub const MAX_LONG_FIELD_LENGTH: usize = 300;
|
||||
pub const MAX_VALIDATOR_INFO: u64 = 576;
|
||||
|
||||
solana_sdk::declare_id!("Va1idator1nfo111111111111111111111111111111");
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize, Default)]
|
||||
pub struct ValidatorInfo {
|
||||
pub info: String,
|
||||
}
|
||||
|
||||
impl ConfigState for ValidatorInfo {
|
||||
fn max_space() -> u64 {
|
||||
MAX_VALIDATOR_INFO
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
name = "solana-accounts-bench"
|
||||
version = "1.11.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.1"
|
||||
log = "0.4.14"
|
||||
rayon = "1.5.1"
|
||||
solana-logger = { path = "../logger", version = "=1.11.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.11.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-version = { path = "../version", version = "=1.11.0" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,165 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
use {
|
||||
clap::{crate_description, crate_name, value_t, App, Arg},
|
||||
rayon::prelude::*,
|
||||
solana_measure::measure::Measure,
|
||||
solana_runtime::{
|
||||
accounts::{
|
||||
test_utils::{create_test_accounts, update_accounts_bench},
|
||||
Accounts,
|
||||
},
|
||||
accounts_db::AccountShrinkThreshold,
|
||||
accounts_index::AccountSecondaryIndexes,
|
||||
ancestors::Ancestors,
|
||||
rent_collector::RentCollector,
|
||||
},
|
||||
solana_sdk::{
|
||||
genesis_config::ClusterType, pubkey::Pubkey, sysvar::epoch_schedule::EpochSchedule,
|
||||
},
|
||||
std::{env, fs, path::PathBuf},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup();
|
||||
|
||||
let matches = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.version(solana_version::version!())
|
||||
.arg(
|
||||
Arg::with_name("num_slots")
|
||||
.long("num_slots")
|
||||
.takes_value(true)
|
||||
.value_name("SLOTS")
|
||||
.help("Number of slots to store to."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("num_accounts")
|
||||
.long("num_accounts")
|
||||
.takes_value(true)
|
||||
.value_name("NUM_ACCOUNTS")
|
||||
.help("Total number of accounts"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("iterations")
|
||||
.long("iterations")
|
||||
.takes_value(true)
|
||||
.value_name("ITERATIONS")
|
||||
.help("Number of bench iterations"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("clean")
|
||||
.long("clean")
|
||||
.takes_value(false)
|
||||
.help("Run clean"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let num_slots = value_t!(matches, "num_slots", usize).unwrap_or(4);
|
||||
let num_accounts = value_t!(matches, "num_accounts", usize).unwrap_or(10_000);
|
||||
let iterations = value_t!(matches, "iterations", usize).unwrap_or(20);
|
||||
let clean = matches.is_present("clean");
|
||||
println!("clean: {:?}", clean);
|
||||
|
||||
let path = PathBuf::from(env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_owned()))
|
||||
.join("accounts-bench");
|
||||
println!("cleaning file system: {:?}", path);
|
||||
if fs::remove_dir_all(path.clone()).is_err() {
|
||||
println!("Warning: Couldn't remove {:?}", path);
|
||||
}
|
||||
let accounts = Accounts::new_with_config_for_benches(
|
||||
vec![path],
|
||||
&ClusterType::Testnet,
|
||||
AccountSecondaryIndexes::default(),
|
||||
false,
|
||||
AccountShrinkThreshold::default(),
|
||||
);
|
||||
println!("Creating {} accounts", num_accounts);
|
||||
let mut create_time = Measure::start("create accounts");
|
||||
let pubkeys: Vec<_> = (0..num_slots)
|
||||
.into_par_iter()
|
||||
.map(|slot| {
|
||||
let mut pubkeys: Vec<Pubkey> = vec![];
|
||||
create_test_accounts(
|
||||
&accounts,
|
||||
&mut pubkeys,
|
||||
num_accounts / num_slots,
|
||||
slot as u64,
|
||||
);
|
||||
pubkeys
|
||||
})
|
||||
.collect();
|
||||
let pubkeys: Vec<_> = pubkeys.into_iter().flatten().collect();
|
||||
create_time.stop();
|
||||
println!(
|
||||
"created {} accounts in {} slots {}",
|
||||
(num_accounts / num_slots) * num_slots,
|
||||
num_slots,
|
||||
create_time
|
||||
);
|
||||
let mut ancestors = Vec::with_capacity(num_slots);
|
||||
ancestors.push(0);
|
||||
for i in 1..num_slots {
|
||||
ancestors.push(i as u64);
|
||||
accounts.add_root(i as u64);
|
||||
}
|
||||
let ancestors = Ancestors::from(ancestors);
|
||||
let mut elapsed = vec![0; iterations];
|
||||
let mut elapsed_store = vec![0; iterations];
|
||||
for x in 0..iterations {
|
||||
if clean {
|
||||
let mut time = Measure::start("clean");
|
||||
accounts.accounts_db.clean_accounts(None, false, None);
|
||||
time.stop();
|
||||
println!("{}", time);
|
||||
for slot in 0..num_slots {
|
||||
update_accounts_bench(&accounts, &pubkeys, ((x + 1) * num_slots + slot) as u64);
|
||||
accounts.add_root((x * num_slots + slot) as u64);
|
||||
}
|
||||
} else {
|
||||
let mut pubkeys: Vec<Pubkey> = vec![];
|
||||
let mut time = Measure::start("hash");
|
||||
let results = accounts.accounts_db.update_accounts_hash(
|
||||
0,
|
||||
&ancestors,
|
||||
&EpochSchedule::default(),
|
||||
&RentCollector::default(),
|
||||
);
|
||||
time.stop();
|
||||
let mut time_store = Measure::start("hash using store");
|
||||
let results_store = accounts.accounts_db.update_accounts_hash_with_index_option(
|
||||
false,
|
||||
false,
|
||||
solana_sdk::clock::Slot::default(),
|
||||
&ancestors,
|
||||
None,
|
||||
false,
|
||||
&EpochSchedule::default(),
|
||||
&RentCollector::default(),
|
||||
false,
|
||||
);
|
||||
time_store.stop();
|
||||
if results != results_store {
|
||||
error!("results different: \n{:?}\n{:?}", results, results_store);
|
||||
}
|
||||
println!(
|
||||
"hash,{},{},{},{}%",
|
||||
results.0,
|
||||
time,
|
||||
time_store,
|
||||
(time_store.as_us() as f64 / time.as_us() as f64 * 100.0f64) as u32
|
||||
);
|
||||
create_test_accounts(&accounts, &mut pubkeys, 1, 0);
|
||||
elapsed[x] = time.as_us();
|
||||
elapsed_store[x] = time_store.as_us();
|
||||
}
|
||||
}
|
||||
|
||||
for x in elapsed {
|
||||
info!("update_accounts_hash(us),{}", x);
|
||||
}
|
||||
for x in elapsed_store {
|
||||
info!("calculate_accounts_hash_without_index(us),{}", x);
|
||||
}
|
||||
}
|
1
accounts-cluster-bench/.gitignore
vendored
1
accounts-cluster-bench/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/farf/
|
@ -1,37 +0,0 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
name = "solana-accounts-cluster-bench"
|
||||
version = "1.11.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.1"
|
||||
log = "0.4.14"
|
||||
rand = "0.7.0"
|
||||
rayon = "1.5.1"
|
||||
solana-account-decoder = { path = "../account-decoder", version = "=1.11.0" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.11.0" }
|
||||
solana-client = { path = "../client", version = "=1.11.0" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.11.0" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.11.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.11.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.11.0" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.11.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.11.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.11.0" }
|
||||
solana-version = { path = "../version", version = "=1.11.0" }
|
||||
spl-token = { version = "=3.2.0", features = ["no-entrypoint"] }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-core = { path = "../core", version = "=1.11.0" }
|
||||
solana-local-cluster = { path = "../local-cluster", version = "=1.11.0" }
|
||||
solana-test-validator = { path = "../test-validator", version = "=1.11.0" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,812 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
use {
|
||||
clap::{crate_description, crate_name, value_t, values_t_or_exit, App, Arg},
|
||||
log::*,
|
||||
rand::{thread_rng, Rng},
|
||||
rayon::prelude::*,
|
||||
solana_account_decoder::parse_token::spl_token_pubkey,
|
||||
solana_clap_utils::input_parsers::pubkey_of,
|
||||
solana_client::{rpc_client::RpcClient, transaction_executor::TransactionExecutor},
|
||||
solana_faucet::faucet::{request_airdrop_transaction, FAUCET_PORT},
|
||||
solana_gossip::gossip_service::discover,
|
||||
solana_runtime::inline_spl_token,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
rpc_port::DEFAULT_RPC_PORT,
|
||||
signature::{read_keypair_file, Keypair, Signer},
|
||||
system_instruction, system_program,
|
||||
transaction::Transaction,
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
solana_transaction_status::parse_token::spl_token_instruction,
|
||||
std::{
|
||||
cmp::min,
|
||||
net::SocketAddr,
|
||||
process::exit,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn airdrop_lamports(
|
||||
client: &RpcClient,
|
||||
faucet_addr: &SocketAddr,
|
||||
id: &Keypair,
|
||||
desired_balance: u64,
|
||||
) -> bool {
|
||||
let starting_balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||
info!("starting balance {}", starting_balance);
|
||||
|
||||
if starting_balance < desired_balance {
|
||||
let airdrop_amount = desired_balance - starting_balance;
|
||||
info!(
|
||||
"Airdropping {:?} lamports from {} for {}",
|
||||
airdrop_amount,
|
||||
faucet_addr,
|
||||
id.pubkey(),
|
||||
);
|
||||
|
||||
let blockhash = client.get_latest_blockhash().unwrap();
|
||||
match request_airdrop_transaction(faucet_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
||||
Ok(transaction) => {
|
||||
let mut tries = 0;
|
||||
loop {
|
||||
tries += 1;
|
||||
let result = client.send_and_confirm_transaction(&transaction);
|
||||
|
||||
if result.is_ok() {
|
||||
break;
|
||||
}
|
||||
if tries >= 5 {
|
||||
panic!(
|
||||
"Error requesting airdrop: to addr: {:?} amount: {} {:?}",
|
||||
faucet_addr, airdrop_amount, result
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
panic!(
|
||||
"Error requesting airdrop: {:?} to addr: {:?} amount: {}",
|
||||
err, faucet_addr, airdrop_amount
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let current_balance = client.get_balance(&id.pubkey()).unwrap_or_else(|e| {
|
||||
panic!("airdrop error {}", e);
|
||||
});
|
||||
info!("current balance {}...", current_balance);
|
||||
|
||||
if current_balance - starting_balance != airdrop_amount {
|
||||
info!(
|
||||
"Airdrop failed? {} {} {} {}",
|
||||
id.pubkey(),
|
||||
current_balance,
|
||||
starting_balance,
|
||||
airdrop_amount,
|
||||
);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
struct SeedTracker {
|
||||
max_created: Arc<AtomicU64>,
|
||||
max_closed: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
fn make_create_message(
|
||||
keypair: &Keypair,
|
||||
base_keypair: &Keypair,
|
||||
max_created_seed: Arc<AtomicU64>,
|
||||
num_instructions: usize,
|
||||
balance: u64,
|
||||
maybe_space: Option<u64>,
|
||||
mint: Option<Pubkey>,
|
||||
) -> Message {
|
||||
let space = maybe_space.unwrap_or_else(|| thread_rng().gen_range(0, 1000));
|
||||
|
||||
let instructions: Vec<_> = (0..num_instructions)
|
||||
.into_iter()
|
||||
.flat_map(|_| {
|
||||
let program_id = if mint.is_some() {
|
||||
inline_spl_token::id()
|
||||
} else {
|
||||
system_program::id()
|
||||
};
|
||||
let seed = max_created_seed.fetch_add(1, Ordering::Relaxed).to_string();
|
||||
let to_pubkey =
|
||||
Pubkey::create_with_seed(&base_keypair.pubkey(), &seed, &program_id).unwrap();
|
||||
let mut instructions = vec![system_instruction::create_account_with_seed(
|
||||
&keypair.pubkey(),
|
||||
&to_pubkey,
|
||||
&base_keypair.pubkey(),
|
||||
&seed,
|
||||
balance,
|
||||
space,
|
||||
&program_id,
|
||||
)];
|
||||
if let Some(mint_address) = mint {
|
||||
instructions.push(spl_token_instruction(
|
||||
spl_token::instruction::initialize_account(
|
||||
&spl_token::id(),
|
||||
&spl_token_pubkey(&to_pubkey),
|
||||
&spl_token_pubkey(&mint_address),
|
||||
&spl_token_pubkey(&base_keypair.pubkey()),
|
||||
)
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
instructions
|
||||
})
|
||||
.collect();
|
||||
|
||||
Message::new(&instructions, Some(&keypair.pubkey()))
|
||||
}
|
||||
|
||||
fn make_close_message(
|
||||
keypair: &Keypair,
|
||||
base_keypair: &Keypair,
|
||||
max_created: Arc<AtomicU64>,
|
||||
max_closed: Arc<AtomicU64>,
|
||||
num_instructions: usize,
|
||||
balance: u64,
|
||||
spl_token: bool,
|
||||
) -> Message {
|
||||
let instructions: Vec<_> = (0..num_instructions)
|
||||
.into_iter()
|
||||
.filter_map(|_| {
|
||||
let program_id = if spl_token {
|
||||
inline_spl_token::id()
|
||||
} else {
|
||||
system_program::id()
|
||||
};
|
||||
let max_created_seed = max_created.load(Ordering::Relaxed);
|
||||
let max_closed_seed = max_closed.load(Ordering::Relaxed);
|
||||
if max_closed_seed >= max_created_seed {
|
||||
return None;
|
||||
}
|
||||
let seed = max_closed.fetch_add(1, Ordering::Relaxed).to_string();
|
||||
let address =
|
||||
Pubkey::create_with_seed(&base_keypair.pubkey(), &seed, &program_id).unwrap();
|
||||
if spl_token {
|
||||
Some(spl_token_instruction(
|
||||
spl_token::instruction::close_account(
|
||||
&spl_token::id(),
|
||||
&spl_token_pubkey(&address),
|
||||
&spl_token_pubkey(&keypair.pubkey()),
|
||||
&spl_token_pubkey(&base_keypair.pubkey()),
|
||||
&[],
|
||||
)
|
||||
.unwrap(),
|
||||
))
|
||||
} else {
|
||||
Some(system_instruction::transfer_with_seed(
|
||||
&address,
|
||||
&base_keypair.pubkey(),
|
||||
seed,
|
||||
&program_id,
|
||||
&keypair.pubkey(),
|
||||
balance,
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Message::new(&instructions, Some(&keypair.pubkey()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_accounts_bench(
|
||||
entrypoint_addr: SocketAddr,
|
||||
faucet_addr: SocketAddr,
|
||||
payer_keypairs: &[&Keypair],
|
||||
iterations: usize,
|
||||
maybe_space: Option<u64>,
|
||||
batch_size: usize,
|
||||
close_nth_batch: u64,
|
||||
maybe_lamports: Option<u64>,
|
||||
num_instructions: usize,
|
||||
mint: Option<Pubkey>,
|
||||
reclaim_accounts: bool,
|
||||
) {
|
||||
assert!(num_instructions > 0);
|
||||
let client =
|
||||
RpcClient::new_socket_with_commitment(entrypoint_addr, CommitmentConfig::confirmed());
|
||||
|
||||
info!("Targeting {}", entrypoint_addr);
|
||||
|
||||
let mut latest_blockhash = Instant::now();
|
||||
let mut last_log = Instant::now();
|
||||
let mut count = 0;
|
||||
let mut blockhash = client.get_latest_blockhash().expect("blockhash");
|
||||
let mut tx_sent_count = 0;
|
||||
let mut total_accounts_created = 0;
|
||||
let mut total_accounts_closed = 0;
|
||||
let mut balances: Vec<_> = payer_keypairs
|
||||
.iter()
|
||||
.map(|keypair| client.get_balance(&keypair.pubkey()).unwrap_or(0))
|
||||
.collect();
|
||||
let mut last_balance = Instant::now();
|
||||
|
||||
let default_max_lamports = 1000;
|
||||
let min_balance = maybe_lamports.unwrap_or_else(|| {
|
||||
let space = maybe_space.unwrap_or(default_max_lamports);
|
||||
client
|
||||
.get_minimum_balance_for_rent_exemption(space as usize)
|
||||
.expect("min balance")
|
||||
});
|
||||
|
||||
let base_keypair = Keypair::new();
|
||||
let seed_tracker = SeedTracker {
|
||||
max_created: Arc::new(AtomicU64::default()),
|
||||
max_closed: Arc::new(AtomicU64::default()),
|
||||
};
|
||||
|
||||
info!("Starting balance(s): {:?}", balances);
|
||||
|
||||
let executor = TransactionExecutor::new(entrypoint_addr);
|
||||
|
||||
// Create and close messages both require 2 signatures, fake a 2 signature message to calculate fees
|
||||
let mut message = Message::new(
|
||||
&[
|
||||
Instruction::new_with_bytes(
|
||||
Pubkey::new_unique(),
|
||||
&[],
|
||||
vec![AccountMeta::new(Pubkey::new_unique(), true)],
|
||||
),
|
||||
Instruction::new_with_bytes(
|
||||
Pubkey::new_unique(),
|
||||
&[],
|
||||
vec![AccountMeta::new(Pubkey::new_unique(), true)],
|
||||
),
|
||||
],
|
||||
None,
|
||||
);
|
||||
|
||||
loop {
|
||||
if latest_blockhash.elapsed().as_millis() > 10_000 {
|
||||
blockhash = client.get_latest_blockhash().expect("blockhash");
|
||||
latest_blockhash = Instant::now();
|
||||
}
|
||||
|
||||
message.recent_blockhash = blockhash;
|
||||
let fee = client
|
||||
.get_fee_for_message(&message)
|
||||
.expect("get_fee_for_message");
|
||||
let lamports = min_balance + fee;
|
||||
|
||||
for (i, balance) in balances.iter_mut().enumerate() {
|
||||
if *balance < lamports || last_balance.elapsed().as_millis() > 2000 {
|
||||
if let Ok(b) = client.get_balance(&payer_keypairs[i].pubkey()) {
|
||||
*balance = b;
|
||||
}
|
||||
last_balance = Instant::now();
|
||||
if *balance < lamports * 2 {
|
||||
info!(
|
||||
"Balance {} is less than needed: {}, doing aidrop...",
|
||||
balance, lamports
|
||||
);
|
||||
if !airdrop_lamports(
|
||||
&client,
|
||||
&faucet_addr,
|
||||
payer_keypairs[i],
|
||||
lamports * 100_000,
|
||||
) {
|
||||
warn!("failed airdrop, exiting");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create accounts
|
||||
let sigs_len = executor.num_outstanding();
|
||||
if sigs_len < batch_size {
|
||||
let num_to_create = batch_size - sigs_len;
|
||||
if num_to_create >= payer_keypairs.len() {
|
||||
info!("creating {} new", num_to_create);
|
||||
let chunk_size = num_to_create / payer_keypairs.len();
|
||||
if chunk_size > 0 {
|
||||
for (i, keypair) in payer_keypairs.iter().enumerate() {
|
||||
let txs: Vec<_> = (0..chunk_size)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
let message = make_create_message(
|
||||
keypair,
|
||||
&base_keypair,
|
||||
seed_tracker.max_created.clone(),
|
||||
num_instructions,
|
||||
min_balance,
|
||||
maybe_space,
|
||||
mint,
|
||||
);
|
||||
let signers: Vec<&Keypair> = vec![keypair, &base_keypair];
|
||||
Transaction::new(&signers, message, blockhash)
|
||||
})
|
||||
.collect();
|
||||
balances[i] = balances[i].saturating_sub(lamports * txs.len() as u64);
|
||||
info!("txs: {}", txs.len());
|
||||
let new_ids = executor.push_transactions(txs);
|
||||
info!("ids: {}", new_ids.len());
|
||||
tx_sent_count += new_ids.len();
|
||||
total_accounts_created += num_instructions * new_ids.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if close_nth_batch > 0 {
|
||||
let num_batches_to_close =
|
||||
total_accounts_created as u64 / (close_nth_batch * batch_size as u64);
|
||||
let expected_closed = num_batches_to_close * batch_size as u64;
|
||||
let max_closed_seed = seed_tracker.max_closed.load(Ordering::Relaxed);
|
||||
// Close every account we've created with seed between max_closed_seed..expected_closed
|
||||
if max_closed_seed < expected_closed {
|
||||
let txs: Vec<_> = (0..expected_closed - max_closed_seed)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
let message = make_close_message(
|
||||
payer_keypairs[0],
|
||||
&base_keypair,
|
||||
seed_tracker.max_created.clone(),
|
||||
seed_tracker.max_closed.clone(),
|
||||
1,
|
||||
min_balance,
|
||||
mint.is_some(),
|
||||
);
|
||||
let signers: Vec<&Keypair> = vec![payer_keypairs[0], &base_keypair];
|
||||
Transaction::new(&signers, message, blockhash)
|
||||
})
|
||||
.collect();
|
||||
balances[0] = balances[0].saturating_sub(fee * txs.len() as u64);
|
||||
info!("close txs: {}", txs.len());
|
||||
let new_ids = executor.push_transactions(txs);
|
||||
info!("close ids: {}", new_ids.len());
|
||||
tx_sent_count += new_ids.len();
|
||||
total_accounts_closed += new_ids.len() as u64;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = executor.drain_cleared();
|
||||
}
|
||||
|
||||
count += 1;
|
||||
if last_log.elapsed().as_millis() > 3000 || count >= iterations {
|
||||
info!(
|
||||
"total_accounts_created: {} total_accounts_closed: {} tx_sent_count: {} loop_count: {} balance(s): {:?}",
|
||||
total_accounts_created, total_accounts_closed, tx_sent_count, count, balances
|
||||
);
|
||||
last_log = Instant::now();
|
||||
}
|
||||
if iterations != 0 && count >= iterations {
|
||||
break;
|
||||
}
|
||||
if executor.num_outstanding() >= batch_size {
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
executor.close();
|
||||
|
||||
if reclaim_accounts {
|
||||
let executor = TransactionExecutor::new(entrypoint_addr);
|
||||
loop {
|
||||
let max_closed_seed = seed_tracker.max_closed.load(Ordering::Relaxed);
|
||||
let max_created_seed = seed_tracker.max_created.load(Ordering::Relaxed);
|
||||
|
||||
if latest_blockhash.elapsed().as_millis() > 10_000 {
|
||||
blockhash = client.get_latest_blockhash().expect("blockhash");
|
||||
latest_blockhash = Instant::now();
|
||||
}
|
||||
message.recent_blockhash = blockhash;
|
||||
let fee = client
|
||||
.get_fee_for_message(&message)
|
||||
.expect("get_fee_for_message");
|
||||
|
||||
let sigs_len = executor.num_outstanding();
|
||||
if sigs_len < batch_size && max_closed_seed < max_created_seed {
|
||||
let num_to_close = min(
|
||||
batch_size - sigs_len,
|
||||
(max_created_seed - max_closed_seed) as usize,
|
||||
);
|
||||
if num_to_close >= payer_keypairs.len() {
|
||||
info!("closing {} accounts", num_to_close);
|
||||
let chunk_size = num_to_close / payer_keypairs.len();
|
||||
info!("{:?} chunk_size", chunk_size);
|
||||
if chunk_size > 0 {
|
||||
for (i, keypair) in payer_keypairs.iter().enumerate() {
|
||||
let txs: Vec<_> = (0..chunk_size)
|
||||
.into_par_iter()
|
||||
.filter_map(|_| {
|
||||
let message = make_close_message(
|
||||
keypair,
|
||||
&base_keypair,
|
||||
seed_tracker.max_created.clone(),
|
||||
seed_tracker.max_closed.clone(),
|
||||
num_instructions,
|
||||
min_balance,
|
||||
mint.is_some(),
|
||||
);
|
||||
if message.instructions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let signers: Vec<&Keypair> = vec![keypair, &base_keypair];
|
||||
Some(Transaction::new(&signers, message, blockhash))
|
||||
})
|
||||
.collect();
|
||||
balances[i] = balances[i].saturating_sub(fee * txs.len() as u64);
|
||||
info!("close txs: {}", txs.len());
|
||||
let new_ids = executor.push_transactions(txs);
|
||||
info!("close ids: {}", new_ids.len());
|
||||
tx_sent_count += new_ids.len();
|
||||
total_accounts_closed += (num_instructions * new_ids.len()) as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = executor.drain_cleared();
|
||||
}
|
||||
count += 1;
|
||||
if last_log.elapsed().as_millis() > 3000 || max_closed_seed >= max_created_seed {
|
||||
info!(
|
||||
"total_accounts_closed: {} tx_sent_count: {} loop_count: {} balance(s): {:?}",
|
||||
total_accounts_closed, tx_sent_count, count, balances
|
||||
);
|
||||
last_log = Instant::now();
|
||||
}
|
||||
|
||||
if max_closed_seed >= max_created_seed {
|
||||
break;
|
||||
}
|
||||
if executor.num_outstanding() >= batch_size {
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
executor.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup_with_default("solana=info");
|
||||
let matches = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.version(solana_version::version!())
|
||||
.arg(
|
||||
Arg::with_name("entrypoint")
|
||||
.long("entrypoint")
|
||||
.takes_value(true)
|
||||
.value_name("HOST:PORT")
|
||||
.help("RPC entrypoint address. Usually <ip>:8899"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("faucet_addr")
|
||||
.long("faucet")
|
||||
.takes_value(true)
|
||||
.value_name("HOST:PORT")
|
||||
.help("Faucet entrypoint address. Usually <ip>:9900"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("space")
|
||||
.long("space")
|
||||
.takes_value(true)
|
||||
.value_name("BYTES")
|
||||
.help("Size of accounts to create"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("lamports")
|
||||
.long("lamports")
|
||||
.takes_value(true)
|
||||
.value_name("LAMPORTS")
|
||||
.help("How many lamports to fund each account"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("identity")
|
||||
.long("identity")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.value_name("FILE")
|
||||
.help("keypair file"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("batch_size")
|
||||
.long("batch-size")
|
||||
.takes_value(true)
|
||||
.value_name("BYTES")
|
||||
.help("Number of transactions to send per batch"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("close_nth_batch")
|
||||
.long("close-frequency")
|
||||
.takes_value(true)
|
||||
.value_name("BYTES")
|
||||
.help(
|
||||
"Every `n` batches, create a batch of close transactions for
|
||||
the earliest remaining batch of accounts created.
|
||||
Note: Should be > 1 to avoid situations where the close \
|
||||
transactions will be submitted before the corresponding \
|
||||
create transactions have been confirmed",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("num_instructions")
|
||||
.long("num-instructions")
|
||||
.takes_value(true)
|
||||
.value_name("NUM")
|
||||
.help("Number of accounts to create on each transaction"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("iterations")
|
||||
.long("iterations")
|
||||
.takes_value(true)
|
||||
.value_name("NUM")
|
||||
.help("Number of iterations to make. 0 = unlimited iterations."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("check_gossip")
|
||||
.long("check-gossip")
|
||||
.help("Just use entrypoint address directly"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("mint")
|
||||
.long("mint")
|
||||
.takes_value(true)
|
||||
.help("Mint address to initialize account"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("reclaim_accounts")
|
||||
.long("reclaim-accounts")
|
||||
.takes_value(false)
|
||||
.help("Reclaim accounts after session ends; incompatible with --iterations 0"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let skip_gossip = !matches.is_present("check_gossip");
|
||||
|
||||
let port = if skip_gossip { DEFAULT_RPC_PORT } else { 8001 };
|
||||
let mut entrypoint_addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
if let Some(addr) = matches.value_of("entrypoint") {
|
||||
entrypoint_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse entrypoint address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
}
|
||||
let mut faucet_addr = SocketAddr::from(([127, 0, 0, 1], FAUCET_PORT));
|
||||
if let Some(addr) = matches.value_of("faucet_addr") {
|
||||
faucet_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse entrypoint address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
}
|
||||
|
||||
let space = value_t!(matches, "space", u64).ok();
|
||||
let lamports = value_t!(matches, "lamports", u64).ok();
|
||||
let batch_size = value_t!(matches, "batch_size", usize).unwrap_or(4);
|
||||
let close_nth_batch = value_t!(matches, "close_nth_batch", u64).unwrap_or(0);
|
||||
let iterations = value_t!(matches, "iterations", usize).unwrap_or(10);
|
||||
let num_instructions = value_t!(matches, "num_instructions", usize).unwrap_or(1);
|
||||
if num_instructions == 0 || num_instructions > 500 {
|
||||
eprintln!("bad num_instructions: {}", num_instructions);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mint = pubkey_of(&matches, "mint");
|
||||
|
||||
let payer_keypairs: Vec<_> = values_t_or_exit!(matches, "identity", String)
|
||||
.iter()
|
||||
.map(|keypair_string| {
|
||||
read_keypair_file(keypair_string)
|
||||
.unwrap_or_else(|_| panic!("bad keypair {:?}", keypair_string))
|
||||
})
|
||||
.collect();
|
||||
let mut payer_keypair_refs: Vec<&Keypair> = vec![];
|
||||
for keypair in payer_keypairs.iter() {
|
||||
payer_keypair_refs.push(keypair);
|
||||
}
|
||||
|
||||
let rpc_addr = if !skip_gossip {
|
||||
info!("Finding cluster entry: {:?}", entrypoint_addr);
|
||||
let (gossip_nodes, _validators) = discover(
|
||||
None, // keypair
|
||||
Some(&entrypoint_addr),
|
||||
None, // num_nodes
|
||||
Duration::from_secs(60), // timeout
|
||||
None, // find_node_by_pubkey
|
||||
Some(&entrypoint_addr), // find_node_by_gossip_addr
|
||||
None, // my_gossip_addr
|
||||
0, // my_shred_version
|
||||
SocketAddrSpace::Unspecified,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("Failed to discover {} node: {:?}", entrypoint_addr, err);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
info!("done found {} nodes", gossip_nodes.len());
|
||||
gossip_nodes[0].rpc
|
||||
} else {
|
||||
info!("Using {:?} as the RPC address", entrypoint_addr);
|
||||
entrypoint_addr
|
||||
};
|
||||
|
||||
run_accounts_bench(
|
||||
rpc_addr,
|
||||
faucet_addr,
|
||||
&payer_keypair_refs,
|
||||
iterations,
|
||||
space,
|
||||
batch_size,
|
||||
close_nth_batch,
|
||||
lamports,
|
||||
num_instructions,
|
||||
mint,
|
||||
matches.is_present("reclaim_accounts"),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use {
|
||||
super::*,
|
||||
solana_core::validator::ValidatorConfig,
|
||||
solana_faucet::faucet::run_local_faucet,
|
||||
solana_local_cluster::{
|
||||
local_cluster::{ClusterConfig, LocalCluster},
|
||||
validator_configs::make_identical_validator_configs,
|
||||
},
|
||||
solana_measure::measure::Measure,
|
||||
solana_sdk::{native_token::sol_to_lamports, poh_config::PohConfig},
|
||||
solana_test_validator::TestValidator,
|
||||
spl_token::{
|
||||
solana_program::program_pack::Pack,
|
||||
state::{Account, Mint},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_accounts_cluster_bench() {
|
||||
solana_logger::setup();
|
||||
let validator_config = ValidatorConfig::default_for_test();
|
||||
let num_nodes = 1;
|
||||
let mut config = ClusterConfig {
|
||||
cluster_lamports: 10_000_000,
|
||||
poh_config: PohConfig::new_sleep(Duration::from_millis(50)),
|
||||
node_stakes: vec![100; num_nodes],
|
||||
validator_configs: make_identical_validator_configs(&validator_config, num_nodes),
|
||||
..ClusterConfig::default()
|
||||
};
|
||||
|
||||
let faucet_addr = SocketAddr::from(([127, 0, 0, 1], 9900));
|
||||
let cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified);
|
||||
let iterations = 10;
|
||||
let maybe_space = None;
|
||||
let batch_size = 100;
|
||||
let close_nth_batch = 100;
|
||||
let maybe_lamports = None;
|
||||
let num_instructions = 2;
|
||||
let mut start = Measure::start("total accounts run");
|
||||
run_accounts_bench(
|
||||
cluster.entry_point_info.rpc,
|
||||
faucet_addr,
|
||||
&[&cluster.funding_keypair],
|
||||
iterations,
|
||||
maybe_space,
|
||||
batch_size,
|
||||
close_nth_batch,
|
||||
maybe_lamports,
|
||||
num_instructions,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
start.stop();
|
||||
info!("{}", start);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_then_reclaim_spl_token_accounts() {
|
||||
solana_logger::setup();
|
||||
let mint_keypair = Keypair::new();
|
||||
let mint_pubkey = mint_keypair.pubkey();
|
||||
let faucet_addr = run_local_faucet(mint_keypair, None);
|
||||
let test_validator = TestValidator::with_custom_fees(
|
||||
mint_pubkey,
|
||||
1,
|
||||
Some(faucet_addr),
|
||||
SocketAddrSpace::Unspecified,
|
||||
);
|
||||
let rpc_client =
|
||||
RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed());
|
||||
|
||||
// Created funder
|
||||
let funder = Keypair::new();
|
||||
let latest_blockhash = rpc_client.get_latest_blockhash().unwrap();
|
||||
let signature = rpc_client
|
||||
.request_airdrop_with_blockhash(
|
||||
&funder.pubkey(),
|
||||
sol_to_lamports(1.0),
|
||||
&latest_blockhash,
|
||||
)
|
||||
.unwrap();
|
||||
rpc_client
|
||||
.confirm_transaction_with_spinner(
|
||||
&signature,
|
||||
&latest_blockhash,
|
||||
CommitmentConfig::confirmed(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create Mint
|
||||
let spl_mint_keypair = Keypair::new();
|
||||
let spl_mint_len = Mint::get_packed_len();
|
||||
let spl_mint_rent = rpc_client
|
||||
.get_minimum_balance_for_rent_exemption(spl_mint_len)
|
||||
.unwrap();
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[
|
||||
system_instruction::create_account(
|
||||
&funder.pubkey(),
|
||||
&spl_mint_keypair.pubkey(),
|
||||
spl_mint_rent,
|
||||
spl_mint_len as u64,
|
||||
&inline_spl_token::id(),
|
||||
),
|
||||
spl_token_instruction(
|
||||
spl_token::instruction::initialize_mint(
|
||||
&spl_token::id(),
|
||||
&spl_token_pubkey(&spl_mint_keypair.pubkey()),
|
||||
&spl_token_pubkey(&spl_mint_keypair.pubkey()),
|
||||
None,
|
||||
2,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
],
|
||||
Some(&funder.pubkey()),
|
||||
&[&funder, &spl_mint_keypair],
|
||||
latest_blockhash,
|
||||
);
|
||||
let _sig = rpc_client
|
||||
.send_and_confirm_transaction(&transaction)
|
||||
.unwrap();
|
||||
|
||||
let account_len = Account::get_packed_len();
|
||||
let minimum_balance = rpc_client
|
||||
.get_minimum_balance_for_rent_exemption(account_len)
|
||||
.unwrap();
|
||||
|
||||
let iterations = 5;
|
||||
let batch_size = 100;
|
||||
let close_nth_batch = 0;
|
||||
let num_instructions = 4;
|
||||
let mut start = Measure::start("total accounts run");
|
||||
let keypair0 = Keypair::new();
|
||||
let keypair1 = Keypair::new();
|
||||
let keypair2 = Keypair::new();
|
||||
run_accounts_bench(
|
||||
test_validator
|
||||
.rpc_url()
|
||||
.replace("http://", "")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
faucet_addr,
|
||||
&[&keypair0, &keypair1, &keypair2],
|
||||
iterations,
|
||||
Some(account_len as u64),
|
||||
batch_size,
|
||||
close_nth_batch,
|
||||
Some(minimum_balance),
|
||||
num_instructions,
|
||||
Some(spl_mint_keypair.pubkey()),
|
||||
true,
|
||||
);
|
||||
start.stop();
|
||||
info!("{}", start);
|
||||
}
|
||||
}
|
2
banking-bench/.gitignore
vendored
2
banking-bench/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
/target/
|
||||
/farf/
|
@ -1,30 +0,0 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
name = "solana-banking-bench"
|
||||
version = "1.11.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.1"
|
||||
crossbeam-channel = "0.5"
|
||||
log = "0.4.14"
|
||||
rand = "0.7.0"
|
||||
rayon = "1.5.1"
|
||||
solana-core = { path = "../core", version = "=1.11.0" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.11.0" }
|
||||
solana-ledger = { path = "../ledger", version = "=1.11.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.11.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.11.0" }
|
||||
solana-perf = { path = "../perf", version = "=1.11.0" }
|
||||
solana-poh = { path = "../poh", version = "=1.11.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.11.0" }
|
||||
solana-version = { path = "../version", version = "=1.11.0" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,418 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
use {
|
||||
clap::{crate_description, crate_name, value_t, App, Arg},
|
||||
crossbeam_channel::{unbounded, Receiver},
|
||||
log::*,
|
||||
rand::{thread_rng, Rng},
|
||||
rayon::prelude::*,
|
||||
solana_core::banking_stage::BankingStage,
|
||||
solana_gossip::cluster_info::{ClusterInfo, Node},
|
||||
solana_ledger::{
|
||||
blockstore::Blockstore,
|
||||
genesis_utils::{create_genesis_config, GenesisConfigInfo},
|
||||
get_tmp_ledger_path,
|
||||
leader_schedule_cache::LeaderScheduleCache,
|
||||
},
|
||||
solana_measure::measure::Measure,
|
||||
solana_perf::packet::to_packet_batches,
|
||||
solana_poh::poh_recorder::{create_test_recorder, PohRecorder, WorkingBankEntry},
|
||||
solana_runtime::{
|
||||
accounts_background_service::AbsRequestSender, bank::Bank, bank_forks::BankForks,
|
||||
cost_model::CostModel,
|
||||
},
|
||||
solana_sdk::{
|
||||
hash::Hash,
|
||||
signature::{Keypair, Signature},
|
||||
system_transaction,
|
||||
timing::{duration_as_us, timestamp},
|
||||
transaction::Transaction,
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
std::{
|
||||
sync::{atomic::Ordering, Arc, Mutex, RwLock},
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
},
|
||||
};
|
||||
|
||||
fn check_txs(
|
||||
receiver: &Arc<Receiver<WorkingBankEntry>>,
|
||||
ref_tx_count: usize,
|
||||
poh_recorder: &Arc<Mutex<PohRecorder>>,
|
||||
) -> bool {
|
||||
let mut total = 0;
|
||||
let now = Instant::now();
|
||||
let mut no_bank = false;
|
||||
loop {
|
||||
if let Ok((_bank, (entry, _tick_height))) = receiver.recv_timeout(Duration::from_millis(10))
|
||||
{
|
||||
total += entry.transactions.len();
|
||||
}
|
||||
if total >= ref_tx_count {
|
||||
break;
|
||||
}
|
||||
if now.elapsed().as_secs() > 60 {
|
||||
break;
|
||||
}
|
||||
if poh_recorder.lock().unwrap().bank().is_none() {
|
||||
trace!("no bank");
|
||||
no_bank = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !no_bank {
|
||||
assert!(total >= ref_tx_count);
|
||||
}
|
||||
no_bank
|
||||
}
|
||||
|
||||
fn make_accounts_txs(
|
||||
total_num_transactions: usize,
|
||||
hash: Hash,
|
||||
same_payer: bool,
|
||||
) -> Vec<Transaction> {
|
||||
let to_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let payer_key = Keypair::new();
|
||||
let dummy = system_transaction::transfer(&payer_key, &to_pubkey, 1, hash);
|
||||
(0..total_num_transactions)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
let mut new = dummy.clone();
|
||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen::<u8>()).collect();
|
||||
if !same_payer {
|
||||
new.message.account_keys[0] = solana_sdk::pubkey::new_rand();
|
||||
}
|
||||
new.message.account_keys[1] = solana_sdk::pubkey::new_rand();
|
||||
new.signatures = vec![Signature::new(&sig[0..64])];
|
||||
new
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct Config {
|
||||
packets_per_batch: usize,
|
||||
chunk_len: usize,
|
||||
num_threads: usize,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn get_transactions_index(&self, chunk_index: usize) -> usize {
|
||||
chunk_index * (self.chunk_len / self.num_threads) * self.packets_per_batch
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_as_usize(bytes: &[u8]) -> usize {
|
||||
bytes[0] as usize | (bytes[1] as usize) << 8
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn main() {
|
||||
solana_logger::setup();
|
||||
|
||||
let matches = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.version(solana_version::version!())
|
||||
.arg(
|
||||
Arg::with_name("num_chunks")
|
||||
.long("num-chunks")
|
||||
.takes_value(true)
|
||||
.value_name("SIZE")
|
||||
.help("Number of transaction chunks."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("packets_per_chunk")
|
||||
.long("packets-per-chunk")
|
||||
.takes_value(true)
|
||||
.value_name("SIZE")
|
||||
.help("Packets per chunk"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("skip_sanity")
|
||||
.long("skip-sanity")
|
||||
.takes_value(false)
|
||||
.help("Skip transaction sanity execution"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("same_payer")
|
||||
.long("same-payer")
|
||||
.takes_value(false)
|
||||
.help("Use the same payer for transfers"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("iterations")
|
||||
.long("iterations")
|
||||
.takes_value(true)
|
||||
.help("Number of iterations"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("num_threads")
|
||||
.long("num-threads")
|
||||
.takes_value(true)
|
||||
.help("Number of iterations"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let num_threads =
|
||||
value_t!(matches, "num_threads", usize).unwrap_or(BankingStage::num_threads() as usize);
|
||||
// a multiple of packet chunk duplicates to avoid races
|
||||
let num_chunks = value_t!(matches, "num_chunks", usize).unwrap_or(16);
|
||||
let packets_per_chunk = value_t!(matches, "packets_per_chunk", usize).unwrap_or(192);
|
||||
let iterations = value_t!(matches, "iterations", usize).unwrap_or(1000);
|
||||
|
||||
let total_num_transactions = num_chunks * num_threads * packets_per_chunk;
|
||||
let mint_total = 1_000_000_000_000;
|
||||
let GenesisConfigInfo {
|
||||
genesis_config,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_genesis_config(mint_total);
|
||||
|
||||
let (verified_sender, verified_receiver) = unbounded();
|
||||
let (vote_sender, vote_receiver) = unbounded();
|
||||
let (tpu_vote_sender, tpu_vote_receiver) = unbounded();
|
||||
let (replay_vote_sender, _replay_vote_receiver) = unbounded();
|
||||
let bank0 = Bank::new_for_benches(&genesis_config);
|
||||
let mut bank_forks = BankForks::new(bank0);
|
||||
let mut bank = bank_forks.working_bank();
|
||||
|
||||
// set cost tracker limits to MAX so it will not filter out TXs
|
||||
bank.write_cost_tracker()
|
||||
.unwrap()
|
||||
.set_limits(std::u64::MAX, std::u64::MAX, std::u64::MAX);
|
||||
|
||||
info!("threads: {} txs: {}", num_threads, total_num_transactions);
|
||||
|
||||
let same_payer = matches.is_present("same_payer");
|
||||
let mut transactions =
|
||||
make_accounts_txs(total_num_transactions, genesis_config.hash(), same_payer);
|
||||
|
||||
// fund all the accounts
|
||||
transactions.iter().for_each(|tx| {
|
||||
let mut fund = system_transaction::transfer(
|
||||
&mint_keypair,
|
||||
&tx.message.account_keys[0],
|
||||
mint_total / total_num_transactions as u64,
|
||||
genesis_config.hash(),
|
||||
);
|
||||
// Ignore any pesky duplicate signature errors in the case we are using single-payer
|
||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen::<u8>()).collect();
|
||||
fund.signatures = vec![Signature::new(&sig[0..64])];
|
||||
let x = bank.process_transaction(&fund);
|
||||
x.unwrap();
|
||||
});
|
||||
|
||||
let skip_sanity = matches.is_present("skip_sanity");
|
||||
if !skip_sanity {
|
||||
//sanity check, make sure all the transactions can execute sequentially
|
||||
transactions.iter().for_each(|tx| {
|
||||
let res = bank.process_transaction(tx);
|
||||
assert!(res.is_ok(), "sanity test transactions error: {:?}", res);
|
||||
});
|
||||
bank.clear_signatures();
|
||||
//sanity check, make sure all the transactions can execute in parallel
|
||||
|
||||
let res = bank.process_transactions(transactions.iter());
|
||||
for r in res {
|
||||
assert!(r.is_ok(), "sanity parallel execution error: {:?}", r);
|
||||
}
|
||||
bank.clear_signatures();
|
||||
}
|
||||
|
||||
let mut verified: Vec<_> = to_packet_batches(&transactions, packets_per_chunk);
|
||||
let ledger_path = get_tmp_ledger_path!();
|
||||
{
|
||||
let blockstore = Arc::new(
|
||||
Blockstore::open(&ledger_path).expect("Expected to be able to open database ledger"),
|
||||
);
|
||||
let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank));
|
||||
let (exit, poh_recorder, poh_service, signal_receiver) = create_test_recorder(
|
||||
&bank,
|
||||
&blockstore,
|
||||
None,
|
||||
Some(leader_schedule_cache.clone()),
|
||||
);
|
||||
let cluster_info = ClusterInfo::new(
|
||||
Node::new_localhost().info,
|
||||
Arc::new(Keypair::new()),
|
||||
SocketAddrSpace::Unspecified,
|
||||
);
|
||||
let cluster_info = Arc::new(cluster_info);
|
||||
let banking_stage = BankingStage::new(
|
||||
&cluster_info,
|
||||
&poh_recorder,
|
||||
verified_receiver,
|
||||
tpu_vote_receiver,
|
||||
vote_receiver,
|
||||
None,
|
||||
replay_vote_sender,
|
||||
Arc::new(RwLock::new(CostModel::default())),
|
||||
);
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
|
||||
let chunk_len = verified.len() / num_chunks;
|
||||
let mut start = 0;
|
||||
|
||||
// This is so that the signal_receiver does not go out of scope after the closure.
|
||||
// If it is dropped before poh_service, then poh_service will error when
|
||||
// calling send() on the channel.
|
||||
let signal_receiver = Arc::new(signal_receiver);
|
||||
let mut total_us = 0;
|
||||
let mut tx_total_us = 0;
|
||||
let base_tx_count = bank.transaction_count();
|
||||
let mut txs_processed = 0;
|
||||
let mut root = 1;
|
||||
let collector = solana_sdk::pubkey::new_rand();
|
||||
let config = Config {
|
||||
packets_per_batch: packets_per_chunk,
|
||||
chunk_len,
|
||||
num_threads,
|
||||
};
|
||||
let mut total_sent = 0;
|
||||
for _ in 0..iterations {
|
||||
let now = Instant::now();
|
||||
let mut sent = 0;
|
||||
|
||||
for (i, v) in verified[start..start + chunk_len]
|
||||
.chunks(chunk_len / num_threads)
|
||||
.enumerate()
|
||||
{
|
||||
let mut byte = 0;
|
||||
let index = config.get_transactions_index(start + i);
|
||||
if index < transactions.len() {
|
||||
byte = bytes_as_usize(transactions[index].signatures[0].as_ref());
|
||||
}
|
||||
trace!(
|
||||
"sending... {}..{} {} v.len: {} sig: {} transactions.len: {} index: {}",
|
||||
start + i,
|
||||
start + chunk_len,
|
||||
timestamp(),
|
||||
v.len(),
|
||||
byte,
|
||||
transactions.len(),
|
||||
index,
|
||||
);
|
||||
for xv in v {
|
||||
sent += xv.packets.len();
|
||||
}
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
}
|
||||
let start_tx_index = config.get_transactions_index(start);
|
||||
let end_tx_index = config.get_transactions_index(start + chunk_len);
|
||||
for tx in &transactions[start_tx_index..end_tx_index] {
|
||||
loop {
|
||||
if bank.get_signature_status(&tx.signatures[0]).is_some() {
|
||||
break;
|
||||
}
|
||||
if poh_recorder.lock().unwrap().bank().is_none() {
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
if check_txs(
|
||||
&signal_receiver,
|
||||
total_num_transactions / num_chunks,
|
||||
&poh_recorder,
|
||||
) {
|
||||
debug!(
|
||||
"resetting bank {} tx count: {} txs_proc: {}",
|
||||
bank.slot(),
|
||||
bank.transaction_count(),
|
||||
txs_processed
|
||||
);
|
||||
assert!(txs_processed < bank.transaction_count());
|
||||
txs_processed = bank.transaction_count();
|
||||
tx_total_us += duration_as_us(&now.elapsed());
|
||||
|
||||
let mut poh_time = Measure::start("poh_time");
|
||||
poh_recorder
|
||||
.lock()
|
||||
.unwrap()
|
||||
.reset(bank.clone(), Some((bank.slot(), bank.slot() + 1)));
|
||||
poh_time.stop();
|
||||
|
||||
let mut new_bank_time = Measure::start("new_bank");
|
||||
let new_bank = Bank::new_from_parent(&bank, &collector, bank.slot() + 1);
|
||||
new_bank_time.stop();
|
||||
|
||||
let mut insert_time = Measure::start("insert_time");
|
||||
bank_forks.insert(new_bank);
|
||||
bank = bank_forks.working_bank();
|
||||
insert_time.stop();
|
||||
|
||||
// set cost tracker limits to MAX so it will not filter out TXs
|
||||
bank.write_cost_tracker().unwrap().set_limits(
|
||||
std::u64::MAX,
|
||||
std::u64::MAX,
|
||||
std::u64::MAX,
|
||||
);
|
||||
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
assert!(poh_recorder.lock().unwrap().bank().is_some());
|
||||
if bank.slot() > 32 {
|
||||
leader_schedule_cache.set_root(&bank);
|
||||
bank_forks.set_root(root, &AbsRequestSender::default(), None);
|
||||
root += 1;
|
||||
}
|
||||
debug!(
|
||||
"new_bank_time: {}us insert_time: {}us poh_time: {}us",
|
||||
new_bank_time.as_us(),
|
||||
insert_time.as_us(),
|
||||
poh_time.as_us(),
|
||||
);
|
||||
} else {
|
||||
tx_total_us += duration_as_us(&now.elapsed());
|
||||
}
|
||||
|
||||
// This signature clear may not actually clear the signatures
|
||||
// in this chunk, but since we rotate between CHUNKS then
|
||||
// we should clear them by the time we come around again to re-use that chunk.
|
||||
bank.clear_signatures();
|
||||
total_us += duration_as_us(&now.elapsed());
|
||||
debug!(
|
||||
"time: {} us checked: {} sent: {}",
|
||||
duration_as_us(&now.elapsed()),
|
||||
total_num_transactions / num_chunks,
|
||||
sent,
|
||||
);
|
||||
total_sent += sent;
|
||||
|
||||
if bank.slot() > 0 && bank.slot() % 16 == 0 {
|
||||
for tx in transactions.iter_mut() {
|
||||
tx.message.recent_blockhash = bank.last_blockhash();
|
||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen::<u8>()).collect();
|
||||
tx.signatures[0] = Signature::new(&sig[0..64]);
|
||||
}
|
||||
verified = to_packet_batches(&transactions.clone(), packets_per_chunk);
|
||||
}
|
||||
|
||||
start += chunk_len;
|
||||
start %= verified.len();
|
||||
}
|
||||
let txs_processed = bank_forks.working_bank().transaction_count();
|
||||
debug!("processed: {} base: {}", txs_processed, base_tx_count);
|
||||
eprintln!(
|
||||
"{{'name': 'banking_bench_total', 'median': '{:.2}'}}",
|
||||
(1000.0 * 1000.0 * total_sent as f64) / (total_us as f64),
|
||||
);
|
||||
eprintln!(
|
||||
"{{'name': 'banking_bench_tx_total', 'median': '{:.2}'}}",
|
||||
(1000.0 * 1000.0 * total_sent as f64) / (tx_total_us as f64),
|
||||
);
|
||||
eprintln!(
|
||||
"{{'name': 'banking_bench_success_tx_total', 'median': '{:.2}'}}",
|
||||
(1000.0 * 1000.0 * (txs_processed - base_tx_count) as f64) / (total_us as f64),
|
||||
);
|
||||
|
||||
drop(verified_sender);
|
||||
drop(tpu_vote_sender);
|
||||
drop(vote_sender);
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
banking_stage.join().unwrap();
|
||||
debug!("waited for banking_stage");
|
||||
poh_service.join().unwrap();
|
||||
sleep(Duration::from_secs(1));
|
||||
debug!("waited for poh_service");
|
||||
}
|
||||
let _unused = Blockstore::destroy(&ledger_path);
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
[package]
|
||||
name = "solana-banks-client"
|
||||
version = "1.11.0"
|
||||
description = "Solana banks client"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-banks-client"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
borsh = "0.9.3"
|
||||
futures = "0.3"
|
||||
solana-banks-interface = { path = "../banks-interface", version = "=1.11.0" }
|
||||
solana-program = { path = "../sdk/program", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
tarpc = { version = "0.27.2", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-serde = { version = "0.8", features = ["bincode"] }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-banks-server = { path = "../banks-server", version = "=1.11.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.11.0" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_banks_client"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,77 +0,0 @@
|
||||
use {
|
||||
solana_sdk::{
|
||||
transaction::TransactionError, transaction_context::TransactionReturnData,
|
||||
transport::TransportError,
|
||||
},
|
||||
std::io,
|
||||
tarpc::client::RpcError,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
/// Errors from BanksClient
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BanksClientError {
|
||||
#[error("client error: {0}")]
|
||||
ClientError(&'static str),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
RpcError(#[from] RpcError),
|
||||
|
||||
#[error("transport transaction error: {0}")]
|
||||
TransactionError(#[from] TransactionError),
|
||||
|
||||
#[error("simulation error: {err:?}, logs: {logs:?}, units_consumed: {units_consumed:?}")]
|
||||
SimulationError {
|
||||
err: TransactionError,
|
||||
logs: Vec<String>,
|
||||
units_consumed: u64,
|
||||
return_data: Option<TransactionReturnData>,
|
||||
},
|
||||
}
|
||||
|
||||
impl BanksClientError {
|
||||
pub fn unwrap(&self) -> TransactionError {
|
||||
match self {
|
||||
BanksClientError::TransactionError(err)
|
||||
| BanksClientError::SimulationError { err, .. } => err.clone(),
|
||||
_ => panic!("unexpected transport error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BanksClientError> for io::Error {
|
||||
fn from(err: BanksClientError) -> Self {
|
||||
match err {
|
||||
BanksClientError::ClientError(err) => Self::new(io::ErrorKind::Other, err.to_string()),
|
||||
BanksClientError::Io(err) => err,
|
||||
BanksClientError::RpcError(err) => Self::new(io::ErrorKind::Other, err.to_string()),
|
||||
BanksClientError::TransactionError(err) => {
|
||||
Self::new(io::ErrorKind::Other, err.to_string())
|
||||
}
|
||||
BanksClientError::SimulationError { err, .. } => {
|
||||
Self::new(io::ErrorKind::Other, err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BanksClientError> for TransportError {
|
||||
fn from(err: BanksClientError) -> Self {
|
||||
match err {
|
||||
BanksClientError::ClientError(err) => {
|
||||
Self::IoError(io::Error::new(io::ErrorKind::Other, err.to_string()))
|
||||
}
|
||||
BanksClientError::Io(err) => {
|
||||
Self::IoError(io::Error::new(io::ErrorKind::Other, err.to_string()))
|
||||
}
|
||||
BanksClientError::RpcError(err) => {
|
||||
Self::IoError(io::Error::new(io::ErrorKind::Other, err.to_string()))
|
||||
}
|
||||
BanksClientError::TransactionError(err) => Self::TransactionError(err),
|
||||
BanksClientError::SimulationError { err, .. } => Self::TransactionError(err),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,572 +0,0 @@
|
||||
//! A client for the ledger state, from the perspective of an arbitrary validator.
|
||||
//!
|
||||
//! Use start_tcp_client() to create a client and then import BanksClientExt to
|
||||
//! access its methods. Additional "*_with_context" methods are also available,
|
||||
//! but they are undocumented, may change over time, and are generally more
|
||||
//! cumbersome to use.
|
||||
|
||||
pub use {
|
||||
crate::error::BanksClientError,
|
||||
solana_banks_interface::{BanksClient as TarpcClient, TransactionStatus},
|
||||
};
|
||||
use {
|
||||
borsh::BorshDeserialize,
|
||||
futures::{future::join_all, Future, FutureExt, TryFutureExt},
|
||||
solana_banks_interface::{BanksRequest, BanksResponse, BanksTransactionResultWithSimulation},
|
||||
solana_program::{
|
||||
clock::Slot, fee_calculator::FeeCalculator, hash::Hash, program_pack::Pack, pubkey::Pubkey,
|
||||
rent::Rent, sysvar::Sysvar,
|
||||
},
|
||||
solana_sdk::{
|
||||
account::{from_account, Account},
|
||||
commitment_config::CommitmentLevel,
|
||||
message::Message,
|
||||
signature::Signature,
|
||||
transaction::{self, Transaction},
|
||||
},
|
||||
tarpc::{
|
||||
client::{self, NewClient, RequestDispatch},
|
||||
context::{self, Context},
|
||||
serde_transport::tcp,
|
||||
ClientMessage, Response, Transport,
|
||||
},
|
||||
tokio::{net::ToSocketAddrs, time::Duration},
|
||||
tokio_serde::formats::Bincode,
|
||||
};
|
||||
|
||||
mod error;
|
||||
|
||||
// This exists only for backward compatibility
|
||||
pub trait BanksClientExt {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BanksClient {
|
||||
inner: TarpcClient,
|
||||
}
|
||||
|
||||
impl BanksClient {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new<C>(
|
||||
config: client::Config,
|
||||
transport: C,
|
||||
) -> NewClient<TarpcClient, RequestDispatch<BanksRequest, BanksResponse, C>>
|
||||
where
|
||||
C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>>,
|
||||
{
|
||||
TarpcClient::new(config, transport)
|
||||
}
|
||||
|
||||
pub fn send_transaction_with_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
transaction: Transaction,
|
||||
) -> impl Future<Output = Result<(), BanksClientError>> + '_ {
|
||||
self.inner
|
||||
.send_transaction_with_context(ctx, transaction)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "1.9.0",
|
||||
note = "Please use `get_fee_for_message` or `is_blockhash_valid` instead"
|
||||
)]
|
||||
pub fn get_fees_with_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<(FeeCalculator, Hash, u64), BanksClientError>> + '_ {
|
||||
#[allow(deprecated)]
|
||||
self.inner
|
||||
.get_fees_with_commitment_and_context(ctx, commitment)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_transaction_status_with_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
signature: Signature,
|
||||
) -> impl Future<Output = Result<Option<TransactionStatus>, BanksClientError>> + '_ {
|
||||
self.inner
|
||||
.get_transaction_status_with_context(ctx, signature)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_slot_with_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<Slot, BanksClientError>> + '_ {
|
||||
self.inner
|
||||
.get_slot_with_context(ctx, commitment)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_block_height_with_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<Slot, BanksClientError>> + '_ {
|
||||
self.inner
|
||||
.get_block_height_with_context(ctx, commitment)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn process_transaction_with_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
transaction: Transaction,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<Option<transaction::Result<()>>, BanksClientError>> + '_ {
|
||||
self.inner
|
||||
.process_transaction_with_commitment_and_context(ctx, transaction, commitment)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn process_transaction_with_preflight_and_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
transaction: Transaction,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<BanksTransactionResultWithSimulation, BanksClientError>> + '_
|
||||
{
|
||||
self.inner
|
||||
.process_transaction_with_preflight_and_commitment_and_context(
|
||||
ctx,
|
||||
transaction,
|
||||
commitment,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_account_with_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
address: Pubkey,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<Option<Account>, BanksClientError>> + '_ {
|
||||
self.inner
|
||||
.get_account_with_commitment_and_context(ctx, address, commitment)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Send a transaction and return immediately. The server will resend the
|
||||
/// transaction until either it is accepted by the cluster or the transaction's
|
||||
/// blockhash expires.
|
||||
pub fn send_transaction(
|
||||
&mut self,
|
||||
transaction: Transaction,
|
||||
) -> impl Future<Output = Result<(), BanksClientError>> + '_ {
|
||||
self.send_transaction_with_context(context::current(), transaction)
|
||||
}
|
||||
|
||||
/// Return the fee parameters associated with a recent, rooted blockhash. The cluster
|
||||
/// will use the transaction's blockhash to look up these same fee parameters and
|
||||
/// use them to calculate the transaction fee.
|
||||
#[deprecated(
|
||||
since = "1.9.0",
|
||||
note = "Please use `get_fee_for_message` or `is_blockhash_valid` instead"
|
||||
)]
|
||||
pub fn get_fees(
|
||||
&mut self,
|
||||
) -> impl Future<Output = Result<(FeeCalculator, Hash, u64), BanksClientError>> + '_ {
|
||||
#[allow(deprecated)]
|
||||
self.get_fees_with_commitment_and_context(context::current(), CommitmentLevel::default())
|
||||
}
|
||||
|
||||
/// Return the cluster Sysvar
|
||||
pub fn get_sysvar<T: Sysvar>(
|
||||
&mut self,
|
||||
) -> impl Future<Output = Result<T, BanksClientError>> + '_ {
|
||||
self.get_account(T::id()).map(|result| {
|
||||
let sysvar = result?.ok_or(BanksClientError::ClientError("Sysvar not present"))?;
|
||||
from_account::<T, _>(&sysvar).ok_or(BanksClientError::ClientError(
|
||||
"Failed to deserialize sysvar",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the cluster rent
|
||||
pub fn get_rent(&mut self) -> impl Future<Output = Result<Rent, BanksClientError>> + '_ {
|
||||
self.get_sysvar::<Rent>()
|
||||
}
|
||||
|
||||
/// Return a recent, rooted blockhash from the server. The cluster will only accept
|
||||
/// transactions with a blockhash that has not yet expired. Use the `get_fees`
|
||||
/// method to get both a blockhash and the blockhash's last valid slot.
|
||||
#[deprecated(since = "1.9.0", note = "Please use `get_latest_blockhash` instead")]
|
||||
pub fn get_recent_blockhash(
|
||||
&mut self,
|
||||
) -> impl Future<Output = Result<Hash, BanksClientError>> + '_ {
|
||||
#[allow(deprecated)]
|
||||
self.get_fees().map(|result| Ok(result?.1))
|
||||
}
|
||||
|
||||
/// Send a transaction and return after the transaction has been rejected or
|
||||
/// reached the given level of commitment.
|
||||
pub fn process_transaction_with_commitment(
|
||||
&mut self,
|
||||
transaction: Transaction,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<(), BanksClientError>> + '_ {
|
||||
let mut ctx = context::current();
|
||||
ctx.deadline += Duration::from_secs(50);
|
||||
self.process_transaction_with_commitment_and_context(ctx, transaction, commitment)
|
||||
.map(|result| match result? {
|
||||
None => Err(BanksClientError::ClientError(
|
||||
"invalid blockhash or fee-payer",
|
||||
)),
|
||||
Some(transaction_result) => Ok(transaction_result?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a transaction and return any preflight (sanitization or simulation) errors, or return
|
||||
/// after the transaction has been rejected or reached the given level of commitment.
|
||||
pub fn process_transaction_with_preflight_and_commitment(
|
||||
&mut self,
|
||||
transaction: Transaction,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<(), BanksClientError>> + '_ {
|
||||
let mut ctx = context::current();
|
||||
ctx.deadline += Duration::from_secs(50);
|
||||
self.process_transaction_with_preflight_and_commitment_and_context(
|
||||
ctx,
|
||||
transaction,
|
||||
commitment,
|
||||
)
|
||||
.map(|result| match result? {
|
||||
BanksTransactionResultWithSimulation {
|
||||
result: None,
|
||||
simulation_details: _,
|
||||
} => Err(BanksClientError::ClientError(
|
||||
"invalid blockhash or fee-payer",
|
||||
)),
|
||||
BanksTransactionResultWithSimulation {
|
||||
result: Some(Err(err)),
|
||||
simulation_details: Some(simulation_details),
|
||||
} => Err(BanksClientError::SimulationError {
|
||||
err,
|
||||
logs: simulation_details.logs,
|
||||
units_consumed: simulation_details.units_consumed,
|
||||
return_data: simulation_details.return_data,
|
||||
}),
|
||||
BanksTransactionResultWithSimulation {
|
||||
result: Some(result),
|
||||
simulation_details: _,
|
||||
} => result.map_err(Into::into),
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a transaction and return any preflight (sanitization or simulation) errors, or return
|
||||
/// after the transaction has been finalized or rejected.
|
||||
pub fn process_transaction_with_preflight(
|
||||
&mut self,
|
||||
transaction: Transaction,
|
||||
) -> impl Future<Output = Result<(), BanksClientError>> + '_ {
|
||||
self.process_transaction_with_preflight_and_commitment(
|
||||
transaction,
|
||||
CommitmentLevel::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a transaction and return until the transaction has been finalized or rejected.
|
||||
pub fn process_transaction(
|
||||
&mut self,
|
||||
transaction: Transaction,
|
||||
) -> impl Future<Output = Result<(), BanksClientError>> + '_ {
|
||||
self.process_transaction_with_commitment(transaction, CommitmentLevel::default())
|
||||
}
|
||||
|
||||
pub async fn process_transactions_with_commitment(
|
||||
&mut self,
|
||||
transactions: Vec<Transaction>,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Result<(), BanksClientError> {
|
||||
let mut clients: Vec<_> = transactions.iter().map(|_| self.clone()).collect();
|
||||
let futures = clients
|
||||
.iter_mut()
|
||||
.zip(transactions)
|
||||
.map(|(client, transaction)| {
|
||||
client.process_transaction_with_commitment(transaction, commitment)
|
||||
});
|
||||
let statuses = join_all(futures).await;
|
||||
statuses.into_iter().collect() // Convert Vec<Result<_, _>> to Result<Vec<_>>
|
||||
}
|
||||
|
||||
/// Send transactions and return until the transaction has been finalized or rejected.
|
||||
pub fn process_transactions(
|
||||
&mut self,
|
||||
transactions: Vec<Transaction>,
|
||||
) -> impl Future<Output = Result<(), BanksClientError>> + '_ {
|
||||
self.process_transactions_with_commitment(transactions, CommitmentLevel::default())
|
||||
}
|
||||
|
||||
/// Return the most recent rooted slot. All transactions at or below this slot
|
||||
/// are said to be finalized. The cluster will not fork to a higher slot.
|
||||
pub fn get_root_slot(&mut self) -> impl Future<Output = Result<Slot, BanksClientError>> + '_ {
|
||||
self.get_slot_with_context(context::current(), CommitmentLevel::default())
|
||||
}
|
||||
|
||||
/// Return the most recent rooted block height. All transactions at or below this height
|
||||
/// are said to be finalized. The cluster will not fork to a higher block height.
|
||||
pub fn get_root_block_height(
|
||||
&mut self,
|
||||
) -> impl Future<Output = Result<Slot, BanksClientError>> + '_ {
|
||||
self.get_block_height_with_context(context::current(), CommitmentLevel::default())
|
||||
}
|
||||
|
||||
/// Return the account at the given address at the slot corresponding to the given
|
||||
/// commitment level. If the account is not found, None is returned.
|
||||
pub fn get_account_with_commitment(
|
||||
&mut self,
|
||||
address: Pubkey,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<Option<Account>, BanksClientError>> + '_ {
|
||||
self.get_account_with_commitment_and_context(context::current(), address, commitment)
|
||||
}
|
||||
|
||||
/// Return the account at the given address at the time of the most recent root slot.
|
||||
/// If the account is not found, None is returned.
|
||||
pub fn get_account(
|
||||
&mut self,
|
||||
address: Pubkey,
|
||||
) -> impl Future<Output = Result<Option<Account>, BanksClientError>> + '_ {
|
||||
self.get_account_with_commitment(address, CommitmentLevel::default())
|
||||
}
|
||||
|
||||
/// Return the unpacked account data at the given address
|
||||
/// If the account is not found, an error is returned
|
||||
pub fn get_packed_account_data<T: Pack>(
|
||||
&mut self,
|
||||
address: Pubkey,
|
||||
) -> impl Future<Output = Result<T, BanksClientError>> + '_ {
|
||||
self.get_account(address).map(|result| {
|
||||
let account = result?.ok_or(BanksClientError::ClientError("Account not found"))?;
|
||||
T::unpack_from_slice(&account.data)
|
||||
.map_err(|_| BanksClientError::ClientError("Failed to deserialize account"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the unpacked account data at the given address
|
||||
/// If the account is not found, an error is returned
|
||||
pub fn get_account_data_with_borsh<T: BorshDeserialize>(
|
||||
&mut self,
|
||||
address: Pubkey,
|
||||
) -> impl Future<Output = Result<T, BanksClientError>> + '_ {
|
||||
self.get_account(address).map(|result| {
|
||||
let account = result?.ok_or(BanksClientError::ClientError("Account not found"))?;
|
||||
T::try_from_slice(&account.data).map_err(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the balance in lamports of an account at the given address at the slot
|
||||
/// corresponding to the given commitment level.
|
||||
pub fn get_balance_with_commitment(
|
||||
&mut self,
|
||||
address: Pubkey,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<u64, BanksClientError>> + '_ {
|
||||
self.get_account_with_commitment_and_context(context::current(), address, commitment)
|
||||
.map(|result| Ok(result?.map(|x| x.lamports).unwrap_or(0)))
|
||||
}
|
||||
|
||||
/// Return the balance in lamports of an account at the given address at the time
|
||||
/// of the most recent root slot.
|
||||
pub fn get_balance(
|
||||
&mut self,
|
||||
address: Pubkey,
|
||||
) -> impl Future<Output = Result<u64, BanksClientError>> + '_ {
|
||||
self.get_balance_with_commitment(address, CommitmentLevel::default())
|
||||
}
|
||||
|
||||
/// Return the status of a transaction with a signature matching the transaction's first
|
||||
/// signature. Return None if the transaction is not found, which may be because the
|
||||
/// blockhash was expired or the fee-paying account had insufficient funds to pay the
|
||||
/// transaction fee. Note that servers rarely store the full transaction history. This
|
||||
/// method may return None if the transaction status has been discarded.
|
||||
pub fn get_transaction_status(
|
||||
&mut self,
|
||||
signature: Signature,
|
||||
) -> impl Future<Output = Result<Option<TransactionStatus>, BanksClientError>> + '_ {
|
||||
self.get_transaction_status_with_context(context::current(), signature)
|
||||
}
|
||||
|
||||
/// Same as get_transaction_status, but for multiple transactions.
|
||||
pub async fn get_transaction_statuses(
|
||||
&mut self,
|
||||
signatures: Vec<Signature>,
|
||||
) -> Result<Vec<Option<TransactionStatus>>, BanksClientError> {
|
||||
// tarpc futures oddly hold a mutable reference back to the client so clone the client upfront
|
||||
let mut clients_and_signatures: Vec<_> = signatures
|
||||
.into_iter()
|
||||
.map(|signature| (self.clone(), signature))
|
||||
.collect();
|
||||
|
||||
let futs = clients_and_signatures
|
||||
.iter_mut()
|
||||
.map(|(client, signature)| client.get_transaction_status(*signature));
|
||||
|
||||
let statuses = join_all(futs).await;
|
||||
|
||||
// Convert Vec<Result<_, _>> to Result<Vec<_>>
|
||||
statuses.into_iter().collect()
|
||||
}
|
||||
|
||||
pub fn get_latest_blockhash(
|
||||
&mut self,
|
||||
) -> impl Future<Output = Result<Hash, BanksClientError>> + '_ {
|
||||
self.get_latest_blockhash_with_commitment(CommitmentLevel::default())
|
||||
.map(|result| {
|
||||
result?
|
||||
.map(|x| x.0)
|
||||
.ok_or(BanksClientError::ClientError("valid blockhash not found"))
|
||||
.map_err(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_latest_blockhash_with_commitment(
|
||||
&mut self,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<Option<(Hash, u64)>, BanksClientError>> + '_ {
|
||||
self.get_latest_blockhash_with_commitment_and_context(context::current(), commitment)
|
||||
}
|
||||
|
||||
pub fn get_latest_blockhash_with_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> impl Future<Output = Result<Option<(Hash, u64)>, BanksClientError>> + '_ {
|
||||
self.inner
|
||||
.get_latest_blockhash_with_commitment_and_context(ctx, commitment)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_fee_for_message_with_commitment_and_context(
|
||||
&mut self,
|
||||
ctx: Context,
|
||||
commitment: CommitmentLevel,
|
||||
message: Message,
|
||||
) -> impl Future<Output = Result<Option<u64>, BanksClientError>> + '_ {
|
||||
self.inner
|
||||
.get_fee_for_message_with_commitment_and_context(ctx, commitment, message)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_client<C>(transport: C) -> Result<BanksClient, BanksClientError>
|
||||
where
|
||||
C: Transport<ClientMessage<BanksRequest>, Response<BanksResponse>> + Send + 'static,
|
||||
{
|
||||
Ok(BanksClient {
|
||||
inner: TarpcClient::new(client::Config::default(), transport).spawn(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start_tcp_client<T: ToSocketAddrs>(addr: T) -> Result<BanksClient, BanksClientError> {
|
||||
let transport = tcp::connect(addr, Bincode::default).await?;
|
||||
Ok(BanksClient {
|
||||
inner: TarpcClient::new(client::Config::default(), transport).spawn(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::*,
|
||||
solana_banks_server::banks_server::start_local_server,
|
||||
solana_runtime::{
|
||||
bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache,
|
||||
genesis_utils::create_genesis_config,
|
||||
},
|
||||
solana_sdk::{message::Message, signature::Signer, system_instruction},
|
||||
std::sync::{Arc, RwLock},
|
||||
tarpc::transport,
|
||||
tokio::{runtime::Runtime, time::sleep},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_banks_client_new() {
|
||||
let (client_transport, _server_transport) = transport::channel::unbounded();
|
||||
BanksClient::new(client::Config::default(), client_transport);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_banks_server_transfer_via_server() -> Result<(), BanksClientError> {
|
||||
// This test shows the preferred way to interact with BanksServer.
|
||||
// It creates a runtime explicitly (no globals via tokio macros) and calls
|
||||
// `runtime.block_on()` just once, to run all the async code.
|
||||
|
||||
let genesis = create_genesis_config(10);
|
||||
let bank = Bank::new_for_tests(&genesis.genesis_config);
|
||||
let slot = bank.slot();
|
||||
let block_commitment_cache = Arc::new(RwLock::new(
|
||||
BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
|
||||
));
|
||||
let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
|
||||
|
||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let mint_pubkey = genesis.mint_keypair.pubkey();
|
||||
let instruction = system_instruction::transfer(&mint_pubkey, &bob_pubkey, 1);
|
||||
let message = Message::new(&[instruction], Some(&mint_pubkey));
|
||||
|
||||
Runtime::new()?.block_on(async {
|
||||
let client_transport =
|
||||
start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
|
||||
.await;
|
||||
let mut banks_client = start_client(client_transport).await?;
|
||||
|
||||
let recent_blockhash = banks_client.get_latest_blockhash().await?;
|
||||
let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_banks_server_transfer_via_client() -> Result<(), BanksClientError> {
|
||||
// The caller may not want to hold the connection open until the transaction
|
||||
// is processed (or blockhash expires). In this test, we verify the
|
||||
// server-side functionality is available to the client.
|
||||
|
||||
let genesis = create_genesis_config(10);
|
||||
let bank = Bank::new_for_tests(&genesis.genesis_config);
|
||||
let slot = bank.slot();
|
||||
let block_commitment_cache = Arc::new(RwLock::new(
|
||||
BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
|
||||
));
|
||||
let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
|
||||
|
||||
let mint_pubkey = &genesis.mint_keypair.pubkey();
|
||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
||||
let instruction = system_instruction::transfer(mint_pubkey, &bob_pubkey, 1);
|
||||
let message = Message::new(&[instruction], Some(mint_pubkey));
|
||||
|
||||
Runtime::new()?.block_on(async {
|
||||
let client_transport =
|
||||
start_local_server(bank_forks, block_commitment_cache, Duration::from_millis(1))
|
||||
.await;
|
||||
let mut banks_client = start_client(client_transport).await?;
|
||||
let (recent_blockhash, last_valid_block_height) = banks_client
|
||||
.get_latest_blockhash_with_commitment(CommitmentLevel::default())
|
||||
.await?
|
||||
.unwrap();
|
||||
let transaction = Transaction::new(&[&genesis.mint_keypair], message, recent_blockhash);
|
||||
let signature = transaction.signatures[0];
|
||||
banks_client.send_transaction(transaction).await?;
|
||||
|
||||
let mut status = banks_client.get_transaction_status(signature).await?;
|
||||
|
||||
while status.is_none() {
|
||||
let root_block_height = banks_client.get_root_block_height().await?;
|
||||
if root_block_height > last_valid_block_height {
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
status = banks_client.get_transaction_status(signature).await?;
|
||||
}
|
||||
assert!(status.unwrap().err.is_none());
|
||||
assert_eq!(banks_client.get_balance(bob_pubkey).await?, 1);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "solana-banks-interface"
|
||||
version = "1.11.0"
|
||||
description = "Solana banks RPC interface"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-banks-interface"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
tarpc = { version = "0.27.2", features = ["full"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_banks_interface"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,96 +0,0 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use {
|
||||
serde::{Deserialize, Serialize},
|
||||
solana_sdk::{
|
||||
account::Account,
|
||||
clock::Slot,
|
||||
commitment_config::CommitmentLevel,
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
transaction::{self, Transaction, TransactionError},
|
||||
transaction_context::TransactionReturnData,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TransactionConfirmationStatus {
|
||||
Processed,
|
||||
Confirmed,
|
||||
Finalized,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TransactionStatus {
|
||||
pub slot: Slot,
|
||||
pub confirmations: Option<usize>, // None = rooted
|
||||
pub err: Option<TransactionError>,
|
||||
pub confirmation_status: Option<TransactionConfirmationStatus>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransactionSimulationDetails {
|
||||
pub logs: Vec<String>,
|
||||
pub units_consumed: u64,
|
||||
pub return_data: Option<TransactionReturnData>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BanksTransactionResultWithSimulation {
|
||||
pub result: Option<transaction::Result<()>>,
|
||||
pub simulation_details: Option<TransactionSimulationDetails>,
|
||||
}
|
||||
|
||||
#[tarpc::service]
|
||||
pub trait Banks {
|
||||
async fn send_transaction_with_context(transaction: Transaction);
|
||||
#[deprecated(
|
||||
since = "1.9.0",
|
||||
note = "Please use `get_fee_for_message_with_commitment_and_context` instead"
|
||||
)]
|
||||
async fn get_fees_with_commitment_and_context(
|
||||
commitment: CommitmentLevel,
|
||||
) -> (FeeCalculator, Hash, Slot);
|
||||
async fn get_transaction_status_with_context(signature: Signature)
|
||||
-> Option<TransactionStatus>;
|
||||
async fn get_slot_with_context(commitment: CommitmentLevel) -> Slot;
|
||||
async fn get_block_height_with_context(commitment: CommitmentLevel) -> u64;
|
||||
async fn process_transaction_with_preflight_and_commitment_and_context(
|
||||
transaction: Transaction,
|
||||
commitment: CommitmentLevel,
|
||||
) -> BanksTransactionResultWithSimulation;
|
||||
async fn process_transaction_with_commitment_and_context(
|
||||
transaction: Transaction,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<transaction::Result<()>>;
|
||||
async fn get_account_with_commitment_and_context(
|
||||
address: Pubkey,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<Account>;
|
||||
async fn get_latest_blockhash_with_context() -> Hash;
|
||||
async fn get_latest_blockhash_with_commitment_and_context(
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<(Hash, u64)>;
|
||||
async fn get_fee_for_message_with_commitment_and_context(
|
||||
commitment: CommitmentLevel,
|
||||
message: Message,
|
||||
) -> Option<u64>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::*,
|
||||
tarpc::{client, transport},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_banks_client_new() {
|
||||
let (client_transport, _server_transport) = transport::channel::unbounded();
|
||||
BanksClient::new(client::Config::default(), client_transport);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "solana-banks-server"
|
||||
version = "1.11.0"
|
||||
description = "Solana banks server"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-banks-server"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.3"
|
||||
crossbeam-channel = "0.5"
|
||||
futures = "0.3"
|
||||
solana-banks-interface = { path = "../banks-interface", version = "=1.11.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-send-transaction-service = { path = "../send-transaction-service", version = "=1.11.0" }
|
||||
tarpc = { version = "0.27.2", features = ["full"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-serde = { version = "0.8", features = ["bincode"] }
|
||||
tokio-stream = "0.1"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_banks_server"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,421 +0,0 @@
|
||||
use {
|
||||
bincode::{deserialize, serialize},
|
||||
crossbeam_channel::{unbounded, Receiver, Sender},
|
||||
futures::{future, prelude::stream::StreamExt},
|
||||
solana_banks_interface::{
|
||||
Banks, BanksRequest, BanksResponse, BanksTransactionResultWithSimulation,
|
||||
TransactionConfirmationStatus, TransactionSimulationDetails, TransactionStatus,
|
||||
},
|
||||
solana_runtime::{
|
||||
bank::{Bank, TransactionSimulationResult},
|
||||
bank_forks::BankForks,
|
||||
commitment::BlockCommitmentCache,
|
||||
},
|
||||
solana_sdk::{
|
||||
account::Account,
|
||||
clock::Slot,
|
||||
commitment_config::CommitmentLevel,
|
||||
feature_set::FeatureSet,
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
message::{Message, SanitizedMessage},
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
transaction::{self, SanitizedTransaction, Transaction},
|
||||
},
|
||||
solana_send_transaction_service::{
|
||||
send_transaction_service::{SendTransactionService, TransactionInfo, DEFAULT_TPU_USE_QUIC},
|
||||
tpu_info::NullTpuInfo,
|
||||
},
|
||||
std::{
|
||||
convert::TryFrom,
|
||||
io,
|
||||
net::{Ipv4Addr, SocketAddr},
|
||||
sync::{Arc, RwLock},
|
||||
thread::Builder,
|
||||
time::Duration,
|
||||
},
|
||||
tarpc::{
|
||||
context::Context,
|
||||
serde_transport::tcp,
|
||||
server::{self, incoming::Incoming, Channel},
|
||||
transport::{self, channel::UnboundedChannel},
|
||||
ClientMessage, Response,
|
||||
},
|
||||
tokio::time::sleep,
|
||||
tokio_serde::formats::Bincode,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BanksServer {
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
||||
transaction_sender: Sender<TransactionInfo>,
|
||||
poll_signature_status_sleep_duration: Duration,
|
||||
}
|
||||
|
||||
impl BanksServer {
|
||||
/// Return a BanksServer that forwards transactions to the
|
||||
/// given sender. If unit-testing, those transactions can go to
|
||||
/// a bank in the given BankForks. Otherwise, the receiver should
|
||||
/// forward them to a validator in the leader schedule.
|
||||
fn new(
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
||||
transaction_sender: Sender<TransactionInfo>,
|
||||
poll_signature_status_sleep_duration: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
bank_forks,
|
||||
block_commitment_cache,
|
||||
transaction_sender,
|
||||
poll_signature_status_sleep_duration,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(bank_forks: Arc<RwLock<BankForks>>, transaction_receiver: Receiver<TransactionInfo>) {
|
||||
while let Ok(info) = transaction_receiver.recv() {
|
||||
let mut transaction_infos = vec![info];
|
||||
while let Ok(info) = transaction_receiver.try_recv() {
|
||||
transaction_infos.push(info);
|
||||
}
|
||||
let transactions: Vec<_> = transaction_infos
|
||||
.into_iter()
|
||||
.map(|info| deserialize(&info.wire_transaction).unwrap())
|
||||
.collect();
|
||||
let bank = bank_forks.read().unwrap().working_bank();
|
||||
let _ = bank.try_process_transactions(transactions.iter());
|
||||
}
|
||||
}
|
||||
|
||||
/// Useful for unit-testing
|
||||
fn new_loopback(
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
||||
poll_signature_status_sleep_duration: Duration,
|
||||
) -> Self {
|
||||
let (transaction_sender, transaction_receiver) = unbounded();
|
||||
let bank = bank_forks.read().unwrap().working_bank();
|
||||
let slot = bank.slot();
|
||||
{
|
||||
// ensure that the commitment cache and bank are synced
|
||||
let mut w_block_commitment_cache = block_commitment_cache.write().unwrap();
|
||||
w_block_commitment_cache.set_all_slots(slot, slot);
|
||||
}
|
||||
let server_bank_forks = bank_forks.clone();
|
||||
Builder::new()
|
||||
.name("solana-bank-forks-client".to_string())
|
||||
.spawn(move || Self::run(server_bank_forks, transaction_receiver))
|
||||
.unwrap();
|
||||
Self::new(
|
||||
bank_forks,
|
||||
block_commitment_cache,
|
||||
transaction_sender,
|
||||
poll_signature_status_sleep_duration,
|
||||
)
|
||||
}
|
||||
|
||||
fn slot(&self, commitment: CommitmentLevel) -> Slot {
|
||||
self.block_commitment_cache
|
||||
.read()
|
||||
.unwrap()
|
||||
.slot_with_commitment(commitment)
|
||||
}
|
||||
|
||||
fn bank(&self, commitment: CommitmentLevel) -> Arc<Bank> {
|
||||
self.bank_forks.read().unwrap()[self.slot(commitment)].clone()
|
||||
}
|
||||
|
||||
async fn poll_signature_status(
|
||||
self,
|
||||
signature: &Signature,
|
||||
blockhash: &Hash,
|
||||
last_valid_block_height: u64,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<transaction::Result<()>> {
|
||||
let mut status = self
|
||||
.bank(commitment)
|
||||
.get_signature_status_with_blockhash(signature, blockhash);
|
||||
while status.is_none() {
|
||||
sleep(self.poll_signature_status_sleep_duration).await;
|
||||
let bank = self.bank(commitment);
|
||||
if bank.block_height() > last_valid_block_height {
|
||||
break;
|
||||
}
|
||||
status = bank.get_signature_status_with_blockhash(signature, blockhash);
|
||||
}
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_transaction(
|
||||
transaction: &Transaction,
|
||||
feature_set: &Arc<FeatureSet>,
|
||||
) -> transaction::Result<()> {
|
||||
if let Err(err) = transaction.verify() {
|
||||
Err(err)
|
||||
} else if let Err(err) = transaction.verify_precompiles(feature_set) {
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tarpc::server]
|
||||
impl Banks for BanksServer {
|
||||
async fn send_transaction_with_context(self, _: Context, transaction: Transaction) {
|
||||
let blockhash = &transaction.message.recent_blockhash;
|
||||
let last_valid_block_height = self
|
||||
.bank_forks
|
||||
.read()
|
||||
.unwrap()
|
||||
.root_bank()
|
||||
.get_blockhash_last_valid_block_height(blockhash)
|
||||
.unwrap();
|
||||
let signature = transaction.signatures.get(0).cloned().unwrap_or_default();
|
||||
let info = TransactionInfo::new(
|
||||
signature,
|
||||
serialize(&transaction).unwrap(),
|
||||
last_valid_block_height,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
self.transaction_sender.send(info).unwrap();
|
||||
}
|
||||
|
||||
async fn get_fees_with_commitment_and_context(
|
||||
self,
|
||||
_: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> (FeeCalculator, Hash, u64) {
|
||||
let bank = self.bank(commitment);
|
||||
let blockhash = bank.last_blockhash();
|
||||
let lamports_per_signature = bank.get_lamports_per_signature();
|
||||
let last_valid_block_height = bank
|
||||
.get_blockhash_last_valid_block_height(&blockhash)
|
||||
.unwrap();
|
||||
(
|
||||
FeeCalculator::new(lamports_per_signature),
|
||||
blockhash,
|
||||
last_valid_block_height,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_transaction_status_with_context(
|
||||
self,
|
||||
_: Context,
|
||||
signature: Signature,
|
||||
) -> Option<TransactionStatus> {
|
||||
let bank = self.bank(CommitmentLevel::Processed);
|
||||
let (slot, status) = bank.get_signature_status_slot(&signature)?;
|
||||
let r_block_commitment_cache = self.block_commitment_cache.read().unwrap();
|
||||
|
||||
let optimistically_confirmed_bank = self.bank(CommitmentLevel::Confirmed);
|
||||
let optimistically_confirmed =
|
||||
optimistically_confirmed_bank.get_signature_status_slot(&signature);
|
||||
|
||||
let confirmations = if r_block_commitment_cache.root() >= slot
|
||||
&& r_block_commitment_cache.highest_confirmed_root() >= slot
|
||||
{
|
||||
None
|
||||
} else {
|
||||
r_block_commitment_cache
|
||||
.get_confirmation_count(slot)
|
||||
.or(Some(0))
|
||||
};
|
||||
Some(TransactionStatus {
|
||||
slot,
|
||||
confirmations,
|
||||
err: status.err(),
|
||||
confirmation_status: if confirmations.is_none() {
|
||||
Some(TransactionConfirmationStatus::Finalized)
|
||||
} else if optimistically_confirmed.is_some() {
|
||||
Some(TransactionConfirmationStatus::Confirmed)
|
||||
} else {
|
||||
Some(TransactionConfirmationStatus::Processed)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_slot_with_context(self, _: Context, commitment: CommitmentLevel) -> Slot {
|
||||
self.slot(commitment)
|
||||
}
|
||||
|
||||
async fn get_block_height_with_context(self, _: Context, commitment: CommitmentLevel) -> u64 {
|
||||
self.bank(commitment).block_height()
|
||||
}
|
||||
|
||||
async fn process_transaction_with_preflight_and_commitment_and_context(
|
||||
self,
|
||||
ctx: Context,
|
||||
transaction: Transaction,
|
||||
commitment: CommitmentLevel,
|
||||
) -> BanksTransactionResultWithSimulation {
|
||||
let sanitized_transaction =
|
||||
match SanitizedTransaction::try_from_legacy_transaction(transaction.clone()) {
|
||||
Err(err) => {
|
||||
return BanksTransactionResultWithSimulation {
|
||||
result: Some(Err(err)),
|
||||
simulation_details: None,
|
||||
};
|
||||
}
|
||||
Ok(tx) => tx,
|
||||
};
|
||||
if let TransactionSimulationResult {
|
||||
result: Err(err),
|
||||
logs,
|
||||
post_simulation_accounts: _,
|
||||
units_consumed,
|
||||
return_data,
|
||||
} = self
|
||||
.bank(commitment)
|
||||
.simulate_transaction_unchecked(sanitized_transaction)
|
||||
{
|
||||
return BanksTransactionResultWithSimulation {
|
||||
result: Some(Err(err)),
|
||||
simulation_details: Some(TransactionSimulationDetails {
|
||||
logs,
|
||||
units_consumed,
|
||||
return_data,
|
||||
}),
|
||||
};
|
||||
}
|
||||
BanksTransactionResultWithSimulation {
|
||||
result: self
|
||||
.process_transaction_with_commitment_and_context(ctx, transaction, commitment)
|
||||
.await,
|
||||
simulation_details: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_transaction_with_commitment_and_context(
|
||||
self,
|
||||
_: Context,
|
||||
transaction: Transaction,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<transaction::Result<()>> {
|
||||
if let Err(err) = verify_transaction(&transaction, &self.bank(commitment).feature_set) {
|
||||
return Some(Err(err));
|
||||
}
|
||||
|
||||
let blockhash = &transaction.message.recent_blockhash;
|
||||
let last_valid_block_height = self
|
||||
.bank(commitment)
|
||||
.get_blockhash_last_valid_block_height(blockhash)
|
||||
.unwrap();
|
||||
let signature = transaction.signatures.get(0).cloned().unwrap_or_default();
|
||||
let info = TransactionInfo::new(
|
||||
signature,
|
||||
serialize(&transaction).unwrap(),
|
||||
last_valid_block_height,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
self.transaction_sender.send(info).unwrap();
|
||||
self.poll_signature_status(&signature, blockhash, last_valid_block_height, commitment)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_account_with_commitment_and_context(
|
||||
self,
|
||||
_: Context,
|
||||
address: Pubkey,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<Account> {
|
||||
let bank = self.bank(commitment);
|
||||
bank.get_account(&address).map(Account::from)
|
||||
}
|
||||
|
||||
async fn get_latest_blockhash_with_context(self, _: Context) -> Hash {
|
||||
let bank = self.bank(CommitmentLevel::default());
|
||||
bank.last_blockhash()
|
||||
}
|
||||
|
||||
async fn get_latest_blockhash_with_commitment_and_context(
|
||||
self,
|
||||
_: Context,
|
||||
commitment: CommitmentLevel,
|
||||
) -> Option<(Hash, u64)> {
|
||||
let bank = self.bank(commitment);
|
||||
let blockhash = bank.last_blockhash();
|
||||
let last_valid_block_height = bank.get_blockhash_last_valid_block_height(&blockhash)?;
|
||||
Some((blockhash, last_valid_block_height))
|
||||
}
|
||||
|
||||
async fn get_fee_for_message_with_commitment_and_context(
|
||||
self,
|
||||
_: Context,
|
||||
commitment: CommitmentLevel,
|
||||
message: Message,
|
||||
) -> Option<u64> {
|
||||
let bank = self.bank(commitment);
|
||||
let sanitized_message = SanitizedMessage::try_from(message).ok()?;
|
||||
bank.get_fee_for_message(&sanitized_message)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_local_server(
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
||||
poll_signature_status_sleep_duration: Duration,
|
||||
) -> UnboundedChannel<Response<BanksResponse>, ClientMessage<BanksRequest>> {
|
||||
let banks_server = BanksServer::new_loopback(
|
||||
bank_forks,
|
||||
block_commitment_cache,
|
||||
poll_signature_status_sleep_duration,
|
||||
);
|
||||
let (client_transport, server_transport) = transport::channel::unbounded();
|
||||
let server = server::BaseChannel::with_defaults(server_transport).execute(banks_server.serve());
|
||||
tokio::spawn(server);
|
||||
client_transport
|
||||
}
|
||||
|
||||
pub async fn start_tcp_server(
|
||||
listen_addr: SocketAddr,
|
||||
tpu_addr: SocketAddr,
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
||||
) -> io::Result<()> {
|
||||
// Note: These settings are copied straight from the tarpc example.
|
||||
let server = tcp::listen(listen_addr, Bincode::default)
|
||||
.await?
|
||||
// Ignore accept errors.
|
||||
.filter_map(|r| future::ready(r.ok()))
|
||||
.map(server::BaseChannel::with_defaults)
|
||||
// Limit channels to 1 per IP.
|
||||
.max_channels_per_key(1, |t| {
|
||||
t.as_ref()
|
||||
.peer_addr()
|
||||
.map(|x| x.ip())
|
||||
.unwrap_or_else(|_| Ipv4Addr::new(0, 0, 0, 0).into())
|
||||
})
|
||||
// serve is generated by the service attribute. It takes as input any type implementing
|
||||
// the generated Banks trait.
|
||||
.map(move |chan| {
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
SendTransactionService::new::<NullTpuInfo>(
|
||||
tpu_addr,
|
||||
&bank_forks,
|
||||
None,
|
||||
receiver,
|
||||
5_000,
|
||||
0,
|
||||
DEFAULT_TPU_USE_QUIC,
|
||||
);
|
||||
|
||||
let server = BanksServer::new(
|
||||
bank_forks.clone(),
|
||||
block_commitment_cache.clone(),
|
||||
sender,
|
||||
Duration::from_millis(200),
|
||||
);
|
||||
chan.execute(server.serve())
|
||||
})
|
||||
// Max 10 channels.
|
||||
.buffer_unordered(10)
|
||||
.for_each(|_| async {});
|
||||
|
||||
server.await;
|
||||
Ok(())
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
pub mod banks_server;
|
||||
pub mod rpc_banks_service;
|
@ -1,118 +0,0 @@
|
||||
//! The `rpc_banks_service` module implements the Solana Banks RPC API.
|
||||
|
||||
use {
|
||||
crate::banks_server::start_tcp_server,
|
||||
futures::{future::FutureExt, pin_mut, prelude::stream::StreamExt, select},
|
||||
solana_runtime::{bank_forks::BankForks, commitment::BlockCommitmentCache},
|
||||
std::{
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread::{self, Builder, JoinHandle},
|
||||
},
|
||||
tokio::{
|
||||
runtime::Runtime,
|
||||
time::{self, Duration},
|
||||
},
|
||||
tokio_stream::wrappers::IntervalStream,
|
||||
};
|
||||
|
||||
pub struct RpcBanksService {
|
||||
thread_hdl: JoinHandle<()>,
|
||||
}
|
||||
|
||||
/// Run the TCP service until `exit` is set to true
|
||||
async fn start_abortable_tcp_server(
|
||||
listen_addr: SocketAddr,
|
||||
tpu_addr: SocketAddr,
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
||||
exit: Arc<AtomicBool>,
|
||||
) {
|
||||
let server = start_tcp_server(
|
||||
listen_addr,
|
||||
tpu_addr,
|
||||
bank_forks.clone(),
|
||||
block_commitment_cache.clone(),
|
||||
)
|
||||
.fuse();
|
||||
let interval = IntervalStream::new(time::interval(Duration::from_millis(100))).fuse();
|
||||
pin_mut!(server, interval);
|
||||
loop {
|
||||
select! {
|
||||
_ = server => {},
|
||||
_ = interval.select_next_some() => {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcBanksService {
|
||||
fn run(
|
||||
listen_addr: SocketAddr,
|
||||
tpu_addr: SocketAddr,
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
||||
exit: Arc<AtomicBool>,
|
||||
) {
|
||||
let server = start_abortable_tcp_server(
|
||||
listen_addr,
|
||||
tpu_addr,
|
||||
bank_forks,
|
||||
block_commitment_cache,
|
||||
exit,
|
||||
);
|
||||
Runtime::new().unwrap().block_on(server);
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
listen_addr: SocketAddr,
|
||||
tpu_addr: SocketAddr,
|
||||
bank_forks: &Arc<RwLock<BankForks>>,
|
||||
block_commitment_cache: &Arc<RwLock<BlockCommitmentCache>>,
|
||||
exit: &Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
let bank_forks = bank_forks.clone();
|
||||
let block_commitment_cache = block_commitment_cache.clone();
|
||||
let exit = exit.clone();
|
||||
let thread_hdl = Builder::new()
|
||||
.name("solana-rpc-banks".to_string())
|
||||
.spawn(move || {
|
||||
Self::run(
|
||||
listen_addr,
|
||||
tpu_addr,
|
||||
bank_forks,
|
||||
block_commitment_cache,
|
||||
exit,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Self { thread_hdl }
|
||||
}
|
||||
|
||||
pub fn join(self) -> thread::Result<()> {
|
||||
self.thread_hdl.join()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {super::*, solana_runtime::bank::Bank};
|
||||
|
||||
#[test]
|
||||
fn test_rpc_banks_server_exit() {
|
||||
let bank_forks = Arc::new(RwLock::new(BankForks::new(Bank::default_for_tests())));
|
||||
let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default()));
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
let addr = "127.0.0.1:0".parse().unwrap();
|
||||
let service = RpcBanksService::new(addr, addr, &bank_forks, &block_commitment_cache, &exit);
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
service.join().unwrap();
|
||||
}
|
||||
}
|
2
bench-streamer/.gitignore
vendored
2
bench-streamer/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
/target/
|
||||
/farf/
|
@ -1,19 +0,0 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
name = "solana-bench-streamer"
|
||||
version = "1.11.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel = "0.5"
|
||||
clap = { version = "3.1.5", features = ["cargo"] }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.11.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.11.0" }
|
||||
solana-version = { path = "../version", version = "=1.11.0" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,147 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
use {
|
||||
clap::{crate_description, crate_name, Arg, Command},
|
||||
crossbeam_channel::unbounded,
|
||||
solana_streamer::{
|
||||
packet::{Packet, PacketBatch, PacketBatchRecycler, PACKET_DATA_SIZE},
|
||||
streamer::{receiver, PacketBatchReceiver},
|
||||
},
|
||||
std::{
|
||||
cmp::max,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::{sleep, spawn, JoinHandle, Result},
|
||||
time::{Duration, SystemTime},
|
||||
},
|
||||
};
|
||||
|
||||
fn producer(addr: &SocketAddr, exit: Arc<AtomicBool>) -> JoinHandle<()> {
|
||||
let send = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
let mut packet_batch = PacketBatch::default();
|
||||
packet_batch.packets.resize(10, Packet::default());
|
||||
for w in packet_batch.packets.iter_mut() {
|
||||
w.meta.size = PACKET_DATA_SIZE;
|
||||
w.meta.set_addr(addr);
|
||||
}
|
||||
let packet_batch = Arc::new(packet_batch);
|
||||
spawn(move || loop {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let mut num = 0;
|
||||
for p in &packet_batch.packets {
|
||||
let a = p.meta.addr();
|
||||
assert!(p.meta.size <= PACKET_DATA_SIZE);
|
||||
send.send_to(&p.data[..p.meta.size], &a).unwrap();
|
||||
num += 1;
|
||||
}
|
||||
assert_eq!(num, 10);
|
||||
})
|
||||
}
|
||||
|
||||
fn sink(exit: Arc<AtomicBool>, rvs: Arc<AtomicUsize>, r: PacketBatchReceiver) -> JoinHandle<()> {
|
||||
spawn(move || loop {
|
||||
if exit.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let timer = Duration::new(1, 0);
|
||||
if let Ok(packet_batch) = r.recv_timeout(timer) {
|
||||
rvs.fetch_add(packet_batch.packets.len(), Ordering::Relaxed);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut num_sockets = 1usize;
|
||||
|
||||
let matches = Command::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.version(solana_version::version!())
|
||||
.arg(
|
||||
Arg::new("num-recv-sockets")
|
||||
.long("num-recv-sockets")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Use NUM receive sockets"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("num-producers")
|
||||
.long("num-producers")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Use this many producer threads."),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if let Some(n) = matches.value_of("num-recv-sockets") {
|
||||
num_sockets = max(num_sockets, n.to_string().parse().expect("integer"));
|
||||
}
|
||||
|
||||
let num_producers: u64 = matches.value_of_t("num_producers").unwrap_or(4);
|
||||
|
||||
let port = 0;
|
||||
let ip_addr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
|
||||
let mut addr = SocketAddr::new(ip_addr, 0);
|
||||
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut read_channels = Vec::new();
|
||||
let mut read_threads = Vec::new();
|
||||
let recycler = PacketBatchRecycler::default();
|
||||
let (_port, read_sockets) = solana_net_utils::multi_bind_in_range(
|
||||
ip_addr,
|
||||
(port, port + num_sockets as u16),
|
||||
num_sockets,
|
||||
)
|
||||
.unwrap();
|
||||
for read in read_sockets {
|
||||
read.set_read_timeout(Some(Duration::new(1, 0))).unwrap();
|
||||
|
||||
addr = read.local_addr().unwrap();
|
||||
let (s_reader, r_reader) = unbounded();
|
||||
read_channels.push(r_reader);
|
||||
read_threads.push(receiver(
|
||||
Arc::new(read),
|
||||
&exit,
|
||||
s_reader,
|
||||
recycler.clone(),
|
||||
"bench-streamer-test",
|
||||
1,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
let producer_threads: Vec<_> = (0..num_producers)
|
||||
.into_iter()
|
||||
.map(|_| producer(&addr, exit.clone()))
|
||||
.collect();
|
||||
|
||||
let rvs = Arc::new(AtomicUsize::new(0));
|
||||
let sink_threads: Vec<_> = read_channels
|
||||
.into_iter()
|
||||
.map(|r_reader| sink(exit.clone(), rvs.clone(), r_reader))
|
||||
.collect();
|
||||
let start = SystemTime::now();
|
||||
let start_val = rvs.load(Ordering::Relaxed);
|
||||
sleep(Duration::new(5, 0));
|
||||
let elapsed = start.elapsed().unwrap();
|
||||
let end_val = rvs.load(Ordering::Relaxed);
|
||||
let time = elapsed.as_secs() * 10_000_000_000 + u64::from(elapsed.subsec_nanos());
|
||||
let ftime = (time as f64) / 10_000_000_000_f64;
|
||||
let fcount = (end_val - start_val) as f64;
|
||||
println!("performance: {:?}", fcount / ftime);
|
||||
exit.store(true, Ordering::Relaxed);
|
||||
for t_reader in read_threads {
|
||||
t_reader.join()?;
|
||||
}
|
||||
for t_producer in producer_threads {
|
||||
t_producer.join()?;
|
||||
}
|
||||
for t_sink in sink_threads {
|
||||
t_sink.join()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
4
bench-tps/.gitignore
vendored
4
bench-tps/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
/target/
|
||||
/config/
|
||||
/config-local/
|
||||
/farf/
|
@ -1,42 +0,0 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
edition = "2021"
|
||||
name = "solana-bench-tps"
|
||||
version = "1.11.0"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.1"
|
||||
crossbeam-channel = "0.5"
|
||||
log = "0.4.14"
|
||||
rayon = "1.5.1"
|
||||
serde_json = "1.0.79"
|
||||
serde_yaml = "0.8.23"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.11.0" }
|
||||
solana-cli-config = { path = "../cli-config", version = "=1.11.0" }
|
||||
solana-client = { path = "../client", version = "=1.11.0" }
|
||||
solana-core = { path = "../core", version = "=1.11.0" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.11.0" }
|
||||
solana-genesis = { path = "../genesis", version = "=1.11.0" }
|
||||
solana-gossip = { path = "../gossip", version = "=1.11.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.11.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.11.0" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.11.0" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.11.0" }
|
||||
solana-rpc = { path = "../rpc", version = "=1.11.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.11.0" }
|
||||
solana-version = { path = "../version", version = "=1.11.0" }
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "0.6.0"
|
||||
solana-local-cluster = { path = "../local-cluster", version = "=1.11.0" }
|
||||
solana-test-validator = { path = "../test-validator", version = "=1.11.0" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
@ -1,986 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
bench_tps_client::*,
|
||||
cli::Config,
|
||||
perf_utils::{sample_txs, SampleStats},
|
||||
},
|
||||
log::*,
|
||||
rayon::prelude::*,
|
||||
solana_core::gen_keys::GenKeys,
|
||||
solana_measure::measure::Measure,
|
||||
solana_metrics::{self, datapoint_info},
|
||||
solana_sdk::{
|
||||
clock::{DEFAULT_MS_PER_SLOT, DEFAULT_S_PER_SLOT, MAX_PROCESSING_AGE},
|
||||
commitment_config::CommitmentConfig,
|
||||
hash::Hash,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
message::Message,
|
||||
native_token::Sol,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
system_instruction, system_transaction,
|
||||
timing::{duration_as_ms, duration_as_s, duration_as_us, timestamp},
|
||||
transaction::Transaction,
|
||||
},
|
||||
std::{
|
||||
collections::{HashSet, VecDeque},
|
||||
process::exit,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
thread::{sleep, Builder, JoinHandle},
|
||||
time::{Duration, Instant},
|
||||
},
|
||||
};
|
||||
|
||||
// The point at which transactions become "too old", in seconds.
|
||||
const MAX_TX_QUEUE_AGE: u64 = (MAX_PROCESSING_AGE as f64 * DEFAULT_S_PER_SLOT) as u64;
|
||||
|
||||
pub const MAX_SPENDS_PER_TX: u64 = 4;
|
||||
|
||||
pub type SharedTransactions = Arc<RwLock<VecDeque<Vec<(Transaction, u64)>>>>;
|
||||
|
||||
fn get_latest_blockhash<T: BenchTpsClient>(client: &T) -> Hash {
|
||||
loop {
|
||||
match client.get_latest_blockhash_with_commitment(CommitmentConfig::processed()) {
|
||||
Ok((blockhash, _)) => return blockhash,
|
||||
Err(err) => {
|
||||
info!("Couldn't get last blockhash: {:?}", err);
|
||||
sleep(Duration::from_secs(1));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_for_target_slots_per_epoch<T>(target_slots_per_epoch: u64, client: &Arc<T>)
|
||||
where
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
if target_slots_per_epoch != 0 {
|
||||
info!(
|
||||
"Waiting until epochs are {} slots long..",
|
||||
target_slots_per_epoch
|
||||
);
|
||||
loop {
|
||||
if let Ok(epoch_info) = client.get_epoch_info() {
|
||||
if epoch_info.slots_in_epoch >= target_slots_per_epoch {
|
||||
info!("Done epoch_info: {:?}", epoch_info);
|
||||
break;
|
||||
}
|
||||
info!(
|
||||
"Waiting for epoch: {} now: {}",
|
||||
target_slots_per_epoch, epoch_info.slots_in_epoch
|
||||
);
|
||||
}
|
||||
sleep(Duration::from_secs(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sampler_thread<T>(
|
||||
client: &Arc<T>,
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
sample_period: u64,
|
||||
maxes: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||
) -> JoinHandle<()>
|
||||
where
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
info!("Sampling TPS every {} second...", sample_period);
|
||||
let exit_signal = exit_signal.clone();
|
||||
let maxes = maxes.clone();
|
||||
let client = client.clone();
|
||||
Builder::new()
|
||||
.name("solana-client-sample".to_string())
|
||||
.spawn(move || {
|
||||
sample_txs(&exit_signal, &maxes, sample_period, &client);
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn generate_chunked_transfers(
|
||||
recent_blockhash: Arc<RwLock<Hash>>,
|
||||
shared_txs: &SharedTransactions,
|
||||
shared_tx_active_thread_count: Arc<AtomicIsize>,
|
||||
source_keypair_chunks: Vec<Vec<&Keypair>>,
|
||||
dest_keypair_chunks: &mut [VecDeque<&Keypair>],
|
||||
threads: usize,
|
||||
duration: Duration,
|
||||
sustained: bool,
|
||||
) {
|
||||
// generate and send transactions for the specified duration
|
||||
let start = Instant::now();
|
||||
let keypair_chunks = source_keypair_chunks.len();
|
||||
let mut reclaim_lamports_back_to_source_account = false;
|
||||
let mut chunk_index = 0;
|
||||
while start.elapsed() < duration {
|
||||
generate_txs(
|
||||
shared_txs,
|
||||
&recent_blockhash,
|
||||
&source_keypair_chunks[chunk_index],
|
||||
&dest_keypair_chunks[chunk_index],
|
||||
threads,
|
||||
reclaim_lamports_back_to_source_account,
|
||||
);
|
||||
|
||||
// In sustained mode, overlap the transfers with generation. This has higher average
|
||||
// performance but lower peak performance in tested environments.
|
||||
if sustained {
|
||||
// Ensure that we don't generate more transactions than we can handle.
|
||||
while shared_txs.read().unwrap().len() > 2 * threads {
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
} else {
|
||||
while !shared_txs.read().unwrap().is_empty()
|
||||
|| shared_tx_active_thread_count.load(Ordering::Relaxed) > 0
|
||||
{
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate destination keypairs so that the next round of transactions will have different
|
||||
// transaction signatures even when blockhash is reused.
|
||||
dest_keypair_chunks[chunk_index].rotate_left(1);
|
||||
|
||||
// Move on to next chunk
|
||||
chunk_index = (chunk_index + 1) % keypair_chunks;
|
||||
|
||||
// Switch directions after transfering for each "chunk"
|
||||
if chunk_index == 0 {
|
||||
reclaim_lamports_back_to_source_account = !reclaim_lamports_back_to_source_account;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sender_threads<T>(
|
||||
client: &Arc<T>,
|
||||
shared_txs: &SharedTransactions,
|
||||
thread_batch_sleep_ms: usize,
|
||||
total_tx_sent_count: &Arc<AtomicUsize>,
|
||||
threads: usize,
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
shared_tx_active_thread_count: &Arc<AtomicIsize>,
|
||||
) -> Vec<JoinHandle<()>>
|
||||
where
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
(0..threads)
|
||||
.map(|_| {
|
||||
let exit_signal = exit_signal.clone();
|
||||
let shared_txs = shared_txs.clone();
|
||||
let shared_tx_active_thread_count = shared_tx_active_thread_count.clone();
|
||||
let total_tx_sent_count = total_tx_sent_count.clone();
|
||||
let client = client.clone();
|
||||
Builder::new()
|
||||
.name("solana-client-sender".to_string())
|
||||
.spawn(move || {
|
||||
do_tx_transfers(
|
||||
&exit_signal,
|
||||
&shared_txs,
|
||||
&shared_tx_active_thread_count,
|
||||
&total_tx_sent_count,
|
||||
thread_batch_sleep_ms,
|
||||
&client,
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn do_bench_tps<T>(client: Arc<T>, config: Config, gen_keypairs: Vec<Keypair>) -> u64
|
||||
where
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
let Config {
|
||||
id,
|
||||
threads,
|
||||
thread_batch_sleep_ms,
|
||||
duration,
|
||||
tx_count,
|
||||
sustained,
|
||||
target_slots_per_epoch,
|
||||
..
|
||||
} = config;
|
||||
|
||||
let mut source_keypair_chunks: Vec<Vec<&Keypair>> = Vec::new();
|
||||
let mut dest_keypair_chunks: Vec<VecDeque<&Keypair>> = Vec::new();
|
||||
assert!(gen_keypairs.len() >= 2 * tx_count);
|
||||
for chunk in gen_keypairs.chunks_exact(2 * tx_count) {
|
||||
source_keypair_chunks.push(chunk[..tx_count].iter().collect());
|
||||
dest_keypair_chunks.push(chunk[tx_count..].iter().collect());
|
||||
}
|
||||
|
||||
let first_tx_count = loop {
|
||||
match client.get_transaction_count() {
|
||||
Ok(count) => break count,
|
||||
Err(err) => {
|
||||
info!("Couldn't get transaction count: {:?}", err);
|
||||
sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
};
|
||||
info!("Initial transaction count {}", first_tx_count);
|
||||
|
||||
let exit_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Setup a thread per validator to sample every period
|
||||
// collect the max transaction rate and total tx count seen
|
||||
let maxes = Arc::new(RwLock::new(Vec::new()));
|
||||
let sample_period = 1; // in seconds
|
||||
let sample_thread = create_sampler_thread(&client, &exit_signal, sample_period, &maxes);
|
||||
|
||||
let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new()));
|
||||
|
||||
let blockhash = Arc::new(RwLock::new(get_latest_blockhash(client.as_ref())));
|
||||
let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0));
|
||||
let total_tx_sent_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let blockhash_thread = {
|
||||
let exit_signal = exit_signal.clone();
|
||||
let blockhash = blockhash.clone();
|
||||
let client = client.clone();
|
||||
let id = id.pubkey();
|
||||
Builder::new()
|
||||
.name("solana-blockhash-poller".to_string())
|
||||
.spawn(move || {
|
||||
poll_blockhash(&exit_signal, &blockhash, &client, &id);
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let s_threads = create_sender_threads(
|
||||
&client,
|
||||
&shared_txs,
|
||||
thread_batch_sleep_ms,
|
||||
&total_tx_sent_count,
|
||||
threads,
|
||||
&exit_signal,
|
||||
&shared_tx_active_thread_count,
|
||||
);
|
||||
|
||||
wait_for_target_slots_per_epoch(target_slots_per_epoch, &client);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
generate_chunked_transfers(
|
||||
blockhash,
|
||||
&shared_txs,
|
||||
shared_tx_active_thread_count,
|
||||
source_keypair_chunks,
|
||||
&mut dest_keypair_chunks,
|
||||
threads,
|
||||
duration,
|
||||
sustained,
|
||||
);
|
||||
|
||||
// Stop the sampling threads so it will collect the stats
|
||||
exit_signal.store(true, Ordering::Relaxed);
|
||||
|
||||
info!("Waiting for sampler threads...");
|
||||
if let Err(err) = sample_thread.join() {
|
||||
info!(" join() failed with: {:?}", err);
|
||||
}
|
||||
|
||||
// join the tx send threads
|
||||
info!("Waiting for transmit threads...");
|
||||
for t in s_threads {
|
||||
if let Err(err) = t.join() {
|
||||
info!(" join() failed with: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Waiting for blockhash thread...");
|
||||
if let Err(err) = blockhash_thread.join() {
|
||||
info!(" join() failed with: {:?}", err);
|
||||
}
|
||||
|
||||
let balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
|
||||
compute_and_report_stats(
|
||||
&maxes,
|
||||
sample_period,
|
||||
&start.elapsed(),
|
||||
total_tx_sent_count.load(Ordering::Relaxed),
|
||||
);
|
||||
|
||||
let r_maxes = maxes.read().unwrap();
|
||||
r_maxes.first().unwrap().1.txs
|
||||
}
|
||||
|
||||
fn metrics_submit_lamport_balance(lamport_balance: u64) {
|
||||
info!("Token balance: {}", lamport_balance);
|
||||
datapoint_info!(
|
||||
"bench-tps-lamport_balance",
|
||||
("balance", lamport_balance, i64)
|
||||
);
|
||||
}
|
||||
|
||||
fn generate_system_txs(
|
||||
source: &[&Keypair],
|
||||
dest: &VecDeque<&Keypair>,
|
||||
reclaim: bool,
|
||||
blockhash: &Hash,
|
||||
) -> Vec<(Transaction, u64)> {
|
||||
let pairs: Vec<_> = if !reclaim {
|
||||
source.iter().zip(dest.iter()).collect()
|
||||
} else {
|
||||
dest.iter().zip(source.iter()).collect()
|
||||
};
|
||||
|
||||
pairs
|
||||
.par_iter()
|
||||
.map(|(from, to)| {
|
||||
(
|
||||
system_transaction::transfer(from, &to.pubkey(), 1, *blockhash),
|
||||
timestamp(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_txs(
|
||||
shared_txs: &SharedTransactions,
|
||||
blockhash: &Arc<RwLock<Hash>>,
|
||||
source: &[&Keypair],
|
||||
dest: &VecDeque<&Keypair>,
|
||||
threads: usize,
|
||||
reclaim: bool,
|
||||
) {
|
||||
let blockhash = *blockhash.read().unwrap();
|
||||
let tx_count = source.len();
|
||||
info!(
|
||||
"Signing transactions... {} (reclaim={}, blockhash={})",
|
||||
tx_count, reclaim, &blockhash
|
||||
);
|
||||
let signing_start = Instant::now();
|
||||
|
||||
let transactions = generate_system_txs(source, dest, reclaim, &blockhash);
|
||||
|
||||
let duration = signing_start.elapsed();
|
||||
let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos());
|
||||
let bsps = (tx_count) as f64 / ns as f64;
|
||||
let nsps = ns as f64 / (tx_count) as f64;
|
||||
info!(
|
||||
"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),
|
||||
blockhash,
|
||||
);
|
||||
datapoint_info!(
|
||||
"bench-tps-generate_txs",
|
||||
("duration", duration_as_us(&duration), i64)
|
||||
);
|
||||
|
||||
let sz = transactions.len() / threads;
|
||||
let chunks: Vec<_> = transactions.chunks(sz).collect();
|
||||
{
|
||||
let mut shared_txs_wl = shared_txs.write().unwrap();
|
||||
for chunk in chunks {
|
||||
shared_txs_wl.push_back(chunk.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_new_latest_blockhash<T: BenchTpsClient>(client: &Arc<T>, blockhash: &Hash) -> Option<Hash> {
|
||||
let start = Instant::now();
|
||||
while start.elapsed().as_secs() < 5 {
|
||||
if let Ok(new_blockhash) = client.get_latest_blockhash() {
|
||||
if new_blockhash != *blockhash {
|
||||
return Some(new_blockhash);
|
||||
}
|
||||
}
|
||||
debug!("Got same blockhash ({:?}), will retry...", blockhash);
|
||||
|
||||
// Retry ~twice during a slot
|
||||
sleep(Duration::from_millis(DEFAULT_MS_PER_SLOT / 2));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn poll_blockhash<T: BenchTpsClient>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
blockhash: &Arc<RwLock<Hash>>,
|
||||
client: &Arc<T>,
|
||||
id: &Pubkey,
|
||||
) {
|
||||
let mut blockhash_last_updated = Instant::now();
|
||||
let mut last_error_log = Instant::now();
|
||||
loop {
|
||||
let blockhash_updated = {
|
||||
let old_blockhash = *blockhash.read().unwrap();
|
||||
if let Some(new_blockhash) = get_new_latest_blockhash(client, &old_blockhash) {
|
||||
*blockhash.write().unwrap() = new_blockhash;
|
||||
blockhash_last_updated = Instant::now();
|
||||
true
|
||||
} else {
|
||||
if blockhash_last_updated.elapsed().as_secs() > 120 {
|
||||
eprintln!("Blockhash is stuck");
|
||||
exit(1)
|
||||
} else if blockhash_last_updated.elapsed().as_secs() > 30
|
||||
&& last_error_log.elapsed().as_secs() >= 1
|
||||
{
|
||||
last_error_log = Instant::now();
|
||||
error!("Blockhash is not updating");
|
||||
}
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if blockhash_updated {
|
||||
let balance = client.get_balance(id).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(balance);
|
||||
}
|
||||
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
|
||||
fn do_tx_transfers<T: BenchTpsClient>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
shared_txs: &SharedTransactions,
|
||||
shared_tx_thread_count: &Arc<AtomicIsize>,
|
||||
total_tx_sent_count: &Arc<AtomicUsize>,
|
||||
thread_batch_sleep_ms: usize,
|
||||
client: &Arc<T>,
|
||||
) {
|
||||
loop {
|
||||
if thread_batch_sleep_ms > 0 {
|
||||
sleep(Duration::from_millis(thread_batch_sleep_ms as u64));
|
||||
}
|
||||
let txs = {
|
||||
let mut shared_txs_wl = shared_txs.write().expect("write lock in do_tx_transfers");
|
||||
shared_txs_wl.pop_front()
|
||||
};
|
||||
if let Some(txs0) = txs {
|
||||
shared_tx_thread_count.fetch_add(1, Ordering::Relaxed);
|
||||
info!("Transferring 1 unit {} times...", txs0.len());
|
||||
let tx_len = txs0.len();
|
||||
let transfer_start = Instant::now();
|
||||
let mut old_transactions = false;
|
||||
let mut transactions = Vec::<_>::new();
|
||||
for tx in txs0 {
|
||||
let now = timestamp();
|
||||
// Transactions that are too old will be rejected by the cluster Don't bother
|
||||
// sending them.
|
||||
if now > tx.1 && now - tx.1 > 1000 * MAX_TX_QUEUE_AGE {
|
||||
old_transactions = true;
|
||||
continue;
|
||||
}
|
||||
transactions.push(tx.0);
|
||||
}
|
||||
|
||||
if let Err(error) = client.send_batch(transactions) {
|
||||
warn!("send_batch_sync in do_tx_transfers failed: {}", error);
|
||||
}
|
||||
|
||||
if old_transactions {
|
||||
let mut shared_txs_wl = shared_txs.write().expect("write lock in do_tx_transfers");
|
||||
shared_txs_wl.clear();
|
||||
}
|
||||
shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed);
|
||||
total_tx_sent_count.fetch_add(tx_len, Ordering::Relaxed);
|
||||
info!(
|
||||
"Tx send done. {} ms {} tps",
|
||||
duration_as_ms(&transfer_start.elapsed()),
|
||||
tx_len as f32 / duration_as_s(&transfer_start.elapsed()),
|
||||
);
|
||||
datapoint_info!(
|
||||
"bench-tps-do_tx_transfers",
|
||||
("duration", duration_as_us(&transfer_start.elapsed()), i64),
|
||||
("count", tx_len, i64)
|
||||
);
|
||||
}
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_funding_transfer<T: BenchTpsClient>(
|
||||
client: &Arc<T>,
|
||||
tx: &Transaction,
|
||||
amount: u64,
|
||||
) -> bool {
|
||||
for a in &tx.message().account_keys[1..] {
|
||||
match client.get_balance_with_commitment(a, CommitmentConfig::processed()) {
|
||||
Ok(balance) => return balance >= amount,
|
||||
Err(err) => error!("failed to get balance {:?}", err),
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
trait FundingTransactions<'a> {
|
||||
fn fund<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
&mut self,
|
||||
client: &Arc<T>,
|
||||
to_fund: &[(&'a Keypair, Vec<(Pubkey, u64)>)],
|
||||
to_lamports: u64,
|
||||
);
|
||||
fn make(&mut self, to_fund: &[(&'a Keypair, Vec<(Pubkey, u64)>)]);
|
||||
fn sign(&mut self, blockhash: Hash);
|
||||
fn send<T: BenchTpsClient>(&self, client: &Arc<T>);
|
||||
fn verify<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
&mut self,
|
||||
client: &Arc<T>,
|
||||
to_lamports: u64,
|
||||
);
|
||||
}
|
||||
|
||||
impl<'a> FundingTransactions<'a> for Vec<(&'a Keypair, Transaction)> {
|
||||
fn fund<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
&mut self,
|
||||
client: &Arc<T>,
|
||||
to_fund: &[(&'a Keypair, Vec<(Pubkey, u64)>)],
|
||||
to_lamports: u64,
|
||||
) {
|
||||
self.make(to_fund);
|
||||
|
||||
let mut tries = 0;
|
||||
while !self.is_empty() {
|
||||
info!(
|
||||
"{} {} each to {} accounts in {} txs",
|
||||
if tries == 0 {
|
||||
"transferring"
|
||||
} else {
|
||||
" retrying"
|
||||
},
|
||||
to_lamports,
|
||||
self.len() * MAX_SPENDS_PER_TX as usize,
|
||||
self.len(),
|
||||
);
|
||||
|
||||
let blockhash = get_latest_blockhash(client.as_ref());
|
||||
|
||||
// re-sign retained to_fund_txes with updated blockhash
|
||||
self.sign(blockhash);
|
||||
self.send(client);
|
||||
|
||||
// Sleep a few slots to allow transactions to process
|
||||
sleep(Duration::from_secs(1));
|
||||
|
||||
self.verify(client, to_lamports);
|
||||
|
||||
// retry anything that seems to have dropped through cracks
|
||||
// again since these txs are all or nothing, they're fine to
|
||||
// retry
|
||||
tries += 1;
|
||||
}
|
||||
info!("transferred");
|
||||
}
|
||||
|
||||
fn make(&mut self, to_fund: &[(&'a Keypair, Vec<(Pubkey, u64)>)]) {
|
||||
let mut make_txs = Measure::start("make_txs");
|
||||
let to_fund_txs: Vec<(&Keypair, Transaction)> = to_fund
|
||||
.par_iter()
|
||||
.map(|(k, t)| {
|
||||
let instructions = system_instruction::transfer_many(&k.pubkey(), t);
|
||||
let message = Message::new(&instructions, Some(&k.pubkey()));
|
||||
(*k, Transaction::new_unsigned(message))
|
||||
})
|
||||
.collect();
|
||||
make_txs.stop();
|
||||
debug!(
|
||||
"make {} unsigned txs: {}us",
|
||||
to_fund_txs.len(),
|
||||
make_txs.as_us()
|
||||
);
|
||||
self.extend(to_fund_txs);
|
||||
}
|
||||
|
||||
fn sign(&mut self, blockhash: Hash) {
|
||||
let mut sign_txs = Measure::start("sign_txs");
|
||||
self.par_iter_mut().for_each(|(k, tx)| {
|
||||
tx.sign(&[*k], blockhash);
|
||||
});
|
||||
sign_txs.stop();
|
||||
debug!("sign {} txs: {}us", self.len(), sign_txs.as_us());
|
||||
}
|
||||
|
||||
fn send<T: BenchTpsClient>(&self, client: &Arc<T>) {
|
||||
let mut send_txs = Measure::start("send_txs");
|
||||
self.iter().for_each(|(_, tx)| {
|
||||
client.send_transaction(tx.clone()).expect("transfer");
|
||||
});
|
||||
send_txs.stop();
|
||||
debug!("send {} txs: {}us", self.len(), send_txs.as_us());
|
||||
}
|
||||
|
||||
fn verify<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
&mut self,
|
||||
client: &Arc<T>,
|
||||
to_lamports: u64,
|
||||
) {
|
||||
let starting_txs = self.len();
|
||||
let verified_txs = Arc::new(AtomicUsize::new(0));
|
||||
let too_many_failures = Arc::new(AtomicBool::new(false));
|
||||
let loops = if starting_txs < 1000 { 3 } else { 1 };
|
||||
// Only loop multiple times for small (quick) transaction batches
|
||||
let time = Arc::new(Mutex::new(Instant::now()));
|
||||
for _ in 0..loops {
|
||||
let time = time.clone();
|
||||
let failed_verify = Arc::new(AtomicUsize::new(0));
|
||||
let client = client.clone();
|
||||
let verified_txs = &verified_txs;
|
||||
let failed_verify = &failed_verify;
|
||||
let too_many_failures = &too_many_failures;
|
||||
let verified_set: HashSet<Pubkey> = self
|
||||
.par_iter()
|
||||
.filter_map(move |(k, tx)| {
|
||||
if too_many_failures.load(Ordering::Relaxed) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let verified = if verify_funding_transfer(&client, tx, to_lamports) {
|
||||
verified_txs.fetch_add(1, Ordering::Relaxed);
|
||||
Some(k.pubkey())
|
||||
} else {
|
||||
failed_verify.fetch_add(1, Ordering::Relaxed);
|
||||
None
|
||||
};
|
||||
|
||||
let verified_txs = verified_txs.load(Ordering::Relaxed);
|
||||
let failed_verify = failed_verify.load(Ordering::Relaxed);
|
||||
let remaining_count = starting_txs.saturating_sub(verified_txs + failed_verify);
|
||||
if failed_verify > 100 && failed_verify > verified_txs {
|
||||
too_many_failures.store(true, Ordering::Relaxed);
|
||||
warn!(
|
||||
"Too many failed transfers... {} remaining, {} verified, {} failures",
|
||||
remaining_count, verified_txs, failed_verify
|
||||
);
|
||||
}
|
||||
if remaining_count > 0 {
|
||||
let mut time_l = time.lock().unwrap();
|
||||
if time_l.elapsed().as_secs() > 2 {
|
||||
info!(
|
||||
"Verifying transfers... {} remaining, {} verified, {} failures",
|
||||
remaining_count, verified_txs, failed_verify
|
||||
);
|
||||
*time_l = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
verified
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.retain(|(k, _)| !verified_set.contains(&k.pubkey()));
|
||||
if self.is_empty() {
|
||||
break;
|
||||
}
|
||||
info!("Looping verifications");
|
||||
|
||||
let verified_txs = verified_txs.load(Ordering::Relaxed);
|
||||
let failed_verify = failed_verify.load(Ordering::Relaxed);
|
||||
let remaining_count = starting_txs.saturating_sub(verified_txs + failed_verify);
|
||||
info!(
|
||||
"Verifying transfers... {} remaining, {} verified, {} failures",
|
||||
remaining_count, verified_txs, failed_verify
|
||||
);
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// fund the dests keys by spending all of the source keys into MAX_SPENDS_PER_TX
|
||||
/// on every iteration. This allows us to replay the transfers because the source is either empty,
|
||||
/// or full
|
||||
pub fn fund_keys<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
client: Arc<T>,
|
||||
source: &Keypair,
|
||||
dests: &[Keypair],
|
||||
total: u64,
|
||||
max_fee: u64,
|
||||
lamports_per_account: u64,
|
||||
) {
|
||||
let mut funded: Vec<&Keypair> = vec![source];
|
||||
let mut funded_funds = total;
|
||||
let mut not_funded: Vec<&Keypair> = dests.iter().collect();
|
||||
while !not_funded.is_empty() {
|
||||
// Build to fund list and prepare funding sources for next iteration
|
||||
let mut new_funded: Vec<&Keypair> = vec![];
|
||||
let mut to_fund: Vec<(&Keypair, Vec<(Pubkey, u64)>)> = vec![];
|
||||
let to_lamports = (funded_funds - lamports_per_account - max_fee) / MAX_SPENDS_PER_TX;
|
||||
for f in funded {
|
||||
let start = not_funded.len() - MAX_SPENDS_PER_TX as usize;
|
||||
let dests: Vec<_> = not_funded.drain(start..).collect();
|
||||
let spends: Vec<_> = dests.iter().map(|k| (k.pubkey(), to_lamports)).collect();
|
||||
to_fund.push((f, spends));
|
||||
new_funded.extend(dests.into_iter());
|
||||
}
|
||||
|
||||
// try to transfer a "few" at a time with recent blockhash
|
||||
// assume 4MB network buffers, and 512 byte packets
|
||||
const FUND_CHUNK_LEN: usize = 4 * 1024 * 1024 / 512;
|
||||
|
||||
to_fund.chunks(FUND_CHUNK_LEN).for_each(|chunk| {
|
||||
Vec::<(&Keypair, Transaction)>::with_capacity(chunk.len()).fund(
|
||||
&client,
|
||||
chunk,
|
||||
to_lamports,
|
||||
);
|
||||
});
|
||||
|
||||
info!("funded: {} left: {}", new_funded.len(), not_funded.len());
|
||||
funded = new_funded;
|
||||
funded_funds = to_lamports;
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_and_report_stats(
|
||||
maxes: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||
sample_period: u64,
|
||||
tx_send_elapsed: &Duration,
|
||||
total_tx_send_count: usize,
|
||||
) {
|
||||
// Compute/report stats
|
||||
let mut max_of_maxes = 0.0;
|
||||
let mut max_tx_count = 0;
|
||||
let mut nodes_with_zero_tps = 0;
|
||||
let mut total_maxes = 0.0;
|
||||
info!(" Node address | Max TPS | Total Transactions");
|
||||
info!("---------------------+---------------+--------------------");
|
||||
|
||||
for (sock, stats) in maxes.read().unwrap().iter() {
|
||||
let maybe_flag = match stats.txs {
|
||||
0 => "!!!!!",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
info!(
|
||||
"{:20} | {:13.2} | {} {}",
|
||||
sock, stats.tps, stats.txs, maybe_flag
|
||||
);
|
||||
|
||||
if stats.tps == 0.0 {
|
||||
nodes_with_zero_tps += 1;
|
||||
}
|
||||
total_maxes += stats.tps;
|
||||
|
||||
if stats.tps > max_of_maxes {
|
||||
max_of_maxes = stats.tps;
|
||||
}
|
||||
if stats.txs > max_tx_count {
|
||||
max_tx_count = stats.txs;
|
||||
}
|
||||
}
|
||||
|
||||
if total_maxes > 0.0 {
|
||||
let num_nodes_with_tps = maxes.read().unwrap().len() - nodes_with_zero_tps;
|
||||
let average_max = total_maxes / num_nodes_with_tps as f32;
|
||||
info!(
|
||||
"\nAverage max TPS: {:.2}, {} nodes had 0 TPS",
|
||||
average_max, nodes_with_zero_tps
|
||||
);
|
||||
}
|
||||
|
||||
let total_tx_send_count = total_tx_send_count as u64;
|
||||
let drop_rate = if total_tx_send_count > max_tx_count {
|
||||
(total_tx_send_count - max_tx_count) as f64 / total_tx_send_count as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
info!(
|
||||
"\nHighest TPS: {:.2} sampling period {}s max transactions: {} clients: {} drop rate: {:.2}",
|
||||
max_of_maxes,
|
||||
sample_period,
|
||||
max_tx_count,
|
||||
maxes.read().unwrap().len(),
|
||||
drop_rate,
|
||||
);
|
||||
info!(
|
||||
"\tAverage TPS: {}",
|
||||
max_tx_count as f32 / duration_as_s(tx_send_elapsed)
|
||||
);
|
||||
}
|
||||
|
||||
pub fn generate_keypairs(seed_keypair: &Keypair, count: u64) -> (Vec<Keypair>, u64) {
|
||||
let mut seed = [0u8; 32];
|
||||
seed.copy_from_slice(&seed_keypair.to_bytes()[..32]);
|
||||
let mut rnd = GenKeys::new(seed);
|
||||
|
||||
let mut total_keys = 0;
|
||||
let mut extra = 0; // This variable tracks the number of keypairs needing extra transaction fees funded
|
||||
let mut delta = 1;
|
||||
while total_keys < count {
|
||||
extra += delta;
|
||||
delta *= MAX_SPENDS_PER_TX;
|
||||
total_keys += delta;
|
||||
}
|
||||
(rnd.gen_n_keypairs(total_keys), extra)
|
||||
}
|
||||
|
||||
pub fn generate_and_fund_keypairs<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
client: Arc<T>,
|
||||
funding_key: &Keypair,
|
||||
keypair_count: usize,
|
||||
lamports_per_account: u64,
|
||||
) -> Result<Vec<Keypair>> {
|
||||
let rent = client.get_minimum_balance_for_rent_exemption(0)?;
|
||||
let lamports_per_account = lamports_per_account + rent;
|
||||
|
||||
info!("Creating {} keypairs...", keypair_count);
|
||||
let (mut keypairs, extra) = generate_keypairs(funding_key, keypair_count as u64);
|
||||
fund_keypairs(client, funding_key, &keypairs, extra, lamports_per_account)?;
|
||||
|
||||
// 'generate_keypairs' generates extra keys to be able to have size-aligned funding batches for fund_keys.
|
||||
keypairs.truncate(keypair_count);
|
||||
|
||||
Ok(keypairs)
|
||||
}
|
||||
|
||||
pub fn fund_keypairs<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
client: Arc<T>,
|
||||
funding_key: &Keypair,
|
||||
keypairs: &[Keypair],
|
||||
extra: u64,
|
||||
lamports_per_account: u64,
|
||||
) -> Result<()> {
|
||||
let rent = client.get_minimum_balance_for_rent_exemption(0)?;
|
||||
info!("Get lamports...");
|
||||
|
||||
// Sample the first keypair, to prevent lamport loss on repeated solana-bench-tps executions
|
||||
let first_key = keypairs[0].pubkey();
|
||||
let first_keypair_balance = client.get_balance(&first_key).unwrap_or(0);
|
||||
|
||||
// Sample the last keypair, to check if funding was already completed
|
||||
let last_key = keypairs[keypairs.len() - 1].pubkey();
|
||||
let last_keypair_balance = client.get_balance(&last_key).unwrap_or(0);
|
||||
|
||||
// Repeated runs will eat up keypair balances from transaction fees. In order to quickly
|
||||
// start another bench-tps run without re-funding all of the keypairs, check if the
|
||||
// keypairs still have at least 80% of the expected funds. That should be enough to
|
||||
// pay for the transaction fees in a new run.
|
||||
let enough_lamports = 8 * lamports_per_account / 10;
|
||||
if first_keypair_balance < enough_lamports || last_keypair_balance < enough_lamports {
|
||||
let single_sig_message = Message::new_with_blockhash(
|
||||
&[Instruction::new_with_bytes(
|
||||
Pubkey::new_unique(),
|
||||
&[],
|
||||
vec![AccountMeta::new(Pubkey::new_unique(), true)],
|
||||
)],
|
||||
None,
|
||||
&client.get_latest_blockhash().unwrap(),
|
||||
);
|
||||
let max_fee = client.get_fee_for_message(&single_sig_message).unwrap();
|
||||
let extra_fees = extra * max_fee;
|
||||
let total_keypairs = keypairs.len() as u64 + 1; // Add one for funding keypair
|
||||
let total = lamports_per_account * total_keypairs + extra_fees;
|
||||
|
||||
let funding_key_balance = client.get_balance(&funding_key.pubkey()).unwrap_or(0);
|
||||
info!(
|
||||
"Funding keypair balance: {} max_fee: {} lamports_per_account: {} extra: {} total: {}",
|
||||
funding_key_balance, max_fee, lamports_per_account, extra, total
|
||||
);
|
||||
|
||||
if funding_key_balance < total + rent {
|
||||
error!(
|
||||
"funder has {}, needed {}",
|
||||
Sol(funding_key_balance),
|
||||
Sol(total)
|
||||
);
|
||||
let latest_blockhash = get_latest_blockhash(client.as_ref());
|
||||
if client
|
||||
.request_airdrop_with_blockhash(
|
||||
&funding_key.pubkey(),
|
||||
total + rent - funding_key_balance,
|
||||
&latest_blockhash,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Err(BenchTpsError::AirdropFailure);
|
||||
}
|
||||
}
|
||||
|
||||
fund_keys(
|
||||
client,
|
||||
funding_key,
|
||||
keypairs,
|
||||
total,
|
||||
max_fee,
|
||||
lamports_per_account,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::*,
|
||||
solana_runtime::{bank::Bank, bank_client::BankClient},
|
||||
solana_sdk::{
|
||||
fee_calculator::FeeRateGovernor, genesis_config::create_genesis_config,
|
||||
native_token::sol_to_lamports,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_bank_client() {
|
||||
let (genesis_config, id) = create_genesis_config(sol_to_lamports(10_000.0));
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
|
||||
let config = Config {
|
||||
id,
|
||||
tx_count: 10,
|
||||
duration: Duration::from_secs(5),
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let keypair_count = config.tx_count * config.keypair_multiplier;
|
||||
let keypairs =
|
||||
generate_and_fund_keypairs(client.clone(), &config.id, keypair_count, 20).unwrap();
|
||||
|
||||
do_bench_tps(client, config, keypairs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_fund_keys() {
|
||||
let (genesis_config, id) = create_genesis_config(sol_to_lamports(10_000.0));
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
let keypair_count = 20;
|
||||
let lamports = 20;
|
||||
let rent = client.get_minimum_balance_for_rent_exemption(0).unwrap();
|
||||
|
||||
let keypairs =
|
||||
generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports).unwrap();
|
||||
|
||||
for kp in &keypairs {
|
||||
assert_eq!(
|
||||
client
|
||||
.get_balance_with_commitment(&kp.pubkey(), CommitmentConfig::processed())
|
||||
.unwrap(),
|
||||
lamports + rent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_fund_keys_with_fees() {
|
||||
let (mut genesis_config, id) = create_genesis_config(sol_to_lamports(10_000.0));
|
||||
let fee_rate_governor = FeeRateGovernor::new(11, 0);
|
||||
genesis_config.fee_rate_governor = fee_rate_governor;
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
let keypair_count = 20;
|
||||
let lamports = 20;
|
||||
let rent = client.get_minimum_balance_for_rent_exemption(0).unwrap();
|
||||
|
||||
let keypairs =
|
||||
generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports).unwrap();
|
||||
|
||||
for kp in &keypairs {
|
||||
assert_eq!(client.get_balance(&kp.pubkey()).unwrap(), lamports + rent);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
use {
|
||||
solana_client::{client_error::ClientError, tpu_client::TpuSenderError},
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig, epoch_info::EpochInfo, hash::Hash, message::Message,
|
||||
pubkey::Pubkey, signature::Signature, transaction::Transaction, transport::TransportError,
|
||||
},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BenchTpsError {
|
||||
#[error("Airdrop failure")]
|
||||
AirdropFailure,
|
||||
#[error("IO error: {0:?}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Client error: {0:?}")]
|
||||
ClientError(#[from] ClientError),
|
||||
#[error("TpuClient error: {0:?}")]
|
||||
TpuSenderError(#[from] TpuSenderError),
|
||||
#[error("Transport error: {0:?}")]
|
||||
TransportError(#[from] TransportError),
|
||||
#[error("Custom error: {0}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
pub(crate) type Result<T> = std::result::Result<T, BenchTpsError>;
|
||||
|
||||
pub trait BenchTpsClient {
|
||||
/// Send a signed transaction without confirmation
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature>;
|
||||
|
||||
/// Send a batch of signed transactions without confirmation.
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()>;
|
||||
|
||||
/// Get latest blockhash
|
||||
fn get_latest_blockhash(&self) -> Result<Hash>;
|
||||
|
||||
/// Get latest blockhash and its last valid block height, using explicit commitment
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)>;
|
||||
|
||||
/// Get transaction count
|
||||
fn get_transaction_count(&self) -> Result<u64>;
|
||||
|
||||
/// Get transaction count, using explicit commitment
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64>;
|
||||
|
||||
/// Get epoch info
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo>;
|
||||
|
||||
/// Get account balance
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64>;
|
||||
|
||||
/// Get account balance, using explicit commitment
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64>;
|
||||
|
||||
/// Calculate the fee for a `Message`
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64>;
|
||||
|
||||
/// Get the rent-exempt minimum for an account
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64>;
|
||||
|
||||
/// Return the address of client
|
||||
fn addr(&self) -> String;
|
||||
|
||||
/// Request, submit, and confirm an airdrop transaction
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
recent_blockhash: &Hash,
|
||||
) -> Result<Signature>;
|
||||
}
|
||||
|
||||
mod bank_client;
|
||||
mod rpc_client;
|
||||
mod thin_client;
|
||||
mod tpu_client;
|
@ -1,85 +0,0 @@
|
||||
use {
|
||||
crate::bench_tps_client::{BenchTpsClient, BenchTpsError, Result},
|
||||
solana_runtime::bank_client::BankClient,
|
||||
solana_sdk::{
|
||||
client::{AsyncClient, SyncClient},
|
||||
commitment_config::CommitmentConfig,
|
||||
epoch_info::EpochInfo,
|
||||
hash::Hash,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
transaction::Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
impl BenchTpsClient for BankClient {
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature> {
|
||||
AsyncClient::async_send_transaction(self, transaction).map_err(|err| err.into())
|
||||
}
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()> {
|
||||
AsyncClient::async_send_batch(self, transactions).map_err(|err| err.into())
|
||||
}
|
||||
fn get_latest_blockhash(&self) -> Result<Hash> {
|
||||
SyncClient::get_latest_blockhash(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)> {
|
||||
SyncClient::get_latest_blockhash_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
SyncClient::get_transaction_count(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
SyncClient::get_transaction_count_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo> {
|
||||
SyncClient::get_epoch_info(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
|
||||
SyncClient::get_balance(self, pubkey).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
SyncClient::get_balance_with_commitment(self, pubkey, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64> {
|
||||
SyncClient::get_fee_for_message(self, message).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
|
||||
SyncClient::get_minimum_balance_for_rent_exemption(self, data_len).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn addr(&self) -> String {
|
||||
"Local BankClient".to_string()
|
||||
}
|
||||
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
_pubkey: &Pubkey,
|
||||
_lamports: u64,
|
||||
_recent_blockhash: &Hash,
|
||||
) -> Result<Signature> {
|
||||
// BankClient doesn't support airdrops
|
||||
Err(BenchTpsError::AirdropFailure)
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
use {
|
||||
crate::bench_tps_client::{BenchTpsClient, Result},
|
||||
solana_client::rpc_client::RpcClient,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig, epoch_info::EpochInfo, hash::Hash, message::Message,
|
||||
pubkey::Pubkey, signature::Signature, transaction::Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
impl BenchTpsClient for RpcClient {
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature> {
|
||||
RpcClient::send_transaction(self, &transaction).map_err(|err| err.into())
|
||||
}
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
BenchTpsClient::send_transaction(self, transaction)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn get_latest_blockhash(&self) -> Result<Hash> {
|
||||
RpcClient::get_latest_blockhash(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)> {
|
||||
RpcClient::get_latest_blockhash_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
RpcClient::get_transaction_count(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
RpcClient::get_transaction_count_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo> {
|
||||
RpcClient::get_epoch_info(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
|
||||
RpcClient::get_balance(self, pubkey).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
RpcClient::get_balance_with_commitment(self, pubkey, commitment_config)
|
||||
.map(|res| res.value)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64> {
|
||||
RpcClient::get_fee_for_message(self, message).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
|
||||
RpcClient::get_minimum_balance_for_rent_exemption(self, data_len).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn addr(&self) -> String {
|
||||
self.url()
|
||||
}
|
||||
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
recent_blockhash: &Hash,
|
||||
) -> Result<Signature> {
|
||||
RpcClient::request_airdrop_with_blockhash(self, pubkey, lamports, recent_blockhash)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
use {
|
||||
crate::bench_tps_client::{BenchTpsClient, Result},
|
||||
solana_client::thin_client::ThinClient,
|
||||
solana_sdk::{
|
||||
client::{AsyncClient, Client, SyncClient},
|
||||
commitment_config::CommitmentConfig,
|
||||
epoch_info::EpochInfo,
|
||||
hash::Hash,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
transaction::Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
impl BenchTpsClient for ThinClient {
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature> {
|
||||
AsyncClient::async_send_transaction(self, transaction).map_err(|err| err.into())
|
||||
}
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()> {
|
||||
AsyncClient::async_send_batch(self, transactions).map_err(|err| err.into())
|
||||
}
|
||||
fn get_latest_blockhash(&self) -> Result<Hash> {
|
||||
SyncClient::get_latest_blockhash(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)> {
|
||||
SyncClient::get_latest_blockhash_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
SyncClient::get_transaction_count(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
SyncClient::get_transaction_count_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo> {
|
||||
SyncClient::get_epoch_info(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
|
||||
SyncClient::get_balance(self, pubkey).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
SyncClient::get_balance_with_commitment(self, pubkey, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64> {
|
||||
SyncClient::get_fee_for_message(self, message).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
|
||||
SyncClient::get_minimum_balance_for_rent_exemption(self, data_len).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn addr(&self) -> String {
|
||||
Client::tpu_addr(self)
|
||||
}
|
||||
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
recent_blockhash: &Hash,
|
||||
) -> Result<Signature> {
|
||||
self.rpc_client()
|
||||
.request_airdrop_with_blockhash(pubkey, lamports, recent_blockhash)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
use {
|
||||
crate::bench_tps_client::{BenchTpsClient, Result},
|
||||
solana_client::tpu_client::TpuClient,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig, epoch_info::EpochInfo, hash::Hash, message::Message,
|
||||
pubkey::Pubkey, signature::Signature, transaction::Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
impl BenchTpsClient for TpuClient {
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature> {
|
||||
let signature = transaction.signatures[0];
|
||||
self.try_send_transaction(&transaction)?;
|
||||
Ok(signature)
|
||||
}
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
BenchTpsClient::send_transaction(self, transaction)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn get_latest_blockhash(&self) -> Result<Hash> {
|
||||
self.rpc_client()
|
||||
.get_latest_blockhash()
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)> {
|
||||
self.rpc_client()
|
||||
.get_latest_blockhash_with_commitment(commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_transaction_count()
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_transaction_count_with_commitment(commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo> {
|
||||
self.rpc_client().get_epoch_info().map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_balance(pubkey)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_balance_with_commitment(pubkey, commitment_config)
|
||||
.map(|res| res.value)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_fee_for_message(message)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_minimum_balance_for_rent_exemption(data_len)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn addr(&self) -> String {
|
||||
self.rpc_client().url()
|
||||
}
|
||||
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
recent_blockhash: &Hash,
|
||||
) -> Result<Signature> {
|
||||
self.rpc_client()
|
||||
.request_airdrop_with_blockhash(pubkey, lamports, recent_blockhash)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
}
|
@ -1,400 +0,0 @@
|
||||
use {
|
||||
clap::{crate_description, crate_name, App, Arg, ArgMatches},
|
||||
solana_clap_utils::input_validators::{is_url, is_url_or_moniker},
|
||||
solana_cli_config::{ConfigInput, CONFIG_FILE},
|
||||
solana_sdk::{
|
||||
fee_calculator::FeeRateGovernor,
|
||||
pubkey::Pubkey,
|
||||
signature::{read_keypair_file, Keypair},
|
||||
},
|
||||
std::{net::SocketAddr, process::exit, time::Duration},
|
||||
};
|
||||
|
||||
const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = solana_sdk::native_token::LAMPORTS_PER_SOL;
|
||||
|
||||
pub enum ExternalClientType {
|
||||
// Submits transactions to an Rpc node using an RpcClient
|
||||
RpcClient,
|
||||
// Submits transactions directly to leaders using a ThinClient, broadcasting to multiple
|
||||
// leaders when num_nodes > 1
|
||||
ThinClient,
|
||||
// Submits transactions directly to leaders using a TpuClient, broadcasting to upcoming leaders
|
||||
// via TpuClient default configuration
|
||||
TpuClient,
|
||||
}
|
||||
|
||||
impl Default for ExternalClientType {
|
||||
fn default() -> Self {
|
||||
Self::ThinClient
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the configuration for a single run of the benchmark
|
||||
pub struct Config {
|
||||
pub entrypoint_addr: SocketAddr,
|
||||
pub json_rpc_url: String,
|
||||
pub websocket_url: String,
|
||||
pub id: Keypair,
|
||||
pub threads: usize,
|
||||
pub num_nodes: usize,
|
||||
pub duration: Duration,
|
||||
pub tx_count: usize,
|
||||
pub keypair_multiplier: usize,
|
||||
pub thread_batch_sleep_ms: usize,
|
||||
pub sustained: bool,
|
||||
pub client_ids_and_stake_file: String,
|
||||
pub write_to_client_file: bool,
|
||||
pub read_from_client_file: bool,
|
||||
pub target_lamports_per_signature: u64,
|
||||
pub multi_client: bool,
|
||||
pub num_lamports_per_account: u64,
|
||||
pub target_slots_per_epoch: u64,
|
||||
pub target_node: Option<Pubkey>,
|
||||
pub external_client_type: ExternalClientType,
|
||||
pub use_quic: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||
json_rpc_url: ConfigInput::default().json_rpc_url,
|
||||
websocket_url: ConfigInput::default().websocket_url,
|
||||
id: Keypair::new(),
|
||||
threads: 4,
|
||||
num_nodes: 1,
|
||||
duration: Duration::new(std::u64::MAX, 0),
|
||||
tx_count: 50_000,
|
||||
keypair_multiplier: 8,
|
||||
thread_batch_sleep_ms: 1000,
|
||||
sustained: false,
|
||||
client_ids_and_stake_file: String::new(),
|
||||
write_to_client_file: false,
|
||||
read_from_client_file: false,
|
||||
target_lamports_per_signature: FeeRateGovernor::default().target_lamports_per_signature,
|
||||
multi_client: true,
|
||||
num_lamports_per_account: NUM_LAMPORTS_PER_ACCOUNT_DEFAULT,
|
||||
target_slots_per_epoch: 0,
|
||||
target_node: None,
|
||||
external_client_type: ExternalClientType::default(),
|
||||
use_quic: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines and builds the CLI args for a run of the benchmark
|
||||
pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
||||
App::new(crate_name!()).about(crate_description!())
|
||||
.version(version)
|
||||
.arg({
|
||||
let arg = Arg::with_name("config_file")
|
||||
.short("C")
|
||||
.long("config")
|
||||
.value_name("FILEPATH")
|
||||
.takes_value(true)
|
||||
.global(true)
|
||||
.help("Configuration file to use");
|
||||
if let Some(ref config_file) = *CONFIG_FILE {
|
||||
arg.default_value(config_file)
|
||||
} else {
|
||||
arg
|
||||
}
|
||||
})
|
||||
.arg(
|
||||
Arg::with_name("json_rpc_url")
|
||||
.short("u")
|
||||
.long("url")
|
||||
.value_name("URL_OR_MONIKER")
|
||||
.takes_value(true)
|
||||
.global(true)
|
||||
.validator(is_url_or_moniker)
|
||||
.help(
|
||||
"URL for Solana's JSON RPC or moniker (or their first letter): \
|
||||
[mainnet-beta, testnet, devnet, localhost]",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket_url")
|
||||
.long("ws")
|
||||
.value_name("URL")
|
||||
.takes_value(true)
|
||||
.global(true)
|
||||
.validator(is_url)
|
||||
.help("WebSocket URL for the solana cluster"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("entrypoint")
|
||||
.short("n")
|
||||
.long("entrypoint")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.help("Rendezvous with the cluster at this entry point; defaults to 127.0.0.1:8001"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("faucet")
|
||||
.short("d")
|
||||
.long("faucet")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.hidden(true)
|
||||
.help("Deprecated. BenchTps no longer queries the faucet directly"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("identity")
|
||||
.short("i")
|
||||
.long("identity")
|
||||
.value_name("PATH")
|
||||
.takes_value(true)
|
||||
.help("File containing a client identity (keypair)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("num-nodes")
|
||||
.short("N")
|
||||
.long("num-nodes")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Wait for NUM nodes to converge"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("threads")
|
||||
.short("t")
|
||||
.long("threads")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Number of threads"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("duration")
|
||||
.long("duration")
|
||||
.value_name("SECS")
|
||||
.takes_value(true)
|
||||
.help("Seconds to run benchmark, then exit; default is forever"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("sustained")
|
||||
.long("sustained")
|
||||
.help("Use sustained performance mode vs. peak mode. This overlaps the tx generation with transfers."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("no-multi-client")
|
||||
.long("no-multi-client")
|
||||
.help("Disable multi-client support, only transact with the entrypoint."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("target_node")
|
||||
.long("target-node")
|
||||
.requires("no-multi-client")
|
||||
.takes_value(true)
|
||||
.value_name("PUBKEY")
|
||||
.help("Specify an exact node to send transactions to."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("tx_count")
|
||||
.long("tx_count")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Number of transactions to send per batch")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("keypair_multiplier")
|
||||
.long("keypair-multiplier")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Multiply by transaction count to determine number of keypairs to create")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("thread-batch-sleep-ms")
|
||||
.short("z")
|
||||
.long("thread-batch-sleep-ms")
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.help("Per-thread-per-iteration sleep in ms"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("write-client-keys")
|
||||
.long("write-client-keys")
|
||||
.value_name("FILENAME")
|
||||
.takes_value(true)
|
||||
.help("Generate client keys and stakes and write the list to YAML file"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("read-client-keys")
|
||||
.long("read-client-keys")
|
||||
.value_name("FILENAME")
|
||||
.takes_value(true)
|
||||
.help("Read client keys and stakes from the YAML file"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("target_lamports_per_signature")
|
||||
.long("target-lamports-per-signature")
|
||||
.value_name("LAMPORTS")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
"The cost in lamports that the cluster will charge for signature \
|
||||
verification when the cluster is operating at target-signatures-per-slot",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("num_lamports_per_account")
|
||||
.long("num-lamports-per-account")
|
||||
.value_name("LAMPORTS")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
"Number of lamports per account.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("target_slots_per_epoch")
|
||||
.long("target-slots-per-epoch")
|
||||
.value_name("SLOTS")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
"Wait until epochs are this many slots long.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("rpc_client")
|
||||
.long("use-rpc-client")
|
||||
.conflicts_with("tpu_client")
|
||||
.takes_value(false)
|
||||
.help("Submit transactions with a RpcClient")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("tpu_client")
|
||||
.long("use-tpu-client")
|
||||
.conflicts_with("rpc_client")
|
||||
.takes_value(false)
|
||||
.help("Submit transactions with a TpuClient")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("tpu_use_quic")
|
||||
.long("tpu-use-quic")
|
||||
.takes_value(false)
|
||||
.help("Submit transactions via QUIC; only affects ThinClient (default) \
|
||||
or TpuClient sends"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Parses a clap `ArgMatches` structure into a `Config`
|
||||
/// # Arguments
|
||||
/// * `matches` - command line arguments parsed by clap
|
||||
/// # Panics
|
||||
/// Panics if there is trouble parsing any of the arguments
|
||||
pub fn extract_args(matches: &ArgMatches) -> Config {
|
||||
let mut args = Config::default();
|
||||
|
||||
let config = if let Some(config_file) = matches.value_of("config_file") {
|
||||
solana_cli_config::Config::load(config_file).unwrap_or_default()
|
||||
} else {
|
||||
solana_cli_config::Config::default()
|
||||
};
|
||||
let (_, json_rpc_url) = ConfigInput::compute_json_rpc_url_setting(
|
||||
matches.value_of("json_rpc_url").unwrap_or(""),
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
args.json_rpc_url = json_rpc_url;
|
||||
|
||||
let (_, websocket_url) = ConfigInput::compute_websocket_url_setting(
|
||||
matches.value_of("websocket_url").unwrap_or(""),
|
||||
&config.websocket_url,
|
||||
matches.value_of("json_rpc_url").unwrap_or(""),
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
args.websocket_url = websocket_url;
|
||||
|
||||
let (_, id_path) = ConfigInput::compute_keypair_path_setting(
|
||||
matches.value_of("identity").unwrap_or(""),
|
||||
&config.keypair_path,
|
||||
);
|
||||
if let Ok(id) = read_keypair_file(id_path) {
|
||||
args.id = id;
|
||||
} else if matches.is_present("identity") {
|
||||
panic!("could not parse identity path");
|
||||
}
|
||||
|
||||
if matches.is_present("tpu_client") {
|
||||
args.external_client_type = ExternalClientType::TpuClient;
|
||||
} else if matches.is_present("rpc_client") {
|
||||
args.external_client_type = ExternalClientType::RpcClient;
|
||||
}
|
||||
|
||||
if matches.is_present("tpu_use_quic") {
|
||||
args.use_quic = true;
|
||||
}
|
||||
|
||||
if let Some(addr) = matches.value_of("entrypoint") {
|
||||
args.entrypoint_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse entrypoint address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(t) = matches.value_of("threads") {
|
||||
args.threads = t.to_string().parse().expect("can't parse threads");
|
||||
}
|
||||
|
||||
if let Some(n) = matches.value_of("num-nodes") {
|
||||
args.num_nodes = n.to_string().parse().expect("can't parse num-nodes");
|
||||
}
|
||||
|
||||
if let Some(duration) = matches.value_of("duration") {
|
||||
args.duration = Duration::new(
|
||||
duration.to_string().parse().expect("can't parse duration"),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(s) = matches.value_of("tx_count") {
|
||||
args.tx_count = s.to_string().parse().expect("can't parse tx_count");
|
||||
}
|
||||
|
||||
if let Some(s) = matches.value_of("keypair_multiplier") {
|
||||
args.keypair_multiplier = s
|
||||
.to_string()
|
||||
.parse()
|
||||
.expect("can't parse keypair-multiplier");
|
||||
assert!(args.keypair_multiplier >= 2);
|
||||
}
|
||||
|
||||
if let Some(t) = matches.value_of("thread-batch-sleep-ms") {
|
||||
args.thread_batch_sleep_ms = t
|
||||
.to_string()
|
||||
.parse()
|
||||
.expect("can't parse thread-batch-sleep-ms");
|
||||
}
|
||||
|
||||
args.sustained = matches.is_present("sustained");
|
||||
|
||||
if let Some(s) = matches.value_of("write-client-keys") {
|
||||
args.write_to_client_file = true;
|
||||
args.client_ids_and_stake_file = s.to_string();
|
||||
}
|
||||
|
||||
if let Some(s) = matches.value_of("read-client-keys") {
|
||||
assert!(!args.write_to_client_file);
|
||||
args.read_from_client_file = true;
|
||||
args.client_ids_and_stake_file = s.to_string();
|
||||
}
|
||||
|
||||
if let Some(v) = matches.value_of("target_lamports_per_signature") {
|
||||
args.target_lamports_per_signature = v.to_string().parse().expect("can't parse lamports");
|
||||
}
|
||||
|
||||
args.multi_client = !matches.is_present("no-multi-client");
|
||||
args.target_node = matches
|
||||
.value_of("target_node")
|
||||
.map(|target_str| target_str.parse().unwrap());
|
||||
|
||||
if let Some(v) = matches.value_of("num_lamports_per_account") {
|
||||
args.num_lamports_per_account = v.to_string().parse().expect("can't parse lamports");
|
||||
}
|
||||
|
||||
if let Some(t) = matches.value_of("target_slots_per_epoch") {
|
||||
args.target_slots_per_epoch = t
|
||||
.to_string()
|
||||
.parse()
|
||||
.expect("can't parse target slots per epoch");
|
||||
}
|
||||
|
||||
args
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
bench::{fund_keypairs, generate_and_fund_keypairs},
|
||||
bench_tps_client::BenchTpsClient,
|
||||
},
|
||||
log::*,
|
||||
solana_genesis::Base64Account,
|
||||
solana_sdk::signature::{Keypair, Signer},
|
||||
std::{collections::HashMap, fs::File, path::Path, process::exit, sync::Arc},
|
||||
};
|
||||
|
||||
pub fn get_keypairs<T>(
|
||||
client: Arc<T>,
|
||||
id: &Keypair,
|
||||
keypair_count: usize,
|
||||
num_lamports_per_account: u64,
|
||||
client_ids_and_stake_file: &str,
|
||||
read_from_client_file: bool,
|
||||
) -> Vec<Keypair>
|
||||
where
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
if read_from_client_file {
|
||||
let path = Path::new(client_ids_and_stake_file);
|
||||
let file = File::open(path).unwrap();
|
||||
|
||||
info!("Reading {}", client_ids_and_stake_file);
|
||||
let accounts: HashMap<String, Base64Account> = serde_yaml::from_reader(file).unwrap();
|
||||
let mut keypairs = vec![];
|
||||
let mut last_balance = 0;
|
||||
|
||||
accounts
|
||||
.into_iter()
|
||||
.for_each(|(keypair, primordial_account)| {
|
||||
let bytes: Vec<u8> = serde_json::from_str(keypair.as_str()).unwrap();
|
||||
keypairs.push(Keypair::from_bytes(&bytes).unwrap());
|
||||
last_balance = primordial_account.balance;
|
||||
});
|
||||
|
||||
if keypairs.len() < keypair_count {
|
||||
eprintln!(
|
||||
"Expected {} accounts in {}, only received {} (--tx_count mismatch?)",
|
||||
keypair_count,
|
||||
client_ids_and_stake_file,
|
||||
keypairs.len(),
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
// Sort keypairs so that do_bench_tps() uses the same subset of accounts for each run.
|
||||
// This prevents the amount of storage needed for bench-tps accounts from creeping up
|
||||
// across multiple runs.
|
||||
keypairs.sort_by_key(|x| x.pubkey().to_string());
|
||||
fund_keypairs(
|
||||
client,
|
||||
id,
|
||||
&keypairs,
|
||||
keypairs.len().saturating_sub(keypair_count) as u64,
|
||||
last_balance,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Error could not fund keys: {:?}", e);
|
||||
exit(1);
|
||||
});
|
||||
keypairs
|
||||
} else {
|
||||
generate_and_fund_keypairs(client, id, keypair_count, num_lamports_per_account)
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Error could not fund keys: {:?}", e);
|
||||
exit(1);
|
||||
})
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
pub mod bench;
|
||||
pub mod bench_tps_client;
|
||||
pub mod cli;
|
||||
pub mod keypairs;
|
||||
mod perf_utils;
|
@ -1,174 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
use {
|
||||
log::*,
|
||||
solana_bench_tps::{
|
||||
bench::{do_bench_tps, generate_keypairs},
|
||||
cli::{self, ExternalClientType},
|
||||
keypairs::get_keypairs,
|
||||
},
|
||||
solana_client::{
|
||||
connection_cache,
|
||||
rpc_client::RpcClient,
|
||||
tpu_client::{TpuClient, TpuClientConfig},
|
||||
},
|
||||
solana_genesis::Base64Account,
|
||||
solana_gossip::gossip_service::{discover_cluster, get_client, get_multi_client},
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig, fee_calculator::FeeRateGovernor, system_program,
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
std::{collections::HashMap, fs::File, io::prelude::*, path::Path, process::exit, sync::Arc},
|
||||
};
|
||||
|
||||
/// Number of signatures for all transactions in ~1 week at ~100K TPS
|
||||
pub const NUM_SIGNATURES_FOR_TXS: u64 = 100_000 * 60 * 60 * 24 * 7;
|
||||
|
||||
fn main() {
|
||||
solana_logger::setup_with_default("solana=info");
|
||||
solana_metrics::set_panic_hook("bench-tps", /*version:*/ None);
|
||||
|
||||
let matches = cli::build_args(solana_version::version!()).get_matches();
|
||||
let cli_config = cli::extract_args(&matches);
|
||||
|
||||
let cli::Config {
|
||||
entrypoint_addr,
|
||||
json_rpc_url,
|
||||
websocket_url,
|
||||
id,
|
||||
num_nodes,
|
||||
tx_count,
|
||||
keypair_multiplier,
|
||||
client_ids_and_stake_file,
|
||||
write_to_client_file,
|
||||
read_from_client_file,
|
||||
target_lamports_per_signature,
|
||||
multi_client,
|
||||
num_lamports_per_account,
|
||||
target_node,
|
||||
external_client_type,
|
||||
use_quic,
|
||||
..
|
||||
} = &cli_config;
|
||||
|
||||
let keypair_count = *tx_count * keypair_multiplier;
|
||||
if *write_to_client_file {
|
||||
info!("Generating {} keypairs", keypair_count);
|
||||
let (keypairs, _) = generate_keypairs(id, keypair_count as u64);
|
||||
let num_accounts = keypairs.len() as u64;
|
||||
let max_fee =
|
||||
FeeRateGovernor::new(*target_lamports_per_signature, 0).max_lamports_per_signature;
|
||||
let num_lamports_per_account = (num_accounts - 1 + NUM_SIGNATURES_FOR_TXS * max_fee)
|
||||
/ num_accounts
|
||||
+ num_lamports_per_account;
|
||||
let mut accounts = HashMap::new();
|
||||
keypairs.iter().for_each(|keypair| {
|
||||
accounts.insert(
|
||||
serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(),
|
||||
Base64Account {
|
||||
balance: num_lamports_per_account,
|
||||
executable: false,
|
||||
owner: system_program::id().to_string(),
|
||||
data: String::new(),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
info!("Writing {}", client_ids_and_stake_file);
|
||||
let serialized = serde_yaml::to_string(&accounts).unwrap();
|
||||
let path = Path::new(&client_ids_and_stake_file);
|
||||
let mut file = File::create(path).unwrap();
|
||||
file.write_all(&serialized.into_bytes()).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Connecting to the cluster");
|
||||
|
||||
match external_client_type {
|
||||
ExternalClientType::RpcClient => {
|
||||
let client = Arc::new(RpcClient::new_with_commitment(
|
||||
json_rpc_url.to_string(),
|
||||
CommitmentConfig::confirmed(),
|
||||
));
|
||||
let keypairs = get_keypairs(
|
||||
client.clone(),
|
||||
id,
|
||||
keypair_count,
|
||||
*num_lamports_per_account,
|
||||
client_ids_and_stake_file,
|
||||
*read_from_client_file,
|
||||
);
|
||||
do_bench_tps(client, cli_config, keypairs);
|
||||
}
|
||||
ExternalClientType::ThinClient => {
|
||||
let nodes = discover_cluster(entrypoint_addr, *num_nodes, SocketAddrSpace::Unspecified)
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
||||
exit(1);
|
||||
});
|
||||
if *use_quic {
|
||||
connection_cache::set_use_quic(true);
|
||||
}
|
||||
let client = if *multi_client {
|
||||
let (client, num_clients) = get_multi_client(&nodes, &SocketAddrSpace::Unspecified);
|
||||
if nodes.len() < num_clients {
|
||||
eprintln!(
|
||||
"Error: Insufficient nodes discovered. Expecting {} or more",
|
||||
num_nodes
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
Arc::new(client)
|
||||
} else if let Some(target_node) = target_node {
|
||||
info!("Searching for target_node: {:?}", target_node);
|
||||
let mut target_client = None;
|
||||
for node in nodes {
|
||||
if node.id == *target_node {
|
||||
target_client =
|
||||
Some(Arc::new(get_client(&[node], &SocketAddrSpace::Unspecified)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
target_client.unwrap_or_else(|| {
|
||||
eprintln!("Target node {} not found", target_node);
|
||||
exit(1);
|
||||
})
|
||||
} else {
|
||||
Arc::new(get_client(&nodes, &SocketAddrSpace::Unspecified))
|
||||
};
|
||||
let keypairs = get_keypairs(
|
||||
client.clone(),
|
||||
id,
|
||||
keypair_count,
|
||||
*num_lamports_per_account,
|
||||
client_ids_and_stake_file,
|
||||
*read_from_client_file,
|
||||
);
|
||||
do_bench_tps(client, cli_config, keypairs);
|
||||
}
|
||||
ExternalClientType::TpuClient => {
|
||||
let rpc_client = Arc::new(RpcClient::new_with_commitment(
|
||||
json_rpc_url.to_string(),
|
||||
CommitmentConfig::confirmed(),
|
||||
));
|
||||
if *use_quic {
|
||||
connection_cache::set_use_quic(true);
|
||||
}
|
||||
let client = Arc::new(
|
||||
TpuClient::new(rpc_client, websocket_url, TpuClientConfig::default())
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("Could not create TpuClient {:?}", err);
|
||||
exit(1);
|
||||
}),
|
||||
);
|
||||
let keypairs = get_keypairs(
|
||||
client.clone(),
|
||||
id,
|
||||
keypair_count,
|
||||
*num_lamports_per_account,
|
||||
client_ids_and_stake_file,
|
||||
*read_from_client_file,
|
||||
);
|
||||
do_bench_tps(client, cli_config, keypairs);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
use {
|
||||
crate::bench_tps_client::BenchTpsClient,
|
||||
log::*,
|
||||
solana_sdk::{commitment_config::CommitmentConfig, timing::duration_as_s},
|
||||
std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SampleStats {
|
||||
/// Maximum TPS reported by this node
|
||||
pub tps: f32,
|
||||
/// Total time taken for those txs
|
||||
pub elapsed: Duration,
|
||||
/// Total transactions reported by this node
|
||||
pub txs: u64,
|
||||
}
|
||||
|
||||
pub fn sample_txs<T>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
sample_stats: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||
sample_period: u64,
|
||||
client: &Arc<T>,
|
||||
) where
|
||||
T: BenchTpsClient,
|
||||
{
|
||||
let mut max_tps = 0.0;
|
||||
let mut total_elapsed;
|
||||
let mut total_txs;
|
||||
let mut now = Instant::now();
|
||||
let start_time = now;
|
||||
let initial_txs = client
|
||||
.get_transaction_count_with_commitment(CommitmentConfig::processed())
|
||||
.expect("transaction count");
|
||||
let mut last_txs = initial_txs;
|
||||
|
||||
loop {
|
||||
total_elapsed = start_time.elapsed();
|
||||
let elapsed = now.elapsed();
|
||||
now = Instant::now();
|
||||
let mut txs =
|
||||
match client.get_transaction_count_with_commitment(CommitmentConfig::processed()) {
|
||||
Err(e) => {
|
||||
// ThinClient with multiple options should pick a better one now.
|
||||
info!("Couldn't get transaction count {:?}", e);
|
||||
sleep(Duration::from_secs(sample_period));
|
||||
continue;
|
||||
}
|
||||
Ok(tx_count) => tx_count,
|
||||
};
|
||||
|
||||
if txs < last_txs {
|
||||
info!("Expected txs({}) >= last_txs({})", txs, last_txs);
|
||||
txs = last_txs;
|
||||
}
|
||||
total_txs = txs - initial_txs;
|
||||
let sample_txs = txs - last_txs;
|
||||
last_txs = txs;
|
||||
|
||||
let tps = sample_txs as f32 / duration_as_s(&elapsed);
|
||||
if tps > max_tps {
|
||||
max_tps = tps;
|
||||
}
|
||||
|
||||
info!(
|
||||
"Sampler {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s",
|
||||
tps,
|
||||
sample_txs,
|
||||
total_txs,
|
||||
total_elapsed.as_secs(),
|
||||
);
|
||||
|
||||
if exit_signal.load(Ordering::Relaxed) {
|
||||
let stats = SampleStats {
|
||||
tps: max_tps,
|
||||
elapsed: total_elapsed,
|
||||
txs: total_txs,
|
||||
};
|
||||
sample_stats.write().unwrap().push((client.addr(), stats));
|
||||
return;
|
||||
}
|
||||
sleep(Duration::from_secs(sample_period));
|
||||
}
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
use {
|
||||
crossbeam_channel::unbounded,
|
||||
serial_test::serial,
|
||||
solana_bench_tps::{
|
||||
bench::{do_bench_tps, generate_and_fund_keypairs},
|
||||
cli::Config,
|
||||
},
|
||||
solana_client::{
|
||||
rpc_client::RpcClient,
|
||||
thin_client::create_client,
|
||||
tpu_client::{TpuClient, TpuClientConfig},
|
||||
},
|
||||
solana_core::validator::ValidatorConfig,
|
||||
solana_faucet::faucet::{run_local_faucet, run_local_faucet_with_port},
|
||||
solana_local_cluster::{
|
||||
local_cluster::{ClusterConfig, LocalCluster},
|
||||
validator_configs::make_identical_validator_configs,
|
||||
},
|
||||
solana_rpc::rpc::JsonRpcConfig,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
signature::{Keypair, Signer},
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
solana_test_validator::TestValidator,
|
||||
std::{sync::Arc, time::Duration},
|
||||
};
|
||||
|
||||
fn test_bench_tps_local_cluster(config: Config) {
|
||||
let native_instruction_processors = vec![];
|
||||
|
||||
solana_logger::setup();
|
||||
|
||||
let faucet_keypair = Keypair::new();
|
||||
let faucet_pubkey = faucet_keypair.pubkey();
|
||||
let (addr_sender, addr_receiver) = unbounded();
|
||||
run_local_faucet_with_port(faucet_keypair, addr_sender, None, 0);
|
||||
let faucet_addr = addr_receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("run_local_faucet")
|
||||
.expect("faucet_addr");
|
||||
|
||||
const NUM_NODES: usize = 1;
|
||||
let mut validator_config = ValidatorConfig::default_for_test();
|
||||
validator_config.rpc_config = JsonRpcConfig {
|
||||
faucet_addr: Some(faucet_addr),
|
||||
..JsonRpcConfig::default_for_test()
|
||||
};
|
||||
let cluster = LocalCluster::new(
|
||||
&mut ClusterConfig {
|
||||
node_stakes: vec![999_990; NUM_NODES],
|
||||
cluster_lamports: 200_000_000,
|
||||
validator_configs: make_identical_validator_configs(
|
||||
&ValidatorConfig {
|
||||
rpc_config: JsonRpcConfig {
|
||||
faucet_addr: Some(faucet_addr),
|
||||
..JsonRpcConfig::default_for_test()
|
||||
},
|
||||
..ValidatorConfig::default_for_test()
|
||||
},
|
||||
NUM_NODES,
|
||||
),
|
||||
native_instruction_processors,
|
||||
..ClusterConfig::default()
|
||||
},
|
||||
SocketAddrSpace::Unspecified,
|
||||
);
|
||||
|
||||
cluster.transfer(&cluster.funding_keypair, &faucet_pubkey, 100_000_000);
|
||||
|
||||
let client = Arc::new(create_client(
|
||||
cluster.entry_point_info.rpc,
|
||||
cluster.entry_point_info.tpu,
|
||||
));
|
||||
|
||||
let lamports_per_account = 100;
|
||||
|
||||
let keypair_count = config.tx_count * config.keypair_multiplier;
|
||||
let keypairs = generate_and_fund_keypairs(
|
||||
client.clone(),
|
||||
&config.id,
|
||||
keypair_count,
|
||||
lamports_per_account,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _total = do_bench_tps(client, config, keypairs);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
assert!(_total > 100);
|
||||
}
|
||||
|
||||
fn test_bench_tps_test_validator(config: Config) {
|
||||
solana_logger::setup();
|
||||
|
||||
let mint_keypair = Keypair::new();
|
||||
let mint_pubkey = mint_keypair.pubkey();
|
||||
|
||||
let faucet_addr = run_local_faucet(mint_keypair, None);
|
||||
|
||||
let test_validator =
|
||||
TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified);
|
||||
|
||||
let rpc_client = Arc::new(RpcClient::new_with_commitment(
|
||||
test_validator.rpc_url(),
|
||||
CommitmentConfig::processed(),
|
||||
));
|
||||
let websocket_url = test_validator.rpc_pubsub_url();
|
||||
|
||||
let client =
|
||||
Arc::new(TpuClient::new(rpc_client, &websocket_url, TpuClientConfig::default()).unwrap());
|
||||
|
||||
let lamports_per_account = 100;
|
||||
|
||||
let keypair_count = config.tx_count * config.keypair_multiplier;
|
||||
let keypairs = generate_and_fund_keypairs(
|
||||
client.clone(),
|
||||
&config.id,
|
||||
keypair_count,
|
||||
lamports_per_account,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _total = do_bench_tps(client, config, keypairs);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
assert!(_total > 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_bench_tps_local_cluster_solana() {
|
||||
test_bench_tps_local_cluster(Config {
|
||||
tx_count: 100,
|
||||
duration: Duration::from_secs(10),
|
||||
..Config::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_bench_tps_tpu_client() {
|
||||
test_bench_tps_test_validator(Config {
|
||||
tx_count: 100,
|
||||
duration: Duration::from_secs(10),
|
||||
..Config::default()
|
||||
});
|
||||
}
|
55
benches/bank.rs
Normal file
55
benches/bank.rs
Normal file
@ -0,0 +1,55 @@
|
||||
#![feature(test)]
|
||||
extern crate bincode;
|
||||
extern crate rayon;
|
||||
extern crate solana;
|
||||
extern crate test;
|
||||
|
||||
use bincode::serialize;
|
||||
use rayon::prelude::*;
|
||||
use solana::bank::*;
|
||||
use solana::hash::hash;
|
||||
use solana::mint::Mint;
|
||||
use solana::signature::{Keypair, KeypairUtil};
|
||||
use solana::system_transaction::SystemTransaction;
|
||||
use solana::transaction::Transaction;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_process_transaction(bencher: &mut Bencher) {
|
||||
let mint = Mint::new(100_000_000);
|
||||
let bank = Bank::new(&mint);
|
||||
|
||||
// Create transactions between unrelated parties.
|
||||
let transactions: Vec<_> = (0..4096)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
// Seed the 'from' account.
|
||||
let rando0 = Keypair::new();
|
||||
let tx = Transaction::system_move(
|
||||
&mint.keypair(),
|
||||
rando0.pubkey(),
|
||||
10_000,
|
||||
mint.last_id(),
|
||||
0,
|
||||
);
|
||||
assert!(bank.process_transaction(&tx).is_ok());
|
||||
|
||||
// Seed the 'to' account and a cell for its signature.
|
||||
let last_id = hash(&serialize(&i).unwrap()); // Unique hash
|
||||
bank.register_entry_id(&last_id);
|
||||
|
||||
let rando1 = Keypair::new();
|
||||
let tx = Transaction::system_move(&rando0, rando1.pubkey(), 1, last_id, 0);
|
||||
assert!(bank.process_transaction(&tx).is_ok());
|
||||
|
||||
// Finally, return the transaction to the benchmark.
|
||||
tx
|
||||
}).collect();
|
||||
|
||||
bencher.iter(|| {
|
||||
// Since benchmarker runs this multiple times, we need to clear the signatures.
|
||||
bank.clear_signatures();
|
||||
let results = bank.process_transactions(&transactions);
|
||||
assert!(results.iter().all(Result::is_ok));
|
||||
})
|
||||
}
|
110
benches/banking_stage.rs
Normal file
110
benches/banking_stage.rs
Normal file
@ -0,0 +1,110 @@
|
||||
#![feature(test)]
|
||||
extern crate bincode;
|
||||
extern crate rand;
|
||||
extern crate rayon;
|
||||
extern crate solana;
|
||||
extern crate solana_program_interface;
|
||||
extern crate test;
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use rayon::prelude::*;
|
||||
use solana::bank::Bank;
|
||||
use solana::banking_stage::{BankingStage, NUM_THREADS};
|
||||
use solana::entry::Entry;
|
||||
use solana::mint::Mint;
|
||||
use solana::packet::to_packets_chunked;
|
||||
use solana::signature::{KeypairUtil, Signature};
|
||||
use solana::system_transaction::SystemTransaction;
|
||||
use solana::transaction::Transaction;
|
||||
use solana_program_interface::pubkey::Pubkey;
|
||||
use std::iter;
|
||||
use std::sync::mpsc::{channel, Receiver};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use test::Bencher;
|
||||
|
||||
fn check_txs(receiver: &Receiver<Vec<Entry>>, ref_tx_count: usize) {
|
||||
let mut total = 0;
|
||||
loop {
|
||||
let entries = receiver.recv_timeout(Duration::new(1, 0));
|
||||
if let Ok(entries) = entries {
|
||||
for entry in &entries {
|
||||
total += entry.transactions.len();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if total >= ref_tx_count {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(total, ref_tx_count);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||
let txes = 1000 * NUM_THREADS;
|
||||
let mint_total = 1_000_000_000_000;
|
||||
let mint = Mint::new(mint_total);
|
||||
|
||||
let (verified_sender, verified_receiver) = channel();
|
||||
let bank = Arc::new(Bank::new(&mint));
|
||||
let dummy = Transaction::system_move(
|
||||
&mint.keypair(),
|
||||
mint.keypair().pubkey(),
|
||||
1,
|
||||
mint.last_id(),
|
||||
0,
|
||||
);
|
||||
let transactions: Vec<_> = (0..txes)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
let mut new = dummy.clone();
|
||||
let from: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
let to: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
let sig: Vec<u8> = (0..64).map(|_| thread_rng().gen()).collect();
|
||||
new.keys[0] = Pubkey::new(&from[0..32]);
|
||||
new.keys[1] = Pubkey::new(&to[0..32]);
|
||||
new.signature = Signature::new(&sig[0..64]);
|
||||
new
|
||||
}).collect();
|
||||
// fund all the accounts
|
||||
transactions.iter().for_each(|tx| {
|
||||
let fund = Transaction::system_move(
|
||||
&mint.keypair(),
|
||||
tx.keys[0],
|
||||
mint_total / txes as i64,
|
||||
mint.last_id(),
|
||||
0,
|
||||
);
|
||||
assert!(bank.process_transaction(&fund).is_ok());
|
||||
});
|
||||
//sanity check, make sure all the transactions can execute sequentially
|
||||
transactions.iter().for_each(|tx| {
|
||||
let res = bank.process_transaction(&tx);
|
||||
assert!(res.is_ok(), "sanity test transactions");
|
||||
});
|
||||
bank.clear_signatures();
|
||||
//sanity check, make sure all the transactions can execute in parallel
|
||||
let res = bank.process_transactions(&transactions);
|
||||
for r in res {
|
||||
assert!(r.is_ok(), "sanity parallel execution");
|
||||
}
|
||||
bank.clear_signatures();
|
||||
let verified: Vec<_> = to_packets_chunked(&transactions.clone(), 192)
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let len = x.read().unwrap().packets.len();
|
||||
(x, iter::repeat(1).take(len).collect())
|
||||
}).collect();
|
||||
let (_stage, signal_receiver) = BankingStage::new(&bank, verified_receiver, Default::default());
|
||||
bencher.iter(move || {
|
||||
for v in verified.chunks(verified.len() / NUM_THREADS) {
|
||||
verified_sender.send(v.to_vec()).unwrap();
|
||||
}
|
||||
check_txs(&signal_receiver, txes);
|
||||
bank.clear_signatures();
|
||||
// make sure the tx last id is still registered
|
||||
bank.register_entry_id(&mint.last_id());
|
||||
});
|
||||
}
|
25
benches/ledger.rs
Normal file
25
benches/ledger.rs
Normal file
@ -0,0 +1,25 @@
|
||||
#![feature(test)]
|
||||
extern crate solana;
|
||||
extern crate test;
|
||||
|
||||
use solana::hash::{hash, Hash};
|
||||
use solana::ledger::{next_entries, reconstruct_entries_from_blobs, Block};
|
||||
use solana::signature::{Keypair, KeypairUtil};
|
||||
use solana::system_transaction::SystemTransaction;
|
||||
use solana::transaction::Transaction;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_block_to_blobs_to_block(bencher: &mut Bencher) {
|
||||
let zero = Hash::default();
|
||||
let one = hash(&zero.as_ref());
|
||||
let keypair = Keypair::new();
|
||||
let tx0 = Transaction::system_move(&keypair, keypair.pubkey(), 1, one, 0);
|
||||
let transactions = vec![tx0; 10];
|
||||
let entries = next_entries(&zero, 1, transactions);
|
||||
|
||||
bencher.iter(|| {
|
||||
let blobs = entries.to_blobs();
|
||||
assert_eq!(reconstruct_entries_from_blobs(blobs).unwrap(), entries);
|
||||
});
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate solana;
|
||||
extern crate test;
|
||||
|
||||
use {solana_core::gen_keys::GenKeys, test::Bencher};
|
||||
use solana::signature::GenKeys;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_gen_keys(b: &mut Bencher) {
|
23
benches/sigverify.rs
Normal file
23
benches/sigverify.rs
Normal file
@ -0,0 +1,23 @@
|
||||
#![feature(test)]
|
||||
extern crate bincode;
|
||||
extern crate rayon;
|
||||
extern crate solana;
|
||||
extern crate test;
|
||||
|
||||
use solana::packet::to_packets;
|
||||
use solana::sigverify;
|
||||
use solana::system_transaction::test_tx;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_sigverify(bencher: &mut Bencher) {
|
||||
let tx = test_tx();
|
||||
|
||||
// generate packet vector
|
||||
let batches = to_packets(&vec![tx; 128]);
|
||||
|
||||
// verify packets
|
||||
bencher.iter(|| {
|
||||
let _ans = sigverify::ed25519_verify(&batches);
|
||||
})
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
[package]
|
||||
name = "solana-bloom"
|
||||
version = "1.11.0"
|
||||
description = "Solana bloom filter"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-bloom"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bv = { version = "0.11.1", features = ["serde"] }
|
||||
fnv = "1.0.7"
|
||||
log = "0.4.14"
|
||||
rand = "0.7.0"
|
||||
rayon = "1.5.1"
|
||||
serde = { version = "1.0.136", features = ["rc"] }
|
||||
serde_derive = "1.0.103"
|
||||
solana-frozen-abi = { path = "../frozen-abi", version = "=1.11.0" }
|
||||
solana-frozen-abi-macro = { path = "../frozen-abi/macro", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_bloom"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4"
|
@ -1,145 +0,0 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate test;
|
||||
use {
|
||||
bv::BitVec,
|
||||
fnv::FnvHasher,
|
||||
rand::Rng,
|
||||
solana_bloom::bloom::{AtomicBloom, Bloom, BloomHashIndex},
|
||||
solana_sdk::{
|
||||
hash::{hash, Hash},
|
||||
signature::Signature,
|
||||
},
|
||||
std::{collections::HashSet, hash::Hasher},
|
||||
test::Bencher,
|
||||
};
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_bits_set(bencher: &mut Bencher) {
|
||||
let mut bits: BitVec<u8> = BitVec::new_fill(false, 38_340_234_u64);
|
||||
let mut hasher = FnvHasher::default();
|
||||
|
||||
bencher.iter(|| {
|
||||
let idx = hasher.finish() % bits.len();
|
||||
bits.set(idx, true);
|
||||
hasher.write_u64(idx);
|
||||
});
|
||||
// subtract the next bencher result from this one to get a number for raw
|
||||
// bits.set()
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_bits_set_hasher(bencher: &mut Bencher) {
|
||||
let bits: BitVec<u8> = BitVec::new_fill(false, 38_340_234_u64);
|
||||
let mut hasher = FnvHasher::default();
|
||||
|
||||
bencher.iter(|| {
|
||||
let idx = hasher.finish() % bits.len();
|
||||
hasher.write_u64(idx);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_sigs_bloom(bencher: &mut Bencher) {
|
||||
// 1M TPS * 1s (length of block in sigs) == 1M items in filter
|
||||
// 1.0E-8 false positive rate
|
||||
// https://hur.st/bloomfilter/?n=1000000&p=1.0E-8&m=&k=
|
||||
let blockhash = hash(Hash::default().as_ref());
|
||||
// info!("blockhash = {:?}", blockhash);
|
||||
let keys = (0..27).map(|i| blockhash.hash_at_index(i)).collect();
|
||||
let mut sigs: Bloom<Signature> = Bloom::new(38_340_234, keys);
|
||||
|
||||
let mut id = blockhash;
|
||||
let mut falses = 0;
|
||||
let mut iterations = 0;
|
||||
bencher.iter(|| {
|
||||
id = hash(id.as_ref());
|
||||
let mut sigbytes = Vec::from(id.as_ref());
|
||||
id = hash(id.as_ref());
|
||||
sigbytes.extend(id.as_ref());
|
||||
|
||||
let sig = Signature::new(&sigbytes);
|
||||
if sigs.contains(&sig) {
|
||||
falses += 1;
|
||||
}
|
||||
sigs.add(&sig);
|
||||
sigs.contains(&sig);
|
||||
iterations += 1;
|
||||
});
|
||||
assert_eq!(falses, 0);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
#[ignore]
|
||||
fn bench_sigs_hashmap(bencher: &mut Bencher) {
|
||||
// same structure as above, new
|
||||
let blockhash = hash(Hash::default().as_ref());
|
||||
// info!("blockhash = {:?}", blockhash);
|
||||
let mut sigs: HashSet<Signature> = HashSet::new();
|
||||
|
||||
let mut id = blockhash;
|
||||
let mut falses = 0;
|
||||
let mut iterations = 0;
|
||||
bencher.iter(|| {
|
||||
id = hash(id.as_ref());
|
||||
let mut sigbytes = Vec::from(id.as_ref());
|
||||
id = hash(id.as_ref());
|
||||
sigbytes.extend(id.as_ref());
|
||||
|
||||
let sig = Signature::new(&sigbytes);
|
||||
if sigs.contains(&sig) {
|
||||
falses += 1;
|
||||
}
|
||||
sigs.insert(sig);
|
||||
sigs.contains(&sig);
|
||||
iterations += 1;
|
||||
});
|
||||
assert_eq!(falses, 0);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_add_hash(bencher: &mut Bencher) {
|
||||
let mut rng = rand::thread_rng();
|
||||
let hash_values: Vec<_> = std::iter::repeat_with(|| solana_sdk::hash::new_rand(&mut rng))
|
||||
.take(1200)
|
||||
.collect();
|
||||
let mut fail = 0;
|
||||
bencher.iter(|| {
|
||||
let mut bloom = Bloom::random(1287, 0.1, 7424);
|
||||
for hash_value in &hash_values {
|
||||
bloom.add(hash_value);
|
||||
}
|
||||
let index = rng.gen_range(0, hash_values.len());
|
||||
if !bloom.contains(&hash_values[index]) {
|
||||
fail += 1;
|
||||
}
|
||||
});
|
||||
assert_eq!(fail, 0);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_add_hash_atomic(bencher: &mut Bencher) {
|
||||
let mut rng = rand::thread_rng();
|
||||
let hash_values: Vec<_> = std::iter::repeat_with(|| solana_sdk::hash::new_rand(&mut rng))
|
||||
.take(1200)
|
||||
.collect();
|
||||
let mut fail = 0;
|
||||
bencher.iter(|| {
|
||||
let bloom: AtomicBloom<_> = Bloom::random(1287, 0.1, 7424).into();
|
||||
// Intentionally not using parallelism here, so that this and above
|
||||
// benchmark only compare the bit-vector ops.
|
||||
// For benchmarking the parallel code, change bellow for loop to:
|
||||
// hash_values.par_iter().for_each(|v| bloom.add(v));
|
||||
for hash_value in &hash_values {
|
||||
bloom.add(hash_value);
|
||||
}
|
||||
let index = rng.gen_range(0, hash_values.len());
|
||||
if !bloom.contains(&hash_values[index]) {
|
||||
fail += 1;
|
||||
}
|
||||
});
|
||||
assert_eq!(fail, 0);
|
||||
}
|
@ -1 +0,0 @@
|
||||
../frozen-abi/build.rs
|
@ -1,443 +0,0 @@
|
||||
//! Simple Bloom Filter
|
||||
use {
|
||||
bv::BitVec,
|
||||
fnv::FnvHasher,
|
||||
rand::{self, Rng},
|
||||
serde::{Deserialize, Serialize},
|
||||
solana_sdk::sanitize::{Sanitize, SanitizeError},
|
||||
std::{
|
||||
cmp, fmt,
|
||||
hash::Hasher,
|
||||
marker::PhantomData,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
/// Generate a stable hash of `self` for each `hash_index`
|
||||
/// Best effort can be made for uniqueness of each hash.
|
||||
pub trait BloomHashIndex {
|
||||
fn hash_at_index(&self, hash_index: u64) -> u64;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, AbiExample)]
|
||||
pub struct Bloom<T: BloomHashIndex> {
|
||||
pub keys: Vec<u64>,
|
||||
pub bits: BitVec<u64>,
|
||||
num_bits_set: u64,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: BloomHashIndex> fmt::Debug for Bloom<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Bloom {{ keys.len: {} bits.len: {} num_set: {} bits: ",
|
||||
self.keys.len(),
|
||||
self.bits.len(),
|
||||
self.num_bits_set
|
||||
)?;
|
||||
const MAX_PRINT_BITS: u64 = 10;
|
||||
for i in 0..std::cmp::min(MAX_PRINT_BITS, self.bits.len()) {
|
||||
if self.bits.get(i) {
|
||||
write!(f, "1")?;
|
||||
} else {
|
||||
write!(f, "0")?;
|
||||
}
|
||||
}
|
||||
if self.bits.len() > MAX_PRINT_BITS {
|
||||
write!(f, "..")?;
|
||||
}
|
||||
write!(f, " }}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BloomHashIndex> Sanitize for Bloom<T> {
|
||||
fn sanitize(&self) -> Result<(), SanitizeError> {
|
||||
// Avoid division by zero in self.pos(...).
|
||||
if self.bits.is_empty() {
|
||||
Err(SanitizeError::InvalidValue)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BloomHashIndex> Bloom<T> {
|
||||
pub fn new(num_bits: usize, keys: Vec<u64>) -> Self {
|
||||
let bits = BitVec::new_fill(false, num_bits as u64);
|
||||
Bloom {
|
||||
keys,
|
||||
bits,
|
||||
num_bits_set: 0,
|
||||
_phantom: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
/// Create filter optimal for num size given the `FALSE_RATE`.
|
||||
///
|
||||
/// The keys are randomized for picking data out of a collision resistant hash of size
|
||||
/// `keysize` bytes.
|
||||
///
|
||||
/// See <https://hur.st/bloomfilter/>.
|
||||
pub fn random(num_items: usize, false_rate: f64, max_bits: usize) -> Self {
|
||||
let m = Self::num_bits(num_items as f64, false_rate);
|
||||
let num_bits = cmp::max(1, cmp::min(m as usize, max_bits));
|
||||
let num_keys = Self::num_keys(num_bits as f64, num_items as f64) as usize;
|
||||
let keys: Vec<u64> = (0..num_keys).map(|_| rand::thread_rng().gen()).collect();
|
||||
Self::new(num_bits, keys)
|
||||
}
|
||||
fn num_bits(num_items: f64, false_rate: f64) -> f64 {
|
||||
let n = num_items;
|
||||
let p = false_rate;
|
||||
((n * p.ln()) / (1f64 / 2f64.powf(2f64.ln())).ln()).ceil()
|
||||
}
|
||||
fn num_keys(num_bits: f64, num_items: f64) -> f64 {
|
||||
let n = num_items;
|
||||
let m = num_bits;
|
||||
// infinity as usize is zero in rust 1.43 but 2^64-1 in rust 1.45; ensure it's zero here
|
||||
if n == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
1f64.max(((m / n) * 2f64.ln()).round())
|
||||
}
|
||||
}
|
||||
fn pos(&self, key: &T, k: u64) -> u64 {
|
||||
key.hash_at_index(k).wrapping_rem(self.bits.len())
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.bits = BitVec::new_fill(false, self.bits.len());
|
||||
self.num_bits_set = 0;
|
||||
}
|
||||
pub fn add(&mut self, key: &T) {
|
||||
for k in &self.keys {
|
||||
let pos = self.pos(key, *k);
|
||||
if !self.bits.get(pos) {
|
||||
self.num_bits_set = self.num_bits_set.saturating_add(1);
|
||||
self.bits.set(pos, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn contains(&self, key: &T) -> bool {
|
||||
for k in &self.keys {
|
||||
let pos = self.pos(key, *k);
|
||||
if !self.bits.get(pos) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn slice_hash(slice: &[u8], hash_index: u64) -> u64 {
|
||||
let mut hasher = FnvHasher::with_key(hash_index);
|
||||
hasher.write(slice);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> BloomHashIndex for T {
|
||||
fn hash_at_index(&self, hash_index: u64) -> u64 {
|
||||
slice_hash(self.as_ref(), hash_index)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AtomicBloom<T> {
|
||||
num_bits: u64,
|
||||
keys: Vec<u64>,
|
||||
bits: Vec<AtomicU64>,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: BloomHashIndex> From<Bloom<T>> for AtomicBloom<T> {
|
||||
fn from(bloom: Bloom<T>) -> Self {
|
||||
AtomicBloom {
|
||||
num_bits: bloom.bits.len(),
|
||||
keys: bloom.keys,
|
||||
bits: bloom
|
||||
.bits
|
||||
.into_boxed_slice()
|
||||
.iter()
|
||||
.map(|&x| AtomicU64::new(x))
|
||||
.collect(),
|
||||
_phantom: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BloomHashIndex> AtomicBloom<T> {
|
||||
fn pos(&self, key: &T, hash_index: u64) -> (usize, u64) {
|
||||
let pos = key.hash_at_index(hash_index).wrapping_rem(self.num_bits);
|
||||
// Divide by 64 to figure out which of the
|
||||
// AtomicU64 bit chunks we need to modify.
|
||||
let index = pos.wrapping_shr(6);
|
||||
// (pos & 63) is equivalent to mod 64 so that we can find
|
||||
// the index of the bit within the AtomicU64 to modify.
|
||||
let mask = 1u64.wrapping_shl(u32::try_from(pos & 63).unwrap());
|
||||
(index as usize, mask)
|
||||
}
|
||||
|
||||
/// Adds an item to the bloom filter and returns true if the item
|
||||
/// was not in the filter before.
|
||||
pub fn add(&self, key: &T) -> bool {
|
||||
let mut added = false;
|
||||
for k in &self.keys {
|
||||
let (index, mask) = self.pos(key, *k);
|
||||
let prev_val = self.bits[index].fetch_or(mask, Ordering::Relaxed);
|
||||
added = added || prev_val & mask == 0u64;
|
||||
}
|
||||
added
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &T) -> bool {
|
||||
self.keys.iter().all(|k| {
|
||||
let (index, mask) = self.pos(key, *k);
|
||||
let bit = self.bits[index].load(Ordering::Relaxed) & mask;
|
||||
bit != 0u64
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_for_tests(&mut self) {
|
||||
self.bits.iter().for_each(|bit| {
|
||||
bit.store(0u64, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
|
||||
// Only for tests and simulations.
|
||||
pub fn mock_clone(&self) -> Self {
|
||||
Self {
|
||||
keys: self.keys.clone(),
|
||||
bits: self
|
||||
.bits
|
||||
.iter()
|
||||
.map(|v| AtomicU64::new(v.load(Ordering::Relaxed)))
|
||||
.collect(),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BloomHashIndex> From<AtomicBloom<T>> for Bloom<T> {
|
||||
fn from(atomic_bloom: AtomicBloom<T>) -> Self {
|
||||
let bits: Vec<_> = atomic_bloom
|
||||
.bits
|
||||
.into_iter()
|
||||
.map(AtomicU64::into_inner)
|
||||
.collect();
|
||||
let num_bits_set = bits.iter().map(|x| x.count_ones() as u64).sum();
|
||||
let mut bits: BitVec<u64> = bits.into();
|
||||
bits.truncate(atomic_bloom.num_bits);
|
||||
Bloom {
|
||||
keys: atomic_bloom.keys,
|
||||
bits,
|
||||
num_bits_set,
|
||||
_phantom: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {
|
||||
super::*,
|
||||
rayon::prelude::*,
|
||||
solana_sdk::hash::{hash, Hash},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_bloom_filter() {
|
||||
//empty
|
||||
let bloom: Bloom<Hash> = Bloom::random(0, 0.1, 100);
|
||||
assert_eq!(bloom.keys.len(), 0);
|
||||
assert_eq!(bloom.bits.len(), 1);
|
||||
|
||||
//normal
|
||||
let bloom: Bloom<Hash> = Bloom::random(10, 0.1, 100);
|
||||
assert_eq!(bloom.keys.len(), 3);
|
||||
assert_eq!(bloom.bits.len(), 48);
|
||||
|
||||
//saturated
|
||||
let bloom: Bloom<Hash> = Bloom::random(100, 0.1, 100);
|
||||
assert_eq!(bloom.keys.len(), 1);
|
||||
assert_eq!(bloom.bits.len(), 100);
|
||||
}
|
||||
#[test]
|
||||
fn test_add_contains() {
|
||||
let mut bloom: Bloom<Hash> = Bloom::random(100, 0.1, 100);
|
||||
//known keys to avoid false positives in the test
|
||||
bloom.keys = vec![0, 1, 2, 3];
|
||||
|
||||
let key = hash(b"hello");
|
||||
assert!(!bloom.contains(&key));
|
||||
bloom.add(&key);
|
||||
assert!(bloom.contains(&key));
|
||||
|
||||
let key = hash(b"world");
|
||||
assert!(!bloom.contains(&key));
|
||||
bloom.add(&key);
|
||||
assert!(bloom.contains(&key));
|
||||
}
|
||||
#[test]
|
||||
fn test_random() {
|
||||
let mut b1: Bloom<Hash> = Bloom::random(10, 0.1, 100);
|
||||
let mut b2: Bloom<Hash> = Bloom::random(10, 0.1, 100);
|
||||
b1.keys.sort_unstable();
|
||||
b2.keys.sort_unstable();
|
||||
assert_ne!(b1.keys, b2.keys);
|
||||
}
|
||||
// Bloom filter math in python
|
||||
// n number of items
|
||||
// p false rate
|
||||
// m number of bits
|
||||
// k number of keys
|
||||
//
|
||||
// n = ceil(m / (-k / log(1 - exp(log(p) / k))))
|
||||
// p = pow(1 - exp(-k / (m / n)), k)
|
||||
// m = ceil((n * log(p)) / log(1 / pow(2, log(2))));
|
||||
// k = round((m / n) * log(2));
|
||||
#[test]
|
||||
fn test_filter_math() {
|
||||
assert_eq!(Bloom::<Hash>::num_bits(100f64, 0.1f64) as u64, 480u64);
|
||||
assert_eq!(Bloom::<Hash>::num_bits(100f64, 0.01f64) as u64, 959u64);
|
||||
assert_eq!(Bloom::<Hash>::num_keys(1000f64, 50f64) as u64, 14u64);
|
||||
assert_eq!(Bloom::<Hash>::num_keys(2000f64, 50f64) as u64, 28u64);
|
||||
assert_eq!(Bloom::<Hash>::num_keys(2000f64, 25f64) as u64, 55u64);
|
||||
//ensure min keys is 1
|
||||
assert_eq!(Bloom::<Hash>::num_keys(20f64, 1000f64) as u64, 1u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_debug() {
|
||||
let mut b: Bloom<Hash> = Bloom::new(3, vec![100]);
|
||||
b.add(&Hash::default());
|
||||
assert_eq!(
|
||||
format!("{:?}", b),
|
||||
"Bloom { keys.len: 1 bits.len: 3 num_set: 1 bits: 001 }"
|
||||
);
|
||||
|
||||
let mut b: Bloom<Hash> = Bloom::new(1000, vec![100]);
|
||||
b.add(&Hash::default());
|
||||
b.add(&hash(&[1, 2]));
|
||||
assert_eq!(
|
||||
format!("{:?}", b),
|
||||
"Bloom { keys.len: 1 bits.len: 1000 num_set: 2 bits: 0000000000.. }"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atomic_bloom() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let hash_values: Vec<_> = std::iter::repeat_with(|| solana_sdk::hash::new_rand(&mut rng))
|
||||
.take(1200)
|
||||
.collect();
|
||||
let bloom: AtomicBloom<_> = Bloom::<Hash>::random(1287, 0.1, 7424).into();
|
||||
assert_eq!(bloom.keys.len(), 3);
|
||||
assert_eq!(bloom.num_bits, 6168);
|
||||
assert_eq!(bloom.bits.len(), 97);
|
||||
hash_values.par_iter().for_each(|v| {
|
||||
bloom.add(v);
|
||||
});
|
||||
let bloom: Bloom<Hash> = bloom.into();
|
||||
assert_eq!(bloom.keys.len(), 3);
|
||||
assert_eq!(bloom.bits.len(), 6168);
|
||||
assert!(bloom.num_bits_set > 2000);
|
||||
for hash_value in hash_values {
|
||||
assert!(bloom.contains(&hash_value));
|
||||
}
|
||||
let false_positive = std::iter::repeat_with(|| solana_sdk::hash::new_rand(&mut rng))
|
||||
.take(10_000)
|
||||
.filter(|hash_value| bloom.contains(hash_value))
|
||||
.count();
|
||||
assert!(false_positive < 2_000, "false_positive: {}", false_positive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atomic_bloom_round_trip() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let keys: Vec<_> = std::iter::repeat_with(|| rng.gen()).take(5).collect();
|
||||
let mut bloom = Bloom::<Hash>::new(9731, keys.clone());
|
||||
let hash_values: Vec<_> = std::iter::repeat_with(|| solana_sdk::hash::new_rand(&mut rng))
|
||||
.take(1000)
|
||||
.collect();
|
||||
for hash_value in &hash_values {
|
||||
bloom.add(hash_value);
|
||||
}
|
||||
let num_bits_set = bloom.num_bits_set;
|
||||
assert!(num_bits_set > 2000, "# bits set: {}", num_bits_set);
|
||||
// Round-trip with no inserts.
|
||||
let bloom: AtomicBloom<_> = bloom.into();
|
||||
assert_eq!(bloom.num_bits, 9731);
|
||||
assert_eq!(bloom.bits.len(), (9731 + 63) / 64);
|
||||
for hash_value in &hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
let bloom: Bloom<_> = bloom.into();
|
||||
assert_eq!(bloom.num_bits_set, num_bits_set);
|
||||
for hash_value in &hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
// Round trip, re-inserting the same hash values.
|
||||
let bloom: AtomicBloom<_> = bloom.into();
|
||||
hash_values.par_iter().for_each(|v| {
|
||||
bloom.add(v);
|
||||
});
|
||||
for hash_value in &hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
let bloom: Bloom<_> = bloom.into();
|
||||
assert_eq!(bloom.num_bits_set, num_bits_set);
|
||||
assert_eq!(bloom.bits.len(), 9731);
|
||||
for hash_value in &hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
// Round trip, inserting new hash values.
|
||||
let more_hash_values: Vec<_> =
|
||||
std::iter::repeat_with(|| solana_sdk::hash::new_rand(&mut rng))
|
||||
.take(1000)
|
||||
.collect();
|
||||
let bloom: AtomicBloom<_> = bloom.into();
|
||||
assert_eq!(bloom.num_bits, 9731);
|
||||
assert_eq!(bloom.bits.len(), (9731 + 63) / 64);
|
||||
more_hash_values.par_iter().for_each(|v| {
|
||||
bloom.add(v);
|
||||
});
|
||||
for hash_value in &hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
for hash_value in &more_hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
let false_positive = std::iter::repeat_with(|| solana_sdk::hash::new_rand(&mut rng))
|
||||
.take(10_000)
|
||||
.filter(|hash_value| bloom.contains(hash_value))
|
||||
.count();
|
||||
assert!(false_positive < 2000, "false_positive: {}", false_positive);
|
||||
let bloom: Bloom<_> = bloom.into();
|
||||
assert_eq!(bloom.bits.len(), 9731);
|
||||
assert!(bloom.num_bits_set > num_bits_set);
|
||||
assert!(
|
||||
bloom.num_bits_set > 4000,
|
||||
"# bits set: {}",
|
||||
bloom.num_bits_set
|
||||
);
|
||||
for hash_value in &hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
for hash_value in &more_hash_values {
|
||||
assert!(bloom.contains(hash_value));
|
||||
}
|
||||
let false_positive = std::iter::repeat_with(|| solana_sdk::hash::new_rand(&mut rng))
|
||||
.take(10_000)
|
||||
.filter(|hash_value| bloom.contains(hash_value))
|
||||
.count();
|
||||
assert!(false_positive < 2000, "false_positive: {}", false_positive);
|
||||
// Assert that the bits vector precisely match if no atomic ops were
|
||||
// used.
|
||||
let bits = bloom.bits;
|
||||
let mut bloom = Bloom::<Hash>::new(9731, keys);
|
||||
for hash_value in &hash_values {
|
||||
bloom.add(hash_value);
|
||||
}
|
||||
for hash_value in &more_hash_values {
|
||||
bloom.add(hash_value);
|
||||
}
|
||||
assert_eq!(bits, bloom.bits);
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))]
|
||||
pub mod bloom;
|
||||
|
||||
#[macro_use]
|
||||
extern crate solana_frozen_abi_macro;
|
@ -1,32 +0,0 @@
|
||||
[package]
|
||||
name = "solana-bucket-map"
|
||||
version = "1.11.0"
|
||||
description = "solana-bucket-map"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-bucket-map"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
license = "Apache-2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
log = { version = "0.4.11" }
|
||||
memmap2 = "0.5.3"
|
||||
modular-bitfield = "0.11.2"
|
||||
rand = "0.7.0"
|
||||
solana-measure = { path = "../measure", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
tempfile = "3.3.0"
|
||||
|
||||
[dev-dependencies]
|
||||
fs_extra = "1.2.0"
|
||||
rayon = "1.5.0"
|
||||
solana-logger = { path = "../logger", version = "=1.11.0" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_bucket_map"
|
||||
|
||||
[[bench]]
|
||||
name = "bucket_map"
|
@ -1,77 +0,0 @@
|
||||
#![feature(test)]
|
||||
|
||||
macro_rules! DEFINE_NxM_BENCH {
|
||||
($i:ident, $n:literal, $m:literal) => {
|
||||
mod $i {
|
||||
use super::*;
|
||||
|
||||
#[bench]
|
||||
fn bench_insert_baseline_hashmap(bencher: &mut Bencher) {
|
||||
do_bench_insert_baseline_hashmap(bencher, $n, $m);
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_insert_bucket_map(bencher: &mut Bencher) {
|
||||
do_bench_insert_bucket_map(bencher, $n, $m);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extern crate test;
|
||||
use {
|
||||
rayon::prelude::*,
|
||||
solana_bucket_map::bucket_map::{BucketMap, BucketMapConfig},
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{collections::hash_map::HashMap, sync::RwLock},
|
||||
test::Bencher,
|
||||
};
|
||||
|
||||
type IndexValue = u64;
|
||||
|
||||
DEFINE_NxM_BENCH!(dim_01x02, 1, 2);
|
||||
DEFINE_NxM_BENCH!(dim_02x04, 2, 4);
|
||||
DEFINE_NxM_BENCH!(dim_04x08, 4, 8);
|
||||
DEFINE_NxM_BENCH!(dim_08x16, 8, 16);
|
||||
DEFINE_NxM_BENCH!(dim_16x32, 16, 32);
|
||||
DEFINE_NxM_BENCH!(dim_32x64, 32, 64);
|
||||
|
||||
/// Benchmark insert with Hashmap as baseline for N threads inserting M keys each
|
||||
fn do_bench_insert_baseline_hashmap(bencher: &mut Bencher, n: usize, m: usize) {
|
||||
let index = RwLock::new(HashMap::new());
|
||||
(0..n).into_iter().into_par_iter().for_each(|i| {
|
||||
let key = Pubkey::new_unique();
|
||||
index
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(key, vec![(i, IndexValue::default())]);
|
||||
});
|
||||
bencher.iter(|| {
|
||||
(0..n).into_iter().into_par_iter().for_each(|_| {
|
||||
for j in 0..m {
|
||||
let key = Pubkey::new_unique();
|
||||
index
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(key, vec![(j, IndexValue::default())]);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark insert with BucketMap with N buckets for N threads inserting M keys each
|
||||
fn do_bench_insert_bucket_map(bencher: &mut Bencher, n: usize, m: usize) {
|
||||
let index = BucketMap::new(BucketMapConfig::new(n));
|
||||
(0..n).into_iter().into_par_iter().for_each(|i| {
|
||||
let key = Pubkey::new_unique();
|
||||
index.update(&key, |_| Some((vec![(i, IndexValue::default())], 0)));
|
||||
});
|
||||
bencher.iter(|| {
|
||||
(0..n).into_iter().into_par_iter().for_each(|_| {
|
||||
for j in 0..m {
|
||||
let key = Pubkey::new_unique();
|
||||
index.update(&key, |_| Some((vec![(j, IndexValue::default())], 0)));
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
@ -1,501 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
bucket_item::BucketItem,
|
||||
bucket_map::BucketMapError,
|
||||
bucket_stats::BucketMapStats,
|
||||
bucket_storage::{BucketStorage, Uid, DEFAULT_CAPACITY_POW2},
|
||||
index_entry::IndexEntry,
|
||||
MaxSearch, RefCount,
|
||||
},
|
||||
rand::{thread_rng, Rng},
|
||||
solana_measure::measure::Measure,
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
ops::RangeBounds,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ReallocatedItems {
|
||||
// Some if the index was reallocated
|
||||
// u64 is random associated with the new index
|
||||
pub index: Option<(u64, BucketStorage)>,
|
||||
// Some for a data bucket reallocation
|
||||
// u64 is data bucket index
|
||||
pub data: Option<(u64, BucketStorage)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Reallocated {
|
||||
/// > 0 if reallocations are encoded
|
||||
pub active_reallocations: AtomicUsize,
|
||||
|
||||
/// actual reallocated bucket
|
||||
/// mutex because bucket grow code runs with a read lock
|
||||
pub items: Mutex<ReallocatedItems>,
|
||||
}
|
||||
|
||||
impl Reallocated {
|
||||
/// specify that a reallocation has occurred
|
||||
pub fn add_reallocation(&self) {
|
||||
assert_eq!(
|
||||
0,
|
||||
self.active_reallocations.fetch_add(1, Ordering::Relaxed),
|
||||
"Only 1 reallocation can occur at a time"
|
||||
);
|
||||
}
|
||||
/// Return true IFF a reallocation has occurred.
|
||||
/// Calling this takes conceptual ownership of the reallocation encoded in the struct.
|
||||
pub fn get_reallocated(&self) -> bool {
|
||||
self.active_reallocations
|
||||
.compare_exchange(1, 0, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
// >= 2 instances of BucketStorage per 'bucket' in the bucket map. 1 for index, >= 1 for data
|
||||
pub struct Bucket<T> {
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
//index
|
||||
pub index: BucketStorage,
|
||||
//random offset for the index
|
||||
random: u64,
|
||||
//storage buckets to store SlotSlice up to a power of 2 in len
|
||||
pub data: Vec<BucketStorage>,
|
||||
_phantom: PhantomData<T>,
|
||||
stats: Arc<BucketMapStats>,
|
||||
|
||||
pub reallocated: Reallocated,
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy> Bucket<T> {
|
||||
pub fn new(
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
max_search: MaxSearch,
|
||||
stats: Arc<BucketMapStats>,
|
||||
count: Arc<AtomicU64>,
|
||||
) -> Self {
|
||||
let index = BucketStorage::new(
|
||||
Arc::clone(&drives),
|
||||
1,
|
||||
std::mem::size_of::<IndexEntry>() as u64,
|
||||
max_search,
|
||||
Arc::clone(&stats.index),
|
||||
count,
|
||||
);
|
||||
Self {
|
||||
random: thread_rng().gen(),
|
||||
drives,
|
||||
index,
|
||||
data: vec![],
|
||||
_phantom: PhantomData::default(),
|
||||
stats,
|
||||
reallocated: Reallocated::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> Vec<Pubkey> {
|
||||
let mut rv = vec![];
|
||||
for i in 0..self.index.capacity() {
|
||||
if self.index.is_free(i) {
|
||||
continue;
|
||||
}
|
||||
let ix: &IndexEntry = self.index.get(i);
|
||||
rv.push(ix.key);
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn items_in_range<R>(&self, range: &Option<&R>) -> Vec<BucketItem<T>>
|
||||
where
|
||||
R: RangeBounds<Pubkey>,
|
||||
{
|
||||
let mut result = Vec::with_capacity(self.index.count.load(Ordering::Relaxed) as usize);
|
||||
for i in 0..self.index.capacity() {
|
||||
let ii = i % self.index.capacity();
|
||||
if self.index.is_free(ii) {
|
||||
continue;
|
||||
}
|
||||
let ix: &IndexEntry = self.index.get(ii);
|
||||
let key = ix.key;
|
||||
if range.map(|r| r.contains(&key)).unwrap_or(true) {
|
||||
let val = ix.read_value(self);
|
||||
result.push(BucketItem {
|
||||
pubkey: key,
|
||||
ref_count: ix.ref_count(),
|
||||
slot_list: val.map(|(v, _ref_count)| v.to_vec()).unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn find_entry(&self, key: &Pubkey) -> Option<(&IndexEntry, u64)> {
|
||||
Self::bucket_find_entry(&self.index, key, self.random)
|
||||
}
|
||||
|
||||
fn find_entry_mut(&self, key: &Pubkey) -> Option<(&mut IndexEntry, u64)> {
|
||||
Self::bucket_find_entry_mut(&self.index, key, self.random)
|
||||
}
|
||||
|
||||
fn bucket_find_entry_mut<'a>(
|
||||
index: &'a BucketStorage,
|
||||
key: &Pubkey,
|
||||
random: u64,
|
||||
) -> Option<(&'a mut IndexEntry, u64)> {
|
||||
let ix = Self::bucket_index_ix(index, key, random);
|
||||
for i in ix..ix + index.max_search() {
|
||||
let ii = i % index.capacity();
|
||||
if index.is_free(ii) {
|
||||
continue;
|
||||
}
|
||||
let elem: &mut IndexEntry = index.get_mut(ii);
|
||||
if elem.key == *key {
|
||||
return Some((elem, ii));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn bucket_find_entry<'a>(
|
||||
index: &'a BucketStorage,
|
||||
key: &Pubkey,
|
||||
random: u64,
|
||||
) -> Option<(&'a IndexEntry, u64)> {
|
||||
let ix = Self::bucket_index_ix(index, key, random);
|
||||
for i in ix..ix + index.max_search() {
|
||||
let ii = i % index.capacity();
|
||||
if index.is_free(ii) {
|
||||
continue;
|
||||
}
|
||||
let elem: &IndexEntry = index.get(ii);
|
||||
if elem.key == *key {
|
||||
return Some((elem, ii));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn bucket_create_key(
|
||||
index: &mut BucketStorage,
|
||||
key: &Pubkey,
|
||||
elem_uid: Uid,
|
||||
random: u64,
|
||||
is_resizing: bool,
|
||||
) -> Result<u64, BucketMapError> {
|
||||
let ix = Self::bucket_index_ix(index, key, random);
|
||||
for i in ix..ix + index.max_search() {
|
||||
let ii = i as u64 % index.capacity();
|
||||
if !index.is_free(ii) {
|
||||
continue;
|
||||
}
|
||||
index.allocate(ii, elem_uid, is_resizing).unwrap();
|
||||
let elem: &mut IndexEntry = index.get_mut(ii);
|
||||
// These fields will be overwritten after allocation by callers.
|
||||
// Since this part of the mmapped file could have previously been used by someone else, there can be garbage here.
|
||||
elem.init(key);
|
||||
//debug!( "INDEX ALLOC {:?} {} {} {}", key, ii, index.capacity, elem_uid );
|
||||
return Ok(ii);
|
||||
}
|
||||
Err(BucketMapError::IndexNoSpace(index.capacity_pow2))
|
||||
}
|
||||
|
||||
pub fn addref(&mut self, key: &Pubkey) -> Option<RefCount> {
|
||||
let (elem, _) = self.find_entry_mut(key)?;
|
||||
elem.ref_count += 1;
|
||||
Some(elem.ref_count)
|
||||
}
|
||||
|
||||
pub fn unref(&mut self, key: &Pubkey) -> Option<RefCount> {
|
||||
let (elem, _) = self.find_entry_mut(key)?;
|
||||
elem.ref_count -= 1;
|
||||
Some(elem.ref_count)
|
||||
}
|
||||
|
||||
fn create_key(&mut self, key: &Pubkey) -> Result<u64, BucketMapError> {
|
||||
Self::bucket_create_key(
|
||||
&mut self.index,
|
||||
key,
|
||||
IndexEntry::key_uid(key),
|
||||
self.random,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn read_value(&self, key: &Pubkey) -> Option<(&[T], RefCount)> {
|
||||
//debug!("READ_VALUE: {:?}", key);
|
||||
let (elem, _) = self.find_entry(key)?;
|
||||
elem.read_value(self)
|
||||
}
|
||||
|
||||
pub fn try_write(
|
||||
&mut self,
|
||||
key: &Pubkey,
|
||||
data: &[T],
|
||||
ref_count: u64,
|
||||
) -> Result<(), BucketMapError> {
|
||||
let best_fit_bucket = IndexEntry::data_bucket_from_num_slots(data.len() as u64);
|
||||
if self.data.get(best_fit_bucket as usize).is_none() {
|
||||
// fail early if the data bucket we need doesn't exist - we don't want the index entry partially allocated
|
||||
return Err(BucketMapError::DataNoSpace((best_fit_bucket, 0)));
|
||||
}
|
||||
let index_entry = self.find_entry_mut(key);
|
||||
let (elem, elem_ix) = match index_entry {
|
||||
None => {
|
||||
let ii = self.create_key(key)?;
|
||||
let elem: &mut IndexEntry = self.index.get_mut(ii);
|
||||
(elem, ii)
|
||||
}
|
||||
Some(res) => res,
|
||||
};
|
||||
elem.ref_count = ref_count;
|
||||
let elem_uid = self.index.uid_unchecked(elem_ix);
|
||||
let bucket_ix = elem.data_bucket_ix();
|
||||
let current_bucket = &self.data[bucket_ix as usize];
|
||||
let num_slots = data.len() as u64;
|
||||
if best_fit_bucket == bucket_ix && elem.num_slots > 0 {
|
||||
// in place update
|
||||
let elem_loc = elem.data_loc(current_bucket);
|
||||
let slice: &mut [T] = current_bucket.get_mut_cell_slice(elem_loc, data.len() as u64);
|
||||
assert_eq!(current_bucket.uid(elem_loc), Some(elem_uid));
|
||||
elem.num_slots = num_slots;
|
||||
slice.copy_from_slice(data);
|
||||
Ok(())
|
||||
} else {
|
||||
// need to move the allocation to a best fit spot
|
||||
let best_bucket = &self.data[best_fit_bucket as usize];
|
||||
let cap_power = best_bucket.capacity_pow2;
|
||||
let cap = best_bucket.capacity();
|
||||
let pos = thread_rng().gen_range(0, cap);
|
||||
for i in pos..pos + self.index.max_search() {
|
||||
let ix = i % cap;
|
||||
if best_bucket.is_free(ix) {
|
||||
let elem_loc = elem.data_loc(current_bucket);
|
||||
let old_slots = elem.num_slots;
|
||||
elem.set_storage_offset(ix);
|
||||
elem.set_storage_capacity_when_created_pow2(best_bucket.capacity_pow2);
|
||||
elem.num_slots = num_slots;
|
||||
if old_slots > 0 {
|
||||
let current_bucket = &mut self.data[bucket_ix as usize];
|
||||
current_bucket.free(elem_loc, elem_uid);
|
||||
}
|
||||
//debug!( "DATA ALLOC {:?} {} {} {}", key, elem.data_location, best_bucket.capacity, elem_uid );
|
||||
if num_slots > 0 {
|
||||
let best_bucket = &mut self.data[best_fit_bucket as usize];
|
||||
best_bucket.allocate(ix, elem_uid, false).unwrap();
|
||||
let slice = best_bucket.get_mut_cell_slice(ix, num_slots);
|
||||
slice.copy_from_slice(data);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(BucketMapError::DataNoSpace((best_fit_bucket, cap_power)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_key(&mut self, key: &Pubkey) {
|
||||
if let Some((elem, elem_ix)) = self.find_entry(key) {
|
||||
let elem_uid = self.index.uid_unchecked(elem_ix);
|
||||
if elem.num_slots > 0 {
|
||||
let ix = elem.data_bucket_ix() as usize;
|
||||
let data_bucket = &self.data[ix];
|
||||
let loc = elem.data_loc(data_bucket);
|
||||
let data_bucket = &mut self.data[ix];
|
||||
//debug!( "DATA FREE {:?} {} {} {}", key, elem.data_location, data_bucket.capacity, elem_uid );
|
||||
data_bucket.free(loc, elem_uid);
|
||||
}
|
||||
//debug!("INDEX FREE {:?} {}", key, elem_uid);
|
||||
self.index.free(elem_ix, elem_uid);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grow_index(&self, current_capacity_pow2: u8) {
|
||||
if self.index.capacity_pow2 == current_capacity_pow2 {
|
||||
let mut m = Measure::start("grow_index");
|
||||
//debug!("GROW_INDEX: {}", current_capacity_pow2);
|
||||
let increment = 1;
|
||||
for i in increment.. {
|
||||
//increasing the capacity by ^4 reduces the
|
||||
//likelyhood of a re-index collision of 2^(max_search)^2
|
||||
//1 in 2^32
|
||||
let mut index = BucketStorage::new_with_capacity(
|
||||
Arc::clone(&self.drives),
|
||||
1,
|
||||
std::mem::size_of::<IndexEntry>() as u64,
|
||||
// *2 causes rapid growth of index buckets
|
||||
self.index.capacity_pow2 + i, // * 2,
|
||||
self.index.max_search,
|
||||
Arc::clone(&self.stats.index),
|
||||
Arc::clone(&self.index.count),
|
||||
);
|
||||
let random = thread_rng().gen();
|
||||
let mut valid = true;
|
||||
for ix in 0..self.index.capacity() {
|
||||
let uid = self.index.uid(ix);
|
||||
if let Some(uid) = uid {
|
||||
let elem: &IndexEntry = self.index.get(ix);
|
||||
let new_ix =
|
||||
Self::bucket_create_key(&mut index, &elem.key, uid, random, true);
|
||||
if new_ix.is_err() {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
let new_ix = new_ix.unwrap();
|
||||
let new_elem: &mut IndexEntry = index.get_mut(new_ix);
|
||||
*new_elem = *elem;
|
||||
/*
|
||||
let dbg_elem: IndexEntry = *new_elem;
|
||||
assert_eq!(
|
||||
Self::bucket_find_entry(&index, &elem.key, random).unwrap(),
|
||||
(&dbg_elem, new_ix)
|
||||
);
|
||||
*/
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
let sz = index.capacity();
|
||||
{
|
||||
let mut max = self.stats.index.max_size.lock().unwrap();
|
||||
*max = std::cmp::max(*max, sz);
|
||||
}
|
||||
let mut items = self.reallocated.items.lock().unwrap();
|
||||
items.index = Some((random, index));
|
||||
self.reallocated.add_reallocation();
|
||||
break;
|
||||
}
|
||||
}
|
||||
m.stop();
|
||||
self.stats.index.resizes.fetch_add(1, Ordering::Relaxed);
|
||||
self.stats
|
||||
.index
|
||||
.resize_us
|
||||
.fetch_add(m.as_us(), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_grow_index(&mut self, random: u64, index: BucketStorage) {
|
||||
self.random = random;
|
||||
self.index = index;
|
||||
}
|
||||
|
||||
fn elem_size() -> u64 {
|
||||
std::mem::size_of::<T>() as u64
|
||||
}
|
||||
|
||||
pub fn apply_grow_data(&mut self, ix: usize, bucket: BucketStorage) {
|
||||
if self.data.get(ix).is_none() {
|
||||
for i in self.data.len()..ix {
|
||||
// insert empty data buckets
|
||||
self.data.push(BucketStorage::new(
|
||||
Arc::clone(&self.drives),
|
||||
1 << i,
|
||||
Self::elem_size(),
|
||||
self.index.max_search,
|
||||
Arc::clone(&self.stats.data),
|
||||
Arc::default(),
|
||||
))
|
||||
}
|
||||
self.data.push(bucket);
|
||||
} else {
|
||||
self.data[ix] = bucket;
|
||||
}
|
||||
}
|
||||
|
||||
/// grow a data bucket
|
||||
/// The application of the new bucket is deferred until the next write lock.
|
||||
pub fn grow_data(&self, data_index: u64, current_capacity_pow2: u8) {
|
||||
let new_bucket = BucketStorage::new_resized(
|
||||
&self.drives,
|
||||
self.index.max_search,
|
||||
self.data.get(data_index as usize),
|
||||
std::cmp::max(current_capacity_pow2 + 1, DEFAULT_CAPACITY_POW2),
|
||||
1 << data_index,
|
||||
Self::elem_size(),
|
||||
&self.stats.data,
|
||||
);
|
||||
self.reallocated.add_reallocation();
|
||||
let mut items = self.reallocated.items.lock().unwrap();
|
||||
items.data = Some((data_index, new_bucket));
|
||||
}
|
||||
|
||||
fn bucket_index_ix(index: &BucketStorage, key: &Pubkey, random: u64) -> u64 {
|
||||
let uid = IndexEntry::key_uid(key);
|
||||
let mut s = DefaultHasher::new();
|
||||
uid.hash(&mut s);
|
||||
//the locally generated random will make it hard for an attacker
|
||||
//to deterministically cause all the pubkeys to land in the same
|
||||
//location in any bucket on all validators
|
||||
random.hash(&mut s);
|
||||
let ix = s.finish();
|
||||
ix % index.capacity()
|
||||
//debug!( "INDEX_IX: {:?} uid:{} loc: {} cap:{}", key, uid, location, index.capacity() );
|
||||
}
|
||||
|
||||
/// grow the appropriate piece. Note this takes an immutable ref.
|
||||
/// The actual grow is set into self.reallocated and applied later on a write lock
|
||||
pub fn grow(&self, err: BucketMapError) {
|
||||
match err {
|
||||
BucketMapError::DataNoSpace((data_index, current_capacity_pow2)) => {
|
||||
//debug!("GROWING SPACE {:?}", (data_index, current_capacity_pow2));
|
||||
self.grow_data(data_index, current_capacity_pow2);
|
||||
}
|
||||
BucketMapError::IndexNoSpace(current_capacity_pow2) => {
|
||||
//debug!("GROWING INDEX {}", sz);
|
||||
self.grow_index(current_capacity_pow2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// if a bucket was resized previously with a read lock, then apply that resize now
|
||||
pub fn handle_delayed_grows(&mut self) {
|
||||
if self.reallocated.get_reallocated() {
|
||||
// swap out the bucket that was resized previously with a read lock
|
||||
let mut items = ReallocatedItems::default();
|
||||
std::mem::swap(&mut items, &mut self.reallocated.items.lock().unwrap());
|
||||
|
||||
if let Some((random, bucket)) = items.index.take() {
|
||||
self.apply_grow_index(random, bucket);
|
||||
} else {
|
||||
// data bucket
|
||||
let (i, new_bucket) = items.data.take().unwrap();
|
||||
self.apply_grow_data(i as usize, new_bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: &Pubkey, value: (&[T], RefCount)) {
|
||||
let (new, refct) = value;
|
||||
loop {
|
||||
let rv = self.try_write(key, new, refct);
|
||||
match rv {
|
||||
Ok(_) => return,
|
||||
Err(err) => {
|
||||
self.grow(err);
|
||||
self.handle_delayed_grows();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update<F>(&mut self, key: &Pubkey, mut updatefn: F)
|
||||
where
|
||||
F: FnMut(Option<(&[T], RefCount)>) -> Option<(Vec<T>, RefCount)>,
|
||||
{
|
||||
let current = self.read_value(key);
|
||||
let new = updatefn(current);
|
||||
if new.is_none() {
|
||||
self.delete_key(key);
|
||||
return;
|
||||
}
|
||||
let (new, refct) = new.unwrap();
|
||||
self.insert(key, (&new, refct));
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
use {
|
||||
crate::{
|
||||
bucket::Bucket, bucket_item::BucketItem, bucket_map::BucketMapError,
|
||||
bucket_stats::BucketMapStats, MaxSearch, RefCount,
|
||||
},
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{
|
||||
ops::RangeBounds,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc, RwLock, RwLockWriteGuard,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type LockedBucket<T> = RwLock<Option<Bucket<T>>>;
|
||||
|
||||
pub struct BucketApi<T: Clone + Copy> {
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
max_search: MaxSearch,
|
||||
pub stats: Arc<BucketMapStats>,
|
||||
|
||||
bucket: LockedBucket<T>,
|
||||
count: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy> BucketApi<T> {
|
||||
pub fn new(
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
max_search: MaxSearch,
|
||||
stats: Arc<BucketMapStats>,
|
||||
) -> Self {
|
||||
Self {
|
||||
drives,
|
||||
max_search,
|
||||
stats,
|
||||
bucket: RwLock::default(),
|
||||
count: Arc::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the items for bucket
|
||||
pub fn items_in_range<R>(&self, range: &Option<&R>) -> Vec<BucketItem<T>>
|
||||
where
|
||||
R: RangeBounds<Pubkey>,
|
||||
{
|
||||
self.bucket
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|bucket| bucket.items_in_range(range))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the Pubkeys
|
||||
pub fn keys(&self) -> Vec<Pubkey> {
|
||||
self.bucket
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map_or_else(Vec::default, |bucket| bucket.keys())
|
||||
}
|
||||
|
||||
/// Get the values for Pubkey `key`
|
||||
pub fn read_value(&self, key: &Pubkey) -> Option<(Vec<T>, RefCount)> {
|
||||
self.bucket.read().unwrap().as_ref().and_then(|bucket| {
|
||||
bucket
|
||||
.read_value(key)
|
||||
.map(|(value, ref_count)| (value.to_vec(), ref_count))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bucket_len(&self) -> u64 {
|
||||
self.count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn delete_key(&self, key: &Pubkey) {
|
||||
let mut bucket = self.get_write_bucket();
|
||||
if let Some(bucket) = bucket.as_mut() {
|
||||
bucket.delete_key(key)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_write_bucket(&self) -> RwLockWriteGuard<Option<Bucket<T>>> {
|
||||
let mut bucket = self.bucket.write().unwrap();
|
||||
if bucket.is_none() {
|
||||
*bucket = Some(Bucket::new(
|
||||
Arc::clone(&self.drives),
|
||||
self.max_search,
|
||||
Arc::clone(&self.stats),
|
||||
Arc::clone(&self.count),
|
||||
));
|
||||
} else {
|
||||
let write = bucket.as_mut().unwrap();
|
||||
write.handle_delayed_grows();
|
||||
}
|
||||
bucket
|
||||
}
|
||||
|
||||
pub fn addref(&self, key: &Pubkey) -> Option<RefCount> {
|
||||
self.get_write_bucket()
|
||||
.as_mut()
|
||||
.and_then(|bucket| bucket.addref(key))
|
||||
}
|
||||
|
||||
pub fn unref(&self, key: &Pubkey) -> Option<RefCount> {
|
||||
self.get_write_bucket()
|
||||
.as_mut()
|
||||
.and_then(|bucket| bucket.unref(key))
|
||||
}
|
||||
|
||||
pub fn insert(&self, pubkey: &Pubkey, value: (&[T], RefCount)) {
|
||||
let mut bucket = self.get_write_bucket();
|
||||
bucket.as_mut().unwrap().insert(pubkey, value)
|
||||
}
|
||||
|
||||
pub fn grow(&self, err: BucketMapError) {
|
||||
// grows are special - they get a read lock and modify 'reallocated'
|
||||
// the grown changes are applied the next time there is a write lock taken
|
||||
if let Some(bucket) = self.bucket.read().unwrap().as_ref() {
|
||||
bucket.grow(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update<F>(&self, key: &Pubkey, updatefn: F)
|
||||
where
|
||||
F: FnMut(Option<(&[T], RefCount)>) -> Option<(Vec<T>, RefCount)>,
|
||||
{
|
||||
let mut bucket = self.get_write_bucket();
|
||||
bucket.as_mut().unwrap().update(key, updatefn)
|
||||
}
|
||||
|
||||
pub fn try_write(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
value: (&[T], RefCount),
|
||||
) -> Result<(), BucketMapError> {
|
||||
let mut bucket = self.get_write_bucket();
|
||||
bucket.as_mut().unwrap().try_write(pubkey, value.0, value.1)
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
use {crate::RefCount, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct BucketItem<T> {
|
||||
pub pubkey: Pubkey,
|
||||
pub ref_count: RefCount,
|
||||
pub slot_list: Vec<T>,
|
||||
}
|
@ -1,522 +0,0 @@
|
||||
//! BucketMap is a mostly contention free concurrent map backed by MmapMut
|
||||
|
||||
use {
|
||||
crate::{bucket_api::BucketApi, bucket_stats::BucketMapStats, MaxSearch, RefCount},
|
||||
solana_sdk::pubkey::Pubkey,
|
||||
std::{convert::TryInto, fmt::Debug, fs, path::PathBuf, sync::Arc},
|
||||
tempfile::TempDir,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct BucketMapConfig {
|
||||
pub max_buckets: usize,
|
||||
pub drives: Option<Vec<PathBuf>>,
|
||||
pub max_search: Option<MaxSearch>,
|
||||
}
|
||||
|
||||
impl BucketMapConfig {
|
||||
/// Create a new BucketMapConfig
|
||||
/// NOTE: BucketMap requires that max_buckets is a power of two
|
||||
pub fn new(max_buckets: usize) -> BucketMapConfig {
|
||||
BucketMapConfig {
|
||||
max_buckets,
|
||||
..BucketMapConfig::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BucketMap<T: Clone + Copy + Debug> {
|
||||
buckets: Vec<Arc<BucketApi<T>>>,
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
max_buckets_pow2: u8,
|
||||
pub stats: Arc<BucketMapStats>,
|
||||
pub temp_dir: Option<TempDir>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy + Debug> Drop for BucketMap<T> {
|
||||
fn drop(&mut self) {
|
||||
if self.temp_dir.is_none() {
|
||||
BucketMap::<T>::erase_previous_drives(&self.drives);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy + Debug> std::fmt::Debug for BucketMap<T> {
|
||||
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BucketMapError {
|
||||
/// (bucket_index, current_capacity_pow2)
|
||||
DataNoSpace((u64, u8)),
|
||||
/// current_capacity_pow2
|
||||
IndexNoSpace(u8),
|
||||
}
|
||||
|
||||
impl<T: Clone + Copy + Debug> BucketMap<T> {
|
||||
pub fn new(config: BucketMapConfig) -> Self {
|
||||
assert_ne!(
|
||||
config.max_buckets, 0,
|
||||
"Max number of buckets must be non-zero"
|
||||
);
|
||||
assert!(
|
||||
config.max_buckets.is_power_of_two(),
|
||||
"Max number of buckets must be a power of two"
|
||||
);
|
||||
// this should be <= 1 << DEFAULT_CAPACITY or we end up searching the same items over and over - probably not a big deal since it is so small anyway
|
||||
const MAX_SEARCH: MaxSearch = 32;
|
||||
let max_search = config.max_search.unwrap_or(MAX_SEARCH);
|
||||
|
||||
if let Some(drives) = config.drives.as_ref() {
|
||||
Self::erase_previous_drives(drives);
|
||||
}
|
||||
let mut temp_dir = None;
|
||||
let drives = config.drives.unwrap_or_else(|| {
|
||||
temp_dir = Some(TempDir::new().unwrap());
|
||||
vec![temp_dir.as_ref().unwrap().path().to_path_buf()]
|
||||
});
|
||||
let drives = Arc::new(drives);
|
||||
|
||||
let stats = Arc::default();
|
||||
let buckets = (0..config.max_buckets)
|
||||
.into_iter()
|
||||
.map(|_| {
|
||||
Arc::new(BucketApi::new(
|
||||
Arc::clone(&drives),
|
||||
max_search,
|
||||
Arc::clone(&stats),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// A simple log2 function that is correct if x is a power of two
|
||||
let log2 = |x: usize| usize::BITS - x.leading_zeros() - 1;
|
||||
|
||||
Self {
|
||||
buckets,
|
||||
drives,
|
||||
max_buckets_pow2: log2(config.max_buckets) as u8,
|
||||
stats,
|
||||
temp_dir,
|
||||
}
|
||||
}
|
||||
|
||||
fn erase_previous_drives(drives: &[PathBuf]) {
|
||||
drives.iter().for_each(|folder| {
|
||||
let _ = fs::remove_dir_all(&folder);
|
||||
let _ = fs::create_dir_all(&folder);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn num_buckets(&self) -> usize {
|
||||
self.buckets.len()
|
||||
}
|
||||
|
||||
/// Get the values for Pubkey `key`
|
||||
pub fn read_value(&self, key: &Pubkey) -> Option<(Vec<T>, RefCount)> {
|
||||
self.get_bucket(key).read_value(key)
|
||||
}
|
||||
|
||||
/// Delete the Pubkey `key`
|
||||
pub fn delete_key(&self, key: &Pubkey) {
|
||||
self.get_bucket(key).delete_key(key);
|
||||
}
|
||||
|
||||
/// Update Pubkey `key`'s value with 'value'
|
||||
pub fn insert(&self, key: &Pubkey, value: (&[T], RefCount)) {
|
||||
self.get_bucket(key).insert(key, value)
|
||||
}
|
||||
|
||||
/// Update Pubkey `key`'s value with 'value'
|
||||
pub fn try_insert(&self, key: &Pubkey, value: (&[T], RefCount)) -> Result<(), BucketMapError> {
|
||||
self.get_bucket(key).try_write(key, value)
|
||||
}
|
||||
|
||||
/// Update Pubkey `key`'s value with function `updatefn`
|
||||
pub fn update<F>(&self, key: &Pubkey, updatefn: F)
|
||||
where
|
||||
F: FnMut(Option<(&[T], RefCount)>) -> Option<(Vec<T>, RefCount)>,
|
||||
{
|
||||
self.get_bucket(key).update(key, updatefn)
|
||||
}
|
||||
|
||||
pub fn get_bucket(&self, key: &Pubkey) -> &Arc<BucketApi<T>> {
|
||||
self.get_bucket_from_index(self.bucket_ix(key))
|
||||
}
|
||||
|
||||
pub fn get_bucket_from_index(&self, ix: usize) -> &Arc<BucketApi<T>> {
|
||||
&self.buckets[ix]
|
||||
}
|
||||
|
||||
/// Get the bucket index for Pubkey `key`
|
||||
pub fn bucket_ix(&self, key: &Pubkey) -> usize {
|
||||
if self.max_buckets_pow2 > 0 {
|
||||
let location = read_be_u64(key.as_ref());
|
||||
(location >> (u64::BITS - self.max_buckets_pow2 as u32)) as usize
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the refcount for Pubkey `key`
|
||||
pub fn addref(&self, key: &Pubkey) -> Option<RefCount> {
|
||||
let ix = self.bucket_ix(key);
|
||||
let bucket = &self.buckets[ix];
|
||||
bucket.addref(key)
|
||||
}
|
||||
|
||||
/// Decrement the refcount for Pubkey `key`
|
||||
pub fn unref(&self, key: &Pubkey) -> Option<RefCount> {
|
||||
let ix = self.bucket_ix(key);
|
||||
let bucket = &self.buckets[ix];
|
||||
bucket.unref(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Look at the first 8 bytes of the input and reinterpret them as a u64
|
||||
fn read_be_u64(input: &[u8]) -> u64 {
|
||||
assert!(input.len() >= std::mem::size_of::<u64>());
|
||||
u64::from_be_bytes(input[0..std::mem::size_of::<u64>()].try_into().unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::*,
|
||||
rand::{thread_rng, Rng},
|
||||
std::{collections::HashMap, sync::RwLock},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_insert() {
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
index.update(&key, |_| Some((vec![0], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_insert2() {
|
||||
for pass in 0..3 {
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
let bucket = index.get_bucket(&key);
|
||||
if pass == 0 {
|
||||
index.insert(&key, (&[0], 0));
|
||||
} else {
|
||||
let result = index.try_insert(&key, (&[0], 0));
|
||||
assert!(result.is_err());
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
if pass == 2 {
|
||||
// another call to try insert again - should still return an error
|
||||
let result = index.try_insert(&key, (&[0], 0));
|
||||
assert!(result.is_err());
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
}
|
||||
bucket.grow(result.unwrap_err());
|
||||
let result = index.try_insert(&key, (&[0], 0));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 0)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_update2() {
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
index.insert(&key, (&[0], 0));
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 0)));
|
||||
index.insert(&key, (&[1], 0));
|
||||
assert_eq!(index.read_value(&key), Some((vec![1], 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_update() {
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
index.update(&key, |_| Some((vec![0], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 0)));
|
||||
index.update(&key, |_| Some((vec![1], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![1], 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_update_to_0_len() {
|
||||
solana_logger::setup();
|
||||
let key = Pubkey::new_unique();
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
index.update(&key, |_| Some((vec![0], 1)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![0], 1)));
|
||||
// sets len to 0, updates in place
|
||||
index.update(&key, |_| Some((vec![], 1)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![], 1)));
|
||||
// sets len to 0, doesn't update in place - finds a new place, which causes us to no longer have an allocation in data
|
||||
index.update(&key, |_| Some((vec![], 2)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![], 2)));
|
||||
// sets len to 1, doesn't update in place - finds a new place
|
||||
index.update(&key, |_| Some((vec![1], 2)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![1], 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_delete() {
|
||||
let config = BucketMapConfig::new(1 << 1);
|
||||
let index = BucketMap::new(config);
|
||||
for i in 0..10 {
|
||||
let key = Pubkey::new_unique();
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
|
||||
index.delete_key(&key);
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
index.delete_key(&key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_delete_2() {
|
||||
let config = BucketMapConfig::new(1 << 2);
|
||||
let index = BucketMap::new(config);
|
||||
for i in 0..100 {
|
||||
let key = Pubkey::new_unique();
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
|
||||
index.delete_key(&key);
|
||||
assert_eq!(index.read_value(&key), None);
|
||||
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
index.delete_key(&key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_n_drives() {
|
||||
let config = BucketMapConfig::new(1 << 2);
|
||||
let index = BucketMap::new(config);
|
||||
for i in 0..100 {
|
||||
let key = Pubkey::new_unique();
|
||||
index.update(&key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(&key), Some((vec![i], 0)));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn bucket_map_test_grow_read() {
|
||||
let config = BucketMapConfig::new(1 << 2);
|
||||
let index = BucketMap::new(config);
|
||||
let keys: Vec<Pubkey> = (0..100).into_iter().map(|_| Pubkey::new_unique()).collect();
|
||||
for k in 0..keys.len() {
|
||||
let key = &keys[k];
|
||||
let i = read_be_u64(key.as_ref());
|
||||
index.update(key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(key), Some((vec![i], 0)));
|
||||
for (ix, key) in keys.iter().enumerate() {
|
||||
let i = read_be_u64(key.as_ref());
|
||||
//debug!("READ: {:?} {}", key, i);
|
||||
let expected = if ix <= k { Some((vec![i], 0)) } else { None };
|
||||
assert_eq!(index.read_value(key), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_map_test_n_delete() {
|
||||
let config = BucketMapConfig::new(1 << 2);
|
||||
let index = BucketMap::new(config);
|
||||
let keys: Vec<Pubkey> = (0..20).into_iter().map(|_| Pubkey::new_unique()).collect();
|
||||
for key in keys.iter() {
|
||||
let i = read_be_u64(key.as_ref());
|
||||
index.update(key, |_| Some((vec![i], 0)));
|
||||
assert_eq!(index.read_value(key), Some((vec![i], 0)));
|
||||
}
|
||||
for key in keys.iter() {
|
||||
let i = read_be_u64(key.as_ref());
|
||||
//debug!("READ: {:?} {}", key, i);
|
||||
assert_eq!(index.read_value(key), Some((vec![i], 0)));
|
||||
}
|
||||
for k in 0..keys.len() {
|
||||
let key = &keys[k];
|
||||
index.delete_key(key);
|
||||
assert_eq!(index.read_value(key), None);
|
||||
for key in keys.iter().skip(k + 1) {
|
||||
let i = read_be_u64(key.as_ref());
|
||||
assert_eq!(index.read_value(key), Some((vec![i], 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashmap_compare() {
|
||||
use std::sync::Mutex;
|
||||
solana_logger::setup();
|
||||
let maps = (0..2)
|
||||
.into_iter()
|
||||
.map(|max_buckets_pow2| {
|
||||
let config = BucketMapConfig::new(1 << max_buckets_pow2);
|
||||
BucketMap::new(config)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let hash_map = RwLock::new(HashMap::<Pubkey, (Vec<(usize, usize)>, RefCount)>::new());
|
||||
let max_slot_list_len = 3;
|
||||
let all_keys = Mutex::new(vec![]);
|
||||
|
||||
let gen_rand_value = || {
|
||||
let count = thread_rng().gen_range(0, max_slot_list_len);
|
||||
let v = (0..count)
|
||||
.into_iter()
|
||||
.map(|x| (x as usize, x as usize /*thread_rng().gen::<usize>()*/))
|
||||
.collect::<Vec<_>>();
|
||||
let rc = thread_rng().gen::<RefCount>();
|
||||
(v, rc)
|
||||
};
|
||||
|
||||
let get_key = || {
|
||||
let mut keys = all_keys.lock().unwrap();
|
||||
if keys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let len = keys.len();
|
||||
Some(keys.remove(thread_rng().gen_range(0, len)))
|
||||
};
|
||||
let return_key = |key| {
|
||||
let mut keys = all_keys.lock().unwrap();
|
||||
keys.push(key);
|
||||
};
|
||||
|
||||
let verify = || {
|
||||
let mut maps = maps
|
||||
.iter()
|
||||
.map(|map| {
|
||||
let mut r = vec![];
|
||||
for bin in 0..map.num_buckets() {
|
||||
r.append(
|
||||
&mut map.buckets[bin]
|
||||
.items_in_range(&None::<&std::ops::RangeInclusive<Pubkey>>),
|
||||
);
|
||||
}
|
||||
r
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let hm = hash_map.read().unwrap();
|
||||
for (k, v) in hm.iter() {
|
||||
for map in maps.iter_mut() {
|
||||
for i in 0..map.len() {
|
||||
if k == &map[i].pubkey {
|
||||
assert_eq!(map[i].slot_list, v.0);
|
||||
assert_eq!(map[i].ref_count, v.1);
|
||||
map.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for map in maps.iter() {
|
||||
assert!(map.is_empty());
|
||||
}
|
||||
};
|
||||
let mut initial = 100; // put this many items in to start
|
||||
|
||||
// do random operations: insert, update, delete, add/unref in random order
|
||||
// verify consistency between hashmap and all bucket maps
|
||||
for i in 0..10000 {
|
||||
if initial > 0 {
|
||||
initial -= 1;
|
||||
}
|
||||
if initial > 0 || thread_rng().gen_range(0, 5) == 0 {
|
||||
// insert
|
||||
let k = solana_sdk::pubkey::new_rand();
|
||||
let v = gen_rand_value();
|
||||
hash_map.write().unwrap().insert(k, v.clone());
|
||||
let insert = thread_rng().gen_range(0, 2) == 0;
|
||||
maps.iter().for_each(|map| {
|
||||
if insert {
|
||||
map.insert(&k, (&v.0, v.1))
|
||||
} else {
|
||||
map.update(&k, |current| {
|
||||
assert!(current.is_none());
|
||||
Some(v.clone())
|
||||
})
|
||||
}
|
||||
});
|
||||
return_key(k);
|
||||
}
|
||||
if thread_rng().gen_range(0, 10) == 0 {
|
||||
// update
|
||||
if let Some(k) = get_key() {
|
||||
let hm = hash_map.read().unwrap();
|
||||
let (v, rc) = gen_rand_value();
|
||||
let v_old = hm.get(&k);
|
||||
let insert = thread_rng().gen_range(0, 2) == 0;
|
||||
maps.iter().for_each(|map| {
|
||||
if insert {
|
||||
map.insert(&k, (&v, rc))
|
||||
} else {
|
||||
map.update(&k, |current| {
|
||||
assert_eq!(current, v_old.map(|(v, rc)| (&v[..], *rc)), "{}", k);
|
||||
Some((v.clone(), rc))
|
||||
})
|
||||
}
|
||||
});
|
||||
drop(hm);
|
||||
hash_map.write().unwrap().insert(k, (v, rc));
|
||||
return_key(k);
|
||||
}
|
||||
}
|
||||
if thread_rng().gen_range(0, 20) == 0 {
|
||||
// delete
|
||||
if let Some(k) = get_key() {
|
||||
let mut hm = hash_map.write().unwrap();
|
||||
hm.remove(&k);
|
||||
maps.iter().for_each(|map| {
|
||||
map.delete_key(&k);
|
||||
});
|
||||
}
|
||||
}
|
||||
if thread_rng().gen_range(0, 10) == 0 {
|
||||
// add/unref
|
||||
if let Some(k) = get_key() {
|
||||
let mut inc = thread_rng().gen_range(0, 2) == 0;
|
||||
let mut hm = hash_map.write().unwrap();
|
||||
let (v, mut rc) = hm.get(&k).map(|(v, rc)| (v.to_vec(), *rc)).unwrap();
|
||||
if !inc && rc == 0 {
|
||||
// can't decrement rc=0
|
||||
inc = true;
|
||||
}
|
||||
rc = if inc { rc + 1 } else { rc - 1 };
|
||||
hm.insert(k, (v.to_vec(), rc));
|
||||
maps.iter().for_each(|map| {
|
||||
if thread_rng().gen_range(0, 2) == 0 {
|
||||
map.update(&k, |current| Some((current.unwrap().0.to_vec(), rc)))
|
||||
} else if inc {
|
||||
map.addref(&k);
|
||||
} else {
|
||||
map.unref(&k);
|
||||
}
|
||||
});
|
||||
|
||||
return_key(k);
|
||||
}
|
||||
}
|
||||
if i % 1000 == 0 {
|
||||
verify();
|
||||
}
|
||||
}
|
||||
verify();
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
use std::sync::{atomic::AtomicU64, Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BucketStats {
|
||||
pub resizes: AtomicU64,
|
||||
pub max_size: Mutex<u64>,
|
||||
pub resize_us: AtomicU64,
|
||||
pub new_file_us: AtomicU64,
|
||||
pub flush_file_us: AtomicU64,
|
||||
pub mmap_us: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BucketMapStats {
|
||||
pub index: Arc<BucketStats>,
|
||||
pub data: Arc<BucketStats>,
|
||||
}
|
@ -1,424 +0,0 @@
|
||||
use {
|
||||
crate::{bucket_stats::BucketStats, MaxSearch},
|
||||
memmap2::MmapMut,
|
||||
rand::{thread_rng, Rng},
|
||||
solana_measure::measure::Measure,
|
||||
std::{
|
||||
fs::{remove_file, OpenOptions},
|
||||
io::{Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
1 2
|
||||
2 4
|
||||
3 8
|
||||
4 16
|
||||
5 32
|
||||
6 64
|
||||
7 128
|
||||
8 256
|
||||
9 512
|
||||
10 1,024
|
||||
11 2,048
|
||||
12 4,096
|
||||
13 8,192
|
||||
14 16,384
|
||||
23 8,388,608
|
||||
24 16,777,216
|
||||
*/
|
||||
pub const DEFAULT_CAPACITY_POW2: u8 = 5;
|
||||
|
||||
/// A Header UID of 0 indicates that the header is unlocked
|
||||
const UID_UNLOCKED: Uid = 0;
|
||||
|
||||
pub(crate) type Uid = u64;
|
||||
|
||||
#[repr(C)]
|
||||
struct Header {
|
||||
lock: u64,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
/// try to lock this entry with 'uid'
|
||||
/// return true if it could be locked
|
||||
fn try_lock(&mut self, uid: Uid) -> bool {
|
||||
if self.lock == UID_UNLOCKED {
|
||||
self.lock = uid;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
/// mark this entry as unlocked
|
||||
fn unlock(&mut self, expected: Uid) {
|
||||
assert_eq!(expected, self.lock);
|
||||
self.lock = UID_UNLOCKED;
|
||||
}
|
||||
/// uid that has locked this entry or None if unlocked
|
||||
fn uid(&self) -> Option<Uid> {
|
||||
if self.lock == UID_UNLOCKED {
|
||||
None
|
||||
} else {
|
||||
Some(self.lock)
|
||||
}
|
||||
}
|
||||
/// true if this entry is unlocked
|
||||
fn is_unlocked(&self) -> bool {
|
||||
self.lock == UID_UNLOCKED
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BucketStorage {
|
||||
path: PathBuf,
|
||||
mmap: MmapMut,
|
||||
pub cell_size: u64,
|
||||
pub capacity_pow2: u8,
|
||||
pub count: Arc<AtomicU64>,
|
||||
pub stats: Arc<BucketStats>,
|
||||
pub max_search: MaxSearch,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BucketStorageError {
|
||||
AlreadyAllocated,
|
||||
}
|
||||
|
||||
impl Drop for BucketStorage {
|
||||
fn drop(&mut self) {
|
||||
let _ = remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
impl BucketStorage {
|
||||
pub fn new_with_capacity(
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
num_elems: u64,
|
||||
elem_size: u64,
|
||||
capacity_pow2: u8,
|
||||
max_search: MaxSearch,
|
||||
stats: Arc<BucketStats>,
|
||||
count: Arc<AtomicU64>,
|
||||
) -> Self {
|
||||
let cell_size = elem_size * num_elems + std::mem::size_of::<Header>() as u64;
|
||||
let (mmap, path) = Self::new_map(&drives, cell_size as usize, capacity_pow2, &stats);
|
||||
Self {
|
||||
path,
|
||||
mmap,
|
||||
cell_size,
|
||||
count,
|
||||
capacity_pow2,
|
||||
stats,
|
||||
max_search,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_search(&self) -> u64 {
|
||||
self.max_search as u64
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
drives: Arc<Vec<PathBuf>>,
|
||||
num_elems: u64,
|
||||
elem_size: u64,
|
||||
max_search: MaxSearch,
|
||||
stats: Arc<BucketStats>,
|
||||
count: Arc<AtomicU64>,
|
||||
) -> Self {
|
||||
Self::new_with_capacity(
|
||||
drives,
|
||||
num_elems,
|
||||
elem_size,
|
||||
DEFAULT_CAPACITY_POW2,
|
||||
max_search,
|
||||
stats,
|
||||
count,
|
||||
)
|
||||
}
|
||||
|
||||
/// return ref to header of item 'ix' in mmapped file
|
||||
fn header_ptr(&self, ix: u64) -> &Header {
|
||||
let ix = (ix * self.cell_size) as usize;
|
||||
let hdr_slice: &[u8] = &self.mmap[ix..ix + std::mem::size_of::<Header>()];
|
||||
unsafe {
|
||||
let hdr = hdr_slice.as_ptr() as *const Header;
|
||||
hdr.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// return ref to header of item 'ix' in mmapped file
|
||||
fn header_mut_ptr(&mut self, ix: u64) -> &mut Header {
|
||||
let ix = (ix * self.cell_size) as usize;
|
||||
let hdr_slice: &mut [u8] = &mut self.mmap[ix..ix + std::mem::size_of::<Header>()];
|
||||
unsafe {
|
||||
let hdr = hdr_slice.as_mut_ptr() as *mut Header;
|
||||
hdr.as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// return uid allocated at index 'ix' or None if vacant
|
||||
pub fn uid(&self, ix: u64) -> Option<Uid> {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
self.header_ptr(ix).uid()
|
||||
}
|
||||
|
||||
/// true if the entry at index 'ix' is free (as opposed to being allocated)
|
||||
pub fn is_free(&self, ix: u64) -> bool {
|
||||
// note that the terminology in the implementation is locked or unlocked.
|
||||
// but our api is allocate/free
|
||||
self.header_ptr(ix).is_unlocked()
|
||||
}
|
||||
|
||||
/// caller knows id is not empty
|
||||
pub fn uid_unchecked(&self, ix: u64) -> Uid {
|
||||
self.uid(ix).unwrap()
|
||||
}
|
||||
|
||||
/// 'is_resizing' true if caller is resizing the index (so don't increment count)
|
||||
/// 'is_resizing' false if caller is adding an item to the index (so increment count)
|
||||
pub fn allocate(
|
||||
&mut self,
|
||||
ix: u64,
|
||||
uid: Uid,
|
||||
is_resizing: bool,
|
||||
) -> Result<(), BucketStorageError> {
|
||||
assert!(ix < self.capacity(), "allocate: bad index size");
|
||||
assert!(UID_UNLOCKED != uid, "allocate: bad uid");
|
||||
let mut e = Err(BucketStorageError::AlreadyAllocated);
|
||||
//debug!("ALLOC {} {}", ix, uid);
|
||||
if self.header_mut_ptr(ix).try_lock(uid) {
|
||||
e = Ok(());
|
||||
if !is_resizing {
|
||||
self.count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
pub fn free(&mut self, ix: u64, uid: Uid) {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
assert!(UID_UNLOCKED != uid, "free: bad uid");
|
||||
self.header_mut_ptr(ix).unlock(uid);
|
||||
self.count.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn get<T: Sized>(&self, ix: u64) -> &T {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let start = (ix * self.cell_size) as usize + std::mem::size_of::<Header>();
|
||||
let end = start + std::mem::size_of::<T>();
|
||||
let item_slice: &[u8] = &self.mmap[start..end];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *const T;
|
||||
&*item
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_empty_cell_slice<T: Sized>(&self) -> &[T] {
|
||||
let len = 0;
|
||||
let item_slice: &[u8] = &self.mmap[0..0];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *const T;
|
||||
std::slice::from_raw_parts(item, len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cell_slice<T: Sized>(&self, ix: u64, len: u64) -> &[T] {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let ix = self.cell_size * ix;
|
||||
let start = ix as usize + std::mem::size_of::<Header>();
|
||||
let end = start + std::mem::size_of::<T>() * len as usize;
|
||||
//debug!("GET slice {} {}", start, end);
|
||||
let item_slice: &[u8] = &self.mmap[start..end];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *const T;
|
||||
std::slice::from_raw_parts(item, len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub fn get_mut<T: Sized>(&self, ix: u64) -> &mut T {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let start = (ix * self.cell_size) as usize + std::mem::size_of::<Header>();
|
||||
let end = start + std::mem::size_of::<T>();
|
||||
let item_slice: &[u8] = &self.mmap[start..end];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *mut T;
|
||||
&mut *item
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub fn get_mut_cell_slice<T: Sized>(&self, ix: u64, len: u64) -> &mut [T] {
|
||||
assert!(ix < self.capacity(), "bad index size");
|
||||
let ix = self.cell_size * ix;
|
||||
let start = ix as usize + std::mem::size_of::<Header>();
|
||||
let end = start + std::mem::size_of::<T>() * len as usize;
|
||||
//debug!("GET mut slice {} {}", start, end);
|
||||
let item_slice: &[u8] = &self.mmap[start..end];
|
||||
unsafe {
|
||||
let item = item_slice.as_ptr() as *mut T;
|
||||
std::slice::from_raw_parts_mut(item, len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
fn new_map(
|
||||
drives: &[PathBuf],
|
||||
cell_size: usize,
|
||||
capacity_pow2: u8,
|
||||
stats: &BucketStats,
|
||||
) -> (MmapMut, PathBuf) {
|
||||
let mut measure_new_file = Measure::start("measure_new_file");
|
||||
let capacity = 1u64 << capacity_pow2;
|
||||
let r = thread_rng().gen_range(0, drives.len());
|
||||
let drive = &drives[r];
|
||||
let pos = format!("{}", thread_rng().gen_range(0, u128::MAX),);
|
||||
let file = drive.join(pos);
|
||||
let mut data = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(file.clone())
|
||||
.map_err(|e| {
|
||||
panic!(
|
||||
"Unable to create data file {} in current dir({:?}): {:?}",
|
||||
file.display(),
|
||||
std::env::current_dir(),
|
||||
e
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Theoretical performance optimization: write a zero to the end of
|
||||
// the file so that we won't have to resize it later, which may be
|
||||
// expensive.
|
||||
//debug!("GROWING file {}", capacity * cell_size as u64);
|
||||
data.seek(SeekFrom::Start(capacity * cell_size as u64 - 1))
|
||||
.unwrap();
|
||||
data.write_all(&[0]).unwrap();
|
||||
data.seek(SeekFrom::Start(0)).unwrap();
|
||||
measure_new_file.stop();
|
||||
let mut measure_flush = Measure::start("measure_flush");
|
||||
data.flush().unwrap(); // can we skip this?
|
||||
measure_flush.stop();
|
||||
let mut measure_mmap = Measure::start("measure_mmap");
|
||||
let res = (unsafe { MmapMut::map_mut(&data).unwrap() }, file);
|
||||
measure_mmap.stop();
|
||||
stats
|
||||
.new_file_us
|
||||
.fetch_add(measure_new_file.as_us(), Ordering::Relaxed);
|
||||
stats
|
||||
.flush_file_us
|
||||
.fetch_add(measure_flush.as_us(), Ordering::Relaxed);
|
||||
stats
|
||||
.mmap_us
|
||||
.fetch_add(measure_mmap.as_us(), Ordering::Relaxed);
|
||||
res
|
||||
}
|
||||
|
||||
/// copy contents from 'old_bucket' to 'self'
|
||||
fn copy_contents(&mut self, old_bucket: &Self) {
|
||||
let mut m = Measure::start("grow");
|
||||
let old_cap = old_bucket.capacity();
|
||||
let old_map = &old_bucket.mmap;
|
||||
|
||||
let increment = self.capacity_pow2 - old_bucket.capacity_pow2;
|
||||
let index_grow = 1 << increment;
|
||||
(0..old_cap as usize).into_iter().for_each(|i| {
|
||||
let old_ix = i * old_bucket.cell_size as usize;
|
||||
let new_ix = old_ix * index_grow;
|
||||
let dst_slice: &[u8] = &self.mmap[new_ix..new_ix + old_bucket.cell_size as usize];
|
||||
let src_slice: &[u8] = &old_map[old_ix..old_ix + old_bucket.cell_size as usize];
|
||||
|
||||
unsafe {
|
||||
let dst = dst_slice.as_ptr() as *mut u8;
|
||||
let src = src_slice.as_ptr() as *const u8;
|
||||
std::ptr::copy_nonoverlapping(src, dst, old_bucket.cell_size as usize);
|
||||
};
|
||||
});
|
||||
m.stop();
|
||||
self.stats.resizes.fetch_add(1, Ordering::Relaxed);
|
||||
self.stats.resize_us.fetch_add(m.as_us(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// allocate a new bucket, copying data from 'bucket'
|
||||
pub fn new_resized(
|
||||
drives: &Arc<Vec<PathBuf>>,
|
||||
max_search: MaxSearch,
|
||||
bucket: Option<&Self>,
|
||||
capacity_pow_2: u8,
|
||||
num_elems: u64,
|
||||
elem_size: u64,
|
||||
stats: &Arc<BucketStats>,
|
||||
) -> Self {
|
||||
let mut new_bucket = Self::new_with_capacity(
|
||||
Arc::clone(drives),
|
||||
num_elems,
|
||||
elem_size,
|
||||
capacity_pow_2,
|
||||
max_search,
|
||||
Arc::clone(stats),
|
||||
bucket
|
||||
.map(|bucket| Arc::clone(&bucket.count))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
if let Some(bucket) = bucket {
|
||||
new_bucket.copy_contents(bucket);
|
||||
}
|
||||
let sz = new_bucket.capacity();
|
||||
{
|
||||
let mut max = new_bucket.stats.max_size.lock().unwrap();
|
||||
*max = std::cmp::max(*max, sz);
|
||||
}
|
||||
new_bucket
|
||||
}
|
||||
|
||||
/// Return the number of cells currently allocated
|
||||
pub fn capacity(&self) -> u64 {
|
||||
1 << self.capacity_pow2
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bucket_storage() {
|
||||
let tmpdir1 = std::env::temp_dir().join("bucket_map_test_mt");
|
||||
let paths: Vec<PathBuf> = [tmpdir1]
|
||||
.iter()
|
||||
.filter(|x| std::fs::create_dir_all(x).is_ok())
|
||||
.cloned()
|
||||
.collect();
|
||||
assert!(!paths.is_empty());
|
||||
|
||||
let mut storage =
|
||||
BucketStorage::new(Arc::new(paths), 1, 1, 1, Arc::default(), Arc::default());
|
||||
let ix = 0;
|
||||
let uid = Uid::MAX;
|
||||
assert!(storage.is_free(ix));
|
||||
assert!(storage.allocate(ix, uid, false).is_ok());
|
||||
assert!(storage.allocate(ix, uid, false).is_err());
|
||||
assert!(!storage.is_free(ix));
|
||||
assert_eq!(storage.uid(ix), Some(uid));
|
||||
assert_eq!(storage.uid_unchecked(ix), uid);
|
||||
storage.free(ix, uid);
|
||||
assert!(storage.is_free(ix));
|
||||
assert_eq!(storage.uid(ix), None);
|
||||
let uid = 1;
|
||||
assert!(storage.is_free(ix));
|
||||
assert!(storage.allocate(ix, uid, false).is_ok());
|
||||
assert!(storage.allocate(ix, uid, false).is_err());
|
||||
assert!(!storage.is_free(ix));
|
||||
assert_eq!(storage.uid(ix), Some(uid));
|
||||
assert_eq!(storage.uid_unchecked(ix), uid);
|
||||
storage.free(ix, uid);
|
||||
assert!(storage.is_free(ix));
|
||||
assert_eq!(storage.uid(ix), None);
|
||||
}
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use {
|
||||
crate::{
|
||||
bucket::Bucket,
|
||||
bucket_storage::{BucketStorage, Uid},
|
||||
RefCount,
|
||||
},
|
||||
modular_bitfield::prelude::*,
|
||||
solana_sdk::{clock::Slot, pubkey::Pubkey},
|
||||
std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
},
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
// one instance of this per item in the index
|
||||
// stored in the index bucket
|
||||
pub struct IndexEntry {
|
||||
pub key: Pubkey, // can this be smaller if we have reduced the keys into buckets already?
|
||||
pub ref_count: RefCount, // can this be smaller? Do we ever need more than 4B refcounts?
|
||||
storage_cap_and_offset: PackedStorage,
|
||||
// if the bucket doubled, the index can be recomputed using create_bucket_capacity_pow2
|
||||
pub num_slots: Slot, // can this be smaller? epoch size should ~ be the max len. this is the num elements in the slot list
|
||||
}
|
||||
|
||||
/// Pack the storage offset and capacity-when-crated-pow2 fields into a single u64
|
||||
#[bitfield(bits = 64)]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
||||
struct PackedStorage {
|
||||
capacity_when_created_pow2: B8,
|
||||
offset: B56,
|
||||
}
|
||||
|
||||
impl IndexEntry {
|
||||
pub fn init(&mut self, pubkey: &Pubkey) {
|
||||
self.key = *pubkey;
|
||||
self.ref_count = 0;
|
||||
self.storage_cap_and_offset = PackedStorage::default();
|
||||
self.num_slots = 0;
|
||||
}
|
||||
|
||||
pub fn set_storage_capacity_when_created_pow2(
|
||||
&mut self,
|
||||
storage_capacity_when_created_pow2: u8,
|
||||
) {
|
||||
self.storage_cap_and_offset
|
||||
.set_capacity_when_created_pow2(storage_capacity_when_created_pow2)
|
||||
}
|
||||
|
||||
pub fn set_storage_offset(&mut self, storage_offset: u64) {
|
||||
self.storage_cap_and_offset
|
||||
.set_offset_checked(storage_offset)
|
||||
.expect("New storage offset must fit into 7 bytes!")
|
||||
}
|
||||
|
||||
/// return closest bucket index fit for the slot slice.
|
||||
/// Since bucket size is 2^index, the return value is
|
||||
/// min index, such that 2^index >= num_slots
|
||||
/// index = ceiling(log2(num_slots))
|
||||
/// special case, when slot slice empty, return 0th index.
|
||||
pub fn data_bucket_from_num_slots(num_slots: Slot) -> u64 {
|
||||
// Compute the ceiling of log2 for integer
|
||||
if num_slots == 0 {
|
||||
0
|
||||
} else {
|
||||
(Slot::BITS - (num_slots - 1).leading_zeros()) as u64
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data_bucket_ix(&self) -> u64 {
|
||||
Self::data_bucket_from_num_slots(self.num_slots)
|
||||
}
|
||||
|
||||
pub fn ref_count(&self) -> RefCount {
|
||||
self.ref_count
|
||||
}
|
||||
|
||||
fn storage_capacity_when_created_pow2(&self) -> u8 {
|
||||
self.storage_cap_and_offset.capacity_when_created_pow2()
|
||||
}
|
||||
|
||||
fn storage_offset(&self) -> u64 {
|
||||
self.storage_cap_and_offset.offset()
|
||||
}
|
||||
|
||||
// This function maps the original data location into an index in the current bucket storage.
|
||||
// This is coupled with how we resize bucket storages.
|
||||
pub fn data_loc(&self, storage: &BucketStorage) -> u64 {
|
||||
self.storage_offset() << (storage.capacity_pow2 - self.storage_capacity_when_created_pow2())
|
||||
}
|
||||
|
||||
pub fn read_value<'a, T>(&self, bucket: &'a Bucket<T>) -> Option<(&'a [T], RefCount)> {
|
||||
let data_bucket_ix = self.data_bucket_ix();
|
||||
let data_bucket = &bucket.data[data_bucket_ix as usize];
|
||||
let slice = if self.num_slots > 0 {
|
||||
let loc = self.data_loc(data_bucket);
|
||||
let uid = Self::key_uid(&self.key);
|
||||
assert_eq!(Some(uid), bucket.data[data_bucket_ix as usize].uid(loc));
|
||||
bucket.data[data_bucket_ix as usize].get_cell_slice(loc, self.num_slots)
|
||||
} else {
|
||||
// num_slots is 0. This means we don't have an actual allocation.
|
||||
// can we trust that the data_bucket is even safe?
|
||||
bucket.data[data_bucket_ix as usize].get_empty_cell_slice()
|
||||
};
|
||||
Some((slice, self.ref_count))
|
||||
}
|
||||
|
||||
pub fn key_uid(key: &Pubkey) -> Uid {
|
||||
let mut s = DefaultHasher::new();
|
||||
key.hash(&mut s);
|
||||
s.finish().max(1u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
impl IndexEntry {
|
||||
pub fn new(key: Pubkey) -> Self {
|
||||
IndexEntry {
|
||||
key,
|
||||
ref_count: 0,
|
||||
storage_cap_and_offset: PackedStorage::default(),
|
||||
num_slots: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// verify that accessors for storage_offset and capacity_when_created are
|
||||
/// correct and independent
|
||||
#[test]
|
||||
fn test_api() {
|
||||
for offset in [0, 1, u32::MAX as u64] {
|
||||
let mut index = IndexEntry::new(solana_sdk::pubkey::new_rand());
|
||||
if offset != 0 {
|
||||
index.set_storage_offset(offset);
|
||||
}
|
||||
assert_eq!(index.storage_offset(), offset);
|
||||
assert_eq!(index.storage_capacity_when_created_pow2(), 0);
|
||||
for pow in [1, 255, 0] {
|
||||
index.set_storage_capacity_when_created_pow2(pow);
|
||||
assert_eq!(index.storage_offset(), offset);
|
||||
assert_eq!(index.storage_capacity_when_created_pow2(), pow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_eq!(std::mem::size_of::<PackedStorage>(), 1 + 7);
|
||||
assert_eq!(std::mem::size_of::<IndexEntry>(), 32 + 8 + 8 + 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "New storage offset must fit into 7 bytes!")]
|
||||
fn test_set_storage_offset_value_too_large() {
|
||||
let too_big = 1 << 56;
|
||||
let mut index = IndexEntry::new(Pubkey::new_unique());
|
||||
index.set_storage_offset(too_big);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_bucket_from_num_slots() {
|
||||
for n in 0..512 {
|
||||
assert_eq!(
|
||||
IndexEntry::data_bucket_from_num_slots(n),
|
||||
(n as f64).log2().ceil() as u64
|
||||
);
|
||||
}
|
||||
assert_eq!(IndexEntry::data_bucket_from_num_slots(u32::MAX as u64), 32);
|
||||
assert_eq!(
|
||||
IndexEntry::data_bucket_from_num_slots(u32::MAX as u64 + 1),
|
||||
32
|
||||
);
|
||||
assert_eq!(
|
||||
IndexEntry::data_bucket_from_num_slots(u32::MAX as u64 + 2),
|
||||
33
|
||||
);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
mod bucket;
|
||||
pub mod bucket_api;
|
||||
mod bucket_item;
|
||||
pub mod bucket_map;
|
||||
mod bucket_stats;
|
||||
mod bucket_storage;
|
||||
mod index_entry;
|
||||
|
||||
pub type MaxSearch = u8;
|
||||
pub type RefCount = u64;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user