Basic validation pool is working
This commit is contained in:
parent
beb1a069d7
commit
c44c70cf03
|
@ -6,6 +6,7 @@ export class Actor {
|
||||||
this.status = this.scene.addDisplayValue(`${this.name} status`);
|
this.status = this.scene.addDisplayValue(`${this.name} status`);
|
||||||
this.status.set('New');
|
this.status.set('New');
|
||||||
this.scene.log(`participant ${this.name}`);
|
this.scene.log(`participant ${this.name}`);
|
||||||
|
this.values = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
send(dest, action, detail) {
|
send(dest, action, detail) {
|
||||||
|
@ -34,4 +35,19 @@ export class Actor {
|
||||||
this.status.set(status);
|
this.status.set(status);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addValue(label) {
|
||||||
|
this.values.set(label, this.scene.addDisplayValue(`${this.name} ${label}`));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(label, value) {
|
||||||
|
let displayValue = this.values.get(label);
|
||||||
|
if (!displayValue) {
|
||||||
|
displayValue = this.scene.addDisplayValue(`${this.name} ${label}`);
|
||||||
|
this.values.set(label, displayValue);
|
||||||
|
}
|
||||||
|
displayValue.set(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ export class CryptoUtil {
|
||||||
|
|
||||||
static hash = 'SHA-256';
|
static hash = 'SHA-256';
|
||||||
|
|
||||||
static async generateSigningKey() {
|
static async generateAsymmetricKey() {
|
||||||
return await window.crypto.subtle.generateKey(
|
return await window.crypto.subtle.generateKey(
|
||||||
{
|
{
|
||||||
name: CryptoUtil.algorithm,
|
name: CryptoUtil.algorithm,
|
||||||
|
@ -55,4 +55,8 @@ export class CryptoUtil {
|
||||||
['verify'],
|
['verify'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static randomUUID() {
|
||||||
|
return window.crypto.randomUUID();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export class ForumNode extends Actor {
|
||||||
|
|
||||||
// Generate a signing key pair and connect to the network
|
// Generate a signing key pair and connect to the network
|
||||||
async initialize(forumNetwork) {
|
async initialize(forumNetwork) {
|
||||||
this.keyPair = await CryptoUtil.generateSigningKey();
|
this.keyPair = await CryptoUtil.generateAsymmetricKey();
|
||||||
this.forumNetwork = forumNetwork.addNode(this);
|
this.forumNetwork = forumNetwork.addNode(this);
|
||||||
this.status.set('Initialized');
|
this.status.set('Initialized');
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -16,31 +16,41 @@ export class Member extends Actor {
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
this.keyPair = await CryptoUtil.generateSigningKey();
|
this.reputationKey = await CryptoUtil.generateAsymmetricKey();
|
||||||
|
this.reputationPublicKey = await CryptoUtil.exportKey(this.reputationKey.publicKey);
|
||||||
this.status.set('Initialized');
|
this.status.set('Initialized');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitPost(forumNode, post, stake) {
|
async submitPost(forumNode, post, stake) {
|
||||||
|
// TODO: Include fee
|
||||||
const postMessage = new PostMessage({ post, stake });
|
const postMessage = new PostMessage({ post, stake });
|
||||||
await postMessage.sign(this.keyPair);
|
await postMessage.sign(this.reputationKey);
|
||||||
this.actions.submitPost.log(this, forumNode, null, { id: post.id });
|
this.actions.submitPost.log(this, forumNode, null, { id: post.id });
|
||||||
// For now, directly call forumNode.receiveMessage();
|
// For now, directly call forumNode.receiveMessage();
|
||||||
await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON()));
|
await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async castVote(validationPool, voteId, position, stake) {
|
initiateVote(validationPool, options) {
|
||||||
const signingKey = await CryptoUtil.generateSigningKey();
|
// For now, directly call validationPool.initiateVote();
|
||||||
this.votes.set(voteId, {signingKey});
|
this.actions.initiateVote.log(this, validationPool);
|
||||||
// TODO: signed CastVoteMessage
|
return validationPool.initiateVote(this.reputationPublicKey, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async castVote(validationPool, voteId, position, stake, lockingTime) {
|
||||||
|
const signingKey = await CryptoUtil.generateAsymmetricKey();
|
||||||
|
const signingPublicKey = await CryptoUtil.exportKey(signingKey.publicKey)
|
||||||
|
this.votes.set(voteId, {signingPublicKey});
|
||||||
|
// TODO: encrypt vote
|
||||||
|
// TODO: sign message
|
||||||
this.actions.castVote.log(this, validationPool);
|
this.actions.castVote.log(this, validationPool);
|
||||||
validationPool.castVote(voteId, signingKey.publicKey, position, stake);
|
validationPool.castVote(voteId, signingPublicKey, position, stake, lockingTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
async revealIdentity(validationPool, voteId) {
|
async revealIdentity(validationPool, voteId) {
|
||||||
const {signingKey} = this.votes.get(voteId);
|
const {signingPublicKey} = this.votes.get(voteId);
|
||||||
// TODO: signed RevealIdentityMessage
|
// TODO: sign message
|
||||||
this.actions.revealIdentity.log(this, validationPool);
|
this.actions.revealIdentity.log(this, validationPool);
|
||||||
validationPool.revealIdentity(voteId, signingKey.publicKey, this.keyPair.publicKey);
|
validationPool.revealIdentity(voteId, signingPublicKey, this.reputationPublicKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { CryptoUtil } from "./crypto.js";
|
||||||
|
|
||||||
export class Post {
|
export class Post {
|
||||||
constructor(content) {
|
constructor(content) {
|
||||||
this.id = crypto.randomUUID();
|
this.id = CryptoUtil.randomUUID();
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.citations = [];
|
this.citations = [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,86 @@
|
||||||
class Reputation {
|
class Lock {
|
||||||
constructor() {}
|
constructor(tokens, duration) {
|
||||||
|
this.dateCreated = new Date();
|
||||||
|
this.tokens = tokens;
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reputation {
|
||||||
|
constructor() {
|
||||||
|
this.tokens = 0;
|
||||||
|
this.locks = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
addTokens(tokens) {
|
||||||
|
if (this.tokens + tokens < 0) {
|
||||||
|
throw new Error(`Token balance can not become negative`);
|
||||||
|
}
|
||||||
|
this.tokens += tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
lockTokens(tokens, duration) {
|
||||||
|
if (tokens > this.getAvailableTokens()) {
|
||||||
|
throw new Error("Can not lock more tokens than are available")
|
||||||
|
}
|
||||||
|
const lock = new Lock(tokens, duration);
|
||||||
|
this.locks.add(lock);
|
||||||
|
// TODO: Prune locks once expired
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokens() {
|
||||||
|
return this.tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableTokens() {
|
||||||
|
const now = new Date();
|
||||||
|
const tokensLocked = Array.from(this.locks.values())
|
||||||
|
.filter(({dateCreated, duration}) => now - dateCreated < duration)
|
||||||
|
.map(({tokens}) => tokens)
|
||||||
|
.reduce((acc, cur) => acc += cur, 0);
|
||||||
|
return Math.max(this.tokens - tokensLocked, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Reputations extends Map {
|
||||||
|
getTokens(reputationPublicKey) {
|
||||||
|
const reputation = this.get(reputationPublicKey);
|
||||||
|
if (!reputation) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return reputation.getTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableTokens(reputationPublicKey) {
|
||||||
|
const reputation = this.get(reputationPublicKey);
|
||||||
|
if (!reputation) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return reputation.getAvailableTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
addTokens(reputationPublicKey, tokens) {
|
||||||
|
const reputation = this.get(reputationPublicKey) ?? new Reputation();
|
||||||
|
reputation.addTokens(tokens);
|
||||||
|
this.set(reputationPublicKey, reputation);
|
||||||
|
}
|
||||||
|
|
||||||
|
lockTokens(reputationPublicKey, tokens, duration) {
|
||||||
|
if (!tokens || !duration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reputation = this.get(reputationPublicKey);
|
||||||
|
if (!reputation) {
|
||||||
|
throw new Error(`${reputationPublicKey} has no tokens to lock`);
|
||||||
|
}
|
||||||
|
reputation.lockTokens(tokens, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotal() {
|
||||||
|
return Array.from(this.values()).reduce((acc, cur) => acc += cur.getTokens(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalAvailable() {
|
||||||
|
return Array.from(this.values()).reduce((acc, cur) => acc += cur.getAvailableTokens(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,214 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { ValidationPool } from "./validation-pool.js";
|
|
@ -0,0 +1,20 @@
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
export default params;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Actor } from "../actor.js";
|
||||||
|
import { Action } from "../action.js";
|
||||||
|
import { Reputations } from "../reputation.js";
|
||||||
|
import { VoteInstance } from "./vote-instance.js";
|
||||||
|
import { Vote } from "./vote.js";
|
||||||
|
import { Voter } from "./voter.js";
|
||||||
|
import params from "./params.js";
|
||||||
|
|
||||||
|
export class ValidationPool extends Actor {
|
||||||
|
constructor(name, scene) {
|
||||||
|
super(name, scene);
|
||||||
|
this.voteInstances = new Map();
|
||||||
|
this.voters = new Map();
|
||||||
|
this.reputations = new Reputations();
|
||||||
|
|
||||||
|
this.actions = {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
listVoteInstances() {
|
||||||
|
Array.from(this.voteInstances.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
listActiveVoters() {
|
||||||
|
const now = new Date();
|
||||||
|
const thresholdSet = !!params.activeVoterThreshold;
|
||||||
|
return Array.from(this.voters.values()).filter(voter => {
|
||||||
|
const hasVoted = !!voter.dateLastVote;
|
||||||
|
const withinThreshold = now - voter.dateLastVote >= params.activeVoterThreshold;
|
||||||
|
return hasVoted && (!thresholdSet || withinThreshold);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
activeAvailableReputation() {
|
||||||
|
return this.listActiveVoters()
|
||||||
|
.map(({reputationPublicKey}) => this.reputations.getAvailableTokens(reputationPublicKey))
|
||||||
|
.reduce((acc, cur) => acc += cur, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
initiateVote(authorId, {fee, duration, tokenLossRatio, contentiousDebate}) {
|
||||||
|
const vote = new VoteInstance(this, authorId, {fee, duration, tokenLossRatio, contentiousDebate});
|
||||||
|
this.voteInstances.set(vote.id, vote);
|
||||||
|
return vote.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
castVote(voteId, signingPublicKey, position, stake, lockingTime) {
|
||||||
|
const vote = new Vote(position, stake, lockingTime);
|
||||||
|
const voteInstance = this.voteInstances.get(voteId);
|
||||||
|
voteInstance.castVote(signingPublicKey, vote);
|
||||||
|
}
|
||||||
|
|
||||||
|
revealIdentity(voteId, signingPublicKey, reputationPublicKey) {
|
||||||
|
const voteInstance = this.voteInstances.get(voteId);
|
||||||
|
const voter = this.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
|
||||||
|
voter.addVoteRecord(voteInstance);
|
||||||
|
this.voters.set(reputationPublicKey, voter);
|
||||||
|
voteInstance.revealIdentity(signingPublicKey, voter);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { CryptoUtil } from "../crypto.js";
|
||||||
|
import params from "./params.js";
|
||||||
|
|
||||||
|
const VoteInstanceStates = Object.freeze({
|
||||||
|
OPEN: "OPEN",
|
||||||
|
CLOSED: "CLOSED",
|
||||||
|
});
|
||||||
|
|
||||||
|
export class VoteInstance {
|
||||||
|
constructor(validationPool, authorId, {fee, duration, tokenLossRatio, contentiousDebate = false}) {
|
||||||
|
// 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.state = VoteInstanceStates.OPEN;
|
||||||
|
this.votes = new Map();
|
||||||
|
this.voters = new Map();
|
||||||
|
this.validationPool = validationPool;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
castVote(signingPublicKey, vote) {
|
||||||
|
if (this.state === VoteInstanceStates.CLOSED) {
|
||||||
|
throw new Error(`Vote ${this.id} is closed`);
|
||||||
|
}
|
||||||
|
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, vote);
|
||||||
|
}
|
||||||
|
|
||||||
|
listVotes(position) {
|
||||||
|
return new Map(Array.from(this.votes.entries())
|
||||||
|
.filter(([_, vote]) => vote.position === position));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.state = VoteInstanceStates.CLOSED;
|
||||||
|
const result = this.evaluateWinningConditions();
|
||||||
|
this.applyTokenLocking();
|
||||||
|
this.distributeTokens(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.validationPool.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.validationPool.activeAvailableReputation();
|
||||||
|
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
|
||||||
|
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
||||||
|
|
||||||
|
// TODO: If quorum is not met, what should happen?
|
||||||
|
if (!quorumMet) {
|
||||||
|
throw new Error("Quorum is not met");
|
||||||
|
}
|
||||||
|
return votePasses && quorumMet;
|
||||||
|
}
|
||||||
|
|
||||||
|
distributeTokens(result) {
|
||||||
|
// Reward the author
|
||||||
|
// TODO: Penalty to the author if the vote does not pass?
|
||||||
|
this.validationPool.reputations.addTokens(this.authorId, this.tokens.author);
|
||||||
|
// Reward the vote winners, in proportion to their stakes
|
||||||
|
const tokensForWinners = result ? this.tokens.for : 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.validationPool.reputations.addTokens(reputationPublicKey, reward);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export class Vote {
|
||||||
|
constructor(position, stake, lockingTime) {
|
||||||
|
this.position = position;
|
||||||
|
this.stake = stake;
|
||||||
|
this.lockingTime = lockingTime;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export class Voter {
|
||||||
|
constructor(reputationPublicKey) {
|
||||||
|
this.reputationPublicKey = reputationPublicKey;
|
||||||
|
this.voteHistory = [];
|
||||||
|
this.dateLastVote = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVoteRecord(vote) {
|
||||||
|
this.voteHistory.push(vote);
|
||||||
|
if (!this.dateLastVote || vote.dateStart > this.dateLastVote) {
|
||||||
|
this.dateLastVote = vote.dateStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
import { Box } from './classes/box.js';
|
import { Box } from './classes/box.js';
|
||||||
import { Scene } from './classes/scene.js';
|
import { Scene } from './classes/scene.js';
|
||||||
import { Member } from './classes/member.js';
|
import { Member } from './classes/member.js';
|
||||||
import { ValidationPool } from './classes/validation-pool.js';
|
import { ValidationPool } from './classes/validation-pool/index.js';
|
||||||
|
|
||||||
|
const delay = async (ms) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
};
|
||||||
|
|
||||||
const rootElement = document.getElementById('validation-pool');
|
const rootElement = document.getElementById('validation-pool');
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
|
@ -10,15 +14,27 @@ const scene = window.scene = new Scene('Validation Pool test', rootBox).log('seq
|
||||||
|
|
||||||
const pool = window.validationPool = new ValidationPool("validationPool", scene);
|
const pool = window.validationPool = new ValidationPool("validationPool", scene);
|
||||||
|
|
||||||
const member1 = window.member1 = await new Member("member1", scene).initialize();
|
const member1 = window.member1 = (await new Member("member1", scene).initialize()).setValue('rep', 0);
|
||||||
const member2 = window.member2 = await new Member("member2", scene).initialize();
|
const member2 = window.member2 = (await new Member("member2", scene).initialize()).setValue('rep', 0);
|
||||||
|
|
||||||
const voteId = pool.initiateVote({fee: 1, duration: 1, isBinding: false});
|
await delay(1000);
|
||||||
|
|
||||||
await member1.castVote(pool, voteId, true, 50);
|
// First member can self-approve
|
||||||
await member2.castVote(pool, voteId, true, 50);
|
const vote1 = member1.initiateVote(pool, {fee: 1, duration: 1000, tokenLossRatio: 1});
|
||||||
|
await member1.castVote(pool, vote1, true, 0, 0);
|
||||||
|
await member1.revealIdentity(pool, vote1); // Vote passes
|
||||||
|
member1.setValue('rep', pool.reputations.getTokens(member1.reputationPublicKey));
|
||||||
|
|
||||||
await member1.revealIdentity(pool, voteId);
|
// const vote2 = member2.initiateVote(pool, {fee: 1, duration: 1000, tokenLossRatio: 1});
|
||||||
await member2.revealIdentity(pool, voteId);
|
// await member2.castVote(pool, vote2, true, 0);
|
||||||
|
// await member2.revealIdentity(pool, vote2); // Quorum is not met!
|
||||||
|
|
||||||
// await scene.renderSequenceDiagram();
|
await delay(1000);
|
||||||
|
|
||||||
|
// Second member must be approved by first member
|
||||||
|
const vote2 = member2.initiateVote(pool, {fee: 1, duration: 1000, tokenLossRatio: 1});
|
||||||
|
await member1.castVote(pool, vote2, true, 0.5, 1);
|
||||||
|
await member1.revealIdentity(pool, vote2); // Vote passes
|
||||||
|
|
||||||
|
member1.setValue('rep', pool.reputations.getTokens(member1.reputationPublicKey));
|
||||||
|
member2.setValue('rep', pool.reputations.getTokens(member2.reputationPublicKey));
|
||||||
|
|
Loading…
Reference in New Issue