Got a message that my witness votes were about to expire on peakd, and to be fair I never looked that much into witnesses and what I'm actually voting for, so this time I thought, why not look into it a bit more, so I got the list with a litlte curl command:
curl -s --data '{"jsonrpc":"2.0", "method":"condenser_api.get_witnesses_by_vote", "params":[null, 500], "id":1}' https://api.hive.blog|json_pp > witnesses.json
Looking into the info I noticed 6 fields that seemed relevant:
- hardfork_version_vote : What hardforh did the witness vote for last
- hbd_exchange_rate.base : What HBD.HIVE exchange rate was the last one in their price feed
- last_hbd_exchange_update : When did they last pulish their price feed
- props.account_creation_fee : What account creation fee did they vote for
- props.hbd_interest_rate : What HBD interest rate did they vote for
- props.maximum_block_size : What maximum block size did they vote for
After some thinking, my thoughts were as follows:
- Newer HF is better, but not too old is forgivable
- Close to accurate price feed is a mild plus, wrong feed is worse than no feed
- More recent pricefeed is better
- Account creation shouldn't be too cheap or expensive, 5..15 seems like the sweetspot
- HBD interest rate shouldn't be too low, but too high is worse, 3% to 7% seems like the sweet spot, this weighs more than most other factors
- A higher maximum blocksize seems like a good idea, but not weighing it too much.
From this I cam to the following script:
#!/usr/bin/env python3
import json
import datetime
# Hardfork vote, prefer newer
HFSCORE = {
"1.28.0": 100,
"1.27.0": 70,
"1.26.0": 0,
"1.25.0": -300,
"default": -1000,
"nondef": -1000
}
# Current HBD to HIVE exchange rates, valid is best, not defined better than wrong.
EXCHSCORE = {
"0.059": 30,
"0.060": 30,
"0.061": 30,
"default": -200,
"nondef": 0
}
# Account creation fee, between 5 and 16 HIVE is best, below 3 and 49 HIVE OK
CFEETABLE = [
[3.0, 5.0, 20],
[5.0, 16.0, 50],
[16.0, 49.0, 20]]
# HBD interest rate, 3%..7% seems like the sweet spot, above 18 is very bad
IRTABLE = [
[0, 300, 500],
[300, 700, 3000],
[700, 1100, 1800],
[1100, 1400, 500],
[1400, 1800, 0],
[1800, 2200, -1000]]
# Slight preference for larger block size.
MBTABLE = [
[30000,100000,0],
[100000,150000, 30]]
def getscore(val, table):
if val is None:
return table["nondef"]
rval = table.get(val,None)
if rval is None:
rval = table["default"]
return rval
def range_score(val, ranges, default, nondef):
if val is None:
return nondef
for onerange in ranges:
if val >= onerange[0] and val <= onerange[1]:
return onerange[2]
return default
def getval(key, obj):
if "." in key:
first, more = key.split(".",1)
if first in obj:
return getval(more, obj[first])
return None
if key in obj:
return obj[key]
return None
def numeric(val):
if val is None:
return val
return float(val.split(" ")[0])
def hbd_update_age_score(witn):
if "last_hbd_exchange_update" in witn:
dt = (datetime.datetime.now() - datetime.datetime.fromisoformat(witn["last_hbd_exchange_update"])).total_seconds()
if dt < 3600*2:
return 40
if dt < 3600*12:
return 20
if dt < 3600*24*2:
return 10
if dt < 3600*24*7:
return 0
return -300
with open("witnesses.json") as f:
witnesses = json.load(f)
for witness in witnesses["result"]:
score = 0
score += getscore(getval("hardfork_version_vote", witness), HFSCORE)
score += getscore(getval("hbd_exchange_rate.base", witness), EXCHSCORE)
score += range_score(numeric(getval("props.account_creation_fee", witness)),CFEETABLE, 0, 0)
score += range_score(getval("props.hbd_interest_rate", witness),IRTABLE, -4000, -4000)
score += range_score(getval("props.maximum_block_size", witness),MBTABLE, -30, -30)
score += hbd_update_age_score(witness)
if score > 1500:
print(score, "@" + witness["owner"])
The result:
- 1740
- 1740
- 1740
- 2940
- 1740
- 2940
- 1740
- 1740
- 1740
- 2940
- 1740
- 2940
- 2940
- 2590
- 1740
- 1740
- 1740
- 1740
- 2940
- 1740
- 1740
- 1740
- 2940
- 1740
- 1720
- 1740
- 2600
- 2620
- 2220
- 1550
This looked perfect, 30 witnesses to vote for, but a few of these were actually disabled, so my script aparently is incomplete. I didn't bother to fix it for now and just voted for a few extra top 20 witnesses.
For now I'm not doing anything more with this, my plate is quite full with development projects and I don't ahve time to add anything new, but I feel that a little service for finding witnesses whose voting behaviour we most agree with weighted against QOS metrics for their node would be something interesting to look at some day. I kinda wish I had time for that, but right now the InnuenDo stack has my full spare-time attention.
Update
With the feedback I got from you guys I updated my script so I can run it once a month. Feel free to copy and adjust, expand and use. I get it is limited because witnesses do more than just run a node and vote on things. When I have time again I'll see if I can fit something more in.
For now I have this script:
#!/usr/bin/env python3
import json
import datetime
OLDVOTES = set([
"gtg",
"stoodkev",
"roelandp",
"threespeak",
"guiltyparties",
"abit",
"howo",
"actifit",
"quochuy",
"oflyhigh",
"techcoderx",
"rishi556",
"neoxian",
"brianoflondon",
"vsc.network",
"hextech",
"aliento",
"sagarkothari88",
"mengao",
"c0ff33a",
"holoz0r",
"liotes",
"thebbhproject",
"stresskiller",
"innerhive",
"condeas",
"hispapro",
"whiterosecoffee",
"borislavzlatanov",
"sn0n"])
HFSCORE = {
"1.28.0": 100,
"default": -30000,
"nondef": -30000
}
EXCHSCORET20 = {
"0.059": 500,
"0.060": 500,
"0.061": 500,
"default": -500,
"nondef": -500
}
EXCHSCORE = {
"0.059": 50,
"0.060": 50,
"0.061": 50,
"default": -50,
"nondef": 0
}
CFEETABLE = [
[3.0, 5.0, 20],
[5.0, 16.0, 50],
[16.0, 49.0, 20]]
IRTABLE = [
[0, 300, 100],
[300, 700, 3000],
[700, 1100, 1800],
[1100, 1400, 500],
[1400, 1800, 0],
[1800, 2200, -1000]]
BAGE = [
[0,4,0],
[4,24,-600],
[24, 24*5, -1800],
[24*5, 24*14, -4000]]
MBTABLE = [
[30000,100000,0],
[100000,150000, 30]]
def getscore(val, table):
if val is None:
return table["nondef"]
rval = table.get(val,None)
if rval is None:
rval = table["default"]
return rval
def range_score(val, ranges, default, nondef):
if val is None:
return nondef
for onerange in ranges:
if val >= onerange[0] and val <= onerange[1]:
return onerange[2]
return default
def getval(key, obj):
if "." in key:
first, more = key.split(".",1)
if first in obj:
return getval(more, obj[first])
return None
if key in obj:
return obj[key]
return None
def numeric(val):
if val is None:
return val
return float(val.split(" ")[0])
def hbd_update_age_score(witn):
if "last_hbd_exchange_update" in witn:
dt = (datetime.datetime.now() - datetime.datetime.fromisoformat(witn["last_hbd_exchange_update"])).total_seconds()
if dt < 3600*2:
return 40
if dt < 3600*12:
return 20
if dt < 3600*24*2:
return 10
if dt < 3600*24*7:
return 0
return -300
with open("witnesses.json") as f:
last_block_num = -1
witnesses = json.load(f)
for witness in witnesses["result"]:
lcb = witness.get("last_confirmed_block_num", -1)
if lcb > last_block_num:
last_block_num = lcb
wno = 0
valid_witnesses = []
for witness in witnesses["result"]:
wno += 1
top21 = True
if wno > 21:
top21 = False
behind = (last_block_num - witness.get("last_confirmed_block_num", -1)) // 1200
score = 0
score += getscore(getval("hardfork_version_vote", witness), HFSCORE)
if top21:
score += getscore(getval("hbd_exchange_rate.base", witness), EXCHSCORET20)
else:
score += getscore(getval("hbd_exchange_rate.base", witness), EXCHSCORE)
score += range_score(numeric(getval("props.account_creation_fee", witness)),CFEETABLE, 0, 0)
score += range_score(getval("props.hbd_interest_rate", witness),IRTABLE, -4000, -4000)
score += range_score(getval("props.maximum_block_size", witness),MBTABLE, -30, -30)
score += hbd_update_age_score(witness)
score += range_score(behind, BAGE, -30000, -30000)
if score > 0:
valid_witnesses.append([
str(score),
"@" + witness["owner"],
str(behind),
str(getval("props.hbd_interest_rate", witness)),
str(getval("props.maximum_block_size", witness)),
getval("props.account_creation_fee", witness),
getval("hbd_exchange_rate.base", witness),
str(top21),
str(witness["owner"] in OLDVOTES)])
valid_witnesses.sort(key=lambda x: int(x[0]), reverse=True)
print("| match | score | witness | hours behind | apr | max-bs | creation-fee | exchange-rate | top 21 | oldvote |")
print("| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |")
linecount = 0
lastscore = -1
for line in valid_witnesses:
linecount += 1
score = line[0]
if linecount < 31 or score == lastscore or line[-1] == "True":
print("|",linecount , "|", " | ".join(line),"|")
if linecount < 31:
lastscore = score
If I run it right now it gives the following output:
If I run it again next month it might give me slightly different results.