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', RESOLVED: 'RESOLVED', }); /** * Purpose: Enable voting */ export class ValidationPool extends Actor { constructor( bench, authorId, { postId, signingPublicKey, fee, duration, tokenLossRatio, contentiousDebate = false, authorStake = 0, anonymous = true, }, 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), }; // 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 console.log('initiateValidationPool casting vote', { signingPublicKey }); this.castVote(signingPublicKey, { position: true, stake: this.tokens.for + authorStake, anonymous, }); } async castVote(signingPublicKey, { position, stake, lockingTime = 0, anonymous = true, }) { console.log('castVote', { signingPublicKey, position, stake, anonymous, }); 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); if (!anonymous) { console.log('castVote: revealing identity since this is not an anonymous vote'); await this.revealIdentity(signingPublicKey, signingPublicKey); } } listVotes(position) { return new Map( Array.from(this.votes.entries()).filter( ([_, vote]) => vote.position === position, ), ); } async 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); } 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. } } async evaluateWinningConditions() { if (this.state === ValidationPoolStates.RESOLVED) { throw new Error('Validation pool has already been resolved!'); } const elapsed = new Date() - this.dateStart; if (elapsed < this.duration) { throw new Error(`Validation pool duration has not yet elapsed! ${this.duration - elapsed} ms remaining.`); } if (this.voters.size < this.votes.size) { throw new Error('Not all voters have revealed their reputation public keys!'); } // Now we can evaluate winning conditions this.state = ValidationPoolStates.CLOSED; this.setStatus('Closed'); const getVoteValue = ({ stake, lockingTime }) => stake * 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; const result = quorumMet ? votePasses : null; if (result === null) { this.setStatus('Resolved - Quorum not met'); this.scene.log(`note over ${this.name} : Quorum not met`); } else { this.setStatus(`Resolved - ${result ? 'Won' : 'Lost'}`); this.scene.log(`note over ${this.name} : ${result ? 'Win' : 'Lose'}`); this.applyTokenLocking(); this.distributeTokens(result); } this.deactivate(); this.state = ValidationPoolStates.RESOLVED; return result; } distributeTokens(result) { // Reward the author // TODO: If the vote fails, distribute tokens.author among winning voters if (result === true) { 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; this.bench.reputations.addTokens(reputationPublicKey, reward); } } } }