Skip to content

Commit 677016a

Browse files
author
Greg S
committed
#356: added hashing of merkle transactions hash for aggregates ; added more unit tests for createTransactionHash
1 parent 60977ee commit 677016a

File tree

2 files changed

+184
-3
lines changed

2 files changed

+184
-3
lines changed

src/model/transaction/Transaction.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,32 @@ export abstract class Transaction {
3636
/**
3737
* Transaction header size
3838
*
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+
*
3962
* @var {number}
4063
*/
41-
public static readonly Header_Size = 8 + 64 + 32 + 4;
64+
public static readonly Body_Index: number = Transaction.Header_Size + 1 + 1 + 2 + 8 + 8;
4265

4366
/**
4467
* @constructor
@@ -101,9 +124,17 @@ export abstract class Transaction {
101124

102125
// prepare
103126
const entityHash: Uint8Array = new Uint8Array(32);
104-
const signSchema: SignSchema = SHA3Hasher.resolveSignSchema(networkType);
105127
const transactionBytes: Uint8Array = Convert.hexToUint8(transactionPayload);
106128

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+
107138
// 1) take "R" part of a signature (first 32 bytes)
108139
const signatureR: Uint8Array = transactionBytes.slice(8, 8 + 32);
109140

@@ -118,7 +149,12 @@ export abstract class Transaction {
118149
// 4) add transaction data without header (EntityDataBuffer)
119150
// @link https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L30
120151
const transactionBodyIdx: number = generationHashIdx + generationHash.length;
121-
const transactionBody: Uint8Array = transactionBytes.slice(Transaction.Header_Size);
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+
}
122158

123159
// 5) concatenate binary hash parts
124160
// layout: `signature_R || signerPublicKey || generationHash || EntityDataBuffer`
@@ -134,6 +170,7 @@ export abstract class Transaction {
134170
entityHashBytes.set(transactionBody, transactionBodyIdx);
135171

136172
// 6) create SHA3 or Keccak hash depending on `signSchema`
173+
const signSchema: SignSchema = SHA3Hasher.resolveSignSchema(networkType);
137174
SHA3Hasher.func(entityHash, entityHashBytes, 32, signSchema);
138175
return Convert.uint8ToHex(entityHash);
139176
}

test/model/transaction/Transaction.spec.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,125 @@ describe('Transaction', () => {
240240
+ 'A0860100000000002AD8FC018D9A49E100056576696173'
241241
);
242242

243+
const knownAggregatePayload = (
244+
'0801000000000000AC1F3E0EE2C16F465CDC2E091DC44D6EB55F7FE3988A5F21'
245+
+ '309DF479BE6D3F0033E155695FB1133EA0EA64A67C1EDC2B430CFAF9722AF36B'
246+
+ 'AE84DBDB1C8F1509C2F93346E27CE6AD1A9F8F5E3066F8326593A406BDF357AC'
247+
+ 'B041E2F9AB402EFE000000000190414200000000000000006BA50FB91A000000'
248+
+ 'EA8F8301E7EDFD701F62E1DC1601ABDE22E5FCD11C9C7E7A01B87F8DFB6B62B0'
249+
+ '60000000000000005D00000000000000C2F93346E27CE6AD1A9F8F5E3066F832'
250+
+ '6593A406BDF357ACB041E2F9AB402EFE00000000019054419050B9837EFAB4BB'
251+
+ 'E8A4B9BB32D812F9885C00D8FC1650E142000D000000000000746573742D6D65'
252+
+ '7373616765000000'
253+
);
254+
243255
// expected values
244256
const knownHash_sha3 = '709373248659274C5933BEA2920942D6C7B48B9C2DA4BAEE233510E71495931F';
245257
const knownHash_keccak = '787423372BEC0CB2BE3EEA58E773074E121989AF29E5E5BD9EE660C1E3A0AF93';
246258
const generationHashBytes = Array.from(Convert.hexToUint8('988C4CDCE4D188013C13DE7914C7FD4D626169EF256722F61C52EFBE06BD5A2C'));
259+
const generationHashBytes_mt = Array.from(Convert.hexToUint8('17FA4747F5014B50413CCF968749604D728D7065DC504291EEE556899A534CBB'));
260+
261+
it ('create different hash given different signatures', () => {
262+
const hash1 = Transaction.createTransactionHash(
263+
knownPayload,
264+
generationHashBytes,
265+
NetworkType.MIJIN_TEST,
266+
);
267+
268+
// modify signature part of the payload ; this must affect produced hash
269+
const tamperedSig = knownPayload.substr(0, 16) + '12' + knownPayload.substr(18);
270+
const hash2 = Transaction.createTransactionHash(
271+
tamperedSig, // replaced two first bytes of signature
272+
generationHashBytes,
273+
NetworkType.MIJIN_TEST,
274+
);
275+
276+
expect(hash1).to.not.equal(hash2);
277+
});
278+
279+
it ('create different hash given different signer public key', () => {
280+
const hash1 = Transaction.createTransactionHash(
281+
knownPayload,
282+
generationHashBytes,
283+
NetworkType.MIJIN_TEST,
284+
);
285+
286+
// modify signer public key part of the payload ; this must affect produced hash
287+
const tamperedSigner = knownPayload.substr(0, 16 + 128) + '12' + knownPayload.substr(16 + 128 + 2);
288+
const hash2 = Transaction.createTransactionHash(
289+
tamperedSigner, // replaced two first bytes of signer public key
290+
generationHashBytes,
291+
NetworkType.MIJIN_TEST,
292+
);
293+
294+
expect(hash1).to.not.equal(hash2);
295+
});
296+
297+
it ('create different hash given different generation hash', () => {
298+
const hash1 = Transaction.createTransactionHash(
299+
knownPayload,
300+
generationHashBytes,
301+
NetworkType.MIJIN_TEST,
302+
);
303+
304+
const hash2 = Transaction.createTransactionHash(
305+
knownPayload,
306+
generationHashBytes_mt, // uses different generation hash
307+
NetworkType.MIJIN_TEST,
308+
);
309+
310+
expect(hash1).to.not.equal(hash2);
311+
});
312+
313+
it ('create different hash given different transaction body', () => {
314+
const hash1 = Transaction.createTransactionHash(
315+
knownPayload,
316+
generationHashBytes,
317+
NetworkType.MIJIN_TEST,
318+
);
319+
320+
// modify "transaction body" part of payload ; this must affect produced transaction hash
321+
const tamperedBody = knownAggregatePayload.substr(0, Transaction.Body_Index * 2)
322+
+ '12' + knownAggregatePayload.substr(Transaction.Body_Index * 2 + 2);
323+
const hash2 = Transaction.createTransactionHash(
324+
tamperedBody,
325+
generationHashBytes, // uses different generation hash
326+
NetworkType.MIJIN_TEST,
327+
);
328+
329+
expect(hash1).to.not.equal(hash2);
330+
});
331+
332+
it ('create same hash given same payloads', () => {
333+
const hash1 = Transaction.createTransactionHash(
334+
knownPayload,
335+
generationHashBytes,
336+
NetworkType.MIJIN_TEST,
337+
);
338+
const hash2 = Transaction.createTransactionHash(
339+
knownPayload,
340+
generationHashBytes,
341+
NetworkType.MIJIN_TEST,
342+
);
343+
344+
expect(hash1).to.equal(hash2);
345+
});
346+
347+
it ('create different hash given different signature schemas', () => {
348+
const hash1 = Transaction.createTransactionHash(
349+
knownPayload,
350+
generationHashBytes,
351+
NetworkType.MIJIN_TEST, // SHA3
352+
);
353+
354+
const hash2 = Transaction.createTransactionHash(
355+
knownPayload,
356+
generationHashBytes,
357+
NetworkType.TEST_NET, // KECCAK
358+
);
359+
360+
expect(hash1).to.not.equal(hash2);
361+
});
247362

248363
it('create correct SHA3 transaction hash given network type MIJIN or MIJIN_TEST', () => {
249364
const hash1 = Transaction.createTransactionHash(
@@ -276,6 +391,35 @@ describe('Transaction', () => {
276391
expect(hash1).to.equal(knownHash_keccak);
277392
expect(hash2).to.equal(knownHash_keccak);
278393
});
394+
395+
it('hash only merkle transaction hash for aggregate transactions', () => {
396+
const hash1 = Transaction.createTransactionHash(
397+
knownAggregatePayload,
398+
generationHashBytes,
399+
NetworkType.MIJIN_TEST,
400+
);
401+
402+
// modify end of payload ; this must not affect produced transaction hash
403+
// this test is valid only for Aggregate Transactions
404+
const tamperedSize = '12' + knownAggregatePayload.substr(2);
405+
const hashTamperedBody = Transaction.createTransactionHash(
406+
tamperedSize, // replace in size (header change should not affect hash)
407+
generationHashBytes,
408+
NetworkType.MIJIN_TEST,
409+
);
410+
411+
// modify "merkle hash" part of payload ; this must affect produced transaction hash
412+
const tamperedPayload = knownAggregatePayload.substr(0, Transaction.Body_Index * 2)
413+
+ '12' + knownAggregatePayload.substr(Transaction.Body_Index * 2 + 2);
414+
const hashTamperedMerkle = Transaction.createTransactionHash(
415+
tamperedPayload, // replace in merkle hash (will affect hash)
416+
generationHashBytes,
417+
NetworkType.MIJIN_TEST,
418+
);
419+
420+
expect(hash1).to.equal(hashTamperedBody);
421+
expect(hash1).to.not.equal(hashTamperedMerkle);
422+
});
279423
});
280424
});
281425

0 commit comments

Comments
 (0)