From 9f5d7dee32186d409590b07a5ec3e4c194905073 Mon Sep 17 00:00:00 2001 From: Theo Ilie Date: Wed, 10 Aug 2022 10:23:55 -0700 Subject: [PATCH] [CON-222] Fix SequelizeUniqueConstraintError for blockchain track+user IDs (#3660) --- creator-node/src/routes/audiusUsers.js | 15 +++ creator-node/src/routes/tracks.js | 16 +++ creator-node/test/audiusUsers.test.js | 62 ++++++++++- creator-node/test/contentBlacklist.test.js | 13 +++ creator-node/test/dbManager.test.js | 106 ++++++++++++------ creator-node/test/pollingTracks.test.js | 119 +++++++++++++++++++-- creator-node/test/sync.test.js | 64 ++++++++++- 7 files changed, 344 insertions(+), 51 deletions(-) diff --git a/creator-node/src/routes/audiusUsers.js b/creator-node/src/routes/audiusUsers.js index df52ceb0b74..3646b3b4740 100644 --- a/creator-node/src/routes/audiusUsers.js +++ b/creator-node/src/routes/audiusUsers.js @@ -112,6 +112,21 @@ router.post( ) } + // Verify that wallet of the user on the blockchain for the given ID matches the user attempting to update + const serviceRegistry = req.app.get('serviceRegistry') + const { libs } = serviceRegistry + const userResp = await libs.contracts.UserFactoryClient.getUser( + blockchainUserId + ) + if ( + !userResp?.wallet || + userResp.wallet.toLowerCase() !== req.session.wallet.toLowerCase() + ) { + throw new Error( + `Owner wallet ${userResp.wallet} of blockchainUserId ${blockchainUserId} does not match the wallet of the user attempting to write this data: ${req.session.wallet}` + ) + } + const cnodeUserUUID = req.session.cnodeUserUUID // Fetch metadataJSON for metadataFileUUID. diff --git a/creator-node/src/routes/tracks.js b/creator-node/src/routes/tracks.js index a7ca7be5d15..d41dd114e84 100644 --- a/creator-node/src/routes/tracks.js +++ b/creator-node/src/routes/tracks.js @@ -421,6 +421,22 @@ router.post( throw new Error('Cannot create track without transcodedTrackUUID.') } + // Verify that blockchain track id is owned by user attempting to upload it + const serviceRegistry = req.app.get('serviceRegistry') + const { libs } = serviceRegistry + const trackResp = await libs.contracts.TrackFactoryClient.getTrack( + blockchainTrackId + ) + if ( + !trackResp?.trackOwnerId || + parseInt(trackResp.trackOwnerId, 10) !== + parseInt(req.session.userId, 10) + ) { + throw new Error( + `Owner ID ${trackResp.trackOwnerId} of blockchainTrackId ${blockchainTrackId} does not match the ID of the user attempting to write this track: ${req.session.userId}` + ) + } + // Associate the transcode file db record with trackUUID const transcodedFile = await models.File.findOne({ where: { diff --git a/creator-node/test/audiusUsers.test.js b/creator-node/test/audiusUsers.test.js index 53cd95b7420..01ed0d724a6 100644 --- a/creator-node/test/audiusUsers.test.js +++ b/creator-node/test/audiusUsers.test.js @@ -118,11 +118,24 @@ describe('Test AudiusUsers', function () { } }) assert.ok(file) + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = session.walletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } + await request(app) .post('/audius_users') .set('X-Session-ID', session.sessionToken) .set('User-Id', session.userId) - .send({ blockchainUserId: 1, blockNumber: 10, metadataFileUUID: resp.body.data.metadataFileUUID }) + .send({ blockchainUserId, blockNumber: 10, metadataFileUUID: resp.body.data.metadataFileUUID }) .expect(200) }) @@ -143,18 +156,31 @@ describe('Test AudiusUsers', function () { throw new Error('invalid return data') } + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = session.walletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } + await request(app) .post('/audius_users') .set('X-Session-ID', session.sessionToken) .set('User-Id', session.userId) - .send({ blockchainUserId: 1, blockNumber: 10, metadataFileUUID: resp.body.data.metadataFileUUID }) + .send({ blockchainUserId, blockNumber: 10, metadataFileUUID: resp.body.data.metadataFileUUID }) .expect(200) await request(app) .post('/audius_users') .set('X-Session-ID', session.sessionToken) .set('User-Id', session.userId) - .send({ blockchainUserId: 1, blockNumber: 10, metadataFileUUID: resp.body.data.metadataFileUUID }) + .send({ blockchainUserId, blockNumber: 10, metadataFileUUID: resp.body.data.metadataFileUUID }) .expect(200) }) @@ -191,11 +217,24 @@ describe('Test AudiusUsers', function () { throw new Error('invalid return data') } + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = session.walletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } + await request(app) .post('/audius_users') .set('X-Session-ID', session.sessionToken) .set('User-Id', session.userId) - .send({ blockchainUserId: 1, blockNumber: 10, metadataFileUUID: resp2.body.data.metadataFileUUID }) + .send({ blockchainUserId, blockNumber: 10, metadataFileUUID: resp2.body.data.metadataFileUUID }) .expect(200) }) @@ -220,11 +259,24 @@ describe('Test AudiusUsers', function () { const cnodeUser = await models.CNodeUser.findOne({ where: { cnodeUserUUID: session.cnodeUserUUID } }) await cnodeUser.update({ latestBlockNumber: 100 }) + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = session.walletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } + await request(app) .post('/audius_users') .set('X-Session-ID', session.sessionToken) .set('User-Id', session.userId) - .send({ blockchainUserId: 1, blockNumber: 10, metadataFileUUID: resp.body.data.metadataFileUUID }) + .send({ blockchainUserId, blockNumber: 10, metadataFileUUID: resp.body.data.metadataFileUUID }) .expect(400, { error: 'Invalid blockNumber param 10. Must be greater or equal to previously processed blocknumber 100.' }) diff --git a/creator-node/test/contentBlacklist.test.js b/creator-node/test/contentBlacklist.test.js index de72f8e4a37..5d2d419124c 100644 --- a/creator-node/test/contentBlacklist.test.js +++ b/creator-node/test/contentBlacklist.test.js @@ -1091,6 +1091,19 @@ describe('test ContentBlacklist', function () { .set('User-Id', inputUserId) .set('Enforce-Write-Quorum', false) .send({ metadata: trackMetadata, source_file: sourceFile }) + + // Make chain recognize wallet as owner of track + const getTrackStub = sinon.stub().callsFake((blockchainTrackId) => { + let trackOwnerId = -1 + if (blockchainTrackId === trackId) { + trackOwnerId = inputUserId + } + return { + trackOwnerId + } + }) + libsMock.contracts.TrackFactoryClient = { getTrack: getTrackStub } + // associate track metadata with track await request(app) .post('/tracks') diff --git a/creator-node/test/dbManager.test.js b/creator-node/test/dbManager.test.js index 8115b857a86..a3f97f3f25e 100644 --- a/creator-node/test/dbManager.test.js +++ b/creator-node/test/dbManager.test.js @@ -27,34 +27,6 @@ const { fetchDBStateForWallet, assertTableEquality } = require('./lib/utils') const TestAudioFilePath = path.resolve(__dirname, 'testTrack.mp3') -/** Add state to AudiusUsers table for given userId */ -const uploadAudiusUserState = async function ({ - app, - userId, - sessionToken, - metadataObj, - audiusUserBlockNumber -}) { - const audiusUserMetadataResp = await request(app) - .post('/audius_users/metadata') - .set('X-Session-ID', sessionToken) - .set('User-Id', userId) - .set('Enforce-Write-Quorum', false) - .send({ metadata: metadataObj }) - .expect(200) - - await request(app) - .post('/audius_users') - .set('X-Session-ID', sessionToken) - .set('User-Id', userId) - .send({ - blockchainUserId: userId, - blockNumber: audiusUserBlockNumber, - metadataFileUUID: audiusUserMetadataResp.body.data.metadataFileUUID - }) - .expect(200) -} - describe('Test createNewDataRecord()', async function () { const req = { logger: { @@ -714,6 +686,18 @@ describe('Test deleteAllCNodeUserDataFromDB()', async function () { .set('Enforce-Write-Quorum', false) .send({ metadata: audiusUserMetadata }) .expect(200) + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = session.walletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } await request(app) .post('/audius_users') .set('X-Session-ID', session.sessionToken) @@ -792,13 +776,26 @@ describe('Test deleteAllCNodeUserDataFromDB()', async function () { expectedTrackMetadataMultihash ) + // Make chain recognize wallet as owner of track + const blockchainTrackId = 1 + const getTrackStub = sinon.stub().callsFake((blockchainTrackIdArg) => { + let trackOwnerId = -1 + if (blockchainTrackIdArg === blockchainTrackId) { + trackOwnerId = userId + } + return { + trackOwnerId + } + }) + libsMock.contracts.TrackFactoryClient = { getTrack: getTrackStub } + // Complete track upload await request(app) .post('/tracks') .set('X-Session-ID', session.sessionToken) .set('User-Id', session.userId) .send({ - blockchainTrackId: 1, + blockchainTrackId, blockNumber: 10, metadataFileUUID: trackMetadataResp.body.data.metadataFileUUID, transcodedTrackUUID @@ -1099,11 +1096,12 @@ describe('Test fetchFilesHashFromDB()', async function () { describe('Test fixInconsistentUser()', async function () { const userId = 1 - let server, app + let server, app, libsMock /** Init server to run DB migrations */ before(async function () { - const appInfo = await getApp(getLibsMock()) + libsMock = getLibsMock() + const appInfo = await getApp(libsMock) server = appInfo.server app = appInfo.app }) @@ -1119,6 +1117,46 @@ describe('Test fixInconsistentUser()', async function () { await server.close() }) + /** Add state to AudiusUsers table for given userId */ + const uploadAudiusUserState = async function ({ + sessionToken, + walletPublicKey, + metadataObj, + audiusUserBlockNumber + }) { + const audiusUserMetadataResp = await request(app) + .post('/audius_users/metadata') + .set('X-Session-ID', sessionToken) + .set('User-Id', userId) + .set('Enforce-Write-Quorum', false) + .send({ metadata: metadataObj }) + .expect(200) + + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = walletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } + + await request(app) + .post('/audius_users') + .set('X-Session-ID', sessionToken) + .set('User-Id', userId) + .send({ + blockchainUserId: userId, + blockNumber: audiusUserBlockNumber, + metadataFileUUID: audiusUserMetadataResp.body.data.metadataFileUUID + }) + .expect(200) + } + it('Confirm no change to healthy users DB state', async function () { const { cnodeUserUUID, walletPublicKey, sessionToken } = await createStarterCNodeUser(userId) @@ -1127,9 +1165,8 @@ describe('Test fixInconsistentUser()', async function () { const audiusUserMetadata = { test: 'field1' } const metadataCID = 'QmQMHXPMuey2AT6fPTKnzKQCrRjPS7AbaQdDTM8VXbHC8W' await uploadAudiusUserState({ - app, - userId, sessionToken, + walletPublicKey, metadataObj: audiusUserMetadata, audiusUserBlockNumber }) @@ -1214,9 +1251,8 @@ describe('Test fixInconsistentUser()', async function () { const audiusUserMetadata = { test: 'field1' } const metadataCID = 'QmQMHXPMuey2AT6fPTKnzKQCrRjPS7AbaQdDTM8VXbHC8W' await uploadAudiusUserState({ - app, - userId, sessionToken, + walletPublicKey, metadataObj: audiusUserMetadata, audiusUserBlockNumber }) diff --git a/creator-node/test/pollingTracks.test.js b/creator-node/test/pollingTracks.test.js index fd31cc6a292..aec1cd5ac70 100644 --- a/creator-node/test/pollingTracks.test.js +++ b/creator-node/test/pollingTracks.test.js @@ -9,8 +9,6 @@ const _ = require('lodash') const config = require('../src/config') const defaultConfig = require('../default-config.json') -const { libs } = require('@audius/sdk') -const Utils = libs const BlacklistManager = require('../src/blacklistManager') const TranscodingQueue = require('../src/TranscodingQueue') const models = require('../src/models') @@ -687,12 +685,25 @@ describe('test Polling Tracks with mocked IPFS', function () { 'QmTWhw49RfSMSJJmfm8cMHFBptgWoBGpNwjAc5jy2qeJfs' ) + // Make chain recognize wallet as owner of track + const blockchainTrackId = 1 + const getTrackStub = sinon.stub().callsFake((blockchainTrackIdArg) => { + let trackOwnerId = -1 + if (blockchainTrackIdArg === blockchainTrackId) { + trackOwnerId = userId + } + return { + trackOwnerId + } + }) + libsMock.contracts.TrackFactoryClient = { getTrack: getTrackStub } + await request(app) .post('/tracks') .set('X-Session-ID', session.sessionToken) .set('User-Id', session.userId) .send({ - blockchainTrackId: 1, + blockchainTrackId, blockNumber: 10, metadataFileUUID: trackMetadataResp.body.data.metadataFileUUID, transcodedTrackUUID @@ -700,6 +711,57 @@ describe('test Polling Tracks with mocked IPFS', function () { .expect(200) }) + // depends on "uploads /track_content_async" and "creates Audius track" tests + it('fails Audius track creation when passing track ID that mismatches on-chain track ID', async function () { + libsMock.User.getUsers.exactly(4) + + const { fileUUID, fileDir } = saveFileToStorage(testAudioFilePath) + const { + track_segments: trackSegments, + source_file: sourceFile, + transcodedTrackUUID + } = await handleTrackContentRoute( + logContext, + getReqObj(fileUUID, fileDir, session) + ) + + const metadata = { + test: 'field1', + track_segments: trackSegments, + owner_id: 1 + } + + const trackMetadataResp = await request(app) + .post('/tracks/metadata') + .set('X-Session-ID', session.sessionToken) + .set('User-Id', session.userId) + .set('Enforce-Write-Quorum', false) + .send({ metadata, sourceFile }) + .expect(200) + + assert.deepStrictEqual( + trackMetadataResp.body.data.metadataMultihash, + 'QmTWhw49RfSMSJJmfm8cMHFBptgWoBGpNwjAc5jy2qeJfs' + ) + + // Make chain NOT recognize wallet as owner of track + const blockchainTrackId = 1 + const getTrackStub = sinon.stub().resolves(-1) + libsMock.contracts.TrackFactoryClient = { getTrack: getTrackStub } + + await request(app) + .post('/tracks') + .set('X-Session-ID', session.sessionToken) + .set('User-Id', session.userId) + .send({ + blockchainTrackId, + blockNumber: 10, + metadataFileUUID: trackMetadataResp.body.data.metadataFileUUID, + transcodedTrackUUID + }) + .expect(500) + }) + // depends on "uploads /track_content_async" it('fails to create downloadable track with no track_id and no source_id present', async function () { libsMock.User.getUsers.exactly(2) @@ -788,12 +850,25 @@ describe('test Polling Tracks with mocked IPFS', function () { 'QmPjrvx9MBcvf495t43ZhiMpKWwu1JnqkcNUN3Z9EBWm49' ) + // Make chain recognize wallet as owner of track + const blockchainTrackId = 1 + const getTrackStub = sinon.stub().callsFake((blockchainTrackIdArg) => { + let trackOwnerId = -1 + if (blockchainTrackIdArg === blockchainTrackId) { + trackOwnerId = userId + } + return { + trackOwnerId + } + }) + libsMock.contracts.TrackFactoryClient = { getTrack: getTrackStub } + await request(app) .post('/tracks') .set('X-Session-ID', session.sessionToken) .set('User-Id', session.userId) .send({ - blockchainTrackId: 1, + blockchainTrackId, blockNumber: 10, metadataFileUUID: trackMetadataResp.body.data.metadataFileUUID, transcodedTrackUUID @@ -1034,13 +1109,26 @@ describe('test Polling Tracks with real files', function () { .expect(200) const trackMetadataFileUUID = trackMetadataResp.body.data.metadataFileUUID + // Make chain recognize wallet as owner of track + const blockchainTrackId = 1 + const getTrackStub = sinon.stub().callsFake((blockchainTrackIdArg) => { + let trackOwnerId = -1 + if (blockchainTrackIdArg === blockchainTrackId) { + trackOwnerId = userId + } + return { + trackOwnerId + } + }) + libsMock.contracts.TrackFactoryClient = { getTrack: getTrackStub } + // Complete track creation await request(app2) .post('/tracks') .set('X-Session-ID', session.sessionToken) .set('User-Id', userId) .send({ - blockchainTrackId: 1, + blockchainTrackId, blockNumber: 10, metadataFileUUID: trackMetadataFileUUID, transcodedTrackUUID @@ -1105,13 +1193,30 @@ describe('test Polling Tracks with real files', function () { .expect(200) const track2MetadataFileUUID = track2MetadataResp.body.data.metadataFileUUID + // Make chain recognize wallet as owner of track + const track1BlockchainId = 1 + const track2BlockchainId = 2 + const getTrackStub = sinon.stub().callsFake((blockchainTrackId) => { + let trackOwnerId = -1 + if ( + blockchainTrackId === track1BlockchainId || + blockchainTrackId === track2BlockchainId + ) { + trackOwnerId = userId + } + return { + trackOwnerId + } + }) + libsMock.contracts.TrackFactoryClient = { getTrack: getTrackStub } + // Complete track1 creation await request(app2) .post('/tracks') .set('X-Session-ID', session.sessionToken) .set('User-Id', userId) .send({ - blockchainTrackId: 1, + blockchainTrackId: track1BlockchainId, blockNumber: 10, metadataFileUUID: trackMetadataFileUUID, transcodedTrackUUID @@ -1124,7 +1229,7 @@ describe('test Polling Tracks with real files', function () { .set('X-Session-ID', session.sessionToken) .set('User-Id', userId) .send({ - blockchainTrackId: 2, + blockchainTrackId: track2BlockchainId, blockNumber: 20, metadataFileUUID: track2MetadataFileUUID, transcodedTrackUUID: transcodedTrack2UUID diff --git a/creator-node/test/sync.test.js b/creator-node/test/sync.test.js index 2362d84300b..1f2e51180e8 100644 --- a/creator-node/test/sync.test.js +++ b/creator-node/test/sync.test.js @@ -87,6 +87,7 @@ describe('test nodesync', async function () { describe('test /export route', async function () { let cnodeUserUUID, sessionToken, + sessionWalletPublicKey, metadataMultihash, metadataFileUUID, transcodedTrackCID, @@ -99,9 +100,12 @@ describe('test nodesync', async function () { const createUserAndTrack = async function () { // Create user - ;({ cnodeUserUUID, sessionToken, userId } = await createStarterCNodeUser( - userId - )) + ;({ + cnodeUserUUID, + sessionToken, + userId, + walletPublicKey: sessionWalletPublicKey + } = await createStarterCNodeUser(userId)) // Upload user metadata const metadata = { @@ -119,6 +123,19 @@ describe('test nodesync', async function () { metadataMultihash = userMetadataResp.body.data.metadataMultihash metadataFileUUID = userMetadataResp.body.data.metadataFileUUID + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = sessionWalletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } + // Associate user with with blockchain ID const associateRequest = { blockchainUserId: 1, @@ -164,13 +181,26 @@ describe('test nodesync', async function () { trackMetadataMultihash = trackMetadataResp.body.data.metadataMultihash trackMetadataFileUUID = trackMetadataResp.body.data.metadataFileUUID + // Make chain recognize wallet as owner of track + const blockchainTrackId = 1 + const getTrackStub = sinon.stub().callsFake((blockchainTrackIdArg) => { + let trackOwnerId = -1 + if (blockchainTrackIdArg === blockchainTrackId) { + trackOwnerId = userId + } + return { + trackOwnerId + } + }) + libsMock.contracts.TrackFactoryClient = { getTrack: getTrackStub } + // associate track + track metadata with blockchain ID await request(app) .post('/tracks') .set('X-Session-ID', sessionToken) .set('User-Id', userId) .send({ - blockchainTrackId: 1, + blockchainTrackId, blockNumber: 10, metadataFileUUID: trackMetadataFileUUID, transcodedTrackUUID @@ -789,6 +819,19 @@ describe('test nodesync', async function () { const metadataFileUUID = userMetadataResp.body.data.metadataFileUUID + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = session.walletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } + // Associate user with with blockchain ID const associateRequest = { blockchainUserId: 1, @@ -1364,6 +1407,19 @@ describe('Test primarySyncFromSecondary() with mocked export', async () => { const metadataFileUUID = userMetadataResp.body.data.metadataFileUUID + // Make chain recognize current session wallet as the wallet for the session user ID + const blockchainUserId = 1 + const getUserStub = sinon.stub().callsFake((blockchainUserIdArg) => { + let wallet = 'no wallet' + if (blockchainUserIdArg === blockchainUserId) { + wallet = session.walletPublicKey + } + return { + wallet + } + }) + libsMock.contracts.UserFactoryClient = { getUser: getUserStub } + // Associate user with with blockchain ID const associateRequest = { blockchainUserId: userId,