-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmark.js
More file actions
389 lines (334 loc) · 14.7 KB
/
mark.js
File metadata and controls
389 lines (334 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
#!/usr/bin/env node
import { execSync, execFileSync } from 'child_process';
import fs from 'fs';
import path from 'path';
// Check for --verbose or -v flag
const VERBOSE = process.argv.includes('--verbose') || process.argv.includes('-v');
// Debug logging helper - only logs when VERBOSE is true
function debug(...args) {
if (VERBOSE) debug('', ...args);
}
// txo_parser will be dynamically imported as it's an ES module
/**
* Converts a Taproot private key (64-character hex string) to a public key (64-character hex string)
*
* @param {string} privateKey - 64-character hex string representing the private key
* @returns {string} 64-character hex string representing the public key
* @throws {Error} if the private key format is invalid
*/
async function key2pub (privateKey) {
// Validate private key format
if (!/^[0-9a-fA-F]{64}$/.test(privateKey)) {
throw new Error(
'Invalid private key format. Expected 64-character hex string.'
)
}
try {
// Get the public key using @noble/secp256k1
const { getPublicKey } = await import('@noble/secp256k1');
// getPublicKey returns a 33-byte compressed key by default, we need to convert it to 32-byte x-only format
const compressedPubkey = getPublicKey(privateKey, true)
// Remove the first byte (0x02 or 0x03) to get the x coordinate only
const pubkeyX = compressedPubkey.slice(1)
// Convert Uint8Array to hex string
return Array.from(pubkeyX, byte => byte.toString(16).padStart(2, '0')).join('')
} catch (error) {
throw new Error(
`Failed to convert private key to public key: ${error instanceof Error ? error.message : String(error)}`
)
}
}
/**
* Add two or more hexadecimal values together
*
* @param {...string} hexValues - Hexadecimal strings (with or without 0x prefix)
* @returns {string} - Lowercase hexadecimal result (without 0x prefix)
*/
function addHex (...hexValues) {
if (hexValues.length === 0) {
return '0';
}
let sum = 0n;
for (const hex of hexValues) {
// Handle hex strings with or without 0x prefix
const cleanHex = hex.toLowerCase().startsWith('0x') ? hex.slice(2) : hex;
// Convert to BigInt to handle large hex values
try {
sum += BigInt(`0x${cleanHex}`);
} catch (e) {
throw new Error(`Invalid hex value: ${hex}`);
}
}
// Convert back to hex string without 0x prefix and in lowercase
return sum.toString(16).toLowerCase();
}
// Helper function to ensure hex values are padded to 64 characters (32 bytes)
function padHex64 (hex) {
// Remove any 0x prefix if present
const cleanHex = hex.replace(/^0x/, '');
// Pad to 64 characters with leading zeros
return cleanHex.padStart(64, '0');
}
// Helper function to safely add hex values and preserve leading zeros
function safeAddHex (hex1, hex2) {
const result = addHex(hex1, hex2);
return padHex64(result);
}
// Benchmark helper functions
function formatTime (ms) {
if (ms < 1) return `${(ms * 1000).toFixed(2)}μs`;
if (ms < 1000) return `${ms.toFixed(2)}ms`;
return `${(ms / 1000).toFixed(2)}s`;
}
function timeOperation (name, operation) {
const start = performance.now();
try {
const result = operation();
const end = performance.now();
const duration = end - start;
console.log(`BENCHMARK: ${name} took ${formatTime(duration)}`);
return result;
} catch (error) {
const end = performance.now();
const duration = end - start;
console.log(`BENCHMARK: ${name} FAILED after ${formatTime(duration)}`);
throw error;
}
}
async function timeOperationAsync (name, operation) {
const start = performance.now();
try {
const result = await operation();
const end = performance.now();
const duration = end - start;
console.log(`BENCHMARK: ${name} took ${formatTime(duration)}`);
return result;
} catch (error) {
const end = performance.now();
const duration = end - start;
console.log(`BENCHMARK: ${name} FAILED after ${formatTime(duration)}`);
throw error;
}
}
if (VERBOSE) console.log('=== GITMARK DEBUG START ===');
const scriptStart = performance.now();
if (VERBOSE) console.log('Starting script execution at:', new Date().toISOString());
async function main () {
try {
// Git operations
debug('Running git add .');
timeOperation('git add', () => execSync('git add .'));
// Get commit message from first argument or use default
const commitMessage = process.argv[2] || "first";
debug(` Running git commit with message: "${commitMessage}"`);
timeOperation('git commit', () => execFileSync('git', ['commit', '-m', commitMessage]));
debug(' Git operations completed successfully');
const TXOFILE = '.well-known/txo/txo.json';
const NETWORK = 'tbtc4';
debug(` Configuration - TXOFILE: ${TXOFILE}, NETWORK: ${NETWORK}`);
// Get commit hash
debug(' Getting commit hash');
const COMMIT_HASH = timeOperation('get commit hash', () =>
execSync('git log -1 --format=%H').toString().trim()
);
debug(` COMMIT_HASH: ${COMMIT_HASH}`);
// Get private key from git config
debug(' Getting private key from git config');
const PRIVKEY = timeOperation('get private key', () =>
execSync('git config nostr.privkey').toString().trim()
);
debug(` PRIVKEY (truncated): ${PRIVKEY.substring(0, 4)}...${PRIVKEY.substring(PRIVKEY.length - 4)}`);
debug(' Generating public key from private key');
const PUBKEY = await timeOperationAsync('generate initial pubkey', () =>
key2pub(PRIVKEY)
);
debug(` PUBKEY: ${PUBKEY}`);
// Read txo.json file
debug(` Reading TXO file from ${TXOFILE}`);
if (!fs.existsSync(TXOFILE)) {
console.error(`DEBUG: ERROR - File not found: ${TXOFILE}`);
process.exit(1);
}
const { txoFileContent, txoData } = timeOperation('read and parse TXO file', () => {
const content = fs.readFileSync(TXOFILE, 'utf8');
debug(` Raw TXO file content: ${content}`);
const data = JSON.parse(content);
return { txoFileContent: content, txoData: data };
});
debug(` Parsed TXO data, contains ${txoData.length} entries`);
if (txoData.length === 0) {
console.error('DEBUG: ERROR - No TXO entries found in file');
process.exit(1);
}
const lastTxo = txoData[txoData.length - 1];
debug(` Last TXO entry: ${lastTxo}`);
// Extract all commit hashes from txo.json items and sum them
debug(' Extracting and summing commit hashes from TXO entries');
const commitHashSum = timeOperation('process TXO entries and sum commits', () => {
let sum = '';
for (let i = 0; i < txoData.length; i++) {
const txoItem = txoData[i];
debug(` Processing TXO entry ${i + 1}: ${txoItem}`);
const commitMatch = txoItem.match(/commit=([0-9a-f]+)/);
if (commitMatch && commitMatch[1]) {
const currentCommit = commitMatch[1];
debug(` Found commit hash in entry ${i + 1}: ${currentCommit}`);
if (sum) {
debug(` Adding commit hash to existing sum: ${sum} + ${currentCommit}`);
sum = safeAddHex(sum, currentCommit);
} else {
debug(` First commit hash, setting as initial sum: ${currentCommit}`);
sum = currentCommit;
}
debug(` Current commit hash sum after entry ${i + 1}: ${sum}`);
} else {
debug(` No commit hash found in entry ${i + 1}`);
}
}
return sum;
});
debug(` Final commit hash sum from TXO entries: ${commitHashSum || 'EMPTY'}`);
// Calculate private key for signing the transaction (PRIVKEY + commit hashes from JSON)
debug(` Calculating signing key: addhex "${PRIVKEY.substring(0, 4)}...${PRIVKEY.substring(PRIVKEY.length - 4)}" "${commitHashSum || 'EMPTY'}"`);
const SIGNING_KEY = timeOperation('calculate signing key', () =>
commitHashSum ? safeAddHex(PRIVKEY, commitHashSum) : PRIVKEY
);
debug(` SIGNING_KEY (truncated): ${SIGNING_KEY.substring(0, 4)}...${SIGNING_KEY.substring(SIGNING_KEY.length - 4)}`);
debug(' Generating public key from signing key');
const SIGNING_PUBKEY = await timeOperationAsync('generate signing pubkey', () =>
key2pub(SIGNING_KEY)
);
debug(` SIGNING_PUBKEY: ${SIGNING_PUBKEY}`);
// Add current commit hash to the sum for the new destination address
debug(` Adding current commit hash to sum: ${commitHashSum || 'EMPTY'} + ${COMMIT_HASH}`);
const finalCommitHashSum = timeOperation('add current commit to sum', () => {
if (commitHashSum) {
const addHexCommand = `addhex "${commitHashSum}" "${COMMIT_HASH}"`;
debug(` Running command: ${addHexCommand}`);
return safeAddHex(commitHashSum, COMMIT_HASH);
} else {
return COMMIT_HASH;
}
});
debug(` Final commit hash sum including current commit: ${finalCommitHashSum}`);
// Calculate new key by adding hex values with all commit hashes (including current commit)
debug(` Calculating destination key: addhex "${PRIVKEY.substring(0, 4)}...${PRIVKEY.substring(PRIVKEY.length - 4)}" "${finalCommitHashSum}"`);
const NEWKEY = timeOperation('calculate destination key', () =>
safeAddHex(PRIVKEY, finalCommitHashSum)
);
debug(` NEWKEY (truncated): ${NEWKEY.substring(0, 4)}...${NEWKEY.substring(NEWKEY.length - 4)}`);
// Parse transaction information
debug(` Parsing transaction info from last TXO: ${lastTxo}`);
const { TXID, OUTPUT, AMOUNT } = await timeOperationAsync('parse TXO URI', async () => {
debug(` Parsing TXO URI directly: ${lastTxo}`);
const { parseTxoUri } = await import('txo_parser');
const parsed = parseTxoUri(lastTxo);
debug(` Parsed TXID: ${parsed.txid}`);
debug(` Parsed OUTPUT: ${parsed.output}`);
debug(` Parsed AMOUNT: ${parsed.amount}`);
return { TXID: parsed.txid, OUTPUT: parsed.output, AMOUNT: parsed.amount };
});
// Calculate fee and new amount
const FEE = 1000;
debug(` Fee set to ${FEE} satoshis`);
const NEWAMOUNT = AMOUNT - FEE;
debug(` New amount after fee: ${NEWAMOUNT} satoshis (${AMOUNT} - ${FEE})`);
// Generate new public key for destination
debug(` Generating new public key from NEWKEY for destination`);
const NEWPUB = await timeOperationAsync('generate destination pubkey', () =>
key2pub(NEWKEY)
);
debug(` NEWPUB (destination): ${NEWPUB}`);
// Build transaction
debug(' Building transaction with txbuilder');
debug(` txbuilder params (sensitive data masked): privateKey=***SIGNING_KEY*** publicKey=${SIGNING_PUBKEY} txid=${TXID} vout=${OUTPUT} inputAmount=${AMOUNT} output=${NEWPUB}:${NEWAMOUNT}`);
// Execute txbuilder
try {
debug(' Executing txbuilder...');
const { buildTx } = await import('btctx');
const { hex: LAST_LINE, txid: builtTxid } = await timeOperationAsync('execute txbuilder', () =>
buildTx({
privateKey: SIGNING_KEY,
publicKey: SIGNING_PUBKEY,
txid: TXID,
vout: OUTPUT,
inputAmount: AMOUNT,
outputs: [{ pubkey: NEWPUB, amount: NEWAMOUNT }],
})
);
debug(` txbuilder output txid: ${builtTxid}`);
debug(` txbuilder output hex: ${LAST_LINE.substring(0, 20)}...`);
// Send transaction
try {
debug(` Sending transaction to network ${NETWORK}`);
debug(` Broadcasting transaction to ${NETWORK}`);
const { default: sendtx } = await import('sendtx');
const NEWTX = await timeOperationAsync('send transaction', () =>
sendtx(LAST_LINE, NETWORK)
);
debug(` Transaction successfully sent, NEWTX: ${NEWTX}`);
// Calculate new values for TXO URI - use the new destination key/pubkey for the URI
debug(' Using calculated NEWKEY/NEWPUB for the TXO URI');
const TXO_URI = `txo:tbtc4:${NEWTX}:0?amount=${NEWAMOUNT}&pubkey=${NEWPUB}&commit=${COMMIT_HASH}`;
debug(` Generated TXO_URI: ${TXO_URI}`);
// Update txo.json file and save to .git/txo.txt
debug(` Updating TXO file (${TXOFILE}) with new TXO_URI`);
debug(` Current txoData has ${txoData.length} entries`);
timeOperation('update TXO files', () => {
txoData.push(TXO_URI);
debug(` After adding new URI, txoData has ${txoData.length} entries`);
const jsonString = JSON.stringify(txoData, null, 2);
debug(` Writing ${jsonString.length} bytes to ${TXOFILE}`);
fs.writeFileSync(TXOFILE, jsonString);
// Also save to .git/txo.txt
const gitTxoPath = '.git/txo.json';
debug(` Also writing ${jsonString.length} bytes to ${gitTxoPath}`);
// Ensure .git directory exists (it should, but just in case)
const gitDir = '.git';
if (!fs.existsSync(gitDir)) {
debug(` Creating ${gitDir} directory`);
fs.mkdirSync(gitDir, { recursive: true });
}
fs.writeFileSync(gitTxoPath, jsonString);
debug(` Successfully wrote TXO data to ${gitTxoPath}`);
});
debug(` Successfully added new TXO URI to ${TXOFILE} and .git/txo.txt`);
} catch (error) {
console.error(`DEBUG: ERROR in sendtx: ${error.message}`);
console.error('DEBUG: Error stack trace:');
console.error(error.stack);
console.error(`DEBUG: Command output: ${error.stdout ? error.stdout.toString() : 'No output'}`);
console.error(`DEBUG: Command stderr: ${error.stderr ? error.stderr.toString() : 'No stderr'}`);
process.exit(1);
}
} catch (error) {
console.error(`DEBUG: ERROR in txbuilder: ${error.message}`);
console.error('DEBUG: Error stack trace:');
console.error(error.stack);
console.error(`DEBUG: Command output: ${error.stdout ? error.stdout.toString() : 'No output'}`);
console.error(`DEBUG: Command stderr: ${error.stderr ? error.stderr.toString() : 'No stderr'}`);
process.exit(1);
}
const scriptEnd = performance.now();
const totalDuration = scriptEnd - scriptStart;
console.log(`BENCHMARK: TOTAL SCRIPT EXECUTION TIME: ${formatTime(totalDuration)}`);
if (VERBOSE) console.log('=== GITMARK DEBUG END ===');
} catch (error) {
const scriptEnd = performance.now();
const totalDuration = scriptEnd - scriptStart;
console.error('=== GITMARK DEBUG ERROR ===');
console.error(`Fatal error: ${error.message}`);
console.error('Error stack trace:');
console.error(error.stack);
if (error.stdout) console.error(`Command output: ${error.stdout.toString()}`);
if (error.stderr) console.error(`Command stderr: ${error.stderr.toString()}`);
console.error(`BENCHMARK: SCRIPT FAILED AFTER: ${formatTime(totalDuration)}`);
if (VERBOSE) console.error('=== GITMARK DEBUG END WITH ERROR ===');
process.exit(1);
}
}
// Run the main function
main().catch(error => {
console.error('Unhandled error in main function:', error);
process.exit(1);
});