diff --git a/__tests__/helpers/transaction-factory.ts b/__tests__/helpers/transaction-factory.ts index 1ca54e5de5..b0285575f6 100644 --- a/__tests__/helpers/transaction-factory.ts +++ b/__tests__/helpers/transaction-factory.ts @@ -193,7 +193,6 @@ export class TransactionFactory { Managers.configManager.setFromPreset(this.network); const transactions: T[] = []; - for (let i = 0; i < quantity; i++) { if (this.builder.constructor.name === "TransferBuilder") { // @FIXME: when we use any of the "withPassphrase*" methods the builder will diff --git a/__tests__/integration/core-api/v2/handlers/transactions.test.ts b/__tests__/integration/core-api/v2/handlers/transactions.test.ts index 37b32ef332..b113683f06 100644 --- a/__tests__/integration/core-api/v2/handlers/transactions.test.ts +++ b/__tests__/integration/core-api/v2/handlers/transactions.test.ts @@ -557,8 +557,8 @@ describe("API 2.0 - Transactions", () => { it.each([3, 5, 8])("should accept and broadcast %i transactions emptying a wallet", async txNumber => { const sender = delegates[txNumber]; // use txNumber so that we use a different delegate for each test case const receivers = generateWallets("testnet", 2); - const amountPlusFee = Math.floor(sender.balance / txNumber); - const lastAmountPlusFee = sender.balance - (txNumber - 1) * amountPlusFee; + const amountPlusFee = Math.floor(+sender.balance / txNumber); + const lastAmountPlusFee = +sender.balance - (txNumber - 1) * amountPlusFee; const transactions = TransactionFactory.transfer(receivers[0].address, amountPlusFee - transferFee) .withNetwork("testnet") @@ -591,8 +591,8 @@ describe("API 2.0 - Transactions", () => { async txNumber => { const sender = delegates[txNumber + 1]; // use txNumber + 1 so that we don't use the same delegates as the above test const receivers = generateWallets("testnet", 2); - const amountPlusFee = Math.floor(sender.balance / txNumber); - const lastAmountPlusFee = sender.balance - (txNumber - 1) * amountPlusFee + 1; + const amountPlusFee = Math.floor(+sender.balance / txNumber); + const lastAmountPlusFee = +sender.balance - (txNumber - 1) * amountPlusFee + 1; const transactions = TransactionFactory.transfer(receivers[0].address, amountPlusFee - transferFee) .withNetwork("testnet") diff --git a/__tests__/integration/core-blockchain/blockchain.test.ts b/__tests__/integration/core-blockchain/blockchain.test.ts index c90206d6d1..d616fbe21c 100644 --- a/__tests__/integration/core-blockchain/blockchain.test.ts +++ b/__tests__/integration/core-blockchain/blockchain.test.ts @@ -55,6 +55,14 @@ const addBlocks = async untilHeight => { } }; +const indexWalletWithSufficientBalance = (transaction: Interfaces.ITransaction): void => { + const walletManager = blockchain.database.walletManager; + + const wallet = walletManager.findByPublicKey(transaction.data.senderPublicKey); + wallet.balance = wallet.balance.abs().plus(transaction.data.amount.plus(transaction.data.fee)); + walletManager.reindex(wallet); +}; + describe("Blockchain", () => { beforeAll(async () => { container = await setUp(); @@ -91,19 +99,25 @@ describe("Blockchain", () => { describe("postTransactions", () => { it("should be ok", async () => { - const transactionsWithoutType2 = genesisBlock.transactions.filter(tx => tx.type !== 2); - blockchain.transactionPool.flush(); - await blockchain.postTransactions(transactionsWithoutType2); - const transactions = blockchain.transactionPool.getTransactions(0, 200); - expect(transactions.length).toBe(transactionsWithoutType2.length); + jest.spyOn(blockchain.transactionPool as any, "removeForgedTransactions").mockReturnValue([]); - expect(transactions).toIncludeAllMembers( - transactionsWithoutType2.map(transaction => transaction.serialized), - ); + for (const transaction of genesisBlock.transactions) { + indexWalletWithSufficientBalance(transaction); + } + + const transferTransactions = genesisBlock.transactions.filter(tx => tx.type === 0); + + await blockchain.postTransactions(transferTransactions); + const transactions = await blockchain.transactionPool.getTransactions(0, 200); + + expect(transactions.length).toBe(transferTransactions.length); + + expect(transactions).toIncludeAllMembers(transferTransactions.map(transaction => transaction.serialized)); blockchain.transactionPool.flush(); + jest.restoreAllMocks(); }); }); diff --git a/__tests__/unit/core-p2p/socket-server/versions/internal/handlers/transactions.test.ts b/__tests__/unit/core-p2p/socket-server/versions/internal/handlers/transactions.test.ts index 13753ba87a..5f623213fa 100644 --- a/__tests__/unit/core-p2p/socket-server/versions/internal/handlers/transactions.test.ts +++ b/__tests__/unit/core-p2p/socket-server/versions/internal/handlers/transactions.test.ts @@ -8,11 +8,11 @@ jest.mock("../../../../../../../packages/core-p2p/src/socket-server/utils/valida describe("Internal handlers - transactions", () => { describe("getUnconfirmedTransactions", () => { - it("should return unconfirmed transactions", () => { + it("should return unconfirmed transactions", async () => { transactionPool.getTransactionsForForging = jest.fn().mockReturnValue(["111"]); transactionPool.getPoolSize = jest.fn().mockReturnValue(1); - expect(getUnconfirmedTransactions()).toEqual({ poolSize: 1, transactions: ["111"] }); + expect(await getUnconfirmedTransactions()).toEqual({ poolSize: 1, transactions: ["111"] }); }); }); }); diff --git a/__tests__/unit/core-transaction-pool/__stubs__/connection.ts b/__tests__/unit/core-transaction-pool/__stubs__/connection.ts index 276497c99f..7758903031 100644 --- a/__tests__/unit/core-transaction-pool/__stubs__/connection.ts +++ b/__tests__/unit/core-transaction-pool/__stubs__/connection.ts @@ -70,7 +70,7 @@ export class Connection implements TransactionPool.IConnection { return; } - public getTransactionsForForging(blockSize: number): string[] { + public async getTransactionsForForging(blockSize: number): Promise { return []; } @@ -78,15 +78,11 @@ export class Connection implements TransactionPool.IConnection { return undefined; } - public getTransactions(start: number, size: number, maxBytes?: number): Buffer[] { + public async getTransactions(start: number, size: number, maxBytes?: number): Promise { return []; } - public getTransactionIdsForForging(start: number, size: number): string[] { - return undefined; - } - - public getTransactionsData(start: number, size: number, maxBytes?: number): Interfaces.ITransaction[] { + public async getTransactionIdsForForging(start: number, size: number): Promise { return undefined; } diff --git a/__tests__/unit/core-transaction-pool/connection.forging.test.ts b/__tests__/unit/core-transaction-pool/connection.forging.test.ts new file mode 100644 index 0000000000..f11d037282 --- /dev/null +++ b/__tests__/unit/core-transaction-pool/connection.forging.test.ts @@ -0,0 +1,510 @@ +import "jest-extended"; + +import "./mocks/core-container"; + +import bs58check from "bs58check"; +import ByteBuffer from "bytebuffer"; + +import { Wallets } from "@arkecosystem/core-state"; +import { Handlers } from "@arkecosystem/core-transactions"; +import { Constants, Crypto, Identities, Interfaces, Managers, Transactions, Utils } from "@arkecosystem/crypto"; +import { Connection } from "../../../packages/core-transaction-pool/src/connection"; +import { defaults } from "../../../packages/core-transaction-pool/src/defaults"; +import { Memory } from "../../../packages/core-transaction-pool/src/memory"; +import { Storage } from "../../../packages/core-transaction-pool/src/storage"; +import { WalletManager } from "../../../packages/core-transaction-pool/src/wallet-manager"; +import { TransactionFactory } from "../../helpers/transaction-factory"; +import { delegates } from "../../utils/fixtures/testnet/delegates"; + +let connection: Connection; +let memory: Memory; +let poolWalletManager: WalletManager; +let databaseWalletManager: Wallets.WalletManager; + +beforeAll(async () => { + Managers.configManager.setFromPreset("testnet"); + + memory = new Memory(); + poolWalletManager = new WalletManager(); + connection = new Connection({ + options: defaults, + walletManager: poolWalletManager, + memory, + storage: new Storage(), + }); + + await connection.make(); +}); + +const mockCurrentHeight = (height: number) => { + // @ts-ignore + jest.spyOn(memory, "currentHeight").mockReturnValue(height); + Managers.configManager.setHeight(height); +}; + +describe("Connection", () => { + beforeEach(() => { + mockCurrentHeight(1); + + connection.flush(); + poolWalletManager.reset(); + + databaseWalletManager = new Wallets.WalletManager(); + + for (let i = 0; i < delegates.length; i++) { + const { publicKey } = delegates[i]; + const wallet = databaseWalletManager.findByPublicKey(publicKey); + wallet.balance = Utils.BigNumber.make(100_000 * Constants.ARKTOSHI); + wallet.username = `delegate-${i + 1}`; + wallet.vote = publicKey; + + if (i === 50) { + wallet.secondPublicKey = Identities.PublicKey.fromPassphrase("second secret"); + } + + databaseWalletManager.reindex(wallet); + } + + databaseWalletManager.buildDelegateRanking(); + databaseWalletManager.buildVoteBalances(); + + // @ts-ignore + connection.databaseService.walletManager = databaseWalletManager; + + jest.restoreAllMocks(); + }); + + const addTransactionsToMemory = transactions => { + for (const tx of transactions) { + memory.remember(tx); + expect(memory.has(tx.id)).toBeTrue(); + } + expect(memory.count()).toBe(transactions.length); + }; + + const expectForgingTransactions = async ( + transactions: Interfaces.ITransaction[], + countGood: number, + ): Promise => { + addTransactionsToMemory(transactions); + + const forgingTransactions = await connection.getTransactionsForForging(100); + expect(forgingTransactions).toHaveLength(countGood); + expect(forgingTransactions).toEqual( + transactions.slice(transactions.length - countGood).map(({ serialized }) => serialized.toString("hex")), + ); + + return forgingTransactions; + }; + + const customSerialize = (transaction: Interfaces.ITransactionData, options: any = {}) => { + const buffer = new ByteBuffer(512, true); + const writeByte = (txField, value) => (options[txField] ? options[txField](buffer) : buffer.writeByte(value)); + const writeUint32 = (txField, value) => + options[txField] ? options[txField](buffer) : buffer.writeUint32(value); + const writeUint64 = (txField, value) => + options[txField] ? options[txField](buffer) : buffer.writeUint64(value); + const append = (txField, value, encoding = "utf8") => + options[txField] ? options[txField](buffer) : buffer.append(value, encoding); + + buffer.writeByte(0xff); // fill, to disambiguate from v1 + writeByte("version", 0x01); + writeByte("network", transaction.network); // ark = 0x17, devnet = 0x30 + writeByte("type", transaction.type); + writeUint32("timestamp", transaction.timestamp); + append("senderPublicKey", transaction.senderPublicKey, "hex"); + writeUint64("fee", +transaction.fee); + + if (options.vendorField) { + options.vendorField(buffer); + } else if (transaction.vendorField) { + const vf: Buffer = Buffer.from(transaction.vendorField, "utf8"); + buffer.writeByte(vf.length); + buffer.append(vf); + } else if (transaction.vendorFieldHex) { + buffer.writeByte(transaction.vendorFieldHex.length / 2); + buffer.append(transaction.vendorFieldHex, "hex"); + } else { + buffer.writeByte(0x00); + } + + // only for transfer right now + writeUint64("amount", +transaction.amount); + writeUint32("expiration", transaction.expiration || 0); + append("recipientId", bs58check.decode(transaction.recipientId)); + + // signatures + if (transaction.signature || options.signature) { + append("signature", transaction.signature, "hex"); + } + + const secondSignature: string = transaction.secondSignature || transaction.signSignature; + + if (secondSignature || options.secondSignature) { + append("secondSignature", secondSignature, "hex"); + } + + if (options.signatures) { + options.signatures(buffer); + } else if (transaction.signatures) { + if (transaction.version === 1 && Utils.isException(transaction)) { + buffer.append("ff", "hex"); // 0xff separator to signal start of multi-signature transactions + buffer.append(transaction.signatures.join(""), "hex"); + } else { + buffer.append(transaction.signatures.join(""), "hex"); + } + } + + return buffer.flip().toBuffer(); + }; + + describe("getTransactionsForForging", () => { + it("should call `TransactionFactory.fromBytes`", async () => { + const transactions = TransactionFactory.transfer().build(5); + const spy = jest.spyOn(Transactions.TransactionFactory, "fromBytes"); + await expectForgingTransactions(transactions, 5); + expect(spy).toHaveBeenCalled(); + }); + + it("should call `TransactionHandler.canBeApplied`", async () => { + const transactions = TransactionFactory.transfer().build(5); + const spy = jest.spyOn(Handlers.Registry.get(0), "canBeApplied"); + await expectForgingTransactions(transactions, 5); + expect(spy).toHaveBeenCalled(); + }); + + it("should call `removeForgedTransactions`", async () => { + const transactions = TransactionFactory.transfer().build(5); + const spy = jest.spyOn(connection as any, "removeForgedTransactions"); + await expectForgingTransactions(transactions, 5); + expect(spy).toHaveBeenCalled(); + }); + + it("should remove transactions that have malformed bytes", async () => { + const malformedBytesFn = [ + { version: (b: ByteBuffer) => b.writeUint64(1111111) }, + { network: (b: ByteBuffer) => b.writeUint64(1111111) }, + { type: (b: ByteBuffer) => b.writeUint64(1111111) }, + { timestamp: (b: ByteBuffer) => b.writeByte(0x01) }, + { senderPublicKey: (b: ByteBuffer) => b.writeByte(0x01) }, + { vendorField: (b: ByteBuffer) => b.writeByte(0x01) }, + { amount: (b: ByteBuffer) => b.writeByte(0x01) }, + { expiration: (b: ByteBuffer) => b.writeByte(0x01) }, + { recipientId: (b: ByteBuffer) => b.writeByte(0x01) }, + { signature: (b: ByteBuffer) => b.writeByte(0x01) }, + { secondSignature: (b: ByteBuffer) => b.writeByte(0x01) }, + { signatures: (b: ByteBuffer) => b.writeByte(0x01) }, + ]; + const transactions = TransactionFactory.transfer().build(malformedBytesFn.length + 5); + transactions.map((tx, i) => (tx.serialized = customSerialize(tx.data, malformedBytesFn[i] || {}))); + + await expectForgingTransactions(transactions, 5); + }); + + it("should remove transactions that have data from another network", async () => { + const transactions = TransactionFactory.transfer().build(5); + + transactions[0].serialized = customSerialize(transactions[0].data, { + network: (b: ByteBuffer) => b.writeUint8(3), + }); + + await expectForgingTransactions(transactions, 4); + }); + + it("should remove transactions that have wrong sender public keys", async () => { + const transactions = TransactionFactory.transfer().build(5); + + transactions[0].serialized = customSerialize(transactions[0].data, { + senderPublicKey: (b: ByteBuffer) => + b.append(Buffer.from(Identities.PublicKey.fromPassphrase("garbage"), "hex")), + }); + + await expectForgingTransactions(transactions, 4); + }); + + it("should remove transactions that have timestamps in the future", async () => { + const transactions = TransactionFactory.transfer().build(5); + + transactions[0].serialized = customSerialize(transactions[0].data, { + timestamp: (b: ByteBuffer) => b.writeUint32(Crypto.Slots.getTime() + 100 * 1000), + }); + + await expectForgingTransactions(transactions, 4); + }); + + it("should remove transactions that have different IDs when entering and leaving", async () => { + const transactions = TransactionFactory.transfer().build(5); + + transactions[0].data.id = "garbage"; + + await expectForgingTransactions(transactions, 4); + }); + + it("should remove transactions that have an unknown type", async () => { + const transactions = TransactionFactory.transfer().build(2); + transactions[0].serialized = customSerialize(transactions[0].data, { + version: (b: ByteBuffer) => b.writeUint8(255), + }); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have a disabled type", async () => { + const transactions = TransactionFactory.transfer() + .withVersion(1) + .build(2); + transactions[0].serialized = customSerialize(transactions[0].data, { + version: (b: ByteBuffer) => b.writeUint8(4), + }); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have have data of a another transaction type", async () => { + const handlers: Handlers.TransactionHandler[] = Handlers.Registry.all(); + const transactions: Interfaces.ITransaction[] = TransactionFactory.transfer().build(handlers.length); + + for (let i = 0; i < handlers.length; i++) { + expect(handlers[0].getConstructor().type).toEqual(0); + transactions[i].serialized = customSerialize(transactions[i].data, { + type: (b: ByteBuffer) => b.writeUint8(handlers[i].getConstructor().type), + }); + } + + await expectForgingTransactions(transactions.reverse(), 1); + }); + + it("should remove transactions that have negative numerical values", async () => { + const transactions = TransactionFactory.transfer().build(2); + transactions[0].serialized = customSerialize(transactions[0].data, { + fee: (b: ByteBuffer) => b.writeUint64(-999999), + amount: (b: ByteBuffer) => b.writeUint64(-999999), + }); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have expired", async () => { + mockCurrentHeight(100); + + const transactions = TransactionFactory.transfer().build(5); + + transactions[0].serialized = customSerialize(transactions[0].data, { + expiration: (b: ByteBuffer) => b.writeByte(0x01), + }); + + await expectForgingTransactions(transactions, 4); + }); + + it("should remove transactions that have an amount or fee of 0", async () => { + const transactions = TransactionFactory.transfer().build(5); + + transactions[0].serialized = customSerialize(transactions[0].data, { + fee: (b: ByteBuffer) => b.writeByte(0x00), + }); + + transactions[1].serialized = customSerialize(transactions[0].data, { + amount: (b: ByteBuffer) => b.writeByte(0), + }); + + await expectForgingTransactions(transactions, 3); + }); + + it("should remove transactions that have been altered after entering the pool", async () => { + const transactions = TransactionFactory.transfer().build(2); + transactions[0].data.id = transactions[0].data.id + .split("") + .reverse() + .join(""); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have an invalid version", async () => { + const transactions = TransactionFactory.transfer().build(2); + transactions[0].serialized = customSerialize(transactions[0].data, { + version: (b: ByteBuffer) => b.writeByte(0), + }); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have a mismatch of expected and actual length of the vendor field", async () => { + const transactions = TransactionFactory.transfer().build(3); + transactions[0].serialized = customSerialize(transactions[0].data, { + vendorField: (b: ByteBuffer) => { + const vendorField = Buffer.from(transactions[0].data.vendorField, "utf8"); + b.writeByte(vendorField.length - 5); + b.append(vendorField); + }, + }); + + transactions[1].serialized = customSerialize(transactions[1].data, { + vendorField: (b: ByteBuffer) => { + const vendorField = Buffer.from(transactions[1].data.vendorField, "utf8"); + b.writeByte(vendorField.length + 5); + b.append(vendorField); + }, + }); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have an invalid vendor field length", async () => { + const transactions = TransactionFactory.transfer().build(3); + transactions[0].serialized = customSerialize(transactions[0].data, { + vendorField: (b: ByteBuffer) => { + const vendorField = Buffer.from(transactions[0].data.vendorField, "utf8"); + b.writeByte(0); + b.append(vendorField); + }, + }); + + transactions[1].serialized = customSerialize(transactions[1].data, { + vendorField: (b: ByteBuffer) => { + b.writeByte(255); + }, + }); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have an invalid vendor field", async () => { + const transactions = TransactionFactory.transfer().build(3); + transactions[0].serialized = customSerialize(transactions[0].data, { + vendorField: (b: ByteBuffer) => { + const vendorField = Buffer.from(transactions[0].data.vendorField.toUpperCase(), "utf8"); + b.writeByte(vendorField.length); + b.append(vendorField); + }, + }); + + transactions[1].serialized = customSerialize(transactions[1].data, { + vendorField: (b: ByteBuffer) => { + b.writeByte(255); + b.fill(0, b.offset); + }, + }); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have additional bytes attached", async () => { + const transactions = TransactionFactory.transfer().build(5); + + const appendBytes = (transaction: Interfaces.ITransaction, garbage: Buffer) => { + const buffer = new ByteBuffer(512, true); + buffer.append(transaction.serialized); + buffer.append(garbage); + + transaction.serialized = buffer.flip().toBuffer(); + }; + + appendBytes(transactions[0], Buffer.from("garbage", "utf8")); + appendBytes(transactions[1], Buffer.from("ff", "hex")); + appendBytes(transactions[2], Buffer.from("00011111", "hex")); + appendBytes(transactions[3], Buffer.from("0001", "hex")); + + await expectForgingTransactions(transactions, 1); + }); + + it("should remove transactions that have malformed signatures", async () => { + const transactions = TransactionFactory.transfer().build(5); + + const makeSignature = (from: string): string => { + return Crypto.Hash.signECDSA( + Buffer.from(Crypto.HashAlgorithms.sha256(from)), + Identities.Keys.fromPassphrase("garbage"), + ); + }; + + transactions[0].serialized = customSerialize(transactions[0].data, { + signatures: (b: ByteBuffer) => { + b.append(Buffer.from(makeSignature("garbage").slice(25), "hex")); + }, + }); + + transactions[1].serialized = customSerialize(transactions[0].data, { + signatures: (b: ByteBuffer) => { + b.append(Buffer.from(makeSignature("garbage").repeat(2), "hex")); + }, + }); + + transactions[2].serialized = customSerialize(transactions[0].data, { + signatures: (b: ByteBuffer) => { + b.append(Buffer.from(makeSignature("garbage") + "affe", "hex")); + }, + }); + + await expectForgingTransactions(transactions, 2); + }); + + it("should remove transactions that have malformed second signatures", async () => { + const transactions = TransactionFactory.transfer() + .withPassphrasePair({ + passphrase: delegates[50].passphrase, + secondPassphrase: "second secret", + }) + .build(5); + + const appendBytes = (transaction: Interfaces.ITransaction, garbage: Buffer) => { + const buffer = new ByteBuffer(512, true); + buffer.append(transaction.serialized); + buffer.append(garbage); + + transaction.serialized = buffer.flip().toBuffer(); + }; + + appendBytes(transactions[0], Buffer.from("ff", "hex")); + appendBytes(transactions[1], Buffer.from("00", "hex")); + appendBytes(transactions[2], Buffer.from("0011001100", "hex")); + + await expectForgingTransactions(transactions, 2); + }); + + it("should remove transactions that have malformed multi signatures", async () => { + const transactions = TransactionFactory.transfer().build(5); + + const appendBytes = (transaction: Interfaces.ITransaction, garbage: Buffer) => { + const buffer = new ByteBuffer(512, true); + buffer.append(transaction.serialized); + buffer.append(garbage); + + transaction.serialized = buffer.flip().toBuffer(); + }; + + const makeSignature = (from: string): string => { + return Crypto.Hash.signECDSA( + Buffer.from(Crypto.HashAlgorithms.sha256(from)), + Identities.Keys.fromPassphrase("garbage"), + ); + }; + + appendBytes(transactions[0], Buffer.from("ff" + makeSignature("garbage").repeat(5), "hex")); + + await expectForgingTransactions(transactions, 4); + }); + + it("should remove transactions that have malformed multi signatures", async () => { + const transactions = TransactionFactory.transfer().build(5); + + const appendBytes = (transaction: Interfaces.ITransaction, garbage: Buffer) => { + const buffer = new ByteBuffer(512, true); + buffer.append(transaction.serialized); + buffer.append(garbage); + + transaction.serialized = buffer.flip().toBuffer(); + }; + + const makeSignature = (from: string): string => { + return Crypto.Hash.signECDSA( + Buffer.from(Crypto.HashAlgorithms.sha256(from)), + Identities.Keys.fromPassphrase("garbage"), + ); + }; + appendBytes(transactions[0], Buffer.from("ff" + makeSignature("garbage").repeat(5), "hex")); + + await expectForgingTransactions(transactions, 4); + }); + }); +}); diff --git a/__tests__/unit/core-transaction-pool/connection.test.ts b/__tests__/unit/core-transaction-pool/connection.test.ts index 661157a830..f11b85bff4 100644 --- a/__tests__/unit/core-transaction-pool/connection.test.ts +++ b/__tests__/unit/core-transaction-pool/connection.test.ts @@ -28,6 +28,15 @@ const delegatesSecrets = delegates.map(d => d.secret); let connection: Connection; let memory: Memory; +const indexWalletWithSufficientBalance = (transaction: Interfaces.ITransaction): void => { + // @ts-ignore + const walletManager = connection.databaseService.walletManager; + + const wallet = walletManager.findByPublicKey(transaction.data.senderPublicKey); + wallet.balance = wallet.balance.plus(transaction.data.amount.plus(transaction.data.fee)); + walletManager.reindex(wallet); +}; + beforeAll(async () => { memory = new Memory(); @@ -41,6 +50,10 @@ beforeAll(async () => { // @ts-ignore connection.databaseService.walletManager = new Wallets.WalletManager(); + for (const transaction of Object.values(mockData)) { + indexWalletWithSufficientBalance(transaction); + } + await connection.make(); }); @@ -134,7 +147,7 @@ describe("Connection", () => { connection.options.maxTransactionsInPool = maxTransactionsInPoolOrig; }); - it("should replace lowest fee transaction when adding 1 more transaction than maxTransactionsInPool", () => { + it("should replace lowest fee transaction when adding 1 more transaction than maxTransactionsInPool", async () => { expect(connection.getPoolSize()).toBe(0); connection.addTransactions([ @@ -150,7 +163,9 @@ describe("Connection", () => { connection.options.maxTransactionsInPool = 4; expect(connection.addTransactions([mockData.dummy5])).toEqual({}); - expect(connection.getTransactionIdsForForging(0, 10)).toEqual([ + + const transactionIds = await connection.getTransactionIdsForForging(0, 10); + expect(transactionIds).toEqual([ mockData.dummy1.id, mockData.dummy2.id, mockData.dummy3.id, @@ -402,7 +417,7 @@ describe("Connection", () => { }); describe("getTransactions", () => { - it("should return transactions within the specified range", () => { + it("should return transactions within the specified range", async () => { const transactions = [mockData.dummy1, mockData.dummy2]; addTransactions(transactions); @@ -412,9 +427,9 @@ describe("Connection", () => { } for (const i of [0, 1]) { - const retrieved = connection - .getTransactions(i, 1) - .map(serializedTx => Transactions.TransactionFactory.fromBytes(serializedTx)); + const retrieved = (await connection.getTransactions(i, 1)).map(serializedTx => + Transactions.TransactionFactory.fromBytes(serializedTx), + ); expect(retrieved.length).toBe(1); expect(retrieved[0]).toBeObject(); @@ -424,7 +439,7 @@ describe("Connection", () => { }); describe("getTransactionIdsForForging", () => { - it("should return an array of transactions ids", () => { + it("should return an array of transactions ids", async () => { addTransactions([ mockData.dummy1, mockData.dummy2, @@ -434,7 +449,7 @@ describe("Connection", () => { mockData.dummy6, ]); - const transactionIds = connection.getTransactionIdsForForging(0, 6); + const transactionIds = await connection.getTransactionIdsForForging(0, 6); expect(transactionIds).toBeArray(); expect(transactionIds[0]).toBe(mockData.dummy1.id); @@ -445,7 +460,7 @@ describe("Connection", () => { expect(transactionIds[5]).toBe(mockData.dummy6.id); }); - it("should only return transaction ids for transactions not exceeding the maximum payload size", () => { + it("should only return transaction ids for transactions not exceeding the maximum payload size", async () => { // @FIXME: Uhm excuse me, what the? mockData.dummyLarge1.data.signatures = mockData.dummyLarge2.data.signatures = [""]; for (let i = 0; i < connection.options.maxTransactionBytes * 0.6; i++) { @@ -467,43 +482,57 @@ describe("Connection", () => { addTransactions(transactions); - let transactionIds = connection.getTransactionIdsForForging(0, 7); + let transactionIds = await connection.getTransactionIdsForForging(0, 7); expect(transactionIds).toBeArray(); - expect(transactionIds.length).toBe(6); - expect(transactionIds[0]).toBe(mockData.dummyLarge1.id); - expect(transactionIds[1]).toBe(mockData.dummy3.id); - expect(transactionIds[2]).toBe(mockData.dummy4.id); - expect(transactionIds[3]).toBe(mockData.dummy5.id); - expect(transactionIds[4]).toBe(mockData.dummy6.id); - expect(transactionIds[5]).toBe(mockData.dummy7.id); + expect(transactionIds).toHaveLength(5); + expect(transactionIds[0]).toBe(mockData.dummy3.id); + expect(transactionIds[1]).toBe(mockData.dummy4.id); + expect(transactionIds[2]).toBe(mockData.dummy5.id); + expect(transactionIds[3]).toBe(mockData.dummy6.id); + expect(transactionIds[4]).toBe(mockData.dummy7.id); - connection.removeTransactionById(mockData.dummyLarge1.id); connection.removeTransactionById(mockData.dummy3.id); connection.removeTransactionById(mockData.dummy4.id); connection.removeTransactionById(mockData.dummy5.id); connection.removeTransactionById(mockData.dummy6.id); connection.removeTransactionById(mockData.dummy7.id); - transactionIds = connection.getTransactionIdsForForging(0, 7); + transactionIds = await connection.getTransactionIdsForForging(0, 7); expect(transactionIds).toBeArray(); - expect(transactionIds.length).toBe(1); - expect(transactionIds[0]).toBe(mockData.dummyLarge2.id); + expect(transactionIds).toHaveLength(0); }); }); describe("getTransactionsForForging", () => { - it("should return an array of transactions serialized", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it("should return an array of transactions serialized", async () => { const transactions = [mockData.dummy1, mockData.dummy2, mockData.dummy3, mockData.dummy4]; addTransactions(transactions); - const spy = jest.spyOn(Handlers.Registry.get(0), "canBeApplied").mockReturnValue(true); - const transactionsForForging = connection.getTransactionsForForging(4); - spy.mockRestore(); + jest.spyOn(Handlers.Registry.get(0), "canBeApplied").mockReturnValue(true); + const transactionsForForging = await connection.getTransactionsForForging(4); expect(transactionsForForging).toEqual(transactions.map(tx => tx.serialized.toString("hex"))); }); - it("should only return transactions not exceeding the maximum payload size", () => { + it("should only return unforged transactions", async () => { + const transactions = [mockData.dummy1, mockData.dummy2, mockData.dummy3]; + addTransactions(transactions); + + jest.spyOn(databaseService, "getForgedTransactionsIds").mockReturnValue([ + mockData.dummy1.id, + mockData.dummy3.id, + ]); + jest.spyOn(Handlers.Registry.get(0), "canBeApplied").mockReturnValue(true); + + const transactionsForForging = await connection.getTransactionsForForging(3); + expect(transactionsForForging.length).toBe(1); + expect(transactionsForForging[0]).toEqual(mockData.dummy2.serialized.toString("hex")); + }); + + it("should only return transactions not exceeding the maximum payload size", async () => { // @FIXME: Uhm excuse me, what the? mockData.dummyLarge1.data.signatures = mockData.dummyLarge2.data.signatures = [""]; for (let i = 0; i < connection.options.maxTransactionBytes * 0.6; i++) { @@ -525,8 +554,8 @@ describe("Connection", () => { addTransactions(transactions); - const spy = jest.spyOn(Handlers.Registry.get(0), "canBeApplied").mockReturnValue(true); - let transactionsForForging = connection.getTransactionsForForging(7); + jest.spyOn(Handlers.Registry.get(0), "canBeApplied").mockReturnValue(true); + let transactionsForForging = await connection.getTransactionsForForging(7); expect(transactionsForForging.length).toBe(6); expect(transactionsForForging[0]).toEqual(mockData.dummyLarge1.serialized.toString("hex")); @@ -543,8 +572,7 @@ describe("Connection", () => { connection.removeTransactionById(mockData.dummy6.id); connection.removeTransactionById(mockData.dummy7.id); - transactionsForForging = connection.getTransactionsForForging(7); - spy.mockRestore(); + transactionsForForging = await connection.getTransactionsForForging(7); expect(transactionsForForging.length).toBe(1); expect(transactionsForForging[0]).toEqual(mockData.dummyLarge2.serialized.toString("hex")); @@ -626,17 +654,19 @@ describe("Connection", () => { expect(+mockWallet.balance).toBe(+balanceBefore.minus(block2.totalFee)); }); - it("should remove transaction from pool if it's in the chained block", () => { + it("should remove transaction from pool if it's in the chained block", async () => { addTransactions([mockData.dummy2]); - expect(connection.getTransactions(0, 10)).toEqual([mockData.dummy2.serialized]); + let transactions = await connection.getTransactions(0, 10); + expect(transactions).toEqual([mockData.dummy2.serialized]); const chainedBlock = BlockFactory.fromData(block2); chainedBlock.transactions.push(mockData.dummy2); connection.acceptChainedBlock(chainedBlock); - expect(connection.getTransactions(0, 10)).toEqual([]); + transactions = await connection.getTransactions(0, 10); + expect(transactions).toEqual([]); }); it("should purge and block sender if throwIfApplyingFails() failed for a transaction in the chained block", () => { @@ -666,12 +696,18 @@ describe("Connection", () => { let findByPublicKey; let canBeApplied; let applyToSenderInPool; - const findByPublicKeyWallet = new Wallets.Wallet("thisIsAnAddressIMadeUpJustLikeThis"); + const findByPublicKeyWallet = new Wallets.Wallet("ANwc3YQe3EBjuE5sNRacP7fhkngAPaBW4Y"); + findByPublicKeyWallet.publicKey = "02778aa3d5b332965ea2a5ef6ac479ce2478535bc681a098dff1d683ff6eccc417"; + beforeEach(() => { const transactionHandler = Handlers.Registry.get(TransactionTypes.Transfer); canBeApplied = jest.spyOn(transactionHandler, "canBeApplied").mockReturnValue(true); applyToSenderInPool = jest.spyOn(transactionHandler, "applyToSenderInPool").mockReturnValue(); + (connection as any).databaseService.walletManager.findByPublicKey( + mockData.dummy1.data.senderPublicKey, + ).balance = Utils.BigNumber.ZERO; + jest.spyOn(connection.walletManager, "has").mockReturnValue(true); findByPublicKey = jest .spyOn(connection.walletManager, "findByPublicKey") @@ -687,7 +723,8 @@ describe("Connection", () => { it("should build wallets from transactions in the pool", async () => { addTransactions([mockData.dummy1]); - expect(connection.getTransactions(0, 10)).toEqual([mockData.dummy1.serialized]); + const transactions = await connection.getTransactions(0, 10); + expect(transactions).toEqual([mockData.dummy1.serialized]); await connection.buildWallets(); @@ -708,7 +745,7 @@ describe("Connection", () => { expect(getTransaction).toHaveBeenCalled(); expect(findByPublicKey).not.toHaveBeenCalled(); - expect(canBeApplied).not.toHaveBeenCalled(); + expect(canBeApplied).toHaveBeenCalled(); expect(applyToSenderInPool).not.toHaveBeenCalled(); }); @@ -728,7 +765,7 @@ describe("Connection", () => { findByPublicKeyWallet, (connection as any).databaseService.walletManager, ); - expect(purgeByPublicKey).toHaveBeenCalledWith(mockData.dummy1.data.senderPublicKey); + expect(purgeByPublicKey).not.toHaveBeenCalledWith(mockData.dummy1.data.senderPublicKey); }); }); @@ -768,6 +805,9 @@ describe("Connection", () => { it("save and restore transactions", async () => { expect(connection.getPoolSize()).toBe(0); + indexWalletWithSufficientBalance(mockData.dummy1); + indexWalletWithSufficientBalance(mockData.dummy4); + const transactions = [mockData.dummy1, mockData.dummy4]; addTransactions(transactions); @@ -792,6 +832,10 @@ describe("Connection", () => { jest.spyOn(databaseService, "getForgedTransactionsIds").mockReturnValue([mockData.dummy2.id]); + indexWalletWithSufficientBalance(mockData.dummy1); + indexWalletWithSufficientBalance(mockData.dummy2); + indexWalletWithSufficientBalance(mockData.dummy4); + const transactions = [mockData.dummy1, mockData.dummy2, mockData.dummy4]; addTransactions(transactions); @@ -840,7 +884,7 @@ describe("Connection", () => { return testTransactions; }; - it("multiple additions and retrievals", () => { + it("multiple additions and retrievals", async () => { // Abstract number which decides how many iterations are run by the test. // Increase it to run more iterations. const testSize = connection.options.syncInterval * 2; @@ -867,7 +911,7 @@ describe("Connection", () => { connection.hasExceededMaxTransactions(senderPublicKey); } connection.getTransaction(transaction.id); - connection.getTransactions(0, i); + await connection.getTransactions(0, i); } for (let i = 0; i < testSize; i++) { @@ -888,18 +932,24 @@ describe("Connection", () => { connection.addTransactions([testTransactions[0]]); }); - it("add many then get first few", () => { + it("add many then get first few", async () => { const nAdd = 2000; // We use a predictable random number calculator in order to get // a deterministic test. const rand = randomSeed.create("0"); - const testTransactions: Interfaces.ITransaction[] = generateTestTransactions(nAdd); + const testTransactions: Interfaces.ITransaction[] = []; for (let i = 0; i < nAdd; i++) { - // This will invalidate the transactions' signatures, not good, but irrelevant for this test. - testTransactions[i].data.fee = Utils.BigNumber.make(rand.intBetween(0.002 * SATOSHI, 2 * SATOSHI)); - testTransactions[i].serialized = Transactions.Utils.toBytes(testTransactions[i].data); + const transaction = TransactionFactory.transfer("AFzQCx5YpGg5vKMBg4xbuYbqkhvMkKfKe5") + .withNetwork("unitnet") + .withFee(rand.intBetween(0.002 * SATOSHI, 2 * SATOSHI)) + .withPassphrase(String(i)) + .build()[0]; + + testTransactions.push(transaction); + + indexWalletWithSufficientBalance(transaction); } // console.time(`time to add ${nAdd}`) @@ -915,7 +965,7 @@ describe("Connection", () => { .map(f => f.toString()); // console.time(`time to get first ${nGet}`) - const topTransactionsSerialized = connection.getTransactions(0, nGet); + const topTransactionsSerialized = await connection.getTransactions(0, nGet); // console.timeEnd(`time to get first ${nGet}`) const topFeesReceived = topTransactionsSerialized.map(e => diff --git a/__tests__/utils/fixtures/testnet/delegates.ts b/__tests__/utils/fixtures/testnet/delegates.ts index d591d84077..34a618ced3 100644 --- a/__tests__/utils/fixtures/testnet/delegates.ts +++ b/__tests__/utils/fixtures/testnet/delegates.ts @@ -1,19 +1,20 @@ -import { Identities, Managers } from "@arkecosystem/crypto"; - -/** - * Get the testnet genesis delegates information - * @return {Array} array of objects like { secret, publicKey, address, balance } - */ +import { Identities, Managers, Utils } from "@arkecosystem/crypto"; Managers.configManager.setFromPreset("testnet"); import { secrets } from "../../config/testnet/delegates.json"; import { genesisBlock } from "../../config/testnet/genesisBlock"; -export const delegates: any = secrets.map(secret => { +export const delegates: Array<{ + secret: string; + passphrase: string; + publicKey: string; + address: string; + balance: Utils.BigNumber; +}> = secrets.map(secret => { const publicKey: string = Identities.PublicKey.fromPassphrase(secret); const address: string = Identities.Address.fromPassphrase(secret); - const balance = genesisBlock.transactions.find( + const balance: Utils.BigNumber = genesisBlock.transactions.find( transaction => transaction.recipientId === address && transaction.type === 0, ).amount; return { diff --git a/packages/core-api/src/versions/1/transactions/controller.ts b/packages/core-api/src/versions/1/transactions/controller.ts index 3ae9523913..18507fccb7 100644 --- a/packages/core-api/src/versions/1/transactions/controller.ts +++ b/packages/core-api/src/versions/1/transactions/controller.ts @@ -33,11 +33,13 @@ export class TransactionsController extends Controller { try { const pagination = super.paginate(request); - const transactions = this.transactionPool - .getTransactions(pagination.offset, pagination.limit, 0) - .map(transaction => ({ - serialized: transaction, - })); + const transactions = (await this.transactionPool.getTransactions( + pagination.offset, + pagination.limit, + 0, + )).map(transaction => ({ + serialized: transaction, + })); return super.respondWith({ transactions: super.toCollection(request, transactions, "transaction"), diff --git a/packages/core-api/src/versions/2/transactions/controller.ts b/packages/core-api/src/versions/2/transactions/controller.ts index 68b1d994a4..facbbd51cc 100644 --- a/packages/core-api/src/versions/2/transactions/controller.ts +++ b/packages/core-api/src/versions/2/transactions/controller.ts @@ -59,10 +59,11 @@ export class TransactionsController extends Controller { try { const pagination = super.paginate(request); - const transactions = this.transactionPool.getTransactions(pagination.offset, pagination.limit); - const data = transactions.map(transaction => ({ - serialized: transaction.toString("hex"), - })); + const data = (await this.transactionPool.getTransactions(pagination.offset, pagination.limit)).map( + transaction => ({ + serialized: transaction.toString("hex"), + }), + ); return super.toPagination( request, diff --git a/packages/core-interfaces/src/core-p2p/server.ts b/packages/core-interfaces/src/core-p2p/server.ts index 0b1055b2ec..1b62956eb3 100644 --- a/packages/core-interfaces/src/core-p2p/server.ts +++ b/packages/core-interfaces/src/core-p2p/server.ts @@ -21,3 +21,8 @@ export interface IForgingTransactions { poolSize: number; count: number; } + +export interface IUnconfirmedTransactions { + transactions: string[]; + poolSize: number; +} diff --git a/packages/core-interfaces/src/core-transaction-pool/connection.ts b/packages/core-interfaces/src/core-transaction-pool/connection.ts index f44c9e0a70..307d2aca03 100644 --- a/packages/core-interfaces/src/core-transaction-pool/connection.ts +++ b/packages/core-interfaces/src/core-transaction-pool/connection.ts @@ -28,11 +28,10 @@ export interface IConnection { buildWallets(): Promise; flush(): void; getTransaction(id: string): Interfaces.ITransaction; - getTransactionIdsForForging(start: number, size: number): string[]; - getTransactions(start: number, size: number, maxBytes?: number): Buffer[]; + getTransactionIdsForForging(start: number, size: number): Promise; + getTransactions(start: number, size: number, maxBytes?: number): Promise; getTransactionsByType(type: any): any; - getTransactionsData(start: number, size: number, maxBytes?: number): Interfaces.ITransaction[]; - getTransactionsForForging(blockSize: number): string[]; + getTransactionsForForging(blockSize: number): Promise; has(transactionId: string): any; hasExceededMaxTransactions(senderPublicKey: string): boolean; isSenderBlocked(senderPublicKey: string): boolean; diff --git a/packages/core-p2p/src/socket-server/versions/internal.ts b/packages/core-p2p/src/socket-server/versions/internal.ts index da9059ddbc..3771a91243 100644 --- a/packages/core-p2p/src/socket-server/versions/internal.ts +++ b/packages/core-p2p/src/socket-server/versions/internal.ts @@ -7,10 +7,7 @@ export const emitEvent = ({ req }): void => { app.resolvePlugin("event-emitter").emit(req.data.event, req.data.body); }; -export const getUnconfirmedTransactions = (): { - transactions: string[]; - poolSize: number; -} => { +export const getUnconfirmedTransactions = async (): Promise => { const blockchain = app.resolvePlugin("blockchain"); const { maxTransactions } = app.getConfig().getMilestone(blockchain.getLastBlock().data.height).block; @@ -19,7 +16,7 @@ export const getUnconfirmedTransactions = (): { ); return { - transactions: transactionPool.getTransactionsForForging(maxTransactions), + transactions: await transactionPool.getTransactionsForForging(maxTransactions), poolSize: transactionPool.getPoolSize(), }; }; diff --git a/packages/core-transaction-pool/src/connection.ts b/packages/core-transaction-pool/src/connection.ts index ff3b5fd0e9..46f4b8c9ea 100644 --- a/packages/core-transaction-pool/src/connection.ts +++ b/packages/core-transaction-pool/src/connection.ts @@ -47,18 +47,18 @@ export class Connection implements TransactionPool.IConnection { this.memory.flush(); this.storage.connect(this.options.storage); - const all: Interfaces.ITransaction[] = this.storage.loadAll(); + let transactionsFromDisk: Interfaces.ITransaction[] = this.storage.loadAll(); + const validTransactions = await this.validateTransactions(transactionsFromDisk); - for (const transaction of all) { + transactionsFromDisk = transactionsFromDisk.filter(transaction => + validTransactions.includes(transaction.serialized.toString("hex")), + ); + + for (const transaction of transactionsFromDisk) { this.memory.remember(transaction, true); } this.purgeExpired(); - - const forgedIds: string[] = await this.databaseService.getForgedTransactionsIds(all.map(t => t.id)); - - this.removeTransactionsById(forgedIds); - this.purgeInvalidTransactions(); this.emitter.on("internal.milestone.changed", () => this.purgeInvalidTransactions()); @@ -138,89 +138,24 @@ export class Connection implements TransactionPool.IConnection { return this.memory.getById(id); } - public getTransactions(start: number, size: number, maxBytes?: number): Buffer[] { - return this.getTransactionsData(start, size, maxBytes).map( + public async getTransactions(start: number, size: number, maxBytes?: number): Promise { + return (await this.getValidTransactions(start, size, maxBytes)).map( (transaction: Interfaces.ITransaction) => transaction.serialized, ); } - public getTransactionsForForging(blockSize: number): string[] { - const transactionMemory: Interfaces.ITransaction[] = this.getTransactionsData( - 0, - blockSize, - this.options.maxTransactionBytes, + public async getTransactionsForForging(blockSize: number): Promise { + return (await this.getValidTransactions(0, blockSize, this.options.maxTransactionBytes)).map(transaction => + transaction.serialized.toString("hex"), ); - - const transactions: string[] = []; - - for (const transaction of transactionMemory) { - try { - const deserialized: Interfaces.ITransaction = Transactions.TransactionFactory.fromBytes( - transaction.serialized, - ); - - strictEqual(transaction.id, deserialized.id); - - const walletManager: State.IWalletManager = this.databaseService.walletManager; - const sender: State.IWallet = walletManager.findByPublicKey(transaction.data.senderPublicKey); - Handlers.Registry.get(transaction.type).canBeApplied(transaction, sender, walletManager); - - transactions.push(deserialized.serialized.toString("hex")); - } catch (error) { - this.removeTransactionById(transaction.id); - - this.logger.error(`Removed ${transaction.id} before forging because it is no longer valid.`); - } - } - - return transactions; } - public getTransactionIdsForForging(start: number, size: number): string[] { - return this.getTransactionsData(start, size, this.options.maxTransactionBytes).map( + public async getTransactionIdsForForging(start: number, size: number): Promise { + return (await this.getValidTransactions(start, size, this.options.maxTransactionBytes)).map( (transaction: Interfaces.ITransaction) => transaction.id, ); } - public getTransactionsData(start: number, size: number, maxBytes: number = 0): Interfaces.ITransaction[] { - this.purgeExpired(); - - const data: Interfaces.ITransaction[] = []; - - let transactionBytes: number = 0; - - let i = 0; - for (const transaction of this.memory.allSortedByFee()) { - if (i >= start + size) { - break; - } - - if (i >= start) { - let pushTransaction: boolean = false; - - if (maxBytes > 0) { - const transactionSize: number = JSON.stringify(transaction.data).length; - - if (transactionBytes + transactionSize <= maxBytes) { - transactionBytes += transactionSize; - pushTransaction = true; - } - } else { - pushTransaction = true; - } - - if (pushTransaction) { - data.push(transaction); - i++; - } - } else { - i++; - } - } - - return data; - } - public removeTransactionsForSender(senderPublicKey: string): void { for (const transaction of this.memory.getBySender(senderPublicKey)) { this.removeTransactionById(transaction.id); @@ -430,6 +365,50 @@ export class Connection implements TransactionPool.IConnection { return false; } + private async getValidTransactions( + start: number, + size: number, + maxBytes: number = 0, + ): Promise { + this.purgeExpired(); + + const data: Interfaces.ITransaction[] = []; + + let transactionBytes: number = 0; + + let i = 0; + for (const transaction of this.memory.allSortedByFee()) { + if (i >= start + size) { + break; + } + + if (i >= start) { + let pushTransaction: boolean = false; + + if (maxBytes > 0) { + const transactionSize: number = JSON.stringify(transaction.data).length; + + if (transactionBytes + transactionSize <= maxBytes) { + transactionBytes += transactionSize; + pushTransaction = true; + } + } else { + pushTransaction = true; + } + + if (pushTransaction) { + data.push(transaction); + i++; + } + } else { + i++; + } + } + + const validTransactions = await this.validateTransactions(data); + return data.filter(transaction => validTransactions.includes(transaction.serialized.toString("hex"))); + } + private addTransaction(transaction: Interfaces.ITransaction): TransactionPool.IAddTransactionResponse { if (this.has(transaction.id)) { this.logger.debug( @@ -495,6 +474,45 @@ export class Connection implements TransactionPool.IConnection { this.storage.bulkRemoveById(this.memory.pullDirtyRemoved()); } + private async validateTransactions(transactions: Interfaces.ITransaction[]): Promise { + const validTransactions: string[] = []; + const forgedIds: string[] = await this.removeForgedTransactions(transactions); + + const unforgedTransactions = transactions.filter( + (transaction: Interfaces.ITransaction) => !forgedIds.includes(transaction.id), + ); + + for (const transaction of unforgedTransactions) { + try { + const deserialized: Interfaces.ITransaction = Transactions.TransactionFactory.fromBytes( + transaction.serialized, + ); + + strictEqual(transaction.id, deserialized.id); + + const walletManager: State.IWalletManager = this.databaseService.walletManager; + const sender: State.IWallet = walletManager.findByPublicKey(transaction.data.senderPublicKey); + Handlers.Registry.get(transaction.type).canBeApplied(transaction, sender, walletManager); + + validTransactions.push(deserialized.serialized.toString("hex")); + } catch (error) { + this.removeTransactionById(transaction.id); + this.logger.error(`Removed ${transaction.id} before forging because it is no longer valid.`); + } + } + + return validTransactions; + } + + private async removeForgedTransactions(transactions: Interfaces.ITransaction[]): Promise { + const forgedIds: string[] = await this.databaseService.getForgedTransactionsIds( + transactions.map(({ id }) => id), + ); + + this.removeTransactionsById(forgedIds); + return forgedIds; + } + private purgeExpired(): void { this.purgeTransactions(ApplicationEvents.TransactionExpired, this.memory.getExpired()); } diff --git a/packages/crypto/src/transactions/deserializer.ts b/packages/crypto/src/transactions/deserializer.ts index 9fd67027dd..8c09ae4cf2 100644 --- a/packages/crypto/src/transactions/deserializer.ts +++ b/packages/crypto/src/transactions/deserializer.ts @@ -107,6 +107,10 @@ class Deserializer { const multiSignature: string = buf.readBytes(buf.limit - buf.offset).toString("hex"); transaction.signatures = [multiSignature]; } + + if (buf.remaining()) { + throw new MalformedTransactionBytesError(); + } } private deserializeSchnorr(transaction: ITransactionData, buf: ByteBuffer) {