Progress in forum implementation

This commit is contained in:
Ladd Hoffman 2023-01-05 01:19:14 -06:00
parent cfa445471f
commit b644d6c119
15 changed files with 162 additions and 200 deletions

View File

@ -75,3 +75,11 @@ We can support dynamic reevaluation if the reputation contract
It can verify a signature... It can verify a signature...
--- ---
Tokens staked for and against a post.
---
Token loss ratio
---

View File

@ -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? // ? Is a particular stake amount required?
const worker = this.workers.get(reputationPublicKey) ?? new Worker(reputationPublicKey); const worker = this.workers.get(reputationPublicKey) ?? new Worker(reputationPublicKey);
if (!worker.available) { if (!worker.available) {
throw new Error('Worker is already registered and busy. Cannot increase stake.'); throw new Error('Worker is already registered and busy. Cannot increase stake.');
} }
worker.stake += stake; worker.stake += stake;
// ? Interact with Bench contract to encumber reputation? // TODO: Interact with Bench contract to encumber reputation?
this.workers.set(reputationPublicKey, worker); this.workers.set(reputationPublicKey, worker);
} }
// unregister() { }
get availableWorkers() { get availableWorkers() {
return Array.from(this.workers.values()).filter(({ available }) => !!available); return Array.from(this.workers.values()).filter(({ available }) => !!available);
} }
async assignWork(requestId) { async assignWork(requestId) {
// Get random worker // Get random worker
// TODO: Probability proportional to stakes
const index = Math.floor(Math.random() * this.availableWorkers.length); const index = Math.floor(Math.random() * this.availableWorkers.length);
const worker = this.availableWorkers[index]; const worker = this.availableWorkers[index];
worker.available = false; worker.available = false;
worker.assignedRequestId = requestId; worker.assignedRequestId = requestId;
// TODO: Notify assignee // TODO: Notify assignee
return worker;
} }
async getAssignedWork(reputationPublicKey) { async getAssignedWork(reputationPublicKey) {

View File

@ -1,7 +1,7 @@
import { Action } from './action.js'; import { Action } from './action.js';
import { Actor } from './actor.js'; import { Actor } from './actor.js';
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
import { PostContent } from './post.js'; import { PostContent } from './post-content.js';
class Request { class Request {
constructor(fee, content) { constructor(fee, content) {
@ -34,7 +34,7 @@ export class Business extends Actor {
const request = new Request(fee, content); const request = new Request(fee, content);
this.requests.set(request.id, request); this.requests.set(request.id, request);
this.actions.assignWork.log(this, this.availability); this.actions.assignWork.log(this, this.availability);
await this.availability.assignWork(request.id); this.worker = await this.availability.assignWork(request.id);
return request.id; return request.id;
} }
@ -55,19 +55,20 @@ export class Business extends Actor {
workEvidence, workEvidence,
}); });
this.actions.submitPost.log(this, this.forum); 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. // Initiate a validation pool for this work evidence.
// Validation pool supports secret ballots but we aren't using that here, since we want // Validation pool supports secret ballots but we aren't using that here, since we want
// the post to be attributable to the reputation holder. // the post to be attributable to the reputation holder.
this.actions.initiateValidationPool.log(this, this.bench); this.actions.initiateValidationPool.log(this, this.bench);
const pool = await this.bench.initiateValidationPool({ const pool = await this.bench.initiateValidationPool({
postId: post.id, postId,
fee: request.fee, fee: request.fee,
duration, duration,
tokenLossRatio, tokenLossRatio,
signingPublicKey: reputationPublicKey, signingPublicKey: reputationPublicKey,
anonymous: false, anonymous: false,
authorStake: this.worker.stake,
}); });
// When the validation pool concludes, // When the validation pool concludes,

View File

@ -1,18 +1,26 @@
import { Actor } from './actor.js'; import { Actor } from './actor.js';
import { Graph } from './graph.js'; import { Graph } from './graph.js';
import { Action } from './action.js';
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
import params from './params.js'; import params from './params.js';
import { Action } from './action.js';
class Post extends Actor { class Post extends Actor {
constructor(forum, authorId, postContent) { constructor(forum, authorPublicKey, postContent) {
const index = forum.posts.countVertices(); const index = forum.posts.countVertices();
const name = `Post${index + 1}`; const name = `Post${index + 1}`;
super(name, forum.scene); super(name, forum.scene);
this.id = postContent.id ?? CryptoUtil.randomUUID(); this.id = postContent.id ?? CryptoUtil.randomUUID();
this.authorId = authorId; this.authorPublicKey = authorPublicKey;
this.value = 0; this.value = 0;
this.citations = postContent.citations; 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) { setPostValue(value) {
@ -55,34 +63,32 @@ export class Forum extends Actor {
return this.posts.getVertices(); return this.posts.getVertices();
} }
distributeReputation(post, amount, depth = 0) { propagateValue(postId, increment, depth = 0) {
console.log('distributeReputation', { post, amount, depth }); if (depth > params.maxPropagationDepth) {
// Add the given value to the current post return [];
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,
);
} }
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;
} }
} }

View File

@ -1,5 +1,5 @@
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
import { PostContent } from './post.js'; import { PostContent } from './post-content.js';
export class Message { export class Message {
constructor(content) { constructor(content) {

View File

@ -2,9 +2,9 @@ const params = {
/* Validation Pool parameters */ /* Validation Pool parameters */
mintingRatio: 1, // c1 mintingRatio: 1, // c1
stakeForWin: 0.5, // c2 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 winningRatio: 0.5, // c4
quorum: 0.5, // c5 quorum: 0, // c5
activeVoterThreshold: null, // c6 activeVoterThreshold: null, // c6
voteDuration: { voteDuration: {
// c7 // c7
@ -20,7 +20,8 @@ const params = {
/* Forum parameters */ /* Forum parameters */
initialPostValue: () => 1, // q1 initialPostValue: () => 1, // q1
citationFraction: 0.3, // q2 revaluationLimit: 1, // q2
maxPropagationDepth: 3, // q3
}; };
export default params; export default params;

View File

@ -30,6 +30,9 @@ export class Scene {
primaryTextColor: '#b6b6b6', primaryTextColor: '#b6b6b6',
noteBkgColor: '#516f77', noteBkgColor: '#516f77',
noteTextColor: '#cecece', noteTextColor: '#cecece',
activationBkgColor: '#1d3f49',
activationBorderColor: '#569595',
signalColor: '#57747d',
}, },
}); });
this.dateLastRender = null; this.dateLastRender = null;

View File

@ -53,13 +53,13 @@ export class ValidationPool extends Actor {
}]; got ${duration}`, }]; got ${duration}`,
); );
} }
this.bench = bench;
this.forum = forum; this.forum = forum;
this.postId = postId; this.postId = postId;
this.state = ValidationPoolStates.OPEN; this.state = ValidationPoolStates.OPEN;
this.setStatus('Open'); this.setStatus('Open');
this.votes = new Map(); this.votes = new Map();
this.voters = new Map(); this.voters = new Map();
this.bench = bench;
this.id = CryptoUtil.randomUUID(); this.id = CryptoUtil.randomUUID();
this.dateStart = new Date(); this.dateStart = new Date();
this.authorSigningPublicKey = signingPublicKey; this.authorSigningPublicKey = signingPublicKey;
@ -70,14 +70,14 @@ export class ValidationPool extends Actor {
this.contentiousDebate = contentiousDebate; this.contentiousDebate = contentiousDebate;
this.tokensMinted = fee * params.mintingRatio; this.tokensMinted = fee * params.mintingRatio;
this.tokens = { this.tokens = {
for: fee * params.mintingRatio * params.stakeForWin, for: this.tokensMinted * params.stakeForWin,
against: fee * params.mintingRatio * (1 - params.stakeForWin), against: this.tokensMinted * (1 - params.stakeForWin),
}; };
// tokens minted "for" the post go toward stake of author voting for their own 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 // Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
this.castVote(signingPublicKey, { this.castVote(signingPublicKey, {
position: true, position: true,
stake: this.tokens.for + authorStake, stake: this.tokensMinted * params.stakeForAuthor + authorStake,
anonymous, anonymous,
}); });
} }
@ -102,7 +102,7 @@ export class ValidationPool extends Actor {
listVotes(position) { listVotes(position) {
return new Map( return new Map(
Array.from(this.votes.entries()).filter( Array.from(this.votes).filter(
([_, vote]) => vote.position === position, ([_, vote]) => vote.position === position,
), ),
); );
@ -148,7 +148,7 @@ export class ValidationPool extends Actor {
for (const [ for (const [
signingPublicKey, signingPublicKey,
{ stake, lockingTime }, { stake, lockingTime },
] of this.votes.entries()) { ] of this.votes) {
const voter = this.voters.get(signingPublicKey); const voter = this.voters.get(signingPublicKey);
this.bench.reputations.lockTokens( this.bench.reputations.lockTokens(
voter.reputationPublicKey, voter.reputationPublicKey,
@ -185,71 +185,76 @@ export class ValidationPool extends Actor {
const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
const result = quorumMet ? votePasses : null; const result = {
votePasses,
upvoteValue,
downvoteValue,
};
if (result === null) { if (quorumMet) {
this.setStatus('Resolved - Quorum not met');
this.scene.log(`note over ${this.name} : Quorum not met`);
} else {
this.setStatus(`Resolved - ${result ? 'Won' : 'Lost'}`); this.setStatus(`Resolved - ${result ? 'Won' : 'Lost'}`);
this.scene.log(`note over ${this.name} : ${result ? 'Win' : 'Lose'}`); this.scene.log(`note over ${this.name} : ${result ? 'Win' : 'Lose'}`);
this.applyTokenLocking(); this.applyTokenLocking();
this.distributeTokens(result); this.distributeTokens(result);
} else {
this.setStatus('Resolved - Quorum not met');
this.scene.log(`note over ${this.name} : Quorum not met`);
} }
this.deactivate(); this.deactivate();
this.state = ValidationPoolStates.RESOLVED; this.state = ValidationPoolStates.RESOLVED;
return result; return result;
} }
propagateValue(nextPost, increment) { distributeTokens({ votePasses }) {
const postValue = nextPost.getPostValue(); const rewards = new Map();
nextPost.setPostValue(postValue + increment); const addReward = (id, value) => rewards.set(id, (rewards.get(id) ?? 0) + value);
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;
// TODO: Take tokenLossRatio into account // 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) { // Reward the winning voters, in proportion to their stakes
// Take initialValue into account for (const [signingPublicKey, { stake }] of winningVotes) {
const initialPostValue = params.initialPostValue() * this.tokensMinted; const { reputationPublicKey } = this.voters.get(signingPublicKey);
const tokensForAuthor = initialPostValue * params.stakeForWin; const reward = (tokensForWinners * stake) / getTotalStaked(votePasses);
const tokensForWinners = initialPostValue * (1 - params.stakeForWin); addReward(reputationPublicKey, reward);
console.log('reward for winning voter', reputationPublicKey, reward);
// 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!");
} }
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');
} }
} }

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<head>
<title>Forum Graph: Debounce test</title>
<link type="text/css" rel="stylesheet" href="/index.css" />
</head>
<body>
<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";
const rootElement = document.getElementById("debounce-test");
const rootBox = new Box("rootBox", rootElement).flex();
const scene = (window.scene = new Scene("Debounce test", rootBox));
const log = () => scene.log("event");
debounce(log, 500);
debounce(log, 500);
await delay(500);
debounce(log, 500);
debounce(log, 500);
</script>

View File

@ -9,7 +9,7 @@
<script type="module"> <script type="module">
import { Box } from "/classes/box.js"; import { Box } from "/classes/box.js";
import { Scene } from "/classes/scene.js"; import { Scene } from "/classes/scene.js";
import { PostContent } from "/classes/post.js"; import { PostContent } from "/classes/post-content.js";
import { Expert } from "/classes/expert.js"; import { Expert } from "/classes/expert.js";
import { ForumNode } from "/classes/forum-node.js"; import { ForumNode } from "/classes/forum-node.js";
import { ForumNetwork } from "/classes/forum-network.js"; import { ForumNetwork } from "/classes/forum-network.js";

View File

@ -16,9 +16,9 @@
import { delay } from "/util.js"; import { delay } from "/util.js";
import { Forum } from "/classes/forum.js"; import { Forum } from "/classes/forum.js";
import { Public } from "/classes/public.js"; import { Public } from "/classes/public.js";
import { PostContent } from "/classes/post.js"; import { PostContent } from "/classes/post-content.js";
const DELAY_INTERVAL = 500; const DEFAULT_DELAY_INTERVAL = 500;
const rootElement = document.getElementById("forum-test"); const rootElement = document.getElementById("forum-test");
const rootBox = new Box("rootBox", rootElement).flex(); const rootBox = new Box("rootBox", rootElement).flex();
@ -38,7 +38,7 @@
const expert1 = await newExpert(); const expert1 = await newExpert();
const expert2 = await newExpert(); const expert2 = await newExpert();
await newExpert(); const expert3 = await newExpert();
const forum = (window.forum = new Forum("Forum", scene)); const forum = (window.forum = new Forum("Forum", scene));
const bench = (window.bench = new Bench(forum, "Bench", scene)); const bench = (window.bench = new Bench(forum, "Bench", scene));
@ -53,9 +53,9 @@
await scene.renderSequenceDiagram(); await scene.renderSequenceDiagram();
}; };
const updateDisplayValuesAndDelay = async () => { const updateDisplayValuesAndDelay = async (delayMs) => {
await updateDisplayValues(); await updateDisplayValues();
await delay(DELAY_INTERVAL); await delay(delayMs ?? DEFAULT_DELAY_INTERVAL);
}; };
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();
@ -70,15 +70,15 @@
tokenLossRatio: 1, tokenLossRatio: 1,
} }
); );
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay(1000);
await expert2.castVote(pool1, { position: true, stake: 1, anonymous: false }); // await expert2.castVote(pool1, { position: true, stake: 1, anonymous: false });
await updateDisplayValuesAndDelay(); // await updateDisplayValuesAndDelay();
await pool1.evaluateWinningConditions(); await pool1.evaluateWinningConditions();
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();
const { pool: pool2 } = await expert2.submitPostWithFee( const { postId: postId2, pool: pool2 } = await expert2.submitPostWithFee(
bench, bench,
forum, forum,
new PostContent({ hello: "to you as well" }).addCitation(postId1, 0.5), new PostContent({ hello: "to you as well" }).addCitation(postId1, 0.5),
@ -88,11 +88,29 @@
tokenLossRatio: 1, tokenLossRatio: 1,
} }
); );
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay(1000);
await expert1.castVote(pool2, { position: true, stake: 1, anonymous: false }); // await expert1.castVote(pool2, { position: true, stake: 1, anonymous: false });
await updateDisplayValuesAndDelay(); // await updateDisplayValuesAndDelay();
await pool2.evaluateWinningConditions(); await pool2.evaluateWinningConditions();
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();
const { pool: pool3 } = await expert3.submitPostWithFee(
bench,
forum,
new PostContent({ hello: "y'all" }).addCitation(postId2, 0.5),
{
fee: 10,
duration: 1000,
tokenLossRatio: 1,
}
);
await updateDisplayValuesAndDelay(1000);
// await expert1.castVote(pool3, { position: true, stake: 1, anonymous: false });
// await updateDisplayValuesAndDelay();
await pool3.evaluateWinningConditions();
await updateDisplayValuesAndDelay();
</script> </script>

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<head>
<title>Forum Graph</title>
<link type="text/css" rel="stylesheet" href="/index.css" />
</head>
<body>
<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";
const rootElement = document.getElementById("graph-test");
const rootBox = new Box("rootBox", rootElement).flex();
window.scene = new Scene("Graph test", rootBox);
window.graph = new Graph();
window.v = [];
function addVertex() {
const vertex = window.graph.addVertex({ seq: window.v.length });
window.v.push(vertex);
}
addVertex();
addVertex();
addVertex();
addVertex();
addVertex();
window.graph.addEdge("e1", 0, 1);
</script>

View File

@ -1,28 +0,0 @@
<!DOCTYPE html>
<head>
<title>Mermaid test</title>
<link type="text/css" rel="stylesheet" href="/index.css" />
<script type="module" defer>
// import mermaid from './mermaid.mjs';
import mermaid from "https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.mjs";
mermaid.mermaidAPI.initialize({ startOnLoad: false });
// Example of using the API var
const element = document.querySelector("#graphDiv");
const insertSvg = function (svgCode, bindFunctions) {
element.innerHTML = svgCode;
};
const graphDefinition = "graph TB\na-->b";
const graph = await mermaid.mermaidAPI.render(
"graphDiv",
graphDefinition,
insertSvg
);
const div = document.createElement("div");
div.innerHTML = graph;
document.body.append(div);
</script>
</head>
<body>
<div id="graphDiv"></div>
</body>

View File

@ -11,6 +11,7 @@
import { Scene } from "/classes/scene.js"; import { Scene } from "/classes/scene.js";
import { Expert } from "/classes/expert.js"; import { Expert } from "/classes/expert.js";
import { Bench } from "/classes/bench.js"; import { Bench } from "/classes/bench.js";
import { Forum } from "/classes/forum.js";
import { delay } from "/util.js"; import { delay } from "/util.js";
const rootElement = document.getElementById("validation-pool"); const rootElement = document.getElementById("validation-pool");
@ -27,7 +28,7 @@
"Expert2", "Expert2",
scene scene
).initialize()); ).initialize());
const bench = (window.bench = new Bench("Bench", scene)); const bench = (window.bench = new Bench(undefined, "Bench", scene));
const updateDisplayValues = async () => { const updateDisplayValues = async () => {
expert1.setValue( expert1.setValue(