Compare commits

..

4 Commits

Author SHA1 Message Date
Jacob Karlsson 451bd263ef 1.0.1 2024-04-27 23:41:00 +02:00
Jacob Karlsson b9dc3778a5 Add returns to validation 2024-04-27 23:40:17 +02:00
Jacob Karlsson 0084f54600 1.0.0 2024-04-27 23:36:17 +02:00
Jacob Karlsson 543a5c4d48 Rename to pzp 2024-04-27 23:34:25 +02:00
5 changed files with 168 additions and 212 deletions

View File

@ -1,158 +0,0 @@
const pull = require('pull-stream')
const p = require('node:util').promisify
const MsgV4 = require('./msg-v4')
/**
* @typedef {string} MsgID
* @typedef {import('./msg-v4').Msg} Msg
*/
/**
* @typedef {{
* id?: never;
* msg?: never;
* received?: never;
* }} RecDeleted
*
* @typedef {{
* id: MsgID;
* msg: Msg;
* received: number;
* }} RecPresent
*
* @typedef {RecPresent | RecDeleted} Rec
*/
/**
* @template T
* @typedef {[T] extends [void] ?
* (...args: [Error] | []) => void :
* (...args: [Error] | [null, T]) => void
* } CB
*/
class DBTangle extends MsgV4.Tangle {
/** @type {(msgID: MsgID, cb: CB<Msg>) => void} */
#getMsg
/**
* @param {MsgID} rootID
* @param {(msgID: MsgID, cb: CB<Msg>) => void} getMsg
*/
constructor(rootID, getMsg) {
super(rootID)
this.#getMsg = getMsg
}
/**
* @param {MsgID} rootID
* @param {AsyncIterable<Rec>} recordsIter
* @param {(msgID: MsgID, cb: any) => void} getMsg
* @return {Promise<DBTangle>}
*/
static async init(rootID, recordsIter, getMsg) {
const dbtangle = new DBTangle(rootID, getMsg)
for await (const rec of recordsIter) {
if (!rec.msg) continue
dbtangle.add(rec.id, rec.msg)
}
return dbtangle
}
/**
* Given a set of msgs (`msgIDs`) in this tangle, find all "deletable" and
* "erasable" msgs that precede that set.
*
* *Deletables* are msgs that precede `msgsIDs` but are not important in any
* validation path toward the root, and thus can be deleted.
*
* *Erasables* are msgs that precede `msgsIDs` and can be erased without
* losing a validation path toward the root.
* @param {Array<MsgID>} msgIDs
* @returns {{ deletables: Set<MsgID>, erasables: Set<MsgID> } | null}
*/
getDeletablesAndErasables(...msgIDs) {
// Determine erasables
const erasables = new Set()
const minimum = this.getMinimumAmong(msgIDs)
for (const msgID of minimum) {
const trail = this.shortestPathToRoot(msgID)
if (!trail) return null
for (const id of trail) {
erasables.add(id)
}
}
// Determine deletables
const deletables = new Set()
const sorted = this.topoSort()
for (const msgID of sorted) {
if (erasables.has(msgID)) continue
if (minimum.some((min) => this.precedes(msgID, min))) {
deletables.add(msgID)
}
}
return { deletables, erasables }
}
/**
* @param {Array<string>=} minSet
* @param {Array<string>=} maxSet
* @param {CB<Array<Msg>>=} cb
* @return {Promise<Array<Msg>>|void}
*/
slice(minSet = [], maxSet = [], cb) {
// @ts-ignore
if (cb === undefined) return p(this.slice).bind(this)(minSet, maxSet)
const minSetGood = minSet.filter((msgID) => this.has(msgID))
const maxSetGood = maxSet.filter((msgID) => this.has(msgID))
const minSetTight = this.getMinimumAmong(minSetGood)
const trail = new Set()
for (const msgID of minSetTight) {
const path = this.shortestPathToRoot(msgID)
if (!path) return cb(Error("Couldn't get shortest path to root when slicing dbtangle"))
for (const msgID of path) {
trail.add(msgID)
}
}
const msgs = /**@type {Array<Msg>}*/ ([])
pull(
pull.values(this.topoSort()),
pull.asyncMap((msgID, cb) => {
this.#getMsg(msgID, (err, msg) => {
if (err) return cb(err)
cb(null, { id: msgID, msg })
})
}),
pull.drain(
(rec) => {
if (trail.has(rec.id)) {
if (rec.msg) msgs.push({ ...rec.msg, data: null })
}
const isMin = minSetGood.includes(rec.id)
const isMax = maxSetGood.includes(rec.id)
const isBeforeMin = minSetGood.some((min) =>
this.precedes(rec.id, min)
)
const isAfterMax = maxSetGood.some((max) =>
this.precedes(max, rec.id)
)
if (!isMin && isBeforeMin) return
if (!isMax && isAfterMax) return
if (rec.msg) msgs.push(rec.msg)
},
(err) => {
if (err) return cb(Error('DBTangle.slice() failed', { cause: err }))
return cb(null, msgs)
}
)
)
}
}
module.exports = DBTangle

View File

@ -8,7 +8,6 @@ const pull = require('pull-stream')
const p = require('node:util').promisify
const Log = require('./log')
const MsgV4 = require('./msg-v4')
const DBTangle = require('./db-tangle')
const {
SIGNATURE_TAG_ACCOUNT_ADD,
ACCOUNT_SELF,
@ -73,7 +72,7 @@ const { decrypt } = require('./encryption')
/**
* @template T
* @typedef {[T] extends [void] ?
* @typedef {T extends void ?
* (...args: [Error] | []) => void :
* (...args: [Error] | [null, T]) => void
* } CB
@ -94,6 +93,128 @@ function assertValidConfig(config) {
}
}
class DBTangle extends MsgV4.Tangle {
/** @type {(msgID: MsgID, cb: CB<Msg>) => void} */
#getMsg
/**
* @param {MsgID} rootID
* @param {(msgID: MsgID, cb: CB<Msg>) => void} getMsg
*/
constructor(rootID, getMsg) {
super(rootID)
this.#getMsg = getMsg
}
/**
* @param {MsgID} rootID
* @param {AsyncIterable<Rec>} recordsIter
* @param {(msgID: MsgID, cb: any) => void} getMsg
* @return {Promise<DBTangle>}
*/
static async init(rootID, recordsIter, getMsg) {
const dbtangle = new DBTangle(rootID, getMsg)
for await (const rec of recordsIter) {
if (!rec.msg) continue
dbtangle.add(rec.id, rec.msg)
}
return dbtangle
}
/**
* Given a set of msgs (`msgIDs`) in this tangle, find all "deletable" and
* "erasable" msgs that precede that set.
*
* *Deletables* are msgs that precede `msgsIDs` but are not important in any
* validation path toward the root, and thus can be deleted.
*
* *Erasables* are msgs that precede `msgsIDs` and can be erased without
* losing a validation path toward the root.
* @param {Array<MsgID>} msgIDs
* @returns {{ deletables: Set<MsgID>, erasables: Set<MsgID> }}
*/
getDeletablesAndErasables(...msgIDs) {
// Determine erasables
const erasables = new Set()
const minimum = this.getMinimumAmong(msgIDs)
for (const msgID of minimum) {
const trail = this.shortestPathToRoot(msgID)
for (const id of trail) {
erasables.add(id)
}
}
// Determine deletables
const deletables = new Set()
const sorted = this.topoSort()
for (const msgID of sorted) {
if (erasables.has(msgID)) continue
if (minimum.some((min) => this.precedes(msgID, min))) {
deletables.add(msgID)
}
}
return { deletables, erasables }
}
/**
* @param {Array<string>=} minSet
* @param {Array<string>=} maxSet
* @param {CB<Array<Msg>>=} cb
* @return {Promise<Array<Msg>>|void}
*/
slice(minSet = [], maxSet = [], cb) {
// @ts-ignore
if (cb === undefined) return p(this.slice).bind(this)(minSet, maxSet)
const minSetGood = minSet.filter((msgID) => this.has(msgID))
const maxSetGood = maxSet.filter((msgID) => this.has(msgID))
const minSetTight = this.getMinimumAmong(minSetGood)
const trail = new Set()
for (const msgID of minSetTight) {
const path = this.shortestPathToRoot(msgID)
for (const msgID of path) {
trail.add(msgID)
}
}
const msgs = /**@type {Array<Msg>}*/ ([])
pull(
pull.values(this.topoSort()),
pull.asyncMap((msgID, cb) => {
this.#getMsg(msgID, (err, msg) => {
if (err) return cb(err)
cb(null, { id: msgID, msg })
})
}),
pull.drain(
(rec) => {
if (trail.has(rec.id)) {
if (rec.msg) msgs.push({ ...rec.msg, data: null })
}
const isMin = minSetGood.includes(rec.id)
const isMax = maxSetGood.includes(rec.id)
const isBeforeMin = minSetGood.some((min) =>
this.precedes(rec.id, min)
)
const isAfterMax = maxSetGood.some((max) =>
this.precedes(max, rec.id)
)
if (!isMin && isBeforeMin) return
if (!isMax && isAfterMax) return
if (rec.msg) msgs.push(rec.msg)
},
(err) => {
if (err) return cb(Error('DBTangle.slice() failed', { cause: err }))
return cb(null, msgs)
}
)
)
}
}
/**
* @param {Peer} peer
* @param {Config} config
@ -300,7 +421,7 @@ function initDB(peer, config) {
/**
* @param {Pick<RecPresent, 'id' | 'msg'>} rec
* @param {CB<DBTangle | null>} cb
* @param {(err: Error | null, tangle: DBTangle | null) => void} cb
*/
function getAccountTangle(rec, cb) {
const accountID = getAccountID(rec)
@ -311,7 +432,8 @@ function initDB(peer, config) {
}
if (!accountTangle.has(accountID)) {
return cb(
Error(`Account tangle "${accountID}" is locally unknown`)
Error(`Account tangle "${accountID}" is locally unknown`),
null
)
}
return cb(null, accountTangle)
@ -361,7 +483,7 @@ function initDB(peer, config) {
}
/**
* @param {CB<void> | void} cb
* @param {CB<void>} cb
*/
function loaded(cb) {
if (cb === void 0) return promisify(loaded)()
@ -377,7 +499,7 @@ function initDB(peer, config) {
*
* @param {Pick<RecPresent, 'id' | 'msg'>} rec
* @param {MsgID} tangleID
* @param {CB<void>} cb
* @param {(err: Error | null, val: null) => void} cb
*/
function verifyRec(rec, tangleID, cb) {
let err
@ -393,35 +515,34 @@ function initDB(peer, config) {
if (
(err = MsgV4.validate(rec.msg, tangle, sigkeys, rec.id, tangleID))
) {
return cb(Error('Invalid msg', { cause: err }))
return cb(Error('Invalid msg', { cause: err }), null)
}
return cb()
return cb(null, null)
}
// Identify the account and its sigkeys:
getAccountTangle(rec, (err, accountTangle) => {
// prettier-ignore
if (err) return cb(Error('Unknown account tangle owning this msg', { cause: err }))
if (err) return cb(Error('Unknown account tangle owning this msg', { cause: err }), null)
getSigkeysInAccount(
accountTangle,
rec.msg.metadata.accountTips,
(err, sigkeys) => {
if (err) return cb(err)
// TODO: how is sigkeys able to be undefined here? typechecker why are you weird?
if (!sigkeys) return cb(Error("Sigkeys missing somehow"))
if (err) return cb(err, null)
// Don't accept ghosts to come back, unless they are trail msgs
if (!!rec.msg.data && ghosts.read(tangleID).has(rec.id)) {
return cb(Error('Refusing a ghost msg to come back'))
return cb(Error('Refusing a ghost msg to come back'), null)
}
const valErr = MsgV4.validate(rec.msg, tangle, sigkeys, rec.id, tangleID)
if (valErr) {
return cb(Error('Invalid msg', { cause: valErr }))
if (
(err = MsgV4.validate(rec.msg, tangle, sigkeys, rec.id, tangleID))
) {
return cb(Error('Invalid msg', { cause: err }), null)
}
/** @param {CB<void>} cb */
/** @param {(err: Error | null, val: null) => void} cb */
function verifyInner(cb) {
// Unwrap encrypted inner msg and verify it too
if (typeof rec.msg.data === 'string') {
@ -433,15 +554,15 @@ function initDB(peer, config) {
verifyRec(innerRec, innerMsgID, (err) => {
// prettier-ignore
if (err) return cb(Error('Failed to verify inner msg', { cause: err }))
if (err) return cb(Error('Failed to verify inner msg', { cause: err }), null)
return cb()
return cb(null, null)
})
} else {
return cb()
return cb(null, null)
}
} else {
return cb()
return cb(null, null)
}
}
@ -450,7 +571,7 @@ function initDB(peer, config) {
const validAccountTangle = /** @type {Tangle} */ (accountTangle)
validateAccountMsg(rec.msg, validAccountTangle, (err) => {
if (err)
return cb(Error('Invalid account msg', { cause: err }))
return cb(Error('Invalid account msg', { cause: err }), null)
return verifyInner(cb)
})
} else {
@ -556,7 +677,7 @@ function initDB(peer, config) {
/**
* @param {Msg} msg
* @param {Tangle} accountTangle
* @param {CB<void>} cb
* @param {(err: Error | null, val: null) => void} cb
*/
function validateAccountMsg(msg, accountTangle, cb) {
if (!MsgV4.isRoot(msg)) {
@ -569,21 +690,21 @@ function initDB(peer, config) {
public: msg.sigkey,
}
getAccountPowers(accountTangle, keypair, (err, powers) => {
if (err) return cb(err)
if (err) return cb(err, null)
if (!powers.has('add')) {
// prettier-ignore
return cb(Error(`invalid account msg: sigkey "${msg.sigkey}" does not have "add" power`))
return cb(Error(`invalid account msg: sigkey "${msg.sigkey}" does not have "add" power`), null)
}
return cb()
return cb(null, null)
})
} else {
return cb()
return cb(null, null)
}
// TODO validate 'del'
} else {
return cb()
return cb(null, null)
}
}
@ -679,7 +800,7 @@ function initDB(peer, config) {
* keypair?: KeypairPublicSlice;
* account: string;
* }} opts
* @param {CB<boolean>} cb
* @param {(err: Error | null, has: boolean | null) => void} cb
*/
function accountHas(opts, cb) {
const keypair = opts?.keypair ?? config.global.keypair
@ -690,7 +811,7 @@ function initDB(peer, config) {
pull.asyncMap((msgID, cb) => {
get(msgID, (err, msg) => {
// prettier-ignore
if (err) return cb(Error("db.account.has() failed to get() account tangle message", { cause: err }))
if (err) return cb(Error("db.account.has() failed to get() account tangle message", { cause: err }), null)
if (!msg?.data) return cb(null, false)
/** @type {AccountData} */
@ -705,7 +826,7 @@ function initDB(peer, config) {
}),
pull.collect((err, results) => {
// prettier-ignore
if (err) return cb(Error('db.account.has() failed to calculate', { cause: err }))
if (err) return cb(Error('db.account.has() failed to calculate', { cause: err }), null)
return cb(
null,
@ -776,6 +897,7 @@ function initDB(peer, config) {
})
}
//* @param {(err: Error | null, val: Set<any> | null) => void} cb
/**
* @param {Tangle} accountTangle
* @param {KeypairPublicSlice} keypair
@ -1164,7 +1286,7 @@ function initDB(peer, config) {
/**
* @param {string} accountId
* @param {string} domain
* @param {CB<RecPresent | null>} cb
* @param {(err: Error | null, rec: RecPresent | null) => void} cb
*/
function findMoot(accountId, domain, cb) {
const findAccount = MsgV4.stripAccount(accountId)
@ -1181,7 +1303,7 @@ function initDB(peer, config) {
/**
* @param {MsgID} msgID
* @param {CB<RecPresent | null>} cb
* @param {(err: Error | null, rec: RecPresent | null) => void} cb
*/
function getRecord(msgID, cb) {
// TODO: improve performance of this when getting many messages, the arg
@ -1200,7 +1322,7 @@ function initDB(peer, config) {
/**
* @param {MsgID} msgID
* @param {CB<Msg | undefined>} cb
* @param {(err: Error | null, msg?: Msg) => void} cb
*/
function get(msgID, cb) {
getRecord(msgID, (err, rec) => {
@ -1344,7 +1466,7 @@ function initDB(peer, config) {
/**
* @param {MsgID} tangleID
* @param {CB<DBTangle | null>} cb
* @param {(err: Error | null, tangle: DBTangle | null) => void} cb
*/
function getTangle(tangleID, cb) {
DBTangle.init(tangleID, records(), get).then((tangle) => {

View File

@ -222,12 +222,11 @@ class Tangle {
}
/**
* @returns {'feed' | 'account' | 'weave' | null}
* @returns {'feed' | 'account' | 'weave'}
*/
get type() {
if (!this.#rootMsg) {
console.trace(new Error(`Tangle "${this.#rootID}" is missing root message`))
return null
throw new Error(`Tangle "${this.#rootID}" is missing root message`)
}
if (this.#isFeed()) return 'feed'
if (this.#rootMsg.metadata.account === 'self') return 'account'
@ -236,8 +235,7 @@ class Tangle {
get root() {
if (!this.#rootMsg) {
console.trace(new Error(`Tangle "${this.#rootID}" is missing root message`))
return null
throw new Error(`Tangle "${this.#rootID}" is missing root message`)
}
return this.#rootMsg
}
@ -258,8 +256,7 @@ class Tangle {
if (!prev) break
if (prev === lastPrev) {
// prettier-ignore
console.trace(new Error(`Tangle "${this.#rootID}" has a cycle or lacking a trail to root`))
return null
throw new Error(`Tangle "${this.#rootID}" has a cycle or lacking a trail to root`)
} else {
lastPrev = prev
}

View File

@ -107,8 +107,6 @@ function validateSigkeyAndAccount(msg, tangle, sigkeys) {
// prettier-ignore
return `invalid msg: accountTips "${msg.metadata.accountTips}" should have been null in an account tangle\n` + JSON.stringify(msg)
}
} else if (tangle.type === null) {
return "Cannot validate tangle of unknown type"
}
return undefined
}
@ -201,8 +199,6 @@ function validateTangle(msg, tangle, tangleID) {
// prettier-ignore
return `invalid msg: account "${msg.metadata.account}" should have been feed account "${account}"\n` + JSON.stringify(msg)
}
} else if (tangle.type === null) {
return "Unknown tangle type"
}
let lastPrev = null
let minDiff = Infinity
@ -346,8 +342,11 @@ function validate(msg, tangle, sigkeys, msgID, rootID) {
if ((err = validateSigkey(msg))) return err
if ((err = validateData(msg))) return err
if (tangle.type === 'feed' && isMoot(msg)) return // nothing else to check
if (tangle.type === null) return "Missing tangle type when validating msg"
try {
if (tangle.type === 'feed' && isMoot(msg)) return // nothing else to check
} catch (/** @type {any} */ err) {
return err
}
if ((err = validateDataSizeHash(msg))) return err
if ((err = validateDomain(msg.metadata.domain))) return err
@ -358,7 +357,6 @@ function validate(msg, tangle, sigkeys, msgID, rootID) {
if ((err = validateTangle(msg, tangle, rootID))) return err
}
if ((err = validateSignature(msg))) return err
return undefined
}
module.exports = {

View File

@ -1,6 +1,6 @@
{
"name": "pzp-db",
"version": "1.0.4",
"version": "1.0.1",
"description": "Default PZP database",
"homepage": "https://codeberg.org/pzp/pzp-db",
"repository": {
@ -24,9 +24,6 @@
},
"./msg-v4": {
"require": "./lib/msg-v4/index.js"
},
"./db-tangle": {
"require": "./lib/db-tangle.js"
}
},
"dependencies": {