Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Fix verifyValidatorsUpdate and mainchain registration logic #9042

Merged
merged 2 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
MODULE_NAME_INTEROPERABILITY,
EMPTY_HASH,
MAX_NUM_VALIDATORS,
MAX_UINT64,
} from './constants';
import { ccmSchema } from './schemas';
import { CCMsg, CrossChainUpdateTransactionParams, ChainAccount, ChainValidators } from './types';
Expand Down Expand Up @@ -369,9 +370,28 @@ export abstract class BaseInteroperabilityInternalMethod extends BaseInternalMet
);
}

let totalWeight = BigInt(0);
for (const currentValidator of newActiveValidators) {
if (currentValidator.bftWeight === BigInt(0)) {
throw new Error('Validator bft weight must be positive integer.');
}
totalWeight += currentValidator.bftWeight;
if (totalWeight > MAX_UINT64) {
throw new Error('Total BFT weight exceeds maximum value.');
}
}
const certificate = codec.decode<Certificate>(certificateSchema, ccu.certificate);
validator.validate(certificateSchema, certificate);

const { certificateThreshold } = ccu;

if (certificateThreshold < totalWeight / BigInt(3) + BigInt(1)) {
throw new Error('Certificate threshold is too small.');
}
if (certificateThreshold > totalWeight) {
throw new Error('Certificate threshold is too large.');
}

const newValidatorsHash = computeValidatorsHash(newActiveValidators, ccu.certificateThreshold);
if (!certificate.validatorsHash.equals(newValidatorsHash)) {
throw new Error('ValidatorsHash in certificate and the computed values do not match.');
Expand Down
2 changes: 1 addition & 1 deletion framework/src/modules/interoperability/certificates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* TODO: Now that we have `certificates.ts`, these methods could be moved there
* (checkCertificateTimestamp, checkCertificateValidity, checkValidatorsHashWithCertificate,
* (checkCertificateTimestamp,
* isCertificateEmpty, verifyCertificateSignature)
*/
import { LastCertificate, LastCertificateJSON } from './types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class RegisterMainchainCommand extends BaseInteroperabilityCommand<Sidech
};
}

if (currentValidator.bftWeight <= 0) {
if (currentValidator.bftWeight === BigInt(0)) {
return {
status: VerifyStatus.FAIL,
error: new Error('Validator bft weight must be positive integer.'),
Expand Down
90 changes: 1 addition & 89 deletions framework/src/modules/interoperability/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
CCMsg,
ChainAccount,
CrossChainUpdateTransactionParams,
ChainValidators,
InboxUpdate,
OutboxRootWitness,
ActiveValidatorsUpdate,
Expand Down Expand Up @@ -119,7 +118,7 @@ export const isInboxUpdateEmpty = (inboxUpdate: InboxUpdate) =>
export const isOutboxRootWitnessEmpty = (outboxRootWitness: OutboxRootWitness) =>
outboxRootWitness.siblingHashes.length === 0 || outboxRootWitness.bitmap.length === 0;

export const checkLivenessRequirementFirstCCU = (
export const checkLivenessRequirement = (
partnerChainAccount: ChainAccount,
txParams: CrossChainUpdateTransactionParams,
): VerificationResult => {
Expand All @@ -142,39 +141,6 @@ export const checkLivenessRequirementFirstCCU = (
};
};

export const checkCertificateValidity = (
partnerChainAccount: ChainAccount,
encodedCertificate: Buffer,
): VerificationResult => {
if (encodedCertificate.equals(EMPTY_BYTES)) {
return {
status: VerifyStatus.OK,
};
}

const certificate = codec.decode<Certificate>(certificateSchema, encodedCertificate);
try {
validator.validate(certificateSchema, certificate);
} catch (err) {
return {
status: VerifyStatus.FAIL,
error: new Error('Certificate is missing required values.'),
};
}

// Last certificate height should be less than new certificate height
if (partnerChainAccount.lastCertificate.height >= certificate.height) {
return {
status: VerifyStatus.FAIL,
error: new Error('Certificate height should be greater than last certificate height.'),
};
}

return {
status: VerifyStatus.OK,
};
};

export const checkCertificateTimestamp = (
txParams: CrossChainUpdateTransactionParams,
certificate: Certificate,
Expand All @@ -190,60 +156,6 @@ export const checkCertificateTimestamp = (
}
};

export const checkValidatorsHashWithCertificate = (
txParams: CrossChainUpdateTransactionParams,
partnerValidators: ChainValidators,
): VerificationResult => {
if (
!emptyActiveValidatorsUpdate(txParams.activeValidatorsUpdate) ||
txParams.certificateThreshold > BigInt(0)
) {
if (txParams.certificate.equals(EMPTY_BYTES)) {
return {
status: VerifyStatus.FAIL,
error: new Error(
'Certificate cannot be empty when activeValidatorsUpdate or certificateThreshold has a non-empty value.',
),
};
}
let certificate: Certificate;
try {
certificate = codec.decode<Certificate>(certificateSchema, txParams.certificate);
validator.validate(certificateSchema, certificate);
} catch (error) {
return {
status: VerifyStatus.FAIL,
error: new Error(
'Certificate should have all required values when activeValidatorsUpdate or certificateThreshold has a non-empty value.',
),
};
}

const newActiveValidators = calculateNewActiveValidators(
partnerValidators.activeValidators,
txParams.activeValidatorsUpdate.blsKeysUpdate,
txParams.activeValidatorsUpdate.bftWeightsUpdate,
txParams.activeValidatorsUpdate.bftWeightsUpdateBitmap,
);

const validatorsHash = computeValidatorsHash(
newActiveValidators,
txParams.certificateThreshold || partnerValidators.certificateThreshold,
);

if (!certificate.validatorsHash.equals(validatorsHash)) {
return {
status: VerifyStatus.FAIL,
error: new Error('Validators hash given in the certificate is incorrect.'),
};
}
}

return {
status: VerifyStatus.OK,
};
};

export const chainAccountToJSON = (chainAccount: ChainAccount) => {
const { lastCertificate, name, status } = chainAccount;

Expand Down
131 changes: 130 additions & 1 deletion framework/test/unit/modules/interoperability/internal_method.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
EMPTY_BYTES,
EMPTY_HASH,
HASH_LENGTH,
MAX_UINT64,
MESSAGE_TAG_CERTIFICATE,
MIN_RETURN_FEE_PER_BYTE_BEDDOWS,
} from '../../../../src/modules/interoperability/constants';
Expand Down Expand Up @@ -149,7 +150,7 @@ describe('Base interoperability internal method', () => {
siblingHashes: [],
},
},
certificateThreshold: BigInt(99),
certificateThreshold: BigInt(9),
sendingChainID: cryptoUtils.getRandomBytes(4),
};
let mainchainInteroperabilityInternalMethod: MainchainInteroperabilityInternalMethod;
Expand Down Expand Up @@ -926,6 +927,134 @@ describe('Base interoperability internal method', () => {
).rejects.toThrow('New validators must have a positive BFT weight.');
});

it('should reject if new active validator bft weight equals 0', async () => {
const ccu = {
...ccuParams,
certificate: codec.encode(certificateSchema, defaultCertificate),
activeValidatorsUpdate: {
blsKeysUpdate: [
Buffer.from([0, 0, 0, 0]),
Buffer.from([0, 0, 0, 1]),
Buffer.from([0, 0, 3, 0]),
],
bftWeightsUpdate: [BigInt(1), BigInt(3), BigInt(4)],
// 7 corresponds to 0111
bftWeightsUpdateBitmap: Buffer.from([7]),
},
};
const existingKey = Buffer.from([0, 2, 3, 0]);
await chainValidatorsSubstore.set(methodContext, ccu.sendingChainID, {
activeValidators: [{ blsKey: existingKey, bftWeight: BigInt(2) }],
certificateThreshold: BigInt(1),
});
const newValidators = [
{ blsKey: Buffer.from([0, 0, 0, 0]), bftWeight: BigInt(1) },
{ blsKey: Buffer.from([0, 0, 0, 1]), bftWeight: BigInt(3) },
{ blsKey: Buffer.from([0, 0, 2, 0]), bftWeight: BigInt(0) },
];
jest.spyOn(utils, 'calculateNewActiveValidators').mockReturnValue(newValidators);

await expect(
mainchainInteroperabilityInternalMethod.verifyValidatorsUpdate(methodContext, ccu),
).rejects.toThrow('Validator bft weight must be positive integer.');
});

it(`should reject if total bft weight > ${MAX_UINT64}`, async () => {
const ccu = {
...ccuParams,
certificate: codec.encode(certificateSchema, defaultCertificate),
activeValidatorsUpdate: {
blsKeysUpdate: [
Buffer.from([0, 0, 0, 0]),
Buffer.from([0, 0, 0, 1]),
Buffer.from([0, 0, 3, 0]),
],
bftWeightsUpdate: [BigInt(1), BigInt(3), BigInt(4)],
// 7 corresponds to 0111
bftWeightsUpdateBitmap: Buffer.from([7]),
},
};
const existingKey = Buffer.from([0, 2, 3, 0]);
await chainValidatorsSubstore.set(methodContext, ccu.sendingChainID, {
activeValidators: [{ blsKey: existingKey, bftWeight: BigInt(2) }],
certificateThreshold: BigInt(1),
});
const newValidators = [
{ blsKey: Buffer.from([0, 0, 0, 0]), bftWeight: BigInt(1) },
{ blsKey: Buffer.from([0, 0, 0, 1]), bftWeight: BigInt(3) },
{ blsKey: Buffer.from([0, 0, 2, 0]), bftWeight: MAX_UINT64 },
];
jest.spyOn(utils, 'calculateNewActiveValidators').mockReturnValue(newValidators);

await expect(
mainchainInteroperabilityInternalMethod.verifyValidatorsUpdate(methodContext, ccu),
).rejects.toThrow('Total BFT weight exceeds maximum value.');
});

it('should reject if certificate threshold is too small', async () => {
const ccu = {
...ccuParams,
certificate: codec.encode(certificateSchema, defaultCertificate),
activeValidatorsUpdate: {
blsKeysUpdate: [
Buffer.from([0, 0, 0, 0]),
Buffer.from([0, 0, 0, 1]),
Buffer.from([0, 0, 3, 0]),
],
bftWeightsUpdate: [BigInt(1), BigInt(3), BigInt(4)],
// 7 corresponds to 0111
bftWeightsUpdateBitmap: Buffer.from([7]),
},
};
const existingKey = Buffer.from([0, 2, 3, 0]);
await chainValidatorsSubstore.set(methodContext, ccu.sendingChainID, {
activeValidators: [{ blsKey: existingKey, bftWeight: BigInt(2) }],
certificateThreshold: BigInt(1),
});
const newValidators = [
{ blsKey: Buffer.from([0, 0, 0, 0]), bftWeight: BigInt(1000000000000) },
{ blsKey: Buffer.from([0, 0, 0, 1]), bftWeight: BigInt(1000000000000) },
{ blsKey: Buffer.from([0, 0, 2, 0]), bftWeight: BigInt(1000000000000) },
];
jest.spyOn(utils, 'calculateNewActiveValidators').mockReturnValue(newValidators);

await expect(
mainchainInteroperabilityInternalMethod.verifyValidatorsUpdate(methodContext, ccu),
).rejects.toThrow('Certificate threshold is too small.');
});

it('should reject if certificate threshold is too large', async () => {
const ccu = {
...ccuParams,
certificate: codec.encode(certificateSchema, defaultCertificate),
activeValidatorsUpdate: {
blsKeysUpdate: [
Buffer.from([0, 0, 0, 0]),
Buffer.from([0, 0, 0, 1]),
Buffer.from([0, 0, 3, 0]),
],
bftWeightsUpdate: [BigInt(1), BigInt(3), BigInt(4)],
// 7 corresponds to 0111
bftWeightsUpdateBitmap: Buffer.from([7]),
},
};
const existingKey = Buffer.from([0, 2, 3, 0]);
await chainValidatorsSubstore.set(methodContext, ccu.sendingChainID, {
activeValidators: [{ blsKey: existingKey, bftWeight: BigInt(2) }],
certificateThreshold: BigInt(1),
});
const newValidators = [
{ blsKey: Buffer.from([0, 0, 0, 0]), bftWeight: BigInt(1) },
{ blsKey: Buffer.from([0, 0, 0, 1]), bftWeight: BigInt(1) },
{ blsKey: Buffer.from([0, 0, 2, 0]), bftWeight: BigInt(1) },
];
jest.spyOn(utils, 'calculateNewActiveValidators').mockReturnValue(newValidators);

await expect(
mainchainInteroperabilityInternalMethod.verifyValidatorsUpdate(methodContext, ccu),
).rejects.toThrow('Certificate threshold is too large.');
});

it('should reject if new validatorsHash does not match with certificate', async () => {
const ccu = {
...ccuParams,
Expand Down
Loading