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