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..47397e0f6 100644 --- a/lib/constants/index.js +++ b/lib/constants/index.js @@ -43,8 +43,9 @@ 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 and SML entries serialize addresses + // as ExtNetInfo, and ProTx payloads have payout lists + PROTX_EXT_ADDR_VERSION: 3, NULL_HASH: '0000000000000000000000000000000000000000000000000000000000000000', // In duffs INSTANTSEND_FEE_PER_INPUT: 10000, 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/proregtxpayload.js b/lib/transaction/payload/proregtxpayload.js index 4ac9bf691..9cf0e31a3 100644 --- a/lib/transaction/payload/proregtxpayload.js +++ b/lib/transaction/payload/proregtxpayload.js @@ -1,40 +1,99 @@ /* 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'); 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'); +var BN = require('../../crypto/bn'); +var netInfo = require('../../util/netinfo'); +var protxUtils = require('./protxutils'); 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; +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 * @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 +101,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 +125,46 @@ 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) { + protxUtils.checkNoFields(options, LEGACY_ONLY_FIELDS, this.version); + this.addresses = _.cloneDeep(options.addresses); + // 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; + 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; - 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 +175,205 @@ 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[]} + */ +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; + }); +} + +/** + * 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( + shares.length >= MIN_SHARES && shares.length <= MAX_SHARES, + 'Expect 2 to 8 shares' + ); + Preconditions.checkArgument( + payload.earlyPeriodBlocks <= MAX_EARLY_PERIOD_BLOCKS, + 'Expect earlyPeriodBlocks to be at most 420480' + ); + + 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 >= 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( + utils.isHexStringOfSize(share.keyIDOwner, constants.PUBKEY_ID_SIZE * 2) && + !protxUtils.isNull(share.keyIDOwner), + 'Expect share keyIDOwner to be a non-null 20 byte key id' + ); + Preconditions.checkArgument( + !ownerKeys.has(protxUtils.bytesKey(share.keyIDOwner)), + 'Expect share keyIDOwner to be unique' + ); + ownerKeys.add(protxUtils.bytesKey(share.keyIDOwner)); + Preconditions.checkArgument( + !refundScripts.has(protxUtils.bytesKey(share.scriptRefund)), + 'Expect share scriptRefund to be unique' + ); + refundScripts.add(protxUtils.bytesKey(share.scriptRefund)); + + Preconditions.checkArgument( + 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 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( + 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), + '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' + ); + + 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( + protxUtils.isNull(payload.keyIDOwner), + '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' + ); + protxUtils.validatePayouts( + payload.payouts, + payload.keyIDOwner, + payload.keyIDVoting + ); + } +} + /* Static methods */ /** @@ -96,50 +386,72 @@ 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(); - payload.collateralHash = payloadBufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + // 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(); - payload.service = ipUtil.bufferToIPAndPort( - payloadBufferReader.read(ipUtil.IP_AND_PORT_SIZE) - ); + if (payload.version >= PROTX_EXT_ADDR_VERSION) { + protxUtils.readAddresses(payload, payloadBufferReader); + } else { + payload.service = ipUtil.bufferToIPAndPort( + readExact(payloadBufferReader, ipUtil.IP_AND_PORT_SIZE) + ); + } - payload.keyIDOwner = payloadBufferReader - .read(constants.PUBKEY_ID_SIZE) - .reverse() + 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 - .read(constants.PUBKEY_ID_SIZE) - .reverse() + payload.keyIDVoting = readExactReverse(payloadBufferReader, constants.PUBKEY_ID_SIZE) .toString('hex'); - payload.operatorReward = payloadBufferReader. readUInt16LE(); - var scriptPayoutSize = payloadBufferReader.readVarintNum(); - payload.scriptPayout = payloadBufferReader - .read(scriptPayoutSize) - .toString('hex'); - payload.inputsHash = payloadBufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + payload.operatorReward = payloadBufferReader.readUInt16LE(); + if (payload.version >= PROTX_EXT_ADDR_VERSION) { + payload.payouts = protxUtils.readPayouts(payloadBufferReader); + var sharesCount = payloadBufferReader.readUInt8(); + payload.shares = []; + for (var i = 0; i < sharesCount; i++) { + payload.shares.push({ + amount: payloadBufferReader.readUInt64LEBN().toNumber(), + scriptRefund: readVarBytes(payloadBufferReader).toString('hex'), + scriptReward: readVarBytes(payloadBufferReader).toString('hex'), + keyIDOwner: readExactReverse(payloadBufferReader, constants.PUBKEY_ID_SIZE) + .toString('hex'), + }); + } + // one join signature per share + payload.joinSigs = []; + for (var j = 0; j < sharesCount; j++) { + payload.joinSigs.push( + readExact(payloadBufferReader, COMPACT_SIGNATURE_SIZE).toString('hex') + ); + } + payload.earlyPeriodBlocks = payloadBufferReader.readUInt32LE(); + payload.earlyPenalty = payloadBufferReader.readUInt64LEBN().toNumber(); + } else { + payload.scriptPayout = readVarBytes(payloadBufferReader).toString('hex'); + } + payload.inputsHash = readExactReverse(payloadBufferReader, 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 = 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(); + payload.platformHTTPPort = payloadBufferReader.readUInt16LE(); + } } - 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( @@ -147,6 +459,7 @@ ProRegTxPayload.fromBuffer = function fromBuffer(rawPayload) { ); } + payload.validate(); return payload; }; @@ -164,14 +477,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( - utils.isUnsignedInteger(this.version), - 'Expect version to be an unsigned integer' + !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', @@ -181,72 +510,64 @@ ProRegTxPayload.prototype.validate = function () { utils.isSha256HexString(this.collateralHash), 'Expect collateralHash to be a hex string representing sha256 hash' ); - if (!ipUtil.isZeroAddress(this.service)) { + if (isExtAddr) { + // a ProRegTx may register without addresses (CProRegTx::IsTriviallyValid) + netInfo.validateMasternodeAddresses(this.addresses, { + evo: isEvo, + allowEmpty: true, + }); + } else if (!ipUtil.isZeroAddress(this.service)) { Preconditions.checkArgument( ipUtil.isIPV4(this.service), 'Expected service to be a string with ip address and port' ); } 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' - ); - - 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.scriptPayout) { - var script = new Script(this.scriptPayout); - Preconditions.checkArgument( - script.isPublicKeyHashOut() || script.isScriptHashOut(), - 'Expected scriptOperatorPayout to be a p2pkh/p2sh' + if (isExtAddr) { + validateExtAddrFields(this); + } else { + // Core checks the payout script as a one-entry payout list (GetOwnerPayouts) + protxUtils.validatePayouts( + [{ scriptPayout: this.scriptPayout, reward: 10000 }], + this.keyIDOwner, + this.keyIDVoting ); } @@ -279,23 +600,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 = protxUtils.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 +668,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(protxUtils.addressesToBuffer(this)); + } 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) { + protxUtils.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) { @@ -352,6 +732,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 new file mode 100644 index 000000000..dcfafbcd0 --- /dev/null +++ b/lib/transaction/payload/protxutils.js @@ -0,0 +1,322 @@ +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 +// (ExtAddr) ProRegTx and ProUpRegTx payloads, Core's MasternodePayoutShares +// (src/evo/providertx.h), and JSON field checks + +// 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; +// 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 + * @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 {string} script - hex + * @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); + // 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); +} + +/** + * Hex strings compare as the bytes they encode, whatever their letter case + * @param {string} hex + * @return {string} + */ +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 + */ +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 + * @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[]} + */ +function readPayouts(reader) { + const payouts = []; + const count = reader.readUInt8(); + for (let i = 0; i < count; i++) { + payouts.push({ + scriptPayout: readVarBytes(reader).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); + }); +} + +/** + * 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, keyIDOwner, keyIDVoting) { + Preconditions.checkArgument( + 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( + !seenScripts.has(bytesKey(payout.scriptPayout)), + 'Expect payout scripts to be unique', + ); + seenScripts.add(bytesKey(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', + ); +} + +/** + * @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, + })); +} + +/** + * 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', + ); +} + +/** + * 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 + * @param {Object} payloadJSON + * @param {string[]} fields + * @param {number|string} payloadKind - the payload's version, or a description + * such as 'a regular masternode payload' + */ +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 ${kind}`, + ); + }); +} + +module.exports = { + readAddresses, + addressesToBuffer, + copyAddressBytes, + bytesKey, + isNull, + withDefault, + validateKeyID, + validateLegacyPlatformPorts, + isPayoutScript, + isP2PKHOf, + checkNoFields, + validateVersion, + readPayouts, + writePayouts, + validatePayouts, + payoutsToJSON, + payoutsFromJSON, +}; diff --git a/lib/transaction/payload/proupregtxpayload.js b/lib/transaction/payload/proupregtxpayload.js index 290959a6a..68b99a59e 100644 --- a/lib/transaction/payload/proupregtxpayload.js +++ b/lib/transaction/payload/proupregtxpayload.js @@ -5,11 +5,15 @@ 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'); +var protxUtils = require('./protxutils'); -var isUnsignedInteger = utils.isUnsignedInteger; var isHexString = utils.isHexaString; var isSha256HexString = utils.isSha256HexString; @@ -17,7 +21,8 @@ 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; /** * @typedef {Object} ProUpRegTransactionPayloadJSON @@ -25,7 +30,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 +44,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 +58,30 @@ function ProUpRegTxPayload(payloadJSON) { this.version = CURRENT_PAYLOAD_VERSION; if (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; this.keyIDVoting = payloadJSON.keyIDVoting; - this.scriptPayout = Script.fromAddress(payloadJSON.payoutAddress).toHex(); + if (this.version >= PROTX_EXT_ADDR_VERSION) { + protxUtils.checkNoFields( + payloadJSON, + ['payoutAddress', 'scriptPayout'], + this.version + ); + this.payouts = protxUtils.payoutsFromJSON(protxUtils.withDefault(payloadJSON.payouts, [])); + } else { + protxUtils.checkNoFields(payloadJSON, ['payouts'], this.version); + this.scriptPayout = Script.fromAddress(payloadJSON.payoutAddress).toHex(); + } this.inputsHash = payloadJSON.inputsHash; if (payloadJSON.payloadSig) { this.payloadSig = payloadJSON.payloadSig; @@ -76,37 +103,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(); - payload.proTxHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + // like Core, reject an unknown version before choosing a layout + protxUtils.validateVersion(payload.version); + // 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) - .toString('hex'); - payload.keyIDVoting = payloadBufferReader - .read(CKEYID_SIZE) - .reverse() - .toString('hex'); - var scriptPayoutSize = payloadBufferReader.readVarintNum(); - payload.scriptPayout = payloadBufferReader - .read(scriptPayoutSize) + payload.pubKeyOperator = readExact(payloadBufferReader, BLSPUBKEY_SIZE) .toString('hex'); - payload.inputsHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + payload.keyIDVoting = readExactReverse(payloadBufferReader, CKEYID_SIZE) .toString('hex'); - - if (!payloadBufferReader.finished()) { - signatureSize = payloadBufferReader.readVarintNum(); + if (payload.version >= PROTX_EXT_ADDR_VERSION) { + payload.payouts = protxUtils.readPayouts(payloadBufferReader); + } else { + payload.scriptPayout = readVarBytes(payloadBufferReader).toString('hex'); } + payload.inputsHash = readExactReverse(payloadBufferReader, HASH_SIZE) + .toString('hex'); - 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()) { @@ -131,23 +150,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 () { - Preconditions.checkArgument( - isUnsignedInteger(this.version), - 'Expected version to be an unsigned integer' - ); + 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), @@ -158,22 +177,25 @@ ProUpRegTxPayload.prototype.validate = function () { 'expected inputsHash to be a sha256 hex string' ); - 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( 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" ); } @@ -199,9 +221,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 = protxUtils.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 +255,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) { + protxUtils.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..3948f4250 100644 --- a/lib/transaction/payload/prouprevtxpayload.js +++ b/lib/transaction/payload/prouprevtxpayload.js @@ -5,14 +5,20 @@ 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'); var isUnsignedInteger = utils.isUnsignedInteger; 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; /** @@ -37,6 +43,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,17 +97,16 @@ ProUpRevTxPayload.fromBuffer = function (rawPayloadBuffer) { var payloadBufferReader = new BufferReader(rawPayloadBuffer); var payload = new ProUpRevTxPayload(); payload.version = payloadBufferReader.readUInt16LE(); - payload.proTxHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + // like Core, reject an unknown version before choosing a layout + protxUtils.validateVersion(payload.version); + // 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 - .read(HASH_SIZE) - .reverse() + 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()) { @@ -128,14 +136,11 @@ 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), - '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 225dd66f4..15dd1db6f 100644 --- a/lib/transaction/payload/proupservtxpayload.js +++ b/lib/transaction/payload/proupservtxpayload.js @@ -5,12 +5,21 @@ 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'); var ipUtils = require('../../util/ip'); -const { MASTERNODE_TYPE_HP, - PLATFORM_NODE_ID_SIZE +var netInfo = require('../../util/netinfo'); +var protxUtils = require('./protxutils'); +var _ = require('lodash'); +const { MASTERNODE_TYPE_BASIC, + MASTERNODE_TYPE_HP, + PLATFORM_NODE_ID_SIZE, + PROTX_EXT_ADDR_VERSION, } = require("../../constants"); var isUnsignedInteger = utils.isUnsignedInteger; @@ -26,12 +35,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 +50,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) { @@ -56,19 +67,41 @@ 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; - this.service = payloadJSON.service; - this.scriptOperatorPayout = Script.fromAddress( - payloadJSON.operatorPayoutAddress - ).toHex(); + 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 + ? 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) { @@ -96,35 +129,41 @@ 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(); } - payload.proTxHash = payloadBufferReader - .read(HASH_SIZE) - .reverse() + // 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.service = ipUtils.bufferToIPAndPort( - payloadBufferReader.read(ipUtils.IP_AND_PORT_SIZE) - ); + if (payload.version >= PROTX_EXT_ADDR_VERSION) { + protxUtils.readAddresses(payload, payloadBufferReader); + } else { + payload.service = ipUtils.bufferToIPAndPort( + 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 - .read(HASH_SIZE) - .reverse() + 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.read(PLATFORM_NODE_ID_SIZE).reverse().toString('hex'); - payload.platformP2PPort = payloadBufferReader.readUInt16LE(); - payload.platformHTTPPort = payloadBufferReader.readUInt16LE(); + 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(); + payload.platformHTTPPort = payloadBufferReader.readUInt16LE(); + } } - payload.payloadSig = payloadBufferReader.read(BLSSIG_SIZE).toString('hex'); + payload.payloadSig = readExact(payloadBufferReader, BLSSIG_SIZE).toString('hex'); if (!payloadBufferReader.finished()) { throw new Error( @@ -148,19 +187,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 () { - Preconditions.checkArgument( - isUnsignedInteger(this.version), - 'Expect version to be an unsigned integer' - ); + 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)' ); } @@ -169,9 +210,16 @@ 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) { + // Core requires addresses in a ProUpServTx (CProUpServTx::IsTriviallyValid) + netInfo.validateMasternodeAddresses(this.addresses, { + evo: this.type === MASTERNODE_TYPE_HP, + allowEmpty: false, + }); + } 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' ); } @@ -181,27 +229,23 @@ 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' - ); - - 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.validateKeyID(this.platformNodeID, 'platformNodeID'); + + if (this.version < PROTX_EXT_ADDR_VERSION) { + protxUtils.validateLegacyPlatformPorts( + this.platformP2PPort, + this.platformHTTPPort, + Number(this.service.slice(this.service.lastIndexOf(':') + 1)) + ); + } } + // 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' ); } @@ -212,7 +256,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" ); } @@ -237,20 +281,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 +337,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(protxUtils.addressesToBuffer(this)); + } else { + payloadBufferWriter.write(ipUtils.ipAndPortToBuffer(this.service)); + } var scriptOperatorPayout = Buffer.from(this.scriptOperatorPayout, 'hex'); payloadBufferWriter.writeVarintNum(scriptOperatorPayout.length); @@ -290,9 +350,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; @@ -312,7 +375,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 b8cd3264b..af7a85842 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -1,4 +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. @@ -21,11 +25,37 @@ 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; const I2P_ADDRESS_SIZE = 32; +const CJDNS_ADDRESS_SIZE = 16; +const CJDNS_PREFIX = 0xfc; + +// 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, the longest domain Core's DomainPort serializes (LIMITED_STRING) +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}$/; @@ -35,6 +65,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; @@ -60,6 +93,99 @@ 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; +} + +/** + * 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 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 + // 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])]), + ); + const checksum = Buffer.from(digest).slice(0, TORV3_CHECKSUM_SIZE); + return Buffer.concat([pubKey, checksum, Buffer.from([TORV3_VERSION])]); +} + +/** + * 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 + * @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).toString(16)); + } + + // 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') { + currentStart = i + 1; + currentLength = 0; + return; + } + currentLength += 1; + if (currentLength > longestLength) { + longestStart = currentStart; + longestLength = currentLength; + } + }); + + // 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}`; +} + /** * @param {string} address - IPv6 address without brackets, as printed by Core * @return {Buffer} @@ -73,11 +199,12 @@ 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); - 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}`); @@ -101,21 +228,24 @@ 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 { - networkId: address[0] === 0xfc ? BIP155_CJDNS : BIP155_IPV6, + networkId: address[0] === CJDNS_PREFIX ? BIP155_CJDNS : BIP155_IPV6, address, }; } 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_ADDRESS_SIZE + 3 || version !== TORV3_VERSION) { + const pubKey = decoded.slice(0, TORV3_ADDRESS_SIZE); + // covers the length, checksum and version + if (!decoded.equals(torV3AddressBytes(pubKey))) { throw new Error(`Invalid Tor v3 address ${host}`); } - return { networkId: BIP155_TORV3, address: decoded.slice(0, TORV3_ADDRESS_SIZE) }; + return { networkId: BIP155_TORV3, address: pubKey }; } if (host.endsWith('.b32.i2p')) { const decoded = decodeBase32(host.replace(/\.b32\.i2p$/, '')); @@ -128,11 +258,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); @@ -142,15 +273,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)) { - throw new Error(`Invalid address entry ${entry}`); - } const domain = Buffer.from(host, 'ascii'); writer.writeUInt8(ENTRY_TYPE_DOMAIN); writer.writeVarintNum(domain.length); @@ -166,6 +307,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}`); @@ -194,6 +338,275 @@ 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 (isDisallowedIPv6(address)) { + throw new Error(`Invalid IPv6 address ${address.toString('hex')}`); + } + 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}`; + 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 = readCompactSize(reader); + if (BIP155_ADDRESS_SIZES[networkId] !== size) { + throw new Error(`Invalid BIP155 address of network ${networkId} and size ${size}`); + } + const address = readExact(reader, size); + return serviceToString(networkId, address, reader.readUInt16BE()); + } + if (type === ENTRY_TYPE_DOMAIN) { + 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 = readExact(reader, size).toString('latin1'); + 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 = readCompactSize(reader); + 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 = readCompactSize(reader); + 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; +} + +// 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) => { + 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(entryKey); + if (hosts.has(hostKey)) { + throw new Error(`Duplicate ${purpose} address ${host}`); + } + hosts.add(hostKey); + + 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/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-d/index.test-d.ts b/test-d/index.test-d.ts index d597ef46a..bcc277b7b 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -55,3 +55,60 @@ 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); +expectType(proUpServTxPayload.type); +expectType(proUpServTxPayload.platformNodeID); +expectType(proUpServTxPayload.platformP2PPort); +expectType(proUpServTxPayload.platformHTTPPort); + +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..9e22d9a85 --- /dev/null +++ b/test/fixtures/core24-protx.json @@ -0,0 +1,680 @@ +{ + "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 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)", + "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..c35b90781 100644 --- a/test/transaction/payload/proregtxpayload.js +++ b/test/transaction/payload/proregtxpayload.js @@ -112,4 +112,496 @@ 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(''); + }); + + function p2pkh(keyID) { + return '76a914' + Buffer.from(keyID, 'hex').reverse().toString('hex') + '88ac'; + } + + function expectInvalid(payload, message) { + expect(function () { + payload.validate(); + }).to.throw(message); + } + + function withPayload(name, change, message) { + var payload = getPayload(name); + change(payload); + expectInvalid(payload, message); + } + + var TWO_PAYOUTS = 'v3 ProRegTx with two payouts'; + 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) { + 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) { + return v.coreKey === 'proRegTx'; + }) + .forEach(function (v) { + new Transaction(v.hex).extraPayload.validate(); + }); + }); + + 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) { + 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'); + }); + + 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'); + }); + + 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'); + }); + + 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 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 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; + }, '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 () { + 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(); + }); + + 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 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'); + withPayload(TWO_PAYOUTS, function (payload) { + payload.earlyPenalty = 1; + }, 'Expect no earlyPeriodBlocks or earlyPenalty without shares'); + }); + + 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)'); + }); + }); + + 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'); + }); + + 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'); + }); + + 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'); + }); + + 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 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'); + }); + + 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); + 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'); + }); + + 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'); + // 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 () { + 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'); + }); + + 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(); + }); + }); + + 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'); + } + ); + }); + + 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'); + } + ); + }); + }); + }); + + 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/protxcorevectors.js b/test/transaction/payload/protxcorevectors.js new file mode 100644 index 000000000..3784acfca --- /dev/null +++ b/test/transaction/payload/protxcorevectors.js @@ -0,0 +1,274 @@ +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); + }); + }); +}); + +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/transaction/payload/proupregtxpayload.js b/test/transaction/payload/proupregtxpayload.js index d46b1416d..42d5b86a8 100644 --- a/test/transaction/payload/proupregtxpayload.js +++ b/test/transaction/payload/proupregtxpayload.js @@ -103,4 +103,146 @@ 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); + }); + + 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 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 = []; + expect(function () { + payload.validate(); + }).to.throw('Expect payouts to be an array of 1 to 8 payouts'); + + payload = getPayload(); + payload.payouts[0].reward = 9999; + expect(function () { + payload.validate(); + }).to.throw('Expect payout rewards to add up to 10000'); + }); + + 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 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); + 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 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'); + }); + }); + + 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/prouprevtxpayload.js b/test/transaction/payload/prouprevtxpayload.js index 28b09ba3d..c1be668c2 100644 --- a/test/transaction/payload/prouprevtxpayload.js +++ b/test/transaction/payload/prouprevtxpayload.js @@ -105,4 +105,43 @@ 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 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(); + [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 c079ce2bf..f161e6808 100644 --- a/test/transaction/payload/proupservtxpayload.js +++ b/test/transaction/payload/proupservtxpayload.js @@ -102,4 +102,234 @@ 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; + + function getPayload() { + var vector = vectors.find(function (v) { + return v.name === 'v3 evonode ProUpServTx'; + }); + 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 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 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(); + 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'); + }); + }); + + 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'); + }); + }); + + 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/test/util/netinfo.js b/test/util/netinfo.js index 5259b546a..13a3761bd 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -1,5 +1,11 @@ const { expect } = require('chai'); -const { extNetInfoToBuffer } = require('../../lib/util/netinfo'); +const BufferReader = require('../../lib/encoding/bufferreader'); +const { + extNetInfoToBuffer, + extNetInfoFromBuffer, + readExtNetInfo, + validateMasternodeAddresses, +} = 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 @@ -70,7 +76,29 @@ 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'); + 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'); + }); + // 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'], + })).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'], @@ -80,4 +108,202 @@ describe('netinfo', () => { })).to.throw('Too many platform_https addresses'); }); }); + + describe('extNetInfoFromBuffer', () => { + it('should read every address kind in Core\'s string form', () => { + // 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', + '[2a01:4f8::1]:19951', + '[fc00::1]:19951', + ], + 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 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( + 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'); + // 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'); + // 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'); + }); + }); + + 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 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}`); + }); + }); + + 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'); + }); + }); + }); }); 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..347061e35 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; @@ -105,9 +153,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 +177,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..96f51363d 100644 --- a/typings/transaction/payload/ProUpRegTxPayload.d.ts +++ b/typings/transaction/payload/ProUpRegTxPayload.d.ts @@ -1,28 +1,24 @@ +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 + * @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; + payoutAddress?: string; + payouts?: MasternodePayoutJSON[]; inputsHash: string; - platformNodeID?: string; - platformP2PPort?: number; - platformHTTPPort?: number; payloadSig?: string; }; @@ -35,7 +31,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 +120,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..a3062a8e3 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; }; @@ -76,14 +88,22 @@ 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 */ proTXHash: string; /** - * string - ip and port + * string - ip and port. Before version 3 */ - service: string; + service?: string; + /** + * "addr:port" lists by purpose. From version 3 + */ + addresses?: MasternodeAddresses; /** * Hash of all the outpoints of the transaction inputs */ @@ -92,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. */