From 8982ac610fbf197bb38f33bb367e6be3df5171ff Mon Sep 17 00:00:00 2001
From: Ladd Hoffman
Date: Wed, 18 Jan 2023 01:07:10 -0600
Subject: [PATCH] Reputation tokens
---
forum-network/src/classes/availability.js | 21 +-
forum-network/src/classes/bench.js | 49 +----
forum-network/src/classes/business.js | 6 +-
forum-network/src/classes/erc721.js | 2 +
forum-network/src/classes/expert.js | 48 ++--
forum-network/src/classes/forum.js | 27 ++-
.../src/classes/reputation-holder.js | 8 +
forum-network/src/classes/reputation-token.js | 88 +++++++-
forum-network/src/classes/stake.js | 5 +-
forum-network/src/classes/validation-pool.js | 206 ++++++++----------
forum-network/src/tests/availability.html | 62 +++---
forum-network/src/tests/basic.html | 106 ++++-----
forum-network/src/tests/debounce.html | 12 +-
forum-network/src/tests/flowchart.html | 29 ++-
forum-network/src/tests/forum-network.html | 64 +++---
forum-network/src/tests/forum.html | 7 +-
forum-network/src/tests/graph.html | 14 +-
forum-network/src/tests/reputation.html | 32 +++
forum-network/src/tests/validation-pool.html | 72 +++---
19 files changed, 456 insertions(+), 402 deletions(-)
create mode 100644 forum-network/src/classes/reputation-holder.js
create mode 100644 forum-network/src/tests/reputation.html
diff --git a/forum-network/src/classes/availability.js b/forum-network/src/classes/availability.js
index c905a53..2bdf7c8 100644
--- a/forum-network/src/classes/availability.js
+++ b/forum-network/src/classes/availability.js
@@ -2,14 +2,11 @@ import { Action } from './action.js';
import { Actor } from './actor.js';
class Worker {
- stake = 0;
-
- available = true;
-
- assignedRequestId = null;
-
- constructor(reputationPublicKey) {
- this.reputationPublicKey = reputationPublicKey;
+ constructor(tokenId) {
+ this.tokenId = tokenId;
+ this.stakeAmount = 0;
+ this.available = true;
+ this.assignedRequestId = null;
}
}
@@ -28,16 +25,16 @@ export class Availability extends Actor {
};
}
- register(reputationPublicKey, stake, __duration) {
+ register({ stakeAmount, tokenId }) {
// TODO: expire after duration
// ? Is a particular stake amount required?
- const worker = this.workers.get(reputationPublicKey) ?? new Worker(reputationPublicKey);
+ const worker = this.workers.get(tokenId) ?? new Worker(tokenId);
if (!worker.available) {
throw new Error('Worker is already registered and busy. Can not increase stake.');
}
- worker.stake += stake;
+ worker.stakeAmount += stakeAmount;
// TODO: Interact with Bench contract to encumber reputation?
- this.workers.set(reputationPublicKey, worker);
+ this.workers.set(tokenId, worker);
}
// unregister() { }
diff --git a/forum-network/src/classes/bench.js b/forum-network/src/classes/bench.js
index 4f26d08..4abe4de 100644
--- a/forum-network/src/classes/bench.js
+++ b/forum-network/src/classes/bench.js
@@ -1,8 +1,8 @@
import { Actor } from './actor.js';
-import { Reputations } from './reputation.js';
import { ValidationPool } from './validation-pool.js';
import params from '../params.js';
import { Action } from './action.js';
+import { ReputationTokenContract } from './reputation-token.js';
/**
* Purpose: Keep track of reputation holders
@@ -13,7 +13,7 @@ export class Bench extends Actor {
this.forum = forum;
this.validationPools = new Map();
this.voters = new Map();
- this.reputations = new Reputations();
+ this.reputation = new ReputationTokenContract();
this.actions = {
createValidationPool: new Action('create validation pool', scene),
@@ -33,53 +33,22 @@ export class Bench extends Actor {
});
}
- getTotalReputation() {
- return this.reputations.getTotal();
- }
-
- getTotalAvailableReputation() {
- return this.reputations.getTotalAvailable();
- }
-
- getTotalActiveReputation() {
+ getActiveReputation() {
return this.listActiveVoters()
- .map(({ reputationPublicKey }) => this.reputations.getTokens(reputationPublicKey))
+ .map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey))
.reduce((acc, cur) => (acc += cur), 0);
}
- getTotalActiveAvailableReputation() {
+ getActiveAvailableReputation() {
return this.listActiveVoters()
- .map(({ reputationPublicKey }) => this.reputations.getAvailableTokens(reputationPublicKey))
+ .map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey))
.reduce((acc, cur) => (acc += cur), 0);
}
- async initiateValidationPool({
- postId,
- fee,
- duration,
- tokenLossRatio,
- contentiousDebate,
- signingPublicKey,
- authorStake,
- anonymous,
- }) {
+ async initiateValidationPool(poolOptions) {
const validationPoolNumber = this.validationPools.size + 1;
- const validationPool = new ValidationPool(
- this,
- this.forum,
- {
- postId,
- fee,
- duration,
- tokenLossRatio,
- contentiousDebate,
- signingPublicKey,
- authorStake,
- anonymous,
- },
- `Pool${validationPoolNumber}`,
- this.scene,
- );
+ const name = `Pool${validationPoolNumber}`;
+ const validationPool = new ValidationPool(this, this.forum, poolOptions, name, this.scene);
this.validationPools.set(validationPool.id, validationPool);
await this.actions.createValidationPool.log(this, validationPool);
validationPool.activate();
diff --git a/forum-network/src/classes/business.js b/forum-network/src/classes/business.js
index 37753af..dc89574 100644
--- a/forum-network/src/classes/business.js
+++ b/forum-network/src/classes/business.js
@@ -66,9 +66,9 @@ export class Business extends Actor {
fee: request.fee,
duration,
tokenLossRatio,
- signingPublicKey: reputationPublicKey,
- anonymous: false,
- authorStake: this.worker.stake,
+ reputationPublicKey,
+ authorStakeAmount: this.worker.stakeAmount,
+ tokenId: this.worker.tokenId,
});
// When the validation pool concludes,
diff --git a/forum-network/src/classes/erc721.js b/forum-network/src/classes/erc721.js
index 239ea28..60da455 100644
--- a/forum-network/src/classes/erc721.js
+++ b/forum-network/src/classes/erc721.js
@@ -56,6 +56,7 @@ export class ERC721 /* is ERC165 */ {
if (!owner) {
throw new Error('ERC721: invalid token ID');
}
+ return owner;
}
transferFrom(from, to, tokenId) {
@@ -65,6 +66,7 @@ export class ERC721 /* is ERC165 */ {
}
this.incrementBalance(from, -1);
this.incrementBalance(to, 1);
+ this.owners.set(tokenId, to);
}
/// @notice Enable or disable approval for a third party ("operator") to manage
diff --git a/forum-network/src/classes/expert.js b/forum-network/src/classes/expert.js
index 9066269..cf35435 100644
--- a/forum-network/src/classes/expert.js
+++ b/forum-network/src/classes/expert.js
@@ -1,22 +1,22 @@
-import { Actor } from './actor.js';
import { Action } from './action.js';
import { PostMessage } from './message.js';
import { CryptoUtil } from './crypto.js';
+import { ReputationHolder } from './reputation-holder.js';
-export class Expert extends Actor {
+export class Expert extends ReputationHolder {
constructor(name, scene) {
- super(name, scene);
+ super(undefined, name, scene);
this.actions = {
submitPostViaNetwork: new Action('submit post via network', scene),
submitPost: new Action('submit post', scene),
initiateValidationPool: new Action('initiate validation pool', scene),
stake: new Action('stake on post', scene),
- revealIdentity: new Action('reveal identity', scene),
registerAvailability: new Action('register availability', scene),
getAssignedWork: new Action('get assigned work', scene),
submitWork: new Action('submit work evidence', scene),
};
this.validationPools = new Map();
+ this.tokens = [];
}
async initialize() {
@@ -46,60 +46,42 @@ export class Expert extends Actor {
await this.actions.submitPost.log(this, forum);
const postId = await forum.addPost(this.reputationPublicKey, postContent);
const pool = await this.initiateValidationPool(bench, { ...poolOptions, postId, anonymous: false });
+ this.tokens.push(pool.tokenId);
return { postId, pool };
}
async initiateValidationPool(bench, poolOptions) {
// For now, directly call bench.initiateValidationPool();
- if (poolOptions.anonymous) {
- const signingKey = await CryptoUtil.generateAsymmetricKey();
- poolOptions.signingPublicKey = await CryptoUtil.exportKey(signingKey.publicKey);
- } else {
- poolOptions.signingPublicKey = this.reputationPublicKey;
- }
+ poolOptions.reputationPublicKey = this.reputationPublicKey;
await this.actions.initiateValidationPool.log(
this,
bench,
- `(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStake ?? 0})`,
+ `(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`,
);
const pool = await bench.initiateValidationPool(poolOptions);
+ this.tokens.push(pool.tokenId);
this.validationPools.set(pool.id, poolOptions);
return pool;
}
async stake(validationPool, {
- position, amount, lockingTime, anonymous = false,
+ position, amount, lockingTime,
}) {
- let signingPublicKey;
- if (anonymous) {
- const signingKey = await CryptoUtil.generateAsymmetricKey();
- signingPublicKey = await CryptoUtil.exportKey(signingKey.publicKey);
- this.validationPools.set(validationPool.id, { signingPublicKey });
- } else {
- signingPublicKey = this.reputationPublicKey;
- }
// TODO: encrypt stake
// TODO: sign message
await this.actions.stake.log(
this,
validationPool,
- `(${position ? 'for' : 'against'}, stake: ${amount}, anonymous: ${anonymous})`,
+ `(${position ? 'for' : 'against'}, stake: ${amount})`,
);
- return validationPool.stake(signingPublicKey, {
- position, amount, lockingTime, anonymous,
+ return validationPool.stake(this, {
+ position, amount, lockingTime, tokenId: this.tokens[0],
});
}
- async revealIdentity(validationPool) {
- const { signingPublicKey } = this.validationPools.get(validationPool.id);
- // TODO: sign message
- await this.actions.revealIdentity.log(this, validationPool);
- validationPool.revealIdentity(signingPublicKey, this.reputationPublicKey);
- }
-
- async registerAvailability(availability, stake) {
- await this.actions.registerAvailability.log(this, availability, `(stake: ${stake})`);
- await availability.register(this.reputationPublicKey, stake);
+ async registerAvailability(availability, stakeAmount) {
+ await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount})`);
+ await availability.register({ stakeAmount, tokenId: this.tokens[0].id });
}
async getAssignedWork(availability, business) {
diff --git a/forum-network/src/classes/forum.js b/forum-network/src/classes/forum.js
index 2e143a6..717b144 100644
--- a/forum-network/src/classes/forum.js
+++ b/forum-network/src/classes/forum.js
@@ -3,13 +3,14 @@ import { Graph } from './graph.js';
import { Action } from './action.js';
import { CryptoUtil } from './crypto.js';
import params from '../params.js';
+import { ReputationHolder } from './reputation-holder.js';
class Post extends Actor {
constructor(forum, authorPublicKey, postContent) {
const index = forum.posts.countVertices();
const name = `Post${index + 1}`;
super(name, forum.scene);
- this.id = postContent.id ?? CryptoUtil.randomUUID();
+ this.id = postContent.id ?? `post_${CryptoUtil.randomUUID()}`;
this.authorPublicKey = authorPublicKey;
this.value = 0;
this.citations = postContent.citations;
@@ -28,9 +29,10 @@ class Post extends Actor {
/**
* Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts
*/
-export class Forum extends Actor {
+export class Forum extends ReputationHolder {
constructor(name, scene) {
- super(name, scene);
+ super(`forum_${CryptoUtil.randomUUID()}`, name, scene);
+ this.id = this.reputationPublicKey;
this.posts = new Graph(scene);
this.actions = {
addPost: new Action('add post', scene),
@@ -74,8 +76,10 @@ export class Forum extends Actor {
return this.getPosts().reduce((total, { value }) => total += value, 0);
}
- async onValidate(bench, pool, postId, initialValue) {
- initialValue *= params.initialPostValue();
+ async onValidate({
+ bench, pool, postId, tokenId,
+ }) {
+ const initialValue = bench.reputation.valueOf(tokenId);
if (this.scene.flowchart) {
this.scene.flowchart.log(`${postId}_initial_value[${initialValue}] -- initial value --> ${postId}`);
@@ -84,14 +88,21 @@ export class Forum extends Actor {
const post = this.getPost(postId);
post.setStatus('Validated');
+ // Store a reference to the reputation token associated with this post,
+ // so that its value can be updated by future validated posts.
+ post.tokenId = tokenId;
+
// Compute rewards
const rewardsAccumulator = new Map();
await this.propagateValue(rewardsAccumulator, pool, post, initialValue);
- // Apply computed rewards
+ // Apply computed rewards to update values of tokens
for (const [id, value] of rewardsAccumulator) {
- bench.reputations.addTokens(id, value);
+ bench.reputation.transferValueFrom(post.tokenId, id, value);
}
+
+ // Transfer ownership of the minted/staked token, from the forum to the post author
+ bench.reputation.transferFrom(this.id, post.authorPublicKey, post.tokenId);
}
async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) {
@@ -123,7 +134,7 @@ export class Forum extends Actor {
const appliedIncrement = newValue - post.value;
// Award reputation to post author
- rewardsAccumulator.set(post.authorPublicKey, appliedIncrement);
+ rewardsAccumulator.set(post.tokenId, appliedIncrement);
// Increment the value of the post
await this.setPostValue(post, newValue);
diff --git a/forum-network/src/classes/reputation-holder.js b/forum-network/src/classes/reputation-holder.js
new file mode 100644
index 0000000..5bfcc42
--- /dev/null
+++ b/forum-network/src/classes/reputation-holder.js
@@ -0,0 +1,8 @@
+import { Actor } from './actor.js';
+
+export class ReputationHolder extends Actor {
+ constructor(reputationPublicKey, name, scene) {
+ super(name, scene);
+ this.reputationPublicKey = reputationPublicKey;
+ }
+}
diff --git a/forum-network/src/classes/reputation-token.js b/forum-network/src/classes/reputation-token.js
index 6e48648..894ff74 100644
--- a/forum-network/src/classes/reputation-token.js
+++ b/forum-network/src/classes/reputation-token.js
@@ -1,28 +1,106 @@
import { ERC721 } from './erc721.js';
import { CryptoUtil } from './crypto.js';
-export class ReputationToken extends ERC721 {
+const EPSILON = 2.23e-16;
+
+class Lock {
+ constructor(tokenId, amount, duration) {
+ this.dateCreated = new Date();
+ this.tokenId = tokenId;
+ this.amount = amount;
+ this.duration = duration;
+ }
+}
+
+export class ReputationTokenContract extends ERC721 {
constructor() {
super('Reputation', 'REP');
this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)}
this.values = new Map(); // token id --> current value
+ this.locks = new Set(); // {tokenId, amount, start, duration}
}
mint(to, value, context) {
- const tokenId = CryptoUtil.randomUUID();
+ const tokenId = `token_${CryptoUtil.randomUUID()}`;
super.mint(to, tokenId);
this.values.set(tokenId, value);
this.histories.set(tokenId, [{ increment: value, context }]);
+ return tokenId;
}
incrementValue(tokenId, increment, context) {
const value = this.values.get(tokenId);
const newValue = value + increment;
- const history = this.histories.get(tokenId);
- if (newValue < 0) {
- throw new Error('Token value can not become negative');
+ const history = this.histories.get(tokenId) || [];
+
+ if (newValue < -EPSILON) {
+ throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
}
this.values.set(tokenId, newValue);
history.push({ increment, context });
+ this.histories.set(tokenId, history);
+ }
+
+ transferValueFrom(fromTokenId, toTokenId, amount) {
+ const sourceAvailable = this.availableValueOf(fromTokenId);
+ const targetAvailable = this.availableValueOf(toTokenId);
+ if (sourceAvailable < amount - EPSILON) {
+ throw new Error('Token value transfer: source has insufficient available value. '
+ + `Needs ${amount}; has ${sourceAvailable}.`);
+ }
+ if (targetAvailable < -amount + EPSILON) {
+ throw new Error('Token value transfer: target has insufficient available value. '
+ + `Needs ${-amount}; has ${targetAvailable}.`);
+ }
+ this.incrementValue(fromTokenId, -amount);
+ this.incrementValue(toTokenId, amount);
+ }
+
+ lock(tokenId, amount, duration) {
+ const lock = new Lock(tokenId, amount, duration);
+ this.locks.add(lock);
+ }
+
+ historyOf(tokenId) {
+ return this.histories.get(tokenId);
+ }
+
+ valueOf(tokenId) {
+ return this.values.get(tokenId);
+ }
+
+ availableValueOf(tokenId) {
+ const amountLocked = Array.from(this.locks.values())
+ .filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId)
+ .filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
+ .reduce((total, { amount }) => total += amount, 0);
+
+ return this.valueOf(tokenId) - amountLocked;
+ }
+
+ valueOwnedBy(ownerId) {
+ return Array.from(this.owners.entries())
+ .filter(([__, owner]) => owner === ownerId)
+ .map(([tokenId, __]) => this.valueOf(tokenId))
+ .reduce((total, value) => total += value, 0);
+ }
+
+ availableValueOwnedBy(ownerId) {
+ return Array.from(this.owners.entries())
+ .filter(([__, owner]) => owner === ownerId)
+ .map(([tokenId, __]) => this.availableValueOf(tokenId))
+ .reduce((total, value) => total += value, 0);
+ }
+
+ getTotal() {
+ return Array.from(this.values.values()).reduce((total, value) => total += value, 0);
+ }
+
+ getTotalAvailable() {
+ const amountLocked = Array.from(this.locks.values())
+ .filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
+ .reduce((total, { amount }) => total += amount, 0);
+
+ return this.getTotal() - amountLocked;
}
}
diff --git a/forum-network/src/classes/stake.js b/forum-network/src/classes/stake.js
index efadb2f..5a5d02a 100644
--- a/forum-network/src/classes/stake.js
+++ b/forum-network/src/classes/stake.js
@@ -1,7 +1,10 @@
import params from '../params.js';
export class Stake {
- constructor(position, amount, lockingTime) {
+ constructor({
+ tokenId, position, amount, lockingTime,
+ }) {
+ this.tokenId = tokenId;
this.position = position;
this.amount = amount;
this.lockingTime = lockingTime;
diff --git a/forum-network/src/classes/validation-pool.js b/forum-network/src/classes/validation-pool.js
index 8fe6fc1..13cb893 100644
--- a/forum-network/src/classes/validation-pool.js
+++ b/forum-network/src/classes/validation-pool.js
@@ -1,7 +1,7 @@
import { CryptoUtil } from './crypto.js';
+import { ReputationHolder } from './reputation-holder.js';
import { Stake } from './stake.js';
import { Voter } from './voter.js';
-import { Actor } from './actor.js';
import params from '../params.js';
const ValidationPoolStates = Object.freeze({
@@ -13,24 +13,24 @@ const ValidationPoolStates = Object.freeze({
/**
* Purpose: Enable voting
*/
-export class ValidationPool extends Actor {
+export class ValidationPool extends ReputationHolder {
constructor(
bench,
forum,
{
postId,
- signingPublicKey,
+ reputationPublicKey,
fee,
duration,
tokenLossRatio,
contentiousDebate = false,
- authorStake = 0,
- anonymous = true,
+ authorStakeAmount = 0,
},
name,
scene,
) {
- super(name, scene);
+ super(`pool_${CryptoUtil.randomUUID()}`, name, scene);
+ this.id = this.reputationPublicKey;
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
if (
!contentiousDebate
@@ -58,60 +58,29 @@ export class ValidationPool extends Actor {
this.postId = postId;
this.state = ValidationPoolStates.OPEN;
this.setStatus('Open');
- this.stakes = new Map();
- this.voters = new Map();
- this.id = CryptoUtil.randomUUID();
+ this.stakes = new Set();
this.dateStart = new Date();
- this.authorSigningPublicKey = signingPublicKey;
- this.anonymous = anonymous;
+ this.authorReputationPublicKey = reputationPublicKey;
this.fee = fee;
this.duration = duration;
this.tokenLossRatio = tokenLossRatio;
this.contentiousDebate = contentiousDebate;
- this.tokensMinted = fee * params.mintingRatio();
+ this.mintedValue = fee * params.mintingRatio();
+ this.tokenId = this.bench.reputation.mint(this.id, this.mintedValue);
// Tokens minted "for" the post go toward stake of author voting for their own post.
// Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
- this.stake(signingPublicKey, {
+ this.stake(this, {
position: true,
- amount: this.tokensMinted * params.stakeForAuthor + authorStake,
- anonymous,
+ amount: this.mintedValue * params.stakeForAuthor + authorStakeAmount,
+ tokenId: this.tokenId,
});
- this.stake(this.id, {
+ this.stake(this, {
position: false,
- amount: this.tokensMinted * (1 - params.stakeForAuthor),
+ amount: this.mintedValue * (1 - params.stakeForAuthor),
+ tokenId: this.tokenId,
});
}
- async stake(signingPublicKey, {
- position, amount, lockingTime = 0, anonymous = false,
- }) {
- if (this.state === ValidationPoolStates.CLOSED) {
- throw new Error(`Validation pool ${this.id} is closed`);
- }
- if (this.duration && new Date() - this.dateStart > this.duration) {
- throw new Error(
- `Validation pool ${this.id} has expired, no new votes may be cast`,
- );
- }
- const stake = new Stake(position, amount, lockingTime);
- this.stakes.set(signingPublicKey, stake);
- console.log('new stake', stake);
- if (!anonymous) {
- await this.revealIdentity(signingPublicKey, signingPublicKey);
- }
- }
-
- async revealIdentity(signingPublicKey, reputationPublicKey) {
- if (!this.stakes.get(signingPublicKey)) {
- throw new Error('Must stake before revealing identity');
- }
- const voter = this.bench.voters.get(reputationPublicKey)
- ?? new Voter(reputationPublicKey);
- voter.addVoteRecord(this);
- this.bench.voters.set(reputationPublicKey, voter);
- this.voters.set(signingPublicKey, voter);
- }
-
getTokenLossRatio() {
if (!this.contentiousDebate) {
return this.tokenLossRatio;
@@ -134,58 +103,79 @@ export class ValidationPool extends Actor {
return stage / (params.contentiousDebate.stages - 1);
}
- applyTokenLocking() {
- // Before evaluating the winning conditions,
- // we need to make sure any staked tokens are locked for the
- // specified amounts of time.
- for (const [
- signingPublicKey,
- { stake, lockingTime },
- ] of this.stakes) {
- const voter = this.voters.get(signingPublicKey);
- this.bench.reputations.lockTokens(
- voter.reputationPublicKey,
- stake,
- lockingTime,
- );
- // TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
- }
- }
-
/**
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
- * @param {object} getStakeEntries options
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
- * @returns [signingPublicKey, stake][]
+ * @returns stake[]
*/
- getStakeEntries(outcome, options = {}) {
- const { excludeSystem = false } = options;
- const entries = Array.from(this.stakes.entries());
- // console.log('entries', entries);
- return entries
- .filter(([signingPublicKey, __]) => !excludeSystem || signingPublicKey !== this.id)
- .filter(([__, { position }]) => outcome === null || position === outcome);
+ getStakes(outcome, { excludeSystem }) {
+ return Array.from(this.stakes.values())
+ .filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId)
+ .filter(({ position }) => outcome === null || position === outcome);
}
/**
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
- * @param {object} getStakeEntries options
* @returns number
*/
- getTotalStakedOnPost(outcome, options) {
- return this.getStakeEntries(outcome, options)
- .map(([__, stake]) => stake.getStakeValue())
+ getTotalStakedOnPost(outcome) {
+ return this.getStakes(outcome, { excludeSystem: false })
+ .map((stake) => stake.getStakeValue())
.reduce((acc, cur) => (acc += cur), 0);
}
/**
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
- * @param {object} getStakeEntries options
* @returns number
*/
- getTotalValueOfStakesForOutcome(outcome, options) {
- return this.getStakeEntries(outcome, options)
- .reduce((total, [__, { amount }]) => (total += amount), 0);
+ getTotalValueOfStakesForOutcome(outcome) {
+ return this.getStakes(outcome, { excludeSystem: false })
+ .reduce((total, { amount }) => (total += amount), 0);
+ }
+
+ // TODO: This can be handled as a hook on receipt of reputation token transfer
+ async stake(reputationHolder, {
+ tokenId, position, amount, lockingTime = 0,
+ }) {
+ if (this.state === ValidationPoolStates.CLOSED) {
+ throw new Error(`Validation pool ${this.id} is closed.`);
+ }
+
+ if (this.duration && new Date() - this.dateStart > this.duration) {
+ throw new Error(
+ `Validation pool ${this.id} has expired, no new votes may be cast.`,
+ );
+ }
+
+ const { reputationPublicKey } = reputationHolder;
+ if (reputationPublicKey !== this.bench.reputation.ownerOf(tokenId)) {
+ throw new Error('Reputation may only be staked by its owner!');
+ }
+
+ const stake = new Stake({
+ tokenId, position, amount, lockingTime,
+ });
+ this.stakes.add(stake);
+
+ // Transfer staked amount from the sender to the validation pool
+ this.bench.reputation.transferValueFrom(tokenId, this.tokenId, amount);
+
+ // Keep a record of voters and their votes
+ if (tokenId !== this.tokenId) {
+ const voter = this.bench.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
+ voter.addVoteRecord(this);
+ this.bench.voters.set(reputationPublicKey, voter);
+ }
+ }
+
+ applyTokenLocking() {
+ // Before evaluating the winning conditions,
+ // we need to make sure any staked tokens are locked for the
+ // specified amounts of time.
+ for (const { tokenId, amount, lockingTime } of this.stakes.values()) {
+ this.bench.reputation.lock(tokenId, amount, lockingTime);
+ // TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
+ }
}
async evaluateWinningConditions() {
@@ -196,16 +186,13 @@ export class ValidationPool extends Actor {
if (elapsed < this.duration) {
throw new Error(`Validation pool duration has not yet elapsed! ${this.duration - elapsed} ms remaining.`);
}
- if (this.voters.size < this.stakes.size) {
- throw new Error('Not all voters have revealed their reputation public keys!');
- }
// Now we can evaluate winning conditions
this.state = ValidationPoolStates.CLOSED;
this.setStatus('Closed');
const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
- const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation();
+ const activeAvailableReputation = this.bench.getActiveAvailableReputation();
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
@@ -238,44 +225,35 @@ export class ValidationPool extends Actor {
// In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
- const winningVotes = this.getStakeEntries(votePasses, { excludeSystem: true });
+ const winningEntries = this.getStakes(votePasses, { excludeSystem: true });
const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
// Compute rewards for the winning voters, in proportion to the value of their stakes.
- const rewards = new Map();
- for (const [signingPublicKey, stake] of winningVotes) {
- const { reputationPublicKey } = this.voters.get(signingPublicKey);
+ for (const stake of winningEntries) {
+ const { tokenId, amount } = stake;
const value = stake.getStakeValue();
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
- rewards.set(reputationPublicKey, reward);
+ // Also return each winning voter their staked amount
+ const reputationPublicKey = this.bench.reputation.ownerOf(tokenId);
+ console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
+ this.bench.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount);
}
- console.log('rewards for stakes', rewards);
-
- const authorReputationPublicKey = this.voters.get(this.authorSigningPublicKey).reputationPublicKey;
-
- // Distribute awards to voters other than the author
- for (const [reputationPublicKey, amount] of rewards) {
- if (reputationPublicKey !== authorReputationPublicKey) {
- this.bench.reputations.addTokens(reputationPublicKey, amount);
- console.log(`reward for stake by ${reputationPublicKey}:`, amount);
- }
- }
-
- if (votePasses) {
+ if (votePasses && !!this.forum) {
// Distribute awards to author via the forum
- const tokensForAuthor = this.tokensMinted * params.stakeForAuthor + rewards.get(authorReputationPublicKey);
- console.log('sending reward for author stake to forum', { tokensForAuthor });
+ // const tokensForAuthor = this.mintedValue * params.stakeForAuthor + rewards.get(this.tokenId);
+ console.log(`sending reward for author stake to forum: ${this.bench.reputation.valueOf(this.tokenId)}`);
+
+ // Transfer ownership of the minted token, from the pool to the forum
+ this.bench.reputation.transferFrom(this.id, this.forum.id, this.tokenId);
- if (votePasses && !!this.forum) {
// Recurse through forum to determine reputation effects
- await this.forum.onValidate(
- this.bench,
- this,
- this.postId,
- tokensForAuthor,
- );
- }
+ await this.forum.onValidate({
+ bench: this.bench,
+ pool: this,
+ postId: this.postId,
+ tokenId: this.tokenId,
+ });
}
console.log('pool complete');
diff --git a/forum-network/src/tests/availability.html b/forum-network/src/tests/availability.html
index c2684bf..11259f7 100644
--- a/forum-network/src/tests/availability.html
+++ b/forum-network/src/tests/availability.html
@@ -7,22 +7,22 @@
+
+
diff --git a/forum-network/src/tests/debounce.html b/forum-network/src/tests/debounce.html
index db51d69..0b3b146 100644
--- a/forum-network/src/tests/debounce.html
+++ b/forum-network/src/tests/debounce.html
@@ -7,14 +7,14 @@
diff --git a/forum-network/src/tests/forum-network.html b/forum-network/src/tests/forum-network.html
index 738b423..ad0242b 100644
--- a/forum-network/src/tests/forum-network.html
+++ b/forum-network/src/tests/forum-network.html
@@ -7,36 +7,36 @@
diff --git a/forum-network/src/tests/reputation.html b/forum-network/src/tests/reputation.html
new file mode 100644
index 0000000..6fc7b3c
--- /dev/null
+++ b/forum-network/src/tests/reputation.html
@@ -0,0 +1,32 @@
+
+
+
diff --git a/forum-network/src/tests/validation-pool.html b/forum-network/src/tests/validation-pool.html
index 47005d1..4280a91 100644
--- a/forum-network/src/tests/validation-pool.html
+++ b/forum-network/src/tests/validation-pool.html
@@ -7,43 +7,45 @@