Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions e2e/service/BlockService.spec.ts
Original file line number Diff line number Diff line change
@@ -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 { 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);
});
});
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', 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', async () => {
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;
});
});
});
91 changes: 91 additions & 0 deletions src/service/BlockService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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, of } from 'rxjs';
import { catchError, 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';

/**
* Transaction Service
*/
export class BlockService {
private readonly blockRepository: BlockRepository;
private readonly receiptRepository: ReceiptRepository;

/**
* Constructor
* @param repositoryFactory
*/
constructor(public readonly repositoryFactory: RepositoryFactory) {
this.blockRepository = repositoryFactory.createBlockRepository();
this.receiptRepository = repositoryFactory.createReceiptRepository();
}

/**
* Validate transaction hash in block
* @param leaf transaction hash
* @param height block height
*/
public validateTransactionInBlock(leaf: string, height: UInt64): Observable<boolean> {
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)),
).pipe(catchError(() => of(false)));
}

/**
* Validate statement hash in block
* @param leaf statement hash
* @param height block height
*/
public validateStatementInBlock(leaf: string, height: UInt64): Observable<boolean> {
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)),
).pipe(catchError(() => of(false)));
}

/**
* @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();
}
Comment on lines +82 to +87
Copy link
Contributor

Choose a reason for hiding this comment

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

The position is being changed from a number to a (left/right) string. Rest has done the change in master, we are still waiting for the open api PR symbol/symbol-openapi#77

@dgarcia360 can we move 77 PR forward? once released we can then generate the clients and update the ts and java PR related to merkle tests

Copy link
Contributor

Choose a reason for hiding this comment

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

}, leaf);
return rootToCompare.toUpperCase() === rootHash.toUpperCase();
}
}
38 changes: 38 additions & 0 deletions src/service/interfaces/IBlockService.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>;

/**
* Validate statement hash in block
* @param leaf statement hash
* @param height block height
*/
validateStatementInBlock(leaf: string, height: UInt64): Observable<boolean>;
}
1 change: 1 addition & 0 deletions src/service/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from './AggregateTransactionService';
export * from './MetadataTransactionService';
export * from './MosaicRestrictionTransactionService';
export * from './TransactionService';
export * from './BlockService';
111 changes: 111 additions & 0 deletions test/service/BlockService.spec.ts
Original file line number Diff line number Diff line change
@@ -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<BlockRepository>();
const mockReceiptRepository = mock<ReceiptRepository>();
const mockRepoFactory = mock<RepositoryFactory>();

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'),
],
);
}
});