Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions doc/ratings-and-trust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Ratings and trust

Rein has two systems in place to help users decide with whom to work with and who to avoid.

## Ratings

The first and more simple of the two systems is a five-star rating system. After a job is completed, either through acceptance of the delivery by the client or through successful mediation, all involved users (client, mediator and worker) are given the option to rate the other two participants on a scale from 0 to 5. If a user so desires, he can also leave a comment regarding the other person's performance.

Across the application, these simple five star ratings are used as an indicator of performance by averaging all ratings a user has received and providing the total number of ratings for reference. A user's ratings, comments on his performance and who he has been rated by can be viewed in more detail on their ratings page.

## Trust score

A slightly more sophisticated system allows the user to calculate trust scores for other users on their ratings page. This can be done either automatically, if the user has enabled the setting on his settings page, or manually by clicking a button on an individual ratings page.

The trust score system is based on the idea that you usually don't trust someone you have no connection to. How would you know that their ratings aren't fake? Usually, you seek out clients, workers or mediators based on someone you do know vouching for them.

If you're interested in the details, check out https://wiki.bitcoin-otc.com/wiki/OTC_Rating_System#Notes_about_gettrust as Rein's implementation is based on that very idea.
48 changes: 39 additions & 9 deletions rein/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from .lib.script import build_2_of_3, build_mandatory_multisig, check_redeem_scripts
from .lib.localization import init_localization
from .lib.transaction import partial_spend_p2sh, spend_p2sh, spend_p2sh_mediator, partial_spend_p2sh_mediator, partial_spend_p2sh_mediator_2
from .lib.rating import add_rating, get_user_jobs, get_average_user_rating, get_average_user_rating_display, get_all_user_ratings
from .lib.rating import add_rating, get_user_jobs, get_average_user_rating, get_average_user_rating_display, get_all_user_ratings, calculate_trust_score

# Import config
import rein.lib.config as config
Expand Down Expand Up @@ -1202,16 +1202,12 @@ def status(multi, identity, jobid):
else:
click.echo("Job id not found")


@cli.command()
@click.argument('key', required=True)
@click.argument('value', required=True)
def config(key, value):
def config_common(key, value):
"""
Set configuration variable. Parses true/false, on/off, and passes
anything else unaltered to the db.
"""
keys = ['testnet', 'tor', 'debug', 'fee']
keys = ['testnet', 'tor', 'debug', 'fee', 'trust_score']
if key not in keys:
click.echo("Invalid config setting. Try one of " + ', '.join(keys))
return
Expand All @@ -1223,6 +1219,17 @@ def config(key, value):
else:
PersistConfig.set(rein, key, value)

@cli.command()
@click.argument('key', required=True)
@click.argument('value', required=True)
def config(key, value):
"""
Set configuration variable. Parses true/false, on/off, and passes
anything else unaltered to the db.
"""

config_common(key, value)


# leave specific config commands in for backwards compatibility, remove in 0.4
@cli.command()
Expand Down Expand Up @@ -1537,8 +1544,9 @@ def rate_web():

@app.route('/ratings/<msin>', methods=['GET'])
def view_ratings(msin):
display_trust_score = PersistConfig.get(rein, 'trust_score', False)
ratings = get_all_user_ratings(log, url, user, rein, msin)
return render_template("ratings.html", user=user, user_rated=get_user_name(log, url, user, rein, msin), msin=msin, ratings=ratings)
return render_template("ratings.html", user=user, user_rated=get_user_name(log, url, user, rein, msin), msin=msin, ratings=ratings, display_trust_score=display_trust_score)

@app.route('/hide', methods=['POST'])
def hide():
Expand Down Expand Up @@ -1576,6 +1584,25 @@ def unhide():
except:
return 'false'

@app.route('/config', methods=['POST'])
def config_web():
"""Allows for changes to the user's config via the web interface"""

try:
key = request.json['key']
value = request.json['value']
config_common(key, value)

except:
return 'false'

return 'true'

@app.route('/trust_score/<dest_msin>', defaults={'source_msin': user.msin})
@app.route('/trust_score/<dest_msin>/<source_msin>', methods=['GET'])
def trust_score(dest_msin, source_msin):
return calculate_trust_score(dest_msin, source_msin, rein)

@app.route('/settings', methods=['GET'])
def settings():
"""Allows for local customization of the web app."""
Expand All @@ -1592,7 +1619,10 @@ def settings():
for hidden_mediator in hidden_mediators:
hidden_mediator['unhide_button'] = HiddenContent.unhide_button('mediator', hidden_mediator['content_identifier'])

return render_template('settings.html', user=user, hidden_jobs=hidden_jobs, hidden_bids=hidden_bids, hidden_mediators=hidden_mediators)
fee = float(PersistConfig.get(rein, 'fee', 0.001))
trust_score = PersistConfig.get(rein, 'trust_score', False)

return render_template('settings.html', user=user, hidden_jobs=hidden_jobs, hidden_bids=hidden_bids, hidden_mediators=hidden_mediators, fee=fee, trust_score=trust_score)

@app.route("/post", methods=['POST', 'GET'])
def job_post():
Expand Down
3 changes: 3 additions & 0 deletions rein/html/css/style_v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,9 @@ border-radius: 50%;}
}
.thumbnail {margin-bottom:0;}
.control-group {margin-bottom:30px;}
hr {
border-top: 1px solid #000;
}
@media (min-width: 768px) {
#sidebar-left.col-sm-2 {
opacity: 1;
Expand Down
1 change: 1 addition & 0 deletions rein/html/js/rate.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function nextJob() {
document.addEventListener("DOMContentLoaded", function(event) {
// Initialize contents of the job and user id fields and their labels
$(".ratingdiv").raty();
$(".ratingdiv").raty({score: 1});
$(".ratingdiv").click(function() {
$("input[name='rating']").val($(".ratingdiv").raty('score'));
});
Expand Down
39 changes: 39 additions & 0 deletions rein/html/js/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
function postError(data) {
if (data != 'true') {
alert('Your desired setting could not be saved.')
} else {
alert('Setting saved successfully!')
}
}

function setFee() {
fee = $('#feeInput').val();
$.ajax({
method: "POST",
url: "/config",
contentType: "application/json",
data: JSON.stringify({
'key': 'fee',
'value': fee
}),
success: function(data) {
postError(data);
}
})
}

function setTrustScore() {
trustScoreEnabled = $('#trustScore').is(':checked');
$.ajax({
method: "POST",
url: "/config",
contentType: "application/json",
data: JSON.stringify({
'key': 'trust_score',
'value': trustScoreEnabled.toString()
}),
success: function(data) {
postError(data);
}
})
}
22 changes: 22 additions & 0 deletions rein/html/js/trustScore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function setTrustScore(msin, displayId) {
trustScore = getTrustScore(msin);
$('#' + displayId).html(trustScore);
}

function getTrustScore(msin) {
result = 'Trust score could not be calculated';
$.ajax({
method: "GET",
url: "/trust_score/" + msin,
contentType: "application/json",
async: false,
success: function(data) {
data = JSON.parse(data);
result = 'No trust links between you and the user were found. A trust score could not be calculated.'
if (data['links'] != 0) {
result = 'Trust score: ' + data['score'] + ', Trust links: ' + data['links'];
}
}
})
return result;
}
16 changes: 15 additions & 1 deletion rein/html/ratings.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@
{% from "_form_helpers.html" import render_error %}
{% block body %}

<script src="/js/trustScore.js"></script>

<br>
<div class="well">
This page displays ratings received by <b>{{ user_rated }}</b> (msin: {{ msin }}).
<p>This page displays ratings received by <b>{{ user_rated }}</b> (msin: {{ msin }}).</p>
<p><div id="trustScoreDisplay"></div></p>

{% if display_trust_score %}
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
setTrustScore('{{ msin }}', 'trustScoreDisplay');
});
</script>
{% else %}
<p><button onclick="setTrustScore('{{ msin }}', 'trustScoreDisplay')">Calculate trust score</button></p>
{% endif %}

<p>
<table class="table m-table table-bordered table-hover table-heading">
<thead>
Expand Down
50 changes: 46 additions & 4 deletions rein/html/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
{% from "_form_helpers.html" import render_error %}
{% block body %}

<script src="/js/settings.js"></script>

<br>
<div class="well">
<h2>Settings</h2>

<h4>Hidden content</h4>
<hr>

<h3>Hidden content</h3>
<p>This section lets you view and unhide hidden content.</p>

<h5>Jobs</h5>
<h4>Jobs</h4>
{% if hidden_jobs %}
<table class="table m-table table-bordered table-hover table-heading">
<thead>
Expand All @@ -31,7 +35,7 @@ <h5>Jobs</h5>
<p>No jobs have been hidden.</p>
{% endif %}

<h5>Bids</h5>
<h4>Bids</h4>
{% if hidden_bids %}
<table class="table m-table table-bordered table-hover table-heading">
<thead>
Expand All @@ -53,7 +57,7 @@ <h5>Bids</h5>
<p>No bids have been hidden.</p>
{% endif %}

<h5>Mediators</h5>
<h4>Mediators</h4>
{% if hidden_mediators %}
<table class="table m-table table-bordered table-hover table-heading">
<thead>
Expand All @@ -74,6 +78,44 @@ <h5>Mediators</h5>
{% else %}
<p>No mediators have been hidden.</p>
{% endif %}

<hr>

<h3>Transaction fee</h3>
<p>This section lets you adjust the fee that is used for transactions from escrows to mediators and workers.</p>

<form onsubmit="setFee(); return false;">
<div class="form-group">
<label for="feeInput" class="control-label">Fee (in BTC):</label>
<input type="text" name="feeInput" id="feeInput" pattern="[0-9.]{1,10}" value="{{ fee }}">
</div>
<div class="form-group">
<input type="submit" value="Save setting"/>
</div>
</form>

<hr>

<h3>Trust score</h3>
<p>Would you like to automatically display trust scores for users on their ratings page? If so, check the box below. This may make ratings pages slower to load.</p>

<form onsubmit="setTrustScore(); return false;">
<div class="form-group">
<input type="checkbox" name="trustScore" id="trustScore">
{% if trust_score %}
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
$('#trustScore').prop('checked', true);
});
</script>
{% endif %}
<label for="trustScore" class="control-label">Enable automatic trust score calculations</label>
</div>
<div class="form-group">
<input type="submit" value="Save setting"/>
</div>
</form>

</div>

{% endblock %}
2 changes: 1 addition & 1 deletion rein/lib/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,5 @@ class RatingForm(Form):
job_id = TextField(_('Select job'), validators = [Required()], default='')
user_id = TextField(_('Select user'), validators = [Required(), avoid_self_rating], default='')
rated_by_id = TextField(_('Your SIN'), validators = [Required()], default='')
rating = TextField(_('Rating'), validators=[Required()], default=0, widget=HiddenInput())
rating = TextField(_('Rating'), validators=[Required()], default=1, widget=HiddenInput())
comments = TextAreaField(_('Comments'), validators = [], default='')
72 changes: 72 additions & 0 deletions rein/lib/rating.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,75 @@ def get_all_user_ratings(log, url, user, rein, msin):
)

return ratings

def calculate_trust_score(dest_msin=None, source_msin=None, rein=None, test=False, test_ratings=[]):
"""Calculates the trust score for a user as identified by his msin.
Algorithm based on the level 2 trust system implemented by Bitcoin OTC
and outlined at https://wiki.bitcoin-otc.com/wiki/OTC_Rating_System#Notes_about_gettrust."""

# Grab all ratings commited by source_msin (the client)
ratings_by_source = None
if not test:
ratings_by_source = rein.session.query(Document).filter(and_(
Document.doc_type == 'rating',
Document.contents.like('%\nRater msin: {}%'.format(source_msin))
)).all()

else:
ratings_by_source = [test_rating for test_rating in test_ratings if test_rating['Rater msin'] == 'SourceMsin']

# Compile list of users (by msin) that have been rated by source and their ratings
rated_by_source = []
for rating in ratings_by_source:
rating_dict = None
if not test:
rating_dict = document_to_dict(rating.contents)

else:
rating_dict = rating

msin_rated = rating_dict['User msin']
rating_value = int(rating_dict['Rating'])
if not msin_rated in rated_by_source:
rated_by_source.append((msin_rated, rating_value))

# Determine if any of the users rated by source or source have rated dest
# Add source to the list of users source has vouched for with full trust
vouched_users = rated_by_source + [(source_msin, 5)]
# Calculate trust links between source and dest
trust_links = []
for vouched_user in vouched_users:
(vouched_user_msin, vouched_user_trust) = vouched_user
dest_ratings_by_vouched_user = None
if not test:
dest_ratings_by_vouched_user = rein.session.query(Document).filter(
and_(
Document.doc_type == 'rating',
Document.contents.like('%\nRater msin: {}%'.format(vouched_user_msin)),
Document.contents.like('%\nUser msin: {}%'.format(dest_msin))
)).all()
else:
dest_ratings_by_vouched_user = [test_rating for test_rating in test_ratings if test_rating['Rater msin'] == vouched_user_msin and test_rating['User msin'] == 'DestMsin']

for rating in dest_ratings_by_vouched_user:
link_rating = None
if not test:
link_rating = document_to_dict(rating.contents)

else:
link_rating = rating

trust_values = [
vouched_user_trust,
int(link_rating['Rating'])
]
trust_links.append(min(trust_values))

dest_trust_score = {
'score': float(sum(trust_links)) / float(len(trust_links)),
'links': len(trust_links)
}
if not test:
dest_trust_score = json.dumps(dest_trust_score)

return dest_trust_score
Loading