Skip to content
1 change: 1 addition & 0 deletions packages/crypto-messages/source/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe<{
}>("Factory", ({ it, assert, beforeEach }) => {
beforeEach(async (context) => {
await prepareSandbox(context);
context.app.get<Contracts.Crypto.Configuration>(Identifiers.Cryptography.Configuration).setHeight(1); // Required by schema to set number for validators

const wallet = {};
const validatorSet = {
Expand Down
1 change: 1 addition & 0 deletions packages/crypto-messages/source/schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe<{

context.app.bind(Identifiers.Cryptography.Configuration).to(Configuration).inSingletonScope();
context.app.get<Configuration>(Identifiers.Cryptography.Configuration).setConfig(cryptoJson);
context.app.get<Configuration>(Identifiers.Cryptography.Configuration).setHeight(1); // Required by schema to set number for validators

context.validator = context.app.resolve(Validator);

Expand Down
104 changes: 12 additions & 92 deletions packages/crypto-validation/source/keywords.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,21 @@ describe<{
.get<Contracts.Crypto.Configuration>(Identifiers.Cryptography.Configuration)
.getMilestone(1);

// Valid cases
let matrix = new Array(roundValidators).fill(true);
assert.undefined(context.validator.validate("test", matrix).error);

matrix = new Array(roundValidators).fill(false);
assert.undefined(context.validator.validate("test", matrix).error);

// We don't check for boolean values, that should be defined at schema level
matrix = new Array(roundValidators).fill(1);
assert.undefined(context.validator.validate("test", matrix).error);

matrix = new Array(roundValidators).fill("a");
assert.undefined(context.validator.validate("test", matrix).error);

// Invalid cases
matrix = new Array(roundValidators - 1).fill(false);
assert.defined(context.validator.validate("test", matrix).error);

Expand All @@ -232,15 +238,13 @@ describe<{
.get<Contracts.Crypto.Configuration>(Identifiers.Cryptography.Configuration)
.getMilestone(1);

let matrix = new Array(roundValidators).fill(true);
assert.undefined(context.validator.validate("test", matrix).error);
for (const minimum of [0, 1, roundValidators - 1, roundValidators]) {
let matrix = new Array(minimum).fill(true);
assert.undefined(context.validator.validate("test", matrix).error);
}

matrix = new Array(roundValidators + 1).fill(true);
let matrix = new Array(roundValidators + 1).fill(true);
assert.defined(context.validator.validate("test", matrix).error);

assert.undefined(context.validator.validate("test", []).error);
assert.undefined(context.validator.validate("test", [false]).error);
assert.undefined(context.validator.validate("test", [true]).error);
});

it("keyword isValidatorIndex - should be ok", (context) => {
Expand All @@ -258,95 +262,11 @@ describe<{
assert.undefined(context.validator.validate("test", index).error);
}

assert.defined(context.validator.validate("test", -1).error);
assert.defined(context.validator.validate("test", 50.000_01).error);
assert.defined(context.validator.validate("test", roundValidators).error);
assert.defined(context.validator.validate("test", roundValidators + 1).error);
assert.defined(context.validator.validate("test", "a").error);
assert.defined(context.validator.validate("test", undefined).error);
});

it("keyword isValidatorIndex - should be ok for parent height", (context) => {
const schema = {
$id: "test",
type: "object",
properties: {
height: {
type: "integer",
},
validatorIndex: { isValidatorIndex: {} },
},
};
context.validator.addSchema(schema);

const { roundValidators } = context.app
.get<Contracts.Crypto.Configuration>(Identifiers.Cryptography.Configuration)
.getMilestone(1);

for (let index = 0; index < roundValidators; index++) {
assert.undefined(context.validator.validate("test", { height: 1, validatorIndex: index }).error);
}

assert.defined(context.validator.validate("test", { height: 1, validatorIndex: roundValidators }).error);
});

it("keyword isValidatorIndex - should be ok for parent block", (context) => {
const schema = {
$id: "test",
type: "object",
properties: {
data: {
type: "object",
properties: {
serialized: {
type: "string",
},
},
},
validatorIndex: { isValidatorIndex: {} },
},
};
context.validator.addSchema(schema);

let { roundValidators } = context.app
.get<Contracts.Crypto.Configuration>(Identifiers.Cryptography.Configuration)
.getMilestone(1);

const block1 = {
// height=2
serialized: "000173452bb48901020000000000000000000000000000000",
};

for (let index = 0; index < roundValidators; index++) {
assert.undefined(context.validator.validate("test", { data: block1, validatorIndex: index }).error);
}

assert.defined(context.validator.validate("test", { data: block1, validatorIndex: roundValidators }).error);

// change milestone to 15 validators at height 15
context.app
.get<Contracts.Crypto.Configuration>(Identifiers.Cryptography.Configuration)
.getMilestones()[2].height = 15;

context.app
.get<Contracts.Crypto.Configuration>(Identifiers.Cryptography.Configuration)
.getMilestones()[2].roundValidators = 15;

const block2 = {
// height=15
serialized: "000173452bb489010f0000000000000000000000000000000",
};

for (let index = 0; index < 15; index++) {
assert.undefined(context.validator.validate("test", { data: block2, validatorIndex: index }).error);
}

assert.defined(context.validator.validate("test", { data: block2, validatorIndex: 15 }).error);

// block 1 still accepted
for (let index = 0; index < roundValidators; index++) {
assert.undefined(context.validator.validate("test", { data: block1, validatorIndex: index }).error);
}

assert.defined(context.validator.validate("test", { data: block1, validatorIndex: 53 }).error);
});
});
71 changes: 14 additions & 57 deletions packages/crypto-validation/source/keywords.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,6 @@
import type { Contracts } from "@mainsail/contracts";
import { BigNumber } from "@mainsail/utils";
import type { AnySchemaObject, FuncKeywordDefinition } from "ajv";

const parseBlockNumber = (parentSchema): number | undefined => {
if (!parentSchema || !parentSchema.parentData) {
return undefined;
}

if (parentSchema.parentData.blockNumber) {
// prevotes / precommits
return parentSchema.parentData.blockNumber;
}

if (!parentSchema.parentData.data) {
return undefined;
}

// Proposals contain the block only in serialized form (hex).
// We can extract the block number at a fixed offset here, without needing to deserialize the whole block.

// See packages/crypto-messages/source/serializer.ts#serializeProposed for reference.

const serialized = parentSchema.parentData.data.serialized;
if (!serialized) {
return undefined;
}

if (serialized.length < 30) {
return undefined;
}

const lockProofSize = 2 + Number.parseInt(serialized.slice(0, 2), 16) * 2;
// version: 1 byte (2 hex)
// timestamp: 6 bytes (12 hex)
// blockNumber: 4 byte (8 hex)
const offset = lockProofSize + 2 + 12;
return Buffer.from(serialized.slice(offset, offset + 8), "hex").readUInt32LE();
};
import type { FuncKeywordDefinition } from "ajv";

export const makeKeywords = (
configuration: Contracts.Crypto.Configuration,
Expand All @@ -59,8 +23,7 @@ export const makeKeywords = (
};

const bignumber: FuncKeywordDefinition = {
// @ts-ignore
compile: (schema) => (data, parentSchema: AnySchemaObject) => {
compile: (schema) => (data) => {
const minimum = schema.minimum !== undefined ? schema.minimum : 0;
const maximum = schema.maximum !== undefined ? schema.maximum : BigNumber.UINT256_MAX;

Expand Down Expand Up @@ -95,22 +58,17 @@ export const makeKeywords = (
},
errors: false,
keyword: "buffer",
metaSchema: {
type: "object",
},
};

// Use by: crypto-proposal, p2p
const limitToRoundValidators: FuncKeywordDefinition = {
// TODO: Check type (same as bignum)
// @ts-ignore
compile(schema: { minimum?: number }) {
return (data, parentSchema: AnySchemaObject) => {
return (data) => {
if (!Array.isArray(data)) {
return false;
}

const blockNumber = parseBlockNumber(parentSchema);
const { roundValidators } = configuration.getMilestone(blockNumber);
const { roundValidators } = configuration.getMilestone();
const minimum = schema.minimum !== undefined ? schema.minimum : roundValidators;

if (data.length < minimum || data.length > roundValidators) {
Expand All @@ -130,26 +88,25 @@ export const makeKeywords = (
},
};

// Used by: crypto-messages (prevotes / precommits) and crypto-proposal
const isValidatorIndex: FuncKeywordDefinition = {
// TODO: Check type (same as bignum)
// @ts-ignore
compile() {
return (data, parentSchema: AnySchemaObject) => {
const blockNumber = parseBlockNumber(parentSchema);
const { roundValidators } = configuration.getMilestone(blockNumber);

return (data) => {
if (!Number.isInteger(data)) {
return false;
}

return data >= 0 && data < roundValidators;
if (data < 0) {
return false;
}

const { roundValidators } = configuration.getMilestone();

return data < roundValidators;
};
},
errors: false,
keyword: "isValidatorIndex",
metaSchema: {
type: "object",
},
};

return { bignumber, buffer, isValidatorIndex, limitToRoundValidators, maxBytes };
Expand Down
42 changes: 42 additions & 0 deletions packages/crypto-validation/source/schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,46 @@ describe<{
assert.defined(validator.validate("hex", char.repeat(20)).error);
}
});

it("prefixedDataHex - should be ok", ({ validator }) => {
const validValues = ["0x", "0x00", "0x0123", "0x0123456789abcdef"];

for (const value of validValues) {
assert.undefined(validator.validate("prefixedDataHex", value).error);
}
});

it("prefixedDataHex - should not be ok", ({ validator }) => {
const invalidValues = ["0x0", "0x000", "deadbeef", "0xGG", "0X00"];

for (const value of invalidValues) {
assert.defined(validator.validate("prefixedDataHex", value).error);
}

assert.defined(validator.validate("prefixedDataHex", 123).error);
assert.defined(validator.validate("prefixedDataHex", null).error);
assert.defined(validator.validate("prefixedDataHex").error);
assert.defined(validator.validate("prefixedDataHex", {}).error);
});

it("prefixedQuantityHex - should be ok", ({ validator }) => {
const validValues = ["0x0", "0x1", "0x01", "0x123456789abcdef"];

for (const value of validValues) {
assert.undefined(validator.validate("prefixedQuantityHex", value).error);
}
});

it("prefixedQuantityHex - should not be ok", ({ validator }) => {
const invalidValues = ["0x", "deadbeef", "0xGG", "0X01"];

for (const value of invalidValues) {
assert.defined(validator.validate("prefixedQuantityHex", value).error);
}

assert.defined(validator.validate("prefixedQuantityHex", 123).error);
assert.defined(validator.validate("prefixedQuantityHex", null).error);
assert.defined(validator.validate("prefixedQuantityHex").error);
assert.defined(validator.validate("prefixedQuantityHex", {}).error);
});
});
1 change: 1 addition & 0 deletions packages/crypto-validation/source/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const schemas: Record<"alphanumeric" | "hex" | "prefixedQuantityHex" | "p
pattern: "^0x([0-9a-f]{2})*$",
type: "string",
},
// requires at least one hex character after the "0x" prefix
prefixedQuantityHex: {
$id: "prefixedQuantityHex",
pattern: "^0x[0-9a-f]+$",
Expand Down
24 changes: 10 additions & 14 deletions packages/crypto-validation/source/service-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
import { Identifiers } from "@mainsail/constants";
import { Configuration } from "@mainsail/crypto-config";
import { Validator } from "@mainsail/validation/source/validator";

import cryptoJson from "../../core/bin/config/devnet/core/crypto.json";
import { Application } from "@mainsail/kernel";
import { Validator } from "@mainsail/validation";
import { describe } from "@mainsail/test-runner";
import { schemas } from "./schemas";
import { ServiceProvider } from "./service-provider";

describe<{
app: Application;
validator: Partial<Validator>;
validator: Validator;
serviceProvider: ServiceProvider;
}>("ServiceProvider", ({ it, beforeEach, assert, spy }) => {
}>("ServiceProvider", ({ it, beforeEach, assert }) => {
beforeEach((context) => {
context.validator = {
addKeyword: () => {},
addSchema: () => {},
};

context.app = new Application();
context.app.bind(Identifiers.Cryptography.Validator).toConstantValue(context.validator);
context.app.bind(Identifiers.Cryptography.Validator).to(Validator).inSingletonScope();
context.validator = context.app.get<Validator>(Identifiers.Cryptography.Validator);
context.app.bind(Identifiers.Cryptography.Configuration).to(Configuration).inSingletonScope();
context.app.get<Configuration>(Identifiers.Cryptography.Configuration).setConfig(cryptoJson);

context.serviceProvider = context.app.resolve(ServiceProvider);
});

it("should register", async ({ validator, serviceProvider }) => {
const spyOnExtend = spy(validator, "addKeyword");
const spyOnAddSchema = spy(validator, "addSchema");

await assert.resolves(() => serviceProvider.register());

spyOnExtend.called();
spyOnAddSchema.called();
assert.true(validator.hasSchema("alphanumeric"));
assert.true(validator.hasSchema("hex"));
assert.true(validator.hasSchema("prefixedDataHex"));
assert.true(validator.hasSchema("prefixedQuantityHex"));
});
});
Loading