-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Fix critical memory leaks in EVM chain wallet transaction streaming #4078
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
Open
nitsujlangston
wants to merge
6
commits into
bitpay:master
Choose a base branch
from
nitsujlangston:fix/evm-cursor-memory-leaks
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
88a71eb
Fix critical memory leaks in EVM chain wallet transaction streaming
nitsujlangston f2fe7d1
Add integration tests for EVM memory leak fixes
nitsujlangston 098402f
Optimize Gnosis API to reuse cached EVM provider instance
nitsujlangston 0ce21ac
Remove unsupported cursor.destroy() calls for MongoDB 3.6 compatibility
nitsujlangston c0992cd
Address PR feedback: fix test listener and correct typos
nitsujlangston 8da0c3f
Use deterministic txid generation in memory leak tests
nitsujlangston File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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
248 changes: 248 additions & 0 deletions
248
packages/bitcore-node/test/integration/ethereum/memory-leaks.spec.ts
This file contains hidden or 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,248 @@ | ||
| import { ObjectId } from 'bson'; | ||
| import { expect } from 'chai'; | ||
| import { Request, Response } from 'express-serve-static-core'; | ||
| import * as sinon from 'sinon'; | ||
| import { PassThrough } from 'stream'; | ||
| import { MongoBound } from '../../../src/models/base'; | ||
| import { IWallet, WalletStorage } from '../../../src/models/wallet'; | ||
| import { WalletAddressStorage } from '../../../src/models/walletAddress'; | ||
| import { EVMTransactionStorage } from '../../../src/providers/chain-state/evm/models/transaction'; | ||
| import { intAfterHelper, intBeforeHelper } from '../../helpers/integration'; | ||
|
|
||
| const chain = 'ETH'; | ||
| const network = 'regtest'; | ||
|
|
||
| describe('EVM Memory Leak Prevention', function() { | ||
| const suite = this; | ||
| this.timeout(30000); | ||
| let globalSandbox: sinon.SinonSandbox; | ||
| let ETH: any; | ||
|
|
||
| before(intBeforeHelper); | ||
| after(async () => intAfterHelper(suite)); | ||
|
|
||
| beforeEach(async () => { | ||
| globalSandbox = sinon.createSandbox(); | ||
| // Use the ETH module instance | ||
| const { ETH: ETHModule } = await import('../../../src/modules/ethereum/api/csp'); | ||
| ETH = ETHModule; | ||
| await EVMTransactionStorage.collection.deleteMany({}); | ||
| await WalletStorage.collection.deleteMany({}); | ||
| await WalletAddressStorage.collection.deleteMany({}); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| globalSandbox.restore(); | ||
| }); | ||
|
|
||
| function createMockReqRes() { | ||
| const reqStream = new PassThrough(); | ||
| const req = reqStream as unknown as Request; | ||
|
|
||
| const resStream = new PassThrough(); | ||
| const res = resStream as unknown as Response; | ||
|
|
||
| (res as any).write = resStream.write.bind(resStream); | ||
| (res as any).end = resStream.end.bind(resStream); | ||
|
|
||
| res.type = () => res; | ||
| res.status = () => res; | ||
| res.send = () => res; | ||
|
|
||
| // Consume data to keep stream flowing | ||
| resStream.on('data', () => {}); | ||
|
|
||
| return { req, res, reqEmitter: reqStream, resEmitter: resStream }; | ||
| } | ||
|
|
||
| describe('Cursor Cleanup on Client Disconnect', () => { | ||
| it('should not crash when client disconnects during streamWalletTransactions', async () => { | ||
| const address = '0x7F17aF79AABC4A297A58D389ab5905fEd4Ec9502'; | ||
| const objectId = ObjectId.createFromHexString('60f9abed0e32086bf9903bb5'); | ||
| const wallet = { | ||
| _id: objectId, | ||
| chain, | ||
| network, | ||
| name: 'test-wallet', | ||
| pubKey: '0x029ec2ebdebe6966259cf3c6f35c4f126b82fe072bf9d0e81dad375f1d6d2d9054', | ||
| path: 'm/44\'/60\'/0\'/0/0', | ||
| singleAddress: true | ||
| } as MongoBound<IWallet>; | ||
|
|
||
| await WalletStorage.collection.insertOne(wallet as any); | ||
| await WalletAddressStorage.collection.insertOne({ | ||
| chain, | ||
| network, | ||
| wallet: objectId, | ||
| address, | ||
| processed: true | ||
| }); | ||
|
|
||
| const txCount = 5; | ||
| const txs = new Array(txCount).fill({}).map((_, i) => ({ | ||
| chain, | ||
| network, | ||
| blockHeight: 1, | ||
| gasPrice: 10 * 1e9, | ||
| data: Buffer.from(''), | ||
| from: address, | ||
| to: '0xRecipient123', | ||
| txid: '0x' + (i + 1).toString(16).padStart(64, '0'), | ||
| wallets: [objectId] | ||
| } as any)); | ||
|
|
||
| await EVMTransactionStorage.collection.insertMany(txs); | ||
|
|
||
| const { req, res, reqEmitter } = createMockReqRes(); | ||
|
|
||
| const streamPromise = ETH.streamWalletTransactions({ | ||
| chain, | ||
| network, | ||
| wallet, | ||
| req, | ||
| res, | ||
| args: {} | ||
| }); | ||
|
|
||
| // Wait for stream to start | ||
| await new Promise(resolve => setTimeout(resolve, 100)); | ||
|
|
||
| // Simulate client disconnect | ||
| reqEmitter.emit('close'); | ||
|
|
||
| // Wait for cleanup | ||
| await new Promise(resolve => setTimeout(resolve, 100)); | ||
|
|
||
| // Should not throw - the implementation should handle cleanup gracefully | ||
| await streamPromise.catch(() => {}); | ||
|
|
||
| // If we get here without crashing, the test passes | ||
| expect(true).to.be.true; | ||
| }); | ||
|
|
||
| it('should handle multiple interrupted requests without accumulating resources', async () => { | ||
| const address = '0x7F17aF79AABC4A297A58D389ab5905fEd4Ec9502'; | ||
| const objectId = ObjectId.createFromHexString('60f9abed0e32086bf9903bb5'); | ||
| const wallet = { | ||
| _id: objectId, | ||
| chain, | ||
| network, | ||
| name: 'test-wallet-multi', | ||
| pubKey: '0x029ec2ebdebe6966259cf3c6f35c4f126b82fe072bf9d0e81dad375f1d6d2d9054', | ||
| path: 'm/44\'/60\'/0\'/0/0', | ||
| singleAddress: true | ||
| } as MongoBound<IWallet>; | ||
|
|
||
| await WalletStorage.collection.insertOne(wallet as any); | ||
| await WalletAddressStorage.collection.insertOne({ | ||
| chain, | ||
| network, | ||
| wallet: objectId, | ||
| address, | ||
| processed: true | ||
| }); | ||
|
|
||
| const txCount = 5; | ||
| const txs = new Array(txCount).fill({}).map((_, i) => ({ | ||
| chain, | ||
| network, | ||
| blockHeight: 1, | ||
| gasPrice: 10 * 1e9, | ||
| data: Buffer.from(''), | ||
| from: address, | ||
| to: '0xRecipient456', | ||
| txid: '0x' + (i + 100).toString(16).padStart(64, '0'), | ||
| wallets: [objectId] | ||
| } as any)); | ||
|
|
||
| await EVMTransactionStorage.collection.insertMany(txs); | ||
|
|
||
| // Make 3 requests that all get interrupted | ||
| const numRequests = 3; | ||
| for (let i = 0; i < numRequests; i++) { | ||
| const { req, res, reqEmitter } = createMockReqRes(); | ||
|
|
||
| const streamPromise = ETH.streamWalletTransactions({ | ||
| chain, | ||
| network, | ||
| wallet, | ||
| req, | ||
| res, | ||
| args: {} | ||
| }); | ||
|
|
||
| await new Promise(resolve => setTimeout(resolve, 50)); | ||
| reqEmitter.emit('close'); | ||
| await new Promise(resolve => setTimeout(resolve, 50)); | ||
| await streamPromise.catch(() => {}); | ||
| } | ||
|
|
||
| // If we completed all requests without issues, test passes | ||
| expect(true).to.be.true; | ||
| }); | ||
| }); | ||
|
|
||
| describe('Provider Instance Reuse', () => { | ||
| it('should complete streaming without errors', async () => { | ||
| const address = '0x7F17aF79AABC4A297A58D389ab5905fEd4Ec9502'; | ||
| const objectId = ObjectId.createFromHexString('60f9abed0e32086bf9903bb5'); | ||
| const wallet = { | ||
| _id: objectId, | ||
| chain, | ||
| network, | ||
| name: 'test-wallet-provider', | ||
| pubKey: '0x029ec2ebdebe6966259cf3c6f35c4f126b82fe072bf9d0e81dad375f1d6d2d9054', | ||
| path: 'm/44\'/60\'/0\'/0/0', | ||
| singleAddress: true | ||
| } as MongoBound<IWallet>; | ||
|
|
||
| await WalletStorage.collection.insertOne(wallet as any); | ||
| await WalletAddressStorage.collection.insertOne({ | ||
| chain, | ||
| network, | ||
| wallet: objectId, | ||
| address, | ||
| processed: true | ||
| }); | ||
|
|
||
| const txCount = 5; | ||
| const txs = new Array(txCount).fill({}).map((_, i) => ({ | ||
| chain, | ||
| network, | ||
| blockHeight: 1, | ||
| gasPrice: 10 * 1e9, | ||
| data: Buffer.from(''), | ||
| from: address, | ||
| to: '0xRecipient789', | ||
| txid: '0x' + (i + 200).toString(16).padStart(64, '0'), | ||
| wallets: [objectId] | ||
| } as any)); | ||
|
|
||
| await EVMTransactionStorage.collection.insertMany(txs); | ||
|
|
||
| const { req, res, resEmitter } = createMockReqRes(); | ||
|
|
||
| let receivedTxCount = 0; | ||
| resEmitter.on('data', () => { | ||
| receivedTxCount++; | ||
| }); | ||
|
|
||
| await new Promise((resolve, reject) => { | ||
| resEmitter.on('finish', resolve); | ||
| resEmitter.on('error', reject); | ||
|
|
||
| ETH.streamWalletTransactions({ | ||
| chain, | ||
| network, | ||
| wallet, | ||
| req, | ||
| res, | ||
| args: {} | ||
| }).catch(reject); | ||
| }); | ||
|
|
||
| // Verify that we received some transactions (stream worked) | ||
| expect(receivedTxCount).to.be.greaterThan(0); | ||
| }); | ||
| }); | ||
| }); | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.