Skip to content

Commit 60977ee

Browse files
author
Greg S
committed
refactor Transaction.createTransactionHash and add unit test for networkType switch
1 parent c1707f9 commit 60977ee

File tree

2 files changed

+97
-15
lines changed

2 files changed

+97
-15
lines changed

src/model/transaction/Transaction.ts

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

36+
/**
37+
* Transaction header size
38+
*
39+
* @var {number}
40+
*/
41+
public static readonly Header_Size = 8 + 64 + 32 + 4;
42+
3643
/**
3744
* @constructor
3845
* @param type
@@ -81,29 +88,54 @@ export abstract class Transaction {
8188

8289
/**
8390
* Generate transaction hash hex
91+
*
92+
* @see https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L32
93+
* @see https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L35
94+
* @see https://github.com/nemtech/catapult-server/blob/master/sdk/src/extensions/TransactionExtensions.cpp#L46
8495
* @param {string} transactionPayload HexString Payload
8596
* @param {Array<number>} generationHashBuffer Network generation hash byte
8697
* @param {NetworkType} networkType Catapult network identifier
8798
* @returns {string} Returns Transaction Payload hash
8899
*/
89100
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);
101101

102-
const hash = new Uint8Array(32);
103-
const signSchema = SHA3Hasher.resolveSignSchema(networkType);
104-
SHA3Hasher.func(hash, signingBytes, 32, signSchema);
102+
// prepare
103+
const entityHash: Uint8Array = new Uint8Array(32);
104+
const signSchema: SignSchema = SHA3Hasher.resolveSignSchema(networkType);
105+
const transactionBytes: Uint8Array = Convert.hexToUint8(transactionPayload);
106+
107+
// 1) take "R" part of a signature (first 32 bytes)
108+
const signatureR: Uint8Array = transactionBytes.slice(8, 8 + 32);
109+
110+
// 2) add public key to match sign/verify behavior (32 bytes)
111+
const pubKeyIdx: number = signatureR.length;
112+
const publicKey: Uint8Array = transactionBytes.slice(8 + 64, 8 + 64 + 32);
113+
114+
// 3) add generationHash (32 bytes)
115+
const generationHashIdx: number = pubKeyIdx + publicKey.length;
116+
const generationHash: Uint8Array = Uint8Array.from(generationHashBuffer);
117+
118+
// 4) add transaction data without header (EntityDataBuffer)
119+
// @link https://github.com/nemtech/catapult-server/blob/master/src/catapult/model/EntityHasher.cpp#L30
120+
const transactionBodyIdx: number = generationHashIdx + generationHash.length;
121+
const transactionBody: Uint8Array = transactionBytes.slice(Transaction.Header_Size);
122+
123+
// 5) concatenate binary hash parts
124+
// layout: `signature_R || signerPublicKey || generationHash || EntityDataBuffer`
125+
const entityHashBytes: Uint8Array = new Uint8Array(
126+
signatureR.length
127+
+ publicKey.length
128+
+ generationHash.length
129+
+ transactionBody.length,
130+
);
131+
entityHashBytes.set(signatureR, 0);
132+
entityHashBytes.set(publicKey, pubKeyIdx);
133+
entityHashBytes.set(generationHash, generationHashIdx);
134+
entityHashBytes.set(transactionBody, transactionBodyIdx);
105135

106-
return Convert.uint8ToHex(hash);
136+
// 6) create SHA3 or Keccak hash depending on `signSchema`
137+
SHA3Hasher.func(entityHash, entityHashBytes, 32, signSchema);
138+
return Convert.uint8ToHex(entityHash);
107139
}
108140

109141
/**

test/model/transaction/Transaction.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { TransactionType } from '../../../src/model/transaction/TransactionType'
2828
import { TransferTransaction } from '../../../src/model/transaction/TransferTransaction';
2929
import { UInt64 } from '../../../src/model/UInt64';
3030
import { TestingAccount } from '../../conf/conf.spec';
31+
import { Convert } from '../../../src/core/format/Convert';
3132

3233
describe('Transaction', () => {
3334
let account: Account;
@@ -227,6 +228,55 @@ describe('Transaction', () => {
227228
expect(transaction.versionToHex()).to.be.equal('0x9001');
228229
});
229230
});
231+
232+
describe('createTransactionHash() should', () => {
233+
234+
// shortcut
235+
const knownPayload = (
236+
'970000000000000075DAC796D500CEFDFBD582BC6E0580401FE6DB02FBEA9367'
237+
+ '3DF47844246CDEA93715EB700F295A459E59D96A2BC6B7E36C79016A96B9FA38'
238+
+ '7E8B8937342FE30C6BE37B726EEE24C4B0E3C943E09A44691553759A89E92C4A'
239+
+ '84BBC4AD9AF5D49C0000000001984E4140420F0000000000E4B580B11A000000'
240+
+ 'A0860100000000002AD8FC018D9A49E100056576696173'
241+
);
242+
243+
// expected values
244+
const knownHash_sha3 = '709373248659274C5933BEA2920942D6C7B48B9C2DA4BAEE233510E71495931F';
245+
const knownHash_keccak = '787423372BEC0CB2BE3EEA58E773074E121989AF29E5E5BD9EE660C1E3A0AF93';
246+
const generationHashBytes = Array.from(Convert.hexToUint8('988C4CDCE4D188013C13DE7914C7FD4D626169EF256722F61C52EFBE06BD5A2C'));
247+
248+
it('create correct SHA3 transaction hash given network type MIJIN or MIJIN_TEST', () => {
249+
const hash1 = Transaction.createTransactionHash(
250+
knownPayload,
251+
generationHashBytes,
252+
NetworkType.MIJIN_TEST,
253+
);
254+
const hash2 = Transaction.createTransactionHash(
255+
knownPayload,
256+
generationHashBytes,
257+
NetworkType.MIJIN,
258+
);
259+
260+
expect(hash1).to.equal(knownHash_sha3);
261+
expect(hash2).to.equal(knownHash_sha3);
262+
});
263+
264+
it('create correct KECCAK transaction hash given network type MAIN_NET or TEST_NET', () => {
265+
const hash1 = Transaction.createTransactionHash(
266+
knownPayload,
267+
generationHashBytes,
268+
NetworkType.TEST_NET,
269+
);
270+
const hash2 = Transaction.createTransactionHash(
271+
knownPayload,
272+
generationHashBytes,
273+
NetworkType.MAIN_NET,
274+
);
275+
276+
expect(hash1).to.equal(knownHash_keccak);
277+
expect(hash2).to.equal(knownHash_keccak);
278+
});
279+
});
230280
});
231281

232282
class FakeTransaction extends Transaction {

0 commit comments

Comments
 (0)