Update business and availability contracts to use rep tokens
This commit is contained in:
parent
8982ac610f
commit
9deaf4db07
|
@ -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?
|
|
@ -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.');
|
||||
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;
|
||||
}
|
||||
worker.stakeAmount += stakeAmount;
|
||||
// TODO: Interact with Bench contract to encumber reputation?
|
||||
this.workers.set(tokenId, worker);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue