Validation pool initial implementation

This commit is contained in:
Ladd Hoffman 2022-11-11 16:52:57 -06:00
parent 715943ec77
commit beb1a069d7
18 changed files with 399 additions and 77 deletions

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<head>
<title>Forum Network</title>
<script type="module" src="./basic.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="basic"></div>
</body>

View File

@ -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(); const rootBox = new Box('rootBox', rootElement).flex();
function randomDelay(min, max) { function randomDelay(min, max) {

View File

@ -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) : ''
}`,
);
}
}

View File

@ -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 { export class Actor {
constructor(name, scene) { constructor(name, scene) {
this.name = name; this.name = name;

View File

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

View File

@ -21,45 +21,3 @@ export class DisplayValue {
return this.value; 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;
}
}

View File

@ -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 { Message, PostMessage, PeerMessage } from './message.js';
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
import { ForumView } from './forum-view.js'; import { ForumView } from './forum-view.js';

View File

@ -26,8 +26,12 @@ export class ForumView {
this.authors = new Map(); this.authors = new Map();
} }
getReputation(publicKey) { getReputation(id) {
return this.reputations.get(publicKey); return this.reputations.get(id);
}
setReputation(id, reputation) {
this.reputations.set(id, reputation);
} }
incrementReputation(publicKey, increment, reason) { incrementReputation(publicKey, increment, reason) {
@ -96,6 +100,7 @@ export class ForumView {
console.log('distributeNonbindingReputation', { post, amount, depth }); console.log('distributeNonbindingReputation', { post, amount, depth });
// Some of the incoming reputation goes to this post // Some of the incoming reputation goes to this post
post.reputation += amount * (1 - this.citationFraction); post.reputation += amount * (1 - this.citationFraction);
this.setReputation(post.id, post.reputation);
// Some of the incoming reputation gets distributed among cited posts // Some of the incoming reputation gets distributed among cited posts
const distributeAmongCitations = amount * this.citationFraction; const distributeAmongCitations = amount * this.citationFraction;

View File

@ -16,6 +16,11 @@ export class Edge {
this.data = data; this.data = data;
} }
} }
export class CategorizedEdges {
}
export class Graph { export class Graph {
constructor() { constructor() {
this.vertices = new Map(); this.vertices = new Map();

View File

@ -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 { PostMessage } from './message.js';
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
@ -7,7 +8,11 @@ export class Member extends Actor {
super(name, scene); super(name, scene);
this.actions = { this.actions = {
submitPost: new Action('submit post', scene), 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() { async initialize() {
@ -23,4 +28,19 @@ export class Member extends Actor {
// For now, directly call forumNode.receiveMessage(); // For now, directly call forumNode.receiveMessage();
await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON())); 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);
}
} }

View File

@ -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 { export class Scene {
constructor(name, rootBox) { constructor(name, rootBox) {
@ -9,6 +11,9 @@ export class Scene {
this.displayValuesBox = this.box.addBox(`${this.name}-values`); this.displayValuesBox = this.box.addBox(`${this.name}-values`);
this.box.addBox('Spacer').setInnerHTML('&nbsp;'); this.box.addBox('Spacer').setInnerHTML('&nbsp;');
this.logBox = this.box.addBox(`${this.name}-log`); 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) { addActor(name) {
@ -30,4 +35,17 @@ export class Scene {
this.logBox.addBox().setInnerHTML(msg).monospace(); this.logBox.addBox().setInnerHTML(msg).monospace();
return this; 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);
// };
} }

View File

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

View File

@ -1,4 +1,4 @@
import { Box } from './classes/display-value.js'; import { Box } from './classes/box.js';
import { Scene } from './classes/scene.js'; import { Scene } from './classes/scene.js';
import { Post } from './classes/post.js'; import { Post } from './classes/post.js';
import { Member } from './classes/member.js'; import { Member } from './classes/member.js';

View File

@ -1,4 +1,4 @@
import { Box } from './classes/display-value.js'; import { Box } from './classes/box.js';
import { Scene } from './classes/scene.js'; import { Scene } from './classes/scene.js';
import { Graph } from './classes/graph.js'; import { Graph } from './classes/graph.js';

View File

@ -1,24 +1,24 @@
.box { .box {
// border: 1px #eee solid; /* border: 1px #eee solid; */
width: fit-content; width: fit-content;
font-family: sans-serif; font-family: sans-serif;
font-size: 12pt; font-size: 12pt;
} }
.box .name { .box .name {
width: 10em; width: 12em;
font-weight: bold; font-weight: bold;
text-align: right; text-align: right;
margin-right: 6pt; margin-right: 6pt;
} }
.box .value { .box .value {
width: fit-content; width: fit-content;
// border: 0px; /* border: 0px; */
} }
.flex { .flex {
display: flex; display: flex;
} }
.monospace { .monospace {
// border: 0px; /* border: 0px; */
font-family: monospace; font-family: monospace;
font-size: 11pt; font-size: 11pt;
} }

View File

@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
<title>Forum Network</title> <title>Forum Network</title>
<script src="./classes/display-value.js"></script> <script type="module" src="./index.js" defer></script>
<script src="./classes/actor.js"></script>
<script src="./classes/scene.js"></script>
<script src="./classes/reputation.js"></script>
<script src="./classes/forum.js"></script>
<script src="./index.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" /> <link type="text/css" rel="stylesheet" href="./index.css" />
</head> </head>
<body> <body>
<div id="forum-network"></div> <ul>
<li><a href="./basic.html">Basic test</a></li>
<li><a href="./forum-test.html">Forum test</a></li>
<li><a href="./graph-test.html">Graph test</a></li>
<li><a href="./validation-pool-test.html">Validation Pool test</a></li>
</ul>
</body> </body>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<head>
<title>Forum</title>
<script type="module" src="./validation-pool-test.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="validation-pool"></div>
</body>

View File

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