From fc3138adab33d2876448c1b6e2b9b41bf6add293 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Mon, 2 Jan 2023 13:14:32 -0600 Subject: [PATCH] Improve Availability test --- forum-network/notes.md | 20 ++++ forum-network/public/availability-test.html | 2 +- forum-network/public/availability-test.js | 92 +++++++++++++------ forum-network/public/classes/availability.js | 12 ++- forum-network/public/classes/bench.js | 2 + forum-network/public/classes/business.js | 23 ++++- forum-network/public/classes/forum.js | 8 -- forum-network/public/classes/member.js | 46 +++++++--- forum-network/public/classes/public.js | 16 ++++ .../public/classes/validation-pool.js | 70 +++++++++----- forum-network/public/debounce-test.html | 2 +- forum-network/public/forum-network-test.html | 2 +- forum-network/public/mermaid-test.html | 16 ++-- .../public/validation-pool-test.html | 2 +- forum-network/public/validation-pool-test.js | 13 ++- 15 files changed, 239 insertions(+), 87 deletions(-) diff --git a/forum-network/notes.md b/forum-network/notes.md index 3dc9c52..6975cb0 100644 --- a/forum-network/notes.md +++ b/forum-network/notes.md @@ -3,3 +3,23 @@ - Receiving payments - Distributing payments to participants - Computing updates to forum graph + +## Receiving payments + +Business SC will need to implement a financial model. + +# Excerpts from DeSciPubDAOArchit22July19PrintCut.pdf + +> With today’s prices, however, we will begin by programming this all off-chain and simplify the reputation tokens to be less dynamic in their evaluation. Next iteration improves the decentralization commensurate with practical realities. + +# Questions + +## Validation pool termination + +How do we want to handle this? +The validation pool specifies a duration. +We don't want to compute results until the end of this duration. +We're currently supporting anonymous voting. +With anonymous voting, we need to wait until the end of the vote duration, +and then have a separate interval in which voters reveal their identities. +For now, we can let anonymous voters reveal their identities at any time diff --git a/forum-network/public/availability-test.html b/forum-network/public/availability-test.html index 459d14c..5f3db62 100644 --- a/forum-network/public/availability-test.html +++ b/forum-network/public/availability-test.html @@ -1,6 +1,6 @@ - Forum + Availability test diff --git a/forum-network/public/availability-test.js b/forum-network/public/availability-test.js index d758536..285e3f6 100644 --- a/forum-network/public/availability-test.js +++ b/forum-network/public/availability-test.js @@ -6,17 +6,32 @@ import { Business } from './classes/business.js'; import { Availability } from './classes/availability.js'; import { delay } from './util.js'; import { Forum } from './classes/forum.js'; +import { Public } from './classes/public.js'; + +const DELAY_INTERVAL = 500; const rootElement = document.getElementById('availability-test'); const rootBox = new Box('rootBox', rootElement).flex(); const scene = window.scene = new Scene('Availability test', rootBox).log('sequenceDiagram'); -const member1 = window.member1 = await new Member('Member1', scene).initialize(); -const member2 = window.member2 = await new Member('Member2', scene).initialize(); + +const members = window.members = []; +const newMember = async () => { + const index = members.length; + const name = `Member${index + 1}`; + const member = await new Member(name, scene).initialize(); + members.push(member); + return member; +}; + +const member1 = await newMember(); +const member2 = await newMember(); +await newMember(); const bench = window.bench = new Bench('Bench', scene); const forum = window.forum = new Forum(bench, 'Forum', scene); const availability = window.bench = new Availability(bench, 'Availability', scene); -const business = window.bench = new Business(bench, forum, availability, 'Business', scene); +const business = window.business = new Business(bench, forum, availability, 'Business', scene); +const requestor = window.requestor = new Public('Public', scene); const updateDisplayValues = async () => { member1.setValue('rep', bench.reputations.getTokens(member1.reputationPublicKey)); @@ -25,44 +40,65 @@ const updateDisplayValues = async () => { await scene.renderSequenceDiagram(); }; -updateDisplayValues(); +const updateDisplayValuesAndDelay = async () => { + await updateDisplayValues(); + await delay(DELAY_INTERVAL); +}; -// const post1 = window.post1 = new PostContent({ message: 'hi' }); -// const post2 = window.post2 = new PostContent({ message: 'hello' }).addCitation(window.post1.id, 1.0); +const getActiveWorker = async () => { + let worker; + let request; + for (const member of members) { + request = await member.getAssignedWork(availability, business); + if (request) { + worker = member; + worker.actions.getAssignedWork.log(worker, availability); + worker.activate(); + break; + } + } + return { worker, request }; +}; + +const voteForWorkEvidence = async (worker, pool) => { + for (const member of members) { + if (member !== worker) { + await member.castVote(pool, { position: true, stake: 1, anonymous: false }); + } + } +}; + +await updateDisplayValuesAndDelay(); // Populate availability pool -availability.register(member1.reputationPublicKey, 1); -availability.register(member2.reputationPublicKey, 1); - -await delay(500); +await member1.registerAvailability(availability, 1); +await member2.registerAvailability(availability, 1); +await updateDisplayValuesAndDelay(); // Submit work request -const requestId = await business.submitRequest(100, { please: 'do some work' }); -await scene.renderSequenceDiagram(); -await delay(500); +await requestor.submitRequest(business, { fee: 100 }, { please: 'do some work' }); +await updateDisplayValuesAndDelay(); + +// Receive work request +const { worker, request } = await getActiveWorker(); // Submit work evidence -const pool = await business.submitWork(member1.reputationPublicKey, requestId, { +const pool = await worker.submitWork(business, request.id, { here: 'is some evidence of work product', }, { tokenLossRatio: 1, duration: 1000, }); - -await scene.renderSequenceDiagram(); -await delay(500); +worker.deactivate(); +await updateDisplayValuesAndDelay(); // Vote on work evidence -await member2.castVote(pool, { position: true, stake: 1 }); -await scene.renderSequenceDiagram(); -await delay(500); +await voteForWorkEvidence(worker, pool); +await updateDisplayValuesAndDelay(); -await member2.revealIdentity(pool); -await scene.renderSequenceDiagram(); -await delay(500); +// Wait for validation pool duration to elapse +await delay(1000); -await member1.revealIdentity(pool, member1.reputationPublicKey); -await scene.renderSequenceDiagram(); - -// Distribute reputation awards -// Distribute fees +// Distribute reputation awards and fees +await pool.evaluateWinningConditions(); +await updateDisplayValuesAndDelay(); diff --git a/forum-network/public/classes/availability.js b/forum-network/public/classes/availability.js index b4cced6..7614c70 100644 --- a/forum-network/public/classes/availability.js +++ b/forum-network/public/classes/availability.js @@ -1,3 +1,4 @@ +import { Action } from './action.js'; import { Actor } from './actor.js'; class Worker { @@ -21,6 +22,10 @@ export class Availability extends Actor { constructor(bench, name, scene) { super(name, scene); this.bench = bench; + + this.actions = { + assignWork: new Action('assign work', scene), + }; } register(reputationPublicKey, stake) { @@ -44,6 +49,11 @@ export class Availability extends Actor { const worker = this.availableWorkers[index]; worker.available = false; worker.assignedRequestId = requestId; - // TOOD: Notify assignee + // TODO: Notify assignee + } + + async getAssignedWork(reputationPublicKey) { + const worker = this.workers.get(reputationPublicKey); + return worker.assignedRequestId; } } diff --git a/forum-network/public/classes/bench.js b/forum-network/public/classes/bench.js index 5c849c2..1c9206e 100644 --- a/forum-network/public/classes/bench.js +++ b/forum-network/public/classes/bench.js @@ -65,6 +65,7 @@ export class Bench extends Actor { contentiousDebate, signingPublicKey, authorStake, + anonymous, }, ) { const validationPoolNumber = this.validationPools.size + 1; @@ -79,6 +80,7 @@ export class Bench extends Actor { contentiousDebate, signingPublicKey, authorStake, + anonymous, }, `pool${validationPoolNumber}`, this.scene, diff --git a/forum-network/public/classes/business.js b/forum-network/public/classes/business.js index 019009e..6b2b13a 100644 --- a/forum-network/public/classes/business.js +++ b/forum-network/public/classes/business.js @@ -1,3 +1,4 @@ +import { Action } from './action.js'; import { Actor } from './actor.js'; import { CryptoUtil } from './crypto.js'; import { PostContent } from './post.js'; @@ -21,6 +22,12 @@ export class Business extends Actor { this.bench = bench; this.forum = forum; this.availability = availability; + + this.actions = { + assignWork: new Action('assign work', scene), + addPost: new Action('add post', scene), + initiateValidationPool: new Action('initiate validation pool', scene), + }; } /** @@ -33,29 +40,41 @@ export class Business extends Actor { async submitRequest(fee, content) { const request = new Request(fee, content); this.requests.set(request.id, request); + this.actions.assignWork.log(this, this.availability); await this.availability.assignWork(request.id); return request.id; } + async getRequest(requestId) { + const request = this.requests.get(requestId); + return request; + } + async submitWork(reputationPublicKey, requestId, workEvidence, { tokenLossRatio, duration }) { - const { fee } = this.requests.get(requestId); + const request = this.requests.get(requestId); + if (!request) { + throw new Error(`Request not found! id: ${requestId}`); + } // Create a post representing this submission. const post = new PostContent({ requestId, workEvidence, }); + this.actions.addPost.log(this, this.forum); 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. + this.actions.initiateValidationPool.log(this, this.bench); const pool = await this.bench.initiateValidationPool(reputationPublicKey, { postId: post.id, - fee, + fee: request.fee, duration, tokenLossRatio, signingPublicKey: reputationPublicKey, + anonymous: false, }); // When the validation pool concludes, diff --git a/forum-network/public/classes/forum.js b/forum-network/public/classes/forum.js index adb0dc5..c7eacae 100644 --- a/forum-network/public/classes/forum.js +++ b/forum-network/public/classes/forum.js @@ -34,14 +34,6 @@ export class Forum extends Actor { for (const { postId: citedPostId, weight } of postContent.citations) { this.posts.addEdge('citation', post.id, citedPostId, { weight }); } - // this.applyReputationEffects(post); - // initiateValidationPool(authorId, {postId, fee, duration, tokenLossRatio, contentiousDebate, signingPublicKey}) { - - // const pool = await this.bench.initiateValidationPool(authorId, { - // ...poolParams, - // postId, - // }); - // return pool; } getPost(postId) { diff --git a/forum-network/public/classes/member.js b/forum-network/public/classes/member.js index 1374896..90b99e8 100644 --- a/forum-network/public/classes/member.js +++ b/forum-network/public/classes/member.js @@ -12,6 +12,9 @@ export class Member extends Actor { initiateValidationPool: new Action('initiate validation pool', scene), castVote: new Action('cast vote', 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(); } @@ -51,30 +54,49 @@ export class Member extends Actor { return pool; } - async castVote(validationPool, { position, stake, lockingTime }) { - const signingKey = await CryptoUtil.generateAsymmetricKey(); - const signingPublicKey = await CryptoUtil.exportKey(signingKey.publicKey); - this.validationPools.set(validationPool.id, { signingPublicKey }); + async castVote(validationPool, { + position, stake, lockingTime, anonymous = true, + }) { + 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 vote // TODO: sign message this.actions.castVote.log( this, validationPool, - `(${position ? 'for' : 'against'}, stake: ${stake})`, + `(${position ? 'for' : 'against'}, stake: ${stake}, anonymous: ${anonymous})`, ); - validationPool.castVote(signingPublicKey, position, stake, lockingTime); + return validationPool.castVote(signingPublicKey, { + position, stake, lockingTime, anonymous, + }); } - async revealIdentity(validationPool, signingPublicKey) { - if (!signingPublicKey) { - signingPublicKey = this.validationPools.get(validationPool.id).signingPublicKey; - } + async revealIdentity(validationPool) { + const { signingPublicKey } = this.validationPools.get(validationPool.id); // TODO: sign message this.actions.revealIdentity.log(this, validationPool); validationPool.revealIdentity(signingPublicKey, this.reputationPublicKey); } - async submitWork(business, requestId, evidence, { tokenLossRatio }) { - await business.submitWork(this.reputationPublicKey, requestId, evidence, { tokenLossRatio }); + async registerAvailability(availability, stake) { + this.actions.registerAvailability.log(this, availability, `(stake: ${stake})`); + await availability.register(this.reputationPublicKey, stake); + } + + async getAssignedWork(availability, business) { + const requestId = await availability.getAssignedWork(this.reputationPublicKey); + const request = await business.getRequest(requestId); + return request; + } + + async submitWork(business, requestId, evidence, { tokenLossRatio, duration }) { + this.actions.submitWork.log(this, business); + return business.submitWork(this.reputationPublicKey, requestId, evidence, { tokenLossRatio, duration }); } } diff --git a/forum-network/public/classes/public.js b/forum-network/public/classes/public.js index e69de29..ad25f22 100644 --- a/forum-network/public/classes/public.js +++ b/forum-network/public/classes/public.js @@ -0,0 +1,16 @@ +import { Action } from './action.js'; +import { Actor } from './actor.js'; + +export class Public extends Actor { + constructor(name, scene) { + super(name, scene); + this.actions = { + submitRequest: new Action('submit work request', scene), + }; + } + + async submitRequest(business, { fee }, content) { + this.actions.submitRequest.log(this, business, `(fee: ${fee})`); + return business.submitRequest(fee, content); + } +} diff --git a/forum-network/public/classes/validation-pool.js b/forum-network/public/classes/validation-pool.js index 3e86788..53a97bc 100644 --- a/forum-network/public/classes/validation-pool.js +++ b/forum-network/public/classes/validation-pool.js @@ -7,6 +7,7 @@ import params from './params.js'; const ValidationPoolStates = Object.freeze({ OPEN: 'OPEN', CLOSED: 'CLOSED', + RESOLVED: 'RESOLVED', }); /** @@ -24,6 +25,7 @@ export class ValidationPool extends Actor { tokenLossRatio, contentiousDebate = false, authorStake = 0, + anonymous = true, }, name, scene, @@ -71,10 +73,19 @@ export class ValidationPool extends Actor { // 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 console.log('initiateValidationPool casting vote', { signingPublicKey }); - this.castVote(signingPublicKey, true, this.tokens.for + authorStake, 0); + this.castVote(signingPublicKey, { + position: true, + stake: this.tokens.for + authorStake, + anonymous, + }); } - castVote(signingPublicKey, position, stake, lockingTime) { + async castVote(signingPublicKey, { + position, stake, lockingTime = 0, anonymous = true, + }) { + console.log('castVote', { + signingPublicKey, position, stake, anonymous, + }); const vote = new Vote(position, stake, lockingTime); if (this.state === ValidationPoolStates.CLOSED) { throw new Error(`Validation pool ${this.id} is closed`); @@ -85,6 +96,10 @@ export class ValidationPool extends Actor { ); } this.votes.set(signingPublicKey, vote); + if (!anonymous) { + console.log('castVote: revealing identity since this is not an anonymous vote'); + await this.revealIdentity(signingPublicKey, signingPublicKey); + } } listVotes(position) { @@ -95,7 +110,7 @@ export class ValidationPool extends Actor { ); } - revealIdentity(signingPublicKey, reputationPublicKey) { + async revealIdentity(signingPublicKey, reputationPublicKey) { if (!this.votes.get(signingPublicKey)) { throw new Error('Must vote before revealing identity'); } @@ -104,23 +119,6 @@ export class ValidationPool extends Actor { voter.addVoteRecord(this); this.bench.voters.set(reputationPublicKey, voter); this.voters.set(signingPublicKey, voter); - if (this.votes.size === this.voters.size) { - // All voters have revealed their reputation public keys - // Now we can evaluate winning conditions - this.state = ValidationPoolStates.CLOSED; - this.setStatus('Closed'); - const result = this.evaluateWinningConditions(); - if (result === null) { - this.setStatus('Closed - Quorum not met'); - this.scene.log(`note over ${this.name} : Quorum not met`); - } else { - this.setStatus(`Closed - ${result ? 'Won' : 'Lost'}`); - this.scene.log(`note over ${this.name} : ${result ? 'Win' : 'Lose'}`); - this.applyTokenLocking(); - this.distributeTokens(result); - } - this.deactivate(); - } } getTokenLossRatio() { @@ -163,7 +161,21 @@ export class ValidationPool extends Actor { } } - evaluateWinningConditions() { + async evaluateWinningConditions() { + if (this.state === ValidationPoolStates.RESOLVED) { + throw new Error('Validation pool has already been resolved!'); + } + const elapsed = new Date() - this.dateStart; + 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) { + 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 = (position) => Array.from(this.listVotes(position).values()) .map(getVoteValue) @@ -175,7 +187,21 @@ export class ValidationPool extends Actor { const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; - return quorumMet ? votePasses : null; + const result = quorumMet ? votePasses : null; + + if (result === null) { + this.setStatus('Resolved - Quorum not met'); + this.scene.log(`note over ${this.name} : Quorum not met`); + } else { + this.setStatus(`Resolved - ${result ? 'Won' : 'Lost'}`); + this.scene.log(`note over ${this.name} : ${result ? 'Win' : 'Lose'}`); + this.applyTokenLocking(); + this.distributeTokens(result); + } + this.deactivate(); + this.state = ValidationPoolStates.RESOLVED; + + return result; } distributeTokens(result) { diff --git a/forum-network/public/debounce-test.html b/forum-network/public/debounce-test.html index 199e36d..028b237 100644 --- a/forum-network/public/debounce-test.html +++ b/forum-network/public/debounce-test.html @@ -1,6 +1,6 @@ - Forum Graph + Forum Graph: Debounce test diff --git a/forum-network/public/forum-network-test.html b/forum-network/public/forum-network-test.html index edabe94..21b7a39 100644 --- a/forum-network/public/forum-network-test.html +++ b/forum-network/public/forum-network-test.html @@ -1,6 +1,6 @@ - Forum + Forum Network test diff --git a/forum-network/public/mermaid-test.html b/forum-network/public/mermaid-test.html index ad33055..f3b0ab4 100644 --- a/forum-network/public/mermaid-test.html +++ b/forum-network/public/mermaid-test.html @@ -1,20 +1,24 @@ - Forum Network + Mermaid test diff --git a/forum-network/public/validation-pool-test.html b/forum-network/public/validation-pool-test.html index dc02c2b..7016a03 100644 --- a/forum-network/public/validation-pool-test.html +++ b/forum-network/public/validation-pool-test.html @@ -1,6 +1,6 @@ - Forum + Validation Pool test diff --git a/forum-network/public/validation-pool-test.js b/forum-network/public/validation-pool-test.js index cfba35b..db485a5 100644 --- a/forum-network/public/validation-pool-test.js +++ b/forum-network/public/validation-pool-test.js @@ -30,8 +30,9 @@ await delay(1000); // First member can self-approve { const pool = await member1.initiateValidationPool(bench, { fee: 7, duration: 1000, tokenLossRatio: 1 }); - // await member1.castVote(pool, true, 0, 0); - await member1.revealIdentity(pool); // Vote passes + await member1.revealIdentity(pool); + await delay(1000); + await pool.evaluateWinningConditions(); // Vote passes await updateDisplayValues(); await delay(1000); } @@ -39,7 +40,9 @@ await delay(1000); // Failure example: second member can not self-approve try { const pool = await member2.initiateValidationPool(bench, { fee: 1, duration: 1000, tokenLossRatio: 1 }); - await member2.revealIdentity(pool); // Quorum not met! + await member2.revealIdentity(pool); + await delay(1000); + await pool.evaluateWinningConditions(); // Quorum not met! await updateDisplayValues(); await delay(1000); } catch (e) { @@ -56,7 +59,9 @@ try { const pool = await member2.initiateValidationPool(bench, { fee: 1, duration: 1000, tokenLossRatio: 1 }); await member1.castVote(pool, { position: true, stake: 4, lockingTime: 0 }); await member1.revealIdentity(pool); - await member2.revealIdentity(pool); // Vote passes + await member2.revealIdentity(pool); + await delay(1000); + await pool.evaluateWinningConditions(); // Vote passes await updateDisplayValues(); await delay(1000); }