dao-governance-framework/forum-network/public/classes/validation-pool.js

225 lines
7.1 KiB
JavaScript
Raw Normal View History

2022-12-31 16:08:42 -06:00
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';
2022-11-12 16:20:42 -06:00
const ValidationPoolStates = Object.freeze({
2022-12-31 16:08:42 -06:00
OPEN: 'OPEN',
CLOSED: 'CLOSED',
2023-01-02 13:14:32 -06:00
RESOLVED: 'RESOLVED',
2022-11-12 16:20:42 -06:00
});
2022-12-31 16:08:42 -06:00
/**
* Purpose: Enable voting
*/
2022-11-13 12:23:30 -06:00
export class ValidationPool extends Actor {
2022-11-30 09:13:52 -06:00
constructor(
bench,
authorId,
{
postId,
signingPublicKey,
fee,
duration,
tokenLossRatio,
contentiousDebate = false,
2023-01-01 21:09:02 -06:00
authorStake = 0,
2023-01-02 13:14:32 -06:00
anonymous = true,
2022-11-30 09:13:52 -06:00
},
name,
2022-12-31 16:08:42 -06:00
scene,
2022-11-30 09:13:52 -06:00
) {
2022-11-13 12:23:30 -06:00
super(name, scene);
2022-11-12 16:20:42 -06:00
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
2022-11-30 09:13:52 -06:00
if (
2022-12-31 16:08:42 -06:00
!contentiousDebate
&& (tokenLossRatio < 0
|| tokenLossRatio > 1
|| [null, undefined].includes(tokenLossRatio))
2022-11-30 09:13:52 -06:00
) {
throw new Error(
2022-12-31 16:08:42 -06:00
`Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}`,
2022-11-30 09:13:52 -06:00
);
2022-11-12 16:20:42 -06:00
}
2022-11-30 09:13:52 -06:00
if (
2022-12-31 16:08:42 -06:00
duration < params.voteDuration.min
|| (params.voteDuration.max && duration > params.voteDuration.max)
|| [null, undefined].includes(duration)
2022-11-30 09:13:52 -06:00
) {
throw new Error(
`Duration must be in the range [${params.voteDuration.min}, ${
2022-12-31 16:08:42 -06:00
params.voteDuration.max ?? 'Inf'
}]; got ${duration}`,
2022-11-30 09:13:52 -06:00
);
2022-11-12 16:20:42 -06:00
}
2022-11-30 09:13:52 -06:00
this.postId = postId;
this.state = ValidationPoolStates.OPEN;
2022-12-31 16:08:42 -06:00
this.setStatus('Open');
2022-11-12 16:20:42 -06:00
this.votes = new Map();
this.voters = new Map();
2022-11-13 12:23:30 -06:00
this.bench = bench;
2022-11-12 16:20:42 -06:00
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),
2022-11-30 09:13:52 -06:00
};
2023-01-01 21:09:02 -06:00
// 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
2023-01-02 13:14:32 -06:00
this.castVote(signingPublicKey, {
position: true,
stake: this.tokens.for + authorStake,
anonymous,
});
2022-11-12 16:20:42 -06:00
}
2023-01-02 13:14:32 -06:00
async castVote(signingPublicKey, {
position, stake, lockingTime = 0, anonymous = true,
}) {
2022-11-13 12:23:30 -06:00
const vote = new Vote(position, stake, lockingTime);
if (this.state === ValidationPoolStates.CLOSED) {
2022-11-13 12:23:30 -06:00
throw new Error(`Validation pool ${this.id} is closed`);
2022-11-12 16:20:42 -06:00
}
if (this.duration && new Date() - this.dateStart > this.duration) {
2022-11-30 09:13:52 -06:00
throw new Error(
2022-12-31 16:08:42 -06:00
`Validation pool ${this.id} has expired, no new votes may be cast`,
2022-11-30 09:13:52 -06:00
);
2022-11-12 16:20:42 -06:00
}
this.votes.set(signingPublicKey, vote);
2023-01-02 13:14:32 -06:00
if (!anonymous) {
console.log('castVote: revealing identity since this is not an anonymous vote');
await this.revealIdentity(signingPublicKey, signingPublicKey);
}
2022-11-12 16:20:42 -06:00
}
listVotes(position) {
2022-11-30 09:13:52 -06:00
return new Map(
Array.from(this.votes.entries()).filter(
2022-12-31 16:08:42 -06:00
([_, vote]) => vote.position === position,
),
2022-11-30 09:13:52 -06:00
);
2022-11-12 16:20:42 -06:00
}
2023-01-02 13:14:32 -06:00
async revealIdentity(signingPublicKey, reputationPublicKey) {
2022-11-12 16:20:42 -06:00
if (!this.votes.get(signingPublicKey)) {
2022-12-31 16:08:42 -06:00
throw new Error('Must vote before revealing identity');
2022-11-12 16:20:42 -06:00
}
2022-12-31 16:08:42 -06:00
const voter = this.bench.voters.get(reputationPublicKey)
?? new Voter(reputationPublicKey);
2022-11-13 12:23:30 -06:00
voter.addVoteRecord(this);
this.bench.voters.set(reputationPublicKey, voter);
2022-11-12 16:20:42 -06:00
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.
2022-11-30 09:13:52 -06:00
for (const [
signingPublicKey,
{ stake, lockingTime },
] of this.votes.entries()) {
2022-11-12 16:20:42 -06:00
const voter = this.voters.get(signingPublicKey);
2022-11-30 09:13:52 -06:00
this.bench.reputations.lockTokens(
voter.reputationPublicKey,
stake,
2022-12-31 16:08:42 -06:00
lockingTime,
2022-11-30 09:13:52 -06:00
);
2022-11-12 16:20:42 -06:00
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
}
}
2023-01-02 13:14:32 -06:00
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');
2022-12-31 16:08:42 -06:00
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);
2022-11-12 16:20:42 -06:00
const upvoteValue = getTotalValue(true);
const downvoteValue = getTotalValue(false);
2022-12-31 16:08:42 -06:00
const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation();
2022-11-12 16:20:42 -06:00
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
2022-12-31 16:08:42 -06:00
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
2022-11-12 16:20:42 -06:00
2023-01-02 13:14:32 -06:00
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;
2022-11-12 16:20:42 -06:00
}
distributeTokens(result) {
// Reward the author
// TODO: If the vote fails, distribute tokens.author among winning voters
2022-11-17 08:30:06 -06:00
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())
2022-11-30 09:13:52 -06:00
.map(({ stake }) => stake)
.reduce((acc, cur) => (acc += cur), 0);
2022-11-17 08:30:06 -06:00
if (!totalStakes) {
return;
}
2022-11-30 09:13:52 -06:00
for (const [signingPublicKey, { stake }] of winningVotes.entries()) {
const { reputationPublicKey } = this.voters.get(signingPublicKey);
const reward = (tokensForWinners * stake) / totalStakes;
2022-11-17 08:30:06 -06:00
this.bench.reputations.addTokens(reputationPublicKey, reward);
}
2022-11-12 16:20:42 -06:00
}
}
}