mirror of https://codeberg.org/pzp/pzp-db.git
remove feed-v1 (it can be found in git tag "rev1")
This commit is contained in:
parent
e52368b92f
commit
f66807a774
|
@ -1,53 +0,0 @@
|
|||
const blake3 = require('blake3')
|
||||
const base58 = require('bs58')
|
||||
const stringify = require('json-canon')
|
||||
|
||||
/**
|
||||
* @typedef {import('./index').Msg} Msg
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Msg} msg
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function getMsgHashBuf(msg) {
|
||||
const metadataBuf = Buffer.from(stringify(msg.metadata), 'utf8')
|
||||
return blake3.hash(metadataBuf).subarray(0, 16)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Msg | string} x
|
||||
* @returns {string}
|
||||
*/
|
||||
function getMsgHash(x) {
|
||||
if (typeof x === 'string') {
|
||||
if (x.startsWith('ppppp:message/v1/')) {
|
||||
const msgUri = x
|
||||
const parts = msgUri.split('/')
|
||||
return parts[parts.length - 1]
|
||||
} else {
|
||||
const msgHash = x
|
||||
return msgHash
|
||||
}
|
||||
} else {
|
||||
const msg = x
|
||||
const msgHashBuf = getMsgHashBuf(msg)
|
||||
return base58.encode(msgHashBuf)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Msg} msg
|
||||
* @returns {string}
|
||||
*/
|
||||
function getMsgId(msg) {
|
||||
const { who, type } = msg.metadata
|
||||
const msgHash = getMsgHash(msg)
|
||||
if (type) {
|
||||
return `ppppp:message/v1/${who}/${type}/${msgHash}`
|
||||
} else {
|
||||
return `ppppp:message/v1/${who}/${msgHash}`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getMsgId, getMsgHash }
|
|
@ -1,212 +0,0 @@
|
|||
const stringify = require('json-canon')
|
||||
const ed25519 = require('ssb-keys/sodium')
|
||||
const base58 = require('bs58')
|
||||
const union = require('set.prototype.union')
|
||||
const { stripAuthor } = require('./strip')
|
||||
const { getMsgId, getMsgHash } = require('./get-msg-id')
|
||||
const representContent = require('./represent-content')
|
||||
const {
|
||||
validateType,
|
||||
validateContent,
|
||||
validate,
|
||||
validateBatch,
|
||||
validateMsgHash,
|
||||
} = require('./validation')
|
||||
const Tangle = require('./tangle')
|
||||
|
||||
function isEmptyObject(obj) {
|
||||
for (const _key in obj) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Iterator<Msg> & {values: () => Iterator<Msg>}} MsgIter
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TangleMetadata
|
||||
* @property {number} depth
|
||||
* @property {Array<string>} prev
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Msg
|
||||
* @property {*} content
|
||||
* @property {Object} metadata
|
||||
* @property {string} metadata.hash
|
||||
* @property {number} metadata.size
|
||||
* @property {Record<string, TangleMetadata>} metadata.tangles
|
||||
* @property {string} metadata.type
|
||||
* @property {1} metadata.v
|
||||
* @property {string} metadata.who
|
||||
* @property {string} sig
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Keys
|
||||
* @property {string} keys.id
|
||||
* @property {string} keys.private
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CreateOpts
|
||||
* @property {*} content
|
||||
* @property {string} type
|
||||
* @property {Keys} keys
|
||||
* @property {Record<string, Tangle>} tangles
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CreateRootOpts
|
||||
* @property {string} type
|
||||
* @property {Keys} keys
|
||||
* @property {string} keys.id
|
||||
* @property {string} keys.private
|
||||
*/
|
||||
|
||||
function isFeedRoot(msg, authorId, findType) {
|
||||
const findWho = stripAuthor(authorId)
|
||||
const { who, type, tangles } = msg.metadata
|
||||
return who === findWho && type === findType && isEmptyObject(tangles)
|
||||
}
|
||||
|
||||
function getFeedRootHash(authorId, type) {
|
||||
const who = stripAuthor(authorId)
|
||||
|
||||
const msg = {
|
||||
content: null,
|
||||
metadata: {
|
||||
hash: null,
|
||||
size: 0,
|
||||
tangles: {},
|
||||
type,
|
||||
v: 1,
|
||||
who,
|
||||
},
|
||||
sig: '',
|
||||
}
|
||||
|
||||
return getMsgHash(msg)
|
||||
}
|
||||
|
||||
function toPlaintextBuffer(opts) {
|
||||
return Buffer.from(stringify(opts.content), 'utf8')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CreateOpts} opts
|
||||
* @returns {Msg}
|
||||
*/
|
||||
function create(opts) {
|
||||
let err
|
||||
if ((err = validateType(opts.type))) throw err
|
||||
if (!opts.tangles) throw new Error('opts.tangles is required')
|
||||
|
||||
const [hash, size] = representContent(opts.content)
|
||||
|
||||
const tangles = {}
|
||||
if (opts.tangles) {
|
||||
for (const rootId in opts.tangles) {
|
||||
if ((err = validateMsgHash(rootId))) throw err
|
||||
const tangle = opts.tangles[rootId]
|
||||
const depth = tangle.getMaxDepth() + 1
|
||||
const tips = tangle.getTips()
|
||||
const lipmaaSet = tangle.getLipmaaSet(depth)
|
||||
const prev = ([...union(lipmaaSet, tips)]).sort()
|
||||
tangles[rootId] = { depth, prev }
|
||||
}
|
||||
} else {
|
||||
// prettier-ignore
|
||||
throw new Error(`cannot create msg without tangles, that's the case for createRoot()`)
|
||||
}
|
||||
|
||||
const msg = {
|
||||
content: opts.content,
|
||||
metadata: {
|
||||
hash,
|
||||
size,
|
||||
tangles,
|
||||
type: opts.type,
|
||||
v: 1,
|
||||
who: stripAuthor(opts.keys.id),
|
||||
},
|
||||
sig: '',
|
||||
}
|
||||
if ((err = validateContent(msg))) throw err
|
||||
|
||||
const privateKey = Buffer.from(opts.keys.private, 'base64')
|
||||
// TODO: add a label prefix to the metadata before signing
|
||||
const metadataBuf = Buffer.from(stringify(msg.metadata), 'utf8')
|
||||
// TODO: when signing, what's the point of a customizable hmac?
|
||||
const sigBuf = ed25519.sign(privateKey, metadataBuf)
|
||||
msg.sig = base58.encode(sigBuf)
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keys} keys
|
||||
* @param {string} type
|
||||
* @returns {Msg}
|
||||
*/
|
||||
function createRoot(keys, type) {
|
||||
let err
|
||||
if ((err = validateType(type))) throw err
|
||||
|
||||
const msg = {
|
||||
content: null,
|
||||
metadata: {
|
||||
hash: null,
|
||||
size: 0,
|
||||
tangles: {},
|
||||
type,
|
||||
v: 1,
|
||||
who: stripAuthor(keys.id),
|
||||
},
|
||||
sig: '',
|
||||
}
|
||||
|
||||
const privateKey = Buffer.from(keys.private, 'base64')
|
||||
// TODO: add a label prefix to the metadata before signing
|
||||
const metadataBuf = Buffer.from(stringify(msg.metadata), 'utf8')
|
||||
// TODO: when signing, what's the point of a customizable hmac?
|
||||
const sigBuf = ed25519.sign(privateKey, metadataBuf)
|
||||
msg.sig = base58.encode(sigBuf)
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Msg} msg
|
||||
* @returns {Msg}
|
||||
*/
|
||||
function erase(msg) {
|
||||
return { ...msg, content: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} plaintextBuf
|
||||
* @param {Msg} msg
|
||||
* @returns {Msg}
|
||||
*/
|
||||
function fromPlaintextBuffer(plaintextBuf, msg) {
|
||||
return { ...msg, content: JSON.parse(plaintextBuf.toString('utf-8')) }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMsgHash,
|
||||
getMsgId,
|
||||
isFeedRoot,
|
||||
getFeedRootHash,
|
||||
create,
|
||||
createRoot,
|
||||
erase,
|
||||
stripAuthor,
|
||||
toPlaintextBuffer,
|
||||
fromPlaintextBuffer,
|
||||
Tangle,
|
||||
validate,
|
||||
validateBatch,
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
const blake3 = require('blake3')
|
||||
const base58 = require('bs58')
|
||||
const stringify = require('json-canon')
|
||||
|
||||
/**
|
||||
* @param {any} content
|
||||
* @returns {[string, number]}
|
||||
*/
|
||||
function representContent(content) {
|
||||
const contentBuf = Buffer.from(stringify(content), 'utf8')
|
||||
const hash = base58.encode(blake3.hash(contentBuf).subarray(0, 16))
|
||||
const size = contentBuf.length
|
||||
return [hash, size]
|
||||
}
|
||||
|
||||
module.exports = representContent
|
|
@ -1,29 +0,0 @@
|
|||
const { getMsgHash } = require('./get-msg-id')
|
||||
|
||||
function stripMsgKey(msgKey) {
|
||||
if (typeof msgKey === 'object') {
|
||||
if (msgKey.key) return stripMsgKey(msgKey.key)
|
||||
else return getMsgHash(msgKey)
|
||||
}
|
||||
if (msgKey.startsWith('ppppp:message/v1/')) {
|
||||
const parts = msgKey.split('/')
|
||||
return parts[parts.length - 1]
|
||||
} else {
|
||||
return msgKey
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {string}
|
||||
*/
|
||||
function stripAuthor(id) {
|
||||
if (id.startsWith('ppppp:feed/v1/') === false) return id
|
||||
const withoutPrefix = id.replace('ppppp:feed/v1/', '')
|
||||
return withoutPrefix.split('/')[0]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stripMsgKey,
|
||||
stripAuthor,
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
/**
|
||||
* @typedef {import("./index").Msg} Msg
|
||||
*/
|
||||
|
||||
function lipmaa(n) {
|
||||
let m = 1
|
||||
let po3 = 3
|
||||
let u = n
|
||||
|
||||
// find k such that (3^k - 1)/2 >= n
|
||||
while (m < n) {
|
||||
po3 *= 3
|
||||
m = (po3 - 1) / 2
|
||||
}
|
||||
|
||||
// find longest possible backjump
|
||||
po3 /= 3
|
||||
if (m !== n) {
|
||||
while (u !== 0) {
|
||||
m = (po3 - 1) / 2
|
||||
po3 /= 3
|
||||
u %= m
|
||||
}
|
||||
|
||||
if (m !== po3) {
|
||||
po3 = m
|
||||
}
|
||||
}
|
||||
|
||||
return n - po3
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
* @returns number
|
||||
*/
|
||||
function compareMsgHashes(a, b) {
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
|
||||
class Tangle {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
#rootHash
|
||||
|
||||
/**
|
||||
* @type {Msg}
|
||||
*/
|
||||
#rootMsg
|
||||
|
||||
/**
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#tips = new Set()
|
||||
|
||||
/**
|
||||
* @type {Map<string, Array<string>>}
|
||||
*/
|
||||
#prev = new Map()
|
||||
|
||||
/**
|
||||
* @type {Map<string, number>}
|
||||
*/
|
||||
#depth = new Map()
|
||||
|
||||
/**
|
||||
* @type {Map<number, Array<string>>}
|
||||
*/
|
||||
#perDepth = new Map()
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
#maxDepth
|
||||
|
||||
/**
|
||||
* @param {string} rootHash
|
||||
* @param {Iterable<Msg>} msgsIter
|
||||
*/
|
||||
constructor(rootHash) {
|
||||
this.#rootHash = rootHash
|
||||
this.#maxDepth = 0
|
||||
}
|
||||
|
||||
add(msgHash, msg) {
|
||||
if (msgHash === this.#rootHash && !this.#rootMsg) {
|
||||
this.#tips.add(msgHash)
|
||||
this.#perDepth.set(0, [msgHash])
|
||||
this.#depth.set(msgHash, 0)
|
||||
this.#rootMsg = msg
|
||||
return
|
||||
}
|
||||
|
||||
const tangles = msg.metadata.tangles
|
||||
if (msgHash !== this.#rootHash && tangles[this.#rootHash]) {
|
||||
this.#tips.add(msgHash)
|
||||
const prev = tangles[this.#rootHash].prev
|
||||
for (const p of prev) {
|
||||
this.#tips.delete(p)
|
||||
}
|
||||
this.#prev.set(msgHash, prev)
|
||||
const depth = tangles[this.#rootHash].depth
|
||||
if (depth > this.#maxDepth) this.#maxDepth = depth
|
||||
this.#depth.set(msgHash, depth)
|
||||
const atDepth = this.#perDepth.get(depth) ?? []
|
||||
atDepth.push(msgHash)
|
||||
atDepth.sort(compareMsgHashes)
|
||||
this.#perDepth.set(depth, atDepth)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} depth
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
#getAllAtDepth(depth) {
|
||||
return this.#perDepth.get(depth) ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
topoSort() {
|
||||
if (!this.#rootMsg) {
|
||||
console.warn('Tangle is missing root message')
|
||||
return []
|
||||
}
|
||||
const sorted = []
|
||||
const max = this.#maxDepth
|
||||
for (let i = 0; i <= max; i++) {
|
||||
const atDepth = this.#getAllAtDepth(i)
|
||||
for (const msgHash of atDepth) {
|
||||
sorted.push(msgHash)
|
||||
}
|
||||
}
|
||||
return sorted
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
getTips() {
|
||||
if (!this.#rootMsg) {
|
||||
console.warn('Tangle is missing root message')
|
||||
return new Set()
|
||||
}
|
||||
return this.#tips
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} depth
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
getLipmaaSet(depth) {
|
||||
if (!this.#rootMsg) {
|
||||
console.warn('Tangle is missing root message')
|
||||
return new Set()
|
||||
}
|
||||
const lipmaaDepth = lipmaa(depth + 1) - 1
|
||||
return new Set(this.#getAllAtDepth(lipmaaDepth))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} msgHash
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(msgHash) {
|
||||
return this.#depth.has(msgHash)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} msgHash
|
||||
* @returns {number}
|
||||
*/
|
||||
getDepth(msgHash) {
|
||||
return this.#depth.get(msgHash) ?? -1
|
||||
}
|
||||
|
||||
isFeed() {
|
||||
if (!this.#rootMsg) {
|
||||
console.warn('Tangle is missing root message')
|
||||
return false
|
||||
}
|
||||
if (this.#rootMsg.content) return false
|
||||
const metadata = this.#rootMsg.metadata
|
||||
return metadata.size === 0 && metadata.hash === null
|
||||
}
|
||||
|
||||
getFeed() {
|
||||
if (!this.isFeed()) return null
|
||||
const { type, who } = this.#rootMsg.metadata
|
||||
return { type, who }
|
||||
}
|
||||
|
||||
shortestPathToRoot(msgHash) {
|
||||
if (!this.#rootMsg) {
|
||||
console.warn('Tangle is missing root message')
|
||||
return []
|
||||
}
|
||||
const path = []
|
||||
let current = msgHash
|
||||
while (true) {
|
||||
const prev = this.#prev.get(current)
|
||||
if (!prev) break
|
||||
let minDepth = this.#depth.get(current)
|
||||
let min = current
|
||||
for (const p of prev) {
|
||||
const d = this.#depth.get(p)
|
||||
if (d < minDepth) {
|
||||
minDepth = d
|
||||
min = p
|
||||
} else if (d === minDepth && compareMsgHashes(p, min) < 0) {
|
||||
min = p
|
||||
}
|
||||
}
|
||||
path.push(min)
|
||||
current = min
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
precedes(a, b) {
|
||||
if (!this.#rootMsg) {
|
||||
console.warn('Tangle is missing root message')
|
||||
return false
|
||||
}
|
||||
if (a === b) return false
|
||||
if (b === this.#rootHash) return false
|
||||
let toCheck = [b]
|
||||
while (toCheck.length > 0) {
|
||||
const prev = this.#prev.get(toCheck.shift())
|
||||
if (!prev) continue
|
||||
if (prev.includes(a)) return true
|
||||
toCheck.push(...prev)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.#depth.size
|
||||
}
|
||||
|
||||
getMaxDepth() {
|
||||
return this.#maxDepth
|
||||
}
|
||||
|
||||
debug() {
|
||||
let str = ''
|
||||
const max = this.#maxDepth
|
||||
for (let i = 0; i <= max; i++) {
|
||||
const atDepth = this.#getAllAtDepth(i)
|
||||
str += `Depth ${i}: ${atDepth.join(', ')}\n`
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tangle
|
|
@ -1,249 +0,0 @@
|
|||
const base58 = require('bs58')
|
||||
const ed25519 = require('ssb-keys/sodium')
|
||||
const stringify = require('json-canon')
|
||||
const Tangle = require('./tangle')
|
||||
const representContent = require('./represent-content')
|
||||
|
||||
function validateShape(msg) {
|
||||
if (!msg || typeof msg !== 'object') {
|
||||
return new Error('invalid message: not an object')
|
||||
}
|
||||
if (!msg.metadata || typeof msg.metadata !== 'object') {
|
||||
return new Error('invalid message: must have metadata')
|
||||
}
|
||||
if (typeof msg.metadata.who === 'undefined') {
|
||||
return new Error('invalid message: must have metadata.who')
|
||||
}
|
||||
if (msg.metadata.v !== 1) {
|
||||
return new Error('invalid message: must have metadata.v 1')
|
||||
}
|
||||
if (typeof msg.metadata.tangles !== 'object') {
|
||||
return new Error('invalid message: must have metadata.tangles')
|
||||
}
|
||||
if (typeof msg.metadata.hash === 'undefined') {
|
||||
return new Error('invalid message: must have metadata.hash')
|
||||
}
|
||||
if (typeof msg.metadata.size === 'undefined') {
|
||||
return new Error('invalid message: must have metadata.size')
|
||||
}
|
||||
if (typeof msg.content === 'undefined') {
|
||||
return new Error('invalid message: must have content')
|
||||
}
|
||||
if (typeof msg.sig === 'undefined') {
|
||||
return new Error('invalid message: must have sig')
|
||||
}
|
||||
}
|
||||
|
||||
function validateWho(msg) {
|
||||
try {
|
||||
const whoBuf = base58.decode(msg.metadata.who)
|
||||
if (whoBuf.length !== 32) {
|
||||
return new Error(
|
||||
`invalid message: decoded "who" should be 32 bytes but was ${whoBuf.length}`
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
return new Error('invalid message: must have "who" as base58 string')
|
||||
}
|
||||
}
|
||||
|
||||
function validateMsgHash(str) {
|
||||
try {
|
||||
const hashBuf = Buffer.from(base58.decode(str))
|
||||
if (hashBuf.length !== 16) {
|
||||
return new Error(
|
||||
`invalid message: decoded hash should be 16 bytes but was ${hashBuf.length}`
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
return new Error(
|
||||
`invalid message: msgHash ${str} should have been a base58 string`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function validateSize(msg) {
|
||||
const {
|
||||
metadata: { size },
|
||||
} = msg
|
||||
if (!Number.isSafeInteger(size) || size < 0) {
|
||||
return new Error(`invalid message: "size" should be an unsigned integer`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateSignature(msg) {
|
||||
const { sig } = msg
|
||||
if (typeof sig !== 'string') {
|
||||
return new Error('invalid message: must have sig as a string')
|
||||
}
|
||||
let sigBuf
|
||||
try {
|
||||
sigBuf = Buffer.from(base58.decode(sig))
|
||||
if (sigBuf.length !== 64) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: sig should be 64 bytes but was ' + sigBuf.length + ', on feed: ' + msg.metadata.who);
|
||||
}
|
||||
} catch (err) {
|
||||
return new Error('invalid message: sig must be a base58 string')
|
||||
}
|
||||
|
||||
const publicKeyBuf = Buffer.from(base58.decode(msg.metadata.who))
|
||||
const signableBuf = Buffer.from(stringify(msg.metadata), 'utf8')
|
||||
const verified = ed25519.verify(publicKeyBuf, sigBuf, signableBuf)
|
||||
if (!verified) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: sig does not match, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} msg
|
||||
* @param {Tangle} tangle
|
||||
* @param {*} tangleId
|
||||
* @returns
|
||||
*/
|
||||
function validateTangle(msg, tangle, tangleId) {
|
||||
if (!msg.metadata.tangles[tangleId]) {
|
||||
return new Error('invalid message: must have metadata.tangles.' + tangleId)
|
||||
}
|
||||
const { depth, prev } = msg.metadata.tangles[tangleId]
|
||||
if (!prev || !Array.isArray(prev)) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: prev must be an array, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
if (!Number.isSafeInteger(depth) || depth <= 0) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: depth must be a positive integer, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
if (tangle.isFeed()) {
|
||||
const { type, who } = tangle.getFeed()
|
||||
if (type !== msg.metadata.type) {
|
||||
// prettier-ignore
|
||||
return new Error(`invalid message: type "${msg.metadata.type}" does not match feed type "${type}"`)
|
||||
}
|
||||
if (who !== msg.metadata.who) {
|
||||
// prettier-ignore
|
||||
return new Error(`invalid message: who "${msg.metadata.who}" does not match feed who "${who}"`)
|
||||
}
|
||||
}
|
||||
let lastPrev = null
|
||||
let minDiff = Infinity
|
||||
let countPrevUnknown = 0
|
||||
for (const p of prev) {
|
||||
if (typeof p !== 'string') {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: prev must contain strings but found ' + p + ', on feed: ' + msg.metadata.who);
|
||||
}
|
||||
if (p.startsWith('ppppp:')) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: prev must not contain URIs, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
if (lastPrev !== null) {
|
||||
if (p === lastPrev) {
|
||||
return new Error(`invalid message: prev must be unique set, on feed ${msg.metadata.who}`)
|
||||
}
|
||||
if (p < lastPrev) {
|
||||
return new Error(`invalid message: prev must be sorted in alphabetical order, on feed ${msg.metadata.who}`)
|
||||
}
|
||||
}
|
||||
lastPrev = p
|
||||
|
||||
if (!tangle.has(p)) {
|
||||
countPrevUnknown += 1
|
||||
continue
|
||||
}
|
||||
const prevDepth = tangle.getDepth(p)
|
||||
|
||||
const diff = depth - prevDepth
|
||||
if (diff <= 0) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: depth of prev ' + p + ' is not lower, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
if (diff < minDiff) minDiff = diff
|
||||
}
|
||||
|
||||
if (countPrevUnknown === prev.length) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: all prev are locally unknown, on feed: ' + msg.metadata.who)
|
||||
}
|
||||
|
||||
if (countPrevUnknown === 0 && minDiff !== 1) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: depth must be the largest prev depth plus one');
|
||||
}
|
||||
}
|
||||
|
||||
function validateTangleRoot(msg, msgHash, tangleId) {
|
||||
if (msgHash !== tangleId) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: tangle root hash must match tangleId, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
if (msg.metadata.tangles[tangleId]) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: tangle root must not have self tangle data, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
}
|
||||
|
||||
function validateType(type) {
|
||||
if (!type || typeof type !== 'string') {
|
||||
// prettier-ignore
|
||||
return new Error('type is not a string');
|
||||
}
|
||||
if (type.length > 100) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid type ' + type + ' is 100+ characters long');
|
||||
}
|
||||
if (type.length < 3) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid type ' + type + ' is shorter than 3 characters');
|
||||
}
|
||||
if (/[^a-zA-Z0-9_]/.test(type)) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid type ' + type + ' contains characters other than a-z, A-Z, 0-9, or _');
|
||||
}
|
||||
}
|
||||
|
||||
function validateContent(msg) {
|
||||
const { content } = msg
|
||||
if (content === null) {
|
||||
return
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return new Error('invalid message: content must not be an array')
|
||||
}
|
||||
if (typeof content !== 'object' && typeof content !== 'string') {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: content must be an object or string, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
const [hash, size] = representContent(content)
|
||||
if (hash !== msg.metadata.hash) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: content hash does not match metadata.hash, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
if (size !== msg.metadata.size) {
|
||||
// prettier-ignore
|
||||
return new Error('invalid message: content size does not match metadata.size, on feed: ' + msg.metadata.who);
|
||||
}
|
||||
}
|
||||
|
||||
function validate(msg, tangle, msgHash, rootHash) {
|
||||
let err
|
||||
if ((err = validateShape(msg))) return err
|
||||
if ((err = validateWho(msg))) return err
|
||||
if ((err = validateSize(msg))) return err
|
||||
if (tangle.size() === 0) {
|
||||
if ((err = validateTangleRoot(msg, msgHash, rootHash))) return err
|
||||
} else {
|
||||
if ((err = validateTangle(msg, tangle, rootHash))) return err
|
||||
}
|
||||
if ((err = validateContent(msg))) return err
|
||||
if ((err = validateSignature(msg))) return err
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateType,
|
||||
validateContent,
|
||||
validate,
|
||||
validateMsgHash,
|
||||
}
|
Loading…
Reference in New Issue