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),
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,
});
}

View File

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

View File

@ -44,7 +44,7 @@ export class PostMessage extends Message {
contentToJSON() {
return {
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 { 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

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

View File

@ -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();

View File

@ -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();

View File

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