From d4bdb1c435e6c64194544da6b39fa220d84b9ef7 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Mon, 17 Apr 2023 01:41:12 +0000 Subject: [PATCH] Add support for posts with multiple authors --- forum-network/notes/chain.md | 16 ++ forum-network/notes/ecosystem.md | 27 +++ forum-network/notes/infrastructure.md | 5 + forum-network/notes/matrix.md | 25 +++ forum-network/notes/todo.md | 17 ++ forum-network/src/classes/actors/expert.js | 43 +++-- forum-network/src/classes/dao/availability.js | 2 +- forum-network/src/classes/dao/business.js | 6 +- forum-network/src/classes/dao/dao.js | 10 +- forum-network/src/classes/dao/forum.js | 164 +++++++++++++----- .../src/classes/dao/validation-pool.js | 15 +- forum-network/src/classes/display/actor.js | 19 +- forum-network/src/classes/display/box.js | 2 +- .../src/classes/display/display-value.js | 2 +- forum-network/src/classes/display/mermaid.js | 2 +- forum-network/src/classes/display/scene.js | 16 +- forum-network/src/classes/display/sequence.js | 2 +- forum-network/src/classes/display/table.js | 2 +- .../src/classes/forum-network/forum-node.js | 4 +- .../src/classes/forum-network/forum-view.js | 67 ------- .../src/classes/forum-network/message.js | 4 +- .../src/classes/forum-network/network-node.js | 3 +- forum-network/src/classes/ideas/storage.js | 2 +- .../classes/reputation/reputation-holder.js | 6 +- .../classes/reputation/reputation-token.js | 15 +- .../classes/{util => supporting}/crypto.js | 0 .../src/classes/supporting/erc721.js | 4 +- .../{util => supporting}/post-content.js | 33 +++- forum-network/src/classes/supporting/wdag.js | 135 ++++++++------ .../src/classes/util/prioritized-queue.js | 24 --- forum-network/src/index.html | 2 + forum-network/src/tests/all.test.html | 2 + forum-network/src/tests/flowchart.test.html | 4 +- forum-network/src/tests/forum10.test.html | 26 +++ forum-network/src/tests/forum11.test.html | 26 +++ forum-network/src/tests/reputation.test.html | 2 +- .../src/tests/scripts/availability.test.js | 8 +- .../src/tests/scripts/business.test.js | 2 +- .../src/tests/scripts/debounce.test.js | 2 +- .../src/tests/scripts/forum-network.test.js | 4 +- .../tests/scripts/forum/forum.test-util.js | 33 ++-- .../src/tests/scripts/forum/forum1.test.js | 17 +- .../src/tests/scripts/forum/forum10.test.js | 44 +++++ .../src/tests/scripts/forum/forum11.test.js | 56 ++++++ .../src/tests/scripts/forum/forum2.test.js | 2 +- .../src/tests/scripts/forum/forum3.test.js | 2 +- .../src/tests/scripts/forum/forum4.test.js | 2 +- .../src/tests/scripts/forum/forum5.test.js | 2 +- .../src/tests/scripts/forum/forum6.test.js | 3 +- .../src/tests/scripts/forum/forum7.test.js | 2 +- .../src/tests/scripts/forum/forum8.test.js | 3 +- .../src/tests/scripts/forum/forum9.test.js | 3 +- .../src/tests/scripts/validation-pool.test.js | 4 +- forum-network/src/tests/scripts/vm.test.js | 2 +- forum-network/src/tests/scripts/wdag.test.js | 12 +- forum-network/src/util/constants.js | 14 ++ .../src/{util.js => util/helpers.js} | 8 +- 57 files changed, 639 insertions(+), 320 deletions(-) create mode 100644 forum-network/notes/ecosystem.md create mode 100644 forum-network/notes/infrastructure.md create mode 100644 forum-network/notes/todo.md delete mode 100644 forum-network/src/classes/forum-network/forum-view.js rename forum-network/src/classes/{util => supporting}/crypto.js (100%) rename forum-network/src/classes/{util => supporting}/post-content.js (58%) delete mode 100644 forum-network/src/classes/util/prioritized-queue.js create mode 100644 forum-network/src/tests/forum10.test.html create mode 100644 forum-network/src/tests/forum11.test.html create mode 100644 forum-network/src/tests/scripts/forum/forum10.test.js create mode 100644 forum-network/src/tests/scripts/forum/forum11.test.js create mode 100644 forum-network/src/util/constants.js rename forum-network/src/{util.js => util/helpers.js} (81%) diff --git a/forum-network/notes/chain.md b/forum-network/notes/chain.md index 6d2ea62..b8bd81b 100644 --- a/forum-network/notes/chain.md +++ b/forum-network/notes/chain.md @@ -28,3 +28,19 @@ 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? + +--- + +# Protocol brainstorming + +Each node must build/maintain a view of the history and/or state of the "smart contract" operations. + +Nodes must sign messages to each other with asymmetric keys. + +This is intended to be an open network that anyone can join. + +Each node must verify the results reported by other nodes, and themselves report results to other nodes. + +In order to receive payments, the network must solve the same problems that (other) block chains have solved, i.e. must prevent double-spend; must prevent tampering with the ledger. + +Storage may be ranked into tiers, where there is core data essential to the integrity of the ledger; ancillary data that is important or desirable for review of the ledger; and supplementary data that is of variable importants for particular use cases, but does not compose the core fabric of the system. diff --git a/forum-network/notes/ecosystem.md b/forum-network/notes/ecosystem.md new file mode 100644 index 0000000..f514f5c --- /dev/null +++ b/forum-network/notes/ecosystem.md @@ -0,0 +1,27 @@ +# Ecosystem Health + +How do we incentivize and reward the producers and maintainers of infrastructure? Of essential goods and services? + +How do we reward pro-social behavior? + +How do we encourage creativity? + +Vision/Mission + +Craig: Give people tools to enable them to better express their values by collaborating + +truth +good +beauty + +thought +action +perception + +ideas +knowledge +beliefs + +utility + +evolution in the true sense -- most directions it can change will be detrimental diff --git a/forum-network/notes/infrastructure.md b/forum-network/notes/infrastructure.md new file mode 100644 index 0000000..221b493 --- /dev/null +++ b/forum-network/notes/infrastructure.md @@ -0,0 +1,5 @@ +At the base layer, we need a peer to peer protocol that allows new nodes to join the network and send and receive messages. It must protect against denial of service attacks. It must support the establishment of consensus, to varying strengths. + +We need a lightweight messaging solution to facilitate gathering information from the edges of the network, but we also need to protect against denial of service by malicious actors. + +[gossipsub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) looks like a good protocol for this. diff --git a/forum-network/notes/matrix.md b/forum-network/notes/matrix.md index f5a0f5b..31898fe 100644 --- a/forum-network/notes/matrix.md +++ b/forum-network/notes/matrix.md @@ -7,3 +7,28 @@ Matrix supports "Application Services", which are limited to funcion in a passiv Synapse, a Matrix server implementation, supports "Modules" The Matrix devs recognize the need for a robust reputation system and are in pursuit of funding and development for that purpose. + +```mermaid +graph LR + +subgraph Matrix +homeserver[Homeserver] +chainClient[Blockchain
connected
client] +publicClient[Public
Matrix-only
client] +end + +blockchain[Blockchain] +%% subgraph Blockchain +%% forum[Forum] +%% post[Post] +%% availability[Availability] +%% wsc[WSC] +%% pool[Validation
Pool] +%% end + +publicClient --> homeserver +chainClient --> homeserver +chainClient --> blockchain +homeserver --> blockchain + +``` diff --git a/forum-network/notes/todo.md b/forum-network/notes/todo.md new file mode 100644 index 0000000..222b715 --- /dev/null +++ b/forum-network/notes/todo.md @@ -0,0 +1,17 @@ +# signature verification + +# voting topologies + +# client implementations + +## example workflows + +- retroactive sign flip + +# storage scaling + +# blockchain underpinnings + +# reputation token lifetime / decay + +- active/passive diff --git a/forum-network/src/classes/actors/expert.js b/forum-network/src/classes/actors/expert.js index 69a1d7c..6928e45 100644 --- a/forum-network/src/classes/actors/expert.js +++ b/forum-network/src/classes/actors/expert.js @@ -1,11 +1,13 @@ import { Action } from '../display/action.js'; import { PostMessage } from '../forum-network/message.js'; -import { CryptoUtil } from '../util/crypto.js'; +import { CryptoUtil } from '../supporting/crypto.js'; import { ReputationHolder } from '../reputation/reputation-holder.js'; +import { EdgeTypes } from '../../util/constants.js'; +import { displayNumber } from '../../util/helpers.js'; export class Expert extends ReputationHolder { - constructor(dao, name, scene) { - super(name, scene); + constructor(dao, name, scene, options) { + super(name, scene, options); this.dao = dao; this.actions = { submitPostViaNetwork: new Action('submit post via network', scene), @@ -20,6 +22,25 @@ export class Expert extends ReputationHolder { this.tokens = []; } + getReputation() { + const authorVertex = this.dao.forum.graph.getVertex(this.reputationPublicKey); + if (!authorVertex) { + return 0; + } + const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false); + const tokenValues = authorEdges.map(({ data: { tokenId } }) => this.dao.reputation.valueOf(tokenId)); + return tokenValues.reduce((value, total) => total += value, 0); + } + + getLabel() { + return `${this.name} + + + +
reputation${displayNumber(this.getReputation())}
` + .replaceAll(/\n\s*/g, ''); + } + async initialize() { this.reputationKey = await CryptoUtil.generateAsymmetricKey(); // this.reputationPublicKey = await CryptoUtil.exportKey(this.reputationKey.publicKey); @@ -28,15 +49,6 @@ export class Expert extends ReputationHolder { return this; } - async submitPostViaNetwork(forumNode, post, stake) { - // TODO: Include fee - const postMessage = new PostMessage({ post, stake }); - await postMessage.sign(this.reputationKey); - await this.actions.submitPostViaNetwork.log(this, forumNode); - // For now, directly call forumNode.receiveMessage(); - await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON())); - } - async submitPostWithFee(postContent, poolOptions) { const post = await this.dao.forum.addPost(this.reputationPublicKey, postContent); await this.actions.submitPost.log(this, post); @@ -49,14 +61,9 @@ export class Expert extends ReputationHolder { async initiateValidationPool(poolOptions) { // For now, make direct call rather than network poolOptions.reputationPublicKey = this.reputationPublicKey; - const pool = await this.dao.initiateValidationPool(poolOptions); + const pool = await this.dao.initiateValidationPool(this, poolOptions); this.tokens.push(pool.tokenId); this.validationPools.set(pool.id, poolOptions); - await this.actions.initiateValidationPool.log( - this, - pool, - `(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`, - ); return pool; } diff --git a/forum-network/src/classes/dao/availability.js b/forum-network/src/classes/dao/availability.js index 47d936d..8cdb638 100644 --- a/forum-network/src/classes/dao/availability.js +++ b/forum-network/src/classes/dao/availability.js @@ -1,6 +1,6 @@ import { Action } from '../display/action.js'; import { Actor } from '../display/actor.js'; -import { CryptoUtil } from '../util/crypto.js'; +import { CryptoUtil } from '../supporting/crypto.js'; class Worker { constructor(reputationPublicKey, tokenId, stakeAmount, duration) { diff --git a/forum-network/src/classes/dao/business.js b/forum-network/src/classes/dao/business.js index 67a2705..da603ed 100644 --- a/forum-network/src/classes/dao/business.js +++ b/forum-network/src/classes/dao/business.js @@ -1,7 +1,7 @@ -import { randomID } from '../../util.js'; +import { randomID } from '../../util/helpers.js'; import { Action } from '../display/action.js'; import { Actor } from '../display/actor.js'; -import { PostContent } from '../util/post-content.js'; +import { PostContent } from '../supporting/post-content.js'; class Request { static nextSeq = 0; @@ -77,7 +77,7 @@ export class Business extends Actor { // Initiate a validation pool for this work evidence. await this.actions.initiateValidationPool.log(this, this.dao); - const pool = await this.dao.initiateValidationPool({ + const pool = await this.dao.initiateValidationPool(this, { postId, fee: request.fee, duration, diff --git a/forum-network/src/classes/dao/dao.js b/forum-network/src/classes/dao/dao.js index 938d019..95929b2 100644 --- a/forum-network/src/classes/dao/dao.js +++ b/forum-network/src/classes/dao/dao.js @@ -13,8 +13,8 @@ import { Actor } from '../display/actor.js'; * - Reputation: Keep track of reputation accrued to each expert */ export class DAO extends Actor { - constructor(name, scene) { - super(name, scene); + constructor(name, scene, options) { + super(name, scene, options); /* Contracts */ this.forum = new Forum(this, 'Forum', scene); @@ -38,7 +38,7 @@ export class DAO extends Actor { return Array.from(this.experts.values()).filter((voter) => { const hasVoted = !!voter.dateLastVote; const withinThreshold = !params.activeVoterThreshold - || new Date() - voter.dateLastVote >= params.activeVoterThreshold; + || new Date() - voter.dateLastVote >= params.activeVoterThreshold; return hasVoted && withinThreshold; }); } @@ -55,10 +55,10 @@ export class DAO extends Actor { .reduce((acc, cur) => (acc += cur), 0); } - async initiateValidationPool(poolOptions, stakeOptions) { + async initiateValidationPool(fromActor, poolOptions, stakeOptions) { const validationPoolNumber = this.validationPools.size + 1; const name = `Pool${validationPoolNumber}`; - const pool = new ValidationPool(this, poolOptions, name, this.scene); + const pool = new ValidationPool(this, poolOptions, name, this.scene, fromActor); this.validationPools.set(pool.id, pool); if (stakeOptions) { diff --git a/forum-network/src/classes/dao/forum.js b/forum-network/src/classes/dao/forum.js index ae65ed5..1a861a6 100644 --- a/forum-network/src/classes/dao/forum.js +++ b/forum-network/src/classes/dao/forum.js @@ -3,20 +3,22 @@ import { Action } from '../display/action.js'; import { Actor } from '../display/actor.js'; 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'; +import { displayNumber } from '../../util/helpers.js'; +import { + EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes, +} from '../../util/constants.js'; 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 +27,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 +53,12 @@ class Post extends Actor { ` .replaceAll(/\n\s*/g, ''); } + + async setValue(value) { + this.value = value; + await this.setDisplayValue('value', value); + this.forum.graph.getVertex(this.id).setDisplayLabel(this.getLabel()); + } } /** @@ -61,7 +71,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,73 +79,120 @@ 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() { return this.getPosts().reduce((total, { value }) => total += value, 0); } + // getLatestContract(type) { } + + // getContract(type) { } + 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()); + postVertex.setDisplayLabel(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) => { + // For graph display purposes, we want to use the existing Expert actors from the current scene. + const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey); + author.setDisplayValue('reputation', () => author.getReputation()); + const authorVertex = this.graph.getVertex(publicKey) + ?? this.graph.addVertex(VertexTypes.AUTHOR, publicKey, author, author.getLabel(), { + hide: author.options.hide, + }); + this.graph.addEdge( + EdgeTypes.AUTHOR, + postVertex, + authorVertex, + weight, + { tokenId: authorTokenId }, + { hide: author.options.hide }, + ); + }; + + // In the case of multiple authors, mint additional (empty) tokens. + // If no authors are specified, treat the sender as the sole author. + // TODO: Verify that cumulative author weight == 1. + 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. + const authorTokenId = (publicKey === post.senderId) ? tokenId : this.dao.reputation.mint(this.id, 0); + addAuthorToGraph(publicKey, weight, authorTokenId); + } + // If the sender is not an author, they will end up with the minted token but with zero value. + if (!post.authors.find(({ publicKey }) => publicKey === post.senderId)) { + addAuthorToGraph(post.senderId, 0, tokenId); + } + } 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 [authorEdge, amount] of rewardsAccumulator) { + const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge; + const { data: author } = authorVertex; + // 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); + } + await author.computeDisplayValues(); + authorVertex.setDisplayLabel(author.getLabel()); } } - // 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); + const senderVertex = this.graph.getVertex(post.senderId); + const { data: sender } = senderVertex; + await sender.computeDisplayValues(); + senderVertex.setDisplayLabel(sender.getLabel()); + + // Transfer ownership of the minted tokens to the authors + for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) { + const authorVertex = authorEdge.to; + const author = authorVertex.data; + const { tokenId: authorTokenId } = authorEdge.data; + this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenId); + } } /** @@ -146,7 +203,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) { @@ -171,13 +228,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. @@ -221,7 +279,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( @@ -250,6 +313,17 @@ export class Forum extends ReputationHolder { const appliedIncrement = newValue - post.value; const refundToInbound = increment - appliedIncrement; + // Apply reputation effects to post authors, not to the post directly + for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) { + const { weight, to: { data: author } } = authorEdge; + const authorIncrement = weight * appliedIncrement; + rewardsAccumulator.set(authorEdge, authorIncrement); + this.actions.propagate.log(post, author, `(${authorIncrement})`); + } + + // Increment the value of the post + await post.setValue(newValue); + console.log('propagateValue end', { depth, increment, @@ -259,12 +333,6 @@ export class Forum extends ReputationHolder { refundToInbound, }); - // Award reputation to post author - rewardsAccumulator.set(post.tokenId, appliedIncrement); - - // Increment the value of the post - await this.setPostValue(post, newValue); - return refundToInbound; } } diff --git a/forum-network/src/classes/dao/validation-pool.js b/forum-network/src/classes/dao/validation-pool.js index d288b29..7be2cff 100644 --- a/forum-network/src/classes/dao/validation-pool.js +++ b/forum-network/src/classes/dao/validation-pool.js @@ -3,7 +3,7 @@ import { Stake } from '../supporting/stake.js'; import { Voter } from '../supporting/voter.js'; import params from '../../params.js'; import { Action } from '../display/action.js'; -import { displayNumber } from '../../util.js'; +import { displayNumber } from '../../util/helpers.js'; const ValidationPoolStates = Object.freeze({ OPEN: 'OPEN', @@ -27,16 +27,21 @@ export class ValidationPool extends ReputationHolder { }, name, scene, + fromActor, ) { super(name, scene); this.id = this.reputationPublicKey; this.actions = { + initiate: new Action('initiate validation pool', scene), reward: new Action('reward', scene), transfer: new Action('transfer', scene), mint: new Action('mint', scene), }; + this.actions.initiate.log(fromActor, this, `(fee: ${fee})`); + this.activate(); + // If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio() if ( !contentiousDebate @@ -90,8 +95,6 @@ export class ValidationPool extends ReputationHolder { const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey); voter.addVoteRecord(this); this.dao.experts.set(reputationPublicKey, voter); - - this.activate(); } getTokenLossRatio() { @@ -180,7 +183,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 +238,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`); diff --git a/forum-network/src/classes/display/actor.js b/forum-network/src/classes/display/actor.js index efdd08c..0f1b011 100644 --- a/forum-network/src/classes/display/actor.js +++ b/forum-network/src/classes/display/actor.js @@ -1,7 +1,13 @@ -import { displayNumber } from '../../util.js'; +import { displayNumber } from '../../util/helpers.js'; export class Actor { - constructor(name, scene) { + /** + * @param {string} name + * @param {Scene} scene + * @param {boolean} options.announce + * @param {boolean} options.hide + */ + constructor(name, scene, options = {}) { if (!scene) throw new Error('An actor without a scene!'); this.name = name; this.scene = scene; @@ -11,6 +17,7 @@ export class Actor { this.values = new Map(); this.valueFunctions = new Map(); this.active = 0; + this.options = options; scene?.registerActor(this); } @@ -58,12 +65,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 +83,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); } } diff --git a/forum-network/src/classes/display/box.js b/forum-network/src/classes/display/box.js index 46f49f6..bee1f88 100644 --- a/forum-network/src/classes/display/box.js +++ b/forum-network/src/classes/display/box.js @@ -1,5 +1,5 @@ import { DisplayValue } from './display-value.js'; -import { randomID } from '../../util.js'; +import { randomID } from '../../util/helpers.js'; export class Box { constructor(name, parentEl, options = {}) { diff --git a/forum-network/src/classes/display/display-value.js b/forum-network/src/classes/display/display-value.js index e92de70..70a2279 100644 --- a/forum-network/src/classes/display/display-value.js +++ b/forum-network/src/classes/display/display-value.js @@ -1,4 +1,4 @@ -import { displayNumber } from '../../util.js'; +import { displayNumber } from '../../util/helpers.js'; export class DisplayValue { constructor(name, box) { diff --git a/forum-network/src/classes/display/mermaid.js b/forum-network/src/classes/display/mermaid.js index 9d42760..72f0171 100644 --- a/forum-network/src/classes/display/mermaid.js +++ b/forum-network/src/classes/display/mermaid.js @@ -1,5 +1,5 @@ import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs'; -import { debounce } from '../../util.js'; +import { debounce } from '../../util/helpers.js'; export class MermaidDiagram { constructor(box, logBox) { diff --git a/forum-network/src/classes/display/scene.js b/forum-network/src/classes/display/scene.js index f0e76b3..8f62ad1 100644 --- a/forum-network/src/classes/display/scene.js +++ b/forum-network/src/classes/display/scene.js @@ -1,5 +1,5 @@ import { Action } from './action.js'; -import { CryptoUtil } from '../util/crypto.js'; +import { CryptoUtil } from '../supporting/crypto.js'; import { MermaidDiagram } from './mermaid.js'; import { SequenceDiagram } from './sequence.js'; import { Table } from './table.js'; @@ -88,7 +88,9 @@ export class Scene { registerActor(actor) { this.actors.add(actor); - // this.sequence?.log(`participant ${actor.name}`); + if (actor.options.announce) { + this.sequence?.log(`participant ${actor.name}`); + } } findActor(fn) { @@ -114,10 +116,12 @@ export class Scene { row.set('elapsedMs', new Date() - this.dateStart); row.set('label', label); for (const actor of this.actors) { - for (const [aKey, { name, value }] of actor.getValuesMap()) { - const key = `${actor.name}:${aKey}`; - columns.push({ key, title: name }); - row.set(key, value); + if (!actor.options.hide) { + for (const [aKey, { name, value }] of actor.getValuesMap()) { + const key = `${actor.name}:${aKey}`; + columns.push({ key, title: name }); + row.set(key, value); + } } } columns.push({ key: 'label', title: '' }); diff --git a/forum-network/src/classes/display/sequence.js b/forum-network/src/classes/display/sequence.js index 39fbe2f..2a3b530 100644 --- a/forum-network/src/classes/display/sequence.js +++ b/forum-network/src/classes/display/sequence.js @@ -1,4 +1,4 @@ -import { hexToRGB } from '../../util.js'; +import { hexToRGB } from '../../util/helpers.js'; import { MermaidDiagram } from './mermaid.js'; export class SequenceDiagram extends MermaidDiagram { diff --git a/forum-network/src/classes/display/table.js b/forum-network/src/classes/display/table.js index ee579d4..670acde 100644 --- a/forum-network/src/classes/display/table.js +++ b/forum-network/src/classes/display/table.js @@ -1,4 +1,4 @@ -import { displayNumber } from '../../util.js'; +import { displayNumber } from '../../util/helpers.js'; export class Table { constructor(box) { diff --git a/forum-network/src/classes/forum-network/forum-node.js b/forum-network/src/classes/forum-network/forum-node.js index 49ae7c5..ff76ed7 100644 --- a/forum-network/src/classes/forum-network/forum-node.js +++ b/forum-network/src/classes/forum-network/forum-node.js @@ -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/helpers.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), diff --git a/forum-network/src/classes/forum-network/forum-view.js b/forum-network/src/classes/forum-network/forum-view.js deleted file mode 100644 index a7e19a0..0000000 --- a/forum-network/src/classes/forum-network/forum-view.js +++ /dev/null @@ -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(); - } -} diff --git a/forum-network/src/classes/forum-network/message.js b/forum-network/src/classes/forum-network/message.js index 2e0ab81..c0cb1be 100644 --- a/forum-network/src/classes/forum-network/message.js +++ b/forum-network/src/classes/forum-network/message.js @@ -1,5 +1,5 @@ -import { CryptoUtil } from '../util/crypto.js'; -import { PostContent } from '../util/post-content.js'; +import { CryptoUtil } from '../supporting/crypto.js'; +import { PostContent } from '../supporting/post-content.js'; export class Message { constructor(content) { diff --git a/forum-network/src/classes/forum-network/network-node.js b/forum-network/src/classes/forum-network/network-node.js index 72072b9..345e3ad 100644 --- a/forum-network/src/classes/forum-network/network-node.js +++ b/forum-network/src/classes/forum-network/network-node.js @@ -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); } diff --git a/forum-network/src/classes/ideas/storage.js b/forum-network/src/classes/ideas/storage.js index e317264..a2d6b1a 100644 --- a/forum-network/src/classes/ideas/storage.js +++ b/forum-network/src/classes/ideas/storage.js @@ -1,4 +1,4 @@ -import { randomID } from '../util/util.js'; +import { randomID } from '../util/util/helpers.js'; class Pledge { constructor({ stake, duration }) { diff --git a/forum-network/src/classes/reputation/reputation-holder.js b/forum-network/src/classes/reputation/reputation-holder.js index b2aaf0e..5d89e61 100644 --- a/forum-network/src/classes/reputation/reputation-holder.js +++ b/forum-network/src/classes/reputation/reputation-holder.js @@ -1,9 +1,9 @@ -import { randomID } from '../../util.js'; +import { randomID } from '../../util/helpers.js'; import { Actor } from '../display/actor.js'; export class ReputationHolder extends Actor { - constructor(name, scene) { - super(name, scene); + constructor(name, scene, options) { + super(name, scene, options); this.reputationPublicKey = `${name}_${randomID()}`; } } diff --git a/forum-network/src/classes/reputation/reputation-token.js b/forum-network/src/classes/reputation/reputation-token.js index 6a479e1..73ea289 100644 --- a/forum-network/src/classes/reputation/reputation-token.js +++ b/forum-network/src/classes/reputation/reputation-token.js @@ -1,6 +1,6 @@ import { ERC721 } from '../supporting/erc721.js'; - -import { EPSILON, randomID } from '../../util.js'; +import { randomID } from '../../util/helpers.js'; +import { EPSILON } from '../../util/constants.js'; class Lock { constructor(tokenId, amount, duration) { @@ -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); @@ -76,7 +79,11 @@ export class ReputationTokenContract extends ERC721 { } valueOf(tokenId) { - return this.values.get(tokenId); + const value = this.values.get(tokenId); + if (value === undefined) { + throw new Error(`Token not found: ${tokenId}`); + } + return value; } availableValueOf(tokenId) { diff --git a/forum-network/src/classes/util/crypto.js b/forum-network/src/classes/supporting/crypto.js similarity index 100% rename from forum-network/src/classes/util/crypto.js rename to forum-network/src/classes/supporting/crypto.js diff --git a/forum-network/src/classes/supporting/erc721.js b/forum-network/src/classes/supporting/erc721.js index 5b2b215..9a68166 100644 --- a/forum-network/src/classes/supporting/erc721.js +++ b/forum-network/src/classes/supporting/erc721.js @@ -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); diff --git a/forum-network/src/classes/util/post-content.js b/forum-network/src/classes/supporting/post-content.js similarity index 58% rename from forum-network/src/classes/util/post-content.js rename to forum-network/src/classes/supporting/post-content.js index 8512f3f..5308d36 100644 --- a/forum-network/src/classes/util/post-content.js +++ b/forum-network/src/classes/supporting/post-content.js @@ -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; diff --git a/forum-network/src/classes/supporting/wdag.js b/forum-network/src/classes/supporting/wdag.js index bec7a34..c28d314 100644 --- a/forum-network/src/classes/supporting/wdag.js +++ b/forum-network/src/classes/supporting/wdag.js @@ -1,26 +1,69 @@ +const getEdgeKey = ({ from, to }) => btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, ''); + export class Vertex { - constructor(id, data) { + constructor(graph, type, id, data, options = {}) { + this.graph = graph; + this.type = type; this.id = id; this.data = data; + this.options = options; this.edges = { from: [], to: [], }; } - getEdges(label, away) { + getEdges(type, away) { return this.edges[away ? 'from' : 'to'].filter( - (edge) => edge.label === label, + (edge) => edge.type === type, ); } + + setDisplayLabel(label) { + if (this.options.hide) { + return; + } + this.graph.flowchart?.log(`${this.id}[${label}]`); + } } export class Edge { - constructor(label, from, to, weight) { + constructor(graph, type, from, to, weight, data, options = {}) { + this.graph = graph; this.from = from; this.to = to; - this.label = label; + this.type = type; this.weight = weight; + this.data = data; + this.options = options; + } + + getHtml() { + let html = ''; + for (const { type, weight } of this.graph.getEdges(null, this.from, this.to)) { + html += ``; + } + html += '
${type}${weight}
'; + return html; + } + + getFlowchartNode() { + return `${getEdgeKey(this)}(${this.getHtml()})`; + } + + displayEdgeNode() { + if (this.options.hide) { + return; + } + this.graph.flowchart?.log(this.getFlowchartNode()); + } + + displayEdge() { + if (this.options.hide) { + return; + } + this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`); + this.graph.flowchart?.log(`class ${getEdgeKey(this)} edge`); } } @@ -28,7 +71,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 +82,7 @@ export class WDAG { return this; } - addVertex(id, data, label) { + addVertex(type, id, data, label, options) { // Support simple case of auto-incremented numeric ids if (typeof id === 'object') { data = id; @@ -48,14 +91,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, options); this.vertices.set(id, vertex); - this.flowchart?.log(`${id}[${label ?? id}]`); - return this; - } - - setVertexLabel(id, label) { - this.flowchart?.log(`${id}[${label}]`); + vertex.setDisplayLabel(label ?? id); + return vertex; } getVertex(id) { @@ -70,74 +109,55 @@ export class WDAG { return Array.from(this.vertices.values()).map(({ data }) => data); } - static getEdgeKey({ from, to }) { - 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 edgeKey = WDAG.getEdgeKey({ from, to }); + const edges = this.edgeTypes.get(type); + const edgeKey = 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 = ''; - for (const { label, weight } of this.getEdges(null, from, to)) { - html += ``; - } - html += '
${label}${weight}
'; - return html; - } - - getEdgeFlowchartNode(edge) { - const edgeKey = WDAG.getEdgeKey(edge); - return `${edgeKey}(${this.getEdgeHtml(edge)})`; - } - - setEdgeWeight(label, from, to, weight) { + setEdgeWeight(type, from, to, weight, data, options) { 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(this, type, from, to, weight, data, options); + 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); + const edgeKey = getEdgeKey(edge); edges.set(edgeKey, edge); - this.flowchart?.log(this.getEdgeFlowchartNode(edge)); + edge.displayEdgeNode(); return edge; } - addEdge(label, from, to, weight) { + addEdge(type, from, to, weight, data, options) { 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, options); 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; + edge.displayEdge(); + 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 +166,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; } } diff --git a/forum-network/src/classes/util/prioritized-queue.js b/forum-network/src/classes/util/prioritized-queue.js deleted file mode 100644 index 79b5851..0000000 --- a/forum-network/src/classes/util/prioritized-queue.js +++ /dev/null @@ -1,24 +0,0 @@ -export class PrioritizedQueue { - constructor() { - this.buffer = []; - } - - // Add an item to the buffer, ahead of the next lowest priority item - add(message, priority) { - const idx = this.buffer.findIndex((item) => item.priority < priority); - if (idx < 0) { - this.buffer.push({ message, priority }); - } else { - this.buffer.splice(idx, 0, { message, priority }); - } - } - - // Return the highest priority item in the buffer - pop() { - if (!this.buffer.length) { - return null; - } - const item = this.buffer.shift(); - return item.message; - } -} diff --git a/forum-network/src/index.html b/forum-network/src/index.html index 03916ad..c0d4a65 100644 --- a/forum-network/src/index.html +++ b/forum-network/src/index.html @@ -24,6 +24,8 @@
  • Negatively cite a zero-valued post
  • Incinerate reputation
  • Use incineration to achieve more balanced reweighting
  • +
  • Post with multiple authors
  • +
  • Multiple posts with overlapping authors