Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache CN SPFactory calls #4516

Merged
merged 4 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions creator-node/src/apiSigning.js → creator-node/src/apiSigning.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { getContentNodeInfoFromSpId } from './services/ContentNodeInfoManager'

const { libs } = require('@audius/sdk')
const LibsUtils = libs.Utils
const Web3 = require('web3')
const web3 = new Web3()
const { logger: genericLogger } = require('./logging')

/**
* Max age of signature in milliseconds
* Set to 5 minutes
*/
const MAX_SIGNATURE_AGE_MS = 300000

const generateSignature = (data, privateKey) => {
export const generateSignature = (data: any, privateKey: string) => {
// JSON stringify automatically removes white space given 1 param
const toSignStr = JSON.stringify(sortKeys(data))
const toSignHash = web3.utils.keccak256(toSignStr)
Expand All @@ -23,7 +26,10 @@ const generateSignature = (data, privateKey) => {
* @param {object} data
* @param {string} privateKey
*/
const generateTimestampAndSignature = (data, privateKey) => {
export const generateTimestampAndSignature = (
data: any,
privateKey: string
) => {
const timestamp = new Date().toISOString()
const toSignObj = { ...data, timestamp }
const signature = generateSignature(toSignObj, privateKey)
Expand All @@ -33,15 +39,15 @@ const generateTimestampAndSignature = (data, privateKey) => {

// Keeps track of a cached listen signature
// Two field object: { timestamp, signature }
let cachedListenSignature = null
let cachedListenSignature: { timestamp: string; signature: any } | null = null

/**
* Generates a signature for `data` if only the previous signature
* generated is invalid (expired). Otherwise returns an existing signature.
* @param {string} privateKey
* @returns {object} {signature, timestamp} signature data
*/
const generateListenTimestampAndSignature = (privateKey) => {
export const generateListenTimestampAndSignature = (privateKey: any) => {
if (cachedListenSignature) {
const signatureTimestamp = cachedListenSignature.timestamp
if (signatureHasExpired(signatureTimestamp)) {
Expand All @@ -67,7 +73,7 @@ const generateListenTimestampAndSignature = (privateKey) => {
* @param {*} signature signature generated with signed data
*/
// eslint-disable-next-line no-unused-vars
const recoverWallet = (data, signature) => {
export const recoverWallet = (data: any, signature: any) => {
const structuredData = JSON.stringify(sortKeys(data))
const hashedData = web3.utils.keccak256(structuredData)
const recoveredWallet = web3.eth.accounts.recover(hashedData, signature)
Expand All @@ -79,21 +85,21 @@ const recoverWallet = (data, signature) => {
* Returns boolean indicating if provided timestamp is older than maxTTL
* @param {string | number} signatureTimestamp unix timestamp string when signature was generated
*/
const signatureHasExpired = (
signatureTimestamp,
export const signatureHasExpired = (
signatureTimestamp: string | number,
maxTTL = MAX_SIGNATURE_AGE_MS
) => {
const signatureTimestampDate = new Date(signatureTimestamp)
const currentTimestampDate = new Date()
const signatureAge = currentTimestampDate - signatureTimestampDate
const signatureAge = +currentTimestampDate - +signatureTimestampDate

return signatureAge >= maxTTL
}

/**
* Recursively sorts keys of object or object array
*/
const sortKeys = (x) => {
export const sortKeys = (x: any): any => {
if (typeof x !== 'object' || !x) {
return x
}
Expand All @@ -105,7 +111,10 @@ const sortKeys = (x) => {
.reduce((o, k) => ({ ...o, [k]: sortKeys(x[k]) }), {})
}

const generateTimestampAndSignatureForSPVerification = (spID, privateKey) => {
export const generateTimestampAndSignatureForSPVerification = (
spID: number,
privateKey: string
) => {
return generateTimestampAndSignature({ spID }, privateKey)
}

Expand All @@ -118,11 +127,14 @@ const generateTimestampAndSignatureForSPVerification = (spID, privateKey) => {
* @param {string} data.reqTimestamp the timestamp from the request body
* @param {string} data.reqSignature the signature from the request body
*/
const verifyRequesterIsValidSP = async ({
audiusLibs,
export const verifyRequesterIsValidSP = async ({
spID,
reqTimestamp,
reqSignature
}: {
spID: number | string
reqTimestamp: string
reqSignature: string
}) => {
if (!reqTimestamp || !reqSignature) {
throw new Error(
Expand All @@ -132,19 +144,14 @@ const verifyRequesterIsValidSP = async ({

spID = validateSPId(spID)

const spRecordFromSPFactory =
await audiusLibs.ethContracts.ServiceProviderFactoryClient.getServiceEndpointInfo(
'content-node',
spID
)

let {
owner: ownerWalletFromSPFactory,
delegateOwnerWallet: delegateOwnerWalletFromSPFactory,
endpoint: nodeEndpointFromSPFactory
} = spRecordFromSPFactory
} = (await getContentNodeInfoFromSpId(spID, genericLogger)) || {}
SidSethi marked this conversation as resolved.
Show resolved Hide resolved

delegateOwnerWalletFromSPFactory =
delegateOwnerWalletFromSPFactory.toLowerCase()
delegateOwnerWalletFromSPFactory?.toLowerCase()

if (!ownerWalletFromSPFactory || !delegateOwnerWalletFromSPFactory) {
throw new Error(
Expand Down Expand Up @@ -192,18 +199,18 @@ const verifyRequesterIsValidSP = async ({
* @param {string} spID
* @returns a parsed spID
*/
function validateSPId(spID) {
export function validateSPId(spID: string | number): number {
if (!spID) {
throw new Error('Must provide all required query parameters: spID')
}

spID = parseInt(spID)
const spIDNum = parseInt(spID + '', 10)

if (isNaN(spID) || spID < 0) {
if (isNaN(spIDNum) || spIDNum < 0) {
throw new Error(`Provided spID is not a valid id. spID=${spID}`)
}

return spID
return spIDNum
}

module.exports = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ const respondToURSMRequestForSignature = async (
nodeEndpointFromSPFactory,
spID
} = await verifyRequesterIsValidSP({
audiusLibs,
spID,
reqTimestamp,
reqSignature
Expand Down
1 change: 0 additions & 1 deletion creator-node/src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,6 @@ async function ensureValidSPMiddleware(req, res, next) {
}

await verifyRequesterIsValidSP({
audiusLibs: req.app.get('audiusLibs'),
spID,
reqTimestamp: timestamp,
reqSignature: signature
Expand Down
4 changes: 1 addition & 3 deletions creator-node/src/reqLimiter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const config = require('./config.js')
const { logger } = require('./logging')
const RedisStore = require('rate-limit-redis')
const client = require('./redis.js')
const { verifyRequesterIsValidSP } = require('./apiSigning.js')
const { verifyRequesterIsValidSP } = require('./apiSigning')

let endpointRateLimits = {}
try {
Expand Down Expand Up @@ -64,7 +64,6 @@ const userReqLimiter = rateLimit({
) {
try {
await verifyRequesterIsValidSP({
audiusLibs: libs,
spID,
reqTimestamp: timestamp,
reqSignature: signature
Expand Down Expand Up @@ -159,7 +158,6 @@ const batchCidsExistReqLimiter = rateLimit({
) {
try {
await verifyRequesterIsValidSP({
audiusLibs: libs,
spID,
reqTimestamp: timestamp,
reqSignature: signature
Expand Down
47 changes: 34 additions & 13 deletions creator-node/test/apiSigning.test.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
const assert = require('assert')

const { getLibsMock } = require('./lib/libsMock')

const apiSigning = require('../src/apiSigning')
const proxyquire = require('proxyquire')

describe('test apiSigning', function () {
let libsMock
beforeEach(async () => {
libsMock = getLibsMock()
const getContentNodeInfoFromSpId = async (spID, _genericLogger) => {
switch (spID) {
case 2:
return {
endpoint: 'http://mock-cn2.audius.co',
owner: '0xBdb47ebFF0eAe1A7647D029450C05666e22864Fb',
delegateOwnerWallet: '0xBdb47ebFF0eAe1A7647D029450C05666e22864Fb'
}
case 3:
return {
endpoint: 'http://mock-cn3.audius.co',
owner: '0x1Fffaa556B42f4506cdb01D7BbE6a9bDbb0E5f36',
delegateOwnerWallet: '0x1Fffaa556B42f4506cdb01D7BbE6a9bDbb0E5f36'
}

case 1:
return {
endpoint: 'http://mock-cn1.audius.co',
owner: '0x1eC723075E67a1a2B6969dC5CfF0C6793cb36D25',
delegateOwnerWallet: '0x1eC723075E67a1a2B6969dC5CfF0C6793cb36D25'
}
default:
return {
owner: '0x0000000000000000000000000000000000000000',
endpoint: '',
delegateOwnerWallet: '0x0000000000000000000000000000000000000000'
}
}
}
const apiSigning = proxyquire('../src/apiSigning.ts', {
'./services/ContentNodeInfoManager': {
getContentNodeInfoFromSpId
}
})

it('given incomplete timestamp, signature, throw error', async function () {
try {
await apiSigning.verifyRequesterIsValidSP({
audiusLibs: libsMock,
spID: 1,
reqTimestamp: undefined,
reqSignature: undefined
Expand All @@ -31,7 +57,6 @@ describe('test apiSigning', function () {
it('given bad spID, throw error', async function () {
try {
await apiSigning.verifyRequesterIsValidSP({
audiusLibs: libsMock,
spID: undefined,
reqTimestamp: '022-03-25T15:02:35.573Z',
reqSignature:
Expand All @@ -48,7 +73,6 @@ describe('test apiSigning', function () {

try {
await apiSigning.verifyRequesterIsValidSP({
audiusLibs: libsMock,
spID: -1,
reqTimestamp: '022-03-25T15:02:35.573Z',
reqSignature:
Expand All @@ -63,7 +87,6 @@ describe('test apiSigning', function () {
it('if the wallets are zero addressed, throw error', async function () {
try {
await apiSigning.verifyRequesterIsValidSP({
audiusLibs: libsMock,
spID: 100,
reqTimestamp: '022-03-25T15:02:35.573Z',
reqSignature:
Expand All @@ -80,7 +103,6 @@ describe('test apiSigning', function () {
it('if the api signing is mismatched, throw error', async function () {
try {
await apiSigning.verifyRequesterIsValidSP({
audiusLibs: libsMock,
spID: 1,
reqTimestamp: '022-03-25T15:02:35.573Z',
reqSignature:
Expand All @@ -101,7 +123,6 @@ describe('test apiSigning', function () {
it('if inputs are valid, return proper return values', async function () {
try {
const resp = await apiSigning.verifyRequesterIsValidSP({
audiusLibs: libsMock,
spID: 1,
reqTimestamp: '2022-03-25T15:22:54.866Z',
reqSignature:
Expand Down
41 changes: 40 additions & 1 deletion creator-node/test/lib/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export async function getApp(
libsClient,
blacklistManager = BlacklistManager,
setMockFn = null,
spId = 1
spId = 1,
mockContentNodeInfoManager = false
) {
// we need to clear the cache that commonjs require builds, otherwise it uses old values for imports etc
// eg if you set a new env var, it doesn't propogate well unless you clear the cache for the config file as well
Expand Down Expand Up @@ -46,6 +47,44 @@ export async function getApp(
prometheusRegistry
}

// Update import to make ensureValidSPMiddleware pass
if (mockContentNodeInfoManager) {
const getContentNodeInfoFromSpId = async (spID, _genericLogger) => {
switch (spID) {
case 2:
return {
endpoint: 'http://mock-cn2.audius.co',
owner: '0xBdb47ebFF0eAe1A7647D029450C05666e22864Fb',
delegateOwnerWallet: '0xBdb47ebFF0eAe1A7647D029450C05666e22864Fb'
}
case 3:
return {
endpoint: 'http://mock-cn3.audius.co',
owner: '0x1Fffaa556B42f4506cdb01D7BbE6a9bDbb0E5f36',
delegateOwnerWallet: '0x1Fffaa556B42f4506cdb01D7BbE6a9bDbb0E5f36'
}

case 1:
return {
endpoint: 'http://mock-cn1.audius.co',
owner: '0x1eC723075E67a1a2B6969dC5CfF0C6793cb36D25',
delegateOwnerWallet: '0x1eC723075E67a1a2B6969dC5CfF0C6793cb36D25'
}
default:
return {
owner: '0x0000000000000000000000000000000000000000',
endpoint: '',
delegateOwnerWallet: '0x0000000000000000000000000000000000000000'
}
}
}
require.cache[
require.resolve('../../src/services/ContentNodeInfoManager')
] = {
exports: { getContentNodeInfoFromSpId }
}
}

// Update the import to be the mocked ServiceRegistry instance
require.cache[require.resolve('../../src/serviceRegistry')] = {
exports: { serviceRegistry: mockServiceRegistry }
Expand Down
3 changes: 2 additions & 1 deletion creator-node/test/transcodeAndSegment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ describe('test transcode_and_segment route', function () {
/* libsMock */ getLibsMock(),
BlacklistManager,
/* setMockFn */ null,
1 /* spId */
1 /* spId */,
true /* mockContentNodeInfoManager */
)

app = appInfo.app
Expand Down