mirror of
https://github.com/pezkuwichain/consensus.git
synced 2026-04-22 02:07:56 +00:00
@@ -0,0 +1 @@
|
||||
.vscode
|
||||
@@ -0,0 +1,2 @@
|
||||
.vscode
|
||||
__pycache__
|
||||
+40
-40
@@ -7,7 +7,7 @@ class edge:
|
||||
#self.index
|
||||
#self.voterindex
|
||||
#self.canindex
|
||||
|
||||
|
||||
|
||||
class voter:
|
||||
def __init__(self,votetuple):
|
||||
@@ -83,7 +83,7 @@ class assignment:
|
||||
def unelect(self,candidate):
|
||||
self.canelected[candidate.index]=False
|
||||
self.electedcandidates.remove(candidate)
|
||||
|
||||
|
||||
def setuplists(votelist):
|
||||
#Instead of Python's dict here, you can use anything with O(log n) addition and lookup.
|
||||
#We can also use a hashmap, by generating a random constant r and useing H(canid+r)
|
||||
@@ -94,7 +94,7 @@ def setuplists(votelist):
|
||||
numcandidates=0
|
||||
numvoters=0
|
||||
numedges=0
|
||||
|
||||
|
||||
#Get an array of candidates that we can reference these by index
|
||||
for nom in voterlist:
|
||||
nom.index=numvoters
|
||||
@@ -115,13 +115,13 @@ def setuplists(votelist):
|
||||
edge.canindex=numcandidates
|
||||
numcandidates += 1
|
||||
return(voterlist,candidatearray)
|
||||
|
||||
|
||||
|
||||
def seqPhragmén(votelist,numtoelect):
|
||||
nomlist,candidates=setuplists(votelist)
|
||||
#creating an assignment now also computes the total possible stake for each candidate
|
||||
a=assignment(nomlist,candidates)
|
||||
|
||||
|
||||
for round in range(numtoelect):
|
||||
for canindex in range(len(candidates)):
|
||||
if not a.canelected[canindex]:
|
||||
@@ -172,8 +172,8 @@ def calculateScores(a,cutoff):
|
||||
#if not a.canelected[canindex]:
|
||||
#print(a.candidates[canindex].canid," has score ", a.canscore[canindex]," with cutoff ",cutoff)
|
||||
#print("Approval stake: ", a.canapproval[canindex]," support: ",a.cansupport[canindex]," denominator: ",a.canscoredenominator[canindex], " numerator: ",a.canscorenumerator[canindex])
|
||||
|
||||
|
||||
|
||||
|
||||
def calculateMaxScore(a):
|
||||
supportList=[a.cansupport[can.index] for can in a.electedcandidates]
|
||||
supportList.append(0.0)
|
||||
@@ -249,7 +249,7 @@ def equalise(a, nom, tolerance):
|
||||
# Attempts to redistribute the nominators budget between elected validators
|
||||
# Assumes that all elected validators have backedstake set correctly
|
||||
# returns the max difference in stakes between sup
|
||||
|
||||
|
||||
electededges=[edge for edge in nom.edges if a.canelected[edge.canindex]]
|
||||
if len(electededges)==0:
|
||||
return 0.0
|
||||
@@ -310,7 +310,7 @@ def seqPhragménwithpostprocessing(votelist,numtoelect, ratio=1):
|
||||
def factor3point15(votelist, numtoelect,tolerance=0.1):
|
||||
nomlist,candidates=setuplists(votelist)
|
||||
a=assignment(nomlist,candidates)
|
||||
|
||||
|
||||
for round in range(numtoelect):
|
||||
bestcandidate,score=calculateMaxScore(a)
|
||||
insertWithScore(a,bestcandidate, score)
|
||||
@@ -325,7 +325,7 @@ def maybecandidate(a,newcandidate,shouldremoveworst, tolerance):
|
||||
if shouldremoveworst:
|
||||
worstcanidate =min(electedcandidates, key = lambda x: b.cansupport[x.index])
|
||||
b.unelect(worstcandidate)
|
||||
b.elect(newcandidate)
|
||||
b.elect(newcandidate)
|
||||
equaliseall(b,100000000,tolerance)
|
||||
newvalue=min([b.cansupport[candidate.index] for candidate in b.electedcandidates])
|
||||
return b, newvalue
|
||||
@@ -395,7 +395,7 @@ def binarysearchfeasible(votelist,numtoelect,tolerance=0.1):
|
||||
else:
|
||||
targetvalue=currentvalue
|
||||
#print(targetvalue,lastgoodindex, maxvalue,bestknownvalue,currentvalue)
|
||||
|
||||
|
||||
|
||||
for round in range(lastgoodindex+1,numtoelect):
|
||||
# First try maxscore candidate, which will help with PJR
|
||||
@@ -460,22 +460,22 @@ def doall(votelist, numtoelect, listvoters=True, listcans=True):
|
||||
if listvoters:
|
||||
print("Votes ",votelist)
|
||||
alglist=[(approvalvoting,"Approval voting"), (seqPhragmén, "Sequential Phragmén"),
|
||||
(seqPhragménwithpostprocessing, "Sequential Phragmén with post processing"),
|
||||
(seqPhragménwithpostprocessing, "Sequential Phragmén with post processing"),
|
||||
(factor3point15, "The factor 3.15 thing"), (binarysearchfeasible,"Factor 2 by binary search"), (SFFB18, "SFFB18")]
|
||||
for alg,name in alglist:
|
||||
st=time.perf_counter()
|
||||
st=time.perf_counter()
|
||||
a = alg(votelist,numtoelect)
|
||||
et=time.perf_counter()
|
||||
et=time.perf_counter()
|
||||
print(name, " gives")
|
||||
printresult(a,listvoters,listcans)
|
||||
print(" in ",et-st," seconds.")
|
||||
print()
|
||||
|
||||
|
||||
|
||||
def example1():
|
||||
votelist=[("A",10.0,["X","Y"]),("B",20.0,["X","Z"]),("C",30.0,["Y","Z"])]
|
||||
doall(votelist,2)
|
||||
|
||||
|
||||
|
||||
def example2():
|
||||
# Approval voting does not do so well for this kind of thing.
|
||||
@@ -491,7 +491,7 @@ def example3():
|
||||
bluevoters = [("BlueV"+str(i),20.0,blueparty) for i in range(20)]
|
||||
votelist= redvoters+bluevoters
|
||||
doall(votelist, 20, False)
|
||||
|
||||
|
||||
|
||||
def example4():
|
||||
#Now we want an example where seq Phragmén is not so good.
|
||||
@@ -518,7 +518,7 @@ def example6():
|
||||
("M",50.0, ["M"])]
|
||||
print("Votes ",votelist)
|
||||
doall(votelist,5)
|
||||
|
||||
|
||||
def exampleLine():
|
||||
votelist = [
|
||||
("a", 2000, ["A"]),
|
||||
@@ -530,7 +530,7 @@ def exampleLine():
|
||||
("g", 1000, ["F","G"])
|
||||
]
|
||||
doall(votelist,7)
|
||||
|
||||
|
||||
def ri(vals=20,noms=2000, votesize=10):
|
||||
#Let's try a random instance
|
||||
candidates=["Val"+str(i) for i in range(vals)]
|
||||
@@ -560,28 +560,28 @@ def riparty(vals=200,noms=2000, votesize=10,seed=1):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
import unittest
|
||||
|
||||
|
||||
def print_list(ll):
|
||||
for item in ll:
|
||||
print(item)
|
||||
|
||||
|
||||
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.scoredenom = 0
|
||||
|
||||
def __str__(self):
|
||||
return "Candidate({}, approval = {:,}, backed_stake = {:,})".format(
|
||||
self.validator_id,
|
||||
self.approval_stake,
|
||||
int(self.backed_stake),
|
||||
)
|
||||
|
||||
|
||||
def seq_phragmen(votelist, num_to_elect):
|
||||
nomlist, candidates = setuplists(votelist)
|
||||
calculate_approval(nomlist)
|
||||
|
||||
elected_candidates = list()
|
||||
for round in range(num_to_elect):
|
||||
for candidate in candidates:
|
||||
if not candidate.elected:
|
||||
candidate.score = 1/candidate.approval_stake
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
if not edge.candidate.elected:
|
||||
edge.candidate.score += nom.budget * nom.load / edge.candidate.approval_stake
|
||||
best_candidate = 0
|
||||
best_score = 1000 # should be infinite but I'm lazy
|
||||
for candidate in candidates:
|
||||
if not candidate.elected and candidate.score < best_score:
|
||||
best_score = candidate.score
|
||||
best_candidate = candidate.valindex
|
||||
elected_candidate = candidates[best_candidate]
|
||||
elected_candidate.elected = True
|
||||
elected_candidate.electedpos = round
|
||||
elected_candidates.append(elected_candidate)
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
if edge.candidate.valindex == best_candidate:
|
||||
edge.load = elected_candidate.score - nom.load
|
||||
nom.load = elected_candidate.score
|
||||
|
||||
for candidate in elected_candidates:
|
||||
candidate.backed_stake = 0
|
||||
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
if nom.load > 0.0:
|
||||
edge.weight = nom.budget * edge.load/nom.load
|
||||
edge.candidate.backed_stake += edge.weight
|
||||
else:
|
||||
edge.weight = 0
|
||||
return (nomlist, elected_candidates)
|
||||
|
||||
|
||||
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 i in range(maxiterations):
|
||||
# for j in range(len(nomlist)):
|
||||
# nom = random.choice(nomlist)
|
||||
# equalise(nom, tolerance)
|
||||
maxdifference = 0
|
||||
for nom in nomlist:
|
||||
difference = equalise(nom, tolerance)
|
||||
maxdifference = max(difference, maxdifference)
|
||||
if maxdifference < tolerance:
|
||||
return
|
||||
|
||||
|
||||
def seq_phragmen_with_equalise(votelist, num_to_elect):
|
||||
nomlist, elected_candidates = seq_phragmen(votelist, num_to_elect)
|
||||
equalise_all(nomlist, 2, 0)
|
||||
return nomlist, elected_candidates
|
||||
|
||||
|
||||
def calculateMaxScoreNoCutoff(nomlist, candidates):
|
||||
# First we compute the denominator of the score
|
||||
for candidate in candidates:
|
||||
if not candidate.elected:
|
||||
candidate.scoredenom = 1.0
|
||||
|
||||
for nom in nomlist:
|
||||
denominator_contrib = 0
|
||||
|
||||
for edge in nom.edges:
|
||||
if edge.candidate.elected:
|
||||
denominator_contrib += edge.weight/edge.candidate.backed_stake
|
||||
|
||||
for edge in nom.edges:
|
||||
if not edge.candidate.elected:
|
||||
edge.candidate.scoredenom += denominator_contrib
|
||||
|
||||
# Then we divide. Not that score here is comparable to the recipricol of the score in
|
||||
# seq-phragmen. In particular there low scores are good whereas here high scores are good.
|
||||
best_candidate = 0
|
||||
best_score = 0.0
|
||||
for candidate in candidates:
|
||||
if candidate.approval_stake > 0.0:
|
||||
candidate.score = candidate.approval_stake / candidate.scoredenom
|
||||
if not candidate.elected and candidate.score > best_score:
|
||||
best_score = candidate.score
|
||||
best_candidate = candidate
|
||||
else:
|
||||
candidate.score = 0.0
|
||||
|
||||
return (best_candidate, best_score)
|
||||
|
||||
|
||||
def electWithScore(nomlist, elected_candidate, cutoff):
|
||||
for nom in nomlist:
|
||||
for new_edge in nom.edges:
|
||||
if new_edge.validator_id == elected_candidate.validator_id:
|
||||
used_budget = sum([edge.weight for edge in nom.edges])
|
||||
|
||||
new_edge.weight = nom.budget - used_budget
|
||||
elected_candidate.backed_stake += nom.budget - used_budget
|
||||
|
||||
for edge in nom.edges:
|
||||
if edge.validator_id != elected_candidate.validator_id and edge.weight > 0.0:
|
||||
if edge.candidate.backed_stake > cutoff:
|
||||
stake_to_take = edge.weight * cutoff / edge.candidate.backed_stake
|
||||
|
||||
new_edge.weight += stake_to_take
|
||||
elected_candidate.backed_stake += stake_to_take
|
||||
|
||||
edge.weight -= stake_to_take
|
||||
edge.candidate.backed_stake -= stake_to_take
|
||||
|
||||
|
||||
def phragmms(votelist, num_to_elect, tolerance=0.1):
|
||||
nomlist, candidates = setuplists(votelist)
|
||||
calculate_approval(nomlist)
|
||||
|
||||
elected_candidates = list()
|
||||
for round in range(num_to_elect):
|
||||
(elected_candidate, score) = calculateMaxScoreNoCutoff(nomlist, candidates)
|
||||
electWithScore(nomlist, elected_candidate, score)
|
||||
|
||||
elected_candidate.elected = True
|
||||
elected_candidates.append(elected_candidate)
|
||||
elected_candidate.electedpos = round
|
||||
|
||||
equalise_all(nomlist, 10, tolerance)
|
||||
|
||||
return nomlist, elected_candidates
|
||||
|
||||
|
||||
def approval_voting(votelist, num_to_elect):
|
||||
nomlist, candidates = setuplists(votelist)
|
||||
# Compute the total possible stake for each candidate
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
edge.candidate.approval_stake += nom.budget
|
||||
edge.weight = nom.budget/min(len(nom.edges), num_to_elect)
|
||||
edge.candidate.backed_stake += edge.weight
|
||||
candidates.sort(key=lambda x: x.approval_stake, reverse=True)
|
||||
elected_candidates = candidates[0:num_to_elect]
|
||||
return nomlist, elected_candidates
|
||||
|
||||
|
||||
def calculate_approval(nomlist):
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
edge.candidate.approval_stake += nom.budget
|
||||
|
||||
|
||||
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 run_and_print_all(votelist, to_elect):
|
||||
print("######\nVotes ", votelist)
|
||||
|
||||
print("\nSequential Phragmén gives")
|
||||
nomlist, elected_candidates = seq_phragmen(votelist, to_elect)
|
||||
printresult(nomlist, elected_candidates)
|
||||
|
||||
print("\nApproval voting gives")
|
||||
nomlist, elected_candidates = approval_voting(votelist, to_elect)
|
||||
printresult(nomlist, elected_candidates)
|
||||
|
||||
print("\nSequential Phragmén with post processing gives")
|
||||
nomlist, elected_candidates = seq_phragmen_with_equalise(votelist, to_elect)
|
||||
printresult(nomlist, elected_candidates)
|
||||
|
||||
print("\nBalanced Heuristic (3.15 factor) gives")
|
||||
nomlist, elected_candidates = phragmms(votelist, to_elect)
|
||||
printresult(nomlist, elected_candidates)
|
||||
|
||||
|
||||
def printresult(nomlist, elected_candidates, verbose=True):
|
||||
for candidate in elected_candidates:
|
||||
print(candidate.validator_id, " is elected with stake ",
|
||||
candidate.backed_stake, "and score ", candidate.score)
|
||||
if verbose:
|
||||
for nom in nomlist:
|
||||
print(nom.nominator_id, " has load ", nom.load, "and supported ")
|
||||
for edge in nom.edges:
|
||||
print(edge.validator_id, " with stake ", edge.weight, end=", ")
|
||||
print()
|
||||
print()
|
||||
|
||||
|
||||
def example1():
|
||||
votelist = [
|
||||
("A", 10.0, ["X", "Y"]),
|
||||
("B", 20.0, ["X", "Z"]),
|
||||
("C", 30.0, ["Y", "Z"]),
|
||||
]
|
||||
run_and_print_all(votelist, 2)
|
||||
|
||||
|
||||
def example2():
|
||||
votelist = [
|
||||
("10", 1000, ["10"]),
|
||||
("20", 1000, ["20"]),
|
||||
("30", 1000, ["30"]),
|
||||
("40", 1000, ["40"]),
|
||||
('2', 500, ['10', '20', '30']),
|
||||
('4', 500, ['10', '20', '40'])
|
||||
]
|
||||
run_and_print_all(votelist, 2)
|
||||
|
||||
|
||||
class MaxScoreTest(unittest.TestCase):
|
||||
def test_max_score_1(self):
|
||||
votelist = [
|
||||
(10, 10.0, [1, 2]),
|
||||
(20, 20.0, [1, 3]),
|
||||
(30, 30.0, [2, 3]),
|
||||
]
|
||||
nomlist, candidates = setuplists(votelist)
|
||||
calculate_approval(nomlist)
|
||||
|
||||
best, score = calculateMaxScoreNoCutoff(nomlist, candidates)
|
||||
self.assertEqual(best.validator_id, 3)
|
||||
self.assertEqual(score, 50)
|
||||
|
||||
def test_balance_heuristic_example_1(self):
|
||||
votelist = [
|
||||
(10, 10.0, [1, 2]),
|
||||
(20, 20.0, [1, 3]),
|
||||
(30, 30.0, [2, 3]),
|
||||
]
|
||||
nomlist, winners = phragmms(votelist, 2, 0)
|
||||
self.assertEqual(winners[0].validator_id, 3)
|
||||
self.assertEqual(winners[1].validator_id, 2)
|
||||
|
||||
self.assertEqual(winners[0].backed_stake, 30)
|
||||
self.assertEqual(winners[1].backed_stake, 30)
|
||||
|
||||
def test_balance_heuristic_example_linear(self):
|
||||
votelist = [
|
||||
(2, 2000, [11]),
|
||||
(4, 1000, [11, 21]),
|
||||
(6, 1000, [21, 31]),
|
||||
(8, 1000, [31, 41]),
|
||||
(110, 1000, [41, 51]),
|
||||
(120, 1000, [51, 61]),
|
||||
(130, 1000, [61, 71]),
|
||||
]
|
||||
|
||||
nomlist, winners = phragmms(votelist, 4, 0)
|
||||
self.assertEqual(winners[0].validator_id, 11)
|
||||
self.assertEqual(winners[0].backed_stake, 3000)
|
||||
|
||||
self.assertEqual(winners[1].validator_id, 31)
|
||||
self.assertEqual(winners[1].backed_stake, 2000)
|
||||
|
||||
self.assertEqual(winners[2].validator_id, 51)
|
||||
self.assertEqual(winners[2].backed_stake, 1500)
|
||||
|
||||
self.assertEqual(winners[3].validator_id, 61)
|
||||
self.assertEqual(winners[3].backed_stake, 1500)
|
||||
|
||||
|
||||
|
||||
class ElectionTest(unittest.TestCase):
|
||||
def test_phragmen(self):
|
||||
votelist = [
|
||||
("A", 10.0, ["X", "Y"]),
|
||||
("B", 20.0, ["X", "Z"]),
|
||||
("C", 30.0, ["Y", "Z"]),
|
||||
]
|
||||
nomlist, elected_candidates = seq_phragmen(votelist, 2)
|
||||
self.assertEqual(elected_candidates[0].validator_id, "Z")
|
||||
self.assertAlmostEqual(elected_candidates[0].score, 0.02)
|
||||
self.assertEqual(elected_candidates[1].validator_id, "Y")
|
||||
self.assertAlmostEqual(elected_candidates[1].score, 0.04)
|
||||
|
||||
def test_approval(self):
|
||||
votelist = [
|
||||
("A", 10.0, ["X", "Y"]),
|
||||
("B", 20.0, ["X", "Z"]),
|
||||
("C", 30.0, ["Y", "Z"]),
|
||||
]
|
||||
nomlist, elected_candidates = approval_voting(votelist, 2)
|
||||
self.assertEqual(elected_candidates[0].validator_id, "Z")
|
||||
self.assertAlmostEqual(elected_candidates[0].approval_stake, 50.0)
|
||||
self.assertEqual(elected_candidates[1].validator_id, "Y")
|
||||
self.assertAlmostEqual(elected_candidates[1].approval_stake, 40.0)
|
||||
|
||||
|
||||
def main():
|
||||
# example1()
|
||||
example2()
|
||||
# example3()
|
||||
@@ -0,0 +1,92 @@
|
||||
import npos
|
||||
import pprint
|
||||
from substrateinterface import SubstrateInterface
|
||||
from substrateinterface.utils.ss58 import ss58_decode, ss58_encode
|
||||
|
||||
|
||||
pp = pprint.PrettyPrinter(indent=4)
|
||||
|
||||
substrate = SubstrateInterface(
|
||||
url="ws://localhost:9944",
|
||||
address_type=0,
|
||||
type_registry={'types': {
|
||||
"StakingLedger<AccountId, BalanceOf>": {
|
||||
"type": "struct",
|
||||
"type_mapping": [
|
||||
["stash", "AccountId"],
|
||||
["total", "Compact<Balance>"],
|
||||
["active", "Compact<Balance>"],
|
||||
["unlocking", "Vec<UnlockChunk<Balance>>"],
|
||||
["claimedReward", "Vec<EraIndex>"]
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
type_registry_preset='polkadot',
|
||||
)
|
||||
|
||||
head = substrate.get_chain_finalised_head()
|
||||
|
||||
|
||||
def get_candidates():
|
||||
prefix = substrate.generate_storage_hash("Staking", "Validators")
|
||||
pairs = substrate.rpc_request(method="state_getPairs", params=[prefix, head])['result']
|
||||
last_32_bytes = list(map(lambda p: "0x" + p[0][-64:], pairs))
|
||||
return list(map(lambda k: ss58_encode(k), last_32_bytes))
|
||||
|
||||
|
||||
def get_nominators():
|
||||
prefix = substrate.generate_storage_hash("Staking", "Nominators")
|
||||
pairs = substrate.rpc_request(method="state_getPairs", params=[prefix, head])['result']
|
||||
|
||||
nominators = list(
|
||||
map(lambda p: ("0x" + p[0][-64:], substrate.decode_scale("Nominations<AccountId>", p[1])['targets']), pairs)
|
||||
)
|
||||
|
||||
nominators = list(map(lambda x: (
|
||||
ss58_encode(x[0], substrate.address_type),
|
||||
x[1],
|
||||
), nominators))
|
||||
|
||||
return list(map(lambda x: (
|
||||
x[0],
|
||||
get_backing_stake_of(x[0]),
|
||||
[ss58_encode(acc, substrate.address_type) for acc in x[1]],
|
||||
), nominators))
|
||||
|
||||
|
||||
def get_backing_stake_of(who):
|
||||
ctrl = substrate.get_runtime_state(
|
||||
module="Staking",
|
||||
storage_function="Bonded",
|
||||
params=[who],
|
||||
block_hash=head,
|
||||
)['result']
|
||||
|
||||
ctrl = ss58_encode(ctrl, substrate.address_type)
|
||||
|
||||
ledger = substrate.get_runtime_state(
|
||||
module="Staking",
|
||||
storage_function="Ledger",
|
||||
params=[ctrl],
|
||||
block_hash=head,
|
||||
)['result']
|
||||
|
||||
return ledger['active']
|
||||
|
||||
|
||||
def validator_count():
|
||||
return substrate.get_runtime_state("Staking", "ValidatorCount", [], head)['result']
|
||||
|
||||
|
||||
candidates = get_candidates()
|
||||
nominators = get_nominators()
|
||||
to_elect = validator_count()
|
||||
|
||||
print("{} validators, {} nominators, electing {}".format(len(candidates), len(nominators), to_elect))
|
||||
distribution, winners = npos.phragmms(nominators, to_elect)
|
||||
npos.print_list(winners)
|
||||
print(sum([c.backed_stake for c in winners]))
|
||||
print(min([c.backed_stake for c in winners]))
|
||||
score = [min([c.backed_stake for c in winners]), sum([c.backed_stake for c in winners])]
|
||||
print("score = [{:,}, {:,}]".format(int(score[0]), int(score[1])))
|
||||
@@ -0,0 +1,2 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
@@ -1,329 +0,0 @@
|
||||
#from itertools import count
|
||||
class edge:
|
||||
def __init__(self,nomid,valiid):
|
||||
self.nomid=nomid
|
||||
self.valiid=valiid
|
||||
#self.validator
|
||||
self.load=0
|
||||
self.weight=0
|
||||
|
||||
class nominator:
|
||||
def __init__(self,votetuple):
|
||||
self.nomid=votetuple[0]
|
||||
self.budget=votetuple[1]
|
||||
self.edges=[edge(self.nomid,valiid) for valiid in votetuple[2]]
|
||||
self.load=0
|
||||
|
||||
class candidate:
|
||||
def __init__(self,valiid,valindex):
|
||||
self.valiid = valiid
|
||||
self.valindex=valindex
|
||||
self.approvalstake=0
|
||||
self.elected=False
|
||||
self.backedstake=0
|
||||
self.score=0
|
||||
self.scoredenom=0
|
||||
|
||||
def setuplists(votelist):
|
||||
#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) for votetuple in votelist]
|
||||
candidatedict=dict()
|
||||
candidatearray=list()
|
||||
numcandidates=0
|
||||
#Get an array of candidates. ]#We could reference these by index
|
||||
#rather than pointer
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
valiid = edge.valiid
|
||||
if valiid in candidatedict:
|
||||
edge.candidate=candidatearray[candidatedict[valiid]]
|
||||
else:
|
||||
candidatedict[valiid]=numcandidates
|
||||
newcandidate=candidate(valiid,numcandidates)
|
||||
candidatearray.append(newcandidate)
|
||||
edge.candidate=newcandidate
|
||||
numcandidates += 1
|
||||
return(nomlist,candidatearray)
|
||||
|
||||
|
||||
def seqPhragmén(votelist,numtoelect):
|
||||
nomlist,candidates=setuplists(votelist)
|
||||
#Compute the total possible stake for each candidate
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
edge.candidate.approvalstake += nom.budget
|
||||
|
||||
electedcandidates=list()
|
||||
for round in range(numtoelect):
|
||||
for candidate in candidates:
|
||||
if not candidate.elected:
|
||||
candidate.score=1/candidate.approvalstake
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
if not edge.candidate.elected:
|
||||
edge.candidate.score +=nom.budget * nom.load / edge.candidate.approvalstake
|
||||
bestcandidate=0
|
||||
bestscore = 1000 #should be infinite but I'm lazy
|
||||
for candidate in candidates:
|
||||
if not candidate.elected and candidate.score < bestscore:
|
||||
bestscore=candidate.score
|
||||
bestcandidate=candidate.valindex
|
||||
electedcandidate=candidates[bestcandidate]
|
||||
electedcandidate.elected=True
|
||||
electedcandidate.electedpos=round
|
||||
electedcandidates.append(electedcandidate)
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
if edge.candidate.valindex == bestcandidate:
|
||||
edge.load=electedcandidate.score - nom.load
|
||||
nom.load=electedcandidate.score
|
||||
|
||||
for candidate in electedcandidates:
|
||||
candidate.backedstake=0
|
||||
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
if nom.load > 0.0:
|
||||
edge.weight = nom.budget * edge.load/nom.load
|
||||
edge.candidate.backedstake += edge.weight
|
||||
else:
|
||||
edge.weight = 0
|
||||
return (nomlist,electedcandidates)
|
||||
|
||||
def calculateMaxScoreNoCutoff(nomlist,candidates):
|
||||
# First we compute the denominator of the score
|
||||
for candidate in candidates:
|
||||
if not candidate.elected:
|
||||
candidate.scoredenom=1.0
|
||||
for nom in nomlist:
|
||||
denominatorcontrib = 0
|
||||
for edge in nom.edges:
|
||||
if edge.candidate.elected:
|
||||
denominatorcontrib += edge.weight/edge.candidate.backedstake
|
||||
# print(nom.nomid, denominatorcontrib)
|
||||
for edge in nom.edges:
|
||||
if not edge.candidate.elected:
|
||||
edge.candidate.scoredenom += denominatorcontrib
|
||||
# print(edge.candidate.valiid, nom.nomid, denominatorcontrib, edge.candidate.scoredenom)
|
||||
# Then we divide. Not that score here is comparable to the recipricol of the score in seqPhragmen.
|
||||
# In particular there low scores are good whereas here high scores are good.
|
||||
bestcandidate=0
|
||||
bestscore = 0.0
|
||||
for candidate in candidates:
|
||||
# print(candidate.valiid, candidate.approvalstake, candidate.scoredenom)
|
||||
if candidate.approvalstake > 0.0:
|
||||
candidate.score = candidate.approvalstake/candidate.scoredenom
|
||||
if not candidate.elected and candidate.score > bestscore:
|
||||
bestscore=candidate.score
|
||||
bestcandidate=candidate
|
||||
else:
|
||||
candidate.score=0.0
|
||||
# print(len(candidates), bestcandidate, bestscore)
|
||||
return (bestcandidate,bestscore)
|
||||
|
||||
def electWithScore(nomlist, electedcandidate, cutoff):
|
||||
for nom in nomlist:
|
||||
for newedge in nom.edges:
|
||||
if newedge.valiid == electedcandidate.valiid:
|
||||
usedbudget = sum([edge.weight for edge in nom.edges])
|
||||
newedge.weight = nom.budget-usedbudget
|
||||
electedcandidate.backedstake += nom.budget-usedbudget
|
||||
for edge in nom.edges:
|
||||
if edge.valiid != electedcandidate.valiid and edge.weight > 0.0:
|
||||
if edge.candidate.backedstake > cutoff:
|
||||
staketotake = edge.weight * cutoff / edge.candidate.backedstake
|
||||
newedge.weight += staketotake
|
||||
edge.weight -= staketotake
|
||||
edge.candidate.backedstake -= staketotake
|
||||
electedcandidate.backedstake += staketotake
|
||||
|
||||
|
||||
def approvalvoting(votelist,numtoelect):
|
||||
nomlist,candidates=setuplists(votelist)
|
||||
#Compute the total possible stake for each candidate
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
edge.candidate.approvalstake += nom.budget
|
||||
edge.weight = nom.budget/min(len(nom.edges),numtoelect)
|
||||
edge.candidate.backedstake += edge.weight
|
||||
candidates.sort( key = lambda x : x.approvalstake, reverse=True)
|
||||
electedcandidates=candidates[0:numtoelect]
|
||||
return nomlist,electedcandidates
|
||||
|
||||
def printresult(nomlist,electedcandidates):
|
||||
for candidate in electedcandidates:
|
||||
print(candidate.valiid," is elected with stake ",candidate.backedstake, "and score ",candidate.score)
|
||||
print()
|
||||
for nom in nomlist:
|
||||
print(nom.nomid," has load ",nom.load, "and supported ")
|
||||
for edge in nom.edges:
|
||||
print(edge.valiid," with stake ",edge.weight, end=" ")
|
||||
print()
|
||||
|
||||
def equalise(nom, tolerance):
|
||||
# Attempts to redistribute the nominators budget between elected validators
|
||||
# Assumes that all elected validators have backedstake set correctly
|
||||
# returns the max difference in stakes between sup
|
||||
|
||||
electededges=[edge for edge in nom.edges if edge.candidate.elected]
|
||||
if len(electededges)==0:
|
||||
return 0.0
|
||||
stakeused = sum([edge.weight for edge in electededges])
|
||||
backedstakes=[edge.candidate.backedstake for edge in electededges]
|
||||
backingbackedstakes=[edge.candidate.backedstake for edge in electededges if edge.weight > 0.0]
|
||||
if len(backingbackedstakes) > 0:
|
||||
difference = max(backingbackedstakes)-min(backedstakes)
|
||||
difference += nom.budget-stakeused
|
||||
if difference < tolerance:
|
||||
return difference
|
||||
else:
|
||||
difference = nom.budget
|
||||
#remove all backing
|
||||
for edge in nom.edges:
|
||||
edge.candidate.backedstake -= edge.weight
|
||||
edge.weight=0
|
||||
electededges.sort(key=lambda x: x.candidate.backedstake)
|
||||
cumulativebackedstake=0
|
||||
lastcandidateindex=len(electededges)-1
|
||||
for i in range(len(electededges)):
|
||||
backedstake=electededges[i].candidate.backedstake
|
||||
#print(nom.nomid,electededges[i].valiid,backedstake,cumulativebackedstake,i)
|
||||
if backedstake * i - cumulativebackedstake > nom.budget:
|
||||
lastcandidateindex=i-1
|
||||
break
|
||||
cumulativebackedstake +=backedstake
|
||||
laststake=electededges[lastcandidateindex].candidate.backedstake
|
||||
waystosplit=lastcandidateindex+1
|
||||
excess = nom.budget + cumulativebackedstake - laststake*waystosplit
|
||||
for edge in electededges[0:waystosplit]:
|
||||
edge.weight = excess / waystosplit + laststake - edge.candidate.backedstake
|
||||
edge.candidate.backedstake += edge.weight
|
||||
return difference
|
||||
|
||||
import random
|
||||
def equaliseall(nomlist,maxiterations,tolerance):
|
||||
for i in range(maxiterations):
|
||||
for j in range(len(nomlist)):
|
||||
nom=random.choice(nomlist)
|
||||
equalise(nom,tolerance/10)
|
||||
maxdifference=0
|
||||
for nom in nomlist:
|
||||
difference=equalise(nom,tolerance/10)
|
||||
maxdifference=max(difference,maxdifference)
|
||||
if maxdifference < tolerance:
|
||||
return
|
||||
|
||||
def seqPhragménwithpostprocessing(votelist,numtoelect):
|
||||
nomlist,electedcandidates = seqPhragmén(votelist,numtoelect)
|
||||
equaliseall(nomlist,2,0.1)
|
||||
return nomlist,electedcandidates
|
||||
|
||||
def factor3point15(votelist, numtoelect,tolerance=0.1):
|
||||
nomlist,candidates=setuplists(votelist)
|
||||
# Compute the total possible stake for each candidate
|
||||
for nom in nomlist:
|
||||
for edge in nom.edges:
|
||||
edge.candidate.approvalstake += nom.budget
|
||||
|
||||
electedcandidates=list()
|
||||
for round in range(numtoelect):
|
||||
electedcandidate,score=calculateMaxScoreNoCutoff(nomlist,candidates)
|
||||
electWithScore(nomlist, electedcandidate, score)
|
||||
electedcandidate.elected=True
|
||||
electedcandidates.append(electedcandidate)
|
||||
electedcandidate.electedpos=round
|
||||
equaliseall(nomlist,100,tolerance)
|
||||
return nomlist,electedcandidates
|
||||
|
||||
def example1():
|
||||
votelist=[("A",10.0,["X","Y"]),("B",20.0,["X","Z"]),("C",30.0,["Y","Z"])]
|
||||
print("Votes ",votelist)
|
||||
nomlist, electedcandidates = seqPhragmén(votelist,2)
|
||||
print("Sequential Phragmén gives")
|
||||
printresult(nomlist, electedcandidates)
|
||||
nomlist, electedcandidates = approvalvoting(votelist,2)
|
||||
print()
|
||||
print("Approval voting gives")
|
||||
printresult(nomlist, electedcandidates)
|
||||
nomlist, electedcandidates = seqPhragménwithpostprocessing(votelist,2)
|
||||
print("Sequential Phragmén with post processing gives")
|
||||
printresult(nomlist, electedcandidates)
|
||||
nomlist, electedcandidates = factor3point15(votelist,2)
|
||||
print("Factor 3.15 thing gives")
|
||||
printresult(nomlist, electedcandidates)
|
||||
|
||||
def example2():
|
||||
votelist = [
|
||||
("10", 1000, ["10"]),
|
||||
("20", 1000, ["20"]),
|
||||
("30", 1000, ["30"]),
|
||||
("40", 1000, ["40"]),
|
||||
('2', 500, ['10', '20', '30']),
|
||||
('4', 500, ['10', '20', '40'])
|
||||
]
|
||||
print("Votes ",votelist)
|
||||
nomlist, electedcandidates = seqPhragmén(votelist,2)
|
||||
print("Sequential Phragmén gives")
|
||||
printresult(nomlist, electedcandidates)
|
||||
nomlist, electedcandidates = approvalvoting(votelist,2)
|
||||
print()
|
||||
print("Approval voting gives")
|
||||
printresult(nomlist, electedcandidates)
|
||||
nomlist, electedcandidates = seqPhragménwithpostprocessing(votelist,2)
|
||||
print("Sequential Phragmén with post processing gives")
|
||||
printresult(nomlist, electedcandidates)
|
||||
nomlist, electedcandidates = factor3point15(votelist,2)
|
||||
print("Factor 3.15 thing gives")
|
||||
printresult(nomlist, electedcandidates)
|
||||
|
||||
import unittest
|
||||
class electiontests(unittest.TestCase):
|
||||
def testexample1Phragmén(self):
|
||||
votelist=[("A",10.0,["X","Y"]),("B",20.0,["X","Z"]),("C",30.0,["Y","Z"])]
|
||||
nomlist, electedcandidates = seqPhragmén(votelist,2)
|
||||
self.assertEqual(electedcandidates[0].valiid,"Z")
|
||||
self.assertAlmostEqual(electedcandidates[0].score,0.02)
|
||||
self.assertEqual(electedcandidates[1].valiid,"Y")
|
||||
self.assertAlmostEqual(electedcandidates[1].score,0.04)
|
||||
def testexample1approval(self):
|
||||
votelist=[("A",10.0,["X","Y"]),("B",20.0,["X","Z"]),("C",30.0,["Y","Z"])]
|
||||
nomlist, electedcandidates = approvalvoting(votelist,2)
|
||||
self.assertEqual(electedcandidates[0].valiid,"Z")
|
||||
self.assertAlmostEqual(electedcandidates[0].approvalstake,50.0)
|
||||
self.assertEqual(electedcandidates[1].valiid,"Y")
|
||||
self.assertAlmostEqual(electedcandidates[1].approvalstake,40.0)
|
||||
def dotests():
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user