Refactor
This commit is contained in:
parent
92da97fede
commit
ea087451ff
|
@ -10,7 +10,7 @@ export class Expert extends Actor {
|
|||
submitPostViaNetwork: new Action('submit post via network', scene),
|
||||
submitPost: new Action('submit post', scene),
|
||||
initiateValidationPool: new Action('initiate validation pool', scene),
|
||||
castVote: new Action('cast vote', scene),
|
||||
stake: new Action('stake on post', scene),
|
||||
revealIdentity: new Action('reveal identity', scene),
|
||||
registerAvailability: new Action('register availability', scene),
|
||||
getAssignedWork: new Action('get assigned work', scene),
|
||||
|
@ -67,8 +67,8 @@ export class Expert extends Actor {
|
|||
return pool;
|
||||
}
|
||||
|
||||
async castVote(validationPool, {
|
||||
position, stake, lockingTime, anonymous = true,
|
||||
async stake(validationPool, {
|
||||
position, amount, lockingTime, anonymous = false,
|
||||
}) {
|
||||
let signingPublicKey;
|
||||
if (anonymous) {
|
||||
|
@ -78,15 +78,15 @@ export class Expert extends Actor {
|
|||
} else {
|
||||
signingPublicKey = this.reputationPublicKey;
|
||||
}
|
||||
// TODO: encrypt vote
|
||||
// TODO: encrypt stake
|
||||
// TODO: sign message
|
||||
await this.actions.castVote.log(
|
||||
await this.actions.stake.log(
|
||||
this,
|
||||
validationPool,
|
||||
`(${position ? 'for' : 'against'}, stake: ${stake}, anonymous: ${anonymous})`,
|
||||
`(${position ? 'for' : 'against'}, stake: ${amount}, anonymous: ${anonymous})`,
|
||||
);
|
||||
return validationPool.castVote(signingPublicKey, {
|
||||
position, stake, lockingTime, anonymous,
|
||||
return validationPool.stake(signingPublicKey, {
|
||||
position, amount, lockingTime, anonymous,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -85,15 +85,16 @@ export class Forum extends Actor {
|
|||
post.setStatus('Validated');
|
||||
|
||||
// Compute rewards
|
||||
const rewards = new Map();
|
||||
await this.propagateValue(rewards, pool, post, initialValue);
|
||||
const rewardsAccumulator = new Map();
|
||||
await this.propagateValue(rewardsAccumulator, pool, post, initialValue);
|
||||
|
||||
// Apply computed rewards
|
||||
for (const [id, value] of rewards) {
|
||||
for (const [id, value] of rewardsAccumulator) {
|
||||
bench.reputations.addTokens(id, value);
|
||||
}
|
||||
}
|
||||
|
||||
async propagateValue(rewards, fromActor, post, increment, depth = 0) {
|
||||
async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) {
|
||||
if (params.referenceChainLimit >= 0 && depth > params.referenceChainLimit) {
|
||||
return [];
|
||||
}
|
||||
|
@ -101,29 +102,44 @@ export class Forum extends Actor {
|
|||
this.actions.propagateValue.log(fromActor, post, `(${increment})`);
|
||||
|
||||
// Recursively distribute reputation to citations, according to weights
|
||||
let downstreamRefund = 0;
|
||||
let totalOutboundAmount = 0;
|
||||
let refundFromOutbound = 0;
|
||||
for (const { postId: citedPostId, weight } of post.citations) {
|
||||
const citedPost = this.getPost(citedPostId);
|
||||
downstreamRefund += await this.propagateValue(rewards, post, citedPost, weight * increment, depth + 1);
|
||||
const outboundAmount = weight * increment;
|
||||
totalOutboundAmount += outboundAmount;
|
||||
refundFromOutbound += await this.propagateValue(rewardsAccumulator, post, citedPost, outboundAmount, depth + 1);
|
||||
}
|
||||
|
||||
// Apply leaching value
|
||||
const adjustedIncrement = increment * (1 - params.leachingValue * post.totalCitationWeight) + downstreamRefund;
|
||||
const incrementAfterLeaching = increment - (totalOutboundAmount - refundFromOutbound) * params.leachingValue;
|
||||
// const adjustedIncrement = increment - outboundReferencesTotal + refundFromOutbound;
|
||||
|
||||
// Prevent value from decreasing below zero
|
||||
const rawNewValue = post.value + adjustedIncrement;
|
||||
const rawNewValue = post.value + incrementAfterLeaching;
|
||||
const newValue = Math.max(0, rawNewValue);
|
||||
const upstreamRefund = rawNewValue < 0 ? rawNewValue : 0;
|
||||
// We "refund" the amount that could not be applied.
|
||||
// Note that this will always be a negative quantity, because this situation only arises when increment is negative.
|
||||
const refundToInbound = rawNewValue - newValue;
|
||||
const appliedIncrement = newValue - post.value;
|
||||
|
||||
// Award reputation to post author
|
||||
console.log(`reward for post author ${post.authorPublicKey}`, {
|
||||
increment,
|
||||
totalOutboundAmount,
|
||||
refundFromOutbound,
|
||||
incrementAfterLeaching,
|
||||
rawNewValue,
|
||||
newValue,
|
||||
appliedIncrement,
|
||||
refundToInbound,
|
||||
});
|
||||
|
||||
rewardsAccumulator.set(post.authorPublicKey, appliedIncrement);
|
||||
|
||||
// Increment the value of the post
|
||||
await this.setPostValue(post, newValue);
|
||||
|
||||
// Award reputation to post author
|
||||
console.log(`reward for post author ${post.authorPublicKey}`, appliedIncrement);
|
||||
|
||||
rewards.set(post.authorPublicKey, appliedIncrement);
|
||||
|
||||
return upstreamRefund;
|
||||
return refundToInbound;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export class PostMessage extends Message {
|
|||
contentToJSON() {
|
||||
return {
|
||||
post: this.content.post.toJSON(),
|
||||
stake: this.content.stake,
|
||||
stakeAmount: this.content.stake,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import params from '../params.js';
|
||||
|
||||
export class Stake {
|
||||
constructor(position, amount, lockingTime) {
|
||||
this.position = position;
|
||||
this.amount = amount;
|
||||
this.lockingTime = lockingTime;
|
||||
}
|
||||
|
||||
getStakeValue() {
|
||||
return this.amount * this.lockingTime ** params.lockingTimeExponent;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { CryptoUtil } from './crypto.js';
|
||||
import { Vote } from './vote.js';
|
||||
import { Stake } from './stake.js';
|
||||
import { Voter } from './voter.js';
|
||||
import { Actor } from './actor.js';
|
||||
import params from '../params.js';
|
||||
|
@ -58,7 +58,7 @@ export class ValidationPool extends Actor {
|
|||
this.postId = postId;
|
||||
this.state = ValidationPoolStates.OPEN;
|
||||
this.setStatus('Open');
|
||||
this.votes = new Map();
|
||||
this.stakes = new Map();
|
||||
this.voters = new Map();
|
||||
this.id = CryptoUtil.randomUUID();
|
||||
this.dateStart = new Date();
|
||||
|
@ -71,26 +71,20 @@ export class ValidationPool extends Actor {
|
|||
this.tokensMinted = fee * params.mintingRatio();
|
||||
// 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.
|
||||
this.castVote(signingPublicKey, {
|
||||
this.stake(signingPublicKey, {
|
||||
position: true,
|
||||
stake: this.tokensMinted * params.stakeForAuthor + authorStake,
|
||||
amount: this.tokensMinted * params.stakeForAuthor + authorStake,
|
||||
anonymous,
|
||||
});
|
||||
this.castVote(undefined, {
|
||||
this.stake(this.id, {
|
||||
position: false,
|
||||
stake: this.tokensMinted * (1 - params.stakeForAuthor),
|
||||
isSystemVote: true,
|
||||
amount: this.tokensMinted * (1 - params.stakeForAuthor),
|
||||
});
|
||||
}
|
||||
|
||||
async castVote(signingPublicKey, {
|
||||
position, stake, lockingTime = 0, anonymous = true, isSystemVote = false,
|
||||
async stake(signingPublicKey, {
|
||||
position, amount, lockingTime = 0, anonymous = false,
|
||||
}) {
|
||||
if (isSystemVote) {
|
||||
signingPublicKey = CryptoUtil.randomUUID();
|
||||
anonymous = false;
|
||||
}
|
||||
const vote = new Vote(position, stake, lockingTime, isSystemVote);
|
||||
if (this.state === ValidationPoolStates.CLOSED) {
|
||||
throw new Error(`Validation pool ${this.id} is closed`);
|
||||
}
|
||||
|
@ -99,23 +93,17 @@ export class ValidationPool extends Actor {
|
|||
`Validation pool ${this.id} has expired, no new votes may be cast`,
|
||||
);
|
||||
}
|
||||
this.votes.set(signingPublicKey, vote);
|
||||
const stake = new Stake(position, amount, lockingTime);
|
||||
this.stakes.set(signingPublicKey, stake);
|
||||
console.log('new stake', stake);
|
||||
if (!anonymous) {
|
||||
await this.revealIdentity(signingPublicKey, signingPublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
listVotes(filter) {
|
||||
return new Map(
|
||||
Array.from(this.votes).filter(
|
||||
([_, vote]) => filter(vote),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async revealIdentity(signingPublicKey, reputationPublicKey) {
|
||||
if (!this.votes.get(signingPublicKey)) {
|
||||
throw new Error('Must vote before revealing identity');
|
||||
if (!this.stakes.get(signingPublicKey)) {
|
||||
throw new Error('Must stake before revealing identity');
|
||||
}
|
||||
const voter = this.bench.voters.get(reputationPublicKey)
|
||||
?? new Voter(reputationPublicKey);
|
||||
|
@ -153,7 +141,7 @@ export class ValidationPool extends Actor {
|
|||
for (const [
|
||||
signingPublicKey,
|
||||
{ stake, lockingTime },
|
||||
] of this.votes) {
|
||||
] of this.stakes) {
|
||||
const voter = this.voters.get(signingPublicKey);
|
||||
this.bench.reputations.lockTokens(
|
||||
voter.reputationPublicKey,
|
||||
|
@ -164,6 +152,42 @@ export class ValidationPool extends Actor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @param {object} getStakeEntries options
|
||||
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
|
||||
* @returns [signingPublicKey, stake][]
|
||||
*/
|
||||
getStakeEntries(outcome, options = {}) {
|
||||
const { excludeSystem = false } = options;
|
||||
const entries = Array.from(this.stakes.entries());
|
||||
// console.log('entries', entries);
|
||||
return entries
|
||||
.filter(([signingPublicKey, __]) => !excludeSystem || signingPublicKey !== this.id)
|
||||
.filter(([__, { position }]) => outcome === null || position === outcome);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @param {object} getStakeEntries options
|
||||
* @returns number
|
||||
*/
|
||||
getTotalStakedOnPost(outcome, options) {
|
||||
return this.getStakeEntries(outcome, options)
|
||||
.map(([__, stake]) => stake.getStakeValue())
|
||||
.reduce((acc, cur) => (acc += cur), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @param {object} getStakeEntries options
|
||||
* @returns number
|
||||
*/
|
||||
getTotalValueOfStakesForOutcome(outcome, options) {
|
||||
return this.getStakeEntries(outcome, options)
|
||||
.reduce((total, [__, { amount }]) => (total += amount), 0);
|
||||
}
|
||||
|
||||
async evaluateWinningConditions() {
|
||||
if (this.state === ValidationPoolStates.RESOLVED) {
|
||||
throw new Error('Validation pool has already been resolved!');
|
||||
|
@ -172,22 +196,15 @@ export class ValidationPool extends Actor {
|
|||
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) {
|
||||
if (this.voters.size < this.stakes.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 = (votePosition) => Array.from(this.listVotes(
|
||||
({ position }) => position === votePosition,
|
||||
).values())
|
||||
.map(getVoteValue)
|
||||
.reduce((acc, cur) => (acc += cur), 0);
|
||||
|
||||
const upvoteValue = getTotalValue(true);
|
||||
const downvoteValue = getTotalValue(false);
|
||||
const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
|
||||
const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
|
||||
const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation();
|
||||
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
|
||||
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
||||
|
@ -202,7 +219,7 @@ export class ValidationPool extends Actor {
|
|||
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
|
||||
this.scene.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
|
||||
this.applyTokenLocking();
|
||||
await this.distributeReputation(result);
|
||||
await this.distributeReputation({ votePasses });
|
||||
// TODO: distribute fees
|
||||
} else {
|
||||
this.setStatus('Resolved - Quorum not met');
|
||||
|
@ -215,38 +232,40 @@ export class ValidationPool extends Actor {
|
|||
}
|
||||
|
||||
async distributeReputation({ votePasses }) {
|
||||
// For now we assume a tightly binding pool, where all staked reputation is lost
|
||||
// TODO: Take tokenLossRatio into account
|
||||
const getTotalStaked = (votePosition, excludeSystem = false) => Array.from(this.listVotes(
|
||||
({ position, isSystemVote }) => position === votePosition && (!excludeSystem || !isSystemVote),
|
||||
).values())
|
||||
.map(({ stake }) => stake)
|
||||
.reduce((acc, cur) => (acc += cur), 0);
|
||||
const tokensForWinners = getTotalStaked(!votePasses);
|
||||
const winningVotes = this.listVotes(({ position, isSystemVote }) => position === votePasses && !isSystemVote);
|
||||
// TODO: revoke staked reputation from losing voters
|
||||
|
||||
// Compute rewards for the winning voters, in proportion to their stakes
|
||||
// In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
|
||||
const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
|
||||
const winningVotes = this.getStakeEntries(votePasses, { excludeSystem: true });
|
||||
const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
|
||||
|
||||
// Compute rewards for the winning voters, in proportion to the value of their stakes.
|
||||
const rewards = new Map();
|
||||
for (const [signingPublicKey, { stake }] of winningVotes) {
|
||||
for (const [signingPublicKey, stake] of winningVotes) {
|
||||
const { reputationPublicKey } = this.voters.get(signingPublicKey);
|
||||
const reward = (tokensForWinners * stake) / getTotalStaked(votePasses);
|
||||
const value = stake.getStakeValue();
|
||||
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
|
||||
rewards.set(reputationPublicKey, reward);
|
||||
}
|
||||
|
||||
console.log('rewards for stakes', rewards);
|
||||
|
||||
const authorReputationPublicKey = this.voters.get(this.authorSigningPublicKey).reputationPublicKey;
|
||||
|
||||
// Distribute awards to voters other than the author
|
||||
for (const [id, value] of rewards) {
|
||||
if (id !== authorReputationPublicKey) {
|
||||
this.bench.reputations.addTokens(id, value);
|
||||
console.log(`reward for winning voter ${id}:`, value);
|
||||
for (const [reputationPublicKey, amount] of rewards) {
|
||||
if (reputationPublicKey !== authorReputationPublicKey) {
|
||||
this.bench.reputations.addTokens(reputationPublicKey, amount);
|
||||
console.log(`reward for stake by ${reputationPublicKey}:`, amount);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: revoke staked reputation from losing voters
|
||||
|
||||
if (votePasses) {
|
||||
// Distribute awards to author via the forum
|
||||
const tokensForAuthor = this.tokensMinted * params.stakeForAuthor + rewards.get(authorReputationPublicKey);
|
||||
console.log('sending reward for author stake to forum', { tokensForAuthor });
|
||||
|
||||
if (votePasses && !!this.forum) {
|
||||
// Recurse through forum to determine reputation effects
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
export class Vote {
|
||||
constructor(position, stake, lockingTime, isSystemVote = false) {
|
||||
this.position = position;
|
||||
this.stake = stake;
|
||||
this.lockingTime = lockingTime;
|
||||
this.isSystemVote = isSystemVote;
|
||||
}
|
||||
}
|
|
@ -5,10 +5,10 @@ export class Voter {
|
|||
this.dateLastVote = null;
|
||||
}
|
||||
|
||||
addVoteRecord(vote) {
|
||||
this.voteHistory.push(vote);
|
||||
if (!this.dateLastVote || vote.dateStart > this.dateLastVote) {
|
||||
this.dateLastVote = vote.dateStart;
|
||||
addVoteRecord(stake) {
|
||||
this.voteHistory.push(stake);
|
||||
if (!this.dateLastVote || stake.dateStart > this.dateLastVote) {
|
||||
this.dateLastVote = stake.dateStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,9 +87,9 @@
|
|||
const voteForWorkEvidence = async (worker, pool) => {
|
||||
for (const expert of experts) {
|
||||
if (expert !== worker) {
|
||||
await expert.castVote(pool, {
|
||||
await expert.stake(pool, {
|
||||
position: true,
|
||||
stake: 1,
|
||||
amount: 1,
|
||||
anonymous: false,
|
||||
});
|
||||
}
|
||||
|
@ -129,7 +129,7 @@
|
|||
worker.deactivate();
|
||||
await updateDisplayValuesAndDelay();
|
||||
|
||||
// Vote on work evidence
|
||||
// Stake on work evidence
|
||||
await voteForWorkEvidence(worker, pool);
|
||||
await updateDisplayValuesAndDelay();
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
);
|
||||
await updateDisplayValuesAndDelay(1000);
|
||||
|
||||
// await expert2.castVote(pool1, { position: true, stake: 1, anonymous: false });
|
||||
// await expert2.stake(pool1, { position: true, amount 1, anonymous: false });
|
||||
// await updateDisplayValuesAndDelay();
|
||||
|
||||
await pool1.evaluateWinningConditions();
|
||||
|
@ -108,7 +108,7 @@
|
|||
);
|
||||
await updateDisplayValuesAndDelay(1000);
|
||||
|
||||
// await expert1.castVote(pool2, { position: true, stake: 1, anonymous: false });
|
||||
// await expert1.stake(pool2, { position: true, amount 1, anonymous: false });
|
||||
// await updateDisplayValuesAndDelay();
|
||||
|
||||
await pool2.evaluateWinningConditions();
|
||||
|
@ -131,7 +131,7 @@
|
|||
);
|
||||
await updateDisplayValuesAndDelay(1000);
|
||||
|
||||
// await expert1.castVote(pool3, { position: true, stake: 1, anonymous: false });
|
||||
// await expert1.stake(pool3, { position: true, amount 1, anonymous: false });
|
||||
// await updateDisplayValuesAndDelay();
|
||||
|
||||
await pool3.evaluateWinningConditions();
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
}
|
||||
}
|
||||
await delay(1000);
|
||||
await pool.evaluateWinningConditions(); // Vote passes
|
||||
await pool.evaluateWinningConditions(); // Stake passes
|
||||
await updateDisplayValues();
|
||||
await delay(1000);
|
||||
}
|
||||
|
@ -105,12 +105,18 @@
|
|||
fee: 1,
|
||||
duration: 1000,
|
||||
tokenLossRatio: 1,
|
||||
anonymous: true,
|
||||
});
|
||||
await expert1.stake(pool, {
|
||||
position: true,
|
||||
amount: 4,
|
||||
lockingTime: 0,
|
||||
anonymous: true,
|
||||
});
|
||||
await expert1.castVote(pool, { position: true, stake: 4, lockingTime: 0 });
|
||||
await expert1.revealIdentity(pool);
|
||||
await expert2.revealIdentity(pool);
|
||||
await delay(1000);
|
||||
await pool.evaluateWinningConditions(); // Vote passes
|
||||
await pool.evaluateWinningConditions(); // Stake passes
|
||||
await updateDisplayValues();
|
||||
await delay(1000);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue