From f1a13eee80828abe4f161d3c0caa6c59d2891a2f Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Mon, 28 Aug 2023 16:57:25 +0300 Subject: [PATCH] refactor validation --- lib/index.js | 53 ++++++++++++++++++-------- lib/msg-v3/tangle.js | 16 +++++++- lib/msg-v3/validation.js | 73 ++++++++++++++++++++++-------------- test/msg-v3/validate.test.js | 2 + 4 files changed, 98 insertions(+), 46 deletions(-) diff --git a/lib/index.js b/lib/index.js index 76f5a54..1ec88a1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -255,6 +255,30 @@ function initDB(peer, config) { return tangles } + /** + * Find which pubkeys are authorized to sign this msg given the account. + * + * @private + * @param {DBTangle | null} accountTangle + * @returns {Set} + */ + function getPubkeysInAccount(accountTangle) { + const pubkeys = new Set() + if (!accountTangle) return pubkeys + // TODO: prune the accountTangle beyond msg.metadata.accountTips + for (const msgID of accountTangle.topoSort()) { + const msg = get(msgID) + if (!msg?.data) continue + /** @type {AccountData} */ + const data = msg.data + if (data.action !== 'add') continue + if (data.add.key.purpose !== 'sig') continue + if (data.add.key.algorithm !== 'ed25519') continue + pubkeys.add(data.add.key.bytes) + } + return pubkeys + } + /** * @param {CB} cb */ @@ -280,29 +304,24 @@ function initDB(peer, config) { // TODO: optimize this. This may be slow if you're adding many msgs in a // row, because it creates a new Map() each time. Perhaps with QuickLRU const tangle = new DBTangle(tangleID, records()) + if (msgID === tangleID) { + tangle.add(msgID, msg) + } - // Find which pubkeys are authorized to sign this msg given the account: - const pubkeys = new Set() + // Identify the account and its pubkeys: const accountID = getAccountID(rec) let accountTangle = /** @type {DBTangle | null} */ (null) - if (accountID && !MsgV3.isRoot(msg)) { + if (accountID) { accountTangle = new DBTangle(accountID, records()) + if (msgID === accountID) { + accountTangle.add(msgID, msg) + } if (!accountTangle.has(accountID)) { // prettier-ignore return cb(new Error('add() failed because the account tangle is unknown')) } - // TODO: prune the accountTangle beyond msg.metadata.accountTips - for (const msgID of accountTangle.topoSort()) { - const msg = get(msgID) - if (!msg?.data) continue - /** @type {AccountData} */ - const data = msg.data - if (data.action !== 'add') continue - if (data.add.key.purpose !== 'sig') continue - if (data.add.key.algorithm !== 'ed25519') continue - pubkeys.add(data.add.key.bytes) - } } + const pubkeys = getPubkeysInAccount(accountTangle) let err if ((err = MsgV3.validate(msg, tangle, pubkeys, msgID, tangleID))) { @@ -310,7 +329,11 @@ function initDB(peer, config) { } // Account tangle related validations - if (msg.metadata.account === ACCOUNT_SELF && accountTangle) { + if ( + msg.metadata.account === ACCOUNT_SELF && + accountTangle && + !MsgV3.isRoot(msg) + ) { /** @type {AccountData} */ const data = msg.data if (data.action === 'add') { diff --git a/lib/msg-v3/tangle.js b/lib/msg-v3/tangle.js index 7784388..1a6e88d 100644 --- a/lib/msg-v3/tangle.js +++ b/lib/msg-v3/tangle.js @@ -188,7 +188,7 @@ class Tangle { return this.#depth.get(msgID) ?? -1 } - isFeed() { + #isFeed() { if (!this.#rootMsg) { console.trace('Tangle is missing root message') return false @@ -197,7 +197,7 @@ class Tangle { } get mootDetails() { - if (!this.isFeed()) return null + if (!this.#isFeed()) return null if (!this.#rootMsg) { console.trace('Tangle is missing root message') return null @@ -206,6 +206,18 @@ class Tangle { return { account, domain, id: this.#rootID } } + /** + * @returns {'feed' | 'account' | 'weave'} + */ + get type() { + if (!this.#rootMsg) { + throw new Error('Tangle is missing root message') + } + if (this.#isFeed()) return 'feed' + if (this.#rootMsg.metadata.account === 'self') return 'account' + return 'weave' + } + /** * @param {string} msgID */ diff --git a/lib/msg-v3/validation.js b/lib/msg-v3/validation.js index c37d50a..74f1149 100644 --- a/lib/msg-v3/validation.js +++ b/lib/msg-v3/validation.js @@ -6,10 +6,15 @@ const stringify = require('json-canon') const Tangle = require('./tangle') const representData = require('./represent-data') const isMoot = require('./is-moot') -const { SIGNATURE_TAG_MSG_V3, ACCOUNT_SELF } = require('./constants') +const { + SIGNATURE_TAG_MSG_V3, + ACCOUNT_SELF, + ACCOUNT_ANY, +} = require('./constants') /** * @typedef {import('.').Msg} Msg + * @typedef {import('.').AccountData} AccountData */ /** @@ -80,20 +85,29 @@ function validatePubkey(msg) { /** * * @param {Msg} msg + * @param {Tangle} tangle * @param {Set} pubkeys * @returns {string | undefined} */ -function validateAccountPubkey(msg, pubkeys) { - // Unusual case: if the msg is a feed root, ignore the account and pubkey - if (isMoot(msg)) return - - if ( - msg.metadata.account && - msg.metadata.account !== ACCOUNT_SELF && - !pubkeys.has(msg.pubkey) - ) { - // prettier-ignore - return `invalid message: pubkey "${msg.pubkey}" should have been one of "${[...pubkeys]}" from the account "${msg.metadata.account}"\n` + JSON.stringify(msg) +function validatePubkeyAndAccount(msg, tangle, pubkeys) { + if (tangle.type === 'feed' || tangle.type === 'weave') { + if (msg.metadata.account === ACCOUNT_SELF) { + // prettier-ignore + return `invalid message: account "${msg.metadata.account}" cannot be "self" in a feed tangle\n` + JSON.stringify(msg) + } + if (msg.metadata.account !== ACCOUNT_ANY && !pubkeys.has(msg.pubkey)) { + // prettier-ignore + return `invalid message: pubkey "${msg.pubkey}" should have been one of "${[...pubkeys]}" from the account "${msg.metadata.account}"\n` + JSON.stringify(msg) + } + } else if (tangle.type === 'account') { + if (msg.metadata.account !== ACCOUNT_SELF) { + // prettier-ignore + return `invalid message: account "${msg.metadata.account}" should have been "self" in an account tangle\n` + JSON.stringify(msg) + } + if (msg.metadata.accountTips !== null) { + // prettier-ignore + return `invalid message: accountTips "${msg.metadata.accountTips}" should have been null in an account tangle\n` + JSON.stringify(msg) + } } } @@ -113,18 +127,6 @@ function validateMsgID(str) { } } -/** - * @param {Msg} msg - * @returns {string | undefined} - */ -function validateDataSize(msg) { - const { dataSize } = msg.metadata - if (!Number.isSafeInteger(dataSize) || dataSize < 0) { - // prettier-ignore - return `invalid message: dataSize ${dataSize} should have been an unsigned integer\n` + JSON.stringify(msg) - } -} - /** * @param {Msg} msg * @returns {string | undefined} @@ -184,7 +186,7 @@ function validateTangle(msg, tangle, tangleID) { // prettier-ignore return `invalid message: depth "${depth}" should have been a positive integer\n` + JSON.stringify(msg) } - if (tangle.isFeed()) { + if (tangle.type === 'feed') { const { account, domain } = /** @type {MootDetails} */ (tangle.mootDetails) if (domain !== msg.metadata.domain) { // prettier-ignore @@ -302,7 +304,17 @@ function validateData(msg) { JSON.stringify(msg) ) } - const [dataHash, dataSize] = representData(data) +} + +/** + * @param {Msg} msg + */ +function validateDataSizeHash(msg) { + const [dataHash, dataSize] = representData(msg.data) + if (!Number.isSafeInteger(dataSize) || dataSize < 0) { + // prettier-ignore + return `invalid message: dataSize ${dataSize} should have been an unsigned integer\n` + JSON.stringify(msg) + } if (dataHash !== msg.metadata.dataHash) { // prettier-ignore return `invalid message: data hash "${dataHash}" does not match metadata.dataHash "${msg.metadata.dataHash}"\n` + JSON.stringify(msg) @@ -324,11 +336,14 @@ function validate(msg, tangle, pubkeys, msgID, rootID) { let err if ((err = validateShape(msg))) return err if ((err = validatePubkey(msg))) return err - if ((err = validateDataSize(msg))) return err if ((err = validateData(msg))) return err + + if (tangle.type === 'feed' && isMoot(msg)) return // nothing else to check + + if ((err = validateDataSizeHash(msg))) return err if ((err = validateDomain(msg.metadata.domain))) return err - if ((err = validateAccountPubkey(msg, pubkeys))) return err - if (tangle.size === 0) { + if ((err = validatePubkeyAndAccount(msg, tangle, pubkeys))) return err + if (msgID === rootID) { if ((err = validateTangleRoot(msg, msgID, rootID))) return err } else { if ((err = validateTangle(msg, tangle, rootID))) return err diff --git a/test/msg-v3/validate.test.js b/test/msg-v3/validate.test.js index b14607e..c64ec9b 100644 --- a/test/msg-v3/validate.test.js +++ b/test/msg-v3/validate.test.js @@ -13,6 +13,7 @@ test('validate root msg', (t) => { const moot = MsgV3.createMoot(account, 'post', keypair) const mootID = MsgV3.getMsgID(moot) const tangle = new MsgV3.Tangle(mootID) + tangle.add(mootID, moot) const err = MsgV3.validate(moot, tangle, pubkeys, mootID, mootID) assert.ifError(err, 'valid root msg') @@ -28,6 +29,7 @@ test('validate account tangle', (t) => { const accountMsg0ID = account const tangle = new MsgV3.Tangle(account) + tangle.add(accountMsg0ID, accountMsg0) let err = MsgV3.validate( accountMsg0,