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 { Action } from './action.js';
import { Actor } from './actor.js'; import { Actor } from './actor.js';
import { CryptoUtil } from './crypto.js';
class Worker { class Worker {
constructor(tokenId) { constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
this.reputationPublicKey = reputationPublicKey;
this.tokenId = tokenId; this.tokenId = tokenId;
this.stakeAmount = 0; this.stakeAmount = stakeAmount;
this.duration = duration;
this.available = true; this.available = true;
this.assignedRequestId = null; this.assignedRequestId = null;
} }
@ -14,8 +17,6 @@ class Worker {
* Purpose: Enable staking reputation to enter the pool of workers * Purpose: Enable staking reputation to enter the pool of workers
*/ */
export class Availability extends Actor { export class Availability extends Actor {
workers = new Map();
constructor(bench, name, scene) { constructor(bench, name, scene) {
super(name, scene); super(name, scene);
this.bench = bench; this.bench = bench;
@ -23,39 +24,48 @@ export class Availability extends Actor {
this.actions = { this.actions = {
assignWork: new Action('assign work', scene), assignWork: new Action('assign work', scene),
}; };
this.workers = new Map();
} }
register({ stakeAmount, tokenId }) { register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
// TODO: expire after duration // TODO: Should be signed by token owner
// ? Is a particular stake amount required? this.bench.reputation.lock(tokenId, stakeAmount, duration);
const worker = this.workers.get(tokenId) ?? new Worker(tokenId); const workerId = CryptoUtil.randomUUID();
if (!worker.available) { this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration));
throw new Error('Worker is already registered and busy. Can not increase stake.'); return workerId;
} }
worker.stakeAmount += stakeAmount;
// TODO: Interact with Bench contract to encumber reputation?
this.workers.set(tokenId, worker);
}
// unregister() { }
get availableWorkers() { get availableWorkers() {
return Array.from(this.workers.values()).filter(({ available }) => !!available); return Array.from(this.workers.values()).filter(({ available }) => !!available);
} }
async assignWork(requestId) { async assignWork(requestId) {
// Get random worker const totalAmountStaked = this.availableWorkers
// TODO: Probability proportional to stakes .reduce((total, { stakeAmount }) => total += stakeAmount, 0);
const index = Math.floor(Math.random() * this.availableWorkers.length); // 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]; const worker = this.availableWorkers[index];
worker.available = false; worker.available = false;
worker.assignedRequestId = requestId; worker.assignedRequestId = requestId;
// TODO: Notify assignee // TODO: Notify assignee
return worker; return worker;
} }
async getAssignedWork(reputationPublicKey) { async getAssignedWork(workerId) {
const worker = this.workers.get(reputationPublicKey); const worker = this.workers.get(workerId);
return worker.assignedRequestId; return worker.assignedRequestId;
} }
} }

View File

@ -45,13 +45,23 @@ export class Bench extends Actor {
.reduce((acc, cur) => (acc += cur), 0); .reduce((acc, cur) => (acc += cur), 0);
} }
async initiateValidationPool(poolOptions) { async initiateValidationPool(poolOptions, stakeOptions) {
const validationPoolNumber = this.validationPools.size + 1; const validationPoolNumber = this.validationPools.size + 1;
const name = `Pool${validationPoolNumber}`; const name = `Pool${validationPoolNumber}`;
const validationPool = new ValidationPool(this, this.forum, poolOptions, name, this.scene); const pool = new ValidationPool(this, this.forum, poolOptions, name, this.scene);
this.validationPools.set(validationPool.id, validationPool); this.validationPools.set(pool.id, pool);
await this.actions.createValidationPool.log(this, validationPool); await this.actions.createValidationPool.log(this, pool);
validationPool.activate(); pool.activate();
return validationPool;
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.id = CryptoUtil.randomUUID();
this.fee = fee; this.fee = fee;
this.content = content; 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 * Purpose: Enable fee-driven work requests, to be completed by workers from the availability pool
*/ */
export class Business extends Actor { export class Business extends Actor {
requests = new Map();
constructor(bench, forum, availability, name, scene) { constructor(bench, forum, availability, name, scene) {
super(name, scene); super(name, scene);
this.bench = bench; this.bench = bench;
@ -28,13 +27,16 @@ export class Business extends Actor {
submitPost: new Action('submit post', scene), submitPost: new Action('submit post', scene),
initiateValidationPool: new Action('initiate validation pool', scene), initiateValidationPool: new Action('initiate validation pool', scene),
}; };
this.requests = new Map();
} }
async submitRequest(fee, content) { async submitRequest(fee, content) {
const request = new Request(fee, content); const request = new Request(fee, content);
this.requests.set(request.id, request); this.requests.set(request.id, request);
await this.actions.assignWork.log(this, this.availability); 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; return request.id;
} }
@ -49,26 +51,33 @@ export class Business extends Actor {
throw new Error(`Request not found! id: ${requestId}`); 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. // Create a post representing this submission.
const post = new PostContent({ const post = new PostContent({
requestId, requestId,
workEvidence, 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); await this.actions.submitPost.log(this, this.forum);
const postId = await this.forum.addPost(reputationPublicKey, post); const postId = await this.forum.addPost(reputationPublicKey, post);
// Initiate a validation pool for this work evidence. // 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); await this.actions.initiateValidationPool.log(this, this.bench);
const pool = await this.bench.initiateValidationPool({ const pool = await this.bench.initiateValidationPool({
postId, postId,
fee: request.fee, fee: request.fee,
duration, duration,
tokenLossRatio, tokenLossRatio,
}, {
reputationPublicKey, reputationPublicKey,
authorStakeAmount: this.worker.stakeAmount, authorStakeAmount: request.worker.stakeAmount,
tokenId: this.worker.tokenId, tokenId: request.worker.tokenId,
}); });
// When the validation pool concludes, // When the validation pool concludes,

View File

@ -54,7 +54,7 @@ export class ERC721 /* is ERC165 */ {
ownerOf(tokenId) { ownerOf(tokenId) {
const owner = this.owners.get(tokenId); const owner = this.owners.get(tokenId);
if (!owner) { if (!owner) {
throw new Error('ERC721: invalid token ID'); throw new Error(`ERC721: invalid token ID: ${tokenId}`);
} }
return owner; return owner;
} }

View File

@ -45,7 +45,7 @@ export class Expert extends ReputationHolder {
async submitPostWithFee(bench, forum, postContent, poolOptions) { async submitPostWithFee(bench, forum, postContent, poolOptions) {
await this.actions.submitPost.log(this, forum); await this.actions.submitPost.log(this, forum);
const postId = await forum.addPost(this.reputationPublicKey, postContent); 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); this.tokens.push(pool.tokenId);
return { postId, pool }; return { postId, pool };
} }
@ -74,18 +74,22 @@ export class Expert extends ReputationHolder {
validationPool, validationPool,
`(${position ? 'for' : 'against'}, stake: ${amount})`, `(${position ? 'for' : 'against'}, stake: ${amount})`,
); );
return validationPool.stake(this, { return validationPool.stake(this.reputationPublicKey, {
position, amount, lockingTime, tokenId: this.tokens[0], position, amount, lockingTime, tokenId: this.tokens[0],
}); });
} }
async registerAvailability(availability, stakeAmount) { async registerAvailability(availability, stakeAmount, duration) {
await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount})`); await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount}, duration: ${duration})`);
await availability.register({ stakeAmount, tokenId: this.tokens[0].id }); this.workerId = await availability.register(this.reputationPublicKey, {
stakeAmount,
tokenId: this.tokens[0],
duration,
});
} }
async getAssignedWork(availability, business) { async getAssignedWork(availability, business) {
const requestId = await availability.getAssignedWork(this.reputationPublicKey); const requestId = await availability.getAssignedWork(this.workerId);
const request = await business.getRequest(requestId); const request = await business.getRequest(requestId);
return request; return request;
} }

View File

@ -37,6 +37,7 @@ export class Forum extends ReputationHolder {
this.actions = { this.actions = {
addPost: new Action('add post', scene), addPost: new Action('add post', scene),
propagateValue: new Action('propagate value', this.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({ async onValidate({
bench, pool, postId, tokenId, bench, pool, postId, tokenId,
}) { }) {
this.activate();
const initialValue = bench.reputation.valueOf(tokenId); const initialValue = bench.reputation.valueOf(tokenId);
if (this.scene.flowchart) { 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 // Transfer ownership of the minted/staked token, from the forum to the post author
bench.reputation.transferFrom(this.id, post.authorPublicKey, post.tokenId); 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) { async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) {

View File

@ -42,6 +42,9 @@ export class ReputationTokenContract extends ERC721 {
} }
transferValueFrom(fromTokenId, toTokenId, amount) { transferValueFrom(fromTokenId, toTokenId, amount) {
if (amount === undefined) {
throw new Error('Transfer value: amount is undefined!');
}
const sourceAvailable = this.availableValueOf(fromTokenId); const sourceAvailable = this.availableValueOf(fromTokenId);
const targetAvailable = this.availableValueOf(toTokenId); const targetAvailable = this.availableValueOf(toTokenId);
if (sourceAvailable < amount - EPSILON) { if (sourceAvailable < amount - EPSILON) {

View File

@ -98,6 +98,10 @@ export class Scene {
this.actors.add(actor); this.actors.add(actor);
} }
findActor(fn) {
return Array.from(this.actors.values()).find(fn);
}
addAction(name) { addAction(name) {
const action = new Action(name, this); const action = new Action(name, this);
return action; return action;

View File

@ -3,6 +3,7 @@ import { ReputationHolder } from './reputation-holder.js';
import { Stake } from './stake.js'; import { Stake } from './stake.js';
import { Voter } from './voter.js'; import { Voter } from './voter.js';
import params from '../params.js'; import params from '../params.js';
import { Action } from './action.js';
const ValidationPoolStates = Object.freeze({ const ValidationPoolStates = Object.freeze({
OPEN: 'OPEN', OPEN: 'OPEN',
@ -24,13 +25,18 @@ export class ValidationPool extends ReputationHolder {
duration, duration,
tokenLossRatio, tokenLossRatio,
contentiousDebate = false, contentiousDebate = false,
authorStakeAmount = 0,
}, },
name, name,
scene, scene,
) { ) {
super(`pool_${CryptoUtil.randomUUID()}`, name, scene); super(`pool_${CryptoUtil.randomUUID()}`, name, scene);
this.id = this.reputationPublicKey; 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 = true, we will follow the progression defined by getTokenLossRatio()
if ( if (
!contentiousDebate !contentiousDebate
@ -69,12 +75,12 @@ export class ValidationPool extends ReputationHolder {
this.tokenId = this.bench.reputation.mint(this.id, this.mintedValue); 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. // 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. // Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
this.stake(this, { this.stake(this.id, {
position: true, position: true,
amount: this.mintedValue * params.stakeForAuthor + authorStakeAmount, amount: this.mintedValue * params.stakeForAuthor,
tokenId: this.tokenId, tokenId: this.tokenId,
}); });
this.stake(this, { this.stake(this.id, {
position: false, position: false,
amount: this.mintedValue * (1 - params.stakeForAuthor), amount: this.mintedValue * (1 - params.stakeForAuthor),
tokenId: this.tokenId, 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 // 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, tokenId, position, amount, lockingTime = 0,
}) { }) {
if (this.state === ValidationPoolStates.CLOSED) { if (this.state === ValidationPoolStates.CLOSED) {
@ -147,7 +153,6 @@ export class ValidationPool extends ReputationHolder {
); );
} }
const { reputationPublicKey } = reputationHolder;
if (reputationPublicKey !== this.bench.reputation.ownerOf(tokenId)) { if (reputationPublicKey !== this.bench.reputation.ownerOf(tokenId)) {
throw new Error('Reputation may only be staked by its owner!'); 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); const reputationPublicKey = this.bench.reputation.ownerOf(tokenId);
console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`); console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
this.bench.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount); 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) { 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 // Transfer ownership of the minted token, from the pool to the forum
this.bench.reputation.transferFrom(this.id, this.forum.id, this.tokenId); 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 // Recurse through forum to determine reputation effects
await this.forum.onValidate({ await this.forum.onValidate({

View File

@ -16,6 +16,7 @@
import { delay } from '../util.js'; import { delay } from '../util.js';
import { Forum } from '../classes/forum.js'; import { Forum } from '../classes/forum.js';
import { Public } from '../classes/public.js'; import { Public } from '../classes/public.js';
import { PostContent } from '../classes/post-content.js';
const DELAY_INTERVAL = 500; const DELAY_INTERVAL = 500;
@ -24,6 +25,7 @@
const scene = (window.scene = new Scene('Availability test', rootBox)); const scene = (window.scene = new Scene('Availability test', rootBox));
scene.withSequenceDiagram(); scene.withSequenceDiagram();
scene.withFlowchart();
const experts = (window.experts = []); const experts = (window.experts = []);
const newExpert = async () => { const newExpert = async () => {
@ -36,10 +38,9 @@
const expert1 = await newExpert(); const expert1 = await newExpert();
const expert2 = await newExpert(); const expert2 = await newExpert();
await newExpert();
const forum = (window.forum = new Forum('Forum', scene)); const forum = (window.forum = new Forum('Forum', scene));
const bench = (window.bench = new Bench(forum, 'Bench', scene)); const bench = (window.bench = new Bench(forum, 'Bench', scene));
const availability = (window.bench = new Availability( const availability = (window.availability = new Availability(
bench, bench,
'Availability', 'Availability',
scene, scene,
@ -64,9 +65,9 @@
await scene.sequence.render(); await scene.sequence.render();
}; };
const updateDisplayValuesAndDelay = async () => { const updateDisplayValuesAndDelay = async (delayMs = DELAY_INTERVAL) => {
await updateDisplayValues(); await updateDisplayValues();
await delay(DELAY_INTERVAL); await delay(delayMs);
}; };
const getActiveWorker = async () => { const getActiveWorker = async () => {
@ -90,7 +91,6 @@
await expert.stake(pool, { await expert.stake(pool, {
position: true, position: true,
amount: 1, amount: 1,
anonymous: false,
}); });
} }
} }
@ -98,9 +98,42 @@
await updateDisplayValuesAndDelay(); 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 // Populate availability pool
await expert1.registerAvailability(availability, 1); await expert1.registerAvailability(availability, 1, 10000);
await expert2.registerAvailability(availability, 1); await expert2.registerAvailability(availability, 1, 10000);
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();
// Submit work request // Submit work request

View File

@ -81,9 +81,6 @@
); );
await updateDisplayValuesAndDelay(1000); await updateDisplayValuesAndDelay(1000);
// await expert2.stake(pool1, { position: true, amount 1, anonymous: false });
// await updateDisplayValuesAndDelay();
await pool1.evaluateWinningConditions(); await pool1.evaluateWinningConditions();
await updateDisplayValuesAndDelay(); await updateDisplayValuesAndDelay();
@ -104,7 +101,7 @@
); );
await updateDisplayValuesAndDelay(1000); await updateDisplayValuesAndDelay(1000);
// await expert1.stake(pool2, { position: true, amount 1, anonymous: false }); // await expert1.stake(pool2, { position: true, amount 1});
// await updateDisplayValuesAndDelay(); // await updateDisplayValuesAndDelay();
await pool2.evaluateWinningConditions(); await pool2.evaluateWinningConditions();
@ -127,7 +124,7 @@
); );
await updateDisplayValuesAndDelay(1000); await updateDisplayValuesAndDelay(1000);
// await expert1.stake(pool3, { position: true, amount 1, anonymous: false }); // await expert1.stake(pool3, { position: true, amount 1});
// await updateDisplayValuesAndDelay(); // await updateDisplayValuesAndDelay();
await pool3.evaluateWinningConditions(); await pool3.evaluateWinningConditions();