diff --git a/forum-network/src/classes/availability.js b/forum-network/src/classes/availability.js index c905a53..2bdf7c8 100644 --- a/forum-network/src/classes/availability.js +++ b/forum-network/src/classes/availability.js @@ -2,14 +2,11 @@ import { Action } from './action.js'; import { Actor } from './actor.js'; class Worker { - stake = 0; - - available = true; - - assignedRequestId = null; - - constructor(reputationPublicKey) { - this.reputationPublicKey = reputationPublicKey; + constructor(tokenId) { + this.tokenId = tokenId; + this.stakeAmount = 0; + this.available = true; + this.assignedRequestId = null; } } @@ -28,16 +25,16 @@ export class Availability extends Actor { }; } - register(reputationPublicKey, stake, __duration) { + register({ stakeAmount, tokenId }) { // TODO: expire after duration // ? Is a particular stake amount required? - const worker = this.workers.get(reputationPublicKey) ?? new Worker(reputationPublicKey); + const worker = this.workers.get(tokenId) ?? new Worker(tokenId); if (!worker.available) { throw new Error('Worker is already registered and busy. Can not increase stake.'); } - worker.stake += stake; + worker.stakeAmount += stakeAmount; // TODO: Interact with Bench contract to encumber reputation? - this.workers.set(reputationPublicKey, worker); + this.workers.set(tokenId, worker); } // unregister() { } diff --git a/forum-network/src/classes/bench.js b/forum-network/src/classes/bench.js index 4f26d08..4abe4de 100644 --- a/forum-network/src/classes/bench.js +++ b/forum-network/src/classes/bench.js @@ -1,8 +1,8 @@ import { Actor } from './actor.js'; -import { Reputations } from './reputation.js'; import { ValidationPool } from './validation-pool.js'; import params from '../params.js'; import { Action } from './action.js'; +import { ReputationTokenContract } from './reputation-token.js'; /** * Purpose: Keep track of reputation holders @@ -13,7 +13,7 @@ export class Bench extends Actor { this.forum = forum; this.validationPools = new Map(); this.voters = new Map(); - this.reputations = new Reputations(); + this.reputation = new ReputationTokenContract(); this.actions = { createValidationPool: new Action('create validation pool', scene), @@ -33,53 +33,22 @@ export class Bench extends Actor { }); } - getTotalReputation() { - return this.reputations.getTotal(); - } - - getTotalAvailableReputation() { - return this.reputations.getTotalAvailable(); - } - - getTotalActiveReputation() { + getActiveReputation() { return this.listActiveVoters() - .map(({ reputationPublicKey }) => this.reputations.getTokens(reputationPublicKey)) + .map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey)) .reduce((acc, cur) => (acc += cur), 0); } - getTotalActiveAvailableReputation() { + getActiveAvailableReputation() { return this.listActiveVoters() - .map(({ reputationPublicKey }) => this.reputations.getAvailableTokens(reputationPublicKey)) + .map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey)) .reduce((acc, cur) => (acc += cur), 0); } - async initiateValidationPool({ - postId, - fee, - duration, - tokenLossRatio, - contentiousDebate, - signingPublicKey, - authorStake, - anonymous, - }) { + async initiateValidationPool(poolOptions) { const validationPoolNumber = this.validationPools.size + 1; - const validationPool = new ValidationPool( - this, - this.forum, - { - postId, - fee, - duration, - tokenLossRatio, - contentiousDebate, - signingPublicKey, - authorStake, - anonymous, - }, - `Pool${validationPoolNumber}`, - this.scene, - ); + const name = `Pool${validationPoolNumber}`; + const validationPool = new ValidationPool(this, this.forum, poolOptions, name, this.scene); this.validationPools.set(validationPool.id, validationPool); await this.actions.createValidationPool.log(this, validationPool); validationPool.activate(); diff --git a/forum-network/src/classes/business.js b/forum-network/src/classes/business.js index 37753af..dc89574 100644 --- a/forum-network/src/classes/business.js +++ b/forum-network/src/classes/business.js @@ -66,9 +66,9 @@ export class Business extends Actor { fee: request.fee, duration, tokenLossRatio, - signingPublicKey: reputationPublicKey, - anonymous: false, - authorStake: this.worker.stake, + reputationPublicKey, + authorStakeAmount: this.worker.stakeAmount, + tokenId: this.worker.tokenId, }); // When the validation pool concludes, diff --git a/forum-network/src/classes/erc721.js b/forum-network/src/classes/erc721.js index 239ea28..60da455 100644 --- a/forum-network/src/classes/erc721.js +++ b/forum-network/src/classes/erc721.js @@ -56,6 +56,7 @@ export class ERC721 /* is ERC165 */ { if (!owner) { throw new Error('ERC721: invalid token ID'); } + return owner; } transferFrom(from, to, tokenId) { @@ -65,6 +66,7 @@ export class ERC721 /* is ERC165 */ { } this.incrementBalance(from, -1); this.incrementBalance(to, 1); + this.owners.set(tokenId, to); } /// @notice Enable or disable approval for a third party ("operator") to manage diff --git a/forum-network/src/classes/expert.js b/forum-network/src/classes/expert.js index 9066269..cf35435 100644 --- a/forum-network/src/classes/expert.js +++ b/forum-network/src/classes/expert.js @@ -1,22 +1,22 @@ -import { Actor } from './actor.js'; import { Action } from './action.js'; import { PostMessage } from './message.js'; import { CryptoUtil } from './crypto.js'; +import { ReputationHolder } from './reputation-holder.js'; -export class Expert extends Actor { +export class Expert extends ReputationHolder { constructor(name, scene) { - super(name, scene); + super(undefined, name, scene); this.actions = { submitPostViaNetwork: new Action('submit post via network', scene), submitPost: new Action('submit post', scene), initiateValidationPool: new Action('initiate validation pool', 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), submitWork: new Action('submit work evidence', scene), }; this.validationPools = new Map(); + this.tokens = []; } async initialize() { @@ -46,60 +46,42 @@ export class Expert extends Actor { await this.actions.submitPost.log(this, forum); const postId = await forum.addPost(this.reputationPublicKey, postContent); const pool = await this.initiateValidationPool(bench, { ...poolOptions, postId, anonymous: false }); + this.tokens.push(pool.tokenId); return { postId, pool }; } async initiateValidationPool(bench, poolOptions) { // For now, directly call bench.initiateValidationPool(); - if (poolOptions.anonymous) { - const signingKey = await CryptoUtil.generateAsymmetricKey(); - poolOptions.signingPublicKey = await CryptoUtil.exportKey(signingKey.publicKey); - } else { - poolOptions.signingPublicKey = this.reputationPublicKey; - } + poolOptions.reputationPublicKey = this.reputationPublicKey; await this.actions.initiateValidationPool.log( this, bench, - `(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStake ?? 0})`, + `(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`, ); const pool = await bench.initiateValidationPool(poolOptions); + this.tokens.push(pool.tokenId); this.validationPools.set(pool.id, poolOptions); return pool; } async stake(validationPool, { - position, amount, lockingTime, anonymous = false, + position, amount, lockingTime, }) { - let signingPublicKey; - if (anonymous) { - const signingKey = await CryptoUtil.generateAsymmetricKey(); - signingPublicKey = await CryptoUtil.exportKey(signingKey.publicKey); - this.validationPools.set(validationPool.id, { signingPublicKey }); - } else { - signingPublicKey = this.reputationPublicKey; - } // TODO: encrypt stake // TODO: sign message await this.actions.stake.log( this, validationPool, - `(${position ? 'for' : 'against'}, stake: ${amount}, anonymous: ${anonymous})`, + `(${position ? 'for' : 'against'}, stake: ${amount})`, ); - return validationPool.stake(signingPublicKey, { - position, amount, lockingTime, anonymous, + return validationPool.stake(this, { + position, amount, lockingTime, tokenId: this.tokens[0], }); } - async revealIdentity(validationPool) { - const { signingPublicKey } = this.validationPools.get(validationPool.id); - // TODO: sign message - await this.actions.revealIdentity.log(this, validationPool); - validationPool.revealIdentity(signingPublicKey, this.reputationPublicKey); - } - - async registerAvailability(availability, stake) { - await this.actions.registerAvailability.log(this, availability, `(stake: ${stake})`); - await availability.register(this.reputationPublicKey, stake); + async registerAvailability(availability, stakeAmount) { + await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount})`); + await availability.register({ stakeAmount, tokenId: this.tokens[0].id }); } async getAssignedWork(availability, business) { diff --git a/forum-network/src/classes/forum.js b/forum-network/src/classes/forum.js index 2e143a6..717b144 100644 --- a/forum-network/src/classes/forum.js +++ b/forum-network/src/classes/forum.js @@ -3,13 +3,14 @@ import { Graph } from './graph.js'; import { Action } from './action.js'; import { CryptoUtil } from './crypto.js'; import params from '../params.js'; +import { ReputationHolder } from './reputation-holder.js'; class Post extends Actor { constructor(forum, authorPublicKey, postContent) { const index = forum.posts.countVertices(); const name = `Post${index + 1}`; super(name, forum.scene); - this.id = postContent.id ?? CryptoUtil.randomUUID(); + this.id = postContent.id ?? `post_${CryptoUtil.randomUUID()}`; this.authorPublicKey = authorPublicKey; this.value = 0; this.citations = postContent.citations; @@ -28,9 +29,10 @@ class Post extends Actor { /** * Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts */ -export class Forum extends Actor { +export class Forum extends ReputationHolder { constructor(name, scene) { - super(name, scene); + super(`forum_${CryptoUtil.randomUUID()}`, name, scene); + this.id = this.reputationPublicKey; this.posts = new Graph(scene); this.actions = { addPost: new Action('add post', scene), @@ -74,8 +76,10 @@ export class Forum extends Actor { return this.getPosts().reduce((total, { value }) => total += value, 0); } - async onValidate(bench, pool, postId, initialValue) { - initialValue *= params.initialPostValue(); + async onValidate({ + bench, pool, postId, tokenId, + }) { + const initialValue = bench.reputation.valueOf(tokenId); if (this.scene.flowchart) { this.scene.flowchart.log(`${postId}_initial_value[${initialValue}] -- initial value --> ${postId}`); @@ -84,14 +88,21 @@ export class Forum extends Actor { const post = this.getPost(postId); post.setStatus('Validated'); + // Store a reference to the reputation token associated with this post, + // so that its value can be updated by future validated posts. + post.tokenId = tokenId; + // Compute rewards const rewardsAccumulator = new Map(); await this.propagateValue(rewardsAccumulator, pool, post, initialValue); - // Apply computed rewards + // Apply computed rewards to update values of tokens for (const [id, value] of rewardsAccumulator) { - bench.reputations.addTokens(id, value); + bench.reputation.transferValueFrom(post.tokenId, id, value); } + + // Transfer ownership of the minted/staked token, from the forum to the post author + bench.reputation.transferFrom(this.id, post.authorPublicKey, post.tokenId); } async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) { @@ -123,7 +134,7 @@ export class Forum extends Actor { const appliedIncrement = newValue - post.value; // Award reputation to post author - rewardsAccumulator.set(post.authorPublicKey, appliedIncrement); + rewardsAccumulator.set(post.tokenId, appliedIncrement); // Increment the value of the post await this.setPostValue(post, newValue); diff --git a/forum-network/src/classes/reputation-holder.js b/forum-network/src/classes/reputation-holder.js new file mode 100644 index 0000000..5bfcc42 --- /dev/null +++ b/forum-network/src/classes/reputation-holder.js @@ -0,0 +1,8 @@ +import { Actor } from './actor.js'; + +export class ReputationHolder extends Actor { + constructor(reputationPublicKey, name, scene) { + super(name, scene); + this.reputationPublicKey = reputationPublicKey; + } +} diff --git a/forum-network/src/classes/reputation-token.js b/forum-network/src/classes/reputation-token.js index 6e48648..894ff74 100644 --- a/forum-network/src/classes/reputation-token.js +++ b/forum-network/src/classes/reputation-token.js @@ -1,28 +1,106 @@ import { ERC721 } from './erc721.js'; import { CryptoUtil } from './crypto.js'; -export class ReputationToken extends ERC721 { +const EPSILON = 2.23e-16; + +class Lock { + constructor(tokenId, amount, duration) { + this.dateCreated = new Date(); + this.tokenId = tokenId; + this.amount = amount; + this.duration = duration; + } +} + +export class ReputationTokenContract extends ERC721 { constructor() { super('Reputation', 'REP'); this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)} this.values = new Map(); // token id --> current value + this.locks = new Set(); // {tokenId, amount, start, duration} } mint(to, value, context) { - const tokenId = CryptoUtil.randomUUID(); + const tokenId = `token_${CryptoUtil.randomUUID()}`; super.mint(to, tokenId); this.values.set(tokenId, value); this.histories.set(tokenId, [{ increment: value, context }]); + return tokenId; } incrementValue(tokenId, increment, context) { const value = this.values.get(tokenId); const newValue = value + increment; - const history = this.histories.get(tokenId); - if (newValue < 0) { - throw new Error('Token value can not become negative'); + const history = this.histories.get(tokenId) || []; + + if (newValue < -EPSILON) { + throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`); } this.values.set(tokenId, newValue); history.push({ increment, context }); + this.histories.set(tokenId, history); + } + + transferValueFrom(fromTokenId, toTokenId, amount) { + const sourceAvailable = this.availableValueOf(fromTokenId); + const targetAvailable = this.availableValueOf(toTokenId); + if (sourceAvailable < amount - EPSILON) { + throw new Error('Token value transfer: source has insufficient available value. ' + + `Needs ${amount}; has ${sourceAvailable}.`); + } + if (targetAvailable < -amount + EPSILON) { + throw new Error('Token value transfer: target has insufficient available value. ' + + `Needs ${-amount}; has ${targetAvailable}.`); + } + this.incrementValue(fromTokenId, -amount); + this.incrementValue(toTokenId, amount); + } + + lock(tokenId, amount, duration) { + const lock = new Lock(tokenId, amount, duration); + this.locks.add(lock); + } + + historyOf(tokenId) { + return this.histories.get(tokenId); + } + + valueOf(tokenId) { + return this.values.get(tokenId); + } + + availableValueOf(tokenId) { + const amountLocked = Array.from(this.locks.values()) + .filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId) + .filter(({ dateCreated, duration }) => new Date() - dateCreated < duration) + .reduce((total, { amount }) => total += amount, 0); + + return this.valueOf(tokenId) - amountLocked; + } + + valueOwnedBy(ownerId) { + return Array.from(this.owners.entries()) + .filter(([__, owner]) => owner === ownerId) + .map(([tokenId, __]) => this.valueOf(tokenId)) + .reduce((total, value) => total += value, 0); + } + + availableValueOwnedBy(ownerId) { + return Array.from(this.owners.entries()) + .filter(([__, owner]) => owner === ownerId) + .map(([tokenId, __]) => this.availableValueOf(tokenId)) + .reduce((total, value) => total += value, 0); + } + + getTotal() { + return Array.from(this.values.values()).reduce((total, value) => total += value, 0); + } + + getTotalAvailable() { + const amountLocked = Array.from(this.locks.values()) + .filter(({ dateCreated, duration }) => new Date() - dateCreated < duration) + .reduce((total, { amount }) => total += amount, 0); + + return this.getTotal() - amountLocked; } } diff --git a/forum-network/src/classes/stake.js b/forum-network/src/classes/stake.js index efadb2f..5a5d02a 100644 --- a/forum-network/src/classes/stake.js +++ b/forum-network/src/classes/stake.js @@ -1,7 +1,10 @@ import params from '../params.js'; export class Stake { - constructor(position, amount, lockingTime) { + constructor({ + tokenId, position, amount, lockingTime, + }) { + this.tokenId = tokenId; this.position = position; this.amount = amount; this.lockingTime = lockingTime; diff --git a/forum-network/src/classes/validation-pool.js b/forum-network/src/classes/validation-pool.js index 8fe6fc1..13cb893 100644 --- a/forum-network/src/classes/validation-pool.js +++ b/forum-network/src/classes/validation-pool.js @@ -1,7 +1,7 @@ import { CryptoUtil } from './crypto.js'; +import { ReputationHolder } from './reputation-holder.js'; import { Stake } from './stake.js'; import { Voter } from './voter.js'; -import { Actor } from './actor.js'; import params from '../params.js'; const ValidationPoolStates = Object.freeze({ @@ -13,24 +13,24 @@ const ValidationPoolStates = Object.freeze({ /** * Purpose: Enable voting */ -export class ValidationPool extends Actor { +export class ValidationPool extends ReputationHolder { constructor( bench, forum, { postId, - signingPublicKey, + reputationPublicKey, fee, duration, tokenLossRatio, contentiousDebate = false, - authorStake = 0, - anonymous = true, + authorStakeAmount = 0, }, name, scene, ) { - super(name, scene); + super(`pool_${CryptoUtil.randomUUID()}`, name, scene); + this.id = this.reputationPublicKey; // If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio() if ( !contentiousDebate @@ -58,60 +58,29 @@ export class ValidationPool extends Actor { this.postId = postId; this.state = ValidationPoolStates.OPEN; this.setStatus('Open'); - this.stakes = new Map(); - this.voters = new Map(); - this.id = CryptoUtil.randomUUID(); + this.stakes = new Set(); this.dateStart = new Date(); - this.authorSigningPublicKey = signingPublicKey; - this.anonymous = anonymous; + this.authorReputationPublicKey = reputationPublicKey; this.fee = fee; this.duration = duration; this.tokenLossRatio = tokenLossRatio; this.contentiousDebate = contentiousDebate; - this.tokensMinted = fee * params.mintingRatio(); + this.mintedValue = fee * params.mintingRatio(); + this.tokenId = this.bench.reputation.mint(this.id, this.mintedValue); // 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.stake(signingPublicKey, { + this.stake(this, { position: true, - amount: this.tokensMinted * params.stakeForAuthor + authorStake, - anonymous, + amount: this.mintedValue * params.stakeForAuthor + authorStakeAmount, + tokenId: this.tokenId, }); - this.stake(this.id, { + this.stake(this, { position: false, - amount: this.tokensMinted * (1 - params.stakeForAuthor), + amount: this.mintedValue * (1 - params.stakeForAuthor), + tokenId: this.tokenId, }); } - async stake(signingPublicKey, { - position, amount, lockingTime = 0, anonymous = false, - }) { - if (this.state === ValidationPoolStates.CLOSED) { - throw new Error(`Validation pool ${this.id} is closed`); - } - if (this.duration && new Date() - this.dateStart > this.duration) { - throw new Error( - `Validation pool ${this.id} has expired, no new votes may be cast`, - ); - } - const stake = new Stake(position, amount, lockingTime); - this.stakes.set(signingPublicKey, stake); - console.log('new stake', stake); - if (!anonymous) { - await this.revealIdentity(signingPublicKey, signingPublicKey); - } - } - - async revealIdentity(signingPublicKey, reputationPublicKey) { - if (!this.stakes.get(signingPublicKey)) { - throw new Error('Must stake before revealing identity'); - } - const voter = this.bench.voters.get(reputationPublicKey) - ?? new Voter(reputationPublicKey); - voter.addVoteRecord(this); - this.bench.voters.set(reputationPublicKey, voter); - this.voters.set(signingPublicKey, voter); - } - getTokenLossRatio() { if (!this.contentiousDebate) { return this.tokenLossRatio; @@ -134,58 +103,79 @@ export class ValidationPool extends Actor { 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.stakes) { - const voter = this.voters.get(signingPublicKey); - this.bench.reputations.lockTokens( - voter.reputationPublicKey, - stake, - lockingTime, - ); - // TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties. - } - } - /** * @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][] + * @returns 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); + getStakes(outcome, { excludeSystem }) { + return Array.from(this.stakes.values()) + .filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId) + .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()) + getTotalStakedOnPost(outcome) { + return this.getStakes(outcome, { excludeSystem: false }) + .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); + getTotalValueOfStakesForOutcome(outcome) { + return this.getStakes(outcome, { excludeSystem: false }) + .reduce((total, { amount }) => (total += amount), 0); + } + + // TODO: This can be handled as a hook on receipt of reputation token transfer + async stake(reputationHolder, { + tokenId, position, amount, lockingTime = 0, + }) { + if (this.state === ValidationPoolStates.CLOSED) { + throw new Error(`Validation pool ${this.id} is closed.`); + } + + if (this.duration && new Date() - this.dateStart > this.duration) { + throw new Error( + `Validation pool ${this.id} has expired, no new votes may be cast.`, + ); + } + + const { reputationPublicKey } = reputationHolder; + if (reputationPublicKey !== this.bench.reputation.ownerOf(tokenId)) { + throw new Error('Reputation may only be staked by its owner!'); + } + + const stake = new Stake({ + tokenId, position, amount, lockingTime, + }); + this.stakes.add(stake); + + // Transfer staked amount from the sender to the validation pool + this.bench.reputation.transferValueFrom(tokenId, this.tokenId, amount); + + // Keep a record of voters and their votes + if (tokenId !== this.tokenId) { + const voter = this.bench.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey); + voter.addVoteRecord(this); + this.bench.voters.set(reputationPublicKey, voter); + } + } + + applyTokenLocking() { + // Before evaluating the winning conditions, + // we need to make sure any staked tokens are locked for the + // specified amounts of time. + for (const { tokenId, amount, lockingTime } of this.stakes.values()) { + this.bench.reputation.lock(tokenId, amount, lockingTime); + // TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties. + } } async evaluateWinningConditions() { @@ -196,16 +186,13 @@ 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.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 upvoteValue = this.getTotalValueOfStakesForOutcome(true); const downvoteValue = this.getTotalValueOfStakesForOutcome(false); - const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation(); + const activeAvailableReputation = this.bench.getActiveAvailableReputation(); const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; @@ -238,44 +225,35 @@ export class ValidationPool extends Actor { // 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 winningEntries = this.getStakes(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) { - const { reputationPublicKey } = this.voters.get(signingPublicKey); + for (const stake of winningEntries) { + const { tokenId, amount } = stake; const value = stake.getStakeValue(); const reward = tokensForWinners * (value / totalValueOfStakesForWin); - rewards.set(reputationPublicKey, reward); + // Also return each winning voter their staked amount + const reputationPublicKey = this.bench.reputation.ownerOf(tokenId); + console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`); + this.bench.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount); } - console.log('rewards for stakes', rewards); - - const authorReputationPublicKey = this.voters.get(this.authorSigningPublicKey).reputationPublicKey; - - // Distribute awards to voters other than the author - for (const [reputationPublicKey, amount] of rewards) { - if (reputationPublicKey !== authorReputationPublicKey) { - this.bench.reputations.addTokens(reputationPublicKey, amount); - console.log(`reward for stake by ${reputationPublicKey}:`, amount); - } - } - - if (votePasses) { + if (votePasses && !!this.forum) { // 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 }); + // const tokensForAuthor = this.mintedValue * params.stakeForAuthor + rewards.get(this.tokenId); + console.log(`sending reward for author stake to forum: ${this.bench.reputation.valueOf(this.tokenId)}`); + + // Transfer ownership of the minted token, from the pool to the forum + this.bench.reputation.transferFrom(this.id, this.forum.id, this.tokenId); - if (votePasses && !!this.forum) { // Recurse through forum to determine reputation effects - await this.forum.onValidate( - this.bench, - this, - this.postId, - tokensForAuthor, - ); - } + await this.forum.onValidate({ + bench: this.bench, + pool: this, + postId: this.postId, + tokenId: this.tokenId, + }); } console.log('pool complete'); diff --git a/forum-network/src/tests/availability.html b/forum-network/src/tests/availability.html index c2684bf..11259f7 100644 --- a/forum-network/src/tests/availability.html +++ b/forum-network/src/tests/availability.html @@ -7,22 +7,22 @@
diff --git a/forum-network/src/tests/debounce.html b/forum-network/src/tests/debounce.html index db51d69..0b3b146 100644 --- a/forum-network/src/tests/debounce.html +++ b/forum-network/src/tests/debounce.html @@ -7,14 +7,14 @@
diff --git a/forum-network/src/tests/forum-network.html b/forum-network/src/tests/forum-network.html index 738b423..ad0242b 100644 --- a/forum-network/src/tests/forum-network.html +++ b/forum-network/src/tests/forum-network.html @@ -7,36 +7,36 @@
diff --git a/forum-network/src/tests/reputation.html b/forum-network/src/tests/reputation.html new file mode 100644 index 0000000..6fc7b3c --- /dev/null +++ b/forum-network/src/tests/reputation.html @@ -0,0 +1,32 @@ + + + Reputation test + + + +
+ + diff --git a/forum-network/src/tests/validation-pool.html b/forum-network/src/tests/validation-pool.html index 47005d1..4280a91 100644 --- a/forum-network/src/tests/validation-pool.html +++ b/forum-network/src/tests/validation-pool.html @@ -7,43 +7,45 @@