diff --git a/__tests__/integration/core-transaction-pool/guard.test.ts b/__tests__/integration/core-transaction-pool/processor.test.ts similarity index 55% rename from __tests__/integration/core-transaction-pool/guard.test.ts rename to __tests__/integration/core-transaction-pool/processor.test.ts index 6d7042985e..1533b0cf65 100644 --- a/__tests__/integration/core-transaction-pool/guard.test.ts +++ b/__tests__/integration/core-transaction-pool/processor.test.ts @@ -4,23 +4,24 @@ import { Container } from "@arkecosystem/core-interfaces"; import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { Blocks, Identities, Interfaces, Utils } from "@arkecosystem/crypto"; import { generateMnemonic } from "bip39"; +import { Processor } from "../../../packages/core-transaction-pool/src/processor"; import { TransactionFactory } from "../../helpers/transaction-factory"; import { delegates, genesisBlock, wallets, wallets2ndSig } from "../../utils/fixtures/unitnet"; import { generateWallets } from "../../utils/generators/wallets"; import { setUpFull, tearDownFull } from "./__support__/setup"; - -let TransactionGuard; +// import { Crypto, Enums, Managers } from "@arkecosystem/crypto"; +// import { Connection } from "../../../packages/core-transaction-pool/src/connection"; +// import { MemoryTransaction } from "../../../packages/core-transaction-pool/src/memory-transaction"; +// import { delegates, wallets } from "../../utils/fixtures/unitnet"; let container: Container.IContainer; -let guard; +let processor; let transactionPool; let blockchain; beforeAll(async () => { container = await setUpFull(); - TransactionGuard = require("../../../packages/core-transaction-pool/src").TransactionGuard; - transactionPool = container.resolvePlugin("transaction-pool"); blockchain = container.resolvePlugin("blockchain"); }); @@ -31,7 +32,7 @@ afterAll(async () => { beforeEach(() => { transactionPool.flush(); - guard = new TransactionGuard(transactionPool); + processor = transactionPool.createProcessor(); }); describe("Transaction Guard", () => { @@ -79,7 +80,7 @@ describe("Transaction Guard", () => { .withPassphrase(t.from.passphrase) .build()[0]; - await guard.validate([transferTx.data]); + await processor.validate([transferTx.data]); } // apply again transfer from 0 to 1 @@ -88,13 +89,13 @@ describe("Transaction Guard", () => { .withPassphrase(transfer1.from.passphrase) .build()[0]; - await guard.validate([transfer.data]); + await processor.validate([transfer.data]); const expectedError = { message: '["Cold wallet is not allowed to send until receiving transaction is confirmed."]', type: "ERR_APPLY", }; - expect(guard.errors[transfer.id]).toContainEqual(expectedError); + expect(processor.errors[transfer.id]).toContainEqual(expectedError); // check final balances expect(+delegateWallet.balance).toBe(delegate.balance - (100 + 0.1) * satoshi); @@ -122,7 +123,7 @@ describe("Transaction Guard", () => { .withPassphrase(delegate0.secret) .build(); - await guard.validate(transfers.map(tx => tx.data)); + await processor.validate(transfers.map(tx => tx.data)); expect(+delegateWallet.balance).toBe(+delegate0.balance); expect(+newWallet.balance).toBe(0); @@ -146,8 +147,8 @@ describe("Transaction Guard", () => { .withFee(fee) .withPassphrase(delegate1.secret) .build(); - await guard.validate(transfers.map(tx => tx.data)); - expect(guard.errors).toEqual({}); + await processor.validate(transfers.map(tx => tx.data)); + expect(processor.errors).toEqual({}); // simulate forged transaction const transactionHandler = TransactionHandlerRegistry.get(transfers[0].type); @@ -168,7 +169,7 @@ describe("Transaction Guard", () => { expect(+delegateWallet.balance).toBe(+delegate2.balance); expect(+newWallet.balance).toBe(0); - expect(guard.errors).toEqual({}); + expect(processor.errors).toEqual({}); const amount1 = +delegateWallet.balance / 2; const fee = 0.1 * 10 ** 8; @@ -201,21 +202,21 @@ describe("Transaction Guard", () => { }); // first validate the 1st transfer so that new wallet is updated with the amount - await guard.validate(transfers.map(tx => tx.data)); + await processor.validate(transfers.map(tx => tx.data)); // simulate forged transaction const transactionHandler = TransactionHandlerRegistry.get(transfers[0].type); transactionHandler.applyToRecipient(transfers[0], newWallet); - expect(guard.errors).toEqual({}); + expect(processor.errors).toEqual({}); expect(+newWallet.balance).toBe(amount1); - // reset guard, if not the 1st transaction will still be in this.accept and mess up - guard = new TransactionGuard(transactionPool); + // reset processor, if not the 1st transaction will still be in this.accept and mess up + processor = new Processor(transactionPool); - await guard.validate([votes[0].data, delegateRegs[0].data, signatures[0].data]); + await processor.validate([votes[0].data, delegateRegs[0].data, signatures[0].data]); - expect(guard.errors).toEqual({}); + expect(processor.errors).toEqual({}); expect(+delegateWallet.balance).toBe(+delegate2.balance - amount1 - fee); expect(+newWallet.balance).toBe(amount1 - voteFee - delegateRegFee - signatureFee); }); @@ -242,7 +243,7 @@ describe("Transaction Guard", () => { .withNetwork("unitnet") .withPassphrase(delegate3.secret) .build(); - await guard.validate(transfers1.map(tx => tx.data)); + await processor.validate(transfers1.map(tx => tx.data)); // simulate forged transaction const transactionHandler = TransactionHandlerRegistry.get(transfers1[0].type); @@ -257,7 +258,7 @@ describe("Transaction Guard", () => { .withNetwork("unitnet") .withPassphrase(newWalletPassphrase) .build(); - await guard.validate(transfers2.map(tx => tx.data)); + await processor.validate(transfers2.map(tx => tx.data)); // simulate forged transaction transactionHandler.applyToRecipient(transfers2[0], delegateWallet); @@ -288,7 +289,7 @@ describe("Transaction Guard", () => { ]; for (const transaction of allTransactions) { - await guard.validate(transaction.map(tx => tx.data)); + await processor.validate(transaction.map(tx => tx.data)); const errorExpected = [ { @@ -296,7 +297,7 @@ describe("Transaction Guard", () => { type: "ERR_APPLY", }, ]; - expect(guard.errors[transaction[0].id]).toEqual(errorExpected); + expect(processor.errors[transaction[0].id]).toEqual(errorExpected); expect(+delegateWallet.balance).toBe(+delegate3.balance - amount1 - fee + amount2); expect(+newWallet.balance).toBe(amount1 - amount2 - fee); @@ -310,7 +311,7 @@ describe("Transaction Guard", () => { .withPassphrase(delegates[0].secret) .create(2); - const result = await guard.validate(transactions); + const result = await processor.validate(transactions); expect(result.errors[transactions[1].id]).toEqual([ { @@ -340,7 +341,7 @@ describe("Transaction Guard", () => { // we change the receiver in lastTransaction to prevent having 2 exact // same transactions with same id (if not, could be same as transactions[0]) - const result = await guard.validate(transactions.concat(lastTransaction)); + const result = await processor.validate(transactions.concat(lastTransaction)); expect(result.errors).toEqual(null); }); @@ -371,7 +372,7 @@ describe("Transaction Guard", () => { const allTransactions = transactions.concat(lastTransaction); - const result = await guard.validate(allTransactions); + const result = await processor.validate(allTransactions); expect(Object.keys(result.errors).length).toBe(1); expect(result.errors[lastTransaction[0].id]).toEqual([ @@ -394,7 +395,7 @@ describe("Transaction Guard", () => { const transactionId = transactions[0].id; transactions[0].id = "a".repeat(64); - const result = await guard.validate(transactions); + const result = await processor.validate(transactions); expect(result.accept).toEqual([transactionId]); expect(result.broadcast).toEqual([transactionId]); expect(result.errors).toBeNull(); @@ -412,11 +413,11 @@ describe("Transaction Guard", () => { .build()[0], ]; - const result = await guard.validate(delegateRegistrations.map(transaction => transaction.data)); + const result = await processor.validate(delegateRegistrations.map(transaction => transaction.data)); expect(result.invalid).toEqual(delegateRegistrations.map(transaction => transaction.id)); delegateRegistrations.forEach(tx => { - expect(guard.errors[tx.id]).toEqual([ + expect(processor.errors[tx.id]).toEqual([ { type: "ERR_CONFLICT", message: `Multiple delegate registrations for "${ @@ -466,7 +467,7 @@ describe("Transaction Guard", () => { senderPublicKey: wallets2ndSig[1].keys.publicKey, }), ]; - const result = await guard.validate(modifiedTransactions); + const result = await processor.validate(modifiedTransactions); const expectedErrors = [ ...[...transfers, ...transfers2ndSigned].map(transfer => [ @@ -530,7 +531,7 @@ describe("Transaction Guard", () => { senderPublicKey: wallets2ndSig[50].keys.publicKey, }), ]; - const result = await guard.validate(modifiedTransactions); + const result = await processor.validate(modifiedTransactions); const expectedErrors = [ ...[...delegateRegs, ...delegateRegs2ndSigned].map(transfer => [ @@ -586,7 +587,7 @@ describe("Transaction Guard", () => { senderPublicKey: wallets2ndSig[50].keys.publicKey, }), ]; - const result = await guard.validate(modifiedTransactions); + const result = await processor.validate(modifiedTransactions); const expectedErrors = [ ...votes.map(tx => [tx.id, "ERR_BAD_DATA", "Transaction didn't pass the verification process."]), @@ -627,7 +628,7 @@ describe("Transaction Guard", () => { const modifiedTransactions = modifiedFields2ndSig.map((objField, index) => Object.assign({}, secondSigs[index], objField), ); - const result = await guard.validate(modifiedTransactions); + const result = await processor.validate(modifiedTransactions); expect( Object.keys(result.errors).map(id => [id, result.errors[id][0].type, result.errors[id][0].message]), @@ -693,7 +694,7 @@ describe("Transaction Guard", () => { .create(); await addBlock(transfers); - const result = await guard.validate(transfers); + const result = await processor.validate(transfers); expect(result.errors).toEqual(forgedErrorMessage(transfers[0].id)); }); @@ -708,7 +709,7 @@ describe("Transaction Guard", () => { const realTransferId = transfers[0].id; transfers[0].id = "c".repeat(64); - const result = await guard.validate(transfers); + const result = await processor.validate(transfers); expect(result.errors).toEqual(forgedErrorMessage(realTransferId)); }); @@ -721,7 +722,9 @@ describe("Transaction Guard", () => { .withNetwork("unitnet") .withPassphrase(wallets[10].passphrase) .build(); - expect(guard.__cacheTransactions(transactions.map(tx => tx.data))).toEqual(transactions.map(tx => tx.data)); + expect(processor.__cacheTransactions(transactions.map(tx => tx.data))).toEqual( + transactions.map(tx => tx.data), + ); }); it("should not add a transaction already in cache and add it as an error", () => { @@ -729,9 +732,11 @@ describe("Transaction Guard", () => { .withNetwork("unitnet") .withPassphrase(wallets[11].passphrase) .build(); - expect(guard.__cacheTransactions(transactions.map(tx => tx.data))).toEqual(transactions.map(tx => tx.data)); - expect(guard.__cacheTransactions([transactions[0].data])).toEqual([]); - expect(guard.errors).toEqual({ + expect(processor.__cacheTransactions(transactions.map(tx => tx.data))).toEqual( + transactions.map(tx => tx.data), + ); + expect(processor.__cacheTransactions([transactions[0].data])).toEqual([]); + expect(processor.errors).toEqual({ [transactions[0].id]: [ { message: "Already in cache.", @@ -741,4 +746,525 @@ describe("Transaction Guard", () => { }); }); }); + + // describe("__cacheTransactions", () => { + // it("should add transactions to cache", () => { + // const transactions = TransactionFactory.transfer(wallets[11].address, 35) + // .withNetwork("unitnet") + // .withPassphrase(wallets[10].passphrase) + // .create(3); + // jest.spyOn(state, "cacheTransactions").mockReturnValueOnce({ added: transactions, notAdded: [] }); + + // expect(processor.__cacheTransactions(transactions)).toEqual(transactions); + // }); + + // it("should not add a transaction already in cache and add it as an error", () => { + // const transactions = TransactionFactory.transfer(wallets[12].address, 35) + // .withNetwork("unitnet") + // .withPassphrase(wallets[11].passphrase) + // .create(3); + + // jest.spyOn(state, "cacheTransactions") + // .mockReturnValueOnce({ added: transactions, notAdded: [] }) + // .mockReturnValueOnce({ added: [], notAdded: [transactions[0]] }); + + // expect(processor.__cacheTransactions(transactions)).toEqual(transactions); + // expect(processor.__cacheTransactions([transactions[0]])).toEqual([]); + // expect(processor.errors).toEqual({ + // [transactions[0].id]: [ + // { + // message: "Already in cache.", + // type: "ERR_DUPLICATE", + // }, + // ], + // }); + // }); + // }); + + // describe("getBroadcastTransactions", () => { + // it("should return broadcast transaction", async () => { + // const transactions = TransactionFactory.transfer(wallets[11].address, 25) + // .withNetwork("unitnet") + // .withPassphrase(wallets[10].passphrase) + // .build(3); + + // jest.spyOn(state, "cacheTransactions").mockReturnValueOnce({ added: transactions, notAdded: [] }); + + // for (const tx of transactions) { + // processor.broadcast.set(tx.id, tx); + // } + + // expect(processor.getBroadcastTransactions()).toEqual(transactions); + // }); + // }); + + // describe("__filterAndTransformTransactions", () => { + // it("should reject duplicate transactions", () => { + // const transactionExists = processor.pool.transactionExists; + // processor.pool.transactionExists = jest.fn(() => true); + + // const tx = { id: "1" }; + // processor.__filterAndTransformTransactions([tx]); + + // expect(processor.errors[tx.id]).toEqual([ + // { + // message: `Duplicate transaction ${tx.id}`, + // type: "ERR_DUPLICATE", + // }, + // ]); + + // processor.pool.transactionExists = transactionExists; + // }); + + // it("should reject blocked senders", () => { + // const transactionExists = processor.pool.transactionExists; + // processor.pool.transactionExists = jest.fn(() => false); + // const isSenderBlocked = processor.pool.isSenderBlocked; + // processor.pool.isSenderBlocked = jest.fn(() => true); + + // const tx = { id: "1", senderPublicKey: "affe" }; + // processor.__filterAndTransformTransactions([tx]); + + // expect(processor.errors[tx.id]).toEqual([ + // { + // message: `Transaction ${tx.id} rejected. Sender ${tx.senderPublicKey} is blocked.`, + // type: "ERR_SENDER_BLOCKED", + // }, + // ]); + + // processor.pool.isSenderBlocked = isSenderBlocked; + // processor.pool.transactionExists = transactionExists; + // }); + + // it("should reject transactions that are too large", () => { + // const tx = TransactionFactory.transfer(wallets[12].address) + // .withNetwork("unitnet") + // .withPassphrase(wallets[11].passphrase) + // .build(3)[0]; + + // // @FIXME: Uhm excuse me, what the? + // tx.data.signatures = [""]; + // for (let i = 0; i < transactionPool.options.maxTransactionBytes; i++) { + // // @ts-ignore + // tx.data.signatures += "1"; + // } + // processor.__filterAndTransformTransactions([tx]); + + // expect(processor.errors[tx.id]).toEqual([ + // { + // message: `Transaction ${tx.id} is larger than ${ + // transactionPool.options.maxTransactionBytes + // } bytes.`, + // type: "ERR_TOO_LARGE", + // }, + // ]); + // }); + + // it("should reject transactions from the future", () => { + // const now = 47157042; // seconds since genesis block + // const transactionExists = processor.pool.transactionExists; + // processor.pool.transactionExists = jest.fn(() => false); + // const getTime = Crypto.slots.getTime; + // Crypto.slots.getTime = jest.fn(() => now); + + // const secondsInFuture = 3601; + // const tx = { + // id: "1", + // senderPublicKey: "affe", + // timestamp: Crypto.slots.getTime() + secondsInFuture, + // }; + // processor.__filterAndTransformTransactions([tx]); + + // expect(processor.errors[tx.id]).toEqual([ + // { + // message: `Transaction ${tx.id} is ${secondsInFuture} seconds in the future`, + // type: "ERR_FROM_FUTURE", + // }, + // ]); + + // Crypto.slots.getTime = getTime; + // processor.pool.transactionExists = transactionExists; + // }); + + // it("should accept transaction with correct network byte", () => { + // const transactionExists = processor.pool.transactionExists; + // processor.pool.transactionExists = jest.fn(() => false); + + // const canApply = processor.pool.walletManager.canApply; + // processor.pool.walletManager.canApply = jest.fn(() => true); + + // const tx = { + // id: "1", + // network: 23, + // type: Enums.TransactionTypes.Transfer, + // senderPublicKey: "023ee98f453661a1cb765fd60df95b4efb1e110660ffb88ae31c2368a70f1f7359", + // recipientId: "DEJHR83JFmGpXYkJiaqn7wPGztwjheLAmY", + // }; + // processor.__filterAndTransformTransactions([tx]); + + // expect(processor.errors[tx.id]).not.toEqual([ + // { + // message: `Transaction network '${tx.network}' does not match '${Managers.configManager.get( + // "pubKeyHash", + // )}'`, + // type: "ERR_WRONG_NETWORK", + // }, + // ]); + + // processor.pool.transactionExists = transactionExists; + // processor.pool.walletManager.canApply = canApply; + // }); + + // it("should accept transaction with missing network byte", () => { + // const transactionExists = processor.pool.transactionExists; + // processor.pool.transactionExists = jest.fn(() => false); + + // const canApply = processor.pool.walletManager.canApply; + // processor.pool.walletManager.canApply = jest.fn(() => true); + + // const tx = { + // id: "1", + // type: Enums.TransactionTypes.Transfer, + // senderPublicKey: "023ee98f453661a1cb765fd60df95b4efb1e110660ffb88ae31c2368a70f1f7359", + // recipientId: "DEJHR83JFmGpXYkJiaqn7wPGztwjheLAmY", + // }; + // processor.__filterAndTransformTransactions([tx]); + + // expect(processor.errors[tx.id].type).not.toEqual("ERR_WRONG_NETWORK"); + + // processor.pool.transactionExists = transactionExists; + // processor.pool.walletManager.canApply = canApply; + // }); + + // it("should not accept transaction with wrong network byte", () => { + // const transactionExists = processor.pool.transactionExists; + // processor.pool.transactionExists = jest.fn(() => false); + + // const canApply = processor.pool.walletManager.canApply; + // processor.pool.walletManager.canApply = jest.fn(() => true); + + // const tx = { + // id: "1", + // network: 2, + // senderPublicKey: "023ee98f453661a1cb765fd60df95b4efb1e110660ffb88ae31c2368a70f1f7359", + // }; + // processor.__filterAndTransformTransactions([tx]); + + // expect(processor.errors[tx.id]).toEqual([ + // { + // message: `Transaction network '${tx.network}' does not match '${Managers.configManager.get( + // "pubKeyHash", + // )}'`, + // type: "ERR_WRONG_NETWORK", + // }, + // ]); + + // processor.pool.transactionExists = transactionExists; + // processor.pool.walletManager.canApply = canApply; + // }); + + // it("should not accept transaction if pool hasExceededMaxTransactions and add it to excess", () => { + // const transactions = TransactionFactory.transfer(wallets[11].address, 35) + // .withNetwork("unitnet") + // .withPassphrase(wallets[10].passphrase) + // .create(3); + + // jest.spyOn(processor.pool, "hasExceededMaxTransactions").mockImplementationOnce(tx => true); + + // processor.__filterAndTransformTransactions(transactions); + + // expect(processor.excess).toEqual([transactions[0].id]); + // expect(processor.accept).toEqual(new Map()); + // expect(processor.broadcast).toEqual(new Map()); + // }); + + // it("should push a ERR_UNKNOWN error if something threw in validated transaction block", () => { + // const transactions = TransactionFactory.transfer(wallets[11].address, 35) + // .withNetwork("unitnet") + // .withPassphrase(wallets[10].passphrase) + // .build(3); + + // // use processor.accept.set() call to introduce a throw + // jest.spyOn(processor.pool.walletManager, "canApply").mockImplementationOnce(() => { + // throw new Error("hey"); + // }); + + // processor.__filterAndTransformTransactions(transactions.map(tx => tx.data)); + + // expect(processor.accept).toEqual(new Map()); + // expect(processor.broadcast).toEqual(new Map()); + // expect(processor.errors[transactions[0].id]).toEqual([ + // { + // message: `hey`, + // type: "ERR_UNKNOWN", + // }, + // ]); + // }); + // }); + + // describe("__validateTransaction", () => { + // it("should not validate when recipient is not on the same network", async () => { + // const transactions = TransactionFactory.transfer("DEJHR83JFmGpXYkJiaqn7wPGztwjheLAmY", 35) + // .withNetwork("unitnet") + // .withPassphrase(wallets[10].passphrase) + // .create(3); + + // expect(processor.__validateTransaction(transactions[0])).toBeFalse(); + // expect(processor.errors).toEqual({ + // [transactions[0].id]: [ + // { + // type: "ERR_INVALID_RECIPIENT", + // message: `Recipient ${ + // transactions[0].recipientId + // } is not on the same network: ${Managers.configManager.get("network.pubKeyHash")}`, + // }, + // ], + // }); + // }); + + // it("should not validate a delegate registration if an existing registration for the same username from a different wallet exists in the pool", async () => { + // const delegateRegistrations = [ + // TransactionFactory.delegateRegistration("test_delegate") + // .withNetwork("unitnet") + // .withPassphrase(wallets[16].passphrase) + // .build()[0], + // TransactionFactory.delegateRegistration("test_delegate") + // .withNetwork("unitnet") + // .withPassphrase(wallets[17].passphrase) + // .build()[0], + // ]; + // const memPoolTx = new MemPoolTransaction(delegateRegistrations[0]); + // jest.spyOn(processor.pool, "getTransactionsByType").mockReturnValueOnce(new Set([memPoolTx])); + + // expect(processor.__validateTransaction(delegateRegistrations[1].data)).toBeFalse(); + // expect(processor.errors[delegateRegistrations[1].id]).toEqual([ + // { + // type: "ERR_PENDING", + // message: `Delegate registration for "${ + // delegateRegistrations[1].data.asset.delegate.username + // }" already in the pool`, + // }, + // ]); + // }); + + // it("should not validate when sender has same type transactions in the pool (only for 2nd sig, delegate registration, vote)", async () => { + // jest.spyOn(processor.pool.walletManager, "canApply").mockImplementation(() => true); + // jest.spyOn(processor.pool, "senderHasTransactionsOfType").mockReturnValue(true); + // const vote = TransactionFactory.vote(delegates[0].publicKey) + // .withNetwork("unitnet") + // .withPassphrase(wallets[10].passphrase) + // .build()[0]; + + // const delegateReg = TransactionFactory.delegateRegistration() + // .withNetwork("unitnet") + // .withPassphrase(wallets[11].passphrase) + // .build()[0]; + + // const signature = TransactionFactory.secondSignature(wallets[12].passphrase) + // .withNetwork("unitnet") + // .withPassphrase(wallets[12].passphrase) + // .build()[0]; + + // for (const tx of [vote, delegateReg, signature]) { + // expect(processor.__validateTransaction(tx.data)).toBeFalse(); + // expect(processor.errors[tx.id]).toEqual([ + // { + // type: "ERR_PENDING", + // message: + // `Sender ${tx.data.senderPublicKey} already has a transaction of type ` + + // `'${Enums.TransactionTypes[tx.type]}' in the pool`, + // }, + // ]); + // } + + // jest.restoreAllMocks(); + // }); + + // it("should not validate unsupported transaction types", async () => { + // jest.spyOn(processor.pool.walletManager, "canApply").mockImplementation(() => true); + + // // use a random transaction as a base - then play with type + // const baseTransaction = TransactionFactory.delegateRegistration() + // .withNetwork("unitnet") + // .withPassphrase(wallets[11].passphrase) + // .build()[0]; + + // for (const transactionType of [ + // Enums.TransactionTypes.MultiSignature, + // Enums.TransactionTypes.Ipfs, + // Enums.TransactionTypes.TimelockTransfer, + // Enums.TransactionTypes.MultiPayment, + // Enums.TransactionTypes.DelegateResignation, + // 99, + // ]) { + // baseTransaction.data.type = transactionType; + // // @FIXME: Uhm excuse me, what the? + // // @ts-ignore + // baseTransaction.data.id = transactionType; + + // expect(processor.__validateTransaction(baseTransaction)).toBeFalse(); + // expect(processor.errors[baseTransaction.id]).toEqual([ + // { + // type: "ERR_UNSUPPORTED", + // message: `Invalidating transaction of unsupported type '${ + // Enums.TransactionTypes[transactionType] + // }'`, + // }, + // ]); + // } + + // jest.restoreAllMocks(); + // }); + // }); + + // describe("__removeForgedTransactions", () => { + // it("should remove forged transactions", async () => { + // const transfers = TransactionFactory.transfer(delegates[0].senderPublicKey) + // .withNetwork("unitnet") + // .withPassphrase(delegates[0].secret) + // .build(4); + + // transfers.forEach(tx => { + // processor.accept.set(tx.id, tx); + // processor.broadcast.set(tx.id, tx); + // }); + + // const forgedTx = transfers[2]; + // jest.spyOn(database, "getForgedTransactionsIds").mockReturnValueOnce([forgedTx.id]); + + // await processor.__removeForgedTransactions(); + + // expect(processor.accept.size).toBe(3); + // expect(processor.broadcast.size).toBe(3); + + // expect(processor.errors[forgedTx.id]).toHaveLength(1); + // expect(processor.errors[forgedTx.id][0].type).toEqual("ERR_FORGED"); + // }); + // }); + + // describe("__addTransactionsToPool", () => { + // it("should add transactions to the pool", () => { + // const transfers = TransactionFactory.transfer(delegates[0].senderPublicKey) + // .withNetwork("unitnet") + // .withPassphrase(delegates[0].secret) + // .create(4); + + // transfers.forEach(tx => { + // processor.accept.set(tx.id, tx); + // processor.broadcast.set(tx.id, tx); + // }); + + // expect(processor.errors).toEqual({}); + // jest.spyOn(processor.pool, "addTransactions").mockReturnValueOnce({ added: transfers, notAdded: [] }); + + // processor.__addTransactionsToPool(); + + // expect(processor.errors).toEqual({}); + // expect(processor.accept.size).toBe(4); + // expect(processor.broadcast.size).toBe(4); + // }); + + // it("should delete from accept and broadcast transactions that were not added to the pool", () => { + // const added = TransactionFactory.transfer(delegates[0].address) + // .withNetwork("unitnet") + // .withPassphrase(delegates[0].secret) + // .build(2); + // const notAddedError = { type: "ERR_TEST", message: "" }; + // const notAdded = TransactionFactory.transfer(delegates[1].address) + // .withNetwork("unitnet") + // .withPassphrase(delegates[0].secret) + // .build(2) + // .map(tx => ({ + // transaction: tx, + // ...notAddedError, + // })); + + // added.forEach(tx => { + // processor.accept.set(tx.id, tx); + // processor.broadcast.set(tx.id, tx); + // }); + // notAdded.forEach(tx => { + // processor.accept.set(tx.transaction.id, tx); + // processor.broadcast.set(tx.transaction.id, tx); + // }); + + // jest.spyOn(processor.pool, "addTransactions").mockReturnValueOnce({ added, notAdded }); + // processor.__addTransactionsToPool(); + + // expect(processor.accept.size).toBe(2); + // expect(processor.broadcast.size).toBe(2); + + // expect(processor.errors[notAdded[0].transaction.id]).toEqual([notAddedError]); + // expect(processor.errors[notAdded[1].transaction.id]).toEqual([notAddedError]); + // }); + + // it("should delete from accept but keep in broadcast transactions that were not added to the pool because of ERR_POOL_FULL", () => { + // const added = TransactionFactory.transfer(delegates[0].address) + // .withNetwork("unitnet") + // .withPassphrase(delegates[0].secret) + // .build(2); + + // const notAddedError = { type: "ERR_POOL_FULL", message: "" }; + // const notAdded = TransactionFactory.transfer(delegates[1].address) + // .withNetwork("unitnet") + // .withPassphrase(delegates[0].secret) + // .build(2) + // .map(tx => ({ + // transaction: tx, + // ...notAddedError, + // })); + + // added.forEach(tx => { + // processor.accept.set(tx.id, tx); + // processor.broadcast.set(tx.id, tx); + // }); + // notAdded.forEach(tx => { + // processor.accept.set(tx.transaction.id, tx); + // processor.broadcast.set(tx.transaction.id, tx); + // }); + + // jest.spyOn(processor.pool, "addTransactions").mockReturnValueOnce({ added, notAdded }); + // processor.__addTransactionsToPool(); + + // expect(processor.accept.size).toBe(2); + // expect(processor.broadcast.size).toBe(4); + + // expect(processor.errors[notAdded[0].transaction.id]).toEqual([notAddedError]); + // expect(processor.errors[notAdded[1].transaction.id]).toEqual([notAddedError]); + // }); + // }); + + // describe("pushError", () => { + // it("should have error for transaction", () => { + // expect(processor.errors).toBeEmpty(); + + // processor.pushError({ id: 1 }, "ERR_INVALID", "Invalid."); + + // expect(processor.errors).toBeObject(); + // expect(processor.errors["1"]).toBeArray(); + // expect(processor.errors["1"]).toHaveLength(1); + // expect(processor.errors["1"]).toEqual([{ message: "Invalid.", type: "ERR_INVALID" }]); + + // expect(processor.invalid.size).toEqual(1); + // expect(processor.invalid.entries().next().value[1]).toEqual({ id: 1 }); + // }); + + // it("should have multiple errors for transaction", () => { + // expect(processor.errors).toBeEmpty(); + + // processor.pushError({ id: 1 }, "ERR_INVALID", "Invalid 1."); + // processor.pushError({ id: 1 }, "ERR_INVALID", "Invalid 2."); + + // expect(processor.errors).toBeObject(); + // expect(processor.errors["1"]).toBeArray(); + // expect(processor.errors["1"]).toHaveLength(2); + // expect(processor.errors["1"]).toEqual([ + // { message: "Invalid 1.", type: "ERR_INVALID" }, + // { message: "Invalid 2.", type: "ERR_INVALID" }, + // ]); + + // expect(processor.invalid.size).toEqual(1); + // expect(processor.invalid.entries().next().value[1]).toEqual({ id: 1 }); + // }); + // }); }); diff --git a/__tests__/unit/core-transaction-pool/connection.test.ts b/__tests__/unit/core-transaction-pool/connection.test.ts index 494657d975..24d7a71440 100644 --- a/__tests__/unit/core-transaction-pool/connection.test.ts +++ b/__tests__/unit/core-transaction-pool/connection.test.ts @@ -10,7 +10,7 @@ import cloneDeep from "lodash.clonedeep"; import randomSeed from "random-seed"; import { Connection } from "../../../packages/core-transaction-pool/src"; import { defaults } from "../../../packages/core-transaction-pool/src/defaults"; -import { MemPoolTransaction } from "../../../packages/core-transaction-pool/src/mem-pool-transaction"; +import { MemPoolTransaction } from "../../../packages/core-transaction-pool/src/pool-transaction"; import { TransactionFactory } from "../../helpers/transaction-factory"; import { block2, delegates } from "../../utils/fixtures/unitnet"; import { transactions as mockData } from "./__fixtures__/transactions"; diff --git a/__tests__/unit/core-transaction-pool/guard.test.ts b/__tests__/unit/core-transaction-pool/guard.test.ts deleted file mode 100644 index 8f4158f773..0000000000 --- a/__tests__/unit/core-transaction-pool/guard.test.ts +++ /dev/null @@ -1,550 +0,0 @@ -import "./mocks/core-container"; - -import { Crypto, Enums, Managers } from "@arkecosystem/crypto"; -import "jest-extended"; -import { Connection } from "../../../packages/core-transaction-pool/src/connection"; -import { defaults } from "../../../packages/core-transaction-pool/src/defaults"; -import { TransactionGuard } from "../../../packages/core-transaction-pool/src/guard"; -import { MemPoolTransaction } from "../../../packages/core-transaction-pool/src/mem-pool-transaction"; -import { TransactionFactory } from "../../helpers/transaction-factory"; -import { delegates, wallets } from "../../utils/fixtures/unitnet"; -import { database } from "./mocks/database"; -import { state } from "./mocks/state"; - -let guard; -let transactionPool; - -beforeAll(async () => { - transactionPool = new Connection(defaults); - - await transactionPool.make(); -}); - -beforeEach(async () => { - transactionPool.flush(); - - guard = new TransactionGuard(transactionPool); -}); - -describe("Transaction Guard", () => { - describe("__cacheTransactions", () => { - it("should add transactions to cache", () => { - const transactions = TransactionFactory.transfer(wallets[11].address, 35) - .withNetwork("unitnet") - .withPassphrase(wallets[10].passphrase) - .create(3); - jest.spyOn(state, "cacheTransactions").mockReturnValueOnce({ added: transactions, notAdded: [] }); - - expect(guard.__cacheTransactions(transactions)).toEqual(transactions); - }); - - it("should not add a transaction already in cache and add it as an error", () => { - const transactions = TransactionFactory.transfer(wallets[12].address, 35) - .withNetwork("unitnet") - .withPassphrase(wallets[11].passphrase) - .create(3); - - jest.spyOn(state, "cacheTransactions") - .mockReturnValueOnce({ added: transactions, notAdded: [] }) - .mockReturnValueOnce({ added: [], notAdded: [transactions[0]] }); - - expect(guard.__cacheTransactions(transactions)).toEqual(transactions); - expect(guard.__cacheTransactions([transactions[0]])).toEqual([]); - expect(guard.errors).toEqual({ - [transactions[0].id]: [ - { - message: "Already in cache.", - type: "ERR_DUPLICATE", - }, - ], - }); - }); - }); - - describe("getBroadcastTransactions", () => { - it("should return broadcast transaction", async () => { - const transactions = TransactionFactory.transfer(wallets[11].address, 25) - .withNetwork("unitnet") - .withPassphrase(wallets[10].passphrase) - .build(3); - - jest.spyOn(state, "cacheTransactions").mockReturnValueOnce({ added: transactions, notAdded: [] }); - - for (const tx of transactions) { - guard.broadcast.set(tx.id, tx); - } - - expect(guard.getBroadcastTransactions()).toEqual(transactions); - }); - }); - - describe("__filterAndTransformTransactions", () => { - it("should reject duplicate transactions", () => { - const transactionExists = guard.pool.transactionExists; - guard.pool.transactionExists = jest.fn(() => true); - - const tx = { id: "1" }; - guard.__filterAndTransformTransactions([tx]); - - expect(guard.errors[tx.id]).toEqual([ - { - message: `Duplicate transaction ${tx.id}`, - type: "ERR_DUPLICATE", - }, - ]); - - guard.pool.transactionExists = transactionExists; - }); - - it("should reject blocked senders", () => { - const transactionExists = guard.pool.transactionExists; - guard.pool.transactionExists = jest.fn(() => false); - const isSenderBlocked = guard.pool.isSenderBlocked; - guard.pool.isSenderBlocked = jest.fn(() => true); - - const tx = { id: "1", senderPublicKey: "affe" }; - guard.__filterAndTransformTransactions([tx]); - - expect(guard.errors[tx.id]).toEqual([ - { - message: `Transaction ${tx.id} rejected. Sender ${tx.senderPublicKey} is blocked.`, - type: "ERR_SENDER_BLOCKED", - }, - ]); - - guard.pool.isSenderBlocked = isSenderBlocked; - guard.pool.transactionExists = transactionExists; - }); - - it("should reject transactions that are too large", () => { - const tx = TransactionFactory.transfer(wallets[12].address) - .withNetwork("unitnet") - .withPassphrase(wallets[11].passphrase) - .build(3)[0]; - - // @FIXME: Uhm excuse me, what the? - tx.data.signatures = [""]; - for (let i = 0; i < transactionPool.options.maxTransactionBytes; i++) { - // @ts-ignore - tx.data.signatures += "1"; - } - guard.__filterAndTransformTransactions([tx]); - - expect(guard.errors[tx.id]).toEqual([ - { - message: `Transaction ${tx.id} is larger than ${ - transactionPool.options.maxTransactionBytes - } bytes.`, - type: "ERR_TOO_LARGE", - }, - ]); - }); - - it("should reject transactions from the future", () => { - const now = 47157042; // seconds since genesis block - const transactionExists = guard.pool.transactionExists; - guard.pool.transactionExists = jest.fn(() => false); - const getTime = Crypto.slots.getTime; - Crypto.slots.getTime = jest.fn(() => now); - - const secondsInFuture = 3601; - const tx = { - id: "1", - senderPublicKey: "affe", - timestamp: Crypto.slots.getTime() + secondsInFuture, - }; - guard.__filterAndTransformTransactions([tx]); - - expect(guard.errors[tx.id]).toEqual([ - { - message: `Transaction ${tx.id} is ${secondsInFuture} seconds in the future`, - type: "ERR_FROM_FUTURE", - }, - ]); - - Crypto.slots.getTime = getTime; - guard.pool.transactionExists = transactionExists; - }); - - it("should accept transaction with correct network byte", () => { - const transactionExists = guard.pool.transactionExists; - guard.pool.transactionExists = jest.fn(() => false); - - const canApply = guard.pool.walletManager.canApply; - guard.pool.walletManager.canApply = jest.fn(() => true); - - const tx = { - id: "1", - network: 23, - type: Enums.TransactionTypes.Transfer, - senderPublicKey: "023ee98f453661a1cb765fd60df95b4efb1e110660ffb88ae31c2368a70f1f7359", - recipientId: "DEJHR83JFmGpXYkJiaqn7wPGztwjheLAmY", - }; - guard.__filterAndTransformTransactions([tx]); - - expect(guard.errors[tx.id]).not.toEqual([ - { - message: `Transaction network '${tx.network}' does not match '${Managers.configManager.get( - "pubKeyHash", - )}'`, - type: "ERR_WRONG_NETWORK", - }, - ]); - - guard.pool.transactionExists = transactionExists; - guard.pool.walletManager.canApply = canApply; - }); - - it("should accept transaction with missing network byte", () => { - const transactionExists = guard.pool.transactionExists; - guard.pool.transactionExists = jest.fn(() => false); - - const canApply = guard.pool.walletManager.canApply; - guard.pool.walletManager.canApply = jest.fn(() => true); - - const tx = { - id: "1", - type: Enums.TransactionTypes.Transfer, - senderPublicKey: "023ee98f453661a1cb765fd60df95b4efb1e110660ffb88ae31c2368a70f1f7359", - recipientId: "DEJHR83JFmGpXYkJiaqn7wPGztwjheLAmY", - }; - guard.__filterAndTransformTransactions([tx]); - - expect(guard.errors[tx.id].type).not.toEqual("ERR_WRONG_NETWORK"); - - guard.pool.transactionExists = transactionExists; - guard.pool.walletManager.canApply = canApply; - }); - - it("should not accept transaction with wrong network byte", () => { - const transactionExists = guard.pool.transactionExists; - guard.pool.transactionExists = jest.fn(() => false); - - const canApply = guard.pool.walletManager.canApply; - guard.pool.walletManager.canApply = jest.fn(() => true); - - const tx = { - id: "1", - network: 2, - senderPublicKey: "023ee98f453661a1cb765fd60df95b4efb1e110660ffb88ae31c2368a70f1f7359", - }; - guard.__filterAndTransformTransactions([tx]); - - expect(guard.errors[tx.id]).toEqual([ - { - message: `Transaction network '${tx.network}' does not match '${Managers.configManager.get( - "pubKeyHash", - )}'`, - type: "ERR_WRONG_NETWORK", - }, - ]); - - guard.pool.transactionExists = transactionExists; - guard.pool.walletManager.canApply = canApply; - }); - - it("should not accept transaction if pool hasExceededMaxTransactions and add it to excess", () => { - const transactions = TransactionFactory.transfer(wallets[11].address, 35) - .withNetwork("unitnet") - .withPassphrase(wallets[10].passphrase) - .create(3); - - jest.spyOn(guard.pool, "hasExceededMaxTransactions").mockImplementationOnce(tx => true); - - guard.__filterAndTransformTransactions(transactions); - - expect(guard.excess).toEqual([transactions[0].id]); - expect(guard.accept).toEqual(new Map()); - expect(guard.broadcast).toEqual(new Map()); - }); - - it("should push a ERR_UNKNOWN error if something threw in validated transaction block", () => { - const transactions = TransactionFactory.transfer(wallets[11].address, 35) - .withNetwork("unitnet") - .withPassphrase(wallets[10].passphrase) - .build(3); - - // use guard.accept.set() call to introduce a throw - jest.spyOn(guard.pool.walletManager, "canApply").mockImplementationOnce(() => { - throw new Error("hey"); - }); - - guard.__filterAndTransformTransactions(transactions.map(tx => tx.data)); - - expect(guard.accept).toEqual(new Map()); - expect(guard.broadcast).toEqual(new Map()); - expect(guard.errors[transactions[0].id]).toEqual([ - { - message: `hey`, - type: "ERR_UNKNOWN", - }, - ]); - }); - }); - - describe("__validateTransaction", () => { - it("should not validate when recipient is not on the same network", async () => { - const transactions = TransactionFactory.transfer("DEJHR83JFmGpXYkJiaqn7wPGztwjheLAmY", 35) - .withNetwork("unitnet") - .withPassphrase(wallets[10].passphrase) - .create(3); - - expect(guard.__validateTransaction(transactions[0])).toBeFalse(); - expect(guard.errors).toEqual({ - [transactions[0].id]: [ - { - type: "ERR_INVALID_RECIPIENT", - message: `Recipient ${ - transactions[0].recipientId - } is not on the same network: ${Managers.configManager.get("network.pubKeyHash")}`, - }, - ], - }); - }); - - it("should not validate a delegate registration if an existing registration for the same username from a different wallet exists in the pool", async () => { - const delegateRegistrations = [ - TransactionFactory.delegateRegistration("test_delegate") - .withNetwork("unitnet") - .withPassphrase(wallets[16].passphrase) - .build()[0], - TransactionFactory.delegateRegistration("test_delegate") - .withNetwork("unitnet") - .withPassphrase(wallets[17].passphrase) - .build()[0], - ]; - const memPoolTx = new MemPoolTransaction(delegateRegistrations[0]); - jest.spyOn(guard.pool, "getTransactionsByType").mockReturnValueOnce(new Set([memPoolTx])); - - expect(guard.__validateTransaction(delegateRegistrations[1].data)).toBeFalse(); - expect(guard.errors[delegateRegistrations[1].id]).toEqual([ - { - type: "ERR_PENDING", - message: `Delegate registration for "${ - delegateRegistrations[1].data.asset.delegate.username - }" already in the pool`, - }, - ]); - }); - - it("should not validate when sender has same type transactions in the pool (only for 2nd sig, delegate registration, vote)", async () => { - jest.spyOn(guard.pool.walletManager, "canApply").mockImplementation(() => true); - jest.spyOn(guard.pool, "senderHasTransactionsOfType").mockReturnValue(true); - const vote = TransactionFactory.vote(delegates[0].publicKey) - .withNetwork("unitnet") - .withPassphrase(wallets[10].passphrase) - .build()[0]; - - const delegateReg = TransactionFactory.delegateRegistration() - .withNetwork("unitnet") - .withPassphrase(wallets[11].passphrase) - .build()[0]; - - const signature = TransactionFactory.secondSignature(wallets[12].passphrase) - .withNetwork("unitnet") - .withPassphrase(wallets[12].passphrase) - .build()[0]; - - for (const tx of [vote, delegateReg, signature]) { - expect(guard.__validateTransaction(tx.data)).toBeFalse(); - expect(guard.errors[tx.id]).toEqual([ - { - type: "ERR_PENDING", - message: - `Sender ${tx.data.senderPublicKey} already has a transaction of type ` + - `'${Enums.TransactionTypes[tx.type]}' in the pool`, - }, - ]); - } - - jest.restoreAllMocks(); - }); - - it("should not validate unsupported transaction types", async () => { - jest.spyOn(guard.pool.walletManager, "canApply").mockImplementation(() => true); - - // use a random transaction as a base - then play with type - const baseTransaction = TransactionFactory.delegateRegistration() - .withNetwork("unitnet") - .withPassphrase(wallets[11].passphrase) - .build()[0]; - - for (const transactionType of [ - Enums.TransactionTypes.MultiSignature, - Enums.TransactionTypes.Ipfs, - Enums.TransactionTypes.TimelockTransfer, - Enums.TransactionTypes.MultiPayment, - Enums.TransactionTypes.DelegateResignation, - 99, - ]) { - baseTransaction.data.type = transactionType; - // @FIXME: Uhm excuse me, what the? - // @ts-ignore - baseTransaction.data.id = transactionType; - - expect(guard.__validateTransaction(baseTransaction)).toBeFalse(); - expect(guard.errors[baseTransaction.id]).toEqual([ - { - type: "ERR_UNSUPPORTED", - message: `Invalidating transaction of unsupported type '${ - Enums.TransactionTypes[transactionType] - }'`, - }, - ]); - } - - jest.restoreAllMocks(); - }); - }); - - describe("__removeForgedTransactions", () => { - it("should remove forged transactions", async () => { - const transfers = TransactionFactory.transfer(delegates[0].senderPublicKey) - .withNetwork("unitnet") - .withPassphrase(delegates[0].secret) - .build(4); - - transfers.forEach(tx => { - guard.accept.set(tx.id, tx); - guard.broadcast.set(tx.id, tx); - }); - - const forgedTx = transfers[2]; - jest.spyOn(database, "getForgedTransactionsIds").mockReturnValueOnce([forgedTx.id]); - - await guard.__removeForgedTransactions(); - - expect(guard.accept.size).toBe(3); - expect(guard.broadcast.size).toBe(3); - - expect(guard.errors[forgedTx.id]).toHaveLength(1); - expect(guard.errors[forgedTx.id][0].type).toEqual("ERR_FORGED"); - }); - }); - - describe("__addTransactionsToPool", () => { - it("should add transactions to the pool", () => { - const transfers = TransactionFactory.transfer(delegates[0].senderPublicKey) - .withNetwork("unitnet") - .withPassphrase(delegates[0].secret) - .create(4); - - transfers.forEach(tx => { - guard.accept.set(tx.id, tx); - guard.broadcast.set(tx.id, tx); - }); - - expect(guard.errors).toEqual({}); - jest.spyOn(guard.pool, "addTransactions").mockReturnValueOnce({ added: transfers, notAdded: [] }); - - guard.__addTransactionsToPool(); - - expect(guard.errors).toEqual({}); - expect(guard.accept.size).toBe(4); - expect(guard.broadcast.size).toBe(4); - }); - - it("should delete from accept and broadcast transactions that were not added to the pool", () => { - const added = TransactionFactory.transfer(delegates[0].address) - .withNetwork("unitnet") - .withPassphrase(delegates[0].secret) - .build(2); - const notAddedError = { type: "ERR_TEST", message: "" }; - const notAdded = TransactionFactory.transfer(delegates[1].address) - .withNetwork("unitnet") - .withPassphrase(delegates[0].secret) - .build(2) - .map(tx => ({ - transaction: tx, - ...notAddedError, - })); - - added.forEach(tx => { - guard.accept.set(tx.id, tx); - guard.broadcast.set(tx.id, tx); - }); - notAdded.forEach(tx => { - guard.accept.set(tx.transaction.id, tx); - guard.broadcast.set(tx.transaction.id, tx); - }); - - jest.spyOn(guard.pool, "addTransactions").mockReturnValueOnce({ added, notAdded }); - guard.__addTransactionsToPool(); - - expect(guard.accept.size).toBe(2); - expect(guard.broadcast.size).toBe(2); - - expect(guard.errors[notAdded[0].transaction.id]).toEqual([notAddedError]); - expect(guard.errors[notAdded[1].transaction.id]).toEqual([notAddedError]); - }); - - it("should delete from accept but keep in broadcast transactions that were not added to the pool because of ERR_POOL_FULL", () => { - const added = TransactionFactory.transfer(delegates[0].address) - .withNetwork("unitnet") - .withPassphrase(delegates[0].secret) - .build(2); - - const notAddedError = { type: "ERR_POOL_FULL", message: "" }; - const notAdded = TransactionFactory.transfer(delegates[1].address) - .withNetwork("unitnet") - .withPassphrase(delegates[0].secret) - .build(2) - .map(tx => ({ - transaction: tx, - ...notAddedError, - })); - - added.forEach(tx => { - guard.accept.set(tx.id, tx); - guard.broadcast.set(tx.id, tx); - }); - notAdded.forEach(tx => { - guard.accept.set(tx.transaction.id, tx); - guard.broadcast.set(tx.transaction.id, tx); - }); - - jest.spyOn(guard.pool, "addTransactions").mockReturnValueOnce({ added, notAdded }); - guard.__addTransactionsToPool(); - - expect(guard.accept.size).toBe(2); - expect(guard.broadcast.size).toBe(4); - - expect(guard.errors[notAdded[0].transaction.id]).toEqual([notAddedError]); - expect(guard.errors[notAdded[1].transaction.id]).toEqual([notAddedError]); - }); - }); - - describe("pushError", () => { - it("should have error for transaction", () => { - expect(guard.errors).toBeEmpty(); - - guard.pushError({ id: 1 }, "ERR_INVALID", "Invalid."); - - expect(guard.errors).toBeObject(); - expect(guard.errors["1"]).toBeArray(); - expect(guard.errors["1"]).toHaveLength(1); - expect(guard.errors["1"]).toEqual([{ message: "Invalid.", type: "ERR_INVALID" }]); - - expect(guard.invalid.size).toEqual(1); - expect(guard.invalid.entries().next().value[1]).toEqual({ id: 1 }); - }); - - it("should have multiple errors for transaction", () => { - expect(guard.errors).toBeEmpty(); - - guard.pushError({ id: 1 }, "ERR_INVALID", "Invalid 1."); - guard.pushError({ id: 1 }, "ERR_INVALID", "Invalid 2."); - - expect(guard.errors).toBeObject(); - expect(guard.errors["1"]).toBeArray(); - expect(guard.errors["1"]).toHaveLength(2); - expect(guard.errors["1"]).toEqual([ - { message: "Invalid 1.", type: "ERR_INVALID" }, - { message: "Invalid 2.", type: "ERR_INVALID" }, - ]); - - expect(guard.invalid.size).toEqual(1); - expect(guard.invalid.entries().next().value[1]).toEqual({ id: 1 }); - }); - }); -}); diff --git a/__tests__/unit/core-transaction-pool/mem-pool-transaction.test.ts b/__tests__/unit/core-transaction-pool/mem-pool-transaction.test.ts index 51f1ce79dd..5da54ea8ba 100644 --- a/__tests__/unit/core-transaction-pool/mem-pool-transaction.test.ts +++ b/__tests__/unit/core-transaction-pool/mem-pool-transaction.test.ts @@ -1,28 +1,28 @@ import { Enums, Interfaces } from "@arkecosystem/crypto"; -import { MemPoolTransaction } from "../../../packages/core-transaction-pool/src/mem-pool-transaction"; +import { MemPoolTransaction } from "../../../packages/core-transaction-pool/src/pool-transaction"; import { transactions } from "./__fixtures__/transactions"; describe("MemPoolTransaction", () => { - describe("expireAt", () => { + describe("expiresAt", () => { it("should return transaction expiration when it is set", () => { const transaction: Interfaces.ITransaction = transactions.dummy1; transaction.data.expiration = 1123; const memPoolTransaction = new MemPoolTransaction(transaction); - expect(memPoolTransaction.expireAt(1)).toBe(1123); + expect(memPoolTransaction.expiresAt(1)).toBe(1123); }); it("should return timestamp + maxTransactionAge when expiration is not set", () => { const transaction: Interfaces.ITransaction = transactions.dummy2; transaction.data.timestamp = 344; const memPoolTransaction = new MemPoolTransaction(transaction); - expect(memPoolTransaction.expireAt(131)).toBe(transaction.data.timestamp + 131); + expect(memPoolTransaction.expiresAt(131)).toBe(transaction.data.timestamp + 131); }); it("should return null for timelock transfer with no expiration set", () => { const transaction: Interfaces.ITransaction = transactions.dummy3; transaction.data.type = Enums.TransactionTypes.TimelockTransfer; const memPoolTransaction = new MemPoolTransaction(transaction); - expect(memPoolTransaction.expireAt(1)).toBe(null); + expect(memPoolTransaction.expiresAt(1)).toBe(null); }); }); }); diff --git a/packages/core-api/src/repositories/transactions.ts b/packages/core-api/src/repositories/transactions.ts index 72b2c42726..f1231d01f1 100644 --- a/packages/core-api/src/repositories/transactions.ts +++ b/packages/core-api/src/repositories/transactions.ts @@ -394,7 +394,7 @@ export class TransactionsRepository extends Repository implements IRepository { * @return {String} */ public __publicKeyFromAddress(senderId): string { - if (this.databaseService.walletManager.exists(senderId)) { + if (this.databaseService.walletManager.has(senderId)) { return this.databaseService.walletManager.findByAddress(senderId).publicKey; } diff --git a/packages/core-api/src/versions/2/transactions/controller.ts b/packages/core-api/src/versions/2/transactions/controller.ts index cde341f114..9fad481170 100644 --- a/packages/core-api/src/versions/2/transactions/controller.ts +++ b/packages/core-api/src/versions/2/transactions/controller.ts @@ -1,7 +1,6 @@ import { app } from "@arkecosystem/core-container"; import { P2P, TransactionPool } from "@arkecosystem/core-interfaces"; -import { TransactionGuard } from "@arkecosystem/core-transaction-pool"; -import { Enums } from "@arkecosystem/crypto"; +import { Enums, Interfaces } from "@arkecosystem/crypto"; import Boom from "boom"; import Hapi from "hapi"; import { Controller } from "../shared/controller"; @@ -22,18 +21,13 @@ export class TransactionsController extends Controller { public async store(request: Hapi.Request, h: Hapi.ResponseToolkit) { try { - if (!this.transactionPool.options.enabled) { - return Boom.serverUnavailable("Transaction pool is disabled."); - } - - const guard = new TransactionGuard(this.transactionPool); - - const result = await guard.validate((request.payload as any).transactions); + const processor: TransactionPool.IProcessor = this.transactionPool.makeProcessor(); + const result = await processor.validate((request.payload as any).transactions); if (result.broadcast.length > 0) { app.resolvePlugin("p2p") .getMonitor() - .broadcastTransactions(guard.getBroadcastTransactions()); + .broadcastTransactions(processor.getBroadcastTransactions()); } return { @@ -63,10 +57,6 @@ export class TransactionsController extends Controller { public async unconfirmed(request: Hapi.Request, h: Hapi.ResponseToolkit) { try { - if (!this.transactionPool.options.enabled) { - return Boom.serverUnavailable("Transaction pool is disabled."); - } - const pagination = super.paginate(request); const transactions = this.transactionPool.getTransactions(pagination.offset, pagination.limit); @@ -89,11 +79,8 @@ export class TransactionsController extends Controller { public async showUnconfirmed(request: Hapi.Request, h: Hapi.ResponseToolkit) { try { - if (!this.transactionPool.options.enabled) { - return Boom.serverUnavailable("Transaction pool is disabled."); - } + const transaction: Interfaces.ITransaction = this.transactionPool.getTransaction(request.params.id); - const transaction = this.transactionPool.getTransaction(request.params.id); if (!transaction) { return Boom.notFound("Transaction not found"); } diff --git a/packages/core-blockchain/src/processor/handlers/accept-block-handler.ts b/packages/core-blockchain/src/processor/handlers/accept-block-handler.ts index 89f23694da..33004963c2 100644 --- a/packages/core-blockchain/src/processor/handlers/accept-block-handler.ts +++ b/packages/core-blockchain/src/processor/handlers/accept-block-handler.ts @@ -43,7 +43,7 @@ export class AcceptBlockHandler extends BlockHandler { this.logger.error(`Refused new block ${JSON.stringify(this.block.data)}`); this.logger.debug(error.stack); - this.blockchain.transactionPool.purgeBlock(this.block); + this.blockchain.transactionPool.purgeByBlock(this.block); this.blockchain.forkBlock(this.block); return super.execute(); diff --git a/packages/core-database/src/database-service.ts b/packages/core-database/src/database-service.ts index 368be91bbb..7fd0bdea3a 100644 --- a/packages/core-database/src/database-service.ts +++ b/packages/core-database/src/database-service.ts @@ -533,7 +533,7 @@ export class DatabaseService implements Database.IDatabaseService { public async verifyTransaction(transaction: Interfaces.ITransaction): Promise { const senderId: string = Identities.Address.fromPublicKey(transaction.data.senderPublicKey); - const sender: Database.IWallet = this.walletManager.findByAddress(senderId); // should exist + const sender: Database.IWallet = this.walletManager.findByAddress(senderId); const transactionHandler: TransactionHandler = TransactionHandlerRegistry.get(transaction.type); if (!sender.publicKey) { diff --git a/packages/core-database/src/repositories/transactions-business-repository.ts b/packages/core-database/src/repositories/transactions-business-repository.ts index 6681ba5741..754414dcc4 100644 --- a/packages/core-database/src/repositories/transactions-business-repository.ts +++ b/packages/core-database/src/repositories/transactions-business-repository.ts @@ -116,7 +116,7 @@ export class TransactionsBusinessRepository implements Database.ITransactionsBus private getPublicKeyFromAddress(senderId: string): string { const { walletManager }: Database.IDatabaseService = this.databaseServiceProvider(); - return walletManager.exists(senderId) ? walletManager.findByAddress(senderId).publicKey : null; + return walletManager.has(senderId) ? walletManager.findByAddress(senderId).publicKey : null; } private async mapBlocksToTransactions(rows): Promise { diff --git a/packages/core-database/src/wallet-manager.ts b/packages/core-database/src/wallet-manager.ts index 7fe13084e9..62cdc8d25d 100644 --- a/packages/core-database/src/wallet-manager.ts +++ b/packages/core-database/src/wallet-manager.ts @@ -7,12 +7,14 @@ import pluralize from "pluralize"; import { Wallet } from "./wallet"; export class WalletManager implements Database.IWalletManager { - public logger = app.resolvePlugin("logger"); - public config = app.getConfig(); - + // @TODO: make this private and read-only public byAddress: { [key: string]: Database.IWallet }; + // @TODO: make this private and read-only public byPublicKey: { [key: string]: Database.IWallet }; + // @TODO: make this private and read-only public byUsername: { [key: string]: Database.IWallet }; + // @TODO: make this private and read-only + public logger: Logger.ILogger = app.resolvePlugin("logger"); /** * Create a new wallet manager instance. @@ -83,6 +85,10 @@ export class WalletManager implements Database.IWalletManager { } } + public has(addressOrPublicKey: string): boolean { + return this.hasByAddress(addressOrPublicKey) || this.hasByPublicKey(addressOrPublicKey); + } + public hasByAddress(address: string): boolean { return !!this.byAddress[address]; } @@ -107,10 +113,6 @@ export class WalletManager implements Database.IWalletManager { delete this.byUsername[username]; } - public exists(addressOrPublicKey: string): boolean { - return this.hasByAddress(addressOrPublicKey) || this.hasByPublicKey(addressOrPublicKey); - } - public index(wallets: Database.IWallet[]): void { for (const wallet of wallets) { this.reindex(wallet); @@ -389,7 +391,7 @@ export class WalletManager implements Database.IWalletManager { const { type, data } = transaction; const transactionHandler: TransactionHandler = TransactionHandlerRegistry.get(transaction.type); - const sender: Database.IWallet = this.findByPublicKey(data.senderPublicKey); // Should exist + const sender: Database.IWallet = this.findByPublicKey(data.senderPublicKey); const recipient: Database.IWallet = this.byAddress[data.recipientId]; transactionHandler.revertForSender(transaction, sender); diff --git a/packages/core-interfaces/src/core-database/wallet-manager.ts b/packages/core-interfaces/src/core-database/wallet-manager.ts index ccbad28bb2..ed5cf655d5 100644 --- a/packages/core-interfaces/src/core-database/wallet-manager.ts +++ b/packages/core-interfaces/src/core-database/wallet-manager.ts @@ -36,8 +36,6 @@ export type IDelegateWallet = IWallet & { rate: number; round: number }; export interface IWalletManager { logger: Logger.ILogger; - config: any; - reset(): void; allByAddress(): IWallet[]; @@ -48,7 +46,7 @@ export interface IWalletManager { findByAddress(address: string): IWallet; - exists(addressOrPublicKey: string): boolean; + has(addressOrPublicKey: string): boolean; findByPublicKey(publicKey: string): IWallet; diff --git a/packages/core-interfaces/src/core-transaction-pool/connection.ts b/packages/core-interfaces/src/core-transaction-pool/connection.ts index 67536fd8e1..1a78b4eb6d 100644 --- a/packages/core-interfaces/src/core-transaction-pool/connection.ts +++ b/packages/core-interfaces/src/core-transaction-pool/connection.ts @@ -1,180 +1,44 @@ import { Enums, Interfaces } from "@arkecosystem/crypto"; import { Dato } from "@faustbrian/dato"; +import { IProcessor } from "./processor"; export interface IAddTransactionResponse { - success: boolean; -} - -export interface IAddTransactionErrorResponse extends IAddTransactionResponse { - transaction: Interfaces.ITransaction; - type: string; - message: string; - success: boolean; + transaction?: Interfaces.ITransaction; + type?: string; + message?: string; } export interface IConnection { - options: any; - loggedAllowedSenders: string[]; - walletManager: any; + makeProcessor(): IProcessor; make(): Promise; - - /** - * Get a driver instance. - */ - driver(): () => any; - - /** - * Disconnect from transaction pool. - * @return {void} - */ disconnect(): void; - - /** - * Get the number of transactions in the pool. - */ getPoolSize(): number; - - /** - * Get the number of transactions in the pool from a specific sender\ - */ getSenderSize(senderPublicKey: string): number; - - /** - * Add many transactions to the pool. - * @param {Array} transactions, already transformed and verified - * by transaction guard - must have serialized field - * @return {Object} like - * { - * added: [ ... successfully added transactions ... ], - * notAdded: [ { transaction: Transaction, type: String, message: String }, ... ] - * } - */ addTransactions( transactions: Interfaces.ITransaction[], ): { added: Interfaces.ITransaction[]; - notAdded: IAddTransactionErrorResponse[]; + notAdded: IAddTransactionResponse[]; }; - - /** - * Add a transaction to the pool. - */ - addTransaction(transaction: Interfaces.ITransaction): IAddTransactionResponse; - - /** - * Remove a transaction from the pool by transaction object. - * @param {Transaction} transaction - * @return {void} - */ - removeTransaction(transaction: Interfaces.ITransaction): void; - - /** - * Remove a transaction from the pool by id. - */ - removeTransactionById(id: string, senderPublicKey?: string): void; - - /** - * Get all transactions that are ready to be forged. - */ - getTransactionsForForging(blockSize: number): string[]; - - /** - * Get a transaction by transaction id. - */ + acceptChainedBlock(block: Interfaces.IBlock): void; + blockSender(senderPublicKey: string): Dato; + buildWallets(): Promise; + flush(): void; getTransaction(id: string): Interfaces.ITransaction; - - /** - * Get all transactions within the specified range [start, start + size), ordered by fee. - * @return {(Array|void)} array of serialized transaction hex strings - */ - getTransactions(start: number, size: number, maxBytes?: number): Buffer[]; - - /** - * Get all transactions within the specified range [start, start + size). - * @return {Array} array of transactions IDs in the specified range - */ getTransactionIdsForForging(start: number, size: number): string[]; - - /** - * Get data from all transactions within the specified range [start, start + size). - * Transactions are ordered by fee (highest fee first) or by - * insertion time, if fees equal (earliest transaction first). - * @return {Array} array of transaction[property] - */ - getTransactionsData(start: number, size: number, property: string, maxBytes?: number): string[] | Buffer[]; - - /** - * Get all transactions of a given type from the pool. - */ + getTransactions(start: number, size: number, maxBytes?: number): Buffer[]; getTransactionsByType(type: any): any; - - /** - * Remove all transactions from the transaction pool belonging to specific sender. - */ - removeTransactionsForSender(senderPublicKey: string): void; - - /** - * Check whether sender of transaction has exceeded max transactions in queue. - */ + getTransactionsData(start: number, size: number, property: string, maxBytes?: number): T[]; + getTransactionsForForging(blockSize: number): string[]; + has(transactionId: string): any; hasExceededMaxTransactions(transaction: Interfaces.ITransactionData): boolean; - - /** - * Flush the pool (delete all transactions from it). - */ - flush(): void; - - /** - * Checks if a transaction exists in the pool. - */ - transactionExists(transactionId: string): any; - - /** - * Check if transaction sender is blocked - * @return {Boolean} - */ isSenderBlocked(senderPublicKey: string): boolean; - - /** - * Blocks sender for a specified time - */ - blockSender(senderPublicKey: string): Dato; - - /** - * Processes recently accepted block by the blockchain. - * It removes block transaction from the pool and adjusts - * pool wallets for non existing transactions. - * - * @param {Object} block - * @return {void} - */ - acceptChainedBlock(block: Interfaces.IBlock): void; - - /** - * Rebuild pool manager wallets - * Removes all the wallets from pool manager and applies transaction from pool - if any - * It waits for the node to sync, and then check the transactions in pool - * and validates them and apply to the pool manager. - */ - buildWallets(): Promise; - + purgeByBlock(block: Interfaces.IBlock): void; purgeByPublicKey(senderPublicKey: string): void; - - /** - * Purges all transactions from senders with at least one - * invalid transaction. - */ purgeSendersWithInvalidTransactions(block: Interfaces.IBlock): void; - - /** - * Purges all transactions from the block. - * Purges if transaction exists. It assumes that if trx exists that also wallet exists in pool - */ - purgeBlock(block: Interfaces.IBlock): void; - - /** - * Check whether a given sender has any transactions of the specified type - * in the pool. - */ + removeTransaction(transaction: Interfaces.ITransaction): void; + removeTransactionById(id: string, senderPublicKey?: string): void; + removeTransactionsForSender(senderPublicKey: string): void; senderHasTransactionsOfType(senderPublicKey: string, transactionType: Enums.TransactionTypes): boolean; } diff --git a/packages/core-interfaces/src/core-transaction-pool/index.ts b/packages/core-interfaces/src/core-transaction-pool/index.ts index 22de865caa..5af15bb77d 100644 --- a/packages/core-interfaces/src/core-transaction-pool/index.ts +++ b/packages/core-interfaces/src/core-transaction-pool/index.ts @@ -1,2 +1,2 @@ export * from "./connection"; -export * from "./guard"; +export * from "./processor"; diff --git a/packages/core-interfaces/src/core-transaction-pool/guard.ts b/packages/core-interfaces/src/core-transaction-pool/processor.ts similarity index 67% rename from packages/core-interfaces/src/core-transaction-pool/guard.ts rename to packages/core-interfaces/src/core-transaction-pool/processor.ts index 040cd32fc1..e741dbddc7 100644 --- a/packages/core-interfaces/src/core-transaction-pool/guard.ts +++ b/packages/core-interfaces/src/core-transaction-pool/processor.ts @@ -1,12 +1,16 @@ import { Interfaces } from "@arkecosystem/crypto"; -import { IConnection } from "./connection"; -export interface ITransactionErrorResponse { - type: string; - message: string; +export interface IProcessor { + validate(transactions: Interfaces.ITransactionData[]): Promise; + + getTransactions(): Interfaces.ITransactionData[]; + getBroadcastTransactions(): Interfaces.ITransaction[]; + getErrors(): { [key: string]: ITransactionErrorResponse[] }; + + pushError(transaction: Interfaces.ITransactionData, type: string, message: string): void; } -export interface IValidationResult { +export interface IProcessorResult { accept: string[]; broadcast: string[]; invalid: string[]; @@ -14,12 +18,7 @@ export interface IValidationResult { errors: { [key: string]: ITransactionErrorResponse[] } | null; } -export interface IGuard { - pool: IConnection; - transactions: Interfaces.ITransactionData[]; - - validate(transactions: Interfaces.ITransactionData[]): Promise; - pushError(transaction: Interfaces.ITransactionData, type: string, message: string); - - getBroadcastTransactions(): Interfaces.ITransaction[]; +export interface ITransactionErrorResponse { + type: string; + message: string; } diff --git a/packages/core-p2p/src/socket-server/versions/peer.ts b/packages/core-p2p/src/socket-server/versions/peer.ts index c30ba8d20d..44067e7fe5 100644 --- a/packages/core-p2p/src/socket-server/versions/peer.ts +++ b/packages/core-p2p/src/socket-server/versions/peer.ts @@ -1,6 +1,5 @@ import { app } from "@arkecosystem/core-container"; import { Blockchain, Database, Logger, P2P, TransactionPool } from "@arkecosystem/core-interfaces"; -import { TransactionGuard } from "@arkecosystem/core-transaction-pool"; import { Crypto, Interfaces } from "@arkecosystem/crypto"; import pluralize from "pluralize"; import { isBlockChained } from "../../../../core-utils/dist"; @@ -8,9 +7,6 @@ import { MissingCommonBlockError } from "../../errors"; import { isWhitelisted } from "../../utils"; import { InvalidTransactionsError, UnchainedBlockError } from "../errors"; -const transactionPool = app.resolvePlugin("transaction-pool"); -const logger = app.resolvePlugin("logger"); - export async function acceptNewPeer({ service, req }: { service: P2P.IPeerService; req }): Promise { const peer = { ip: req.data.ip }; @@ -81,15 +77,18 @@ export async function postBlock({ req }): Promise { } export async function postTransactions({ service, req }: { service: P2P.IPeerService; req }): Promise { - const guard: TransactionPool.IGuard = new TransactionGuard(transactionPool); - const result: TransactionPool.IValidationResult = await guard.validate(req.data.transactions); + const processor: TransactionPool.IProcessor = app + .resolvePlugin("transaction-pool") + .makeProcessor(); + + const result: TransactionPool.IProcessorResult = await processor.validate(req.data.transactions); if (result.invalid.length > 0) { throw new InvalidTransactionsError(); } if (result.broadcast.length > 0) { - service.getMonitor().broadcastTransactions(guard.getBroadcastTransactions()); + service.getMonitor().broadcastTransactions(processor.getBroadcastTransactions()); } return result.accept; @@ -112,7 +111,7 @@ export async function getBlocks({ req }): Promise { blocks = await database.getBlocks(reqBlockHeight, 400); } - logger.info( + app.resolvePlugin("logger").info( `${req.headers.remoteAddress} has downloaded ${pluralize("block", blocks.length, true)} from height ${(!isNaN( reqBlockHeight, ) diff --git a/packages/core-transaction-pool/src/connection.ts b/packages/core-transaction-pool/src/connection.ts index 8e6bc6de19..d59f985398 100644 --- a/packages/core-transaction-pool/src/connection.ts +++ b/packages/core-transaction-pool/src/connection.ts @@ -1,133 +1,101 @@ import { app } from "@arkecosystem/core-container"; import { Blockchain, Database, EventEmitter, Logger, TransactionPool } from "@arkecosystem/core-interfaces"; -import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; +import { ITransactionHandler, TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { Enums, Interfaces, Utils } from "@arkecosystem/crypto"; import { dato, Dato } from "@faustbrian/dato"; import assert from "assert"; -import { Mem } from "./mem"; -import { MemPoolTransaction } from "./mem-pool-transaction"; -import { PoolWalletManager } from "./pool-wallet-manager"; +import { ITransactionsProcessed } from "./interfaces"; +import { Memory } from "./memory"; +import { MemoryTransaction } from "./memory-transaction"; +import { Processor } from "./processor"; import { Storage } from "./storage"; +import { WalletManager } from "./wallet-manager"; -/** - * Transaction pool. It uses a hybrid storage - caching the data - * in memory and occasionally saving it to a persistent, on-disk storage (SQLite), - * every N modifications, and also during shutdown. The operations that only read - * data (everything other than add or remove transaction) are served from the - * in-memory storage. - */ export class Connection implements TransactionPool.IConnection { - public walletManager: PoolWalletManager; - public mem: Mem; - public storage: Storage; - public loggedAllowedSenders: string[]; - private blockedByPublicKey: { [key: string]: Dato }; - - private readonly databaseService = app.resolvePlugin("database"); - private readonly emitter = app.resolvePlugin("event-emitter"); - private readonly logger = app.resolvePlugin("logger"); - - /** - * Create a new transaction pool instance. - * @param {Object} options - */ - constructor(public options) { - this.walletManager = new PoolWalletManager(); - this.blockedByPublicKey = {}; + private readonly options: Record; + private readonly walletManager: WalletManager; + private readonly memory: Memory; + private readonly storage: Storage; + private readonly loggedAllowedSenders: string[] = []; + private readonly blockedByPublicKey: { [key: string]: Dato } = {}; + private readonly databaseService: Database.IDatabaseService = app.resolvePlugin( + "database", + ); + private readonly emitter: EventEmitter.EventEmitter = app.resolvePlugin("event-emitter"); + private readonly logger: Logger.ILogger = app.resolvePlugin("logger"); + + constructor({ + options, + walletManager, + memory, + storage, + }: { + options: Record; + walletManager: WalletManager; + memory: Memory; + storage: Storage; + }) { + this.options = options; + this.walletManager = walletManager; + this.memory = memory; + this.storage = storage; } - /** - * Make the transaction pool instance. Load all transactions in the pool from - * the on-disk database, saved there from a previous run. - * @return {TransactionPool} - */ public async make(): Promise { - this.mem = new Mem(); - this.storage = new Storage(this.options.storage); - this.loggedAllowedSenders = []; + const all: MemoryTransaction[] = this.storage.loadAll(); - const all = this.storage.loadAll(); - all.forEach(t => this.mem.add(t, this.options.maxTransactionAge, true)); - - this.__purgeExpired(); + for (const transaction of all) { + this.memory.remember(transaction, this.options.maxTransactionAge, true); + } - // Remove transactions that were forged while we were offline. - const allIds = all.map(memPoolTransaction => memPoolTransaction.transaction.id); + this.purgeExpired(); - const forgedIds = await this.databaseService.getForgedTransactionsIds(allIds); + const forgedIds: string[] = await this.databaseService.getForgedTransactionsIds( + all.map((transaction: MemoryTransaction) => transaction.transaction.id), + ); - forgedIds.forEach(id => this.removeTransactionById(id)); + for (const id of forgedIds) { + this.removeTransactionById(id); + } return this; } - /** - * Get a driver instance. - * @return {TransactionPoolInterface} - */ - public driver() { - return this.driver; + public disconnect(): void { + this.syncToPersistentStorage(); + this.storage.close(); } - /** - * Disconnect from transaction pool. - */ - public disconnect() { - this.__syncToPersistentStorage(); - this.storage.close(); + public makeProcessor(): TransactionPool.IProcessor { + return new Processor(this, this.walletManager); } - /** - * Get all transactions of a given type from the pool. - * @param {Number} type of transaction - * @return {Set of MemPoolTransaction} all transactions of the given type, could be empty Set - */ - public getTransactionsByType(type) { - this.__purgeExpired(); + public getTransactionsByType(type: number): Set { + this.purgeExpired(); - return this.mem.getByType(type); + return this.memory.getByType(type); } - /** - * Get the number of transactions in the pool. - */ public getPoolSize(): number { - this.__purgeExpired(); + this.purgeExpired(); - return this.mem.getSize(); + return this.memory.count(); } - /** - * Get the number of transactions in the pool from a specific sender - */ public getSenderSize(senderPublicKey: string): number { - this.__purgeExpired(); + this.purgeExpired(); - return this.mem.getBySender(senderPublicKey).size; + return this.memory.getBySender(senderPublicKey).size; } - /** - * Add many transactions to the pool. - * @param {Array} transactions, already transformed and verified - * by transaction guard - must have serialized field - * @return {Object} like - * { - * added: [ ... successfully added transactions ... ], - * notAdded: [ { transaction: Transaction, type: String, message: String }, ... ] - * } - */ - public addTransactions(transactions: Interfaces.ITransaction[]) { - const added = []; - const notAdded = []; + public addTransactions(transactions: Interfaces.ITransaction[]): ITransactionsProcessed { + const added: Interfaces.ITransaction[] = []; + const notAdded: TransactionPool.IAddTransactionResponse[] = []; for (const transaction of transactions) { - const result = this.addTransaction(transaction); + const result: TransactionPool.IAddTransactionResponse = this.addTransaction(transaction); - if (result.success) { - added.push(transaction); - } else { - notAdded.push(result); - } + result.message ? notAdded.push(result) : added.push(transaction); } if (added.length > 0) { @@ -141,140 +109,57 @@ export class Connection implements TransactionPool.IConnection { return { added, notAdded }; } - /** - * Add a transaction to the pool. - * @param {Transaction} transaction - * @return {Object} The success property indicates wether the transaction was successfully added - * and applied to the pool or not. In case it was not successful, the type and message - * property yield information about the error. - */ - public addTransaction(transaction: Interfaces.ITransaction): TransactionPool.IAddTransactionResponse { - if (this.transactionExists(transaction.id)) { - this.logger.debug( - "Transaction pool: ignoring attempt to add a transaction that is already " + - `in the pool, id: ${transaction.id}`, - ); - - return this.__createError(transaction, "ERR_ALREADY_IN_POOL", "Already in pool"); - } - - const poolSize = this.mem.getSize(); - - if (this.options.maxTransactionsInPool <= poolSize) { - // The pool can't accommodate more transactions. Either decline the newcomer or remove - // an existing transaction from the pool in order to free up space. - const all = this.mem.getTransactionsOrderedByFee(); - const lowest = all[all.length - 1].transaction; - - const fee = transaction.data.fee as Utils.BigNumber; - const lowestFee = lowest.data.fee as Utils.BigNumber; - - if (lowestFee.isLessThan(fee)) { - this.walletManager.revertTransactionForSender(lowest); - this.mem.remove(lowest.id, lowest.data.senderPublicKey); - } else { - return this.__createError( - transaction, - "ERR_POOL_FULL", - `Pool is full (has ${poolSize} transactions) and this transaction's fee ` + - `${fee.toFixed()} is not higher than the lowest fee already in pool ` + - `${lowestFee.toFixed()}`, - ); - } - } - - this.mem.add(new MemPoolTransaction(transaction), this.options.maxTransactionAge); - - // Apply transaction to pool wallet manager. - const senderWallet = this.walletManager.findByPublicKey(transaction.data.senderPublicKey); - - // TODO: rework error handling - const errors = []; - if (this.walletManager.canApply(transaction, errors)) { - const transactionHandler = TransactionHandlerRegistry.get(transaction.type); - transactionHandler.applyToSender(transaction, senderWallet); - } else { - // Remove tx again from the pool - this.mem.remove(transaction.id); - return this.__createError(transaction, "ERR_APPLY", JSON.stringify(errors)); - } - - this.__syncToPersistentStorageIfNecessary(); - return { success: true }; - } - - /** - * Remove a transaction from the pool by transaction. - */ - public removeTransaction(transaction: Interfaces.ITransaction) { + public removeTransaction(transaction: Interfaces.ITransaction): void { this.removeTransactionById(transaction.id, transaction.data.senderPublicKey); } - /** - * Remove a transaction from the pool by id. - */ - public removeTransactionById(id: string, senderPublicKey?: string) { - this.mem.remove(id, senderPublicKey); + public removeTransactionById(id: string, senderPublicKey?: string): void { + this.memory.forget(id, senderPublicKey); - this.__syncToPersistentStorageIfNecessary(); + this.syncToPersistentStorageIfNecessary(); this.emitter.emit("transaction.pool.removed", id); } - /** - * Get all transactions that are ready to be forged. - */ - public getTransactionsForForging(blockSize: number): string[] { - return this.getTransactions(0, blockSize, this.options.maxTransactionBytes).map(tx => tx.toString("hex")); - } - - /** - * Get a transaction by transaction id. - */ public getTransaction(id: string): Interfaces.ITransaction { - this.__purgeExpired(); + this.purgeExpired(); - return this.mem.getTransactionById(id); + return this.memory.getById(id); } - /** - * Get all transactions within the specified range [start, start + size), ordered by fee. - */ public getTransactions(start: number, size: number, maxBytes?: number): Buffer[] { - return this.getTransactionsData(start, size, "serialized", maxBytes) as Buffer[]; + return this.getTransactionsData(start, size, "serialized", maxBytes); + } + + public getTransactionsForForging(blockSize: number): string[] { + return this.getTransactions(0, blockSize, this.options.maxTransactionBytes).map(tx => tx.toString("hex")); } - /** - * Get all transactions within the specified range [start, start + size). - */ public getTransactionIdsForForging(start: number, size: number): string[] { - return this.getTransactionsData(start, size, "id", this.options.maxTransactionBytes) as string[]; + return this.getTransactionsData(start, size, "id", this.options.maxTransactionBytes); } - /** - * Get data from all transactions within the specified range [start, start + size). - * Transactions are ordered by fee (highest fee first) or by - * insertion time, if fees equal (earliest transaction first). - */ - public getTransactionsData(start: number, size: number, property: string, maxBytes = 0): string[] | Buffer[] { - this.__purgeExpired(); + public getTransactionsData(start: number, size: number, property: string, maxBytes: number = 0): T[] { + this.purgeExpired(); - const data = []; + const data: T[] = []; - let transactionBytes = 0; + let transactionBytes: number = 0; let i = 0; - for (const memPoolTransaction of this.mem.getTransactionsOrderedByFee()) { + for (const MemoryTransaction of this.memory.allSortedByFee()) { if (i >= start + size) { break; } if (i >= start) { - let pushTransaction = false; - assert.notStrictEqual(memPoolTransaction.transaction[property], undefined); + let pushTransaction: boolean = false; + + assert.notStrictEqual(MemoryTransaction.transaction[property], undefined); + if (maxBytes > 0) { - // Only add the transaction if it will not make the total payload size exceed the maximum - const transactionSize = JSON.stringify(memPoolTransaction.transaction.data).length; + const transactionSize: number = JSON.stringify(MemoryTransaction.transaction.data).length; + if (transactionBytes + transactionSize <= maxBytes) { transactionBytes += transactionSize; pushTransaction = true; @@ -282,8 +167,9 @@ export class Connection implements TransactionPool.IConnection { } else { pushTransaction = true; } + if (pushTransaction) { - data.push(memPoolTransaction.transaction[property]); + data.push(MemoryTransaction.transaction[property]); i++; } } else { @@ -294,18 +180,15 @@ export class Connection implements TransactionPool.IConnection { return data; } - /** - * Remove all transactions from the transaction pool belonging to specific sender. - */ - public removeTransactionsForSender(senderPublicKey: string) { - this.mem.getBySender(senderPublicKey).forEach(e => this.removeTransactionById(e.transaction.id)); + public removeTransactionsForSender(senderPublicKey: string): void { + for (const transaction of this.memory.getBySender(senderPublicKey)) { + this.removeTransactionById(transaction.transaction.id); + } } - /** - * Check whether sender of transaction has exceeded max transactions in queue. - */ + // @TODO: move this to a more appropriate place public hasExceededMaxTransactions(transaction: Interfaces.ITransactionData): boolean { - this.__purgeExpired(); + this.purgeExpired(); if (this.options.allowedSenders.includes(transaction.senderPublicKey)) { if (!this.loggedAllowedSenders.includes(transaction.senderPublicKey)) { @@ -314,44 +197,33 @@ export class Connection implements TransactionPool.IConnection { transaction.senderPublicKey } (listed in options.allowedSenders), thus skipping throttling.`, ); + this.loggedAllowedSenders.push(transaction.senderPublicKey); } return false; } - const count = this.mem.getBySender(transaction.senderPublicKey).size; - - return !(count <= this.options.maxTransactionsPerSender); + return this.memory.getBySender(transaction.senderPublicKey).size >= this.options.maxTransactionsPerSender; } - /** - * Flush the pool (delete all transactions from it). - */ - public flush() { - this.mem.flush(); + public flush(): void { + this.memory.flush(); this.storage.deleteAll(); } - /** - * Checks if a transaction exists in the pool. - */ - public transactionExists(transactionId: string): boolean { - if (!this.mem.transactionExists(transactionId)) { - // If it does not exist then no need to purge expired transactions because - // we know it will not exist after purge too. + public has(transactionId: string): boolean { + if (!this.memory.has(transactionId)) { return false; } - this.__purgeExpired(); + this.purgeExpired(); - return this.mem.transactionExists(transactionId); + return this.memory.has(transactionId); } - /** - * Check if transaction sender is blocked - */ + // @TODO: move this to a more appropriate place public isSenderBlocked(senderPublicKey: string): boolean { if (!this.blockedByPublicKey[senderPublicKey]) { return false; @@ -359,17 +231,15 @@ export class Connection implements TransactionPool.IConnection { if (dato().isAfter(this.blockedByPublicKey[senderPublicKey])) { delete this.blockedByPublicKey[senderPublicKey]; + return false; } return true; } - /** - * Blocks sender for a specified time - */ public blockSender(senderPublicKey: string): Dato { - const blockReleaseTime = dato().addHours(1); + const blockReleaseTime: Dato = dato().addHours(1); this.blockedByPublicKey[senderPublicKey] = blockReleaseTime; @@ -378,25 +248,20 @@ export class Connection implements TransactionPool.IConnection { return blockReleaseTime; } - /** - * Processes recently accepted block by the blockchain. - * It removes block transaction from the pool and adjusts - * pool wallets for non existing transactions. - */ - public acceptChainedBlock(block: Interfaces.IBlock) { + public acceptChainedBlock(block: Interfaces.IBlock): void { for (const transaction of block.transactions) { - const { data } = transaction; - const exists = this.transactionExists(data.id); - const senderPublicKey = data.senderPublicKey; - const transactionHandler = TransactionHandlerRegistry.get(transaction.type); + const { data }: Interfaces.ITransaction = transaction; + const exists: boolean = this.has(data.id); + const senderPublicKey: string = data.senderPublicKey; + const transactionHandler: ITransactionHandler = TransactionHandlerRegistry.get(transaction.type); - const senderWallet = this.walletManager.exists(senderPublicKey) + const senderWallet: Database.IWallet = this.walletManager.has(senderPublicKey) ? this.walletManager.findByPublicKey(senderPublicKey) - : false; + : undefined; - const recipientWallet = this.walletManager.exists(data.recipientId) + const recipientWallet: Database.IWallet = this.walletManager.has(data.recipientId) ? this.walletManager.findByAddress(data.recipientId) - : false; + : undefined; if (recipientWallet) { transactionHandler.applyToRecipient(transaction, recipientWallet); @@ -417,6 +282,7 @@ export class Connection implements TransactionPool.IConnection { data.id } due to ${error.message}. Possible double spending attack`, ); + return; } @@ -428,93 +294,86 @@ export class Connection implements TransactionPool.IConnection { this.walletManager.canBePurged(senderWallet) && this.getSenderSize(senderPublicKey) === 0 ) { - this.walletManager.deleteWallet(senderPublicKey); + this.walletManager.forget(senderPublicKey); } } // if delegate in poll wallet manager - apply rewards and fees - if (this.walletManager.exists(block.data.generatorPublicKey)) { - const delegateWallet = this.walletManager.findByPublicKey(block.data.generatorPublicKey); - const increase = (block.data.reward as Utils.BigNumber).plus(block.data.totalFee); - delegateWallet.balance = delegateWallet.balance.plus(increase); + if (this.walletManager.has(block.data.generatorPublicKey)) { + const delegateWallet: Database.IWallet = this.walletManager.findByPublicKey(block.data.generatorPublicKey); + + delegateWallet.balance = delegateWallet.balance.plus(block.data.reward.plus(block.data.totalFee)); } app.resolve("state").removeCachedTransactionIds(block.transactions.map(tx => tx.id)); } - /** - * Rebuild pool manager wallets - * Removes all the wallets from pool manager and applies transaction from pool - if any - * It waits for the node to sync, and then check the transactions in pool - * and validates them and apply to the pool manager. - */ - public async buildWallets() { + public async buildWallets(): Promise { this.walletManager.reset(); - const poolTransactionIds = await this.getTransactionIdsForForging(0, this.getPoolSize()); - app.resolve("state").removeCachedTransactionIds(poolTransactionIds); + const transactionIds: string[] = await this.getTransactionIdsForForging(0, this.getPoolSize()); + + app.resolve("state").removeCachedTransactionIds(transactionIds); + + for (const transactionId of transactionIds) { + const transaction: Interfaces.ITransaction = this.getTransaction(transactionId); - poolTransactionIds.forEach(transactionId => { - const transaction = this.getTransaction(transactionId); if (!transaction) { return; } - const transactionHandler = TransactionHandlerRegistry.get(transaction.type); - const senderWallet = this.walletManager.findByPublicKey(transaction.data.senderPublicKey); + const senderWallet: Database.IWallet = this.walletManager.findByPublicKey(transaction.data.senderPublicKey); // TODO: rework error handling try { + const transactionHandler: ITransactionHandler = TransactionHandlerRegistry.get(transaction.type); transactionHandler.canBeApplied(transaction, senderWallet); transactionHandler.applyToSender(transaction, senderWallet); } catch (error) { this.logger.error(`BuildWallets from pool: ${error.message}`); + this.purgeByPublicKey(transaction.data.senderPublicKey); } - }); + } + this.logger.info("Transaction Pool Manager build wallets complete"); } - public purgeByPublicKey(senderPublicKey: string) { + public purgeByBlock(block: Interfaces.IBlock): void { + for (const transaction of block.transactions) { + if (this.has(transaction.id)) { + this.removeTransaction(transaction); + + this.walletManager.revertTransactionForSender(transaction); + } + } + } + + public purgeByPublicKey(senderPublicKey: string): void { this.logger.debug(`Purging sender: ${senderPublicKey} from pool wallet manager`); this.removeTransactionsForSender(senderPublicKey); - this.walletManager.deleteWallet(senderPublicKey); + this.walletManager.forget(senderPublicKey); } - /** - * Purges all transactions from senders with at least one - * invalid transaction. - */ - public purgeSendersWithInvalidTransactions(block: Interfaces.IBlock) { - const publicKeys = new Set(block.transactions.filter(tx => !tx.verified).map(tx => tx.data.senderPublicKey)); - - publicKeys.forEach(publicKey => this.purgeByPublicKey(publicKey)); - } + public purgeSendersWithInvalidTransactions(block: Interfaces.IBlock): void { + const publicKeys: Set = new Set( + block.transactions + .filter(transaction => !transaction.verified) + .map(transaction => transaction.data.senderPublicKey), + ); - /** - * Purges all transactions from the block. - * Purges if transaction exists. It assumes that if trx exists that also wallet exists in pool - */ - public purgeBlock(block: Interfaces.IBlock) { - block.transactions.forEach(tx => { - if (this.transactionExists(tx.id)) { - this.removeTransaction(tx); - this.walletManager.revertTransactionForSender(tx); - } - }); + for (const publicKey of publicKeys) { + this.purgeByPublicKey(publicKey); + } } - /** - * Check whether a given sender has any transactions of the specified type - * in the pool. - */ public senderHasTransactionsOfType(senderPublicKey: string, transactionType: Enums.TransactionTypes): boolean { - this.__purgeExpired(); + this.purgeExpired(); - for (const memPoolTransaction of this.mem.getBySender(senderPublicKey)) { - if (memPoolTransaction.transaction.type === transactionType) { + for (const MemoryTransaction of this.memory.getBySender(senderPublicKey)) { + if (MemoryTransaction.transaction.type === transactionType) { return true; } } @@ -522,53 +381,84 @@ export class Connection implements TransactionPool.IConnection { return false; } - /** - * Sync the in-memory storage to the persistent (on-disk) storage if too - * many changes have been accumulated in-memory. - */ - public __syncToPersistentStorageIfNecessary() { - if (this.options.syncInterval <= this.mem.getNumberOfDirty()) { - this.__syncToPersistentStorage(); + private addTransaction(transaction: Interfaces.ITransaction): TransactionPool.IAddTransactionResponse { + if (this.has(transaction.id)) { + this.logger.debug( + "Transaction pool: ignoring attempt to add a transaction that is already " + + `in the pool, id: ${transaction.id}`, + ); + + return { transaction, type: "ERR_ALREADY_IN_POOL", message: "Already in pool" }; + } + + const poolSize: number = this.memory.count(); + + if (this.options.maxTransactionsInPool <= poolSize) { + // The pool can't accommodate more transactions. Either decline the newcomer or remove + // an existing transaction from the pool in order to free up space. + const all: MemoryTransaction[] = this.memory.allSortedByFee(); + const lowest: Interfaces.ITransaction = all[all.length - 1].transaction; + + const fee: Utils.BigNumber = transaction.data.fee; + const lowestFee: Utils.BigNumber = lowest.data.fee; + + if (lowestFee.isLessThan(fee)) { + this.walletManager.revertTransactionForSender(lowest); + this.memory.forget(lowest.id, lowest.data.senderPublicKey); + } else { + return { + transaction, + type: "ERR_POOL_FULL", + message: + `Pool is full (has ${poolSize} transactions) and this transaction's fee ` + + `${fee.toFixed()} is not higher than the lowest fee already in pool ` + + `${lowestFee.toFixed()}`, + }; + } } - } - /** - * Sync the in-memory storage to the persistent (on-disk) storage. - */ - public __syncToPersistentStorage() { - const added = this.mem.getDirtyAddedAndForget(); - this.storage.bulkAdd(added); + this.memory.remember(new MemoryTransaction(transaction), this.options.maxTransactionAge); - const removed = this.mem.getDirtyRemovedAndForget(); - this.storage.bulkRemoveById(removed); + // Apply transaction to pool wallet manager. + const senderWallet: Database.IWallet = this.walletManager.findByPublicKey(transaction.data.senderPublicKey); + + try { + this.walletManager.throwIfApplyingFails(transaction); + + TransactionHandlerRegistry.get(transaction.type).applyToSender(transaction, senderWallet); + } catch (error) { + this.logger.error(error.message); + + this.memory.forget(transaction.id); + + return { transaction, type: "ERR_APPLY", message: error.message }; + } + + this.syncToPersistentStorageIfNecessary(); + + return {}; + } + + private syncToPersistentStorageIfNecessary(): void { + if (this.options.syncInterval <= this.memory.countDirty()) { + this.syncToPersistentStorage(); + } } - /** - * Create an error object which the TransactionGuard understands. - */ - public __createError( - transaction: Interfaces.ITransaction, - type: string, - message: string, - ): TransactionPool.IAddTransactionErrorResponse { - return { - transaction, - type, - message, - success: false, - }; + private syncToPersistentStorage(): void { + this.storage.bulkAdd(this.memory.pullDirtyAdded()); + this.storage.bulkRemoveById(this.memory.pullDirtyRemoved()); } - /** - * Remove all transactions from the pool that have expired. - */ - private __purgeExpired() { - for (const transaction of this.mem.getExpired(this.options.maxTransactionAge)) { + private purgeExpired() { + for (const transaction of this.memory.getExpired(this.options.maxTransactionAge)) { this.emitter.emit("transaction.expired", transaction.data); this.walletManager.revertTransactionForSender(transaction); - this.mem.remove(transaction.id, transaction.data.senderPublicKey); - this.__syncToPersistentStorageIfNecessary(); + + this.memory.forget(transaction.id, transaction.data.senderPublicKey); + + this.syncToPersistentStorageIfNecessary(); } } } diff --git a/packages/core-transaction-pool/src/dynamic-fee/matcher.ts b/packages/core-transaction-pool/src/dynamic-fee.ts similarity index 67% rename from packages/core-transaction-pool/src/dynamic-fee/matcher.ts rename to packages/core-transaction-pool/src/dynamic-fee.ts index f747965f60..939ee4a965 100644 --- a/packages/core-transaction-pool/src/dynamic-fee/matcher.ts +++ b/packages/core-transaction-pool/src/dynamic-fee.ts @@ -2,54 +2,42 @@ import { app } from "@arkecosystem/core-container"; import { Logger } from "@arkecosystem/core-interfaces"; import { Enums, Interfaces, Managers, Utils } from "@arkecosystem/crypto"; import camelCase from "lodash.camelcase"; +import { IDynamicFeeMatch } from "./interfaces"; -/** - * Calculate minimum fee of a transaction for entering the pool. - */ -export function calculateFee(satoshiPerByte: number, transaction: Interfaces.ITransaction): Utils.BigNumber { +function calculateMinimumFee(satoshiPerByte: number, transaction: Interfaces.ITransaction): Utils.BigNumber { if (satoshiPerByte <= 0) { satoshiPerByte = 1; } - let key; - if (transaction.type in Enums.TransactionTypes) { - key = camelCase(Enums.TransactionTypes[transaction.type]); - } else { - key = camelCase(transaction.constructor.name.replace("Transaction", "")); - } + const key: string = camelCase( + transaction.type in Enums.TransactionTypes + ? Enums.TransactionTypes[transaction.type] + : transaction.constructor.name.replace("Transaction", ""), + ); - const addonBytes = app.resolveOptions("transaction-pool").dynamicFees.addonBytes[key]; - - // serialized is in hex - const transactionSizeInBytes = transaction.serialized.length / 2; + const addonBytes: number = app.resolveOptions("transaction-pool").dynamicFees.addonBytes[key]; + const transactionSizeInBytes: number = transaction.serialized.length / 2; return Utils.BigNumber.make(addonBytes + transactionSizeInBytes).times(satoshiPerByte); } -/** - * Determine if a transaction's fee meets the minimum requirements for broadcasting - * and for entering the transaction pool. - * @param {Transaction} Transaction - transaction to check - * @return {Object} { broadcast: Boolean, enterPool: Boolean } - */ -export function dynamicFeeMatcher(transaction: Interfaces.ITransaction): { broadcast: boolean; enterPool: boolean } { - const logger = app.resolvePlugin("logger"); - +// @TODO: better name +export function dynamicFeeMatcher(transaction: Interfaces.ITransaction): IDynamicFeeMatch { const fee: Utils.BigNumber = transaction.data.fee; const id: string = transaction.id; const { dynamicFees } = app.resolveOptions("transaction-pool"); - let broadcast; - let enterPool; + let broadcast: boolean; + let enterPool: boolean; if (dynamicFees.enabled) { - const minFeeBroadcast = calculateFee(dynamicFees.minFeeBroadcast, transaction); + const minFeeBroadcast: Utils.BigNumber = calculateMinimumFee(dynamicFees.minFeeBroadcast, transaction); if (fee.isGreaterThanOrEqualTo(minFeeBroadcast)) { broadcast = true; - logger.debug( + app.resolvePlugin("logger").debug( `Transaction ${id} eligible for broadcast - fee of ${Utils.formatSatoshi(fee)} is ${ fee.isEqualTo(minFeeBroadcast) ? "equal to" : "greater than" } minimum fee (${Utils.formatSatoshi(minFeeBroadcast)})`, @@ -57,19 +45,19 @@ export function dynamicFeeMatcher(transaction: Interfaces.ITransaction): { broad } else { broadcast = false; - logger.debug( + app.resolvePlugin("logger").debug( `Transaction ${id} not eligible for broadcast - fee of ${Utils.formatSatoshi( fee, )} is smaller than minimum fee (${Utils.formatSatoshi(minFeeBroadcast)})`, ); } - const minFeePool: Utils.BigNumber = calculateFee(dynamicFees.minFeePool, transaction); + const minFeePool: Utils.BigNumber = calculateMinimumFee(dynamicFees.minFeePool, transaction); if (fee.isGreaterThanOrEqualTo(minFeePool)) { enterPool = true; - logger.debug( + app.resolvePlugin("logger").debug( `Transaction ${id} eligible to enter pool - fee of ${Utils.formatSatoshi(fee)} is ${ fee.isEqualTo(minFeePool) ? "equal to" : "greater than" } minimum fee (${Utils.formatSatoshi(minFeePool)})`, @@ -77,21 +65,20 @@ export function dynamicFeeMatcher(transaction: Interfaces.ITransaction): { broad } else { enterPool = false; - logger.debug( + app.resolvePlugin("logger").debug( `Transaction ${id} not eligible to enter pool - fee of ${Utils.formatSatoshi( fee, )} is smaller than minimum fee (${Utils.formatSatoshi(minFeePool)})`, ); } } else { - // Static fees const staticFee: Utils.BigNumber = Managers.feeManager.getForTransaction(transaction.data); if (fee.isEqualTo(staticFee)) { broadcast = true; enterPool = true; - logger.debug( + app.resolvePlugin("logger").debug( `Transaction ${id} eligible for broadcast and to enter pool - fee of ${Utils.formatSatoshi( fee, )} is equal to static fee (${Utils.formatSatoshi(staticFee)})`, @@ -100,7 +87,7 @@ export function dynamicFeeMatcher(transaction: Interfaces.ITransaction): { broad broadcast = false; enterPool = false; - logger.debug( + app.resolvePlugin("logger").debug( `Transaction ${id} not eligible for broadcast and not eligible to enter pool - fee of ${Utils.formatSatoshi( fee, )} does not match static fee (${Utils.formatSatoshi(staticFee)})`, diff --git a/packages/core-transaction-pool/src/dynamic-fee/index.ts b/packages/core-transaction-pool/src/dynamic-fee/index.ts deleted file mode 100644 index 97810659c6..0000000000 --- a/packages/core-transaction-pool/src/dynamic-fee/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { calculateFee, dynamicFeeMatcher } from "./matcher"; diff --git a/packages/core-transaction-pool/src/index.ts b/packages/core-transaction-pool/src/index.ts index 556ef73758..837c9be1da 100644 --- a/packages/core-transaction-pool/src/index.ts +++ b/packages/core-transaction-pool/src/index.ts @@ -1,5 +1,3 @@ -export * from "./guard"; export * from "./connection"; export * from "./manager"; -export * from "./pool-wallet-manager"; export * from "./plugin"; diff --git a/packages/core-transaction-pool/src/interfaces.ts b/packages/core-transaction-pool/src/interfaces.ts new file mode 100644 index 0000000000..95b51e1db3 --- /dev/null +++ b/packages/core-transaction-pool/src/interfaces.ts @@ -0,0 +1,17 @@ +import { TransactionPool } from "@arkecosystem/core-interfaces"; +import { Interfaces } from "@arkecosystem/crypto"; + +export interface ITransactionsCached { + added: Interfaces.ITransactionData[]; + notAdded: Interfaces.ITransactionData[]; +} + +export interface ITransactionsProcessed { + added: Interfaces.ITransaction[]; + notAdded: TransactionPool.IAddTransactionResponse[]; +} + +export interface IDynamicFeeMatch { + broadcast: boolean; + enterPool: boolean; +} diff --git a/packages/core-transaction-pool/src/manager.ts b/packages/core-transaction-pool/src/manager.ts index 8be830c914..bbdd74c7e0 100644 --- a/packages/core-transaction-pool/src/manager.ts +++ b/packages/core-transaction-pool/src/manager.ts @@ -8,13 +8,13 @@ export class ConnectionManager { TransactionPool.IConnection >(); - public connection(name = "default"): TransactionPool.IConnection { + public connection(name: string = "default"): TransactionPool.IConnection { return this.connections.get(name); } public async createConnection( connection: TransactionPool.IConnection, - name = "default", + name: string = "default", ): Promise { this.connections.set(name, await this.factory.make(connection)); diff --git a/packages/core-transaction-pool/src/mem-pool-transaction.ts b/packages/core-transaction-pool/src/mem-pool-transaction.ts deleted file mode 100644 index 03e3687e16..0000000000 --- a/packages/core-transaction-pool/src/mem-pool-transaction.ts +++ /dev/null @@ -1,62 +0,0 @@ -// tslint:disable:variable-name - -import { Enums, Interfaces } from "@arkecosystem/crypto"; -import assert from "assert"; - -const { TransactionTypes } = Enums; - -/** - * A mem pool transaction. - * A normal transaction - * + a sequence number used to order by insertion time - * + a get-expiration-time method used to remove old transactions from the pool - */ -export class MemPoolTransaction { - private _transaction: Interfaces.ITransaction; - private _sequence: number; - - /** - * Construct a MemPoolTransaction object. - */ - constructor(transaction: Interfaces.ITransaction, sequence?: number) { - this._transaction = transaction; - - if (sequence !== undefined) { - assert(Number.isInteger(sequence)); - this._sequence = sequence; - } - } - - get transaction(): Interfaces.ITransaction { - return this._transaction; - } - - get sequence(): number { - return this._sequence; - } - - set sequence(seq: number) { - assert.strictEqual(this._sequence, undefined); - this._sequence = seq; - } - - /** - * Derive the transaction expiration time in number of seconds since - * the genesis block. - * @param {Number} maxTransactionAge maximum age (in seconds) of a transaction - * @return {Number} expiration time or null if the transaction does not expire - */ - public expireAt(maxTransactionAge: number): number { - const t = this._transaction; - - if (t.data.expiration > 0) { - return t.data.expiration; - } - - if (t.type !== TransactionTypes.TimelockTransfer) { - return t.data.timestamp + maxTransactionAge; - } - - return null; - } -} diff --git a/packages/core-transaction-pool/src/mem.ts b/packages/core-transaction-pool/src/mem.ts deleted file mode 100644 index c7cd30f3ff..0000000000 --- a/packages/core-transaction-pool/src/mem.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { Crypto, Interfaces, Utils } from "@arkecosystem/crypto"; -import assert from "assert"; -import { MemPoolTransaction } from "./mem-pool-transaction"; - -const { slots } = Crypto; - -export class Mem { - public sequence: number; - public all: MemPoolTransaction[]; - public allIsSorted: boolean; - public byId: { [key: string]: MemPoolTransaction }; - public bySender: { [key: string]: Set }; - public byType: { [key: number]: Set }; - public byExpiration: MemPoolTransaction[]; - public byExpirationIsSorted: boolean; - public dirty: { added: Set; removed: Set }; - - /** - * Create the in-memory transaction pool structures. - */ - constructor() { - /** - * A monotonically increasing number, assigned to each new transaction and - * then incremented. - * Used to: - * - keep insertion order. - */ - this.sequence = 0; - - /** - * An array of MemPoolTransaction sorted by fee (the transaction with the - * highest fee is first). If the fee is equal, they are sorted by insertion - * order. - * Used to: - * - get the transactions with the highest fee - * - get the number of all transactions in the pool - */ - this.all = []; - - /** - * A boolean flag indicating whether `this.all` is indeed sorted or - * temporarily left unsorted. We use lazy sorting of `this.all`: - * - insertion just appends at the end (O(1)) + flag it as unsorted - * - deletion removes by using splice() (O(n)) + flag it as unsorted - * - lookup sorts if it is not sorted (O(n*log(n)) + flag it as sorted - */ - this.allIsSorted = true; - - /** - * A map of (key=transaction id, value=MemPoolTransaction). - * Used to: - * - get a transaction, given its ID - */ - this.byId = {}; - - /** - * A map of (key=sender public key, value=Set of MemPoolTransaction). - * Used to: - * - get all transactions from a given sender - * - get the number of all transactions from a given sender. - */ - this.bySender = {}; - - /** - * A map of (key=transaction type, value=Set of MemPoolTransaction). - * Used to: - * - get all transactions of a given type - */ - this.byType = {}; - - /** - * An array of MemPoolTransaction, sorted by expiration (earliest date - * comes first). This array may not contain all transactions that are - * in the pool, transactions that are without expiration are not included. - * Used to: - * - find all transactions that have expired (have an expiration date - * earlier than a given date) - they are at the beginning of the array. - */ - this.byExpiration = []; - this.byExpirationIsSorted = true; - - /** - * List of dirty transactions ids (that are not saved in the on-disk - * database yet). Used to delay and group operations to the on-disk database. - */ - this.dirty = { - added: new Set(), - removed: new Set(), - }; - } - - /** - * Add a transaction. - * @param {MemPoolTransaction} memPoolTransaction transaction to add - * @param {Number} maxTransactionAge maximum age of a transaction in seconds - * @param {Boolean} thisIsDBLoad if true, then this is the initial - * loading from the database and we do - * not need to schedule the transaction - * that is being added for saving to disk - */ - public add(memPoolTransaction: MemPoolTransaction, maxTransactionAge: number, thisIsDBLoad?: boolean) { - const transaction = memPoolTransaction.transaction; - - assert.strictEqual(this.byId[transaction.id], undefined); - - if (thisIsDBLoad) { - // Sequence is provided from outside, make sure we avoid duplicates - // later when we start using our this.sequence. - assert.strictEqual(typeof memPoolTransaction.sequence, "number"); - this.sequence = Math.max(this.sequence, memPoolTransaction.sequence) + 1; - } else { - // Sequence should only be set during DB load (when sequences come - // from the database). In other scenarios sequence is not set and we - // set it here. - memPoolTransaction.sequence = this.sequence++; - } - - this.all.push(memPoolTransaction); - this.allIsSorted = false; - - this.byId[transaction.id] = memPoolTransaction; - - const sender = transaction.data.senderPublicKey; - const type = transaction.type; - - if (this.bySender[sender] === undefined) { - // First transaction from this sender, create a new Set. - this.bySender[sender] = new Set([memPoolTransaction]); - } else { - // Append to existing transaction ids for this sender. - this.bySender[sender].add(memPoolTransaction); - } - - if (this.byType[type] === undefined) { - // First transaction of this type, create a new Set. - this.byType[type] = new Set([memPoolTransaction]); - } else { - // Append to existing transaction ids for this type. - this.byType[type].add(memPoolTransaction); - } - - if (memPoolTransaction.expireAt(maxTransactionAge) !== null) { - this.byExpiration.push(memPoolTransaction); - this.byExpirationIsSorted = false; - } - - if (!thisIsDBLoad) { - if (this.dirty.removed.has(transaction.id)) { - // If the transaction has been already in the pool and has been removed - // and the removal has not propagated to disk yet, just wipe it from the - // list of removed transactions, so that the old copy stays on disk. - this.dirty.removed.delete(transaction.id); - } else { - this.dirty.added.add(transaction.id); - } - } - } - - /** - * Remove a transaction. - */ - public remove(id: string, senderPublicKey?: string) { - if (this.byId[id] === undefined) { - // Not found, not in pool - return; - } - - if (senderPublicKey === undefined) { - senderPublicKey = this.byId[id].transaction.data.senderPublicKey; - } - - const memPoolTransaction = this.byId[id]; - const type = this.byId[id].transaction.type; - - // XXX worst case: O(n) - let i = this.byExpiration.findIndex(e => e.transaction.id === id); - if (i !== -1) { - this.byExpiration.splice(i, 1); - } - - this.bySender[senderPublicKey].delete(memPoolTransaction); - if (this.bySender[senderPublicKey].size === 0) { - delete this.bySender[senderPublicKey]; - } - - this.byType[type].delete(memPoolTransaction); - if (this.byType[type].size === 0) { - delete this.byType[type]; - } - - delete this.byId[id]; - - i = this.all.findIndex(e => e.transaction.id === id); - assert.notStrictEqual(i, -1); - this.all.splice(i, 1); - this.allIsSorted = false; - - if (this.dirty.added.has(id)) { - // This transaction has been added and deleted without data being synced - // to disk in between, so it will never touch the disk, just remove it - // from the added list. - this.dirty.added.delete(id); - } else { - this.dirty.removed.add(id); - } - } - - /** - * Get the number of transactions. - */ - public getSize(): number { - return this.all.length; - } - - /** - * Get all transactions from a given sender. - */ - public getBySender(senderPublicKey: string): Set { - const memPoolTransactions = this.bySender[senderPublicKey]; - if (memPoolTransactions !== undefined) { - return memPoolTransactions; - } - return new Set(); - } - - /** - * Get all transactions of a given type. - * @param {Number} type of transaction - * @return {Set of MemPoolTransaction} all transactions of the given type, could be empty Set - */ - public getByType(type) { - const memPoolTransactions = this.byType[type]; - if (memPoolTransactions !== undefined) { - return memPoolTransactions; - } - return new Set(); - } - - /** - * Get a transaction, given its id. - */ - public getTransactionById(id: string): Interfaces.ITransaction | undefined { - if (this.byId[id] === undefined) { - return undefined; - } - return this.byId[id].transaction; - } - - /** - * Get an array of all transactions ordered by fee. - * Transactions are ordered by fee (highest fee first) or by - * insertion time, if fees equal (earliest transaction first). - */ - public getTransactionsOrderedByFee(): MemPoolTransaction[] { - if (!this.allIsSorted) { - this.all.sort((a, b) => { - const feeA = a.transaction.data.fee as Utils.BigNumber; - const feeB = b.transaction.data.fee as Utils.BigNumber; - if (feeA.isGreaterThan(feeB)) { - return -1; - } - if (feeA.isLessThan(feeB)) { - return 1; - } - return a.sequence - b.sequence; - }); - this.allIsSorted = true; - } - - return this.all; - } - - /** - * Check if a transaction with a given id exists. - */ - public transactionExists(id: string): boolean { - return this.byId[id] !== undefined; - } - - /** - * Get the expired transactions. - */ - public getExpired(maxTransactionAge: number): Interfaces.ITransaction[] { - if (!this.byExpirationIsSorted) { - this.byExpiration.sort((a, b) => a.expireAt(maxTransactionAge) - b.expireAt(maxTransactionAge)); - this.byExpirationIsSorted = true; - } - - const now = slots.getTime(); - - const transactions = []; - - for (const memPoolTransaction of this.byExpiration) { - if (memPoolTransaction.expireAt(maxTransactionAge) <= now) { - transactions.push(memPoolTransaction.transaction); - } else { - break; - } - } - - return transactions; - } - - /** - * Remove all transactions. - */ - public flush() { - this.all = []; - this.allIsSorted = true; - this.byId = {}; - this.bySender = {}; - this.byType = {}; - this.byExpiration = []; - this.byExpirationIsSorted = true; - this.dirty.added.clear(); - this.dirty.removed.clear(); - } - - /** - * Get the number of dirty transactions (added or removed, but those additions or - * removals have not been applied to the persistent storage). - */ - public getNumberOfDirty(): number { - return this.dirty.added.size + this.dirty.removed.size; - } - - /** - * Get the dirty transactions that were added and forget they are dirty. - * In other words, get the transactions that were added since the last - * call to this method (or to the flush() method). - */ - public getDirtyAddedAndForget(): MemPoolTransaction[] { - const added: MemPoolTransaction[] = []; - this.dirty.added.forEach(id => added.push(this.byId[id])); - this.dirty.added.clear(); - return added; - } - - /** - * Get the ids of dirty transactions that were removed and forget them completely. - * In other words, get the transactions that were removed since the last - * call to this method (or to the flush() method). - */ - public getDirtyRemovedAndForget(): string[] { - const removed = Array.from(this.dirty.removed); - this.dirty.removed.clear(); - return removed; - } -} diff --git a/packages/core-transaction-pool/src/memory-transaction.ts b/packages/core-transaction-pool/src/memory-transaction.ts new file mode 100644 index 0000000000..feb14dc8c0 --- /dev/null +++ b/packages/core-transaction-pool/src/memory-transaction.ts @@ -0,0 +1,41 @@ +// tslint:disable:variable-name + +import { Enums, Interfaces } from "@arkecosystem/crypto"; +import assert from "assert"; + +export class MemoryTransaction { + // @TODO: remove the need for disabling tslint rules + private _sequence: number; + + constructor(readonly transaction: Interfaces.ITransaction, sequence?: number) { + if (sequence !== undefined) { + assert(Number.isInteger(sequence)); + + this._sequence = sequence; + } + } + + get sequence(): number { + return this._sequence; + } + + set sequence(seq: number) { + assert.strictEqual(this._sequence, undefined); + + this._sequence = seq; + } + + public expiresAt(maxTransactionAge: number): number { + const transaction: Interfaces.ITransaction = this.transaction; + + if (transaction.data.expiration > 0) { + return transaction.data.expiration; + } + + if (transaction.type !== Enums.TransactionTypes.TimelockTransfer) { + return transaction.data.timestamp + maxTransactionAge; + } + + return null; + } +} diff --git a/packages/core-transaction-pool/src/memory.ts b/packages/core-transaction-pool/src/memory.ts new file mode 100644 index 0000000000..695546daf6 --- /dev/null +++ b/packages/core-transaction-pool/src/memory.ts @@ -0,0 +1,245 @@ +import { Crypto, Interfaces, Utils } from "@arkecosystem/crypto"; +import assert from "assert"; +import { MemoryTransaction } from "./memory-transaction"; + +export class Memory { + private sequence: number = 0; + private all: MemoryTransaction[] = []; + /** + * A boolean flag indicating whether `this.all` is indeed sorted or + * temporarily left unsorted. We use lazy sorting of `this.all`: + * - insertion just appends at the end (O(1)) + flag it as unsorted + * - deletion removes by using splice() (O(n)) + flag it as unsorted + * - lookup sorts if it is not sorted (O(n*log(n)) + flag it as sorted + * + * @TODO: remove the need for a comment + */ + private allIsSorted: boolean = true; + private byId: { [key: string]: MemoryTransaction } = {}; + private bySender: { [key: string]: Set } = {}; + private byType: { [key: number]: Set } = {}; + private byExpiration: MemoryTransaction[] = []; + private byExpirationIsSorted: boolean = true; + private readonly dirty: { added: Set; removed: Set } = { + added: new Set(), + removed: new Set(), + }; + + public allSortedByFee(): MemoryTransaction[] { + if (!this.allIsSorted) { + this.all.sort((a, b) => { + const feeA: Utils.BigNumber = a.transaction.data.fee; + const feeB: Utils.BigNumber = b.transaction.data.fee; + + if (feeA.isGreaterThan(feeB)) { + return -1; + } + + if (feeA.isLessThan(feeB)) { + return 1; + } + + return a.sequence - b.sequence; + }); + + this.allIsSorted = true; + } + + return this.all; + } + + public getExpired(maxTransactionAge: number): Interfaces.ITransaction[] { + if (!this.byExpirationIsSorted) { + this.byExpiration.sort((a, b) => a.expiresAt(maxTransactionAge) - b.expiresAt(maxTransactionAge)); + this.byExpirationIsSorted = true; + } + + const now: number = Crypto.slots.getTime(); + const transactions: Interfaces.ITransaction[] = []; + + for (const MemoryTransaction of this.byExpiration) { + if (MemoryTransaction.expiresAt(maxTransactionAge) >= now) { + break; + } + + transactions.push(MemoryTransaction.transaction); + } + + return transactions; + } + + public getById(id: string): Interfaces.ITransaction | undefined { + if (this.byId[id] === undefined) { + return undefined; + } + + return this.byId[id].transaction; + } + + public getByType(type: number): Set { + const MemoryTransactions: Set = this.byType[type]; + + if (MemoryTransactions !== undefined) { + return MemoryTransactions; + } + + return new Set(); + } + + public getBySender(senderPublicKey: string): Set { + const MemoryTransactions: Set = this.bySender[senderPublicKey]; + + if (MemoryTransactions !== undefined) { + return MemoryTransactions; + } + + return new Set(); + } + + public remember(MemoryTransaction: MemoryTransaction, maxTransactionAge: number, databaseReady?: boolean): void { + const transaction: Interfaces.ITransaction = MemoryTransaction.transaction; + + assert.strictEqual(this.byId[transaction.id], undefined); + + if (databaseReady) { + // Sequence is provided from outside, make sure we avoid duplicates + // later when we start using our this.sequence. + assert.strictEqual(typeof MemoryTransaction.sequence, "number"); + + this.sequence = Math.max(this.sequence, MemoryTransaction.sequence) + 1; + } else { + // Sequence should only be set during DB load (when sequences come + // from the database). In other scenarios sequence is not set and we + // set it here. + MemoryTransaction.sequence = this.sequence++; + } + + this.all.push(MemoryTransaction); + this.allIsSorted = false; + + this.byId[transaction.id] = MemoryTransaction; + + const sender: string = transaction.data.senderPublicKey; + const type: number = transaction.type; + + if (this.bySender[sender] === undefined) { + // First transaction from this sender, create a new Set. + this.bySender[sender] = new Set([MemoryTransaction]); + } else { + // Append to existing transaction ids for this sender. + this.bySender[sender].add(MemoryTransaction); + } + + if (this.byType[type] === undefined) { + // First transaction of this type, create a new Set. + this.byType[type] = new Set([MemoryTransaction]); + } else { + // Append to existing transaction ids for this type. + this.byType[type].add(MemoryTransaction); + } + + if (MemoryTransaction.expiresAt(maxTransactionAge) !== null) { + this.byExpiration.push(MemoryTransaction); + this.byExpirationIsSorted = false; + } + + if (!databaseReady) { + if (this.dirty.removed.has(transaction.id)) { + // If the transaction has been already in the pool and has been removed + // and the removal has not propagated to disk yet, just wipe it from the + // list of removed transactions, so that the old copy stays on disk. + this.dirty.removed.delete(transaction.id); + } else { + this.dirty.added.add(transaction.id); + } + } + } + + public forget(id: string, senderPublicKey?: string): void { + if (this.byId[id] === undefined) { + return; + } + + if (senderPublicKey === undefined) { + senderPublicKey = this.byId[id].transaction.data.senderPublicKey; + } + + const MemoryTransaction: MemoryTransaction = this.byId[id]; + const type: number = this.byId[id].transaction.type; + + // XXX worst case: O(n) + let i: number = this.byExpiration.findIndex(e => e.transaction.id === id); + if (i !== -1) { + this.byExpiration.splice(i, 1); + } + + this.bySender[senderPublicKey].delete(MemoryTransaction); + if (this.bySender[senderPublicKey].size === 0) { + delete this.bySender[senderPublicKey]; + } + + this.byType[type].delete(MemoryTransaction); + if (this.byType[type].size === 0) { + delete this.byType[type]; + } + + delete this.byId[id]; + + i = this.all.findIndex(e => e.transaction.id === id); + assert.notStrictEqual(i, -1); + this.all.splice(i, 1); + this.allIsSorted = false; + + if (this.dirty.added.has(id)) { + // This transaction has been added and deleted without data being synced + // to disk in between, so it will never touch the disk, just remove it + // from the added list. + this.dirty.added.delete(id); + } else { + this.dirty.removed.add(id); + } + } + + public has(id: string): boolean { + return this.byId[id] !== undefined; + } + + public flush(): void { + this.all = []; + this.allIsSorted = true; + this.byId = {}; + this.bySender = {}; + this.byType = {}; + this.byExpiration = []; + this.byExpirationIsSorted = true; + this.dirty.added.clear(); + this.dirty.removed.clear(); + } + + public count(): number { + return this.all.length; + } + + public countDirty(): number { + return this.dirty.added.size + this.dirty.removed.size; + } + + public pullDirtyAdded(): MemoryTransaction[] { + const added: MemoryTransaction[] = []; + + for (const id of this.dirty.added) { + added.push(this.byId[id]); + } + + this.dirty.added.clear(); + + return added; + } + + public pullDirtyRemoved(): string[] { + const removed: string[] = Array.from(this.dirty.removed); + this.dirty.removed.clear(); + + return removed; + } +} diff --git a/packages/core-transaction-pool/src/plugin.ts b/packages/core-transaction-pool/src/plugin.ts index cc322d6c53..e4c268ac9b 100644 --- a/packages/core-transaction-pool/src/plugin.ts +++ b/packages/core-transaction-pool/src/plugin.ts @@ -2,6 +2,9 @@ import { Container, Logger, TransactionPool } from "@arkecosystem/core-interface import { Connection } from "./connection"; import { defaults } from "./defaults"; import { ConnectionManager } from "./manager"; +import { Memory } from "./memory"; +import { Storage } from "./storage"; +import { WalletManager } from "./wallet-manager"; export const plugin: Container.PluginDescriptor = { pkg: require("../package.json"), @@ -10,11 +13,16 @@ export const plugin: Container.PluginDescriptor = { async register(container: Container.IContainer, options) { container.resolvePlugin("logger").info("Connecting to transaction pool"); - const connectionManager: ConnectionManager = new ConnectionManager(); - - return connectionManager.createConnection(new Connection(options)); + return new ConnectionManager().createConnection( + new Connection({ + options, + walletManager: new WalletManager(), + memory: new Memory(), + storage: new Storage(options.storage.toString()), + }), + ); }, - async deregister(container: Container.IContainer, options) { + async deregister(container: Container.IContainer) { container.resolvePlugin("logger").info("Disconnecting from transaction pool"); return container.resolvePlugin("transaction-pool").disconnect(); diff --git a/packages/core-transaction-pool/src/pool-wallet-manager.ts b/packages/core-transaction-pool/src/pool-wallet-manager.ts deleted file mode 100644 index b4dd087209..0000000000 --- a/packages/core-transaction-pool/src/pool-wallet-manager.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { app } from "@arkecosystem/core-container"; -import { Wallet, WalletManager } from "@arkecosystem/core-database"; -import { Database } from "@arkecosystem/core-interfaces"; -import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; -import { Identities, Interfaces, Utils } from "@arkecosystem/crypto"; - -export class PoolWalletManager extends WalletManager { - public readonly databaseService = app.resolvePlugin("database"); - - /** - * Get a wallet by the given address. If wallet is not found it is copied from blockchain - * wallet manager. Method overrides base class method from WalletManager. - * WARNING: call only upon guard apply, as if wallet not found it gets it from blockchain. - * For existing key checks use function exists(key) - * @param {String} address - * @return {(Wallet|null)} - */ - public findByAddress(address): Database.IWallet { - if (address && !this.byAddress[address]) { - const blockchainWallet = this.databaseService.walletManager.findByAddress(address); - const wallet = Object.assign(new Wallet(address), blockchainWallet); // do not modify - - this.reindex(wallet); - } - - return this.byAddress[address]; - } - - public deleteWallet(publicKey) { - this.forgetByPublicKey(publicKey); - this.forgetByAddress(Identities.Address.fromPublicKey(publicKey)); - } - - /** - * Checks if the transaction can be applied. - */ - public canApply(transaction: Interfaces.ITransaction, errors): boolean { - // Edge case if sender is unknown and has no balance. - // NOTE: Check is performed against the database wallet manager. - if (!this.databaseService.walletManager.exists(transaction.data.senderPublicKey)) { - const senderAddress = Identities.Address.fromPublicKey(transaction.data.senderPublicKey); - - if (this.databaseService.walletManager.findByAddress(senderAddress).balance.isZero()) { - errors.push("Cold wallet is not allowed to send until receiving transaction is confirmed."); - return false; - } - } - - const { data } = transaction; - const sender = this.findByPublicKey(data.senderPublicKey); - - if (Utils.isException(data)) { - this.logger.warn( - `Transaction forcibly applied because it has been added as an exception: ${transaction.id}`, - ); - } else { - try { - const transactionHandler = TransactionHandlerRegistry.get(transaction.type); - transactionHandler.canBeApplied(transaction, sender, this.databaseService.walletManager); - } catch (error) { - const message = `[PoolWalletManager] Can't apply transaction ${transaction.id} from ${sender.address}`; - this.logger.error(`${message} due to ${JSON.stringify(error.message)}`); - errors.unshift(error.message); - } - } - - return errors.length === 0; - } - - /** - * Remove the given transaction from a sender only. - */ - public revertTransactionForSender(transaction: Interfaces.ITransaction) { - const { data } = transaction; - const sender = this.findByPublicKey(data.senderPublicKey); // Should exist - - const transactionHandler = TransactionHandlerRegistry.get(transaction.type); - transactionHandler.revertForSender(transaction, sender); - } -} diff --git a/packages/core-transaction-pool/src/guard.ts b/packages/core-transaction-pool/src/processor.ts similarity index 54% rename from packages/core-transaction-pool/src/guard.ts rename to packages/core-transaction-pool/src/processor.ts index f50c1e998e..b2dba247d5 100644 --- a/packages/core-transaction-pool/src/guard.ts +++ b/packages/core-transaction-pool/src/processor.ts @@ -4,34 +4,34 @@ import { errors, TransactionHandlerRegistry } from "@arkecosystem/core-transacti import { Crypto, Enums, Errors as CryptoErrors, Interfaces, Managers, Transactions } from "@arkecosystem/crypto"; import pluralize from "pluralize"; import { dynamicFeeMatcher } from "./dynamic-fee"; - -export class TransactionGuard implements TransactionPool.IGuard { - public transactions: Interfaces.ITransactionData[] = []; - public excess: string[] = []; - public accept: Map = new Map(); - public broadcast: Map = new Map(); - public invalid: Map = new Map(); - public errors: { [key: string]: TransactionPool.ITransactionErrorResponse[] } = {}; - - constructor(public pool: TransactionPool.IConnection) {} - - public async validate(transactions: Interfaces.ITransactionData[]): Promise { - this.pool.loggedAllowedSenders = []; - - // Cache transactions - this.transactions = this.__cacheTransactions(transactions); +import { IDynamicFeeMatch, ITransactionsCached, ITransactionsProcessed } from "./interfaces"; +import { WalletManager } from "./wallet-manager"; + +/** + * @TODO: this class has too many responsibilities at the moment. + * Its sole responsibility should be to validate transactions and return them. + */ +export class Processor implements TransactionPool.IProcessor { + private transactions: Interfaces.ITransactionData[] = []; + private readonly excess: string[] = []; + private readonly accept: Map = new Map(); + private readonly broadcast: Map = new Map(); + private readonly invalid: Map = new Map(); + private readonly errors: { [key: string]: TransactionPool.ITransactionErrorResponse[] } = {}; + + constructor(private readonly pool: TransactionPool.IConnection, private readonly walletManager: WalletManager) {} + + public async validate(transactions: Interfaces.ITransactionData[]): Promise { + this.cacheTransactions(transactions); if (this.transactions.length > 0) { - // Filter transactions and create Transaction instances from accepted ones - this.__filterAndTransformTransactions(this.transactions); + this.filterAndTransformTransactions(this.transactions); - // Remove already forged tx... Not optimal here - await this.__removeForgedTransactions(); + await this.removeForgedTransactions(); - // Add transactions to the pool - this.__addTransactionsToPool(); + this.addTransactionsToPool(); - this.__printStats(); + this.printStats(); } return { @@ -43,43 +43,62 @@ export class TransactionGuard implements TransactionPool.IGuard { }; } - /** - * Cache the given transactions and return which got added. Already cached - * transactions are not returned. - */ - public __cacheTransactions(transactions: Interfaces.ITransactionData[]) { - const { added, notAdded } = app.resolve("state").cacheTransactions(transactions); + public getTransactions(): Interfaces.ITransactionData[] { + return this.transactions; + } + + public getBroadcastTransactions(): Interfaces.ITransaction[] { + return Array.from(this.broadcast.values()); + } + + public getErrors(): { [key: string]: TransactionPool.ITransactionErrorResponse[] } { + return this.errors; + } + + public pushError(transaction: Interfaces.ITransactionData, type: string, message: string): void { + if (!this.errors[transaction.id]) { + this.errors[transaction.id] = []; + } + + this.errors[transaction.id].push({ type, message }); + + this.invalid.set(transaction.id, transaction); + } + + private cacheTransactions(transactions: Interfaces.ITransactionData[]): void { + const { added, notAdded }: ITransactionsCached = app + .resolve("state") + .cacheTransactions(transactions); + + this.transactions = added; - notAdded.forEach(transaction => { + for (const transaction of notAdded) { if (!this.errors[transaction.id]) { this.pushError(transaction, "ERR_DUPLICATE", "Already in cache."); } - }); - - return added; + } } - /** - * Get broadcast transactions. - */ - public getBroadcastTransactions(): Interfaces.ITransaction[] { - return Array.from(this.broadcast.values()); + private async removeForgedTransactions(): Promise { + const forgedIdsSet: string[] = await app + .resolvePlugin("database") + .getForgedTransactionsIds([...new Set([...this.accept.keys(), ...this.broadcast.keys()])]); + + app.resolve("state").removeCachedTransactionIds(forgedIdsSet); + + for (const id of forgedIdsSet) { + this.pushError(this.accept.get(id).data, "ERR_FORGED", "Already forged."); + + this.accept.delete(id); + this.broadcast.delete(id); + } } - /** - * Transforms and filters incoming transactions. - * It skips: - * - transactions already in the pool - * - transactions from blocked senders - * - transactions that are too large - * - transactions from the future - * - dynamic fee mismatch - * - transactions based on type specific restrictions - * - not valid crypto transactions - */ - public __filterAndTransformTransactions(transactions: Interfaces.ITransactionData[]): void { - transactions.forEach(transaction => { - const exists = this.pool.transactionExists(transaction.id); + private filterAndTransformTransactions(transactions: Interfaces.ITransactionData[]): void { + const { maxTransactionBytes } = app.resolveOptions("transaction-pool"); + + for (const transaction of transactions) { + const exists: boolean = this.pool.has(transaction.id); if (exists) { this.pushError(transaction, "ERR_DUPLICATE", `Duplicate transaction ${transaction.id}`); @@ -89,22 +108,25 @@ export class TransactionGuard implements TransactionPool.IGuard { "ERR_SENDER_BLOCKED", `Transaction ${transaction.id} rejected. Sender ${transaction.senderPublicKey} is blocked.`, ); - } else if (JSON.stringify(transaction).length > this.pool.options.maxTransactionBytes) { + } else if (JSON.stringify(transaction).length > maxTransactionBytes) { this.pushError( transaction, "ERR_TOO_LARGE", - `Transaction ${transaction.id} is larger than ${this.pool.options.maxTransactionBytes} bytes.`, + `Transaction ${transaction.id} is larger than ${maxTransactionBytes} bytes.`, ); } else if (this.pool.hasExceededMaxTransactions(transaction)) { this.excess.push(transaction.id); - } else if (this.__validateTransaction(transaction)) { + } else if (this.validateTransaction(transaction)) { try { - const receivedId = transaction.id; - const trx = Transactions.TransactionFactory.fromData(transaction); + const receivedId: string = transaction.id; + const trx: Interfaces.ITransaction = Transactions.TransactionFactory.fromData(transaction); + if (trx.verified) { - const applyErrors = []; - if (this.pool.walletManager.canApply(trx, applyErrors)) { - const dynamicFee = dynamicFeeMatcher(trx); + try { + this.walletManager.throwIfApplyingFails(trx); + + const dynamicFee: IDynamicFeeMatch = dynamicFeeMatcher(trx); + if (!dynamicFee.enterPool && !dynamicFee.broadcast) { this.pushError( transaction, @@ -120,11 +142,12 @@ export class TransactionGuard implements TransactionPool.IGuard { this.broadcast.set(trx.data.id, trx); } } - } else { - this.pushError(transaction, "ERR_APPLY", JSON.stringify(applyErrors)); + } catch (error) { + this.pushError(transaction, "ERR_APPLY", error.message); } } else { transaction.id = receivedId; + this.pushError( transaction, "ERR_BAD_DATA", @@ -139,28 +162,21 @@ export class TransactionGuard implements TransactionPool.IGuard { } } } - }); + } } - /** - * Determines valid transactions by checking rules, according to: - * - transaction timestamp - * - wallet balance - * - network if set - * - transaction type specifics: - * - if recipient is on the same network - * - if sender already has another transaction of the same type, for types that - * - only allow one transaction at a time in the pool (e.g. vote) - */ - public __validateTransaction(transaction: Interfaces.ITransactionData): boolean { - const now = Crypto.slots.getTime(); + private validateTransaction(transaction: Interfaces.ITransactionData): boolean { + const now: number = Crypto.slots.getTime(); + if (transaction.timestamp > now + 3600) { - const secondsInFuture = transaction.timestamp - now; + const secondsInFuture: number = transaction.timestamp - now; + this.pushError( transaction, "ERR_FROM_FUTURE", `Transaction ${transaction.id} is ${secondsInFuture} seconds in the future`, ); + return false; } @@ -172,19 +188,23 @@ export class TransactionGuard implements TransactionPool.IGuard { "pubKeyHash", )}'`, ); + return false; } - const { type } = transaction; try { - const handler = TransactionHandlerRegistry.get(type); - return handler.canEnterTransactionPool(transaction, this); + // @TODO: this leaks private members, refactor this + return TransactionHandlerRegistry.get(transaction.type).canEnterTransactionPool( + transaction, + this.pool, + this, + ); } catch (error) { if (error instanceof errors.InvalidTransactionTypeError) { this.pushError( transaction, "ERR_UNSUPPORTED", - `Invalidating transaction of unsupported type '${Enums.TransactionTypes[type]}'`, + `Invalidating transaction of unsupported type '${Enums.TransactionTypes[transaction.type]}'`, ); } else { this.pushError(transaction, "ERR_UNKNOWN", error.message); @@ -194,67 +214,22 @@ export class TransactionGuard implements TransactionPool.IGuard { return false; } - /** - * Remove already forged transactions. - */ - public async __removeForgedTransactions() { - const databaseService = app.resolvePlugin("database"); - - const forgedIdsSet = await databaseService.getForgedTransactionsIds([ - ...new Set([...this.accept.keys(), ...this.broadcast.keys()]), - ]); - - app.resolve("state").removeCachedTransactionIds(forgedIdsSet); - - forgedIdsSet.forEach(id => { - this.pushError(this.accept.get(id).data, "ERR_FORGED", "Already forged."); - - this.accept.delete(id); - this.broadcast.delete(id); - }); - } - - /** - * Add accepted transactions to the pool and filter rejected ones. - */ - public __addTransactionsToPool() { - // Add transactions to the transaction pool - const { notAdded } = this.pool.addTransactions(Array.from(this.accept.values())); + private addTransactionsToPool(): void { + const { notAdded }: ITransactionsProcessed = this.pool.addTransactions(Array.from(this.accept.values())); - // Exclude transactions which were refused from the pool - notAdded.forEach(item => { + for (const item of notAdded) { this.accept.delete(item.transaction.id); - // The transaction should still be broadcasted if the pool is full if (item.type !== "ERR_POOL_FULL") { this.broadcast.delete(item.transaction.id); } this.pushError(item.transaction.data, item.type, item.message); - }); - } - - /** - * Adds a transaction to the errors object. The transaction id is mapped to an - * array of errors. There may be multiple errors associated with a transaction in - * which case pushError is called multiple times. - */ - public pushError(transaction: Interfaces.ITransactionData, type: string, message: string) { - if (!this.errors[transaction.id]) { - this.errors[transaction.id] = []; } - - this.errors[transaction.id].push({ type, message }); - - this.invalid.set(transaction.id, transaction); } - /** - * Print compact transaction stats. - */ - public __printStats() { - const properties = ["accept", "broadcast", "excess", "invalid"]; - const stats = properties + private printStats(): void { + const stats: string = ["accept", "broadcast", "excess", "invalid"] .map(prop => `${prop}: ${this[prop] instanceof Array ? this[prop].length : this[prop].size}`) .join(" "); diff --git a/packages/core-transaction-pool/src/storage.ts b/packages/core-transaction-pool/src/storage.ts index 30c42c6ad6..66cf76e34d 100644 --- a/packages/core-transaction-pool/src/storage.ts +++ b/packages/core-transaction-pool/src/storage.ts @@ -1,25 +1,18 @@ import { Transactions } from "@arkecosystem/crypto"; import BetterSqlite3 from "better-sqlite3"; -import fs from "fs-extra"; -import { MemPoolTransaction } from "./mem-pool-transaction"; +import { ensureFileSync } from "fs-extra"; +import { MemoryTransaction } from "./memory-transaction"; -/** - * A permanent storage (on-disk), supporting some basic functionalities required - * by the transaction pool. - */ export class Storage { private readonly table: string = "pool"; - private db: BetterSqlite3.Database; + private database: BetterSqlite3.Database; - /** - * Construct the storage. - */ constructor(file: string) { - fs.ensureFileSync(file); + ensureFileSync(file); - this.db = new BetterSqlite3(file); + this.database = new BetterSqlite3(file); - this.db.exec(` + this.database.exec(` PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS ${this.table} ( "sequence" INTEGER PRIMARY KEY, @@ -29,78 +22,69 @@ export class Storage { `); } - /** - * Close the storage. - */ public close(): void { - this.db.close(); - this.db = null; + this.database.close(); + this.database = null; } - /** - * Add a bunch of new entries to the storage. - */ - public bulkAdd(data: MemPoolTransaction[]): void { + public bulkAdd(data: MemoryTransaction[]): void { if (data.length === 0) { return; } - const insertStatement = this.db.prepare( + const insertStatement = this.database.prepare( `INSERT INTO ${this.table} ` + "(sequence, id, serialized) VALUES " + "(:sequence, :id, :serialized);", ); try { - this.db.prepare("BEGIN;").run(); + this.database.prepare("BEGIN;").run(); - data.forEach(d => + for (const d of data) { insertStatement.run({ sequence: d.sequence, id: d.transaction.id, serialized: d.transaction.serialized, - }), - ); + }); + } - this.db.prepare("COMMIT;").run(); + this.database.prepare("COMMIT;").run(); } finally { - if (this.db.inTransaction) { - this.db.prepare("ROLLBACK;").run(); + if (this.database.inTransaction) { + this.database.prepare("ROLLBACK;").run(); } } } - /** - * Remove a bunch of entries, given their ids. - */ public bulkRemoveById(ids: string[]): void { if (ids.length === 0) { return; } - const deleteStatement = this.db.prepare(`DELETE FROM ${this.table} WHERE id = :id;`); + const deleteStatement: BetterSqlite3.Statement = this.database.prepare( + `DELETE FROM ${this.table} WHERE id = :id;`, + ); - this.db.prepare("BEGIN;").run(); + this.database.prepare("BEGIN;").run(); - ids.forEach(id => deleteStatement.run({ id })); + for (const id of ids) { + deleteStatement.run({ id }); + } - this.db.prepare("COMMIT;").run(); + this.database.prepare("COMMIT;").run(); } - /** - * Load all entries. - */ - public loadAll(): MemPoolTransaction[] { - const rows = this.db.prepare(`SELECT sequence, lower(HEX(serialized)) AS serialized FROM ${this.table};`).all(); + public loadAll(): MemoryTransaction[] { + const rows: Array<{ sequence: number; serialized: string }> = this.database + .prepare(`SELECT sequence, lower(HEX(serialized)) AS serialized FROM ${this.table};`) + .all(); return rows - .map(r => ({ tx: Transactions.TransactionFactory.fromHex(r.serialized), ...r })) - .filter(r => r.tx.verified) - .map(r => new MemPoolTransaction(r.tx, r.sequence)); + .map(r => ({ transaction: Transactions.TransactionFactory.fromHex(r.serialized), ...r })) + .filter(r => r.transaction.verified) + .map(r => new MemoryTransaction(r.transaction, r.sequence)); } - /** - * Delete all entries. - */ public deleteAll(): void { - this.db.exec(`DELETE FROM ${this.table};`); + this.database.exec(`DELETE FROM ${this.table};`); } } diff --git a/packages/core-transaction-pool/src/utils.ts b/packages/core-transaction-pool/src/utils.ts deleted file mode 100644 index 0300b4788a..0000000000 --- a/packages/core-transaction-pool/src/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { app } from "@arkecosystem/core-container"; -import { Logger } from "@arkecosystem/core-interfaces"; -import { Interfaces, Managers } from "@arkecosystem/crypto"; -import bs58check from "bs58check"; - -export function isRecipientOnActiveNetwork(transaction: Interfaces.ITransactionData): boolean { - const recipientPrefix = bs58check.decode(transaction.recipientId).readUInt8(0); - - if (recipientPrefix === Managers.configManager.get("network.pubKeyHash")) { - return true; - } - - app.resolvePlugin("logger").error( - `Recipient ${transaction.recipientId} is not on the same network: ${Managers.configManager.get( - "network.pubKeyHash", - )}`, - ); - - return false; -} diff --git a/packages/core-transaction-pool/src/wallet-manager.ts b/packages/core-transaction-pool/src/wallet-manager.ts new file mode 100644 index 0000000000..f172e76b0f --- /dev/null +++ b/packages/core-transaction-pool/src/wallet-manager.ts @@ -0,0 +1,65 @@ +import { app } from "@arkecosystem/core-container"; +import { WalletManager as BaseWalletManager } from "@arkecosystem/core-database"; +import { Database } from "@arkecosystem/core-interfaces"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; +import { Identities, Interfaces, Utils } from "@arkecosystem/crypto"; + +export class WalletManager extends BaseWalletManager { + private readonly databaseService: Database.IDatabaseService = app.resolvePlugin( + "database", + ); + + public findByAddress(address: string): Database.IWallet { + if (address && !this.byAddress[address]) { + this.reindex({ ...this.databaseService.walletManager.findByAddress(address) }); + } + + return this.byAddress[address]; + } + + public forget(publicKey: string): void { + this.forgetByPublicKey(publicKey); + this.forgetByAddress(Identities.Address.fromPublicKey(publicKey)); + } + + public throwIfApplyingFails(transaction: Interfaces.ITransaction): void { + // Edge case if sender is unknown and has no balance. + // NOTE: Check is performed against the database wallet manager. + if (!this.databaseService.walletManager.has(transaction.data.senderPublicKey)) { + const senderAddress: string = Identities.Address.fromPublicKey(transaction.data.senderPublicKey); + + if (this.databaseService.walletManager.findByAddress(senderAddress).balance.isZero()) { + throw new Error("Cold wallet is not allowed to send until receiving transaction is confirmed."); + } + } + + if (Utils.isException(transaction.data)) { + this.logger.warn( + `Transaction forcibly applied because it has been added as an exception: ${transaction.id}`, + ); + } else { + const sender: Database.IWallet = this.findByPublicKey(transaction.data.senderPublicKey); + + try { + TransactionHandlerRegistry.get(transaction.type).canBeApplied( + transaction, + sender, + this.databaseService.walletManager, + ); + } catch (error) { + throw new Error( + `[PoolWalletManager] Can't apply transaction ${transaction.id} from ${ + sender.address + } due to ${JSON.stringify(error.message)}`, + ); + } + } + } + + public revertTransactionForSender(transaction: Interfaces.ITransaction): void { + TransactionHandlerRegistry.get(transaction.type).revertForSender( + transaction, + this.findByPublicKey(transaction.data.senderPublicKey), + ); + } +} diff --git a/packages/core-transactions/src/handlers/delegate-registration.ts b/packages/core-transactions/src/handlers/delegate-registration.ts index 921c09b8f4..369bc683d8 100644 --- a/packages/core-transactions/src/handlers/delegate-registration.ts +++ b/packages/core-transactions/src/handlers/delegate-registration.ts @@ -47,18 +47,22 @@ export class DelegateRegistrationTransactionHandler extends TransactionHandler { emitter.emit("delegate.registered", transaction.data); } - public canEnterTransactionPool(data: Interfaces.ITransactionData, guard: TransactionPool.IGuard): boolean { - if (this.typeFromSenderAlreadyInPool(data, guard)) { + public canEnterTransactionPool( + data: Interfaces.ITransactionData, + pool: TransactionPool.IConnection, + processor: TransactionPool.IProcessor, + ): boolean { + if (this.typeFromSenderAlreadyInPool(data, pool, processor)) { return false; } const { username } = data.asset.delegate; - const delegateRegistrationsSameNameInPayload = guard.transactions.filter( - tx => tx.type === TransactionTypes.DelegateRegistration && tx.asset.delegate.username === username, - ); + const delegateRegistrationsSameNameInPayload = processor + .getTransactions() + .filter(tx => tx.type === TransactionTypes.DelegateRegistration && tx.asset.delegate.username === username); if (delegateRegistrationsSameNameInPayload.length > 1) { - guard.pushError( + processor.pushError( data, "ERR_CONFLICT", `Multiple delegate registrations for "${username}" in transaction payload`, @@ -67,14 +71,14 @@ export class DelegateRegistrationTransactionHandler extends TransactionHandler { } const delegateRegistrationsInPool: Interfaces.ITransactionData[] = Array.from( - guard.pool.getTransactionsByType(TransactionTypes.DelegateRegistration), + pool.getTransactionsByType(TransactionTypes.DelegateRegistration), ).map((memTx: any) => memTx.transaction.data); const containsDelegateRegistrationForSameNameInPool = delegateRegistrationsInPool.some( transaction => transaction.asset.delegate.username === username, ); if (containsDelegateRegistrationForSameNameInPool) { - guard.pushError(data, "ERR_PENDING", `Delegate registration for "${username}" already in the pool`); + processor.pushError(data, "ERR_PENDING", `Delegate registration for "${username}" already in the pool`); return false; } diff --git a/packages/core-transactions/src/handlers/second-signature.ts b/packages/core-transactions/src/handlers/second-signature.ts index cd8f54594e..fb372f1f04 100644 --- a/packages/core-transactions/src/handlers/second-signature.ts +++ b/packages/core-transactions/src/handlers/second-signature.ts @@ -28,7 +28,11 @@ export class SecondSignatureTransactionHandler extends TransactionHandler { wallet.secondPublicKey = null; } - public canEnterTransactionPool(data: Interfaces.ITransactionData, guard: TransactionPool.IGuard): boolean { - return !this.typeFromSenderAlreadyInPool(data, guard); + public canEnterTransactionPool( + data: Interfaces.ITransactionData, + pool: TransactionPool.IConnection, + processor: TransactionPool.IProcessor, + ): boolean { + return !this.typeFromSenderAlreadyInPool(data, pool, processor); } } diff --git a/packages/core-transactions/src/handlers/transaction.ts b/packages/core-transactions/src/handlers/transaction.ts index 59fba9a21a..a5ee357fe7 100644 --- a/packages/core-transactions/src/handlers/transaction.ts +++ b/packages/core-transactions/src/handlers/transaction.ts @@ -110,8 +110,12 @@ export abstract class TransactionHandler implements ITransactionHandler { /** * Transaction Pool logic */ - public canEnterTransactionPool(data: Interfaces.ITransactionData, guard: TransactionPool.IGuard): boolean { - guard.pushError( + public canEnterTransactionPool( + data: Interfaces.ITransactionData, + pool: TransactionPool.IConnection, + processor: TransactionPool.IProcessor, + ): boolean { + processor.pushError( data, "ERR_UNSUPPORTED", `Invalidating transaction of unsupported type '${Enums.TransactionTypes[data.type]}'`, @@ -119,10 +123,15 @@ export abstract class TransactionHandler implements ITransactionHandler { return false; } - protected typeFromSenderAlreadyInPool(data: Interfaces.ITransactionData, guard: TransactionPool.IGuard): boolean { + protected typeFromSenderAlreadyInPool( + data: Interfaces.ITransactionData, + pool: TransactionPool.IConnection, + processor: TransactionPool.IProcessor, + ): boolean { const { senderPublicKey, type } = data; - if (guard.pool.senderHasTransactionsOfType(senderPublicKey, type)) { - guard.pushError( + + if (pool.senderHasTransactionsOfType(senderPublicKey, type)) { + processor.pushError( data, "ERR_PENDING", `Sender ${senderPublicKey} already has a transaction of type '${ diff --git a/packages/core-transactions/src/handlers/transfer.ts b/packages/core-transactions/src/handlers/transfer.ts index 98b753629b..cc26c5338c 100644 --- a/packages/core-transactions/src/handlers/transfer.ts +++ b/packages/core-transactions/src/handlers/transfer.ts @@ -28,9 +28,13 @@ export class TransferTransactionHandler extends TransactionHandler { return; } - public canEnterTransactionPool(data: Interfaces.ITransactionData, guard: TransactionPool.IGuard): boolean { + public canEnterTransactionPool( + data: Interfaces.ITransactionData, + pool: TransactionPool.IConnection, + processor: TransactionPool.IProcessor, + ): boolean { if (!isRecipientOnActiveNetwork(data)) { - guard.pushError( + processor.pushError( data, "ERR_INVALID_RECIPIENT", `Recipient ${data.recipientId} is not on the same network: ${Managers.configManager.get( diff --git a/packages/core-transactions/src/handlers/vote.ts b/packages/core-transactions/src/handlers/vote.ts index 26725bed0a..d7ecbf9aaa 100644 --- a/packages/core-transactions/src/handlers/vote.ts +++ b/packages/core-transactions/src/handlers/vote.ts @@ -65,7 +65,11 @@ export class VoteTransactionHandler extends TransactionHandler { }); } - public canEnterTransactionPool(data: Interfaces.ITransactionData, guard: TransactionPool.IGuard): boolean { - return !this.typeFromSenderAlreadyInPool(data, guard); + public canEnterTransactionPool( + data: Interfaces.ITransactionData, + pool: TransactionPool.IConnection, + processor: TransactionPool.IProcessor, + ): boolean { + return !this.typeFromSenderAlreadyInPool(data, pool, processor); } } diff --git a/packages/core-transactions/src/interfaces.ts b/packages/core-transactions/src/interfaces.ts index 1900ba4fc8..00ebc5d006 100644 --- a/packages/core-transactions/src/interfaces.ts +++ b/packages/core-transactions/src/interfaces.ts @@ -16,6 +16,11 @@ export interface ITransactionHandler { apply(transaction: Interfaces.ITransaction, wallet: Database.IWallet): void; revert(transaction: Interfaces.ITransaction, wallet: Database.IWallet): void; - canEnterTransactionPool(data: Interfaces.ITransactionData, guard: TransactionPool.IGuard): boolean; + canEnterTransactionPool( + data: Interfaces.ITransactionData, + pool: TransactionPool.IConnection, + processor: TransactionPool.IProcessor, + ): boolean; + emitEvents(transaction: Interfaces.ITransaction, emitter: EventEmitter.EventEmitter): void; } diff --git a/packages/core-transactions/src/utils.ts b/packages/core-transactions/src/utils.ts index cf22d8ab44..8831cdd7a5 100644 --- a/packages/core-transactions/src/utils.ts +++ b/packages/core-transactions/src/utils.ts @@ -2,7 +2,5 @@ import { Interfaces, Managers } from "@arkecosystem/crypto"; import bs58check from "bs58check"; export function isRecipientOnActiveNetwork(transaction: Interfaces.ITransactionData): boolean { - const recipientPrefix = bs58check.decode(transaction.recipientId).readUInt8(0); - - return recipientPrefix === Managers.configManager.get("network.pubKeyHash"); + return bs58check.decode(transaction.recipientId).readUInt8(0) === Managers.configManager.get("network.pubKeyHash"); }