From beb1a069d74102fa0c1e74e93cff88ba7cfdfb74 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Fri, 11 Nov 2022 16:52:57 -0600 Subject: [PATCH] Validation pool initial implementation --- forum-network/public/basic.html | 9 + forum-network/public/{index.js => basic.js} | 5 +- forum-network/public/classes/action.js | 15 ++ forum-network/public/classes/actor.js | 16 -- forum-network/public/classes/box.js | 57 +++++ forum-network/public/classes/display-value.js | 42 ---- forum-network/public/classes/forum-node.js | 3 +- forum-network/public/classes/forum-view.js | 9 +- forum-network/public/classes/graph.js | 5 + forum-network/public/classes/member.js | 22 +- forum-network/public/classes/scene.js | 20 +- .../public/classes/validation-pool.js | 214 ++++++++++++++++++ forum-network/public/forum-test.js | 2 +- forum-network/public/graph-test.js | 2 +- forum-network/public/index.css | 8 +- forum-network/public/index.html | 14 +- .../public/validation-pool-test.html | 9 + forum-network/public/validation-pool-test.js | 24 ++ 18 files changed, 399 insertions(+), 77 deletions(-) create mode 100644 forum-network/public/basic.html rename forum-network/public/{index.js => basic.js} (97%) create mode 100644 forum-network/public/classes/action.js create mode 100644 forum-network/public/classes/box.js create mode 100644 forum-network/public/classes/validation-pool.js create mode 100644 forum-network/public/validation-pool-test.html create mode 100644 forum-network/public/validation-pool-test.js diff --git a/forum-network/public/basic.html b/forum-network/public/basic.html new file mode 100644 index 0000000..f0a81f1 --- /dev/null +++ b/forum-network/public/basic.html @@ -0,0 +1,9 @@ + + + Forum Network + + + + +
+ diff --git a/forum-network/public/index.js b/forum-network/public/basic.js similarity index 97% rename from forum-network/public/index.js rename to forum-network/public/basic.js index 3d36526..a4fb47b 100644 --- a/forum-network/public/index.js +++ b/forum-network/public/basic.js @@ -1,4 +1,7 @@ -const rootElement = document.getElementById('forum-network'); +import { Box } from "./classes/box.js"; +import { Scene } from "./classes/scene.js"; + +const rootElement = document.getElementById('basic'); const rootBox = new Box('rootBox', rootElement).flex(); function randomDelay(min, max) { diff --git a/forum-network/public/classes/action.js b/forum-network/public/classes/action.js new file mode 100644 index 0000000..7f559bc --- /dev/null +++ b/forum-network/public/classes/action.js @@ -0,0 +1,15 @@ +export class Action { + constructor(name, scene) { + this.name = name; + this.scene = scene; + } + + log(src, dest, msg, obj, symbol = '->>') { + const logObj = false; + this.scene.log( + `${src.name} ${symbol} ${dest.name} : ${this.name} ${msg ?? ''} ${ + logObj && obj ? JSON.stringify(obj) : '' + }`, + ); + } +} diff --git a/forum-network/public/classes/actor.js b/forum-network/public/classes/actor.js index 4b7aa84..86f3eb2 100644 --- a/forum-network/public/classes/actor.js +++ b/forum-network/public/classes/actor.js @@ -1,19 +1,3 @@ -export class Action { - constructor(name, scene) { - this.name = name; - this.scene = scene; - } - - log(src, dest, msg, obj, symbol = '->>') { - const logObj = false; - this.scene.log( - `${src.name} ${symbol} ${dest.name} : ${this.name} ${msg ?? ''} ${ - logObj && obj ? JSON.stringify(obj) : '' - }`, - ); - } -} - export class Actor { constructor(name, scene) { this.name = name; diff --git a/forum-network/public/classes/box.js b/forum-network/public/classes/box.js new file mode 100644 index 0000000..399a6c1 --- /dev/null +++ b/forum-network/public/classes/box.js @@ -0,0 +1,57 @@ +import {DisplayValue} from "./display-value.js"; + +export class Box { + constructor(name, parentEl, elementType = 'div') { + this.name = name; + this.el = document.createElement(elementType); + this.el.classList.add('box'); + this.el.setAttribute('box-name', name); + if (parentEl) { + parentEl.appendChild(this.el); + } + } + + flex() { + this.el.classList.add('flex'); + return this; + } + + monospace() { + this.el.classList.add('monospace'); + return this; + } + + addClass(className) { + this.el.classList.add(className); + return this; + } + + addBox(name, elementType) { + const box = new Box(name, null, elementType); + this.el.appendChild(box.el); + return box; + } + + addDisplayValue(value) { + const box = this.addBox(value.name).flex(); + return new DisplayValue(value, box); + } + + setInnerHTML(html) { + this.el.innerHTML = html; + return this; + } + + getInnerText() { + return this.el.innerText; + } + + setId(id) { + this.el.id = id || this.name; + return this; + } + + getId() { + return this.el.id; + } +} diff --git a/forum-network/public/classes/display-value.js b/forum-network/public/classes/display-value.js index 265911f..88c9173 100644 --- a/forum-network/public/classes/display-value.js +++ b/forum-network/public/classes/display-value.js @@ -21,45 +21,3 @@ export class DisplayValue { return this.value; } } - -export class Box { - constructor(name, parentEl) { - this.el = document.createElement('div'); - this.el.classList.add('box'); - this.el.setAttribute('box-name', name); - if (parentEl) { - parentEl.appendChild(this.el); - } - } - - flex() { - this.el.classList.add('flex'); - return this; - } - - monospace() { - this.el.classList.add('monospace'); - return this; - } - - addClass(className) { - this.el.classList.add(className); - return this; - } - - addBox(name) { - const box = new Box(name); - this.el.appendChild(box.el); - return box; - } - - addDisplayValue(value) { - const box = this.addBox(value.name).flex(); - return new DisplayValue(value, box); - } - - setInnerHTML(html) { - this.el.innerHTML = html; - return this; - } -} diff --git a/forum-network/public/classes/forum-node.js b/forum-network/public/classes/forum-node.js index 731fd57..0c7154a 100644 --- a/forum-network/public/classes/forum-node.js +++ b/forum-network/public/classes/forum-node.js @@ -1,4 +1,5 @@ -import { Actor, Action } from './actor.js'; +import { Actor } from './actor.js'; +import { Action } from './action.js'; import { Message, PostMessage, PeerMessage } from './message.js'; import { CryptoUtil } from './crypto.js'; import { ForumView } from './forum-view.js'; diff --git a/forum-network/public/classes/forum-view.js b/forum-network/public/classes/forum-view.js index 3abb07a..2e63cca 100644 --- a/forum-network/public/classes/forum-view.js +++ b/forum-network/public/classes/forum-view.js @@ -26,8 +26,12 @@ export class ForumView { this.authors = new Map(); } - getReputation(publicKey) { - return this.reputations.get(publicKey); + getReputation(id) { + return this.reputations.get(id); + } + + setReputation(id, reputation) { + this.reputations.set(id, reputation); } incrementReputation(publicKey, increment, reason) { @@ -96,6 +100,7 @@ export class ForumView { console.log('distributeNonbindingReputation', { post, amount, depth }); // Some of the incoming reputation goes to this post post.reputation += amount * (1 - this.citationFraction); + this.setReputation(post.id, post.reputation); // Some of the incoming reputation gets distributed among cited posts const distributeAmongCitations = amount * this.citationFraction; diff --git a/forum-network/public/classes/graph.js b/forum-network/public/classes/graph.js index dfb68fc..7e1a61d 100644 --- a/forum-network/public/classes/graph.js +++ b/forum-network/public/classes/graph.js @@ -16,6 +16,11 @@ export class Edge { this.data = data; } } + +export class CategorizedEdges { + +} + export class Graph { constructor() { this.vertices = new Map(); diff --git a/forum-network/public/classes/member.js b/forum-network/public/classes/member.js index 4a9591a..e6bd126 100644 --- a/forum-network/public/classes/member.js +++ b/forum-network/public/classes/member.js @@ -1,4 +1,5 @@ -import { Actor, Action } from './actor.js'; +import { Actor } from './actor.js'; +import { Action } from './action.js'; import { PostMessage } from './message.js'; import { CryptoUtil } from './crypto.js'; @@ -7,7 +8,11 @@ export class Member extends Actor { super(name, scene); this.actions = { submitPost: new Action('submit post', scene), + initiateVote: new Action('initiate vote', scene), + castVote: new Action('cast vote', scene), + revealIdentity: new Action('reveal identity', scene), }; + this.votes = new Map(); } async initialize() { @@ -23,4 +28,19 @@ export class Member extends Actor { // For now, directly call forumNode.receiveMessage(); await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON())); } + + async castVote(validationPool, voteId, position, stake) { + const signingKey = await CryptoUtil.generateSigningKey(); + this.votes.set(voteId, {signingKey}); + // TODO: signed CastVoteMessage + this.actions.castVote.log(this, validationPool); + validationPool.castVote(voteId, signingKey.publicKey, position, stake); + } + + async revealIdentity(validationPool, voteId) { + const {signingKey} = this.votes.get(voteId); + // TODO: signed RevealIdentityMessage + this.actions.revealIdentity.log(this, validationPool); + validationPool.revealIdentity(voteId, signingKey.publicKey, this.keyPair.publicKey); + } } diff --git a/forum-network/public/classes/scene.js b/forum-network/public/classes/scene.js index 8712714..d90be0e 100644 --- a/forum-network/public/classes/scene.js +++ b/forum-network/public/classes/scene.js @@ -1,4 +1,6 @@ -import { Actor, Action } from './actor.js'; +import { Actor } from './actor.js'; +import { Action } from './action.js'; +// import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.mjs'; export class Scene { constructor(name, rootBox) { @@ -9,6 +11,9 @@ export class Scene { this.displayValuesBox = this.box.addBox(`${this.name}-values`); this.box.addBox('Spacer').setInnerHTML(' '); this.logBox = this.box.addBox(`${this.name}-log`); + // this.seqDiagramContainer = this.box.addBox(`${this.name}-seq-diagram-container`); + // this.seqDiagramBox = this.box.addBox(`${this.name}-seq-diagram`); + // mermaid.mermaidAPI.initialize({ startOnLoad: false }); } addActor(name) { @@ -30,4 +35,17 @@ export class Scene { this.logBox.addBox().setInnerHTML(msg).monospace(); return this; } + + // async renderSequenceDiagram() { + // await mermaid.mermaidAPI.render( + // `${this.name}-seq-diagram-element`, + // this.logBox.getInnerText(), + // this.insertSvg, + // this.seqDiagramContainer.el + // ); + // } + + // insertSvg (svgCode) { + // this.seqDiagramBox.setInnerHTML(svgCode); + // }; } diff --git a/forum-network/public/classes/validation-pool.js b/forum-network/public/classes/validation-pool.js new file mode 100644 index 0000000..e9565ad --- /dev/null +++ b/forum-network/public/classes/validation-pool.js @@ -0,0 +1,214 @@ +import { Actor } from "./actor.js"; +import { Action } from "./action.js"; + +const params = { + mintingRatio: 1, // c1 + stakeForWin: 0.5, // c2 + stakeForAuthor: 0.5, // c3 + winningRatio: 0.5, // c4 + quorum: 1, // c5 + activeVoterThreshold: null, // c6 + voteDuration: { // c7 + min: 0, + max: null, + }, + // NOTE: c8 is the token loss ratio, which is specified as a runtime argument + contentiousDebate: { + period: 5000, // c9 + stages: 3, // c10 + }, + lockingTimeExponent: 0, // c11 +}; + +function getTokenLossRatio(elapsed) { + let stageDuration = params.contentiousDebate.period / 2; + let stage = 0; + let t = 0; + while (true) { + t += stageDuration; + stageDuration /= 2; + if (t > elapsed) { + break; + } + stage += 1; + if (stage >= params.contentiousDebate.stages - 1) { + break; + } + } + return stage / (params.contentiousDebate.stages - 1); +} + +class Voter { + constructor(reputationPublicKey) { + this.reputationPublicKey = reputationPublicKey; + this.voteHistory = []; + this.reputation = 0; + this.dateLastVote = null; + } + + addVoteRecord(vote) { + this.voteHistory.push(vote); + if (!this.dateLastVote || vote.dateStart > this.dateLastVote) { + this.dateLastVote = vote.dateStart; + } + } + + getReputation() { + return this.reputation; + } +} + +class Vote { + constructor(validationPool, {fee, duration, tokenLossRatio, contentiousDebate = false}) { + if (tokenLossRatio < 0 || tokenLossRatio > 1) { + throw new Error(`Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}`) + } + if (duration < params.voteDuration.min || duration > params.voteDuration.max) { + throw new Error(`Duration must be in the range [${params.voteDuration.min}, ${params.voteDuration.max ?? 'Inf'}]; got ${duration}`); + } + this.votes = new Map(); + this.voters = new Map(); + this.validationPool = validationPool; + this.id = window.crypto.randomUUID(); + this.dateStart = new Date(); + this.fee = fee; + this.duration = duration; + this.tokenLossRatio = tokenLossRatio; + this.contentiousDebate = contentiousDebate; + + this.tokens = { + win: fee * params.mintingRatio * params.stakeForWin, + lose: fee * params.mintingRatio * (1 - params.stakeForWin), + author: fee * params.mintingRatio * params.stakeForAuthor, + } + } + + castVote(signingPublicKey, position, stake, lockingTime) { + if (this.duration && new Date() - this.dateStart > this.duration) { + throw new Error(`Vote ${this.id} has expired, no new votes may be cast`); + } + this.votes.set(signingPublicKey, { position, stake, lockingTime }); + } + + revealIdentity(signingPublicKey, voter) { + if (!this.votes.get(signingPublicKey)) { + throw new Error("Must vote before revealing identity"); + } + this.voters.set(signingPublicKey, voter); + if (this.votes.size === this.voters.size) { + // All voters have revealed their reputation public keys + // Now we can evaluate winning conditions + this.applyTokenLocking(); + this.evaluateWinningConditions(); + } + } + + getTokenLossRatio() { + if (!this.contentiousDebate) { + return this.tokenLossRatio; + } + const elapsed = new Date() - this.dateStart; + let stageDuration = params.contentiousDebate.period / 2; + let stage = 0; + let t = 0; + while (true) { + t += stageDuration; + stageDuration /= 2; + if (t > elapsed) { + break; + } + stage += 1; + if (stage >= params.contentiousDebate.stages - 1) { + break; + } + } + 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. + // TODO: Implement token locking + } + + evaluateWinningConditions() { + let upvotes = 0; + let downvotes = 0; + + for (const {position, stake, lockingTime} of this.votes.values()) { + const value = stake * Math.pow(lockingTime, params.lockingTimeExponent); + if (position === true) { + upvotes += value; + } else { + downvotes += value; + } + } + + const activeVoterCount = this.validationPool.countActiveVoters(); + const votePasses = upvotes >= params.winningRatio * downvotes; + const quorumMet = upvotes + downvotes >= params.quorum * activeVoterCount; + + if (votePasses && quorumMet) { + + } + } + + listVoters() { + return Array.from(this.voters.values()); + } +} + +export class ValidationPool extends Actor { + constructor(name, scene) { + super(name, scene); + this.votes = []; + this.voters = new Map(); + + this.actions = { + initializeVote: new Action('initialize vote', scene), + }; + } + + listVotes() { + Array.from(this.votes.values()); + } + + listActiveVoters() { + const now = new Date(); + return Array.from(this.voters.values()).filter(voter => { + if (!params.activeVoterThreshold) { + return true; + } + if (!voter.dateLastVote) { + return false; + } + return now - voter.dateLastVote >= params.activeVoterThreshold; + }); + } + + countActiveVoters() { + return this.listActiveVoters().length; + } + + initiateVote({fee, duration, tokenLossRatio, contentiousDebate}) { + const vote = new Vote(this, {fee, duration, tokenLossRatio, contentiousDebate}); + this.actions.initializeVote.log(this, this); + this.votes.set(vote.id, vote); + return vote.id; + } + + castVote(voteId, signingPublicKey, position, stake, lockingTime) { + // TODO: Implement vote encryption + const vote = this.votes.get(voteId); + vote.castVote(signingPublicKey, position, stake, lockingTime); + } + + revealIdentity(voteId, signingPublicKey, reputationPublicKey) { + const vote = this.votes.get(voteId); + const voter = this.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey); + voter.addVoteRecord(vote); + this.voters.set(reputationPublicKey, voter); + vote.revealIdentity(signingPublicKey, voter); + } +} diff --git a/forum-network/public/forum-test.js b/forum-network/public/forum-test.js index 6109ffa..4957534 100644 --- a/forum-network/public/forum-test.js +++ b/forum-network/public/forum-test.js @@ -1,4 +1,4 @@ -import { Box } from './classes/display-value.js'; +import { Box } from './classes/box.js'; import { Scene } from './classes/scene.js'; import { Post } from './classes/post.js'; import { Member } from './classes/member.js'; diff --git a/forum-network/public/graph-test.js b/forum-network/public/graph-test.js index bebddf5..8c50aa3 100644 --- a/forum-network/public/graph-test.js +++ b/forum-network/public/graph-test.js @@ -1,4 +1,4 @@ -import { Box } from './classes/display-value.js'; +import { Box } from './classes/box.js'; import { Scene } from './classes/scene.js'; import { Graph } from './classes/graph.js'; diff --git a/forum-network/public/index.css b/forum-network/public/index.css index c038721..81be757 100644 --- a/forum-network/public/index.css +++ b/forum-network/public/index.css @@ -1,24 +1,24 @@ .box { - // border: 1px #eee solid; + /* border: 1px #eee solid; */ width: fit-content; font-family: sans-serif; font-size: 12pt; } .box .name { - width: 10em; + width: 12em; font-weight: bold; text-align: right; margin-right: 6pt; } .box .value { width: fit-content; - // border: 0px; + /* border: 0px; */ } .flex { display: flex; } .monospace { - // border: 0px; + /* border: 0px; */ font-family: monospace; font-size: 11pt; } diff --git a/forum-network/public/index.html b/forum-network/public/index.html index 71c301f..f58a166 100644 --- a/forum-network/public/index.html +++ b/forum-network/public/index.html @@ -1,14 +1,14 @@ Forum Network - - - - - - + -
+ diff --git a/forum-network/public/validation-pool-test.html b/forum-network/public/validation-pool-test.html new file mode 100644 index 0000000..8e13539 --- /dev/null +++ b/forum-network/public/validation-pool-test.html @@ -0,0 +1,9 @@ + + + Forum + + + + +
+ diff --git a/forum-network/public/validation-pool-test.js b/forum-network/public/validation-pool-test.js new file mode 100644 index 0000000..1a16dc7 --- /dev/null +++ b/forum-network/public/validation-pool-test.js @@ -0,0 +1,24 @@ +import { Box } from './classes/box.js'; +import { Scene } from './classes/scene.js'; +import { Member } from './classes/member.js'; +import { ValidationPool } from './classes/validation-pool.js'; + +const rootElement = document.getElementById('validation-pool'); +const rootBox = new Box('rootBox', rootElement).flex(); + +const scene = window.scene = new Scene('Validation Pool test', rootBox).log('sequenceDiagram'); + +const pool = window.validationPool = new ValidationPool("validationPool", scene); + +const member1 = window.member1 = await new Member("member1", scene).initialize(); +const member2 = window.member2 = await new Member("member2", scene).initialize(); + +const voteId = pool.initiateVote({fee: 1, duration: 1, isBinding: false}); + +await member1.castVote(pool, voteId, true, 50); +await member2.castVote(pool, voteId, true, 50); + +await member1.revealIdentity(pool, voteId); +await member2.revealIdentity(pool, voteId); + +// await scene.renderSequenceDiagram();