const bignum = require('bignum'); const base58 = require('base58-native'); const bech32 = require('bech32'); const bitcoin = require('bitcoinjs-lib'); const diff1 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; function reverseBuffer(buff) { let reversed = Buffer.alloc(buff.length); for (let i = buff.length - 1; i >= 0; i--) reversed[buff.length - i - 1] = buff[i]; return reversed; } function reverseByteOrder(buff) { for (let i = 0; i < 8; i++) buff.writeUInt32LE(buff.readUInt32BE(i * 4), i * 4); return reverseBuffer(buff); } function packInt32LE(num) { let buff = Buffer.alloc(4); buff.writeInt32LE(num, 0); return buff; } function packInt32BE(num) { let buff = Buffer.alloc(4); buff.writeInt32BE(num, 0); return buff; } function packUInt16LE(num) { let buff = Buffer.alloc(2); buff.writeUInt16LE(num, 0); return buff; } function packUInt32LE(num) { let buff = Buffer.alloc(4); buff.writeUInt32LE(num, 0); return buff; } function packUInt32BE(num) { let buff = Buffer.alloc(4); buff.writeUInt32BE(num, 0); return buff; } function packInt64LE(num){ let buff = Buffer.alloc(8); buff.writeUInt32LE(num % Math.pow(2, 32), 0); buff.writeUInt32LE(Math.floor(num / Math.pow(2, 32)), 4); return buff; } // Defined in bitcoin protocol here: // https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer function varIntBuffer(n) { if (n < 0xfd) { return Buffer.from([n]); } else if (n <= 0xffff) { let buff = Buffer.alloc(3); buff[0] = 0xfd; buff.writeUInt16LE(n, 1); return buff; } else if (n <= 0xffffffff) { let buff = Buffer.alloc(5); buff[0] = 0xfe; buff.writeUInt32LE(n, 1); return buff; } else{ let buff = Buffer.alloc(9); buff[0] = 0xff; packUInt16LE(n).copy(buff, 1); return buff; } } // "serialized CScript" formatting as defined here: // https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki#specification // Used to format height and date when putting into script signature: // https://en.bitcoin.it/wiki/Script function serializeNumber(n) { // New version from TheSeven if (n >= 1 && n <= 16) return Buffer.from([0x50 + n]); var l = 1; var buff = Buffer.alloc(9); while (n > 0x7f) { buff.writeUInt8(n & 0xff, l++); n >>= 8; } buff.writeUInt8(l, 0); buff.writeUInt8(n, l++); return buff.slice(0, l); } // Used for serializing strings used in script signature function serializeString(s) { if (s.length < 253) { return Buffer.concat([ Buffer.from([s.length]), Buffer.from(s) ]); } else if (s.length < 0x10000) { return Buffer.concat([ Buffer.from([253]), packUInt16LE(s.length), Buffer.from(s) ]); } else if (s.length < 0x100000000) { return Buffer.concat([ Buffer.from([254]), packUInt32LE(s.length), Buffer.from(s) ]); } else { return Buffer.concat([ Buffer.from([255]), packUInt16LE(s.length), Buffer.from(s) ]); } } // An exact copy of python's range feature. Written by Tadeck: // http://stackoverflow.com/a/8273091 function range(start, stop, step) { if (typeof stop === 'undefined') { stop = start; start = 0; } if (typeof step === 'undefined') { step = 1; } if ((step > 0 && start >= stop) || (step < 0 && start <= stop)) { return []; } let result = []; for (let i = start; step > 0 ? i < stop : i > stop; i += step) { result.push(i); } return result; } function uint256BufferFromHash(hex) { let fromHex = Buffer.from(hex, 'hex'); if (fromHex.length != 32) { let empty = Buffer.alloc(32); empty.fill(0); fromHex.copy(empty); fromHex = empty; } return reverseBuffer(fromHex); } function getTransactionBuffers(txs) { let txHashes = txs.map(function(tx) { if (tx.txid !== undefined) { return uint256BufferFromHash(tx.txid); } return uint256BufferFromHash(tx.hash); }); return [null].concat(txHashes); } function addressToScript(addr) { let decoded; try { decoded = base58.decode(addr); } catch(err) {} if (!decoded || decoded.length != 25) { const decoded2 = Buffer.from(bech32.bech32.fromWords(bech32.bech32.decode(addr).words.slice(1))); if (decoded2.length != 20) throw new Error('Invalid address ' + addr); return Buffer.concat([Buffer.from([0x0, 0x14]), decoded2]); } const pubkey = decoded.slice(1, -4); return Buffer.concat([Buffer.from([0x76, 0xa9, 0x14]), pubkey, Buffer.from([0x88, 0xac])]); } function createTransactionOutput(amount, payee, rewardToPool, reward, txOutputBuffers, payeeScript) { const payeeReward = amount; if (!payeeScript) payeeScript = addressToScript(payee); txOutputBuffers.push(Buffer.concat([ packInt64LE(payeeReward), varIntBuffer(payeeScript.length), payeeScript ])); return { reward: reward - amount, rewardToPool: rewardToPool - amount }; } function generateTransactionOutputs(rpcData, poolAddress) { let reward = rpcData.coinbasevalue + (rpcData.coinbasedevreward ? rpcData.coinbasedevreward.value : 0); let rewardToPool = reward; let txOutputBuffers = []; if (rpcData.coinbasedevreward) { const rewards = createTransactionOutput(rpcData.coinbasedevreward.value, null, rewardToPool, reward, txOutputBuffers, Buffer.from(rpcData.coinbasedevreward.scriptpubkey, 'hex')); reward = rewards.reward; rewardToPool = rewards.rewardToPool; } if (rpcData.smartnode) { if (rpcData.smartnode.payee) { const rewards = createTransactionOutput(rpcData.smartnode.amount, rpcData.smartnode.payee, rewardToPool, reward, txOutputBuffers); reward = rewards.reward; rewardToPool = rewards.rewardToPool; } else if (Array.isArray(rpcData.smartnode)) { for (let i in rpcData.smartnode) { const rewards = createTransactionOutput(rpcData.smartnode[i].amount, rpcData.smartnode[i].payee, rewardToPool, reward, txOutputBuffers); reward = rewards.reward; rewardToPool = rewards.rewardToPool; } } } if (rpcData.superblock) { for (let i in rpcData.superblock) { const rewards = createTransactionOutput(rpcData.superblock[i].amount, rpcData.superblock[i].payee, rewardToPool, reward, txOutputBuffers); reward = rewards.reward; rewardToPool = rewards.rewardToPool; } } if (rpcData.founder_payments_started && rpcData.founder) { const founderReward = rpcData.founder.amount || 0; const rewards = createTransactionOutput(founderReward, rpcData.founder.payee, rewardToPool, reward, txOutputBuffers); reward = rewards.reward; rewardToPool = rewards.rewardToPool; } createTransactionOutput(rewardToPool, null, rewardToPool, reward, txOutputBuffers, Buffer.from(addressToScript(poolAddress), "hex")); if (rpcData.default_witness_commitment) { createTransactionOutput(0, null, rewardToPool, reward, txOutputBuffers, Buffer.from(rpcData.default_witness_commitment, 'hex')); txOutputBuffers.push(Buffer.concat([ varIntBuffer(1), varIntBuffer(32), Buffer.alloc(32, 0) ])); } return Buffer.concat([ varIntBuffer(rpcData.default_witness_commitment ? txOutputBuffers.length - 1 : txOutputBuffers.length), Buffer.concat(txOutputBuffers)]); } module.exports.RtmBlockTemplate = function(rpcData, poolAddress) { const extraNoncePlaceholderLength = 17; const coinbaseVersion = rpcData.coinbasedevreward ? Buffer.concat([packUInt16LE(1), packUInt16LE(0)]) : Buffer.concat([packUInt16LE(3), packUInt16LE(5)]); const scriptSigPart1 = Buffer.concat([ serializeNumber(rpcData.height), Buffer.from(rpcData.coinbaseaux.flags ? rpcData.coinbaseaux.flags : "", 'hex'), serializeNumber(Date.now() / 1000 | 0), Buffer.from([extraNoncePlaceholderLength]) ]); const scriptSigPart2 = serializeString('/nodeStratum/'); const is_witness = rpcData.default_witness_commitment !== undefined; const blob1 = Buffer.concat([ coinbaseVersion, // transaction input Buffer.from(is_witness ? "0001" : "", 'hex'), varIntBuffer(1), // txInputsCount uint256BufferFromHash(""), // txInPrevOutHash packUInt32LE(Math.pow(2, 32) - 1), // txInPrevOutIndex varIntBuffer(scriptSigPart1.length + extraNoncePlaceholderLength + scriptSigPart2.length), scriptSigPart1 ]); let blob2 = Buffer.concat([ scriptSigPart2, packUInt32LE(0), // txInSequence // end transaction input // transaction output generateTransactionOutputs(rpcData, poolAddress, is_witness), // end transaction ouput packUInt32LE(0) // txLockTime ]); if (rpcData.coinbase_payload) { blob2 = Buffer.concat([ blob2, varIntBuffer(rpcData.coinbase_payload.length / 2), Buffer.from(rpcData.coinbase_payload, 'hex') ]); } const prev_hash = reverseBuffer(Buffer.from(rpcData.previousblockhash, 'hex')).toString('hex'); const version = packInt32LE(rpcData.version).toString('hex'); const curtime = packUInt32LE(rpcData.curtime).toString('hex'); let bits = Buffer.from(rpcData.bits, 'hex'); bits.writeUInt32LE(bits.readUInt32BE()); let txs = []; // skip version 1 transaction because they contain some OP_RETURN(0x6A) opcode in the beginning of // tx input scripts instead of size of script part so not sure how to parse them // just drop them for now // example: https://explorer.raptoreum.com/tx/1461d70fa8362b0896e2e9be6312521f2684f22c9b0f9152695f33f67d9f9d3f rpcData.transactions.forEach(function(tx) { if (tx.version != 1) { try { bitcoin.Transaction.fromBuffer(Buffer.from(tx.data, 'hex'), false, false); } catch(err) { console.error("Skip RTM tx due to parse error: " + tx.data); return; // skip transaction if it is not parsed OK (varint coding seems to be different for RTM) } txs.push(tx); } else { console.error("Skip RTM v1 tx: " + tx.data); } }); const txn = varIntBuffer(txs.length + 1); return { difficulty: parseFloat((diff1 / bignum(rpcData.target, 16).toNumber()).toFixed(9)), height: rpcData.height, prev_hash: prev_hash, blocktemplate_blob: version + prev_hash + Buffer.alloc(32, 0).toString('hex') + curtime + bits.toString('hex') + Buffer.alloc(4, 0).toString('hex') + txn.toString('hex') + blob1.toString('hex') + Buffer.alloc(extraNoncePlaceholderLength, 0xCC).toString('hex') + blob2.toString('hex') + Buffer.concat(txs.map(function(tx) { return Buffer.from(tx.data, 'hex'); })).toString('hex'), reserved_offset: 80 + txn.length + blob1.length } }