Reputation tokens

This commit is contained in:
Ladd Hoffman 2023-01-18 01:07:10 -06:00
parent cd5fce820c
commit 8982ac610f
19 changed files with 456 additions and 402 deletions

View File

@ -2,14 +2,11 @@ import { Action } from './action.js';
import { Actor } from './actor.js'; import { Actor } from './actor.js';
class Worker { class Worker {
stake = 0; constructor(tokenId) {
this.tokenId = tokenId;
available = true; this.stakeAmount = 0;
this.available = true;
assignedRequestId = null; this.assignedRequestId = null;
constructor(reputationPublicKey) {
this.reputationPublicKey = reputationPublicKey;
} }
} }
@ -28,16 +25,16 @@ export class Availability extends Actor {
}; };
} }
register(reputationPublicKey, stake, __duration) { register({ stakeAmount, tokenId }) {
// TODO: expire after duration // TODO: expire after duration
// ? Is a particular stake amount required? // ? 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) { if (!worker.available) {
throw new Error('Worker is already registered and busy. Can not increase stake.'); 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? // TODO: Interact with Bench contract to encumber reputation?
this.workers.set(reputationPublicKey, worker); this.workers.set(tokenId, worker);
} }
// unregister() { } // unregister() { }

View File

@ -1,8 +1,8 @@
import { Actor } from './actor.js'; import { Actor } from './actor.js';
import { Reputations } from './reputation.js';
import { ValidationPool } from './validation-pool.js'; import { ValidationPool } from './validation-pool.js';
import params from '../params.js'; import params from '../params.js';
import { Action } from './action.js'; import { Action } from './action.js';
import { ReputationTokenContract } from './reputation-token.js';
/** /**
* Purpose: Keep track of reputation holders * Purpose: Keep track of reputation holders
@ -13,7 +13,7 @@ export class Bench extends Actor {
this.forum = forum; this.forum = forum;
this.validationPools = new Map(); this.validationPools = new Map();
this.voters = new Map(); this.voters = new Map();
this.reputations = new Reputations(); this.reputation = new ReputationTokenContract();
this.actions = { this.actions = {
createValidationPool: new Action('create validation pool', scene), createValidationPool: new Action('create validation pool', scene),
@ -33,53 +33,22 @@ export class Bench extends Actor {
}); });
} }
getTotalReputation() { getActiveReputation() {
return this.reputations.getTotal();
}
getTotalAvailableReputation() {
return this.reputations.getTotalAvailable();
}
getTotalActiveReputation() {
return this.listActiveVoters() return this.listActiveVoters()
.map(({ reputationPublicKey }) => this.reputations.getTokens(reputationPublicKey)) .map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey))
.reduce((acc, cur) => (acc += cur), 0); .reduce((acc, cur) => (acc += cur), 0);
} }
getTotalActiveAvailableReputation() { getActiveAvailableReputation() {
return this.listActiveVoters() return this.listActiveVoters()
.map(({ reputationPublicKey }) => this.reputations.getAvailableTokens(reputationPublicKey)) .map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey))
.reduce((acc, cur) => (acc += cur), 0); .reduce((acc, cur) => (acc += cur), 0);
} }
async initiateValidationPool({ async initiateValidationPool(poolOptions) {
postId,
fee,
duration,
tokenLossRatio,
contentiousDebate,
signingPublicKey,
authorStake,
anonymous,
}) {
const validationPoolNumber = this.validationPools.size + 1; const validationPoolNumber = this.validationPools.size + 1;
const validationPool = new ValidationPool( const name = `Pool${validationPoolNumber}`;
this, const validationPool = new ValidationPool(this, this.forum, poolOptions, name, this.scene);
this.forum,
{
postId,
fee,
duration,
tokenLossRatio,
contentiousDebate,
signingPublicKey,
authorStake,
anonymous,
},
`Pool${validationPoolNumber}`,
this.scene,
);
this.validationPools.set(validationPool.id, validationPool); this.validationPools.set(validationPool.id, validationPool);
await this.actions.createValidationPool.log(this, validationPool); await this.actions.createValidationPool.log(this, validationPool);
validationPool.activate(); validationPool.activate();

View File

@ -66,9 +66,9 @@ export class Business extends Actor {
fee: request.fee, fee: request.fee,
duration, duration,
tokenLossRatio, tokenLossRatio,
signingPublicKey: reputationPublicKey, reputationPublicKey,
anonymous: false, authorStakeAmount: this.worker.stakeAmount,
authorStake: this.worker.stake, tokenId: this.worker.tokenId,
}); });
// When the validation pool concludes, // When the validation pool concludes,

View File

@ -56,6 +56,7 @@ export class ERC721 /* is ERC165 */ {
if (!owner) { if (!owner) {
throw new Error('ERC721: invalid token ID'); throw new Error('ERC721: invalid token ID');
} }
return owner;
} }
transferFrom(from, to, tokenId) { transferFrom(from, to, tokenId) {
@ -65,6 +66,7 @@ export class ERC721 /* is ERC165 */ {
} }
this.incrementBalance(from, -1); this.incrementBalance(from, -1);
this.incrementBalance(to, 1); this.incrementBalance(to, 1);
this.owners.set(tokenId, to);
} }
/// @notice Enable or disable approval for a third party ("operator") to manage /// @notice Enable or disable approval for a third party ("operator") to manage

View File

@ -1,22 +1,22 @@
import { Actor } from './actor.js';
import { Action } from './action.js'; import { Action } from './action.js';
import { PostMessage } from './message.js'; import { PostMessage } from './message.js';
import { CryptoUtil } from './crypto.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) { constructor(name, scene) {
super(name, scene); super(undefined, name, scene);
this.actions = { this.actions = {
submitPostViaNetwork: new Action('submit post via network', scene), submitPostViaNetwork: new Action('submit post via network', scene),
submitPost: new Action('submit post', scene), submitPost: new Action('submit post', scene),
initiateValidationPool: new Action('initiate validation pool', scene), initiateValidationPool: new Action('initiate validation pool', scene),
stake: new Action('stake on post', scene), stake: new Action('stake on post', scene),
revealIdentity: new Action('reveal identity', scene),
registerAvailability: new Action('register availability', scene), registerAvailability: new Action('register availability', scene),
getAssignedWork: new Action('get assigned work', scene), getAssignedWork: new Action('get assigned work', scene),
submitWork: new Action('submit work evidence', scene), submitWork: new Action('submit work evidence', scene),
}; };
this.validationPools = new Map(); this.validationPools = new Map();
this.tokens = [];
} }
async initialize() { async initialize() {
@ -46,60 +46,42 @@ export class Expert extends Actor {
await this.actions.submitPost.log(this, forum); await this.actions.submitPost.log(this, forum);
const postId = await forum.addPost(this.reputationPublicKey, postContent); const postId = await forum.addPost(this.reputationPublicKey, postContent);
const pool = await this.initiateValidationPool(bench, { ...poolOptions, postId, anonymous: false }); const pool = await this.initiateValidationPool(bench, { ...poolOptions, postId, anonymous: false });
this.tokens.push(pool.tokenId);
return { postId, pool }; return { postId, pool };
} }
async initiateValidationPool(bench, poolOptions) { async initiateValidationPool(bench, poolOptions) {
// For now, directly call bench.initiateValidationPool(); // For now, directly call bench.initiateValidationPool();
if (poolOptions.anonymous) { poolOptions.reputationPublicKey = this.reputationPublicKey;
const signingKey = await CryptoUtil.generateAsymmetricKey();
poolOptions.signingPublicKey = await CryptoUtil.exportKey(signingKey.publicKey);
} else {
poolOptions.signingPublicKey = this.reputationPublicKey;
}
await this.actions.initiateValidationPool.log( await this.actions.initiateValidationPool.log(
this, this,
bench, bench,
`(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStake ?? 0})`, `(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`,
); );
const pool = await bench.initiateValidationPool(poolOptions); const pool = await bench.initiateValidationPool(poolOptions);
this.tokens.push(pool.tokenId);
this.validationPools.set(pool.id, poolOptions); this.validationPools.set(pool.id, poolOptions);
return pool; return pool;
} }
async stake(validationPool, { 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: encrypt stake
// TODO: sign message // TODO: sign message
await this.actions.stake.log( await this.actions.stake.log(
this, this,
validationPool, validationPool,
`(${position ? 'for' : 'against'}, stake: ${amount}, anonymous: ${anonymous})`, `(${position ? 'for' : 'against'}, stake: ${amount})`,
); );
return validationPool.stake(signingPublicKey, { return validationPool.stake(this, {
position, amount, lockingTime, anonymous, position, amount, lockingTime, tokenId: this.tokens[0],
}); });
} }
async revealIdentity(validationPool) { async registerAvailability(availability, stakeAmount) {
const { signingPublicKey } = this.validationPools.get(validationPool.id); await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount})`);
// TODO: sign message await availability.register({ stakeAmount, tokenId: this.tokens[0].id });
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 getAssignedWork(availability, business) { async getAssignedWork(availability, business) {

View File

@ -3,13 +3,14 @@ import { Graph } from './graph.js';
import { Action } from './action.js'; import { Action } from './action.js';
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
import params from '../params.js'; import params from '../params.js';
import { ReputationHolder } from './reputation-holder.js';
class Post extends Actor { class Post extends Actor {
constructor(forum, authorPublicKey, postContent) { constructor(forum, authorPublicKey, postContent) {
const index = forum.posts.countVertices(); const index = forum.posts.countVertices();
const name = `Post${index + 1}`; const name = `Post${index + 1}`;
super(name, forum.scene); super(name, forum.scene);
this.id = postContent.id ?? CryptoUtil.randomUUID(); this.id = postContent.id ?? `post_${CryptoUtil.randomUUID()}`;
this.authorPublicKey = authorPublicKey; this.authorPublicKey = authorPublicKey;
this.value = 0; this.value = 0;
this.citations = postContent.citations; this.citations = postContent.citations;
@ -28,9 +29,10 @@ class Post extends Actor {
/** /**
* Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts * 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) { constructor(name, scene) {
super(name, scene); super(`forum_${CryptoUtil.randomUUID()}`, name, scene);
this.id = this.reputationPublicKey;
this.posts = new Graph(scene); this.posts = new Graph(scene);
this.actions = { this.actions = {
addPost: new Action('add post', scene), addPost: new Action('add post', scene),
@ -74,8 +76,10 @@ export class Forum extends Actor {
return this.getPosts().reduce((total, { value }) => total += value, 0); return this.getPosts().reduce((total, { value }) => total += value, 0);
} }
async onValidate(bench, pool, postId, initialValue) { async onValidate({
initialValue *= params.initialPostValue(); bench, pool, postId, tokenId,
}) {
const initialValue = bench.reputation.valueOf(tokenId);
if (this.scene.flowchart) { if (this.scene.flowchart) {
this.scene.flowchart.log(`${postId}_initial_value[${initialValue}] -- initial value --> ${postId}`); 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); const post = this.getPost(postId);
post.setStatus('Validated'); 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 // Compute rewards
const rewardsAccumulator = new Map(); const rewardsAccumulator = new Map();
await this.propagateValue(rewardsAccumulator, pool, post, initialValue); await this.propagateValue(rewardsAccumulator, pool, post, initialValue);
// Apply computed rewards // Apply computed rewards to update values of tokens
for (const [id, value] of rewardsAccumulator) { 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) { async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) {
@ -123,7 +134,7 @@ export class Forum extends Actor {
const appliedIncrement = newValue - post.value; const appliedIncrement = newValue - post.value;
// Award reputation to post author // Award reputation to post author
rewardsAccumulator.set(post.authorPublicKey, appliedIncrement); rewardsAccumulator.set(post.tokenId, appliedIncrement);
// Increment the value of the post // Increment the value of the post
await this.setPostValue(post, newValue); await this.setPostValue(post, newValue);

View File

@ -0,0 +1,8 @@
import { Actor } from './actor.js';
export class ReputationHolder extends Actor {
constructor(reputationPublicKey, name, scene) {
super(name, scene);
this.reputationPublicKey = reputationPublicKey;
}
}

View File

@ -1,28 +1,106 @@
import { ERC721 } from './erc721.js'; import { ERC721 } from './erc721.js';
import { CryptoUtil } from './crypto.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() { constructor() {
super('Reputation', 'REP'); super('Reputation', 'REP');
this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)} this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)}
this.values = new Map(); // token id --> current value this.values = new Map(); // token id --> current value
this.locks = new Set(); // {tokenId, amount, start, duration}
} }
mint(to, value, context) { mint(to, value, context) {
const tokenId = CryptoUtil.randomUUID(); const tokenId = `token_${CryptoUtil.randomUUID()}`;
super.mint(to, tokenId); super.mint(to, tokenId);
this.values.set(tokenId, value); this.values.set(tokenId, value);
this.histories.set(tokenId, [{ increment: value, context }]); this.histories.set(tokenId, [{ increment: value, context }]);
return tokenId;
} }
incrementValue(tokenId, increment, context) { incrementValue(tokenId, increment, context) {
const value = this.values.get(tokenId); const value = this.values.get(tokenId);
const newValue = value + increment; const newValue = value + increment;
const history = this.histories.get(tokenId); const history = this.histories.get(tokenId) || [];
if (newValue < 0) {
throw new Error('Token value can not become negative'); if (newValue < -EPSILON) {
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
} }
this.values.set(tokenId, newValue); this.values.set(tokenId, newValue);
history.push({ increment, context }); 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;
} }
} }

View File

@ -1,7 +1,10 @@
import params from '../params.js'; import params from '../params.js';
export class Stake { export class Stake {
constructor(position, amount, lockingTime) { constructor({
tokenId, position, amount, lockingTime,
}) {
this.tokenId = tokenId;
this.position = position; this.position = position;
this.amount = amount; this.amount = amount;
this.lockingTime = lockingTime; this.lockingTime = lockingTime;

View File

@ -1,7 +1,7 @@
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
import { ReputationHolder } from './reputation-holder.js';
import { Stake } from './stake.js'; import { Stake } from './stake.js';
import { Voter } from './voter.js'; import { Voter } from './voter.js';
import { Actor } from './actor.js';
import params from '../params.js'; import params from '../params.js';
const ValidationPoolStates = Object.freeze({ const ValidationPoolStates = Object.freeze({
@ -13,24 +13,24 @@ const ValidationPoolStates = Object.freeze({
/** /**
* Purpose: Enable voting * Purpose: Enable voting
*/ */
export class ValidationPool extends Actor { export class ValidationPool extends ReputationHolder {
constructor( constructor(
bench, bench,
forum, forum,
{ {
postId, postId,
signingPublicKey, reputationPublicKey,
fee, fee,
duration, duration,
tokenLossRatio, tokenLossRatio,
contentiousDebate = false, contentiousDebate = false,
authorStake = 0, authorStakeAmount = 0,
anonymous = true,
}, },
name, name,
scene, 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 = true, we will follow the progression defined by getTokenLossRatio()
if ( if (
!contentiousDebate !contentiousDebate
@ -58,60 +58,29 @@ export class ValidationPool extends Actor {
this.postId = postId; this.postId = postId;
this.state = ValidationPoolStates.OPEN; this.state = ValidationPoolStates.OPEN;
this.setStatus('Open'); this.setStatus('Open');
this.stakes = new Map(); this.stakes = new Set();
this.voters = new Map();
this.id = CryptoUtil.randomUUID();
this.dateStart = new Date(); this.dateStart = new Date();
this.authorSigningPublicKey = signingPublicKey; this.authorReputationPublicKey = reputationPublicKey;
this.anonymous = anonymous;
this.fee = fee; this.fee = fee;
this.duration = duration; this.duration = duration;
this.tokenLossRatio = tokenLossRatio; this.tokenLossRatio = tokenLossRatio;
this.contentiousDebate = contentiousDebate; 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. // 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. // Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
this.stake(signingPublicKey, { this.stake(this, {
position: true, position: true,
amount: this.tokensMinted * params.stakeForAuthor + authorStake, amount: this.mintedValue * params.stakeForAuthor + authorStakeAmount,
anonymous, tokenId: this.tokenId,
}); });
this.stake(this.id, { this.stake(this, {
position: false, 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() { getTokenLossRatio() {
if (!this.contentiousDebate) { if (!this.contentiousDebate) {
return this.tokenLossRatio; return this.tokenLossRatio;
@ -134,58 +103,79 @@ export class ValidationPool extends Actor {
return stage / (params.contentiousDebate.stages - 1); 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 {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 * @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
* @returns [signingPublicKey, stake][] * @returns stake[]
*/ */
getStakeEntries(outcome, options = {}) { getStakes(outcome, { excludeSystem }) {
const { excludeSystem = false } = options; return Array.from(this.stakes.values())
const entries = Array.from(this.stakes.entries()); .filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId)
// console.log('entries', entries); .filter(({ position }) => outcome === null || position === outcome);
return entries
.filter(([signingPublicKey, __]) => !excludeSystem || signingPublicKey !== this.id)
.filter(([__, { position }]) => outcome === null || position === outcome);
} }
/** /**
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome. * @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
* @param {object} getStakeEntries options
* @returns number * @returns number
*/ */
getTotalStakedOnPost(outcome, options) { getTotalStakedOnPost(outcome) {
return this.getStakeEntries(outcome, options) return this.getStakes(outcome, { excludeSystem: false })
.map(([__, stake]) => stake.getStakeValue()) .map((stake) => stake.getStakeValue())
.reduce((acc, cur) => (acc += cur), 0); .reduce((acc, cur) => (acc += cur), 0);
} }
/** /**
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome. * @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
* @param {object} getStakeEntries options
* @returns number * @returns number
*/ */
getTotalValueOfStakesForOutcome(outcome, options) { getTotalValueOfStakesForOutcome(outcome) {
return this.getStakeEntries(outcome, options) return this.getStakes(outcome, { excludeSystem: false })
.reduce((total, [__, { amount }]) => (total += amount), 0); .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() { async evaluateWinningConditions() {
@ -196,16 +186,13 @@ export class ValidationPool extends Actor {
if (elapsed < this.duration) { if (elapsed < this.duration) {
throw new Error(`Validation pool duration has not yet elapsed! ${this.duration - elapsed} ms remaining.`); 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 // Now we can evaluate winning conditions
this.state = ValidationPoolStates.CLOSED; this.state = ValidationPoolStates.CLOSED;
this.setStatus('Closed'); this.setStatus('Closed');
const upvoteValue = this.getTotalValueOfStakesForOutcome(true); const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
const downvoteValue = this.getTotalValueOfStakesForOutcome(false); const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation(); const activeAvailableReputation = this.bench.getActiveAvailableReputation();
const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; 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. // In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
const tokensForWinners = this.getTotalStakedOnPost(!votePasses); const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
const winningVotes = this.getStakeEntries(votePasses, { excludeSystem: true }); const winningEntries = this.getStakes(votePasses, { excludeSystem: true });
const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses); const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
// Compute rewards for the winning voters, in proportion to the value of their stakes. // Compute rewards for the winning voters, in proportion to the value of their stakes.
const rewards = new Map(); for (const stake of winningEntries) {
for (const [signingPublicKey, stake] of winningVotes) { const { tokenId, amount } = stake;
const { reputationPublicKey } = this.voters.get(signingPublicKey);
const value = stake.getStakeValue(); const value = stake.getStakeValue();
const reward = tokensForWinners * (value / totalValueOfStakesForWin); 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) {
// 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 });
if (votePasses && !!this.forum) { if (votePasses && !!this.forum) {
// Distribute awards to author via the forum
// 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);
// Recurse through forum to determine reputation effects // Recurse through forum to determine reputation effects
await this.forum.onValidate( await this.forum.onValidate({
this.bench, bench: this.bench,
this, pool: this,
this.postId, postId: this.postId,
tokensForAuthor, tokenId: this.tokenId,
); });
}
} }
console.log('pool complete'); console.log('pool complete');

View File

@ -7,22 +7,22 @@
<div id="availability-test"></div> <div id="availability-test"></div>
</body> </body>
<script type="module"> <script type="module">
import { Box } from "/../classes/box.js"; import { Box } from '../classes/box.js';
import { Scene } from "/../classes/scene.js"; import { Scene } from '../classes/scene.js';
import { Expert } from "/../classes/expert.js"; import { Expert } from '../classes/expert.js';
import { Bench } from "/../classes/bench.js"; import { Bench } from '../classes/bench.js';
import { Business } from "/../classes/business.js"; import { Business } from '../classes/business.js';
import { Availability } from "/../classes/availability.js"; import { Availability } from '../classes/availability.js';
import { delay } from "/util.js"; import { delay } from '../util.js';
import { Forum } from "/../classes/forum.js"; import { Forum } from '../classes/forum.js';
import { Public } from "/../classes/public.js"; import { Public } from '../classes/public.js';
const DELAY_INTERVAL = 500; const DELAY_INTERVAL = 500;
const rootElement = document.getElementById("availability-test"); const rootElement = document.getElementById('availability-test');
const rootBox = new Box("rootBox", rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
const scene = (window.scene = new Scene("Availability test", rootBox)); const scene = (window.scene = new Scene('Availability test', rootBox));
scene.withSequenceDiagram(); scene.withSequenceDiagram();
const experts = (window.experts = []); const experts = (window.experts = []);
@ -37,30 +37,30 @@
const expert1 = await newExpert(); const expert1 = await newExpert();
const expert2 = await newExpert(); const expert2 = await newExpert();
await newExpert(); await newExpert();
const forum = (window.forum = new Forum("Forum", scene)); const forum = (window.forum = new Forum('Forum', scene));
const bench = (window.bench = new Bench(forum, "Bench", scene)); const bench = (window.bench = new Bench(forum, 'Bench', scene));
const availability = (window.bench = new Availability( const availability = (window.bench = new Availability(
bench, bench,
"Availability", 'Availability',
scene scene,
)); ));
const business = (window.business = new Business( const business = (window.business = new Business(
bench, bench,
forum, forum,
availability, availability,
"Business", 'Business',
scene scene,
)); ));
const requestor = new Public("Public", scene); const requestor = new Public('Public', scene);
const updateDisplayValues = async () => { const updateDisplayValues = async () => {
for (const expert of experts) { for (const expert of experts) {
await expert.setValue( await expert.setValue(
"rep", 'rep',
bench.reputations.getTokens(expert.reputationPublicKey) bench.reputation.valueOwnedBy(expert.reputationPublicKey),
); );
} }
await bench.setValue("total rep", bench.getTotalReputation()); await bench.setValue('total rep', bench.reputation.getTotal());
await scene.sequence.render(); await scene.sequence.render();
}; };
@ -107,7 +107,7 @@
await requestor.submitRequest( await requestor.submitRequest(
business, business,
{ fee: 100 }, { fee: 100 },
{ please: "do some work" } { please: 'do some work' },
); );
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();
@ -119,12 +119,12 @@
business, business,
request.id, request.id,
{ {
here: "is some evidence of work product", here: 'is some evidence of work product',
}, },
{ {
tokenLossRatio: 1, tokenLossRatio: 1,
duration: 1000, duration: 1000,
} },
); );
worker.deactivate(); worker.deactivate();
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();
@ -146,10 +146,10 @@
} catch (e) { } catch (e) {
if (e.message.match(/Validation pool has already been resolved/)) { if (e.message.match(/Validation pool has already been resolved/)) {
console.log( console.log(
"Caught expected error: Validation pool has already been resolved" 'Caught expected error: Validation pool has already been resolved',
); );
} else { } else {
console.error("Unexpected error"); console.error('Unexpected error');
throw e; throw e;
} }
} }

View File

@ -7,11 +7,11 @@
<div id="basic"></div> <div id="basic"></div>
</body> </body>
<script type="module"> <script type="module">
import { Box } from "/../classes/box.js"; import { Box } from '../classes/box.js';
import { Scene } from "/../classes/scene.js"; import { Scene } from '../classes/scene.js';
const rootElement = document.getElementById("basic"); const rootElement = document.getElementById('basic');
const rootBox = new Box("rootBox", rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
function randomDelay(min, max) { function randomDelay(min, max) {
const delayMs = min + Math.random() * max; const delayMs = min + Math.random() * max;
@ -26,30 +26,30 @@
} }
if (true) { if (true) {
const scene = new Scene("Scene 1", rootBox); const scene = new Scene('Scene 1', rootBox);
const webClientStatus = scene.addDisplayValue("WebClient Status"); const webClientStatus = scene.addDisplayValue('WebClient Status');
const node1Status = scene.addDisplayValue("Node 1 Status"); const node1Status = scene.addDisplayValue('Node 1 Status');
const blockchainStatus = scene.addDisplayValue("Blockchain Status"); const blockchainStatus = scene.addDisplayValue('Blockchain Status');
const webClient = scene.addActor("web client"); const webClient = scene.addActor('web client');
const node1 = scene.addActor("node 1"); const node1 = scene.addActor('node 1');
const blockchain = scene.addActor("blockchain"); const blockchain = scene.addActor('blockchain');
const requestForumPage = scene.addAction("requestForumPage"); const requestForumPage = scene.addAction('requestForumPage');
const readBlockchainData = scene.addAction("readBlockchainData"); const readBlockchainData = scene.addAction('readBlockchainData');
const blockchainData = scene.addAction("blockchainData"); const blockchainData = scene.addAction('blockchainData');
const forumPage = scene.addAction("forumPage"); const forumPage = scene.addAction('forumPage');
webClientStatus.set("Initialized"); webClientStatus.set('Initialized');
node1Status.set("Idle"); node1Status.set('Idle');
blockchainStatus.set("Idle"); blockchainStatus.set('Idle');
node1.on(requestForumPage, (src, detail) => { node1.on(requestForumPage, (src, detail) => {
node1Status.set("Processing request"); node1Status.set('Processing request');
node1.on(blockchainData, (_src, data) => { node1.on(blockchainData, (_src, data) => {
node1Status.set("Processing response"); node1Status.set('Processing response');
setTimeout(() => { setTimeout(() => {
node1.send(src, forumPage, data); node1.send(src, forumPage, data);
node1Status.set("Idle"); node1Status.set('Idle');
}, randomDelay(500, 1000)); }, randomDelay(500, 1000));
}); });
setTimeout(() => { setTimeout(() => {
@ -58,27 +58,27 @@
}); });
blockchain.on(readBlockchainData, (src, _detail) => { blockchain.on(readBlockchainData, (src, _detail) => {
blockchainStatus.set("Processing request"); blockchainStatus.set('Processing request');
setTimeout(() => { setTimeout(() => {
blockchain.send(src, blockchainData, {}); blockchain.send(src, blockchainData, {});
blockchainStatus.set("Idle"); blockchainStatus.set('Idle');
}, randomDelay(500, 1500)); }, randomDelay(500, 1500));
}); });
webClient.on(forumPage, (_src, _detail) => { webClient.on(forumPage, (_src, _detail) => {
webClientStatus.set("Received forum page"); webClientStatus.set('Received forum page');
}); });
setInterval(() => { setInterval(() => {
webClient.send(node1, requestForumPage); webClient.send(node1, requestForumPage);
webClientStatus.set("Requested forum page"); webClientStatus.set('Requested forum page');
}, randomDelay(6000, 12000)); }, randomDelay(6000, 12000));
} }
(async function run() { (async function run() {
const scene = new Scene("Scene 2", rootBox); const scene = new Scene('Scene 2', rootBox);
const webClient = scene.addActor("webClient"); const webClient = scene.addActor('webClient');
const nodes = []; const nodes = [];
const memories = []; const memories = [];
@ -117,71 +117,71 @@
requestStorageData, requestStorageData,
storageData, storageData,
] = [ ] = [
"seek truth", 'seek truth',
"consider available information", 'consider available information',
"evaluate confidence", 'evaluate confidence',
"choose response", 'choose response',
"qualified opinions", 'qualified opinions',
"request in-memory data", 'request in-memory data',
"in-memory data", 'in-memory data',
"request storage data", 'request storage data',
"storage data", 'storage data',
].map((name) => scene.addAction(name)); ].map((name) => scene.addAction(name));
memories.forEach((memory) => { memories.forEach((memory) => {
memory.setStatus("Idle"); memory.setStatus('Idle');
memory.on(requestMemoryData, async (src, _detail) => { memory.on(requestMemoryData, async (src, _detail) => {
memory.setStatus("Retrieving data"); memory.setStatus('Retrieving data');
await delay(1000); await delay(1000);
memory.send(src, memoryData, {}); memory.send(src, memoryData, {});
memory.setStatus("Idle"); memory.setStatus('Idle');
}); });
}); });
storages.forEach((storage) => { storages.forEach((storage) => {
storage.setStatus("Idle"); storage.setStatus('Idle');
storage.on(requestStorageData, async (src, _detail) => { storage.on(requestStorageData, async (src, _detail) => {
storage.setStatus("Retrieving data"); storage.setStatus('Retrieving data');
await delay(1000); await delay(1000);
storage.send(src, storageData, {}); storage.send(src, storageData, {});
storage.setStatus("Idle"); storage.setStatus('Idle');
}); });
}); });
nodes.forEach((node) => { nodes.forEach((node) => {
node.setStatus("Idle"); node.setStatus('Idle');
node.on(seekTruth, async (seeker, detail) => { node.on(seekTruth, async (seeker, detail) => {
node.setStatus("Processing request"); node.setStatus('Processing request');
node.on(chooseResponse, async (_src, _info) => { node.on(chooseResponse, async (_src, _info) => {
node.setStatus("Choosing response"); node.setStatus('Choosing response');
await delay(1000); await delay(1000);
node.send(seeker, qualifiedOpinions, {}); node.send(seeker, qualifiedOpinions, {});
node.setStatus("Idle"); node.setStatus('Idle');
}); });
node.on(evaluateConfidence, async (_src, _info) => { node.on(evaluateConfidence, async (_src, _info) => {
node.setStatus("Evaluating confidence"); node.setStatus('Evaluating confidence');
await delay(1000); await delay(1000);
node.send(node, chooseResponse); node.send(node, chooseResponse);
}); });
node.on(considerInfo, async (_src, _info) => { node.on(considerInfo, async (_src, _info) => {
node.setStatus("Considering info"); node.setStatus('Considering info');
await delay(1000); await delay(1000);
node.send(node, evaluateConfidence); node.send(node, evaluateConfidence);
}); });
node.on(memoryData, (_src, _data) => { node.on(memoryData, (_src, _data) => {
node.on(storageData, (__src, __data) => { node.on(storageData, (__src, __data) => {
if (detail?.readConcern === "single") { if (detail?.readConcern === 'single') {
node.send(node, considerInfo, {}); node.send(node, considerInfo, {});
} else { } else {
const peer = getPeer(node); const peer = getPeer(node);
node.on(qualifiedOpinions, (___src, info) => { node.on(qualifiedOpinions, (___src, info) => {
node.send(node, considerInfo, info); node.send(node, considerInfo, info);
}); });
node.send(peer, seekTruth, { readConcern: "single" }); node.send(peer, seekTruth, { readConcern: 'single' });
} }
}); });
node.send(node.storage, requestStorageData); node.send(node.storage, requestStorageData);
@ -193,11 +193,11 @@
}); });
webClient.on(qualifiedOpinions, (_src, _detail) => { webClient.on(qualifiedOpinions, (_src, _detail) => {
webClient.setStatus("Received opinions and qualifications"); webClient.setStatus('Received opinions and qualifications');
}); });
await delay(1000); await delay(1000);
webClient.setStatus("Seek truth"); webClient.setStatus('Seek truth');
webClient.send(nodes[0], seekTruth); webClient.send(nodes[0], seekTruth);
})(); }());
</script> </script>

View File

@ -7,14 +7,14 @@
<div id="debounce-test"></div> <div id="debounce-test"></div>
</body> </body>
<script type="module"> <script type="module">
import { Box } from "/../classes/box.js"; import { Box } from '../classes/box.js';
import { Scene } from "/../classes/scene.js"; import { Scene } from '../classes/scene.js';
import { debounce, delay } from "/util.js"; import { debounce, delay } from '../util.js';
const rootElement = document.getElementById("debounce-test"); const rootElement = document.getElementById('debounce-test');
const rootBox = new Box("rootBox", rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
const scene = (window.scene = new Scene("Debounce test", rootBox)); window.scene = new Scene('Debounce test', rootBox);
let eventCount = 0; let eventCount = 0;
const event = () => { const event = () => {

View File

@ -7,33 +7,32 @@
<div id="flowchart-test"></div> <div id="flowchart-test"></div>
</body> </body>
<script type="module"> <script type="module">
import { Box } from "/../classes/box.js"; import { Box } from '../classes/box.js';
import { Scene } from "/../classes/scene.js"; import { Scene } from '../classes/scene.js';
import { Actor } from "/../classes/actor.js"; import { Actor } from '../classes/actor.js';
import { Action } from "/../classes/action.js"; import { Action } from '../classes/action.js';
import { Expert } from "/../classes/expert.js"; import { delay } from '../util.js';
import { delay } from "/util.js";
const DEFAULT_DELAY_INTERVAL = 500; const DEFAULT_DELAY_INTERVAL = 500;
const rootElement = document.getElementById("flowchart-test"); const rootElement = document.getElementById('flowchart-test');
const rootBox = new Box("rootBox", rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
const scene = (window.scene = new Scene("Flowchart test", rootBox)); const scene = (window.scene = new Scene('Flowchart test', rootBox));
scene.withSequenceDiagram(); scene.withSequenceDiagram();
const actor1 = new Actor("A", scene); const actor1 = new Actor('A', scene);
const actor2 = new Actor("B", scene); const actor2 = new Actor('B', scene);
const action1 = new Action("Action 1", scene); const action1 = new Action('Action 1', scene);
await action1.log(actor1, actor2); await action1.log(actor1, actor2);
await actor1.setValue("value", 1); await actor1.setValue('value', 1);
await scene.withFlowchart(); await scene.withFlowchart();
await scene.flowchart.log("A --> B"); await scene.flowchart.log('A --> B');
await delay(DEFAULT_DELAY_INTERVAL); await delay(DEFAULT_DELAY_INTERVAL);
action1.log(actor1, actor2); action1.log(actor1, actor2);
await delay(DEFAULT_DELAY_INTERVAL); await delay(DEFAULT_DELAY_INTERVAL);
await scene.flowchart.log("A --> C"); await scene.flowchart.log('A --> C');
</script> </script>

View File

@ -7,35 +7,35 @@
<div id="forum-network"></div> <div id="forum-network"></div>
</body> </body>
<script type="module"> <script type="module">
import { Box } from "/../classes/box.js"; import { Box } from '../classes/box.js';
import { Scene } from "/../classes/scene.js"; import { Scene } from '../classes/scene.js';
import { PostContent } from "/../classes/post-content.js"; import { PostContent } from '../classes/post-content.js';
import { Expert } from "/../classes/expert.js"; import { Expert } from '../classes/expert.js';
import { ForumNode } from "/../classes/forum-node.js"; import { ForumNode } from '../classes/forum-node.js';
import { ForumNetwork } from "/../classes/forum-network.js"; import { ForumNetwork } from '../classes/forum-network.js';
import { CryptoUtil } from "/../classes/crypto.js"; import { CryptoUtil } from '../classes/crypto.js';
import { delay } from "/util.js"; import { delay } from '../util.js';
const rootElement = document.getElementById("forum-network"); const rootElement = document.getElementById('forum-network');
const rootBox = new Box("rootBox", rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
window.scene = new Scene("Forum Network test", rootBox).log( window.scene = new Scene('Forum Network test', rootBox).log(
"sequenceDiagram" 'sequenceDiagram',
); );
window.author1 = await new Expert("author1", window.scene).initialize(); window.author1 = await new Expert('author1', window.scene).initialize();
window.author2 = await new Expert("author2", window.scene).initialize(); window.author2 = await new Expert('author2', window.scene).initialize();
window.forumNetwork = new ForumNetwork(); window.forumNetwork = new ForumNetwork();
window.forumNode1 = await new ForumNode("node1", window.scene).initialize( window.forumNode1 = await new ForumNode('node1', window.scene).initialize(
window.forumNetwork window.forumNetwork,
); );
window.forumNode2 = await new ForumNode("node2", window.scene).initialize( window.forumNode2 = await new ForumNode('node2', window.scene).initialize(
window.forumNetwork window.forumNetwork,
); );
window.forumNode3 = await new ForumNode("node3", window.scene).initialize( window.forumNode3 = await new ForumNode('node3', window.scene).initialize(
window.forumNetwork window.forumNetwork,
); );
const processInterval = setInterval(async () => { const processInterval = setInterval(async () => {
@ -48,24 +48,24 @@
// const blockchain = new Blockchain(); // const blockchain = new Blockchain();
window.post1 = new PostContent({ message: "hi" }); window.post1 = new PostContent({ message: 'hi' });
window.post1.id = CryptoUtil.randomUUID(); window.post1.id = CryptoUtil.randomUUID();
window.post2 = new PostContent({ message: "hello" }).addCitation( window.post2 = new PostContent({ message: 'hello' }).addCitation(
window.post1.id, window.post1.id,
1.0 1.0,
); );
await delay(1000); await delay(1000);
await window.author1.submitPostViaNetwork( await window.author1.submitPostViaNetwork(
window.forumNode1, window.forumNode1,
window.post1, window.post1,
50 50,
); );
await delay(1000); await delay(1000);
await window.author2.submitPostViaNetwork( await window.author2.submitPostViaNetwork(
window.forumNode2, window.forumNode2,
window.post2, window.post2,
100 100,
); );
await delay(1000); await delay(1000);

View File

@ -39,7 +39,6 @@
const name = `Expert${index + 1}`; const name = `Expert${index + 1}`;
const expert = await new Expert(name, scene).initialize(); const expert = await new Expert(name, scene).initialize();
experts.push(expert); experts.push(expert);
// bench.reputations.addTokens(expert.reputationPublicKey, 50);
return expert; return expert;
}; };
@ -53,10 +52,10 @@
for (const expert of experts) { for (const expert of experts) {
await expert.setValue( await expert.setValue(
'rep', 'rep',
bench.reputations.getTokens(expert.reputationPublicKey), bench.reputation.valueOwnedBy(expert.reputationPublicKey),
); );
} }
await bench.setValue('total rep', bench.getTotalReputation()); await bench.setValue('total rep', bench.reputation.getTotal());
await forum.setValue('total value', forum.getTotalValue()); await forum.setValue('total value', forum.getTotalValue());
}; };
@ -77,7 +76,7 @@
fee: 10, fee: 10,
duration: 1000, duration: 1000,
tokenLossRatio: 1, tokenLossRatio: 1,
// authorStake: 10, // authorStakeAmount: 10,
}, },
); );
await updateDisplayValuesAndDelay(1000); await updateDisplayValuesAndDelay(1000);

View File

@ -7,14 +7,14 @@
<div id="graph-test"></div> <div id="graph-test"></div>
</body> </body>
<script type="module"> <script type="module">
import { Box } from "/../classes/box.js"; import { Box } from '../classes/box.js';
import { Scene } from "/../classes/scene.js"; import { Scene } from '../classes/scene.js';
import { Graph } from "/../classes/graph.js"; import { Graph } from '../classes/graph.js';
const rootElement = document.getElementById("graph-test"); const rootElement = document.getElementById('graph-test');
const rootBox = new Box("rootBox", rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
window.scene = new Scene("Graph test", rootBox); window.scene = new Scene('Graph test', rootBox);
window.graph = new Graph(); window.graph = new Graph();
@ -29,5 +29,5 @@
addVertex(); addVertex();
addVertex(); addVertex();
window.graph.addEdge("e1", 0, 1); window.graph.addEdge('e1', 0, 1);
</script> </script>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<head>
<title>Reputation test</title>
<link type="text/css" rel="stylesheet" href="/index.css" />
</head>
<body>
<div id="test"></div>
</body>
<script type="module">
import { Box } from '../classes/box.js';
import { Scene } from '../classes/scene.js';
import { ValidationPool } from '../classes/validation-pool.js';
import { TokenHolder } from '../classes/token-holder.js';
import { ReputationToken } from '../classes/reputation-token.js';
import { delay } from '../util.js';
const DEFAULT_DELAY_INTERVAL = 500;
const rootElement = document.getElementById('forum-test');
const rootBox = new Box('rootBox', rootElement).flex();
const scene = (window.scene = new Scene('Forum test', rootBox));
scene.withSequenceDiagram();
scene.withFlowchart();
const pool = new ValidationPool();
const repToken = new ReputationToken();
// const tokenMinter = new TokenHolder('TokenMinter', scene);
await delay(DEFAULT_DELAY_INTERVAL);
</script>

View File

@ -7,43 +7,45 @@
<div id="validation-pool"></div> <div id="validation-pool"></div>
</body> </body>
<script type="module"> <script type="module">
import { Box } from "/../classes/box.js"; import { Box } from '../classes/box.js';
import { Scene } from "/../classes/scene.js"; import { Scene } from '../classes/scene.js';
import { Expert } from "/../classes/expert.js"; import { Expert } from '../classes/expert.js';
import { Bench } from "/../classes/bench.js"; import { Bench } from '../classes/bench.js';
import { Forum } from "/../classes/forum.js"; import { Forum } from '../classes/forum.js';
import { delay } from "/util.js"; import { PostContent } from '../classes/post-content.js';
import { delay } from '../util.js';
const rootElement = document.getElementById("validation-pool"); const rootElement = document.getElementById('validation-pool');
const rootBox = new Box("rootBox", rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
const scene = (window.scene = new Scene("Validation Pool test", rootBox)); const scene = (window.scene = new Scene('Validation Pool test', rootBox));
await scene.withSequenceDiagram(); await scene.withSequenceDiagram();
const expert1 = (window.expert1 = await new Expert( const expert1 = (window.expert1 = await new Expert(
"Expert1", 'Expert1',
scene scene,
).initialize()); ).initialize());
const expert2 = (window.expert2 = await new Expert( const expert2 = (window.expert2 = await new Expert(
"Expert2", 'Expert2',
scene scene,
).initialize()); ).initialize());
const bench = (window.bench = new Bench(undefined, "Bench", scene)); const forum = (window.forum = new Forum('Forum', scene));
const bench = (window.bench = new Bench(forum, 'Bench', scene));
const updateDisplayValues = async () => { const updateDisplayValues = async () => {
await expert1.setValue( await expert1.setValue(
"rep", 'rep',
bench.reputations.getTokens(expert1.reputationPublicKey) bench.reputation.valueOwnedBy(expert1.reputationPublicKey),
); );
await expert2.setValue( await expert2.setValue(
"rep", 'rep',
bench.reputations.getTokens(expert2.reputationPublicKey) bench.reputation.valueOwnedBy(expert2.reputationPublicKey),
); );
await bench.setValue("total rep", bench.getTotalReputation()); await bench.setValue('total rep', bench.reputation.getTotal());
// With params.lockingTimeExponent = 0 and params.activeVoterThreshold = null, // With params.lockingTimeExponent = 0 and params.activeVoterThreshold = null,
// these next 3 propetries are all equal to total rep // these next 3 propetries are all equal to total rep
// await bench.setValue('available rep', bench.getTotalAvailableReputation()); // await bench.setValue('available rep', bench.reputation.getTotalAvailable());
// await bench.setValue('active rep', bench.getTotalActiveReputation()); // await bench.setValue('active rep', bench.getActiveReputation());
// await bench.setValue('active available rep', bench.getTotalActiveAvailableReputation()); // await bench.setValue('active available rep', bench.getActiveAvailableReputation());
await scene.sequence.render(); await scene.sequence.render();
}; };
@ -52,12 +54,11 @@
// First expert can self-approve // First expert can self-approve
{ {
const pool = await expert1.initiateValidationPool(bench, { const { pool } = await expert1.submitPostWithFee(bench, forum, new PostContent(), {
fee: 7, fee: 7,
duration: 1000, duration: 1000,
tokenLossRatio: 1, tokenLossRatio: 1,
}); });
await expert1.revealIdentity(pool);
// Attempting to evaluate winning conditions before the duration has expired // Attempting to evaluate winning conditions before the duration has expired
// should result in an exception // should result in an exception
try { try {
@ -65,56 +66,51 @@
} catch (e) { } catch (e) {
if (e.message.match(/Validation pool duration has not yet elapsed/)) { if (e.message.match(/Validation pool duration has not yet elapsed/)) {
console.log( console.log(
"Caught expected error: Validation pool duration has not yet elapsed" 'Caught expected error: Validation pool duration has not yet elapsed',
); );
} else { } else {
console.error("Unexpected error"); console.error('Unexpected error');
throw e; throw e;
} }
} }
await delay(1000); await delay(1000);
await pool.evaluateWinningConditions(); // Stake passes await pool.evaluateWinningConditions(); // Vote passes
await updateDisplayValues(); await updateDisplayValues();
await delay(1000); await delay(1000);
} }
// Failure example: second expert can not self-approve // Failure example: second expert can not self-approve
try { try {
const pool = await expert2.initiateValidationPool(bench, { const { pool } = await expert2.submitPostWithFee(bench, forum, new PostContent(), {
fee: 1, fee: 1,
duration: 1000, duration: 1000,
tokenLossRatio: 1, tokenLossRatio: 1,
}); });
await expert2.revealIdentity(pool);
await delay(1000); await delay(1000);
await pool.evaluateWinningConditions(); // Quorum not met! await pool.evaluateWinningConditions(); // Quorum not met!
await updateDisplayValues(); await updateDisplayValues();
await delay(1000); await delay(1000);
} catch (e) { } catch (e) {
if (e.message.match(/Quorum is not met/)) { if (e.message.match(/Quorum is not met/)) {
console.log("Caught expected error: Quorum not met"); console.log('Caught expected error: Quorum not met');
} else { } else {
console.error("Unexpected error"); console.error('Unexpected error');
throw e; throw e;
} }
} }
// Second expert must be approved by first expert // Second expert must be approved by first expert
{ {
const pool = await expert2.initiateValidationPool(bench, { const { pool } = await expert2.submitPostWithFee(bench, forum, new PostContent(), {
fee: 1, fee: 1,
duration: 1000, duration: 1000,
tokenLossRatio: 1, tokenLossRatio: 1,
anonymous: true,
}); });
await expert1.stake(pool, { await expert1.stake(pool, {
position: true, position: true,
amount: 4, amount: 4,
lockingTime: 0, lockingTime: 0,
anonymous: true,
}); });
await expert1.revealIdentity(pool);
await expert2.revealIdentity(pool);
await delay(1000); await delay(1000);
await pool.evaluateWinningConditions(); // Stake passes await pool.evaluateWinningConditions(); // Stake passes
await updateDisplayValues(); await updateDisplayValues();