diff --git a/forum-network/notes/client-or-ui.md b/forum-network/notes/client-or-ui.md deleted file mode 100644 index ad24afd..0000000 --- a/forum-network/notes/client-or-ui.md +++ /dev/null @@ -1,9 +0,0 @@ -## Client/UI - -Voting consists of staking operations performed by software operated by owners of EOA. - -This software may be referred to as "The UI". It may also be considered "a client". - -It will need to be a network-connected application. It will need a certain minimum of RAM, -and for some features disk storage, -and for some features uptime . diff --git a/forum-network/notes/client.md b/forum-network/notes/client.md index 55df288..ee6db86 100644 --- a/forum-network/notes/client.md +++ b/forum-network/notes/client.md @@ -1,5 +1,18 @@ -# Client Operations +## Client -Client must communicate with one or more servers. +Clients play a key role in an MVPR DAO. -Client must build a local view +Clients must be operated by reputation holders. + +Clients are the agents that submit posts to the forum, initiate validation pools, and vote in validation pools. + +We sometimes refer to the client as "the UI". + +It will need to be a network-connected application. It will need a certain minimum of RAM, +and for some features disk storage, +and for some features uptime . + +The behavior of the client constitutes what we refer to as the DAO's "soft protocols". + +Malicious actors may freely modify their own client's behavior. +Therefore honest clients must engage in policing to preserve the integrity of the network. diff --git a/forum-network/src/classes/actors/expert.js b/forum-network/src/classes/actors/expert.js index 6928e45..112f867 100644 --- a/forum-network/src/classes/actors/expert.js +++ b/forum-network/src/classes/actors/expert.js @@ -1,5 +1,4 @@ import { Action } from '../display/action.js'; -import { PostMessage } from '../forum-network/message.js'; import { CryptoUtil } from '../supporting/crypto.js'; import { ReputationHolder } from '../reputation/reputation-holder.js'; import { EdgeTypes } from '../../util/constants.js'; @@ -18,7 +17,6 @@ export class Expert extends ReputationHolder { getAssignedWork: new Action('get assigned work', scene), submitWork: new Action('submit work evidence', scene), }; - this.validationPools = new Map(); this.tokens = []; } @@ -49,21 +47,23 @@ export class Expert extends ReputationHolder { return this; } - async submitPostWithFee(postContent, poolOptions) { + async submitPostWithFee(postContent, { fee }, params) { const post = await this.dao.forum.addPost(this.reputationPublicKey, postContent); await this.actions.submitPost.log(this, post); const postId = post.id; - const pool = await this.initiateValidationPool({ ...poolOptions, postId }); + const pool = await this.initiateValidationPool({ fee, postId }, params); this.tokens.push(pool.tokenId); return { postId, pool }; } - async initiateValidationPool(poolOptions) { + async initiateValidationPool({ postId, fee }, params) { // For now, make direct call rather than network - poolOptions.reputationPublicKey = this.reputationPublicKey; - const pool = await this.dao.initiateValidationPool(this, poolOptions); + const pool = await this.dao.initiateValidationPool(this, { + reputationPublicKey: this.reputationPublicKey, + postId, + fee, + }, params); this.tokens.push(pool.tokenId); - this.validationPools.set(pool.id, poolOptions); return pool; } diff --git a/forum-network/src/classes/dao/business.js b/forum-network/src/classes/dao/business.js index da603ed..7dcf53d 100644 --- a/forum-network/src/classes/dao/business.js +++ b/forum-network/src/classes/dao/business.js @@ -80,12 +80,16 @@ export class Business extends Actor { const pool = await this.dao.initiateValidationPool(this, { postId, fee: request.fee, + reputationPublicKey, + }, { duration, tokenLossRatio, - }, { - reputationPublicKey, - authorStakeAmount: request.worker.stakeAmount, + }); + + await pool.stake(reputationPublicKey, { tokenId: request.worker.tokenId, + amount: request.worker.stakeAmount, + position: true, }); // When the validation pool concludes, diff --git a/forum-network/src/classes/dao/client.js b/forum-network/src/classes/dao/client.js new file mode 100644 index 0000000..366fa7f --- /dev/null +++ b/forum-network/src/classes/dao/client.js @@ -0,0 +1,6 @@ +export class Client { + constructor(dao, expert) { + this.dao = dao; + this.expert = expert; + } +} diff --git a/forum-network/src/classes/dao/dao.js b/forum-network/src/classes/dao/dao.js index 867423c..4e31fa8 100644 --- a/forum-network/src/classes/dao/dao.js +++ b/forum-network/src/classes/dao/dao.js @@ -3,6 +3,7 @@ import { ReputationTokenContract } from '../reputation/reputation-token.js'; import { ValidationPool } from './validation-pool.js'; import { Availability } from './availability.js'; import { Business } from './business.js'; +import { Voter } from '../supporting/voter.js'; import { Actor } from '../display/actor.js'; /** @@ -33,6 +34,12 @@ export class DAO extends Actor { Array.from(this.validationPools.values()); } + addVoteRecord(reputationPublicKey, validationPool) { + const voter = this.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey); + voter.addVoteRecord(validationPool); + this.experts.set(reputationPublicKey, voter); + } + listActiveVoters({ activeVoterThreshold } = {}) { return Array.from(this.experts.values()).filter((voter) => { const hasVoted = !!voter.dateLastVote; @@ -54,21 +61,14 @@ export class DAO extends Actor { .reduce((acc, cur) => (acc += cur), 0); } - async initiateValidationPool(fromActor, poolOptions, stakeOptions) { + async initiateValidationPool(fromActor, { postId, reputationPublicKey, fee }, params) { const validationPoolNumber = this.validationPools.size + 1; const name = `Pool${validationPoolNumber}`; - const pool = new ValidationPool(this, poolOptions, name, this.scene, fromActor); + const pool = new ValidationPool(this, { + postId, reputationPublicKey, fee, + }, params, name, this.scene, fromActor); this.validationPools.set(pool.id, pool); - if (stakeOptions) { - const { reputationPublicKey, tokenId, authorStakeAmount } = stakeOptions; - await pool.stake(reputationPublicKey, { - tokenId, - position: true, - amount: authorStakeAmount, - }); - } - return pool; } } diff --git a/forum-network/src/classes/dao/forum.js b/forum-network/src/classes/dao/forum.js index 0038745..2ac0d5b 100644 --- a/forum-network/src/classes/dao/forum.js +++ b/forum-network/src/classes/dao/forum.js @@ -190,7 +190,7 @@ export class Forum extends ReputationHolder { depth = 0, initialNegative = false, referenceChainLimit, - leachingValue + leachingValue, }) { const postVertex = edge.to; const post = postVertex.data; @@ -260,7 +260,7 @@ export class Forum extends ReputationHolder { depth: depth + 1, initialNegative: initialNegative || (depth === 0 && outboundAmount < 0), referenceChainLimit, - leachingValue + leachingValue, }); // Any excess (negative) amount that could not be propagated, diff --git a/forum-network/src/classes/dao/validation-pool.js b/forum-network/src/classes/dao/validation-pool.js index c46f213..520fc83 100644 --- a/forum-network/src/classes/dao/validation-pool.js +++ b/forum-network/src/classes/dao/validation-pool.js @@ -1,6 +1,5 @@ import { ReputationHolder } from '../reputation/reputation-holder.js'; import { Stake } from '../supporting/stake.js'; -import { Voter } from '../supporting/voter.js'; import { Action } from '../display/action.js'; import { displayNumber } from '../../util/helpers.js'; @@ -47,6 +46,8 @@ export class ValidationPool extends ReputationHolder { postId, reputationPublicKey, fee, + }, + { duration, tokenLossRatio, contentiousDebate = false, @@ -113,6 +114,13 @@ export class ValidationPool extends ReputationHolder { throw new Error(`Each citation magnitude must not exceed revaluation limit ${params.revaluationLimit}`); } + if (post.authors?.length) { + const totalAuthorWeight = post.authors.reduce((total, { weight }) => total += weight, 0); + if (totalAuthorWeight !== 1) { + throw new Error(`Total author weight ${totalAuthorWeight} !== 1`); + } + } + this.state = ValidationPoolStates.OPEN; this.setStatus('Open'); this.stakes = new Set(); @@ -140,9 +148,7 @@ export class ValidationPool extends ReputationHolder { this.actions.mint.log(this, this, `(${this.mintedValue})`); // Keep a record of voters and their votes - const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey); - voter.addVoteRecord(this); - this.dao.experts.set(reputationPublicKey, voter); + this.dao.addVoteRecord(reputationPublicKey, this); } getTokenLossRatio() { @@ -225,12 +231,10 @@ export class ValidationPool extends ReputationHolder { // Keep a record of voters and their votes if (reputationPublicKey !== this.id) { - const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey); - voter.addVoteRecord(this); - this.dao.experts.set(reputationPublicKey, voter); + this.dao.addVoteRecord(reputationPublicKey, this); // Update computed display values - const actor = this.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey); + const actor = this.scene?.findActor((a) => a.reputationPublicKey === reputationPublicKey); await actor.computeDisplayValues(); } } diff --git a/forum-network/src/classes/display/box.js b/forum-network/src/classes/display/box.js index bee1f88..1ed666a 100644 --- a/forum-network/src/classes/display/box.js +++ b/forum-network/src/classes/display/box.js @@ -5,7 +5,9 @@ export class Box { constructor(name, parentEl, options = {}) { this.name = name; this.el = document.createElement('div'); - this.el.id = `box_${randomID()}`; + this.el.box = this; + const id = options.id ?? randomID(); + this.el.id = `${parentEl.id}_box_${id}`; this.el.classList.add('box'); if (name) { this.el.setAttribute('box-name', name); diff --git a/forum-network/src/classes/display/document.js b/forum-network/src/classes/display/document.js new file mode 100644 index 0000000..69545be --- /dev/null +++ b/forum-network/src/classes/display/document.js @@ -0,0 +1,37 @@ +import { Box } from './box.js'; +import { Form } from './form.js'; + +export class Remark extends Box { + constructor(doc, text, opts) { + super('Remark', doc.el, opts); + this.setInnerHTML(text); + } +} + +/** + * @example + * ```typescript + * const doc = new Document(); + * const form1 = doc.form(); + * ``` + */ +export class Document extends Box { + form() { + return this.addElement(new Form(this)); + } + + remark(text, opts) { + return this.addElement(new Remark(this, text, opts)); + } + + addElement(element) { + this.elements = this.elements ?? []; + this.elements.push(element); + return this; + } + + get lastElement() { + if (!this.elements?.length) return null; + return this.elements[this.elements.length - 1]; + } +} diff --git a/forum-network/src/classes/display/form.js b/forum-network/src/classes/display/form.js new file mode 100644 index 0000000..a818f11 --- /dev/null +++ b/forum-network/src/classes/display/form.js @@ -0,0 +1,60 @@ +import { randomID } from '../../util/helpers.js'; +import { Box } from './box.js'; + +const updateValuesOnEventTypes = ['keyup', 'mouseup']; + +export class FormElement extends Box { + constructor(name, parentEl, opts) { + super(name, parentEl, opts); + this.id = opts.id ?? name; + const { cb } = opts; + if (cb) { + updateValuesOnEventTypes.forEach((eventType) => this.el.addEventListener(eventType, () => { + cb(this); + })); + cb(this); + } + } +} + +export class Button extends FormElement { } + +export class TextField extends FormElement { + constructor(name, parentEl, opts) { + super(name, parentEl, opts); + this.label = document.createElement('label'); + this.label.innerHTML = name; + this.input = document.createElement('input'); + this.label.appendChild(this.input); + this.el.appendChild(this.label); + } + + get value() { + return this.input?.value || null; + } +} + +export class TextArea extends FormElement { } + +export class Form { + constructor(document, opts = {}) { + this.document = document; + this.items = []; + this.id = opts.id ?? `form_${randomID()}`; + } + + button(opts) { + this.items.push(new Button(opts.name, this.document.el, opts)); + return this; + } + + textField(opts) { + this.items.push(new TextField(opts.name, this.document.el, opts)); + return this; + } + + textArea(opts) { + this.items.push(new TextArea(opts.name, this.document.el, opts)); + return this; + } +} diff --git a/forum-network/src/classes/display/scene.js b/forum-network/src/classes/display/scene.js index 8f62ad1..3b14ec6 100644 --- a/forum-network/src/classes/display/scene.js +++ b/forum-network/src/classes/display/scene.js @@ -6,6 +6,7 @@ import { Table } from './table.js'; import { Flowchart } from './flowchart.js'; import { Controls } from './controls.js'; import { Box } from './box.js'; +import { Document } from './document.js'; export class Scene { constructor(name, rootBox) { @@ -86,6 +87,24 @@ export class Scene { return this; } + /** + * + * @param {string} name + * @param {(Document): Document} cb + * @returns {Scene} + */ + withDocument(name, cb) { + this.documents = this.documents ?? []; + const doc = new Document(name, this.middleSection.el); + this.documents.push(cb ? cb(doc) : doc); + return this; + } + + get lastDocument() { + if (!this.documents?.length) return null; + return this.documents[this.documents.length - 1]; + } + registerActor(actor) { this.actors.add(actor); if (actor.options.announce) { diff --git a/forum-network/src/classes/forum-network/forum-node.js b/forum-network/src/classes/forum-network/forum-node.js deleted file mode 100644 index ff76ed7..0000000 --- a/forum-network/src/classes/forum-network/forum-node.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Action } from '../display/action.js'; -import { - Message, PostMessage, PeerMessage, messageFromJSON, -} from './message.js'; -import { NetworkNode } from './network-node.js'; -import { randomID } from '../../util/helpers.js'; - -export class ForumNode extends NetworkNode { - constructor(name, scene) { - super(name, scene); - this.actions = { - ...this.actions, - storePost: new Action('store post', scene), - }; - } - - // Process a message from the queue - async processMessage(messageJson) { - try { - await Message.verify(messageJson); - } catch (e) { - await this.actions.processMessage.log(this, this, 'invalid signature', null, '-x'); - console.log(`${this.name}: received message with invalid signature`); - return; - } - - const { publicKey } = messageJson; - const message = messageFromJSON(messageJson); - - if (message instanceof PostMessage) { - await this.processPostMessage(publicKey, message.content); - } else if (message instanceof PeerMessage) { - await this.processPeerMessage(publicKey, message.content); - } else { - // Unknown message type - // Penalize sender for wasting our time - } - } - - // Process an incoming post, received by whatever means - async processPost(authorId, post) { - if (!post.id) { - post.id = randomID(); - } - await this.actions.storePost.log(this, this); - // this.forumView.addPost(authorId, post.id, post, stake); - } - - // Process a post we received in a message - async processPostMessage(authorId, { post, stake }) { - this.processPost(authorId, post, stake); - await this.broadcast( - new PeerMessage({ - posts: [{ authorId, post, stake }], - }), - ); - } - - // Process a message we receive from a peer - async processPeerMessage(peerId, { posts }) { - // We are trusting that the peer verified the signatures of the posts they're forwarding. - // We could instead have the peer forward the signed messages and re-verify them. - for (const { authorId, post, stake } of posts) { - this.processPost(authorId, post, stake); - } - } -} diff --git a/forum-network/src/classes/forum-network/message.js b/forum-network/src/classes/forum-network/message.js deleted file mode 100644 index c0cb1be..0000000 --- a/forum-network/src/classes/forum-network/message.js +++ /dev/null @@ -1,65 +0,0 @@ -import { CryptoUtil } from '../supporting/crypto.js'; -import { PostContent } from '../supporting/post-content.js'; - -export class Message { - constructor(content) { - this.content = content; - } - - async sign({ publicKey, privateKey }) { - this.publicKey = await CryptoUtil.exportKey(publicKey); - // Call toJSON before signing, to match what we'll later send - this.signature = await CryptoUtil.sign(this.contentToJSON(), privateKey); - return this; - } - - static async verify({ content, publicKey, signature }) { - return CryptoUtil.verify(content, publicKey, signature); - } - - contentToJSON() { - return this.content; - } - - toJSON() { - return { - type: this.type, - content: this.contentToJSON(), - publicKey: this.publicKey, - signature: this.signature, - }; - } -} - -export class PostMessage extends Message { - type = 'post'; - - constructor({ post, stake }) { - super({ - post: PostContent.fromJSON(post), - stake, - }); - } - - contentToJSON() { - return { - post: this.content.post.toJSON(), - stakeAmount: this.content.stake, - }; - } -} - -export class PeerMessage extends Message { - type = 'peer'; -} - -const messageTypes = new Map([ - ['post', PostMessage], - ['peer', PeerMessage], -]); - -export const messageFromJSON = ({ type, content }) => { - const MessageType = messageTypes.get(type) || Message; - // const messageContent = MessageType.contentFromJSON(content); - return new MessageType(content); -}; diff --git a/forum-network/src/classes/forum-network/network-node.js b/forum-network/src/classes/forum-network/network-node.js deleted file mode 100644 index 345e3ad..0000000 --- a/forum-network/src/classes/forum-network/network-node.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Actor } from '../display/actor.js'; -import { Action } from '../display/action.js'; -import { CryptoUtil } from '../util/crypto.js'; -import { PrioritizedQueue } from '../util/prioritized-queue.js'; - -export class NetworkNode extends Actor { - constructor(name, scene) { - super(name, scene); - this.queue = new PrioritizedQueue(); - this.actions = { - peerMessage: new Action('peer message', scene), - }; - } - - // Generate a signing key pair and connect to the network - async initialize(forumNetwork) { - this.keyPair = await CryptoUtil.generateAsymmetricKey(); - this.forumNetwork = forumNetwork.addNode(this); - this.status.set('Initialized'); - return this; - } - - // Send a message to all other nodes in the network - async broadcast(message) { - await message.sign(this.keyPair); - const otherForumNodes = this.forumNetwork - .listNodes() - .filter((forumNode) => forumNode.keyPair.publicKey !== this.keyPair.publicKey); - for (const forumNode of otherForumNodes) { - // For now just call receiveMessage on the target node - // await this.actions.peerMessage.log(this, forumNode, null, message.content); - await this.actions.peerMessage.log(this, forumNode); - await forumNode.receiveMessage(JSON.stringify(message.toJSON())); - } - } - - // Perform minimal processing to ingest a message. - // Enqueue it for further processing. - async receiveMessage(messageStr) { - const messageJson = JSON.parse(messageStr); - // const senderReputation = this.forumView.getReputation(messageJson.publicKey) || 0; - const senderReputation = 0; - this.queue.add(messageJson, senderReputation); - } - - // Process next highest priority message in the queue - async processNextMessage() { - const messageJson = this.queue.pop(); - if (!messageJson) { - return null; - } - return this.processMessage(messageJson); - } - - // Process a message from the queue - // async processMessage(messageJson) { - // } -} diff --git a/forum-network/src/classes/forum-network/network.js b/forum-network/src/classes/forum-network/network.js deleted file mode 100644 index 6e7e4cd..0000000 --- a/forum-network/src/classes/forum-network/network.js +++ /dev/null @@ -1,14 +0,0 @@ -export class Network { - constructor() { - this.nodes = new Map(); - } - - addNode(node) { - this.nodes.set(node.keyPair.publicKey, node); - return this; - } - - listNodes() { - return Array.from(this.nodes.values()); - } -} diff --git a/forum-network/src/index.css b/forum-network/src/index.css index 1ff9007..237a290 100644 --- a/forum-network/src/index.css +++ b/forum-network/src/index.css @@ -71,3 +71,12 @@ button:disabled { background-color: #2a535e; color: #919191; } +label > input { + margin-left: 1em; +} +label { + font-family: monospace; + font-weight: bold; + font-size: smaller; + color: #999999; +} diff --git a/forum-network/src/index.html b/forum-network/src/index.html index c0d4a65..3c3e1fe 100644 --- a/forum-network/src/index.html +++ b/forum-network/src/index.html @@ -29,7 +29,12 @@ +