multi-tangle msgs

This commit is contained in:
Andre Staltz 2023-04-07 17:27:49 +03:00
parent 94ba7246e5
commit 544ca09e9c
4 changed files with 118 additions and 35 deletions

View File

@ -15,8 +15,19 @@ const {
validateOOO, validateOOO,
validateBatch, validateBatch,
validateOOOBatch, validateOOOBatch,
validateMsgHash,
} = require('./validation') } = require('./validation')
/**
* @typedef {Iterator<Msg> & {values: () => Iterator<Msg>}} MsgIter
*/
/**
* @typedef {Object} TangleData
* @property {number} depth
* @property {Array<string>} prev
*/
/** /**
* @typedef {Object} Msg * @typedef {Object} Msg
* @property {*} content * @property {*} content
@ -25,6 +36,7 @@ const {
* @property {Array<string>} metadata.prev * @property {Array<string>} metadata.prev
* @property {string} metadata.proof * @property {string} metadata.proof
* @property {number} metadata.size * @property {number} metadata.size
* @property {Record<string, TangleData>=} metadata.tangles
* @property {string=} metadata.type * @property {string=} metadata.type
* @property {string} metadata.who * @property {string} metadata.who
* @property {number=} metadata.when * @property {number=} metadata.when
@ -39,7 +51,8 @@ const {
* @property {Object} keys * @property {Object} keys
* @property {string} keys.id * @property {string} keys.id
* @property {string} keys.private * @property {string} keys.private
* @property {Iterator<Msg> & {values: () => Iterator<Msg>}} existing * @property {MsgIter} existing
* @property {Record<string, MsgIter>=} tangles
*/ */
/** /**
@ -122,19 +135,45 @@ function calculatePrev(existing, depth, lipmaaDepth) {
return prev return prev
} }
function prevalidatePrevious(prev, name) { function prevalidateExisting(existing, tangleId = null) {
if (!prev?.[Symbol.iterator]) { if (!existing?.[Symbol.iterator]) {
// prettier-ignore // prettier-ignore
throw new Error(`opts.${name} must be an iterator, but got ${typeof prev}`) return new Error(`existing must be an iterator, but got ${typeof existing}`)
} }
if (typeof prev?.values !== 'function') { if (typeof existing?.values !== 'function') {
// prettier-ignore // prettier-ignore
throw new Error(`opts.${name} must be a Map, Set, or Array, but got ${prev}`) return new Error(`existing must be a Map, Set, or Array, but got ${existing}`)
} }
for (const p of prev.values()) { let isEmpty = true
let hasDepthZeroMsg = false
for (const p of existing.values()) {
isEmpty = false
if (!p.metadata) { if (!p.metadata) {
throw new Error(`opts.${name} must contain messages, but got ${typeof p}`) // prettier-ignore
return new Error(`existing must contain messages, but got ${typeof p}`)
} }
if (!tangleId && p.metadata.depth === 0) {
if (hasDepthZeroMsg) {
// prettier-ignore
return new Error(`existing must contain only 1 message with depth 0`)
} else {
hasDepthZeroMsg = true
}
} else if (tangleId) {
if (!p.metadata.tangles?.[tangleId] && getMsgHash(p) === tangleId) {
if (hasDepthZeroMsg) {
// prettier-ignore
return new Error(`existing must contain only 1 message with depth 0`)
} else {
hasDepthZeroMsg = true
}
}
}
}
if (!isEmpty && !hasDepthZeroMsg) {
// prettier-ignore
return new Error(`opts.existing must contain the message with depth 0`)
} }
} }
@ -145,12 +184,27 @@ function prevalidatePrevious(prev, name) {
function create(opts) { function create(opts) {
let err let err
if ((err = validateType(opts.type))) throw err if ((err = validateType(opts.type))) throw err
prevalidatePrevious(opts.existing, 'existing') if ((err = prevalidateExisting(opts.existing))) throw err
const [proof, size] = representContent(opts.content) const [proof, size] = representContent(opts.content)
const depth = calculateDepth(opts.existing) const depth = calculateDepth(opts.existing)
const lipmaaDepth = lipmaa(depth + 1) - 1 const lipmaaDepth = lipmaa(depth + 1) - 1
const prev = calculatePrev(opts.existing, depth, lipmaaDepth) const prev = calculatePrev(opts.existing, depth, lipmaaDepth)
let tangles = null
if (opts.tangles) {
for (const rootId in opts.tangles) {
if ((err = validateMsgHash(rootId))) throw err
const existing = opts.tangles[rootId]
if ((err = prevalidateExisting(existing, rootId))) throw err
const depth = calculateDepth(existing)
const lipmaaDepth = lipmaa(depth + 1) - 1
const prev = calculatePrev(existing, depth, lipmaaDepth)
tangles ??= {}
tangles[rootId] = { depth, prev }
}
}
const msg = { const msg = {
content: opts.content, content: opts.content,
metadata: { metadata: {
@ -158,6 +212,7 @@ function create(opts) {
prev, prev,
proof, proof,
size, size,
...(tangles ? { tangles } : null),
type: opts.type, type: opts.type,
who: stripAuthor(opts.keys.id), who: stripAuthor(opts.keys.id),
when: +opts.when, when: +opts.when,

View File

@ -41,6 +41,14 @@ function validateWho(msg) {
// FIXME: if there are prev, then `who` must match // FIXME: if there are prev, then `who` must match
} }
function validateMsgHash(str) {
try {
base58.decode(str)
} catch (err) {
return new Error(`invalid message: msgHash ${str} should have been a base58 string`)
}
}
function validateSignature(msg) { function validateSignature(msg) {
const { sig } = msg const { sig } = msg
if (typeof sig !== 'string') { if (typeof sig !== 'string') {
@ -224,6 +232,7 @@ module.exports = {
validateContent, validateContent,
validate, validate,
validateMsgHash,
// validateBatch, // validateBatch,
// validateOOO, // validateOOO,
// validateOOOBatch, // validateOOOBatch,

View File

@ -3,28 +3,6 @@ const base58 = require('bs58')
const FeedV1 = require('../lib/feed-v1') const FeedV1 = require('../lib/feed-v1')
const { generateKeypair } = require('./util') const { generateKeypair } = require('./util')
tape('invalid 1st msg with non-empty prev', (t) => {
const keys = generateKeypair('alice')
const msg = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
existing: new Map([['1234', { metadata: { depth: 10 }, sig: 'fake' }]]),
when: 1652030001000,
})
FeedV1.validate(msg, new Map(), (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/prev .+ is not locally known/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid 1st msg with non-array prev', (t) => { tape('invalid 1st msg with non-array prev', (t) => {
const keys = generateKeypair('alice') const keys = generateKeypair('alice')
@ -60,7 +38,7 @@ tape('invalid msg with non-array prev', (t) => {
keys, keys,
content: { text: 'Hello world!' }, content: { text: 'Hello world!' },
type: 'post', type: 'post',
existing: new Map([['1234', { metadata: { depth: 10 }, sig: 'fake' }]]), existing: new Map(),
when: 1652030002000, when: 1652030002000,
}) })
msg2.metadata.prev = null msg2.metadata.prev = null
@ -71,7 +49,7 @@ tape('invalid msg with non-array prev', (t) => {
t.ok(err, 'invalid 2nd msg throws') t.ok(err, 'invalid 2nd msg throws')
t.match( t.match(
err.message, err.message,
/prev must be an iterator/, /prev must be an array/,
'invalid 2nd msg description' 'invalid 2nd msg description'
) )
t.end() t.end()
@ -94,9 +72,10 @@ tape('invalid msg with bad prev', (t) => {
keys, keys,
content: { text: 'Hello world!' }, content: { text: 'Hello world!' },
type: 'post', type: 'post',
existing: new Map([['1234', { metadata: { depth: 10 }, sig: 'fake' }]]), existing: new Map(),
when: 1652030002000, when: 1652030002000,
}) })
msg2.metadata.depth = 1
msg2.metadata.prev = [1234] msg2.metadata.prev = [1234]
const existing = new Map() const existing = new Map()
@ -128,11 +107,12 @@ tape('invalid msg with URI in prev', (t) => {
keys, keys,
content: { text: 'Hello world!' }, content: { text: 'Hello world!' },
type: 'post', type: 'post',
existing: new Map([['1234', { metadata: { depth: 10 }, sig: 'fake' }]]), existing: new Map(),
when: 1652030002000, when: 1652030002000,
}) })
const randBuf = Buffer.alloc(16).fill(16) const randBuf = Buffer.alloc(16).fill(16)
const fakeMsgKey1 = `ppppp:message/v1/${base58.encode(randBuf)}` const fakeMsgKey1 = `ppppp:message/v1/${base58.encode(randBuf)}`
msg2.metadata.depth = 1
msg2.metadata.prev = [fakeMsgKey1] msg2.metadata.prev = [fakeMsgKey1]
const existing = new Map() const existing = new Map()

View File

@ -0,0 +1,39 @@
const tape = require('tape')
const FeedV1 = require('../lib/feed-v1')
const { generateKeypair } = require('./util')
tape('simple multi-author tangle', (t) => {
const keysA = generateKeypair('alice')
const keysB = generateKeypair('bob')
const msg1 = FeedV1.create({
keys: keysA,
content: { text: 'Hello world!' },
type: 'post',
existing: new Map(),
when: 1652030001000,
})
const msgHash1 = FeedV1.getMsgHash(msg1)
t.notOk(msg1.metadata.tangles, 'msg1 has no extra tangles')
const msg2 = FeedV1.create({
keys: keysB,
content: { text: 'Hello world!' },
type: 'post',
existing: new Map(),
tangles: {
[msgHash1]: new Map([[msgHash1, msg1]]),
},
when: 1652030002000,
})
t.ok(msg2.metadata.tangles, 'msg2 has extra tangles')
t.ok(msg2.metadata.tangles[msgHash1], 'msg2 has tangle for msgHash1')
t.equal(msg2.metadata.tangles[msgHash1].depth, 1, 'msg2 has tangle depth 1')
t.deepEquals(
msg2.metadata.tangles[msgHash1].prev,
[msgHash1],
'msg2 has tangle prev'
)
t.end()
})