Update business and availability contracts to use rep tokens

This commit is contained in:
Ladd Hoffman 2023-01-26 10:44:57 -06:00
parent 8982ac610f
commit 9deaf4db07
12 changed files with 174 additions and 59 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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