From ea087451ff8f3d58798267adde23b97f66c4ef33 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Thu, 12 Jan 2023 16:41:55 -0600 Subject: [PATCH] Refactor --- forum-network/src/classes/expert.js | 16 +-- forum-network/src/classes/forum.js | 46 ++++--- forum-network/src/classes/message.js | 2 +- forum-network/src/classes/stake.js | 13 ++ forum-network/src/classes/validation-pool.js | 125 +++++++++++-------- forum-network/src/classes/vote.js | 8 -- forum-network/src/classes/voter.js | 8 +- forum-network/src/tests/availability.html | 6 +- forum-network/src/tests/forum.html | 6 +- forum-network/src/tests/validation-pool.html | 12 +- 10 files changed, 144 insertions(+), 98 deletions(-) create mode 100644 forum-network/src/classes/stake.js delete mode 100644 forum-network/src/classes/vote.js diff --git a/forum-network/src/classes/expert.js b/forum-network/src/classes/expert.js index 24437f7..9066269 100644 --- a/forum-network/src/classes/expert.js +++ b/forum-network/src/classes/expert.js @@ -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, }); } diff --git a/forum-network/src/classes/forum.js b/forum-network/src/classes/forum.js index dccea26..bd5a5a3 100644 --- a/forum-network/src/classes/forum.js +++ b/forum-network/src/classes/forum.js @@ -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; } } diff --git a/forum-network/src/classes/message.js b/forum-network/src/classes/message.js index aa31b19..03b7592 100644 --- a/forum-network/src/classes/message.js +++ b/forum-network/src/classes/message.js @@ -44,7 +44,7 @@ export class PostMessage extends Message { contentToJSON() { return { post: this.content.post.toJSON(), - stake: this.content.stake, + stakeAmount: this.content.stake, }; } } diff --git a/forum-network/src/classes/stake.js b/forum-network/src/classes/stake.js new file mode 100644 index 0000000..efadb2f --- /dev/null +++ b/forum-network/src/classes/stake.js @@ -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; + } +} diff --git a/forum-network/src/classes/validation-pool.js b/forum-network/src/classes/validation-pool.js index 63cb33f..8fe6fc1 100644 --- a/forum-network/src/classes/validation-pool.js +++ b/forum-network/src/classes/validation-pool.js @@ -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 diff --git a/forum-network/src/classes/vote.js b/forum-network/src/classes/vote.js deleted file mode 100644 index c91fc81..0000000 --- a/forum-network/src/classes/vote.js +++ /dev/null @@ -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; - } -} diff --git a/forum-network/src/classes/voter.js b/forum-network/src/classes/voter.js index 046fc80..ea5fed8 100644 --- a/forum-network/src/classes/voter.js +++ b/forum-network/src/classes/voter.js @@ -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; } } } diff --git a/forum-network/src/tests/availability.html b/forum-network/src/tests/availability.html index 4e1766a..1d957eb 100644 --- a/forum-network/src/tests/availability.html +++ b/forum-network/src/tests/availability.html @@ -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(); diff --git a/forum-network/src/tests/forum.html b/forum-network/src/tests/forum.html index 176e9c1..a9b0257 100644 --- a/forum-network/src/tests/forum.html +++ b/forum-network/src/tests/forum.html @@ -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(); diff --git a/forum-network/src/tests/validation-pool.html b/forum-network/src/tests/validation-pool.html index c5cb21e..39821cc 100644 --- a/forum-network/src/tests/validation-pool.html +++ b/forum-network/src/tests/validation-pool.html @@ -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); }