diff --git a/forum-network/public/classes/bench.js b/forum-network/public/classes/bench.js new file mode 100644 index 0000000..9267456 --- /dev/null +++ b/forum-network/public/classes/bench.js @@ -0,0 +1,72 @@ +import { Actor } from "./actor.js"; +import { Reputations } from "./reputation.js"; +import { ValidationPool } from "./validation-pool.js"; +import { Vote } from "./vote.js"; +import { Voter } from "./voter.js"; +import params from "./params.js"; + +export class Bench extends Actor { + constructor(name, scene) { + super(name, scene); + this.validationPools = new Map(); + this.voters = new Map(); + this.reputations = new Reputations(); + + this.actions = { + }; + } + + listValidationPools() { + Array.from(this.validationPools.values()); + } + + listActiveVoters() { + const now = new Date(); + const thresholdSet = !!params.activeVoterThreshold; + return Array.from(this.voters.values()).filter(voter => { + const hasVoted = !!voter.dateLastVote; + const withinThreshold = now - voter.dateLastVote >= params.activeVoterThreshold; + return hasVoted && (!thresholdSet || withinThreshold); + }); + } + + getTotalReputation() { + return this.reputations.getTotal(); + } + + getTotalAvailableReputation() { + return this.reputations.getTotalAvailable(); + } + + getTotalActiveReputation() { + return this.listActiveVoters() + .map(({reputationPublicKey}) => this.reputations.getTokens(reputationPublicKey)) + .reduce((acc, cur) => acc += cur, 0); + } + + getTotalActiveAvailableReputation() { + return this.listActiveVoters() + .map(({reputationPublicKey}) => this.reputations.getAvailableTokens(reputationPublicKey)) + .reduce((acc, cur) => acc += cur, 0); + } + + initiateValidationPool(authorId, {fee, duration, tokenLossRatio, contentiousDebate}) { + const vote = new ValidationPool(this, authorId, {fee, duration, tokenLossRatio, contentiousDebate}); + this.validationPools.set(vote.id, vote); + return vote.id; + } + + castVote(voteId, signingPublicKey, position, stake, lockingTime) { + const vote = new Vote(position, stake, lockingTime); + const validationPool = this.validationPools.get(voteId); + validationPool.castVote(signingPublicKey, vote); + } + + revealIdentity(voteId, signingPublicKey, reputationPublicKey) { + const validationPool = this.validationPools.get(voteId); + const voter = this.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey); + voter.addVoteRecord(validationPool); + this.voters.set(reputationPublicKey, voter); + validationPool.revealIdentity(signingPublicKey, voter); + } +} diff --git a/forum-network/public/classes/blockchain.js b/forum-network/public/classes/blockchain.js deleted file mode 100644 index 02655df..0000000 --- a/forum-network/public/classes/blockchain.js +++ /dev/null @@ -1,72 +0,0 @@ -class Blockchain { - constructor() { - this.posts = new CollectionWithReputation(); - this.authors = new CollectionWithReputation(); - this.nodes = new CollectionWithReputation(); - } - - vote(voter, batch) { - if (!this.activeVote) { - this.activeVote = new VoteInstance(batch); - this.activeVote.vote(voter, true); - return; - } - if (this.activeVote.matches(batch)) { - this.activeVote.vote(voter, true); - } else { - this.activeVote.vote(voter, false); - } - } - - applyBatch(batch) {} -} - -class CollectionWithReputation { - constructor() { - this.collection = new Map(); - this.reputations = new Map(); - } - - set(id, value) { - this.collection.set(id, value); - } - - get(id) { - this.collection.get(id); - } - - setReputation(id, value) { - this.reputations.set(id, value); - } - - get(id) { - this.reputations.get(id); - } -} - -class VoteInstance { - constructor(content) { - this.content = content; - this.votes = new Map(); - } - - matches(content) { - return JSON.stringify(content) === JSON.stringify(this.content); - } - - vote(voter, opinion) { - this.votes.set(voter.id, { voter, opinion }); - } - - finalize() { - const count = { for: 0, against: 0 }; - for (const vote of this.votes) { - if (vote.opinion === true) { - count.for++; - } else { - count.against++; - } - } - return count.for > count.against; - } -} diff --git a/forum-network/public/classes/member.js b/forum-network/public/classes/member.js index ef2350d..263b6b0 100644 --- a/forum-network/public/classes/member.js +++ b/forum-network/public/classes/member.js @@ -8,7 +8,7 @@ export class Member extends Actor { super(name, scene); this.actions = { submitPost: new Action('submit post', scene), - initiateVote: new Action('initiate vote', scene), + initiateValidationPool: new Action('initiate vote', scene), castVote: new Action('cast vote', scene), revealIdentity: new Action('reveal identity', scene), }; @@ -31,10 +31,10 @@ export class Member extends Actor { await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON())); } - initiateVote(validationPool, options) { - // For now, directly call validationPool.initiateVote(); - this.actions.initiateVote.log(this, validationPool); - return validationPool.initiateVote(this.reputationPublicKey, options); + initiateValidationPool(bench, options) { + // For now, directly call bench.initiateValidationPool(); + this.actions.initiateValidationPool.log(this, bench); + return bench.initiateValidationPool(this.reputationPublicKey, options); } async castVote(validationPool, voteId, position, stake, lockingTime) { diff --git a/forum-network/public/classes/validation-pool/params.js b/forum-network/public/classes/params.js similarity index 100% rename from forum-network/public/classes/validation-pool/params.js rename to forum-network/public/classes/params.js diff --git a/forum-network/public/classes/validation-pool/vote-instance.js b/forum-network/public/classes/validation-pool.js similarity index 93% rename from forum-network/public/classes/validation-pool/vote-instance.js rename to forum-network/public/classes/validation-pool.js index f21f56d..58099eb 100644 --- a/forum-network/public/classes/validation-pool/vote-instance.js +++ b/forum-network/public/classes/validation-pool.js @@ -1,12 +1,12 @@ -import { CryptoUtil } from "../crypto.js"; +import { CryptoUtil } from "./crypto.js"; import params from "./params.js"; -const VoteInstanceStates = Object.freeze({ +const ValidationPoolStates = Object.freeze({ OPEN: "OPEN", CLOSED: "CLOSED", }); -export class VoteInstance { +export class ValidationPool { constructor(validationPool, authorId, {fee, duration, tokenLossRatio, contentiousDebate = false}) { // If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio() if (!contentiousDebate && (tokenLossRatio < 0 || tokenLossRatio > 1 || [null, undefined].includes(tokenLossRatio))) { @@ -15,7 +15,7 @@ export class VoteInstance { if (duration < params.voteDuration.min || (params.voteDuration.max && duration > params.voteDuration.max) || [null, undefined].includes(duration)) { throw new Error(`Duration must be in the range [${params.voteDuration.min}, ${params.voteDuration.max ?? 'Inf'}]; got ${duration}`); } - this.state = VoteInstanceStates.OPEN; + this.state = ValidationPoolStates.OPEN; this.votes = new Map(); this.voters = new Map(); this.validationPool = validationPool; @@ -34,7 +34,7 @@ export class VoteInstance { } castVote(signingPublicKey, vote) { - if (this.state === VoteInstanceStates.CLOSED) { + if (this.state === ValidationPoolStates.CLOSED) { throw new Error(`Vote ${this.id} is closed`); } if (this.duration && new Date() - this.dateStart > this.duration) { @@ -56,7 +56,7 @@ export class VoteInstance { if (this.votes.size === this.voters.size) { // All voters have revealed their reputation public keys // Now we can evaluate winning conditions - this.state = VoteInstanceStates.CLOSED; + this.state = ValidationPoolStates.CLOSED; const result = this.evaluateWinningConditions(); this.applyTokenLocking(); this.distributeTokens(result); @@ -103,7 +103,7 @@ export class VoteInstance { const upvoteValue = getTotalValue(true); const downvoteValue = getTotalValue(false); - const activeAvailableReputation = this.validationPool.activeAvailableReputation(); + const activeAvailableReputation = this.validationPool.getTotalActiveAvailableReputation(); const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; diff --git a/forum-network/public/classes/validation-pool/index.js b/forum-network/public/classes/validation-pool/index.js deleted file mode 100644 index 181664c..0000000 --- a/forum-network/public/classes/validation-pool/index.js +++ /dev/null @@ -1 +0,0 @@ -export { ValidationPool } from "./validation-pool.js"; diff --git a/forum-network/public/classes/validation-pool/validation-pool.js b/forum-network/public/classes/validation-pool/validation-pool.js deleted file mode 100644 index 9310952..0000000 --- a/forum-network/public/classes/validation-pool/validation-pool.js +++ /dev/null @@ -1,59 +0,0 @@ -import { Actor } from "../actor.js"; -import { Action } from "../action.js"; -import { Reputations } from "../reputation.js"; -import { VoteInstance } from "./vote-instance.js"; -import { Vote } from "./vote.js"; -import { Voter } from "./voter.js"; -import params from "./params.js"; - -export class ValidationPool extends Actor { - constructor(name, scene) { - super(name, scene); - this.voteInstances = new Map(); - this.voters = new Map(); - this.reputations = new Reputations(); - - this.actions = { - }; - } - - listVoteInstances() { - Array.from(this.voteInstances.values()); - } - - listActiveVoters() { - const now = new Date(); - const thresholdSet = !!params.activeVoterThreshold; - return Array.from(this.voters.values()).filter(voter => { - const hasVoted = !!voter.dateLastVote; - const withinThreshold = now - voter.dateLastVote >= params.activeVoterThreshold; - return hasVoted && (!thresholdSet || withinThreshold); - }); - } - - activeAvailableReputation() { - return this.listActiveVoters() - .map(({reputationPublicKey}) => this.reputations.getAvailableTokens(reputationPublicKey)) - .reduce((acc, cur) => acc += cur, 0); - } - - initiateVote(authorId, {fee, duration, tokenLossRatio, contentiousDebate}) { - const vote = new VoteInstance(this, authorId, {fee, duration, tokenLossRatio, contentiousDebate}); - this.voteInstances.set(vote.id, vote); - return vote.id; - } - - castVote(voteId, signingPublicKey, position, stake, lockingTime) { - const vote = new Vote(position, stake, lockingTime); - const voteInstance = this.voteInstances.get(voteId); - voteInstance.castVote(signingPublicKey, vote); - } - - revealIdentity(voteId, signingPublicKey, reputationPublicKey) { - const voteInstance = this.voteInstances.get(voteId); - const voter = this.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey); - voter.addVoteRecord(voteInstance); - this.voters.set(reputationPublicKey, voter); - voteInstance.revealIdentity(signingPublicKey, voter); - } -} diff --git a/forum-network/public/classes/validation-pool/vote.js b/forum-network/public/classes/vote.js similarity index 100% rename from forum-network/public/classes/validation-pool/vote.js rename to forum-network/public/classes/vote.js diff --git a/forum-network/public/classes/validation-pool/voter.js b/forum-network/public/classes/voter.js similarity index 100% rename from forum-network/public/classes/validation-pool/voter.js rename to forum-network/public/classes/voter.js diff --git a/forum-network/public/index.css b/forum-network/public/index.css index 81be757..b987a08 100644 --- a/forum-network/public/index.css +++ b/forum-network/public/index.css @@ -5,7 +5,7 @@ font-size: 12pt; } .box .name { - width: 12em; + width: 15em; font-weight: bold; text-align: right; margin-right: 6pt; diff --git a/forum-network/public/validation-pool-test.js b/forum-network/public/validation-pool-test.js index 7d4d20f..072b71c 100644 --- a/forum-network/public/validation-pool-test.js +++ b/forum-network/public/validation-pool-test.js @@ -1,7 +1,7 @@ import { Box } from './classes/box.js'; import { Scene } from './classes/scene.js'; import { Member } from './classes/member.js'; -import { ValidationPool } from './classes/validation-pool/index.js'; +import { Bench } from './classes/bench.js'; const delay = async (ms) => { await new Promise((resolve) => setTimeout(resolve, ms)); @@ -12,29 +12,39 @@ 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 bench = window.bench = new Bench("Bench", scene); -const member1 = window.member1 = (await new Member("member1", scene).initialize()).setValue('rep', 0); -const member2 = window.member2 = (await new Member("member2", scene).initialize()).setValue('rep', 0); +const member1 = window.member1 = await new Member("member1", scene).initialize(); +const member2 = window.member2 = await new Member("member2", scene).initialize(); + +const updateDisplayValues = () => { + member1.setValue('rep', bench.reputations.getTokens(member1.reputationPublicKey)); + member2.setValue('rep', bench.reputations.getTokens(member2.reputationPublicKey)); + bench.setValue('total rep', bench.getTotalReputation()); + bench.setValue('available rep', bench.getTotalAvailableReputation()); + bench.setValue('active rep', bench.getTotalActiveReputation()); + bench.setValue('active available rep', bench.getTotalActiveAvailableReputation()); +}; + +updateDisplayValues(); await delay(1000); // First member can self-approve -const vote1 = member1.initiateVote(pool, {fee: 1, duration: 1000, tokenLossRatio: 1}); -await member1.castVote(pool, vote1, true, 0, 0); -await member1.revealIdentity(pool, vote1); // Vote passes -member1.setValue('rep', pool.reputations.getTokens(member1.reputationPublicKey)); +const vote1 = member1.initiateValidationPool(bench, {fee: 1, duration: 1000, tokenLossRatio: 1}); +await member1.castVote(bench, vote1, true, 0, 0); +await member1.revealIdentity(bench, vote1); // Vote passes +updateDisplayValues(); -// const vote2 = member2.initiateVote(pool, {fee: 1, duration: 1000, tokenLossRatio: 1}); -// await member2.castVote(pool, vote2, true, 0); -// await member2.revealIdentity(pool, vote2); // Quorum is not met! +// const vote2 = member2.initiateValidationPool(bench, {fee: 1, duration: 1000, tokenLossRatio: 1}); +// await member2.castVote(bench, vote2, true, 0); +// await member2.revealIdentity(bench, vote2); // Quorum is not met! await delay(1000); // Second member must be approved by first member -const vote2 = member2.initiateVote(pool, {fee: 1, duration: 1000, tokenLossRatio: 1}); -await member1.castVote(pool, vote2, true, 0.5, 1); -await member1.revealIdentity(pool, vote2); // Vote passes +const vote2 = member2.initiateValidationPool(bench, {fee: 1, duration: 1000, tokenLossRatio: 1}); +await member1.castVote(bench, vote2, true, 0.5, 1); +await member1.revealIdentity(bench, vote2); // Vote passes -member1.setValue('rep', pool.reputations.getTokens(member1.reputationPublicKey)); -member2.setValue('rep', pool.reputations.getTokens(member2.reputationPublicKey)); +updateDisplayValues();