From b7e28cc576ce76f6b5f8dc5601778d8f63113bd0 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Tue, 18 Feb 2020 11:23:12 +0000 Subject: [PATCH 1/5] Fixed #449 Added blockService --- e2e/service/BlockService.spec.ts | 117 +++++++++++++++++++++++++++++++ src/service/BlockService.ts | 87 +++++++++++++++++++++++ src/service/service.ts | 1 + 3 files changed, 205 insertions(+) create mode 100644 e2e/service/BlockService.spec.ts create mode 100644 src/service/BlockService.ts diff --git a/e2e/service/BlockService.spec.ts b/e2e/service/BlockService.spec.ts new file mode 100644 index 0000000000..18df0cd34c --- /dev/null +++ b/e2e/service/BlockService.spec.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert, expect } from 'chai'; +import { ReceiptRepository } from '../../src/infrastructure/ReceiptRepository'; +import { TransactionRepository } from '../../src/infrastructure/TransactionRepository'; +import { Account } from '../../src/model/account/Account'; +import { NetworkType } from '../../src/model/blockchain/NetworkType'; +import { PlainMessage } from '../../src/model/message/PlainMessage'; +import { NetworkCurrencyMosaic } from '../../src/model/mosaic/NetworkCurrencyMosaic'; +import { Deadline } from '../../src/model/transaction/Deadline'; +import { TransferTransaction } from '../../src/model/transaction/TransferTransaction'; +import { UInt64 } from '../../src/model/UInt64'; +import { BlockService } from '../../src/service/BlockService'; +import { IntegrationTestHelper } from '../infrastructure/IntegrationTestHelper'; + +describe('BlockService', () => { + const helper = new IntegrationTestHelper(); + let generationHash: string; + let account: Account; + let account2: Account; + let account3: Account; + let networkType: NetworkType; + let transactionHash: string; + let blockService: BlockService; + let transactionRepository: TransactionRepository; + let receiptRepository: ReceiptRepository; + + before(() => { + return helper.start().then(() => { + account = helper.account; + account2 = helper.account2; + account3 = helper.account3; + generationHash = helper.generationHash; + networkType = helper.networkType; + transactionRepository = helper.repositoryFactory.createTransactionRepository(); + receiptRepository = helper.repositoryFactory.createReceiptRepository(); + blockService = new BlockService(helper.repositoryFactory.createBlockRepository(), receiptRepository); + }); + }); + before(() => { + return helper.listener.open(); + }); + + after(() => { + helper.listener.close(); + }); + + /** + * ========================= + * Setup test data + * ========================= + */ + describe('Create a transfer', () => { + it('Announce TransferTransaction', () => { + const transferTransaction = TransferTransaction.create( + Deadline.create(), + account2.address, + [NetworkCurrencyMosaic.createAbsolute(1)], + PlainMessage.create('test-message'), + networkType, + helper.maxFee, + ); + + const signedTransaction = transferTransaction.signWith(account, generationHash); + transactionHash = signedTransaction.hash; + return helper.announce(signedTransaction); + }); + }); + + /** + * ========================= + * Test + * ========================= + */ + + describe('Validate transansaction', () => { + it('call block service', () => { + return transactionRepository.getTransaction(transactionHash).subscribe( + (transaction) => { + const transactionInfo = transaction.transactionInfo; + if (transactionInfo && transactionInfo.height !== undefined) { + const validationResult = blockService.validateTransactionInBlock(transactionHash, transactionInfo.height); + expect(validationResult).to.be.true; + } + assert(false, `Transaction (hash: ${transactionHash}) not found`); + }, + ); + }); + }); + + describe('Validate receipt', () => { + it('call block service', () => { + return receiptRepository.getBlockReceipts(UInt64.fromUint(1)).subscribe( + (statement) => { + const receipt = statement.transactionStatements[0]; + const validationResult = blockService.validateReceiptInBlock(receipt.generateHash(), UInt64.fromUint(1)); + expect(validationResult).to.be.true; + }, + ); + }); + }); + +}); diff --git a/src/service/BlockService.ts b/src/service/BlockService.ts new file mode 100644 index 0000000000..40b98983bc --- /dev/null +++ b/src/service/BlockService.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2020 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { sha3_256 } from 'js-sha3'; +import { combineLatest, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { BlockRepository } from '../infrastructure/BlockRepository'; +import { ReceiptRepository } from '../infrastructure/ReceiptRepository'; +import { MerklePathItem } from '../model/blockchain/MerklePathItem'; +import { UInt64 } from '../model/UInt64'; + +/** + * Transaction Service + */ +export class BlockService { + + /** + * Constructor + * @param blockRepository + * @param receiptRepository + */ + constructor(private readonly blockRepository: BlockRepository, private readonly receiptRepository: ReceiptRepository) { + } + + /** + * Validate transaction hash in block + * @param leaf transaction hash + * @param height block height + */ + public validateTransactionInBlock(leaf: string, height: UInt64): Observable { + const rootHashObservable = this.blockRepository.getBlockByHeight(height); + const merklePathItemObservable = this.blockRepository.getMerkleTransaction(height, leaf); + return combineLatest(rootHashObservable, merklePathItemObservable).pipe( + map((combined) => this.validateInBlock(leaf, combined[1].merklePath, combined[0].blockTransactionsHash)), + ); + } + + /** + * Validate receipt hash in block + * @param leaf receipt hash + * @param height block height + */ + public validateReceiptInBlock(leaf: string, height: UInt64): Observable { + const rootHashObservable = this.blockRepository.getBlockByHeight(height); + const merklePathItemObservable = this.receiptRepository.getMerkleReceipts(height, leaf); + return combineLatest(rootHashObservable, merklePathItemObservable).pipe( + map((combined) => this.validateInBlock(leaf, combined[1].merklePath, combined[0].blockReceiptsHash)), + ); + } + + /** + * @internal + * Validate leaf against merkle tree in block + * @param leaf Leaf hash in merkle tree + * @param merklePathItem Merkle path item array + * @param rootHash Block root hash + */ + private validateInBlock(leaf: string, merklePathItem: MerklePathItem[] = [], rootHash: string): boolean { + if (merklePathItem.length === 0) { + return leaf.toUpperCase() === rootHash.toUpperCase(); + } + const rootToCompare = merklePathItem.reduce((proofHash, pathItem) => { + const hasher = sha3_256.create(); + // Left + if (pathItem.position === 1) { + return hasher.update(Buffer.from(pathItem.hash + proofHash, 'hex')).hex(); + } else { + // Right + return hasher.update(Buffer.from(proofHash + pathItem.hash, 'hex')).hex(); + } + }, leaf); + return rootToCompare.toUpperCase() === rootHash.toUpperCase(); + } +} diff --git a/src/service/service.ts b/src/service/service.ts index fd6074d5c4..ed73b3ff59 100644 --- a/src/service/service.ts +++ b/src/service/service.ts @@ -20,3 +20,4 @@ export * from './AggregateTransactionService'; export * from './MetadataTransactionService'; export * from './MosaicRestrictionTransactionService'; export * from './TransactionService'; +export * from './BlockService'; From 0bb1832b9d9d3e60168e56e33a88c61d54de2611 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Tue, 18 Feb 2020 11:43:45 +0000 Subject: [PATCH 2/5] Updated tests to use async --- e2e/service/BlockService.spec.ts | 34 +++++++++++++------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/e2e/service/BlockService.spec.ts b/e2e/service/BlockService.spec.ts index 18df0cd34c..1bd2440e58 100644 --- a/e2e/service/BlockService.spec.ts +++ b/e2e/service/BlockService.spec.ts @@ -88,30 +88,24 @@ describe('BlockService', () => { */ describe('Validate transansaction', () => { - it('call block service', () => { - return transactionRepository.getTransaction(transactionHash).subscribe( - (transaction) => { - const transactionInfo = transaction.transactionInfo; - if (transactionInfo && transactionInfo.height !== undefined) { - const validationResult = blockService.validateTransactionInBlock(transactionHash, transactionInfo.height); - expect(validationResult).to.be.true; - } - assert(false, `Transaction (hash: ${transactionHash}) not found`); - }, - ); + it('call block service', async () => { + const transaction = await transactionRepository.getTransaction(transactionHash).toPromise(); + const transactionInfo = transaction.transactionInfo; + if (transactionInfo && transactionInfo.height !== undefined) { + const validationResult = await blockService.validateTransactionInBlock(transactionHash, transactionInfo.height).toPromise(); + expect(validationResult).to.be.true; + } else { + assert(false, `Transaction (hash: ${transactionHash}) not found`); + } }); }); describe('Validate receipt', () => { - it('call block service', () => { - return receiptRepository.getBlockReceipts(UInt64.fromUint(1)).subscribe( - (statement) => { - const receipt = statement.transactionStatements[0]; - const validationResult = blockService.validateReceiptInBlock(receipt.generateHash(), UInt64.fromUint(1)); - expect(validationResult).to.be.true; - }, - ); + it('call block service', async () => { + const statement = await receiptRepository.getBlockReceipts(UInt64.fromUint(1)).toPromise(); + const receipt = statement.transactionStatements[0]; + const validationResult = await blockService.validateReceiptInBlock(receipt.generateHash(), UInt64.fromUint(1)).toPromise(); + expect(validationResult).to.be.true; }); }); - }); From ec3833127b461f24ae4219a02bbb1fb15f5cfc06 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Tue, 18 Feb 2020 13:10:50 +0000 Subject: [PATCH 3/5] Added interface, --- e2e/service/BlockService.spec.ts | 8 +++--- src/service/BlockService.ts | 16 +++++++---- src/service/interfaces/IBlockService.ts | 38 +++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 src/service/interfaces/IBlockService.ts diff --git a/e2e/service/BlockService.spec.ts b/e2e/service/BlockService.spec.ts index 1bd2440e58..43c1b8ca38 100644 --- a/e2e/service/BlockService.spec.ts +++ b/e2e/service/BlockService.spec.ts @@ -48,7 +48,7 @@ describe('BlockService', () => { networkType = helper.networkType; transactionRepository = helper.repositoryFactory.createTransactionRepository(); receiptRepository = helper.repositoryFactory.createReceiptRepository(); - blockService = new BlockService(helper.repositoryFactory.createBlockRepository(), receiptRepository); + blockService = new BlockService(helper.repositoryFactory); }); }); before(() => { @@ -102,9 +102,9 @@ describe('BlockService', () => { describe('Validate receipt', () => { it('call block service', async () => { - const statement = await receiptRepository.getBlockReceipts(UInt64.fromUint(1)).toPromise(); - const receipt = statement.transactionStatements[0]; - const validationResult = await blockService.validateReceiptInBlock(receipt.generateHash(), UInt64.fromUint(1)).toPromise(); + const statements = await receiptRepository.getBlockReceipts(UInt64.fromUint(1)).toPromise(); + const statement = statements.transactionStatements[0]; + const validationResult = await blockService.validateStatementInBlock(statement.generateHash(), UInt64.fromUint(1)).toPromise(); expect(validationResult).to.be.true; }); }); diff --git a/src/service/BlockService.ts b/src/service/BlockService.ts index 40b98983bc..0c6a2c60d4 100644 --- a/src/service/BlockService.ts +++ b/src/service/BlockService.ts @@ -19,6 +19,7 @@ import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { BlockRepository } from '../infrastructure/BlockRepository'; import { ReceiptRepository } from '../infrastructure/ReceiptRepository'; +import { RepositoryFactory } from '../infrastructure/RepositoryFactory'; import { MerklePathItem } from '../model/blockchain/MerklePathItem'; import { UInt64 } from '../model/UInt64'; @@ -26,13 +27,16 @@ import { UInt64 } from '../model/UInt64'; * Transaction Service */ export class BlockService { + private readonly blockRepository: BlockRepository; + private readonly receiptRepository: ReceiptRepository; /** * Constructor - * @param blockRepository - * @param receiptRepository + * @param repositoryFactory */ - constructor(private readonly blockRepository: BlockRepository, private readonly receiptRepository: ReceiptRepository) { + constructor(public readonly repositoryFactory: RepositoryFactory) { + this.blockRepository = repositoryFactory.createBlockRepository(); + this.receiptRepository = repositoryFactory.createReceiptRepository(); } /** @@ -49,11 +53,11 @@ export class BlockService { } /** - * Validate receipt hash in block - * @param leaf receipt hash + * Validate statement hash in block + * @param leaf statement hash * @param height block height */ - public validateReceiptInBlock(leaf: string, height: UInt64): Observable { + public validateStatementInBlock(leaf: string, height: UInt64): Observable { const rootHashObservable = this.blockRepository.getBlockByHeight(height); const merklePathItemObservable = this.receiptRepository.getMerkleReceipts(height, leaf); return combineLatest(rootHashObservable, merklePathItemObservable).pipe( diff --git a/src/service/interfaces/IBlockService.ts b/src/service/interfaces/IBlockService.ts new file mode 100644 index 0000000000..e8d349c25b --- /dev/null +++ b/src/service/interfaces/IBlockService.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Observable } from 'rxjs'; +import { UInt64 } from '../../model/UInt64'; + +/** + * Block Service Interface + */ +export interface IBlockService { + + /** + * Validate transaction hash in block + * @param leaf transaction hash + * @param height block height + */ + validateTransactionInBlock(leaf: string, height: UInt64): Observable; + + /** + * Validate statement hash in block + * @param leaf statement hash + * @param height block height + */ + validateStatementInBlock(leaf: string, height: UInt64): Observable; +} From 5c6ef67bc8e2af27dbc85c1e9efd77326aa1737a Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Tue, 18 Feb 2020 14:34:57 +0000 Subject: [PATCH 4/5] Added unit tests --- test/service/BlockService.spec.ts | 111 ++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 test/service/BlockService.spec.ts diff --git a/test/service/BlockService.spec.ts b/test/service/BlockService.spec.ts new file mode 100644 index 0000000000..0fe6aa0068 --- /dev/null +++ b/test/service/BlockService.spec.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { of as observableOf } from 'rxjs'; +import { deepEqual, instance, mock, when } from 'ts-mockito'; +import { BlockRepository } from '../../src/infrastructure/BlockRepository'; +import { ReceiptRepository } from '../../src/infrastructure/ReceiptRepository'; +import { RepositoryFactory } from '../../src/infrastructure/RepositoryFactory'; +import { Account } from '../../src/model/account/Account'; +import { BlockInfo } from '../../src/model/blockchain/BlockInfo'; +import { MerklePathItem } from '../../src/model/blockchain/MerklePathItem'; +import { MerkleProofInfo } from '../../src/model/blockchain/MerkleProofInfo'; +import { NetworkType } from '../../src/model/blockchain/NetworkType'; +import { UInt64 } from '../../src/model/UInt64'; +import { BlockService } from '../../src/service/BlockService'; +import { TestingAccount } from '../conf/conf.spec'; + +describe('BlockService', () => { + + const mockBlockHash = 'D4EC16FCFE696EFDBF1820F68245C88135ACF4C6F888599C8E18BC09B9F08C7B'; + const leaf = '2717C8AAB0A21896D0C56375209E761F84383C3882F37A11D9D0159007263EB2'; + let blockService: BlockService; + let account: Account; + before(() => { + account = TestingAccount; + const mockBlockRepository = mock(); + const mockReceiptRepository = mock(); + const mockRepoFactory = mock(); + + when(mockBlockRepository.getBlockByHeight(deepEqual(UInt64.fromUint(1)))) + .thenReturn(observableOf(mockBlockInfo())); + when(mockBlockRepository.getBlockByHeight(deepEqual(UInt64.fromUint(2)))) + .thenReturn(observableOf(mockBlockInfo(true))); + when(mockBlockRepository.getMerkleTransaction(deepEqual(UInt64.fromUint(1)), leaf)) + .thenReturn(observableOf(mockMerklePath())); + when(mockBlockRepository.getMerkleTransaction(deepEqual(UInt64.fromUint(2)), leaf)) + .thenReturn(observableOf(mockMerklePath())); + when(mockReceiptRepository.getMerkleReceipts(deepEqual(UInt64.fromUint(1)), leaf)) + .thenReturn(observableOf(mockMerklePath())); + when(mockReceiptRepository.getMerkleReceipts(deepEqual(UInt64.fromUint(2)), leaf)) + .thenReturn(observableOf(mockMerklePath())); + const blockRepository = instance(mockBlockRepository); + const receiptRepository = instance(mockReceiptRepository); + + when(mockRepoFactory.createBlockRepository()).thenReturn(blockRepository); + when(mockRepoFactory.createReceiptRepository()).thenReturn(receiptRepository); + const repoFactory = instance(mockRepoFactory); + blockService = new BlockService(repoFactory); + }); + + it('should validate transaction', async () => { + const result = await blockService.validateTransactionInBlock(leaf, UInt64.fromUint(1)).toPromise(); + expect(result).to.be.true; + }); + + it('should validate transaction - wrong hash', async () => { + const result = await blockService.validateTransactionInBlock(leaf, UInt64.fromUint(2)).toPromise(); + expect(result).to.be.false; + }); + + it('should validate statement', async () => { + const result = await blockService.validateStatementInBlock(leaf, UInt64.fromUint(1)).toPromise(); + expect(result).to.be.true; + }); + + it('should validate statement - wrong hash', async () => { + const result = await blockService.validateStatementInBlock(leaf, UInt64.fromUint(2)).toPromise(); + expect(result).to.be.false; + }); + + function mockBlockInfo(isFake: boolean = false): BlockInfo { + if (isFake) { + return new BlockInfo( + 'hash', 'generationHash', UInt64.fromNumericString('0'), 1, 'signature', account.publicAccount, + NetworkType.MIJIN_TEST, 0, 0, UInt64.fromUint(1), UInt64.fromUint(0), UInt64.fromUint(0), 0, 'previousHash', + 'fakeHash', 'fakeHash', 'stateHash', undefined, + ); + } + return new BlockInfo( + 'hash', 'generationHash', UInt64.fromNumericString('0'), 1, 'signature', account.publicAccount, + NetworkType.MIJIN_TEST, 0, 0, UInt64.fromUint(1), UInt64.fromUint(0), UInt64.fromUint(0), 0, 'previousHash', + mockBlockHash, mockBlockHash, 'stateHash', undefined, + ); + } + + function mockMerklePath(): MerkleProofInfo { + return new MerkleProofInfo( + [ + new MerklePathItem(1, 'CDE45D740536E5361F392025A44B26546A138958E69CD6F18D22908F8F11ECF2'), + new MerklePathItem(2, '4EF55DAB8FEF9711B23DA71D2ACC58EFFF3969C3D572E06ACB898F99BED4827A'), + new MerklePathItem(1, '1BB95470065ED69D184948A0175EDC2EAB9E86A0CEB47B648A58A02A5445AF66'), + new MerklePathItem(2, 'D96B03809B8B198EFA5824191A979F7B85C0E9B7A6623DAFF38D4B2927EFDFB5'), + new MerklePathItem(2, '9981EBDBCA8E36BA4D4D4A450072026AC8C85BA6497666219E0E049BE3362E51'), + ], + ); + } +}); From c4878c757475d76ebc1351ad16f07164f9da6744 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Tue, 18 Feb 2020 14:38:21 +0000 Subject: [PATCH 5/5] Capture error after combineLatest --- src/service/BlockService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/service/BlockService.ts b/src/service/BlockService.ts index 0c6a2c60d4..0c3b5ec955 100644 --- a/src/service/BlockService.ts +++ b/src/service/BlockService.ts @@ -15,8 +15,8 @@ */ import { sha3_256 } from 'js-sha3'; -import { combineLatest, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { combineLatest, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; import { BlockRepository } from '../infrastructure/BlockRepository'; import { ReceiptRepository } from '../infrastructure/ReceiptRepository'; import { RepositoryFactory } from '../infrastructure/RepositoryFactory'; @@ -49,7 +49,7 @@ export class BlockService { const merklePathItemObservable = this.blockRepository.getMerkleTransaction(height, leaf); return combineLatest(rootHashObservable, merklePathItemObservable).pipe( map((combined) => this.validateInBlock(leaf, combined[1].merklePath, combined[0].blockTransactionsHash)), - ); + ).pipe(catchError(() => of(false))); } /** @@ -62,7 +62,7 @@ export class BlockService { const merklePathItemObservable = this.receiptRepository.getMerkleReceipts(height, leaf); return combineLatest(rootHashObservable, merklePathItemObservable).pipe( map((combined) => this.validateInBlock(leaf, combined[1].merklePath, combined[0].blockReceiptsHash)), - ); + ).pipe(catchError(() => of(false))); } /**