import { CryptoUtil } from "./crypto.js"; import { Vote } from "./vote.js"; import { Voter } from "./voter.js"; import { Actor } from "./actor.js"; import params from "./params.js"; const ValidationPoolStates = Object.freeze({ OPEN: "OPEN", CLOSED: "CLOSED", }); export class ValidationPool extends Actor { constructor( bench, authorId, { postId, signingPublicKey, fee, duration, tokenLossRatio, contentiousDebate = false, }, name, scene ) { super(name, scene); // If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio() if ( !contentiousDebate && (tokenLossRatio < 0 || tokenLossRatio > 1 || [null, undefined].includes(tokenLossRatio)) ) { throw new Error( `Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}` ); } 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.postId = postId; this.state = ValidationPoolStates.OPEN; this.setStatus("Open"); this.votes = new Map(); this.voters = new Map(); this.bench = bench; this.id = CryptoUtil.randomUUID(); this.dateStart = new Date(); this.authorId = authorId; this.fee = fee; this.duration = duration; this.tokenLossRatio = tokenLossRatio; this.contentiousDebate = contentiousDebate; this.tokens = { for: fee * params.mintingRatio * params.stakeForWin, against: fee * params.mintingRatio * (1 - params.stakeForWin), // author: fee * params.mintingRatio * params.stakeForAuthor, }; // TODO: Consider availability stakes this.castVote(signingPublicKey, true, this.tokens.for, 0); } castVote(signingPublicKey, position, stake, lockingTime) { const vote = new Vote(position, stake, lockingTime); if (this.state === ValidationPoolStates.CLOSED) { throw new Error(`Validation pool ${this.id} is closed`); } if (this.duration && new Date() - this.dateStart > this.duration) { throw new Error( `Validation pool ${this.id} has expired, no new votes may be cast` ); } this.votes.set(signingPublicKey, vote); } listVotes(position) { return new Map( Array.from(this.votes.entries()).filter( ([_, vote]) => vote.position === position ) ); } revealIdentity(signingPublicKey, reputationPublicKey) { if (!this.votes.get(signingPublicKey)) { throw new Error("Must vote before revealing identity"); } const voter = this.bench.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey); voter.addVoteRecord(this); this.bench.voters.set(reputationPublicKey, voter); 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.state = ValidationPoolStates.CLOSED; this.setStatus("Closed"); const result = this.evaluateWinningConditions(); if (result === null) { this.setStatus("Closed - Quorum not met"); this.scene.log(`note over ${this.name} : Quorum not met`); } else { this.setStatus(`Closed - ${result ? "Won" : "Lost"}`); this.scene.log(`note over ${this.name} : ${result ? "Win" : "Lose"}`); this.applyTokenLocking(); this.distributeTokens(result); } this.deactivate(); } } 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. for (const [ signingPublicKey, { stake, lockingTime }, ] of this.votes.entries()) { const voter = this.voters.get(signingPublicKey); this.bench.reputations.lockTokens( voter.reputationPublicKey, stake, lockingTime ); // TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties. } } evaluateWinningConditions() { const getVoteValue = ({ stake, lockingTime }) => stake * Math.pow(lockingTime, params.lockingTimeExponent); const getTotalValue = (position) => Array.from(this.listVotes(position).values()) .map(getVoteValue) .reduce((acc, cur) => (acc += cur), 0); const upvoteValue = getTotalValue(true); const downvoteValue = getTotalValue(false); const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation(); const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; return quorumMet ? votePasses : null; } distributeTokens(result) { // Reward the author // TODO: If the vote fails, distribute tokens.author among winning voters if (result === true) { // console.log("awarding to author", {id: this.authorId, tokens: this.tokens.for}); this.bench.reputations.addTokens(this.authorId, this.tokens.for); // Reward the vote winners, in proportion to their stakes const tokensForWinners = this.tokens.against; 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; // console.log("awarding to winning voter", {id: reputationPublicKey, tokens: reward, stake, totalStakes, tokensForWinners}); this.bench.reputations.addTokens(reputationPublicKey, reward); } } } }