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';
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() { }

View File

@ -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();

View File

@ -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,

View File

@ -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

View File

@ -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) {

View File

@ -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);

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 { 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;
}
}

View File

@ -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;

View File

@ -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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,6 @@
const name = `Expert${index + 1}`;
const expert = await new Expert(name, scene).initialize();
experts.push(expert);
// bench.reputations.addTokens(expert.reputationPublicKey, 50);
return expert;
};
@ -53,10 +52,10 @@
for (const expert of experts) {
await expert.setValue(
'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());
};
@ -77,7 +76,7 @@
fee: 10,
duration: 1000,
tokenLossRatio: 1,
// authorStake: 10,
// authorStakeAmount: 10,
},
);
await updateDisplayValuesAndDelay(1000);

View File

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