From 544ca09e9c0856bad121313095f0405670f128ba Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Fri, 7 Apr 2023 17:27:49 +0300 Subject: [PATCH] multi-tangle msgs --- lib/feed-v1/index.js | 73 +++++++++++++++++++++++++++---- lib/feed-v1/validation.js | 9 ++++ test/feed-v1-invalid-prev.test.js | 32 +++----------- test/feed-v1-tangles.test.js | 39 +++++++++++++++++ 4 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 test/feed-v1-tangles.test.js diff --git a/lib/feed-v1/index.js b/lib/feed-v1/index.js index 5115c0e..70b2cc0 100644 --- a/lib/feed-v1/index.js +++ b/lib/feed-v1/index.js @@ -15,8 +15,19 @@ const { validateOOO, validateBatch, validateOOOBatch, + validateMsgHash, } = require('./validation') +/** + * @typedef {Iterator & {values: () => Iterator}} MsgIter + */ + +/** + * @typedef {Object} TangleData + * @property {number} depth + * @property {Array} prev + */ + /** * @typedef {Object} Msg * @property {*} content @@ -25,6 +36,7 @@ const { * @property {Array} metadata.prev * @property {string} metadata.proof * @property {number} metadata.size + * @property {Record=} metadata.tangles * @property {string=} metadata.type * @property {string} metadata.who * @property {number=} metadata.when @@ -39,7 +51,8 @@ const { * @property {Object} keys * @property {string} keys.id * @property {string} keys.private - * @property {Iterator & {values: () => Iterator}} existing + * @property {MsgIter} existing + * @property {Record=} tangles */ /** @@ -122,19 +135,45 @@ function calculatePrev(existing, depth, lipmaaDepth) { return prev } -function prevalidatePrevious(prev, name) { - if (!prev?.[Symbol.iterator]) { +function prevalidateExisting(existing, tangleId = null) { + if (!existing?.[Symbol.iterator]) { // 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 - 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) { - 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) { let 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 depth = calculateDepth(opts.existing) const lipmaaDepth = lipmaa(depth + 1) - 1 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 = { content: opts.content, metadata: { @@ -158,6 +212,7 @@ function create(opts) { prev, proof, size, + ...(tangles ? { tangles } : null), type: opts.type, who: stripAuthor(opts.keys.id), when: +opts.when, diff --git a/lib/feed-v1/validation.js b/lib/feed-v1/validation.js index 7ca143e..5da915c 100644 --- a/lib/feed-v1/validation.js +++ b/lib/feed-v1/validation.js @@ -41,6 +41,14 @@ function validateWho(msg) { // 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) { const { sig } = msg if (typeof sig !== 'string') { @@ -224,6 +232,7 @@ module.exports = { validateContent, validate, + validateMsgHash, // validateBatch, // validateOOO, // validateOOOBatch, diff --git a/test/feed-v1-invalid-prev.test.js b/test/feed-v1-invalid-prev.test.js index 523bf75..8ab9b08 100644 --- a/test/feed-v1-invalid-prev.test.js +++ b/test/feed-v1-invalid-prev.test.js @@ -3,28 +3,6 @@ const base58 = require('bs58') const FeedV1 = require('../lib/feed-v1') 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) => { const keys = generateKeypair('alice') @@ -60,7 +38,7 @@ tape('invalid msg with non-array prev', (t) => { keys, content: { text: 'Hello world!' }, type: 'post', - existing: new Map([['1234', { metadata: { depth: 10 }, sig: 'fake' }]]), + existing: new Map(), when: 1652030002000, }) msg2.metadata.prev = null @@ -71,7 +49,7 @@ tape('invalid msg with non-array prev', (t) => { t.ok(err, 'invalid 2nd msg throws') t.match( err.message, - /prev must be an iterator/, + /prev must be an array/, 'invalid 2nd msg description' ) t.end() @@ -94,9 +72,10 @@ tape('invalid msg with bad prev', (t) => { keys, content: { text: 'Hello world!' }, type: 'post', - existing: new Map([['1234', { metadata: { depth: 10 }, sig: 'fake' }]]), + existing: new Map(), when: 1652030002000, }) + msg2.metadata.depth = 1 msg2.metadata.prev = [1234] const existing = new Map() @@ -128,11 +107,12 @@ tape('invalid msg with URI in prev', (t) => { keys, content: { text: 'Hello world!' }, type: 'post', - existing: new Map([['1234', { metadata: { depth: 10 }, sig: 'fake' }]]), + existing: new Map(), when: 1652030002000, }) const randBuf = Buffer.alloc(16).fill(16) const fakeMsgKey1 = `ppppp:message/v1/${base58.encode(randBuf)}` + msg2.metadata.depth = 1 msg2.metadata.prev = [fakeMsgKey1] const existing = new Map() diff --git a/test/feed-v1-tangles.test.js b/test/feed-v1-tangles.test.js new file mode 100644 index 0000000..f3509d4 --- /dev/null +++ b/test/feed-v1-tangles.test.js @@ -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() +})