Skip to content

Commit 181d926

Browse files
authored
Merge pull request #359 from evias/refactor-txhash
Refactor createTransactionHash and MerkleHashBuilder
2 parents 579f589 + 75432e3 commit 181d926

File tree

5 files changed

+309
-47
lines changed

5 files changed

+309
-47
lines changed

src/core/crypto/MerkleHashBuilder.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,46 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
16+
import { SHA3Hasher } from './SHA3Hasher';
1717
import { SignSchema } from './SignSchema';
1818

1919
export class MerkleHashBuilder {
2020

21-
hashes: Uint8Array[] = new Array<Uint8Array>();
22-
hasherFactory: any;
23-
signSchema: SignSchema;
24-
length: number;
21+
/**
22+
* The list of hashes used to calculate root hash.
23+
*
24+
* @var {Uint8Array}
25+
*/
26+
protected hashes: Uint8Array[] = new Array<Uint8Array>();
2527

2628
/**
2729
* Constructor
28-
* @param hasherFactory Hasher (SHA3_256)
2930
* @param signSchema Sign schema
3031
* @param length Hash size
3132
*/
32-
constructor(hasherFactory: any, signSchema: SignSchema = SignSchema.SHA3, length: number = 32) {
33-
this.hasherFactory = hasherFactory;
34-
this.signSchema = signSchema;
35-
this.length = length;
33+
constructor(/**
34+
* Length of produced merkle hash in bytes.
35+
*
36+
* @var {number}
37+
*/
38+
public readonly length: number,
39+
/**
40+
* Signature schema used (hash algorithm diff)
41+
*
42+
* @var {SignSchema}
43+
*/
44+
public readonly signSchema: SignSchema) {
3645
}
3746

38-
/** @internal
47+
/**
3948
* Hash inner transactions
49+
*
50+
* @internal
4051
* @param hashes Inner transaction hashes
52+
* @return {Uint8Array}
4153
*/
4254
protected hash(hashes: Uint8Array[]): Uint8Array {
43-
const hasher = this.hasherFactory(this.length, this.signSchema);
55+
const hasher = SHA3Hasher.createHasher(this.length, this.signSchema);
4456
hasher.reset();
4557

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

55-
/** @internal
56-
* Get root hash of Merkle Trees
57-
* @param hashes Inner transaction hashes
67+
/**
68+
* Get root hash of Merkle Tree
69+
*
70+
* @internal
71+
* @param {Uint8Array[]} hashes Inner transaction hashes
72+
* @return {Uint8Array}
5873
*/
5974
protected calculateRootHash(hashes: Uint8Array[]): Uint8Array {
6075

@@ -80,18 +95,22 @@ export class MerkleHashBuilder {
8095
}
8196

8297
/**
83-
* Return root hash from Merkle tree
98+
* Get root hash of Merkle tree
99+
*
100+
* @return {Uint8Array}
84101
*/
85102
public getRootHash(): Uint8Array {
86103
return this.calculateRootHash(this.hashes);
87104
}
88105

89106
/**
90-
* Update hashes array
107+
* Update hashes array (add hash)
108+
*
91109
* @param hash Inner transaction hash buffer
110+
* @return {MerkleHashBuilder}
92111
*/
93-
public update(hash: Uint8Array): void {
112+
public update(hash: Uint8Array): MerkleHashBuilder {
94113
this.hashes.push(hash);
114+
return this;
95115
}
96-
97116
}

src/model/transaction/AggregateTransaction.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { sha3_256 } from 'js-sha3';
18-
import {KeyPair, MerkleHashBuilder, SHA3Hasher} from '../../core/crypto';
18+
import {KeyPair, MerkleHashBuilder, SHA3Hasher, SignSchema} from '../../core/crypto';
1919
import {Convert, RawArray} from '../../core/format';
2020
import {AggregateBondedTransactionBuilder} from '../../infrastructure/catbuffer/AggregateBondedTransactionBuilder';
2121
import {AggregateCompleteTransactionBuilder} from '../../infrastructure/catbuffer/AggregateCompleteTransactionBuilder';
@@ -373,10 +373,22 @@ export class AggregateTransaction extends Transaction {
373373
* @returns {Uint8Array}
374374
*/
375375
private calculateInnerTransactionHash(): Uint8Array {
376-
const builder = new MerkleHashBuilder(SHA3Hasher.createHasher);
376+
// Note: Transaction hashing *always* uses SHA3
377+
const hasher = SHA3Hasher.createHasher(32, SignSchema.SHA3);
378+
const builder = new MerkleHashBuilder(32, SignSchema.SHA3);
377379
this.innerTransactions.forEach((transaction) => {
378-
builder.update(RawArray.uint8View(sha3_256.arrayBuffer(transaction.toAggregateTransactionBytes())));
380+
const entityHash: Uint8Array = new Uint8Array(32);
381+
382+
// for each embedded transaction hash their body
383+
hasher.reset();
384+
hasher.update(transaction.toAggregateTransactionBytes());
385+
hasher.finalize(entityHash);
386+
387+
// update merkle tree (add transaction hash)
388+
builder.update(entityHash);
379389
});
390+
391+
// calculate root hash with all transactions
380392
return builder.getRootHash();
381393
}
382394

src/model/transaction/Transaction.ts

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,36 @@ import { TransactionType } from './TransactionType';
3333
*/
3434
export abstract class Transaction {
3535

36+
/**
37+
* Transaction header size
38+
*
39+
* Included fields are `size`, `verifiableEntityHeader_Reserved1`,
40+
* `signature`, `signerPublicKey` and `entityBody_Reserved1`.
41+
*
42+
* @var {number}
43+
*/
44+
public static readonly Header_Size: number = 8 + 64 + 32 + 4;
45+
46+
/**
47+
* Index of the transaction *type*
48+
*
49+
* Included fields are the transaction header, `version`
50+
* and `network`
51+
*
52+
* @var {number}
53+
*/
54+
public static readonly Type_Index: number = Transaction.Header_Size + 2;
55+
56+
/**
57+
* Index of the transaction *body*
58+
*
59+
* Included fields are the transaction header, `version`,
60+
* `network`, `type`, `maxFee` and `deadline`
61+
*
62+
* @var {number}
63+
*/
64+
public static readonly Body_Index: number = Transaction.Header_Size + 1 + 1 + 2 + 8 + 8;
65+
3666
/**
3767
* @constructor
3868
* @param type
@@ -81,29 +111,68 @@ export abstract class Transaction {
81111

82112
/**
83113
* Generate transaction hash hex
114+
*
115+
* @see https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L32
116+
* @see https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L35
117+
* @see https://github.com/nemtech/catapult-server/blob/master/sdk/src/extensions/TransactionExtensions.cpp#L46
84118
* @param {string} transactionPayload HexString Payload
85119
* @param {Array<number>} generationHashBuffer Network generation hash byte
86120
* @param {NetworkType} networkType Catapult network identifier
87121
* @returns {string} Returns Transaction Payload hash
88122
*/
89123
public static createTransactionHash(transactionPayload: string, generationHashBuffer: number[], networkType: NetworkType): string {
90-
const type = parseInt(Convert.uint8ToHex(Convert.hexToUint8(transactionPayload.substring(220, 224)).reverse()), 16);
91-
const byteBuffer = Array.from(Convert.hexToUint8(transactionPayload));
92-
const byteBufferWithoutHeader = byteBuffer.slice(4 + 64 + 32 + 8);
93-
const dataBytes = type === TransactionType.AGGREGATE_BONDED || type === TransactionType.AGGREGATE_COMPLETE ?
94-
generationHashBuffer.concat(byteBufferWithoutHeader.slice(0, 52)) :
95-
generationHashBuffer.concat(byteBufferWithoutHeader);
96-
const signingBytes = byteBuffer
97-
.slice(8, 40) // first half of signature
98-
.concat(byteBuffer
99-
.slice(4 + 4 + 64, 8 + 64 + 32)) // signer
100-
.concat(dataBytes);
101124

102-
const hash = new Uint8Array(32);
103-
const signSchema = SHA3Hasher.resolveSignSchema(networkType);
104-
SHA3Hasher.func(hash, signingBytes, 32, signSchema);
125+
// prepare
126+
const entityHash: Uint8Array = new Uint8Array(32);
127+
const transactionBytes: Uint8Array = Convert.hexToUint8(transactionPayload);
128+
129+
// read transaction type
130+
const typeIdx: number = Transaction.Type_Index;
131+
const typeBytes: Uint8Array = transactionBytes.slice(typeIdx, typeIdx + 2).reverse(); // REVERSED
132+
const entityType: TransactionType = parseInt(Convert.uint8ToHex(typeBytes), 16);
133+
const isAggregateTransaction = [
134+
TransactionType.AGGREGATE_BONDED,
135+
TransactionType.AGGREGATE_COMPLETE,
136+
].find((type: TransactionType) => entityType === type) !== undefined;
137+
138+
// 1) take "R" part of a signature (first 32 bytes)
139+
const signatureR: Uint8Array = transactionBytes.slice(8, 8 + 32);
140+
141+
// 2) add public key to match sign/verify behavior (32 bytes)
142+
const pubKeyIdx: number = signatureR.length;
143+
const publicKey: Uint8Array = transactionBytes.slice(8 + 64, 8 + 64 + 32);
144+
145+
// 3) add generationHash (32 bytes)
146+
const generationHashIdx: number = pubKeyIdx + publicKey.length;
147+
const generationHash: Uint8Array = Uint8Array.from(generationHashBuffer);
148+
149+
// 4) add transaction data without header (EntityDataBuffer)
150+
// @link https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L30
151+
const transactionBodyIdx: number = generationHashIdx + generationHash.length;
152+
let transactionBody: Uint8Array = transactionBytes.slice(Transaction.Header_Size);
153+
154+
// in case of aggregate transactions, we hash only the merkle transaction hash.
155+
if (isAggregateTransaction) {
156+
transactionBody = transactionBytes.slice(Transaction.Header_Size, Transaction.Body_Index + 32);
157+
}
158+
159+
// 5) concatenate binary hash parts
160+
// layout: `signature_R || signerPublicKey || generationHash || EntityDataBuffer`
161+
const entityHashBytes: Uint8Array = new Uint8Array(
162+
signatureR.length
163+
+ publicKey.length
164+
+ generationHash.length
165+
+ transactionBody.length,
166+
);
167+
entityHashBytes.set(signatureR, 0);
168+
entityHashBytes.set(publicKey, pubKeyIdx);
169+
entityHashBytes.set(generationHash, generationHashIdx);
170+
entityHashBytes.set(transactionBody, transactionBodyIdx);
105171

106-
return Convert.uint8ToHex(hash);
172+
// 6) create SHA3 hash of transaction data
173+
// Note: Transaction hashing *always* uses SHA3
174+
SHA3Hasher.func(entityHash, entityHashBytes, 32, SignSchema.SHA3);
175+
return Convert.uint8ToHex(entityHash);
107176
}
108177

109178
/**

test/core/crypto/MerkleHashBuilder.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,23 @@
1515
*/
1616

1717
import { expect } from 'chai';
18-
import { MerkleHashBuilder, SHA3Hasher } from '../../../src/core/crypto';
18+
import { MerkleHashBuilder, SHA3Hasher, SignSchema } from '../../../src/core/crypto';
1919
import { Convert } from '../../../src/core/format';
2020

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

2626
const rootHash = builder.getRootHash();
2727

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

3030
});
3131

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

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

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

4242
});
4343

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

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

5555
});
5656

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

6161
builder.update(Convert.hexToUint8('215b158f0bd416b596271bce527cd9dc8e4a639cc271d896f9156af6f441eeb9'));
6262
builder.update(Convert.hexToUint8('976c5ce6bf3f797113e5a3a094c7801c885daf783c50563ffd3ca6a5ef580e25'));

0 commit comments

Comments
 (0)