@kyve/core-beta
Version:
🚀 The base KYVE node implementation.
262 lines (221 loc) • 8.45 kB
text/typescript
import { VoteType } from "@kyve/proto-beta/client/kyve/bundles/v1beta1/tx";
import { Node } from "../..";
import { sha256, standardizeJSON, VOTE } from "../../utils";
/**
* validateBundleProposal validates a proposed bundle proposal
* by first downloading the proposed data bundle from the storage
* provider and then comparing it with a locally created validation
* bundle. Furthermore, custom validation from the runtime is applied
* at the end.
*
* @method validateBundleProposal
* @param {Node} this
* @param {number} updatedAt
* @return {Promise<void>}
*/
export async function validateBundleProposal(
this: Node,
updatedAt: number
): Promise<void> {
try {
this.logger.info(
`Validating bundle proposal = ${this.pool.bundle_proposal!.storage_id}`
);
// retrieve the data of the bundle proposal in a save way
// by retrying the retrieval if it fails
const storageProviderResult = await this.saveBundleDownload(updatedAt);
// if no bundle got returned it means that the pool is not active anymore
// or a new bundle proposal round has started
if (storageProviderResult === null) {
return;
}
// vote invalid if data size does not match with proposed data size
this.logger.debug(`Validating bundle proposal by data size`);
this.logger.debug(`Proposed = ${this.pool.bundle_proposal!.data_size}`);
this.logger.debug(`Actual = ${storageProviderResult.byteLength}`);
if (
parseInt(this.pool.bundle_proposal!.data_size) !==
storageProviderResult.byteLength
) {
this.logger.info(
`Found different byte size on bundle downloaded from storage provider`
);
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
VOTE.INVALID
);
return;
}
this.logger.info(
`Found matching data size = ${this.pool.bundle_proposal!.data_size} Bytes`
);
// vote invalid if data hash does not match with proposed data hash
this.logger.debug(`Validating bundle proposal by data hash`);
this.logger.debug(`Proposed = ${this.pool.bundle_proposal!.data_hash}`);
this.logger.debug(`Actual = ${sha256(storageProviderResult)}`);
if (
this.pool.bundle_proposal!.data_hash !== sha256(storageProviderResult)
) {
this.logger.info(
`Found different hash on bundle downloaded from storage provider`
);
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
VOTE.INVALID
);
return;
}
this.logger.info(
`Found matching data hash = ${this.pool.bundle_proposal!.data_hash}`
);
const validationBundle = await this.saveLoadValidationBundle(updatedAt);
// if no bundle got returned it means that the pool is not active anymore
// or a new bundle proposal round has started
if (validationBundle === null) {
return;
}
// vote invalid if bundle key does not match with proposed from key
this.logger.debug(`Validating bundle proposal by bundle from_key`);
this.logger.debug(`Proposed = ${this.pool.bundle_proposal!.from_key}`);
this.logger.debug(`Actual = ${validationBundle.at(0)?.key}`);
if (this.pool.bundle_proposal!.from_key !== validationBundle.at(0)?.key) {
this.logger.info(`Found different value on proposed bundle from_key`);
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
VOTE.INVALID
);
return;
}
this.logger.info(
`Found matching from key = ${this.pool.bundle_proposal!.from_key}`
);
// vote invalid if bundle key does not match with proposed to key
this.logger.debug(`Validating bundle proposal by bundle to_key`);
this.logger.debug(`Proposed = ${this.pool.bundle_proposal!.to_key}`);
this.logger.debug(`Actual = ${validationBundle.at(-1)?.key}`);
if (this.pool.bundle_proposal!.to_key !== validationBundle.at(-1)?.key) {
this.logger.info(`Found different value on proposed bundle to_key`);
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
VOTE.INVALID
);
return;
}
this.logger.info(
`Found matching to key = ${this.pool.bundle_proposal!.to_key}`
);
// vote invalid if bundle summary does not match with proposed summary
this.logger.debug(`Validating bundle proposal by bundle summary`);
this.logger.debug(`this.runtime.summarizeDataBundle($VALIDATION_BUNDLE)`);
const bundleSummary = await this.runtime
.summarizeDataBundle(this, validationBundle)
.catch((err) => {
this.logger.error(
`Unexpected error summarizing bundle with runtime. Voting abstain ...`
);
this.logger.error(standardizeJSON(err));
return null;
});
// vote abstain if bundleSummary is null
if (bundleSummary === null) {
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
VOTE.ABSTAIN
);
return;
}
this.logger.debug(
`Proposed = ${this.pool.bundle_proposal!.bundle_summary}`
);
this.logger.debug(`Actual = ${bundleSummary}`);
if (this.pool.bundle_proposal!.bundle_summary !== bundleSummary) {
this.logger.info(`Found different value on proposed bundle summary`);
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
VOTE.INVALID
);
return;
}
this.logger.info(
`Found matching bundle summary = ${
this.pool.bundle_proposal!.bundle_summary
}`
);
// if storage provider result is empty skip runtime validation
if (storageProviderResult.byteLength) {
// decompress the bundle with the specified compression type
// and convert the bytes into a JSON format
const proposedBundle = await this.saveBundleDecompress(
storageProviderResult
);
try {
// perform custom runtime bundle validation
this.logger.debug(
`Validating bundle proposal by custom runtime validation`
);
// validate if bundle size matches
let valid = proposedBundle.length === validationBundle.length;
// validate each data item in bundle with custom runtime validation
for (let i = 0; i < proposedBundle.length; i++) {
if (valid) {
this.logger.debug(
`this.runtime.validateDataItem($THIS, $PROPOSED_DATA_ITEM, $VALIDATION_DATA_ITEM)`
);
valid = await this.runtime.validateDataItem(
this,
proposedBundle[i],
validationBundle[i]
);
this.logger.debug(
`Validated data item: index:${i} - key:${proposedBundle[i].key} - result:${valid}`
);
} else {
// abort further validation if an invalid data item was
// found in bundle
break;
}
}
this.logger.debug(
`Finished validating bundle by custom runtime validation. Result = ${valid}`
);
// vote with either valid or invalid
const vote = valid
? VoteType.VOTE_TYPE_VALID
: VoteType.VOTE_TYPE_INVALID;
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
vote
);
} catch (err) {
this.logger.error(
`Unexpected error validating data items with runtime. Voting abstain ...`
);
this.logger.error(standardizeJSON(err));
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
VoteType.VOTE_TYPE_ABSTAIN
);
}
// update metrics
this.m.bundles_amount.inc();
this.m.bundles_data_items.set(proposedBundle.length);
this.m.bundles_byte_size.set(storageProviderResult.byteLength);
} else {
await this.voteBundleProposal(
this.pool.bundle_proposal!.storage_id,
VoteType.VOTE_TYPE_VALID
);
// update metrics
this.m.bundles_amount.inc();
this.m.bundles_data_items.set(
parseInt(this.pool.bundle_proposal!.bundle_size)
);
this.m.bundles_byte_size.set(storageProviderResult.byteLength);
}
} catch (err) {
this.logger.error(
`Unexpected error validating bundle proposal. Skipping validation ...`
);
this.logger.error(standardizeJSON(err));
}
}