From 9deaf4db078f3408a469fd1fae4090a53775133d Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Thu, 26 Jan 2023 10:44:57 -0600 Subject: [PATCH] Update business and availability contracts to use rep tokens --- forum-network/notes/chain.md | 30 +++++++++++ forum-network/src/classes/availability.js | 52 +++++++++++-------- forum-network/src/classes/bench.js | 22 +++++--- forum-network/src/classes/business.js | 23 +++++--- forum-network/src/classes/erc721.js | 2 +- forum-network/src/classes/expert.js | 16 +++--- forum-network/src/classes/forum.js | 6 +++ forum-network/src/classes/reputation-token.js | 3 ++ forum-network/src/classes/scene.js | 4 ++ forum-network/src/classes/validation-pool.js | 21 +++++--- forum-network/src/tests/availability.html | 47 ++++++++++++++--- forum-network/src/tests/forum.html | 7 +-- 12 files changed, 174 insertions(+), 59 deletions(-) create mode 100644 forum-network/notes/chain.md diff --git a/forum-network/notes/chain.md b/forum-network/notes/chain.md new file mode 100644 index 0000000..6d2ea62 --- /dev/null +++ b/forum-network/notes/chain.md @@ -0,0 +1,30 @@ +We've considered implementing this validation pool + forum structure as smart contracts. +However, we expect that such contracts would be expensive to run, because the recursive algorithm for distributing reputation via the forum will incur a lot of computation, consuming a lot of gas. + +Can we bake this reputation algorithm into the core protocol of our blockchain? + +The structure seems to be similar to proof-of-stake. A big difference is that what is staked and awarded is reputation rather than currency. +The idea with reputation is that it entitles you to a proportional share of revenue earned by the network. +So what does that look like in this context? + +Let's say we are extending Ethereum. ETH would continue to be the currency that users must spend in order to execute transactions. +So when a user wants to execute a transaction, they must pay a fee. +A portion of this fee could then be distributed to reputation holders. + +- https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/ +- https://ethereum.org/en/developers/docs/nodes-and-clients/ + +--- + +execution client +execution gossip network + +consensus client +consensus gossip network + +--- + +cardano -- "dynamic availability"? +staking pools -- does it make sense with reputation? +what about for governance voting -- +do we want a representative republic or a democracy? diff --git a/forum-network/src/classes/availability.js b/forum-network/src/classes/availability.js index 2bdf7c8..cf34d3d 100644 --- a/forum-network/src/classes/availability.js +++ b/forum-network/src/classes/availability.js @@ -1,10 +1,13 @@ import { Action } from './action.js'; import { Actor } from './actor.js'; +import { CryptoUtil } from './crypto.js'; class Worker { - constructor(tokenId) { + constructor(reputationPublicKey, tokenId, stakeAmount, duration) { + this.reputationPublicKey = reputationPublicKey; this.tokenId = tokenId; - this.stakeAmount = 0; + this.stakeAmount = stakeAmount; + this.duration = duration; this.available = true; this.assignedRequestId = null; } @@ -14,8 +17,6 @@ class Worker { * Purpose: Enable staking reputation to enter the pool of workers */ export class Availability extends Actor { - workers = new Map(); - constructor(bench, name, scene) { super(name, scene); this.bench = bench; @@ -23,39 +24,48 @@ export class Availability extends Actor { this.actions = { assignWork: new Action('assign work', scene), }; + + this.workers = new Map(); } - register({ stakeAmount, tokenId }) { - // TODO: expire after duration - // ? Is a particular stake amount required? - 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.stakeAmount += stakeAmount; - // TODO: Interact with Bench contract to encumber reputation? - this.workers.set(tokenId, worker); + register(reputationPublicKey, { stakeAmount, tokenId, duration }) { + // TODO: Should be signed by token owner + this.bench.reputation.lock(tokenId, stakeAmount, duration); + const workerId = CryptoUtil.randomUUID(); + this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration)); + return workerId; } - // unregister() { } - get availableWorkers() { return Array.from(this.workers.values()).filter(({ available }) => !!available); } async assignWork(requestId) { - // Get random worker - // TODO: Probability proportional to stakes - const index = Math.floor(Math.random() * this.availableWorkers.length); + const totalAmountStaked = this.availableWorkers + .reduce((total, { stakeAmount }) => total += stakeAmount, 0); + // Imagine all these amounts layed end-to-end along a number line. + // To weight choice by amount staked, pick a stake by choosing a number at random + // from within that line segment. + const randomChoice = Math.random() * totalAmountStaked; + let index = 0; + let acc = 0; + for (const { stakeAmount } of this.workers.values()) { + acc += stakeAmount; + if (acc >= randomChoice) { + break; + } + index += 1; + } const worker = this.availableWorkers[index]; worker.available = false; worker.assignedRequestId = requestId; + // TODO: Notify assignee return worker; } - async getAssignedWork(reputationPublicKey) { - const worker = this.workers.get(reputationPublicKey); + async getAssignedWork(workerId) { + const worker = this.workers.get(workerId); return worker.assignedRequestId; } } diff --git a/forum-network/src/classes/bench.js b/forum-network/src/classes/bench.js index 4abe4de..7354e50 100644 --- a/forum-network/src/classes/bench.js +++ b/forum-network/src/classes/bench.js @@ -45,13 +45,23 @@ export class Bench extends Actor { .reduce((acc, cur) => (acc += cur), 0); } - async initiateValidationPool(poolOptions) { + async initiateValidationPool(poolOptions, stakeOptions) { const validationPoolNumber = this.validationPools.size + 1; 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(); - return validationPool; + const pool = new ValidationPool(this, this.forum, poolOptions, name, this.scene); + this.validationPools.set(pool.id, pool); + await this.actions.createValidationPool.log(this, pool); + pool.activate(); + + if (stakeOptions) { + const { reputationPublicKey, tokenId, authorStakeAmount } = stakeOptions; + await pool.stake(reputationPublicKey, { + tokenId, + position: true, + amount: authorStakeAmount, + }); + } + + return pool; } } diff --git a/forum-network/src/classes/business.js b/forum-network/src/classes/business.js index dc89574..4dd5f42 100644 --- a/forum-network/src/classes/business.js +++ b/forum-network/src/classes/business.js @@ -8,6 +8,7 @@ class Request { this.id = CryptoUtil.randomUUID(); this.fee = fee; this.content = content; + this.worker = null; } } @@ -15,8 +16,6 @@ class Request { * Purpose: Enable fee-driven work requests, to be completed by workers from the availability pool */ export class Business extends Actor { - requests = new Map(); - constructor(bench, forum, availability, name, scene) { super(name, scene); this.bench = bench; @@ -28,13 +27,16 @@ export class Business extends Actor { submitPost: new Action('submit post', scene), initiateValidationPool: new Action('initiate validation pool', scene), }; + + this.requests = new Map(); } async submitRequest(fee, content) { const request = new Request(fee, content); this.requests.set(request.id, request); await this.actions.assignWork.log(this, this.availability); - this.worker = await this.availability.assignWork(request.id); + const worker = await this.availability.assignWork(request.id); + request.worker = worker; return request.id; } @@ -49,26 +51,33 @@ export class Business extends Actor { throw new Error(`Request not found! id: ${requestId}`); } + if (reputationPublicKey !== request.worker.reputationPublicKey) { + throw new Error('Work evidence must be submitted by the assigned worker!'); + } + // Create a post representing this submission. const post = new PostContent({ requestId, workEvidence, }); + const requestIndex = Array.from(this.requests.values()) + .findIndex(({ id }) => id === request.id); + post.setTitle(`Work Evidence ${requestIndex + 1}`); + await this.actions.submitPost.log(this, this.forum); const postId = await this.forum.addPost(reputationPublicKey, post); // Initiate a validation pool for this work evidence. - // Validation pool supports secret ballots but we aren't using that here, since we want - // the post to be attributable to the reputation holder. await this.actions.initiateValidationPool.log(this, this.bench); const pool = await this.bench.initiateValidationPool({ postId, fee: request.fee, duration, tokenLossRatio, + }, { reputationPublicKey, - authorStakeAmount: this.worker.stakeAmount, - tokenId: this.worker.tokenId, + authorStakeAmount: request.worker.stakeAmount, + tokenId: request.worker.tokenId, }); // When the validation pool concludes, diff --git a/forum-network/src/classes/erc721.js b/forum-network/src/classes/erc721.js index 60da455..246dac3 100644 --- a/forum-network/src/classes/erc721.js +++ b/forum-network/src/classes/erc721.js @@ -54,7 +54,7 @@ export class ERC721 /* is ERC165 */ { ownerOf(tokenId) { const owner = this.owners.get(tokenId); if (!owner) { - throw new Error('ERC721: invalid token ID'); + throw new Error(`ERC721: invalid token ID: ${tokenId}`); } return owner; } diff --git a/forum-network/src/classes/expert.js b/forum-network/src/classes/expert.js index cf35435..10781a1 100644 --- a/forum-network/src/classes/expert.js +++ b/forum-network/src/classes/expert.js @@ -45,7 +45,7 @@ export class Expert extends ReputationHolder { async submitPostWithFee(bench, forum, postContent, poolOptions) { 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 }); + const pool = await this.initiateValidationPool(bench, { ...poolOptions, postId }); this.tokens.push(pool.tokenId); return { postId, pool }; } @@ -74,18 +74,22 @@ export class Expert extends ReputationHolder { validationPool, `(${position ? 'for' : 'against'}, stake: ${amount})`, ); - return validationPool.stake(this, { + return validationPool.stake(this.reputationPublicKey, { position, amount, lockingTime, tokenId: this.tokens[0], }); } - async registerAvailability(availability, stakeAmount) { - await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount})`); - await availability.register({ stakeAmount, tokenId: this.tokens[0].id }); + async registerAvailability(availability, stakeAmount, duration) { + await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount}, duration: ${duration})`); + this.workerId = await availability.register(this.reputationPublicKey, { + stakeAmount, + tokenId: this.tokens[0], + duration, + }); } async getAssignedWork(availability, business) { - const requestId = await availability.getAssignedWork(this.reputationPublicKey); + const requestId = await availability.getAssignedWork(this.workerId); const request = await business.getRequest(requestId); return request; } diff --git a/forum-network/src/classes/forum.js b/forum-network/src/classes/forum.js index 717b144..6b7eb88 100644 --- a/forum-network/src/classes/forum.js +++ b/forum-network/src/classes/forum.js @@ -37,6 +37,7 @@ export class Forum extends ReputationHolder { this.actions = { addPost: new Action('add post', scene), propagateValue: new Action('propagate value', this.scene), + transfer: new Action('transfer', this.scene), }; } @@ -79,6 +80,7 @@ export class Forum extends ReputationHolder { async onValidate({ bench, pool, postId, tokenId, }) { + this.activate(); const initialValue = bench.reputation.valueOf(tokenId); if (this.scene.flowchart) { @@ -103,6 +105,10 @@ export class Forum extends ReputationHolder { // Transfer ownership of the minted/staked token, from the forum to the post author bench.reputation.transferFrom(this.id, post.authorPublicKey, post.tokenId); + const toActor = this.scene.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey); + const value = bench.reputation.valueOf(post.tokenId); + this.actions.transfer.log(this, toActor, `(value: ${value})`); + this.deactivate(); } async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) { diff --git a/forum-network/src/classes/reputation-token.js b/forum-network/src/classes/reputation-token.js index 894ff74..ee93440 100644 --- a/forum-network/src/classes/reputation-token.js +++ b/forum-network/src/classes/reputation-token.js @@ -42,6 +42,9 @@ export class ReputationTokenContract extends ERC721 { } transferValueFrom(fromTokenId, toTokenId, amount) { + if (amount === undefined) { + throw new Error('Transfer value: amount is undefined!'); + } const sourceAvailable = this.availableValueOf(fromTokenId); const targetAvailable = this.availableValueOf(toTokenId); if (sourceAvailable < amount - EPSILON) { diff --git a/forum-network/src/classes/scene.js b/forum-network/src/classes/scene.js index f98afb4..4d71e48 100644 --- a/forum-network/src/classes/scene.js +++ b/forum-network/src/classes/scene.js @@ -98,6 +98,10 @@ export class Scene { this.actors.add(actor); } + findActor(fn) { + return Array.from(this.actors.values()).find(fn); + } + addAction(name) { const action = new Action(name, this); return action; diff --git a/forum-network/src/classes/validation-pool.js b/forum-network/src/classes/validation-pool.js index 13cb893..9bbea84 100644 --- a/forum-network/src/classes/validation-pool.js +++ b/forum-network/src/classes/validation-pool.js @@ -3,6 +3,7 @@ import { ReputationHolder } from './reputation-holder.js'; import { Stake } from './stake.js'; import { Voter } from './voter.js'; import params from '../params.js'; +import { Action } from './action.js'; const ValidationPoolStates = Object.freeze({ OPEN: 'OPEN', @@ -24,13 +25,18 @@ export class ValidationPool extends ReputationHolder { duration, tokenLossRatio, contentiousDebate = false, - authorStakeAmount = 0, }, name, scene, ) { super(`pool_${CryptoUtil.randomUUID()}`, name, scene); this.id = this.reputationPublicKey; + + this.actions = { + reward: new Action('reward', scene), + transfer: new Action('transfer', scene), + }; + // If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio() if ( !contentiousDebate @@ -69,12 +75,12 @@ export class ValidationPool extends ReputationHolder { 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(this, { + this.stake(this.id, { position: true, - amount: this.mintedValue * params.stakeForAuthor + authorStakeAmount, + amount: this.mintedValue * params.stakeForAuthor, tokenId: this.tokenId, }); - this.stake(this, { + this.stake(this.id, { position: false, amount: this.mintedValue * (1 - params.stakeForAuthor), tokenId: this.tokenId, @@ -134,7 +140,7 @@ export class ValidationPool extends ReputationHolder { } // TODO: This can be handled as a hook on receipt of reputation token transfer - async stake(reputationHolder, { + async stake(reputationPublicKey, { tokenId, position, amount, lockingTime = 0, }) { if (this.state === ValidationPoolStates.CLOSED) { @@ -147,7 +153,6 @@ export class ValidationPool extends ReputationHolder { ); } - const { reputationPublicKey } = reputationHolder; if (reputationPublicKey !== this.bench.reputation.ownerOf(tokenId)) { throw new Error('Reputation may only be staked by its owner!'); } @@ -237,6 +242,8 @@ export class ValidationPool extends ReputationHolder { 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); + const toActor = this.scene.findActor((actor) => actor.reputationPublicKey === reputationPublicKey); + this.actions.reward.log(this, toActor, `(${reward})`); } if (votePasses && !!this.forum) { @@ -246,6 +253,8 @@ export class ValidationPool extends ReputationHolder { // Transfer ownership of the minted token, from the pool to the forum this.bench.reputation.transferFrom(this.id, this.forum.id, this.tokenId); + const value = this.bench.reputation.valueOf(this.tokenId); + this.actions.transfer.log(this, this.forum, `(value: ${value})`); // Recurse through forum to determine reputation effects await this.forum.onValidate({ diff --git a/forum-network/src/tests/availability.html b/forum-network/src/tests/availability.html index 11259f7..2486459 100644 --- a/forum-network/src/tests/availability.html +++ b/forum-network/src/tests/availability.html @@ -16,6 +16,7 @@ import { delay } from '../util.js'; import { Forum } from '../classes/forum.js'; import { Public } from '../classes/public.js'; + import { PostContent } from '../classes/post-content.js'; const DELAY_INTERVAL = 500; @@ -24,6 +25,7 @@ const scene = (window.scene = new Scene('Availability test', rootBox)); scene.withSequenceDiagram(); + scene.withFlowchart(); const experts = (window.experts = []); const newExpert = async () => { @@ -36,10 +38,9 @@ const expert1 = await newExpert(); const expert2 = await newExpert(); - await newExpert(); const forum = (window.forum = new Forum('Forum', scene)); const bench = (window.bench = new Bench(forum, 'Bench', scene)); - const availability = (window.bench = new Availability( + const availability = (window.availability = new Availability( bench, 'Availability', scene, @@ -64,9 +65,9 @@ await scene.sequence.render(); }; - const updateDisplayValuesAndDelay = async () => { + const updateDisplayValuesAndDelay = async (delayMs = DELAY_INTERVAL) => { await updateDisplayValues(); - await delay(DELAY_INTERVAL); + await delay(delayMs); }; const getActiveWorker = async () => { @@ -90,7 +91,6 @@ await expert.stake(pool, { position: true, amount: 1, - anonymous: false, }); } } @@ -98,9 +98,42 @@ await updateDisplayValuesAndDelay(); + // Experts gain initial reputation by submitting a post with fee + const { postId: postId1, pool: pool1 } = await expert1.submitPostWithFee( + bench, + forum, + new PostContent({ hello: 'there' }).setTitle('Post 1'), + { + fee: 10, + duration: 1000, + tokenLossRatio: 1, + }, +); + await updateDisplayValuesAndDelay(1000); + + await pool1.evaluateWinningConditions(); + await updateDisplayValuesAndDelay(); + + const { pool: pool2 } = await expert2.submitPostWithFee( + bench, + forum, + new PostContent({ hello: 'to you as well' }) + .setTitle('Post 2') + .addCitation(postId1, 0.5), + { + fee: 10, + duration: 1000, + tokenLossRatio: 1, + }, +); + await updateDisplayValuesAndDelay(1000); + + await pool2.evaluateWinningConditions(); + await updateDisplayValuesAndDelay(); + // Populate availability pool - await expert1.registerAvailability(availability, 1); - await expert2.registerAvailability(availability, 1); + await expert1.registerAvailability(availability, 1, 10000); + await expert2.registerAvailability(availability, 1, 10000); await updateDisplayValuesAndDelay(); // Submit work request diff --git a/forum-network/src/tests/forum.html b/forum-network/src/tests/forum.html index 7a03b44..27f953c 100644 --- a/forum-network/src/tests/forum.html +++ b/forum-network/src/tests/forum.html @@ -81,9 +81,6 @@ ); await updateDisplayValuesAndDelay(1000); - // await expert2.stake(pool1, { position: true, amount 1, anonymous: false }); - // await updateDisplayValuesAndDelay(); - await pool1.evaluateWinningConditions(); await updateDisplayValuesAndDelay(); @@ -104,7 +101,7 @@ ); await updateDisplayValuesAndDelay(1000); - // await expert1.stake(pool2, { position: true, amount 1, anonymous: false }); + // await expert1.stake(pool2, { position: true, amount 1}); // await updateDisplayValuesAndDelay(); await pool2.evaluateWinningConditions(); @@ -127,7 +124,7 @@ ); await updateDisplayValuesAndDelay(1000); - // await expert1.stake(pool3, { position: true, amount 1, anonymous: false }); + // await expert1.stake(pool3, { position: true, amount 1}); // await updateDisplayValuesAndDelay(); await pool3.evaluateWinningConditions();