This commit is contained in:
Ladd Hoffman 2023-01-12 16:41:55 -06:00
parent 92da97fede
commit ea087451ff
10 changed files with 144 additions and 98 deletions

View File

@ -10,7 +10,7 @@ export class Expert extends Actor {
submitPostViaNetwork: new Action('submit post via network', scene), submitPostViaNetwork: new Action('submit post via network', scene),
submitPost: new Action('submit post', scene), submitPost: new Action('submit post', scene),
initiateValidationPool: new Action('initiate validation pool', 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), revealIdentity: new Action('reveal identity', scene),
registerAvailability: new Action('register availability', scene), registerAvailability: new Action('register availability', scene),
getAssignedWork: new Action('get assigned work', scene), getAssignedWork: new Action('get assigned work', scene),
@ -67,8 +67,8 @@ export class Expert extends Actor {
return pool; return pool;
} }
async castVote(validationPool, { async stake(validationPool, {
position, stake, lockingTime, anonymous = true, position, amount, lockingTime, anonymous = false,
}) { }) {
let signingPublicKey; let signingPublicKey;
if (anonymous) { if (anonymous) {
@ -78,15 +78,15 @@ export class Expert extends Actor {
} else { } else {
signingPublicKey = this.reputationPublicKey; signingPublicKey = this.reputationPublicKey;
} }
// TODO: encrypt vote // TODO: encrypt stake
// TODO: sign message // TODO: sign message
await this.actions.castVote.log( await this.actions.stake.log(
this, this,
validationPool, validationPool,
`(${position ? 'for' : 'against'}, stake: ${stake}, anonymous: ${anonymous})`, `(${position ? 'for' : 'against'}, stake: ${amount}, anonymous: ${anonymous})`,
); );
return validationPool.castVote(signingPublicKey, { return validationPool.stake(signingPublicKey, {
position, stake, lockingTime, anonymous, position, amount, lockingTime, anonymous,
}); });
} }

View File

@ -85,15 +85,16 @@ export class Forum extends Actor {
post.setStatus('Validated'); post.setStatus('Validated');
// Compute rewards // Compute rewards
const rewards = new Map(); const rewardsAccumulator = new Map();
await this.propagateValue(rewards, pool, post, initialValue); await this.propagateValue(rewardsAccumulator, pool, post, initialValue);
// Apply computed rewards // Apply computed rewards
for (const [id, value] of rewards) { for (const [id, value] of rewardsAccumulator) {
bench.reputations.addTokens(id, value); 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) { if (params.referenceChainLimit >= 0 && depth > params.referenceChainLimit) {
return []; return [];
} }
@ -101,29 +102,44 @@ export class Forum extends Actor {
this.actions.propagateValue.log(fromActor, post, `(${increment})`); this.actions.propagateValue.log(fromActor, post, `(${increment})`);
// Recursively distribute reputation to citations, according to weights // 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) { for (const { postId: citedPostId, weight } of post.citations) {
const citedPost = this.getPost(citedPostId); 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 // 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 // Prevent value from decreasing below zero
const rawNewValue = post.value + adjustedIncrement; const rawNewValue = post.value + incrementAfterLeaching;
const newValue = Math.max(0, rawNewValue); 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; 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 // Increment the value of the post
await this.setPostValue(post, newValue); await this.setPostValue(post, newValue);
// Award reputation to post author return refundToInbound;
console.log(`reward for post author ${post.authorPublicKey}`, appliedIncrement);
rewards.set(post.authorPublicKey, appliedIncrement);
return upstreamRefund;
} }
} }

View File

@ -44,7 +44,7 @@ export class PostMessage extends Message {
contentToJSON() { contentToJSON() {
return { return {
post: this.content.post.toJSON(), post: this.content.post.toJSON(),
stake: this.content.stake, stakeAmount: this.content.stake,
}; };
} }
} }

View File

@ -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;
}
}

View File

@ -1,5 +1,5 @@
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
import { Vote } from './vote.js'; import { Stake } from './stake.js';
import { Voter } from './voter.js'; import { Voter } from './voter.js';
import { Actor } from './actor.js'; import { Actor } from './actor.js';
import params from '../params.js'; import params from '../params.js';
@ -58,7 +58,7 @@ export class ValidationPool extends Actor {
this.postId = postId; this.postId = postId;
this.state = ValidationPoolStates.OPEN; this.state = ValidationPoolStates.OPEN;
this.setStatus('Open'); this.setStatus('Open');
this.votes = new Map(); this.stakes = new Map();
this.voters = new Map(); this.voters = new Map();
this.id = CryptoUtil.randomUUID(); this.id = CryptoUtil.randomUUID();
this.dateStart = new Date(); this.dateStart = new Date();
@ -71,26 +71,20 @@ export class ValidationPool extends Actor {
this.tokensMinted = fee * params.mintingRatio(); this.tokensMinted = fee * params.mintingRatio();
// Tokens minted "for" the post go toward stake of author voting for their own post. // 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. // Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
this.castVote(signingPublicKey, { this.stake(signingPublicKey, {
position: true, position: true,
stake: this.tokensMinted * params.stakeForAuthor + authorStake, amount: this.tokensMinted * params.stakeForAuthor + authorStake,
anonymous, anonymous,
}); });
this.castVote(undefined, { this.stake(this.id, {
position: false, position: false,
stake: this.tokensMinted * (1 - params.stakeForAuthor), amount: this.tokensMinted * (1 - params.stakeForAuthor),
isSystemVote: true,
}); });
} }
async castVote(signingPublicKey, { async stake(signingPublicKey, {
position, stake, lockingTime = 0, anonymous = true, isSystemVote = false, 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) { if (this.state === ValidationPoolStates.CLOSED) {
throw new Error(`Validation pool ${this.id} is 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`, `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) { if (!anonymous) {
await this.revealIdentity(signingPublicKey, signingPublicKey); await this.revealIdentity(signingPublicKey, signingPublicKey);
} }
} }
listVotes(filter) {
return new Map(
Array.from(this.votes).filter(
([_, vote]) => filter(vote),
),
);
}
async revealIdentity(signingPublicKey, reputationPublicKey) { async revealIdentity(signingPublicKey, reputationPublicKey) {
if (!this.votes.get(signingPublicKey)) { if (!this.stakes.get(signingPublicKey)) {
throw new Error('Must vote before revealing identity'); throw new Error('Must stake before revealing identity');
} }
const voter = this.bench.voters.get(reputationPublicKey) const voter = this.bench.voters.get(reputationPublicKey)
?? new Voter(reputationPublicKey); ?? new Voter(reputationPublicKey);
@ -153,7 +141,7 @@ export class ValidationPool extends Actor {
for (const [ for (const [
signingPublicKey, signingPublicKey,
{ stake, lockingTime }, { stake, lockingTime },
] of this.votes) { ] of this.stakes) {
const voter = this.voters.get(signingPublicKey); const voter = this.voters.get(signingPublicKey);
this.bench.reputations.lockTokens( this.bench.reputations.lockTokens(
voter.reputationPublicKey, 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() { async evaluateWinningConditions() {
if (this.state === ValidationPoolStates.RESOLVED) { if (this.state === ValidationPoolStates.RESOLVED) {
throw new Error('Validation pool has already been resolved!'); throw new Error('Validation pool has already been resolved!');
@ -172,22 +196,15 @@ export class ValidationPool extends Actor {
if (elapsed < this.duration) { if (elapsed < this.duration) {
throw new Error(`Validation pool duration has not yet elapsed! ${this.duration - elapsed} ms remaining.`); 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!'); throw new Error('Not all voters have revealed their reputation public keys!');
} }
// Now we can evaluate winning conditions // Now we can evaluate winning conditions
this.state = ValidationPoolStates.CLOSED; this.state = ValidationPoolStates.CLOSED;
this.setStatus('Closed'); this.setStatus('Closed');
const getVoteValue = ({ stake, lockingTime }) => stake * lockingTime ** params.lockingTimeExponent; const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
const getTotalValue = (votePosition) => Array.from(this.listVotes( const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
({ position }) => position === votePosition,
).values())
.map(getVoteValue)
.reduce((acc, cur) => (acc += cur), 0);
const upvoteValue = getTotalValue(true);
const downvoteValue = getTotalValue(false);
const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation(); const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation();
const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
@ -202,7 +219,7 @@ export class ValidationPool extends Actor {
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`); this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
this.scene.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`); this.scene.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
this.applyTokenLocking(); this.applyTokenLocking();
await this.distributeReputation(result); await this.distributeReputation({ votePasses });
// TODO: distribute fees // TODO: distribute fees
} else { } else {
this.setStatus('Resolved - Quorum not met'); this.setStatus('Resolved - Quorum not met');
@ -215,38 +232,40 @@ export class ValidationPool extends Actor {
} }
async distributeReputation({ votePasses }) { async distributeReputation({ votePasses }) {
// For now we assume a tightly binding pool, where all staked reputation is lost
// TODO: Take tokenLossRatio into account // TODO: Take tokenLossRatio into account
const getTotalStaked = (votePosition, excludeSystem = false) => Array.from(this.listVotes( // TODO: revoke staked reputation from losing voters
({ 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);
// 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(); const rewards = new Map();
for (const [signingPublicKey, { stake }] of winningVotes) { for (const [signingPublicKey, stake] of winningVotes) {
const { reputationPublicKey } = this.voters.get(signingPublicKey); 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); rewards.set(reputationPublicKey, reward);
} }
console.log('rewards for stakes', rewards);
const authorReputationPublicKey = this.voters.get(this.authorSigningPublicKey).reputationPublicKey; const authorReputationPublicKey = this.voters.get(this.authorSigningPublicKey).reputationPublicKey;
// Distribute awards to voters other than the author // Distribute awards to voters other than the author
for (const [id, value] of rewards) { for (const [reputationPublicKey, amount] of rewards) {
if (id !== authorReputationPublicKey) { if (reputationPublicKey !== authorReputationPublicKey) {
this.bench.reputations.addTokens(id, value); this.bench.reputations.addTokens(reputationPublicKey, amount);
console.log(`reward for winning voter ${id}:`, value); console.log(`reward for stake by ${reputationPublicKey}:`, amount);
} }
} }
// TODO: revoke staked reputation from losing voters
if (votePasses) { if (votePasses) {
// Distribute awards to author via the forum // Distribute awards to author via the forum
const tokensForAuthor = this.tokensMinted * params.stakeForAuthor + rewards.get(authorReputationPublicKey); const tokensForAuthor = this.tokensMinted * params.stakeForAuthor + rewards.get(authorReputationPublicKey);
console.log('sending reward for author stake to forum', { tokensForAuthor });
if (votePasses && !!this.forum) { if (votePasses && !!this.forum) {
// Recurse through forum to determine reputation effects // Recurse through forum to determine reputation effects

View File

@ -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;
}
}

View File

@ -5,10 +5,10 @@ export class Voter {
this.dateLastVote = null; this.dateLastVote = null;
} }
addVoteRecord(vote) { addVoteRecord(stake) {
this.voteHistory.push(vote); this.voteHistory.push(stake);
if (!this.dateLastVote || vote.dateStart > this.dateLastVote) { if (!this.dateLastVote || stake.dateStart > this.dateLastVote) {
this.dateLastVote = vote.dateStart; this.dateLastVote = stake.dateStart;
} }
} }
} }

View File

@ -87,9 +87,9 @@
const voteForWorkEvidence = async (worker, pool) => { const voteForWorkEvidence = async (worker, pool) => {
for (const expert of experts) { for (const expert of experts) {
if (expert !== worker) { if (expert !== worker) {
await expert.castVote(pool, { await expert.stake(pool, {
position: true, position: true,
stake: 1, amount: 1,
anonymous: false, anonymous: false,
}); });
} }
@ -129,7 +129,7 @@
worker.deactivate(); worker.deactivate();
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();
// Vote on work evidence // Stake on work evidence
await voteForWorkEvidence(worker, pool); await voteForWorkEvidence(worker, pool);
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();

View File

@ -85,7 +85,7 @@
); );
await updateDisplayValuesAndDelay(1000); 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 updateDisplayValuesAndDelay();
await pool1.evaluateWinningConditions(); await pool1.evaluateWinningConditions();
@ -108,7 +108,7 @@
); );
await updateDisplayValuesAndDelay(1000); 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 updateDisplayValuesAndDelay();
await pool2.evaluateWinningConditions(); await pool2.evaluateWinningConditions();
@ -131,7 +131,7 @@
); );
await updateDisplayValuesAndDelay(1000); 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 updateDisplayValuesAndDelay();
await pool3.evaluateWinningConditions(); await pool3.evaluateWinningConditions();

View File

@ -73,7 +73,7 @@
} }
} }
await delay(1000); await delay(1000);
await pool.evaluateWinningConditions(); // Vote passes await pool.evaluateWinningConditions(); // Stake passes
await updateDisplayValues(); await updateDisplayValues();
await delay(1000); await delay(1000);
} }
@ -105,12 +105,18 @@
fee: 1, fee: 1,
duration: 1000, duration: 1000,
tokenLossRatio: 1, 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 expert1.revealIdentity(pool);
await expert2.revealIdentity(pool); await expert2.revealIdentity(pool);
await delay(1000); await delay(1000);
await pool.evaluateWinningConditions(); // Vote passes await pool.evaluateWinningConditions(); // Stake passes
await updateDisplayValues(); await updateDisplayValues();
await delay(1000); await delay(1000);
} }