From 5cef2b03c28f8886cdcb4f9f37b2e9c8416bcfd5 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 17:15:21 +0000 Subject: [PATCH 01/15] feat: read Core ExtNetInfo addresses and check Tor v3 checksums Add readExtNetInfo/extNetInfoFromBuffer to lib/util/netinfo.js. They read Core's ExtNetInfo and return the same `addresses` map Core reports (purpose => "addr:port" strings), formatting IPv4, IPv6 and CJDNS (Core's RFC 5952 compression), Tor v3, I2P and domain entries exactly like CService::ToStringAddrPort, so reading and then calling extNetInfoToBuffer gives back the same bytes. Input Core would not read back as the same addresses is rejected: unknown versions, purposes that are unknown or out of order, wrong BIP155 address sizes, IPv6 with an embedded IPv4/Tor v2/internal prefix, unknown entry types and invalid domains. Tor v3 names carry a SHA3-256 checksum, so add @noble/hashes 1.x (audited, dependency-free, ships CommonJS and bundles with the existing webpack and karma config). With it the writer now rejects a .onion address whose checksum is wrong. Co-Authored-By: Claude Opus 5.5 --- lib/util/netinfo.js | 224 ++++++++++++++++++++++++++++++++++++++++++- package-lock.json | 18 ++++ package.json | 1 + test/util/netinfo.js | 80 +++++++++++++++- 4 files changed, 320 insertions(+), 3 deletions(-) diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index b8cd3264b..8ee2cc537 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -1,3 +1,5 @@ +const { sha3_256: sha3256 } = require('@noble/hashes/sha3'); +const BufferReader = require('../encoding/bufferreader'); const BufferWriter = require('../encoding/bufferwriter'); // Serialization of Dash Core's ExtNetInfo (src/evo/netinfo.h), the address @@ -21,9 +23,34 @@ const BIP155_TORV3 = 0x04; const BIP155_I2P = 0x05; const BIP155_CJDNS = 0x06; +const IPV4_ADDRESS_SIZE = 4; +const IPV6_ADDRESS_SIZE = 16; const TORV3_ADDRESS_SIZE = 32; +const TORV3_CHECKSUM_SIZE = 2; const TORV3_VERSION = 3; +// base32 payload of a .onion name: pubkey, checksum, version +const TORV3_ENCODED_SIZE = TORV3_ADDRESS_SIZE + TORV3_CHECKSUM_SIZE + 1; const I2P_ADDRESS_SIZE = 32; +const CJDNS_ADDRESS_SIZE = 16; + +// Address sizes by BIP155 network id +const BIP155_ADDRESS_SIZES = { + [BIP155_IPV4]: IPV4_ADDRESS_SIZE, + [BIP155_IPV6]: IPV6_ADDRESS_SIZE, + [BIP155_TORV3]: TORV3_ADDRESS_SIZE, + [BIP155_I2P]: I2P_ADDRESS_SIZE, + [BIP155_CJDNS]: CJDNS_ADDRESS_SIZE, +}; + +// IPv6 prefixes Core doesn't read as IPv6 in BIP155 form (CNetAddr::UnserializeV2Stream) +const IPV6_DISALLOWED_PREFIXES = [ + Buffer.from('00000000000000000000ffff', 'hex'), // IPv4-mapped + Buffer.from('fd87d87eeb43', 'hex'), // Tor v2 + Buffer.from('fd6b88c08724', 'hex'), // internal +]; + +// DOMAIN_MAX_LEN +const DOMAIN_MAX_LENGTH = 253; const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'; const DOMAIN_REGEX = /^[a-z0-9.-]+$/; @@ -60,6 +87,82 @@ function decodeBase32(string) { return Buffer.from(bytes); } +/** + * @param {Buffer} buffer + * @return {string} unpadded lowercase RFC 4648 base32 + */ +function encodeBase32(buffer) { + let string = ''; + let value = 0; + let bits = 0; + for (let i = 0; i < buffer.length; i++) { + // eslint-disable-next-line no-bitwise + value = (value << 8) | buffer[i]; + bits += 8; + while (bits >= 5) { + bits -= 5; + // eslint-disable-next-line no-bitwise + string += BASE32_ALPHABET[(value >> bits) & 0x1f]; + } + } + if (bits > 0) { + // eslint-disable-next-line no-bitwise + string += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f]; + } + return string; +} + +/** + * Tor v3 address checksum (torv3::Checksum in Core's netaddress.cpp) + * @param {Buffer} pubKey + * @return {Buffer} + */ +function torV3Checksum(pubKey) { + const digest = sha3256( + Buffer.concat([Buffer.from('.onion checksum', 'ascii'), pubKey, Buffer.from([TORV3_VERSION])]), + ); + return Buffer.from(digest).slice(0, TORV3_CHECKSUM_SIZE); +} + +/** + * Formats an IPv6 address like Core (IPv6ToString in netaddress.cpp): RFC 5952 + * zero compression of the first longest run of at least two zero groups + * @param {Buffer} address + * @return {string} + */ +function ipv6ToString(address) { + const groups = []; + for (let i = 0; i < IPV6_ADDRESS_SIZE; i += 2) { + groups.push(address.readUInt16BE(i)); + } + + let longest = { start: 0, length: 0 }; + let current = { start: 0, length: 0 }; + groups.forEach((group, i) => { + if (group !== 0) { + current = { start: i + 1, length: 0 }; + return; + } + current.length += 1; + if (current.length > longest.length) { + longest = { start: current.start, length: current.length }; + } + }); + + let string = ''; + groups.forEach((group, i) => { + if (longest.length >= 2 && i >= longest.start && i < longest.start + longest.length) { + if (i === longest.start) { + string += '::'; + } + return; + } + const separator = string !== '' && !string.endsWith(':') ? ':' : ''; + string += `${separator}${group.toString(16)}`; + }); + return string; +} + /** * @param {string} address - IPv6 address without brackets, as printed by Core * @return {Buffer} @@ -112,10 +215,15 @@ function parseServiceHost(host) { // base32(pubkey || checksum[2] || version[1]) const decoded = decodeBase32(host.replace(/\.onion$/, '')); const version = decoded[decoded.length - 1]; - if (decoded.length !== TORV3_ADDRESS_SIZE + 3 || version !== TORV3_VERSION) { + if (decoded.length !== TORV3_ENCODED_SIZE || version !== TORV3_VERSION) { throw new Error(`Invalid Tor v3 address ${host}`); } - return { networkId: BIP155_TORV3, address: decoded.slice(0, TORV3_ADDRESS_SIZE) }; + const pubKey = decoded.slice(0, TORV3_ADDRESS_SIZE); + const checksum = decoded.slice(TORV3_ADDRESS_SIZE, TORV3_ADDRESS_SIZE + TORV3_CHECKSUM_SIZE); + if (!checksum.equals(torV3Checksum(pubKey))) { + throw new Error(`Invalid Tor v3 address checksum in ${host}`); + } + return { networkId: BIP155_TORV3, address: pubKey }; } if (host.endsWith('.b32.i2p')) { const decoded = decodeBase32(host.replace(/\.b32\.i2p$/, '')); @@ -194,6 +302,118 @@ function extNetInfoToBuffer(addresses) { return writer.toBuffer(); } +/** + * Formats a BIP155 address and port like Core's CService::ToStringAddrPort() + * @param {number} networkId + * @param {Buffer} address + * @param {number} port + * @return {string} + */ +function serviceToString(networkId, address, port) { + switch (networkId) { + case BIP155_IPV4: + return `${Array.from(address).join('.')}:${port}`; + case BIP155_IPV6: + if (IPV6_DISALLOWED_PREFIXES.some((prefix) => address.indexOf(prefix) === 0)) { + throw new Error(`Invalid IPv6 address ${address.toString('hex')}`); + } + return `[${ipv6ToString(address)}]:${port}`; + case BIP155_CJDNS: + return `[${ipv6ToString(address)}]:${port}`; + case BIP155_TORV3: { + const onion = Buffer.concat([address, torV3Checksum(address), Buffer.from([TORV3_VERSION])]); + return `${encodeBase32(onion)}.onion:${port}`; + } + case BIP155_I2P: + return `${encodeBase32(address)}.b32.i2p:${port}`; + default: + throw new Error(`Unknown BIP155 network id ${networkId}`); + } +} + +/** + * Reads one NetInfoEntry and returns its "addr:port" JSON form + * @param {BufferReader} reader + * @return {string} + */ +function readNetInfoEntry(reader) { + const type = reader.readUInt8(); + if (type === ENTRY_TYPE_SERVICE) { + const networkId = reader.readUInt8(); + const size = reader.readVarintNum(); + if (BIP155_ADDRESS_SIZES[networkId] !== size) { + throw new Error(`Invalid BIP155 address of network ${networkId} and size ${size}`); + } + const address = reader.read(size); + return serviceToString(networkId, address, reader.readUInt16BE()); + } + if (type === ENTRY_TYPE_DOMAIN) { + const size = reader.readVarintNum(); + if (size > DOMAIN_MAX_LENGTH) { + throw new Error(`Domain of ${size} bytes is too long`); + } + const domain = reader.read(size).toString('ascii'); + if (!DOMAIN_REGEX.test(domain)) { + throw new Error(`Invalid domain ${domain}`); + } + return `${domain}:${reader.readUInt16BE()}`; + } + throw new Error(`Unknown address entry type ${type}`); +} + +/** + * Reads ExtNetInfo and returns the `addresses` object Core reports for it + * (purpose name => list of "addr:port" strings), the input of extNetInfoToBuffer + * @param {BufferReader} reader - left positioned after the ExtNetInfo + * @return {Object} + */ +function readExtNetInfo(reader) { + const version = reader.readUInt8(); + if (version !== EXT_NET_INFO_VERSION) { + throw new Error(`Unsupported ExtNetInfo version ${version}`); + } + + const addresses = {}; + const purposesCount = reader.readVarintNum(); + let previousCode = -1; + for (let i = 0; i < purposesCount; i++) { + const code = reader.readUInt8(); + // Core requires known purposes in ascending order + if (code >= PURPOSES.length || code <= previousCode) { + throw new Error(`Address purpose ${code} is unknown or out of order`); + } + previousCode = code; + + const purpose = PURPOSES[code]; + const entriesCount = reader.readVarintNum(); + if (entriesCount > MAX_ENTRIES_PER_PURPOSE) { + throw new Error(`Too many ${purpose} addresses`); + } + addresses[purpose] = []; + for (let j = 0; j < entriesCount; j++) { + addresses[purpose].push(readNetInfoEntry(reader)); + } + } + + return addresses; +} + +/** + * Parses serialized ExtNetInfo into the `addresses` object Core reports for it + * @param {Buffer} buffer + * @return {Object} + */ +function extNetInfoFromBuffer(buffer) { + const reader = new BufferReader(buffer); + const addresses = readExtNetInfo(reader); + if (!reader.finished()) { + throw new Error('ExtNetInfo is followed by unexpected data'); + } + return addresses; +} + module.exports = { extNetInfoToBuffer, + extNetInfoFromBuffer, + readExtNetInfo, }; diff --git a/package-lock.json b/package-lock.json index 637465c45..c7963e133 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@dashevo/bls": "~1.2.9", "@dashevo/x11-hash-js": "^1.0.2", + "@noble/hashes": "^1.8.0", "@types/node": "^12.12.47", "bloom-filter": "^0.2.0", "bn.js": "^4.12.0", @@ -869,6 +870,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10500,6 +10513,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 51480904f..17b724058 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "dependencies": { "@dashevo/bls": "~1.2.9", "@dashevo/x11-hash-js": "^1.0.2", + "@noble/hashes": "^1.8.0", "@types/node": "^12.12.47", "bloom-filter": "^0.2.0", "bn.js": "^4.12.0", diff --git a/test/util/netinfo.js b/test/util/netinfo.js index 5259b546a..8a29f248e 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -1,5 +1,10 @@ const { expect } = require('chai'); -const { extNetInfoToBuffer } = require('../../lib/util/netinfo'); +const BufferReader = require('../../lib/encoding/bufferreader'); +const { + extNetInfoToBuffer, + extNetInfoFromBuffer, + readExtNetInfo, +} = require('../../lib/util/netinfo'); // Expected bytes follow Core's ExtNetInfo / NetInfoEntry serialization // (src/evo/netinfo.h): version, purpose map, then per entry a type byte and @@ -71,6 +76,10 @@ describe('netinfo', () => { expect(() => extNetInfoToBuffer({ platform_https: ['Example.com:443'] })).to.throw('Invalid address entry'); expect(() => extNetInfoToBuffer({ core_p2p: ['[2001:db8::1::2]:9999'] })).to.throw('Invalid IPv6'); expect(() => extNetInfoToBuffer({ core_p2p: ['aaaa.onion:9999'] })).to.throw('Invalid Tor v3'); + // a valid address with the last checksum character changed + expect(() => extNetInfoToBuffer({ + core_p2p: ['pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscsyd.onion:9999'], + })).to.throw('Invalid Tor v3 address checksum'); // the last character of a 52 character I2P address carries 4 unused bits expect(() => extNetInfoToBuffer({ core_p2p: ['eaqseizeeutcokbjfivsyljof4ydcmrtgq2tmnzyhe5dwpb5hy7r.b32.i2p:0'], @@ -80,4 +89,73 @@ describe('netinfo', () => { })).to.throw('Too many platform_https addresses'); }); }); + + describe('extNetInfoFromBuffer', () => { + it('should read every address kind in Core\'s string form', () => { + // Tor, I2P and domain entries as Core v24 reports them (rpc_netinfo.py) + const addresses = { + core_p2p: [ + '127.0.0.1:19951', + 'kpgvmscirrdqpekbqjsvw5teanhatztpp2gl6eee4zkowvwfxwenqaid.onion:19951', + '[2001:db8::1]:9999', + '[fc00::1]:9999', + ], + platform_p2p: ['c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p:0'], + platform_https: ['server-1.example.com:22201'], + }; + const buffer = extNetInfoToBuffer(addresses); + + expect(extNetInfoFromBuffer(buffer)).to.deep.equal(addresses); + }); + + it('should read an empty address map', () => { + expect(extNetInfoFromBuffer(Buffer.from('0100', 'hex'))).to.deep.equal({}); + }); + + it('should compress IPv6 addresses like Core', () => { + // one core_p2p IPv6 service on port 9999 + const read = (hex) => extNetInfoFromBuffer( + Buffer.from('0101000101' + '02' + '10' + hex + '270f', 'hex'), + ).core_p2p[0]; + + expect(read('00000000000000000000000000000001')).to.equal('[::1]:9999'); + expect(read('20010db8000000000000000000000000')).to.equal('[2001:db8::]:9999'); + // a single zero group is not compressed + expect(read('20010db8000100010001000100000001')).to.equal('[2001:db8:1:1:1:1:0:1]:9999'); + // the first of two equally long zero runs is compressed + expect(read('20010db8000000000001000000000001')).to.equal('[2001:db8::1:0:0:1]:9999'); + // the longest zero run is compressed + expect(read('20010000000100000000000000000001')).to.equal('[2001:0:1::1]:9999'); + }); + + it('should leave a BufferReader after the ExtNetInfo', () => { + const reader = new BufferReader(Buffer.concat([ + extNetInfoToBuffer({ core_p2p: ['1.2.3.4:9999'] }), + Buffer.from('ff', 'hex'), + ])); + + expect(readExtNetInfo(reader)).to.deep.equal({ core_p2p: ['1.2.3.4:9999'] }); + expect(reader.readUInt8()).to.equal(0xff); + expect(reader.finished()).to.equal(true); + }); + + it('should reject input Core would not read back as the same addresses', () => { + const entry = (hex) => Buffer.from(`01010001${hex}`, 'hex'); + + expect(() => extNetInfoFromBuffer(Buffer.from('0200', 'hex'))).to.throw('Unsupported ExtNetInfo version 2'); + expect(() => extNetInfoFromBuffer(Buffer.from('010103', 'hex'))).to.throw('unknown or out of order'); + expect(() => extNetInfoFromBuffer(Buffer.from('0102010000', 'hex'))).to.throw('unknown or out of order'); + expect(() => extNetInfoFromBuffer(Buffer.from('01010005', 'hex'))).to.throw('Too many core_p2p addresses'); + expect(() => extNetInfoFromBuffer(entry('ff'))).to.throw('Unknown address entry type 255'); + expect(() => extNetInfoFromBuffer(entry('010303'))).to.throw('Invalid BIP155 address of network 3'); + expect(() => extNetInfoFromBuffer(entry('01010501020304'))).to.throw('Invalid BIP155 address of network 1 and size 5'); + expect(() => extNetInfoFromBuffer(entry(`010210${'00000000000000000000ffff01020304'}270f`))) + .to.throw('Invalid IPv6 address'); + expect(() => extNetInfoFromBuffer(entry(`0203${Buffer.from('A.b').toString('hex')}01bb`))) + .to.throw('Invalid domain'); + expect(() => extNetInfoFromBuffer(entry('02fdfe00'))).to.throw('too long'); + expect(() => extNetInfoFromBuffer(Buffer.concat([extNetInfoToBuffer({}), Buffer.from('00', 'hex')]))) + .to.throw('ExtNetInfo is followed by unexpected data'); + }); + }); }); From 43e2674ba9fe8f9c4aa7b2e0a5be3eede51ea144 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 17:15:35 +0000 Subject: [PATCH 02/15] feat: support Core V24 version 3 ProTx payloads V24 activates ProTx version 3 (ExtAddr). Parsing its ProRegTxs and ProUpServTxs failed with "raw payload is bigger than expected", "Expected scriptOperatorPayout to be a p2pkh/p2sh" or "offset out of range". Read and write the version 3 layouts of Core's CProRegTx, CProUpServTx and CProUpRegTx: - ProRegTx: ExtNetInfo `addresses` instead of the CService, a `payouts` list (script + reward) instead of scriptPayout, collateral `shares` with one 65-byte join signature each, earlyPeriodBlocks and earlyPenalty. Evonodes keep platformNodeID but no longer carry the platform ports. - ProUpServTx: ExtNetInfo `addresses`; evonodes drop the ports. - ProUpRegTx: a `payouts` list instead of scriptPayout. - ProUpRevTx keeps its layout; its JSON now keeps the version, so v3 round-trips. The JSON forms gain the matching fields (Core's RPC names where the class had none) and validate() checks them structurally. Version 1 and 2 bytes and JSON stay as they were, and new payloads keep their default versions. Fix round trips that were already broken for older versions: - The parsers reversed BufferReader.read() slices in place, which reversed the hashes and key ids inside the caller's buffer. Parsed payloads were right (collateralHash matches Core) but the raw transaction buffer passed to `new Transaction(raw)` was modified, so it no longer matched the re-serialized transaction. Use readReverse(), which copies. - fromJSON ignored the version of ProRegTx, ProUpRegTx and ProUpRevTx JSON, and ProRegTx JSON had no type, so v1 and evonode payloads came back with different bytes. JSON now carries the version, and ProRegTx reports the type for evonodes and version 3. - A ProUpServTx without operator payee reported operatorPayoutAddress "false" and could not be read back; the field is now left out, as Core does. Tests use Core-produced vectors: testnet v1/v2/v3 transactions, Core v24.0.0-rc.3 devnet transactions and blocks, and Core v24.0.0-rc.4 regtest transactions with shares, IPv6, Tor, I2P and domain addresses. Each one re-serializes byte for byte, hashes to Core's txid, round-trips through JSON and the transaction object form, and reports the fields Core's decoderawtransaction reports. Co-Authored-By: Claude Opus 5.5 --- index.d.ts | 12 +- lib/constants/index.js | 2 + lib/transaction/payload/masternodepayouts.js | 111 ++++ lib/transaction/payload/proregtxpayload.js | 384 ++++++++++--- lib/transaction/payload/proupregtxpayload.js | 58 +- lib/transaction/payload/prouprevtxpayload.js | 10 +- lib/transaction/payload/proupservtxpayload.js | 135 +++-- test-d/index.test-d.ts | 53 ++ test/fixtures/core24-protx.json | 541 ++++++++++++++++++ test/transaction/payload/proregtxpayload.js | 135 +++++ test/transaction/payload/protxcorevectors.js | 182 ++++++ test/transaction/payload/proupregtxpayload.js | 23 + .../transaction/payload/proupservtxpayload.js | 32 ++ .../transaction/payload/MasternodePayout.d.ts | 30 + .../transaction/payload/ProRegTxPayload.d.ts | 99 +++- .../payload/ProUpRegTxPayload.d.ts | 19 +- .../payload/ProUpServTxPayload.d.ts | 26 +- 17 files changed, 1701 insertions(+), 151 deletions(-) create mode 100644 lib/transaction/payload/masternodepayouts.js create mode 100644 test/fixtures/core24-protx.json create mode 100644 test/transaction/payload/protxcorevectors.js create mode 100644 typings/transaction/payload/MasternodePayout.d.ts diff --git a/index.d.ts b/index.d.ts index 5ab8a22d0..9dbc4d139 100644 --- a/index.d.ts +++ b/index.d.ts @@ -61,7 +61,17 @@ export { AbstractPayload }; export { CoinbasePayload } from './typings/transaction/payload/CoinbasePayload'; export { CommitmentTxPayload } from './typings/transaction/payload/CommitmentTxPayload'; export { MnHfSignalPayload } from './typings/transaction/payload/MnHfSignalPayload'; -export { ProRegTxPayload } from './typings/transaction/payload/ProRegTxPayload'; +export { + ProRegTxPayload, + ProRegTxPayloadJSON, + CollateralShare, + CollateralShareJSON, +} from './typings/transaction/payload/ProRegTxPayload'; +export { + MasternodeAddresses, + MasternodePayout, + MasternodePayoutJSON, +} from './typings/transaction/payload/MasternodePayout'; export { ProUpRegTxPayload } from './typings/transaction/payload/ProUpRegTxPayload'; export { ProUpRevTxPayload } from './typings/transaction/payload/ProUpRevTxPayload'; export { ProUpServTxPayload } from './typings/transaction/payload/ProUpServTxPayload'; diff --git a/lib/constants/index.js b/lib/constants/index.js index aee3dd30c..763bf68c4 100644 --- a/lib/constants/index.js +++ b/lib/constants/index.js @@ -45,6 +45,8 @@ const constants = { SML_ENTRY_TYPE_2_ADDITION_SIZE: 22, // ProTxVersion::ExtAddr: addresses are serialized as ExtNetInfo SML_ENTRY_EXT_ADDR_VERSION: 3, + // ProTxVersion::ExtAddr: ProTx payloads with ExtNetInfo addresses and payout lists + PROTX_EXT_ADDR_VERSION: 3, NULL_HASH: '0000000000000000000000000000000000000000000000000000000000000000', // In duffs INSTANTSEND_FEE_PER_INPUT: 10000, diff --git a/lib/transaction/payload/masternodepayouts.js b/lib/transaction/payload/masternodepayouts.js new file mode 100644 index 000000000..262388489 --- /dev/null +++ b/lib/transaction/payload/masternodepayouts.js @@ -0,0 +1,111 @@ +const Preconditions = require('../../util/preconditions'); +const utils = require('../../util/js'); +const Script = require('../../script'); + +// Owner payout list of version 3 (ExtAddr) ProRegTx and ProUpRegTx payloads, +// Core's MasternodePayoutShares (src/evo/providertx.h) + +// MasternodePayoutShare::MAX_REWARD, the reward of a single payee +const MAX_REWARD = 10000; +// Core writes the payout count as one byte +const MAX_PAYOUTS_COUNT = 0xff; + +/** + * @typedef {Object} MasternodePayout + * @property {string} scriptPayout - payee script (p2pkh/p2sh) + * @property {number} reward - share of the owner reward in basis points + */ + +/** + * @typedef {Object} MasternodePayoutJSON + * @property {string} address - payee address + * @property {number} reward - share of the owner reward in basis points + */ + +/** + * @param {BufferReader} reader + * @return {MasternodePayout[]} + */ +function readPayouts(reader) { + const payouts = []; + const count = reader.readUInt8(); + for (let i = 0; i < count; i++) { + const scriptPayoutSize = reader.readVarintNum(); + payouts.push({ + scriptPayout: reader.read(scriptPayoutSize).toString('hex'), + reward: reader.readUInt16LE(), + }); + } + return payouts; +} + +/** + * @param {BufferWriter} writer + * @param {MasternodePayout[]} payouts + */ +function writePayouts(writer, payouts) { + writer.writeUInt8(payouts.length); + payouts.forEach((payout) => { + const scriptPayout = Buffer.from(payout.scriptPayout, 'hex'); + writer.writeVarintNum(scriptPayout.length); + writer.write(scriptPayout); + writer.writeUInt16LE(payout.reward); + }); +} + +/** + * @param {MasternodePayout[]} payouts + */ +function validatePayouts(payouts) { + Preconditions.checkArgument( + Array.isArray(payouts) && payouts.length <= MAX_PAYOUTS_COUNT, + 'Expect payouts to be an array of at most 255 payouts', + ); + payouts.forEach((payout) => { + Preconditions.checkArgument( + utils.isHexaString(payout.scriptPayout), + 'Expect payout scriptPayout to be a hex string', + ); + const script = new Script(payout.scriptPayout); + Preconditions.checkArgument( + script.isPublicKeyHashOut() || script.isScriptHashOut(), + 'Expected payout scriptPayout to be a p2pkh/p2sh', + ); + Preconditions.checkArgument( + utils.isUnsignedInteger(payout.reward) && payout.reward <= MAX_REWARD, + 'Expect payout reward to be an integer from 0 to 10000', + ); + }); +} + +/** + * @param {MasternodePayout[]} payouts + * @param {Network|string} [network] - network for address serialization + * @return {MasternodePayoutJSON[]} + */ +function payoutsToJSON(payouts, network) { + return payouts.map((payout) => ({ + address: new Script(payout.scriptPayout).toAddress(network).toString(), + reward: payout.reward, + })); +} + +/** + * @param {MasternodePayoutJSON[]} payoutsJSON + * @return {MasternodePayout[]} + */ +function payoutsFromJSON(payoutsJSON) { + Preconditions.checkArgument(Array.isArray(payoutsJSON), 'Expect payouts to be an array'); + return payoutsJSON.map((payout) => ({ + scriptPayout: Script.fromAddress(payout.address).toHex(), + reward: payout.reward, + })); +} + +module.exports = { + readPayouts, + writePayouts, + validatePayouts, + payoutsToJSON, + payoutsFromJSON, +}; diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index 4ac9bf691..75552909c 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -1,6 +1,7 @@ /* eslint-disable */ // TODO: Remove previous line and work through linting issues at next edit +var _ = require('lodash'); var utils = require('../../util/js'); var constants = require('../../constants'); var Preconditions = require('../../util/preconditions'); @@ -9,32 +10,62 @@ var BufferReader = require('../../encoding/bufferreader'); var AbstractPayload = require('./abstractpayload'); var Script = require('../../script'); var ipUtil = require('../../util/ip'); +var BN = require('../../crypto/bn'); +var netInfo = require('../../util/netinfo'); +var payouts = require('./masternodepayouts'); const { PLATFORM_NODE_ID_SIZE, - MASTERNODE_TYPE_HP + MASTERNODE_TYPE_BASIC, + MASTERNODE_TYPE_HP, + PROTX_EXT_ADDR_VERSION, + COMPACT_SIGNATURE_SIZE, } = require("../../constants"); var CURRENT_PAYLOAD_VERSION = 2; +// Core writes the share count as one byte +var MAX_SHARES_COUNT = 0xff; /** * @typedef {Object} ProRegTxPayloadJSON * @property {number} version uint_16 2 Provider transaction version number. Currently set to 1. + * @property {number} [type] Masternode type, regular (0) if omitted * @property {string} collateralHash * @property {number} collateralIndex uint_32 4 The collateral index. - * @property {string} service - service address, ip and port + * @property {string} [service] - service address, ip and port. Before version 3 + * @property {Object} [addresses] - "addr:port" lists by purpose. From version 3 * @property {string} keyIDOwner CKeyID 20 The public key hash used for owner related signing (ProTx updates, governance voting) * @property {string} pubKeyOperator BLSPubKey 48 The public key used for operational related signing (network messages, ProTx updates) * @property {string} keyIDVoting CKeyID 20 The public key hash used for voting. * @property {number} operatorReward uint_16 2 A value from 0 to 10000. - * @property {number} operatorReward uint_16 2 A value from 0 to 10000. - * @property {string} payoutAddress + * @property {string} [payoutAddress] Before version 3 + * @property {MasternodePayoutJSON[]} [payouts] Owner payees with their reward shares. From version 3 + * @property {CollateralShareJSON[]} [shares] Collateral shares of a shared masternode. From version 3 + * @property {string[]} [joinSigs] One compact signature per share. From version 3 + * @property {number} [earlyPeriodBlocks] From version 3 + * @property {number} [earlyPenalty] In duffs. From version 3 * @property {string} inputsHash uint256 32 Hash of all the outpoints of the transaction inputs * @property {string} [platformNodeID] Platform Node ID - * @property {number} [platformP2PPort] Platform P2P port - * @property {number} [platformHTTPPort] Platform HTTP port + * @property {number} [platformP2PPort] Platform P2P port. Before version 3 + * @property {number} [platformHTTPPort] Platform HTTP port. Before version 3 * @property {number} [payloadSigSize] Size of the Signature * @property {string} [payloadSig] Signature of the hash of the ProTx fields. Signed with keyIDOwner */ +/** + * @typedef {Object} CollateralShare + * @property {number} amount - contributed collateral in duffs + * @property {string} scriptRefund - refund script (p2pkh/p2sh) + * @property {string} scriptReward - reward script (p2pkh/p2sh), empty to reward scriptRefund + * @property {string} keyIDOwner - the share owner's public key hash + */ + +/** + * @typedef {Object} CollateralShareJSON + * @property {number} amount - contributed collateral in duffs + * @property {string} refundAddress + * @property {string} [rewardAddress] - omitted when rewards go to refundAddress + * @property {string} keyIDOwner - the share owner's public key hash + */ + /** * @class ProRegTxPayload * @property {number} version uint_16 2 Provider transaction version number. Currently set to 1. @@ -42,16 +73,22 @@ var CURRENT_PAYLOAD_VERSION = 2; * @property {number} mode * @property {string} collateralHash * @property {number} collateralIndex uint_32 4 The collateral index. - * @property {string} service - service address, ip and port + * @property {string} [service] - service address, ip and port. Before version 3 + * @property {Object} [addresses] - "addr:port" lists by purpose. From version 3 * @property {string} keyIDOwner CKeyID 20 The public key hash used for owner related signing (ProTx updates, governance voting) * @property {string} pubKeyOperator BLSPubKey 48 The public key used for operational related signing (network messages, ProTx updates) * @property {string} keyIDVoting CKeyID 20 The public key hash used for voting. * @property {number} operatorReward uint_16 2 A value from 0 to 10000. - * @property {string} scriptPayout Script Variable Payee script (p2pkh/p2sh) + * @property {string} [scriptPayout] Script Variable Payee script (p2pkh/p2sh). Before version 3 + * @property {MasternodePayout[]} [payouts] Owner payees with their reward shares. From version 3 + * @property {CollateralShare[]} [shares] Collateral shares of a shared masternode. From version 3 + * @property {string[]} [joinSigs] One 65 byte compact signature per share. From version 3 + * @property {number} [earlyPeriodBlocks] uint_32. From version 3 + * @property {number} [earlyPenalty] In duffs. From version 3 * @property {string} inputsHash uint256 32 Hash of all the outpoints of the transaction inputs * @property {string} [platformNodeID] Platform Node ID - * @property {number} [platformP2PPort] Platform P2P port - * @property {number} [platformHTTPPort] Platform HTTP port + * @property {number} [platformP2PPort] Platform P2P port. Before version 3 + * @property {number} [platformHTTPPort] Platform HTTP port. Before version 3 * @property {number} [payloadSigSize] Size of the Signature * @property {string} [payloadSig] Signature of the hash of the ProTx fields. Signed with keyIDOwner */ @@ -60,20 +97,34 @@ function ProRegTxPayload(options) { this.version = CURRENT_PAYLOAD_VERSION; if (options) { - this.type = options.type; - this.mode = options.mode; + if (options.version !== undefined) { + this.version = options.version; + } + this.type = options.type !== undefined ? options.type : MASTERNODE_TYPE_BASIC; + this.mode = options.mode !== undefined ? options.mode : 0; this.collateralHash = options.collateralHash; this.collateralIndex = options.collateralIndex; - this.service = options.service; this.keyIDOwner = options.keyIDOwner; this.pubKeyOperator = options.pubKeyOperator; this.keyIDVoting = options.keyIDVoting; this.operatorReward = options.operatorReward; - this.scriptPayout = Script.fromAddress(options.payoutAddress).toHex(); + if (this.version >= PROTX_EXT_ADDR_VERSION) { + this.addresses = _.cloneDeep(options.addresses); + this.payouts = payouts.payoutsFromJSON(options.payouts || []); + this.shares = sharesFromJSON(options.shares || []); + this.joinSigs = (options.joinSigs || []).slice(); + this.earlyPeriodBlocks = options.earlyPeriodBlocks || 0; + this.earlyPenalty = options.earlyPenalty || 0; + } else { + this.service = options.service; + this.scriptPayout = Script.fromAddress(options.payoutAddress).toHex(); + } this.inputsHash = options.inputsHash; this.platformNodeID = options.platformNodeID; - this.platformP2PPort = options.platformP2PPort; - this.platformHTTPPort = options.platformHTTPPort; + if (this.version < PROTX_EXT_ADDR_VERSION) { + this.platformP2PPort = options.platformP2PPort; + this.platformHTTPPort = options.platformHTTPPort; + } this.payloadSig = options.payloadSig; this.payloadSigSize = this.payloadSig ? Buffer.from(this.payloadSig, 'hex').length @@ -84,6 +135,62 @@ function ProRegTxPayload(options) { ProRegTxPayload.prototype = Object.create(AbstractPayload.prototype); ProRegTxPayload.prototype.constructor = AbstractPayload; +/** + * @param {CollateralShareJSON[]} sharesJSON + * @return {CollateralShare[]} + */ +function sharesFromJSON(sharesJSON) { + Preconditions.checkArgument( + Array.isArray(sharesJSON), + 'Expect shares to be an array' + ); + return sharesJSON.map(function (share) { + return { + amount: share.amount, + scriptRefund: Script.fromAddress(share.refundAddress).toHex(), + scriptReward: share.rewardAddress + ? Script.fromAddress(share.rewardAddress).toHex() + : '', + keyIDOwner: share.keyIDOwner, + }; + }); +} + +/** + * @param {CollateralShare[]} shares + * @param {Network|string} [network] + * @return {CollateralShareJSON[]} + */ +function sharesToJSON(shares, network) { + return shares.map(function (share) { + var shareJSON = { + amount: share.amount, + refundAddress: new Script(share.scriptRefund) + .toAddress(network) + .toString(), + keyIDOwner: share.keyIDOwner, + }; + if (share.scriptReward) { + shareJSON.rewardAddress = new Script(share.scriptReward) + .toAddress(network) + .toString(); + } + return shareJSON; + }); +} + +/** + * @param {string} script - hex + * @return {boolean} + */ +function isPayoutScript(script) { + if (!utils.isHexaString(script)) { + return false; + } + var parsed = new Script(script); + return parsed.isPublicKeyHashOut() || parsed.isScriptHashOut(); +} + /* Static methods */ /** @@ -98,41 +205,70 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { payload.version = payloadBufferReader.readUInt16LE(); payload.type = payloadBufferReader.readUInt16LE(); payload.mode = payloadBufferReader.readUInt16LE(); + // readReverse copies: reversing a read() slice in place would modify rawPayload payload.collateralHash = payloadBufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + .readReverse(constants.SHA256_HASH_SIZE) .toString('hex'); payload.collateralIndex = payloadBufferReader.readUInt32LE(); - payload.service = ipUtil.bufferToIPAndPort( - payloadBufferReader.read(ipUtil.IP_AND_PORT_SIZE) - ); + if (payload.version >= PROTX_EXT_ADDR_VERSION) { + payload.addresses = netInfo.readExtNetInfo(payloadBufferReader); + } else { + payload.service = ipUtil.bufferToIPAndPort( + payloadBufferReader.read(ipUtil.IP_AND_PORT_SIZE) + ); + } payload.keyIDOwner = payloadBufferReader - .read(constants.PUBKEY_ID_SIZE) - .reverse() + .readReverse(constants.PUBKEY_ID_SIZE) .toString('hex'); payload.pubKeyOperator = payloadBufferReader .read(constants.BLS_PUBLIC_KEY_SIZE) .toString('hex'); payload.keyIDVoting = payloadBufferReader - .read(constants.PUBKEY_ID_SIZE) - .reverse() + .readReverse(constants.PUBKEY_ID_SIZE) .toString('hex'); - payload.operatorReward = payloadBufferReader. readUInt16LE(); - var scriptPayoutSize = payloadBufferReader.readVarintNum(); - payload.scriptPayout = payloadBufferReader - .read(scriptPayoutSize) - .toString('hex'); + payload.operatorReward = payloadBufferReader.readUInt16LE(); + if (payload.version >= PROTX_EXT_ADDR_VERSION) { + payload.payouts = payouts.readPayouts(payloadBufferReader); + var sharesCount = payloadBufferReader.readUInt8(); + payload.shares = []; + for (var i = 0; i < sharesCount; i++) { + payload.shares.push({ + amount: payloadBufferReader.readUInt64LEBN().toNumber(), + scriptRefund: payloadBufferReader.readVarLengthBuffer().toString('hex'), + scriptReward: payloadBufferReader.readVarLengthBuffer().toString('hex'), + keyIDOwner: payloadBufferReader + .readReverse(constants.PUBKEY_ID_SIZE) + .toString('hex'), + }); + } + // one join signature per share + payload.joinSigs = []; + for (var j = 0; j < sharesCount; j++) { + payload.joinSigs.push( + payloadBufferReader.read(COMPACT_SIGNATURE_SIZE).toString('hex') + ); + } + payload.earlyPeriodBlocks = payloadBufferReader.readUInt32LE(); + payload.earlyPenalty = payloadBufferReader.readUInt64LEBN().toNumber(); + } else { + var scriptPayoutSize = payloadBufferReader.readVarintNum(); + payload.scriptPayout = payloadBufferReader + .read(scriptPayoutSize) + .toString('hex'); + } payload.inputsHash = payloadBufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + .readReverse(constants.SHA256_HASH_SIZE) .toString('hex'); if (payload.version >= 2 && payload.type === MASTERNODE_TYPE_HP) { - payload.platformNodeID = payloadBufferReader.read(PLATFORM_NODE_ID_SIZE).reverse().toString('hex'); - payload.platformP2PPort = payloadBufferReader.readUInt16LE(); - payload.platformHTTPPort = payloadBufferReader.readUInt16LE(); + payload.platformNodeID = payloadBufferReader.readReverse(PLATFORM_NODE_ID_SIZE).toString('hex'); + // ExtAddr evonodes keep the platform ports in their addresses + if (payload.version < PROTX_EXT_ADDR_VERSION) { + payload.platformP2PPort = payloadBufferReader.readUInt16LE(); + payload.platformHTTPPort = payloadBufferReader.readUInt16LE(); + } } payload.payloadSigSize = payloadBufferReader.readVarintNum(); @@ -181,7 +317,14 @@ ProRegTxPayload.prototype.validate = function () { utils.isSha256HexString(this.collateralHash), 'Expect collateralHash to be a hex string representing sha256 hash' ); - if (!ipUtil.isZeroAddress(this.service)) { + if (this.version >= PROTX_EXT_ADDR_VERSION) { + Preconditions.checkArgument( + _.isPlainObject(this.addresses), + 'Expected addresses to be an object' + ); + // throws if the addresses can't be serialized + netInfo.extNetInfoToBuffer(this.addresses); + } else if (!ipUtil.isZeroAddress(this.service)) { Preconditions.checkArgument( ipUtil.isIPV4(this.service), 'Expected service to be a string with ip address and port' @@ -231,15 +374,22 @@ ProRegTxPayload.prototype.validate = function () { 'Expect platformNodeID to be a hex string' ); - Preconditions.checkArgument( - utils.isUnsignedInteger(this.platformP2PPort), - 'Expect platformP2PPort to be an integer' - ); + if (this.version < PROTX_EXT_ADDR_VERSION) { + Preconditions.checkArgument( + utils.isUnsignedInteger(this.platformP2PPort), + 'Expect platformP2PPort to be an integer' + ); + + Preconditions.checkArgument( + utils.isUnsignedInteger(this.platformHTTPPort), + 'Expect platformHTTPPort to be an integer' + ); + } + } - Preconditions.checkArgument( - utils.isUnsignedInteger(this.platformHTTPPort), - 'Expect platformHTTPPort to be an integer' - ); + if (this.version >= PROTX_EXT_ADDR_VERSION) { + payouts.validatePayouts(this.payouts); + this.validateShares(); } if (this.scriptPayout) { @@ -267,6 +417,53 @@ ProRegTxPayload.prototype.validate = function () { } }; +/** + * Validate the collateral shares of a version 3 payload + */ +ProRegTxPayload.prototype.validateShares = function validateShares() { + Preconditions.checkArgument( + Array.isArray(this.shares) && this.shares.length <= MAX_SHARES_COUNT, + 'Expect shares to be an array of at most 255 shares' + ); + Preconditions.checkArgument( + Array.isArray(this.joinSigs) && this.joinSigs.length === this.shares.length, + 'Expect one join signature per share' + ); + this.shares.forEach(function (share) { + Preconditions.checkArgument( + Number.isSafeInteger(share.amount) && share.amount >= 0, + 'Expect share amount to be an unsigned integer' + ); + Preconditions.checkArgument( + isPayoutScript(share.scriptRefund), + 'Expected share scriptRefund to be a p2pkh/p2sh' + ); + Preconditions.checkArgument( + share.scriptReward === '' || isPayoutScript(share.scriptReward), + 'Expected share scriptReward to be empty or a p2pkh/p2sh' + ); + Preconditions.checkArgument( + utils.isHexStringOfSize(share.keyIDOwner, constants.PUBKEY_ID_SIZE * 2), + 'Expect share keyIDOwner to be 20 bytes in size' + ); + }); + this.joinSigs.forEach(function (joinSig) { + Preconditions.checkArgument( + utils.isHexStringOfSize(joinSig, COMPACT_SIGNATURE_SIZE * 2), + 'Expect join signatures to be 65 bytes in size' + ); + }); + Preconditions.checkArgument( + utils.isUnsignedInteger(this.earlyPeriodBlocks) && + this.earlyPeriodBlocks <= 0xffffffff, + 'Expect earlyPeriodBlocks to be an uint32' + ); + Preconditions.checkArgument( + Number.isSafeInteger(this.earlyPenalty) && this.earlyPenalty >= 0, + 'Expect earlyPenalty to be an unsigned integer' + ); +}; + /** * Serializes payload to JSON * @param [options] @@ -279,23 +476,47 @@ ProRegTxPayload.prototype.toJSON = function toJSON(options) { var skipSignature = noSignature || (options && options.skipSignature); var network = options && options.network; this.validate(); + var isExtAddr = this.version >= PROTX_EXT_ADDR_VERSION; var payloadJSON = { version: this.version, - collateralHash: this.collateralHash, - collateralIndex: this.collateralIndex, - service: this.service, - keyIDOwner: this.keyIDOwner, - pubKeyOperator: this.pubKeyOperator, - keyIDVoting: this.keyIDVoting, - operatorReward: this.operatorReward, - payoutAddress: new Script(this.scriptPayout).toAddress(network).toString(), - inputsHash: this.inputsHash, }; + // fromJSON reads a missing type as regular, so older versions only report it + // for evonodes and keep their regular masternode JSON as it was + if (isExtAddr || this.type !== MASTERNODE_TYPE_BASIC) { + payloadJSON.type = this.type; + } + + payloadJSON.collateralHash = this.collateralHash; + payloadJSON.collateralIndex = this.collateralIndex; + if (isExtAddr) { + payloadJSON.addresses = _.cloneDeep(this.addresses); + } else { + payloadJSON.service = this.service; + } + payloadJSON.keyIDOwner = this.keyIDOwner; + payloadJSON.pubKeyOperator = this.pubKeyOperator; + payloadJSON.keyIDVoting = this.keyIDVoting; + payloadJSON.operatorReward = this.operatorReward; + if (isExtAddr) { + payloadJSON.payouts = payouts.payoutsToJSON(this.payouts, network); + payloadJSON.shares = sharesToJSON(this.shares, network); + payloadJSON.joinSigs = this.joinSigs.slice(); + payloadJSON.earlyPeriodBlocks = this.earlyPeriodBlocks; + payloadJSON.earlyPenalty = this.earlyPenalty; + } else { + payloadJSON.payoutAddress = new Script(this.scriptPayout) + .toAddress(network) + .toString(); + } + payloadJSON.inputsHash = this.inputsHash; + if (this.version >= 2 && this.type === MASTERNODE_TYPE_HP) { payloadJSON.platformNodeID = this.platformNodeID; - payloadJSON.platformP2PPort = this.platformP2PPort; - payloadJSON.platformHTTPPort = this.platformHTTPPort; + if (this.version < PROTX_EXT_ADDR_VERSION) { + payloadJSON.platformP2PPort = this.platformP2PPort; + payloadJSON.platformHTTPPort = this.platformHTTPPort; + } } if (!skipSignature) { @@ -323,20 +544,55 @@ ProRegTxPayload.prototype.toBuffer = function toBuffer(options) { .writeUInt16LE(this.type) .writeUInt16LE(this.mode) .write(Buffer.from(this.collateralHash, 'hex').reverse()) - .writeInt32LE(this.collateralIndex) - .write(ipUtil.ipAndPortToBuffer(this.service)) + .writeInt32LE(this.collateralIndex); + + if (this.version >= PROTX_EXT_ADDR_VERSION) { + payloadBufferWriter.write(netInfo.extNetInfoToBuffer(this.addresses)); + } else { + payloadBufferWriter.write(ipUtil.ipAndPortToBuffer(this.service)); + } + + payloadBufferWriter .write(Buffer.from(this.keyIDOwner, 'hex').reverse()) .write(Buffer.from(this.pubKeyOperator, 'hex')) .write(Buffer.from(this.keyIDVoting, 'hex').reverse()) - .writeUInt16LE(this.operatorReward) - .writeVarintNum(Buffer.from(this.scriptPayout, 'hex').length) - .write(Buffer.from(this.scriptPayout, 'hex')) - .write(Buffer.from(this.inputsHash, 'hex').reverse()); + .writeUInt16LE(this.operatorReward); + + if (this.version >= PROTX_EXT_ADDR_VERSION) { + payouts.writePayouts(payloadBufferWriter, this.payouts); + payloadBufferWriter.writeUInt8(this.shares.length); + this.shares.forEach(function (share) { + var scriptRefund = Buffer.from(share.scriptRefund, 'hex'); + var scriptReward = Buffer.from(share.scriptReward, 'hex'); + payloadBufferWriter + .writeUInt64LEBN(new BN(share.amount)) + .writeVarintNum(scriptRefund.length) + .write(scriptRefund) + .writeVarintNum(scriptReward.length) + .write(scriptReward) + .write(Buffer.from(share.keyIDOwner, 'hex').reverse()); + }); + this.joinSigs.forEach(function (joinSig) { + payloadBufferWriter.write(Buffer.from(joinSig, 'hex')); + }); + payloadBufferWriter + .writeUInt32LE(this.earlyPeriodBlocks) + .writeUInt64LEBN(new BN(this.earlyPenalty)); + } else { + payloadBufferWriter + .writeVarintNum(Buffer.from(this.scriptPayout, 'hex').length) + .write(Buffer.from(this.scriptPayout, 'hex')); + } + + payloadBufferWriter.write(Buffer.from(this.inputsHash, 'hex').reverse()); if (this.version >= 2 && this.type === MASTERNODE_TYPE_HP) { - payloadBufferWriter.write(Buffer.from(this.platformNodeID, 'hex').reverse()) - .writeUInt16LE(this.platformP2PPort) - .writeUInt16LE(this.platformHTTPPort); + payloadBufferWriter.write(Buffer.from(this.platformNodeID, 'hex').reverse()); + if (this.version < PROTX_EXT_ADDR_VERSION) { + payloadBufferWriter + .writeUInt16LE(this.platformP2PPort) + .writeUInt16LE(this.platformHTTPPort); + } } if (!skipSignature && this.payloadSig) { diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index 290959a6a..eb66bf2d3 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -8,6 +8,7 @@ var BufferReader = require('../../encoding/bufferreader'); var AbstractPayload = require('./abstractpayload'); var Script = require('../../script'); var utils = require('../../util/js'); +var payouts = require('./masternodepayouts'); var isUnsignedInteger = utils.isUnsignedInteger; var isHexString = utils.isHexaString; @@ -18,6 +19,7 @@ var HASH_SIZE = constants.SHA256_HASH_SIZE; var CKEYID_SIZE = constants.PUBKEY_ID_SIZE; var BLSPUBKEY_SIZE = constants.BLS_PUBLIC_KEY_SIZE; var BLSSIG_SIZE = constants.BLS_SIGNATURE_SIZE; +var PROTX_EXT_ADDR_VERSION = constants.PROTX_EXT_ADDR_VERSION; /** * @typedef {Object} ProUpRegTransactionPayloadJSON @@ -25,7 +27,8 @@ var BLSSIG_SIZE = constants.BLS_SIGNATURE_SIZE; * @property {string} proTxHash * @property {string} pubKeyOperator * @property {string} keyIDVoting - * @property {string} payoutAddress + * @property {string} [payoutAddress] Before version 3 + * @property {MasternodePayoutJSON[]} [payouts] Owner payees with their reward shares. From version 3 * @property {string} inputsHash * @property {string} [payloadSig] */ @@ -38,7 +41,8 @@ var BLSSIG_SIZE = constants.BLS_SIGNATURE_SIZE; * @property {string} pubKeyOperator BLSPubKey 48 The public key hash used for operational related signing (network messages, ProTx updates) * @property {string} keyIDVoting CKeyID 20 The public key hash used for voting. * @property {number} scriptPayoutSize compactSize uint 1-9 Size of the Payee Script. - * @property {string} scriptPayout Script Variable Payee script (p2pkh/p2sh) + * @property {string} [scriptPayout] Script Variable Payee script (p2pkh/p2sh). Before version 3 + * @property {MasternodePayout[]} [payouts] Owner payees with their reward shares. From version 3 * @property {string} inputsHash uint256 32 Hash of all the outpoints of the transaction inputs * @property {number} payloadSigSize compactSize uint 1-9 Size of the Signature * @property {string} payloadSig vector Variable Signature of the hash of the ProTx fields. Signed by the Owner. @@ -51,10 +55,18 @@ function ProUpRegTxPayload(payloadJSON) { this.version = CURRENT_PAYLOAD_VERSION; if (payloadJSON) { + if (payloadJSON.version !== undefined) { + this.version = payloadJSON.version; + } this.proTxHash = payloadJSON.proTxHash; + this.mode = payloadJSON.mode !== undefined ? payloadJSON.mode : 0; this.pubKeyOperator = payloadJSON.pubKeyOperator; this.keyIDVoting = payloadJSON.keyIDVoting; - this.scriptPayout = Script.fromAddress(payloadJSON.payoutAddress).toHex(); + if (this.version >= PROTX_EXT_ADDR_VERSION) { + this.payouts = payouts.payoutsFromJSON(payloadJSON.payouts || []); + } else { + this.scriptPayout = Script.fromAddress(payloadJSON.payoutAddress).toHex(); + } this.inputsHash = payloadJSON.inputsHash; if (payloadJSON.payloadSig) { this.payloadSig = payloadJSON.payloadSig; @@ -78,25 +90,27 @@ ProUpRegTxPayload.fromBuffer = function (rawPayload) { var payload = new ProUpRegTxPayload(); var signatureSize = 0; payload.version = payloadBufferReader.readUInt16LE(); + // readReverse copies: reversing a read() slice in place would modify rawPayload payload.proTxHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + .readReverse(HASH_SIZE) .toString('hex'); payload.mode = payloadBufferReader.readUInt16LE(); payload.pubKeyOperator = payloadBufferReader .read(BLSPUBKEY_SIZE) .toString('hex'); payload.keyIDVoting = payloadBufferReader - .read(CKEYID_SIZE) - .reverse() - .toString('hex'); - var scriptPayoutSize = payloadBufferReader.readVarintNum(); - payload.scriptPayout = payloadBufferReader - .read(scriptPayoutSize) + .readReverse(CKEYID_SIZE) .toString('hex'); + if (payload.version >= PROTX_EXT_ADDR_VERSION) { + payload.payouts = payouts.readPayouts(payloadBufferReader); + } else { + var scriptPayoutSize = payloadBufferReader.readVarintNum(); + payload.scriptPayout = payloadBufferReader + .read(scriptPayoutSize) + .toString('hex'); + } payload.inputsHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + .readReverse(HASH_SIZE) .toString('hex'); if (!payloadBufferReader.finished()) { @@ -158,6 +172,10 @@ ProUpRegTxPayload.prototype.validate = function () { 'expected inputsHash to be a sha256 hex string' ); + if (this.version >= PROTX_EXT_ADDR_VERSION) { + payouts.validatePayouts(this.payouts); + } + if (this.scriptPayout) { var script = new Script(this.scriptPayout); Preconditions.checkArgument( @@ -199,9 +217,15 @@ ProUpRegTxPayload.prototype.toJSON = function toJSON(options) { proTxHash: this.proTxHash, pubKeyOperator: this.pubKeyOperator, keyIDVoting: this.keyIDVoting, - payoutAddress: new Script(this.scriptPayout).toAddress(network).toString(), - inputsHash: this.inputsHash, }; + if (this.version >= PROTX_EXT_ADDR_VERSION) { + payloadJSON.payouts = payouts.payoutsToJSON(this.payouts, network); + } else { + payloadJSON.payoutAddress = new Script(this.scriptPayout) + .toAddress(network) + .toString(); + } + payloadJSON.inputsHash = this.inputsHash; if (!skipSignature) { payloadJSON.payloadSig = this.payloadSig; } @@ -227,7 +251,9 @@ ProUpRegTxPayload.prototype.toBuffer = function toBuffer(options) { .write(Buffer.from(this.pubKeyOperator, 'hex')) .write(Buffer.from(this.keyIDVoting, 'hex').reverse()); - if (this.scriptPayout) { + if (this.version >= PROTX_EXT_ADDR_VERSION) { + payouts.writePayouts(payloadBufferWriter, this.payouts); + } else if (this.scriptPayout) { var scriptPayoutBuf = Buffer.from(this.scriptPayout, 'hex'); var scriptPayoutSize = scriptPayoutBuf.length; payloadBufferWriter.writeVarintNum(scriptPayoutSize); diff --git a/lib/transaction/payload/prouprevtxpayload.js b/lib/transaction/payload/prouprevtxpayload.js index 59694f357..68ebf5751 100644 --- a/lib/transaction/payload/prouprevtxpayload.js +++ b/lib/transaction/payload/prouprevtxpayload.js @@ -37,6 +37,9 @@ function ProUpRevTxPayload(payloadJSON) { this.version = CURRENT_PAYLOAD_VERSION; if (payloadJSON) { + if (payloadJSON.version !== undefined) { + this.version = payloadJSON.version; + } this.proTxHash = payloadJSON.proTxHash; this.reason = payloadJSON.reason; this.inputsHash = payloadJSON.inputsHash; @@ -88,14 +91,13 @@ ProUpRevTxPayload.fromBuffer = function (rawPayloadBuffer) { var payloadBufferReader = new BufferReader(rawPayloadBuffer); var payload = new ProUpRevTxPayload(); payload.version = payloadBufferReader.readUInt16LE(); + // readReverse copies: reversing a read() slice in place would modify rawPayloadBuffer payload.proTxHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + .readReverse(HASH_SIZE) .toString('hex'); payload.reason = payloadBufferReader.readUInt16LE(); payload.inputsHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + .readReverse(HASH_SIZE) .toString('hex'); payload.payloadSig = payloadBufferReader .read(constants.BLS_SIGNATURE_SIZE) diff --git a/lib/transaction/payload/proupservtxpayload.js b/lib/transaction/payload/proupservtxpayload.js index 225dd66f4..cd6b133e8 100644 --- a/lib/transaction/payload/proupservtxpayload.js +++ b/lib/transaction/payload/proupservtxpayload.js @@ -9,8 +9,11 @@ var AbstractPayload = require('./abstractpayload'); var utils = require('../../util/js'); var Script = require('../../script'); var ipUtils = require('../../util/ip'); +var netInfo = require('../../util/netinfo'); +var _ = require('lodash'); const { MASTERNODE_TYPE_HP, - PLATFORM_NODE_ID_SIZE + PLATFORM_NODE_ID_SIZE, + PROTX_EXT_ADDR_VERSION, } = require("../../constants"); var isUnsignedInteger = utils.isUnsignedInteger; @@ -26,12 +29,13 @@ var BLSSIG_SIZE = constants.BLS_SIGNATURE_SIZE; * @property {number} version * @property {number} [type] * @property {string} proTxHash - * @property {string} service - Service string, ip and port - * @property {string} [operatorPayoutAddress] + * @property {string} [service] - Service string, ip and port. Before version 3 + * @property {Object} [addresses] - "addr:port" lists by purpose. From version 3 + * @property {string} [operatorPayoutAddress] - omitted when there is no operator payee * @property {string} inputsHash * @property {string} [platformNodeID] Platform Node ID - * @property {number} [platformP2PPort] Platform P2P port - * @property {number} [platformHTTPPort] Platform HTTP port + * @property {number} [platformP2PPort] Platform P2P port. Before version 3 + * @property {number} [platformHTTPPort] Platform HTTP port. Before version 3 * @property {string} [payloadSig] */ @@ -40,12 +44,13 @@ var BLSSIG_SIZE = constants.BLS_SIGNATURE_SIZE; * @property {number} version ProUpServTx version number. Currently set to 1. * @property {number} [type] * @property {string} proTXHash The hash of the initial ProRegTx - * @property {string} service string - ip and port + * @property {string} [service] string - ip and port. Before version 3 + * @property {Object} [addresses] - "addr:port" lists by purpose. From version 3 * @property {string} inputsHash Hash of all the outpoints of the transaction inputs * @property {string} [platformNodeID] Platform Node ID - * @property {number} [platformP2PPort] Platform P2P port - * @property {number} [platformHTTPPort] Platform HTTP port - * @property {string} [scriptOperatorPayout] Payee script (p2pkh/p2sh) + * @property {number} [platformP2PPort] Platform P2P port. Before version 3 + * @property {number} [platformHTTPPort] Platform HTTP port. Before version 3 + * @property {string} [scriptOperatorPayout] Payee script (p2pkh/p2sh), empty if none * @property {string} [payloadSig] BLSSig Signature of the hash of the ProUpServTx fields. Signed by the Operator. */ function ProUpServTxPayload(payloadJSON) { @@ -59,16 +64,22 @@ function ProUpServTxPayload(payloadJSON) { } this.proTxHash = payloadJSON.proTxHash; - this.service = payloadJSON.service; - this.scriptOperatorPayout = Script.fromAddress( - payloadJSON.operatorPayoutAddress - ).toHex(); + if (this.version >= PROTX_EXT_ADDR_VERSION) { + this.addresses = _.cloneDeep(payloadJSON.addresses); + } else { + this.service = payloadJSON.service; + } + this.scriptOperatorPayout = payloadJSON.operatorPayoutAddress + ? Script.fromAddress(payloadJSON.operatorPayoutAddress).toHex() + : ''; this.inputsHash = payloadJSON.inputsHash; if (this.version >= 2 && this.type === MASTERNODE_TYPE_HP) { this.platformNodeID = payloadJSON.platformNodeID; - this.platformP2PPort = payloadJSON.platformP2PPort; - this.platformHTTPPort = payloadJSON.platformHTTPPort; + if (this.version < PROTX_EXT_ADDR_VERSION) { + this.platformP2PPort = payloadJSON.platformP2PPort; + this.platformHTTPPort = payloadJSON.platformHTTPPort; + } } if (payloadJSON.payloadSig) { @@ -101,27 +112,33 @@ ProUpServTxPayload.fromBuffer = function (rawPayload) { payload.type = payloadBufferReader.readUInt16LE(); } + // readReverse copies: reversing a read() slice in place would modify rawPayload payload.proTxHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + .readReverse(HASH_SIZE) .toString('hex'); - payload.service = ipUtils.bufferToIPAndPort( - payloadBufferReader.read(ipUtils.IP_AND_PORT_SIZE) - ); + if (payload.version >= PROTX_EXT_ADDR_VERSION) { + payload.addresses = netInfo.readExtNetInfo(payloadBufferReader); + } else { + payload.service = ipUtils.bufferToIPAndPort( + payloadBufferReader.read(ipUtils.IP_AND_PORT_SIZE) + ); + } // Note: can be 0 if not updated! var scriptOperatorPayoutSize = payloadBufferReader.readVarintNum(); payload.scriptOperatorPayout = payloadBufferReader .read(scriptOperatorPayoutSize) .toString('hex'); payload.inputsHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + .readReverse(HASH_SIZE) .toString('hex'); if (payload.version >= 2 && payload.type === MASTERNODE_TYPE_HP) { - payload.platformNodeID = payloadBufferReader.read(PLATFORM_NODE_ID_SIZE).reverse().toString('hex'); - payload.platformP2PPort = payloadBufferReader.readUInt16LE(); - payload.platformHTTPPort = payloadBufferReader.readUInt16LE(); + payload.platformNodeID = payloadBufferReader.readReverse(PLATFORM_NODE_ID_SIZE).toString('hex'); + // ExtAddr evonodes keep the platform ports in their addresses + if (payload.version < PROTX_EXT_ADDR_VERSION) { + payload.platformP2PPort = payloadBufferReader.readUInt16LE(); + payload.platformHTTPPort = payloadBufferReader.readUInt16LE(); + } } payload.payloadSig = payloadBufferReader.read(BLSSIG_SIZE).toString('hex'); @@ -169,7 +186,14 @@ ProUpServTxPayload.prototype.validate = function () { 'Expect proTXHash to be a hex string representing sha256 hash' ); - if (!ipUtils.isZeroAddress(this.service)) { + if (this.version >= PROTX_EXT_ADDR_VERSION) { + Preconditions.checkArgument( + _.isPlainObject(this.addresses), + 'Expected addresses to be an object' + ); + // throws if the addresses can't be serialized + netInfo.extNetInfoToBuffer(this.addresses); + } else if (!ipUtils.isZeroAddress(this.service)) { Preconditions.checkArgument( ipUtils.isIPV4(this.service), 'Expected service to be a string with ip address and port' @@ -187,15 +211,17 @@ ProUpServTxPayload.prototype.validate = function () { 'Expect platformNodeID to be a hex string' ); - Preconditions.checkArgument( - utils.isUnsignedInteger(this.platformP2PPort), - 'Expect platformP2PPort to be an integer' - ); + if (this.version < PROTX_EXT_ADDR_VERSION) { + Preconditions.checkArgument( + utils.isUnsignedInteger(this.platformP2PPort), + 'Expect platformP2PPort to be an integer' + ); - Preconditions.checkArgument( - utils.isUnsignedInteger(this.platformHTTPPort), - 'Expect platformHTTPPort to be an integer' - ); + Preconditions.checkArgument( + utils.isUnsignedInteger(this.platformHTTPPort), + 'Expect platformHTTPPort to be an integer' + ); + } } if (this.scriptOperatorPayout) { @@ -237,20 +263,32 @@ ProUpServTxPayload.prototype.toJSON = function toJSON(options) { var payloadJSON = { version: this.version, proTxHash: this.proTxHash, - service: this.service, - operatorPayoutAddress: new Script(this.scriptOperatorPayout) - .toAddress(network) - .toString(), - inputsHash: this.inputsHash, }; + if (this.version >= PROTX_EXT_ADDR_VERSION) { + payloadJSON.addresses = _.cloneDeep(this.addresses); + } else { + payloadJSON.service = this.service; + } + + // An empty script means no operator payee; like Core, leave the address out + if (this.scriptOperatorPayout) { + payloadJSON.operatorPayoutAddress = new Script(this.scriptOperatorPayout) + .toAddress(network) + .toString(); + } + + payloadJSON.inputsHash = this.inputsHash; + if (this.version >= 2) { payloadJSON.type = this.type; if (this.type === MASTERNODE_TYPE_HP) { payloadJSON.platformNodeID = this.platformNodeID; - payloadJSON.platformP2PPort = this.platformP2PPort; - payloadJSON.platformHTTPPort = this.platformHTTPPort; + if (this.version < PROTX_EXT_ADDR_VERSION) { + payloadJSON.platformP2PPort = this.platformP2PPort; + payloadJSON.platformHTTPPort = this.platformHTTPPort; + } } } @@ -281,7 +319,11 @@ ProUpServTxPayload.prototype.toBuffer = function toBuffer(options) { payloadBufferWriter.write(Buffer.from(this.proTxHash, 'hex').reverse()); - payloadBufferWriter.write(ipUtils.ipAndPortToBuffer(this.service)); + if (this.version >= PROTX_EXT_ADDR_VERSION) { + payloadBufferWriter.write(netInfo.extNetInfoToBuffer(this.addresses)); + } else { + payloadBufferWriter.write(ipUtils.ipAndPortToBuffer(this.service)); + } var scriptOperatorPayout = Buffer.from(this.scriptOperatorPayout, 'hex'); payloadBufferWriter.writeVarintNum(scriptOperatorPayout.length); @@ -290,9 +332,12 @@ ProUpServTxPayload.prototype.toBuffer = function toBuffer(options) { payloadBufferWriter.write(Buffer.from(this.inputsHash, 'hex').reverse()); if (this.version >= 2 && this.type === MASTERNODE_TYPE_HP) { - payloadBufferWriter.write(Buffer.from(this.platformNodeID, 'hex').reverse()) - .writeUInt16LE(this.platformP2PPort) - .writeUInt16LE(this.platformHTTPPort); + payloadBufferWriter.write(Buffer.from(this.platformNodeID, 'hex').reverse()); + if (this.version < PROTX_EXT_ADDR_VERSION) { + payloadBufferWriter + .writeUInt16LE(this.platformP2PPort) + .writeUInt16LE(this.platformHTTPPort); + } } var payloadSignature; diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index d597ef46a..84f21cfe7 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -55,3 +55,56 @@ import type { UnspentOutput, ChainLockSigMessage, } from ".."; + +import { expectAssignable, expectType } from 'tsd'; +import type { + CollateralShare, + CollateralShareJSON, + MasternodeAddresses, + MasternodePayout, + MasternodePayoutJSON, + ProRegTxPayloadJSON, +} from '..'; + +// Version 3 (ExtAddr) ProTx payloads +declare const proRegTxPayload: ProRegTxPayload; +expectType(proRegTxPayload.addresses); +expectType(proRegTxPayload.payouts); +expectType(proRegTxPayload.shares); +expectType(proRegTxPayload.joinSigs); +expectType(proRegTxPayload.earlyPeriodBlocks); +expectType(proRegTxPayload.earlyPenalty); +expectType(proRegTxPayload.service); + +expectAssignable({ + version: 3, + type: 1, + collateralHash: '00'.repeat(32), + collateralIndex: 0, + addresses: { core_p2p: ['1.2.3.4:9999'], platform_https: ['example.com:443'] }, + keyIDOwner: '00'.repeat(20), + pubKeyOperator: '00'.repeat(48), + keyIDVoting: '00'.repeat(20), + operatorReward: 0, + payouts: [{ address: 'yRyv33x1PzwSTW3B2DV3XXRyr7Z5M2P4V7', reward: 10000 }], + shares: [], + joinSigs: [], + earlyPeriodBlocks: 0, + earlyPenalty: 0, + inputsHash: '00'.repeat(32), + platformNodeID: '00'.repeat(20), +}); +expectAssignable({ + amount: 50000000000, + refundAddress: 'yRyv33x1PzwSTW3B2DV3XXRyr7Z5M2P4V7', + keyIDOwner: '00'.repeat(20), +}); +expectAssignable({ address: 'yRyv33x1PzwSTW3B2DV3XXRyr7Z5M2P4V7', reward: 10000 }); + +declare const proUpServTxPayload: ProUpServTxPayload; +expectType(proUpServTxPayload.addresses); +expectType(proUpServTxPayload.service); + +declare const proUpRegTxPayload: ProUpRegTxPayload; +expectType(proUpRegTxPayload.payouts); +expectType(proUpRegTxPayload.scriptPayout); diff --git a/test/fixtures/core24-protx.json b/test/fixtures/core24-protx.json new file mode 100644 index 000000000..adf6217ae --- /dev/null +++ b/test/fixtures/core24-protx.json @@ -0,0 +1,541 @@ +{ + "note": "Core-produced ProTx vectors; core is Core v24.0.0-rc.4 decoderawtransaction output for the payload", + "transactions": [ + { + "name": "v1 ProRegTx", + "source": "testnet block 150953", + "txid": "85412e8586e7e2015db1d2e9b4dd380e89251ed812e40bf8d5e220ee40bc18a0", + "hex": "0300010001e61aad8cf2b3d654c43c7385923bf939bdc0c912b9321f23ff5fad868bb94b72010000006b483045022100f65b090b11f6e5727de1471f861437380331194d94a9e28c21ba7c94fab44f2e0220369d141b93f96c9ce6ce34eb9f1b10e1a13edceb077c109e522378ea291cdd270121033863da4c7950e1f17d799d9a386d30f8db61d59a9cf1a90e9b265051d50d28f2feffffff0121b81dd2050000001976a914448958c302120b68b31fffaa5012bf578d6b039188ac00000000fd1201010000000000da0c7a3d5af8a813d69d33357370066a2f5e1f7aa6fd6672090f355b81c3d5660100000000000000000000000000ffffa747dfd44e1e0835d0e8b7da48d8880e8d3420b7c8351c7a31d880b7defb6341399f9e9b4ed7c2d627fc828d0eff9c168165b75b24e5fc6c3f5bc8a9eeaee2bc655fdaa58c0d2f3b1b944b682e8847992b8e9a2531cef5a3169ea5b80c5801001976a914d458f5157d91f4832e66eb8630ab3dbff95551d888ac6e2b7fd2cf34db9097a8dd4865d79d273e84d7cd7e72ff9e7bd2d1892f91cad84120b157d4437272eaf0ed1525a0f4bf14444b53b343b65dcd3e24b581e3ac6eb6ab5e0864ad746b7aace3b4ee452a91986316c003db3fa4bc568badb5098e73b2d3", + "coreKey": "proRegTx", + "core": { + "version": 1, + "type": 0, + "collateralHash": "66d5c3815b350f097266fda67a1f5e2f6a06707335339dd613a8f85a3d7a0cda", + "collateralIndex": 1, + "addresses": { + "core_p2p": [ + "167.71.223.212:19998" + ] + }, + "ownerAddress": "yM4riwcjz8sJdsHedwAHpHdoY6vJhSs825", + "votingAddress": "yTCALGQTFNsA4pMPLTKAWdaLRmxfGpbujY", + "payoutAddress": "yfgEfjgmwsTFvqHbiNeMQ48jz1PQdAPmbC", + "pubKeyOperator": "80b7defb6341399f9e9b4ed7c2d627fc828d0eff9c168165b75b24e5fc6c3f5bc8a9eeaee2bc655fdaa58c0d2f3b1b94", + "operatorReward": 0.01, + "inputsHash": "d8ca912f89d1d27b9eff727ecdd7843e279dd76548dda89790db34cfd27f2b6e" + } + }, + { + "name": "v2 ProRegTx with the collateral in its own outputs", + "source": "testnet block 1154851", + "txid": "99ae7010c1c4ef77658ad86abda661828e0933c503b0b5e9941139dded393c80", + "hex": "030001000105cc2df608821afc9b57625e4737d61c39e039e0c3a91689a28e2948d282e290010000006a473044022044c860c0bb1a59144749205fea2b3869c12f508dd4201333470c9433ca35a949022057102ad2c4bf1dfc4f525eb8a14a4c1f640fbe57f3a0668d973db3d76dc2a1d0012103c503ba2aa1b3caf3738199be5b13d3d364496f3823e8edb914276496ef86e372feffffff02969d0e00000000001976a914fa5aadd517848287621bb0cc0046e3a7e1812ba488ac00e876481700000017a9146322236e88a915728e72f6d7a7c7fc1129ebeb998700000000d102000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000ffff592803814e1f05d561177987cb0cab926ce78e4d7bdc4ce461be844e036dd08f2a973014023016dfb5fe4d533be55aeb81da956becd312345aeca4a41b168a51b87ef5e42c3c6892fce06bbc79395663389c2864b7e423e111b9d73b338100001976a914c623e2d926af5eb52dcc2b931993d3e52714511888ac2b00c9bc88c98677be079e57e7d346d0638fcd630cd5b0e47754fe0b1a0d454f00", + "coreKey": "proRegTx", + "core": { + "version": 2, + "type": 0, + "collateralHash": "0000000000000000000000000000000000000000000000000000000000000000", + "collateralIndex": 1, + "addresses": { + "core_p2p": [ + "89.40.3.129:19999" + ] + }, + "ownerAddress": "yLrHr1jPSrAi5bVk3Bij8T2iVAu2mHHeqw", + "votingAddress": "yW96w6ikD45nX8uGddrhVyKPkJ6rgcVeeJ", + "payoutAddress": "yeP7dbbKCZHhzrEHP6goTsPVqXJ91E2Bdy", + "pubKeyOperator": "844e036dd08f2a973014023016dfb5fe4d533be55aeb81da956becd312345aeca4a41b168a51b87ef5e42c3c6892fce0", + "operatorReward": 0, + "inputsHash": "4f450d1a0bfe5477e4b0d50c63cd8f63d046d3e7579e07be7786c988bcc9002b" + } + }, + { + "name": "v2 evonode ProRegTx", + "source": "testnet block 850334", + "txid": "8e11eb784883d3dc9d0d74a74633f067dc61c408dfdee49b8f93bb161f2916c0", + "hex": "03000100016ebca7f8c2852d1034b37fa4e464d6710dd952acad4f252752d9fb703bed2928200000006a47304402202ec687b9f4e8f8047690521d18196644b33b8310f64f854c0b05d2a2d90010380220120fefc98587fc0fe66a89727b1b9bb3880f7fd382148bd35be25e09d2287f760121020a25e4e16b91a17a5ef5627755d9cb81f1cd6d84026262f94ce0f4a4f2cb829dfeffffff01307af505000000001976a914f53e3ff160dab7ec3cc2a6b0afa6dd8d36e3879488ac00000000fd2a01020001000000593aaec65459ca9a97bd34b2657c8201ae83952d062715ba3af0d4255e54e86c2000000000000000000000000000ffff34599a304e1f0063eb51fcfd121e6c3eae23b90b26cd1100217b8160877a911d8bb7d1e75e2320e98cc3233c1f6972cb642424bfcec7c182c56d2c0ebb59e45f788f4d5dbfa2ebff3e3a0063eb51fcfd121e6c3eae23b90b26cd1100217b00001976a914c69a0bda7daaae481be8def95e5f347a1d00a4b488acddb1df193e8da1e028e7985d096ac11e8f38e5f85df16508487ff4ec434af2ae357cb7f558f264c99460327d48066e197c70576d308fa305412084722e83388b959b26cca1ef4aef3106b849f91e1d345c620cafece7b97426164dce31a56ca0736608b0b8ba6969a6c91e763d13db90bd9b213f7bd2a36f8ea5", + "coreKey": "proRegTx", + "core": { + "version": 2, + "type": 1, + "collateralHash": "6ce8545e25d4f03aba1527062d9583ae01827c65b234bd979aca5954c6ae3a59", + "collateralIndex": 32, + "addresses": { + "core_p2p": [ + "52.89.154.48:19999" + ], + "platform_https": [ + "52.89.154.48:1443" + ], + "platform_p2p": [ + "52.89.154.48:36656" + ] + }, + "ownerAddress": "yLMWYiFxCpPwim9YuosKAozkqsGf283XCa", + "votingAddress": "yLMWYiFxCpPwim9YuosKAozkqsGf283XCa", + "payoutAddress": "yeRZBWYfeNE4yVUHV4ZLs83Ppn9aMRH57A", + "pubKeyOperator": "8160877a911d8bb7d1e75e2320e98cc3233c1f6972cb642424bfcec7c182c56d2c0ebb59e45f788f4d5dbfa2ebff3e3a", + "operatorReward": 0, + "platformNodeID": "6d57707c196e06487d326094c964f258f5b77c35", + "inputsHash": "aef24a43ecf47f480865f15df8e5388f1ec16a095d98e728e0a18d3e19dfb1dd" + } + }, + { + "name": "v3 ProRegTx with one payout", + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "txid": "20814c480e94dce7f3fe3f3370ec6bacd1fdc4f446f17aa7de7886be0b5d07c2", + "hex": "0300010001d02a36181466f1f3ec0f61a73114bdf31fa7f137fa880441d7e5e7b672493c04010000006a473044022004f9bb946ee7fab7d823a812cfb27b576f2c0cda4e597a8c92be26d8af33f155022034d05190a9b7a9aaae89fc89148639af1eb7f0601d9c959232b7a159cb2ca991012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff0200e87648170000001976a914fc52fb4219926f353f5475da7885b213c384a24188ac4d4fbfb88b0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000d10300000000000000000000000000000000000000000000000000000000000000000000000000000000000100c1dad84fdab5738de910b64d3a9ae7baf6afbd69806b8889b4ed6ccecefe7861fc3d69b7598f6ab9621fe2568403f94a1c5df398891433cdf301c67fda82f80bfe7f321465e088b8b1360c62946afcbea403cf927d5bfe230000011976a91431389f57b2f32565dcf713d2b2420914885bedcd88ac1027000000000000000000000000004f8f8fe9fd4ad5565cd6c4887bc54e2910a340bb7583c0d8d6a92d52b324095700", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 0, + "collateralHash": "0000000000000000000000000000000000000000000000000000000000000000", + "collateralIndex": 0, + "addresses": {}, + "ownerAddress": "ydzTRqkRHgX8F8iXSguSWordbpFF7g1kS2", + "votingAddress": "yVc85UhtRhQTCiwUCx7EgrVCaLrkMRBmac", + "payouts": [ + { + "address": "yQohnoFhM5avKnzVzAX79xT8uCKviaPxj4", + "script": "76a91431389f57b2f32565dcf713d2b2420914885bedcd88ac", + "reward": 10000 + } + ], + "pubKeyOperator": "806b8889b4ed6ccecefe7861fc3d69b7598f6ab9621fe2568403f94a1c5df398891433cdf301c67fda82f80bfe7f3214", + "operatorReward": 0, + "inputsHash": "570924b3522da9d6d8c08375bb40a310294ec57b88c4d65c56d54afde98f8f4f" + } + }, + { + "name": "v3 ProRegTx with two payouts", + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "txid": "f7e6fad0084ae34e410a8b710e5d74c0af1b95a29c1f3ae5507bed8e08b38adb", + "hex": "0300010001c2075d0bbe8678dea77af146f4c4fdd1ac6bec70333ffef3e7dc940e484c8120010000006a473044022065a0067b55484e06109b4282a8bb14a3392ed76ed0a1195a91a3e3b531098c7f022002aafa428439c858e721664c6b4a71aae2f5bc4ad77bdfc1651a9e8e35428c40012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff0200e87648170000001976a9140b80dfe15263ad7abd2421b82a1291e07d30571c88ac7e654870740000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000ed03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000c707b9fc7af33ddf9a9cf50ff45fc587f590f5693bec56a034e3bdb3ce1e978d5487d03129c507bd15e73fcb7dac95c7333a68f561450702cd1aac511e40275a501ea07d5b43701d895d09a68803bd67d9c23ab94d4428d0000021976a91494a6df7e4fc8f474b28c634c748d5ddf88b7ee3788ac70171976a91417340a777ca7c690d65d19d2f7aca4b1a0387fa388aca00f000000000000000000000000008576d28cdc5f9022cd2988a05001e5ed2c318854bb3c9d19ce3dcb38ad3ce52e00", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 0, + "collateralHash": "0000000000000000000000000000000000000000000000000000000000000000", + "collateralIndex": 0, + "addresses": {}, + "ownerAddress": "yMTDhxJrXzHB4eHtFqoZHMXcTHL7Qe52WA", + "votingAddress": "yfoQfcSCEqAxNkXritaLwXUvjaeCwXNWGB", + "payouts": [ + { + "address": "yZsSm8mVNYuf8ZKayRcwoF3iNsYA5GcWDY", + "script": "76a91494a6df7e4fc8f474b28c634c748d5ddf88b7ee3788ac", + "reward": 6000 + }, + { + "address": "yNS8jUkaEjTwqvUkmPvQvcevDto5o5VNXx", + "script": "76a91417340a777ca7c690d65d19d2f7aca4b1a0387fa388ac", + "reward": 4000 + } + ], + "pubKeyOperator": "93bec56a034e3bdb3ce1e978d5487d03129c507bd15e73fcb7dac95c7333a68f561450702cd1aac511e40275a501ea07", + "operatorReward": 0, + "inputsHash": "2ee53cad38cb3dce199d3cbb5488312cede50150a08829cd22905fdc8cd27685" + } + }, + { + "name": "v3 ProRegTx with a P2SH payout", + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "txid": "1bff9d395c2647e370202e87c80fa3942930567d217238392363cd2542fb496f", + "hex": "0300010001db8ab3088eed7b50e53a1f9ca2951bafc0745d0e718b0a414ee34a08d0fae6f7010000006a4730440220433c544395783b738a418c8b16f0368f13d56e9c56af3d7595e8eb61d41b0a650220771f2132348b28be1b853e7000ea6fa5d5f0d5913dac7bfc07b8f5c8a20d19f5012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff0200e87648170000001976a9140d1b303302b3a691a912fc9b0f2bd4f33f70816488accd7bd1275d0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000cf03000000000000000000000000000000000000000000000000000000000000000000000000000000000001003a02101abc24108438ebe3af704fd64a658fef6bb96d7c328cb02753c2487a780ef8401d621ad5c505beeba3ba5958984fab293bfe411fe9cfeba617ae9b7ada2002c32d9dd47e671a9f43fec688085b7cc462a0e594a26200000117a914d675f38f31792b691cc7c724d8dc44134c710e7e8710270000000000000000000000000070dc754d77ae0cb2034f079c87a764f47f1aaa16afa64baa6aa6df335cb379a800", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 0, + "collateralHash": "0000000000000000000000000000000000000000000000000000000000000000", + "collateralIndex": 0, + "addresses": {}, + "ownerAddress": "yRcAWfPATjBpFpP14gViq3uFPhmmR89sSx", + "votingAddress": "yahyVeQc6RYosRSKaoE4Bf4qmjQXvKFxcr", + "payouts": [ + { + "address": "8yyRcXHGCXypDWTCNZMHUYZJJEC87GqLfK", + "script": "a914d675f38f31792b691cc7c724d8dc44134c710e7e87", + "reward": 10000 + } + ], + "pubKeyOperator": "b96d7c328cb02753c2487a780ef8401d621ad5c505beeba3ba5958984fab293bfe411fe9cfeba617ae9b7ada2002c32d", + "operatorReward": 0, + "inputsHash": "a879b35c33dfa66aaa4ba6af16aa1a7ff464a7879c074f03b20cae774d75dc70" + } + }, + { + "name": "v3 evonode ProRegTx", + "source": "testnet block 1578168", + "txid": "2770d2493755b59b3426474b2a18eb5c34df636530ae8059aa1fe4b22ce47c3c", + "hex": "030001000106ad96cfe67c0fac8f4a54be91fe086c9da510bcf6456ee2554cf45018210f4e000000006a4730440220445ce1f55571ce9b924cb68f7857464bbc918c000c425de9f1ac49c43c470162022056c5481adf9282d90ff01398ffa7d0990e4f21b536195c538651c13fe8fa5a9a01210368037eedf9974ae19dbf7afad02f781ebf59b6e1ab07e9d27375604e0ae55e70feffffff017fef0d00000000001976a9140f85adbdfdf6f484f433e3c756430b85297a97ab88ac00000000fd4701030001000000372ae26ee512d5fafde50b8411ca21080a868d55640cd4d1e5e1d5280f63593f010000000103000101010455d1f30a4e1f010101010455d1f30a8f30020101010455d1f30a05a32351d825841fe0b86f5d5e9b4de6e50dfe831f9ab03a69e21639859a0448d9ad97ee33d9c7807adc1aba1791c759b87aa81b99d79c19ba8778cdda6533ce9f0534a8392f0c51904ec5f4eeeb8d54b4f7b43c67a9d7a202ff0000011976a914482551fcc51ba10000202e7d6a7938cba1cd28ef88ac10270000000000000000000000000029b0ac1ebdd8472b5d4dc4f628e4cb3e70167440e098871bec569eed8658abee0f4f47471c554804e4ab6ec7bf6fd289c438c6e3411f95a5abeadb2a012c78337172a41c8315b5e9a9e38e458338689ce4edc4a6a12a79330d2ee5298a899a80d4ecf42e3501229a81e7f04592f43bccd8ce11b52fde", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 1, + "collateralHash": "3f59630f28d5e1e5d1d40c64558d860a0821ca11840be5fdfad512e56ee22a37", + "collateralIndex": 1, + "addresses": { + "core_p2p": [ + "85.209.243.10:19999" + ], + "platform_p2p": [ + "85.209.243.10:36656" + ], + "platform_https": [ + "85.209.243.10:1443" + ] + }, + "ownerAddress": "yPYCYZTERfPECQBRoup2ZGmK8scxjoUvkD", + "votingAddress": "yMSafeg9yvi9mMhpL9xQJqVZVkaidjFMDK", + "payouts": [ + { + "address": "yStvD597am8AEy6wgqb112mExYD1ipKk7C", + "script": "76a914482551fcc51ba10000202e7d6a7938cba1cd28ef88ac", + "reward": 10000 + } + ], + "pubKeyOperator": "b03a69e21639859a0448d9ad97ee33d9c7807adc1aba1791c759b87aa81b99d79c19ba8778cdda6533ce9f0534a8392f", + "operatorReward": 0, + "platformNodeID": "e3c638c489d26fbfc76eabe40448551c47474f0f", + "inputsHash": "eeab5886ed9e56ec1b8798e0407416703ecbe428f6c44d5d2b47d8bd1eacb029" + } + }, + { + "name": "v3 ProRegTx with a core_p2p address", + "source": "testnet block 1578185", + "txid": "13ccca675ed8a8a788693f46191e7237e2896701ede3058c20263e1b2343923d", + "hex": "03000100013c7ce42cb2e41faa5980ae306563df345ceb182a4b4726349bb5553749d27027000000006a47304402204f239b22bb793a2ad1f0da13269484b1f7644e8ac39dc8a71043e990215517f802202686ddde448a508ca41a032de1f05974b27440a2712a6039173c4d76422cffda01210368037eedf9974ae19dbf7afad02f781ebf59b6e1ab07e9d27375604e0ae55e70feffffff0192ed0d00000000001976a9140f85adbdfdf6f484f433e3c756430b85297a97ab88ac00000000fd1d01030000000000f02bdee9b0c994f184b1e83a731b1c0b0afcecf3aa1dd5376be21ce535292199000000000101000101010455d1f32a4e1f50c455d6408cbe0ff9223ea627d7c1330587ec1e9321b7d8d72b12d10b413673a65b81d375bd0cc1b7f05f027d974d30209452167e577eddad6a76ac737d1563c659d1e1804d680fe032b74dd93132e8ffcfc10dcea976b00000011976a914482551fcc51ba10000202e7d6a7938cba1cd28ef88ac1027000000000000000000000000000e306067b2a7ea73fd4fa6cf4263241d7d7e2eec1f9a3f74f6584b057eac8078411f760624efb2010edff7624e8e13ad95ab27d77e9b28cfd3a7e024cc6cfc6cf95b093c2aa7a91b2678cd7013028c434bd42efe0caf45bdd01c156b385501deea1e", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 0, + "collateralHash": "99212935e51ce26b37d51daaf3ecfc0a0b1c1b733ae8b184f194c9b0e9de2bf0", + "collateralIndex": 0, + "addresses": { + "core_p2p": [ + "85.209.243.42:19999" + ] + }, + "ownerAddress": "yTgW7AYPjSLvWwEJNkJYrpx6Th9n4Qtjjw", + "votingAddress": "yY1r548L2o6r5TMUxrUabJS1bmKGPs4dub", + "payouts": [ + { + "address": "yStvD597am8AEy6wgqb112mExYD1ipKk7C", + "script": "76a914482551fcc51ba10000202e7d6a7938cba1cd28ef88ac", + "reward": 10000 + } + ], + "pubKeyOperator": "9321b7d8d72b12d10b413673a65b81d375bd0cc1b7f05f027d974d30209452167e577eddad6a76ac737d1563c659d1e1", + "operatorReward": 0, + "inputsHash": "7880ac7e054b58f6743f9a1fec2e7e7d1d246342cfa64ffd73eaa7b26760300e" + } + }, + { + "name": "v3 evonode ProRegTx with IPv4 and IPv6 addresses", + "source": "Core v24.0.0-rc.4 regtest (rpc_netinfo.py)", + "txid": "e359f3d1618185876cb2509a7c273cbcc38b4e1c8e787d29b07548f8eaf37d9b", + "hex": "0300010001ff4d96414d8a55ff0ca683802b844894cc7abdab27d0974551fe65bc473fdf3c010000006a47304402204de0a1fa92db272c298b3d44f79e43285872c2fdb072654beeb0bfa561c1b151022068c46fb34e5ff29dd9c3ef0af4c3aeb2e50c3c3691f315f7d39cab005a6a145101210295b6ff1a73e2ae375580f227ad2cb8aff3f177a3d7bb0bbdf0a2cdc6679a2804feffffff013fc9f205000000001976a914b1875638baa1a23836668bbacc4cf786a5feb99488ac00000000fd8601030001000000ff4d96414d8a55ff0ca683802b844894cc7abdab27d0974551fe65bc473fdf3c00000000010300020101047f0000014def010210000000000000000000000000000000014def01020101047f00000256b80102100000000000000000000000000000000256b802020101047f00000356b90102100000000000000000000000000000000356b970a7ddca9cf0e172d53cd91b053a5754213530bda29b430b2f3ed0c46fa54444945809658316ed61aed349be9fc27a513e1ad80a9bfae46a2c24d242b369f981610976ae302b0b436bce3be3e46bfb9c0ef53e2f7fe6d4260000011976a914952cfa4a2957ec9672b54cb47208cc1f6dd9216388ac10270000000000000000000000000051c966582899cfae0b70ffaff0da550937c08314213cb0de684c09fe670920a5cf6534fbfeaeeb5fc3329989773fc8148fb33405411f1ee16bc83a6bf7b171e428ff582c65accab15c6e2bea3c6b478df6341683a7856f104f9e637f3374a1d59a479370e749348423eb6d11d935ab2c5204b8a2d574", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 1, + "collateralHash": "3cdf3f47bc65fe514597d027abbd7acc9448842b8083a60cff558a4d41964dff", + "collateralIndex": 0, + "addresses": { + "core_p2p": [ + "127.0.0.1:19951", + "[::1]:19951" + ], + "platform_p2p": [ + "127.0.0.2:22200", + "[::2]:22200" + ], + "platform_https": [ + "127.0.0.3:22201", + "[::3]:22201" + ] + }, + "ownerAddress": "yWb7dFLxDsb3mtEj96k2PsgUtNftTdUvu4", + "votingAddress": "yQi8rAFVBUDeiGkrKygCvH7N5oFUKTjGdS", + "payouts": [ + { + "address": "yZvDQtcnAnEV41cZTBvkGxuznw4qi1mpoF", + "script": "76a914952cfa4a2957ec9672b54cb47208cc1f6dd9216388ac", + "reward": 10000 + } + ], + "pubKeyOperator": "a29b430b2f3ed0c46fa54444945809658316ed61aed349be9fc27a513e1ad80a9bfae46a2c24d242b369f981610976ae", + "operatorReward": 0, + "platformNodeID": "0534b38f14c83f77899932c35febaefefb3465cf", + "inputsHash": "a5200967fe094c68deb03c211483c0370955daf0afff700baecf99285866c951" + } + }, + { + "name": "v3 shared ProRegTx rewarding the refund scripts", + "source": "Core v24.0.0-rc.4 regtest (feature_masternode_shares)", + "txid": "940a4366e2882c54d9fa5603a2fcafc9ea5c61e0c70880dc72836c7c37d20dd2", + "hex": "030001000301ea4b269852eea5b757425338a2ecc4821dca7c98d5bf705f17924ac2673216010000006a4730440220611214312012fb198d6b9037c956f1383b3a737439a7731c3b59769ab59028d3022040a39c4fd7ab9266978b475af85a19adead58349f2bbe3e5daeb64f29ac50e9f012103cecf2c15f343e2b2f686791bf0b9cb52e23ba20d026bd2c5d2554b6d4f144ffafeffffff0b83eb20c7c1a09a82e6ed2eb64747398057dc0e7ad724d9b75bf824a607da84000000006a4730440220649824c91b2cdb212eab7acd3b0d0da65124bc184db5d0da127cfbe8df67bfc502202b441640cc7f0352dbc7985082acf9b7d1bfc1a322b5d37ea4ebe9af2df893610121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0fefffffffd74c6263d8142cbd75edb80d90c3feff4edba714fd7cd1e3181e8d604271298000000006a47304402200c648394571a7edabd47e6573e64092bf21fa1ebc55596f2de487570bb10d234022045aaf980cbc5ec9e20c8ef01de173de48079d4b8d9bedc2d3352093247e4632b0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff0271aa750f000000001976a91475fa0cfb1d2dc8388502a0e01fa40b223e28d71b88ac00e8764817000000070444534843755100000000fdb001030000000000000000000000000000000000000000000000000000000000000000000000000001000000010100010101047f0000014e180000000000000000000000000000000000000000adb83dedab3c3ee36b615ca03529e4fe6eeaf1197d22fc9f83c599f264d52d9c948ec15480fb887c8ad476219c857b142c81e23625d39ff4807c7536746522c6bfa4c64d00000002005847f80d0000001976a9146df7dbd625c1bc8cf69fd9e0463cd578330989e788ac00ac037e3237cf06abc3122dac494a89371d3c385f00902f50090000001976a91488922546490c4c88f43f0f99c3764e0399daa97888ac009ce475ae05fac0db32d0147f5949cd8602efaf501f0acf45f76a5d6d859ecf4cd513ea2b22933ae0b11a20d4207f4f0ca95cf417855b0c337c8fbeaee6fad2d3250818210d614bb13eadb99a003efe6b4887e93fb61f7b32a7d6e2e003847ac04e87d61204e9d6b21cb9f51e690605303cfdea4a3ea6679f7f41eedc6c564cee4566aba3f4fdd649e88bb51fdff442b8fb77e4db61741e00000000f2052a010000009be55ced0b45f03c41fafbbe3c7b64d4a4364288b06806717160291fc9a0fd7d00", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 0, + "collateralHash": "0000000000000000000000000000000000000000000000000000000000000000", + "collateralIndex": 1, + "addresses": { + "core_p2p": [ + "127.0.0.1:19992" + ] + }, + "votingAddress": "yQNnB7jvLNiQUMru35cvdNQmjfVj38rv3w", + "shares": [ + { + "amount": 60000000000, + "refundAddress": "yWLuRe3VkQP4Cpin5dxzTNUC1Ywtx8tjhu", + "refundScript": "76a9146df7dbd625c1bc8cf69fd9e0463cd578330989e788ac", + "rewardAddress": "yWLuRe3VkQP4Cpin5dxzTNUC1Ywtx8tjhu", + "rewardScript": "76a9146df7dbd625c1bc8cf69fd9e0463cd578330989e788ac", + "ownerAddress": "ybzyFtirxybtR3xxsnGYo7xQJUMjRi51pF" + }, + { + "amount": 40000000000, + "refundAddress": "yYmZpfcZ8Zwz68hX3ACUWpqr3fBjfmQvek", + "refundScript": "76a91488922546490c4c88f43f0f99c3764e0399daa97888ac", + "rewardAddress": "yYmZpfcZ8Zwz68hX3ACUWpqr3fBjfmQvek", + "rewardScript": "76a91488922546490c4c88f43f0f99c3764e0399daa97888ac", + "ownerAddress": "yad1wnY4m75L56n7imX3tc14QTAUFiFVs8" + } + ], + "earlyPeriodBlocks": 30, + "earlyPenalty": 5000000000, + "pubKeyOperator": "adb83dedab3c3ee36b615ca03529e4fe6eeaf1197d22fc9f83c599f264d52d9c948ec15480fb887c8ad476219c857b14", + "operatorReward": 0, + "inputsHash": "7dfda0c91f296071710668b0884236a4d4647b3cbefbfa413cf0450bed5ce59b" + } + }, + { + "name": "v3 shared ProRegTx with an explicit reward script", + "source": "Core v24.0.0-rc.4 regtest (feature_masternode_shares)", + "txid": "19e35b18d0e3cf92ade96f39a645e2744c8b95c3188e2de0e4bb13240cb54557", + "hex": "0300010002c37f310923a7ef49dbdcc76346453a0b39a9661813bee7ec085bfd14e89b2d39000000006a473044022072fa1ce5b8411642e68ac39e23935eff5388c15fc25e7be6f3d617dc07f0632702202462586617acf41a1269c6fcc96437449f9d6b2b1ef9fb51c7ba284034f5eff5012102db047a01c61fd6236256f291489a834edf5baf356ea0576936e8a44458e1cf6cfeffffff7e5dab6d0538585c1a6b4eb5a1ad9399562f032408cb46549af5f25ca0c09ab7000000006a473044022030ee0af62d8e5b4cc4f777637938442e57ff9f914f7b9487bb019590c7a56ca8022068fed57fb915b71ddf3dc6520da45204a502482ef9c3d7224d46d8b0628e3cac0121037362607c46cb011c7adebe9c859d8db4b43cd64aab909a3a9b3ce86c6e0f3a40feffffff020bae0002000000001976a9145d14741996ee81faad813dec69aa35318421693288ac00e8764817000000070444534843755100000000fdc901030000000000000000000000000000000000000000000000000000000000000000000000000001000000010100010101047f0000014e19000000000000000000000000000000000000000084a7590b35a7bf88a4cb0349a4d4b9a516109b313af058ad7fefa8d3fcc9bb8f2ad05901dbac7c811908a9b4aa2ad2df3492289bf0a5d3903cd6d9265efb8aa82b9b5d1b0000000200743ba40b0000001976a914a45144abc7b983fc43d4def8234f5c7a424b6dd988ac00d20fe29392af096bd59f1d930ded7e920885dd7600743ba40b0000001976a914c508b6677c1ead65e9378877507dccc10d0aea5488ac1976a914c38c67bc952cada46d157b7dda86ddbdfb767a6e88acf29bedc1c9141c5fc8d2ebacb3de686c6648cac41f8f5c54d122c2a7d0a0dd5e3d3504f31e25dc4ad1957488bd46766ca57363ee6d06ec565f1e7ea0173a924afc254c488fa522c6b3f57ac3563c6f184927e5b9fe204b3f6b509508665f585e6e64823db85ab8d4c065cdc79743634a38c7dca031a707697318297e93713e5522e025581e4ad218c2a33d5c4c7eef61b5d7172859421e00000000f2052a01000000d45b7193b9f3ea8cabe285659ab454b172ba031544e73b93c2adca7f07e2901300", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 0, + "collateralHash": "0000000000000000000000000000000000000000000000000000000000000000", + "collateralIndex": 1, + "addresses": { + "core_p2p": [ + "127.0.0.1:19993" + ] + }, + "votingAddress": "yR7R5TWT5DS5fez9JD5GMQ5wUiyaeEjC9o", + "shares": [ + { + "amount": 50000000000, + "refundAddress": "ybJH2HC78nMUKfjR2b8yS7YMrkN6iJZzhX", + "refundScript": "76a914a45144abc7b983fc43d4def8234f5c7a424b6dd988ac", + "rewardAddress": "ybJH2HC78nMUKfjR2b8yS7YMrkN6iJZzhX", + "rewardScript": "76a914a45144abc7b983fc43d4def8234f5c7a424b6dd988ac", + "ownerAddress": "yfU9nCccy4FXsFE4emBJanb4DJPEwF3hRk" + }, + { + "amount": 50000000000, + "refundAddress": "yeHGQMFMesaWWx8A46y4rQzX2gTxzwC9Jw", + "refundScript": "76a914c508b6677c1ead65e9378877507dccc10d0aea5488ac", + "rewardAddress": "ye9Qp8q3qZhz9H9UgeSTMxebd7zQtXDL3N", + "rewardScript": "76a914c38c67bc952cada46d157b7dda86ddbdfb767a6e88ac", + "ownerAddress": "yiSFAk9MzN5NSRtp5eBuU8nLsgisjCtSiF" + } + ], + "earlyPeriodBlocks": 30, + "earlyPenalty": 5000000000, + "pubKeyOperator": "84a7590b35a7bf88a4cb0349a4d4b9a516109b313af058ad7fefa8d3fcc9bb8f2ad05901dbac7c811908a9b4aa2ad2df", + "operatorReward": 0, + "inputsHash": "1390e2077fcaadc2933be7441503ba72b154b49a6585e2ab8ceaf3b993715bd4" + } + }, + { + "name": "v3 evonode ProUpServTx", + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "txid": "7c58d9776ef31869b8cae3f94366e9fedb914e41a5b4c26d048c12f30f5e51ac", + "hex": "03000200016f49fb4225cd6323393872217d56302994a30fc8872e2070e347265c399dff1b010000006a47304402207bbed2c28fe9710187db1da4ab50a3c380108996b6cbcd972407ea8e36ad0da202200ffb02cc90a027e35a452cf8ed1edf33d567aaf2797150a797124e83871bad08012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff01277ad1275d0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000dc0300010022a1b2590651ff843222d7564397d0e7af94e94c35b94621822c268fcdebcba90103000101010444437aed4e21010101010444437aed6820020101010444437aed05a300e4b92ed4f5d34d4eb060b39c385ad1cf0ab338eb26619187e762c5e0c7c1080415d36e5fdf5b02343adb01a134f2aa78513658f9a751dcd16e848065f3b548fd24268a7f330346049b39e3e75bf75c0a77584fd2c02832000f39c15f38e43444ca05e898077bfad4bc0f91cb9158f10066ae056971a8011d05e9152f1017a305e8dfd7a3b292de2eb5230c434c59e622f3c0652f", + "coreKey": "proUpServTx", + "core": { + "version": 3, + "type": 1, + "proTxHash": "a9cbebcd8f262c822146b9354ce994afe7d0974356d7223284ff510659b2a122", + "addresses": { + "core_p2p": [ + "68.67.122.237:20001" + ], + "platform_p2p": [ + "68.67.122.237:26656" + ], + "platform_https": [ + "68.67.122.237:1443" + ] + }, + "platformNodeID": "f958365178aaf234a101db3a34025bdf5f6ed315", + "inputsHash": "0408c1c7e0c562e787916126eb38b30acfd15a389cb360b04e4dd3f5d42eb9e4" + } + }, + { + "name": "v3 ProUpServTx", + "source": "testnet block 1579049", + "txid": "d0faa9b0748eb837f5d55f6aad7d3e83352cf69de161521e7fc37fda877ba098", + "hex": "03000200018e8b4dea67509df4c39c4697f6bfb8e68bbb460d3d8b32c9cab16543a82fd720000000006a4730440220349f96398d556fbaec3ed9e0b37eb0a6a202bcbf012354034062b11d503bca7402205e6ff7820151321953ceffb95256cd4086b75328e5b5c190f3fd42683de5d13f012102325e83cb1285c92eee0ab95a95e51ec81431bf8311384fbbb3bc2fd5ff63424afeffffff014aa10e00000000001976a914dbe0812a19d7e3e8e168fd060e8680fc6a05d2f288ac00000000b203000000b27541f9766ea980fa9df7c3b41808530509cdba5f2d52238628df32b8ad56a3010100010101044e1146af4dbb0098b86264de16a59ad62d848a7e953c5a6a68a523e4d1eadbd68a42745ab60dcfa0e0459b98edb459d9431da6e8f84b7eefa4ee6736d97f2ff0708f3d0800b036ad27cdab33bb031762a9fb09a433d68b13d713d8f0e0f0a046bdeba3831c8228c549614374d5bc78ffd74973b923e3848883a5feee3f4ced04d60471ea207b7f", + "coreKey": "proUpServTx", + "core": { + "version": 3, + "type": 0, + "proTxHash": "a356adb832df288623522d5fbacd0905530818b4c3f79dfa80a96e76f94175b2", + "addresses": { + "core_p2p": [ + "78.17.70.175:19899" + ] + }, + "inputsHash": "cf0db65a74428ad6dbead1e423a5686a5a3c957e8a842dd69aa516de6462b898" + } + }, + { + "name": "v3 evonode ProUpServTx with IPv6 addresses", + "source": "Core v24.0.0-rc.4 regtest (rpc_netinfo.py)", + "txid": "93c7e5501a7a2eb6fb4857e38b6e1724d5c234a8e19fcbee0eb2d3d43f4e1123", + "hex": "03000200019b7df3eaf84875b0297d788e1c4e8bc3bc3c277c9a50b26c87858161d1f359e3000000006a473044022055f5ec08f21ebc5144b494ece181dc91c4f41456f2a3ceac41e951d6304ecf0d0220515a065961b9a22aaa47283b8b3679d5528ee3c231a08f73c04b82c589e7501a01210295b6ff1a73e2ae375580f227ad2cb8aff3f177a3d7bb0bbdf0a2cdc6679a2804feffffff016dc7f205000000001976a914b1875638baa1a23836668bbacc4cf786a5feb99488ac00000000fd0601030001009b7df3eaf84875b0297d788e1c4e8bc3bc3c277c9a50b26c87858161d1f359e3010300020101047f0000014def010210000000000000000000000000000000014def01010101047f00000156c402020101047f00000356b90102100000000000000000000000000000000356b900236b3438083fa57d1fea5e04ca139fde89a29b7a20a94252f9eb983db75e1045cf6534fbfeaeeb5fc3329989773fc8148fb33405a2aa84d633b037a5badfce1ab2d236aa1e99565f7cf5494a091a86ded92c71c081fc7f27771f5d67a473e30d26ac8b16048f4e99a09f917143f8f1ba5ba2c810c72dd963a66d07de8a2129a3ec301596a746d34ff7f16e7594e790b4794b8562", + "coreKey": "proUpServTx", + "core": { + "version": 3, + "type": 1, + "proTxHash": "e359f3d1618185876cb2509a7c273cbcc38b4e1c8e787d29b07548f8eaf37d9b", + "addresses": { + "core_p2p": [ + "127.0.0.1:19951", + "[::1]:19951" + ], + "platform_p2p": [ + "127.0.0.1:22212" + ], + "platform_https": [ + "127.0.0.3:22201", + "[::3]:22201" + ] + }, + "platformNodeID": "0534b38f14c83f77899932c35febaefefb3465cf", + "inputsHash": "45105eb73d98ebf95242a9207a9ba289de9f13ca045eea1f7da53f0838346b23" + } + }, + { + "name": "v3 evonode ProUpServTx with Tor, I2P and domain addresses", + "source": "Core v24.0.0-rc.4 regtest (rpc_netinfo.py)", + "txid": "5eacf4c3c61cd973786f3bd0ff4c859736f1bbd09c228a76f1d6b226f746c4ca", + "hex": "030002000155e8d2540358188eccdfba9294012d6fcfa6f894cf0660ff9c0579d88fa7c0d3000000006a473044022052891f43aa6fc7c7db72a5c562cc604be67cb5fffcaea27f11259cf4091c0bb20220431f343e7b38b6043db2d3af8b031f9ec50a9c788268510f65042f4d6748cbfa012103409f891a75b1acfdc8dfc7dfb3af0ffa31d66370da456162f05315b01f0f101ffeffffff01d13cf105000000001976a9147392164e126073bf3f2933cb5751a017ac8d83ff88ac00000000fd3e010300010055e8d2540358188eccdfba9294012d6fcfa6f894cf0660ff9c0579d88fa7c0d3010300020101047f0000014def01042053cd5648488c4707914182655b7664034e09e66f7e8cbf1084e654eb56c5bd884def01020101047f00000156b8010520170c56ce72a5a0e62306a3c7084318ee3a46355d17f67896a09c51efbe23fd71000002020101047f00000156b902147365727665722d312e6578616d706c652e636f6d56b900ebf1d1a38a530e1b8e1384fd51454f3b97698eab4ba033e9258694c307d8165dc5a576a5f3ec9ec80ceea45c36cbe1d34e53663f95a2f52322c31a7376ed01403ab5038fde188c7e1fbe56c14a46d197da0e16c41e69ef77d9a7e1b6af3a61bde8352d810e1f17ac33fdfc55790d1a1823e91572a31f1e34804620e10bec5b062aaab960b94e1c7c5f10a4ebaadac5ed7a1ceee7", + "coreKey": "proUpServTx", + "core": { + "version": 3, + "type": 1, + "proTxHash": "d3c0a78fd879059cff6006cf94f8a6cf6f2d019492badfcc8e18580354d2e855", + "addresses": { + "core_p2p": [ + "127.0.0.1:19951", + "kpgvmscirrdqpekbqjsvw5teanhatztpp2gl6eee4zkowvwfxwenqaid.onion:19951" + ], + "platform_p2p": [ + "127.0.0.1:22200", + "c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p:0" + ], + "platform_https": [ + "127.0.0.1:22201", + "server-1.example.com:22201" + ] + }, + "platformNodeID": "3f66534ed3e1cb365ca4ee0cc89eecf3a576a5c5", + "inputsHash": "5d16d807c3948625e933a04bab8e69973b4f4551fd84138e1b0e538aa3d1f1eb" + } + }, + { + "name": "v3 ProUpServTx with an operator payout", + "source": "Core v24.0.0-rc.4 regtest (feature_masternode_shares)", + "txid": "620080e59861a2b6f8dbd08c10916e3d2a980dd054576a25989183f01d7a36eb", + "hex": "030002000116fcab13c23995189ea2af024c52b66b93fa60e6c4e3114e5416411df03af237010000006a473044022073f09bef4aa6cb9d63eb7f3c2b3f59f925c4aa0a662a7a562c4a611e4cfe57fe0220597cffba60b3cba10f5aa845f2f5c7f8e6d97b2c290bf84ffc6c0674206571c901210293fe1cce3f5fd90923bf131b55f3188a5f6f689a329797d88e80fe97d34327ccfeffffff016bdff505000000001976a914a529b9dd7431f7b074aedf281129f243badafcf588ac00000000cb03000000a5192a90c76040cfa3bde5137981b8f4a3bf6d6077e55942987ce43e40b98090010100010101047f0000014e1d1976a914bfd0313566acd351eb57ef5c2e8b9a89245bb56488ac1fb55ee81c3dd0b8057d8da5c6e18fb69421734f6b9afc102877e61e1b3b4ec9b9d1a5c18d1a63581e8ec83276ac605b7d7b447544cbfa69e1908153e8b491e40c0339943d6154c28104b5f09c08f4481292fa3bb3aff2cd72a1bfc4344aa81a4a59879d0f94d228ed9c70f94abbdb9dc9231df3b5262c096d1b75e57542b7af", + "coreKey": "proUpServTx", + "core": { + "version": 3, + "type": 0, + "proTxHash": "9080b9403ee47c984259e577606dbfa3f4b8817913e5bda3cf4060c7902a19a5", + "addresses": { + "core_p2p": [ + "127.0.0.1:19997" + ] + }, + "operatorPayoutAddress": "ydofKHspDAhHW7ZJsuJTVyRAfHBtiErCka", + "inputsHash": "c94e3b1b1ee6772810fc9a6b4f732194b68fe1c6a58d7d05b8d03d1ce85eb51f" + } + }, + { + "name": "v3 ProUpRegTx", + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "txid": "16b69f49a4aba21954e199efe7209478763133f7c473fab6668d5d2b79e55775", + "hex": "0300030001ac515e0ff3128c046dc2b4a5414e91dbfee96643f9e3cab86918f36e77d9587c000000006a47304402202e23fe274788884c91a48592dbe2cfec421286c1c56a0a4f83b27e731b40bdd5022040b0818dae31cd8213d530c31768bf4fdd9e84a3d7f95ac77214c79ec1e35cf7012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff017678d1275d0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000e70300c2075d0bbe8678dea77af146f4c4fdd1ac6bec70333ffef3e7dc940e484c81200000806b8889b4ed6ccecefe7861fc3d69b7598f6ab9621fe2568403f94a1c5df398891433cdf301c67fda82f80bfe7f321465e088b8b1360c62946afcbea403cf927d5bfe23011976a9144a83c35e84c83510f20989dee7845ff133c567af88ac1027415720a36cce2d68158913b3523ff1167e016cfd049560109894dd9d844fa99c411f5e48101069586123670bd805a9f7b28ed3ebbf18292d18c3f5bfc819bb76ab8a76b5730eab51427a15742ef809693c8e0dbe27737a17f000823859ce58c30f03", + "coreKey": "proUpRegTx", + "core": { + "version": 3, + "proTxHash": "20814c480e94dce7f3fe3f3370ec6bacd1fdc4f446f17aa7de7886be0b5d07c2", + "votingAddress": "yVc85UhtRhQTCiwUCx7EgrVCaLrkMRBmac", + "payouts": [ + { + "address": "yT7ShTqEqPBskpTAiHzWaAggFmD4j1MCT3", + "script": "76a9144a83c35e84c83510f20989dee7845ff133c567af88ac", + "reward": 10000 + } + ], + "pubKeyOperator": "806b8889b4ed6ccecefe7861fc3d69b7598f6ab9621fe2568403f94a1c5df398891433cdf301c67fda82f80bfe7f3214", + "inputsHash": "9ca94f849ddd949810609504fd6c017e16f13f52b3138915682dce6ca3205741" + } + }, + { + "name": "v3 ProUpRevTx", + "source": "testnet block 1578068", + "txid": "4e0f211850f44c55e26e45f6bc10a59d6c08fe91be544a8fac0f7ce6cf96ad06", + "hex": "03000400015e1fc0bfae7beed6df5a36b7c21d8984202ecd22763a9e475c14e4c3aa204d04000000006a47304402203c0e4d07c274180ed64b71ff03ca7ee465d55fcc5bec2cd063554c1b76bdcbee02205097f29c81a4dc764bd003c3fab56911f4feb2fc5a6cd496b16e04f18dd8107a01210368037eedf9974ae19dbf7afad02f781ebf59b6e1ab07e9d27375604e0ae55e70feffffff0195f10d00000000001976a9140f85adbdfdf6f484f433e3c756430b85297a97ab88ac00000000a403005e1fc0bfae7beed6df5a36b7c21d8984202ecd22763a9e475c14e4c3aa204d04010036f1ba69a89f35bdec658106cd8d11e58ae48c2d1274f2c6ba14a343ddf828a7978ee11937eb3534414217b54301ac823fda0964f8ba67ab1c3a7171271c184226673a8cadef01ee7dc1bad302d5008c0a771abe16417a6077d0615118f4e4471ada150fe4fd4cc464cbf7c3f58b13b06a921e1199cbbe61adfe097f1c8447be", + "coreKey": "proUpRevTx", + "core": { + "version": 3, + "proTxHash": "044d20aac3e4145c479e3a7622cd2e2084891dc2b7365adfd6ee7baebfc01f5e", + "reason": 1, + "inputsHash": "a728f8dd43a314bac6f274122d8ce48ae5118dcd068165ecbd359fa869baf136" + } + } + ], + "blocks": [ + { + "height": 15146, + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "protxTxid": "20814c480e94dce7f3fe3f3370ec6bacd1fdc4f446f17aa7de7886be0b5d07c2", + "hex": "00000020624025135b0936675af8abbe364e7d21c8e1e42eab3f2b6f6528d6ea875dad51e2664e33e00b3bf6b05a67603cddadf9daa4149e9130c7e5180e38426d20a695d5a6c76affff7f20010000000203000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff036de1f505000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac209db40600000000016a26072d0b000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac00000000cf04002a3b0000db318d0bad75d80a6e14088f71657af817056edee711360d25c729905f33ed502692b21d078be4bc3103156612ba6bdcf387efdd6320a50a97985f5cdce4ead8008df9d40906f1c7c36c545b06daec45a84ffd43b5caf4045c9c9a39a146a7771de6cbdd11f5d16b22e9e358f5c483e35b12a5f3cab190026c5445dec34e632c36a0ba36a1f87bb411d7d292c5fd55cd36128877c707747286e40abec1aee7e4812eb72a56c407000000000000000000000000000000000000000000000000000000000000000000000300010001d02a36181466f1f3ec0f61a73114bdf31fa7f137fa880441d7e5e7b672493c04010000006a473044022004f9bb946ee7fab7d823a812cfb27b576f2c0cda4e597a8c92be26d8af33f155022034d05190a9b7a9aaae89fc89148639af1eb7f0601d9c959232b7a159cb2ca991012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff0200e87648170000001976a914fc52fb4219926f353f5475da7885b213c384a24188ac4d4fbfb88b0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000d10300000000000000000000000000000000000000000000000000000000000000000000000000000000000100c1dad84fdab5738de910b64d3a9ae7baf6afbd69806b8889b4ed6ccecefe7861fc3d69b7598f6ab9621fe2568403f94a1c5df398891433cdf301c67fda82f80bfe7f321465e088b8b1360c62946afcbea403cf927d5bfe230000011976a91431389f57b2f32565dcf713d2b2420914885bedcd88ac1027000000000000000000000000004f8f8fe9fd4ad5565cd6c4887bc54e2910a340bb7583c0d8d6a92d52b324095700" + }, + { + "height": 15153, + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "protxTxid": "f7e6fad0084ae34e410a8b710e5d74c0af1b95a29c1f3ae5507bed8e08b38adb", + "hex": "00000020aecd473bc731735ee4f65189ed998c2cdf7af153685380a12e56158bfb5258302fbedf46cb9369460ecef6d630d28ae3115038e402486b14f53c75166e2efa311ba7c76affff7f20040000000203000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff0374e1f505000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac209db40600000000016a3b072d0b000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac00000000cf0400313b00001beefdffbdc8ba4c53159dee72569b140244965f6e51038b4f5cc78f03926ecd2692b21d078be4bc3103156612ba6bdcf387efdd6320a50a97985f5cdce4ead80090c1f563c2ad4b04f16204c812f14399a37d2868f00c90ef293425f95d4af691e2bdf0d208aaa461579a497c8ffa7e3b128ddfb6900b17b6ed1321b33f51185d9767dab1e0b43cce6f391634e7d2b9baed2e2729d01c35c118e049963852e24c0e031b85c407000000000000000000000000000000000000000000000000000000000000000000000300010001c2075d0bbe8678dea77af146f4c4fdd1ac6bec70333ffef3e7dc940e484c8120010000006a473044022065a0067b55484e06109b4282a8bb14a3392ed76ed0a1195a91a3e3b531098c7f022002aafa428439c858e721664c6b4a71aae2f5bc4ad77bdfc1651a9e8e35428c40012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff0200e87648170000001976a9140b80dfe15263ad7abd2421b82a1291e07d30571c88ac7e654870740000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000ed03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000c707b9fc7af33ddf9a9cf50ff45fc587f590f5693bec56a034e3bdb3ce1e978d5487d03129c507bd15e73fcb7dac95c7333a68f561450702cd1aac511e40275a501ea07d5b43701d895d09a68803bd67d9c23ab94d4428d0000021976a91494a6df7e4fc8f474b28c634c748d5ddf88b7ee3788ac70171976a91417340a777ca7c690d65d19d2f7aca4b1a0387fa388aca00f000000000000000000000000008576d28cdc5f9022cd2988a05001e5ed2c318854bb3c9d19ce3dcb38ad3ce52e00" + }, + { + "height": 15166, + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "protxTxid": "1bff9d395c2647e370202e87c80fa3942930567d217238392363cd2542fb496f", + "hex": "00000020bb3c1469798a37b5b02d4bee8e64d0097ac3a3c67d781a1b47f5783f6b6a1f208a9bbe8d9059a1f3d13ff03b264d22095a081bd7dd7935e74ef280f9dfe2eaaf9da7c76affff7f20010000000203000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff036de1f505000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac209db40600000000016a24072d0b000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac00000000cf04003e3b0000906748b92fbd1eedde34683b6d0de5a07f2ef17f2e360010bb494f1b97c4e08adb67a987dca290d49310f2e30d411e7302d605505d7b9679199b3966f2da8d74008280e8ad04186510b99bc596dd191a5114dfc47f6520ec0d14f3bc829bc6e6a2e5d54511780ae8f690822ca97b8f4be8056a464d76ea5617b396f44723080575f28f629fe27eec0994c8dfe71bd51c05976438b551008fa1f7f2944473fc221caefd46dcc407000000000000000000000000000000000000000000000000000000000000000000000300010001db8ab3088eed7b50e53a1f9ca2951bafc0745d0e718b0a414ee34a08d0fae6f7010000006a4730440220433c544395783b738a418c8b16f0368f13d56e9c56af3d7595e8eb61d41b0a650220771f2132348b28be1b853e7000ea6fa5d5f0d5913dac7bfc07b8f5c8a20d19f5012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff0200e87648170000001976a9140d1b303302b3a691a912fc9b0f2bd4f33f70816488accd7bd1275d0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000cf03000000000000000000000000000000000000000000000000000000000000000000000000000000000001003a02101abc24108438ebe3af704fd64a658fef6bb96d7c328cb02753c2487a780ef8401d621ad5c505beeba3ba5958984fab293bfe411fe9cfeba617ae9b7ada2002c32d9dd47e671a9f43fec688085b7cc462a0e594a26200000117a914d675f38f31792b691cc7c724d8dc44134c710e7e8710270000000000000000000000000070dc754d77ae0cb2034f079c87a764f47f1aaa16afa64baa6aa6df335cb379a800" + }, + { + "height": 15182, + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "protxTxid": "7c58d9776ef31869b8cae3f94366e9fedb914e41a5b4c26d048c12f30f5e51ac", + "hex": "00000020e15485247bff24725812afda5783d9d373ac34b3fd90d36791612fb2c8916571f6728bc67b45f7a5c0bf3c690609d7476975d54fc97ccfc3d8f2748829482f0b3ea8c76affff7f20010000000403000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff036ae1f505000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac209db40600000000016a1c072d0b000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac00000000cf04004e3b0000253c062fceff7313f133cb202b76371e6e833c4c290b63ea3bbca71b7312d3f56af25a0bbc62ce8d902e065898e7dee5ac58ea319516847eb8133a29eb5627ed00a4314705759d162b94e5b6e87f20c7e79e542c8771b79757846678ab758d9fdc39913fcccfa20c86c4137c51c5a5f1af157a0aa37ecf12644842e0e996146572a86edd6b4d555eb144f2ba91991cfc946b56af9d76e63df49db9538f588b5caeaecf9047c5070000000000000000000000000000000000000000000000000000000000000000000003000600000000000000fd490101004e3b00000300015ba9be1228ce7c86458fd453f80e28509d4d3e3d1f3943d0aca0c7339b7a1d6332000000000000003200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000600000000000000fd550101004e3b00000300045ba9be1228ce7c86458fd453f80e28509d4d3e3d1f3943d0aca0c7339b7a1d6364000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000200016f49fb4225cd6323393872217d56302994a30fc8872e2070e347265c399dff1b010000006a47304402207bbed2c28fe9710187db1da4ab50a3c380108996b6cbcd972407ea8e36ad0da202200ffb02cc90a027e35a452cf8ed1edf33d567aaf2797150a797124e83871bad08012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff01277ad1275d0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000dc0300010022a1b2590651ff843222d7564397d0e7af94e94c35b94621822c268fcdebcba90103000101010444437aed4e21010101010444437aed6820020101010444437aed05a300e4b92ed4f5d34d4eb060b39c385ad1cf0ab338eb26619187e762c5e0c7c1080415d36e5fdf5b02343adb01a134f2aa78513658f9a751dcd16e848065f3b548fd24268a7f330346049b39e3e75bf75c0a77584fd2c02832000f39c15f38e43444ca05e898077bfad4bc0f91cb9158f10066ae056971a8011d05e9152f1017a305e8dfd7a3b292de2eb5230c434c59e622f3c0652f" + }, + { + "height": 15190, + "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", + "protxTxid": "16b69f49a4aba21954e199efe7209478763133f7c473fab6668d5d2b79e55775", + "hex": "000000200773b95d530f3458568210e4e342688ebb9a8f60d44854dd969e40e3692dc0395ccecbc262e566629a3248e6acba353ade51462bed0fae302e78875411b002f78ea8c76affff7f20000000000203000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff036de1f505000000001976a91456208a8adff9433262bffee3c4894d2a160c21bd88ac209db40600000000016a24072d0b000000001976a9145e72396e93b8ea5ba7819473c1bc6505d212c42688ac00000000cf0400563b0000253c062fceff7313f133cb202b76371e6e833c4c290b63ea3bbca71b7312d3f56af25a0bbc62ce8d902e065898e7dee5ac58ea319516847eb8133a29eb5627ed00a704761aa34fb5c41d67feb720fdb6e1ecaa55255ab794c04734bf758f8405897058aa5c180be19d06ca576e2c7df8e900c71ceab5801356f2bc047d40d8908ec42a07f5e4b5a60ae0ce63c7e8d5e7e94b914a536c90afb8b3b041e507392010aeb8357dc507000000000000000000000000000000000000000000000000000000000000000000000300030001ac515e0ff3128c046dc2b4a5414e91dbfee96643f9e3cab86918f36e77d9587c000000006a47304402202e23fe274788884c91a48592dbe2cfec421286c1c56a0a4f83b27e731b40bdd5022040b0818dae31cd8213d530c31768bf4fdd9e84a3d7f95ac77214c79ec1e35cf7012102c861a156ca6c6abecdeea95eab3ec7c93c6a37d63a5a6f62368ac94a424af15ffeffffff017678d1275d0000001976a914c39aa109a7f20cefa69079374c7324e01998a12488ac00000000e70300c2075d0bbe8678dea77af146f4c4fdd1ac6bec70333ffef3e7dc940e484c81200000806b8889b4ed6ccecefe7861fc3d69b7598f6ab9621fe2568403f94a1c5df398891433cdf301c67fda82f80bfe7f321465e088b8b1360c62946afcbea403cf927d5bfe23011976a9144a83c35e84c83510f20989dee7845ff133c567af88ac1027415720a36cce2d68158913b3523ff1167e016cfd049560109894dd9d844fa99c411f5e48101069586123670bd805a9f7b28ed3ebbf18292d18c3f5bfc819bb76ab8a76b5730eab51427a15742ef809693c8e0dbe27737a17f000823859ce58c30f03" + } + ] +} diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index 64a6a18e3..0a94351e5 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -112,4 +112,139 @@ describe('ProRegTxPayload', function () { expect(payload.validate.callCount).to.be.equal(1); }); }); + + describe('version 3 (ExtAddr)', function () { + var vectors = require('../../fixtures/core24-protx.json').transactions; + var Transaction = DashcoreLib.Transaction; + + function getPayload(name) { + var vector = vectors.find(function (v) { + return v.name === name; + }); + return new Transaction(vector.hex).extraPayload; + } + + function getSharedPayload() { + return getPayload('v3 shared ProRegTx with an explicit reward script'); + } + + it('Should keep version 2 as the default for new payloads', function () { + var json = proRegTxFixture.getProRegPayloadJSON(); + delete json.version; + + expect(new ProRegTxPayload().version).to.be.equal(2); + expect(ProRegTxPayload.fromJSON(json).version).to.be.equal(2); + }); + + it('Should keep the version given in JSON', function () { + var json = proRegTxFixture.getProRegPayloadJSON(); + json.version = 1; + + var payload = ProRegTxPayload.fromJSON(json); + + expect(payload.version).to.be.equal(1); + expect(payload.toBuffer().readUInt16LE(0)).to.be.equal(1); + }); + + it('Should serialize a share without a reward script as an empty script', function () { + var payload = getPayload('v3 shared ProRegTx rewarding the refund scripts'); + var json = payload.toJSON({ network: 'testnet' }); + + expect(payload.shares[0].scriptReward).to.be.equal(''); + expect(json.shares[0]).to.not.have.property('rewardAddress'); + expect(ProRegTxPayload.fromJSON(json).shares[0].scriptReward).to.be.equal(''); + }); + + it('Should require one join signature per share', function () { + var payload = getSharedPayload(); + payload.joinSigs.pop(); + + expect(function () { + payload.toBuffer(); + }).to.throw('Expect one join signature per share'); + }); + + it('Should require 65 byte join signatures', function () { + var payload = getSharedPayload(); + payload.joinSigs[0] = payload.joinSigs[0].slice(2); + + expect(function () { + payload.validate(); + }).to.throw('Expect join signatures to be 65 bytes in size'); + }); + + it('Should require p2pkh/p2sh share scripts', function () { + var payload = getSharedPayload(); + payload.shares[1].scriptReward = '6a'; + + expect(function () { + payload.validate(); + }).to.throw('Expected share scriptReward to be empty or a p2pkh/p2sh'); + + payload = getSharedPayload(); + payload.shares[0].scriptRefund = ''; + + expect(function () { + payload.validate(); + }).to.throw('Expected share scriptRefund to be a p2pkh/p2sh'); + }); + + it('Should require valid share amounts and early period terms', function () { + var payload = getSharedPayload(); + payload.shares[0].amount = -1; + + expect(function () { + payload.validate(); + }).to.throw('Expect share amount to be an unsigned integer'); + + payload = getSharedPayload(); + payload.earlyPeriodBlocks = 0x100000000; + + expect(function () { + payload.validate(); + }).to.throw('Expect earlyPeriodBlocks to be an uint32'); + }); + + it('Should require p2pkh/p2sh payouts with rewards of at most 10000', function () { + var payload = getPayload('v3 ProRegTx with two payouts'); + payload.payouts[0].reward = 10001; + + expect(function () { + payload.validate(); + }).to.throw('Expect payout reward to be an integer from 0 to 10000'); + + payload = getPayload('v3 ProRegTx with two payouts'); + payload.payouts[0].scriptPayout = '6a'; + + expect(function () { + payload.validate(); + }).to.throw('Expected payout scriptPayout to be a p2pkh/p2sh'); + }); + + it('Should require addresses that can be serialized', function () { + var payload = getPayload('v3 evonode ProRegTx'); + payload.addresses = { core_p2p: ['not an address'] }; + + expect(function () { + payload.validate(); + }).to.throw('Invalid address entry'); + + payload.addresses = undefined; + + expect(function () { + payload.validate(); + }).to.throw('Expected addresses to be an object'); + }); + + it('Should not write platform ports for a version 3 evonode', function () { + var payload = getPayload('v3 evonode ProRegTx'); + var json = payload.toJSON({ network: 'testnet' }); + json.platformP2PPort = 26656; + json.platformHTTPPort = 443; + + expect(ProRegTxPayload.fromJSON(json).toBuffer()).to.be.deep.equal( + payload.toBuffer() + ); + }); + }); }); diff --git a/test/transaction/payload/protxcorevectors.js b/test/transaction/payload/protxcorevectors.js new file mode 100644 index 000000000..b3565a0e5 --- /dev/null +++ b/test/transaction/payload/protxcorevectors.js @@ -0,0 +1,182 @@ +const { expect } = require('chai'); + +const DashcoreLib = require('../../../index'); +const fixtures = require('../../fixtures/core24-protx.json'); + +const { Address, Block, Transaction } = DashcoreLib; +const { + ProRegTxPayload, + ProTxUpServPayload, + ProUpRegTxPayload, + ProUpRevTxPayload, +} = DashcoreLib.Transaction.Payload; + +// Regtest, devnet and testnet share address prefixes +const network = 'testnet'; + +const payloadClasses = { + proRegTx: ProRegTxPayload, + proUpServTx: ProTxUpServPayload, + proUpRegTx: ProUpRegTxPayload, + proUpRevTx: ProUpRevTxPayload, +}; + +/** + * @param {string} keyID - hex in the payload JSON byte order + * @return {string} + */ +function keyIDToAddress(keyID) { + return Address.fromPublicKeyHash(Buffer.from(keyID, 'hex').reverse(), network).toString(); +} + +function expectPayoutsLikeCore(payload, json, core) { + const corePayouts = core.payouts || []; + expect(json.payouts).to.deep.equal( + corePayouts.map((payout) => ({ address: payout.address, reward: payout.reward })), + ); + expect(payload.payouts.map((payout) => payout.scriptPayout)).to.deep.equal( + corePayouts.map((payout) => payout.script), + ); +} + +const expectLikeCore = { + proRegTx(payload, json, core) { + expect(payload.type).to.equal(core.type); + expect(json.collateralHash).to.equal(core.collateralHash); + expect(json.collateralIndex).to.equal(core.collateralIndex); + expect(json.pubKeyOperator).to.equal(core.pubKeyOperator); + expect(keyIDToAddress(json.keyIDVoting)).to.equal(core.votingAddress); + if (core.ownerAddress) { + expect(keyIDToAddress(json.keyIDOwner)).to.equal(core.ownerAddress); + } + expect(json.operatorReward).to.equal(Math.round(core.operatorReward * 100)); + expect(json.inputsHash).to.equal(core.inputsHash); + if (core.platformNodeID) { + expect(json.platformNodeID).to.equal(core.platformNodeID); + } + + if (core.version < 3) { + expect(json.service).to.equal(core.addresses.core_p2p[0]); + expect(json.payoutAddress).to.equal(core.payoutAddress); + return; + } + + expect(json.addresses).to.deep.equal(core.addresses); + expect(json).to.not.have.property('service'); + expect(json).to.not.have.property('platformP2PPort'); + expect(json).to.not.have.property('platformHTTPPort'); + expectPayoutsLikeCore(payload, json, core); + // Core reports the effective reward script, the refund script when the share has none + expect( + json.shares.map((share) => ({ + amount: share.amount, + refundAddress: share.refundAddress, + rewardAddress: share.rewardAddress || share.refundAddress, + ownerAddress: keyIDToAddress(share.keyIDOwner), + })), + ).to.deep.equal( + (core.shares || []).map((share) => ({ + amount: share.amount, + refundAddress: share.refundAddress, + rewardAddress: share.rewardAddress, + ownerAddress: share.ownerAddress, + })), + ); + expect(json.joinSigs).to.have.lengthOf(json.shares.length); + expect(json.earlyPeriodBlocks).to.equal(core.earlyPeriodBlocks || 0); + expect(json.earlyPenalty).to.equal(core.earlyPenalty || 0); + }, + + proUpServTx(payload, json, core) { + expect(json.type).to.equal(core.type); + expect(json.proTxHash).to.equal(core.proTxHash); + expect(json.addresses).to.deep.equal(core.addresses); + expect(json.operatorPayoutAddress).to.equal(core.operatorPayoutAddress); + expect(json.platformNodeID).to.equal(core.platformNodeID); + expect(json.inputsHash).to.equal(core.inputsHash); + expect(json).to.not.have.property('service'); + expect(json).to.not.have.property('platformP2PPort'); + expect(json).to.not.have.property('platformHTTPPort'); + }, + + proUpRegTx(payload, json, core) { + expect(json.proTxHash).to.equal(core.proTxHash); + expect(json.pubKeyOperator).to.equal(core.pubKeyOperator); + expect(keyIDToAddress(json.keyIDVoting)).to.equal(core.votingAddress); + expect(json).to.not.have.property('payoutAddress'); + expectPayoutsLikeCore(payload, json, core); + expect(json.inputsHash).to.equal(core.inputsHash); + }, + + proUpRevTx(payload, json, core) { + expect(json.proTxHash).to.equal(core.proTxHash); + expect(json.reason).to.equal(core.reason); + expect(json.inputsHash).to.equal(core.inputsHash); + }, +}; + +describe('ProTx payloads produced by Dash Core', () => { + fixtures.transactions.forEach((vector) => { + describe(`${vector.name} (${vector.source})`, () => { + const PayloadClass = payloadClasses[vector.coreKey]; + let transaction; + let rawPayload; + + beforeEach(() => { + transaction = new Transaction(Buffer.from(vector.hex, 'hex')); + rawPayload = transaction.extraPayload.toBuffer(); + }); + + it('should parse and re-serialize the transaction byte for byte', () => { + const raw = Buffer.from(vector.hex, 'hex'); + const parsed = new Transaction(raw); + + expect(parsed.extraPayload).to.be.an.instanceOf(PayloadClass); + expect(parsed.extraPayload.version).to.equal(vector.core.version); + expect(parsed.toString()).to.equal(vector.hex); + expect(parsed.hash).to.equal(vector.txid); + // parsing must not modify the caller's buffer + expect(raw.toString('hex')).to.equal(vector.hex); + }); + + it('should parse and re-serialize the payload byte for byte', () => { + const payload = PayloadClass.fromBuffer(Buffer.from(rawPayload)); + + expect(payload.toBuffer()).to.deep.equal(rawPayload); + expect(payload.copy().toBuffer()).to.deep.equal(rawPayload); + }); + + it('should report the fields Core reports', () => { + const payload = PayloadClass.fromBuffer(Buffer.from(rawPayload)); + const json = payload.toJSON({ network }); + + expect(json.version).to.equal(vector.core.version); + expectLikeCore[vector.coreKey](payload, json, vector.core); + }); + + it('should restore the same bytes from JSON', () => { + const json = JSON.parse(JSON.stringify(transaction.extraPayload.toJSON({ network }))); + + expect(PayloadClass.fromJSON(json).toBuffer()).to.deep.equal(rawPayload); + }); + + it('should restore the same transaction from its object form', () => { + const object = JSON.parse(JSON.stringify(transaction.toObject())); + const restored = new Transaction(object); + + expect(restored.toString()).to.equal(vector.hex); + expect(restored.hash).to.equal(vector.txid); + }); + }); + }); + + fixtures.blocks.forEach((vector) => { + it(`should parse and re-serialize devnet block ${vector.height} with its ProTx`, () => { + const block = new Block(Buffer.from(vector.hex, 'hex')); + + expect(block.toBuffer().toString('hex')).to.equal(vector.hex); + expect(block.validMerkleRoot()).to.equal(true); + expect(block.transactions.map((transaction) => transaction.hash)).to.include(vector.protxTxid); + }); + }); +}); diff --git a/test/transaction/payload/proupregtxpayload.js b/test/transaction/payload/proupregtxpayload.js index d46b1416d..4a358ae55 100644 --- a/test/transaction/payload/proupregtxpayload.js +++ b/test/transaction/payload/proupregtxpayload.js @@ -103,4 +103,27 @@ describe('ProUpRegTxPayload', function () { expect(payload.validate.callCount).to.be.equal(1); }); }); + + describe('version 3 (ExtAddr)', function () { + var vector = require('../../fixtures/core24-protx.json').transactions.find( + function (v) { + return v.name === 'v3 ProUpRegTx'; + } + ); + + it('Should keep version 1 as the default for new payloads', function () { + expect(new ProUpRegTxPayload().version).to.be.equal(1); + }); + + it('Should require valid payouts', function () { + var payload = new DashcoreLib.Transaction(vector.hex).extraPayload; + payload.payouts = []; + payload.validate(); + + payload.payouts = undefined; + expect(function () { + payload.validate(); + }).to.throw('Expect payouts to be an array'); + }); + }); }); diff --git a/test/transaction/payload/proupservtxpayload.js b/test/transaction/payload/proupservtxpayload.js index c079ce2bf..8bafb6347 100644 --- a/test/transaction/payload/proupservtxpayload.js +++ b/test/transaction/payload/proupservtxpayload.js @@ -102,4 +102,36 @@ describe('ProTxUpServPayload', function () { expect(payload.validate.callCount).to.be.equal(1); }); }); + + describe('operator payout', function () { + it('Should leave out the operator payout address when there is none', function () { + var json = proUpServTxFixture.getProUpServPayloadJSON(); + delete json.operatorPayoutAddress; + + var payload = ProTxUpServPayload.fromJSON(json); + var restored = ProTxUpServPayload.fromJSON( + payload.toJSON({ network: 'testnet' }) + ); + + expect(payload.scriptOperatorPayout).to.be.equal(''); + expect(payload.toJSON({ network: 'testnet' })).to.be.deep.equal(json); + expect(restored.toBuffer()).to.be.deep.equal(payload.toBuffer()); + }); + }); + + describe('version 3 (ExtAddr)', function () { + var vectors = require('../../fixtures/core24-protx.json').transactions; + + it('Should require addresses that can be serialized', function () { + var vector = vectors.find(function (v) { + return v.name === 'v3 evonode ProUpServTx'; + }); + var payload = new DashcoreLib.Transaction(vector.hex).extraPayload; + payload.addresses = { core_p2p: ['1.2.3.4:9999'], unknown: [] }; + + expect(function () { + payload.validate(); + }).to.throw('Unknown address purpose unknown'); + }); + }); }); diff --git a/typings/transaction/payload/MasternodePayout.d.ts b/typings/transaction/payload/MasternodePayout.d.ts new file mode 100644 index 000000000..d122ab11e --- /dev/null +++ b/typings/transaction/payload/MasternodePayout.d.ts @@ -0,0 +1,30 @@ +/** + * One owner payee of a version 3 (ExtAddr) ProRegTx or ProUpRegTx + * @typedef {Object} MasternodePayout + * @property {string} scriptPayout - payee script (p2pkh/p2sh) + * @property {number} reward - share of the owner reward in basis points + */ +export type MasternodePayout = { + scriptPayout: string; + reward: number; +}; + +/** + * @typedef {Object} MasternodePayoutJSON + * @property {string} address - payee address + * @property {number} reward - share of the owner reward in basis points + */ +export type MasternodePayoutJSON = { + address: string; + reward: number; +}; + +/** + * "addr:port" lists by purpose (core_p2p, platform_p2p, platform_https), + * as Core reports a masternode's ExtNetInfo + */ +export type MasternodeAddresses = { + core_p2p?: string[]; + platform_p2p?: string[]; + platform_https?: string[]; +}; diff --git a/typings/transaction/payload/ProRegTxPayload.d.ts b/typings/transaction/payload/ProRegTxPayload.d.ts index 33823a29a..7b22f6fc3 100644 --- a/typings/transaction/payload/ProRegTxPayload.d.ts +++ b/typings/transaction/payload/ProRegTxPayload.d.ts @@ -1,32 +1,80 @@ +import { + MasternodeAddresses, + MasternodePayout, + MasternodePayoutJSON, +} from './MasternodePayout'; + +/** + * One participant's share of a shared masternode's collateral + * @typedef {Object} CollateralShare + * @property {number} amount - contributed collateral in duffs + * @property {string} scriptRefund - refund script (p2pkh/p2sh) + * @property {string} scriptReward - reward script (p2pkh/p2sh), empty to reward scriptRefund + * @property {string} keyIDOwner - the share owner's public key hash + */ +export type CollateralShare = { + amount: number; + scriptRefund: string; + scriptReward: string; + keyIDOwner: string; +}; + +/** + * @typedef {Object} CollateralShareJSON + * @property {number} amount - contributed collateral in duffs + * @property {string} refundAddress + * @property {string} [rewardAddress] - omitted when rewards go to refundAddress + * @property {string} keyIDOwner - the share owner's public key hash + */ +export type CollateralShareJSON = { + amount: number; + refundAddress: string; + rewardAddress?: string; + keyIDOwner: string; +}; + /** * @typedef {Object} ProRegTxPayloadJSON * @property {number} version uint_16 2 Provider transaction version number. Currently set to 1. + * @property {number} [type] Masternode type, regular (0) if omitted * @property {string} collateralHash * @property {number} collateralIndex uint_32 4 The collateral index. - * @property {string} service - service address, ip and port + * @property {string} [service] - service address, ip and port. Before version 3 + * @property {MasternodeAddresses} [addresses] - "addr:port" lists by purpose. From version 3 * @property {string} keyIDOwner CKeyID 20 The public key hash used for owner related signing (ProTx updates, governance voting) * @property {string} pubKeyOperator BLSPubKey 48 The public key used for operational related signing (network messages, ProTx updates) * @property {string} keyIDVoting CKeyID 20 The public key hash used for voting. * @property {number} operatorReward uint_16 2 A value from 0 to 10000. - * @property {string} payoutAddress + * @property {string} [payoutAddress] Before version 3 + * @property {MasternodePayoutJSON[]} [payouts] Owner payees with their reward shares. From version 3 + * @property {CollateralShareJSON[]} [shares] Collateral shares of a shared masternode. From version 3 + * @property {string[]} [joinSigs] One compact signature per share. From version 3 + * @property {number} [earlyPeriodBlocks] From version 3 + * @property {number} [earlyPenalty] In duffs. From version 3 * @property {string} inputsHash uint256 32 Hash of all the outpoints of the transaction inputs * @property {string} [platformNodeID] Platform Node ID - * @property {number} [payloadSigSize] Size of the Signature - * @property {number} [platformP2PPort] Platform P2P port - * @property {number} [platformHTTPPort] Platform HTTP port + * @property {number} [platformP2PPort] Platform P2P port. Before version 3 + * @property {number} [platformHTTPPort] Platform HTTP port. Before version 3 * @property {number} [payloadSigSize] Size of the Signature * @property {string} [payloadSig] Signature of the hash of the ProTx fields. Signed with keyIDOwner */ export type ProRegTxPayloadJSON = { version: number; + type?: number; collateralHash: string; collateralIndex: number; - service: string; + service?: string; + addresses?: MasternodeAddresses; keyIDOwner: string; pubKeyOperator: string; keyIDVoting: string; operatorReward: number; - payoutAddress: string; + payoutAddress?: string; + payouts?: MasternodePayoutJSON[]; + shares?: CollateralShareJSON[]; + joinSigs?: string[]; + earlyPeriodBlocks?: number; + earlyPenalty?: number; inputsHash: string; payloadSigSize?: number; payloadSig?: string; @@ -76,6 +124,11 @@ export class ProRegTxPayload { */ validate(): boolean; + /** + * Validate the collateral shares of a version 3 payload + */ + validateShares(): void; + /** * Serializes payload to JSON * @param [options] @@ -105,9 +158,13 @@ export class ProRegTxPayload { */ collateralIndex: number; /** - * service address, ip and port + * service address, ip and port. Before version 3 + */ + service?: string; + /** + * "addr:port" lists by purpose. From version 3 */ - service: string; + addresses?: MasternodeAddresses; /** * CKeyID 20 The public key hash used for owner related signing (ProTx updates, governance voting) */ @@ -125,9 +182,29 @@ export class ProRegTxPayload { */ operatorReward: number; /** - * Script Variable Payee script (p2pkh/p2sh) + * Script Variable Payee script (p2pkh/p2sh). Before version 3 + */ + scriptPayout?: string; + /** + * Owner payees with their reward shares. From version 3 + */ + payouts?: MasternodePayout[]; + /** + * Collateral shares of a shared masternode, empty if not shared. From version 3 + */ + shares?: CollateralShare[]; + /** + * One 65 byte compact signature per share. From version 3 + */ + joinSigs?: string[]; + /** + * uint_32. From version 3 + */ + earlyPeriodBlocks?: number; + /** + * In duffs. From version 3 */ - scriptPayout: string; + earlyPenalty?: number; /** * uint256 32 Hash of all the outpoints of the transaction inputs */ diff --git a/typings/transaction/payload/ProUpRegTxPayload.d.ts b/typings/transaction/payload/ProUpRegTxPayload.d.ts index b9360437d..5b7a8fd7b 100644 --- a/typings/transaction/payload/ProUpRegTxPayload.d.ts +++ b/typings/transaction/payload/ProUpRegTxPayload.d.ts @@ -1,3 +1,5 @@ +import { MasternodePayout, MasternodePayoutJSON } from './MasternodePayout'; + /** * @typedef {Object} ProUpRegTransactionPayloadJSON * @property {number} version @@ -5,7 +7,8 @@ * @property {string} proTxHash * @property {string} pubKeyOperator * @property {string} keyIDVoting - * @property {string} payoutAddress + * @property {string} [payoutAddress] Before version 3 + * @property {MasternodePayoutJSON[]} [payouts] Owner payees with their reward shares. From version 3 * @property {string} inputsHash * @property {string} [platformNodeID] Platform Node ID * @property {number} [platformP2PPort] Platform P2P port @@ -18,7 +21,8 @@ export type ProUpRegTransactionPayloadJSON = { proTxHash: string; pubKeyOperator: string; keyIDVoting: string; - payoutAddress: string; + payoutAddress?: string; + payouts?: MasternodePayoutJSON[]; inputsHash: string; platformNodeID?: string; platformP2PPort?: number; @@ -35,7 +39,8 @@ export type ProUpRegTransactionPayloadJSON = { * @property {string} pubKeyOperator BLSPubKey 48 The public key hash used for operational related signing (network messages, ProTx updates) * @property {string} keyIDVoting CKeyID 20 The public key hash used for voting. * @property {number} scriptPayoutSize compactSize uint 1-9 Size of the Payee Script. - * @property {string} scriptPayout Script Variable Payee script (p2pkh/p2sh) + * @property {string} [scriptPayout] Script Variable Payee script (p2pkh/p2sh). Before version 3 + * @property {MasternodePayout[]} [payouts] Owner payees with their reward shares. From version 3 * @property {string} inputsHash uint256 32 Hash of all the outpoints of the transaction inputs * @property {number} payloadSigSize compactSize uint 1-9 Size of the Signature * @property {string} [platformNodeID] Platform Node ID @@ -123,9 +128,13 @@ export class ProUpRegTxPayload { */ scriptPayoutSize: number; /** - * Script Variable Payee script (p2pkh/p2sh) + * Script Variable Payee script (p2pkh/p2sh). Before version 3 + */ + scriptPayout?: string; + /** + * Owner payees with their reward shares. From version 3 */ - scriptPayout: string; + payouts?: MasternodePayout[]; /** * uint256 32 Hash of all the outpoints of the transaction inputs */ diff --git a/typings/transaction/payload/ProUpServTxPayload.d.ts b/typings/transaction/payload/ProUpServTxPayload.d.ts index ea7b4288f..3d3283a93 100644 --- a/typings/transaction/payload/ProUpServTxPayload.d.ts +++ b/typings/transaction/payload/ProUpServTxPayload.d.ts @@ -1,18 +1,30 @@ +import { MasternodeAddresses } from './MasternodePayout'; + /** * @typedef {Object} ProUpServTxPayloadJSON * @property {number} version + * @property {number} [type] * @property {string} proTxHash - * @property {string} service - Service string, ip and port - * @property {string} [operatorPayoutAddress] + * @property {string} [service] - Service string, ip and port. Before version 3 + * @property {MasternodeAddresses} [addresses] - "addr:port" lists by purpose. From version 3 + * @property {string} [operatorPayoutAddress] - omitted when there is no operator payee * @property {string} inputsHash + * @property {string} [platformNodeID] Platform Node ID + * @property {number} [platformP2PPort] Platform P2P port. Before version 3 + * @property {number} [platformHTTPPort] Platform HTTP port. Before version 3 * @property {string} [payloadSig] */ export type ProUpServTxPayloadJSON = { version: number; + type?: number; proTxHash: string; - service: string; + service?: string; + addresses?: MasternodeAddresses; operatorPayoutAddress?: string; inputsHash: string; + platformNodeID?: string; + platformP2PPort?: number; + platformHTTPPort?: number; payloadSig?: string; }; @@ -81,9 +93,13 @@ export class ProUpServTxPayload { */ proTXHash: string; /** - * string - ip and port + * string - ip and port. Before version 3 + */ + service?: string; + /** + * "addr:port" lists by purpose. From version 3 */ - service: string; + addresses?: MasternodeAddresses; /** * Hash of all the outpoints of the transaction inputs */ From 7e9793caa0eaec5669f53752d0cef60463e6941c Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 17:37:40 +0000 Subject: [PATCH 03/15] fix: align ExtNetInfo writer with reader and require v3 payouts - The writer accepted domains longer than Core's DOMAIN_MAX_LEN (253), which Core's DomainPort serialization (LIMITED_STRING) and our reader reject. Reject them when writing. - The writer accepted IPv6 addresses with an IPv4-mapped, Tor v2 or internal prefix, which Core (and our reader) don't read back as IPv6. Both sides now share one check. - Load @noble/hashes/sha3 only when a Tor v3 address is encoded or decoded: it calls BigInt when it loads, and the rest of the library doesn't need BigInt. - A v3 ProRegTx or ProUpRegTx built from JSON with payoutAddress but no payouts silently got an empty payout list. Reject payoutAddress from version 3, and follow Core's IsTriviallyValid: a v3 ProUpRegTx and a non-shared v3 ProRegTx need at least one payout, a shared ProRegTx none. - The reader test claimed [2001:db8::1] is Core's string form, but Core prints the documentation range as [invalid entry]. Use IPv6 and CJDNS entries Core itself decoded. Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/masternodepayouts.js | 12 ++++++-- lib/transaction/payload/proregtxpayload.js | 15 +++++++++- lib/transaction/payload/proupregtxpayload.js | 7 ++++- lib/util/netinfo.js | 25 ++++++++++++++--- test/transaction/payload/proregtxpayload.js | 28 +++++++++++++++++++ test/transaction/payload/proupregtxpayload.js | 18 ++++++++++-- test/util/netinfo.js | 20 +++++++++++-- 7 files changed, 112 insertions(+), 13 deletions(-) diff --git a/lib/transaction/payload/masternodepayouts.js b/lib/transaction/payload/masternodepayouts.js index 262388489..940ff65f0 100644 --- a/lib/transaction/payload/masternodepayouts.js +++ b/lib/transaction/payload/masternodepayouts.js @@ -91,10 +91,18 @@ function payoutsToJSON(payouts, network) { } /** - * @param {MasternodePayoutJSON[]} payoutsJSON + * Reads the payouts of a version 3 payload's JSON + * @param {Object} payloadJSON + * @param {MasternodePayoutJSON[]} [payloadJSON.payouts] * @return {MasternodePayout[]} */ -function payoutsFromJSON(payoutsJSON) { +function payoutsFromJSON(payloadJSON) { + // Don't silently drop a version 1/2 payee + Preconditions.checkArgument( + payloadJSON.payoutAddress === undefined, + 'Expect payouts instead of payoutAddress from version 3', + ); + const payoutsJSON = payloadJSON.payouts || []; Preconditions.checkArgument(Array.isArray(payoutsJSON), 'Expect payouts to be an array'); return payoutsJSON.map((payout) => ({ scriptPayout: Script.fromAddress(payout.address).toHex(), diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index 75552909c..c3c17fd9a 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -110,7 +110,7 @@ function ProRegTxPayload(options) { this.operatorReward = options.operatorReward; if (this.version >= PROTX_EXT_ADDR_VERSION) { this.addresses = _.cloneDeep(options.addresses); - this.payouts = payouts.payoutsFromJSON(options.payouts || []); + this.payouts = payouts.payoutsFromJSON(options); this.shares = sharesFromJSON(options.shares || []); this.joinSigs = (options.joinSigs || []).slice(); this.earlyPeriodBlocks = options.earlyPeriodBlocks || 0; @@ -390,6 +390,19 @@ ProRegTxPayload.prototype.validate = function () { if (this.version >= PROTX_EXT_ADDR_VERSION) { payouts.validatePayouts(this.payouts); this.validateShares(); + // Core pays a shared masternode's owner rewards by share and any other + // masternode's to its payouts (CProRegTx::IsTriviallyValid) + if (this.shares.length > 0) { + Preconditions.checkArgument( + this.payouts.length === 0, + 'Expect no payouts for a shared masternode' + ); + } else { + Preconditions.checkArgument( + this.payouts.length > 0, + 'Expect at least one payout' + ); + } } if (this.scriptPayout) { diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index eb66bf2d3..b2cd4f1b4 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -63,7 +63,7 @@ function ProUpRegTxPayload(payloadJSON) { this.pubKeyOperator = payloadJSON.pubKeyOperator; this.keyIDVoting = payloadJSON.keyIDVoting; if (this.version >= PROTX_EXT_ADDR_VERSION) { - this.payouts = payouts.payoutsFromJSON(payloadJSON.payouts || []); + this.payouts = payouts.payoutsFromJSON(payloadJSON); } else { this.scriptPayout = Script.fromAddress(payloadJSON.payoutAddress).toHex(); } @@ -174,6 +174,11 @@ ProUpRegTxPayload.prototype.validate = function () { if (this.version >= PROTX_EXT_ADDR_VERSION) { payouts.validatePayouts(this.payouts); + // Core's IsPayoutListTriviallyValid + Preconditions.checkArgument( + this.payouts.length > 0, + 'Expect at least one payout' + ); } if (this.scriptPayout) { diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index 8ee2cc537..800911545 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -1,4 +1,3 @@ -const { sha3_256: sha3256 } = require('@noble/hashes/sha3'); const BufferReader = require('../encoding/bufferreader'); const BufferWriter = require('../encoding/bufferwriter'); @@ -49,7 +48,7 @@ const IPV6_DISALLOWED_PREFIXES = [ Buffer.from('fd6b88c08724', 'hex'), // internal ]; -// DOMAIN_MAX_LEN +// DOMAIN_MAX_LEN, the longest domain Core's DomainPort serializes (LIMITED_STRING) const DOMAIN_MAX_LENGTH = 253; const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'; @@ -118,12 +117,27 @@ function encodeBase32(buffer) { * @return {Buffer} */ function torV3Checksum(pubKey) { + // Required here rather than at the top: @noble/hashes/sha3 calls BigInt when it + // loads, and only Tor v3 addresses need it, so the rest of the library doesn't + // need BigInt support + // eslint-disable-next-line global-require + const { sha3_256: sha3256 } = require('@noble/hashes/sha3'); const digest = sha3256( Buffer.concat([Buffer.from('.onion checksum', 'ascii'), pubKey, Buffer.from([TORV3_VERSION])]), ); return Buffer.from(digest).slice(0, TORV3_CHECKSUM_SIZE); } +/** + * Whether Core reads a BIP155 IPv6 address as something other than IPv6 (an + * embedded IPv4, Tor v2 or internal address, see CNetAddr::UnserializeV2Stream) + * @param {Buffer} address + * @return {boolean} + */ +function isDisallowedIPv6(address) { + return IPV6_DISALLOWED_PREFIXES.some((prefix) => address.indexOf(prefix) === 0); +} + /** * Formats an IPv6 address like Core (IPv6ToString in netaddress.cpp): RFC 5952 * zero compression of the first longest run of at least two zero groups @@ -204,6 +218,9 @@ function parseServiceHost(host) { } if (host.startsWith('[') && host.endsWith(']')) { const address = ipv6ToBuffer(host.slice(1, -1)); + if (isDisallowedIPv6(address)) { + throw new Error(`Invalid IPv6 address ${host}`); + } // Core prints CJDNS addresses like IPv6. Mainnet and testnet reject unroutable // fc00::/8 IPv6, so there it is always CJDNS (regtest may store it as IPv6). return { @@ -256,7 +273,7 @@ function writeNetInfoEntry(writer, entry) { writer.writeVarintNum(service.address.length); writer.write(service.address); } else { - if (!DOMAIN_REGEX.test(host)) { + if (!DOMAIN_REGEX.test(host) || host.length > DOMAIN_MAX_LENGTH) { throw new Error(`Invalid address entry ${entry}`); } const domain = Buffer.from(host, 'ascii'); @@ -314,7 +331,7 @@ function serviceToString(networkId, address, port) { case BIP155_IPV4: return `${Array.from(address).join('.')}:${port}`; case BIP155_IPV6: - if (IPV6_DISALLOWED_PREFIXES.some((prefix) => address.indexOf(prefix) === 0)) { + if (isDisallowedIPv6(address)) { throw new Error(`Invalid IPv6 address ${address.toString('hex')}`); } return `[${ipv6ToString(address)}]:${port}`; diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index 0a94351e5..3719f7d7a 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -221,6 +221,34 @@ describe('ProRegTxPayload', function () { }).to.throw('Expected payout scriptPayout to be a p2pkh/p2sh'); }); + it('Should require payouts unless the masternode is shared', function () { + var payload = getPayload('v3 ProRegTx with two payouts'); + payload.payouts = []; + + expect(function () { + payload.validate(); + }).to.throw('Expect at least one payout'); + + payload = getSharedPayload(); + payload.payouts = getPayload('v3 ProRegTx with two payouts').payouts; + + expect(function () { + payload.validate(); + }).to.throw('Expect no payouts for a shared masternode'); + }); + + it('Should reject a version 3 payoutAddress', function () { + var json = getPayload('v3 ProRegTx with two payouts').toJSON({ + network: 'testnet', + }); + json.payoutAddress = json.payouts[0].address; + delete json.payouts; + + expect(function () { + ProRegTxPayload.fromJSON(json); + }).to.throw('Expect payouts instead of payoutAddress from version 3'); + }); + it('Should require addresses that can be serialized', function () { var payload = getPayload('v3 evonode ProRegTx'); payload.addresses = { core_p2p: ['not an address'] }; diff --git a/test/transaction/payload/proupregtxpayload.js b/test/transaction/payload/proupregtxpayload.js index 4a358ae55..2b9763753 100644 --- a/test/transaction/payload/proupregtxpayload.js +++ b/test/transaction/payload/proupregtxpayload.js @@ -115,15 +115,29 @@ describe('ProUpRegTxPayload', function () { expect(new ProUpRegTxPayload().version).to.be.equal(1); }); - it('Should require valid payouts', function () { + it('Should require at least one payout', function () { var payload = new DashcoreLib.Transaction(vector.hex).extraPayload; payload.payouts = []; - payload.validate(); + expect(function () { + payload.validate(); + }).to.throw('Expect at least one payout'); payload.payouts = undefined; expect(function () { payload.validate(); }).to.throw('Expect payouts to be an array'); }); + + it('Should reject a version 3 payoutAddress', function () { + var json = new DashcoreLib.Transaction(vector.hex).extraPayload.toJSON({ + network: 'testnet', + }); + json.payoutAddress = json.payouts[0].address; + delete json.payouts; + + expect(function () { + ProUpRegTxPayload.fromJSON(json); + }).to.throw('Expect payouts instead of payoutAddress from version 3'); + }); }); }); diff --git a/test/util/netinfo.js b/test/util/netinfo.js index 8a29f248e..9e77ca4b1 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -76,6 +76,18 @@ describe('netinfo', () => { expect(() => extNetInfoToBuffer({ platform_https: ['Example.com:443'] })).to.throw('Invalid address entry'); expect(() => extNetInfoToBuffer({ core_p2p: ['[2001:db8::1::2]:9999'] })).to.throw('Invalid IPv6'); expect(() => extNetInfoToBuffer({ core_p2p: ['aaaa.onion:9999'] })).to.throw('Invalid Tor v3'); + // IPv6 forms Core reads as IPv4, Tor v2 or internal addresses + ['[::ffff:102:304]:9999', '[fd87:d87e:eb43::1]:9999', '[fd6b:88c0:8724::1]:9999'].forEach((entry) => { + expect(() => extNetInfoToBuffer({ core_p2p: [entry] })).to.throw('Invalid IPv6 address'); + }); + // DOMAIN_MAX_LEN is 253 characters + const label = 'a'.repeat(63); + const domain253 = [label, label, label, 'a'.repeat(61)].join('.'); + expect(domain253).to.have.lengthOf(253); + expect(extNetInfoFromBuffer(extNetInfoToBuffer({ platform_https: [`${domain253}:443`] }))) + .to.deep.equal({ platform_https: [`${domain253}:443`] }); + expect(() => extNetInfoToBuffer({ platform_https: [`${domain253}aa:443`] })) + .to.throw('Invalid address entry'); // a valid address with the last checksum character changed expect(() => extNetInfoToBuffer({ core_p2p: ['pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscsyd.onion:9999'], @@ -92,13 +104,15 @@ describe('netinfo', () => { describe('extNetInfoFromBuffer', () => { it('should read every address kind in Core\'s string form', () => { - // Tor, I2P and domain entries as Core v24 reports them (rpc_netinfo.py) + // Entries as Core v24 reports them: Tor, I2P and domain from rpc_netinfo.py; + // the IPv6 and CJDNS ones decoded by Core from an rpc_netinfo.py + // ProUpServTx with its [::1] entry replaced const addresses = { core_p2p: [ '127.0.0.1:19951', 'kpgvmscirrdqpekbqjsvw5teanhatztpp2gl6eee4zkowvwfxwenqaid.onion:19951', - '[2001:db8::1]:9999', - '[fc00::1]:9999', + '[2a01:4f8::1]:19951', + '[fc00::1]:19951', ], platform_p2p: ['c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p:0'], platform_https: ['server-1.example.com:22201'], From 3f2d209449b9018654e515eefa2b8982f14c68f3 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 17:50:29 +0000 Subject: [PATCH 04/15] refactor: simplify ExtNetInfo helpers and ProTx v3 validation No change to serialized bytes or JSON. - ipv6ToString joins the groups before and after the first longest zero run instead of tracking separators group by group. - One torV3AddressBytes helper builds pubkey || checksum || version for both formatting and parsing .onion names; comparing against it checks the length, checksum and version at once. - isPayoutScript lives in masternodepayouts.js and is used for payouts and shares alike. - extNetInfoToBuffer checks that addresses is an object, so ProRegTx, ProUpServTx and SimplifiedMNListEntry validate() just call it. - validateShares is private to proregtxpayload.js. - PROTX_EXT_ADDR_VERSION replaces SML_ENTRY_EXT_ADDR_VERSION (unreleased), so ProTx payloads and SML entries share one ProTxVersion::ExtAddr. Co-Authored-By: Claude Opus 5.5 --- lib/constants/index.js | 5 +- .../SimplifiedMNListEntry.js | 13 +-- lib/transaction/payload/masternodepayouts.js | 20 +++- lib/transaction/payload/proregtxpayload.js | 107 ++++++++---------- lib/transaction/payload/proupservtxpayload.js | 5 - lib/util/netinfo.js | 73 ++++++------ test/util/netinfo.js | 2 +- .../transaction/payload/ProRegTxPayload.d.ts | 5 - 8 files changed, 102 insertions(+), 128 deletions(-) diff --git a/lib/constants/index.js b/lib/constants/index.js index 763bf68c4..47397e0f6 100644 --- a/lib/constants/index.js +++ b/lib/constants/index.js @@ -43,9 +43,8 @@ const constants = { CURRENT_PROTOCOL_VERSION: 70211, SML_ENTRY_VERSION_1_SIZE: 151, SML_ENTRY_TYPE_2_ADDITION_SIZE: 22, - // ProTxVersion::ExtAddr: addresses are serialized as ExtNetInfo - SML_ENTRY_EXT_ADDR_VERSION: 3, - // ProTxVersion::ExtAddr: ProTx payloads with ExtNetInfo addresses and payout lists + // ProTxVersion::ExtAddr: ProTx payloads and SML entries serialize addresses + // as ExtNetInfo, and ProTx payloads have payout lists PROTX_EXT_ADDR_VERSION: 3, NULL_HASH: '0000000000000000000000000000000000000000000000000000000000000000', // In duffs diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index a56dd15b0..33aa22508 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -23,7 +23,7 @@ const { PUBKEY_ID_SIZE, PLATFORM_NODE_ID_SIZE, MASTERNODE_TYPE_HP, - SML_ENTRY_EXT_ADDR_VERSION, + PROTX_EXT_ADDR_VERSION, } = constants; /** @@ -144,7 +144,7 @@ SimplifiedMNListEntry.fromHexString = function fromHexString(string, network) { */ SimplifiedMNListEntry.prototype.toBuffer = function toBuffer() { this.validate(); - const hasExtAddr = this.nVersion >= SML_ENTRY_EXT_ADDR_VERSION; + const hasExtAddr = this.nVersion >= PROTX_EXT_ADDR_VERSION; const bufferWriter = new BufferWriter(); bufferWriter.write(Buffer.from(this.proRegTxHash, 'hex').reverse()); @@ -203,7 +203,7 @@ SimplifiedMNListEntry.fromObject = function fromObject(obj) { // and derives both from `addresses`. ExtAddr entries hash `addresses` instead of // these fields, so for them always take both from `addresses`. if (SMLEntry.addresses) { - const hasExtAddr = SMLEntry.nVersion >= SML_ENTRY_EXT_ADDR_VERSION; + const hasExtAddr = SMLEntry.nVersion >= PROTX_EXT_ADDR_VERSION; const [coreP2P] = SMLEntry.addresses.core_p2p || []; const [platformHTTPS] = SMLEntry.addresses.platform_https || []; if (hasExtAddr || SMLEntry.service === undefined) { @@ -227,7 +227,7 @@ SimplifiedMNListEntry.fromObject = function fromObject(obj) { }; SimplifiedMNListEntry.prototype.validate = function validate() { - const hasExtAddr = this.nVersion >= SML_ENTRY_EXT_ADDR_VERSION; + const hasExtAddr = this.nVersion >= PROTX_EXT_ADDR_VERSION; $.checkArgument( isSha256(this.proRegTxHash), 'Expected proRegTxHash to be a sha256 hex string' @@ -237,11 +237,6 @@ SimplifiedMNListEntry.prototype.validate = function validate() { 'Expected confirmedHash to be a sha256 hex string' ); if (hasExtAddr) { - $.checkArgument( - _.isPlainObject(this.addresses), - 'Expected addresses to be an object' - ); - // throws if the addresses can't be serialized extNetInfoToBuffer(this.addresses); } else if (!ipUtils.isZeroAddress(this.service)) { $.checkArgument( diff --git a/lib/transaction/payload/masternodepayouts.js b/lib/transaction/payload/masternodepayouts.js index 940ff65f0..21812e1c7 100644 --- a/lib/transaction/payload/masternodepayouts.js +++ b/lib/transaction/payload/masternodepayouts.js @@ -22,6 +22,18 @@ const MAX_PAYOUTS_COUNT = 0xff; * @property {number} reward - share of the owner reward in basis points */ +/** + * @param {string} script - hex + * @return {boolean} whether the script is p2pkh or p2sh + */ +function isPayoutScript(script) { + if (!utils.isHexaString(script)) { + return false; + } + const parsed = new Script(script); + return parsed.isPublicKeyHashOut() || parsed.isScriptHashOut(); +} + /** * @param {BufferReader} reader * @return {MasternodePayout[]} @@ -63,12 +75,7 @@ function validatePayouts(payouts) { ); payouts.forEach((payout) => { Preconditions.checkArgument( - utils.isHexaString(payout.scriptPayout), - 'Expect payout scriptPayout to be a hex string', - ); - const script = new Script(payout.scriptPayout); - Preconditions.checkArgument( - script.isPublicKeyHashOut() || script.isScriptHashOut(), + isPayoutScript(payout.scriptPayout), 'Expected payout scriptPayout to be a p2pkh/p2sh', ); Preconditions.checkArgument( @@ -111,6 +118,7 @@ function payoutsFromJSON(payloadJSON) { } module.exports = { + isPayoutScript, readPayouts, writePayouts, validatePayouts, diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index c3c17fd9a..fdcdabf1d 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -180,15 +180,52 @@ function sharesToJSON(shares, network) { } /** - * @param {string} script - hex - * @return {boolean} + * Validate the collateral shares of a version 3 payload + * @param {ProRegTxPayload} payload */ -function isPayoutScript(script) { - if (!utils.isHexaString(script)) { - return false; - } - var parsed = new Script(script); - return parsed.isPublicKeyHashOut() || parsed.isScriptHashOut(); +function validateShares(payload) { + Preconditions.checkArgument( + Array.isArray(payload.shares) && payload.shares.length <= MAX_SHARES_COUNT, + 'Expect shares to be an array of at most 255 shares' + ); + Preconditions.checkArgument( + Array.isArray(payload.joinSigs) && + payload.joinSigs.length === payload.shares.length, + 'Expect one join signature per share' + ); + payload.shares.forEach(function (share) { + Preconditions.checkArgument( + Number.isSafeInteger(share.amount) && share.amount >= 0, + 'Expect share amount to be an unsigned integer' + ); + Preconditions.checkArgument( + payouts.isPayoutScript(share.scriptRefund), + 'Expected share scriptRefund to be a p2pkh/p2sh' + ); + Preconditions.checkArgument( + share.scriptReward === '' || payouts.isPayoutScript(share.scriptReward), + 'Expected share scriptReward to be empty or a p2pkh/p2sh' + ); + Preconditions.checkArgument( + utils.isHexStringOfSize(share.keyIDOwner, constants.PUBKEY_ID_SIZE * 2), + 'Expect share keyIDOwner to be 20 bytes in size' + ); + }); + payload.joinSigs.forEach(function (joinSig) { + Preconditions.checkArgument( + utils.isHexStringOfSize(joinSig, COMPACT_SIGNATURE_SIZE * 2), + 'Expect join signatures to be 65 bytes in size' + ); + }); + Preconditions.checkArgument( + utils.isUnsignedInteger(payload.earlyPeriodBlocks) && + payload.earlyPeriodBlocks <= 0xffffffff, + 'Expect earlyPeriodBlocks to be an uint32' + ); + Preconditions.checkArgument( + Number.isSafeInteger(payload.earlyPenalty) && payload.earlyPenalty >= 0, + 'Expect earlyPenalty to be an unsigned integer' + ); } /* Static methods */ @@ -318,11 +355,6 @@ ProRegTxPayload.prototype.validate = function () { 'Expect collateralHash to be a hex string representing sha256 hash' ); if (this.version >= PROTX_EXT_ADDR_VERSION) { - Preconditions.checkArgument( - _.isPlainObject(this.addresses), - 'Expected addresses to be an object' - ); - // throws if the addresses can't be serialized netInfo.extNetInfoToBuffer(this.addresses); } else if (!ipUtil.isZeroAddress(this.service)) { Preconditions.checkArgument( @@ -389,7 +421,7 @@ ProRegTxPayload.prototype.validate = function () { if (this.version >= PROTX_EXT_ADDR_VERSION) { payouts.validatePayouts(this.payouts); - this.validateShares(); + validateShares(this); // Core pays a shared masternode's owner rewards by share and any other // masternode's to its payouts (CProRegTx::IsTriviallyValid) if (this.shares.length > 0) { @@ -430,53 +462,6 @@ ProRegTxPayload.prototype.validate = function () { } }; -/** - * Validate the collateral shares of a version 3 payload - */ -ProRegTxPayload.prototype.validateShares = function validateShares() { - Preconditions.checkArgument( - Array.isArray(this.shares) && this.shares.length <= MAX_SHARES_COUNT, - 'Expect shares to be an array of at most 255 shares' - ); - Preconditions.checkArgument( - Array.isArray(this.joinSigs) && this.joinSigs.length === this.shares.length, - 'Expect one join signature per share' - ); - this.shares.forEach(function (share) { - Preconditions.checkArgument( - Number.isSafeInteger(share.amount) && share.amount >= 0, - 'Expect share amount to be an unsigned integer' - ); - Preconditions.checkArgument( - isPayoutScript(share.scriptRefund), - 'Expected share scriptRefund to be a p2pkh/p2sh' - ); - Preconditions.checkArgument( - share.scriptReward === '' || isPayoutScript(share.scriptReward), - 'Expected share scriptReward to be empty or a p2pkh/p2sh' - ); - Preconditions.checkArgument( - utils.isHexStringOfSize(share.keyIDOwner, constants.PUBKEY_ID_SIZE * 2), - 'Expect share keyIDOwner to be 20 bytes in size' - ); - }); - this.joinSigs.forEach(function (joinSig) { - Preconditions.checkArgument( - utils.isHexStringOfSize(joinSig, COMPACT_SIGNATURE_SIZE * 2), - 'Expect join signatures to be 65 bytes in size' - ); - }); - Preconditions.checkArgument( - utils.isUnsignedInteger(this.earlyPeriodBlocks) && - this.earlyPeriodBlocks <= 0xffffffff, - 'Expect earlyPeriodBlocks to be an uint32' - ); - Preconditions.checkArgument( - Number.isSafeInteger(this.earlyPenalty) && this.earlyPenalty >= 0, - 'Expect earlyPenalty to be an unsigned integer' - ); -}; - /** * Serializes payload to JSON * @param [options] diff --git a/lib/transaction/payload/proupservtxpayload.js b/lib/transaction/payload/proupservtxpayload.js index cd6b133e8..f25d7b071 100644 --- a/lib/transaction/payload/proupservtxpayload.js +++ b/lib/transaction/payload/proupservtxpayload.js @@ -187,11 +187,6 @@ ProUpServTxPayload.prototype.validate = function () { ); if (this.version >= PROTX_EXT_ADDR_VERSION) { - Preconditions.checkArgument( - _.isPlainObject(this.addresses), - 'Expected addresses to be an object' - ); - // throws if the addresses can't be serialized netInfo.extNetInfoToBuffer(this.addresses); } else if (!ipUtils.isZeroAddress(this.service)) { Preconditions.checkArgument( diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index 800911545..06aa3e1e9 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -1,3 +1,4 @@ +const _ = require('lodash'); const BufferReader = require('../encoding/bufferreader'); const BufferWriter = require('../encoding/bufferwriter'); @@ -27,8 +28,6 @@ const IPV6_ADDRESS_SIZE = 16; const TORV3_ADDRESS_SIZE = 32; const TORV3_CHECKSUM_SIZE = 2; const TORV3_VERSION = 3; -// base32 payload of a .onion name: pubkey, checksum, version -const TORV3_ENCODED_SIZE = TORV3_ADDRESS_SIZE + TORV3_CHECKSUM_SIZE + 1; const I2P_ADDRESS_SIZE = 32; const CJDNS_ADDRESS_SIZE = 16; @@ -112,11 +111,12 @@ function encodeBase32(buffer) { } /** - * Tor v3 address checksum (torv3::Checksum in Core's netaddress.cpp) + * The bytes a .onion name encodes: pubkey || checksum[2] || version[1], with + * torv3::Checksum from Core's netaddress.cpp * @param {Buffer} pubKey * @return {Buffer} */ -function torV3Checksum(pubKey) { +function torV3AddressBytes(pubKey) { // Required here rather than at the top: @noble/hashes/sha3 calls BigInt when it // loads, and only Tor v3 addresses need it, so the rest of the library doesn't // need BigInt support @@ -125,7 +125,8 @@ function torV3Checksum(pubKey) { const digest = sha3256( Buffer.concat([Buffer.from('.onion checksum', 'ascii'), pubKey, Buffer.from([TORV3_VERSION])]), ); - return Buffer.from(digest).slice(0, TORV3_CHECKSUM_SIZE); + const checksum = Buffer.from(digest).slice(0, TORV3_CHECKSUM_SIZE); + return Buffer.concat([pubKey, checksum, Buffer.from([TORV3_VERSION])]); } /** @@ -147,34 +148,34 @@ function isDisallowedIPv6(address) { function ipv6ToString(address) { const groups = []; for (let i = 0; i < IPV6_ADDRESS_SIZE; i += 2) { - groups.push(address.readUInt16BE(i)); + groups.push(address.readUInt16BE(i).toString(16)); } - let longest = { start: 0, length: 0 }; - let current = { start: 0, length: 0 }; + // The strict > keeps the first of equally long zero runs + let longestStart = 0; + let longestLength = 0; + let currentStart = 0; + let currentLength = 0; groups.forEach((group, i) => { - if (group !== 0) { - current = { start: i + 1, length: 0 }; + if (group !== '0') { + currentStart = i + 1; + currentLength = 0; return; } - current.length += 1; - if (current.length > longest.length) { - longest = { start: current.start, length: current.length }; + currentLength += 1; + if (currentLength > longestLength) { + longestStart = currentStart; + longestLength = currentLength; } }); - let string = ''; - groups.forEach((group, i) => { - if (longest.length >= 2 && i >= longest.start && i < longest.start + longest.length) { - if (i === longest.start) { - string += '::'; - } - return; - } - const separator = string !== '' && !string.endsWith(':') ? ':' : ''; - string += `${separator}${group.toString(16)}`; - }); - return string; + // A single zero group isn't compressed + if (longestLength < 2) { + return groups.join(':'); + } + const head = groups.slice(0, longestStart).join(':'); + const tail = groups.slice(longestStart + longestLength).join(':'); + return `${head}::${tail}`; } /** @@ -194,7 +195,7 @@ function ipv6ToBuffer(address) { throw new Error(`Invalid IPv6 address ${address}`); } const groups = head.concat(new Array(missing).fill('0'), tail); - const buffer = Buffer.alloc(16); + const buffer = Buffer.alloc(IPV6_ADDRESS_SIZE); groups.forEach((group, i) => { if (!/^[0-9a-fA-F]{1,4}$/.test(group)) { throw new Error(`Invalid IPv6 address ${address}`); @@ -229,16 +230,11 @@ function parseServiceHost(host) { }; } if (host.endsWith('.onion')) { - // base32(pubkey || checksum[2] || version[1]) const decoded = decodeBase32(host.replace(/\.onion$/, '')); - const version = decoded[decoded.length - 1]; - if (decoded.length !== TORV3_ENCODED_SIZE || version !== TORV3_VERSION) { - throw new Error(`Invalid Tor v3 address ${host}`); - } const pubKey = decoded.slice(0, TORV3_ADDRESS_SIZE); - const checksum = decoded.slice(TORV3_ADDRESS_SIZE, TORV3_ADDRESS_SIZE + TORV3_CHECKSUM_SIZE); - if (!checksum.equals(torV3Checksum(pubKey))) { - throw new Error(`Invalid Tor v3 address checksum in ${host}`); + // covers the length, checksum and version + if (!decoded.equals(torV3AddressBytes(pubKey))) { + throw new Error(`Invalid Tor v3 address ${host}`); } return { networkId: BIP155_TORV3, address: pubKey }; } @@ -291,6 +287,9 @@ function writeNetInfoEntry(writer, entry) { * @return {Buffer} */ function extNetInfoToBuffer(addresses) { + if (!_.isPlainObject(addresses)) { + throw new Error('Expected addresses to be an object'); + } Object.keys(addresses).forEach((purpose) => { if (!PURPOSES.includes(purpose)) { throw new Error(`Unknown address purpose ${purpose}`); @@ -337,10 +336,8 @@ function serviceToString(networkId, address, port) { return `[${ipv6ToString(address)}]:${port}`; case BIP155_CJDNS: return `[${ipv6ToString(address)}]:${port}`; - case BIP155_TORV3: { - const onion = Buffer.concat([address, torV3Checksum(address), Buffer.from([TORV3_VERSION])]); - return `${encodeBase32(onion)}.onion:${port}`; - } + case BIP155_TORV3: + return `${encodeBase32(torV3AddressBytes(address))}.onion:${port}`; case BIP155_I2P: return `${encodeBase32(address)}.b32.i2p:${port}`; default: diff --git a/test/util/netinfo.js b/test/util/netinfo.js index 9e77ca4b1..d7eee95e3 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -91,7 +91,7 @@ describe('netinfo', () => { // a valid address with the last checksum character changed expect(() => extNetInfoToBuffer({ core_p2p: ['pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscsyd.onion:9999'], - })).to.throw('Invalid Tor v3 address checksum'); + })).to.throw('Invalid Tor v3 address'); // the last character of a 52 character I2P address carries 4 unused bits expect(() => extNetInfoToBuffer({ core_p2p: ['eaqseizeeutcokbjfivsyljof4ydcmrtgq2tmnzyhe5dwpb5hy7r.b32.i2p:0'], diff --git a/typings/transaction/payload/ProRegTxPayload.d.ts b/typings/transaction/payload/ProRegTxPayload.d.ts index 7b22f6fc3..347061e35 100644 --- a/typings/transaction/payload/ProRegTxPayload.d.ts +++ b/typings/transaction/payload/ProRegTxPayload.d.ts @@ -124,11 +124,6 @@ export class ProRegTxPayload { */ validate(): boolean; - /** - * Validate the collateral shares of a version 3 payload - */ - validateShares(): void; - /** * Serializes payload to JSON * @param [options] From 01c4a794db1de28148e37dd4d28ff9776b2f3b03 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 21:36:32 +0000 Subject: [PATCH 05/15] fix: validate ProTx payouts, shares and signatures like Core - ProUpServTx's payloadSig length check was inverted: it accepted any length but 48 bytes. Require the 96-byte BLS signature. ProUpRegTx had the same inverted check against the BLS size, but its signature is the owner's compact ECDSA signature, so require 65 bytes. ProUpRevTx was already right and ProRegTx has no length check. - Version 3 payout lists get the checks of Core's IsPayoutListTriviallyValid: 1 to 8 payouts, rewards from 100 to 10000 adding up to 10000, unique p2pkh/p2sh payees, none paying the voting key's P2PKH or (ProRegTx only, as Core passes no owner key for a ProUpRegTx) the owner key's. - Version 3 ProRegTxs get the context-free checks of CProRegTx::IsTriviallyValid and IsShareListTriviallyValid: a regular or evo type; with shares a null collateral hash and owner key and no payouts, 2 to 8 shares of 100 DASH up to the collateral that add up to the type's collateral (1000 or 4000 DASH), non-null unique owner keys, unique refund scripts, p2pkh/p2sh scripts not paying the voting key or a share owner key, an early period of at most 420480 blocks and a penalty below the smallest share and zero without an early period; without shares no early terms and a non-null owner key. - fromJSON rejects fields the payload's version doesn't have (and toJSON never emits) instead of dropping them: service, payoutAddress, scriptPayout and platform ports from version 3; addresses, payouts, shares, joinSigs and early terms before it. - The payout helpers move to protxutils.js, which now also holds the field check shared by the three payloads. Every Core vector still validates, including the 8-share and the shared evonode registrations, now part of the fixtures. Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/proregtxpayload.js | 195 +++++++++--- .../{masternodepayouts.js => protxutils.js} | 88 +++-- lib/transaction/payload/proupregtxpayload.js | 31 +- lib/transaction/payload/proupservtxpayload.js | 9 +- test/fixtures/core24-protx.json | 139 ++++++++ test/transaction/payload/proregtxpayload.js | 300 +++++++++++++----- test/transaction/payload/proupregtxpayload.js | 60 +++- .../transaction/payload/proupservtxpayload.js | 38 ++- 8 files changed, 688 insertions(+), 172 deletions(-) rename lib/transaction/payload/{masternodepayouts.js => protxutils.js} (52%) diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index fdcdabf1d..74bdd920b 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -12,7 +12,7 @@ var Script = require('../../script'); var ipUtil = require('../../util/ip'); var BN = require('../../crypto/bn'); var netInfo = require('../../util/netinfo'); -var payouts = require('./masternodepayouts'); +var protxUtils = require('./protxutils'); const { PLATFORM_NODE_ID_SIZE, MASTERNODE_TYPE_BASIC, MASTERNODE_TYPE_HP, @@ -21,8 +21,32 @@ const { PLATFORM_NODE_ID_SIZE, } = require("../../constants"); var CURRENT_PAYLOAD_VERSION = 2; -// Core writes the share count as one byte -var MAX_SHARES_COUNT = 0xff; +var COIN = 100000000; +// Collateral by masternode type (src/evo/dmn_types.h) +var REGULAR_COLLATERAL = 1000 * COIN; +var EVO_COLLATERAL = 4000 * COIN; +// CProRegTx::MIN_SHARES, MAX_SHARES and MAX_EARLY_PERIOD_BLOCKS +var MIN_SHARES = 2; +var MAX_SHARES = 8; +var MAX_EARLY_PERIOD_BLOCKS = 420480; +// CCollateralShare::MIN_AMOUNT +var MIN_SHARE_AMOUNT = 100 * COIN; +// Fields older versions have and version 3 doesn't, and the other way round +var LEGACY_ONLY_FIELDS = [ + 'service', + 'payoutAddress', + 'scriptPayout', + 'platformP2PPort', + 'platformHTTPPort', +]; +var EXT_ADDR_ONLY_FIELDS = [ + 'addresses', + 'payouts', + 'shares', + 'joinSigs', + 'earlyPeriodBlocks', + 'earlyPenalty', +]; /** * @typedef {Object} ProRegTxPayloadJSON @@ -109,13 +133,15 @@ function ProRegTxPayload(options) { this.keyIDVoting = options.keyIDVoting; this.operatorReward = options.operatorReward; if (this.version >= PROTX_EXT_ADDR_VERSION) { + protxUtils.checkNoFields(options, LEGACY_ONLY_FIELDS, this.version); this.addresses = _.cloneDeep(options.addresses); - this.payouts = payouts.payoutsFromJSON(options); + this.payouts = protxUtils.payoutsFromJSON(options.payouts || []); this.shares = sharesFromJSON(options.shares || []); this.joinSigs = (options.joinSigs || []).slice(); this.earlyPeriodBlocks = options.earlyPeriodBlocks || 0; this.earlyPenalty = options.earlyPenalty || 0; } else { + protxUtils.checkNoFields(options, EXT_ADDR_ONLY_FIELDS, this.version); this.service = options.service; this.scriptPayout = Script.fromAddress(options.payoutAddress).toHex(); } @@ -180,37 +206,114 @@ function sharesToJSON(shares, network) { } /** - * Validate the collateral shares of a version 3 payload + * The context-free checks Core's IsShareListTriviallyValid makes on the + * collateral shares of a shared masternode (src/evo/providertx.cpp) * @param {ProRegTxPayload} payload */ function validateShares(payload) { + var shares = payload.shares; + var requiredCollateral = + payload.type === MASTERNODE_TYPE_HP ? EVO_COLLATERAL : REGULAR_COLLATERAL; + Preconditions.checkArgument( - Array.isArray(payload.shares) && payload.shares.length <= MAX_SHARES_COUNT, - 'Expect shares to be an array of at most 255 shares' + shares.length >= MIN_SHARES && shares.length <= MAX_SHARES, + 'Expect 2 to 8 shares' ); Preconditions.checkArgument( - Array.isArray(payload.joinSigs) && - payload.joinSigs.length === payload.shares.length, - 'Expect one join signature per share' + payload.earlyPeriodBlocks <= MAX_EARLY_PERIOD_BLOCKS, + 'Expect earlyPeriodBlocks to be at most 420480' ); - payload.shares.forEach(function (share) { + + var totalAmount = 0; + var minAmount = Infinity; + var ownerKeys = new Set(); + var refundScripts = new Set(); + shares.forEach(function (share) { Preconditions.checkArgument( - Number.isSafeInteger(share.amount) && share.amount >= 0, - 'Expect share amount to be an unsigned integer' + Number.isSafeInteger(share.amount) && + share.amount >= MIN_SHARE_AMOUNT && + share.amount <= requiredCollateral, + 'Expect share amount to be from 100 DASH to the masternode collateral' ); + totalAmount += share.amount; + minAmount = Math.min(minAmount, share.amount); + Preconditions.checkArgument( - payouts.isPayoutScript(share.scriptRefund), - 'Expected share scriptRefund to be a p2pkh/p2sh' + utils.isHexStringOfSize(share.keyIDOwner, constants.PUBKEY_ID_SIZE * 2) && + !/^0+$/.test(share.keyIDOwner), + 'Expect share keyIDOwner to be a non-null 20 byte key id' ); Preconditions.checkArgument( - share.scriptReward === '' || payouts.isPayoutScript(share.scriptReward), - 'Expected share scriptReward to be empty or a p2pkh/p2sh' + !ownerKeys.has(share.keyIDOwner), + 'Expect share keyIDOwner to be unique' + ); + ownerKeys.add(share.keyIDOwner); + Preconditions.checkArgument( + !refundScripts.has(share.scriptRefund), + 'Expect share scriptRefund to be unique' ); + refundScripts.add(share.scriptRefund); + Preconditions.checkArgument( - utils.isHexStringOfSize(share.keyIDOwner, constants.PUBKEY_ID_SIZE * 2), - 'Expect share keyIDOwner to be 20 bytes in size' + protxUtils.isPayoutScript(share.scriptRefund), + 'Expected share scriptRefund to be a p2pkh/p2sh' ); + Preconditions.checkArgument( + share.scriptReward === '' || protxUtils.isPayoutScript(share.scriptReward), + 'Expected share scriptReward to be empty or a p2pkh/p2sh' + ); + [share.scriptRefund, share.scriptReward].forEach(function (script) { + if (script === '') { + return; + } + var paysKey = function (keyID) { + return protxUtils.isP2PKHOf(script, keyID); + }; + Preconditions.checkArgument( + !paysKey(payload.keyIDVoting) && + !shares.some(function (other) { + return paysKey(other.keyIDOwner); + }), + 'Expect share scripts not to pay the voting key or a share owner key' + ); + }); }); + + Preconditions.checkArgument( + totalAmount === requiredCollateral, + 'Expect share amounts to add up to the masternode collateral' + ); + Preconditions.checkArgument( + payload.earlyPenalty < minAmount, + 'Expect earlyPenalty to be less than the smallest share' + ); + // Without an early period the penalty would still cap the unilateral bonus + Preconditions.checkArgument( + payload.earlyPeriodBlocks !== 0 || payload.earlyPenalty === 0, + 'Expect no earlyPenalty without an early period' + ); +} + +/** + * The context-free checks Core's CProRegTx::IsTriviallyValid makes on the + * version 3 fields. Left to Core: the operator key scheme, the address rules + * (IsNetInfoTriviallyValid), and everything that needs the chain or the rest + * of the transaction (activation, collateral, signatures and join signatures). + * @param {ProRegTxPayload} payload + */ +function validateExtAddrFields(payload) { + Preconditions.checkArgument( + payload.type === MASTERNODE_TYPE_BASIC || payload.type === MASTERNODE_TYPE_HP, + 'Expect type to be 0 (regular) or 1 (evo)' + ); + Preconditions.checkArgument( + Array.isArray(payload.shares) && Array.isArray(payload.joinSigs), + 'Expect shares and joinSigs to be arrays' + ); + Preconditions.checkArgument( + payload.joinSigs.length === payload.shares.length, + 'Expect one join signature per share' + ); payload.joinSigs.forEach(function (joinSig) { Preconditions.checkArgument( utils.isHexStringOfSize(joinSig, COMPACT_SIGNATURE_SIZE * 2), @@ -226,6 +329,38 @@ function validateShares(payload) { Number.isSafeInteger(payload.earlyPenalty) && payload.earlyPenalty >= 0, 'Expect earlyPenalty to be an unsigned integer' ); + + var hasNullOwnerKey = /^0+$/.test(payload.keyIDOwner); + if (payload.shares.length > 0) { + // the share owners replace the owner key and are paid by share + Preconditions.checkArgument( + payload.collateralHash === constants.NULL_HASH, + 'Expect a shared masternode to have its collateral in the registration' + ); + Preconditions.checkArgument( + hasNullOwnerKey, + 'Expect a null keyIDOwner for a shared masternode' + ); + Preconditions.checkArgument( + Array.isArray(payload.payouts) && payload.payouts.length === 0, + 'Expect no payouts for a shared masternode' + ); + validateShares(payload); + } else { + Preconditions.checkArgument( + payload.earlyPeriodBlocks === 0 && payload.earlyPenalty === 0, + 'Expect no earlyPeriodBlocks or earlyPenalty without shares' + ); + Preconditions.checkArgument( + !hasNullOwnerKey, + 'Expect a non-null keyIDOwner' + ); + protxUtils.validatePayouts( + payload.payouts, + payload.keyIDOwner, + payload.keyIDVoting + ); + } } /* Static methods */ @@ -267,7 +402,7 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { payload.operatorReward = payloadBufferReader.readUInt16LE(); if (payload.version >= PROTX_EXT_ADDR_VERSION) { - payload.payouts = payouts.readPayouts(payloadBufferReader); + payload.payouts = protxUtils.readPayouts(payloadBufferReader); var sharesCount = payloadBufferReader.readUInt8(); payload.shares = []; for (var i = 0; i < sharesCount; i++) { @@ -420,21 +555,7 @@ ProRegTxPayload.prototype.validate = function () { } if (this.version >= PROTX_EXT_ADDR_VERSION) { - payouts.validatePayouts(this.payouts); - validateShares(this); - // Core pays a shared masternode's owner rewards by share and any other - // masternode's to its payouts (CProRegTx::IsTriviallyValid) - if (this.shares.length > 0) { - Preconditions.checkArgument( - this.payouts.length === 0, - 'Expect no payouts for a shared masternode' - ); - } else { - Preconditions.checkArgument( - this.payouts.length > 0, - 'Expect at least one payout' - ); - } + validateExtAddrFields(this); } if (this.scriptPayout) { @@ -497,7 +618,7 @@ ProRegTxPayload.prototype.toJSON = function toJSON(options) { payloadJSON.keyIDVoting = this.keyIDVoting; payloadJSON.operatorReward = this.operatorReward; if (isExtAddr) { - payloadJSON.payouts = payouts.payoutsToJSON(this.payouts, network); + payloadJSON.payouts = protxUtils.payoutsToJSON(this.payouts, network); payloadJSON.shares = sharesToJSON(this.shares, network); payloadJSON.joinSigs = this.joinSigs.slice(); payloadJSON.earlyPeriodBlocks = this.earlyPeriodBlocks; @@ -557,7 +678,7 @@ ProRegTxPayload.prototype.toBuffer = function toBuffer(options) { .writeUInt16LE(this.operatorReward); if (this.version >= PROTX_EXT_ADDR_VERSION) { - payouts.writePayouts(payloadBufferWriter, this.payouts); + protxUtils.writePayouts(payloadBufferWriter, this.payouts); payloadBufferWriter.writeUInt8(this.shares.length); this.shares.forEach(function (share) { var scriptRefund = Buffer.from(share.scriptRefund, 'hex'); diff --git a/lib/transaction/payload/masternodepayouts.js b/lib/transaction/payload/protxutils.js similarity index 52% rename from lib/transaction/payload/masternodepayouts.js rename to lib/transaction/payload/protxutils.js index 21812e1c7..95a0ecea2 100644 --- a/lib/transaction/payload/masternodepayouts.js +++ b/lib/transaction/payload/protxutils.js @@ -2,13 +2,16 @@ const Preconditions = require('../../util/preconditions'); const utils = require('../../util/js'); const Script = require('../../script'); -// Owner payout list of version 3 (ExtAddr) ProRegTx and ProUpRegTx payloads, -// Core's MasternodePayoutShares (src/evo/providertx.h) +// Helpers shared by the ProTx payloads: the owner payout list of version 3 +// (ExtAddr) ProRegTx and ProUpRegTx payloads, Core's MasternodePayoutShares +// (src/evo/providertx.h), and JSON field checks -// MasternodePayoutShare::MAX_REWARD, the reward of a single payee +// MasternodePayoutShare::MIN_REWARD and MAX_REWARD, in basis points; the rewards +// of a payout list add up to MAX_REWARD +const MIN_REWARD = 100; const MAX_REWARD = 10000; -// Core writes the payout count as one byte -const MAX_PAYOUTS_COUNT = 0xff; +// IsPayoutListTriviallyValid +const MAX_PAYOUTS_COUNT = 8; /** * @typedef {Object} MasternodePayout @@ -34,6 +37,19 @@ function isPayoutScript(script) { return parsed.isPublicKeyHashOut() || parsed.isScriptHashOut(); } +/** + * @param {string} script - hex + * @param {string} keyID - public key hash, in the byte order of the payload JSON + * @return {boolean} whether the script is the P2PKH of the key + */ +function isP2PKHOf(script, keyID) { + const parsed = new Script(script); + return ( + parsed.isPublicKeyHashOut() && + parsed.chunks[2].buf.equals(Buffer.from(keyID, 'hex').reverse()) + ); +} + /** * @param {BufferReader} reader * @return {MasternodePayout[]} @@ -66,23 +82,45 @@ function writePayouts(writer, payouts) { } /** + * The checks of Core's IsPayoutListTriviallyValid (src/evo/providertx.cpp) * @param {MasternodePayout[]} payouts + * @param {string|null} keyIDOwner - owner key the payees must not pay, if any + * @param {string} keyIDVoting - voting key the payees must not pay */ -function validatePayouts(payouts) { +function validatePayouts(payouts, keyIDOwner, keyIDVoting) { Preconditions.checkArgument( - Array.isArray(payouts) && payouts.length <= MAX_PAYOUTS_COUNT, - 'Expect payouts to be an array of at most 255 payouts', + Array.isArray(payouts) && payouts.length >= 1 && payouts.length <= MAX_PAYOUTS_COUNT, + 'Expect payouts to be an array of 1 to 8 payouts', ); + const seenScripts = new Set(); + let totalReward = 0; payouts.forEach((payout) => { + Preconditions.checkArgument( + utils.isUnsignedInteger(payout.reward) && + payout.reward >= MIN_REWARD && + payout.reward <= MAX_REWARD, + 'Expect payout reward to be an integer from 100 to 10000', + ); + totalReward += payout.reward; Preconditions.checkArgument( isPayoutScript(payout.scriptPayout), 'Expected payout scriptPayout to be a p2pkh/p2sh', ); Preconditions.checkArgument( - utils.isUnsignedInteger(payout.reward) && payout.reward <= MAX_REWARD, - 'Expect payout reward to be an integer from 0 to 10000', + !seenScripts.has(payout.scriptPayout), + 'Expect payout scripts to be unique', + ); + seenScripts.add(payout.scriptPayout); + Preconditions.checkArgument( + !(keyIDOwner && isP2PKHOf(payout.scriptPayout, keyIDOwner)) && + !isP2PKHOf(payout.scriptPayout, keyIDVoting), + 'Expect payouts not to pay the owner or voting key', ); }); + Preconditions.checkArgument( + totalReward === MAX_REWARD, + 'Expect payout rewards to add up to 10000', + ); } /** @@ -98,18 +136,10 @@ function payoutsToJSON(payouts, network) { } /** - * Reads the payouts of a version 3 payload's JSON - * @param {Object} payloadJSON - * @param {MasternodePayoutJSON[]} [payloadJSON.payouts] + * @param {MasternodePayoutJSON[]} payoutsJSON * @return {MasternodePayout[]} */ -function payoutsFromJSON(payloadJSON) { - // Don't silently drop a version 1/2 payee - Preconditions.checkArgument( - payloadJSON.payoutAddress === undefined, - 'Expect payouts instead of payoutAddress from version 3', - ); - const payoutsJSON = payloadJSON.payouts || []; +function payoutsFromJSON(payoutsJSON) { Preconditions.checkArgument(Array.isArray(payoutsJSON), 'Expect payouts to be an array'); return payoutsJSON.map((payout) => ({ scriptPayout: Script.fromAddress(payout.address).toHex(), @@ -117,8 +147,26 @@ function payoutsFromJSON(payloadJSON) { })); } +/** + * Rejects JSON fields a payload of this version doesn't have, rather than + * silently dropping them + * @param {Object} payloadJSON + * @param {string[]} fields + * @param {number} version + */ +function checkNoFields(payloadJSON, fields, version) { + fields.forEach((field) => { + Preconditions.checkArgument( + payloadJSON[field] === undefined, + `Expect no ${field} in a version ${version} payload`, + ); + }); +} + module.exports = { isPayoutScript, + isP2PKHOf, + checkNoFields, readPayouts, writePayouts, validatePayouts, diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index b2cd4f1b4..051162bce 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -8,7 +8,7 @@ var BufferReader = require('../../encoding/bufferreader'); var AbstractPayload = require('./abstractpayload'); var Script = require('../../script'); var utils = require('../../util/js'); -var payouts = require('./masternodepayouts'); +var protxUtils = require('./protxutils'); var isUnsignedInteger = utils.isUnsignedInteger; var isHexString = utils.isHexaString; @@ -18,7 +18,7 @@ var CURRENT_PAYLOAD_VERSION = 1; var HASH_SIZE = constants.SHA256_HASH_SIZE; var CKEYID_SIZE = constants.PUBKEY_ID_SIZE; var BLSPUBKEY_SIZE = constants.BLS_PUBLIC_KEY_SIZE; -var BLSSIG_SIZE = constants.BLS_SIGNATURE_SIZE; +var COMPACT_SIGNATURE_SIZE = constants.COMPACT_SIGNATURE_SIZE; var PROTX_EXT_ADDR_VERSION = constants.PROTX_EXT_ADDR_VERSION; /** @@ -63,8 +63,14 @@ function ProUpRegTxPayload(payloadJSON) { this.pubKeyOperator = payloadJSON.pubKeyOperator; this.keyIDVoting = payloadJSON.keyIDVoting; if (this.version >= PROTX_EXT_ADDR_VERSION) { - this.payouts = payouts.payoutsFromJSON(payloadJSON); + protxUtils.checkNoFields( + payloadJSON, + ['payoutAddress', 'scriptPayout'], + this.version + ); + this.payouts = protxUtils.payoutsFromJSON(payloadJSON.payouts || []); } else { + protxUtils.checkNoFields(payloadJSON, ['payouts'], this.version); this.scriptPayout = Script.fromAddress(payloadJSON.payoutAddress).toHex(); } this.inputsHash = payloadJSON.inputsHash; @@ -102,7 +108,7 @@ ProUpRegTxPayload.fromBuffer = function (rawPayload) { .readReverse(CKEYID_SIZE) .toString('hex'); if (payload.version >= PROTX_EXT_ADDR_VERSION) { - payload.payouts = payouts.readPayouts(payloadBufferReader); + payload.payouts = protxUtils.readPayouts(payloadBufferReader); } else { var scriptPayoutSize = payloadBufferReader.readVarintNum(); payload.scriptPayout = payloadBufferReader @@ -173,12 +179,8 @@ ProUpRegTxPayload.prototype.validate = function () { ); if (this.version >= PROTX_EXT_ADDR_VERSION) { - payouts.validatePayouts(this.payouts); - // Core's IsPayoutListTriviallyValid - Preconditions.checkArgument( - this.payouts.length > 0, - 'Expect at least one payout' - ); + // Core passes no owner key for a ProUpRegTx (CProUpRegTx::IsTriviallyValid) + protxUtils.validatePayouts(this.payouts, null, this.keyIDVoting); } if (this.scriptPayout) { @@ -194,9 +196,10 @@ ProUpRegTxPayload.prototype.validate = function () { isHexString(this.payloadSig), 'Expect payloadSig to be a hex string' ); + // signed by the owner key: a compact ECDSA signature, not a BLS one Preconditions.checkArgument( - this.payloadSig.length * 2 !== BLSSIG_SIZE, - "payloadSig size doesn't match BLS signature size" + this.payloadSig.length === COMPACT_SIGNATURE_SIZE * 2, + "payloadSig size doesn't match compact signature size" ); } @@ -224,7 +227,7 @@ ProUpRegTxPayload.prototype.toJSON = function toJSON(options) { keyIDVoting: this.keyIDVoting, }; if (this.version >= PROTX_EXT_ADDR_VERSION) { - payloadJSON.payouts = payouts.payoutsToJSON(this.payouts, network); + payloadJSON.payouts = protxUtils.payoutsToJSON(this.payouts, network); } else { payloadJSON.payoutAddress = new Script(this.scriptPayout) .toAddress(network) @@ -257,7 +260,7 @@ ProUpRegTxPayload.prototype.toBuffer = function toBuffer(options) { .write(Buffer.from(this.keyIDVoting, 'hex').reverse()); if (this.version >= PROTX_EXT_ADDR_VERSION) { - payouts.writePayouts(payloadBufferWriter, this.payouts); + protxUtils.writePayouts(payloadBufferWriter, this.payouts); } else if (this.scriptPayout) { var scriptPayoutBuf = Buffer.from(this.scriptPayout, 'hex'); var scriptPayoutSize = scriptPayoutBuf.length; diff --git a/lib/transaction/payload/proupservtxpayload.js b/lib/transaction/payload/proupservtxpayload.js index f25d7b071..a100334e3 100644 --- a/lib/transaction/payload/proupservtxpayload.js +++ b/lib/transaction/payload/proupservtxpayload.js @@ -10,6 +10,7 @@ var utils = require('../../util/js'); var Script = require('../../script'); var ipUtils = require('../../util/ip'); var netInfo = require('../../util/netinfo'); +var protxUtils = require('./protxutils'); var _ = require('lodash'); const { MASTERNODE_TYPE_HP, PLATFORM_NODE_ID_SIZE, @@ -65,8 +66,14 @@ function ProUpServTxPayload(payloadJSON) { this.proTxHash = payloadJSON.proTxHash; if (this.version >= PROTX_EXT_ADDR_VERSION) { + protxUtils.checkNoFields( + payloadJSON, + ['service', 'platformP2PPort', 'platformHTTPPort'], + this.version + ); this.addresses = _.cloneDeep(payloadJSON.addresses); } else { + protxUtils.checkNoFields(payloadJSON, ['addresses'], this.version); this.service = payloadJSON.service; } this.scriptOperatorPayout = payloadJSON.operatorPayoutAddress @@ -233,7 +240,7 @@ ProUpServTxPayload.prototype.validate = function () { 'Expect payloadSig to be a hex string' ); Preconditions.checkArgument( - this.payloadSig.length * 2 !== BLSSIG_SIZE, + this.payloadSig.length === BLSSIG_SIZE * 2, "payloadSig size doesn't match BLS signature size" ); } diff --git a/test/fixtures/core24-protx.json b/test/fixtures/core24-protx.json index adf6217ae..9e22d9a85 100644 --- a/test/fixtures/core24-protx.json +++ b/test/fixtures/core24-protx.json @@ -354,6 +354,145 @@ "inputsHash": "1390e2077fcaadc2933be7441503ba72b154b49a6585e2ab8ceaf3b993715bd4" } }, + { + "name": "v3 shared ProRegTx with eight shares", + "source": "Core v24.0.0-rc.4 regtest (feature_masternode_shares)", + "txid": "5a6fcb32221f665a42d18678122f3d78c8236c0c3d649622c905dbb14456f6c5", + "hex": "0300010008191f2059f73c768c718bd8b2ff6b1c5364c11ed969e48cb91d0ec4064ec2f2ba060000006a47304402203145f384b05e20ad45db180b05ded52c512786652a096ab8066b4b61c8d58fdf022079c98a0f5ff255ab5168b3b9b1c2319b74c8a248095e7951b3e4bff96c734f370121024a236c8236823f7b8990fc5beb89b41693992729ffb5bd8b6e384f3a419c8e0dffffffff191f2059f73c768c718bd8b2ff6b1c5364c11ed969e48cb91d0ec4064ec2f2ba040000006a47304402203bdb1c931a4ed7bc348fc755ccb430abc8fb1c2a0ad4b22a21b73b4806690ff902201fdc779b17f8c351adbf317038e27a188fc178af6f986e7469c2592a53b4bbfa012103fc6e61095bf675ed0867e3b73db45cf347b3c0a5d3f0183c391a9fae10bbb509ffffffff191f2059f73c768c718bd8b2ff6b1c5364c11ed969e48cb91d0ec4064ec2f2ba020000006a47304402200bcb752e318dc9af41a5f9eb5ea02839e07c65090f838e9e0bf7305762e0c02e02200d851604d4cc08136fcb0c1ae7ba154ba3f24f628c56481cc4c6a0596ec272b60121024f91bb733909859abe74466c367a87a5134b45e54852d29225f83526c7077878ffffffff191f2059f73c768c718bd8b2ff6b1c5364c11ed969e48cb91d0ec4064ec2f2ba070000006a473044022011fc243398018188bb1e8f66ef135f1766acf2d7ddda7e2f4267783bbcaa9fa00220403ebe44cbef7d2566cce77f030513c03a73e2a7697f01e7f94fb0ab5ddafd4701210338b7fb65703f882ef1fc0a1a58f3eed683adce69f1d39f73bec76317fc5dda04ffffffff191f2059f73c768c718bd8b2ff6b1c5364c11ed969e48cb91d0ec4064ec2f2ba030000006a47304402202eca887e4ae0f694ab7059510ffd4ee74c0d73ef0ee980489960fdcbedb1e204022016ed1640fbac409124cd8cc51c8485c03ec5638209ad5888bdb5c2a6ff3c143a01210362068642f85bc3a1a3096dc623d93953a8a2bb7eec1cbfbd8f591ea3a20abd53ffffffff191f2059f73c768c718bd8b2ff6b1c5364c11ed969e48cb91d0ec4064ec2f2ba0500000069463043021f7482e6297df25abc20788413fef8b6c39cfaaf489989bf9bb202c381baa48b0220315fb625bd8bf0af5f529996538f0843283f9f637cc072ea5369d2af41aef731012103f31d7469d4cca85e80de63107dcf2a4788825c2698844b46de20569d95fb7f5affffffff191f2059f73c768c718bd8b2ff6b1c5364c11ed969e48cb91d0ec4064ec2f2ba010000006a47304402207c6bf77a9db340d847a1731eb11ec4c11067f6f937b8bb6b1087f27013bcd2020220194fff43fecbd9d2ed24231481f020272aa4cfb13b5c52f247162632f7aa00450121036c9375d67a764ad9abbeb723a8c77d8aecec0e10ad9b35f842446ee071c144afffffffff191f2059f73c768c718bd8b2ff6b1c5364c11ed969e48cb91d0ec4064ec2f2ba080000006a47304402204aebbc1cdb1ee17e5d72475a380e4a44b3f0e10b79aa3e619a94c244ab6e4d22022067e3a110ddaf926f6052a134d4100ad6de6deecaaf85b18e3c3ad4a145f2c19701210383d92b537690785b323fd22e5d62744e0272638d7c3af27881b73f443fdf16d3ffffffff0978cdf505000000001976a9145be8c005a255c77883d6451379c97ef0f764d7cf88ac78cdf505000000001976a91461272d3bca3922cb9c9316154a6fc7b302033a4c88ac78cdf505000000001976a91454ebe8fb30cfb200e51c37fae99d4ac2df1d7a9588ac78cdf505000000001976a9144f93ec3e6a818e7a9d116bd3cdf2400902adec5f88ac78cdf505000000001976a91453ad8cd896252da77c12d7a4ca444025db45443d88ac78cdf505000000001976a91444a07ca04965e6890b526fbaa1d8a8890f44926e88ac78cdf505000000001976a914116531f49a635bb475ecd84e2c02d923ec6019d988ac78cdf505000000001976a914d0d52a5888a1a0d9effa0f1008159836a739579288ac00e8764817000000070444534843755100000000fd4605030000000000000000000000000000000000000000000000000000000000000000000000000008000000010100010101047f0000014e1d000000000000000000000000000000000000000085f346604370ed1d29fb85d53ed076ae3cda7fa60161e85a090c925bc19b94372a291dea30a45020471b6b918fbb2d132291393ee77a556e9322611f76adf59ceb7e85ece204000800e40b54020000001976a914eb10f3ee3a24dbd8c4a97698504b7ab70478900988ac1976a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac377eacc47e752a2c11a8c5c2ae18bfc74c9f0b0000e40b54020000001976a9148be336fc6c53cba2a0838d64fbdf862b528234fa88ac1976a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac6ca35f8fd14285061a1332f658156d3f802a769a00e40b54020000001976a914c2287b5958f7d2b85c4ce3f9a40de72aebf3a80788ac1976a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac471f168184c486a28e444d3f518a3072f687829e00e40b54020000001976a9147bcee0310a9e92a68684d754b186001ba656969288ac1976a9140a51e08e37fbe9800a1ea53377fe292960933f1088acdf5ab4a1e1a1f300ad401842eb338470935fc30b00e40b54020000001976a9148219a2b691e17eff500ebe4aabca48c18ea5093d88ac1976a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac2ead562303a0cab330625d7449039ddb37fe27c700e40b54020000001976a9140ce89bae9136077cc0a65efd424a81c9bc129d1188ac1976a9140a51e08e37fbe9800a1ea53377fe292960933f1088acede6b5f0579ad6ba06c6603078874088de3ca7fa00e40b54020000001976a91436d82e336c4a18a33cb0adaedc729f89971a1bb688ac1976a9140a51e08e37fbe9800a1ea53377fe292960933f1088acbc85bd9e82c02e4a4d4d5c13d7255d178248b00800ac23fc0600000017a914ec672e1d8e09e79ec04b7a10e9f0800c885eff96871976a9140a51e08e37fbe9800a1ea53377fe292960933f1088acc63c5d2e986f7af7e3d9c8887f19b9e48fb1f7271f2742e50f39e02732618d753cc1851ade81a084b18c580ae8cf47100ed50dbcdc6a44bea87945ca53f4eaa1a3fea69afbe537fa4022a17d3b1bdf80e84f94d7cc201ed7a086a9dd9e1c9755a09ba0dc36d57747d2711f8745d1e9f2c94427cf7a4c6f47580228492829868030d830908209021292b3ecbb94bdd8e6eeb35b0680fa2099143905713d5f483c7947b610af1faa15446304e8d1f561c78e99ff2179bfb82ebafff02d55e19115b6de1958969ca933dcf60b1f8ed4af5c5d46d494dae10120b693c28ee44b1b788eb1507fcc96332e1d066296f005187e8437f49821e3dcf343d1adac75965b2dda91a3847aad11fd1c218cf756305bafa8b344b9877dbec61fba86de73e22c4843fba9e4c31297c644a5a886c32d669e80d5d3ac58beb9999a58730e50c21a102f3a562afa3b1552e67aec6def8345a39821819f57777783082059b8c9a74fc13ebd019556e5f9041467feb0e7e13906cc9cd707b7f26351437c2cac4feacbf4137816d55ed5f8010ad987af9e459b6ab7f9c31caa39aa7c76aa1f831a748c7c75deec670f22c11c6f8e1e969069d379d04df74118da66e88093220e258a921e8646768dde53d698f135f583815f18b06e3e90b9bb00f1a00c6709206e39e5fc054a4fe8150d04a917d2ff63be975db9976d3dc42a269ca255f0a07f115ea54d92e0751df368adc146fda541114716903a88e23b0e6ad6d3eaef6f996400000000ca9a3b000000001cb2f52a24dc86bb316227340756aa46d36c017a1c5e51285afafa2bb547373600", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 0, + "collateralHash": "0000000000000000000000000000000000000000000000000000000000000000", + "collateralIndex": 8, + "addresses": { + "core_p2p": [ + "127.0.0.1:19997" + ] + }, + "votingAddress": "yPUDo3DZEqnX9SrgqbHy4DgtiXBhSyNwxt", + "shares": [ + { + "amount": 10000000000, + "refundAddress": "yhkMxBs9ZuMZnNVz4g9QTScrQQ76EKwZ2x", + "refundScript": "76a914eb10f3ee3a24dbd8c4a97698504b7ab70478900988ac", + "rewardAddress": "yMG1h3fvtJWrhHnATEpRt6Ss6yLz4aJoAb", + "rewardScript": "76a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac", + "ownerAddress": "yRNsmH6CdpsyRq3BUzKJCA7YFqrTEfCqjy" + }, + { + "amount": 10000000000, + "refundAddress": "yZ56y2ZnXuTuKCfF3vApuNM2uSmYsk3Fk2", + "refundScript": "76a9148be336fc6c53cba2a0838d64fbdf862b528234fa88ac", + "rewardAddress": "yMG1h3fvtJWrhHnATEpRt6Ss6yLz4aJoAb", + "rewardScript": "76a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac", + "ownerAddress": "yWDsYJ7Mv6r89m1MLBYYZjgEuFUdhSmRcg" + }, + { + "amount": 10000000000, + "refundAddress": "ye24S9NaQsJRUUz26uxZAeeVbWF19gAcwB", + "refundScript": "76a914c2287b5958f7d2b85c4ce3f9a40de72aebf3a80788ac", + "rewardAddress": "yMG1h3fvtJWrhHnATEpRt6Ss6yLz4aJoAb", + "rewardScript": "76a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac", + "ownerAddress": "ySoW4rCk22ttWJQEmjx7YmPoxSBctdEEUi" + }, + { + "amount": 10000000000, + "refundAddress": "yXc5nn2hWhqwe4df83AmRtBgdYgcpucQom", + "refundScript": "76a9147bcee0310a9e92a68684d754b186001ba656969288ac", + "rewardAddress": "yMG1h3fvtJWrhHnATEpRt6Ss6yLz4aJoAb", + "rewardScript": "76a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac", + "ownerAddress": "yggSCKJstwEqFWS3KrJV6AsXV5gmZpKeAN" + }, + { + "amount": 10000000000, + "refundAddress": "yYBMQLJWNDkzfbcpNPiURvwVgMnP9eVdd3", + "refundScript": "76a9148219a2b691e17eff500ebe4aabca48c18ea5093d88ac", + "rewardAddress": "yMG1h3fvtJWrhHnATEpRt6Ss6yLz4aJoAb", + "rewardScript": "76a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac", + "ownerAddress": "yQaFaf2ZdLohxLbJCvV4RLFQ8qpzutKKYE" + }, + { + "amount": 10000000000, + "refundAddress": "yMVhcQEZdYUT4YQxF5BadT5MN8oT8wm5fz", + "refundScript": "76a9140ce89bae9136077cc0a65efd424a81c9bc129d1188ac", + "rewardAddress": "yMG1h3fvtJWrhHnATEpRt6Ss6yLz4aJoAb", + "rewardScript": "76a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac", + "ownerAddress": "yi1MNj8enWjHJ2xc9P69nvxHCjydcCvuu6" + }, + { + "amount": 10000000000, + "refundAddress": "yRKSK3XigNXQ3LW15j7mNnNADbsddoH5Bo", + "refundScript": "76a91436d82e336c4a18a33cb0adaedc729f89971a1bb688ac", + "rewardAddress": "yMG1h3fvtJWrhHnATEpRt6Ss6yLz4aJoAb", + "rewardScript": "76a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac", + "ownerAddress": "ydWG6jX9BJ6bmdvSHaVxY1TXZvmUXBCKdX" + }, + { + "amount": 30000000000, + "refundAddress": "91ySnRBGBRpetPTTrSLZ4ZteaoXSV42PpK", + "refundScript": "a914ec672e1d8e09e79ec04b7a10e9f0800c885eff9687", + "rewardAddress": "yMG1h3fvtJWrhHnATEpRt6Ss6yLz4aJoAb", + "rewardScript": "76a9140a51e08e37fbe9800a1ea53377fe292960933f1088ac", + "ownerAddress": "yePcxMCvcpibaWLyZfX7M5uVMpDVvqAhWU" + } + ], + "earlyPeriodBlocks": 100, + "earlyPenalty": 1000000000, + "pubKeyOperator": "85f346604370ed1d29fb85d53ed076ae3cda7fa60161e85a090c925bc19b94372a291dea30a45020471b6b918fbb2d13", + "operatorReward": 12.5, + "inputsHash": "363747b52bfafa5a28515e1c7a016cd346aa560734276231bb86dc242af5b21c" + } + }, + { + "name": "v3 shared evonode ProRegTx", + "source": "Core v24.0.0-rc.4 regtest (feature_masternode_shares)", + "txid": "e3eb76f2d5498a7d1c4d2c2f5550c9ad6390ddb663df90b7bcbda63230ded15d", + "hex": "03000100358ebd355180a186ccbbc62d79d460c6aac3722f8738a18df0bfaa08dc6d2abe11000000006a473044022024bf836c2261548ef94f45a642091089d1054c877e0b3d24e3708d7caf356aa6022021472f8690a8c41294665e783148a97990d5a2b3e5d15373d84b4435a448a7f60121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffb6d8b5c5fd4a5048849a4255e0af1d4cf7e49677f5bcda50056861c4a2ab2413000000006a4730440220341d44f428bb3d80c8d55f848b21c49f72c0d98846a9b6800a30358052afcb9a022024858166ba26549ae6fc42143a33b3c361cbee0e3eeb76e53989fb6eb1ed76d50121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff6d5f6802c2e5ac6e0ae51fa577a5e8ffa21e41f25cee443e3db3635766abee17000000006a4730440220707590aeba430e7d1bdad4b87074b3a1cc639e5aeae78ad7bbfe9d9addeac16302205ab23bc0c94608ecb88e9fe6751fe83111b637987d70c805d46e2465068d4c9c0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff568b5bcee19f25cecddeb5d49c535c3963dc392223c6a187f2f6644530daf617000000006a473044022024a17982587fa70bbead7d0d05f48cdda99c334a58873bf06187e00a9482b4af0220029b59cf178d857367ede7bff8323578fd803d9a7a13594b0e7d264b0b5f8ee30121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff5a36113ccf42a7aa2cf9a9e2c91213410a06a8dee881f07de59cdd990bcd9f1b000000006a47304402203fcd2ba46ee506ff0481949a1595810c4070c3d92fb83182fd3f70d9f6fe0ed402204fa6398c1323bd1bba2a118a2066902c9a0e7a5c1cb26ca8195d6225c60d93ae0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffc9e29e8ceda7cb51fbe688b241eb8f27dfd7ea4e7dc1897e4b12588af070b31f000000006a473044022048e719822452c10547b5dd323bac6d492c28b50488c941b7e3ea214e45d7ba67022059737c742ea7344518288b39b108c76d7135a59a3cdef7ec3e2118e5c9567e060121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffca70b6c5594264eeb6c9571ce616199baef0f96b9f23bc08d410a8b2ccbab522000000006a47304402206fa6e2895ae7fe193e0394e3019e70b45b37ca89cc14a0a6a05fd637603a2c5a022048d88f13d96cf6ec1f15121b058b4ba8b1e09886d3260114c81f645d40368d8c0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffdccc91de9a40ecbf331d9a76468480379ff2d35bccda9fed6840bee973fd8132000000006a47304402207e359afdd1cdea443b584f7dae2f595928c002e1658143bd0ef33e9917a166cf02203735fa0df1a8e899a493ee8d606652246dd52de2e7a1d9c59370a8e4b5031ad30121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffffa26cd49a539997432a777c829cc9ecdda083f8a5d2538fae7e8e81d357bf33000000006a47304402207ea8aaf6d7ebb8543bd3f32c127b5c6bc6dd982a0ca1cd288151b8af5a83476c0220701bf5fda725b895fa66f655646b6af463039113e3b69ef3fe3ac8fd840e53d60121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffb909f2f8c5c36561231fb9af4a1cb558ffc8d71c2ef3c149847359cf01e2fd38000000006a47304402207c32ba97113f6811ede840e906a74a1f41a58d7e726014f55b98bd8d46ad6d79022023b329943437a61e9ff211e9e5319a7ee5b84d4cb558873b41c56c6f0731f01e0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff8b499305e5f971a0e5db0b6377e0dcbbe18776210ea91c7a972f7bdfea90bc3d000000006a473044022077667d5cbe660c5dc00ede3cf243d2ed756eb622c0c3bb47a55e8894487168ca02206e4834a1cb6f514f0d8c9bc5e47b2dda75bf17c98d7c3680e6d64bb035f5378c0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffafc5752cc497433cad9b2a942c36b12f337bb73d3ac3dc2977e81e99dfb2c23d000000006a47304402203c05347ae4a13861dc9cae1d882efc4f9db90f960c101a13fac3d9125f13353b02206f63d070a2c0c67e9e8e1498adc09a7f9c78f1edc42b29bef406cc1bac4e0dbc0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff18756f4dd48f01e16acc87810ea9e254649094155c40bfcc24a17875daeacf3d000000006a473044022061f6978fa699b06c49a95980fd6d4957579e7d7d2810a28aa177e5fda9f446ad02205dee68925c20e5a38c5144ad1bc5be67f93fec983926d1e227eccc92f63d628a0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff86a7974c130a485da86d4987a967c41555efdf74790ad66170ae4c4bdaeef13d000000006a4730440220343abd96630bcb1ee00dddd0cbcd6f99e0a5441ea19a23b245bba32e191593df022070db5f505da8de63541e2d752e41c499983466d20823d820841979c0b4f3b7cc0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff72e1d3137af8757700e01104f55c27992fc3cd34e3b5bc1290a36f00bb9e2c41000000006a47304402202563c3b2852906de96df2c111c7ed59beefe3bb38f8ef7d9ea1e165a8d1014b9022029fd1aa49372c71d94dc18e02b0af3b71bdf4dc37968ccd02f8775a50bc2c4300121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff1f5a8e8f213e87053da787faad22a788d3aa5653aa4640ab08757fe2058a1d47000000006a47304402200a91abde7207c3de9ad56174f6e48fcd37baa3f5e25b468a445777040ac8b07502200b3dfa129345ba27dc26941a6031c85dcafdb93a041c58675c9dc829143311c80121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff64d82b3cb18ff80361fcef58380344fd726fb164a0fff73471f6844a1f596d47000000006a4730440220582e62a2ef5fe1750862ecfb30018454ada04c2fe9448a9d4457c203930dc0e00220074829b3a08d90c8ba3250f1fc4a9afc7a1e1af81c63f27b4f046252f99c122e0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff3e406a164899faedc8b9211d8d6e0eb603887d17fd35ff8ee29a2d4e1e93744b000000006a47304402201937ceaa94467f3e7454d4bdd45b2cbb74e3296036c44f15461ab5567dbc8c810220211e29c38469a99fb9e824fa35efe0ba3486ed863ffbf5ba46908651eda06e640121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff0f86bcf62fa1dd7ce13d9896893ed81d86e1c7eea98ef9631b1160875cbbb54e000000006a47304402206363536dad0f61711667721c1506a34d23698eb5b54b18adf04416bce89b9c1102206e153cff11c845c8f83c11460a639dc8119e81910d12012d809fbce6cab41f190121025e1ce353ff60d7c283b5a0b4b60994372b758fb0f27b1f0f3bf0e05989bb3d24feffffffdc129c6b643145443d9de2053baa72fe78eb938623019ad56789397cffc6f550000000006a47304402202c7e2f141466efe08f85c309636aa793f6ab3163f79a636b8de464cd70ebb5c90220387e31e358160e6a9415767663dce30a78a8e8d6afef67be1084e21c3563581f0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff09724bbebdff73c29eab9f561c476fd527c06784472c796845bcdeba0622db59000000006a47304402201ab23a17790f20823e63069ef5ae5e9656ddd7887594a86ebccaa10a669774c102207a02eb33bcfec3b4cb53ae8dc6604d1b85b6734a7170bed8fee8d73822855a800121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffb1f6b3fdccaf72966eaf60e6e4f0b51fc44f500b7c69db07e6c8fa47cacd6a5a010000006a47304402203291f35502e0126a4b0f9ef72a0e5be4cd3fab973d79e514b12f4384ac0ad30a02202ba7c327bdee30f372da13dcd8d0ed00a799c564a08dd18d52f4b95117f5ba90012103dd62a74037cabbf7f36f2b05f14c8fb09b370dd0bdc173b2c468e8b8bac4bf61feffffffdcbf33f655ebcc6c3af32c2b531f502d35fe5700a523acc82219cc83facab05f000000006a4730440220748b23492eff747cb6898c27b8b08a1bd82e02631b90f3e0179401b5612acd42022010b31417bdf590d1180ea5a13d8b143e1c9cb4ee1c78b4efcaf260f5edd85b770121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff3f3705d551c821e42e7ac88d52af9c7d294041a73181d62bd4dc872055aa6065000000006a473044022066d9d0c229bfed3deaf511686823cd3fb0ea3ebd16e9a5724a10fb312082675602206efdb30b55630d623cb153b18e75051ccaf579062ba16f9d94fbbad30a9498e40121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffe1ca182eb979ec48873a1447e5d9e4a8553b7a1219dc766f62b65476a02c4167000000006a4730440220218327fb1f6712e0e9bf1cdef14740c15f9e606ea4e4c7bcec4f597b5d50bc0c0220177274204a9aca07d1b9e49891d7cf3738e47bf1ae64b962eb30fddc300ebc740121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffdd61d3c59457c782f7c2c6c5be7b5641c513f52d1268cb7a692de6f1a24bbe67000000006a47304402200acc60a1244e1479a6ada47c9fcc7cfa7226fe43c165f5437c8d34059e1a59bb0220379d7e05fa4606ec8d0f213b991c05fe0b1c80094a152b692b72168952f27e770121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff428550304cd9c63dd5d40fe543af9ba1ef4e9fe0638423cc7d67718f7c16ed71000000006a47304402203ec97f41fc8ab0caae2dcfc9b82dbd0ff4df3c0c9701a5db92b20723c3a469f4022072a809c2df95772bed676e6543643332b474142f70f067087ecc1d6454e279830121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff9cda6aa6f057e31a2a37d94e1ea41c3233142ac2c59640ed1023214bc8004372000000006a47304402205f8bc68d745a9ed6108191a4b73ce7bcd5ba2a194502909713803a658e17c3bc02205ea725eaa1185fd90c0f1f18051c74f4e322c7352d7d15496a9552ebfa6534d70121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff14ca9023af1b55890b249f89959efc954a1760e9355e8c165f5053405112ca73000000006a473044022057dfc85edf725d8316f295f56c9ec6c3f4f8c92cabd87f73b827760ab814606e022001eee685f2b58ceba6d9e3398d37182118b46acba7f4b9e08ef4f1ec62a3b6730121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff2e5b0d335bd62bf5f4c6f15e2e157862026233a563c37f014c16fc5750127179000000006a4730440220451652f52f632af5c67ae65a4b819ba47723f971b9e202b652255e6c88c7ec890220109eab51c029839ceb9c79cb3bda31ede2d9fee8999c8cfaf1706166e6a5a2e70121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff475d88782295219719f8a9a169bcad69e33cb3491c3dc52ea4dc52b7f5e4a17f000000006a47304402202fd214a8af57a145ad5991a63b4181f98fa2667f20b82e3e18767f3bac4e90c0022028c55650d323d0715db3ae4ea336ca43ccbe01665d34e540361d96af62778a47012102d359c6c6d37ad42bb67ffb16db9f3be4d566550491f09dbae8c7a46c3d66c64ffeffffff8a652de7750423d23deded135251137ebf92a9d7629f3a97bb145cadc3f2e686000000006a4730440220131d829ec8cab00e4b01f34cdcd22b543b74b82924ea31d612daa615a57470e002202030a0bc21e4b1e110799070d951e2a200e19fa7ec180b289e20478312ccf2ca0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0fefffffff08beebc13b752fa3a8c53ee556afe4ef44e979885c5192c6abb55491bb8188a000000006a47304402201410049eb13a4d561c22ba24905450be7a413206e7bafb33e7b543aad116ace0022077faa53ba82118cea7e4381bd33fe99799b8242352f54454aa0beb6d09c8d4010121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff4405cc197b182cbd844140a2e310442dc85b6ddea7bac5e11662382bc4baf98e000000006a473044022059adaf8b87f7e0f95325b9031b0d603c3f1e60480da2040844becbbbb4453ed002205faf7c4d0dbd49534aa94a5cc457a01fabfc3b1598dba24612cae2439e24aa1d0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff9eb7da691127fddcf1a2d614f041b83599b2e68decef4011a3e79ccf8fb11192000000006a473044022040f6b58c790cf76e60b8a3aaf660c43df4b5da1482c83a94548c3fed606bb3ab022000a41285d0bb88f453ad694632b844bc2666fd267e3f74f84f6478c265281a160121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff52f92d1e7adcc69126b5fc966d19fc9921f6c0f477930d4a055f2c70658d1193030000006a4730440220396465bbc6ac5afd9779a84615c3b5c8fa15ac2ba12f32ac53377f4b149d9c070220566d25258b345cc3a4077a029f3dc45532f1b4f0febcab5f0ab012acf7e44cef0121020110cdc1b0a20f393a9b9f59644eaa8a3b16cff27b5b69c63d573bb1d54bd043feffffff7cee7aeae1649c03fb98ea59b494108a88ef484ef6e6318bccefb449b3c8789b000000006a47304402206f4d55775a625c201db59a772439b1c10a1d4e04d83c88b2889efb5a06c555590220430161f374532c392689e62921c2cea4f0c6f98f94d2077a7f9338912cc2c63f0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffc3085012a13ef09dc6ef9792b5358219fdb97297ea96bbde9cad5c9f7ba0259d000000006a473044022077dadd82600e5ebddcb05e4ae03a3461b5cf3d1d64277187f721d4bdf4d76c070220633d11ddea4c4d09dcd94f5aef0db97a1419dded4eb74dee7399b45aba1ca88f0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0fefffffff305281b33acc6293092348685a757776eb1effd8bf603cdec9a29e8280372a0000000006a47304402205eb1ef67d6146205b75b9e2e010cb16a7243b9d5d1268af46dce54eb1b6ded01022057f57534a1ad32022763cffefcb92657eecf3264d2df4d06a975e56afeda9bac0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffe0f97b6474d0b58eada1aa0e7113b071f81902d1f60a4090bae381635a6288a8000000006a473044022029a0f3009b90dbabc3c26280634aa320637cc2e7354638f20d84069f512dfa85022036cba7d0dddccfc0c43f16696b78c825978e1c2ae2b32ec9bb6c75b5c5a5abfd0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff9baf53eb123afb4f19bc7cfc381225f42f7458fb1ca0da8f12d9d898e9665fad000000006a4730440220681a7fc299ee8d2d6803a0e4f81def678b9968700256fe9f22e8bece04200f780220329804d0097997041856259a2d6ac7eb5c71d5629271519d342b04dcad75de2d0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff36dc0220be2dcf2cb785d64a3ff4d04b259d6f4cca669aadf3853972be8a24b9000000006a47304402200e3d5f0f249ebb1a84457dc229afb64c0c7a6cef476f437feec20f9bbf81e4850220716eb68cf1e6b5f65d4c05466a1a130f026b8154613561d59e6f3e125ca1ff580121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff501173c732d20b61953221b0e6141e36e49b12425497715f2cd8bc0e75091dbb000000006a47304402200bb31a6268390e60b298bfffc41aba19be3640c3888f85727c04dda69acac9f802200ed00728960780b81a7ff6c265c06b0c2b302602292dca84c7d4b8e0e560211e0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff881dade44ee629e804b5d53e8958435c2e5c09deaa4b10f1ba672435ca0422bc000000006a47304402203a8be097b265aaff41a10f5ef0ec914ed86c3c059d4b5efb2cd6c1a3dc67716902201dc0d2e103a798ae6d39bdb99cc7053fb8dd615686eeb2f69da4d5ae57463ba50121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff1bd45981f6114a76aff223586e067e7274024666f2b2b6b9ab466c9298fab8bf000000006a473044022031da5422c2a0c783f315771674c94d788dbdb1c3dab1af5c801fa1d0009936e102202569791ad62763f06fc8a42ae18e7e3cbf45ed26ff268ae242e01cdef6e8c0bd0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff8c19b2ff7b23c4a4b1fbdcfa8b41a4f406a0dd4cac829ff4759713d357cd33ca000000006a4730440220071013571c89fa0eba56b2e2869458fa28571b63fa602a9c14ed8993fcb1d50e02206edd511cb507fb45f19f906df9592261d196b903f33e3459a2a174c383ceecd50121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffe001c039618c32181835ec95a2e65555a39224fda82db4e006cc8e7f0097edca000000006a47304402207ad945fe5c6169630507af388423ba125589a21fcff0a0f78836e9ead92443b00220672009171828d30fb21cd601c1c6bfab2f4fa89092bd883e38e33d797e18a5fa0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff746c9e06cacc8b029ecd4f054d8b87f3244cb22c12bb7810a7500a638de031cf000000006a47304402203dceb78ce672419726d6708fd654ba4aa1406c5a283657766154cdb759409f18022068e2bd141f0c9025508e8d9ec0f95073b1875f938dc23a9b2738806682d5b2bd0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff11f3da72c9bc88ad5654b77d0044f6ceac5daf72ce53e769d6e450486ec474d0000000006a473044022001d29c451d97dd7f47b12a261132ea977864c049d801841927aea12e43f5d20102205eb75321f9dd8ec85785bc915456acf71876abdcf994a88cdb6009f572911a630121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff825c8818d76e2a76afb9292b5efa662c474da46fbc37eaf688428e460a9de5d3000000006a4730440220606241fcc1cbea83bb9545b840803459c93f7e08b2f7418baa9afa56fa1cdc530220077cc2a21de27e83b9783b333a7a3f3c84ec15b5303fdf399f3008be1b9a437d0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff74dfdc45b03a25187a575608cd95183f418bdb29c03d6764480933004d451cdb000000006a473044022020ac6cb6d21b621fffe1194febffd1c0dee55ad29992acc729ed707c286090e102200f8c8fbfe587ae914d47ecba60704e223ec91985305d467b0bdbf4f7db7efed20121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff90d687952c54f93b936cb2f4c0a023e92afeaed045d107722cef7e1c7f47a2f8000000006a473044022028932fad2382f02b441dd7a3da04eb8ec608c105508ef7288e1cf0fa311ae4b50220661a9d92ef47946b2755849afe0bded24ca88e8958b74ecb24a0fc2da7c7bff10121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffffd26abc9b6212466648d096e6d8b78c2c9dbea07cde770480276706009257dff9000000006a47304402202af139ff1517c927c66c2f32ed6d7b4de10aec6c1ca22b18c88d304b3007858502203d1babb611e693a2dc872cc925d55860796ef0f6aaeb680616ba0765d846522d0121038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0feffffff02aaa30c00000000001976a914343c9f834cd547fec74c58228f5b7978fd9d552288ac00a0db215d000000070444534843755100000000fdda01030001000000000000000000000000000000000000000000000000000000000000000000000001000000010300010101047f0000014e2001010101047f000001520802010101047f00000155f00000000000000000000000000000000000000000b0492bfd2d80591c9a8a6431e233a4f3d37018a3404167e41165cd318d076c97c5ebfaa76012ee867093462483b09ba7dda6bfb04cf8bea5a71863985ea3fefd0f5f76a60000000200b864d9450000001976a9141a0a27550e570f0ff07916e901a2862b131826fe88ac00c50e76b74fcf5e1740b4f73ae996f73cf9c1236300e87648170000001976a914e49ea30de7d0b1478f1ee627c92cfae77597820a88ac00f9f8d629d15526fc8982debeb4fc42f309d3ae941fd7949fc296ecc5cd427467275493904aa1de71ef2e3606b4548c17f6a27dd1c74b1d4dc590818803216440dc06c0ca2c429f5b0feaef1a792c770d996a54eda41f0ac024f514d1f3873ac8cc661ac0e58656d600c0dbff7b8f97faf4f31ca5065f65356e75467d6008dc7ccec5aa405854db400e2cdab585b21317c5f5ba97c8da1e00000000f2052a010000003e93ba464fc26e8ed5ef19fcf061f3164d44e31a4cb6b29dddc91cb5fb2f2639204e00000000000000000000000000000000000000", + "coreKey": "proRegTx", + "core": { + "version": 3, + "type": 1, + "collateralHash": "0000000000000000000000000000000000000000000000000000000000000000", + "collateralIndex": 1, + "addresses": { + "core_p2p": [ + "127.0.0.1:20000" + ], + "platform_p2p": [ + "127.0.0.1:21000" + ], + "platform_https": [ + "127.0.0.1:22000" + ] + }, + "votingAddress": "ygXRwWK6yny944tSKK5BtwQejEVhi2F2VP", + "shares": [ + { + "amount": 300000000000, + "refundAddress": "yNh8agHACRpTgGrchpShoodpiH8PffGPfi", + "refundScript": "76a9141a0a27550e570f0ff07916e901a2862b131826fe88ac", + "rewardAddress": "yNh8agHACRpTgGrchpShoodpiH8PffGPfi", + "rewardScript": "76a9141a0a27550e570f0ff07916e901a2862b131826fe88ac", + "ownerAddress": "yeHPHx9G9E6BMTYKW9h71Y2jNnyKaoKMz5" + }, + { + "amount": 100000000000, + "refundAddress": "yhAGxE9rbGMyvx1xWDdr7Az22WHaHYoame", + "refundScript": "76a914e49ea30de7d0b1478f1ee627c92cfae77597820a88ac", + "rewardAddress": "yhAGxE9rbGMyvx1xWDdr7Az22WHaHYoame", + "rewardScript": "76a914e49ea30de7d0b1478f1ee627c92cfae77597820a88ac", + "ownerAddress": "yj7BCSK25iaXQNRrBgx3rH1FzjK5Dp1m7x" + } + ], + "earlyPeriodBlocks": 30, + "earlyPenalty": 5000000000, + "pubKeyOperator": "b0492bfd2d80591c9a8a6431e233a4f3d37018a3404167e41165cd318d076c97c5ebfaa76012ee867093462483b09ba7", + "operatorReward": 0, + "platformNodeID": "0000000000000000000000000000000000004e20", + "inputsHash": "39262ffbb51cc9dd9db2b64c1ae3444d16f361f0fc19efd58e6ec24f46ba933e" + } + }, { "name": "v3 evonode ProUpServTx", "source": "Core v24.0.0-rc.3 devnet (rust-dashcore vectors)", diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index 3719f7d7a..e34df6491 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -155,124 +155,252 @@ describe('ProRegTxPayload', function () { expect(ProRegTxPayload.fromJSON(json).shares[0].scriptReward).to.be.equal(''); }); - it('Should require one join signature per share', function () { - var payload = getSharedPayload(); - payload.joinSigs.pop(); - - expect(function () { - payload.toBuffer(); - }).to.throw('Expect one join signature per share'); - }); - - it('Should require 65 byte join signatures', function () { - var payload = getSharedPayload(); - payload.joinSigs[0] = payload.joinSigs[0].slice(2); + function p2pkh(keyID) { + return '76a914' + Buffer.from(keyID, 'hex').reverse().toString('hex') + '88ac'; + } + function expectInvalid(payload, message) { expect(function () { payload.validate(); - }).to.throw('Expect join signatures to be 65 bytes in size'); - }); - - it('Should require p2pkh/p2sh share scripts', function () { - var payload = getSharedPayload(); - payload.shares[1].scriptReward = '6a'; + }).to.throw(message); + } - expect(function () { - payload.validate(); - }).to.throw('Expected share scriptReward to be empty or a p2pkh/p2sh'); + function withPayload(name, change, message) { + var payload = getPayload(name); + change(payload); + expectInvalid(payload, message); + } - payload = getSharedPayload(); - payload.shares[0].scriptRefund = ''; + var TWO_PAYOUTS = 'v3 ProRegTx with two payouts'; + var SHARED = 'v3 shared ProRegTx with an explicit reward script'; + var COIN = 100000000; - expect(function () { - payload.validate(); - }).to.throw('Expected share scriptRefund to be a p2pkh/p2sh'); + it('Should validate every Core vector', function () { + vectors + .filter(function (v) { + return v.coreKey === 'proRegTx'; + }) + .forEach(function (v) { + new Transaction(v.hex).extraPayload.validate(); + }); }); - it('Should require valid share amounts and early period terms', function () { - var payload = getSharedPayload(); - payload.shares[0].amount = -1; + describe('payouts', function () { + it('Should require 1 to 8 payouts', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts = []; + }, 'Expect payouts to be an array of 1 to 8 payouts'); + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts = []; + for (var i = 0; i < 9; i++) { + payload.payouts.push({ + scriptPayout: '76a914' + ('0' + i).repeat(20) + '88ac', + reward: i < 8 ? 1100 : 1200, + }); + } + }, 'Expect payouts to be an array of 1 to 8 payouts'); + }); - expect(function () { - payload.validate(); - }).to.throw('Expect share amount to be an unsigned integer'); + it('Should require rewards from 100 to 10000', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[0].reward = 9901; + payload.payouts[1].reward = 99; + }, 'Expect payout reward to be an integer from 100 to 10000'); + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[0].reward = 10001; + }, 'Expect payout reward to be an integer from 100 to 10000'); + }); - payload = getSharedPayload(); - payload.earlyPeriodBlocks = 0x100000000; + it('Should require rewards adding up to 10000', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[1].reward = 3999; + }, 'Expect payout rewards to add up to 10000'); + }); - expect(function () { - payload.validate(); - }).to.throw('Expect earlyPeriodBlocks to be an uint32'); - }); + it('Should require p2pkh/p2sh payees', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[0].scriptPayout = '6a'; + }, 'Expected payout scriptPayout to be a p2pkh/p2sh'); + }); - it('Should require p2pkh/p2sh payouts with rewards of at most 10000', function () { - var payload = getPayload('v3 ProRegTx with two payouts'); - payload.payouts[0].reward = 10001; + it('Should require unique payees', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[1].scriptPayout = payload.payouts[0].scriptPayout; + }, 'Expect payout scripts to be unique'); + }); - expect(function () { + it('Should reject payees paying the owner or voting key', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[0].scriptPayout = p2pkh(payload.keyIDOwner); + }, 'Expect payouts not to pay the owner or voting key'); + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[1].scriptPayout = p2pkh(payload.keyIDVoting); + }, 'Expect payouts not to pay the owner or voting key'); + }); + + it('Should accept a P2SH payee with the hash of a key', function () { + var payload = getPayload(TWO_PAYOUTS); + payload.payouts[0].scriptPayout = + 'a914' + Buffer.from(payload.keyIDVoting, 'hex').reverse().toString('hex') + '87'; payload.validate(); - }).to.throw('Expect payout reward to be an integer from 0 to 10000'); + }); - payload = getPayload('v3 ProRegTx with two payouts'); - payload.payouts[0].scriptPayout = '6a'; + it('Should require a non-null owner key and no early terms without shares', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.keyIDOwner = '00'.repeat(20); + }, 'Expect a non-null keyIDOwner'); + withPayload(TWO_PAYOUTS, function (payload) { + payload.earlyPeriodBlocks = 1; + }, 'Expect no earlyPeriodBlocks or earlyPenalty without shares'); + withPayload(TWO_PAYOUTS, function (payload) { + payload.earlyPenalty = 1; + }, 'Expect no earlyPeriodBlocks or earlyPenalty without shares'); + }); - expect(function () { - payload.validate(); - }).to.throw('Expected payout scriptPayout to be a p2pkh/p2sh'); + it('Should require a regular or evo type', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.type = 2; + }, 'Expect type to be 0 (regular) or 1 (evo)'); + }); }); - it('Should require payouts unless the masternode is shared', function () { - var payload = getPayload('v3 ProRegTx with two payouts'); - payload.payouts = []; + describe('shares', function () { + it('Should require one 65 byte join signature per share', function () { + withPayload(SHARED, function (payload) { + payload.joinSigs.pop(); + }, 'Expect one join signature per share'); + withPayload(SHARED, function (payload) { + payload.joinSigs[0] = payload.joinSigs[0].slice(2); + }, 'Expect join signatures to be 65 bytes in size'); + }); - expect(function () { - payload.validate(); - }).to.throw('Expect at least one payout'); + it('Should require the collateral in the registration, a null owner key and no payouts', function () { + withPayload(SHARED, function (payload) { + payload.collateralHash = '11'.repeat(32); + }, 'Expect a shared masternode to have its collateral in the registration'); + withPayload(SHARED, function (payload) { + payload.keyIDOwner = '11'.repeat(20); + }, 'Expect a null keyIDOwner for a shared masternode'); + withPayload(SHARED, function (payload) { + payload.payouts = getPayload(TWO_PAYOUTS).payouts; + }, 'Expect no payouts for a shared masternode'); + }); - payload = getSharedPayload(); - payload.payouts = getPayload('v3 ProRegTx with two payouts').payouts; + it('Should require 2 to 8 shares', function () { + withPayload(SHARED, function (payload) { + payload.shares.pop(); + payload.joinSigs.pop(); + }, 'Expect 2 to 8 shares'); + var eightShares = getPayload('v3 shared ProRegTx with eight shares'); + eightShares.validate(); + eightShares.shares.push(eightShares.shares[0]); + eightShares.joinSigs.push(eightShares.joinSigs[0]); + expectInvalid(eightShares, 'Expect 2 to 8 shares'); + }); - expect(function () { - payload.validate(); - }).to.throw('Expect no payouts for a shared masternode'); - }); + it('Should require an early period of at most 420480 blocks', function () { + withPayload(SHARED, function (payload) { + payload.earlyPeriodBlocks = 420481; + }, 'Expect earlyPeriodBlocks to be at most 420480'); + }); - it('Should reject a version 3 payoutAddress', function () { - var json = getPayload('v3 ProRegTx with two payouts').toJSON({ - network: 'testnet', + it('Should require amounts from 100 DASH to the collateral', function () { + withPayload(SHARED, function (payload) { + payload.shares[0].amount = 100 * COIN - 1; + payload.shares[1].amount = 900 * COIN + 1; + }, 'Expect share amount to be from 100 DASH to the masternode collateral'); + withPayload(SHARED, function (payload) { + payload.shares[0].amount = 1000 * COIN + 1; + }, 'Expect share amount to be from 100 DASH to the masternode collateral'); + withPayload(SHARED, function (payload) { + payload.shares[0].amount = -1; + }, 'Expect share amount to be from 100 DASH to the masternode collateral'); }); - json.payoutAddress = json.payouts[0].address; - delete json.payouts; - expect(function () { - ProRegTxPayload.fromJSON(json); - }).to.throw('Expect payouts instead of payoutAddress from version 3'); - }); + it('Should require amounts adding up to the collateral of the node type', function () { + withPayload(SHARED, function (payload) { + payload.shares[1].amount -= 1; + }, 'Expect share amounts to add up to the masternode collateral'); + // 1000 DASH is a regular masternode's collateral, not an evonode's + withPayload(SHARED, function (payload) { + payload.type = 1; + payload.platformNodeID = '11'.repeat(20); + }, 'Expect share amounts to add up to the masternode collateral'); + }); - it('Should require addresses that can be serialized', function () { - var payload = getPayload('v3 evonode ProRegTx'); - payload.addresses = { core_p2p: ['not an address'] }; + it('Should require non-null and unique owner keys', function () { + withPayload(SHARED, function (payload) { + payload.shares[0].keyIDOwner = '00'.repeat(20); + }, 'Expect share keyIDOwner to be a non-null 20 byte key id'); + withPayload(SHARED, function (payload) { + payload.shares[1].keyIDOwner = payload.shares[0].keyIDOwner; + }, 'Expect share keyIDOwner to be unique'); + }); - expect(function () { - payload.validate(); - }).to.throw('Invalid address entry'); + it('Should require unique refund scripts', function () { + withPayload(SHARED, function (payload) { + payload.shares[1].scriptRefund = payload.shares[0].scriptRefund; + }, 'Expect share scriptRefund to be unique'); + }); - payload.addresses = undefined; + it('Should require p2pkh/p2sh share scripts', function () { + withPayload(SHARED, function (payload) { + payload.shares[0].scriptRefund = ''; + }, 'Expected share scriptRefund to be a p2pkh/p2sh'); + withPayload(SHARED, function (payload) { + payload.shares[1].scriptReward = '6a'; + }, 'Expected share scriptReward to be empty or a p2pkh/p2sh'); + }); - expect(function () { + it('Should reject share scripts paying the voting key or a share owner key', function () { + withPayload(SHARED, function (payload) { + payload.shares[0].scriptRefund = p2pkh(payload.keyIDVoting); + }, 'Expect share scripts not to pay the voting key or a share owner key'); + withPayload(SHARED, function (payload) { + payload.shares[1].scriptReward = p2pkh(payload.shares[0].keyIDOwner); + }, 'Expect share scripts not to pay the voting key or a share owner key'); + }); + + it('Should require a penalty below the smallest share, and none without an early period', function () { + withPayload(SHARED, function (payload) { + payload.earlyPenalty = 500 * COIN; + }, 'Expect earlyPenalty to be less than the smallest share'); + withPayload(SHARED, function (payload) { + payload.earlyPeriodBlocks = 0; + }, 'Expect no earlyPenalty without an early period'); + var payload = getPayload(SHARED); + payload.earlyPeriodBlocks = 0; + payload.earlyPenalty = 0; payload.validate(); - }).to.throw('Expected addresses to be an object'); + }); }); - it('Should not write platform ports for a version 3 evonode', function () { - var payload = getPayload('v3 evonode ProRegTx'); - var json = payload.toJSON({ network: 'testnet' }); - json.platformP2PPort = 26656; - json.platformHTTPPort = 443; + describe('JSON fields', function () { + it('Should reject fields version 3 payloads do not have', function () { + var json = getPayload('v3 evonode ProRegTx').toJSON({ network: 'testnet' }); + ['service', 'payoutAddress', 'scriptPayout', 'platformP2PPort', 'platformHTTPPort'].forEach( + function (field) { + var withField = Object.assign({}, json); + withField[field] = field === 'platformP2PPort' || field === 'platformHTTPPort' ? 1 : 'x'; + expect(function () { + ProRegTxPayload.fromJSON(withField); + }).to.throw('Expect no ' + field + ' in a version 3 payload'); + } + ); + }); - expect(ProRegTxPayload.fromJSON(json).toBuffer()).to.be.deep.equal( - payload.toBuffer() - ); + it('Should reject fields version 1 and 2 payloads do not have', function () { + var json = proRegTxFixture.getProRegPayloadJSON(); + ['addresses', 'payouts', 'shares', 'joinSigs', 'earlyPeriodBlocks', 'earlyPenalty'].forEach( + function (field) { + var withField = Object.assign({}, json); + withField[field] = field.startsWith('early') ? 0 : []; + expect(function () { + ProRegTxPayload.fromJSON(withField); + }).to.throw('Expect no ' + field + ' in a version 2 payload'); + } + ); + }); }); }); }); diff --git a/test/transaction/payload/proupregtxpayload.js b/test/transaction/payload/proupregtxpayload.js index 2b9763753..83c766563 100644 --- a/test/transaction/payload/proupregtxpayload.js +++ b/test/transaction/payload/proupregtxpayload.js @@ -115,29 +115,65 @@ describe('ProUpRegTxPayload', function () { expect(new ProUpRegTxPayload().version).to.be.equal(1); }); - it('Should require at least one payout', function () { - var payload = new DashcoreLib.Transaction(vector.hex).extraPayload; + function getPayload() { + return new DashcoreLib.Transaction(vector.hex).extraPayload; + } + + function p2pkh(keyID) { + return '76a914' + Buffer.from(keyID, 'hex').reverse().toString('hex') + '88ac'; + } + + it('Should validate the Core vector', function () { + getPayload().validate(); + }); + + it('Should require valid payouts', function () { + var payload = getPayload(); payload.payouts = []; expect(function () { payload.validate(); - }).to.throw('Expect at least one payout'); + }).to.throw('Expect payouts to be an array of 1 to 8 payouts'); - payload.payouts = undefined; + payload = getPayload(); + payload.payouts[0].reward = 9999; expect(function () { payload.validate(); - }).to.throw('Expect payouts to be an array'); + }).to.throw('Expect payout rewards to add up to 10000'); }); - it('Should reject a version 3 payoutAddress', function () { - var json = new DashcoreLib.Transaction(vector.hex).extraPayload.toJSON({ - network: 'testnet', - }); - json.payoutAddress = json.payouts[0].address; - delete json.payouts; + it('Should reject a payee paying the voting key but not check an owner key', function () { + var payload = getPayload(); + payload.payouts[0].scriptPayout = p2pkh(payload.keyIDVoting); + expect(function () { + payload.validate(); + }).to.throw('Expect payouts not to pay the owner or voting key'); + }); + it('Should require a compact signature', function () { + var payload = getPayload(); + expect(payload.payloadSig).to.have.lengthOf(130); + payload.payloadSig = payload.payloadSig.slice(2); + expect(function () { + payload.validate(); + }).to.throw("payloadSig size doesn't match compact signature size"); + payload.payloadSig = getPayload().payloadSig + '00'; + expect(function () { + payload.validate(); + }).to.throw("payloadSig size doesn't match compact signature size"); + }); + + it('Should reject fields of the other versions', function () { + var json = getPayload().toJSON({ network: 'testnet' }); + json.payoutAddress = json.payouts[0].address; expect(function () { ProUpRegTxPayload.fromJSON(json); - }).to.throw('Expect payouts instead of payoutAddress from version 3'); + }).to.throw('Expect no payoutAddress in a version 3 payload'); + + var legacyJSON = proUpRegTxFixture.getProUpRegPayloadJSON(); + legacyJSON.payouts = []; + expect(function () { + ProUpRegTxPayload.fromJSON(legacyJSON); + }).to.throw('Expect no payouts in a version 1 payload'); }); }); }); diff --git a/test/transaction/payload/proupservtxpayload.js b/test/transaction/payload/proupservtxpayload.js index 8bafb6347..1b15ed8c4 100644 --- a/test/transaction/payload/proupservtxpayload.js +++ b/test/transaction/payload/proupservtxpayload.js @@ -122,16 +122,50 @@ describe('ProTxUpServPayload', function () { describe('version 3 (ExtAddr)', function () { var vectors = require('../../fixtures/core24-protx.json').transactions; - it('Should require addresses that can be serialized', function () { + function getPayload() { var vector = vectors.find(function (v) { return v.name === 'v3 evonode ProUpServTx'; }); - var payload = new DashcoreLib.Transaction(vector.hex).extraPayload; + return new DashcoreLib.Transaction(vector.hex).extraPayload; + } + + it('Should require addresses that can be serialized', function () { + var payload = getPayload(); payload.addresses = { core_p2p: ['1.2.3.4:9999'], unknown: [] }; expect(function () { payload.validate(); }).to.throw('Unknown address purpose unknown'); }); + + it('Should require a 96 byte BLS signature', function () { + var payload = getPayload(); + payload.validate(); + payload.payloadSig = payload.payloadSig.slice(2); + expect(function () { + payload.validate(); + }).to.throw("payloadSig size doesn't match BLS signature size"); + payload.payloadSig = getPayload().payloadSig + '00'; + expect(function () { + payload.validate(); + }).to.throw("payloadSig size doesn't match BLS signature size"); + }); + + it('Should reject fields of the other versions', function () { + var json = getPayload().toJSON({ network: 'testnet' }); + ['service', 'platformP2PPort', 'platformHTTPPort'].forEach(function (field) { + var withField = Object.assign({}, json); + withField[field] = field === 'service' ? '1.2.3.4:9999' : 1; + expect(function () { + ProTxUpServPayload.fromJSON(withField); + }).to.throw('Expect no ' + field + ' in a version 3 payload'); + }); + + var legacyJSON = proUpServTxFixture.getProUpServPayloadJSON(); + legacyJSON.addresses = {}; + expect(function () { + ProTxUpServPayload.fromJSON(legacyJSON); + }).to.throw('Expect no addresses in a version 1 payload'); + }); }); }); From 11a232a65a30477d79ff27c84df49cd1dd6863f6 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 21:36:42 +0000 Subject: [PATCH 06/15] fix: type ProUpServTx platform fields and bound base32 names - Declare the ProUpServTxPayload fields the class sets at runtime: type, platformNodeID, platformP2PPort and platformHTTPPort. - decodeBase32 rejects names longer than a Tor v3 name (56 characters, I2P names have 52) before decoding them. Co-Authored-By: Claude Opus 5.5 --- lib/util/netinfo.js | 5 +++++ test-d/index.test-d.ts | 4 ++++ test/util/netinfo.js | 3 +++ .../transaction/payload/ProUpServTxPayload.d.ts | 16 ++++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index 06aa3e1e9..698b7efcc 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -51,6 +51,8 @@ const IPV6_DISALLOWED_PREFIXES = [ const DOMAIN_MAX_LENGTH = 253; const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'; +// The longest base32 name decoded: a Tor v3 name (I2P names have 52 characters) +const BASE32_MAX_LENGTH = 56; const DOMAIN_REGEX = /^[a-z0-9.-]+$/; const IPV4_REGEX = /^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/; @@ -60,6 +62,9 @@ const IPV4_REGEX = * @return {Buffer} */ function decodeBase32(string) { + if (string.length > BASE32_MAX_LENGTH) { + throw new Error(`Base32 name ${string.slice(0, 16)}... is too long`); + } const bytes = []; let buffer = 0; let bits = 0; diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index 84f21cfe7..bcc277b7b 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -104,6 +104,10 @@ expectAssignable({ address: 'yRyv33x1PzwSTW3B2DV3XXRyr7Z5M declare const proUpServTxPayload: ProUpServTxPayload; expectType(proUpServTxPayload.addresses); expectType(proUpServTxPayload.service); +expectType(proUpServTxPayload.type); +expectType(proUpServTxPayload.platformNodeID); +expectType(proUpServTxPayload.platformP2PPort); +expectType(proUpServTxPayload.platformHTTPPort); declare const proUpRegTxPayload: ProUpRegTxPayload; expectType(proUpRegTxPayload.payouts); diff --git a/test/util/netinfo.js b/test/util/netinfo.js index d7eee95e3..6bc41fd7a 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -76,6 +76,9 @@ describe('netinfo', () => { expect(() => extNetInfoToBuffer({ platform_https: ['Example.com:443'] })).to.throw('Invalid address entry'); expect(() => extNetInfoToBuffer({ core_p2p: ['[2001:db8::1::2]:9999'] })).to.throw('Invalid IPv6'); expect(() => extNetInfoToBuffer({ core_p2p: ['aaaa.onion:9999'] })).to.throw('Invalid Tor v3'); + // longer than any base32 name, rejected before decoding + expect(() => extNetInfoToBuffer({ core_p2p: [`${'a'.repeat(57)}.onion:9999`] })).to.throw('is too long'); + expect(() => extNetInfoToBuffer({ core_p2p: [`${'a'.repeat(10000)}.b32.i2p:0`] })).to.throw('is too long'); // IPv6 forms Core reads as IPv4, Tor v2 or internal addresses ['[::ffff:102:304]:9999', '[fd87:d87e:eb43::1]:9999', '[fd6b:88c0:8724::1]:9999'].forEach((entry) => { expect(() => extNetInfoToBuffer({ core_p2p: [entry] })).to.throw('Invalid IPv6 address'); diff --git a/typings/transaction/payload/ProUpServTxPayload.d.ts b/typings/transaction/payload/ProUpServTxPayload.d.ts index 3d3283a93..a3062a8e3 100644 --- a/typings/transaction/payload/ProUpServTxPayload.d.ts +++ b/typings/transaction/payload/ProUpServTxPayload.d.ts @@ -88,6 +88,10 @@ export class ProUpServTxPayload { * ProUpServTx version number. Currently set to 1. */ version: number; + /** + * Masternode type. From version 2 + */ + type?: number; /** * The hash of the initial ProRegTx */ @@ -108,6 +112,18 @@ export class ProUpServTxPayload { * Payee script (p2pkh/p2sh) */ scriptOperatorPayout?: string; + /** + * Platform Node ID. Evonodes from version 2 + */ + platformNodeID?: string; + /** + * Platform P2P port. Evonodes of version 2 + */ + platformP2PPort?: number; + /** + * Platform HTTP port. Evonodes of version 2 + */ + platformHTTPPort?: number; /** * BLSSig Signature of the hash of the ProUpServTx fields. Signed by the Operator. */ From 1039a6517ea10d085fa7b228eae0b5a033bb232f Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 22:09:25 +0000 Subject: [PATCH 07/15] fix: check ProTx versions and masternode addresses like Core - ProRegTx, ProUpServTx, ProUpRegTx and ProUpRevTx reject versions 0 and above 3, as each class's IsTriviallyValid does (ProUpRevTx's maximum is ExtAddr too). - Version 3 ProRegTx and ProUpServTx addresses get the context-free checks of Core's IsNetInfoTriviallyValid and ExtNetInfo::Validate: core_p2p entries for every node, platform_p2p and platform_https for evonodes and neither for regular nodes, no empty purpose, no duplicate addr:port anywhere or address within a purpose, no unspecified, broadcast or documentation addresses, Core's bad ports (443 allowed for Platform HTTPS, port 0 required for I2P), domains only for Platform HTTPS and Core's domain rules (ValidateDomain and ValidateDomainPort: length, labels, hyphens, dotless names, alphabetic TLD, reserved and privacy TLDs). A ProRegTx may register without addresses, a ProUpServTx may not. Routability, mainnet port rules and node reachability depend on the network and are left to Core. The reader stays as lenient as Core's deserialization. - The reader rejects a CJDNS address without the fc prefix, which Core reads as an invalid entry; it used to come back as IPv6 and re-encode with a different network id. - ipv6ToBuffer rejects a `::` that stands for no zero group. Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/proregtxpayload.js | 11 +- lib/transaction/payload/protxutils.js | 13 ++ lib/transaction/payload/proupregtxpayload.js | 6 +- lib/transaction/payload/prouprevtxpayload.js | 6 +- lib/transaction/payload/proupservtxpayload.js | 11 +- lib/util/netinfo.js | 181 +++++++++++++++++- test/transaction/payload/proregtxpayload.js | 23 +++ test/transaction/payload/proupregtxpayload.js | 10 + test/transaction/payload/prouprevtxpayload.js | 19 ++ .../transaction/payload/proupservtxpayload.js | 30 +++ test/util/netinfo.js | 99 ++++++++++ 11 files changed, 382 insertions(+), 27 deletions(-) diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index 74bdd920b..4450d3ec2 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -476,10 +476,7 @@ ProRegTxPayload.fromJSON = function fromJSON(payloadJson) { * @return {boolean} */ ProRegTxPayload.prototype.validate = function () { - Preconditions.checkArgument( - utils.isUnsignedInteger(this.version), - 'Expect version to be an unsigned integer' - ); + protxUtils.validateVersion(this.version); Preconditions.checkArgumentType( this.collateralIndex, 'number', @@ -490,7 +487,11 @@ ProRegTxPayload.prototype.validate = function () { 'Expect collateralHash to be a hex string representing sha256 hash' ); if (this.version >= PROTX_EXT_ADDR_VERSION) { - netInfo.extNetInfoToBuffer(this.addresses); + // a ProRegTx may register without addresses (CProRegTx::IsTriviallyValid) + netInfo.validateMasternodeAddresses(this.addresses, { + evo: this.type === MASTERNODE_TYPE_HP, + allowEmpty: true, + }); } else if (!ipUtil.isZeroAddress(this.service)) { Preconditions.checkArgument( ipUtil.isIPV4(this.service), diff --git a/lib/transaction/payload/protxutils.js b/lib/transaction/payload/protxutils.js index 95a0ecea2..e868aabe3 100644 --- a/lib/transaction/payload/protxutils.js +++ b/lib/transaction/payload/protxutils.js @@ -1,6 +1,7 @@ const Preconditions = require('../../util/preconditions'); const utils = require('../../util/js'); const Script = require('../../script'); +const { PROTX_EXT_ADDR_VERSION } = require('../../constants'); // Helpers shared by the ProTx payloads: the owner payout list of version 3 // (ExtAddr) ProRegTx and ProUpRegTx payloads, Core's MasternodePayoutShares @@ -147,6 +148,17 @@ function payoutsFromJSON(payoutsJSON) { })); } +/** + * Core rejects ProTx versions 0 and above ProTxVersion::ExtAddr (IsTriviallyValid) + * @param {number} version + */ +function validateVersion(version) { + Preconditions.checkArgument( + utils.isUnsignedInteger(version) && version >= 1 && version <= PROTX_EXT_ADDR_VERSION, + 'Expect version to be an integer from 1 to 3', + ); +} + /** * Rejects JSON fields a payload of this version doesn't have, rather than * silently dropping them @@ -167,6 +179,7 @@ module.exports = { isPayoutScript, isP2PKHOf, checkNoFields, + validateVersion, readPayouts, writePayouts, validatePayouts, diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index 051162bce..affb2d296 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -10,7 +10,6 @@ var Script = require('../../script'); var utils = require('../../util/js'); var protxUtils = require('./protxutils'); -var isUnsignedInteger = utils.isUnsignedInteger; var isHexString = utils.isHexaString; var isSha256HexString = utils.isSha256HexString; @@ -155,10 +154,7 @@ ProUpRegTxPayload.fromJSON = function fromJSON(payloadJSON) { * @return {boolean} */ ProUpRegTxPayload.prototype.validate = function () { - Preconditions.checkArgument( - isUnsignedInteger(this.version), - 'Expected version to be an unsigned integer' - ); + protxUtils.validateVersion(this.version); Preconditions.checkArgument( isHexString(this.pubKeyOperator), diff --git a/lib/transaction/payload/prouprevtxpayload.js b/lib/transaction/payload/prouprevtxpayload.js index 68ebf5751..084c7fa94 100644 --- a/lib/transaction/payload/prouprevtxpayload.js +++ b/lib/transaction/payload/prouprevtxpayload.js @@ -7,6 +7,7 @@ var BufferWriter = require('../../encoding/bufferwriter'); var BufferReader = require('../../encoding/bufferreader'); var AbstractPayload = require('./abstractpayload'); var utils = require('../../util/js'); +var protxUtils = require('./protxutils'); var isUnsignedInteger = utils.isUnsignedInteger; var isHexString = utils.isHexaString; @@ -130,10 +131,7 @@ ProUpRevTxPayload.fromJSON = function fromJSON(payloadJSON) { * @return {boolean} */ ProUpRevTxPayload.prototype.validate = function () { - Preconditions.checkArgument( - isUnsignedInteger(this.version), - 'Expected version to be an unsigned integer' - ); + protxUtils.validateVersion(this.version); Preconditions.checkArgument( isUnsignedInteger(this.reason), diff --git a/lib/transaction/payload/proupservtxpayload.js b/lib/transaction/payload/proupservtxpayload.js index a100334e3..2577cebb4 100644 --- a/lib/transaction/payload/proupservtxpayload.js +++ b/lib/transaction/payload/proupservtxpayload.js @@ -176,10 +176,7 @@ ProUpServTxPayload.fromJSON = function fromJSON(payloadJson) { * @return {boolean} */ ProUpServTxPayload.prototype.validate = function () { - Preconditions.checkArgument( - isUnsignedInteger(this.version), - 'Expect version to be an unsigned integer' - ); + protxUtils.validateVersion(this.version); if (this.version >= 2) { Preconditions.checkArgument( @@ -194,7 +191,11 @@ ProUpServTxPayload.prototype.validate = function () { ); if (this.version >= PROTX_EXT_ADDR_VERSION) { - netInfo.extNetInfoToBuffer(this.addresses); + // Core requires addresses in a ProUpServTx (CProUpServTx::IsTriviallyValid) + netInfo.validateMasternodeAddresses(this.addresses, { + evo: this.type === MASTERNODE_TYPE_HP, + allowEmpty: false, + }); } else if (!ipUtils.isZeroAddress(this.service)) { Preconditions.checkArgument( ipUtils.isIPV4(this.service), diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index 698b7efcc..d8e22afcf 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -30,6 +30,7 @@ const TORV3_CHECKSUM_SIZE = 2; const TORV3_VERSION = 3; const I2P_ADDRESS_SIZE = 32; const CJDNS_ADDRESS_SIZE = 16; +const CJDNS_PREFIX = 0xfc; // Address sizes by BIP155 network id const BIP155_ADDRESS_SIZES = { @@ -196,7 +197,8 @@ function ipv6ToBuffer(address) { const head = parseGroups(halves[0]); const tail = halves.length === 2 ? parseGroups(halves[1]) : []; const missing = 8 - head.length - tail.length; - if (missing < 0 || (halves.length === 1 && missing !== 0)) { + // `::` stands for at least one zero group + if (halves.length === 2 ? missing < 1 : missing !== 0) { throw new Error(`Invalid IPv6 address ${address}`); } const groups = head.concat(new Array(missing).fill('0'), tail); @@ -230,7 +232,7 @@ function parseServiceHost(host) { // Core prints CJDNS addresses like IPv6. Mainnet and testnet reject unroutable // fc00::/8 IPv6, so there it is always CJDNS (regtest may store it as IPv6). return { - networkId: address[0] === 0xfc ? BIP155_CJDNS : BIP155_IPV6, + networkId: address[0] === CJDNS_PREFIX ? BIP155_CJDNS : BIP155_IPV6, address, }; } @@ -254,11 +256,12 @@ function parseServiceHost(host) { } /** - * Serializes one NetInfoEntry from its "addr:port" JSON form - * @param {BufferWriter} writer + * Parses one entry from its "addr:port" JSON form * @param {string} entry + * @return {{host: string, port: number, service: ?{networkId: number, address: Buffer}}} + * service is null for a domain */ -function writeNetInfoEntry(writer, entry) { +function parseEntry(entry) { const separator = entry.lastIndexOf(':'); const host = entry.slice(0, separator); const portString = entry.slice(separator + 1); @@ -268,15 +271,25 @@ function writeNetInfoEntry(writer, entry) { } const service = parseServiceHost(host); + if (!service && (!DOMAIN_REGEX.test(host) || host.length > DOMAIN_MAX_LENGTH)) { + throw new Error(`Invalid address entry ${entry}`); + } + return { host, port, service }; +} + +/** + * Serializes one NetInfoEntry from its "addr:port" JSON form + * @param {BufferWriter} writer + * @param {string} entry + */ +function writeNetInfoEntry(writer, entry) { + const { host, port, service } = parseEntry(entry); if (service) { writer.writeUInt8(ENTRY_TYPE_SERVICE); writer.writeUInt8(service.networkId); writer.writeVarintNum(service.address.length); writer.write(service.address); } else { - if (!DOMAIN_REGEX.test(host) || host.length > DOMAIN_MAX_LENGTH) { - throw new Error(`Invalid address entry ${entry}`); - } const domain = Buffer.from(host, 'ascii'); writer.writeUInt8(ENTRY_TYPE_DOMAIN); writer.writeVarintNum(domain.length); @@ -340,6 +353,10 @@ function serviceToString(networkId, address, port) { } return `[${ipv6ToString(address)}]:${port}`; case BIP155_CJDNS: + // Core reads a CJDNS address without the fc prefix as an invalid entry + if (address[0] !== CJDNS_PREFIX) { + throw new Error(`Invalid CJDNS address ${address.toString('hex')}`); + } return `[${ipv6ToString(address)}]:${port}`; case BIP155_TORV3: return `${encodeBase32(torV3AddressBytes(address))}.onion:${port}`; @@ -431,7 +448,155 @@ function extNetInfoFromBuffer(buffer) { return addresses; } +// The context-free rules of Core's ExtNetInfo::Validate (src/evo/netinfo.cpp) + +// PRIVILEGED_PORTS_THRESHOLD and the other ports of IsBadPort (src/netbase.cpp) +const PRIVILEGED_PORTS_THRESHOLD = 1023; +const BAD_PORTS = [ + 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, + 6669, 6697, 8332, 8333, 10080, 18332, 18333, +]; +// The only bad port Platform HTTPS may use (IsAllowedPlatformHTTPPort) +const PLATFORM_HTTPS_PORT = 443; +// I2P_SAM31_PORT: I2P SAM 3.1 has no ports +const I2P_PORT = 0; +const DOMAIN_MIN_LENGTH = 3; +const DOMAIN_LABEL_MAX_LENGTH = 63; +const TLDS_BAD = [ + '.mail', '.arpa', '.example', '.invalid', '.localhost', '.test', '.local', '.corp', + '.home', '.internal', '.intranet', '.lan', '.private', +]; +const TLDS_PRIVACY = ['.i2p', '.onion']; +const IPV6_DOCUMENTATION_PREFIX = Buffer.from('20010db8', 'hex'); + +/** + * @param {number} port + * @return {boolean} + */ +function isBadPort(port) { + return (port > 0 && port <= PRIVILEGED_PORTS_THRESHOLD) || BAD_PORTS.includes(port); +} + +/** + * Core's DomainPort::ValidateDomain and ExtNetInfo::ValidateDomainPort, less the + * port; the reader stays more lenient, like Core's deserialization + * @param {string} domain + * @return {boolean} + */ +function isValidDomain(domain) { + if (domain.length < DOMAIN_MIN_LENGTH || domain.length > DOMAIN_MAX_LENGTH) { + return false; + } + const labels = domain.split('.'); + return ( + labels.length >= 2 && + labels.every( + (label) => + label.length >= 1 && + label.length <= DOMAIN_LABEL_MAX_LENGTH && + !label.startsWith('-') && + !label.endsWith('-'), + ) && + /^[a-z]+$/.test(labels[labels.length - 1]) && + !TLDS_BAD.concat(TLDS_PRIVACY).some((tld) => domain.endsWith(tld)) + ); +} + +/** + * Core's CNetAddr::IsValid for the addresses parseServiceHost returns + * @param {{networkId: number, address: Buffer}} service + * @return {boolean} + */ +function isValidServiceAddress({ networkId, address }) { + if (networkId === BIP155_IPV4) { + const value = address.readUInt32BE(0); + return value !== 0 && value !== 0xffffffff; + } + if (networkId === BIP155_IPV6) { + return !address.equals(Buffer.alloc(IPV6_ADDRESS_SIZE)) && + address.indexOf(IPV6_DOCUMENTATION_PREFIX) !== 0; + } + return true; +} + +/** + * Checks masternode addresses like Core does for a version 3 ProRegTx or + * ProUpServTx: IsNetInfoTriviallyValid and the context-free parts of + * ExtNetInfo::Validate. Left to Core because they depend on the network or + * the node: routable addresses (RequireRoutableExternalIP), mainnet port rules + * and which networks the node can reach (e.g. -cjdnsreachable). + * @param {Object} addresses + * @param {Object} options + * @param {boolean} options.evo - whether the masternode is an evonode + * @param {boolean} options.allowEmpty - whether an empty address map is allowed + */ +function validateMasternodeAddresses(addresses, { evo, allowEmpty }) { + // throws on anything that can't be serialized + extNetInfoToBuffer(addresses); + + const purposes = Object.keys(addresses); + if (purposes.length === 0) { + if (!allowEmpty) { + throw new Error('Expected addresses to have core_p2p entries'); + } + return; + } + purposes.forEach((purpose) => { + if (addresses[purpose].length === 0) { + throw new Error(`Expected ${purpose} addresses to be non-empty`); + } + }); + if (!addresses.core_p2p) { + throw new Error('Expected addresses to have core_p2p entries'); + } + const hasPlatform = ['platform_p2p', 'platform_https'].map((purpose) => purpose in addresses); + if (evo && !hasPlatform.every(Boolean)) { + throw new Error('Expected evonode addresses to have platform_p2p and platform_https entries'); + } + if (!evo && hasPlatform.some(Boolean)) { + throw new Error('Expected regular masternode addresses to have no platform entries'); + } + + const allEntries = new Set(); + purposes.forEach((purpose) => { + const hosts = new Set(); + addresses[purpose].forEach((entry) => { + if (allEntries.has(entry)) { + throw new Error(`Duplicate address ${entry}`); + } + allEntries.add(entry); + const { host, port, service } = parseEntry(entry); + if (hosts.has(host)) { + throw new Error(`Duplicate ${purpose} address ${host}`); + } + hosts.add(host); + + const isHTTPS = purpose === 'platform_https'; + if (service) { + if (!isValidServiceAddress(service)) { + throw new Error(`Invalid address ${entry}`); + } + } else { + if (!isHTTPS) { + throw new Error(`Expected a domain only in platform_https addresses: ${entry}`); + } + if (!isValidDomain(host)) { + throw new Error(`Invalid domain ${entry}`); + } + } + + const validPort = service && service.networkId === BIP155_I2P + ? port === I2P_PORT + : port !== 0 && (!isBadPort(port) || (isHTTPS && port === PLATFORM_HTTPS_PORT)); + if (!validPort) { + throw new Error(`Invalid port in ${entry}`); + } + }); + }); +} + module.exports = { + validateMasternodeAddresses, extNetInfoToBuffer, extNetInfoFromBuffer, readExtNetInfo, diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index e34df6491..2dc44ca67 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -175,6 +175,27 @@ describe('ProRegTxPayload', function () { var SHARED = 'v3 shared ProRegTx with an explicit reward script'; var COIN = 100000000; + it('Should require a version from 1 to 3', function () { + [0, 4].forEach(function (version) { + withPayload(TWO_PAYOUTS, function (payload) { + payload.version = version; + }, 'Expect version to be an integer from 1 to 3'); + }); + }); + + it('Should allow registering without addresses, but check given ones', function () { + var payload = getPayload(TWO_PAYOUTS); + expect(payload.addresses).to.be.deep.equal({}); + payload.validate(); + + withPayload(TWO_PAYOUTS, function (payload) { + payload.addresses = { core_p2p: ['1.2.3.4:9999'], platform_p2p: ['1.2.3.4:26656'] }; + }, 'Expected regular masternode addresses to have no platform entries'); + withPayload('v3 evonode ProRegTx', function (payload) { + delete payload.addresses.platform_https; + }, 'Expected evonode addresses to have platform_p2p and platform_https entries'); + }); + it('Should validate every Core vector', function () { vectors .filter(function (v) { @@ -325,6 +346,8 @@ describe('ProRegTxPayload', function () { withPayload(SHARED, function (payload) { payload.type = 1; payload.platformNodeID = '11'.repeat(20); + payload.addresses.platform_p2p = ['127.0.0.1:22000']; + payload.addresses.platform_https = ['127.0.0.1:22001']; }, 'Expect share amounts to add up to the masternode collateral'); }); diff --git a/test/transaction/payload/proupregtxpayload.js b/test/transaction/payload/proupregtxpayload.js index 83c766563..f2adc6e5f 100644 --- a/test/transaction/payload/proupregtxpayload.js +++ b/test/transaction/payload/proupregtxpayload.js @@ -127,6 +127,16 @@ describe('ProUpRegTxPayload', function () { getPayload().validate(); }); + it('Should require a version from 1 to 3', function () { + [0, 4].forEach(function (version) { + var payload = getPayload(); + payload.version = version; + expect(function () { + payload.validate(); + }).to.throw('Expect version to be an integer from 1 to 3'); + }); + }); + it('Should require valid payouts', function () { var payload = getPayload(); payload.payouts = []; diff --git a/test/transaction/payload/prouprevtxpayload.js b/test/transaction/payload/prouprevtxpayload.js index 28b09ba3d..3795cd614 100644 --- a/test/transaction/payload/prouprevtxpayload.js +++ b/test/transaction/payload/prouprevtxpayload.js @@ -105,4 +105,23 @@ describe('ProUpRevTxPayload', function () { expect(payload.validate.callCount).to.be.equal(1); }); }); + + describe('#validate', function () { + var vector = require('../../fixtures/core24-protx.json').transactions.find( + function (v) { + return v.name === 'v3 ProUpRevTx'; + } + ); + + it('Should require a version from 1 to 3', function () { + var payload = new DashcoreLib.Transaction(vector.hex).extraPayload; + payload.validate(); + [0, 4].forEach(function (version) { + payload.version = version; + expect(function () { + payload.validate(); + }).to.throw('Expect version to be an integer from 1 to 3'); + }); + }); + }); }); diff --git a/test/transaction/payload/proupservtxpayload.js b/test/transaction/payload/proupservtxpayload.js index 1b15ed8c4..c9adb470a 100644 --- a/test/transaction/payload/proupservtxpayload.js +++ b/test/transaction/payload/proupservtxpayload.js @@ -138,6 +138,36 @@ describe('ProTxUpServPayload', function () { }).to.throw('Unknown address purpose unknown'); }); + it('Should require a version from 1 to 3', function () { + [0, 4].forEach(function (version) { + var payload = getPayload(); + payload.version = version; + expect(function () { + payload.validate(); + }).to.throw('Expect version to be an integer from 1 to 3'); + }); + }); + + it('Should require addresses, with the purposes of the node type', function () { + var payload = getPayload(); + payload.addresses = {}; + expect(function () { + payload.validate(); + }).to.throw('Expected addresses to have core_p2p entries'); + + payload = getPayload(); + payload.type = 0; + expect(function () { + payload.validate(); + }).to.throw('Expected regular masternode addresses to have no platform entries'); + + payload = getPayload(); + delete payload.addresses.platform_p2p; + expect(function () { + payload.validate(); + }).to.throw('Expected evonode addresses to have platform_p2p and platform_https entries'); + }); + it('Should require a 96 byte BLS signature', function () { var payload = getPayload(); payload.validate(); diff --git a/test/util/netinfo.js b/test/util/netinfo.js index 6bc41fd7a..729cc1265 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -4,6 +4,7 @@ const { extNetInfoToBuffer, extNetInfoFromBuffer, readExtNetInfo, + validateMasternodeAddresses, } = require('../../lib/util/netinfo'); // Expected bytes follow Core's ExtNetInfo / NetInfoEntry serialization @@ -75,6 +76,9 @@ describe('netinfo', () => { expect(() => extNetInfoToBuffer({ core_p2p: ['1.2.3.4:0x10'] })).to.throw('Invalid address entry'); expect(() => extNetInfoToBuffer({ platform_https: ['Example.com:443'] })).to.throw('Invalid address entry'); expect(() => extNetInfoToBuffer({ core_p2p: ['[2001:db8::1::2]:9999'] })).to.throw('Invalid IPv6'); + // `::` must stand for at least one zero group + expect(() => extNetInfoToBuffer({ core_p2p: ['[1:2:3:4:5:6:7:8::]:9999'] })).to.throw('Invalid IPv6'); + expect(() => extNetInfoToBuffer({ core_p2p: ['[::1:2:3:4:5:6:7:8]:9999'] })).to.throw('Invalid IPv6'); expect(() => extNetInfoToBuffer({ core_p2p: ['aaaa.onion:9999'] })).to.throw('Invalid Tor v3'); // longer than any base32 name, rejected before decoding expect(() => extNetInfoToBuffer({ core_p2p: [`${'a'.repeat(57)}.onion:9999`] })).to.throw('is too long'); @@ -168,6 +172,12 @@ describe('netinfo', () => { expect(() => extNetInfoFromBuffer(entry('01010501020304'))).to.throw('Invalid BIP155 address of network 1 and size 5'); expect(() => extNetInfoFromBuffer(entry(`010210${'00000000000000000000ffff01020304'}270f`))) .to.throw('Invalid IPv6 address'); + // Core reads CJDNS without the fc prefix as an invalid entry + expect(() => extNetInfoFromBuffer(entry(`010610${'fd000000000000000000000000000001'}270f`))) + .to.throw('Invalid CJDNS address'); + // a domain Core's validation rejects still reads, like Core deserializes it + expect(extNetInfoFromBuffer(extNetInfoToBuffer({ platform_https: ['-a.b:443'] }))) + .to.deep.equal({ platform_https: ['-a.b:443'] }); expect(() => extNetInfoFromBuffer(entry(`0203${Buffer.from('A.b').toString('hex')}01bb`))) .to.throw('Invalid domain'); expect(() => extNetInfoFromBuffer(entry('02fdfe00'))).to.throw('too long'); @@ -175,4 +185,93 @@ describe('netinfo', () => { .to.throw('ExtNetInfo is followed by unexpected data'); }); }); + + describe('validateMasternodeAddresses', () => { + const regular = { core_p2p: ['1.2.3.4:9999'] }; + const evo = { + core_p2p: ['1.2.3.4:9999'], + platform_p2p: ['1.2.3.4:26656'], + platform_https: ['1.2.3.4:443', 'evo.example.org:443'], + }; + const valid = (addresses, options = {}) => validateMasternodeAddresses( + addresses, + { evo: false, allowEmpty: false, ...options }, + ); + const withHTTPS = (entry) => ({ ...evo, platform_https: [entry] }); + + it('should accept the addresses of Core vectors', () => { + valid(regular); + valid(evo, { evo: true }); + valid({ + core_p2p: ['127.0.0.1:19951', 'kpgvmscirrdqpekbqjsvw5teanhatztpp2gl6eee4zkowvwfxwenqaid.onion:19951'], + platform_p2p: ['127.0.0.1:22200', 'c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p:0'], + platform_https: ['127.0.0.1:22201', 'server-1.example.com:22201'], + }, { evo: true }); + }); + + it('should allow an empty map only where Core does', () => { + valid({}, { allowEmpty: true }); + expect(() => valid({})).to.throw('Expected addresses to have core_p2p entries'); + }); + + it('should require the purposes of the node type', () => { + expect(() => valid({ platform_https: ['1.2.3.4:443'] }, { allowEmpty: true })) + .to.throw('Expected addresses to have core_p2p entries'); + expect(() => valid({ core_p2p: [] })).to.throw('Expected core_p2p addresses to be non-empty'); + expect(() => valid(evo)).to.throw('Expected regular masternode addresses to have no platform entries'); + expect(() => valid(regular, { evo: true })) + .to.throw('Expected evonode addresses to have platform_p2p and platform_https entries'); + }); + + it('should reject duplicate addresses', () => { + expect(() => valid({ ...evo, platform_p2p: ['1.2.3.4:9999'] }, { evo: true })) + .to.throw('Duplicate address 1.2.3.4:9999'); + expect(() => valid({ core_p2p: ['1.2.3.4:9999', '1.2.3.4:9998'] })) + .to.throw('Duplicate core_p2p address 1.2.3.4'); + }); + + it('should reject addresses Core considers invalid', () => { + ['0.0.0.0:9999', '255.255.255.255:9999', '[::]:9999', '[2001:db8::1]:9999'].forEach((entry) => { + expect(() => valid({ core_p2p: [entry] })).to.throw(`Invalid address ${entry}`); + }); + }); + + it('should reject bad ports', () => { + expect(() => valid({ core_p2p: ['1.2.3.4:0'] })).to.throw('Invalid port'); + expect(() => valid({ core_p2p: ['1.2.3.4:1023'] })).to.throw('Invalid port'); + expect(() => valid({ core_p2p: ['1.2.3.4:6667'] })).to.throw('Invalid port'); + // 443 is only allowed for Platform HTTPS + expect(() => valid({ core_p2p: ['1.2.3.4:443'] })).to.throw('Invalid port'); + expect(() => valid({ ...evo, platform_https: ['1.2.3.4:80'] }, { evo: true })).to.throw('Invalid port'); + // I2P has no ports + expect(() => valid({ + core_p2p: ['c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p:9999'], + })).to.throw('Invalid port'); + }); + + it('should allow domains only for Platform HTTPS', () => { + expect(() => valid({ core_p2p: ['example.org:9999'] })) + .to.throw('Expected a domain only in platform_https addresses'); + }); + + it('should apply Core\'s domain rules', () => { + const label = 'a'.repeat(63); + valid(withHTTPS(`${label}.${label}.org:443`), { evo: true }); + [ + 'ab:443', // too short and dotless + 'example:443', // dotless + `${'a'.repeat(64)}.org:443`, // label too long + 'a..org:443', // empty label + '.a.org:443', // leading dot + 'a.org.:443', // trailing dot + '-a.org:443', // label starting with a hyphen + 'a-.org:443', // label ending with a hyphen + 'a.org1:443', // non-alphabetic top-level label + 'a.example:443', // reserved TLD + 'a.local:443', // reserved TLD + ].forEach((entry) => { + expect(() => valid(withHTTPS(entry), { evo: true }), entry).to.throw('Invalid domain'); + }); + }); + }); }); From 741409360f6f2367d265e2984107135488071381 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 22:19:14 +0000 Subject: [PATCH 08/15] fix: compare masternode addresses by their bytes when checking duplicates One IPv6 address has several spellings, so string comparison let two entries Core sees as the same address through. Co-Authored-By: Claude Opus 5.5 --- lib/util/netinfo.js | 15 ++++++++++----- test/util/netinfo.js | 7 +++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index d8e22afcf..afe006f3c 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -561,15 +561,20 @@ function validateMasternodeAddresses(addresses, { evo, allowEmpty }) { purposes.forEach((purpose) => { const hosts = new Set(); addresses[purpose].forEach((entry) => { - if (allEntries.has(entry)) { + const { host, port, service } = parseEntry(entry); + // Core compares addresses, and one IPv6 address has several spellings + const hostKey = service + ? `${service.networkId}:${service.address.toString('hex')}` + : host; + const entryKey = `${hostKey}:${port}`; + if (allEntries.has(entryKey)) { throw new Error(`Duplicate address ${entry}`); } - allEntries.add(entry); - const { host, port, service } = parseEntry(entry); - if (hosts.has(host)) { + allEntries.add(entryKey); + if (hosts.has(hostKey)) { throw new Error(`Duplicate ${purpose} address ${host}`); } - hosts.add(host); + hosts.add(hostKey); const isHTTPS = purpose === 'platform_https'; if (service) { diff --git a/test/util/netinfo.js b/test/util/netinfo.js index 729cc1265..b1ae02b85 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -230,6 +230,13 @@ describe('netinfo', () => { .to.throw('Duplicate core_p2p address 1.2.3.4'); }); + it('should reject the same IPv6 address written two ways', () => { + expect(() => valid({ core_p2p: ['[2a01:4f8::1]:9999', '[2A01:4F8:0:0::1]:9999'] })) + .to.throw('Duplicate address [2A01:4F8:0:0::1]:9999'); + expect(() => valid({ core_p2p: ['[2a01:4f8::1]:9999', '[2a01:4f8:0::1]:9998'] })) + .to.throw('Duplicate core_p2p address [2a01:4f8:0::1]'); + }); + it('should reject addresses Core considers invalid', () => { ['0.0.0.0:9999', '255.255.255.255:9999', '[::]:9999', '[2001:db8::1]:9999'].forEach((entry) => { expect(() => valid({ core_p2p: [entry] })).to.throw(`Invalid address ${entry}`); From 6945bf7cb81b616bb237bf87140436e5f060d32c Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 23:20:21 +0000 Subject: [PATCH 09/15] fix: mirror the rest of Core's context-free ProTx checks Went through CProRegTx, CProUpServTx, CProUpRegTx and CProUpRevTx IsTriviallyValid and the helpers they call, and added what validate() still missed, for the versions Core applies it to: - All four parsers reject versions 0 and above 3 right after reading the version, before picking a layout. - ProRegTx, every version: type 0 or 1, no evonode before version 2, mode 0, non-null 20-byte voting key and (unless shared) owner key, 48-byte operator key, 32-byte inputsHash; a version 1/2 payout script is checked as a one-entry payout list, so it must be p2pkh/p2sh and not pay the owner or voting key. - ProUpServTx: type 0 or 1 from version 2, and a service address in every version. - Both, for evonodes (CheckProviderNetworkFields): a non-null 20-byte platformNodeID; version 2 platform ports other than 9999 and different from each other and from the Core port. - ProUpRegTx, every version: mode 0, 48-byte operator key, non-null 20-byte voting key, and the payout list check for the version 1/2 payout script with no owner key, as Core passes. - ProUpRevTx: reason 0 to 3 (REASON_LAST). - Duplicate owner keys, refund scripts and payout scripts are compared as bytes, whatever the hex letter case. Every Core vector, including the pre-V24 ones, still validates. Left to Core: BLS key validity, CNetAddr::IsValid for version 1/2 services, routability, mainnet port rules, deployment rules and anything needing the chain, the masternode list or signatures. Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/proregtxpayload.js | 144 +++++++++--------- lib/transaction/payload/protxutils.js | 65 +++++++- lib/transaction/payload/proupregtxpayload.js | 40 ++--- lib/transaction/payload/prouprevtxpayload.js | 8 +- lib/transaction/payload/proupservtxpayload.js | 39 ++--- test/transaction/payload/proregtxpayload.js | 104 ++++++++++++- test/transaction/payload/proupregtxpayload.js | 45 ++++++ test/transaction/payload/prouprevtxpayload.js | 20 +++ .../transaction/payload/proupservtxpayload.js | 68 +++++++++ 9 files changed, 417 insertions(+), 116 deletions(-) diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index 4450d3ec2..128151097 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -161,6 +161,14 @@ function ProRegTxPayload(options) { ProRegTxPayload.prototype = Object.create(AbstractPayload.prototype); ProRegTxPayload.prototype.constructor = AbstractPayload; +/** + * @param {string} service - "ip:port" + * @return {number} its port, 0 for no address + */ +function servicePort(service) { + return Number(service.slice(service.lastIndexOf(':') + 1)); +} + /** * @param {CollateralShareJSON[]} sharesJSON * @return {CollateralShare[]} @@ -240,19 +248,19 @@ function validateShares(payload) { Preconditions.checkArgument( utils.isHexStringOfSize(share.keyIDOwner, constants.PUBKEY_ID_SIZE * 2) && - !/^0+$/.test(share.keyIDOwner), + !protxUtils.isNull(share.keyIDOwner), 'Expect share keyIDOwner to be a non-null 20 byte key id' ); Preconditions.checkArgument( - !ownerKeys.has(share.keyIDOwner), + !ownerKeys.has(protxUtils.bytesKey(share.keyIDOwner)), 'Expect share keyIDOwner to be unique' ); - ownerKeys.add(share.keyIDOwner); + ownerKeys.add(protxUtils.bytesKey(share.keyIDOwner)); Preconditions.checkArgument( - !refundScripts.has(share.scriptRefund), + !refundScripts.has(protxUtils.bytesKey(share.scriptRefund)), 'Expect share scriptRefund to be unique' ); - refundScripts.add(share.scriptRefund); + refundScripts.add(protxUtils.bytesKey(share.scriptRefund)); Preconditions.checkArgument( protxUtils.isPayoutScript(share.scriptRefund), @@ -295,17 +303,11 @@ function validateShares(payload) { } /** - * The context-free checks Core's CProRegTx::IsTriviallyValid makes on the - * version 3 fields. Left to Core: the operator key scheme, the address rules - * (IsNetInfoTriviallyValid), and everything that needs the chain or the rest - * of the transaction (activation, collateral, signatures and join signatures). + * The context-free checks of Core's CProRegTx::IsTriviallyValid on the + * version 3 fields; validate() makes the ones for every version * @param {ProRegTxPayload} payload */ function validateExtAddrFields(payload) { - Preconditions.checkArgument( - payload.type === MASTERNODE_TYPE_BASIC || payload.type === MASTERNODE_TYPE_HP, - 'Expect type to be 0 (regular) or 1 (evo)' - ); Preconditions.checkArgument( Array.isArray(payload.shares) && Array.isArray(payload.joinSigs), 'Expect shares and joinSigs to be arrays' @@ -330,7 +332,6 @@ function validateExtAddrFields(payload) { 'Expect earlyPenalty to be an unsigned integer' ); - var hasNullOwnerKey = /^0+$/.test(payload.keyIDOwner); if (payload.shares.length > 0) { // the share owners replace the owner key and are paid by share Preconditions.checkArgument( @@ -338,7 +339,7 @@ function validateExtAddrFields(payload) { 'Expect a shared masternode to have its collateral in the registration' ); Preconditions.checkArgument( - hasNullOwnerKey, + protxUtils.isNull(payload.keyIDOwner), 'Expect a null keyIDOwner for a shared masternode' ); Preconditions.checkArgument( @@ -351,10 +352,6 @@ function validateExtAddrFields(payload) { payload.earlyPeriodBlocks === 0 && payload.earlyPenalty === 0, 'Expect no earlyPeriodBlocks or earlyPenalty without shares' ); - Preconditions.checkArgument( - !hasNullOwnerKey, - 'Expect a non-null keyIDOwner' - ); protxUtils.validatePayouts( payload.payouts, payload.keyIDOwner, @@ -375,6 +372,8 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { var payload = new ProRegTxPayload(); payload.version = payloadBufferReader.readUInt16LE(); + // like Core, reject an unknown version before choosing a layout + protxUtils.validateVersion(payload.version); payload.type = payloadBufferReader.readUInt16LE(); payload.mode = payloadBufferReader.readUInt16LE(); // readReverse copies: reversing a read() slice in place would modify rawPayload @@ -472,11 +471,30 @@ ProRegTxPayload.fromJSON = function fromJSON(payloadJson) { /* Instance methods */ /** - * Validate payload + * Validate payload: its structure and the context-free checks of Core's + * CProRegTx::IsTriviallyValid and CheckProviderNetworkFields. Left to Core: + * whether pubKeyOperator is a valid BLS key of the version's scheme, and + * everything that needs the chain, the network or the rest of the transaction: + * deployment rules (legacy versions after V24, evonode shares and payout lists + * before their activation), the collateral, CNetAddr::IsValid for a version 1 + * or 2 service beyond the zero address, routable addresses, mainnet port + * rules, and the payload and join signatures. * @return {boolean} */ ProRegTxPayload.prototype.validate = function () { + var isExtAddr = this.version >= PROTX_EXT_ADDR_VERSION; + var isEvo = this.type === MASTERNODE_TYPE_HP; + protxUtils.validateVersion(this.version); + Preconditions.checkArgument( + this.type === MASTERNODE_TYPE_BASIC || isEvo, + 'Expect type to be 0 (regular) or 1 (evo)' + ); + Preconditions.checkArgument( + !isEvo || this.version >= 2, + 'Expect evonodes to register with version 2 or later' + ); + Preconditions.checkArgument(this.mode === 0, 'Expect mode to be 0'); Preconditions.checkArgumentType( this.collateralIndex, 'number', @@ -486,10 +504,10 @@ ProRegTxPayload.prototype.validate = function () { utils.isSha256HexString(this.collateralHash), 'Expect collateralHash to be a hex string representing sha256 hash' ); - if (this.version >= PROTX_EXT_ADDR_VERSION) { + if (isExtAddr) { // a ProRegTx may register without addresses (CProRegTx::IsTriviallyValid) netInfo.validateMasternodeAddresses(this.addresses, { - evo: this.type === MASTERNODE_TYPE_HP, + evo: isEvo, allowEmpty: true, }); } else if (!ipUtil.isZeroAddress(this.service)) { @@ -499,71 +517,51 @@ ProRegTxPayload.prototype.validate = function () { ); } Preconditions.checkArgument( - utils.isHexaString(this.keyIDOwner), - 'Expect keyIDOwner to be a hex string' - ); - Preconditions.checkArgument( - utils.isHexaString(this.pubKeyOperator), - 'Expect pubKeyOperator to be a hex string' - ); - Preconditions.checkArgument( - utils.isHexaString(this.keyIDVoting), - 'Expect keyIDVoting to be a hex string' - ); - Preconditions.checkArgument( - this.keyIDOwner.length === constants.PUBKEY_ID_SIZE * 2, - 'Expect keyIDOwner to be 20 bytes in size ' - ); - Preconditions.checkArgument( - this.pubKeyOperator.length === constants.BLS_PUBLIC_KEY_SIZE * 2, - 'Expect keyIDOwner to be 48 bytes in size ' + utils.isHexStringOfSize(this.keyIDOwner, constants.PUBKEY_ID_SIZE * 2), + 'Expect keyIDOwner to be 20 bytes in size' ); + // the share owners replace the owner key of a shared masternode + var isShared = Array.isArray(this.shares) && this.shares.length > 0; + if (!isShared) { + protxUtils.validateKeyID(this.keyIDOwner, 'keyIDOwner'); + } Preconditions.checkArgument( - this.keyIDVoting.length === constants.PUBKEY_ID_SIZE * 2, - 'Expect keyIDOwner to be 20 bytes in size ' - ); - Preconditions.checkArgumentType( - this.operatorReward, - 'number', - 'operatorReward' + utils.isHexStringOfSize( + this.pubKeyOperator, + constants.BLS_PUBLIC_KEY_SIZE * 2 + ), + 'Expect pubKeyOperator to be 48 bytes in size' ); + protxUtils.validateKeyID(this.keyIDVoting, 'keyIDVoting'); Preconditions.checkArgument( - this.operatorReward <= 10000, + utils.isUnsignedInteger(this.operatorReward) && + this.operatorReward <= 10000, 'Expect operatorReward to be lesser than or equal 10000' ); Preconditions.checkArgument( - utils.isHexaString(this.inputsHash), - 'Expect inputsHash to be a hex string' + utils.isSha256HexString(this.inputsHash), + 'Expect inputsHash to be a hex string representing sha256 hash' ); - if (this.version >= 2 && this.type === MASTERNODE_TYPE_HP) { - Preconditions.checkArgument( - utils.isHexaString(this.platformNodeID), - 'Expect platformNodeID to be a hex string' - ); - - if (this.version < PROTX_EXT_ADDR_VERSION) { - Preconditions.checkArgument( - utils.isUnsignedInteger(this.platformP2PPort), - 'Expect platformP2PPort to be an integer' - ); - - Preconditions.checkArgument( - utils.isUnsignedInteger(this.platformHTTPPort), - 'Expect platformHTTPPort to be an integer' + if (this.version >= 2 && isEvo) { + protxUtils.validateKeyID(this.platformNodeID, 'platformNodeID'); + if (!isExtAddr) { + protxUtils.validateLegacyPlatformPorts( + this.platformP2PPort, + this.platformHTTPPort, + servicePort(this.service) ); } } - if (this.version >= PROTX_EXT_ADDR_VERSION) { + if (isExtAddr) { validateExtAddrFields(this); - } - - if (this.scriptPayout) { - var script = new Script(this.scriptPayout); - Preconditions.checkArgument( - script.isPublicKeyHashOut() || script.isScriptHashOut(), - 'Expected scriptOperatorPayout to be a p2pkh/p2sh' + } else { + // Core checks the payout script as a one-entry payout list (GetOwnerPayouts) + protxUtils.validatePayouts( + [{ scriptPayout: this.scriptPayout, reward: 10000 }], + this.keyIDOwner, + this.keyIDVoting ); } diff --git a/lib/transaction/payload/protxutils.js b/lib/transaction/payload/protxutils.js index e868aabe3..de815302b 100644 --- a/lib/transaction/payload/protxutils.js +++ b/lib/transaction/payload/protxutils.js @@ -1,7 +1,7 @@ const Preconditions = require('../../util/preconditions'); const utils = require('../../util/js'); const Script = require('../../script'); -const { PROTX_EXT_ADDR_VERSION } = require('../../constants'); +const { PROTX_EXT_ADDR_VERSION, PUBKEY_ID_SIZE } = require('../../constants'); // Helpers shared by the ProTx payloads: the owner payout list of version 3 // (ExtAddr) ProRegTx and ProUpRegTx payloads, Core's MasternodePayoutShares @@ -13,6 +13,8 @@ const MIN_REWARD = 100; const MAX_REWARD = 10000; // IsPayoutListTriviallyValid const MAX_PAYOUTS_COUNT = 8; +// MainParams().GetDefaultPort(), which legacy platform ports may not use on any network +const MAINNET_DEFAULT_PORT = 9999; /** * @typedef {Object} MasternodePayout @@ -38,6 +40,59 @@ function isPayoutScript(script) { return parsed.isPublicKeyHashOut() || parsed.isScriptHashOut(); } +/** + * Hex strings compare as the bytes they encode, whatever their letter case + * @param {string} hex + * @return {string} + */ +function bytesKey(hex) { + return hex.toLowerCase(); +} + +/** + * @param {string} hex + * @return {boolean} whether all the bytes are zero + */ +function isNull(hex) { + return /^0+$/.test(hex); +} + +/** + * Checks a 20 byte id (a key id or platformNodeID) Core requires to be non-null + * @param {string} keyID + * @param {string} name + */ +function validateKeyID(keyID, name) { + Preconditions.checkArgument( + utils.isHexStringOfSize(keyID, PUBKEY_ID_SIZE * 2) && !isNull(keyID), + `Expect ${name} to be a non-null 20 byte key id`, + ); +} + +/** + * The context-free platform port checks of Core's CheckProviderNetworkFields + * for a version 2 evonode; the mainnet-only port rules are left to Core + * @param {number} p2pPort + * @param {number} httpPort + * @param {number} corePort - port of the service address, 0 if none + */ +function validateLegacyPlatformPorts(p2pPort, httpPort, corePort) { + [p2pPort, httpPort].forEach((port) => { + Preconditions.checkArgument( + utils.isUnsignedInteger(port) && port <= 0xffff, + 'Expect platform ports to be 16-bit unsigned integers', + ); + Preconditions.checkArgument( + port !== MAINNET_DEFAULT_PORT, + 'Expect platform ports not to be the mainnet Core port 9999', + ); + }); + Preconditions.checkArgument( + p2pPort !== httpPort && p2pPort !== corePort && httpPort !== corePort, + 'Expect the platform ports and the Core port to differ', + ); +} + /** * @param {string} script - hex * @param {string} keyID - public key hash, in the byte order of the payload JSON @@ -108,10 +163,10 @@ function validatePayouts(payouts, keyIDOwner, keyIDVoting) { 'Expected payout scriptPayout to be a p2pkh/p2sh', ); Preconditions.checkArgument( - !seenScripts.has(payout.scriptPayout), + !seenScripts.has(bytesKey(payout.scriptPayout)), 'Expect payout scripts to be unique', ); - seenScripts.add(payout.scriptPayout); + seenScripts.add(bytesKey(payout.scriptPayout)); Preconditions.checkArgument( !(keyIDOwner && isP2PKHOf(payout.scriptPayout, keyIDOwner)) && !isP2PKHOf(payout.scriptPayout, keyIDVoting), @@ -176,6 +231,10 @@ function checkNoFields(payloadJSON, fields, version) { } module.exports = { + bytesKey, + isNull, + validateKeyID, + validateLegacyPlatformPorts, isPayoutScript, isP2PKHOf, checkNoFields, diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index affb2d296..d96dcfd1f 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -95,6 +95,8 @@ ProUpRegTxPayload.fromBuffer = function (rawPayload) { var payload = new ProUpRegTxPayload(); var signatureSize = 0; payload.version = payloadBufferReader.readUInt16LE(); + // like Core, reject an unknown version before choosing a layout + protxUtils.validateVersion(payload.version); // readReverse copies: reversing a read() slice in place would modify rawPayload payload.proTxHash = payloadBufferReader .readReverse(HASH_SIZE) @@ -150,20 +152,23 @@ ProUpRegTxPayload.fromJSON = function fromJSON(payloadJSON) { /* Instance methods */ /** - * Validates ProUpRegTxPayload payload data + * Validates ProUpRegTxPayload payload data: its structure and the checks of + * Core's CProUpRegTx::IsTriviallyValid. Left to Core: whether pubKeyOperator + * is a valid BLS key of the version's scheme, and everything that needs the + * masternode list (shared masternodes, version changes, duplicate keys) or + * the signature. * @return {boolean} */ ProUpRegTxPayload.prototype.validate = function () { protxUtils.validateVersion(this.version); + // CProUpRegTx::IsTriviallyValid, in every version + Preconditions.checkArgument(this.mode === 0, 'Expect mode to be 0'); Preconditions.checkArgument( - isHexString(this.pubKeyOperator), - 'Expect pubKeyOperator to be a hex string' - ); - Preconditions.checkArgument( - isHexString(this.keyIDVoting), - 'Expect keyIDVoting to be a hex string' + utils.isHexStringOfSize(this.pubKeyOperator, BLSPUBKEY_SIZE * 2), + 'Expect pubKeyOperator to be 48 bytes in size' ); + protxUtils.validateKeyID(this.keyIDVoting, 'keyIDVoting'); Preconditions.checkArgument( isSha256HexString(this.proTxHash), @@ -174,18 +179,15 @@ ProUpRegTxPayload.prototype.validate = function () { 'expected inputsHash to be a sha256 hex string' ); - if (this.version >= PROTX_EXT_ADDR_VERSION) { - // Core passes no owner key for a ProUpRegTx (CProUpRegTx::IsTriviallyValid) - protxUtils.validatePayouts(this.payouts, null, this.keyIDVoting); - } - - if (this.scriptPayout) { - var script = new Script(this.scriptPayout); - Preconditions.checkArgument( - script.isPublicKeyHashOut() || script.isScriptHashOut(), - 'Expected scriptPayout to be a p2pkh/p2sh' - ); - } + // Core passes no owner key for a ProUpRegTx, and checks an older version's + // payout script as a one-entry payout list (GetOwnerPayouts) + protxUtils.validatePayouts( + this.version >= PROTX_EXT_ADDR_VERSION + ? this.payouts + : [{ scriptPayout: this.scriptPayout, reward: 10000 }], + null, + this.keyIDVoting + ); if (Boolean(this.payloadSig)) { Preconditions.checkArgument( diff --git a/lib/transaction/payload/prouprevtxpayload.js b/lib/transaction/payload/prouprevtxpayload.js index 084c7fa94..5c37ca140 100644 --- a/lib/transaction/payload/prouprevtxpayload.js +++ b/lib/transaction/payload/prouprevtxpayload.js @@ -14,6 +14,8 @@ var isHexString = utils.isHexaString; var isSha256HexString = utils.isSha256HexString; var CURRENT_PAYLOAD_VERSION = 1; +// CProUpRevTx::REASON_LAST (REASON_CHANGE_OF_KEYS) +var REASON_LAST = 3; var HASH_SIZE = constants.SHA256_HASH_SIZE; /** @@ -92,6 +94,8 @@ ProUpRevTxPayload.fromBuffer = function (rawPayloadBuffer) { var payloadBufferReader = new BufferReader(rawPayloadBuffer); var payload = new ProUpRevTxPayload(); payload.version = payloadBufferReader.readUInt16LE(); + // like Core, reject an unknown version before choosing a layout + protxUtils.validateVersion(payload.version); // readReverse copies: reversing a read() slice in place would modify rawPayloadBuffer payload.proTxHash = payloadBufferReader .readReverse(HASH_SIZE) @@ -134,8 +138,8 @@ ProUpRevTxPayload.prototype.validate = function () { protxUtils.validateVersion(this.version); Preconditions.checkArgument( - isUnsignedInteger(this.reason), - 'Expected reason to be an unsigned integer' + isUnsignedInteger(this.reason) && this.reason <= REASON_LAST, + 'Expected reason to be an integer from 0 to 3' ); Preconditions.checkArgument( diff --git a/lib/transaction/payload/proupservtxpayload.js b/lib/transaction/payload/proupservtxpayload.js index 2577cebb4..187f61b6e 100644 --- a/lib/transaction/payload/proupservtxpayload.js +++ b/lib/transaction/payload/proupservtxpayload.js @@ -12,7 +12,8 @@ var ipUtils = require('../../util/ip'); var netInfo = require('../../util/netinfo'); var protxUtils = require('./protxutils'); var _ = require('lodash'); -const { MASTERNODE_TYPE_HP, +const { MASTERNODE_TYPE_BASIC, + MASTERNODE_TYPE_HP, PLATFORM_NODE_ID_SIZE, PROTX_EXT_ADDR_VERSION, } = require("../../constants"); @@ -114,6 +115,8 @@ ProUpServTxPayload.fromBuffer = function (rawPayload) { var payload = new ProUpServTxPayload(); payload.version = payloadBufferReader.readUInt16LE(); + // like Core, reject an unknown version before choosing a layout + protxUtils.validateVersion(payload.version); if (payload.version >= 2) { payload.type = payloadBufferReader.readUInt16LE(); @@ -172,16 +175,21 @@ ProUpServTxPayload.fromJSON = function fromJSON(payloadJson) { /* Instance methods */ /** - * Validates payload data + * Validates payload data: its structure and the context-free checks of Core's + * CProUpServTx::IsTriviallyValid and CheckProviderNetworkFields. Left to Core: + * CNetAddr::IsValid for a version 1 or 2 service beyond the zero address, + * routable addresses, mainnet port rules, and everything that needs the + * masternode list or the signature. * @return {boolean} */ ProUpServTxPayload.prototype.validate = function () { protxUtils.validateVersion(this.version); + // version 1 has no type and is always a regular masternode if (this.version >= 2) { Preconditions.checkArgument( - isUnsignedInteger(this.type), - 'Expect type to be an unsigned integer' + this.type === MASTERNODE_TYPE_BASIC || this.type === MASTERNODE_TYPE_HP, + 'Expect type to be 0 (regular) or 1 (evo)' ); } @@ -196,9 +204,10 @@ ProUpServTxPayload.prototype.validate = function () { evo: this.type === MASTERNODE_TYPE_HP, allowEmpty: false, }); - } else if (!ipUtils.isZeroAddress(this.service)) { + } else { + // ...in every version Preconditions.checkArgument( - ipUtils.isIPV4(this.service), + !ipUtils.isZeroAddress(this.service) && ipUtils.isIPV4(this.service), 'Expected service to be a string with ip address and port' ); } @@ -208,21 +217,15 @@ ProUpServTxPayload.prototype.validate = function () { 'Expect inputsHash to be a hex string representing sha256 hash' ); + // CheckProviderNetworkFields; its mainnet-only port rules are left to Core if (this.version >= 2 && this.type === MASTERNODE_TYPE_HP) { - Preconditions.checkArgument( - utils.isHexaString(this.platformNodeID), - 'Expect platformNodeID to be a hex string' - ); + protxUtils.validateKeyID(this.platformNodeID, 'platformNodeID'); if (this.version < PROTX_EXT_ADDR_VERSION) { - Preconditions.checkArgument( - utils.isUnsignedInteger(this.platformP2PPort), - 'Expect platformP2PPort to be an integer' - ); - - Preconditions.checkArgument( - utils.isUnsignedInteger(this.platformHTTPPort), - 'Expect platformHTTPPort to be an integer' + protxUtils.validateLegacyPlatformPorts( + this.platformP2PPort, + this.platformHTTPPort, + Number(this.service.slice(this.service.lastIndexOf(':') + 1)) ); } } diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index 2dc44ca67..d83a96c24 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -206,6 +206,96 @@ describe('ProRegTxPayload', function () { }); }); + describe('Core checks of every version', function () { + var V1 = 'v1 ProRegTx'; + var V2_EVO = 'v2 evonode ProRegTx'; + var V3_EVO = 'v3 evonode ProRegTx'; + + it('Should reject an unknown version while parsing', function () { + var raw = Buffer.from(getPayload(TWO_PAYOUTS).toBuffer()); + [0, 4].forEach(function (version) { + raw.writeUInt16LE(version, 0); + expect(function () { + ProRegTxPayload.fromBuffer(raw); + }).to.throw('Expect version to be an integer from 1 to 3'); + }); + }); + + it('Should require a regular or evo type, and evonodes from version 2', function () { + [V1, V2_EVO, TWO_PAYOUTS].forEach(function (name) { + withPayload(name, function (payload) { + payload.type = 2; + }, 'Expect type to be 0 (regular) or 1 (evo)'); + }); + withPayload(V1, function (payload) { + payload.type = 1; + }, 'Expect evonodes to register with version 2 or later'); + }); + + it('Should require mode 0', function () { + [V1, TWO_PAYOUTS].forEach(function (name) { + withPayload(name, function (payload) { + payload.mode = 1; + }, 'Expect mode to be 0'); + }); + }); + + it('Should require non-null owner and voting keys of 20 bytes', function () { + withPayload(V1, function (payload) { + payload.keyIDOwner = '00'.repeat(20); + }, 'Expect keyIDOwner to be a non-null 20 byte key id'); + [V1, TWO_PAYOUTS, SHARED].forEach(function (name) { + withPayload(name, function (payload) { + payload.keyIDVoting = '00'.repeat(20); + }, 'Expect keyIDVoting to be a non-null 20 byte key id'); + withPayload(name, function (payload) { + payload.keyIDVoting = payload.keyIDVoting.slice(2); + }, 'Expect keyIDVoting to be a non-null 20 byte key id'); + }); + }); + + it('Should require a 48 byte operator key and a 32 byte inputsHash', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.pubKeyOperator = payload.pubKeyOperator.slice(2); + }, 'Expect pubKeyOperator to be 48 bytes in size'); + withPayload(V1, function (payload) { + payload.inputsHash = payload.inputsHash.slice(2); + }, 'Expect inputsHash to be a hex string representing sha256 hash'); + }); + + it('Should require a non-null platformNodeID for evonodes', function () { + [V2_EVO, V3_EVO].forEach(function (name) { + withPayload(name, function (payload) { + payload.platformNodeID = '00'.repeat(20); + }, 'Expect platformNodeID to be a non-null 20 byte key id'); + }); + }); + + it('Should check the platform ports of a version 2 evonode', function () { + withPayload(V2_EVO, function (payload) { + payload.platformP2PPort = 9999; + }, 'Expect platform ports not to be the mainnet Core port 9999'); + withPayload(V2_EVO, function (payload) { + payload.platformHTTPPort = payload.platformP2PPort; + }, 'Expect the platform ports and the Core port to differ'); + withPayload(V2_EVO, function (payload) { + payload.platformHTTPPort = 19999; + }, 'Expect the platform ports and the Core port to differ'); + }); + + it('Should check a version 1 or 2 payout script like a payout list', function () { + withPayload(V1, function (payload) { + payload.scriptPayout = ''; + }, 'Expected payout scriptPayout to be a p2pkh/p2sh'); + withPayload(V1, function (payload) { + payload.scriptPayout = p2pkh(payload.keyIDOwner); + }, 'Expect payouts not to pay the owner or voting key'); + withPayload(V1, function (payload) { + payload.scriptPayout = p2pkh(payload.keyIDVoting); + }, 'Expect payouts not to pay the owner or voting key'); + }); + }); + describe('payouts', function () { it('Should require 1 to 8 payouts', function () { withPayload(TWO_PAYOUTS, function (payload) { @@ -248,6 +338,10 @@ describe('ProRegTxPayload', function () { withPayload(TWO_PAYOUTS, function (payload) { payload.payouts[1].scriptPayout = payload.payouts[0].scriptPayout; }, 'Expect payout scripts to be unique'); + // compared as bytes + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[1].scriptPayout = payload.payouts[0].scriptPayout.toUpperCase(); + }, 'Expect payout scripts to be unique'); }); it('Should reject payees paying the owner or voting key', function () { @@ -269,7 +363,7 @@ describe('ProRegTxPayload', function () { it('Should require a non-null owner key and no early terms without shares', function () { withPayload(TWO_PAYOUTS, function (payload) { payload.keyIDOwner = '00'.repeat(20); - }, 'Expect a non-null keyIDOwner'); + }, 'Expect keyIDOwner to be a non-null 20 byte key id'); withPayload(TWO_PAYOUTS, function (payload) { payload.earlyPeriodBlocks = 1; }, 'Expect no earlyPeriodBlocks or earlyPenalty without shares'); @@ -358,12 +452,20 @@ describe('ProRegTxPayload', function () { withPayload(SHARED, function (payload) { payload.shares[1].keyIDOwner = payload.shares[0].keyIDOwner; }, 'Expect share keyIDOwner to be unique'); + // compared as bytes + withPayload(SHARED, function (payload) { + payload.shares[1].keyIDOwner = payload.shares[0].keyIDOwner.toUpperCase(); + }, 'Expect share keyIDOwner to be unique'); }); it('Should require unique refund scripts', function () { withPayload(SHARED, function (payload) { payload.shares[1].scriptRefund = payload.shares[0].scriptRefund; }, 'Expect share scriptRefund to be unique'); + // compared as bytes + withPayload(SHARED, function (payload) { + payload.shares[1].scriptRefund = payload.shares[0].scriptRefund.toUpperCase(); + }, 'Expect share scriptRefund to be unique'); }); it('Should require p2pkh/p2sh share scripts', function () { diff --git a/test/transaction/payload/proupregtxpayload.js b/test/transaction/payload/proupregtxpayload.js index f2adc6e5f..4f63c3352 100644 --- a/test/transaction/payload/proupregtxpayload.js +++ b/test/transaction/payload/proupregtxpayload.js @@ -159,6 +159,51 @@ describe('ProUpRegTxPayload', function () { }).to.throw('Expect payouts not to pay the owner or voting key'); }); + it('Should reject an unknown version while parsing', function () { + var raw = Buffer.from(getPayload().toBuffer()); + [0, 4].forEach(function (version) { + raw.writeUInt16LE(version, 0); + expect(function () { + ProUpRegTxPayload.fromBuffer(raw); + }).to.throw('Expect version to be an integer from 1 to 3'); + }); + }); + + it('Should require mode 0, a 48 byte operator key and a non-null 20 byte voting key', function () { + var payload = getPayload(); + payload.mode = 1; + expect(function () { + payload.validate(); + }).to.throw('Expect mode to be 0'); + + payload = getPayload(); + payload.pubKeyOperator = payload.pubKeyOperator.slice(2); + expect(function () { + payload.validate(); + }).to.throw('Expect pubKeyOperator to be 48 bytes in size'); + + ['00'.repeat(20), getPayload().keyIDVoting.slice(2)].forEach(function (keyID) { + payload = getPayload(); + payload.keyIDVoting = keyID; + expect(function () { + payload.validate(); + }).to.throw('Expect keyIDVoting to be a non-null 20 byte key id'); + }); + }); + + it('Should check a version 1 or 2 payout script like a payout list', function () { + var payload = ProUpRegTxPayload.fromBuffer(proUpRegTxFixture.getProUpRegPayloadBuffer()); + payload.validate(); + payload.scriptPayout = p2pkh(payload.keyIDVoting); + expect(function () { + payload.validate(); + }).to.throw('Expect payouts not to pay the owner or voting key'); + payload.scriptPayout = ''; + expect(function () { + payload.validate(); + }).to.throw('Expected payout scriptPayout to be a p2pkh/p2sh'); + }); + it('Should require a compact signature', function () { var payload = getPayload(); expect(payload.payloadSig).to.have.lengthOf(130); diff --git a/test/transaction/payload/prouprevtxpayload.js b/test/transaction/payload/prouprevtxpayload.js index 3795cd614..c1be668c2 100644 --- a/test/transaction/payload/prouprevtxpayload.js +++ b/test/transaction/payload/prouprevtxpayload.js @@ -113,6 +113,26 @@ describe('ProUpRevTxPayload', function () { } ); + it('Should reject an unknown version while parsing', function () { + var raw = Buffer.from(new DashcoreLib.Transaction(vector.hex).extraPayload.toBuffer()); + [0, 4].forEach(function (version) { + raw.writeUInt16LE(version, 0); + expect(function () { + ProUpRevTxPayload.fromBuffer(raw); + }).to.throw('Expect version to be an integer from 1 to 3'); + }); + }); + + it('Should require a reason from 0 to 3', function () { + var payload = new DashcoreLib.Transaction(vector.hex).extraPayload; + payload.reason = 3; + payload.validate(); + payload.reason = 4; + expect(function () { + payload.validate(); + }).to.throw('Expected reason to be an integer from 0 to 3'); + }); + it('Should require a version from 1 to 3', function () { var payload = new DashcoreLib.Transaction(vector.hex).extraPayload; payload.validate(); diff --git a/test/transaction/payload/proupservtxpayload.js b/test/transaction/payload/proupservtxpayload.js index c9adb470a..04c8075b9 100644 --- a/test/transaction/payload/proupservtxpayload.js +++ b/test/transaction/payload/proupservtxpayload.js @@ -168,6 +168,32 @@ describe('ProTxUpServPayload', function () { }).to.throw('Expected evonode addresses to have platform_p2p and platform_https entries'); }); + it('Should reject an unknown version while parsing', function () { + var raw = Buffer.from(getPayload().toBuffer()); + [0, 4].forEach(function (version) { + raw.writeUInt16LE(version, 0); + expect(function () { + ProTxUpServPayload.fromBuffer(raw); + }).to.throw('Expect version to be an integer from 1 to 3'); + }); + }); + + it('Should require a regular or evo type', function () { + var payload = getPayload(); + payload.type = 2; + expect(function () { + payload.validate(); + }).to.throw('Expect type to be 0 (regular) or 1 (evo)'); + }); + + it('Should require a non-null platformNodeID for evonodes', function () { + var payload = getPayload(); + payload.platformNodeID = '00'.repeat(20); + expect(function () { + payload.validate(); + }).to.throw('Expect platformNodeID to be a non-null 20 byte key id'); + }); + it('Should require a 96 byte BLS signature', function () { var payload = getPayload(); payload.validate(); @@ -198,4 +224,46 @@ describe('ProTxUpServPayload', function () { }).to.throw('Expect no addresses in a version 1 payload'); }); }); + + describe('versions 1 and 2', function () { + function getV2Evo() { + var json = proUpServTxFixture.getProUpServPayloadJSON(); + json.version = 2; + json.type = 1; + json.platformNodeID = '11'.repeat(20); + json.platformP2PPort = 36656; + json.platformHTTPPort = 1443; + return ProTxUpServPayload.fromJSON(json); + } + + it('Should require a service address', function () { + var json = proUpServTxFixture.getProUpServPayloadJSON(); + json.service = '[0:0:0:0:0:0:0:0]:0'; + expect(function () { + ProTxUpServPayload.fromJSON(json); + }).to.throw('Expected service to be a string with ip address and port'); + }); + + it('Should check the platform fields of a version 2 evonode', function () { + var payload = getV2Evo(); + payload.validate(); + + payload.platformNodeID = '00'.repeat(20); + expect(function () { + payload.validate(); + }).to.throw('Expect platformNodeID to be a non-null 20 byte key id'); + + payload = getV2Evo(); + payload.platformHTTPPort = 9999; + expect(function () { + payload.validate(); + }).to.throw('Expect platform ports not to be the mainnet Core port 9999'); + + payload = getV2Evo(); + payload.platformP2PPort = 19999; + expect(function () { + payload.validate(); + }).to.throw('Expect the platform ports and the Core port to differ'); + }); + }); }); From 5b7f0e0ada95cbd3a7c9fefc4106c2f675edbc00 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 23:32:19 +0000 Subject: [PATCH 10/15] fix: reject non-ASCII domain bytes when reading ExtNetInfo 'ascii' decoding clears the high bit, so byte 0xe1 read as 'a' and the domain re-serialized to different bytes. Co-Authored-By: Claude Opus 5.5 --- lib/util/netinfo.js | 4 +++- test/util/netinfo.js | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index afe006f3c..1f72f9bb6 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -388,7 +388,9 @@ function readNetInfoEntry(reader) { if (size > DOMAIN_MAX_LENGTH) { throw new Error(`Domain of ${size} bytes is too long`); } - const domain = reader.read(size).toString('ascii'); + // latin1 keeps every byte ('ascii' clears the high bit), so DOMAIN_REGEX + // rejects bytes above 0x7f instead of reading a different domain + const domain = reader.read(size).toString('latin1'); if (!DOMAIN_REGEX.test(domain)) { throw new Error(`Invalid domain ${domain}`); } diff --git a/test/util/netinfo.js b/test/util/netinfo.js index b1ae02b85..9eb7ae130 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -133,6 +133,18 @@ describe('netinfo', () => { expect(extNetInfoFromBuffer(Buffer.from('0100', 'hex'))).to.deep.equal({}); }); + it('should reject a domain with a byte above 0x7f', () => { + // platform_https, one domain entry: "ex" + 0xe1 + "mple.com", port 443 + const domain = Buffer.concat([Buffer.from('ex'), Buffer.from([0xe1]), Buffer.from('mple.com')]); + const buffer = Buffer.concat([ + Buffer.from('0101020102', 'hex'), + Buffer.from([domain.length]), + domain, + Buffer.from('01bb', 'hex'), + ]); + expect(() => extNetInfoFromBuffer(buffer)).to.throw('Invalid domain'); + }); + it('should compress IPv6 addresses like Core', () => { // one core_p2p IPv6 service on port 9999 const read = (hex) => extNetInfoFromBuffer( From 2ba48eb587ebbcf03ffbe393926505d1ccbfbde8 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 00:28:22 +0000 Subject: [PATCH 11/15] fix: read ProTx payloads exactly and keep their address bytes - The four ProTx parsers read every field exactly, as Core's deserializer does: fixed fields with readExact/readExactReverse, scripts and signatures with readVarBytes (Core-canonical CompactSize, no short reads). A signature shorter than its declared length used to be accepted from a short slice. ProUpRegTx always reads the signature length, which Core requires. ProRegTx.fromBuffer now validates the payload like the other parsers. - The ExtNetInfo reader reads its purpose and entry counts and address and domain lengths as Core-canonical CompactSizes, and the address and domain bytes exactly. - A version 3 ProRegTx or ProUpServTx parsed from bytes keeps the ExtNetInfo bytes it read and writes them back while its addresses are unchanged. An fc00::/8 address stored as BIP155 IPv6 (valid on regtest) prints like the CJDNS address the writer produces, so writing it from its string changed the txid. Changed addresses, and JSON input, which can't tell the two apart either, are still written from their strings. copy() keeps the bytes. Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/proregtxpayload.js | 53 +++++------ lib/transaction/payload/protxutils.js | 61 +++++++++++- lib/transaction/payload/proupregtxpayload.js | 36 +++----- lib/transaction/payload/prouprevtxpayload.js | 15 +-- lib/transaction/payload/proupservtxpayload.js | 34 ++++--- lib/util/netinfo.js | 14 +-- test/transaction/payload/protxcorevectors.js | 92 +++++++++++++++++++ test/util/netinfo.js | 13 +++ 8 files changed, 238 insertions(+), 80 deletions(-) diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index 128151097..23e612415 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -7,6 +7,10 @@ var constants = require('../../constants'); var Preconditions = require('../../util/preconditions'); var BufferWriter = require('../../encoding/bufferwriter'); var BufferReader = require('../../encoding/bufferreader'); +var strictReader = require('./strictpayloadreader'); +var readExact = strictReader.readExact; +var readExactReverse = strictReader.readExactReverse; +var readVarBytes = strictReader.readVarBytes; var AbstractPayload = require('./abstractpayload'); var Script = require('../../script'); var ipUtil = require('../../util/ip'); @@ -376,27 +380,24 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { protxUtils.validateVersion(payload.version); payload.type = payloadBufferReader.readUInt16LE(); payload.mode = payloadBufferReader.readUInt16LE(); - // readReverse copies: reversing a read() slice in place would modify rawPayload - payload.collateralHash = payloadBufferReader - .readReverse(constants.SHA256_HASH_SIZE) + // exact reads, as Core's deserializer; readExactReverse copies, so the + // caller's buffer isn't reversed in place + payload.collateralHash = readExactReverse(payloadBufferReader, constants.SHA256_HASH_SIZE) .toString('hex'); payload.collateralIndex = payloadBufferReader.readUInt32LE(); if (payload.version >= PROTX_EXT_ADDR_VERSION) { - payload.addresses = netInfo.readExtNetInfo(payloadBufferReader); + protxUtils.readAddresses(payload, payloadBufferReader); } else { payload.service = ipUtil.bufferToIPAndPort( - payloadBufferReader.read(ipUtil.IP_AND_PORT_SIZE) + readExact(payloadBufferReader, ipUtil.IP_AND_PORT_SIZE) ); } - payload.keyIDOwner = payloadBufferReader - .readReverse(constants.PUBKEY_ID_SIZE) + payload.keyIDOwner = readExactReverse(payloadBufferReader, constants.PUBKEY_ID_SIZE) .toString('hex'); - payload.pubKeyOperator = payloadBufferReader - .read(constants.BLS_PUBLIC_KEY_SIZE) + payload.pubKeyOperator = readExact(payloadBufferReader, constants.BLS_PUBLIC_KEY_SIZE) .toString('hex'); - payload.keyIDVoting = payloadBufferReader - .readReverse(constants.PUBKEY_ID_SIZE) + payload.keyIDVoting = readExactReverse(payloadBufferReader, constants.PUBKEY_ID_SIZE) .toString('hex'); payload.operatorReward = payloadBufferReader.readUInt16LE(); @@ -407,10 +408,9 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { for (var i = 0; i < sharesCount; i++) { payload.shares.push({ amount: payloadBufferReader.readUInt64LEBN().toNumber(), - scriptRefund: payloadBufferReader.readVarLengthBuffer().toString('hex'), - scriptReward: payloadBufferReader.readVarLengthBuffer().toString('hex'), - keyIDOwner: payloadBufferReader - .readReverse(constants.PUBKEY_ID_SIZE) + scriptRefund: readVarBytes(payloadBufferReader).toString('hex'), + scriptReward: readVarBytes(payloadBufferReader).toString('hex'), + keyIDOwner: readExactReverse(payloadBufferReader, constants.PUBKEY_ID_SIZE) .toString('hex'), }); } @@ -418,23 +418,19 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { payload.joinSigs = []; for (var j = 0; j < sharesCount; j++) { payload.joinSigs.push( - payloadBufferReader.read(COMPACT_SIGNATURE_SIZE).toString('hex') + readExact(payloadBufferReader, COMPACT_SIGNATURE_SIZE).toString('hex') ); } payload.earlyPeriodBlocks = payloadBufferReader.readUInt32LE(); payload.earlyPenalty = payloadBufferReader.readUInt64LEBN().toNumber(); } else { - var scriptPayoutSize = payloadBufferReader.readVarintNum(); - payload.scriptPayout = payloadBufferReader - .read(scriptPayoutSize) - .toString('hex'); + payload.scriptPayout = readVarBytes(payloadBufferReader).toString('hex'); } - payload.inputsHash = payloadBufferReader - .readReverse(constants.SHA256_HASH_SIZE) + payload.inputsHash = readExactReverse(payloadBufferReader, constants.SHA256_HASH_SIZE) .toString('hex'); if (payload.version >= 2 && payload.type === MASTERNODE_TYPE_HP) { - payload.platformNodeID = payloadBufferReader.readReverse(PLATFORM_NODE_ID_SIZE).toString('hex'); + payload.platformNodeID = readExactReverse(payloadBufferReader, PLATFORM_NODE_ID_SIZE).toString('hex'); // ExtAddr evonodes keep the platform ports in their addresses if (payload.version < PROTX_EXT_ADDR_VERSION) { payload.platformP2PPort = payloadBufferReader.readUInt16LE(); @@ -442,11 +438,10 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { } } - payload.payloadSigSize = payloadBufferReader.readVarintNum(); + var payloadSig = readVarBytes(payloadBufferReader); + payload.payloadSigSize = payloadSig.length; if (payload.payloadSigSize > 0) { - payload.payloadSig = payloadBufferReader - .read(payload.payloadSigSize) - .toString('hex'); + payload.payloadSig = payloadSig.toString('hex'); } if (!payloadBufferReader.finished()) { throw new Error( @@ -454,6 +449,7 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { ); } + payload.validate(); return payload; }; @@ -665,7 +661,7 @@ ProRegTxPayload.prototype.toBuffer = function toBuffer(options) { .writeInt32LE(this.collateralIndex); if (this.version >= PROTX_EXT_ADDR_VERSION) { - payloadBufferWriter.write(netInfo.extNetInfoToBuffer(this.addresses)); + payloadBufferWriter.write(protxUtils.addressesToBuffer(this)); } else { payloadBufferWriter.write(ipUtil.ipAndPortToBuffer(this.service)); } @@ -726,6 +722,7 @@ ProRegTxPayload.prototype.toBuffer = function toBuffer(options) { }; ProRegTxPayload.prototype.copy = function copy() { + // fromBuffer keeps the exact address bytes return ProRegTxPayload.fromBuffer(this.toBuffer()); }; diff --git a/lib/transaction/payload/protxutils.js b/lib/transaction/payload/protxutils.js index de815302b..818efae5c 100644 --- a/lib/transaction/payload/protxutils.js +++ b/lib/transaction/payload/protxutils.js @@ -1,6 +1,9 @@ +const _ = require('lodash'); const Preconditions = require('../../util/preconditions'); const utils = require('../../util/js'); const Script = require('../../script'); +const netInfo = require('../../util/netinfo'); +const { readVarBytes } = require('./strictpayloadreader'); const { PROTX_EXT_ADDR_VERSION, PUBKEY_ID_SIZE } = require('../../constants'); // Helpers shared by the ProTx payloads: the owner payout list of version 3 @@ -114,9 +117,8 @@ function readPayouts(reader) { const payouts = []; const count = reader.readUInt8(); for (let i = 0; i < count; i++) { - const scriptPayoutSize = reader.readVarintNum(); payouts.push({ - scriptPayout: reader.read(scriptPayoutSize).toString('hex'), + scriptPayout: readVarBytes(reader).toString('hex'), reward: reader.readUInt16LE(), }); } @@ -214,6 +216,58 @@ function validateVersion(version) { ); } +/** + * Reads the ExtNetInfo of a version 3 payload into payload.addresses and + * remembers its exact bytes. Some bytes have no string form of their own: an + * fc00::/8 address stored as BIP155 IPv6 (valid on regtest) prints like the + * CJDNS address extNetInfoToBuffer writes for it. Core's JSON can't tell them + * apart either, so payloads built from JSON keep that rule. + * @param {Object} payload + * @param {BufferReader} reader + */ +function readAddresses(payload, reader) { + const start = reader.pos; + // eslint-disable-next-line no-param-reassign + payload.addresses = netInfo.readExtNetInfo(reader); + Object.defineProperty(payload, '_extNetInfo', { + value: { + buffer: Buffer.from(reader.buf.slice(start, reader.pos)), + addresses: _.cloneDeep(payload.addresses), + }, + configurable: true, + writable: true, + }); +} + +/** + * Serializes payload.addresses: the bytes they were read from while they + * haven't changed, otherwise extNetInfoToBuffer's encoding + * @param {Object} payload + * @return {Buffer} + */ +function addressesToBuffer(payload) { + const read = payload._extNetInfo; + if (read && _.isEqual(payload.addresses, read.addresses)) { + return read.buffer; + } + return netInfo.extNetInfoToBuffer(payload.addresses); +} + +/** + * Lets a copy keep the bytes its original's addresses were read from + * @param {Object} from + * @param {Object} to + */ +function copyAddressBytes(from, to) { + if (from._extNetInfo) { + Object.defineProperty(to, '_extNetInfo', { + value: from._extNetInfo, + configurable: true, + writable: true, + }); + } +} + /** * Rejects JSON fields a payload of this version doesn't have, rather than * silently dropping them @@ -231,6 +285,9 @@ function checkNoFields(payloadJSON, fields, version) { } module.exports = { + readAddresses, + addressesToBuffer, + copyAddressBytes, bytesKey, isNull, validateKeyID, diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index d96dcfd1f..ab20b109b 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -5,6 +5,10 @@ var constants = require('../../constants'); var Preconditions = require('../../util/preconditions'); var BufferWriter = require('../../encoding/bufferwriter'); var BufferReader = require('../../encoding/bufferreader'); +var strictReader = require('./strictpayloadreader'); +var readExact = strictReader.readExact; +var readExactReverse = strictReader.readExactReverse; +var readVarBytes = strictReader.readVarBytes; var AbstractPayload = require('./abstractpayload'); var Script = require('../../script'); var utils = require('../../util/js'); @@ -93,41 +97,29 @@ ProUpRegTxPayload.prototype.constructor = AbstractPayload; ProUpRegTxPayload.fromBuffer = function (rawPayload) { var payloadBufferReader = new BufferReader(rawPayload); var payload = new ProUpRegTxPayload(); - var signatureSize = 0; payload.version = payloadBufferReader.readUInt16LE(); // like Core, reject an unknown version before choosing a layout protxUtils.validateVersion(payload.version); - // readReverse copies: reversing a read() slice in place would modify rawPayload - payload.proTxHash = payloadBufferReader - .readReverse(HASH_SIZE) + // exact reads, as Core's deserializer; readExactReverse copies, so the + // caller's buffer isn't reversed in place + payload.proTxHash = readExactReverse(payloadBufferReader, HASH_SIZE) .toString('hex'); payload.mode = payloadBufferReader.readUInt16LE(); - payload.pubKeyOperator = payloadBufferReader - .read(BLSPUBKEY_SIZE) + payload.pubKeyOperator = readExact(payloadBufferReader, BLSPUBKEY_SIZE) .toString('hex'); - payload.keyIDVoting = payloadBufferReader - .readReverse(CKEYID_SIZE) + payload.keyIDVoting = readExactReverse(payloadBufferReader, CKEYID_SIZE) .toString('hex'); if (payload.version >= PROTX_EXT_ADDR_VERSION) { payload.payouts = protxUtils.readPayouts(payloadBufferReader); } else { - var scriptPayoutSize = payloadBufferReader.readVarintNum(); - payload.scriptPayout = payloadBufferReader - .read(scriptPayoutSize) - .toString('hex'); + payload.scriptPayout = readVarBytes(payloadBufferReader).toString('hex'); } - payload.inputsHash = payloadBufferReader - .readReverse(HASH_SIZE) + payload.inputsHash = readExactReverse(payloadBufferReader, HASH_SIZE) .toString('hex'); - if (!payloadBufferReader.finished()) { - signatureSize = payloadBufferReader.readVarintNum(); - } - - if (signatureSize > 0) { - payload.payloadSig = payloadBufferReader - .read(signatureSize) - .toString('hex'); + var payloadSig = readVarBytes(payloadBufferReader); + if (payloadSig.length > 0) { + payload.payloadSig = payloadSig.toString('hex'); } if (!payloadBufferReader.finished()) { diff --git a/lib/transaction/payload/prouprevtxpayload.js b/lib/transaction/payload/prouprevtxpayload.js index 5c37ca140..3948f4250 100644 --- a/lib/transaction/payload/prouprevtxpayload.js +++ b/lib/transaction/payload/prouprevtxpayload.js @@ -5,6 +5,9 @@ var constants = require('../../constants'); var Preconditions = require('../../util/preconditions'); var BufferWriter = require('../../encoding/bufferwriter'); var BufferReader = require('../../encoding/bufferreader'); +var strictReader = require('./strictpayloadreader'); +var readExact = strictReader.readExact; +var readExactReverse = strictReader.readExactReverse; var AbstractPayload = require('./abstractpayload'); var utils = require('../../util/js'); var protxUtils = require('./protxutils'); @@ -96,16 +99,14 @@ ProUpRevTxPayload.fromBuffer = function (rawPayloadBuffer) { payload.version = payloadBufferReader.readUInt16LE(); // like Core, reject an unknown version before choosing a layout protxUtils.validateVersion(payload.version); - // readReverse copies: reversing a read() slice in place would modify rawPayloadBuffer - payload.proTxHash = payloadBufferReader - .readReverse(HASH_SIZE) + // exact reads, as Core's deserializer; readExactReverse copies, so the + // caller's buffer isn't reversed in place + payload.proTxHash = readExactReverse(payloadBufferReader, HASH_SIZE) .toString('hex'); payload.reason = payloadBufferReader.readUInt16LE(); - payload.inputsHash = payloadBufferReader - .readReverse(HASH_SIZE) + payload.inputsHash = readExactReverse(payloadBufferReader, HASH_SIZE) .toString('hex'); - payload.payloadSig = payloadBufferReader - .read(constants.BLS_SIGNATURE_SIZE) + payload.payloadSig = readExact(payloadBufferReader, constants.BLS_SIGNATURE_SIZE) .toString('hex'); if (!payloadBufferReader.finished()) { diff --git a/lib/transaction/payload/proupservtxpayload.js b/lib/transaction/payload/proupservtxpayload.js index 187f61b6e..139dc3cdc 100644 --- a/lib/transaction/payload/proupservtxpayload.js +++ b/lib/transaction/payload/proupservtxpayload.js @@ -5,6 +5,10 @@ var constants = require('../../constants'); var Preconditions = require('../../util/preconditions'); var BufferWriter = require('../../encoding/bufferwriter'); var BufferReader = require('../../encoding/bufferreader'); +var strictReader = require('./strictpayloadreader'); +var readExact = strictReader.readExact; +var readExactReverse = strictReader.readExactReverse; +var readVarBytes = strictReader.readVarBytes; var AbstractPayload = require('./abstractpayload'); var utils = require('../../util/js'); var Script = require('../../script'); @@ -122,28 +126,26 @@ ProUpServTxPayload.fromBuffer = function (rawPayload) { payload.type = payloadBufferReader.readUInt16LE(); } - // readReverse copies: reversing a read() slice in place would modify rawPayload - payload.proTxHash = payloadBufferReader - .readReverse(HASH_SIZE) + // exact reads, as Core's deserializer; readExactReverse copies, so the + // caller's buffer isn't reversed in place + payload.proTxHash = readExactReverse(payloadBufferReader, HASH_SIZE) .toString('hex'); if (payload.version >= PROTX_EXT_ADDR_VERSION) { - payload.addresses = netInfo.readExtNetInfo(payloadBufferReader); + protxUtils.readAddresses(payload, payloadBufferReader); } else { payload.service = ipUtils.bufferToIPAndPort( - payloadBufferReader.read(ipUtils.IP_AND_PORT_SIZE) + readExact(payloadBufferReader, ipUtils.IP_AND_PORT_SIZE) ); } // Note: can be 0 if not updated! - var scriptOperatorPayoutSize = payloadBufferReader.readVarintNum(); - payload.scriptOperatorPayout = payloadBufferReader - .read(scriptOperatorPayoutSize) - .toString('hex'); - payload.inputsHash = payloadBufferReader - .readReverse(HASH_SIZE) + payload.scriptOperatorPayout = readVarBytes(payloadBufferReader).toString( + 'hex' + ); + payload.inputsHash = readExactReverse(payloadBufferReader, HASH_SIZE) .toString('hex'); if (payload.version >= 2 && payload.type === MASTERNODE_TYPE_HP) { - payload.platformNodeID = payloadBufferReader.readReverse(PLATFORM_NODE_ID_SIZE).toString('hex'); + payload.platformNodeID = readExactReverse(payloadBufferReader, PLATFORM_NODE_ID_SIZE).toString('hex'); // ExtAddr evonodes keep the platform ports in their addresses if (payload.version < PROTX_EXT_ADDR_VERSION) { payload.platformP2PPort = payloadBufferReader.readUInt16LE(); @@ -151,7 +153,7 @@ ProUpServTxPayload.fromBuffer = function (rawPayload) { } } - payload.payloadSig = payloadBufferReader.read(BLSSIG_SIZE).toString('hex'); + payload.payloadSig = readExact(payloadBufferReader, BLSSIG_SIZE).toString('hex'); if (!payloadBufferReader.finished()) { throw new Error( @@ -326,7 +328,7 @@ ProUpServTxPayload.prototype.toBuffer = function toBuffer(options) { payloadBufferWriter.write(Buffer.from(this.proTxHash, 'hex').reverse()); if (this.version >= PROTX_EXT_ADDR_VERSION) { - payloadBufferWriter.write(netInfo.extNetInfoToBuffer(this.addresses)); + payloadBufferWriter.write(protxUtils.addressesToBuffer(this)); } else { payloadBufferWriter.write(ipUtils.ipAndPortToBuffer(this.service)); } @@ -363,7 +365,9 @@ ProUpServTxPayload.prototype.toBuffer = function toBuffer(options) { * @return {ProUpServTxPayload} */ ProUpServTxPayload.prototype.copy = function copy() { - return ProUpServTxPayload.fromJSON(this.toJSON()); + var copy = ProUpServTxPayload.fromJSON(this.toJSON()); + protxUtils.copyAddressBytes(this, copy); + return copy; }; module.exports = ProUpServTxPayload; diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index 1f72f9bb6..af7a85842 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -1,6 +1,8 @@ const _ = require('lodash'); const BufferReader = require('../encoding/bufferreader'); const BufferWriter = require('../encoding/bufferwriter'); +// Core-canonical CompactSizes and exact reads, as Core's deserializer +const { readExact, readCompactSize } = require('../transaction/payload/strictpayloadreader'); // Serialization of Dash Core's ExtNetInfo (src/evo/netinfo.h), the address // format of ExtAddr (version 3) masternode entries. @@ -376,21 +378,21 @@ function readNetInfoEntry(reader) { const type = reader.readUInt8(); if (type === ENTRY_TYPE_SERVICE) { const networkId = reader.readUInt8(); - const size = reader.readVarintNum(); + const size = readCompactSize(reader); if (BIP155_ADDRESS_SIZES[networkId] !== size) { throw new Error(`Invalid BIP155 address of network ${networkId} and size ${size}`); } - const address = reader.read(size); + const address = readExact(reader, size); return serviceToString(networkId, address, reader.readUInt16BE()); } if (type === ENTRY_TYPE_DOMAIN) { - const size = reader.readVarintNum(); + const size = readCompactSize(reader); if (size > DOMAIN_MAX_LENGTH) { throw new Error(`Domain of ${size} bytes is too long`); } // latin1 keeps every byte ('ascii' clears the high bit), so DOMAIN_REGEX // rejects bytes above 0x7f instead of reading a different domain - const domain = reader.read(size).toString('latin1'); + const domain = readExact(reader, size).toString('latin1'); if (!DOMAIN_REGEX.test(domain)) { throw new Error(`Invalid domain ${domain}`); } @@ -412,7 +414,7 @@ function readExtNetInfo(reader) { } const addresses = {}; - const purposesCount = reader.readVarintNum(); + const purposesCount = readCompactSize(reader); let previousCode = -1; for (let i = 0; i < purposesCount; i++) { const code = reader.readUInt8(); @@ -423,7 +425,7 @@ function readExtNetInfo(reader) { previousCode = code; const purpose = PURPOSES[code]; - const entriesCount = reader.readVarintNum(); + const entriesCount = readCompactSize(reader); if (entriesCount > MAX_ENTRIES_PER_PURPOSE) { throw new Error(`Too many ${purpose} addresses`); } diff --git a/test/transaction/payload/protxcorevectors.js b/test/transaction/payload/protxcorevectors.js index b3565a0e5..3784acfca 100644 --- a/test/transaction/payload/protxcorevectors.js +++ b/test/transaction/payload/protxcorevectors.js @@ -180,3 +180,95 @@ describe('ProTx payloads produced by Dash Core', () => { }); }); }); + +describe('ProTx payload parsing', () => { + const getVector = (name) => fixtures.transactions.find((v) => v.name === name); + const getRawPayload = (name) => new Transaction(getVector(name).hex).extraPayload.toBuffer(); + const parsers = { + proRegTx: ProRegTxPayload, + proUpServTx: ProTxUpServPayload, + proUpRegTx: ProUpRegTxPayload, + proUpRevTx: ProUpRevTxPayload, + }; + + it('should reject truncated payloads at every length', () => { + [ + 'v1 ProRegTx', + 'v3 shared ProRegTx with an explicit reward script', + 'v3 evonode ProUpServTx with Tor, I2P and domain addresses', + 'v3 ProUpRegTx', + 'v3 ProUpRevTx', + ].forEach((name) => { + const PayloadClass = parsers[getVector(name).coreKey]; + const raw = getRawPayload(name); + for (let length = 0; length < raw.length; length++) { + expect(() => PayloadClass.fromBuffer(raw.slice(0, length)), `${name} cut at ${length}`).to.throw(); + } + }); + }); + + it('should reject a signature shorter than its declared length', () => { + ['v1 ProRegTx', 'v3 ProUpRegTx'].forEach((name) => { + const raw = getRawPayload(name); + // the 65-byte signature is the last field + expect(raw[raw.length - 66]).to.equal(65); + expect(() => parsers[getVector(name).coreKey].fromBuffer(raw.slice(0, -1))) + .to.throw('expected 65 more bytes, found 64'); + }); + const upServ = getRawPayload('v3 evonode ProUpServTx'); + expect(() => ProTxUpServPayload.fromBuffer(upServ.slice(0, -1))) + .to.throw('expected 96 more bytes, found 95'); + }); + + it('should validate a parsed ProRegTx', () => { + const raw = Buffer.from(getRawPayload('v3 ProRegTx with two payouts')); + raw.writeUInt16LE(1, 4); // mode + expect(() => ProRegTxPayload.fromBuffer(raw)).to.throw('Expect mode to be 0'); + }); + + describe('addresses without a string form of their own', () => { + // fc00::1 stored as BIP155 IPv6 (network id 2) instead of CJDNS (6); valid + // on regtest, and printed like the CJDNS address + const IPV6_ENTRY = `0210${'00'.repeat(15)}01`; + const FC00_AS_IPV6_ENTRY = `0210fc${'00'.repeat(14)}01`; + const withFc00AsIPv6 = (name) => { + const hex = getRawPayload(name).toString('hex'); + expect(hex).to.include(IPV6_ENTRY); + return Buffer.from(hex.replace(IPV6_ENTRY, FC00_AS_IPV6_ENTRY), 'hex'); + }; + + [ + ['v3 evonode ProRegTx with IPv4 and IPv6 addresses', ProRegTxPayload], + ['v3 evonode ProUpServTx with IPv6 addresses', ProTxUpServPayload], + ].forEach(([name, PayloadClass]) => { + it(`should re-serialize a ${PayloadClass === ProRegTxPayload ? 'ProRegTx' : 'ProUpServTx'} byte for byte until its addresses change`, () => { + const raw = withFc00AsIPv6(name); + const payload = PayloadClass.fromBuffer(raw); + + expect(payload.addresses.core_p2p).to.include('[fc00::1]:19951'); + expect(payload.toBuffer()).to.deep.equal(raw); + expect(payload.copy().toBuffer()).to.deep.equal(raw); + expect(Object.keys(payload)).to.not.include('_extNetInfo'); + expect(payload.toJSON()).to.not.have.property('_extNetInfo'); + + // equal addresses keep the bytes + payload.addresses = JSON.parse(JSON.stringify(payload.addresses)); + expect(payload.toBuffer()).to.deep.equal(raw); + + // changed addresses are written from their strings, fc00::/8 as CJDNS + payload.addresses.core_p2p = payload.addresses.core_p2p.map( + (entry) => entry.replace('19951', '19952'), + ); + const rewritten = payload.toBuffer().toString('hex'); + expect(rewritten).to.include(`0610fc${'00'.repeat(14)}01`); + expect(rewritten).to.not.include(FC00_AS_IPV6_ENTRY); + }); + }); + + it('should apply the string rule to JSON, which cannot tell the two apart', () => { + const payload = ProTxUpServPayload.fromBuffer(withFc00AsIPv6('v3 evonode ProUpServTx with IPv6 addresses')); + const fromJSON = ProTxUpServPayload.fromJSON(payload.toJSON()); + expect(fromJSON.toBuffer().toString('hex')).to.include(`0610fc${'00'.repeat(14)}01`); + }); + }); +}); diff --git a/test/util/netinfo.js b/test/util/netinfo.js index 9eb7ae130..13a3761bd 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -193,6 +193,19 @@ describe('netinfo', () => { expect(() => extNetInfoFromBuffer(entry(`0203${Buffer.from('A.b').toString('hex')}01bb`))) .to.throw('Invalid domain'); expect(() => extNetInfoFromBuffer(entry('02fdfe00'))).to.throw('too long'); + // CompactSizes must be canonical and data complete, as Core reads them + expect(() => extNetInfoFromBuffer(Buffer.from('01fd0100' + '00' + '00', 'hex'))) + .to.throw('non-canonical CompactSize'); + expect(() => extNetInfoFromBuffer(Buffer.from('0101' + '00' + 'fd0100', 'hex'))) + .to.throw('non-canonical CompactSize'); + expect(() => extNetInfoFromBuffer(entry('0102fd1000' + '00'.repeat(16) + '270f'))) + .to.throw('non-canonical CompactSize'); + expect(() => extNetInfoFromBuffer(entry('02fd0d00' + Buffer.from('example.org').toString('hex')))) + .to.throw('non-canonical CompactSize'); + expect(() => extNetInfoFromBuffer(entry('020b' + Buffer.from('example').toString('hex')))) + .to.throw('expected 11 more bytes, found 7'); + expect(() => extNetInfoFromBuffer(entry('010210' + '00'.repeat(8)))) + .to.throw('expected 16 more bytes, found 8'); expect(() => extNetInfoFromBuffer(Buffer.concat([extNetInfoToBuffer({}), Buffer.from('00', 'hex')]))) .to.throw('ExtNetInfo is followed by unexpected data'); }); From c3f2b270989612a4fa4396042e46b451497717e1 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 01:23:29 +0000 Subject: [PATCH 12/15] fix: default v3 ProTx fields only when they are omitted Explicit null or false values were replaced with defaults before validate() could reject them. Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/proregtxpayload.js | 12 ++++++----- lib/transaction/payload/protxutils.js | 12 +++++++++++ lib/transaction/payload/proupregtxpayload.js | 2 +- test/transaction/payload/proregtxpayload.js | 22 ++++++++++++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index 23e612415..624cb604a 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -139,11 +139,13 @@ function ProRegTxPayload(options) { if (this.version >= PROTX_EXT_ADDR_VERSION) { protxUtils.checkNoFields(options, LEGACY_ONLY_FIELDS, this.version); this.addresses = _.cloneDeep(options.addresses); - this.payouts = protxUtils.payoutsFromJSON(options.payouts || []); - this.shares = sharesFromJSON(options.shares || []); - this.joinSigs = (options.joinSigs || []).slice(); - this.earlyPeriodBlocks = options.earlyPeriodBlocks || 0; - this.earlyPenalty = options.earlyPenalty || 0; + // only an omitted field gets its default; anything supplied is validated + this.payouts = protxUtils.payoutsFromJSON(protxUtils.withDefault(options.payouts, [])); + this.shares = sharesFromJSON(protxUtils.withDefault(options.shares, [])); + var joinSigs = protxUtils.withDefault(options.joinSigs, []); + this.joinSigs = Array.isArray(joinSigs) ? joinSigs.slice() : joinSigs; + this.earlyPeriodBlocks = protxUtils.withDefault(options.earlyPeriodBlocks, 0); + this.earlyPenalty = protxUtils.withDefault(options.earlyPenalty, 0); } else { protxUtils.checkNoFields(options, EXT_ADDR_ONLY_FIELDS, this.version); this.service = options.service; diff --git a/lib/transaction/payload/protxutils.js b/lib/transaction/payload/protxutils.js index 818efae5c..6a48634e7 100644 --- a/lib/transaction/payload/protxutils.js +++ b/lib/transaction/payload/protxutils.js @@ -52,6 +52,17 @@ function bytesKey(hex) { return hex.toLowerCase(); } +/** + * The default for an omitted field; a supplied value, even null, is kept for + * validate() to check + * @param {*} value + * @param {*} fallback + * @return {*} + */ +function withDefault(value, fallback) { + return value === undefined ? fallback : value; +} + /** * @param {string} hex * @return {boolean} whether all the bytes are zero @@ -290,6 +301,7 @@ module.exports = { copyAddressBytes, bytesKey, isNull, + withDefault, validateKeyID, validateLegacyPlatformPorts, isPayoutScript, diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index ab20b109b..295eea79f 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -71,7 +71,7 @@ function ProUpRegTxPayload(payloadJSON) { ['payoutAddress', 'scriptPayout'], this.version ); - this.payouts = protxUtils.payoutsFromJSON(payloadJSON.payouts || []); + this.payouts = protxUtils.payoutsFromJSON(protxUtils.withDefault(payloadJSON.payouts, [])); } else { protxUtils.checkNoFields(payloadJSON, ['payouts'], this.version); this.scriptPayout = Script.fromAddress(payloadJSON.payoutAddress).toHex(); diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index d83a96c24..efc9bfdc8 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -175,6 +175,28 @@ describe('ProRegTxPayload', function () { var SHARED = 'v3 shared ProRegTx with an explicit reward script'; var COIN = 100000000; + it('Should keep supplied v3 fields for validation instead of defaulting them', function () { + var json = getPayload(TWO_PAYOUTS).toJSON({ network: 'testnet' }); + [ + ['shares', null, 'Expect shares to be an array'], + ['joinSigs', null, 'Expect shares and joinSigs to be arrays'], + ['earlyPenalty', null, 'Expect earlyPenalty to be an unsigned integer'], + ['earlyPeriodBlocks', false, 'Expect earlyPeriodBlocks to be an uint32'], + ].forEach(function (testCase) { + var changed = Object.assign({}, json, { [testCase[0]]: testCase[1] }); + expect(function () { + ProRegTxPayload.fromJSON(changed).validate(); + }).to.throw(testCase[2]); + }); + var omitted = Object.assign({}, json); + delete omitted.shares; + delete omitted.joinSigs; + delete omitted.earlyPeriodBlocks; + delete omitted.earlyPenalty; + expect(ProRegTxPayload.fromJSON(omitted).toBuffer()) + .to.deep.equal(getPayload(TWO_PAYOUTS).toBuffer()); + }); + it('Should require a version from 1 to 3', function () { [0, 4].forEach(function (version) { withPayload(TWO_PAYOUTS, function (payload) { From f59c39f51ff9a0a448a313c4e5c0e569d3b37507 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 03:12:01 +0000 Subject: [PATCH 13/15] fix: accept only Core's exact P2PKH form for ProTx payout scripts A PUSHDATA1 form of the same key hash passed isPublicKeyHashOut() but Core's IsPayToPublicKeyHash rejects it. Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/protxutils.js | 6 ++++-- test/transaction/payload/proregtxpayload.js | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/transaction/payload/protxutils.js b/lib/transaction/payload/protxutils.js index 6a48634e7..c061b6ea1 100644 --- a/lib/transaction/payload/protxutils.js +++ b/lib/transaction/payload/protxutils.js @@ -33,14 +33,16 @@ const MAINNET_DEFAULT_PORT = 9999; /** * @param {string} script - hex - * @return {boolean} whether the script is p2pkh or p2sh + * @return {boolean} whether the script is p2pkh or p2sh in Core's exact form */ function isPayoutScript(script) { if (!utils.isHexaString(script)) { return false; } const parsed = new Script(script); - return parsed.isPublicKeyHashOut() || parsed.isScriptHashOut(); + // Core's IsPayToPublicKeyHash takes only the 25-byte template, not a + // PUSHDATA1 form of the same key hash; isScriptHashOut() is already exact + return (parsed.isPublicKeyHashOut() && script.length === 25 * 2) || parsed.isScriptHashOut(); } /** diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index efc9bfdc8..3e190eff5 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -356,6 +356,15 @@ describe('ProRegTxPayload', function () { }, 'Expected payout scriptPayout to be a p2pkh/p2sh'); }); + it('Should reject a p2pkh payee that pushes the key hash with PUSHDATA1', function () { + withPayload(TWO_PAYOUTS, function (payload) { + // 76a91488ac becomes 76a94c1488ac, which Core doesn't accept + var script = payload.payouts[0].scriptPayout; + expect(script.slice(0, 6)).to.equal('76a914'); + payload.payouts[0].scriptPayout = '76a94c14' + script.slice(6); + }, 'Expected payout scriptPayout to be a p2pkh/p2sh'); + }); + it('Should require unique payees', function () { withPayload(TWO_PAYOUTS, function (payload) { payload.payouts[1].scriptPayout = payload.payouts[0].scriptPayout; From 1eb33b29ca0886d89600a3e5a3e92f8cee81eb86 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 03:24:15 +0000 Subject: [PATCH 14/15] fix: require the exact P2SH length for ProTx payout scripts A trailing hex digit was dropped when decoding, so two strings for one script could pass the uniqueness check. Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/protxutils.js | 8 +++++--- test/transaction/payload/proregtxpayload.js | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/transaction/payload/protxutils.js b/lib/transaction/payload/protxutils.js index c061b6ea1..a5579173b 100644 --- a/lib/transaction/payload/protxutils.js +++ b/lib/transaction/payload/protxutils.js @@ -40,9 +40,11 @@ function isPayoutScript(script) { return false; } const parsed = new Script(script); - // Core's IsPayToPublicKeyHash takes only the 25-byte template, not a - // PUSHDATA1 form of the same key hash; isScriptHashOut() is already exact - return (parsed.isPublicKeyHashOut() && script.length === 25 * 2) || parsed.isScriptHashOut(); + // Core's IsPayToPublicKeyHash/IsPayToScriptHash take only the exact 25 and 23 + // byte templates; the lengths also reject a trailing hex digit, which decoding + // would silently drop + return (parsed.isPublicKeyHashOut() && script.length === 25 * 2) || + (parsed.isScriptHashOut() && script.length === 23 * 2); } /** diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index 3e190eff5..3ad865b4c 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -365,6 +365,15 @@ describe('ProRegTxPayload', function () { }, 'Expected payout scriptPayout to be a p2pkh/p2sh'); }); + it('Should reject a payee script with a trailing hex digit', function () { + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[0].scriptPayout = 'a914' + '11'.repeat(20) + '87' + '0'; + }, 'Expected payout scriptPayout to be a p2pkh/p2sh'); + withPayload(TWO_PAYOUTS, function (payload) { + payload.payouts[0].scriptPayout += '0'; + }, 'Expected payout scriptPayout to be a p2pkh/p2sh'); + }); + it('Should require unique payees', function () { withPayload(TWO_PAYOUTS, function (payload) { payload.payouts[1].scriptPayout = payload.payouts[0].scriptPayout; From 8f5fde27c63ab2730e62ef22e035264e89340aee Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 04:30:27 +0000 Subject: [PATCH 15/15] fix: check ProUpServTx operator payees exactly and reject foreign fields - A ProUpServTx operator payout script, when not empty, must be Core's exact P2PKH or P2SH template (isPayoutScript), as CheckProUpServTx requires; a PUSHDATA1 P2PKH or a trailing hex digit used to pass. - fromJSON rejects fields that toJSON never emits for the payload's version or node type instead of dropping them: platform fields on a regular or version 1 ProRegTx; type on a version 1 ProUpServTx and platform fields on a regular or version 1 one; type and platform fields on any ProUpRegTx, which has none (also dropped from its JSON typing). Co-Authored-By: Claude Opus 5.5 --- lib/transaction/payload/proregtxpayload.js | 8 +++ lib/transaction/payload/protxutils.js | 8 ++- lib/transaction/payload/proupregtxpayload.js | 6 ++ lib/transaction/payload/proupservtxpayload.js | 14 +++- test/transaction/payload/proregtxpayload.js | 36 ++++++++++ test/transaction/payload/proupregtxpayload.js | 14 ++++ .../transaction/payload/proupservtxpayload.js | 66 +++++++++++++++++++ .../payload/ProUpRegTxPayload.d.ts | 8 --- 8 files changed, 147 insertions(+), 13 deletions(-) diff --git a/lib/transaction/payload/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index 624cb604a..9cf0e31a3 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -152,6 +152,14 @@ function ProRegTxPayload(options) { this.scriptPayout = Script.fromAddress(options.payoutAddress).toHex(); } this.inputsHash = options.inputsHash; + // only version 2+ evonodes have platform fields + if (this.version < 2 || this.type !== MASTERNODE_TYPE_HP) { + protxUtils.checkNoFields( + options, + ['platformNodeID', 'platformP2PPort', 'platformHTTPPort'], + this.version < 2 ? this.version : 'a regular masternode payload' + ); + } this.platformNodeID = options.platformNodeID; if (this.version < PROTX_EXT_ADDR_VERSION) { this.platformP2PPort = options.platformP2PPort; diff --git a/lib/transaction/payload/protxutils.js b/lib/transaction/payload/protxutils.js index a5579173b..dcfafbcd0 100644 --- a/lib/transaction/payload/protxutils.js +++ b/lib/transaction/payload/protxutils.js @@ -288,13 +288,15 @@ function copyAddressBytes(from, to) { * silently dropping them * @param {Object} payloadJSON * @param {string[]} fields - * @param {number} version + * @param {number|string} payloadKind - the payload's version, or a description + * such as 'a regular masternode payload' */ -function checkNoFields(payloadJSON, fields, version) { +function checkNoFields(payloadJSON, fields, payloadKind) { + const kind = typeof payloadKind === 'number' ? `a version ${payloadKind} payload` : payloadKind; fields.forEach((field) => { Preconditions.checkArgument( payloadJSON[field] === undefined, - `Expect no ${field} in a version ${version} payload`, + `Expect no ${field} in ${kind}`, ); }); } diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index 295eea79f..68b99a59e 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -61,6 +61,12 @@ function ProUpRegTxPayload(payloadJSON) { if (payloadJSON.version !== undefined) { this.version = payloadJSON.version; } + // a ProUpRegTx has no type or platform fields + protxUtils.checkNoFields( + payloadJSON, + ['type', 'platformNodeID', 'platformP2PPort', 'platformHTTPPort'], + 'a ProUpRegTx payload' + ); this.proTxHash = payloadJSON.proTxHash; this.mode = payloadJSON.mode !== undefined ? payloadJSON.mode : 0; this.pubKeyOperator = payloadJSON.pubKeyOperator; diff --git a/lib/transaction/payload/proupservtxpayload.js b/lib/transaction/payload/proupservtxpayload.js index 139dc3cdc..15dd1db6f 100644 --- a/lib/transaction/payload/proupservtxpayload.js +++ b/lib/transaction/payload/proupservtxpayload.js @@ -67,6 +67,16 @@ function ProUpServTxPayload(payloadJSON) { if (this.version >= 2) { this.type = payloadJSON.type; + } else { + protxUtils.checkNoFields(payloadJSON, ['type'], this.version); + } + // only version 2+ evonodes have platform fields + if (this.version < 2 || this.type !== MASTERNODE_TYPE_HP) { + protxUtils.checkNoFields( + payloadJSON, + ['platformNodeID', 'platformP2PPort', 'platformHTTPPort'], + this.version < 2 ? this.version : 'a regular masternode payload' + ); } this.proTxHash = payloadJSON.proTxHash; @@ -232,10 +242,10 @@ ProUpServTxPayload.prototype.validate = function () { } } + // empty for no operator payee, else Core's exact P2PKH/P2SH (CheckProUpServTx) if (this.scriptOperatorPayout) { - var script = new Script(this.scriptOperatorPayout); Preconditions.checkArgument( - script.isPublicKeyHashOut() || script.isScriptHashOut(), + protxUtils.isPayoutScript(this.scriptOperatorPayout), 'Expected scriptOperatorPayout to be a p2pkh/p2sh' ); } diff --git a/test/transaction/payload/proregtxpayload.js b/test/transaction/payload/proregtxpayload.js index 3ad865b4c..c35b90781 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -568,4 +568,40 @@ describe('ProRegTxPayload', function () { }); }); }); + + describe('fields of other versions and node types', function () { + function withField(json, field, value) { + var copy = Object.assign({}, json); + copy[field] = value; + return copy; + } + + it('Should reject platform fields of a regular masternode or version 1', function () { + var json = proRegTxFixture.getProRegPayloadJSON(); + var v1JSON = withField(json, 'version', 1); + ProRegTxPayload.fromJSON(v1JSON); + ['platformNodeID', 'platformP2PPort', 'platformHTTPPort'].forEach(function (field) { + expect(function () { + ProRegTxPayload.fromJSON(withField(json, field, 1)); + }).to.throw('Expect no ' + field + ' in a regular masternode payload'); + expect(function () { + ProRegTxPayload.fromJSON(withField(v1JSON, field, 1)); + }).to.throw('Expect no ' + field + ' in a version 1 payload'); + }); + }); + + it('Should keep the platform fields of an evonode', function () { + var vectors = require('../../fixtures/core24-protx.json').transactions; + ['v2 evonode ProRegTx', 'v3 evonode ProRegTx'].forEach(function (name) { + var payload = new DashcoreLib.Transaction( + vectors.find(function (v) { + return v.name === name; + }).hex + ).extraPayload; + var restored = ProRegTxPayload.fromJSON(payload.toJSON({ network: 'testnet' })); + expect(restored.platformNodeID).to.be.equal(payload.platformNodeID); + expect(restored.toBuffer()).to.be.deep.equal(payload.toBuffer()); + }); + }); + }); }); diff --git a/test/transaction/payload/proupregtxpayload.js b/test/transaction/payload/proupregtxpayload.js index 4f63c3352..42d5b86a8 100644 --- a/test/transaction/payload/proupregtxpayload.js +++ b/test/transaction/payload/proupregtxpayload.js @@ -231,4 +231,18 @@ describe('ProUpRegTxPayload', function () { }).to.throw('Expect no payouts in a version 1 payload'); }); }); + + describe('fields it does not have', function () { + it('Should reject type and platform fields', function () { + var json = proUpRegTxFixture.getProUpRegPayloadJSON(); + ProUpRegTxPayload.fromJSON(json); + ['type', 'platformNodeID', 'platformP2PPort', 'platformHTTPPort'].forEach(function (field) { + var withField = Object.assign({}, json); + withField[field] = 0; + expect(function () { + ProUpRegTxPayload.fromJSON(withField); + }).to.throw('Expect no ' + field + ' in a ProUpRegTx payload'); + }); + }); + }); }); diff --git a/test/transaction/payload/proupservtxpayload.js b/test/transaction/payload/proupservtxpayload.js index 04c8075b9..f161e6808 100644 --- a/test/transaction/payload/proupservtxpayload.js +++ b/test/transaction/payload/proupservtxpayload.js @@ -266,4 +266,70 @@ describe('ProTxUpServPayload', function () { }).to.throw('Expect the platform ports and the Core port to differ'); }); }); + + describe('fields of other versions and node types', function () { + function withField(json, field, value) { + var copy = Object.assign({}, json); + copy[field] = value; + return copy; + } + + it('Should reject a version 1 type or platform field', function () { + var json = proUpServTxFixture.getProUpServPayloadJSON(); + expect(function () { + ProTxUpServPayload.fromJSON(withField(json, 'type', 0)); + }).to.throw('Expect no type in a version 1 payload'); + ['platformNodeID', 'platformP2PPort', 'platformHTTPPort'].forEach(function (field) { + expect(function () { + ProTxUpServPayload.fromJSON(withField(json, field, 1)); + }).to.throw('Expect no ' + field + ' in a version 1 payload'); + }); + }); + + it('Should reject platform fields of a regular masternode', function () { + var json = proUpServTxFixture.getProUpServPayloadJSON(); + json.version = 2; + json.type = 0; + ProTxUpServPayload.fromJSON(json); + ['platformNodeID', 'platformP2PPort', 'platformHTTPPort'].forEach(function (field) { + expect(function () { + ProTxUpServPayload.fromJSON(withField(json, field, 1)); + }).to.throw('Expect no ' + field + ' in a regular masternode payload'); + }); + }); + }); + + describe('operator payout script', function () { + var vector = require('../../fixtures/core24-protx.json').transactions.find( + function (v) { + return v.name === 'v3 ProUpServTx with an operator payout'; + } + ); + + function getPayload() { + return new DashcoreLib.Transaction(vector.hex).extraPayload; + } + + it('Should accept no script or Core\'s exact P2PKH/P2SH', function () { + var payload = getPayload(); + expect(payload.scriptOperatorPayout).to.have.lengthOf(50); + payload.validate(); + payload.scriptOperatorPayout = ''; + payload.validate(); + payload.scriptOperatorPayout = 'a914' + '11'.repeat(20) + '87'; + payload.validate(); + }); + + it('Should reject a P2PKH pushed with PUSHDATA1 and a trailing hex digit', function () { + var p2pkh = getPayload().scriptOperatorPayout; + var hash = p2pkh.slice(6, 46); + ['76a94c14' + hash + '88ac', p2pkh + '0'].forEach(function (script) { + var payload = getPayload(); + payload.scriptOperatorPayout = script; + expect(function () { + payload.validate(); + }).to.throw('Expected scriptOperatorPayout to be a p2pkh/p2sh'); + }); + }); + }); }); diff --git a/typings/transaction/payload/ProUpRegTxPayload.d.ts b/typings/transaction/payload/ProUpRegTxPayload.d.ts index 5b7a8fd7b..96f51363d 100644 --- a/typings/transaction/payload/ProUpRegTxPayload.d.ts +++ b/typings/transaction/payload/ProUpRegTxPayload.d.ts @@ -3,30 +3,22 @@ import { MasternodePayout, MasternodePayoutJSON } from './MasternodePayout'; /** * @typedef {Object} ProUpRegTransactionPayloadJSON * @property {number} version - * @property {number} [type] * @property {string} proTxHash * @property {string} pubKeyOperator * @property {string} keyIDVoting * @property {string} [payoutAddress] Before version 3 * @property {MasternodePayoutJSON[]} [payouts] Owner payees with their reward shares. From version 3 * @property {string} inputsHash - * @property {string} [platformNodeID] Platform Node ID - * @property {number} [platformP2PPort] Platform P2P port - * @property {number} [platformHTTPPort] Platform HTTP port * @property {string} [payloadSig] */ export type ProUpRegTransactionPayloadJSON = { version: number; - type?: number; proTxHash: string; pubKeyOperator: string; keyIDVoting: string; payoutAddress?: string; payouts?: MasternodePayoutJSON[]; inputsHash: string; - platformNodeID?: string; - platformP2PPort?: number; - platformHTTPPort?: number; payloadSig?: string; };