@kyve/core-beta
Version:
🚀 The base KYVE node implementation.
268 lines (235 loc) • 8.34 kB
text/typescript
import { Node } from "../..";
import { BundleTag, DataItem } from "../../types";
import { bundleToBytes, sha256, standardizeJSON } from "../../utils";
/**
* createBundleProposal assembles a bundle proposal by loading
* data from the local cache and uploading it to a storage provider.
* After the data was successfully saved the node submits the bundle
* proposal with the storage id and other information to the network
* so that other participants can validate and vote on it.
*
* If one of the steps fails the node should skip it's uploader role
* to prevent slashes.
*
* @method createBundleProposal
* @param {Node} this
* @return {Promise<void>}
*/
export async function createBundleProposal(this: Node): Promise<void> {
try {
this.logger.info(
`Creating a new bundle proposal for the next bundle proposal round`
);
// create bundle proposal from the current bundle proposal index
const fromIndex =
parseInt(this.pool.data!.current_index) +
parseInt(this.pool.bundle_proposal!.bundle_size);
// create the bundle proposal from the determined bundle start index
// and index all the way until the maximum bundle size is reached
const toIndex = fromIndex + parseInt(this.pool.data!.max_bundle_size);
// load bundle proposal from local cache
const bundleProposal: DataItem[] = [];
// here we try to fetch data items from the current index
// to the proposal index. If we fail before we simply
// abort and and submit the data collected we have available
// right now
this.logger.debug(
`Loading bundle from index ${fromIndex} to index ${toIndex}`
);
for (let i = fromIndex; i < toIndex; i++) {
try {
// try to get the data item from local cache
this.logger.debug(`this.cacheProvider.get(${i.toString()})`);
const item = await this.cacheProvider.get(i.toString());
bundleProposal.push(item);
} catch {
// if the data item was not found simply abort
// and submit what we just have now
break;
}
}
// if no data was found on the cache skip the uploader role
// so that this node does not receive an upload slash
if (!bundleProposal.length) {
this.logger.info(`No data was found on local cache from required range`);
await this.skipUploaderRole(fromIndex);
return;
}
this.logger.info(`Data was found on local cache from required range`);
// get the first key of the bundle proposal which gets
// included in the bundle proposal and saved on chain
// as from_key
const fromKey = bundleProposal.at(0)?.key ?? "";
// get the last key of the bundle proposal which gets
// included in the bundle proposal and saved on chain
// as to_key
const toKey = bundleProposal.at(-1)?.key ?? "";
// get the last value of the bundle proposal and format
// it so it can be included in the bundle proposal and
// saved on chain
this.logger.debug(`this.runtime.summarizeDataBundle($BUNDLE_PROPOSAL)`);
const bundleSummary = await this.runtime
.summarizeDataBundle(this, bundleProposal)
.catch((err) => {
this.logger.error(
`Unexpected error summarizing bundle. Skipping Uploader Role ...`
);
this.logger.error(standardizeJSON(err));
return null;
});
// skip uploader role if bundleSummary is null
if (bundleSummary === null) {
await this.skipUploaderRole(fromIndex);
return;
}
// get current compression defined on pool
this.logger.debug(
`compressionFactory(${this.pool.data?.current_compression_id ?? 0})`
);
const compression = this.compressionFactory(
this.pool.data?.current_compression_id ?? 0
);
// if data was found on the cache proceed with compressing the
// bundle for the upload to the storage provider
this.logger.debug(`this.compression.compress($RAW_BUNDLE_PROPOSAL)`);
const storageProviderData = await compression
.compress(bundleToBytes(bundleProposal))
.catch((err) => {
this.logger.error(
`Unexpected error compressing bundle. Skipping Uploader Role ...`
);
this.logger.error(standardizeJSON(err));
return null;
});
// skip uploader role if compression returns null
if (storageProviderData === null) {
await this.skipUploaderRole(fromIndex);
return;
}
this.logger.info(
`Successfully compressed bundle with Compression:${compression.name}`
);
// create tags for bundle to make it easier to find KYVE data
// on the storage provider itself
const tags: BundleTag[] = [
{
name: "Content-Type",
value: compression.mimeType,
},
{
name: "Application",
value: "KYVE",
},
{
name: "ChainId",
value: await this.client.nativeClient.getChainId(),
},
{
name: "@kyve/core",
value: "KYVE",
},
{
name: this.runtime.name,
value: this.runtime.version,
},
{
name: "Pool",
value: this.poolId.toString(),
},
{
name: "Uploader",
value: this.client.account.address,
},
{
name: "FromIndex",
value: toIndex.toString(),
},
{
name: "ToIndex",
value: (toIndex + bundleProposal.length).toString(),
},
{
name: "BundleSize",
value: bundleProposal.length.toString(),
},
{
name: "FromKey",
value: fromKey,
},
{
name: "ToKey",
value: toKey,
},
{
name: "BundleSummary",
value: bundleSummary,
},
];
// try to upload the bundle proposal to the storage provider
// if the upload fails the node should immediately skip the
// uploader role to prevent upload slashes
try {
// get current storage provider defined on pool
this.logger.debug(
`storageProviderFactory(${
this.pool.data?.current_storage_provider_id ?? 0
}, $STORAGE_PRIV)`
);
const storageProvider = await this.storageProviderFactory(
this.pool.data?.current_storage_provider_id ?? 0
);
// upload the bundle proposal to the storage provider
// and get a storage id. With that other participants in the
// network can retrieve the data again and validate it
this.logger.debug(
`this.storageProvider.saveBundle($STORAGE_PROVIDER_DATA,$TAGS)`
);
const { storageId, storageData } = await storageProvider.saveBundle(
storageProviderData,
tags
);
// throw error if storage provider returns an empty storage id
if (!storageId) {
throw new Error("Storage Provider returned empty storageId");
}
// hash the raw data which gets uploaded to the storage provider
// with sha256
const dataSize = storageData.byteLength;
// hash the raw data which gets uploaded to the storage provider
// with sha256
const dataHash = sha256(storageData);
this.m.storage_provider_save_successful.inc();
this.logger.info(
`Successfully saved bundle on StorageProvider:${storageProvider.name}`
);
// if the bundle was successfully uploaded to the storage provider
// the node can finally submit the actual bundle proposal to
// the network
await this.submitBundleProposal(
storageId,
dataSize,
dataHash,
fromIndex,
bundleProposal.length,
fromKey,
toKey,
bundleSummary
);
this.logger.info(`Successfully submitted BundleProposal:${storageId}`);
} catch (err) {
this.logger.info(
`Saving bundle proposal on StorageProvider was unsucessful`
);
this.logger.debug(standardizeJSON(err));
this.m.storage_provider_save_failed.inc();
// if the bundle fails to the uploaded to the storage provider
// let the node skip the uploader role and continue
await this.skipUploaderRole(fromIndex);
}
} catch (err) {
this.logger.error(
`Unexpected error creating bundle proposal. Skipping proposal ...`
);
this.logger.error(standardizeJSON(err));
}
}