Remove stake-o-matic
This commit is contained in:
parent
a8ef29df27
commit
b7aa366758
|
@ -5219,30 +5219,6 @@ dependencies = [
|
|||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-stake-o-matic"
|
||||
version = "1.7.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"log 0.4.11",
|
||||
"reqwest",
|
||||
"semver 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"solana-clap-utils",
|
||||
"solana-cli-config",
|
||||
"solana-cli-output",
|
||||
"solana-client",
|
||||
"solana-logger 1.7.0",
|
||||
"solana-metrics",
|
||||
"solana-notifier",
|
||||
"solana-sdk",
|
||||
"solana-stake-program",
|
||||
"solana-transaction-status",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-stake-program"
|
||||
version = "1.7.0"
|
||||
|
|
|
@ -31,7 +31,6 @@ members = [
|
|||
"log-analyzer",
|
||||
"merkle-root-bench",
|
||||
"merkle-tree",
|
||||
"stake-o-matic",
|
||||
"storage-bigtable",
|
||||
"storage-proto",
|
||||
"streamer",
|
||||
|
|
|
@ -95,7 +95,6 @@ else
|
|||
solana-net-shaper
|
||||
solana-stake-accounts
|
||||
solana-stake-monitor
|
||||
solana-stake-o-matic
|
||||
solana-sys-tuner
|
||||
solana-test-validator
|
||||
solana-tokens
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
target/
|
||||
solana-install/
|
|
@ -1,33 +0,0 @@
|
|||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
description = "I will find you and I will stake you"
|
||||
edition = "2018"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/"
|
||||
license = "Apache-2.0"
|
||||
name = "solana-stake-o-matic"
|
||||
repository = "https://github.com/solana-labs/stake-o-matic"
|
||||
version = "1.7.0"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
log = "0.4.11"
|
||||
reqwest = { version = "0.11.2", default-features = false, features = ["blocking", "rustls-tls", "json"] }
|
||||
semver = "0.9.0"
|
||||
serde = { version = "1.0.122", features = ["derive"] }
|
||||
serde_json = "1.0.62"
|
||||
serde_yaml = "0.8.13"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.7.0" }
|
||||
solana-client = { path = "../client", version = "=1.7.0" }
|
||||
solana-cli-config = { path = "../cli-config", version = "=1.7.0" }
|
||||
solana-cli-output = { path = "../cli-output", version = "=1.7.0" }
|
||||
solana-logger = { path = "../logger", version = "=1.7.0" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.7.0" }
|
||||
solana-notifier = { path = "../notifier", version = "=1.7.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.7.0" }
|
||||
solana-stake-program = { path = "../programs/stake", version = "=1.7.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.7.0" }
|
||||
thiserror = "1.0.21"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
|
@ -1,21 +0,0 @@
|
|||
## Effortlessly Manage Cluster Stakes
|
||||
The testnet and mainnet-beta clusters currently have a large population of
|
||||
validators that need to be staked by a central authority.
|
||||
|
||||
## Staking Criteria
|
||||
1. All non-delinquent validators receive 50,000 SOL stake
|
||||
1. Additionally, non-deliquent validators that have produced a block in 75% of
|
||||
their slots in the previous epoch receive bonus stake of 500,000 SOL
|
||||
|
||||
A validator that is delinquent for more than 24 hours will have all stake
|
||||
removed. However stake-o-matic has no memory, so if the same validator resolves
|
||||
their delinquency then they will be re-staked again
|
||||
|
||||
## Validator Whitelist
|
||||
To be eligible for staking, a validator's identity pubkey must be added to a
|
||||
YAML whitelist file.
|
||||
|
||||
## Stake Account Management
|
||||
Stake-o-matic will split the individual validator stake accounts from a master
|
||||
stake account, and must be given the authorized staker keypair for the master
|
||||
stake account.
|
|
@ -1,248 +0,0 @@
|
|||
use crate::retry_rpc_operation;
|
||||
use log::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::{
|
||||
clock::{Slot, DEFAULT_SLOTS_PER_EPOCH},
|
||||
commitment_config::CommitmentConfig,
|
||||
epoch_info::EpochInfo,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs::{self, File, OpenOptions},
|
||||
io,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Entry {
|
||||
slots: Range<Slot>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn new<P: AsRef<Path>>(base_path: P, slots: Range<Slot>) -> Self {
|
||||
let file_name = format!("{}-{}.json", slots.start, slots.end);
|
||||
let path = base_path.as_ref().join(file_name);
|
||||
Self { slots, path }
|
||||
}
|
||||
|
||||
fn parse_filename<F: AsRef<Path>>(filename: F) -> Option<Range<Slot>> {
|
||||
let filename = filename.as_ref();
|
||||
let slot_range = filename.file_stem();
|
||||
let extension = filename.extension();
|
||||
extension
|
||||
.zip(slot_range)
|
||||
.and_then(|(extension, slot_range)| {
|
||||
if extension == "json" {
|
||||
slot_range.to_str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|slot_range| {
|
||||
let mut parts = slot_range.splitn(2, '-');
|
||||
let start = parts.next().and_then(|p| p.parse::<Slot>().ok());
|
||||
let end = parts.next().and_then(|p| p.parse::<Slot>().ok());
|
||||
start.zip(end).map(|(start, end)| start..end)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_pathbuf(path: PathBuf) -> Option<Self> {
|
||||
path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.and_then(Self::parse_filename)
|
||||
.map(|slots| Self { slots, path })
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
const CACHE_VERSION: u64 = 0;
|
||||
const DEFAULT_SLOTS_PER_ENTRY: u64 = 2500;
|
||||
const DEFAULT_MAX_CACHED_SLOTS: u64 = 5 * DEFAULT_SLOTS_PER_EPOCH;
|
||||
const CONFIG_FILENAME: &str = "config.yaml";
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct Config {
|
||||
version: u64,
|
||||
slots_per_chunk: u64,
|
||||
max_cached_slots: u64,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: CACHE_VERSION,
|
||||
slots_per_chunk: DEFAULT_SLOTS_PER_ENTRY,
|
||||
max_cached_slots: DEFAULT_MAX_CACHED_SLOTS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfirmedBlockCache {
|
||||
rpc_client: RpcClient,
|
||||
base_path: PathBuf,
|
||||
entries: RefCell<Vec<Entry>>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl ConfirmedBlockCache {
|
||||
fn store_config<P: AsRef<Path>>(config_path: P, config: &Config) -> io::Result<()> {
|
||||
let config_path = config_path.as_ref();
|
||||
let file = File::create(config_path)?;
|
||||
serde_yaml::to_writer(file, config).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"error: cannot store config `{}`: {:?}",
|
||||
config_path.to_string_lossy(),
|
||||
e,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_config<P: AsRef<Path>>(config_path: P) -> io::Result<Config> {
|
||||
let config_path = config_path.as_ref();
|
||||
let file = File::open(config_path)?;
|
||||
serde_yaml::from_reader(file).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"error: cannot load config `{}`: {:?}",
|
||||
config_path.to_string_lossy(),
|
||||
e,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open<P: AsRef<Path>, U: AsRef<str>>(path: P, rpc_url: U) -> io::Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let config_path = path.join(CONFIG_FILENAME);
|
||||
let rpc_url = rpc_url.as_ref();
|
||||
let rpc_client = RpcClient::new(rpc_url.to_string());
|
||||
let (config, entries) = match fs::read_dir(path) {
|
||||
Ok(dir_entries) => {
|
||||
let config = Self::load_config(&config_path)?;
|
||||
if config.version != CACHE_VERSION {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"unexpected cache version",
|
||||
));
|
||||
}
|
||||
let current_slot = rpc_client
|
||||
.get_slot()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{}", e)))?;
|
||||
let eviction_slot = current_slot.saturating_sub(config.max_cached_slots);
|
||||
let (delete, mut entries) = dir_entries
|
||||
.filter_map(|de| Entry::from_pathbuf(de.unwrap().path()))
|
||||
.fold(
|
||||
(Vec::new(), Vec::new()),
|
||||
|(mut delete, mut entries), entry| {
|
||||
if entry.slots.end < eviction_slot {
|
||||
delete.push(entry);
|
||||
} else {
|
||||
entries.push(entry);
|
||||
}
|
||||
(delete, entries)
|
||||
},
|
||||
);
|
||||
let mut evicted_ranges = Vec::new();
|
||||
for d in &delete {
|
||||
match std::fs::remove_file(&d.path) {
|
||||
Ok(()) => evicted_ranges.push(format!("{:?}", d.slots)),
|
||||
Err(e) => warn!("entry eviction for slots {:?} failed: {}", d.slots, e),
|
||||
}
|
||||
}
|
||||
debug!("entries evicted for slots: {}", evicted_ranges.join(", "));
|
||||
entries.sort_by(|l, r| l.slots.start.cmp(&r.slots.start));
|
||||
Ok((config, entries))
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
let config = Config::default();
|
||||
fs::create_dir_all(path)?;
|
||||
Self::store_config(config_path, &config)?;
|
||||
Ok((config, Vec::new()))
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}?;
|
||||
Ok(Self {
|
||||
rpc_client,
|
||||
base_path: path.to_path_buf(),
|
||||
entries: RefCell::new(entries),
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
fn lookup(&self, start: Slot) -> Option<Entry> {
|
||||
let entries = self.entries.borrow();
|
||||
for i in entries.iter() {
|
||||
if i.slots.start == start {
|
||||
debug!("HIT: {}", start);
|
||||
return Some(i.clone());
|
||||
}
|
||||
}
|
||||
debug!("MISS: {}", start);
|
||||
None
|
||||
}
|
||||
|
||||
fn fetch(&self, start: Slot, end: Slot, epoch_info: &EpochInfo) -> io::Result<Vec<Slot>> {
|
||||
debug!("fetching slot range: {}..{}", start, end);
|
||||
// Fingers crossed we hit the same RPC backend...
|
||||
let slots = retry_rpc_operation(42, || {
|
||||
self.rpc_client.get_confirmed_blocks(start, Some(end))
|
||||
})
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{:?}", e)))?;
|
||||
|
||||
// Only cache complete chunks
|
||||
if end + self.config.slots_per_chunk < epoch_info.absolute_slot {
|
||||
debug!("committing entry for slots {}..{}", start, end);
|
||||
let entry = Entry::new(&self.base_path, start..end);
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(entry.path())?;
|
||||
serde_json::to_writer(file, &slots)?;
|
||||
|
||||
self.entries.borrow_mut().push(entry);
|
||||
}
|
||||
|
||||
Ok(slots)
|
||||
}
|
||||
|
||||
pub fn query(&self, start: Slot, end: Slot) -> io::Result<Vec<Slot>> {
|
||||
let chunk_size = self.config.slots_per_chunk;
|
||||
let mut chunk_start = (start / chunk_size) * chunk_size;
|
||||
let mut slots = Vec::new();
|
||||
let epoch_info = self
|
||||
.rpc_client
|
||||
.get_epoch_info_with_commitment(CommitmentConfig::finalized())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{:?}", e)))?;
|
||||
let last_slot = end.min(epoch_info.absolute_slot);
|
||||
while chunk_start < last_slot {
|
||||
let mut chunk_slots = if let Some(entry) = self.lookup(chunk_start) {
|
||||
let file = File::open(entry.path())?;
|
||||
serde_json::from_reader(file)?
|
||||
} else {
|
||||
let chunk_end = chunk_start + chunk_size - 1;
|
||||
self.fetch(chunk_start, chunk_end, &epoch_info)?
|
||||
};
|
||||
slots.append(&mut chunk_slots);
|
||||
chunk_start += chunk_size;
|
||||
}
|
||||
let slots = slots
|
||||
.drain(..)
|
||||
.skip_while(|s| *s < start)
|
||||
.take_while(|s| *s <= end)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(slots)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,196 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ClusterJson {
|
||||
MainnetBeta,
|
||||
Testnet,
|
||||
}
|
||||
|
||||
impl Default for ClusterJson {
|
||||
fn default() -> Self {
|
||||
Self::MainnetBeta
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ClusterJson {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::MainnetBeta => "mainnet.json",
|
||||
Self::Testnet => "testnet.json",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_BASE_URL: &str = "https://www.validators.app/api/v1/";
|
||||
const TOKEN_HTTP_HEADER_NAME: &str = "Token";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientConfig {
|
||||
pub base_url: String,
|
||||
pub cluster: ClusterJson,
|
||||
pub api_token: String,
|
||||
}
|
||||
|
||||
impl Default for ClientConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: DEFAULT_BASE_URL.to_string(),
|
||||
cluster: ClusterJson::default(),
|
||||
api_token: String::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Endpoint {
|
||||
Ping,
|
||||
Validators,
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
fn with_cluster(path: &str, cluster: &ClusterJson) -> String {
|
||||
format!("{}/{}", path, cluster.as_ref())
|
||||
}
|
||||
pub fn path(&self, cluster: &ClusterJson) -> String {
|
||||
match self {
|
||||
Self::Ping => "ping.json".to_string(),
|
||||
Self::Validators => Self::with_cluster("validators", cluster),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct PingResponse {
|
||||
answer: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ValidatorsResponseEntry {
|
||||
pub account: Option<String>,
|
||||
pub active_stake: Option<u64>,
|
||||
pub commission: Option<u8>,
|
||||
pub created_at: Option<String>,
|
||||
pub data_center_concentration_score: Option<i64>,
|
||||
pub data_center_host: Option<String>,
|
||||
pub data_center_key: Option<String>,
|
||||
pub delinquent: Option<bool>,
|
||||
pub details: Option<String>,
|
||||
pub keybase_id: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub network: Option<String>,
|
||||
pub ping_time: Option<f64>,
|
||||
pub published_information_score: Option<i64>,
|
||||
pub root_distance_score: Option<i64>,
|
||||
pub security_report_score: Option<i64>,
|
||||
pub skipped_slot_percent: Option<String>,
|
||||
pub skipped_slot_score: Option<i64>,
|
||||
pub skipped_slots: Option<u64>,
|
||||
pub software_version: Option<String>,
|
||||
pub software_version_score: Option<i64>,
|
||||
pub stake_concentration_score: Option<i64>,
|
||||
pub total_score: Option<i64>,
|
||||
pub updated_at: Option<String>,
|
||||
pub url: Option<String>,
|
||||
pub vote_account: Option<String>,
|
||||
pub vote_distance_score: Option<i64>,
|
||||
pub www_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ValidatorsResponse(Vec<ValidatorsResponseEntry>);
|
||||
|
||||
impl AsRef<Vec<ValidatorsResponseEntry>> for ValidatorsResponse {
|
||||
fn as_ref(&self) -> &Vec<ValidatorsResponseEntry> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SortKind {
|
||||
Score,
|
||||
Name,
|
||||
Stake,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SortKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Score => write!(f, "score"),
|
||||
Self::Name => write!(f, "name"),
|
||||
Self::Stake => write!(f, "stake"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Limit = u32;
|
||||
|
||||
pub struct Client {
|
||||
base_url: reqwest::Url,
|
||||
cluster: ClusterJson,
|
||||
api_token: String,
|
||||
client: reqwest::blocking::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new<T: AsRef<str>>(api_token: T) -> Self {
|
||||
let config = ClientConfig {
|
||||
api_token: api_token.as_ref().to_string(),
|
||||
..ClientConfig::default()
|
||||
};
|
||||
Self::new_with_config(config)
|
||||
}
|
||||
|
||||
pub fn new_with_config(config: ClientConfig) -> Self {
|
||||
let ClientConfig {
|
||||
base_url,
|
||||
cluster,
|
||||
api_token,
|
||||
} = config;
|
||||
Self {
|
||||
base_url: reqwest::Url::parse(&base_url).unwrap(),
|
||||
cluster,
|
||||
api_token,
|
||||
client: reqwest::blocking::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn request(
|
||||
&self,
|
||||
endpoint: Endpoint,
|
||||
query: &HashMap<String, String>,
|
||||
) -> reqwest::Result<reqwest::blocking::Response> {
|
||||
let url = self.base_url.join(&endpoint.path(&self.cluster)).unwrap();
|
||||
let request = self
|
||||
.client
|
||||
.get(url)
|
||||
.header(TOKEN_HTTP_HEADER_NAME, &self.api_token)
|
||||
.query(&query)
|
||||
.build()?;
|
||||
self.client.execute(request)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn ping(&self) -> reqwest::Result<()> {
|
||||
let response = self.request(Endpoint::Ping, &HashMap::new())?;
|
||||
response.json::<PingResponse>().map(|_| ())
|
||||
}
|
||||
|
||||
pub fn validators(
|
||||
&self,
|
||||
sort: Option<SortKind>,
|
||||
limit: Option<Limit>,
|
||||
) -> reqwest::Result<ValidatorsResponse> {
|
||||
let mut query = HashMap::new();
|
||||
if let Some(sort) = sort {
|
||||
query.insert("sort".into(), sort.to_string());
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
query.insert("limit".into(), limit.to_string());
|
||||
}
|
||||
let response = self.request(Endpoint::Validators, &query)?;
|
||||
response.json::<ValidatorsResponse>()
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Downloads and runs the latest stake-o-matic binary
|
||||
#
|
||||
set -e
|
||||
|
||||
solana_version=edge
|
||||
curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v1.0.0/install/solana-install-init.sh \
|
||||
| sh -s - $solana_version \
|
||||
--no-modify-path \
|
||||
--data-dir ./solana-install \
|
||||
--config ./solana-install/config.yml
|
||||
|
||||
PATH="$(realpath "$PWD"/solana-install/releases/"$solana_version"*/solana-release/bin/):$PATH"
|
||||
echo PATH="$PATH"
|
||||
|
||||
set -x
|
||||
solana --version
|
||||
exec solana-stake-o-matic "$@"
|
Loading…
Reference in New Issue