Add support for posts with multiple authors

This commit is contained in:
Ladd Hoffman 2023-04-15 11:50:57 -05:00
parent 0a954a01f3
commit 48491751dc
14 changed files with 202 additions and 171 deletions

View File

@ -5,18 +5,28 @@ import params from '../../params.js';
import { ReputationHolder } from '../reputation/reputation-holder.js';
import { displayNumber, EPSILON, INCINERATOR_ADDRESS } from '../../util.js';
const CITATION = 'citation';
const BALANCE = 'balance';
const EdgeTypes = {
CITATION: 'citation',
BALANCE: 'balance',
AUTHORED_BY: 'authored by',
};
const VertexTypes = {
POST: 'post',
AUTHOR: 'author',
};
class Post extends Actor {
constructor(forum, authorPublicKey, postContent) {
const index = forum.posts.countVertices();
constructor(forum, senderId, postContent) {
const index = forum.graph.countVertices(VertexTypes.POST);
const name = `Post${index + 1}`;
super(name, forum.scene);
this.forum = forum;
this.id = postContent.id ?? name;
this.authorPublicKey = authorPublicKey;
this.senderId = senderId;
this.value = 0;
this.initialValue = 0;
this.authors = postContent.authors;
this.citations = postContent.citations;
this.title = postContent.title;
const leachingTotal = this.citations
@ -25,6 +35,8 @@ class Post extends Actor {
const donationTotal = this.citations
.filter(({ weight }) => weight > 0)
.reduce((total, { weight }) => total += weight, 0);
// TODO: Move evaluation of these parameters to Validation Pool
if (leachingTotal > params.revaluationLimit) {
throw new Error('Post leaching total exceeds revaluation limit '
+ `(${leachingTotal} > ${params.revaluationLimit})`);
@ -49,6 +61,12 @@ class Post extends Actor {
</tr></table>`
.replaceAll(/\n\s*/g, '');
}
async setValue(value) {
this.value = value;
await this.setDisplayValue('value', value);
this.forum.graph.setVertexDisplayLabel(this.id, this.getLabel());
}
}
/**
@ -61,7 +79,7 @@ export class Forum extends ReputationHolder {
super(name, scene);
this.dao = dao;
this.id = this.reputationPublicKey;
this.posts = new WDAG(scene);
this.graph = new WDAG(scene);
this.actions = {
propagate: new Action('propagate', scene),
confirm: new Action('confirm', scene),
@ -69,32 +87,26 @@ export class Forum extends ReputationHolder {
};
}
async addPost(authorId, postContent) {
console.log('addPost', { authorId, postContent });
const post = new Post(this, authorId, postContent);
this.posts.addVertex(post.id, post, post.getLabel());
async addPost(senderId, postContent) {
console.log('addPost', { senderId, postContent });
const post = new Post(this, senderId, postContent);
this.graph.addVertex(VertexTypes.POST, post.id, post, post.getLabel());
for (const { postId: citedPostId, weight } of post.citations) {
// Special case: Incinerator
if (citedPostId === INCINERATOR_ADDRESS && !this.posts.getVertex(INCINERATOR_ADDRESS)) {
this.posts.addVertex(INCINERATOR_ADDRESS, { name: 'Incinerator' }, 'Incinerator');
if (citedPostId === INCINERATOR_ADDRESS && !this.graph.getVertex(INCINERATOR_ADDRESS)) {
this.graph.addVertex(VertexTypes.POST, INCINERATOR_ADDRESS, { name: 'Incinerator' }, 'Incinerator');
}
this.posts.addEdge(CITATION, post.id, citedPostId, weight);
this.graph.addEdge(EdgeTypes.CITATION, post.id, citedPostId, weight);
}
return post;
}
getPost(postId) {
return this.posts.getVertexData(postId);
return this.graph.getVertexData(postId);
}
getPosts() {
return this.posts.getVerticesData();
}
async setPostValue(post, value) {
post.value = value;
await post.setValue('value', value);
this.posts.setVertexLabel(post.id, post.getLabel());
return this.graph.getVerticesData();
}
getTotalValue() {
@ -108,38 +120,74 @@ export class Forum extends ReputationHolder {
async onValidate({
pool, postId, tokenId,
}) {
console.log('onValidate', { pool, postId, tokenId });
const initialValue = this.dao.reputation.valueOf(tokenId);
const postVertex = this.posts.getVertex(postId);
const postVertex = this.graph.getVertex(postId);
const post = postVertex.data;
post.setStatus('Validated');
post.initialValue = initialValue;
this.posts.setVertexLabel(post.id, post.getLabel());
this.graph.setVertexDisplayLabel(post.id, post.getLabel());
// Store a reference to the reputation token associated with this post,
// so that its value can be updated by future validated posts.
post.tokenId = tokenId;
const addAuthorToGraph = (publicKey, weight, authorTokenId) => {
const authorVertex = this.graph.getVertex(publicKey)
?? this.graph.addVertex(VertexTypes.AUTHOR, publicKey, { name: publicKey, publicKey }, publicKey);
const authorEdge = this.graph.addEdge(
EdgeTypes.AUTHORED_BY,
postVertex,
authorVertex,
weight,
{ tokenId: authorTokenId },
);
console.log('addAuthorToGraph', { authorVertex, authorEdge });
};
// In the case of multiple authors, mint additional (empty) tokens.
// If no authors are specified, treat the sender as the sole author.
if (!post.authors?.length) {
addAuthorToGraph(post.senderId, 1, tokenId);
} else {
for (const { publicKey, weight } of post.authors) {
// If the sender is also listed among the authors, do not mint them an additional token.
if (publicKey === post.senderId) {
addAuthorToGraph(publicKey, weight, tokenId);
} else {
addAuthorToGraph(publicKey, weight, this.dao.reputation.mint(this.id, 0));
}
}
}
// TODO: Verify that cumulative author weight === 1
const rewardsAccumulator = new Map();
// Compute rewards
// Compute reputation rewards
await this.propagateValue(
{ to: postVertex, from: { data: pool } },
{ rewardsAccumulator, increment: initialValue },
);
// Apply computed rewards to update values of tokens
for (const [id, value] of rewardsAccumulator) {
if (value < 0) {
this.dao.reputation.transferValueFrom(id, post.tokenId, -value);
} else {
this.dao.reputation.transferValueFrom(post.tokenId, id, value);
for (const [authorTokenId, amount] of rewardsAccumulator) {
console.log('reward', { authorTokenId, amount });
// The primary author gets the validation pool minted token.
// So we don't need to transfer any reputation to the primary author.
// Their reward will be the remaining balance after all other transfers.
if (authorTokenId !== tokenId) {
if (amount < 0) {
this.dao.reputation.transferValueFrom(authorTokenId, tokenId, -amount);
} else {
this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount);
}
}
}
// Transfer ownership of the minted/staked token, from the posts to the post author
this.dao.reputation.transfer(this.id, post.authorPublicKey, post.tokenId);
// const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey);
// const value = this.dao.reputation.valueOf(post.tokenId);
// Transfer ownership of the minted tokens to the authors
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHORED_BY, true)) {
const authorVertex = authorEdge.to;
const { publicKey } = authorVertex.data;
const { tokenId: authorTokenId } = authorEdge.data;
this.dao.reputation.transfer(this.id, publicKey, authorTokenId);
}
}
/**
@ -150,7 +198,7 @@ export class Forum extends ReputationHolder {
rewardsAccumulator, increment, depth = 0, initialNegative = false,
}) {
const postVertex = edge.to;
const post = postVertex?.data;
const post = postVertex.data;
this.actions.propagate.log(edge.from.data, post, `(${increment})`);
if (!!params.referenceChainLimit && depth > params.referenceChainLimit) {
@ -175,13 +223,14 @@ export class Forum extends ReputationHolder {
const propagate = async (positive) => {
let totalOutboundAmount = 0;
const citationEdges = postVertex.getEdges(CITATION, true)
const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
for (const citationEdge of citationEdges) {
const { weight } = citationEdge;
let outboundAmount = weight * increment;
if (Math.abs(outboundAmount) > EPSILON) {
const balanceToOutbound = this.posts.getEdgeWeight(BALANCE, citationEdge.from, citationEdge.to) ?? 0;
const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
?? 0;
let refundFromOutbound = 0;
// Special case: Incineration.
@ -225,7 +274,12 @@ export class Forum extends ReputationHolder {
// Keep a record of the effect of the reputation transferred along this edge in the graph,
// so that later, negative citations can be constrained to at most undo these effects.
this.posts.setEdgeWeight(BALANCE, citationEdge.from, citationEdge.to, balanceToOutbound + outboundAmount);
this.graph.setEdgeWeight(
EdgeTypes.BALANCE,
citationEdge.from,
citationEdge.to,
balanceToOutbound + outboundAmount,
);
totalOutboundAmount += outboundAmount;
this.actions.confirm.log(
@ -263,11 +317,15 @@ export class Forum extends ReputationHolder {
refundToInbound,
});
// Award reputation to post author
rewardsAccumulator.set(post.tokenId, appliedIncrement);
// Apply reputation effects to post authors, not to the post directly
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHORED_BY, true)) {
const { weight, data: { tokenId } } = authorEdge;
const authorIncrement = weight * appliedIncrement;
rewardsAccumulator.set(tokenId, authorIncrement);
}
// Increment the value of the post
await this.setPostValue(post, newValue);
await post.setValue(newValue);
return refundToInbound;
}

View File

@ -180,7 +180,7 @@ export class ValidationPool extends ReputationHolder {
// Update computed display values
const actor = this.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey);
await actor.computeValues();
await actor.computeDisplayValues();
}
}
@ -235,9 +235,9 @@ export class ValidationPool extends ReputationHolder {
if (!actor) {
throw new Error('Actor not found!');
}
await actor.computeValues();
await actor.computeDisplayValues();
}
await this.dao.computeValues();
await this.dao.computeDisplayValues();
this.scene?.stateToTable(`validation pool ${this.name} complete`);

View File

@ -58,12 +58,12 @@ export class Actor {
this.values.set(label, this.scene?.addDisplayValue(`${this.name} ${label}`));
if (fn) {
this.valueFunctions.set(label, fn);
await this.computeValues();
await this.computeDisplayValues();
}
return this;
}
async setValue(label, value) {
async setDisplayValue(label, value) {
if (typeof value === 'function') {
return this.addComputedValue(label, value);
}
@ -76,10 +76,10 @@ export class Actor {
return this;
}
async computeValues() {
async computeDisplayValues() {
for (const [label, fn] of this.valueFunctions.entries()) {
const value = fn();
await this.setValue(label, value);
await this.setDisplayValue(label, value);
}
}

View File

@ -2,14 +2,12 @@ import { Action } from '../display/action.js';
import {
Message, PostMessage, PeerMessage, messageFromJSON,
} from './message.js';
import { ForumView } from './forum-view.js';
import { NetworkNode } from './network-node.js';
import { randomID } from '../util/util.js';
import { randomID } from '../../util.js';
export class ForumNode extends NetworkNode {
constructor(name, scene) {
super(name, scene);
this.forumView = new ForumView();
this.actions = {
...this.actions,
storePost: new Action('store post', scene),

View File

@ -1,67 +0,0 @@
import { WDAG } from '../supporting/wdag.js';
class Author {
constructor() {
this.posts = new Map();
this.reputation = 0;
}
}
class PostVertex {
constructor(id, author, stake, content, citations) {
this.id = id;
this.author = author;
this.content = content;
this.stake = stake;
this.citations = citations;
this.reputation = 0;
}
}
export class ForumView {
constructor() {
this.reputations = new Map();
this.posts = new WDAG();
this.authors = new Map();
}
getReputation(id) {
return this.reputations.get(id);
}
setReputation(id, reputation) {
this.reputations.set(id, reputation);
}
incrementReputation(publicKey, increment, _reason) {
const reputation = this.getReputation(publicKey) || 0;
return this.reputations.set(publicKey, reputation + increment);
}
getOrInitializeAuthor(authorId) {
let author = this.authors.get(authorId);
if (!author) {
author = new Author(authorId);
this.authors.set(authorId, author);
}
return author;
}
addPost(authorId, postId, postContent, stake) {
const { citations = [], content } = postContent;
const author = this.getOrInitializeAuthor(authorId);
const postVertex = new PostVertex(postId, author, stake, content, citations);
this.posts.addVertex(postId, postVertex);
for (const { postId: citedPostId, weight } of citations) {
this.posts.addEdge('citation', postId, citedPostId, weight);
}
}
getPost(postId) {
return this.posts.getVertexData(postId);
}
getPosts() {
return this.posts.getVertices();
}
}

View File

@ -38,7 +38,8 @@ export class NetworkNode extends Actor {
// Enqueue it for further processing.
async receiveMessage(messageStr) {
const messageJson = JSON.parse(messageStr);
const senderReputation = this.forumView.getReputation(messageJson.publicKey) || 0;
// const senderReputation = this.forumView.getReputation(messageJson.publicKey) || 0;
const senderReputation = 0;
this.queue.add(messageJson, senderReputation);
}

View File

@ -36,6 +36,9 @@ export class ReputationTokenContract extends ERC721 {
incrementValue(tokenId, increment, context) {
const value = this.values.get(tokenId);
if (value === undefined) {
throw new Error(`Token not found: ${tokenId}`);
}
const newValue = value + increment;
const history = this.histories.get(tokenId) || [];
@ -60,7 +63,7 @@ export class ReputationTokenContract extends ERC721 {
const sourceAvailable = this.availableValueOf(fromTokenId);
if (sourceAvailable < amount - EPSILON) {
throw new Error('Token value transfer: source has insufficient available value. '
+ `Needs ${amount}; has ${sourceAvailable}.`);
+ `Needs ${amount}; has ${sourceAvailable}.`);
}
this.incrementValue(fromTokenId, -amount);
this.incrementValue(toTokenId, amount);

View File

@ -31,6 +31,7 @@ export class ERC721 {
}
mint(to, tokenId) {
console.log('ERC721.mint', { to, tokenId });
if (this.owners.get(tokenId)) {
throw new Error('ERC721: token already minted');
}
@ -60,9 +61,10 @@ export class ERC721 {
}
transfer(from, to, tokenId) {
console.log('ERC721.transfer', { from, to, tokenId });
const owner = this.owners.get(tokenId);
if (owner !== from) {
throw new Error('ERC721: transfer from incorrect owner');
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${owner}`);
}
this.incrementBalance(from, -1);
this.incrementBalance(to, 1);

View File

@ -1,5 +1,7 @@
export class Vertex {
constructor(id, data) {
constructor(graph, type, id, data) {
this.graph = graph;
this.type = type;
this.id = id;
this.data = data;
this.edges = {
@ -8,19 +10,20 @@ export class Vertex {
};
}
getEdges(label, away) {
getEdges(type, away) {
return this.edges[away ? 'from' : 'to'].filter(
(edge) => edge.label === label,
(edge) => edge.type === type,
);
}
}
export class Edge {
constructor(label, from, to, weight) {
constructor(type, from, to, weight, data) {
this.from = from;
this.to = to;
this.label = label;
this.type = type;
this.weight = weight;
this.data = data;
}
}
@ -28,7 +31,7 @@ export class WDAG {
constructor(scene) {
this.scene = scene;
this.vertices = new Map();
this.edgeLabels = new Map();
this.edgeTypes = new Map();
this.nextVertexId = 0;
this.flowchart = scene?.flowchart;
}
@ -39,7 +42,11 @@ export class WDAG {
return this;
}
addVertex(id, data, label) {
setVertexDisplayLabel(id, label) {
this.flowchart?.log(`${id}[${label}]`);
}
addVertex(type, id, data, label) {
// Support simple case of auto-incremented numeric ids
if (typeof id === 'object') {
data = id;
@ -48,14 +55,10 @@ export class WDAG {
if (this.vertices.has(id)) {
throw new Error(`Vertex already exists with id: ${id}`);
}
const vertex = new Vertex(id, data);
const vertex = new Vertex(this, type, id, data);
this.vertices.set(id, vertex);
this.flowchart?.log(`${id}[${label ?? id}]`);
return this;
}
setVertexLabel(id, label) {
this.flowchart?.log(`${id}[${label}]`);
this.setVertexDisplayLabel(id, label ?? id);
return vertex;
}
getVertex(id) {
@ -74,25 +77,25 @@ export class WDAG {
return btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, '');
}
getEdge(label, from, to) {
getEdge(type, from, to) {
from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to);
if (!from || !to) {
return undefined;
}
const edges = this.edgeLabels.get(label);
const edges = this.edgeTypes.get(type);
const edgeKey = WDAG.getEdgeKey({ from, to });
return edges?.get(edgeKey);
}
getEdgeWeight(label, from, to) {
return this.getEdge(label, from, to)?.weight;
getEdgeWeight(type, from, to) {
return this.getEdge(type, from, to)?.weight;
}
getEdgeHtml({ from, to }) {
let html = '<table>';
for (const { label, weight } of this.getEdges(null, from, to)) {
html += `<tr><td>${label}</td><td>${weight}</td></tr>`;
for (const { type, weight } of this.getEdges(null, from, to)) {
html += `<tr><td>${type}</td><td>${weight}</td></tr>`;
}
html += '</table>';
return html;
@ -103,14 +106,14 @@ export class WDAG {
return `${edgeKey}(${this.getEdgeHtml(edge)})`;
}
setEdgeWeight(label, from, to, weight) {
setEdgeWeight(type, from, to, weight, data) {
from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to);
const edge = new Edge(label, from, to, weight);
let edges = this.edgeLabels.get(label);
const edge = new Edge(type, from, to, weight, data);
let edges = this.edgeTypes.get(type);
if (!edges) {
edges = new Map();
this.edgeLabels.set(label, edges);
this.edgeTypes.set(type, edges);
}
const edgeKey = WDAG.getEdgeKey(edge);
edges.set(edgeKey, edge);
@ -118,26 +121,26 @@ export class WDAG {
return edge;
}
addEdge(label, from, to, weight) {
addEdge(type, from, to, weight, data) {
from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to);
if (this.getEdge(label, from, to)) {
throw new Error(`Edge ${label} from ${from.id} to ${to.id} already exists`);
if (this.getEdge(type, from, to)) {
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
}
const edge = this.setEdgeWeight(label, from, to, weight);
const edge = this.setEdgeWeight(type, from, to, weight, data);
from.edges.from.push(edge);
to.edges.to.push(edge);
this.flowchart?.log(`${from.id} --- ${this.getEdgeFlowchartNode(edge)} --> ${to.id}`);
this.flowchart?.log(`class ${WDAG.getEdgeKey(edge)} edge`);
return this;
return edge;
}
getEdges(label, from, to) {
getEdges(type, from, to) {
from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to);
const edgeLabels = label ? [label] : Array.from(this.edgeLabels.keys());
return edgeLabels.flatMap((edgeLabel) => {
const edges = this.edgeLabels.get(edgeLabel);
const edgeTypes = type ? [type] : Array.from(this.edgeTypes.keys());
return edgeTypes.flatMap((edgeType) => {
const edges = this.edgeTypes.get(edgeType);
return Array.from(edges?.values() || []).filter((edge) => {
const matchFrom = from === null || from === undefined || from === edge.from;
const matchTo = to === null || to === undefined || to === edge.to;
@ -146,7 +149,10 @@ export class WDAG {
});
}
countVertices() {
return this.vertices.size;
countVertices(type) {
if (!type) {
return this.vertices.size;
}
return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length;
}
}

View File

@ -1,4 +1,22 @@
export class Citation {
class Author {
constructor(publicKey, weight) {
this.publicKey = publicKey;
this.weight = weight;
}
toJSON() {
return {
publicKey: this.publicKey,
weight: this.weight,
};
}
static fromJSON({ publicKey, weight }) {
return new Author(publicKey, weight);
}
}
class Citation {
constructor(postId, weight) {
this.postId = postId;
this.weight = weight;
@ -17,11 +35,18 @@ export class Citation {
}
export class PostContent {
constructor(content) {
constructor(content = {}) {
this.content = content;
this.authors = [];
this.citations = [];
}
addAuthor(authorPublicKey, weight) {
const author = new Author(authorPublicKey, weight);
this.authors.push(author);
return this;
}
addCitation(postId, weight) {
const citation = new Citation(postId, weight);
this.citations.push(citation);
@ -36,6 +61,7 @@ export class PostContent {
toJSON() {
return {
content: this.content,
authors: this.authors.map((author) => author.toJSON()),
citations: this.citations.map((citation) => citation.toJSON()),
...(this.id ? { id: this.id } : {}),
title: this.title,
@ -43,9 +69,10 @@ export class PostContent {
}
static fromJSON({
id, content, citations, title,
id, content, authors, citations, title,
}) {
const post = new PostContent(content);
post.authors = authors.map((author) => Author.fromJSON(author));
post.citations = citations.map((citation) => Citation.fromJSON(citation));
post.id = id;
post.title = title;

View File

@ -28,7 +28,7 @@
const actor2 = new Actor('B', scene);
const action1 = new Action('Action 1', scene);
await action1.log(actor1, actor2);
await actor1.setValue('value', 1);
await actor1.setDisplayValue('value', 1);
await scene.withFlowchart();
await scene.flowchart.log('A --> B');

View File

@ -18,7 +18,7 @@ const newExpert = async () => {
const index = experts.length;
const name = `Expert${index + 1}`;
const expert = await new Expert(dao, name, scene).initialize();
expert.setValue(
expert.setDisplayValue(
'rep',
() => dao.reputation.valueOwnedBy(expert.reputationPublicKey),
);
@ -36,7 +36,7 @@ const setup = async () => {
scene.withTable();
dao = new DAO('DGF', scene);
await dao.setValue('total rep', () => dao.reputation.getTotal());
await dao.setDisplayValue('total rep', () => dao.reputation.getTotal());
experts = [];

View File

@ -24,7 +24,10 @@ export class ForumTest {
const title = `posts[${postIndex}]`;
await this.scene.sequence.startSection();
const postContent = new PostContent({}).setTitle(title);
const postContent = new PostContent().setTitle(title);
postContent.addAuthor(author.reputationPublicKey, 1);
for (const { postId, weight } of citations) {
postContent.addCitation(postId, weight);
}
@ -82,6 +85,6 @@ export class ForumTest {
await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
// await this.dao.addComputedValue('total reputation', () => this.dao.forum.getTotalValue());
this.dao.computeValues();
this.dao.computeDisplayValues();
}
}

View File

@ -15,11 +15,11 @@ describe('Query the graph', function tests() {
before(() => {
graph = (window.graph = new WDAG()).withFlowchart();
graph.addVertex({});
graph.addVertex({});
graph.addVertex({});
graph.addVertex({});
graph.addVertex({});
graph.addVertex('v1', {});
graph.addVertex('v1', {});
graph.addVertex('v1', {});
graph.addVertex('v1', {});
graph.addVertex('v1', {});
graph.addEdge('e1', 0, 1, 1);
graph.addEdge('e1', 2, 1, 0.5);