2022-11-13 10:54:07 -06:00
|
|
|
import { CryptoUtil } from "./crypto.js";
|
2022-11-13 12:23:30 -06:00
|
|
|
import { Vote } from "./vote.js";
|
|
|
|
import { Voter } from "./voter.js";
|
|
|
|
import { Actor } from "./actor.js";
|
2022-11-12 16:20:42 -06:00
|
|
|
import params from "./params.js";
|
|
|
|
|
2022-11-13 10:54:07 -06:00
|
|
|
const ValidationPoolStates = Object.freeze({
|
2022-11-12 16:20:42 -06:00
|
|
|
OPEN: "OPEN",
|
|
|
|
CLOSED: "CLOSED",
|
|
|
|
});
|
|
|
|
|
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,
|
|
|
|
},
|
|
|
|
name,
|
|
|
|
scene
|
|
|
|
) {
|
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 (
|
|
|
|
!contentiousDebate &&
|
|
|
|
(tokenLossRatio < 0 ||
|
|
|
|
tokenLossRatio > 1 ||
|
|
|
|
[null, undefined].includes(tokenLossRatio))
|
|
|
|
) {
|
|
|
|
throw new Error(
|
|
|
|
`Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}`
|
|
|
|
);
|
2022-11-12 16:20:42 -06:00
|
|
|
}
|
2022-11-30 09:13:52 -06:00
|
|
|
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}`
|
|
|
|
);
|
2022-11-12 16:20:42 -06:00
|
|
|
}
|
2022-11-30 09:13:52 -06:00
|
|
|
this.postId = postId;
|
2022-11-13 10:54:07 -06:00
|
|
|
this.state = ValidationPoolStates.OPEN;
|
2022-11-17 09:07:11 -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-17 08:30:06 -06:00
|
|
|
// author: fee * params.mintingRatio * params.stakeForAuthor,
|
2022-11-30 09:13:52 -06:00
|
|
|
};
|
2022-11-14 10:17:43 -06:00
|
|
|
// TODO: Consider availability stakes
|
2022-11-17 08:30:06 -06:00
|
|
|
this.castVote(signingPublicKey, true, this.tokens.for, 0);
|
2022-11-12 16:20:42 -06:00
|
|
|
}
|
|
|
|
|
2022-11-13 12:23:30 -06:00
|
|
|
castVote(signingPublicKey, position, stake, lockingTime) {
|
|
|
|
const vote = new Vote(position, stake, lockingTime);
|
2022-11-13 10:54:07 -06:00
|
|
|
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(
|
|
|
|
`Validation pool ${this.id} has expired, no new votes may be cast`
|
|
|
|
);
|
2022-11-12 16:20:42 -06:00
|
|
|
}
|
|
|
|
this.votes.set(signingPublicKey, vote);
|
|
|
|
}
|
|
|
|
|
|
|
|
listVotes(position) {
|
2022-11-30 09:13:52 -06:00
|
|
|
return new Map(
|
|
|
|
Array.from(this.votes.entries()).filter(
|
|
|
|
([_, vote]) => vote.position === position
|
|
|
|
)
|
|
|
|
);
|
2022-11-12 16:20:42 -06:00
|
|
|
}
|
|
|
|
|
2022-11-13 12:23:30 -06:00
|
|
|
revealIdentity(signingPublicKey, reputationPublicKey) {
|
2022-11-12 16:20:42 -06:00
|
|
|
if (!this.votes.get(signingPublicKey)) {
|
|
|
|
throw new Error("Must vote before revealing identity");
|
|
|
|
}
|
2022-11-30 09:13:52 -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);
|
|
|
|
if (this.votes.size === this.voters.size) {
|
|
|
|
// All voters have revealed their reputation public keys
|
|
|
|
// Now we can evaluate winning conditions
|
2022-11-13 10:54:07 -06:00
|
|
|
this.state = ValidationPoolStates.CLOSED;
|
2022-11-17 09:07:11 -06:00
|
|
|
this.setStatus("Closed");
|
2022-11-12 16:20:42 -06:00
|
|
|
const result = this.evaluateWinningConditions();
|
2022-11-17 09:07:11 -06:00
|
|
|
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);
|
|
|
|
}
|
2022-11-13 12:23:30 -06:00
|
|
|
this.deactivate();
|
2022-11-12 16:20:42 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
lockingTime
|
|
|
|
);
|
2022-11-12 16:20:42 -06:00
|
|
|
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
evaluateWinningConditions() {
|
2022-11-30 09:13:52 -06:00
|
|
|
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);
|
2022-11-12 16:20:42 -06:00
|
|
|
|
|
|
|
const upvoteValue = getTotalValue(true);
|
|
|
|
const downvoteValue = getTotalValue(false);
|
2022-11-30 09:13:52 -06:00
|
|
|
const activeAvailableReputation =
|
|
|
|
this.bench.getTotalActiveAvailableReputation();
|
2022-11-12 16:20:42 -06:00
|
|
|
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
|
2022-11-30 09:13:52 -06:00
|
|
|
const quorumMet =
|
|
|
|
upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
2022-11-12 16:20:42 -06:00
|
|
|
|
2022-11-17 09:07:11 -06:00
|
|
|
return quorumMet ? votePasses : null;
|
2022-11-12 16:20:42 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
distributeTokens(result) {
|
|
|
|
// Reward the author
|
2022-11-14 10:17:43 -06:00
|
|
|
// TODO: If the vote fails, distribute tokens.author among winning voters
|
2022-11-17 08:30:06 -06:00
|
|
|
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())
|
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
|
|
|
// console.log("awarding to winning voter", {id: reputationPublicKey, tokens: reward, stake, totalStakes, tokensForWinners});
|
|
|
|
this.bench.reputations.addTokens(reputationPublicKey, reward);
|
|
|
|
}
|
2022-11-12 16:20:42 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|