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

Commit

Permalink
Merge pull request #7145 from LiskHQ/7119-update_token_genesis
Browse files Browse the repository at this point in the history
Update token module initGenesisState hook - Closes #7119
  • Loading branch information
shuse2 authored May 6, 2022
2 parents 99778a3 + f4289fb commit 5f4016b
Show file tree
Hide file tree
Showing 7 changed files with 708 additions and 84 deletions.
3 changes: 3 additions & 0 deletions framework/src/modules/token/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const STORE_PREFIX_USER = 0x0000;
export const STORE_PREFIX_SUPPLY = 0x8000;
export const STORE_PREFIX_ESCROW = 0xc000;
export const STORE_PREFIX_AVAILABLE_LOCAL_ID = 0xd000;
export const STORE_PREFIX_TERMINATED_ESCROW = 0xe000;

export const CCM_STATUS_OK = 0;
export const CCM_STATUS_TOKEN_NOT_SUPPORTED = 64;
Expand All @@ -48,3 +49,5 @@ export const defaultConfig = {
],
supportedTokenIDs: [],
};

export const EMPTY_BYTES = Buffer.alloc(0);
247 changes: 237 additions & 10 deletions framework/src/modules/token/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,41 @@
*
* Removal or modification of this copyright notice is prohibited.
*/
import { LiskValidationError, validator } from '@liskhq/lisk-validator';
import { isUInt64, LiskValidationError, validator } from '@liskhq/lisk-validator';
import { codec } from '@liskhq/lisk-codec';
import { objects } from '@liskhq/lisk-utils';
import { objects, dataStructures } from '@liskhq/lisk-utils';
import {
CHAIN_ID_ALIAS_NATIVE,
defaultConfig,
EMPTY_BYTES,
LOCAL_ID_LENGTH,
MODULE_ID_TOKEN,
STORE_PREFIX_AVAILABLE_LOCAL_ID,
STORE_PREFIX_ESCROW,
STORE_PREFIX_SUPPLY,
STORE_PREFIX_TERMINATED_ESCROW,
STORE_PREFIX_USER,
} from './constants';
import { TransferCommand } from './commands/transfer';
import { BaseModule, ModuleInitArgs } from '../base_module';
import { GenesisBlockExecuteContext } from '../../node/state_machine';
import {
AvailableLocalIDStoreData,
availableLocalIDStoreSchema,
configSchema,
EscrowStoreData,
escrowStoreSchema,
genesisTokenStoreSchema,
SupplyStoreData,
supplyStoreSchema,
terminatedEscrowStoreSchema,
UserStoreData,
userStoreSchema,
} from './schemas';
import { TokenAPI } from './api';
import { TokenEndpoint } from './endpoint';
import { GenesisTokenStore, InteroperabilityAPI, MinBalance, ModuleConfig } from './types';
import { getUserStoreKey } from './utils';
import { getUserStoreKey, splitTokenID } from './utils';

export class TokenModule extends BaseModule {
public name = 'token';
Expand Down Expand Up @@ -75,21 +88,235 @@ export class TokenModule extends BaseModule {
return;
}
const genesisStore = codec.decode<GenesisTokenStore>(genesisTokenStoreSchema, assetBytes);
const errors = validator.validate(genesisTokenStoreSchema, genesisStore);
if (errors.length) {
throw new LiskValidationError(errors);
}

const userStore = context.getStore(this.id, STORE_PREFIX_USER);
for (const userData of genesisStore.userSubstore) {
await userStore.setWithSchema(
getUserStoreKey(userData.address, userData.tokenID),
userData,
userStoreSchema,
);
const copiedUserStore = [...genesisStore.userSubstore];
copiedUserStore.sort((a, b) => {
if (!a.address.equals(b.address)) {
return a.address.compare(b.address);
}
return a.tokenID.compare(b.tokenID);
});
const userKeySet = new dataStructures.BufferSet();
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < genesisStore.userSubstore.length; i += 1) {
const userData = genesisStore.userSubstore[i];
const key = getUserStoreKey(userData.address, userData.tokenID);
// Validate uniqueness of address/tokenID pair
if (userKeySet.has(key)) {
throw new Error(
`Address ${userData.address.toString('hex')} and tokenID ${userData.tokenID.toString(
'hex',
)} pair is duplicated.`,
);
}
userKeySet.add(key);

// Validate sorting of userSubstore
if (
!userData.address.equals(copiedUserStore[i].address) ||
!userData.tokenID.equals(copiedUserStore[i].tokenID)
) {
throw new Error('UserSubstore must be sorted by address and tokenID.');
}

const lockedBalanceModuleIDSet = new Set();
let lastModuleID = -1;
for (const lockedBalance of userData.lockedBalances) {
lockedBalanceModuleIDSet.add(lockedBalance.moduleID);
// Validate locked balances must not be zero
if (lockedBalance.amount === BigInt(0)) {
throw new Error(
`Address ${userData.address.toString('hex')} contains 0 amount locked balance.`,
);
}
// Validate locked balances must be sorted
if (lockedBalance.moduleID < lastModuleID) {
throw new Error('Locked balances must be sorted by moduleID.');
}
lastModuleID = lockedBalance.moduleID;
}
// Validate locked balance module ID uniqueness
if (lockedBalanceModuleIDSet.size !== userData.lockedBalances.length) {
throw new Error(
`Address ${userData.address.toString('hex')} has duplicate moduleID in locked balances.`,
);
}
// Validate userSubstore not to be empty
if (userData.lockedBalances.length === 0 && userData.availableBalance === BigInt(0)) {
throw new Error(`Address ${userData.address.toString('hex')} has empty data.`);
}

await userStore.setWithSchema(key, userData, userStoreSchema);
}

const copiedSupplyStore = [...genesisStore.supplySubstore];
copiedSupplyStore.sort((a, b) => a.localID.compare(b.localID));

const supplyStoreKeySet = new dataStructures.BufferSet();
const supplyStore = context.getStore(this.id, STORE_PREFIX_SUPPLY);
for (const supplyData of genesisStore.supplySubstore) {
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < genesisStore.supplySubstore.length; i += 1) {
const supplyData = genesisStore.supplySubstore[i];
if (supplyStoreKeySet.has(supplyData.localID)) {
throw new Error(
`Supply store local ID ${supplyData.localID.toString('hex')} is duplicated.`,
);
}
supplyStoreKeySet.add(supplyData.localID);
// Validate sorting of userSubstore
if (!supplyData.localID.equals(copiedSupplyStore[i].localID)) {
throw new Error('SupplySubstore must be sorted by localID.');
}

await supplyStore.setWithSchema(
supplyData.localID,
{ totalSupply: supplyData.totalSupply },
supplyStoreSchema,
);
}

const copiedEscrowStore = [...genesisStore.escrowSubstore];
copiedEscrowStore.sort((a, b) => {
if (!a.escrowChainID.equals(b.escrowChainID)) {
return a.escrowChainID.compare(b.escrowChainID);
}
return a.localID.compare(b.localID);
});

const escrowStore = context.getStore(this.id, STORE_PREFIX_ESCROW);
const escrowKeySet = new dataStructures.BufferSet();
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < genesisStore.escrowSubstore.length; i += 1) {
const escrowData = genesisStore.escrowSubstore[i];
// validate terminated escrow chain ID/local ID uniqueness
const key = Buffer.concat([escrowData.escrowChainID, escrowData.localID]);
if (escrowKeySet.has(key)) {
throw new Error(
`Escrow store escrowChainID ${escrowData.escrowChainID.toString(
'hex',
)} and localID ${escrowData.localID.toString('hex')} pair is duplicated.`,
);
}
escrowKeySet.add(key);
// validate terminated escrow chain ID/local ID order
if (
!escrowData.escrowChainID.equals(copiedEscrowStore[i].escrowChainID) ||
!escrowData.localID.equals(copiedEscrowStore[i].localID)
) {
throw new Error('EscrowSubstore must be sorted by escrowChainID and localID.');
}
await escrowStore.setWithSchema(key, { amount: escrowData.amount }, escrowStoreSchema);
}

const nextAvailableLocalIDStore = context.getStore(this.id, STORE_PREFIX_AVAILABLE_LOCAL_ID);
await nextAvailableLocalIDStore.setWithSchema(
EMPTY_BYTES,
{ nextAvailableLocalID: genesisStore.availableLocalIDSubstore.nextAvailableLocalID },
availableLocalIDStoreSchema,
);

const terminatedEscrowStore = context.getStore(this.id, STORE_PREFIX_TERMINATED_ESCROW);
const terminatedEscrowKeySet = new dataStructures.BufferSet(
genesisStore.terminatedEscrowSubstore,
);
// validate terminated escrow chain ID uniqueness
if (terminatedEscrowKeySet.size !== genesisStore.terminatedEscrowSubstore.length) {
throw new Error(`Terminated escrow store chainID has duplicate.`);
}
// validate terminated escrow chain ID order
if (!objects.bufferArrayOrderByLex(genesisStore.terminatedEscrowSubstore)) {
throw new Error(`Terminated escrow store must be sorted by chainID.`);
}
for (const terminatedChainID of genesisStore.terminatedEscrowSubstore) {
await terminatedEscrowStore.setWithSchema(
terminatedChainID,
{ escrowTerminated: true },
terminatedEscrowStoreSchema,
);
}

// verify result
// validateSupplyStoreEntries
const computedSupply = new dataStructures.BufferMap<bigint>();
const allUsers = await userStore.iterateWithSchema<UserStoreData>(
{
start: Buffer.alloc(26, 0),
end: Buffer.alloc(26, 255),
},
userStoreSchema,
);
for (const { key, value: user } of allUsers) {
const tokenID = key.slice(20);
const [chainID, localID] = splitTokenID(tokenID);
if (chainID.equals(CHAIN_ID_ALIAS_NATIVE)) {
const existingSupply = computedSupply.get(localID) ?? BigInt(0);
computedSupply.set(
localID,
existingSupply +
user.availableBalance +
user.lockedBalances.reduce((prev, current) => prev + current.amount, BigInt(0)),
);
}
}
const allEscrows = await escrowStore.iterateWithSchema<EscrowStoreData>(
{
start: Buffer.alloc(6, 0),
end: Buffer.alloc(6, 255),
},
escrowStoreSchema,
);
for (const { key, value } of allEscrows) {
const [, localID] = splitTokenID(key);
const existingSupply = computedSupply.get(localID) ?? BigInt(0);
computedSupply.set(localID, existingSupply + value.amount);
}
for (const [localID, supply] of computedSupply.entries()) {
if (!isUInt64(supply)) {
throw new Error(
`Total supply for LocalID: ${localID.toString('hex')} exceeds uint64 range.`,
);
}
}
const storedSupply = new dataStructures.BufferMap<bigint>();
const allSupplies = await supplyStore.iterateWithSchema<SupplyStoreData>(
{
start: Buffer.alloc(LOCAL_ID_LENGTH, 0),
end: Buffer.alloc(LOCAL_ID_LENGTH, 255),
},
supplyStoreSchema,
);
const maxLocalID = allSupplies[allSupplies.length - 1].key;
for (const { key, value } of allSupplies) {
storedSupply.set(key, value.totalSupply);
}

for (const [localID, supply] of computedSupply.entries()) {
const stored = storedSupply.get(localID);
if (!stored || stored !== supply) {
throw new Error('Stored total supply conflicts with computed supply.');
}
}
for (const [localID, supply] of storedSupply.entries()) {
if (!computedSupply.has(localID) && supply !== BigInt(0)) {
throw new Error('Stored total supply is non zero but cannot be computed.');
}
}

// validate next available ID
const {
nextAvailableLocalID,
} = await nextAvailableLocalIDStore.getWithSchema<AvailableLocalIDStoreData>(
EMPTY_BYTES,
availableLocalIDStoreSchema,
);
// If maxLocalID is larger than nextAvailableLocalID, it is invalid
if (maxLocalID.compare(nextAvailableLocalID) > 0) {
throw new Error('Max local ID is higher than next availableLocalID');
}
}
}
Loading

0 comments on commit 5f4016b

Please sign in to comment.