-
Notifications
You must be signed in to change notification settings - Fork 106
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use delegate wallet to determine content node for listen tracking (#1314
) * Add isFromContentNode check * Add signed message to listen recording * Add signature params to listen recording * Clean up * Address comments * Add try catch * Clean up * Write out math
- Loading branch information
1 parent
20c0611
commit 0f60fff
Showing
10 changed files
with
319 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
const Web3 = require('web3') | ||
const web3 = new Web3() | ||
|
||
// TODO: This is copied from the same code path in content node | ||
// and should be standardized across this file as well as the method in libs | ||
// Do not modify this file without touching the other! | ||
|
||
/** | ||
* Max age of signature in milliseconds | ||
* Set to 5 minutes | ||
*/ | ||
const MAX_SIGNATURE_AGE_MS = 5 * 60 * 1000 | ||
|
||
/** | ||
* Generate the timestamp and signature for api signing | ||
* @param {object} data | ||
* @param {string} privateKey | ||
*/ | ||
const generateTimestampAndSignature = (data, privateKey) => { | ||
const timestamp = new Date().toISOString() | ||
const toSignObj = { ...data, timestamp } | ||
// JSON stringify automatically removes white space given 1 param | ||
const toSignStr = JSON.stringify(sortKeys(toSignObj)) | ||
const toSignHash = web3.utils.keccak256(toSignStr) | ||
const signedResponse = web3.eth.accounts.sign(toSignHash, privateKey) | ||
|
||
return { timestamp, signature: signedResponse.signature } | ||
} | ||
|
||
/** | ||
* Recover the public wallet address | ||
* @param {object} data obj with structure {...data, timestamp} | ||
* @param {string} signature signature generated with signed data | ||
*/ | ||
const recoverWallet = (data, signature) => { | ||
let structuredData = JSON.stringify(sortKeys(data)) | ||
const hashedData = web3.utils.keccak256(structuredData) | ||
const recoveredWallet = web3.eth.accounts.recover(hashedData, signature) | ||
|
||
return recoveredWallet | ||
} | ||
|
||
/** | ||
* Returns boolean indicating if provided timestamp is older than MAX_SIGNATURE_AGE | ||
* @param {string} signatureTimestamp unix timestamp string when signature was generated | ||
*/ | ||
const signatureHasExpired = (signatureTimestamp) => { | ||
const signatureTimestampDate = new Date(signatureTimestamp) | ||
const currentTimestampDate = new Date() | ||
const signatureAge = currentTimestampDate - signatureTimestampDate | ||
|
||
return (signatureAge >= MAX_SIGNATURE_AGE_MS) | ||
} | ||
|
||
const sortKeys = x => { | ||
if (typeof x !== 'object' || !x) { return x } | ||
if (Array.isArray(x)) { return x.map(sortKeys) } | ||
return Object.keys(x).sort().reduce((o, k) => ({ ...o, [k]: sortKeys(x[k]) }), {}) | ||
} | ||
|
||
module.exports = { | ||
generateTimestampAndSignature, | ||
recoverWallet, | ||
sortKeys, | ||
MAX_SIGNATURE_AGE_MS, | ||
signatureHasExpired | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
const { logger } = require('../logging') | ||
const { recoverWallet, signatureHasExpired } = require('./apiSigning') | ||
const audiusLibsWrapper = require('../audiusLibsInstance') | ||
|
||
const FIND_CONTENT_NODES_INTERVAL_MS = 10 * 60 * 1000 | ||
|
||
const KNOWN_CONTENT_NODE_IP_ADDRESSES = new Set([]) | ||
let KNOWN_CONTENT_NODE_WALLETS = new Set([]) | ||
|
||
/** | ||
* Poll for content nodes and memoizes their delegate owner wallets | ||
*/ | ||
const findContentNodes = async () => { | ||
const libs = await audiusLibsWrapper.getAudiusLibsAsync() | ||
const { ethContracts, ethWeb3Manager } = libs | ||
const nodes = await ethContracts.getServiceProviderList('content-node') | ||
const toChecksumAddress = ethWeb3Manager.getWeb3().utils.toChecksumAddress | ||
KNOWN_CONTENT_NODE_WALLETS = new Set( | ||
nodes.map(node => toChecksumAddress(node.delegateOwnerWallet)) | ||
) | ||
logger.info(`findContentNodes - Known wallets: ${[...KNOWN_CONTENT_NODE_WALLETS]}`) | ||
} | ||
|
||
findContentNodes() | ||
setInterval(findContentNodes, FIND_CONTENT_NODES_INTERVAL_MS) | ||
|
||
/** | ||
* Find whether a given IP belongs to a registered content node. | ||
* If the ip is from an already known content node IP address, return true | ||
* Otherwise, if the request has a signature and timestamp, recover the signing | ||
* wallet and determine whether it is a registered content node | ||
* @param {string} ip | ||
* @param {Request} req | ||
* @param {Set<string>} knownContentNodeWallets | ||
* @param {Set<string>} knownContentNodeIPAddresses | ||
*/ | ||
const _isIPFromContentNode = (ip, req, knownContentNodeWallets, knownContentNodeIPAddresses) => { | ||
if (knownContentNodeIPAddresses.has(ip)) { | ||
return true | ||
} | ||
|
||
if (!req.body || !req.body.signature || !req.body.timestamp) { | ||
return false | ||
} | ||
|
||
const { signature, timestamp } = req.body | ||
|
||
const hasExpired = signatureHasExpired(timestamp) | ||
if (hasExpired) { | ||
return false | ||
} | ||
|
||
req.logger.info(`isIPFromContentNode - Recovering signature: ${signature}, timestamp: ${timestamp}`) | ||
req.logger.info(`isIPFromContentNode - Known wallets: ${[...knownContentNodeWallets]}, ips: ${[...knownContentNodeIPAddresses]}`) | ||
const wallet = recoverWallet({ | ||
data: 'listen', | ||
timestamp | ||
}, signature) | ||
|
||
req.logger.info(`isIPFromContentNode - Recovered wallet: ${wallet}`) | ||
if (knownContentNodeWallets.has(wallet)) { | ||
req.logger.info(`isIPFromContentNode - Was from content node`) | ||
knownContentNodeIPAddresses.add(ip) | ||
return true | ||
} | ||
req.logger.info(`isIPFromContentNode - Was not from content node`) | ||
return false | ||
} | ||
|
||
const isIPFromContentNode = (ip, req) => { | ||
return _isIPFromContentNode(ip, req, KNOWN_CONTENT_NODE_WALLETS, KNOWN_CONTENT_NODE_IP_ADDRESSES) | ||
} | ||
|
||
module.exports = { | ||
// Exposed for testing purposes only | ||
_isIPFromContentNode, | ||
isIPFromContentNode | ||
} |
Oops, something went wrong.