From 675fd17734f3393141888ea8d0fc598d014b7a3c Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Sun, 8 Jan 2023 12:27:53 -0600 Subject: [PATCH] Implement leaching value --- forum-network/notes/notes.md | 2 + forum-network/src/classes/bench.js | 2 +- forum-network/src/classes/erc721.js | 89 ++++++++++++++++++++ forum-network/src/classes/forum.js | 59 +++++++++---- forum-network/src/classes/graph.js | 5 +- forum-network/src/classes/scene.js | 23 ++++- forum-network/src/classes/validation-pool.js | 21 +++-- forum-network/src/{classes => }/params.js | 1 + forum-network/src/tests/forum.html | 15 ++++ forum-network/src/util.js | 10 +++ 10 files changed, 192 insertions(+), 35 deletions(-) create mode 100644 forum-network/src/classes/erc721.js rename forum-network/src/{classes => }/params.js (95%) diff --git a/forum-network/notes/notes.md b/forum-network/notes/notes.md index 147c7dd..b072c8e 100644 --- a/forum-network/notes/notes.md +++ b/forum-network/notes/notes.md @@ -83,3 +83,5 @@ Tokens staked for and against a post. Token loss ratio --- + +parameter q_4 -- what is c_n? diff --git a/forum-network/src/classes/bench.js b/forum-network/src/classes/bench.js index 679fc10..3eea51b 100644 --- a/forum-network/src/classes/bench.js +++ b/forum-network/src/classes/bench.js @@ -1,7 +1,7 @@ import { Actor } from './actor.js'; import { Reputations } from './reputation.js'; import { ValidationPool } from './validation-pool.js'; -import params from './params.js'; +import params from '../params.js'; import { Action } from './action.js'; /** diff --git a/forum-network/src/classes/erc721.js b/forum-network/src/classes/erc721.js new file mode 100644 index 0000000..239ea28 --- /dev/null +++ b/forum-network/src/classes/erc721.js @@ -0,0 +1,89 @@ +/** + * ERC-721 Non-Fungible Token Standard + * See https://eips.ethereum.org/EIPS/eip-721 + * and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol + * + * This implementation is currently incomplete. It lacks the following: + * - Token approvals + * - Operator approvals + * - Emitting events + */ + +export class ERC721 /* is ERC165 */ { + constructor(name, symbol) { + this.name = name; + this.symbol = symbol; + this.balances = new Map(); // owner address --> token count + this.owners = new Map(); // token id --> owner address + // this.tokenApprovals = new Map(); // token id --> approved addresses + // this.operatorApprovals = new Map(); // owner --> operator approvals + + this.events = { + // Transfer: (_from, _to, _tokenId) => {}, + // Approval: (_owner, _approved, _tokenId) => {}, + // ApprovalForAll: (_owner, _operator, _approved) => {}, + }; + } + + incrementBalance(owner, increment) { + const balance = this.balances.get(owner) ?? 0; + this.balances.set(owner, balance + increment); + } + + mint(to, tokenId) { + if (this.owners.get(tokenId)) { + throw new Error('ERC721: token already minted'); + } + this.incrementBalance(to, 1); + this.owners.set(tokenId, to); + } + + burn(tokenId) { + const owner = this.owners.get(tokenId); + this.incrementBalance(owner, -1); + this.owners.delete(tokenId); + } + + balanceOf(owner) { + if (!owner) { + throw new Error('ERC721: address zero is not a valid owner'); + } + return this.balances.get(owner) ?? 0; + } + + ownerOf(tokenId) { + const owner = this.owners.get(tokenId); + if (!owner) { + throw new Error('ERC721: invalid token ID'); + } + } + + transferFrom(from, to, tokenId) { + const owner = this.owners.get(tokenId); + if (owner !== from) { + throw new Error('ERC721: transfer from incorrect owner'); + } + this.incrementBalance(from, -1); + this.incrementBalance(to, 1); + } + + /// @notice Enable or disable approval for a third party ("operator") to manage + /// all of `msg.sender`'s assets + /// @dev Emits the ApprovalForAll event. The contract MUST allow + /// multiple operators per owner. + /// @param _operator Address to add to the set of authorized operators + /// @param _approved True if the operator is approved, false to revoke approval + // setApprovalForAll(_operator, _approved) {} + + /// @notice Get the approved address for a single NFT + /// @dev Throws if `_tokenId` is not a valid NFT. + /// @param _tokenId The NFT to find the approved address for + /// @return The approved address for this NFT, or the zero address if there is none + // getApproved(_tokenId) {} + + /// @notice Query if an address is an authorized operator for another address + /// @param _owner The address that owns the NFTs + /// @param _operator The address that acts on behalf of the owner + /// @return True if `_operator` is an approved operator for `_owner`, false otherwise + // isApprovedForAll(_owner, _operator) {} +} diff --git a/forum-network/src/classes/forum.js b/forum-network/src/classes/forum.js index 36a2b31..1ae6eab 100644 --- a/forum-network/src/classes/forum.js +++ b/forum-network/src/classes/forum.js @@ -2,7 +2,7 @@ import { Actor } from './actor.js'; import { Graph } from './graph.js'; import { Action } from './action.js'; import { CryptoUtil } from './crypto.js'; -import params from './params.js'; +import params from '../params.js'; class Post extends Actor { constructor(forum, authorPublicKey, postContent) { @@ -23,15 +23,6 @@ class Post extends Actor { throw new Error('Each citation weight must be in the range [-1, 1]'); } } - - async setPostValue(value) { - await this.setValue('value', value); - this.value = value; - } - - getPostValue() { - return this.value; - } } /** @@ -43,6 +34,7 @@ export class Forum extends Actor { this.posts = new Graph(scene); this.actions = { addPost: new Action('add post', scene), + propagateValue: new Action('propagate value', this.scene), }; } @@ -50,8 +42,14 @@ export class Forum extends Actor { const post = new Post(this, authorId, postContent); await this.actions.addPost.log(this, post); this.posts.addVertex(post.id, post, post.title); + if (this.scene.flowchart) { + this.scene.flowchart.log(`${post.id} -- value --> ${post.id}_value[0]`); + } for (const { postId: citedPostId, weight } of post.citations) { this.posts.addEdge('citation', post.id, citedPostId, { weight }); + if (this.scene.flowchart) { + this.scene.flowchart.log(`${post.id} -- ${weight} --> ${citedPostId}`); + } } return post.id; } @@ -61,14 +59,39 @@ export class Forum extends Actor { } getPosts() { - return this.posts.getVertices(); + return this.posts.getVerticesData(); } - async propagateValue(postId, increment, depth = 0) { - if (depth > params.maxPropagationDepth) { + async setPostValue(postId, value) { + const post = this.getPost(postId); + post.value = value; + await post.setValue('value', value); + if (this.scene.flowchart) { + this.scene.flowchart.log(`${post.id}_value[${value}]`); + } + } + + getPostValue(postId) { + const post = this.getPost(postId); + return post.value; + } + + getTotalValue() { + return this.getPosts().reduce((total, { value }) => total += value, 0); + } + + async propagateValue(fromActor, postId, increment, depth = 0) { + if (depth === 0 && this.scene.flowchart) { + const randomId = CryptoUtil.randomUUID().replaceAll('-', '').slice(0, 8); + this.scene.flowchart.log(`${postId}_initial_value_${randomId}[${increment}] -- initial value --> ${postId}`); + } + if (params.maxPropagationDepth >= 0 && depth > params.maxPropagationDepth) { return []; } const post = this.getPost(postId); + + this.actions.propagateValue.log(fromActor, post, `(increment: ${increment})`); + const rewards = new Map(); const addReward = (id, value) => rewards.set(id, (rewards.get(id) ?? 0) + value); const addRewards = (r) => { @@ -77,9 +100,11 @@ export class Forum extends Actor { } }; - // Increment the value of the given post - const postValue = post.getPostValue(); - await post.setPostValue(postValue + increment); + // Increment the value of the post + // Apply leaching value + const currentValue = this.getPostValue(postId); + const newValue = currentValue + increment * (1 - params.leachingValue * post.totalCitationWeight); + await this.setPostValue(postId, newValue); // Award reputation to post author console.log('reward for post author', post.authorPublicKey, increment); @@ -87,7 +112,7 @@ export class Forum extends Actor { // Recursively distribute reputation to citations, according to weights for (const { postId: citedPostId, weight } of post.citations) { - addRewards(await this.propagateValue(citedPostId, weight * increment, depth + 1)); + addRewards(await this.propagateValue(post, citedPostId, weight * increment, depth + 1)); } return rewards; diff --git a/forum-network/src/classes/graph.js b/forum-network/src/classes/graph.js index 84bbd74..6136d4e 100644 --- a/forum-network/src/classes/graph.js +++ b/forum-network/src/classes/graph.js @@ -58,7 +58,7 @@ export class Graph { return this.getVertex(id)?.data; } - getVertices() { + getVerticesData() { return Array.from(this.vertices.values()).map(({ data }) => data); } @@ -84,9 +84,6 @@ export class Graph { this.setEdge(label, from, to, edge); this.getVertex(from).edges.from.push(edge); this.getVertex(to).edges.to.push(edge); - if (this.scene.flowchart) { - this.scene.flowchart.log(`${from} --> ${to}`); - } return this; } diff --git a/forum-network/src/classes/scene.js b/forum-network/src/classes/scene.js index 783a76c..f98afb4 100644 --- a/forum-network/src/classes/scene.js +++ b/forum-network/src/classes/scene.js @@ -1,7 +1,7 @@ import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs'; import { Actor } from './actor.js'; import { Action } from './action.js'; -import { debounce } from '../util.js'; +import { debounce, hexToRGB } from '../util.js'; class MermaidDiagram { constructor(box, logBox) { @@ -11,6 +11,7 @@ class MermaidDiagram { this.renderBox = this.box.addBox('Render'); this.box.addBox('Spacer').setInnerHTML(' '); this.logBox = logBox; + this.inSection = 0; } async log(msg, render = true) { @@ -23,7 +24,10 @@ class MermaidDiagram { async render() { const render = async () => { - const innerText = this.logBox.getInnerText(); + let innerText = this.logBox.getInnerText(); + for (let i = 0; i < this.inSection; i++) { + innerText += '\nend'; + } const graph = await mermaid.mermaidAPI.render( this.element.getId(), innerText, @@ -104,11 +108,22 @@ export class Scene { return dv; } - deactivateAll() { + async deactivateAll() { for (const actor of this.actors.values()) { while (actor.active) { - actor.deactivate(); + await actor.deactivate(); } } } + + async startSection(color = '#08252c') { + const { r, g, b } = hexToRGB(color); + this.sequence.inSection++; + this.sequence.log(`rect rgb(${r}, ${g}, ${b})`, false); + } + + async endSection() { + this.sequence.inSection--; + this.sequence.log('end'); + } } diff --git a/forum-network/src/classes/validation-pool.js b/forum-network/src/classes/validation-pool.js index 84fc053..8a79c6b 100644 --- a/forum-network/src/classes/validation-pool.js +++ b/forum-network/src/classes/validation-pool.js @@ -2,7 +2,7 @@ import { CryptoUtil } from './crypto.js'; import { Vote } from './vote.js'; import { Voter } from './voter.js'; import { Actor } from './actor.js'; -import params from './params.js'; +import params from '../params.js'; const ValidationPoolStates = Object.freeze({ OPEN: 'OPEN', @@ -195,7 +195,8 @@ export class ValidationPool extends Actor { this.setStatus(`Resolved - ${result ? 'Won' : 'Lost'}`); this.scene.sequence.log(`note over ${this.name} : ${result ? 'Win' : 'Lose'}`); this.applyTokenLocking(); - await this.distributeTokens(result); + await this.distributeReputation(result); + // TODO: distribute fees } else { this.setStatus('Resolved - Quorum not met'); this.scene.sequence.log(`note over ${this.name} : Quorum not met`); @@ -206,7 +207,7 @@ export class ValidationPool extends Actor { return result; } - async distributeTokens({ votePasses }) { + async distributeReputation({ votePasses }) { const rewards = new Map(); const addReward = (id, value) => rewards.set(id, (rewards.get(id) ?? 0) + value); @@ -223,20 +224,20 @@ export class ValidationPool extends Actor { const { reputationPublicKey } = this.voters.get(signingPublicKey); const reward = (tokensForWinners * stake) / getTotalStaked(votePasses); addReward(reputationPublicKey, reward); - console.log('reward for winning voter', reputationPublicKey, reward); + console.log(`reward for winning voter ${reputationPublicKey}:`, reward); } const awardsFromVoting = Array.from(rewards.values()).reduce((total, value) => total += value, 0); - console.log('awardsFromVoting', awardsFromVoting); + console.log('total awards from voting:', awardsFromVoting); if (votePasses && !!this.forum) { // Recurse through forum to determine reputation effects - const forumReputationEffects = await this.forum.propagateValue(this.postId, this.tokensMinted); + const forumReputationEffects = await this.forum.propagateValue(this, this.postId, this.tokensMinted); for (const [id, value] of forumReputationEffects) { addReward(id, value); } const awardsFromForum = Array.from(forumReputationEffects.values()).reduce((total, value) => total += value, 0); - console.log('awardsFromForum', awardsFromForum); + console.log('total awards from forum:', awardsFromForum); } // Allow for possible attenuation of total value of post, e.g. based on degree of contention @@ -245,9 +246,11 @@ export class ValidationPool extends Actor { // Scale all rewards so that the total is correct // TODO: Add more precise assertions; otherwise this operation could mask errors. const currentTotal = Array.from(rewards.values()).reduce((total, value) => total += value, 0); - console.log('currentTotal', currentTotal); + console.log('total awards before normalization:', currentTotal); for (const [id, value] of rewards) { - rewards.set(id, (value * initialPostValue) / currentTotal); + const normalizedValue = (value * initialPostValue) / currentTotal; + console.log(`normalized reward for ${id}: ${value} -> ${normalizedValue}`); + rewards.set(id, normalizedValue); } // Apply computed rewards diff --git a/forum-network/src/classes/params.js b/forum-network/src/params.js similarity index 95% rename from forum-network/src/classes/params.js rename to forum-network/src/params.js index d8e49bc..4cbb267 100644 --- a/forum-network/src/classes/params.js +++ b/forum-network/src/params.js @@ -22,6 +22,7 @@ const params = { initialPostValue: () => 1, // q1 revaluationLimit: 1, // q2 maxPropagationDepth: 3, // q3 + leachingValue: 1, // q4 }; export default params; diff --git a/forum-network/src/tests/forum.html b/forum-network/src/tests/forum.html index c17bd37..3d1c7ac 100644 --- a/forum-network/src/tests/forum.html +++ b/forum-network/src/tests/forum.html @@ -17,6 +17,7 @@ import { Forum } from "/classes/forum.js"; import { Public } from "/classes/public.js"; import { PostContent } from "/classes/post-content.js"; + import params from "/params.js"; const DEFAULT_DELAY_INTERVAL = 500; @@ -27,6 +28,9 @@ scene.withSequenceDiagram(); scene.withFlowchart(); + scene.addDisplayValue("leachingValue").set(params.leachingValue); + scene.addDisplayValue(" "); + const experts = (window.experts = []); const newExpert = async () => { const index = experts.length; @@ -50,6 +54,7 @@ ); } await bench.setValue("total rep", bench.getTotalReputation()); + await forum.setValue("total value", forum.getTotalValue()); }; const updateDisplayValuesAndDelay = async (delayMs) => { @@ -59,6 +64,8 @@ await updateDisplayValuesAndDelay(); + await scene.startSection(); + const { postId: postId1, pool: pool1 } = await expert1.submitPostWithFee( bench, forum, @@ -77,6 +84,9 @@ await pool1.evaluateWinningConditions(); await updateDisplayValuesAndDelay(); + await scene.endSection(); + await scene.startSection(); + const { postId: postId2, pool: pool2 } = await expert2.submitPostWithFee( bench, forum, @@ -97,6 +107,9 @@ await pool2.evaluateWinningConditions(); await updateDisplayValuesAndDelay(); + await scene.endSection(); + await scene.startSection(); + const { pool: pool3 } = await expert3.submitPostWithFee( bench, forum, @@ -116,4 +129,6 @@ await pool3.evaluateWinningConditions(); await updateDisplayValuesAndDelay(); + + await scene.endSection(); diff --git a/forum-network/src/util.js b/forum-network/src/util.js index 3422ba8..68db951 100644 --- a/forum-network/src/util.js +++ b/forum-network/src/util.js @@ -18,3 +18,13 @@ export const delay = async (delayMs) => { setTimeout(resolve, delayMs); }); }; + +export const hexToRGB = (input) => { + if (input.startsWith('#')) { + input = input.slice(1); + } + const r = parseInt(`${input[0]}${input[1]}`, 16); + const g = parseInt(`${input[2]}${input[3]}`, 16); + const b = parseInt(`${input[4]}${input[5]}`, 16); + return { r, g, b }; +};