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

Commit

Permalink
Update state recovery command (#8801)
Browse files Browse the repository at this point in the history
* ♻️ Update state recovery command

* ♻️ Update state recovery cmd  based on #8807
  • Loading branch information
ishantiw authored Aug 4, 2023
1 parent a352fef commit b7b4b51
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 104 deletions.
1 change: 1 addition & 0 deletions framework/src/modules/base_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface StoreGetter {
getStore: (moduleID: Buffer, storePrefix: Buffer) => SubStore;
}

// LIP: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0040.md#module-store-prefix-1
export const computeStorePrefix = (name: string): Buffer => {
const prefix = utils.hash(Buffer.from(name, 'utf-8')).slice(0, 4);
// eslint-disable-next-line no-bitwise
Expand Down
106 changes: 55 additions & 51 deletions framework/src/modules/interoperability/base_state_recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { objects as objectUtils } from '@liskhq/lisk-utils';
import { SparseMerkleTree } from '@liskhq/lisk-db';
import { utils } from '@liskhq/lisk-cryptography';
import { BaseInteroperabilityCommand } from './base_interoperability_command';
import { EMPTY_BYTES, EMPTY_HASH } from './constants';
import { RECOVERED_STORE_VALUE } from './constants';
import { stateRecoveryParamsSchema } from './schemas';
import { RecoverContext, StateRecoveryParams } from './types';
import {
Expand All @@ -29,7 +29,9 @@ import { TerminatedStateStore } from './stores/terminated_state';
import { computeStorePrefix } from '../base_store';
import { BaseCCMethod } from './base_cc_method';
import { BaseInteroperabilityInternalMethod } from './base_interoperability_internal_methods';
import { InvalidSMTVerification } from './events/invalid_smt_verification';

// LIP: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0054.md#state-recovery-command
export class BaseStateRecoveryCommand<
T extends BaseInteroperabilityInternalMethod,
> extends BaseInteroperabilityCommand<T> {
Expand All @@ -39,12 +41,13 @@ export class BaseStateRecoveryCommand<
context: CommandVerifyContext<StateRecoveryParams>,
): Promise<VerificationResult> {
const {
params: { chainID, storeEntries, siblingHashes, module },
params: { chainID, storeEntries, module },
} = context;

const terminatedStateSubstore = this.stores.get(TerminatedStateStore);
const terminatedStateAccountExists = await terminatedStateSubstore.has(context, chainID);

// The terminated account has to exist for this sidechain.
if (!terminatedStateAccountExists) {
return {
status: VerifyStatus.FAIL,
Expand All @@ -70,57 +73,30 @@ export class BaseStateRecoveryCommand<
};
}

// The module indicated in the transaction params must have a recover function.
// For example, this means that modules such as Interoperability or Auth cannot be recovered.
if (!moduleMethod.recover) {
return {
status: VerifyStatus.FAIL,
error: new Error('Module is not recoverable.'),
};
}

const { stateRoot } = terminatedStateAccount;
const queryKeys = [];
const storeQueries = [];

const storePrefix = computeStorePrefix(module);

// For efficiency, only subStorePrefix+storeKey is enough to check for pairwise distinct keys in verification
for (const entry of storeEntries) {
if (entry.storeValue.equals(EMPTY_BYTES)) {
return {
status: VerifyStatus.FAIL,
error: new Error('Recovered store value cannot be empty.'),
};
}
const queryKey = Buffer.concat([
storePrefix,
entry.substorePrefix,
utils.hash(entry.storeKey),
]);
const queryKey = Buffer.concat([entry.substorePrefix, entry.storeKey]);
queryKeys.push(queryKey);
storeQueries.push({
key: queryKey,
value: utils.hash(entry.storeValue),
bitmap: entry.bitmap,
});
}

// Check that all keys are pairwise distinct, meaning that we are not trying to recover the same entry twice.
if (!objectUtils.bufferArrayUniqueItems(queryKeys)) {
return {
status: VerifyStatus.FAIL,
error: new Error('Recovered store keys are not pairwise distinct.'),
};
}

const proofOfInclusionStores = { siblingHashes, queries: storeQueries };
const sparseMerkleTree = new SparseMerkleTree();
const verified = await sparseMerkleTree.verify(stateRoot, queryKeys, proofOfInclusionStores);

if (!verified) {
return {
status: VerifyStatus.FAIL,
error: new Error('State recovery proof of inclusion is not valid.'),
};
}

return {
status: VerifyStatus.OK,
};
Expand All @@ -130,16 +106,50 @@ export class BaseStateRecoveryCommand<
const {
params: { chainID, storeEntries, module, siblingHashes },
} = context;
const storeQueries = [];
const storeQueriesVerify = [];
const queryKeys = [];
// Calculate store prefix from the module name according to LIP 0040.
const storePrefix = computeStorePrefix(module);

for (const entry of storeEntries) {
const queryKey = Buffer.concat([
storePrefix,
entry.substorePrefix,
utils.hash(entry.storeKey),
]);
queryKeys.push(queryKey);
storeQueriesVerify.push({
key: queryKey,
value: utils.hash(entry.storeValue),
bitmap: entry.bitmap,
});
}
const terminatedStateAccount = await this.stores
.get(TerminatedStateStore)
.get(context, chainID);

const proofOfInclusionStores = { siblingHashes, queries: storeQueriesVerify };
// The SMT verification step is computationally expensive. Therefore,
// it is done in the execution step such that the transaction fee must be paid.
const smtVerified = await new SparseMerkleTree().verify(
terminatedStateAccount.stateRoot,
queryKeys,
proofOfInclusionStores,
);

if (!smtVerified) {
this.events.get(InvalidSMTVerification).error(context);
throw new Error('State recovery proof of inclusion is not valid.');
}

// Casting for type issue. `recover` already verified to exist in module for verify
const moduleMethod = this.interoperableCCMethods.get(module) as BaseCCMethod & {
recover: (ctx: RecoverContext) => Promise<void>;
};
const storePrefix = computeStorePrefix(module);

const storeQueriesUpdate = [];
for (const entry of storeEntries) {
try {
// The recover function corresponding to trsParams.module applies the recovery logic.
await moduleMethod.recover({
...context,
module,
Expand All @@ -148,28 +158,22 @@ export class BaseStateRecoveryCommand<
storeKey: entry.storeKey,
storeValue: entry.storeValue,
});
storeQueriesUpdate.push({
key: Buffer.concat([storePrefix, entry.substorePrefix, utils.hash(entry.storeKey)]),
value: RECOVERED_STORE_VALUE,
bitmap: entry.bitmap,
});
} catch (err) {
throw new Error(`Recovery failed for module: ${module}`);
}

storeQueries.push({
key: Buffer.concat([storePrefix, entry.substorePrefix, utils.hash(entry.storeKey)]),
value: EMPTY_HASH,
bitmap: entry.bitmap,
});
}

const sparseMerkleTree = new SparseMerkleTree();
const root = await sparseMerkleTree.calculateRoot({
queries: storeQueries,
const root = await new SparseMerkleTree().calculateRoot({
queries: storeQueriesUpdate,
siblingHashes,
});

const terminatedStateSubstore = this.stores.get(TerminatedStateStore);

const terminatedStateAccount = await terminatedStateSubstore.get(context, chainID);

await terminatedStateSubstore.set(context, chainID, {
await this.stores.get(TerminatedStateStore).set(context, chainID, {
...terminatedStateAccount,
stateRoot: root,
});
Expand Down
1 change: 1 addition & 0 deletions framework/src/modules/interoperability/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const COMMAND_NAME_STATE_RECOVERY = 'recoverState';
export const COMMAND_NAME_MESSAGE_RECOVERY = 'recoverMessage';
export const COMMAND_NAME_STATE_RECOVERY_INIT = 'initializeStateRecovery';
export const COMMAND_NAME_LIVENESS_TERMINATION = 'terminateSidechainForLiveness';
export const RECOVERED_STORE_VALUE = Buffer.alloc(32);

// Events
export const EVENT_NAME_CHAIN_ACCOUNT_UPDATED = 'chainAccountUpdated';
Expand Down
Loading

0 comments on commit b7b4b51

Please sign in to comment.