Skip to content

Commit 326b024

Browse files
committed
feat: port the visible logic from the C++ codebase
0 parents  commit 326b024

File tree

1 file changed

+251
-0
lines changed

1 file changed

+251
-0
lines changed

gobject-hash-debugger.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
"use strict";
2+
3+
// Adapted from
4+
// github.com/dashpay/dash/tree/develop/src/governance/common.cpp
5+
6+
// USAGE
7+
// node ./gobject-hash-debugger.js
8+
9+
const DV_LE = true;
10+
// const DV_BE = false;
11+
12+
var GObj = module.exports;
13+
14+
GObj._type = 0b0000010; // from SER_GETHASH (bitwise enum)
15+
GObj._typeBytes = Uint8Array.from([0b0000010]);
16+
GObj._protocalVersion = 70231; // 0x00011257 (BE) => 0x57120100 (LE)
17+
GObj._protocalVersionBytes = Uint8Array.from([0x57, 0x12, 0x01, 0x00]);
18+
19+
/**
20+
* Only the serialize hex (string) form is canonical.
21+
* Typically the serialization sorts keys in lexicographical order.
22+
* Example:
23+
* {
24+
* "end_epoch": 1721285247,
25+
* "name": "test-proposal-4",
26+
* "payment_address": "yM7M4YJFv58hgea9FUxLtjKBpKXCsjxWNX",
27+
* "payment_amount": 100,
28+
* "start_epoch": 1721275247,
29+
* "type": 1,
30+
* "url": "https://www.dashcentral.org/p/test-proposal-4"
31+
* }
32+
* @typedef {GObjectData}
33+
* @prop {Uint53} end_epoch - whole seconds since epoch (like web-standard `exp`)
34+
* @prop {String} name - kebab case (no spaces)
35+
* @prop {String} payment_address - base58-encoded p2pkh
36+
* @prop {String} payment_amount - in whole DASH
37+
* @prop {Uint53} start_epoch - whole seconds since epoch (like web-standard `iat`)
38+
* @prop {Uint32} type - TODO
39+
* @prop {Uint32} url - conventionally dashcentral, with page the same as the 'name'
40+
*/
41+
42+
/**
43+
* This serialization is used exclusively for creating a hash to place in the OP_RETURN memo
44+
* of the collateral transaction.
45+
*
46+
* As such, it does NOT match the MN gobject serialization.
47+
* - NO collateral tx id (this is for that)
48+
* - NO masternodeOutpoint (this is for that)
49+
* - NO bls data signature (happens on MN)
50+
*
51+
* However, it does include all pieces of data required to verify authenticity from proposer.
52+
*
53+
* @typedef GObject
54+
* @prop {Uint8Array} hashParent - typically null / all 0s (32 bytes)
55+
* @prop {Uint32} revision - typically 1 or 2, etc (4 bytes)
56+
* @prop {Uint53} time - seconds since epoch (8 bytes)
57+
* @prop {String} hexJson - variable
58+
* @prop {null} [masternodeOutpoint] - ??
59+
* @prop {null} [collateralTxOutputIndex] - 4 bytes of 0xffs
60+
* @prop {null} [collateralTxId] - 32 bytes of 0x00s
61+
* @prop {null} [collateralTxOutputIndex] - 4 bytes of 0xffs
62+
* @prop {null} [signature] - 0 bytes
63+
*/
64+
GObj.serializeForCollateralTx = function (gobj) {
65+
let dataLen = 32 + 4 + 8 + gobj.hexJson.length + 36 + 0;
66+
67+
// ORIGINAL C++ CODE:
68+
// CHashWriter ss(SER_GETHASH, PROTOCOL_VERSION);
69+
// ss << hashParent;
70+
// ss << revision;
71+
// ss << time;
72+
// ss << HexStr(vchData);
73+
// ss << masternodeOutpoint << uint8_t{} << 0xffffffff; // adding dummy values here to match old hashing
74+
// ss << vchSig;
75+
// return ss.GetHash();
76+
77+
let bytes = new Uint8Array(dataLen);
78+
let dv = new DataView(bytes.buffer);
79+
80+
// IMPORTANT
81+
// dv.set and bytes.set SWAP THE ORDER of VALUE and OFFSET !!!
82+
let offset = 0;
83+
84+
bytes.set(gobj.hashParent, offset); // TODO swap byte order or no?
85+
offset += 32; // 32
86+
87+
dv.setInt32(offset, gobj.revision, DV_LE);
88+
offset += 4; // 36
89+
90+
{
91+
let time = BigInt(gobj.time);
92+
dv.setBigInt64(36, time, DV_LE);
93+
offset += 8; // 44
94+
}
95+
96+
{
97+
let encoder = new TextEncoder();
98+
let hexBytes = encoder.encode(gobj.hexJson);
99+
bytes.set(hexBytes, offset);
100+
offset += hexBytes.length; // 44 + n
101+
}
102+
103+
// 'bytes' is zero-filled, and there is no masternodeOutpoint yet
104+
// bytes.set(gobj.masternodeOutpointTxId, offset);
105+
offset += 32; // 76 + n
106+
107+
{
108+
let masternodeOutpointIndex = 0xffffffff;
109+
dv.setUint32(offset, masternodeOutpointIndex, DV_LE);
110+
offset += 4; // 80 + n
111+
}
112+
113+
// no BLS signature, so no bytes
114+
// bytes.set(gobj.signature, offset);
115+
// offset += 32 // 112 + n
116+
offset += 0; // 80 + n
117+
118+
return bytes;
119+
};
120+
121+
function bytesToHex(bytes) {
122+
let hexes = [];
123+
for (let i = 0; i < bytes.length; i += 1) {
124+
let b = bytes[i];
125+
let h = b.toString(16);
126+
h = h.padStart(2, "0");
127+
hexes.push(h);
128+
}
129+
let hex = hexes.join("");
130+
131+
return hex;
132+
}
133+
134+
function hexToBytes(hex) {
135+
let len = hex.length / 2;
136+
let bytes = new Uint8Array(len);
137+
let j = 0;
138+
for (let i = 0; i < hex.length; i += 2) {
139+
let h = hex.substr(i, 2);
140+
let b = parseInt(h, 16);
141+
bytes[j] = b;
142+
j += 1;
143+
}
144+
145+
return bytes;
146+
}
147+
148+
/** @typedef {Number} Uint8 */
149+
/** @typedef {Number} Uint32 */
150+
/** @typedef {Number} Uint53 */
151+
152+
async function main() {
153+
// Taken from
154+
// 2024-07-18T04:04:22Z gobject_prepare -- params: 0 1 1721275147 7b2273746172745f65706f6368223a313732313237353234372c22656e645f65706f6368223a313732313238353234372c226e616d65223a22746573742d70726f706f73616c2d34222c227061796d656e745f61646472657373223a22794d374d34594a4676353868676561394655784c746a4b42704b5843736a78574e58222c227061796d656e745f616d6f756e74223a3130302c2274797065223a312c2275726c223a2268747470733a2f2f7777772e6461736863656e7472616c2e6f72672f702f746573742d70726f706f73616c2d34227d, data: {"start_epoch":1721275247,"end_epoch":1721285247,"name":"test-proposal-4","payment_address":"yM7M4YJFv58hgea9FUxLtjKBpKXCsjxWNX","payment_amount":100,"type":1,"url":"https://www.dashcentral.org/p/test-proposal-4"}, hash: a9f2d073c2e6c80c340f15580fbfd622e8d74f4c6719708560bb94b259ae7e25
155+
let gobj = {
156+
hashParent: new Uint8Array(32),
157+
revision: 1,
158+
time: 1721275147,
159+
hexJson:
160+
"7b2273746172745f65706f6368223a313732313237353234372c22656e645f65706f6368223a313732313238353234372c226e616d65223a22746573742d70726f706f73616c2d34222c227061796d656e745f61646472657373223a22794d374d34594a4676353868676561394655784c746a4b42704b5843736a78574e58222c227061796d656e745f616d6f756e74223a3130302c2274797065223a312c2275726c223a2268747470733a2f2f7777772e6461736863656e7472616c2e6f72672f702f746573742d70726f706f73616c2d34227d",
161+
};
162+
let knownHash =
163+
"a9f2d073c2e6c80c340f15580fbfd622e8d74f4c6719708560bb94b259ae7e25";
164+
165+
let gobjCollateralBytes = GObj.serializeForCollateralTx(gobj);
166+
{
167+
let gobjCollateralHex = bytesToHex(gobjCollateralBytes);
168+
169+
console.log("Parsed Hex:");
170+
let offset = 0;
171+
172+
let hashParent = gobjCollateralHex.substr(offset, 2 * 32);
173+
offset += 2 * 32;
174+
logLine64(hashParent, "# hashParent (??LE/BE??)");
175+
176+
let revision = gobjCollateralHex.substr(offset, 2 * 4);
177+
offset += 2 * 4;
178+
logLine64(revision, `# revision (LE)`, gobj.revision);
179+
180+
let time = gobjCollateralHex.substr(offset, 2 * 8);
181+
offset += 2 * 8;
182+
logLine64(time, `# time (LE)`, gobj.time);
183+
184+
let hexJson = gobjCollateralHex.substr(offset, 2 * gobj.hexJson.length);
185+
offset += 2 * gobj.hexJson.length;
186+
logLine64(hexJson, `# hexJson (not json utf8 bytes)`);
187+
188+
let txId = gobjCollateralHex.substr(offset, 2 * 32);
189+
offset += 2 * 32;
190+
logLine64(txId, ` # masternodeOutpointId (??LE/BE??)`);
191+
192+
let txOut = gobjCollateralHex.substr(offset, 2 * 4);
193+
offset += 2 * 4;
194+
logLine64(txOut, ` # masternodeOutpointIndex (LE)`);
195+
196+
let sig = gobjCollateralHex.substr(offset, 2 * 32);
197+
offset += 2 * 0;
198+
logLine64(sig, " # signature (0 bytes)");
199+
200+
console.log("");
201+
console.log("Full Hex");
202+
console.log(gobjCollateralHex);
203+
}
204+
205+
console.log("");
206+
207+
let hashBytes;
208+
{
209+
let hash1 = await crypto.subtle.digest("SHA-256", gobjCollateralBytes);
210+
let hash2 = await crypto.subtle.digest("SHA-256", hash1);
211+
212+
let hash1Bytes = new Uint8Array(hash1);
213+
let hash1Hex = bytesToHex(hash1Bytes);
214+
console.log(hash1Hex, "(single hash)");
215+
216+
hashBytes = new Uint8Array(hash2);
217+
let hashHex = bytesToHex(hashBytes);
218+
console.log(hashHex, "(double hash)");
219+
if (hashHex !== knownHash) {
220+
throw new Error(
221+
`known hash doesn't match generated hash:\n ${hashHex}\n ${knownHash} (expected)`,
222+
);
223+
}
224+
}
225+
226+
return hashBytes;
227+
}
228+
229+
function logLine64(str, comment, value) {
230+
for (let i = 0; i < str.length; i += 64) {
231+
let line = str.substring(i, i + 64);
232+
if (i === 0) {
233+
if (typeof value !== "undefined") {
234+
line = `${line} (${value})`;
235+
}
236+
line = line.padEnd(64, " ");
237+
console.info(line, comment);
238+
} else {
239+
console.info(line);
240+
}
241+
}
242+
}
243+
244+
main()
245+
.then(function () {
246+
console.info("Sweet, Sweet Victory!");
247+
})
248+
.catch(function (err) {
249+
console.error(`Not there yet: ${err.message}`);
250+
process.exit(1);
251+
});

0 commit comments

Comments
 (0)