Skip to content
Merged
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
57 changes: 38 additions & 19 deletions src/core/crypto/MerkleHashBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,46 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { SHA3Hasher } from './SHA3Hasher';
import { SignSchema } from './SignSchema';

export class MerkleHashBuilder {

hashes: Uint8Array[] = new Array<Uint8Array>();
hasherFactory: any;
signSchema: SignSchema;
length: number;
/**
* The list of hashes used to calculate root hash.
*
* @var {Uint8Array}
*/
protected hashes: Uint8Array[] = new Array<Uint8Array>();

/**
* Constructor
* @param hasherFactory Hasher (SHA3_256)
* @param signSchema Sign schema
* @param length Hash size
*/
constructor(hasherFactory: any, signSchema: SignSchema = SignSchema.SHA3, length: number = 32) {
this.hasherFactory = hasherFactory;
this.signSchema = signSchema;
this.length = length;
constructor(/**
* Length of produced merkle hash in bytes.
*
* @var {number}
*/
public readonly length: number,
/**
* Signature schema used (hash algorithm diff)
*
* @var {SignSchema}
*/
public readonly signSchema: SignSchema) {
}

/** @internal
/**
* Hash inner transactions
*
* @internal
* @param hashes Inner transaction hashes
* @return {Uint8Array}
*/
protected hash(hashes: Uint8Array[]): Uint8Array {
const hasher = this.hasherFactory(this.length, this.signSchema);
const hasher = SHA3Hasher.createHasher(this.length, this.signSchema);
hasher.reset();

hashes.forEach((hashVal: Uint8Array) => {
Expand All @@ -52,9 +64,12 @@ export class MerkleHashBuilder {
return hash;
}

/** @internal
* Get root hash of Merkle Trees
* @param hashes Inner transaction hashes
/**
* Get root hash of Merkle Tree
*
* @internal
* @param {Uint8Array[]} hashes Inner transaction hashes
* @return {Uint8Array}
*/
protected calculateRootHash(hashes: Uint8Array[]): Uint8Array {

Expand All @@ -80,18 +95,22 @@ export class MerkleHashBuilder {
}

/**
* Return root hash from Merkle tree
* Get root hash of Merkle tree
*
* @return {Uint8Array}
*/
public getRootHash(): Uint8Array {
return this.calculateRootHash(this.hashes);
}

/**
* Update hashes array
* Update hashes array (add hash)
*
* @param hash Inner transaction hash buffer
* @return {MerkleHashBuilder}
*/
public update(hash: Uint8Array): void {
public update(hash: Uint8Array): MerkleHashBuilder {
this.hashes.push(hash);
return this;
}

}
18 changes: 15 additions & 3 deletions src/model/transaction/AggregateTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { sha3_256 } from 'js-sha3';
import {KeyPair, MerkleHashBuilder, SHA3Hasher} from '../../core/crypto';
import {KeyPair, MerkleHashBuilder, SHA3Hasher, SignSchema} from '../../core/crypto';
import {Convert, RawArray} from '../../core/format';
import {AggregateBondedTransactionBuilder} from '../../infrastructure/catbuffer/AggregateBondedTransactionBuilder';
import {AggregateCompleteTransactionBuilder} from '../../infrastructure/catbuffer/AggregateCompleteTransactionBuilder';
Expand Down Expand Up @@ -373,10 +373,22 @@ export class AggregateTransaction extends Transaction {
* @returns {Uint8Array}
*/
private calculateInnerTransactionHash(): Uint8Array {
const builder = new MerkleHashBuilder(SHA3Hasher.createHasher);
// Note: Transaction hashing *always* uses SHA3
const hasher = SHA3Hasher.createHasher(32, SignSchema.SHA3);
const builder = new MerkleHashBuilder(32, SignSchema.SHA3);
this.innerTransactions.forEach((transaction) => {
builder.update(RawArray.uint8View(sha3_256.arrayBuffer(transaction.toAggregateTransactionBytes())));
const entityHash: Uint8Array = new Uint8Array(32);

// for each embedded transaction hash their body
hasher.reset();
hasher.update(transaction.toAggregateTransactionBytes());
hasher.finalize(entityHash);

// update merkle tree (add transaction hash)
builder.update(entityHash);
});

// calculate root hash with all transactions
return builder.getRootHash();
}

Expand Down
99 changes: 84 additions & 15 deletions src/model/transaction/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,36 @@ import { TransactionType } from './TransactionType';
*/
export abstract class Transaction {

/**
* Transaction header size
*
* Included fields are `size`, `verifiableEntityHeader_Reserved1`,
* `signature`, `signerPublicKey` and `entityBody_Reserved1`.
*
* @var {number}
*/
public static readonly Header_Size: number = 8 + 64 + 32 + 4;

/**
* Index of the transaction *type*
*
* Included fields are the transaction header, `version`
* and `network`
*
* @var {number}
*/
public static readonly Type_Index: number = Transaction.Header_Size + 2;

/**
* Index of the transaction *body*
*
* Included fields are the transaction header, `version`,
* `network`, `type`, `maxFee` and `deadline`
*
* @var {number}
*/
public static readonly Body_Index: number = Transaction.Header_Size + 1 + 1 + 2 + 8 + 8;

/**
* @constructor
* @param type
Expand Down Expand Up @@ -81,29 +111,68 @@ export abstract class Transaction {

/**
* Generate transaction hash hex
*
* @see https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L32
* @see https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L35
* @see https://github.com/nemtech/catapult-server/blob/master/sdk/src/extensions/TransactionExtensions.cpp#L46
* @param {string} transactionPayload HexString Payload
* @param {Array<number>} generationHashBuffer Network generation hash byte
* @param {NetworkType} networkType Catapult network identifier
* @returns {string} Returns Transaction Payload hash
*/
public static createTransactionHash(transactionPayload: string, generationHashBuffer: number[], networkType: NetworkType): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

again, don't think we need networkType anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this one i would rather keep as it affects CosignatureTransaction down the road ; lets wait for more clarity on keccak for removing it ?

const type = parseInt(Convert.uint8ToHex(Convert.hexToUint8(transactionPayload.substring(220, 224)).reverse()), 16);
const byteBuffer = Array.from(Convert.hexToUint8(transactionPayload));
const byteBufferWithoutHeader = byteBuffer.slice(4 + 64 + 32 + 8);
const dataBytes = type === TransactionType.AGGREGATE_BONDED || type === TransactionType.AGGREGATE_COMPLETE ?
generationHashBuffer.concat(byteBufferWithoutHeader.slice(0, 52)) :
generationHashBuffer.concat(byteBufferWithoutHeader);
const signingBytes = byteBuffer
.slice(8, 40) // first half of signature
.concat(byteBuffer
.slice(4 + 4 + 64, 8 + 64 + 32)) // signer
.concat(dataBytes);

const hash = new Uint8Array(32);
const signSchema = SHA3Hasher.resolveSignSchema(networkType);
SHA3Hasher.func(hash, signingBytes, 32, signSchema);
// prepare
const entityHash: Uint8Array = new Uint8Array(32);
const transactionBytes: Uint8Array = Convert.hexToUint8(transactionPayload);

// read transaction type
const typeIdx: number = Transaction.Type_Index;
const typeBytes: Uint8Array = transactionBytes.slice(typeIdx, typeIdx + 2).reverse(); // REVERSED
const entityType: TransactionType = parseInt(Convert.uint8ToHex(typeBytes), 16);
const isAggregateTransaction = [
TransactionType.AGGREGATE_BONDED,
TransactionType.AGGREGATE_COMPLETE,
].find((type: TransactionType) => entityType === type) !== undefined;

// 1) take "R" part of a signature (first 32 bytes)
const signatureR: Uint8Array = transactionBytes.slice(8, 8 + 32);

// 2) add public key to match sign/verify behavior (32 bytes)
const pubKeyIdx: number = signatureR.length;
const publicKey: Uint8Array = transactionBytes.slice(8 + 64, 8 + 64 + 32);

// 3) add generationHash (32 bytes)
const generationHashIdx: number = pubKeyIdx + publicKey.length;
const generationHash: Uint8Array = Uint8Array.from(generationHashBuffer);

// 4) add transaction data without header (EntityDataBuffer)
// @link https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L30
const transactionBodyIdx: number = generationHashIdx + generationHash.length;
let transactionBody: Uint8Array = transactionBytes.slice(Transaction.Header_Size);

// in case of aggregate transactions, we hash only the merkle transaction hash.
if (isAggregateTransaction) {
transactionBody = transactionBytes.slice(Transaction.Header_Size, Transaction.Body_Index + 32);
}

// 5) concatenate binary hash parts
// layout: `signature_R || signerPublicKey || generationHash || EntityDataBuffer`
const entityHashBytes: Uint8Array = new Uint8Array(
signatureR.length
+ publicKey.length
+ generationHash.length
+ transactionBody.length,
);
entityHashBytes.set(signatureR, 0);
entityHashBytes.set(publicKey, pubKeyIdx);
entityHashBytes.set(generationHash, generationHashIdx);
entityHashBytes.set(transactionBody, transactionBodyIdx);

return Convert.uint8ToHex(hash);
// 6) create SHA3 hash of transaction data
// Note: Transaction hashing *always* uses SHA3
SHA3Hasher.func(entityHash, entityHashBytes, 32, SignSchema.SHA3);
return Convert.uint8ToHex(entityHash);
}

/**
Expand Down
20 changes: 10 additions & 10 deletions test/core/crypto/MerkleHashBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@
*/

import { expect } from 'chai';
import { MerkleHashBuilder, SHA3Hasher } from '../../../src/core/crypto';
import { MerkleHashBuilder, SHA3Hasher, SignSchema } from '../../../src/core/crypto';
import { Convert } from '../../../src/core/format';

describe('MerkleHashBuilder tests', () => {
it('Zero Value', () => {
describe('MerkleHashBuilder should', () => {
it('fill 0s for empty merkle tree', () => {
// Arrange:
const builder = new MerkleHashBuilder(SHA3Hasher.createHasher);
const builder = new MerkleHashBuilder(32, SignSchema.SHA3);

const rootHash = builder.getRootHash();

expect(Convert.uint8ToHex(rootHash)).equal('0000000000000000000000000000000000000000000000000000000000000000');

});

it('One Value', () => {
it('return first hash given single child', () => {
// Arrange:
const builder = new MerkleHashBuilder(SHA3Hasher.createHasher);
const builder = new MerkleHashBuilder(32, SignSchema.SHA3);

builder.update(Convert.hexToUint8('215B158F0BD416B596271BCE527CD9DC8E4A639CC271D896F9156AF6F441EEB9'));

Expand All @@ -41,9 +41,9 @@ describe('MerkleHashBuilder tests', () => {

});

it('Two Values', () => {
it('create correct merkle hash given two children', () => {
// Arrange:
const builder = new MerkleHashBuilder(SHA3Hasher.createHasher);
const builder = new MerkleHashBuilder(32, SignSchema.SHA3);

builder.update(Convert.hexToUint8('215b158f0bd416b596271bce527cd9dc8e4a639cc271d896f9156af6f441eeb9'));
builder.update(Convert.hexToUint8('976c5ce6bf3f797113e5a3a094c7801c885daf783c50563ffd3ca6a5ef580e25'));
Expand All @@ -54,9 +54,9 @@ describe('MerkleHashBuilder tests', () => {

});

it('Three Values', () => {
it('create correct merkle hash given three children', () => {
// Arrange:
const builder = new MerkleHashBuilder(SHA3Hasher.createHasher);
const builder = new MerkleHashBuilder(32, SignSchema.SHA3);

builder.update(Convert.hexToUint8('215b158f0bd416b596271bce527cd9dc8e4a639cc271d896f9156af6f441eeb9'));
builder.update(Convert.hexToUint8('976c5ce6bf3f797113e5a3a094c7801c885daf783c50563ffd3ca6a5ef580e25'));
Expand Down
Loading