From b644d6c119585759a6ee09648ab8c2e37da67bc6 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Thu, 5 Jan 2023 01:19:14 -0600 Subject: [PATCH] Progress in forum implementation --- forum-network/notes/notes.md | 8 ++ forum-network/src/classes/availability.js | 9 +- forum-network/src/classes/business.js | 9 +- forum-network/src/classes/forum.js | 68 +++++----- forum-network/src/classes/message.js | 2 +- forum-network/src/classes/params.js | 7 +- .../src/classes/{post.js => post-content.js} | 0 forum-network/src/classes/scene.js | 3 + forum-network/src/classes/validation-pool.js | 123 +++++++++--------- forum-network/src/tests/debounce.html | 25 ---- forum-network/src/tests/forum-network.html | 2 +- forum-network/src/tests/forum.html | 42 ++++-- forum-network/src/tests/graph.html | 33 ----- forum-network/src/tests/mermaid.html | 28 ---- forum-network/src/tests/validation-pool.html | 3 +- 15 files changed, 162 insertions(+), 200 deletions(-) rename forum-network/src/classes/{post.js => post-content.js} (100%) delete mode 100644 forum-network/src/tests/debounce.html delete mode 100644 forum-network/src/tests/graph.html delete mode 100644 forum-network/src/tests/mermaid.html diff --git a/forum-network/notes/notes.md b/forum-network/notes/notes.md index 16b5053..147c7dd 100644 --- a/forum-network/notes/notes.md +++ b/forum-network/notes/notes.md @@ -75,3 +75,11 @@ We can support dynamic reevaluation if the reputation contract It can verify a signature... --- + +Tokens staked for and against a post. + +--- + +Token loss ratio + +--- diff --git a/forum-network/src/classes/availability.js b/forum-network/src/classes/availability.js index 7614c70..4b108ed 100644 --- a/forum-network/src/classes/availability.js +++ b/forum-network/src/classes/availability.js @@ -28,28 +28,33 @@ export class Availability extends Actor { }; } - register(reputationPublicKey, stake) { + register(reputationPublicKey, stake, __duration) { + // TODO: expire after duration // ? Is a particular stake amount required? const worker = this.workers.get(reputationPublicKey) ?? new Worker(reputationPublicKey); if (!worker.available) { throw new Error('Worker is already registered and busy. Cannot increase stake.'); } worker.stake += stake; - // ? Interact with Bench contract to encumber reputation? + // TODO: Interact with Bench contract to encumber reputation? this.workers.set(reputationPublicKey, worker); } + // unregister() { } + get availableWorkers() { return Array.from(this.workers.values()).filter(({ available }) => !!available); } async assignWork(requestId) { // Get random worker + // TODO: Probability proportional to stakes const index = Math.floor(Math.random() * this.availableWorkers.length); const worker = this.availableWorkers[index]; worker.available = false; worker.assignedRequestId = requestId; // TODO: Notify assignee + return worker; } async getAssignedWork(reputationPublicKey) { diff --git a/forum-network/src/classes/business.js b/forum-network/src/classes/business.js index e9cffbd..6dbb104 100644 --- a/forum-network/src/classes/business.js +++ b/forum-network/src/classes/business.js @@ -1,7 +1,7 @@ import { Action } from './action.js'; import { Actor } from './actor.js'; import { CryptoUtil } from './crypto.js'; -import { PostContent } from './post.js'; +import { PostContent } from './post-content.js'; class Request { constructor(fee, content) { @@ -34,7 +34,7 @@ export class Business extends Actor { const request = new Request(fee, content); this.requests.set(request.id, request); this.actions.assignWork.log(this, this.availability); - await this.availability.assignWork(request.id); + this.worker = await this.availability.assignWork(request.id); return request.id; } @@ -55,19 +55,20 @@ export class Business extends Actor { workEvidence, }); this.actions.submitPost.log(this, this.forum); - await this.forum.addPost(reputationPublicKey, post); + const postId = await this.forum.addPost(reputationPublicKey, post); // Initiate a validation pool for this work evidence. // Validation pool supports secret ballots but we aren't using that here, since we want // the post to be attributable to the reputation holder. this.actions.initiateValidationPool.log(this, this.bench); const pool = await this.bench.initiateValidationPool({ - postId: post.id, + postId, fee: request.fee, duration, tokenLossRatio, signingPublicKey: reputationPublicKey, anonymous: false, + authorStake: this.worker.stake, }); // When the validation pool concludes, diff --git a/forum-network/src/classes/forum.js b/forum-network/src/classes/forum.js index da1c0f4..74cc183 100644 --- a/forum-network/src/classes/forum.js +++ b/forum-network/src/classes/forum.js @@ -1,18 +1,26 @@ import { Actor } from './actor.js'; import { Graph } from './graph.js'; +import { Action } from './action.js'; import { CryptoUtil } from './crypto.js'; import params from './params.js'; -import { Action } from './action.js'; class Post extends Actor { - constructor(forum, authorId, postContent) { + 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.authorId = authorId; + this.authorPublicKey = authorPublicKey; this.value = 0; this.citations = postContent.citations; + this.totalCitationWeight = this.citations.reduce((total, { weight }) => total += weight, 0); + if (this.totalCitationWeight > params.revaluationLimit) { + throw new Error('Post total citation weight exceeds revaluation limit ' + + `(${this.totalCitationWeight} > ${params.revaluationLimit})`); + } + if (this.citations.some(({ weight }) => Math.abs(weight) > 1)) { + throw new Error('Each citation weight must be in the range [-1, 1]'); + } } setPostValue(value) { @@ -55,34 +63,32 @@ export class Forum extends Actor { return this.posts.getVertices(); } - distributeReputation(post, amount, depth = 0) { - console.log('distributeReputation', { post, amount, depth }); - // Add the given value to the current post - post.value += amount; - // Distribute a fraction of the added value among cited posts - const distributeAmongCitations = amount * params.citationFraction; - - // Here we allow an arbitrary scale for the amount of the citations. - // We normalize by dividing each by the total. - const totalWeight = post.citations - ?.map(({ weight }) => weight) - .reduce((acc, cur) => (acc += cur), 0); - - for (const { - to: citedPostId, - data: { weight }, - } of post.getEdges('citation', true)) { - const citedPost = this.getPost(citedPostId); - if (!citedPost) { - throw new Error( - `Post ${post.postId} cites unknown post ${citedPostId}`, - ); - } - this.distributeReputation( - citedPost, - (weight / totalWeight) * distributeAmongCitations, - depth + 1, - ); + propagateValue(postId, increment, depth = 0) { + if (depth > params.maxPropagationDepth) { + return []; } + const post = this.getPost(postId); + const rewards = new Map(); + const addReward = (id, value) => rewards.set(id, (rewards.get(id) ?? 0) + value); + const addRewards = (r) => { + for (const [id, value] of r) { + addReward(id, value); + } + }; + + // Increment the value of the given post + const postValue = post.getPostValue(); + post.setPostValue(postValue + increment); + + // Award reputation to post author + console.log('reward for post author', post.authorPublicKey, increment); + addReward(post.authorPublicKey, increment); + + // Recursively distribute reputation to citations, according to weights + for (const { postId: citedPostId, weight } of post.citations) { + addRewards(this.propagateValue(citedPostId, weight * increment, depth + 1)); + } + + return rewards; } } diff --git a/forum-network/src/classes/message.js b/forum-network/src/classes/message.js index e0ed6c8..aa31b19 100644 --- a/forum-network/src/classes/message.js +++ b/forum-network/src/classes/message.js @@ -1,5 +1,5 @@ import { CryptoUtil } from './crypto.js'; -import { PostContent } from './post.js'; +import { PostContent } from './post-content.js'; export class Message { constructor(content) { diff --git a/forum-network/src/classes/params.js b/forum-network/src/classes/params.js index 6018106..d8e49bc 100644 --- a/forum-network/src/classes/params.js +++ b/forum-network/src/classes/params.js @@ -2,9 +2,9 @@ const params = { /* Validation Pool parameters */ mintingRatio: 1, // c1 stakeForWin: 0.5, // c2 - // stakeForAuthor: 0.5, // c3 - For now we keep the default that stakeForAuthor = stakeForWin + stakeForAuthor: 0.5, // c3 winningRatio: 0.5, // c4 - quorum: 0.5, // c5 + quorum: 0, // c5 activeVoterThreshold: null, // c6 voteDuration: { // c7 @@ -20,7 +20,8 @@ const params = { /* Forum parameters */ initialPostValue: () => 1, // q1 - citationFraction: 0.3, // q2 + revaluationLimit: 1, // q2 + maxPropagationDepth: 3, // q3 }; export default params; diff --git a/forum-network/src/classes/post.js b/forum-network/src/classes/post-content.js similarity index 100% rename from forum-network/src/classes/post.js rename to forum-network/src/classes/post-content.js diff --git a/forum-network/src/classes/scene.js b/forum-network/src/classes/scene.js index 4b53489..a8db134 100644 --- a/forum-network/src/classes/scene.js +++ b/forum-network/src/classes/scene.js @@ -30,6 +30,9 @@ export class Scene { primaryTextColor: '#b6b6b6', noteBkgColor: '#516f77', noteTextColor: '#cecece', + activationBkgColor: '#1d3f49', + activationBorderColor: '#569595', + signalColor: '#57747d', }, }); this.dateLastRender = null; diff --git a/forum-network/src/classes/validation-pool.js b/forum-network/src/classes/validation-pool.js index 136a268..0c2a4c0 100644 --- a/forum-network/src/classes/validation-pool.js +++ b/forum-network/src/classes/validation-pool.js @@ -53,13 +53,13 @@ export class ValidationPool extends Actor { }]; got ${duration}`, ); } + this.bench = bench; this.forum = forum; this.postId = postId; this.state = ValidationPoolStates.OPEN; this.setStatus('Open'); this.votes = new Map(); this.voters = new Map(); - this.bench = bench; this.id = CryptoUtil.randomUUID(); this.dateStart = new Date(); this.authorSigningPublicKey = signingPublicKey; @@ -70,14 +70,14 @@ export class ValidationPool extends Actor { this.contentiousDebate = contentiousDebate; this.tokensMinted = fee * params.mintingRatio; this.tokens = { - for: fee * params.mintingRatio * params.stakeForWin, - against: fee * params.mintingRatio * (1 - params.stakeForWin), + for: this.tokensMinted * params.stakeForWin, + against: this.tokensMinted * (1 - params.stakeForWin), }; - // 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 + // 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.castVote(signingPublicKey, { position: true, - stake: this.tokens.for + authorStake, + stake: this.tokensMinted * params.stakeForAuthor + authorStake, anonymous, }); } @@ -102,7 +102,7 @@ export class ValidationPool extends Actor { listVotes(position) { return new Map( - Array.from(this.votes.entries()).filter( + Array.from(this.votes).filter( ([_, vote]) => vote.position === position, ), ); @@ -148,7 +148,7 @@ export class ValidationPool extends Actor { for (const [ signingPublicKey, { stake, lockingTime }, - ] of this.votes.entries()) { + ] of this.votes) { const voter = this.voters.get(signingPublicKey); this.bench.reputations.lockTokens( voter.reputationPublicKey, @@ -185,71 +185,76 @@ export class ValidationPool extends Actor { const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; - const result = quorumMet ? votePasses : null; + const result = { + votePasses, + upvoteValue, + downvoteValue, + }; - if (result === null) { - this.setStatus('Resolved - Quorum not met'); - this.scene.log(`note over ${this.name} : Quorum not met`); - } else { + if (quorumMet) { this.setStatus(`Resolved - ${result ? 'Won' : 'Lost'}`); this.scene.log(`note over ${this.name} : ${result ? 'Win' : 'Lose'}`); this.applyTokenLocking(); this.distributeTokens(result); + } else { + this.setStatus('Resolved - Quorum not met'); + this.scene.log(`note over ${this.name} : Quorum not met`); } + this.deactivate(); this.state = ValidationPoolStates.RESOLVED; - return result; } - propagateValue(nextPost, increment) { - const postValue = nextPost.getPostValue(); - nextPost.setPostValue(postValue + increment); - for (const { postId: citedPostId, weight } of nextPost.citations) { - console.log('citedPostId', citedPostId); - const citedPost = this.forum.getPost(citedPostId); - this.propagateValue(citedPost, weight * increment); - } - } - - distributeTokens(result) { - const authorReputationPublicKey = this.anonymous - ? this.voters.get(this.authorSigningPublicKey).reputationPublicKey - : this.authorSigningPublicKey; + distributeTokens({ votePasses }) { + const rewards = new Map(); + const addReward = (id, value) => rewards.set(id, (rewards.get(id) ?? 0) + value); // TODO: Take tokenLossRatio into account + const getTotalStaked = (position) => Array.from(this.listVotes(position).values()) + .map(({ stake }) => stake) + .reduce((acc, cur) => (acc += cur), 0); + const tokensForWinners = votePasses ? (this.tokens.for + getTotalStaked(false)) + : (this.tokens.against + getTotalStaked(true)); + const winningVotes = this.listVotes(votePasses); - if (result === true) { - // Take initialValue into account - const initialPostValue = params.initialPostValue() * this.tokensMinted; - const tokensForAuthor = initialPostValue * params.stakeForWin; - const tokensForWinners = initialPostValue * (1 - params.stakeForWin); - - // Reward the author - this.bench.reputations.addTokens(authorReputationPublicKey, tokensForAuthor); - - // Reward the vote winners, in proportion to their stakes - const winningVotes = this.listVotes(result); - const totalStakes = Array.from(winningVotes.values()) - .map(({ stake }) => stake) - .reduce((acc, cur) => (acc += cur), 0); - if (!totalStakes) { - return; - } - for (const [signingPublicKey, { stake }] of winningVotes.entries()) { - const { reputationPublicKey } = this.voters.get(signingPublicKey); - const reward = (tokensForWinners * stake) / totalStakes; - this.bench.reputations.addTokens(reputationPublicKey, reward); - } - - // Update the forum post to set its (initial) value - const post = this.forum.getPost(this.postId); - - // Recursively update values of referenced posts - this.propagateValue(post, initialPostValue); - } else { - // TODO: If the vote fails, distribute tokens.author among winning voters. - throw new Error("Vote did not pass -- we don't currently handle that!"); + // Reward the winning voters, in proportion to their stakes + for (const [signingPublicKey, { stake }] of winningVotes) { + const { reputationPublicKey } = this.voters.get(signingPublicKey); + const reward = (tokensForWinners * stake) / getTotalStaked(votePasses); + addReward(reputationPublicKey, reward); + console.log('reward for winning voter', reputationPublicKey, reward); } + + const awardsFromVoting = Array.from(rewards.values()).reduce((total, value) => total += value, 0); + console.log('awardsFromVoting', awardsFromVoting); + + if (votePasses && !!this.forum) { + // Recurse through forum to determine reputation effects + const forumReputationEffects = this.forum.propagateValue(this.postId, this.tokensMinted); + for (const [id, value] of forumReputationEffects) { + addReward(id, value); + } + const awardsFromForum = Array.from(forumReputationEffects.values()).reduce((total, value) => total += value, 0); + console.log('awardsFromForum', awardsFromForum); + } + + // Allow for possible attenuation of total value of post, e.g. based on degree of contention + const initialPostValue = this.tokensMinted * params.initialPostValue(); + + // Scale all rewards so that the total is correct + // TODO: Add more precise assertions; otherwise this operation could mask errors. + const currentTotal = Array.from(rewards.values()).reduce((total, value) => total += value, 0); + console.log('currentTotal', currentTotal); + for (const [id, value] of rewards) { + rewards.set(id, (value * initialPostValue) / currentTotal); + } + + // Apply computed rewards + for (const [id, value] of rewards) { + this.bench.reputations.addTokens(id, value); + } + + console.log('pool complete'); } } diff --git a/forum-network/src/tests/debounce.html b/forum-network/src/tests/debounce.html deleted file mode 100644 index 86259ad..0000000 --- a/forum-network/src/tests/debounce.html +++ /dev/null @@ -1,25 +0,0 @@ - - - Forum Graph: Debounce test - - - -
- - diff --git a/forum-network/src/tests/forum-network.html b/forum-network/src/tests/forum-network.html index 0dd4fdd..9ff7814 100644 --- a/forum-network/src/tests/forum-network.html +++ b/forum-network/src/tests/forum-network.html @@ -9,7 +9,7 @@ diff --git a/forum-network/src/tests/graph.html b/forum-network/src/tests/graph.html deleted file mode 100644 index d905ce2..0000000 --- a/forum-network/src/tests/graph.html +++ /dev/null @@ -1,33 +0,0 @@ - - - Forum Graph - - - -
- - diff --git a/forum-network/src/tests/mermaid.html b/forum-network/src/tests/mermaid.html deleted file mode 100644 index 2c9c87b..0000000 --- a/forum-network/src/tests/mermaid.html +++ /dev/null @@ -1,28 +0,0 @@ - - - Mermaid test - - - - - -
- diff --git a/forum-network/src/tests/validation-pool.html b/forum-network/src/tests/validation-pool.html index d81fd88..c018e61 100644 --- a/forum-network/src/tests/validation-pool.html +++ b/forum-network/src/tests/validation-pool.html @@ -11,6 +11,7 @@ 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"; const rootElement = document.getElementById("validation-pool"); @@ -27,7 +28,7 @@ "Expert2", scene ).initialize()); - const bench = (window.bench = new Bench("Bench", scene)); + const bench = (window.bench = new Bench(undefined, "Bench", scene)); const updateDisplayValues = async () => { expert1.setValue(