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

Commit

Permalink
Fix bug in initialize state recovery (#8799)
Browse files Browse the repository at this point in the history
* 🌱 Add SMT & RMT verification events - #8719

* ♻️ Move SMT verification to execute

* ♻️ Improve SMT verify section and update test description
  • Loading branch information
ishantiw committed Aug 3, 2023
1 parent 3f6e3db commit 566c7a6
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright © 2023 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
import { BaseEvent, EventQueuer } from '../../base_event';

export class InvalidRMTVerification extends BaseEvent<undefined> {
public error(ctx: EventQueuer): void {
this.add(ctx, undefined);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright © 2023 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
import { BaseEvent, EventQueuer } from '../../base_event';

export class InvalidSMTVerification extends BaseEvent<undefined> {
public error(ctx: EventQueuer): void {
this.add(ctx, undefined);
}
}
4 changes: 4 additions & 0 deletions framework/src/modules/interoperability/mainchain/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import { GenesisBlockExecuteContext } from '../../../state_machine';
import { getMainchainID, isValidName } from '../utils';
import { RegisteredNamesStore } from '../stores/registered_names';
import { InvalidNameError } from '../errors';
import { InvalidSMTVerification } from '../events/invalid_smt_verification';
import { InvalidRMTVerification } from '../events/invalid_rmt_verification';

export class MainchainInteroperabilityModule extends BaseInteroperabilityModule {
public crossChainMethod = new MainchainCCMethod(this.stores, this.events);
Expand Down Expand Up @@ -173,6 +175,8 @@ export class MainchainInteroperabilityModule extends BaseInteroperabilityModule
InvalidCertificateSignatureEvent,
new InvalidCertificateSignatureEvent(this.name),
);
this.events.register(InvalidSMTVerification, new InvalidSMTVerification(this.name));
this.events.register(InvalidRMTVerification, new InvalidRMTVerification(this.name));
}

public addDependencies(tokenMethod: TokenMethod, feeMethod: FeeMethod) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { TerminatedStateAccount, TerminatedStateStore } from '../../stores/termi
import { ChainAccount, StateRecoveryInitParams } from '../../types';
import { getMainchainID } from '../../utils';
import { SidechainInteroperabilityInternalMethod } from '../internal_method';
import { InvalidSMTVerification } from '../../events/invalid_smt_verification';

export class InitializeStateRecoveryCommand extends BaseInteroperabilityCommand<SidechainInteroperabilityInternalMethod> {
public schema = stateRecoveryInitParamsSchema;
Expand All @@ -41,7 +42,7 @@ export class InitializeStateRecoveryCommand extends BaseInteroperabilityCommand<
): Promise<VerificationResult> {
const { params } = context;

const { chainID, bitmap, siblingHashes, sidechainAccount } = params;
const { chainID, sidechainAccount } = params;

const ownChainAccount = await this.stores.get(OwnChainAccountStore).get(context, EMPTY_BYTES);

Expand Down Expand Up @@ -69,78 +70,81 @@ export class InitializeStateRecoveryCommand extends BaseInteroperabilityCommand<
validator.validate(chainDataSchema, deserializedSidechainAccount);

const mainchainAccount = await this.stores.get(ChainAccountStore).get(context, mainchainID);
// The commands fails if the sidechain is not terminated and did not violate the liveness requirement.
// The sidechain must either be terminated or must have violated the liveness limit while being active.
if (deserializedSidechainAccount.status === ChainStatus.REGISTERED) {
throw new Error('Sidechain has status registered.');
}
if (
deserializedSidechainAccount.status !== ChainStatus.TERMINATED &&
deserializedSidechainAccount.status === ChainStatus.ACTIVE &&
mainchainAccount.lastCertificate.timestamp -
deserializedSidechainAccount.lastCertificate.timestamp <=
LIVENESS_LIMIT
) {
throw new Error('Sidechain is not terminated.');
throw new Error('Sidechain is still active and obeys the liveness requirement.');
}

return {
status: VerifyStatus.OK,
};
}

// LIP: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0054.md#execution-3
public async execute(context: CommandExecuteContext<StateRecoveryInitParams>): Promise<void> {
const {
params: { chainID, siblingHashes, bitmap, sidechainAccount: sidechainAccountBuffer },
} = context;

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

const chainStore = this.stores.get(ChainAccountStore);
const queryKey = Buffer.concat([chainStore.key, utils.hash(chainID)]);

const query = { key: queryKey, value: utils.hash(sidechainAccount), bitmap };
const query = { key: queryKey, value: utils.hash(sidechainAccountBuffer), bitmap };

const proofOfInclusion = { siblingHashes, queries: [query] };

const smt = new SparseMerkleTree();
let stateRoot: Buffer;
if (terminatedStateAccountExists) {
terminatedStateAccount = await terminatedStateSubstore.get(context, chainID);
if (!terminatedStateAccount.mainchainStateRoot) {
throw new Error('Sidechain account has missing property: mainchain state root');
}
const verified = await smt.verify(
terminatedStateAccount.mainchainStateRoot,
[queryKey],
proofOfInclusion,
);
if (!verified) {
throw new Error('State recovery initialization proof of inclusion is not valid.');
}
const terminatedStateAccount = await terminatedStateSubstore.get(context, chainID);
stateRoot = terminatedStateAccount.mainchainStateRoot;
} else {
const verified = await smt.verify(
mainchainAccount.lastCertificate.stateRoot,
[queryKey],
proofOfInclusion,
);
if (!verified) {
throw new Error('State recovery initialization proof of inclusion is not valid.');
}
const mainchainID = getMainchainID(context.chainID);
const mainchainAccount = await this.stores.get(ChainAccountStore).get(context, mainchainID);
stateRoot = mainchainAccount.lastCertificate.stateRoot;
}

return {
status: VerifyStatus.OK,
};
}

// LIP: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0054.md#execution-3
public async execute(context: CommandExecuteContext<StateRecoveryInitParams>): Promise<void> {
const { params } = context;
const sidechainAccount = codec.decode<ChainAccount>(chainDataSchema, params.sidechainAccount);
const verified = await smt.verify(stateRoot, [queryKey], proofOfInclusion);
if (!verified) {
this.events.get(InvalidSMTVerification).error(context);
throw new Error('State recovery initialization proof of inclusion is not valid.');
}

const deserializedSidechainAccount = codec.decode<ChainAccount>(
chainDataSchema,
sidechainAccountBuffer,
);
const doesTerminatedStateAccountExist = await this.stores
.get(TerminatedStateStore)
.has(context, params.chainID);
.has(context, chainID);
if (doesTerminatedStateAccountExist) {
const newTerminatedStateAccount: TerminatedStateAccount = {
stateRoot: sidechainAccount.lastCertificate.stateRoot,
stateRoot: deserializedSidechainAccount.lastCertificate.stateRoot,
mainchainStateRoot: EMPTY_HASH,
initialized: true,
};

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

await store.set(context, params.chainID, newTerminatedStateAccount);
await store.set(context, chainID, newTerminatedStateAccount);
return;
}

await this.internalMethod.createTerminatedStateAccount(
context,
params.chainID,
sidechainAccount.lastCertificate.stateRoot,
chainID,
deserializedSidechainAccount.lastCertificate.stateRoot,
);
}
}
4 changes: 4 additions & 0 deletions framework/src/modules/interoperability/sidechain/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ import { getMainchainID, isValidName } from '../utils';
import { TokenMethod } from '../../token';
import { InvalidCertificateSignatureEvent } from '../events/invalid_certificate_signature';
import { InvalidNameError } from '../errors';
import { InvalidRMTVerification } from '../events/invalid_rmt_verification';
import { InvalidSMTVerification } from '../events/invalid_smt_verification';

export class SidechainInteroperabilityModule extends BaseInteroperabilityModule {
public crossChainMethod: BaseCCMethod = new SidechainCCMethod(this.stores, this.events);
Expand Down Expand Up @@ -149,6 +151,8 @@ export class SidechainInteroperabilityModule extends BaseInteroperabilityModule
InvalidCertificateSignatureEvent,
new InvalidCertificateSignatureEvent(this.name),
);
this.events.register(InvalidSMTVerification, new InvalidSMTVerification(this.name));
this.events.register(InvalidRMTVerification, new InvalidRMTVerification(this.name));
}

public addDependencies(validatorsMethod: ValidatorsMethod, tokenMethod: TokenMethod) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
import { createStoreGetter } from '../../../../../../src/testing/utils';
import { OwnChainAccountStore } from '../../../../../../src/modules/interoperability/stores/own_chain_account';
import { getMainchainID } from '../../../../../../src/modules/interoperability/utils';
import { InvalidSMTVerification } from '../../../../../../src/modules/interoperability/events/invalid_smt_verification';

describe('Sidechain InitializeStateRecoveryCommand', () => {
const interopMod = new SidechainInteroperabilityModule();
Expand Down Expand Up @@ -86,13 +87,7 @@ describe('Sidechain InitializeStateRecoveryCommand', () => {
let mainchainAccount: ChainAccount;

beforeEach(async () => {
stateRecoveryInitCommand = new InitializeStateRecoveryCommand(
interopMod.stores,
interopMod.events,
new Map(),
new Map(),
interopMod['internalMethod'],
);
stateRecoveryInitCommand = interopMod['_stateRecoveryInitCommand'];

sidechainChainAccount = {
name: 'sidechain1',
Expand Down Expand Up @@ -232,7 +227,7 @@ describe('Sidechain InitializeStateRecoveryCommand', () => {
});

await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow(
'Sidechain is already terminated',
'Sidechain is already terminated.',
);
});

Expand Down Expand Up @@ -273,7 +268,11 @@ describe('Sidechain InitializeStateRecoveryCommand', () => {
);
});

it('should return error if the sidechain is not terminated on the mainchain but the sidechain violates the liveness requirement', async () => {
it('should return error if the sidechain is active on the mainchain and does not violate the liveness requirement', async () => {
await terminatedStateSubstore.set(createStoreGetter(stateStore), transactionParams.chainID, {
...terminatedStateAccount,
initialized: false,
});
const mainchainID = getMainchainID(transactionParams.chainID);
when(chainAccountStoreMock.get)
.calledWith(expect.anything(), mainchainID)
Expand Down Expand Up @@ -314,35 +313,113 @@ describe('Sidechain InitializeStateRecoveryCommand', () => {
);

await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow(
'Sidechain is not terminated.',
'Sidechain is still active and obeys the liveness requirement.',
);
});

it('should return error if the sidechain has ChainStatus.REGISTERED status', async () => {
await terminatedStateSubstore.set(createStoreGetter(stateStore), transactionParams.chainID, {
...terminatedStateAccount,
initialized: false,
});
const mainchainID = getMainchainID(transactionParams.chainID);
when(chainAccountStoreMock.get)
.calledWith(expect.anything(), mainchainID)
.mockResolvedValue(mainchainAccount);
sidechainChainAccount = {
name: 'sidechain1',
lastCertificate: {
height: 10,
stateRoot: utils.getRandomBytes(32),
timestamp: 100,
validatorsHash: utils.getRandomBytes(32),
},
status: ChainStatus.REGISTERED,
};
sidechainChainAccountEncoded = codec.encode(chainDataSchema, sidechainChainAccount);
transactionParams = {
chainID: utils.intToBuffer(3, 4),
bitmap: Buffer.alloc(0),
siblingHashes: [],
sidechainAccount: sidechainChainAccountEncoded,
};
encodedTransactionParams = codec.encode(stateRecoveryInitParamsSchema, transactionParams);
transaction = new Transaction({
module: MODULE_NAME_INTEROPERABILITY,
command: COMMAND_NAME_STATE_RECOVERY_INIT,
fee: BigInt(100000000),
nonce: BigInt(0),
params: encodedTransactionParams,
senderPublicKey: utils.getRandomBytes(32),
signatures: [],
});
transactionContext = createTransactionContext({
transaction,
stateStore,
});
commandVerifyContext = transactionContext.createCommandVerifyContext<StateRecoveryInitParams>(
stateRecoveryInitParamsSchema,
);

await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow(
'Sidechain has status registered.',
);
});
});

describe('execute', () => {
let invalidSMTVerificationEvent: InvalidSMTVerification;
beforeEach(() => {
mainchainAccount = {
name: 'mainchain',
lastCertificate: {
height: 10,
stateRoot: utils.getRandomBytes(32),
timestamp: 100 + LIVENESS_LIMIT,
validatorsHash: utils.getRandomBytes(32),
},
status: ChainStatus.ACTIVE,
};
invalidSMTVerificationEvent = new InvalidSMTVerification(interopMod.name);
stateRecoveryInitCommand['events'].register(
InvalidSMTVerification,
invalidSMTVerificationEvent,
);
});

it('should return error if terminated state account exists and proof of inclusion is not verified', async () => {
jest.spyOn(SparseMerkleTree.prototype, 'verify').mockResolvedValue(false);
jest.spyOn(invalidSMTVerificationEvent, 'error');

await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow(
await expect(stateRecoveryInitCommand.execute(commandExecuteContext)).rejects.toThrow(
'State recovery initialization proof of inclusion is not valid',
);
expect(interopStoreMock.createTerminatedStateAccount).not.toHaveBeenCalled();
expect(invalidSMTVerificationEvent.error).toHaveBeenCalled();
});

it('should return error if terminated state account does not exist and proof of inclusion is not verified', async () => {
jest.spyOn(SparseMerkleTree.prototype, 'verify').mockResolvedValue(false);
const mainchainID = getMainchainID(transactionParams.chainID);
jest.spyOn(invalidSMTVerificationEvent, 'error');

const mainchainID = getMainchainID(commandExecuteContext.chainID);

when(chainAccountStoreMock.get)
.calledWith(expect.anything(), mainchainID)
.mockResolvedValue(mainchainAccount);

await terminatedStateSubstore.del(createStoreGetter(stateStore), transactionParams.chainID);
await terminatedStateSubstore.del(
createStoreGetter(commandExecuteContext.stateStore as any),
transactionParams.chainID,
);

await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow(
await expect(stateRecoveryInitCommand.execute(commandExecuteContext)).rejects.toThrow(
'State recovery initialization proof of inclusion is not valid',
);
expect(interopStoreMock.createTerminatedStateAccount).not.toHaveBeenCalled();
expect(invalidSMTVerificationEvent.error).toHaveBeenCalled();
});
});

describe('execute', () => {
it('should create a terminated state account when there is none', async () => {
// Arrange & Assign & Act
await stateRecoveryInitCommand.execute(commandExecuteContext);
Expand Down

0 comments on commit 566c7a6

Please sign in to comment.