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