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

Add asset root on block generation - Closes #6750 #6882

22 changes: 22 additions & 0 deletions elements/lisk-chain/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { codec } from '@liskhq/lisk-codec';
import { BlockAssets } from './block_assets';
import { BlockHeader } from './block_header';
import { MAX_ASSET_DATA_SIZE_BYTES } from './constants';
import { blockSchema } from './schema';
import { Transaction } from './transaction';

Expand Down Expand Up @@ -76,5 +77,26 @@ export class Block {
for (const tx of this.payload) {
tx.validate();
}

const assets = this.assets.getAll();
let last = assets[0];
let i = 0;
for (const asset of assets) {
// Data size of each module should not be greater than max asset data size
if (asset.data.byteLength > MAX_ASSET_DATA_SIZE_BYTES) {
throw new Error(
`Module with ID ${asset.moduleID} has data size more than ${MAX_ASSET_DATA_SIZE_BYTES} bytes.`,
);
}
if (last.moduleID > asset.moduleID) {
throw new Error('Assets are not sorted in the increasing values of moduleID.');
}
// Check for duplicates
if (i > 0 && asset.moduleID === last.moduleID) {
throw new Error(`Module with ID ${assets[i].moduleID} has duplicate entries.`);
}
i += 1;
last = asset;
}
}
}
19 changes: 19 additions & 0 deletions elements/lisk-chain/src/block_assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

import { codec } from '@liskhq/lisk-codec';
import { MerkleTree } from '@liskhq/lisk-tree';
import { blockAssetSchema } from './schema';

export interface BlockAsset {
Expand All @@ -22,6 +23,7 @@ export interface BlockAsset {

export class BlockAssets {
private readonly _assets: BlockAsset[] = [];
private _assetRoot!: Buffer;

public constructor(assets: BlockAsset[] = []) {
this._assets = assets;
Expand All @@ -38,6 +40,12 @@ export class BlockAssets {
return new BlockAssets(assets);
}

public async getRoot(): Promise<Buffer> {
this._assetRoot = await this._calculateRoot();

return this._assetRoot;
}

public getBytes(): Buffer[] {
return this._assets.map(asset => codec.encode(blockAssetSchema, asset));
}
Expand All @@ -46,6 +54,10 @@ export class BlockAssets {
return this._assets.find(a => a.moduleID === moduleID)?.data;
}

public getAll(): BlockAsset[] {
return [...this._assets];
}

public setAsset(moduleID: number, value: Buffer): void {
const asset = this.getAsset(moduleID);
if (asset) {
Expand All @@ -58,4 +70,11 @@ export class BlockAssets {
public sort(): void {
this._assets.sort((a1, a2) => a1.moduleID - a2.moduleID);
}

private async _calculateRoot(): Promise<Buffer> {
const merkleTree = new MerkleTree();
await merkleTree.init(this.getBytes());

return merkleTree.root;
}
}
3 changes: 3 additions & 0 deletions elements/lisk-chain/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ export const GENESIS_BLOCK_TRANSACTION_ROOT = EMPTY_HASH;

export const TAG_BLOCK_HEADER = createMessageTag('BH');
export const TAG_TRANSACTION = createMessageTag('TX');

// TODO: Actual size TBD
export const MAX_ASSET_DATA_SIZE_BYTES = 64;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think genesis block will exceed this value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what could be the safe value until we get the announced value?

144 changes: 142 additions & 2 deletions elements/lisk-chain/test/unit/block.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,41 @@
*
* Removal or modification of this copyright notice is prohibited.
*/
import { Block, Transaction } from '../../src';
import { getRandomBytes } from '@liskhq/lisk-cryptography';
import { Block, BlockAsset, BlockAssets, Transaction } from '../../src';
import { MAX_ASSET_DATA_SIZE_BYTES } from '../../src/constants';
import { createValidDefaultBlock } from '../utils/block';
import { getTransaction } from '../utils/transaction';

describe('block', () => {
describe('validate', () => {
let block: Block;
let tx: Transaction;
let assetList: BlockAsset[];
let blockAssets: BlockAssets;

beforeEach(() => {
assetList = [
{
moduleID: 6,
data: getRandomBytes(64),
},
{
moduleID: 3,
data: getRandomBytes(64),
},
];
blockAssets = new BlockAssets(assetList);
tx = getTransaction();
});

describe('when previousBlockID is empty', () => {
it('should throw error', async () => {
// Arrange
block = await createValidDefaultBlock({ header: { previousBlockID: Buffer.alloc(0) } });
block = await createValidDefaultBlock({
header: { previousBlockID: Buffer.alloc(0) },
assets: blockAssets,
});
// Act & assert
expect(() => block.validate()).toThrow('Previous block id must not be empty');
});
Expand All @@ -53,5 +71,127 @@ describe('block', () => {
expect(() => block.validate()).not.toThrow();
});
});

describe('when an asset data has size more than the limit', () => {
it(`should throw error when asset data length is greater than ${MAX_ASSET_DATA_SIZE_BYTES}`, async () => {
// Arrange
const assets = [
{
moduleID: 3,
data: getRandomBytes(64),
},
{
moduleID: 4,
data: getRandomBytes(128),
},
];
block = await createValidDefaultBlock({ assets: new BlockAssets(assets) });
// Act & assert
expect(() => block.validate()).toThrow(
`Module with ID ${assets[1].moduleID} has data size more than ${MAX_ASSET_DATA_SIZE_BYTES} bytes.`,
);
});

it(`should pass when asset data length is equal or less than ${MAX_ASSET_DATA_SIZE_BYTES}`, async () => {
// Arrange
const assets = [
{
moduleID: 3,
data: getRandomBytes(64),
},
{
moduleID: 4,
data: getRandomBytes(64),
},
];
block = await createValidDefaultBlock({ assets: new BlockAssets(assets) });
// Act & assert
expect(block.validate()).toBeUndefined();
});
});

describe('when the assets are not sorted by moduleID', () => {
it('should throw error when assets are not sorted by moduleID', async () => {
// Arrange
const assets = [
{
moduleID: 4,
data: getRandomBytes(64),
},
{
moduleID: 3,
data: getRandomBytes(64),
},
];
block = await createValidDefaultBlock({ assets: new BlockAssets(assets) });
// Act & assert
expect(() => block.validate()).toThrow(
'Assets are not sorted in the increasing values of moduleID.',
);
});

it('should pass when assets are sorted by moduleID', async () => {
// Arrange
const assets = [
{
moduleID: 2,
data: getRandomBytes(64),
},
{
moduleID: 3,
data: getRandomBytes(64),
},
];
block = await createValidDefaultBlock({ assets: new BlockAssets(assets) });
// Act & assert
expect(block.validate()).toBeUndefined();
});
});

describe('when there are multiple asset entries for a moduleID', () => {
it('should throw error when there are more than 1 assets for a module', async () => {
// Arrange
const assets = [
{
moduleID: 2,
data: getRandomBytes(64),
},
{
moduleID: 3,
data: getRandomBytes(64),
},
{
moduleID: 3,
data: getRandomBytes(64),
},
];
block = await createValidDefaultBlock({ assets: new BlockAssets(assets) });
// Act & assert
expect(() => block.validate()).toThrow(
`Module with ID ${assets[1].moduleID} has duplicate entries.`,
);
});

it('should pass when there is atmost 1 asset for a module', async () => {
// Arrange
const assets = [
{
moduleID: 2,
data: getRandomBytes(64),
},
{
moduleID: 3,
data: getRandomBytes(64),
},
{
moduleID: 4,
data: getRandomBytes(64),
},
];
block = await createValidDefaultBlock({ assets: new BlockAssets(assets) });
// Act & assert
expect(block.validate()).toBeUndefined();
});
});
});
});
24 changes: 21 additions & 3 deletions elements/lisk-chain/test/unit/block_assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
* Removal or modification of this copyright notice is prohibited.
*/
import { getRandomBytes } from '@liskhq/lisk-cryptography';
import { BlockAssets } from '../../src';
import { MerkleTree } from '@liskhq/lisk-tree';
import { BlockAsset, BlockAssets } from '../../src';

describe('block assets', () => {
let assets: BlockAssets;
let assetList: BlockAsset[];

beforeEach(() => {
assets = new BlockAssets([
assetList = [
{
moduleID: 6,
data: getRandomBytes(64),
Expand All @@ -27,7 +29,8 @@ describe('block assets', () => {
moduleID: 3,
data: getRandomBytes(64),
},
]);
];
assets = new BlockAssets(assetList);
});

describe('sort', () => {
Expand Down Expand Up @@ -72,4 +75,19 @@ describe('block assets', () => {
expect(assets.getAsset(4)).toBeInstanceOf(Buffer);
});
});

describe('getRoot', () => {
it('should calculate and return asset root', async () => {
const root = await assets.getRoot();
const merkleT = new MerkleTree();
await merkleT.init(assets.getBytes());
await expect(assets.getRoot()).resolves.toEqual(root);
});
});

describe('getAllAsset', () => {
it('should return list of all assets', () => {
expect(assets.getAll()).toContainAllValues(assetList);
});
});
});
15 changes: 15 additions & 0 deletions framework/src/node/consensus/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,15 @@ export class Consensus {

// verify Block signature
await this._verifyBlockSignature(apiContext, block);

// Verify validatorsHash
await this._verifyValidatorsHash(apiContext, block);

// Validate a block
block.validate();

// Check if moduleID is registered
this._validateBlockAsset(block);
}

private async _verifyTimestamp(apiContext: APIContext, block: Block): Promise<void> {
Expand Down Expand Up @@ -626,6 +633,14 @@ export class Consensus {
}
}

private _validateBlockAsset(block: Block): void {
for (const asset of block.assets.getAll()) {
if (!this._stateMachine.getAllModuleIDs().includes(asset.moduleID)) {
throw new Error(`Module with ID: ${asset.moduleID} is not registered.`);
}
}
}

private async _deleteBlock(block: Block, saveTempBlock = false): Promise<void> {
if (block.header.height <= this._chain.finalizedHeight) {
throw new Error('Can not delete block below or same as finalized height');
Expand Down
3 changes: 1 addition & 2 deletions framework/src/node/generator/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,7 @@ export class Generator {
await txTree.init(transactions.map(tx => tx.id));
const transactionRoot = txTree.root;
blockHeader.transactionRoot = transactionRoot;
// TODO: Update the assetsRoot with proper calculation
blockHeader.assetsRoot = hash(Buffer.concat(blockAssets.getBytes()));
blockHeader.assetsRoot = await blockAssets.getRoot();
// TODO: Update the stateRoot with proper calculation
blockHeader.stateRoot = hash(Buffer.alloc(0));
// Set validatorsHash
Expand Down
9 changes: 9 additions & 0 deletions framework/src/node/state_machine/state_machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,24 @@ export interface StateMachineModule {
export class StateMachine {
private readonly _modules: StateMachineModule[] = [];
private readonly _systemModules: StateMachineModule[] = [];
private readonly _moduleIDs: number[] = [];

public registerModule(mod: StateMachineModule): void {
this._validateExistingModuleID(mod.id);
this._modules.push(mod);
this._moduleIDs.push(mod.id);
this._moduleIDs.sort((a, b) => a - b);
}

public registerSystemModule(mod: StateMachineModule): void {
this._validateExistingModuleID(mod.id);
this._systemModules.push(mod);
this._moduleIDs.push(mod.id);
this._moduleIDs.sort((a, b) => a - b);
}

public getAllModuleIDs() {
return this._moduleIDs;
}

public async executeGenesisBlock(ctx: GenesisBlockContext): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions framework/test/fixtures/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const createFakeBlockHeader = (header?: Partial<BlockHeaderAttrs>): Block
* Calculates the signature, transactionRoot etc. internally. Facilitating the creation of block with valid signature and other properties
*/
export const createValidDefaultBlock = async (
block?: { header?: Partial<BlockHeaderAttrs>; payload?: Transaction[] },
block?: { header?: Partial<BlockHeaderAttrs>; payload?: Transaction[]; assets?: BlockAssets },
networkIdentifier: Buffer = defaultNetworkIdentifier,
): Promise<Block> => {
const keypair = getKeyPair();
Expand Down Expand Up @@ -115,5 +115,5 @@ export const createValidDefaultBlock = async (
// Assigning the id ahead
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
blockHeader.id;
return new Block(blockHeader, payload, new BlockAssets());
return new Block(blockHeader, payload, block?.assets ?? new BlockAssets());
};
Loading