diff --git a/packages/lisk-transaction-pool/fixtures/transactions.json b/packages/lisk-transaction-pool/fixtures/transactions.json index c2ced7bb1b4..fb134ff4490 100644 --- a/packages/lisk-transaction-pool/fixtures/transactions.json +++ b/packages/lisk-transaction-pool/fixtures/transactions.json @@ -63,5 +63,109 @@ }, "signature":"faaf3fcfe58e6435b47716fa89b322d855e062ffa52d6e01fe569628c48a2310d6390736d2ff47efd2543c26df309c16f8e789a406da03b1e4dd2511176bb408", "id":"927268910456751599" + }, + { + "type": 0, + "amount": "10008298357", + "fee": 0, + "timestamp": 0, + "recipientId": "17033820735302139166L", + "senderId": "6566229458323231555L", + "senderPublicKey": + "d121d3abf5425fdc0f161d9ddb32f89b7750b4bdb0bff7d18b191d4b4bafa6d4", + "signature": + "72c9b2aa734ec1b97549718ddf0d4737fd38a7f0fd105ea28486f2d989e9b3e399238d81a93aa45c27309d91ce604a5db9d25c9c90a138821f2011bc6636c60a", + "id": "5449806225917864483" + }, + { + "type": 0, + "amount": "100181268334", + "fee": 0, + "timestamp": 0, + "recipientId": "6346132659289875504L", + "senderId": "6566229458323231555L", + "senderPublicKey": + "d121d3abf5425fdc0f161d9ddb32f89b7750b4bdb0bff7d18b191d4b4bafa6d4", + "signature": + "9b09dcdb61db43bcc7b789177c415b1d78b4033a783577d905f12568cc3b9434fb5999ecacf9a027eaa5444fce374681010dcce3ff87e6eca2f28a4ef9c83b03", + "id": "12166312224816013186" + }, + { + "type": 0, + "amount": "100362463", + "fee": 0, + "timestamp": 0, + "recipientId": "11464849422097900507L", + "senderId": "6566229458323231555L", + "senderPublicKey": + "d121d3abf5425fdc0f161d9ddb32f89b7750b4bdb0bff7d18b191d4b4bafa6d4", + "signature": + "ad2ed14b42899c8b4a827e7e8afd081162c1b7de1fccf46c9a0ec68e382f467d775a6eecd560244bc3da6beba15fe386955a77e8176aec405844d199520be009", + "id": "4164649425208084448" + }, + { + "type": 0, + "amount": "100369372455", + "fee": 0, + "timestamp": 0, + "recipientId": "4236366971618433722L", + "senderId": "6566229458323231555L", + "senderPublicKey": + "d121d3abf5425fdc0f161d9ddb32f89b7750b4bdb0bff7d18b191d4b4bafa6d4", + "signature": + "f9ff9ee3b8312ff3825b4d37322779e1edb2ef50844f09f1935a03e9075d7eae93a71cb1181bf48a45148450ecce7a44aa91741a501c1ee42debdef2da377500", + "id": "16794796872512157174" + }, + { + "type": 0, + "amount": "100381478227", + "fee": 0, + "timestamp": 0, + "recipientId": "2072845667742629181L", + "senderId": "6566229458323231555L", + "senderPublicKey": + "d121d3abf5425fdc0f161d9ddb32f89b7750b4bdb0bff7d18b191d4b4bafa6d4", + "signature": + "d648d8d5173e42a4e7cac208b4e53eb54656b1cd98ac342c3e6888338d3660c1891f18300e63b7e777ddb355c619c792cce895bf08d7fd6bbb2ce3fdcebc1e0a", + "id": "12945087750653182687" + }, + { + "type": 0, + "amount": "1005469534", + "fee": 0, + "timestamp": 0, + "recipientId": "4433488237980614411L", + "senderId": "6566229458323231555L", + "senderPublicKey": + "d121d3abf5425fdc0f161d9ddb32f89b7750b4bdb0bff7d18b191d4b4bafa6d4", + "signature": + "dcb362ae9393ef69ac2b5c50d9cb605b126defd7623e9eaa22bd3d377feaeae226463cd3326fade770d94c280b86d47c18ccee33a35bbaafdd3e31cf0b910d07", + "id": "771831816220047333" + }, + { + "type": 0, + "amount": "100590879667", + "fee": 0, + "timestamp": 0, + "recipientId": "4050335756210208828L", + "senderId": "6566229458323231555L", + "senderPublicKey": + "d121d3abf5425fdc0f161d9ddb32f89b7750b4bdb0bff7d18b191d4b4bafa6d4", + "signature": + "bb503ca5a7cea4330bdc10c13ef6619d6609936b181003c3b0eb76521750ab8088840a2e5fdc8bedcb08a37bb58f916f643a2e5f9d7ff13de6a764f39e591d08", + "id": "8226427494934824916" + }, + { + "type": 0, + "amount": "10074039329", + "fee": 0, + "timestamp": 0, + "recipientId": "11034244441535906697L", + "senderId": "6566229458323231555L", + "senderPublicKey": + "d121d3abf5425fdc0f161d9ddb32f89b7750b4bdb0bff7d18b191d4b4bafa6d4", + "signature": + "db4eec2efab2f15e8a1b2cc514ce31ef26b4c444bc4f5a7b0b9a64742d423486a0c162d953af214a63eec108c9a1d460cc1145dfb02f0fad8648991db1bab40e", + "id": "1522947044527641725" } ] \ No newline at end of file diff --git a/packages/lisk-transaction-pool/src/check_transactions.ts b/packages/lisk-transaction-pool/src/check_transactions.ts index 629305d8e9c..85af26d8340 100644 --- a/packages/lisk-transaction-pool/src/check_transactions.ts +++ b/packages/lisk-transaction-pool/src/check_transactions.ts @@ -16,38 +16,88 @@ export interface TransactionResponse { } export enum Status { + FAIL = 0, OK = 1, - FAIL = 2, + PENDING = 2, } -export interface CheckTransactionsResponse { +export interface CheckTransactionsResponseWithPassAndFail { failedTransactions: ReadonlyArray; passedTransactions: ReadonlyArray; } -export const checkTransactions = async ( +export interface CheckTransactionsResponseWithPassFailAndPending { + failedTransactions: ReadonlyArray; + passedTransactions: ReadonlyArray; + pendingTransactions: ReadonlyArray; +} + +const getTransactionByStatus = ( + transactions: ReadonlyArray, + responses: ReadonlyArray, + status: Status, +): ReadonlyArray => { + const transactionIdsByStatus = responses + .filter(transactionResponse => transactionResponse.status === status) + .map(transactionStatus => transactionStatus.id); + + const transactionsByStatus = transactions.filter(transaction => + transactionIdsByStatus.includes(transaction.id), + ); + + return transactionsByStatus; +}; + +export const checkTransactionsWithPassAndFail = async ( transactions: ReadonlyArray, checkerFunction: CheckerFunction, -): Promise => { +): Promise => { // Process transactions and check their validity const { transactionsResponses } = await checkerFunction(transactions); - // Get ids of failed transactions from the response - const failedTransactionIds = transactionsResponses - .filter(transactionResponse => transactionResponse.status === Status.FAIL) - .map(transactionStatus => transactionStatus.id); + const failedTransactions = getTransactionByStatus( + transactions, + transactionsResponses, + Status.FAIL, + ); + const passedTransactions = getTransactionByStatus( + transactions, + transactionsResponses, + Status.OK, + ); + + return { + failedTransactions, + passedTransactions, + }; +}; - // Filter transactions which were failed - const failedTransactions = transactions.filter(transaction => - failedTransactionIds.includes(transaction.id), +export const checkTransactionsWithPassFailAndPending = async ( + transactions: ReadonlyArray, + checkerFunction: CheckerFunction, +): Promise => { + // Process transactions and check their validity + const { transactionsResponses } = await checkerFunction(transactions); + + const failedTransactions = getTransactionByStatus( + transactions, + transactionsResponses, + Status.FAIL, + ); + const passedTransactions = getTransactionByStatus( + transactions, + transactionsResponses, + Status.OK, ); - // Filter transactions which were ok - const passedTransactions = transactions.filter( - transaction => !failedTransactionIds.includes(transaction.id), + const pendingTransactions = getTransactionByStatus( + transactions, + transactionsResponses, + Status.PENDING, ); return { failedTransactions, passedTransactions, + pendingTransactions, }; }; diff --git a/packages/lisk-transaction-pool/src/queue.ts b/packages/lisk-transaction-pool/src/queue.ts index 84979e0c35b..5afea261a3d 100644 --- a/packages/lisk-transaction-pool/src/queue.ts +++ b/packages/lisk-transaction-pool/src/queue.ts @@ -1,7 +1,7 @@ import { Transaction } from './transaction_pool'; interface QueueIndex { - [index: string]: Transaction; + [index: string]: Transaction | undefined; } interface RemoveForReduceObject { @@ -88,6 +88,12 @@ export class Queue { return !!this._index[transaction.id]; } + public filter( + condition: (transaction: Transaction) => boolean, + ): ReadonlyArray { + return this._transactions.filter(condition); + } + public peekUntil( condition: (transaction: Transaction) => boolean, ): ReadonlyArray { @@ -146,4 +152,8 @@ export class Queue { public size(): number { return this._transactions.length; } + + public sizeBy(condition: (transaction: Transaction) => boolean): number { + return this._transactions.filter(condition).length; + } } diff --git a/packages/lisk-transaction-pool/src/transaction_pool.ts b/packages/lisk-transaction-pool/src/transaction_pool.ts index 0b888c813fd..2fc613f75e8 100644 --- a/packages/lisk-transaction-pool/src/transaction_pool.ts +++ b/packages/lisk-transaction-pool/src/transaction_pool.ts @@ -14,8 +14,12 @@ */ import { CheckerFunction, - checkTransactions, - CheckTransactionsResponse, + CheckTransactionsResponseWithPassAndFail, + CheckTransactionsResponseWithPassFailAndPending, + checkTransactionsWithPassAndFail, + checkTransactionsWithPassFailAndPending, + Status, + TransactionResponse, } from './check_transactions'; import { Job } from './job'; import { Queue } from './queue'; @@ -28,14 +32,22 @@ export interface TransactionObject { readonly senderPublicKey: string; signatures?: ReadonlyArray; readonly type: number; + containsUniqueData: boolean; +} + +export interface SignatureObject { + transactionId: string; + signature: string; + publicKey: string; } export interface TransactionFunctions { - containsUniqueData(): boolean; isExpired(date: Date): boolean; verifyAgainstOtherTransactions( otherTransactions: ReadonlyArray, ): boolean; + addVerifiedSignature(signature: string): TransactionResponse; + isReady(): boolean; } interface TransactionPoolConfiguration { @@ -47,6 +59,7 @@ interface TransactionPoolConfiguration { readonly validatedTransactionsProcessingInterval: number; readonly verifiedTransactionsLimitPerProcessing: number; readonly verifiedTransactionsProcessingInterval: number; + readonly pendingTransactionsProcessingLimit: number; } export interface AddTransactionResult { @@ -76,6 +89,7 @@ interface Queues { readonly [queue: string]: Queue; } +const DEFAULT_PENDING_TRANSACTIONS_PROCESSING_LIMIT = 5; const DEFAULT_EXPIRE_TRANSACTION_INTERVAL = 30000; const DEFAULT_MAX_TRANSACTIONS_PER_QUEUE = 1000; const DEFAULT_RECEIVED_TRANSACTIONS_PROCESSING_INTERVAL = 30000; @@ -86,6 +100,7 @@ const DEFAULT_VERIFIED_TRANSACTIONS_PROCESSING_INTERVAL = 30000; const DEFAULT_VERIFIED_TRANSACTIONS_LIMIT_PER_PROCESSING = 100; export class TransactionPool { + private readonly _pendingTransactionsProcessingLimit: number; private readonly _expireTransactionsInterval: number; private readonly _expireTransactionsJob: Job>; private readonly _maxTransactionsPerQueue: number; @@ -112,11 +127,13 @@ export class TransactionPool { validatedTransactionsLimitPerProcessing = DEFAULT_VALIDATED_TRANSACTIONS_LIMIT_PER_PROCESSING, verifiedTransactionsProcessingInterval = DEFAULT_VERIFIED_TRANSACTIONS_PROCESSING_INTERVAL, verifiedTransactionsLimitPerProcessing = DEFAULT_VERIFIED_TRANSACTIONS_LIMIT_PER_PROCESSING, + pendingTransactionsProcessingLimit = DEFAULT_PENDING_TRANSACTIONS_PROCESSING_LIMIT, validateTransactions, verifyTransactions, processTransactions, }: TransactionPoolOptions) { this._maxTransactionsPerQueue = maxTransactionsPerQueue; + this._pendingTransactionsProcessingLimit = pendingTransactionsProcessingLimit; this._queues = { received: new Queue(), @@ -198,14 +215,40 @@ export class TransactionPool { this._queues.verified.enqueueMany(transactions); } + // It is assumed that signature is verified for this transaction before this function is called + public addVerifiedSignature( + signatureObject: SignatureObject, + ): TransactionResponse { + const transaction = this.findInTransactionPool( + signatureObject.transactionId, + ); + if (transaction) { + return transaction.addVerifiedSignature(signatureObject.signature); + } + + return { + id: signatureObject.transactionId, + status: Status.FAIL, + errors: [new Error('Could not find transaction in transaction pool')], + }; + } + public existsInTransactionPool(transaction: Transaction): boolean { return Object.keys(this._queues).reduce( - (previousValue, currentValue) => - previousValue || this._queues[currentValue].exists(transaction), + (previousValue, queueName) => + previousValue || this._queues[queueName].exists(transaction), false, ); } + public findInTransactionPool(id: string): Transaction | undefined { + return Object.keys(this._queues).reduce( + (previousValue: Transaction | undefined, queueName) => + previousValue || this._queues[queueName].index[id], + undefined, + ); + } + public get queues(): Queues { return this._queues; } @@ -233,10 +276,8 @@ export class TransactionPool { ); // Remove all transactions from the verified, pending and ready queues if they are of a type which includes unique data and that type is included in the confirmed transactions - // TODO: remove the condition for checking `containsUniqueData` exists, because it should always exist const confirmedTransactionsWithUniqueData = transactions.filter( - (transaction: Transaction) => - transaction.containsUniqueData && transaction.containsUniqueData(), + (transaction: Transaction) => transaction.containsUniqueData, ); const removedTransactionsByTypes = this.removeTransactionsFromQueues( Object.keys(otherQueues), @@ -316,15 +357,19 @@ export class TransactionPool { } private async processVerifiedTransactions(): Promise< - CheckTransactionsResponse + CheckTransactionsResponseWithPassAndFail > { const transactionsInReadyQueue = this._queues.ready.size(); const transactionsInVerifiedQueue = this._queues.verified.size(); + const processableTransactionsInPendingQueue = this._queues.pending.sizeBy( + transaction => transaction.isReady(), + ); if ( transactionsInReadyQueue >= this._verifiedTransactionsProcessingLimitPerInterval || - transactionsInVerifiedQueue === 0 + (transactionsInVerifiedQueue === 0 && + processableTransactionsInPendingQueue === 0) ) { return { passedTransactions: [], @@ -332,9 +377,25 @@ export class TransactionPool { }; } + const additionalTransactionsToProcessLimit = + this._verifiedTransactionsProcessingLimitPerInterval - + transactionsInReadyQueue; + const transactionsFromPendingQueueLimit = Math.min( + additionalTransactionsToProcessLimit, + this._pendingTransactionsProcessingLimit, + ); + // Filter at max transactionsFromPendingQueueLimit from the pending queue which are also ready + const transactionsFromPendingQueue = this._queues.pending + .filter(transaction => transaction.isReady()) + .slice(0, transactionsFromPendingQueueLimit); + + const additionalVerifiedTransactionsToProcessLimit = + additionalTransactionsToProcessLimit - + transactionsFromPendingQueue.length; + const transactionsFromVerifiedQueue = this._queues.verified.peekUntil( queueCheckers.returnTrueUntilLimit( - this._verifiedTransactionsProcessingInterval - transactionsInReadyQueue, + additionalVerifiedTransactionsToProcessLimit, ), ); const transactionsFromReadyQueue = this._queues.ready.peekUntil( @@ -342,20 +403,25 @@ export class TransactionPool { ); const toProcessTransactions = [ ...transactionsFromReadyQueue, + ...transactionsFromPendingQueue, ...transactionsFromVerifiedQueue, ]; - const { passedTransactions, failedTransactions } = await checkTransactions( + const { + passedTransactions, + failedTransactions, + } = await checkTransactionsWithPassAndFail( toProcessTransactions, this._processTransactions, ); - // Remove invalid transactions from verified and ready queues - this._queues.verified.removeFor( - queueCheckers.checkTransactionForId(failedTransactions), - ); - this._queues.ready.removeFor( + const { received, validated, ...otherQueues } = this._queues; + + // Remove invalid transactions from verified, pending and ready queues + this.removeTransactionsFromQueues( + Object.keys(otherQueues), queueCheckers.checkTransactionForId(failedTransactions), ); + // Keep transactions in the ready queue which still exist this._queues.ready.enqueueMany( this._queues.ready.removeFor( @@ -370,6 +436,13 @@ export class TransactionPool { ), ); + // Move processable transactions from the pending queue to the ready queue + this._queues.ready.enqueueMany( + this._queues.pending.removeFor( + queueCheckers.checkTransactionForId(passedTransactions), + ), + ); + return { passedTransactions, failedTransactions, @@ -395,14 +468,17 @@ export class TransactionPool { } private async validateReceivedTransactions(): Promise< - CheckTransactionsResponse + CheckTransactionsResponseWithPassAndFail > { const toValidateTransactions = this._queues.received.peekUntil( queueCheckers.returnTrueUntilLimit( this._receivedTransactionsProcessingLimitPerInterval, ), ); - const { passedTransactions, failedTransactions } = await checkTransactions( + const { + passedTransactions, + failedTransactions, + } = await checkTransactionsWithPassAndFail( toValidateTransactions, this._validateTransactions, ); @@ -425,14 +501,19 @@ export class TransactionPool { } private async verifyValidatedTransactions(): Promise< - CheckTransactionsResponse + CheckTransactionsResponseWithPassFailAndPending > { const toVerifyTransactions = this._queues.validated.peekUntil( queueCheckers.returnTrueUntilLimit( this._validatedTransactionsProcessingLimitPerInterval, ), ); - const { passedTransactions, failedTransactions } = await checkTransactions( + + const { + failedTransactions, + pendingTransactions, + passedTransactions, + } = await checkTransactionsWithPassFailAndPending( toVerifyTransactions, this._verifyTransactions, ); @@ -441,6 +522,7 @@ export class TransactionPool { this._queues.validated.removeFor( queueCheckers.checkTransactionForId(failedTransactions), ); + // Move verified transactions from the validated queue to the verified queue this._queues.verified.enqueueMany( this._queues.validated.removeFor( @@ -448,9 +530,17 @@ export class TransactionPool { ), ); + // Move verified pending transactions from the validated queue to the pending queue + this._queues.pending.enqueueMany( + this._queues.validated.removeFor( + queueCheckers.checkTransactionForId(pendingTransactions), + ), + ); + return { passedTransactions, failedTransactions, + pendingTransactions, }; } } diff --git a/packages/lisk-transaction-pool/test/check_transactions.ts b/packages/lisk-transaction-pool/test/check_transactions.ts index 415cdb46825..495f099f0cd 100644 --- a/packages/lisk-transaction-pool/test/check_transactions.ts +++ b/packages/lisk-transaction-pool/test/check_transactions.ts @@ -16,13 +16,13 @@ import { CheckerFunctionResponse, Status, - checkTransactions, + checkTransactionsWithPassAndFail, } from '../src/check_transactions'; import { expect } from 'chai'; import transactionObjects from '../fixtures/transactions.json'; import { wrapTransferTransaction } from './utils/add_transaction_functions'; -describe('#checkTransactions', () => { +describe('checkTransactions', () => { const transactions = transactionObjects.map(wrapTransferTransaction); const passedTransactions = transactions.slice(0, 2); const failedTransactions = transactions.slice(2, 5); @@ -60,32 +60,37 @@ describe('#checkTransactions', () => { let checkerFunction: sinon.SinonStub; - beforeEach(async () => { - checkerFunction = sandbox.stub().resolves(checkerFunctionResponse); - }); + describe('#checkTransactionWithPassAndFail', () => { + beforeEach(async () => { + checkerFunction = sandbox.stub().resolves(checkerFunctionResponse); + }); - it('should call checkerFunction with the transactions passed', async () => { - await checkTransactions(transactionsToCheck, checkerFunction); - expect(checkerFunction).to.be.calledOnceWithExactly(transactions); - }); + it('should call checkerFunction with the transactions passed', async () => { + await checkTransactionsWithPassAndFail( + transactionsToCheck, + checkerFunction, + ); + expect(checkerFunction).to.be.calledOnceWithExactly(transactionsToCheck); + }); - it('should return transactions which passed the checkerFunction', async () => { - const checkTransactionsResponse = await checkTransactions( - transactionsToCheck, - checkerFunction, - ); - expect(checkTransactionsResponse.passedTransactions).to.be.deep.equal( - passedTransactions, - ); - }); + it('should return transactions which passed the checkerFunction', async () => { + const checkTransactionsResponse = await checkTransactionsWithPassAndFail( + transactionsToCheck, + checkerFunction, + ); + expect(checkTransactionsResponse.passedTransactions).to.be.deep.equal( + passedTransactions, + ); + }); - it('should return transactions which failed the checkerFunction', async () => { - const checkTransactionsResponse = await checkTransactions( - transactionsToCheck, - checkerFunction, - ); - expect(checkTransactionsResponse.failedTransactions).to.be.deep.equal( - failedTransactions, - ); + it('should return transactions which failed the checkerFunction', async () => { + const checkTransactionsResponse = await checkTransactionsWithPassAndFail( + transactionsToCheck, + checkerFunction, + ); + expect(checkTransactionsResponse.failedTransactions).to.be.deep.equal( + failedTransactions, + ); + }); }); }); diff --git a/packages/lisk-transaction-pool/test/transaction_pool.ts b/packages/lisk-transaction-pool/test/transaction_pool.ts index ed6d0fa1b95..f7633f7e7e7 100644 --- a/packages/lisk-transaction-pool/test/transaction_pool.ts +++ b/packages/lisk-transaction-pool/test/transaction_pool.ts @@ -23,6 +23,7 @@ describe('transaction pool', () => { const transactions = transactionObjects.map(wrapTransferTransaction); const verifiedTransactionsProcessingInterval = 100; const verifiedTransactionsLimitPerProcessing = 100; + const pendingTransactionsProcessingLimit = 5; let transactionPool: TransactionPool; @@ -30,7 +31,8 @@ describe('transaction pool', () => { [key: string]: sinon.SinonStub; }; - let checkTransactionsStub: sinon.SinonStub; + let checkTransactionsWithPassAndFailStub: sinon.SinonStub; + let checkTransactionsWithPassFailAndPendingStub: sinon.SinonStub; let validateTransactionsStub: sinon.SinonStub; let verifyTransactionsStub: sinon.SinonStub; let processTransactionsStub: sinon.SinonStub; @@ -65,9 +67,13 @@ describe('transaction pool', () => { ), }; - checkTransactionsStub = sandbox.stub( + checkTransactionsWithPassAndFailStub = sandbox.stub( checkTransactions, - 'checkTransactions', + 'checkTransactionsWithPassAndFail', + ); + checkTransactionsWithPassFailAndPendingStub = sandbox.stub( + checkTransactions, + 'checkTransactionsWithPassFailAndPending', ); validateTransactionsStub = sandbox.stub(); verifyTransactionsStub = sandbox.stub(); @@ -76,6 +82,7 @@ describe('transaction pool', () => { transactionPool = new TransactionPool({ expireTransactionsInterval, maxTransactionsPerQueue, + pendingTransactionsProcessingLimit, receivedTransactionsProcessingInterval, receivedTransactionsLimitPerProcessing, validateTransactions: validateTransactionsStub, @@ -395,37 +402,55 @@ describe('transaction pool', () => { const processableTransactionsInVerifiedQueue = transactions.slice(0, 1); const unprocesableTransactionsInVerifiedQueue = transactions.slice(1, 2); const transactionsInVerifiedQueue = [ - processableTransactionsInVerifiedQueue, - unprocesableTransactionsInVerifiedQueue, + ...processableTransactionsInVerifiedQueue, + ...unprocesableTransactionsInVerifiedQueue, ]; - const processableTransactionsInReadyQueue = transactions.slice(2, 3); - const unprocessableTransactionsInReadyQueue = transactions.slice(3, 5); + const processableTransactionsInPendingQueue = transactions.slice(2, 3); + const unprocessableTransactionsInPendingQueue = transactions.slice(3, 4); + const unprocessableUnsignedTransactionsInPendingQueue = transactions.slice( + 4, + 5, + ); + const transactionsInPendingQueue = [ + ...processableTransactionsInPendingQueue, + ...unprocessableTransactionsInPendingQueue, + ...unprocessableUnsignedTransactionsInPendingQueue, + ]; + const signedTransactionsInPendingQueue = [ + ...processableTransactionsInPendingQueue, + ...unprocessableTransactionsInPendingQueue, + ]; + const processableTransactionsInReadyQueue = transactions.slice(5, 6); + const unprocessableTransactionsInReadyQueue = transactions.slice(6, 7); const transactionsInReadyQueue = [ - processableTransactionsInReadyQueue, - unprocessableTransactionsInReadyQueue, + ...processableTransactionsInReadyQueue, + ...unprocessableTransactionsInReadyQueue, ]; const processableTransactions = [ ...processableTransactionsInReadyQueue, + ...processableTransactionsInPendingQueue, ...processableTransactionsInVerifiedQueue, ]; const unprocessableTransactions = [ - ...unprocesableTransactionsInVerifiedQueue, ...unprocessableTransactionsInReadyQueue, + ...unprocessableTransactionsInPendingQueue, + ...unprocesableTransactionsInVerifiedQueue, ]; const transactionsToProcess = [ ...transactionsInReadyQueue, + ...signedTransactionsInPendingQueue, ...transactionsInVerifiedQueue, ]; let processVerifiedTransactions: () => Promise< - checkTransactions.CheckTransactionsResponse + checkTransactions.CheckTransactionsResponseWithPassAndFail >; // Dummy functions to check used for assertions in tests const checkForTransactionUnprocessableTransactionId = sandbox.stub(); const checkForTransactionProcessableTransactionId = sandbox.stub(); - const checkTransactionsResponse: checkTransactions.CheckTransactionsResponse = { + const checkTransactionsResponse: checkTransactions.CheckTransactionsResponseWithPassAndFail = { passedTransactions: processableTransactions, failedTransactions: unprocessableTransactions, }; @@ -437,46 +462,59 @@ describe('transaction pool', () => { (transactionPool.queues.verified.size as sinon.SinonStub).returns( transactionsInVerifiedQueue.length, ); + (transactionPool.queues.pending.size as sinon.SinonStub).returns( + transactionsInPendingQueue.length, + ); (transactionPool.queues.verified.peekUntil as sinon.SinonStub).returns( transactionsInVerifiedQueue, ); + (transactionPool.queues.pending.peekUntil as sinon.SinonStub).returns( + transactionsInPendingQueue, + ); (transactionPool.queues.ready.peekUntil as sinon.SinonStub).returns( transactionsInReadyQueue, ); + + (transactionPool.queues.pending.filter as sinon.SinonStub).returns( + signedTransactionsInPendingQueue, + ); processVerifiedTransactions = (transactionPool as any)[ 'processVerifiedTransactions' ].bind(transactionPool); - checkTransactionsStub.resolves(checkTransactionsResponse); + checkTransactionsWithPassAndFailStub.resolves(checkTransactionsResponse); }); - it('should not call checkTransactions if the size of the ready queue is bigger than verifiedTransactionsLimitPerProcessing', async () => { + it('should not call checkTransactionsWithPassAndFail if the size of the ready queue is bigger than verifiedTransactionsLimitPerProcessing', async () => { (transactionPool.queues.ready.size as sinon.SinonStub).returns( verifiedTransactionsLimitPerProcessing + 1, ); await processVerifiedTransactions(); - expect(checkTransactionsStub).to.not.be.called; + expect(checkTransactionsWithPassAndFailStub).to.not.be.called; }); - it('should not call checkTransactions if verified queue is empty', async () => { + it('should not call checkTransactionsWithPassAndFail if verified and pending queues are empty', async () => { (transactionPool.queues.verified.size as sinon.SinonStub).returns(0); + (transactionPool.queues.pending.sizeBy as sinon.SinonStub).returns(0); await processVerifiedTransactions(); - expect(checkTransactionsStub).to.not.be.called; + expect(checkTransactionsWithPassAndFailStub).to.not.be.called; }); - it('should return empty passedTransactions, failedTransactions arrays if checkTransactions is not called', async () => { - (transactionPool.queues.verified.size as sinon.SinonStub).returns(0); - const {passedTransactions, failedTransactions} = await processVerifiedTransactions(); + it('should return empty passedTransactions, failedTransactions arrays if checkTransactionsWithPassAndFail is not called', async () => { + (transactionPool.queues.ready.size as sinon.SinonStub).returns( + verifiedTransactionsLimitPerProcessing + 1, + ); + const { + passedTransactions, + failedTransactions, + } = await processVerifiedTransactions(); expect(passedTransactions).to.deep.equal([]); expect(failedTransactions).to.deep.equal([]); }); - it('should remove unprocessable transactions from the verified and ready queues', async () => { + it('should remove unprocessable transactions from the verified, pending and ready queues', async () => { checkerStubs.checkTransactionForId .onCall(0) .returns(checkForTransactionUnprocessableTransactionId); - checkerStubs.checkTransactionForId - .onCall(1) - .returns(checkForTransactionUnprocessableTransactionId); await processVerifiedTransactions(); expect(checkerStubs.checkTransactionForId.getCall(0)).to.be.calledWith( unprocessableTransactions, @@ -487,23 +525,29 @@ describe('transaction pool', () => { ), ).to.be.calledWith(checkForTransactionUnprocessableTransactionId); - expect(checkerStubs.checkTransactionForId.getCall(1)).to.be.calledWith( - unprocessableTransactions, - ); + expect( + (transactionPool.queues.pending.removeFor as sinon.SinonStub).getCall( + 0, + ), + ).to.be.calledWith(checkForTransactionUnprocessableTransactionId); + expect( (transactionPool.queues.ready.removeFor as sinon.SinonStub).getCall(0), ).to.be.calledWith(checkForTransactionUnprocessableTransactionId); }); - it('should call checkTransactions with transactions and processTransactionsStub', async () => { + it('should call checkTransactionsWithPassAndFail with transactions and processTransactionsStub', async () => { await processVerifiedTransactions(); - expect(checkTransactionsStub.getCall(0)).to.be.calledWith( + expect(checkTransactionsWithPassAndFailStub.getCall(0)).to.be.calledWith( transactionsToProcess, processTransactionsStub, ); }); it('should move processable transactions to the ready queue', async () => { + checkerStubs.checkTransactionForId + .onCall(1) + .returns(checkForTransactionProcessableTransactionId); checkerStubs.checkTransactionForId .onCall(2) .returns(checkForTransactionProcessableTransactionId); @@ -513,10 +557,16 @@ describe('transaction pool', () => { (transactionPool.queues.verified.removeFor as sinon.SinonStub) .onCall(1) .returns(processableTransactions); + (transactionPool.queues.pending.removeFor as sinon.SinonStub) + .onCall(1) + .returns(processableTransactions); (transactionPool.queues.ready.removeFor as sinon.SinonStub) .onCall(1) .returns(processableTransactions); await processVerifiedTransactions(); + expect(checkerStubs.checkTransactionForId.getCall(1)).to.be.calledWith( + processableTransactions, + ); expect(checkerStubs.checkTransactionForId.getCall(2)).to.be.calledWith( processableTransactions, ); @@ -528,6 +578,11 @@ describe('transaction pool', () => { 1, ), ).to.be.calledWith(checkForTransactionProcessableTransactionId); + expect( + (transactionPool.queues.pending.removeFor as sinon.SinonStub).getCall( + 1, + ), + ).to.be.calledWith(checkForTransactionProcessableTransactionId); expect( (transactionPool.queues.ready.removeFor as sinon.SinonStub).getCall(1), ).to.be.calledWith(checkForTransactionProcessableTransactionId); @@ -570,12 +625,12 @@ describe('transaction pool', () => { const checkForTransactionInvalidTransactionId = sandbox.stub(); const checkForTransactionValidTransactionId = sandbox.stub(); - const checkTransactionsResponse: checkTransactions.CheckTransactionsResponse = { + const checkTransactionsResponse: checkTransactions.CheckTransactionsResponseWithPassAndFail = { passedTransactions: validTransactions, failedTransactions: invalidTransactions, }; let validateReceivedTransactions: () => Promise< - checkTransactions.CheckTransactionsResponse + checkTransactions.CheckTransactionsResponseWithPassAndFail >; beforeEach(async () => { @@ -585,7 +640,7 @@ describe('transaction pool', () => { validateReceivedTransactions = (transactionPool as any)[ 'validateReceivedTransactions' ].bind(transactionPool); - checkTransactionsStub.resolves(checkTransactionsResponse); + checkTransactionsWithPassAndFailStub.resolves(checkTransactionsResponse); }); it('should remove invalid transactions from the received queue', async () => { @@ -603,9 +658,9 @@ describe('transaction pool', () => { ).to.be.calledWith(checkForTransactionInvalidTransactionId); }); - it('should call checkTransactions with transactions and validateTransactionsStub', async () => { + it('should call checkTransactionsWithPassAndFail with transactions and validateTransactionsStub', async () => { await validateReceivedTransactions(); - expect(checkTransactionsStub).to.be.calledOnceWith( + expect(checkTransactionsWithPassAndFailStub).to.be.calledOnceWith( transactionsToValidate, validateTransactionsStub, ); @@ -656,21 +711,25 @@ describe('transaction pool', () => { describe('#verifyValidatedTransactions', () => { const verifiableTransactions = transactions.slice(0, 2); - const unverifiableTransactions = transactions.slice(2, 5); + const unverifiableTransactions = transactions.slice(2, 4); + const pendingTransactions = transactions.slice(4, 6); const transactionsToVerify = [ ...verifiableTransactions, ...unverifiableTransactions, + ...pendingTransactions, ]; // Dummy functions to check used for assertions in tests const checkForTransactionUnverifiableTransactionId = sandbox.stub(); const checkForTransactionVerifiableTransactionId = sandbox.stub(); + const checkForTransactionPendingTransactionId = sandbox.stub(); - const checkTransactionsResponse: checkTransactions.CheckTransactionsResponse = { + const checkTransactionsResponse: checkTransactions.CheckTransactionsResponseWithPassFailAndPending = { passedTransactions: verifiableTransactions, failedTransactions: unverifiableTransactions, + pendingTransactions: pendingTransactions, }; let verifyValidatedTransactions: () => Promise< - checkTransactions.CheckTransactionsResponse + checkTransactions.CheckTransactionsResponseWithPassFailAndPending >; beforeEach(async () => { @@ -680,7 +739,9 @@ describe('transaction pool', () => { verifyValidatedTransactions = (transactionPool as any)[ 'verifyValidatedTransactions' ].bind(transactionPool); - checkTransactionsStub.resolves(checkTransactionsResponse); + checkTransactionsWithPassFailAndPendingStub.resolves( + checkTransactionsResponse, + ); }); it('should remove unverifiable transactions from the validated queue', async () => { @@ -698,9 +759,9 @@ describe('transaction pool', () => { ).to.be.calledWith(checkForTransactionUnverifiableTransactionId); }); - it('should call checkTransactions with transactions and verifyTransactionsStub', async () => { + it('should call checkTransactionsWithPassFailAndPendingStub with transactions and verifyTransactionsStub', async () => { await verifyValidatedTransactions(); - expect(checkTransactionsStub).to.be.calledOnceWith( + expect(checkTransactionsWithPassFailAndPendingStub).to.be.calledOnceWith( transactionsToVerify, verifyTransactionsStub, ); @@ -742,7 +803,43 @@ describe('transaction pool', () => { ); }); - it('should return passed and failed transactions', async () => { + it('should move pending transactions to the pending queue', async () => { + checkerStubs.checkTransactionForId + .onCall(2) + .returns(checkForTransactionPendingTransactionId); + (transactionPool.queues.validated.removeFor as sinon.SinonStub) + .onCall(2) + .returns(pendingTransactions); + await verifyValidatedTransactions(); + expect(checkerStubs.checkTransactionForId.getCall(2)).to.be.calledWith( + pendingTransactions, + ); + expect(transactionPool.queues.validated + .removeFor as sinon.SinonStub).to.be.calledWith( + checkForTransactionPendingTransactionId, + ); + expect(transactionPool.queues.pending.enqueueMany).to.be.calledWith( + pendingTransactions, + ); + }); + + it('should not move pending transactions to the pending queue which no longer exist in the validated queue', async () => { + const pendingTransactionsExistingInValidatedQueue = pendingTransactions.slice( + 1, + ); + (transactionPool.queues.validated.removeFor as sinon.SinonStub) + .onCall(2) + .returns(pendingTransactionsExistingInValidatedQueue); + await verifyValidatedTransactions(); + expect(checkerStubs.checkTransactionForId.getCall(2)).to.be.calledWith( + pendingTransactions, + ); + expect(transactionPool.queues.pending.enqueueMany).to.be.calledWith( + pendingTransactionsExistingInValidatedQueue, + ); + }); + + it('should return passed, failed and pending transactions', async () => { expect(await verifyValidatedTransactions()).to.deep.equal( checkTransactionsResponse, ); diff --git a/packages/lisk-transaction-pool/test/utils/add_transaction_functions.ts b/packages/lisk-transaction-pool/test/utils/add_transaction_functions.ts index 1cb573c605e..03f7dfc4537 100644 --- a/packages/lisk-transaction-pool/test/utils/add_transaction_functions.ts +++ b/packages/lisk-transaction-pool/test/utils/add_transaction_functions.ts @@ -1,12 +1,21 @@ -import { TransactionObject } from '../../src/transaction_pool'; +import { TransactionObject, Transaction } from '../../src/transaction_pool'; +import { TransactionResponse, Status } from '../../src/check_transactions'; export const wrapTransferTransaction = ( transferTransaction: TransactionObject, -) => { +): Transaction => { return { ...transferTransaction, - containsUniqueData: () => false, + containsUniqueData: false, verifyAgainstOtherTransactions: () => true, isExpired: (time: Date) => time.getTime() > 0, + isReady: () => true, + addVerifiedSignature: (signature: string): TransactionResponse => { + return { + status: Status.OK, + errors: [], + id: signature, + }; + }, }; };