Improve Availability test

This commit is contained in:
Ladd Hoffman 2023-01-02 13:14:32 -06:00
parent 6ad293b5b8
commit fc3138adab
15 changed files with 239 additions and 87 deletions

View File

@ -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 todays 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

View File

@ -1,6 +1,6 @@
<!DOCTYPE html>
<head>
<title>Forum</title>
<title>Availability test</title>
<script type="module" src="./availability-test.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>

View File

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

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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) {

View File

@ -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 }) {
async castVote(validationPool, {
position, stake, lockingTime, anonymous = true,
}) {
let signingPublicKey;
if (anonymous) {
const signingKey = await CryptoUtil.generateAsymmetricKey();
const signingPublicKey = await CryptoUtil.exportKey(signingKey.publicKey);
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 });
}
}

View File

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

View File

@ -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) {

View File

@ -1,6 +1,6 @@
<!DOCTYPE html>
<head>
<title>Forum Graph</title>
<title>Forum Graph: Debounce test</title>
<script type="module" src="./debounce-test.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>

View File

@ -1,6 +1,6 @@
<!DOCTYPE html>
<head>
<title>Forum</title>
<title>Forum Network test</title>
<script type="module" src="./forum-network-test.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>

View File

@ -1,20 +1,24 @@
<!DOCTYPE html>
<head>
<title>Forum Network</title>
<title>Mermaid test</title>
<link type="text/css" rel="stylesheet" href="./index.css" />
<script type="module" defer>
// import mermaid from './mermaid.mjs';
import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.mjs';
import mermaid from "https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.mjs";
mermaid.mermaidAPI.initialize({ startOnLoad: false });
// Example of using the API var
const element = document.querySelector('#graphDiv');
const element = document.querySelector("#graphDiv");
const insertSvg = function (svgCode, bindFunctions) {
element.innerHTML = svgCode;
};
const graphDefinition = 'graph TB\na-->b';
const graph = await mermaid.mermaidAPI.render('graphDiv', graphDefinition, insertSvg);
const div = document.createElement('div');
const graphDefinition = "graph TB\na-->b";
const graph = await mermaid.mermaidAPI.render(
"graphDiv",
graphDefinition,
insertSvg
);
const div = document.createElement("div");
div.innerHTML = graph;
document.body.append(div);
</script>

View File

@ -1,6 +1,6 @@
<!DOCTYPE html>
<head>
<title>Forum</title>
<title>Validation Pool test</title>
<script type="module" src="./validation-pool-test.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>

View File

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