Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core-transaction-pool): rebuild vs update race #3800

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ describe("Transaction Forging - Entity registration", () => {
describe("Signed with 1 Passphrase", () => {
it("should broadcast, accept and forge it [Signed with 1 Passphrase]", async () => {
// Registering a desktop wallet plugin
await snoozeForBlock(1);
const entityRegistration = TransactionFactory.initialize(app)
.entity({
type: Enums.EntityType.Plugin,
Expand All @@ -29,10 +28,12 @@ describe("Transaction Forging - Entity registration", () => {
})
.withPassphrase(secrets[0])
.createOne();

await expect(entityRegistration).toBeAccepted();
await snoozeForBlock(1);
// await expect(entityRegistration.id).toBeForged();
// await expect(entityRegistration).entityRegistered();
await expect(entityRegistration.id).toBeForged();

await expect(entityRegistration).entityRegistered();
});

it("should reject entity registration, because entity name contains unicode control characters [Signed with 1 Passphrase]", async () => {
Expand Down
3 changes: 3 additions & 0 deletions __tests__/unit/core-api/__support__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const buildSenderWallet = (app: Application, passphrase: string | null =

export const initApp = (): Application => {
const app = new Application(new Container.Container());
const logger = { error: jest.fn() };

app.bind(Container.Identifiers.LogService).toConstantValue(logger);

app.bind(Container.Identifiers.PluginConfiguration).to(Providers.PluginConfiguration).inSingletonScope();

Expand Down
2 changes: 2 additions & 0 deletions __tests__/unit/core-magistrate-api/service-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ beforeEach(() => {
app.bind(Container.Identifiers.TransactionHistoryService).toConstantValue({});

app.bind(Container.Identifiers.TransactionHandlerRegistry).toConstantValue({});

app.bind(Container.Identifiers.LogService).toConstantValue({});
});

describe("ServiceProvider", () => {
Expand Down
4 changes: 2 additions & 2 deletions __tests__/unit/core-state/block-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,9 @@ describe("BlockState", () => {
expect(forgingWallet.balance).toEqual(balanceBefore);
});

it("should throw if there is no forger wallet", () => {
it("should throw if there is no forger wallet", async () => {
walletRepo.forgetByPublicKey(forgingWallet.publicKey);
expect(async () => await blockState.applyBlock(blocks[0])).toReject();
await expect(blockState.applyBlock(blocks[0])).toReject();
});

it("should update sender's and recipient's delegate's vote balance when applying transaction", async () => {
Expand Down
24 changes: 11 additions & 13 deletions __tests__/unit/core-state/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface Spies {
logger: {
error: jest.SpyInstance;
info: jest.SpyInstance;
notice: jest.SpyInstance;
debug: jest.SpyInstance;
warning: jest.SpyInstance;
};
Expand Down Expand Up @@ -68,6 +69,16 @@ export const setUpDefaults = {
export const setUp = async (setUpOptions = setUpDefaults, skipBoot = false): Promise<Setup> => {
const sandbox = new Sandbox();

const logger = {
error: jest.fn(),
info: jest.fn(),
notice: jest.fn(),
debug: jest.fn(),
warning: jest.fn(),
};

sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger);

sandbox.app.bind(Container.Identifiers.WalletAttributes).to(Services.Attributes.AttributeSet).inSingletonScope();

sandbox.app.get<Services.Attributes.AttributeSet>(Container.Identifiers.WalletAttributes).set("delegate");
Expand Down Expand Up @@ -193,19 +204,6 @@ export const setUp = async (setUpOptions = setUpDefaults, skipBoot = false): Pro

const applySpy: jest.SpyInstance = jest.fn();
const revertSpy: jest.SpyInstance = jest.fn();
const error: jest.SpyInstance = jest.fn();
const info: jest.SpyInstance = jest.fn();
const debug: jest.SpyInstance = jest.fn();
const warning: jest.SpyInstance = jest.fn();

const logger = {
error,
info,
debug,
warning,
};

sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger);

const getRegisteredHandlersSpy = jest.fn();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,10 @@ describe("ExpirationService.getExpirationHeight", () => {
const expirationService = container.resolve(ExpirationService);
const check = async () => await expirationService.getExpirationHeight(transaction);

expect(await check).toReject();
await expect(check()).rejects.toThrowError();
});

it("should return value stored in expiration field when checking v2 transaciton with expiration field", async () => {
it("should return value stored in expiration field when checking v2 transaction with expiration field", async () => {
const transaction = { data: { version: 2, expiration: 100 } } as Interfaces.ITransaction;
const expirationService = container.resolve(ExpirationService);
const expirationHeight = await expirationService.getExpirationHeight(transaction);
Expand Down
163 changes: 95 additions & 68 deletions __tests__/unit/core-transaction-pool/sender-state.test.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,46 @@
import { Container, Contracts, Enums } from "@packages/core-kernel";
import { Services } from "@packages/core-kernel/dist";
import { Sandbox } from "@packages/core-test-framework";
import {
ApplyTransactionAction,
RevertTransactionAction,
ThrowIfCannotEnterPoolAction,
VerifyTransactionAction,
} from "@packages/core-transaction-pool/src/actions";
import { SenderState } from "@packages/core-transaction-pool/src/sender-state";
import { Crypto, Interfaces, Managers } from "@packages/crypto";
import { Container, Contracts, Enums } from "@arkecosystem/core-kernel";
import { Crypto, Interfaces, Managers } from "@arkecosystem/crypto";

jest.mock("@packages/crypto");
import { SenderState } from "../../../packages/core-transaction-pool/src/sender-state";

const configuration = { getRequired: jest.fn(), getOptional: jest.fn() };
const handler = { verify: jest.fn(), throwIfCannotEnterPool: jest.fn(), apply: jest.fn(), revert: jest.fn() };
const handlerRegistry = { getActivatedHandlerForData: jest.fn() };
const expirationService = { isExpired: jest.fn(), getExpirationHeight: jest.fn() };
const eventDispatcherService = { dispatch: jest.fn() };
jest.mock("@packages/crypto");

let sandbox: Sandbox;
const configuration = {
getRequired: jest.fn(),
getOptional: jest.fn(),
};
const handlerRegistry = {
getActivatedHandlerForData: jest.fn(),
};
const expirationService = {
isExpired: jest.fn(),
getExpirationHeight: jest.fn(),
};
const triggers = {
call: jest.fn(),
};
const emitter = {
dispatch: jest.fn(),
};

const container = new Container.Container();
container.bind(Container.Identifiers.PluginConfiguration).toConstantValue(configuration);
container.bind(Container.Identifiers.TransactionHandlerRegistry).toConstantValue(handlerRegistry);
container.bind(Container.Identifiers.TransactionPoolExpirationService).toConstantValue(expirationService);
container.bind(Container.Identifiers.TriggerService).toConstantValue(triggers);
container.bind(Container.Identifiers.EventDispatcherService).toConstantValue(emitter);

beforeEach(() => {
sandbox = new Sandbox();

sandbox.app.bind(Container.Identifiers.PluginConfiguration).toConstantValue(configuration);
sandbox.app.bind(Container.Identifiers.TransactionHandlerRegistry).toConstantValue(handlerRegistry);
sandbox.app.bind(Container.Identifiers.TransactionPoolExpirationService).toConstantValue(expirationService);
sandbox.app.bind(Container.Identifiers.EventDispatcherService).toConstantValue(eventDispatcherService);
sandbox.app.bind(Container.Identifiers.TriggerService).to(Services.Triggers.Triggers).inSingletonScope();

sandbox.app
.get<Services.Triggers.Triggers>(Container.Identifiers.TriggerService)
.bind("applyTransaction", new ApplyTransactionAction());

sandbox.app
.get<Services.Triggers.Triggers>(Container.Identifiers.TriggerService)
.bind("revertTransaction", new RevertTransactionAction());

sandbox.app
.get<Services.Triggers.Triggers>(Container.Identifiers.TriggerService)
.bind("throwIfCannotEnterPool", new ThrowIfCannotEnterPoolAction());

sandbox.app
.get<Services.Triggers.Triggers>(Container.Identifiers.TriggerService)
.bind("verifyTransaction", new VerifyTransactionAction());

(Managers.configManager.get as jest.Mock).mockReset();
(Crypto.Slots.getTime as jest.Mock).mockReset();

configuration.getRequired.mockReset();
configuration.getOptional.mockReset();
handler.verify.mockReset();
handler.throwIfCannotEnterPool.mockReset();
handler.apply.mockReset();
handler.revert.mockReset();
handlerRegistry.getActivatedHandlerForData.mockReset();
expirationService.isExpired.mockReset();
expirationService.getExpirationHeight.mockReset();

handlerRegistry.getActivatedHandlerForData.mockReturnValue(Promise.resolve(handler));
triggers.call.mockReset();
emitter.dispatch.mockReset();
});

const transaction = {
Expand All @@ -69,121 +51,166 @@ const transaction = {

describe("SenderState.apply", () => {
it("should throw when transaction exceeds maximum byte size", async () => {
const senderState = container.resolve(SenderState);

configuration.getRequired.mockReturnValueOnce(0); // maxTransactionBytes

const senderState = sandbox.app.resolve(SenderState);
const promise = senderState.apply(transaction);

await expect(promise).rejects.toBeInstanceOf(Contracts.TransactionPool.PoolError);
await expect(promise).rejects.toHaveProperty("type", "ERR_TOO_LARGE");
});

it("should throw when transaction is from wrong network", async () => {
const senderState = container.resolve(SenderState);

(Managers.configManager.get as jest.Mock).mockReturnValue(321); // network.pubKeyHash
configuration.getRequired.mockReturnValueOnce(1024); // maxTransactionBytes

const senderState = sandbox.app.resolve(SenderState);
const promise = senderState.apply(transaction);

await expect(promise).rejects.toBeInstanceOf(Contracts.TransactionPool.PoolError);
await expect(promise).rejects.toHaveProperty("type", "ERR_WRONG_NETWORK");
});

it("should throw when transaction is from future", async () => {
const senderState = container.resolve(SenderState);

(Managers.configManager.get as jest.Mock).mockReturnValue(123); // network.pubKeyHash
(Crypto.Slots.getTime as jest.Mock).mockReturnValue(9999);
configuration.getRequired.mockReturnValueOnce(1024); // maxTransactionBytes

const senderState = sandbox.app.resolve(SenderState);
const promise = senderState.apply(transaction);

await expect(promise).rejects.toBeInstanceOf(Contracts.TransactionPool.PoolError);
await expect(promise).rejects.toHaveProperty("type", "ERR_FROM_FUTURE");
});

it("should throw when transaction expired", async () => {
const senderState = container.resolve(SenderState);

(Managers.configManager.get as jest.Mock).mockReturnValue(123); // network.pubKeyHash
(Crypto.Slots.getTime as jest.Mock).mockReturnValue(13600);
configuration.getRequired.mockReturnValueOnce(1024); // maxTransactionBytes
expirationService.isExpired.mockReturnValueOnce(true);
expirationService.getExpirationHeight.mockReturnValueOnce(10);

const senderState = sandbox.app.resolve(SenderState);
const promise = senderState.apply(transaction);

await expect(promise).rejects.toBeInstanceOf(Contracts.TransactionPool.PoolError);
await expect(promise).rejects.toHaveProperty("type", "ERR_EXPIRED");

expect(eventDispatcherService.dispatch).toHaveBeenCalledTimes(1);
expect(eventDispatcherService.dispatch).toHaveBeenCalledWith(Enums.TransactionEvent.Expired, expect.anything());
expect(emitter.dispatch).toHaveBeenCalledTimes(1);
expect(emitter.dispatch).toHaveBeenCalledWith(Enums.TransactionEvent.Expired, expect.anything());
});

it("should throw when transaction fails to verify", async () => {
const senderState = container.resolve(SenderState);
const handler = {};

(Managers.configManager.get as jest.Mock).mockReturnValue(123); // network.pubKeyHash
(Crypto.Slots.getTime as jest.Mock).mockReturnValue(13600);
configuration.getRequired.mockReturnValueOnce(1024); // maxTransactionBytes
expirationService.isExpired.mockReturnValueOnce(false);
handler.verify.mockResolvedValue(false);
handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler);
triggers.call.mockResolvedValueOnce(false); // verifyTransaction

const senderState = sandbox.app.resolve(SenderState);
const promise = senderState.apply(transaction);

await expect(promise).rejects.toBeInstanceOf(Contracts.TransactionPool.PoolError);
await expect(promise).rejects.toHaveProperty("type", "ERR_BAD_DATA");

expect(handlerRegistry.getActivatedHandlerForData).toBeCalledWith(transaction.data);
expect(triggers.call).toBeCalledWith("verifyTransaction", { handler, transaction });
});

it("should throw when state is corrupted", async () => {
handler.revert.mockRejectedValueOnce(new Error("Corrupt it"));
const senderState = container.resolve(SenderState);
const handler = {};

(Managers.configManager.get as jest.Mock).mockReturnValue(123); // network.pubKeyHash
(Crypto.Slots.getTime as jest.Mock).mockReturnValue(13600);
configuration.getRequired.mockReturnValueOnce(1024); // maxTransactionBytes
expirationService.isExpired.mockReturnValueOnce(false);
handler.verify.mockResolvedValue(true);

const senderState = sandbox.app.resolve(SenderState);
// revert
handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler);
triggers.call.mockRejectedValueOnce(new Error("Corrupt it!")); // revertTransaction

// apply
handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler);
triggers.call.mockResolvedValueOnce(true); // verifyTransaction

await senderState.revert(transaction).catch(() => undefined);
const promise = senderState.apply(transaction);

await expect(promise).rejects.toBeInstanceOf(Contracts.TransactionPool.PoolError);
await expect(promise).rejects.toHaveProperty("type", "ERR_RETRY");

expect(handlerRegistry.getActivatedHandlerForData).toBeCalledWith(transaction.data);
expect(triggers.call).toBeCalledWith("revertTransaction", { handler, transaction });

expect(handlerRegistry.getActivatedHandlerForData).toBeCalledWith(transaction.data);
expect(triggers.call).toBeCalledWith("verifyTransaction", { handler, transaction });
});

it("should throw when transaction fails to apply", async () => {
const senderState = container.resolve(SenderState);
const handler = {};

(Managers.configManager.get as jest.Mock).mockReturnValue(123); // network.pubKeyHash
(Crypto.Slots.getTime as jest.Mock).mockReturnValue(13600);
configuration.getRequired.mockReturnValueOnce(1024); // maxTransactionBytes
expirationService.isExpired.mockReturnValueOnce(false);
handler.verify.mockResolvedValue(true);
handler.throwIfCannotEnterPool.mockRejectedValueOnce(new Error("Something terrible"));
handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler);
triggers.call.mockResolvedValueOnce(true); // verifyTransaction
triggers.call.mockResolvedValueOnce(undefined); // throwIfCannotEnterPool
triggers.call.mockRejectedValueOnce(new Error("Some apply error")); // applyTransaction

const senderState = sandbox.app.resolve(SenderState);
const promise = senderState.apply(transaction);

await expect(promise).rejects.toBeInstanceOf(Contracts.TransactionPool.PoolError);
await expect(promise).rejects.toHaveProperty("type", "ERR_APPLY");

expect(handlerRegistry.getActivatedHandlerForData).toBeCalledWith(transaction.data);
expect(triggers.call).toBeCalledWith("verifyTransaction", { handler, transaction });
expect(triggers.call).toBeCalledWith("throwIfCannotEnterPool", { handler, transaction });
expect(triggers.call).toBeCalledWith("applyTransaction", { handler, transaction });
});

it("should call handler to apply transaction", async () => {
const senderState = container.resolve(SenderState);
const handler = {};

(Managers.configManager.get as jest.Mock).mockReturnValue(123); // network.pubKeyHash
(Crypto.Slots.getTime as jest.Mock).mockReturnValue(13600);
configuration.getRequired.mockReturnValueOnce(1024); // maxTransactionBytes
expirationService.isExpired.mockReturnValueOnce(false);
handler.verify.mockResolvedValue(true);
handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler);
triggers.call.mockResolvedValueOnce(true); // verifyTransaction
triggers.call.mockResolvedValueOnce(undefined); // throwIfCannotEnterPool
triggers.call.mockResolvedValueOnce(undefined); // applyTransaction

const senderState = sandbox.app.resolve(SenderState);
await senderState.apply(transaction);

expect(handler.throwIfCannotEnterPool).toBeCalledWith(transaction);
expect(handler.apply).toBeCalledWith(transaction);
expect(handlerRegistry.getActivatedHandlerForData).toBeCalledWith(transaction.data);
expect(triggers.call).toBeCalledWith("verifyTransaction", { handler, transaction });
expect(triggers.call).toBeCalledWith("throwIfCannotEnterPool", { handler, transaction });
expect(triggers.call).toBeCalledWith("applyTransaction", { handler, transaction });
});
});

describe("SenderState.revert", () => {
it("should call handler to revert transaction", async () => {
const senderState = sandbox.app.resolve(SenderState);
const senderState = container.resolve(SenderState);
const handler = {};

handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler);
triggers.call.mockResolvedValueOnce(undefined); // revertTransaction

await senderState.revert(transaction);

expect(handler.revert).toBeCalledWith(transaction);
expect(handlerRegistry.getActivatedHandlerForData).toBeCalledWith(transaction.data);
expect(triggers.call).toBeCalledWith("revertTransaction", { handler, transaction });
});
});
Loading