Skip to content

Commit 9717d7e

Browse files
committed
flux delegates support
1 parent 8e82f6e commit 9717d7e

2 files changed

Lines changed: 593 additions & 2 deletions

File tree

lib/zelnode.js

Lines changed: 338 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@ var bs58check = require('bs58check');
33
var zcrypto = require('./crypto');
44
var btcmessage = require('./message');
55

6+
// FluxNode transaction version constants
7+
const FLUXNODE_TX_VERSION = 5;
8+
const FLUXNODE_TX_UPGRADEABLE_VERSION = 6;
9+
10+
// Legacy version constants for backward compatibility
11+
const FLUXNODE_INTERNAL_NORMAL_TX_VERSION = 1;
12+
const FLUXNODE_INTERNAL_P2SH_TX_VERSION = 2;
13+
14+
// Bit-based version system
15+
// Bits 0-7: Transaction type
16+
const FLUXNODE_TX_TYPE_MASK = 0xFF;
17+
const FLUXNODE_TX_TYPE_NORMAL_BIT = 0x01; // Bit 0
18+
const FLUXNODE_TX_TYPE_P2SH_BIT = 0x02; // Bit 1
19+
20+
// Bits 8-15: Feature flags
21+
const FLUXNODE_TX_FEATURE_MASK = 0xFF00;
22+
const FLUXNODE_TX_FEATURE_DELEGATES_BIT = 0x0100; // Bit 8
23+
24+
// Delegate type constants
25+
const DELEGATE_TYPE_NONE = 0;
26+
const DELEGATE_TYPE_UPDATE = 1;
27+
const DELEGATE_TYPE_SIGNING = 2;
28+
29+
const DELEGATE_VERSION_INITIAL = 1;
30+
const MAX_DELEGATE_PUBKEYS = 4;
31+
632
function doubleHash(msg) {
733
const bufMessage = Buffer.from(msg, 'hex')
834
message = zcrypto.sha256x2(bufMessage).toString('hex');
@@ -65,10 +91,10 @@ function signStartMessage(message, privKey, compressed = true) {
6591
return mysignature.toString("hex");
6692
}
6793

68-
function getZelNodePublicKey(zelnodePrivateKey) {
94+
function getZelNodePublicKey(zelnodePrivateKey, compressed = false) {
6995
var og = WIFToPrivKey(zelnodePrivateKey);
7096

71-
toCompressed = false;
97+
var toCompressed = compressed;
7298

7399
const pkBuffer = Buffer.from(og, 'hex');
74100
var publicKey = secp256k1.publicKeyCreate(pkBuffer, toCompressed);
@@ -86,12 +112,81 @@ function getCollateralPublicKey(collateralPrivateKey, compressed = true) {
86112
return publicKey.toString('hex');
87113
}
88114

115+
// Helper function to get public key from private key
116+
function getPublicKeyFromPrivateKey(privateKey, compressed = true) {
117+
var og = WIFToPrivKey(privateKey);
118+
const pkBuffer = Buffer.from(og, 'hex');
119+
var publicKey = secp256k1.publicKeyCreate(pkBuffer, compressed);
120+
return publicKey.toString('hex');
121+
}
122+
89123
// reverse hex string byte order
90124
function reverseHex(hex) {
91125
const buf = Buffer.from(hex, 'hex').reverse();
92126
return buf.toString('hex');
93127
}
94128

129+
// Helper functions for version checking
130+
function hasConflictingBits(version) {
131+
return (version & FLUXNODE_TX_TYPE_NORMAL_BIT) !== 0 && (version & FLUXNODE_TX_TYPE_P2SH_BIT) !== 0;
132+
}
133+
134+
function isFluxTxNormalType(version) {
135+
if (hasConflictingBits(version)) {
136+
return false;
137+
}
138+
return (version & FLUXNODE_TX_TYPE_NORMAL_BIT) !== 0 || version === FLUXNODE_INTERNAL_NORMAL_TX_VERSION;
139+
}
140+
141+
function isFluxTxP2SHType(version) {
142+
if (hasConflictingBits(version)) {
143+
return false;
144+
}
145+
return (version & FLUXNODE_TX_TYPE_P2SH_BIT) !== 0 || version === FLUXNODE_INTERNAL_P2SH_TX_VERSION;
146+
}
147+
148+
function hasFluxTxDelegatesFeature(version) {
149+
return (version & FLUXNODE_TX_FEATURE_DELEGATES_BIT) !== 0;
150+
}
151+
152+
// Serialize delegate data
153+
function serializeDelegateData(delegateData) {
154+
var serialized = '';
155+
156+
// nDelegateVersion (1 byte)
157+
var versionBuf = Buffer.alloc(1);
158+
versionBuf.writeUInt8(delegateData.version || DELEGATE_VERSION_INITIAL);
159+
serialized += versionBuf.toString('hex');
160+
161+
// nType (1 byte)
162+
var typeBuf = Buffer.alloc(1);
163+
typeBuf.writeUInt8(delegateData.type);
164+
serialized += typeBuf.toString('hex');
165+
166+
// If type is UPDATE, serialize the delegate public keys vector
167+
if (delegateData.type === DELEGATE_TYPE_UPDATE && delegateData.delegatePublicKeys) {
168+
// Number of keys (varint) - this is how std::vector is serialized
169+
var numKeys = delegateData.delegatePublicKeys.length;
170+
if (numKeys > MAX_DELEGATE_PUBKEYS) {
171+
throw new Error('Too many delegate public keys. Maximum is ' + MAX_DELEGATE_PUBKEYS);
172+
}
173+
174+
var numKeysVarint = varintBufNum(numKeys);
175+
serialized += numKeysVarint.toString('hex');
176+
177+
// Each CPubKey in the vector
178+
for (var i = 0; i < delegateData.delegatePublicKeys.length; i++) {
179+
var pubKey = delegateData.delegatePublicKeys[i];
180+
// CPubKey serialization uses CompactSize for length prefix
181+
var pubKeyLength = varintBufNum(pubKey.length / 2);
182+
serialized += pubKeyLength.toString('hex');
183+
serialized += pubKey;
184+
}
185+
}
186+
187+
return serialized;
188+
}
189+
95190
// zelnodePrivateKey as WIF formate, collateralPrivateKey as WIF, timestamp in seconds
96191
function startZelNode(collateralOutHash, collateralOutIndex, collateralPrivateKey, zelnodePrivateKey, timestamp, compressedCollateralPrivateKey = true) {
97192
// it is up to wallet to find out collateral Public Key.
@@ -157,6 +252,233 @@ function startZelNode(collateralOutHash, collateralOutIndex, collateralPrivateKe
157252
return serializedTx;
158253
}
159254

255+
// FluxNode v6 start with optional delegate support and P2SH
256+
function startFluxNodev6(collateralOutHash, collateralOutIndex, collateralPrivateKey, fluxnodePrivateKey, timestamp, compressedCollateralPrivateKey = true, compressedFluxnodePrivateKey = false, redeemScript, delegateData = null) {
257+
const version = 6;
258+
const nType = 2;
259+
260+
const FLUXNODE_NORMAL_TX_VERSION = 1;
261+
const FLUXNODE_P2SH_TX_VERSION = 2;
262+
263+
var nFluxNodeTxVersion = FLUXNODE_NORMAL_TX_VERSION;
264+
if (redeemScript) {
265+
nFluxNodeTxVersion = FLUXNODE_P2SH_TX_VERSION;
266+
}
267+
268+
// Add delegate feature bit if delegate data is provided
269+
if (delegateData) {
270+
nFluxNodeTxVersion |= FLUXNODE_TX_FEATURE_DELEGATES_BIT;
271+
}
272+
273+
var serializedTx = '';
274+
275+
// Version
276+
var _buf32 = Buffer.alloc(4);
277+
_buf32.writeUInt32LE(version);
278+
serializedTx += _buf32.toString('hex');
279+
280+
// nType
281+
var _buf8 = Buffer.alloc(1);
282+
_buf8.writeUInt8(nType);
283+
serializedTx += _buf8.toString('hex');
284+
285+
// nFluxNodeTxVersion
286+
var _buf32A = Buffer.alloc(4);
287+
_buf32A.writeUInt32LE(nFluxNodeTxVersion, 0);
288+
serializedTx += _buf32A.toString('hex');
289+
290+
// collateralOutHash
291+
serializedTx += reverseHex(collateralOutHash);
292+
293+
// collateralOutIndex
294+
var _buf32B = Buffer.alloc(4);
295+
_buf32B.writeUInt32LE(collateralOutIndex, 0);
296+
serializedTx += _buf32B.toString('hex');
297+
298+
// Check if this is a normal tx (not P2SH)
299+
var isNormalTx = isFluxTxNormalType(nFluxNodeTxVersion) && !(nFluxNodeTxVersion & FLUXNODE_TX_TYPE_P2SH_BIT);
300+
var isP2SHTx = isFluxTxP2SHType(nFluxNodeTxVersion);
301+
302+
if (isNormalTx) {
303+
// get collateral public key
304+
var collateralPublicKey = getCollateralPublicKey(collateralPrivateKey, compressedCollateralPrivateKey);
305+
306+
// collateralPublicKeyLength
307+
var pubKeyLength = varintBufNum(collateralPublicKey.length / 2);
308+
serializedTx += pubKeyLength.toString('hex');
309+
310+
// collateralPublicKey
311+
serializedTx += collateralPublicKey;
312+
}
313+
314+
// get public key from fluxnode private key;
315+
var fluxnodePublicKey = getZelNodePublicKey(fluxnodePrivateKey, compressedFluxnodePrivateKey);
316+
317+
// fluxnodePublicKeyLength
318+
var fnPubKeyLength = varintBufNum(fluxnodePublicKey.length / 2);
319+
serializedTx += fnPubKeyLength.toString('hex');
320+
321+
// fluxnodePublicKey
322+
serializedTx += fluxnodePublicKey;
323+
324+
if (isP2SHTx) {
325+
// redeemScriptLength
326+
var redeemScriptLength = varintBufNum(redeemScript.length / 2);
327+
serializedTx += redeemScriptLength.toString('hex');
328+
329+
// redeemScript
330+
serializedTx += redeemScript;
331+
}
332+
333+
// timestamp
334+
var _buf32C = Buffer.alloc(4);
335+
_buf32C.writeUInt32LE(timestamp);
336+
serializedTx += _buf32C.toString('hex');
337+
338+
// Build transaction for signing (includes delegate data but not signature)
339+
var txForSigning = serializedTx;
340+
341+
// Add delegate data to the transaction we're going to sign
342+
if (hasFluxTxDelegatesFeature(nFluxNodeTxVersion)) {
343+
// fUsingDelegates (1 byte boolean)
344+
var usingDelegates = delegateData !== null;
345+
var delegateBoolBuf = Buffer.alloc(1);
346+
delegateBoolBuf.writeUInt8(usingDelegates ? 1 : 0);
347+
txForSigning += delegateBoolBuf.toString('hex');
348+
349+
if (usingDelegates) {
350+
// Serialize the delegate data
351+
txForSigning += serializeDelegateData(delegateData);
352+
}
353+
}
354+
355+
// Sign the transaction (including delegate data)
356+
var signature = signStartMessage(txForSigning, collateralPrivateKey, compressedCollateralPrivateKey);
357+
358+
// Now build the final transaction with signature in the right place
359+
// Add signature BEFORE delegate data (matches C++ serialization order)
360+
var sigLength = varintBufNum(signature.length / 2);
361+
serializedTx += sigLength.toString('hex');
362+
serializedTx += signature;
363+
364+
// Then add delegate data after signature
365+
if (hasFluxTxDelegatesFeature(nFluxNodeTxVersion)) {
366+
// fUsingDelegates (1 byte boolean)
367+
var usingDelegates = delegateData !== null;
368+
var delegateBoolBuf = Buffer.alloc(1);
369+
delegateBoolBuf.writeUInt8(usingDelegates ? 1 : 0);
370+
serializedTx += delegateBoolBuf.toString('hex');
371+
372+
if (usingDelegates) {
373+
// Serialize the delegate data
374+
serializedTx += serializeDelegateData(delegateData);
375+
}
376+
}
377+
378+
return serializedTx;
379+
}
380+
381+
// Function to start FluxNode and add delegates (owner adding delegate permissions)
382+
function startFluxNodeAddDelegate(collateralOutHash, collateralOutIndex, collateralPrivateKey, fluxnodePrivateKey, timestamp, delegatePublicKeys, compressedCollateralPrivateKey = true, compressedFluxnodePrivateKey = false, redeemScript = null) {
383+
// Validate delegate public keys
384+
if (!delegatePublicKeys || !Array.isArray(delegatePublicKeys)) {
385+
throw new Error('delegatePublicKeys array is required for startFluxNodeAddDelegate');
386+
}
387+
388+
if (delegatePublicKeys.length === 0) {
389+
throw new Error('At least one delegate public key must be provided');
390+
}
391+
392+
if (delegatePublicKeys.length > MAX_DELEGATE_PUBKEYS) {
393+
throw new Error('Too many delegate public keys. Maximum is ' + MAX_DELEGATE_PUBKEYS);
394+
}
395+
396+
// Validate each public key
397+
for (var i = 0; i < delegatePublicKeys.length; i++) {
398+
var pubKey = delegatePublicKeys[i];
399+
if (typeof pubKey !== 'string' || pubKey.length !== 66) {
400+
throw new Error('Invalid delegate public key format. Must be 33 bytes (66 hex chars) compressed public key');
401+
}
402+
}
403+
404+
// Create delegate data for UPDATE type
405+
var delegateData = {
406+
version: DELEGATE_VERSION_INITIAL,
407+
type: DELEGATE_TYPE_UPDATE,
408+
delegatePublicKeys: delegatePublicKeys,
409+
};
410+
411+
// Use v6 with delegate data
412+
return startFluxNodev6(
413+
collateralOutHash,
414+
collateralOutIndex,
415+
collateralPrivateKey,
416+
fluxnodePrivateKey,
417+
timestamp,
418+
compressedCollateralPrivateKey,
419+
compressedFluxnodePrivateKey,
420+
redeemScript,
421+
delegateData
422+
);
423+
}
424+
425+
// Function for delegates to start a FluxNode (delegate using their permission)
426+
function startFluxNodeAsDelegate(collateralOutHash, collateralOutIndex, delegatePrivateKey, fluxnodePrivateKey, timestamp, compressedDelegatePrivateKey = true, compressedFluxnodePrivateKey = false, redeemScript = null) {
427+
// Create delegate data for SIGNING type
428+
var delegateData = {
429+
version: DELEGATE_VERSION_INITIAL,
430+
type: DELEGATE_TYPE_SIGNING,
431+
};
432+
433+
// Note: When signing as a delegate, we use the delegate's private key instead of collateral private key
434+
// Use v6 with delegate data
435+
return startFluxNodev6(
436+
collateralOutHash,
437+
collateralOutIndex,
438+
delegatePrivateKey, // Using delegate's private key for signing
439+
fluxnodePrivateKey,
440+
timestamp,
441+
compressedDelegatePrivateKey, // Compression for delegate key
442+
compressedFluxnodePrivateKey,
443+
redeemScript,
444+
delegateData
445+
);
446+
}
447+
448+
// Helper function to create delegate data object for advanced use cases
449+
function createDelegateData(type, delegatePublicKeys = null, delegatePrivateKeys = null) {
450+
var delegateData = {
451+
version: DELEGATE_VERSION_INITIAL,
452+
type: type,
453+
};
454+
455+
if (type === DELEGATE_TYPE_UPDATE) {
456+
if (!delegatePublicKeys && delegatePrivateKeys) {
457+
// Convert private keys to public keys
458+
delegateData.delegatePublicKeys = delegatePrivateKeys.map(function(privKey) {
459+
return getPublicKeyFromPrivateKey(privKey, true); // Always use compressed keys for delegates
460+
});
461+
} else if (delegatePublicKeys) {
462+
delegateData.delegatePublicKeys = delegatePublicKeys;
463+
} else {
464+
throw new Error('Either delegatePublicKeys or delegatePrivateKeys must be provided for UPDATE type');
465+
}
466+
}
467+
468+
return delegateData;
469+
}
470+
471+
// Helper to convert delegate private keys to public keys
472+
function convertDelegatePrivateKeysToPublic(delegatePrivateKeys) {
473+
if (!delegatePrivateKeys || !Array.isArray(delegatePrivateKeys)) {
474+
throw new Error('delegatePrivateKeys must be an array');
475+
}
476+
477+
return delegatePrivateKeys.map(function(privKey) {
478+
return getPublicKeyFromPrivateKey(privKey, true); // Always use compressed keys for delegates
479+
});
480+
}
481+
160482
function txidStart(rawtx) {
161483
// signature is 41 hex length in bytes, remove last 41 -> 65 in decimal. Remove last 65+1 byte (byte before states the signature size);
162484
const adjustedRawTx = rawtx.substr(0, rawtx.length - 132);
@@ -175,12 +497,26 @@ function txidConfirm(rawtx) {
175497

176498

177499
module.exports = {
500+
// Legacy v5 function
178501
startZelNode,
502+
// v6 functions with delegation support
503+
startFluxNodev6,
504+
startFluxNodeAddDelegate,
505+
startFluxNodeAsDelegate,
506+
// Helper functions
507+
createDelegateData,
508+
convertDelegatePrivateKeysToPublic,
179509
getZelNodePublicKey,
180510
getCollateralPublicKey,
511+
getPublicKeyFromPrivateKey,
181512
signMessage,
182513
doubleHash,
183514
signStartMessage,
184515
txidStart,
185516
txidConfirm,
517+
// Constants
518+
DELEGATE_TYPE_NONE,
519+
DELEGATE_TYPE_UPDATE,
520+
DELEGATE_TYPE_SIGNING,
521+
MAX_DELEGATE_PUBKEYS,
186522
};

0 commit comments

Comments
 (0)