Skip to content
Snippets Groups Projects
Commit 4af9995b authored by Bernardo Cardoso's avatar Bernardo Cardoso
Browse files

Multiple updates, including ability to run phragmen elections

parent e4cedb31
No related branches found
No related tags found
No related merge requests found
# scripts-py # scripts-py
Examples of interactions with xx network blockchain using python. Examples of interactions with xx network blockchain using python.
### check_cmix_variables_proposal.py
Verify an on-chain proposal to change cmix variables (which includes geo multipliers).
### economics.py ### economics.py
Contains an example of getting current economic parameters and estimating average validator payout. Get current economic parameters and estimating average validator payout.
### election.py
Run the phragmen election algorithm on the current Staking state of the chain.
### nominate.py
Nominate the top 16 validators by performance over last week.
### rewards.py ### rewards.py
Contains an example of getting rewards both in value and APY for a given set of accounts for a range of eras. Get rewards information both in value and APY for a given set of accounts for a range of eras.
### set_cmix_variables.py
Create a proposal to change cmix variables (which includes geo multipliers).
### Using the API ### Using the API
An URL can be provided to `XXNetworkInterface`in order to connect to the blockchain. It defaults to `ws://localhost:9944`. An URL can be provided to `XXNetworkInterface` in order to connect to the blockchain. It defaults to `ws://localhost:63007`.
Node operators can run the script on their node computer, or on another machine using an [SSH Tunnel](https://xxnetwork.wiki/Explorer_-_Custom_Endpoint). Node operators can run the script on their node computer, or on another machine using an [SSH Tunnel](https://xxnetwork.wiki/Wallet_-_Custom_Endpoint).
For development purposes, a local network can be launched following the instructions from [localnet](https://github.com/xx-labs/scripts#localnet). For development purposes, a local network can be launched following the instructions from [localnet](https://github.com/xx-labs/scripts#localnet).
...@@ -24,7 +24,7 @@ multiplier_decimals = 10**3 ...@@ -24,7 +24,7 @@ multiplier_decimals = 10**3
def main(): def main():
# Connect to chain # Connect to chain
# Change the endpoint if necessary # Change the endpoint if necessary
xxchain = xxapi.XXNetworkInterface('ws://127.0.0.1:63007') xxchain = xxapi.XXNetworkInterface()
if xxchain is None: if xxchain is None:
exit(1) exit(1)
......
import src.xxapi as xxapi
def main():
# Connect to chain
xxchain = xxapi.XXNetworkInterface()
if xxchain is None:
exit(1)
# Run phragmen
xxchain.seq_phragmen()
if __name__ == "__main__":
main()
...@@ -35,14 +35,14 @@ def main(): ...@@ -35,14 +35,14 @@ def main():
# (Maximum is 16 which is the best value to allow Phragmen to optimize distribution of stake) # (Maximum is 16 which is the best value to allow Phragmen to optimize distribution of stake)
num_targets = 16 num_targets = 16
# Get list of validators ranked by points in ast 7 eras # Get list of validators ranked by performance in last 7 eras
ranked = xxchain.rank_validators() ranked = xxchain.rank_validators_performance()
# Nominate # Nominate
for idx, account in enumerate(accounts): for idx, account in enumerate(accounts):
targets = [] targets = []
for j in range(num_targets): for j in range(num_targets):
targets.append(ranked[idx+j*len(accounts)]) targets.append(ranked[idx+j*len(accounts)][0])
xxchain.nominate(account, targets, False) xxchain.nominate(account, targets, False)
......
...@@ -6,7 +6,7 @@ multiplier_decimals = 10**3 ...@@ -6,7 +6,7 @@ multiplier_decimals = 10**3
def main(): def main():
# Connect to chain # Connect to chain
# Change the endpoint if necessary # Change the endpoint if necessary
xxchain = xxapi.XXNetworkInterface('ws://127.0.0.1:63007') xxchain = xxapi.XXNetworkInterface()
if xxchain is None: if xxchain is None:
exit(1) exit(1)
......
...@@ -145,3 +145,11 @@ def derive_csv_apy(raw_data): ...@@ -145,3 +145,11 @@ def derive_csv_apy(raw_data):
era_totals[idx] = 0 if era_total_stake[idx] == 0 else 100 * (era_total_reward[idx] * 365 / era_total_stake[idx]) era_totals[idx] = 0 if era_total_stake[idx] == 0 else 100 * (era_total_reward[idx] * 365 / era_total_stake[idx])
csv_data.append(era_totals) csv_data.append(era_totals)
return headers, csv_data return headers, csv_data
def get_active_stake(addr, bonded, ledger):
if addr in bonded:
ctrl = bonded[addr]
if ctrl in ledger:
return ledger[ctrl]["active"]
return None
# This file is taken from Sequential Phragmen reference
# implementation by WEB3 Foundation
# https://github.com/w3f/consensus/blob/master/NPoS/npos.py
# Small modifications were made
import logging as log
#################################
# Class definitions
#################################
class edge:
def __init__(self, nominator_id, validator_id):
self.nominator_id = nominator_id
self.validator_id = validator_id
self.load = 0
self.weight = 0
self.candidate = None
def __str__(self):
return "Edge({}, weight = {:,})".format(
self.validator_id,
self.weight,
)
class nominator:
def __init__(self, nominator_id, budget, targets):
self.nominator_id = nominator_id
self.budget = budget
self.edges = [edge(self.nominator_id, validator_id) for validator_id in targets]
self.load = 0
def __str__(self):
return "Nominator({}, budget = {:,}, load = {}, edges = {})".format(
self.nominator_id,
self.budget,
self.load,
[str(e) for e in self.edges]
)
class candidate:
def __init__(self, validator_id, index):
self.validator_id = validator_id
self.valindex = index
self.approval_stake = 0
self.backed_stake = 0
self.elected = False
self.score = 0
self.backers = 0
def __str__(self):
return "Candidate({}, approval = {:,}, backed_stake = {:,})".format(
self.validator_id,
self.approval_stake,
int(self.backed_stake),
)
#################################
# Helper methods
#################################
def calculate_approval(nomlist):
for nom in nomlist:
for edge in nom.edges:
edge.candidate.approval_stake += nom.budget
edge.candidate.backers += 1
def setuplists(votelist):
'''
Basically populates edge.candidate, and returns nomlist and candidate array. The former is a
flat list of nominators and the latter is a flat list of validator candidates.
Instead of Python's dict here, you can use anything with O(log n) addition and lookup. We can
also use a hashmap like dict, by generating a random constant r and useing H(canid+r) since the
naive thing is obviously attackable.
'''
nomlist = [nominator(votetuple[0], votetuple[1], votetuple[2]) for votetuple in votelist]
# Basically used as a cache.
candidate_dict = dict()
candidate_array = list()
num_candidates = 0
# Get an array of candidates.# We could reference these by index rather than pointer
for nom in nomlist:
for edge in nom.edges:
validator_id = edge.validator_id
if validator_id in candidate_dict:
index = candidate_dict[validator_id]
edge.candidate = candidate_array[index]
else:
candidate_dict[validator_id] = num_candidates
newcandidate = candidate(validator_id, num_candidates)
candidate_array.append(newcandidate)
edge.candidate = newcandidate
num_candidates += 1
return nomlist, candidate_array
def printresult(nomlist, candidates):
for candidate in candidates:
if candidate.elected:
log.info(f"{candidate.validator_id} is elected with stake {candidate.backed_stake / 1e9}, and score {candidate.score}")
else:
log.info(f"{candidate.validator_id} is NOT elected, with stake of {candidate.backed_stake / 1e9}")
for nom in nomlist:
log.debug(f"{nom.nominator_id} has load {nom.load} and supported ")
for edge in nom.edges:
log.debug(f"{edge.validator_id} with stake {edge.weight / 1e9}, ")
log.debug("")
log.debug("")
#################################
# Sequential Phragmen Core
#################################
def seq_phragmen_core(votelist, num_to_elect):
nomlist, candidates = setuplists(votelist)
calculate_approval(nomlist)
# main election loop
for _ in range(num_to_elect):
# loop 1: initialize score
for candidate in candidates:
if not candidate.elected:
if candidate.approval_stake > 0:
candidate.score = 1 / candidate.approval_stake
else:
candidate.score = 1000
# loop 2: increment score
for nom in nomlist:
for edge in nom.edges:
if not edge.candidate.elected and edge.candidate.approval_stake > 0:
edge.candidate.score += nom.budget * nom.load / edge.candidate.approval_stake
best_candidate = 0
best_score = 1000 # should be infinite but I'm lazy
# loop 3: find winner
for candidate in candidates:
if not candidate.elected and candidate.score < best_score:
best_score = candidate.score
best_candidate = candidate.valindex
candidates[best_candidate].elected = True
# loop 3: update voter and edge load
for nom in nomlist:
for edge in nom.edges:
if edge.candidate.valindex == best_candidate:
edge.load = candidates[best_candidate].score - nom.load
nom.load = candidates[best_candidate].score
# update backing stake of candidates and voters
for nom in nomlist:
for edge in nom.edges:
if edge.candidate.elected:
edge.weight = nom.budget * edge.load/nom.load
else:
edge.weight = 0
edge.candidate.backed_stake += edge.weight
nom.edges = list(filter(lambda edge: edge.weight > 0, nom.edges))
# populate backing stake of non elected
for candidate in candidates:
if not candidate.elected:
candidate.backed_stake = 1 / candidate.score
return (nomlist, candidates)
#################################
# Equalisation of elected stake
#################################
def equalise(nom, tolerance):
# Attempts to redistribute the nominators budget between elected validators. Assumes that all
# elected validators have backed_stake set correctly. Returns the max difference in stakes
# between sup.
elected_edges = [edge for edge in nom.edges if edge.candidate.elected]
if len(elected_edges) < 2:
return 0.0
stake_used = sum([edge.weight for edge in elected_edges])
backed_stakes = [edge.candidate.backed_stake for edge in elected_edges]
backingbacked_stakes = [
edge.candidate.backed_stake for edge in elected_edges if edge.weight > 0.0
]
if len(backingbacked_stakes) > 0:
difference = max(backingbacked_stakes)-min(backed_stakes)
difference += nom.budget - stake_used
if difference < tolerance:
return difference
else:
difference = nom.budget
# remove all backing
for edge in nom.edges:
edge.candidate.backed_stake -= edge.weight
edge.weight = 0
elected_edges.sort(key=lambda x: x.candidate.backed_stake)
cumulative_backed_stake = 0
last_index = len(elected_edges) - 1
for i in range(len(elected_edges)):
backed_stake = elected_edges[i].candidate.backed_stake
if backed_stake * i - cumulative_backed_stake > nom.budget:
last_index = i-1
break
cumulative_backed_stake += backed_stake
last_stake = elected_edges[last_index].candidate.backed_stake
ways_to_split = last_index+1
excess = nom.budget + cumulative_backed_stake - last_stake*ways_to_split
for edge in elected_edges[0:ways_to_split]:
edge.weight = excess / ways_to_split + last_stake - edge.candidate.backed_stake
edge.candidate.backed_stake += edge.weight
return difference
def equalise_all(nomlist, maxiterations, tolerance):
for _ in range(maxiterations):
maxdifference = 0
for nom in nomlist:
difference = equalise(nom, tolerance)
maxdifference = max(difference, maxdifference)
if maxdifference < tolerance:
return
#################################
# Sequential Phragmen Algorithm
#################################
def seq_phragmen(votelist, num_to_elect):
nomlist, candidates = seq_phragmen_core(votelist, num_to_elect)
equalise_all(nomlist, 10, 0)
return nomlist, sorted(candidates, key=lambda item: item.backed_stake, reverse=True)
def compute_score(candidates):
min_support = min([c.backed_stake if c.elected else 1e18 for c in candidates])
sum_support = sum([c.backed_stake if c.elected else 0 for c in candidates])
sum_squared = sum([c.backed_stake*c.backed_stake if c.elected else 0 for c in candidates])
return [int(min_support), int(sum_support), int(sum_squared)]
import json
import logging as log import logging as log
import math
from substrateinterface import SubstrateInterface, Keypair # pip3 install substrate-interface from substrateinterface import SubstrateInterface, Keypair # pip3 install substrate-interface
from substrateinterface.exceptions import SubstrateRequestException from substrateinterface.exceptions import SubstrateRequestException
import src.helpers as helpers import src.helpers as helpers
import src.phragmen as phragmen
from datetime import datetime from datetime import datetime
import sys import sys
...@@ -13,14 +16,6 @@ import sys ...@@ -13,14 +16,6 @@ import sys
class XXNetworkInterface(SubstrateInterface): class XXNetworkInterface(SubstrateInterface):
# Constructor # Constructor
def __init__(self, url: str = "ws://localhost:63007", logfile: str = "", verbose: bool = False): def __init__(self, url: str = "ws://localhost:63007", logfile: str = "", verbose: bool = False):
try:
super(XXNetworkInterface, self).__init__(url=url)
except ConnectionRefusedError:
log.error("Can't connect to specified xx network node endpoint")
raise
except Exception as e:
log.error("Failed to get xx network connection: %s" % e)
raise
# Keychain # Keychain
self.keychain = {} self.keychain = {}
# Known accounts # Known accounts
...@@ -40,6 +35,7 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -40,6 +35,7 @@ class XXNetworkInterface(SubstrateInterface):
self.cache = {} self.cache = {}
# Setup logging # Setup logging
handlers = [log.StreamHandler(sys.stdout)] handlers = [log.StreamHandler(sys.stdout)]
self.verbose = verbose
if logfile != "": if logfile != "":
handlers.append(log.FileHandler(logfile, mode='w')) handlers.append(log.FileHandler(logfile, mode='w'))
log.basicConfig( log.basicConfig(
...@@ -48,6 +44,15 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -48,6 +44,15 @@ class XXNetworkInterface(SubstrateInterface):
level=log.INFO if not verbose else log.DEBUG, level=log.INFO if not verbose else log.DEBUG,
datefmt='%Y-%m-%d %H:%M:%S' datefmt='%Y-%m-%d %H:%M:%S'
) )
# Initialize substrate interface
try:
super(XXNetworkInterface, self).__init__(url=url)
except ConnectionRefusedError:
log.error("Can't connect to specified xx network node endpoint")
raise
except Exception as e:
log.error("Failed to get xx network connection: %s" % e)
raise
############################## ##############################
# Generic query functions # Generic query functions
...@@ -377,18 +382,28 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -377,18 +382,28 @@ class XXNetworkInterface(SubstrateInterface):
def stakeable_history(self, start_block: int = None, block_step: int = None): def stakeable_history(self, start_block: int = None, block_step: int = None):
return self.query_history(start_block, block_step, self.stakeable, None) return self.query_history(start_block, block_step, self.stakeable, None)
# Compute estimate of validator rewards for current era # Get economic info at a given block
# If a validator address is provided, it computes its portion based on current era points def economic_info(self, block_number):
# Otherwise the average payout is given
def estimate_payout(self, validator_address: str = ""):
# Get inflation params # Get inflation params
inflation = self.item_query("XXEconomics", "InflationParams") inflation = self.item_query("XXEconomics", "InflationParams")
min_inflation = inflation['min_inflation'] / helpers.DECIMALS min_inflation = inflation['min_inflation'] / helpers.DECIMALS
ideal_stake = inflation['ideal_stake'] / helpers.DECIMALS ideal_stake = inflation['ideal_stake'] / helpers.DECIMALS
# Get interest points # Get interest points
interest_points = self.item_query("XXEconomics", "InterestPoints") interest_points = self.item_query("XXEconomics", "InterestPoints")
# Compute fixed economics
interest = helpers.get_interest(interest_points, block_number)
ideal_inflation = ideal_stake * interest
return {
"min_inflation": min_inflation,
"ideal_stake": ideal_stake,
"interest": interest,
'ideal_inflation': ideal_inflation,
}
# Compute estimate of validator rewards for current era
# If a validator address is provided, it computes its portion based on current era points
# Otherwise the average payout is given
def estimate_payout(self, validator_address: str = ""):
# Get current era stake # Get current era stake
era = self.item_query("Staking", "ActiveEra") era = self.item_query("Staking", "ActiveEra")
start = era['start'] start = era['start']
...@@ -399,6 +414,10 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -399,6 +414,10 @@ class XXNetworkInterface(SubstrateInterface):
# Get validator set size # Get validator set size
val_set = self.double_map_query("Staking", "ErasStakers", era) val_set = self.double_map_query("Staking", "ErasStakers", era)
val_size = len(val_set) val_size = len(val_set)
# Compute total TM stake
tm_stake = sum([exposures["custody"] for exposures in val_set.values()])
tm_stake = helpers.remove_decimals(tm_stake)
tm_impact = stake / (stake + tm_stake)
# Estimate block number for middle of curr era # Estimate block number for middle of curr era
block = self.get_block() block = self.get_block()
...@@ -412,24 +431,29 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -412,24 +431,29 @@ class XXNetworkInterface(SubstrateInterface):
predicted_middle_curr = block_no - int((network_time-start)/self.block_time) + int(self.blocks_per_era/2) predicted_middle_curr = block_no - int((network_time-start)/self.block_time) + int(self.blocks_per_era/2)
log.info(f"Middle curr era block: {predicted_middle_curr}") log.info(f"Middle curr era block: {predicted_middle_curr}")
# Compute fixed economics # Fet fixed economics info
interest = helpers.get_interest(interest_points, predicted_middle_curr) economics = self.economic_info(predicted_middle_curr)
ideal_inflation = ideal_stake * interest min_inflation = economics["min_inflation"]
ideal_stake = economics["ideal_stake"]
interest = economics["interest"]
ideal_inflation = economics["ideal_inflation"]
log.info(f"Min inflation: {min_inflation}") log.info(f"Min inflation: {min_inflation}")
log.info(f"Ideal staking ratio: {ideal_stake}") log.info(f"Ideal staking ratio: {ideal_stake}")
log.info(f"Interest ratio: {interest}") log.info(f"Interest ratio: {interest}")
log.info(f"Ideal inflation: {ideal_inflation}") log.info(f"Ideal inflation: {ideal_inflation}")
# Compute economics # Compute staking ratio and inflation
stakeable = self.stakeable() stakeable = self.stakeable()
stake_ratio = stake / stakeable stake_ratio = stake / stakeable
inflation = min_inflation + (ideal_inflation - min_inflation)*stake_ratio/ideal_stake
log.info(f"Era: {era}") log.info(f"Era: {era}")
log.info(f"Stakeable: {stakeable}") log.info(f"Stakeable: {stakeable}")
log.info(f"Staked: {stake}") log.info(f"Staked: {stake}")
log.info(f"Team multipliers: {tm_stake}")
log.info(f"Stake ratio: {stake_ratio}") log.info(f"Stake ratio: {stake_ratio}")
inflation = min_inflation + (ideal_inflation - min_inflation)*stake_ratio/ideal_stake
log.info(f"Inflation: {inflation}") log.info(f"Inflation: {inflation}")
log.info(f"Average return: {tm_impact*inflation/stake_ratio}")
# Compute era payout # Compute era payout
total_payout = ideal_inflation*stakeable*self.era_milis/self.year_milis total_payout = ideal_inflation*stakeable*self.era_milis/self.year_milis
...@@ -457,10 +481,14 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -457,10 +481,14 @@ class XXNetworkInterface(SubstrateInterface):
val_payout = payout / val_size val_payout = payout / val_size
log.info(f"Validator set size: {val_size}") log.info(f"Validator set size: {val_size}")
log.info("Estimated validator payment, assuming equal performance") log.info(f"Estimated validator payment, assuming equal performance")
log.info(val_payout) log.info(val_payout)
return val_payout return val_payout
##############################
# Staking queries
##############################
# Get staking rewards per validator earned by given account(s) in given era range # Get staking rewards per validator earned by given account(s) in given era range
# Defaults to start_era = 0, end_era = None, which gets all rewards up to last era # Defaults to start_era = 0, end_era = None, which gets all rewards up to last era
def staking_rewards(self, accounts, start_era: int = 0, end_era: int = None): def staking_rewards(self, accounts, start_era: int = 0, end_era: int = None):
...@@ -522,6 +550,9 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -522,6 +550,9 @@ class XXNetworkInterface(SubstrateInterface):
# Validator info # Validator info
validator_stake = info["own"] validator_stake = info["own"]
total_stake = info["total"] total_stake = info["total"]
# Count TM in total stake if exposure contains it
if "custody" in info:
total_stake += info["custody"]
validator_commission = helpers.remove_decimals(prefs[validator]["commission"]) validator_commission = helpers.remove_decimals(prefs[validator]["commission"])
# Find points # Find points
...@@ -565,10 +596,6 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -565,10 +596,6 @@ class XXNetworkInterface(SubstrateInterface):
}) })
return result return result
##############################
# Staking queries
##############################
# Check current nomination targets of given accounts are validators # Check current nomination targets of given accounts are validators
def check_nominations(self, accounts): def check_nominations(self, accounts):
nominators = self.map_query("Staking", "Nominators", "") nominators = self.map_query("Staking", "Nominators", "")
...@@ -591,8 +618,9 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -591,8 +618,9 @@ class XXNetworkInterface(SubstrateInterface):
log.info(f"Account {acct} has valid nomination targets") log.info(f"Account {acct} has valid nomination targets")
return bad_targets return bad_targets
# Rank validators based on performance over the last given number of eras (defaults to 7) # Rank validators based on average performance over the last given number of eras (defaults to 7)
def rank_validators(self, eras: int = 7): # Returns a list of [address, points] sorted by most points
def rank_validators_performance(self, eras: int = 7):
validators = self.map_query("Staking", "Validators", "") validators = self.map_query("Staking", "Validators", "")
curr_era = self.item_query("Staking", "ActiveEra") curr_era = self.item_query("Staking", "ActiveEra")
curr_era = curr_era['index'] curr_era = curr_era['index']
...@@ -629,7 +657,163 @@ class XXNetworkInterface(SubstrateInterface): ...@@ -629,7 +657,163 @@ class XXNetworkInterface(SubstrateInterface):
else: else:
totals[validator] += validator_points totals[validator] += validator_points
return sorted(totals, key=totals.get, reverse=True) return sorted(list(totals.items()), key=lambda item: item[1], reverse=True)
# Rank validators based on average nominator return over the last given number of eras (defaults to 7)
# Returns a list of [address, ROI] sorted by highest ROI
def rank_validators_return(self, eras: int = 7):
validators = self.map_query("Staking", "Validators", "")
curr_era = self.item_query("Staking", "ActiveEra")
curr_era = curr_era['index']
end_era = curr_era - 1
start_era = end_era - eras + 1
# Gather necessary information for era range
# History depth
depth = self.item_query("Staking", "HistoryDepth")
points = {}
rewards = {}
target_era = start_era
while True:
eras_points = self.query_era(target_era, self.map_query, "Staking", "ErasRewardPoints", "")
points = {**points, **eras_points}
eras_rewards = self.query_era(target_era, self.map_query, "Staking", "ErasValidatorReward", "")
rewards = {**rewards, **eras_rewards}
target_era += depth - 1
if target_era >= end_era:
break
totals = {}
for era in range(start_era, end_era+1):
log.info(f"Processing era {era}")
era_total_reward = rewards[era]
era_points = points[era]
points_total = era_points["total"]
# Get stakers for specified era
stakers = self.query_era(era, self.double_map_query, "Staking", "ErasStakers", era)
# Get validator preferences for specified era
prefs = self.query_era(era, self.double_map_query, "Staking", "ErasValidatorPrefs", era)
for validator, info in stakers.items():
# Validator info
total_stake = info["total"]
# Count TM in total stake if exposure contains it
if "custody" in info:
total_stake += info["custody"]
validator_commission = helpers.remove_decimals(prefs[validator]["commission"])
# Find points
validator_points = 0
for value in era_points["individual"]:
if value[0] == validator:
validator_points = value[1]
break
# Validator reward according to performance points
validator_reward = validator_points * era_total_reward / points_total
# Validator commission reward
commission_reward = validator_commission * validator_reward
# Deduct commission reward from validator reward
validator_leftover_reward = validator_reward - commission_reward
# Compute nominator return in APY
nominator_return = (validator_leftover_reward * 365) / total_stake
# Accumulate return
if validator not in totals:
totals[validator] = nominator_return / eras
else:
totals[validator] += nominator_return / eras
# Populate any missing validators (the ones that have never been elected in the given eras)
for validator in validators.keys():
if validator not in totals:
totals[validator] = 0.0
return sorted(list(totals.items()), key=lambda item: item[1], reverse=True)
##############################
# Phragmen Elections
##############################
# Cache staking state necessary for Phragmen
def cache_staking_state(self):
self.map_query("Staking", "Bonded", "", force_cache_refresh=True)
self.map_query("Staking", "Ledger", "", force_cache_refresh=True)
self.map_query("Staking", "Nominators", "", force_cache_refresh=True)
self.map_query("Staking", "Validators", "", force_cache_refresh=True)
self.map_query("Staking", "SlashingSpans", "", force_cache_refresh=True)
# Build the list of voters, excluding the given nominator if provided
# This consists of all nominators + validators' self vote
# Format: (address, active_stake, targets)
# NOTE: make sure to cache the following maps before calling:
# Staking.Validators
# Staking.Nominators
# Staking.Bonded
# Staking.Ledger
# Staking.SlashingSpans
def build_voters(self, exclude: str = ""):
# Get maps from cache
validators = None
nominators = None
bonded = None
ledger = None
slash_spans = None
if "Staking" in self.cache:
if "Validators" in self.cache["Staking"]:
validators = self.cache["Staking"]["Validators"]
if "Nominators" in self.cache["Staking"]:
nominators = self.cache["Staking"]["Nominators"]
if "Bonded" in self.cache["Staking"]:
bonded = self.cache["Staking"]["Bonded"]
if "Ledger" in self.cache["Staking"]:
ledger = self.cache["Staking"]["Ledger"]
if "SlashingSpans" in self.cache["Staking"]:
slash_spans = self.cache["Staking"]["SlashingSpans"]
if validators is None or nominators is None or bonded is None or ledger is None or slash_spans is None:
log.error("Can't build voters without having Staking maps cached")
raise Exception
voters = []
for validator in validators.keys():
stake = helpers.get_active_stake(validator, bonded, ledger)
if stake is not None:
voters.append([validator, stake, [validator]])
for nominator, noms in nominators.items():
if nominator == exclude:
continue
stake = helpers.get_active_stake(nominator, bonded, ledger)
# Remove non validators, duplicates and slashed validators needing renomination from targets
if stake is not None:
targets = []
for target in noms["targets"]:
if target not in validators:
continue
if target in targets:
continue
slashed_era = 0 if target not in slash_spans else slash_spans[target]["last_nonzero_slash"]
if noms["submitted_in"] < slashed_era:
continue
targets.append(target)
voters.append([nominator, stake, targets])
return voters
# Run sequential phragmen election on the current state of the network
# NOTE: this function doesn't perform the reduction algorithm after the election
# This means that the winning validators and their total stake are correct
# but there is no optimized distribution of nominator stake that reduces the overall number
# of exposed validators per nominator
def seq_phragmen(self):
# Cache Staking state
self.cache_staking_state()
to_elect = self.item_query("Staking", "ValidatorCount")
log.info(f"{len(self.cache['Staking']['Validators'])} validators, {len(self.cache['Staking']['Nominators'])} nominators, electing {to_elect}")
voters = self.build_voters()
# Run sequential phragmen algorithmn
distribution, validators = phragmen.seq_phragmen(voters, to_elect)
phragmen.printresult(distribution, validators)
log.info(f"score = {phragmen.compute_score(validators)}")
############################## ##############################
# Actions # Actions
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment