Skip to content
11 changes: 5 additions & 6 deletions packages/crypto-commit/source/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export class CommitFactory implements Contracts.Crypto.CommitFactory {

const proofBuffer = buffer.readBytes(this.proofSize());
const proof = await this.commitDeserializer.deserializeCommitProof(proofBuffer);
this.#verifySchema("commitProof", proof);

const block = await this.blockFactory.fromBytes(buffer.getRemainder());

Expand All @@ -39,7 +38,7 @@ export class CommitFactory implements Contracts.Crypto.CommitFactory {
serialized: buff.toString("hex"),
};

this.#verifySchema("commit", commit);
this.#verifySchema(commit);

return commit;
}
Expand Down Expand Up @@ -75,15 +74,15 @@ export class CommitFactory implements Contracts.Crypto.CommitFactory {
proof: json.proof,
serialized: json.serialized,
};
this.#verifySchema("commit", commit);
this.#verifySchema(commit);
return commit;
}

#verifySchema<T>(schema: string, data: T): void {
const result = this.validator.validate(schema, data);
#verifySchema<T>(data: T): void {
const result = this.validator.validate("commit", data);

if (result.error) {
throw new MessageSchemaError(schema, result.error);
throw new MessageSchemaError("commit", result.error);
}
}
}
26 changes: 21 additions & 5 deletions packages/crypto-commit/source/schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ import { schemas } from "./schemas";
describe<{
app: Application;
validator: Validator;
}>("Schemas", ({ it, assert, beforeEach }) => {
configuration: Configuration;
}>("Schemas", ({ it, assert, beforeEach, spy }) => {
beforeEach((context) => {
context.app = new Application();

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);
context.configuration = context.app.get<Configuration>(Identifiers.Cryptography.Configuration);
context.configuration.setConfig(cryptoJson);
context.configuration.setHeight(1);

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

for (const keyword of Object.values({
...makeBaseKeywords(context.app.get<Configuration>(Identifiers.Cryptography.Configuration)),
...makeTransactionKeywords(context.app.get<Configuration>(Identifiers.Cryptography.Configuration)),
...makeBaseKeywords(context.configuration),
...makeTransactionKeywords(context.configuration),
})) {
context.validator.addKeyword(keyword);
}
Expand Down Expand Up @@ -60,6 +62,20 @@ describe<{
assert.undefined(result.error);
});

it("commit - should correctly parse block number", ({ validator, configuration }) => {
const spyGetMilestone = spy(configuration, "getMilestone");

const result = validator.validate("commit", {
block: blockData,
proof: commitProof1,
serialized: commitSerialized,
});
assert.undefined(result.error);

spyGetMilestone.calledTimes(7); // 6 x for block.number and 1 x for limitToRoundValidators
spyGetMilestone.calledWith(blockData.number);
});

it("commit - should not allow additional fields", ({ validator }) => {
const result = validator.validate("commit", {
block: blockData,
Expand Down
3 changes: 3 additions & 0 deletions packages/crypto-commit/source/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const schemas: Record<"commit" | "commitProof", AnySchemaObject> = {
signature: { $ref: "consensusSignature" },
validators: {
items: { type: "boolean" },
// NOTE: This is not an actual property of the commit proof, but we need it to validate the commit proof against the correct set of validators.
// We take value from block.number, which is available in the commit schema
limitToRoundValidators: { blockNumberPath: "block.number" },
type: "array",
},
},
Expand Down
20 changes: 16 additions & 4 deletions packages/crypto-messages/source/schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@ import { schemas } from "./schemas";
describe<{
app: Application;
validator: Validator;
}>("Schemas", ({ it, assert, beforeEach }) => {
configuration: Configuration;
}>("Schemas", ({ it, assert, beforeEach, spy }) => {
beforeEach((context) => {
context.app = new Application();

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.configuration = context.app.get<Configuration>(Identifiers.Cryptography.Configuration);
context.configuration.setConfig(cryptoJson);
context.configuration.setHeight(1); // Required by schema to set number for validators

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

for (const keyword of Object.values({
...makeBaseKeywords(context.app.get<Configuration>(Identifiers.Cryptography.Configuration)),
...makeBaseKeywords(context.configuration),
})) {
context.validator.addKeyword(keyword);
}
Expand Down Expand Up @@ -144,4 +146,14 @@ describe<{
const result = validator.validate("message", { ...prevoteData, extraField: 123 });
assert.defined(result.error);
});

it("message - should correctly parse block number", async ({ validator, configuration }) => {
const spyGetMilestone = spy(configuration, "getMilestone");

const result = validator.validate("message", { ...prevoteData, blockNumber: 3 });
assert.undefined(result.error);

spyGetMilestone.calledOnce();
spyGetMilestone.calledWith(3);
});
});
2 changes: 1 addition & 1 deletion packages/crypto-messages/source/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const schemas: Record<"message", AnySchemaObject> = {
round: { minimum: 0, type: "integer" },
signature: { $ref: "consensusSignature" },
type: { enum: [Enums.Crypto.MessageType.Prevote, Enums.Crypto.MessageType.Precommit] },
validatorIndex: { isValidatorIndex: {} },
validatorIndex: { isValidatorIndex: { blockNumberPath: "blockNumber" } },
},
required: ["type", "blockNumber", "round", "validatorIndex", "signature"],
type: "object",
Expand Down
10 changes: 8 additions & 2 deletions packages/crypto-proposal/source/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export class Factory implements Contracts.Crypto.ProposalFactory {
const lockProof = await this.#getLockProof(buffer);
const block = await this.blockFactory.fromBytes(buffer.getRemainder());

if (lockProof) {
this.#verifySchema("lockProof", { ...lockProof, number: block.number });
}

return {
block,
lockProof,
Expand All @@ -77,8 +81,6 @@ export class Factory implements Contracts.Crypto.ProposalFactory {
if (lockProofLength > 0) {
const lockProofBuffer = buffer.readBytes(lockProofLength);
lockProof = await this.deserializer.deserializeLockProof(lockProofBuffer);

this.#verifySchema("lockProof", lockProof);
}

return lockProof;
Expand All @@ -92,6 +94,10 @@ export class Factory implements Contracts.Crypto.ProposalFactory {
const lockProof = await this.#getLockProof(buffer);
const blockHeader = await this.blockFactory.headerFromBytes(buffer.getRemainder());

if (lockProof) {
this.#verifySchema("lockProof", { ...lockProof, number: blockHeader.number });
}

return {
blockHeader,
lockProof,
Expand Down
47 changes: 39 additions & 8 deletions packages/crypto-proposal/source/schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,27 @@ import {
} from "../test/fixtures/index.js";
import { schemas } from "./schemas";
import { signature } from "../test/fixtures/proposal.js";
import { numberArray } from "@mainsail/utils";

describe<{
app: Application;
validator: Validator;
}>("Schemas", ({ it, assert, beforeEach }) => {
configuration: Configuration;
}>("Schemas", ({ it, assert, beforeEach, spy }) => {
const proposals = [Proposal, ProposalWithValidRound, ProposalWithLockProof, ProposalWithLockProofAndValidRound];

beforeEach((context) => {
context.app = new Application();

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);
context.configuration = context.app.get<Configuration>(Identifiers.Cryptography.Configuration);
context.configuration.setConfig(cryptoJson);
context.configuration.setHeight(1);

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

for (const keyword of Object.values({
...makeBaseKeywords(context.app.get<Configuration>(Identifiers.Cryptography.Configuration)),
...makeBaseKeywords(context.configuration),
})) {
context.validator.addKeyword(keyword);
}
Expand Down Expand Up @@ -180,22 +183,23 @@ describe<{
});

it("lockProof - should be ok", ({ validator }) => {
const result = validator.validate("lockProof", lockProof);
const result = validator.validate("lockProof", { ...lockProof, number: 1 });
assert.undefined(result.error);
});

it("lockProof - all fields are required", ({ validator }) => {
it("lockProof - all fields are required (except number)", ({ validator }) => {
const keys = Object.keys(schemas.lockProof.properties);
for (let key of keys) {
const lockProofCopy = { ...lockProof, [key]: undefined };
if (key === "number") continue;
const lockProofCopy = { ...lockProof, [key]: undefined, number: 1 };
const result = validator.validate("lockProof", lockProofCopy);
assert.defined(result.error);
assert.true(result.error?.includes(key));
}
});

it("lockProof - should not allow additional fields", ({ validator }) => {
const lockProofCopy = { ...lockProof, extraField: "extraValue" };
const lockProofCopy = { ...lockProof, extraField: "extraValue", number: 1 };
const result = validator.validate("lockProof", lockProofCopy);
assert.defined(result.error);
assert.true(result.error!.includes("additional properties"));
Expand All @@ -204,6 +208,7 @@ describe<{
it("lockProof - signature should be ok", ({ validator }) => {
for (let char of "0123456789abcdef") {
const result = validator.validate("lockProof", {
number: 1,
signature: char.repeat(192),
validators: Array(53).fill(true),
});
Expand All @@ -221,6 +226,7 @@ describe<{
const result = validator.validate("lockProof", {
signature,
validators: Array(53).fill(true),
number: 1,
});
assert.defined(result.error);
assert.true(result.error!.includes("signature"));
Expand All @@ -238,6 +244,7 @@ describe<{

for (let validators of validValidators) {
const result = validator.validate("lockProof", {
number: 1,
signature,
validators,
});
Expand All @@ -258,9 +265,33 @@ describe<{
const result = validator.validate("lockProof", {
signature,
validators,
number: 1,
});
assert.defined(result.error);
assert.true(result.error!.includes("validators"));
}
});

it("proposalUnsigned - should correctly deserialize block number from payloadSerialized", ({
validator,
configuration,
}) => {
const spyConfigurationGetMilestone = spy(configuration, "getMilestone");

const result = validator.validate("proposalUnsigned", Proposal.proposalDataSerializableUnsigned);
assert.undefined(result.error);

spyConfigurationGetMilestone.calledOnce();
spyConfigurationGetMilestone.calledWith(2);
});

it("lockProof - should correctly parse block number from number", ({ validator, configuration }) => {
const spyConfigurationGetMilestone = spy(configuration, "getMilestone");

const result = validator.validate("lockProof", { ...lockProof, number: 3 });
assert.undefined(result.error);

spyConfigurationGetMilestone.calledOnce();
spyConfigurationGetMilestone.calledWith(3);
});
});
10 changes: 7 additions & 3 deletions packages/crypto-proposal/source/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const proposalUnsigned = {
payloadSerialized: { $ref: "hex" },
round: { minimum: 0, type: "integer" },
validRound: { minimum: 0, type: "integer" },
validatorIndex: { isValidatorIndex: {} },
validatorIndex: { isValidatorIndex: { blockNumberPath: "payloadSerialized" } },
},
required: ["round", "payloadSerialized", "validatorIndex"],
type: "object",
Expand All @@ -18,14 +18,18 @@ export const schemas: Record<"lockProof" | "proposal" | "proposalUnsigned", AnyS
$id: "lockProof",
additionalProperties: false,
properties: {
// NOTE: This is not an actual property of the lock proof, but we need it to validate the lock proof against the correct set of validators.
number: { minimum: 0, type: "integer" },

signature: { $ref: "consensusSignature" },

validators: {
items: { type: "boolean" },
limitToRoundValidators: {},
limitToRoundValidators: { blockNumberPath: "number" },
type: "array",
},
},
required: ["signature", "validators"],
required: ["signature", "validators", "number"],
type: "object",
},
proposal: {
Expand Down
23 changes: 17 additions & 6 deletions packages/crypto-validation/source/keywords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Contracts } from "@mainsail/contracts";
import { BigNumber } from "@mainsail/utils";
import type { FuncKeywordDefinition } from "ajv";

import { parseBlockNumber } from "./parse-block-number.js";

export const makeKeywords = (
configuration: Contracts.Crypto.Configuration,
): {
Expand Down Expand Up @@ -62,13 +64,14 @@ export const makeKeywords = (

// Use by: crypto-proposal, p2p
const limitToRoundValidators: FuncKeywordDefinition = {
compile(schema: { minimum?: number }) {
return (data) => {
compile(schema: { minimum?: number; blockNumberPath?: string }) {
return (data, parentSchema) => {
if (!Array.isArray(data)) {
return false;
}

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

if (data.length < minimum || data.length > roundValidators) {
Expand All @@ -82,6 +85,7 @@ export const makeKeywords = (
keyword: "limitToRoundValidators",
metaSchema: {
properties: {
blockNumberPath: { type: "string" },
minimum: { type: "integer" },
},
type: "object",
Expand All @@ -90,8 +94,8 @@ export const makeKeywords = (

// Used by: crypto-messages (prevotes / precommits) and crypto-proposal
const isValidatorIndex: FuncKeywordDefinition = {
compile() {
return (data) => {
compile(schema: { blockNumberPath?: string }) {
return (data, parentSchema) => {
if (!Number.isInteger(data)) {
return false;
}
Expand All @@ -100,13 +104,20 @@ export const makeKeywords = (
return false;
}

const { roundValidators } = configuration.getMilestone();
const blockNumber = parseBlockNumber(schema.blockNumberPath, parentSchema);
const { roundValidators } = configuration.getMilestone(blockNumber);

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

return { bignumber, buffer, isValidatorIndex, limitToRoundValidators, maxBytes };
Expand Down
Loading
Loading