From c3f01cb1ff318ecf6eeef83c0f4b073360351389 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 17:24:42 +0000 Subject: [PATCH 01/21] fix: stop parsers from reversing the caller's buffer in place BufferReader.read() returns a slice that shares memory with the input, so read(n).reverse() byte-swapped the caller's Buffer while parsing it. CoinbasePayload, QuorumEntry, SimplifiedMNListEntry, SimplifiedMNListDiff and PartialMerkleTree now use readReverse(), which reverses a copy. Co-Authored-By: Claude Opus 5.5 --- lib/block/PartialMerkleTree.js | 2 +- lib/deterministicmnlist/QuorumEntry.js | 9 +++------ lib/deterministicmnlist/SimplifiedMNListDiff.js | 11 ++++------- lib/deterministicmnlist/SimplifiedMNListEntry.js | 7 +++---- lib/transaction/payload/coinbasepayload.js | 6 ++---- test/block/PartialMerkleTree.js | 15 +++++++++++++++ test/deterministicmnlist/QuorumEntry.js | 7 +++++++ test/deterministicmnlist/SimplifiedMNListDiff.js | 7 +++++++ test/deterministicmnlist/SimplifiedMNListEntry.js | 7 +++++++ test/transaction/payload/coinbasepayload.js | 8 ++++++++ 10 files changed, 57 insertions(+), 22 deletions(-) diff --git a/lib/block/PartialMerkleTree.js b/lib/block/PartialMerkleTree.js index 87498d7b7..716e1b0a8 100644 --- a/lib/block/PartialMerkleTree.js +++ b/lib/block/PartialMerkleTree.js @@ -67,7 +67,7 @@ PartialMerkleTree.fromBufferReader = function fromBufferReader(bufferReader) { partialMerkleTree.merkleHashes = []; for (let i = 0; i < merkleHashesCount; i += 1) { partialMerkleTree.merkleHashes.push( - bufferReader.read(constants.SHA256_HASH_SIZE).reverse().toString('hex') + bufferReader.readReverse(constants.SHA256_HASH_SIZE).toString('hex') ); } diff --git a/lib/deterministicmnlist/QuorumEntry.js b/lib/deterministicmnlist/QuorumEntry.js index 90fed29a1..109a4ae27 100644 --- a/lib/deterministicmnlist/QuorumEntry.js +++ b/lib/deterministicmnlist/QuorumEntry.js @@ -99,8 +99,7 @@ QuorumEntry.fromBuffer = function fromBuffer(buffer, llmqParamsOverride = {}) { SMLQuorumEntry.version = bufferReader.readUInt16LE(); SMLQuorumEntry.llmqType = bufferReader.readUInt8(); SMLQuorumEntry.quorumHash = bufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + .readReverse(constants.SHA256_HASH_SIZE) .toString('hex'); if (isHashQuorumIndexRequired(this.version)) { @@ -121,8 +120,7 @@ QuorumEntry.fromBuffer = function fromBuffer(buffer, llmqParamsOverride = {}) { SMLQuorumEntry.version = bufferReader.readUInt16LE(); SMLQuorumEntry.llmqType = bufferReader.readUInt8(); SMLQuorumEntry.quorumHash = bufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + .readReverse(constants.SHA256_HASH_SIZE) .toString('hex'); if (isHashQuorumIndexRequired(SMLQuorumEntry.version)) { @@ -147,8 +145,7 @@ QuorumEntry.fromBuffer = function fromBuffer(buffer, llmqParamsOverride = {}) { .read(BLS_PUBLIC_KEY_SIZE) .toString('hex'); SMLQuorumEntry.quorumVvecHash = bufferReader - .read(SHA256_HASH_SIZE) - .reverse() + .readReverse(SHA256_HASH_SIZE) .toString('hex'); SMLQuorumEntry.quorumSig = bufferReader .read(BLS_SIGNATURE_SIZE) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 7f8e46f9c..06d6f9c31 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -62,12 +62,10 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam const data = {}; data.baseBlockHash = bufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + .readReverse(constants.SHA256_HASH_SIZE) .toString('hex'); data.blockHash = bufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + .readReverse(constants.SHA256_HASH_SIZE) .toString('hex'); data.cbTxMerkleTree = PartialMerkleTree.fromBufferReader(bufferReader); @@ -79,7 +77,7 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam data.deletedMNs = []; for (let i = 0; i < deletedMNsCount; i += 1) { data.deletedMNs.push( - bufferReader.read(constants.SHA256_HASH_SIZE).reverse().toString('hex') + bufferReader.readReverse(constants.SHA256_HASH_SIZE).toString('hex') ); } @@ -117,8 +115,7 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam const deletedQuorum = {}; deletedQuorum.llmqType = bufferReader.readUInt8(); deletedQuorum.quorumHash = bufferReader - .read(constants.SHA256_HASH_SIZE) - .reverse() + .readReverse(constants.SHA256_HASH_SIZE) .toString('hex'); data.deletedQuorums.push(deletedQuorum); } diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index 33aa22508..c856a7d1e 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -96,10 +96,9 @@ SimplifiedMNListEntry.fromBuffer = function fromBuffer(buffer, network) { const object = { }; - object.proRegTxHash = bufferReader.read(SHA256_HASH_SIZE).reverse().toString('hex'); + object.proRegTxHash = bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'); object.confirmedHash = bufferReader - .read(SHA256_HASH_SIZE) - .reverse() + .readReverse(SHA256_HASH_SIZE) .toString('hex'); object.service = parseIp(bufferReader.read(ipUtils.IP_AND_PORT_SIZE)); object.pubKeyOperator = bufferReader.read(BLS_PUBLIC_KEY_SIZE).toString('hex'); @@ -123,7 +122,7 @@ SimplifiedMNListEntry.fromBuffer = function fromBuffer(buffer, network) { if (object.nType === MASTERNODE_TYPE_HP) { object.platformHTTPPort = bufferReader.readUInt16LE(); - object.platformNodeID = bufferReader.read(PLATFORM_NODE_ID_SIZE).reverse().toString('hex'); + object.platformNodeID = bufferReader.readReverse(PLATFORM_NODE_ID_SIZE).toString('hex'); } return SimplifiedMNListEntry.fromObject(object); diff --git a/lib/transaction/payload/coinbasepayload.js b/lib/transaction/payload/coinbasepayload.js index f242e5bb5..277b65b05 100644 --- a/lib/transaction/payload/coinbasepayload.js +++ b/lib/transaction/payload/coinbasepayload.js @@ -59,13 +59,11 @@ CoinbasePayload.fromBuffer = function (rawPayload) { payload.version = payloadBufferReader.readUInt16LE(); payload.height = payloadBufferReader.readUInt32LE(); payload.merkleRootMNList = payloadBufferReader - .read(HASH_SIZE) - .reverse() + .readReverse(HASH_SIZE) .toString('hex'); if (payload.version >= 2) { payload.merkleRootQuorums = payloadBufferReader - .read(HASH_SIZE) - .reverse() + .readReverse(HASH_SIZE) .toString('hex'); } diff --git a/test/block/PartialMerkleTree.js b/test/block/PartialMerkleTree.js index e7b03bff3..8e9910472 100644 --- a/test/block/PartialMerkleTree.js +++ b/test/block/PartialMerkleTree.js @@ -77,4 +77,19 @@ describe('PartialMerkleTree', function () { ); }); }); + + describe('.fromBuffer', function () { + // cbTxMerkleTree of Core's testnet mnlistdiff at block 1580498 + var coreTreeHex = + '0800000004a868b2ed8d2301e11fe10af03de99739daabf69fd189ea44fb5aa1c47df56f1d389ffc77b4ade36785589c1161ed70c244ff26dbfda7830e56bad2a9300d165bb9f91e0478b64957632ae2729374a2c7c0f386052c87ac24864baf6bf0f711c70eb605f91fd18b14932cb62fa77678cfffc9ab19d7990e7283e456398d44d5b0010f'; + + it('Should not modify the buffer it parses', function () { + var buffer = Buffer.from(coreTreeHex, 'hex'); + + var tree = PartialMerkleTree.fromBuffer(buffer); + + expect(buffer.toString('hex')).to.be.equal(coreTreeHex); + expect(tree.toString()).to.be.equal(coreTreeHex); + }); + }); }); diff --git a/test/deterministicmnlist/QuorumEntry.js b/test/deterministicmnlist/QuorumEntry.js index ce033ddf5..dbb1ba7fa 100644 --- a/test/deterministicmnlist/QuorumEntry.js +++ b/test/deterministicmnlist/QuorumEntry.js @@ -91,6 +91,13 @@ describe('QuorumEntry', function () { Buffer.from(quorumEntryHash, 'hex') ); }); + it('Should not modify the buffer it parses', function () { + var buffer = Buffer.from(quorumEntryHex, 'hex'); + + QuorumEntry.fromBuffer(buffer); + + expect(buffer.toString('hex')).to.be.equal(quorumEntryHex); + }); }); describe('to buffer', function () { it('Should be able to generate correct buffer', function () { diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index 3ea6ae743..26c493291 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -98,6 +98,13 @@ describe('SimplifiedMNListDiff', function () { var parsed = smlDiff.toObject(); expect(parsed).to.be.deep.equal(mnListDiffJSON); }); + it('Should not modify the buffer it parses', function () { + var buf = Buffer.from(mnListDiffHexString, 'hex'); + + SimplifiedMNListDiff.fromBuffer(buf, Networks.testnet); + + expect(buf.toString('hex')).to.be.equal(mnListDiffHexString); + }); }); describe('toBuffer', function () { it('Should be able to serialize data', function () { diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index f2cbaa308..5973c57e6 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -131,6 +131,13 @@ describe('SimplifiedMNListEntry', function () { var entryJSON = entry.toObject(); expect(entryJSON).to.be.deep.equal(smlEntryJSON); }); + it('Should not modify the buffer it parses', function () { + var buffer = Buffer.from(smlEntryHex, 'hex'); + + SimplifiedMNListEntry.fromBuffer(buffer, Networks.testnet); + + expect(buffer.toString('hex')).to.be.equal(smlEntryHex); + }); }); describe('toBuffer', function () { it('Should serialize data to same buffer', function () { diff --git a/test/transaction/payload/coinbasepayload.js b/test/transaction/payload/coinbasepayload.js index a0220f535..f58b18178 100644 --- a/test/transaction/payload/coinbasepayload.js +++ b/test/transaction/payload/coinbasepayload.js @@ -69,6 +69,14 @@ describe('CoinbasePayload', function () { 'Failed to parse payload: raw payload is bigger than expected.' ); }); + + it('Should not modify the buffer it parses', function () { + var buffer = Buffer.from(validCoinbasePayloadBuffer); + + CoinbasePayload.fromBuffer(buffer); + + expect(buffer).to.be.deep.equal(validCoinbasePayloadBuffer); + }); }); describe('.fromJSON', function () { From 5e246e635265f4a8f4eb8817964eff59f01e7321 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 17:31:10 +0000 Subject: [PATCH 02/21] fix: read and write masternode list diffs in Core's P2P format SimplifiedMNListDiff.fromBuffer/toBuffer used a layout of their own: entries in their hash layout without Core's per-entry nVersion, quorums prefixed by their length, the diff nVersion after the coinbase, and no quorumsCLSigs. toBuffer output could not be read back ("Expected nType to be a number"), Core's bytes could not be read at all, and version 3 (ExtAddr) entries could not be told apart from older ones. They now read and write Core's `mnlistdiff` message as sent at protocol 70230 and later (unchanged through 70242): nVersion first, entries prefixed by their nVersion, final commitments unprefixed, then quorumsCLSigs, which the diff now keeps in Core's RPC form. Diffs from Core RPC serialize to the bytes Core sends. - SimplifiedMNListEntry.fromP2PBufferReader / toP2PBuffer handle the entry layout; toBuffer (the hashed layout) and fromBuffer are unchanged. - QuorumEntry.fromP2PBufferReader reads a commitment from a stream and counts set signer and member bits like Core RPC. Like Core, it rejects bit sets whose size differs from the quorum type's, and for unknown types any longer than the remaining data; the diff writes commitments with toBufferForHashing, which already is Core's layout. Fixtures are mnlistdiff payloads captured over P2P from a Core 24.0.0 testnet peer (full list at 1580498, the block Core RPC JSON in core24-testnet-mnlistdiffs.json describes, and 1580498 to 1580546) and a Core 23.1.8 mainnet peer (2487500 to 2487600), with each block's header. Co-Authored-By: Claude Opus 5.5 --- lib/deterministicmnlist/QuorumEntry.js | 107 +++++++--- .../SimplifiedMNListDiff.js | 90 +++++---- .../SimplifiedMNListEntry.js | 57 +++++- test-d/index.test-d.ts | 11 ++ test/deterministicmnlist/QuorumEntry.js | 20 ++ .../SimplifiedMNListDiff.js | 183 ++++++++++++++++-- .../SimplifiedMNListEntry.js | 40 ++++ test/fixtures/core-p2p-mnlistdiffs.json | 24 +++ typings/deterministicmnlist/QuorumEntry.d.ts | 14 ++ .../SimplifiedMNListDiff.d.ts | 11 +- .../SimplifiedMNListEntry.d.ts | 21 ++ 11 files changed, 501 insertions(+), 77 deletions(-) create mode 100644 test/fixtures/core-p2p-mnlistdiffs.json diff --git a/lib/deterministicmnlist/QuorumEntry.js b/lib/deterministicmnlist/QuorumEntry.js index 109a4ae27..515772e38 100644 --- a/lib/deterministicmnlist/QuorumEntry.js +++ b/lib/deterministicmnlist/QuorumEntry.js @@ -83,6 +83,36 @@ function QuorumEntry(arg, llmqParamsOverride = {}) { } } +/** + * Reads the fields that precede a commitment's bit sets + * @param {BufferReader} bufferReader + * @return {{version: number, llmqType: number, quorumHash: string, quorumIndex: number}} + */ +function readCommitmentHead(bufferReader) { + const version = bufferReader.readUInt16LE(); + return { + version, + llmqType: bufferReader.readUInt8(), + quorumHash: bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'), + quorumIndex: isHashQuorumIndexRequired(version) ? bufferReader.readInt16LE() : 0, + }; +} + +/** + * Reads the fields that follow a commitment's bit sets + * @param {BufferReader} bufferReader + * @return {{quorumPublicKey: string, quorumVvecHash: string, quorumSig: string, + * membersSig: string}} + */ +function readCommitmentTail(bufferReader) { + return { + quorumPublicKey: bufferReader.read(BLS_PUBLIC_KEY_SIZE).toString('hex'), + quorumVvecHash: bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'), + quorumSig: bufferReader.read(BLS_SIGNATURE_SIZE).toString('hex'), + membersSig: bufferReader.read(BLS_SIGNATURE_SIZE).toString('hex'), + }; +} + /** * Parse buffer and returns QuorumEntry * @param {Buffer} buffer @@ -117,17 +147,7 @@ QuorumEntry.fromBuffer = function fromBuffer(buffer, llmqParamsOverride = {}) { return SMLQuorumEntry; } SMLQuorumEntry.isOutdatedRPC = false; - SMLQuorumEntry.version = bufferReader.readUInt16LE(); - SMLQuorumEntry.llmqType = bufferReader.readUInt8(); - SMLQuorumEntry.quorumHash = bufferReader - .readReverse(constants.SHA256_HASH_SIZE) - .toString('hex'); - - if (isHashQuorumIndexRequired(SMLQuorumEntry.version)) { - SMLQuorumEntry.quorumIndex = bufferReader.readInt16LE(); - } else { - SMLQuorumEntry.quorumIndex = 0; - } + Object.assign(SMLQuorumEntry, readCommitmentHead(bufferReader)); SMLQuorumEntry.signersCount = bufferReader.readVarintNum(); const signersBytesToRead = @@ -141,22 +161,63 @@ QuorumEntry.fromBuffer = function fromBuffer(buffer, llmqParamsOverride = {}) { SMLQuorumEntry.validMembers = bufferReader .read(validMembersBytesToRead) .toString('hex'); - SMLQuorumEntry.quorumPublicKey = bufferReader - .read(BLS_PUBLIC_KEY_SIZE) - .toString('hex'); - SMLQuorumEntry.quorumVvecHash = bufferReader - .readReverse(SHA256_HASH_SIZE) - .toString('hex'); - SMLQuorumEntry.quorumSig = bufferReader - .read(BLS_SIGNATURE_SIZE) - .toString('hex'); - SMLQuorumEntry.membersSig = bufferReader - .read(BLS_SIGNATURE_SIZE) - .toString('hex'); + Object.assign(SMLQuorumEntry, readCommitmentTail(bufferReader)); return SMLQuorumEntry; }; +/** + * Reads Core's dynamic bit set: its bit count, then the bits packed into bytes + * @param {BufferReader} bufferReader + * @param {Object} [params] - LLMQ type params, if known; Core requires the + * bit count to equal their size + * @return {{count: number, bits: string}} number of set bits, packed bits hex + */ +function readDynamicBitSet(bufferReader, params) { + const size = bufferReader.readVarintNum(); + if (params && size !== params.size) { + throw new Error(`Expected a bit set of ${params.size} bits, got ${size}`); + } + const bytesCount = Math.floor((size + 7) / 8); + if (bytesCount > bufferReader.buf.length - bufferReader.pos) { + throw new Error(`Bit set of ${size} bits is longer than the remaining data`); + } + const bytes = bufferReader.read(bytesCount); + let count = 0; + for (let i = 0; i < size; i += 1) { + // eslint-disable-next-line no-bitwise + count += (bytes[Math.floor(i / 8)] >> (i % 8)) & 1; + } + return { count, bits: bytes.toString('hex') }; +} + +/** + * Reads a final commitment as Core sends it in the P2P `mnlistdiff` message, + * the layout toBufferForHashing() writes. Like Core's RPC, signersCount and + * validMembersCount are the numbers of set bits. + * @param {BufferReader} bufferReader + * @param {Object} [llmqParamsOverride] + * @return {QuorumEntry} + */ +QuorumEntry.fromP2PBufferReader = function fromP2PBufferReader( + bufferReader, + llmqParamsOverride = {} +) { + const obj = readCommitmentHead(bufferReader); + + // Unknown types are read without the size check, but can't be serialized + const params = _.merge({}, constants.LLMQ_TYPE_PARAMS, llmqParamsOverride)[obj.llmqType]; + const signers = readDynamicBitSet(bufferReader, params); + obj.signersCount = signers.count; + obj.signers = signers.bits; + const validMembers = readDynamicBitSet(bufferReader, params); + obj.validMembersCount = validMembers.count; + obj.validMembers = validMembers.bits; + Object.assign(obj, readCommitmentTail(bufferReader)); + + return QuorumEntry.fromObject(obj, llmqParamsOverride); +}; + /** * @param {string} string * @param {Object} llmqParamsOverride diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 06d6f9c31..92996d28f 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -2,7 +2,8 @@ const _ = require('lodash'); const BufferReader = require('../encoding/bufferreader'); const BufferWriter = require('../encoding/bufferwriter'); const BufferUtil = require('../util/buffer'); -const isHexString = require('../util/js').isHexa; +const $ = require('../util/preconditions'); +const { isHexa: isHexString, isHexStringOfSize } = require('../util/js'); const SimplifiedMNListEntry = require('./SimplifiedMNListEntry'); const QuorumEntry = require('./QuorumEntry'); @@ -27,6 +28,8 @@ const Networks = require('../networks'); * @property {Array} newQuorums * @property {string} merkleRootMNList - merkle root of the whole mn list * @property {string} merkleRootQuorums - merkle root of the quorum list + * @property {Array>} [quorumsCLSigs] - chainlock signatures + * used for quorum rotation, each mapped to indexes into newQuorums */ function SimplifiedMNListDiff(arg, network, llmqParamsOverride = {}) { if (arg) { @@ -51,7 +54,8 @@ function SimplifiedMNListDiff(arg, network, llmqParamsOverride = {}) { } /** - * Creates MnListDiff from a Buffer. + * Creates MnListDiff from a Buffer holding Core's P2P `mnlistdiff` message + * (protocol 70230 and later, unchanged through 70242). * @param {Buffer} buffer * @param {string} [network] * @param {Object} [llmqParamsOverride] @@ -61,6 +65,7 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam const bufferReader = new BufferReader(Buffer.from(buffer)); const data = {}; + data.nVersion = bufferReader.readUInt16LE(); data.baseBlockHash = bufferReader .readReverse(constants.SHA256_HASH_SIZE) .toString('hex'); @@ -71,8 +76,6 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam data.cbTxMerkleTree = PartialMerkleTree.fromBufferReader(bufferReader); data.cbTx = new Transaction().fromBufferReader(bufferReader); - data.nVersion = bufferReader.readUInt16LE(); - const deletedMNsCount = bufferReader.readVarintNum(); data.deletedMNs = []; for (let i = 0; i < deletedMNsCount; i += 1) { @@ -84,28 +87,8 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam const mnListSize = bufferReader.readVarintNum(); data.mnList = []; for (let i = 0; i < mnListSize; i += 1) { - const entryBufferParts = [ - bufferReader.read(constants.SML_ENTRY_VERSION_1_SIZE) - ]; - - if (data.nVersion >= 2) { - const type = bufferReader.readUInt16LE(); - - const typeBuffer = Buffer.alloc(2); - typeBuffer.writeUInt16LE(type); - - entryBufferParts.push(typeBuffer); - - if (type === constants.MASTERNODE_TYPE_HP) { - entryBufferParts.push(bufferReader.read(constants.SML_ENTRY_TYPE_2_ADDITION_SIZE)); - } - } - data.mnList.push( - SimplifiedMNListEntry.fromBuffer( - Buffer.concat(entryBufferParts), - network - ) + SimplifiedMNListEntry.fromP2PBufferReader(bufferReader, network) ); } @@ -123,10 +106,28 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam const newQuorumsSize = bufferReader.readVarintNum(); data.newQuorums = []; for (let i = 0; i < newQuorumsSize; i += 1) { - const quorumEntrySize = bufferReader.readVarintNum(); - const entry = bufferReader.read(quorumEntrySize); + data.newQuorums.push( + QuorumEntry.fromP2PBufferReader(bufferReader, llmqParamsOverride) + ); + } - data.newQuorums.push(QuorumEntry.fromBuffer(entry, llmqParamsOverride)); + // Core's RPC form: [{ : [indexes into newQuorums] }, ...] + const quorumsCLSigsCount = bufferReader.readVarintNum(); + data.quorumsCLSigs = []; + for (let i = 0; i < quorumsCLSigsCount; i += 1) { + const signature = bufferReader + .read(constants.BLS_SIGNATURE_SIZE) + .toString('hex'); + const indexesCount = bufferReader.readVarintNum(); + const indexes = []; + for (let j = 0; j < indexesCount; j += 1) { + indexes.push(bufferReader.readUInt16LE()); + } + data.quorumsCLSigs.push({ [signature]: indexes }); + } + + if (!bufferReader.finished()) { + throw new Error('MnListDiff is followed by unexpected data'); } data.merkleRootMNList = data.cbTx.extraPayload.merkleRootMNList; @@ -155,20 +156,21 @@ SimplifiedMNListDiff.fromHexString = function fromHexString( }; /** - * Serializes mnlist diff to a Buffer + * Serializes mnlist diff as Core's P2P `mnlistdiff` message (protocol 70230 + * and later). Quorums need their LLMQ type params, for the bit set sizes. * @return {Buffer} */ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { const bufferWriter = new BufferWriter(); + // Diffs from Core RPC that predate nVersion are version 1 + bufferWriter.writeUInt16LE(typeof this.nVersion === 'number' ? this.nVersion : 1); bufferWriter.write(Buffer.from(this.baseBlockHash, 'hex').reverse()); bufferWriter.write(Buffer.from(this.blockHash, 'hex').reverse()); bufferWriter.write(this.cbTxMerkleTree.toBuffer()); bufferWriter.write(this.cbTx.toBuffer()); - bufferWriter.writeInt16LE(this.nVersion); - bufferWriter.writeVarintNum(this.deletedMNs.length); this.deletedMNs.forEach((deleteMNHash) => { bufferWriter.write(Buffer.from(deleteMNHash, 'hex').reverse()); @@ -176,7 +178,7 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { bufferWriter.writeVarintNum(this.mnList.length); this.mnList.forEach((simplifiedMNListEntry) => { - bufferWriter.write(simplifiedMNListEntry.toBuffer()); + bufferWriter.write(simplifiedMNListEntry.toP2PBuffer()); }); bufferWriter.writeVarintNum(this.deletedQuorums.length); @@ -187,8 +189,22 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { bufferWriter.writeVarintNum(this.newQuorums.length); this.newQuorums.forEach((quorumEntry) => { - bufferWriter.writeVarintNum(quorumEntry.toBuffer().length); - bufferWriter.write(quorumEntry.toBuffer()); + // the hashed serialization is the one Core sends + bufferWriter.write(quorumEntry.toBufferForHashing()); + }); + + const quorumsCLSigs = this.quorumsCLSigs || []; + bufferWriter.writeVarintNum(quorumsCLSigs.length); + quorumsCLSigs.forEach((quorumsCLSig) => { + const [signature] = Object.keys(quorumsCLSig); + const indexes = quorumsCLSig[signature]; + $.checkArgument( + isHexStringOfSize(signature, constants.BLS_SIGNATURE_SIZE * 2), + 'Expected quorumsCLSigs keys to be bls signatures' + ); + bufferWriter.write(Buffer.from(signature, 'hex')); + bufferWriter.writeVarintNum(indexes.length); + indexes.forEach((index) => bufferWriter.writeUInt16LE(index)); }); return bufferWriter.toBuffer(); @@ -232,6 +248,9 @@ SimplifiedMNListDiff.fromObject = function fromObject(obj, network, llmqParamsOv if (obj.merkleRootQuorums) { simplifiedMNListDiff.merkleRootQuorums = obj.merkleRootQuorums; } + if (obj.quorumsCLSigs) { + simplifiedMNListDiff.quorumsCLSigs = _.cloneDeep(obj.quorumsCLSigs); + } if (simplifiedMNListDiff.mnList.length > 0) { if ( @@ -296,6 +315,9 @@ SimplifiedMNListDiff.prototype.toObject = function toObject() { if (this.merkleRootQuorums) { obj.merkleRootQuorums = this.merkleRootQuorums; } + if (this.quorumsCLSigs) { + obj.quorumsCLSigs = _.cloneDeep(this.quorumsCLSigs); + } return obj; }; diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index c856a7d1e..7f3ac426d 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -7,7 +7,7 @@ const Hash = require('../crypto/hash'); const constants = require('../constants'); const utils = require('../util/js'); const ipUtils = require('../util/ip'); -const { extNetInfoToBuffer } = require('../util/netinfo'); +const { extNetInfoToBuffer, readExtNetInfo } = require('../util/netinfo'); const Address = require('../address'); const Networks = require('../networks'); @@ -22,6 +22,7 @@ const { BLS_PUBLIC_KEY_SIZE, PUBKEY_ID_SIZE, PLATFORM_NODE_ID_SIZE, + MASTERNODE_TYPE_BASIC, MASTERNODE_TYPE_HP, PROTX_EXT_ADDR_VERSION, } = constants; @@ -128,6 +129,47 @@ SimplifiedMNListEntry.fromBuffer = function fromBuffer(buffer, network) { return SimplifiedMNListEntry.fromObject(object); }; +/** + * Reads an entry as Core sends it in the P2P `mnlistdiff` message: + * nVersion followed by the hashed fields (see toP2PBuffer) + * @param {BufferReader} bufferReader + * @param {string} [network] + * @return {SimplifiedMNListEntry} + */ +SimplifiedMNListEntry.fromP2PBufferReader = function fromP2PBufferReader(bufferReader, network) { + const object = {}; + + object.nVersion = bufferReader.readUInt16LE(); + object.proRegTxHash = bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'); + object.confirmedHash = bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'); + if (object.nVersion >= PROTX_EXT_ADDR_VERSION) { + object.addresses = readExtNetInfo(bufferReader); + } else { + object.service = parseIp(bufferReader.read(ipUtils.IP_AND_PORT_SIZE)); + } + object.pubKeyOperator = bufferReader.read(BLS_PUBLIC_KEY_SIZE).toString('hex'); + object.votingAddress = Address.fromPublicKeyHash( + bufferReader.read(PUBKEY_ID_SIZE), + network + ).toString(); + object.isValid = Boolean(bufferReader.readUInt8()); + + // Version 1 entries carry no type; Core reads them as regular masternodes, + // though its RPC can report type 1 for revoked version 1 evonodes + object.nType = MASTERNODE_TYPE_BASIC; + if (object.nVersion >= 2) { + object.nType = bufferReader.readUInt16LE(); + if (object.nType === MASTERNODE_TYPE_HP) { + if (object.nVersion < PROTX_EXT_ADDR_VERSION) { + object.platformHTTPPort = bufferReader.readUInt16LE(); + } + object.platformNodeID = bufferReader.readReverse(PLATFORM_NODE_ID_SIZE).toString('hex'); + } + } + + return SimplifiedMNListEntry.fromObject(object); +}; + /** * @param {string} string * @param {string} [network] @@ -176,6 +218,19 @@ SimplifiedMNListEntry.prototype.toBuffer = function toBuffer() { return bufferWriter.toBuffer(); }; +/** + * Serialize SML entry as Core sends it in the P2P `mnlistdiff` message + * (protocol 70228 and later): nVersion, then the fields toBuffer() hashes + * @return {Buffer} + */ +SimplifiedMNListEntry.prototype.toP2PBuffer = function toP2PBuffer() { + const bufferWriter = new BufferWriter(); + // Entries from Core RPC that predate nVersion are version 1 (legacy BLS) + bufferWriter.writeUInt16LE(typeof this.nVersion === 'number' ? this.nVersion : 1); + bufferWriter.write(this.toBuffer()); + return bufferWriter.toBuffer(); +}; + /** * Create SMLEntry from an object * @param {SMLEntry} obj diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index bcc277b7b..290be8a61 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -112,3 +112,14 @@ expectType(proUpServTxPayload.platformHTTPPort); declare const proUpRegTxPayload: ProUpRegTxPayload; expectType(proUpRegTxPayload.payouts); expectType(proUpRegTxPayload.scriptPayout); + +// Core P2P masternode list diffs +declare const bufferReader: BufferReader; +declare const SMLEntryClass: typeof SimplifiedMNListEntry; +declare const QuorumEntryClass: typeof QuorumEntry; +expectType(SMLEntryClass.fromP2PBufferReader(bufferReader, 'testnet')); +expectType(QuorumEntryClass.fromP2PBufferReader(bufferReader)); +declare const smlEntry: SimplifiedMNListEntry; +expectType(smlEntry.toP2PBuffer()); +declare const smlDiff: SimplifiedMNListDiff; +expectType[] | undefined>(smlDiff.quorumsCLSigs); diff --git a/test/deterministicmnlist/QuorumEntry.js b/test/deterministicmnlist/QuorumEntry.js index dbb1ba7fa..28fc9865e 100644 --- a/test/deterministicmnlist/QuorumEntry.js +++ b/test/deterministicmnlist/QuorumEntry.js @@ -4,6 +4,7 @@ var expect = require('chai').expect; var QuorumEntry = require('../../lib/deterministicmnlist/QuorumEntry'); var SimplifiedMNList = require('../../lib/deterministicmnlist/SimplifiedMNList'); +var BufferReader = require('../../lib/encoding/bufferreader'); var SMNListFixture = require('../fixtures/mnList'); const DashcoreLib = require("../../index"); @@ -99,6 +100,25 @@ describe('QuorumEntry', function () { expect(buffer.toString('hex')).to.be.equal(quorumEntryHex); }); }); + describe('fromP2PBufferReader', function () { + it('Should read the commitment Core sends and count set bits like Core RPC', function () { + // 50-member bit sets with 48 signers and 49 valid members + var hex = quorumEntryHex.replace('0cff0f0cff0f', '32fffffffffffe0232ffffffffffff02'); + var reader = new BufferReader(Buffer.from(hex, 'hex')); + var entry = QuorumEntry.fromP2PBufferReader(reader, { + 107: { size: 50 }, + }); + + expect(reader.finished()).to.be.true; + expect(entry.toObject()).to.be.deep.equal(Object.assign({}, quorumEntryJSON, { + signersCount: 48, + signers: 'fffffffffffe02', + validMembersCount: 49, + validMembers: 'ffffffffffff02', + })); + expect(entry.toBufferForHashing().toString('hex')).to.be.equal(hex); + }); + }); describe('to buffer', function () { it('Should be able to generate correct buffer', function () { var entry = QuorumEntry.fromBuffer(Buffer.from(quorumEntryHex, 'hex')); diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index 26c493291..7095f5c66 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -6,9 +6,15 @@ var SMNListFixture = require('../fixtures/mnList'); var expect = require('chai').expect; var sinon = require('sinon'); var Networks = require('../../lib/networks'); +var BlockHeader = require('../../lib/block/blockheader'); +var MerkleBlock = require('../../lib/block/merkleblock'); +var SimplifiedMNList = require('../../lib/deterministicmnlist/SimplifiedMNList'); -var mnListDiffHexString = - '256b871b6d78a3659545e11576be763efb97a1343937a7d78ebdc1ff0800000070c7a7deb8dae2650c6c867fcb12771f6d54952cb14fa325296fa60ccc020000030000000399b849a70a77784018b41be5abe6e6baa1041d873f32c1c287f9b70303c6d05e33853149ef48201ddbf71c12c2b889e315837faedb3ec0ce21903e4728547919256e3b70dea340b85a3b33ae0188d20ff7c7016bc493b401a380920e9b608992010703000500010000000000000000000000000000000000000000000000000000000000000000ffffffff0502581b0101ffffffff0200694d48010000001976a9144544b54eb482944c315bacfe9b5392ecdc1cd56b88ac801d74ec010000001976a9144544b54eb482944c315bacfe9b5392ecdc1cd56b88ac00000000460200581b000010228246d725173a25631c575a47f33f25973bc749fc61d3fe8f5035f149c6ee545199bcba5e20a8a8d2c8ac39210d6de3e0076e833b2225f15b0625a8738ad9020000000a650e790a074f6e71e270c27d43b9d1cb2513537744d3cd638541c989685e020000650eedb02ef0dd46d5263c72b8e83c932a705a48b7b978b5ae41658c9433010000659f40f8b0272a3ef8bc74f01b3a9f2d8cb90388fba5785e5ef69ceb648601000065f833f4b6f249069ac3f5f9784115b586113af7d6f936a285a574705989010000694228e2454355c39bc167ddfd1c8dae9ef391c4c358d4c053b688d5757700000069f833f4b6f249069ac3f5f9784115b586113af7d6f936a285a5747059890100006b0e790a074f6e71e270c27d43b9d1cb2513537744d3cd638541c989685e0200006b0eedb02ef0dd46d5263c72b8e83c932a705a48b7b978b5ae41658c94330100006b9f40f8b0272a3ef8bc74f01b3a9f2d8cb90388fba5785e5ef69ceb64860100006bf833f4b6f249069ac3f5f9784115b586113af7d6f936a285a5747059890100000afd39010300651a9062a40cc2dde34af8fdccdcb091e957d5804dbc57eb207215e36df60500000cff0f0cff0fb7ce8404327d7e2d51ebcda3ef8b0de5efe3d47d754f345363b4fb3ed1f0f58e0a57e5fdd78b8f540d8e530cc1d9a2950dddc7221af137e9ec65644c01f1c60104ce558e33b2821080691fac2a6034a690a53267f8ce63887ed83924a5b7ff4cc527e9dab4394f794864ea5c940280fe3813f4823a961d2b00bfa14fffa82b7916cebf95a1db362a55430e46cc168a9b3c7f4b956d531b93928044829857058147b96fb3ce4a2ba5101abfe19a56983cab3a6e1deb9e2ecdc2338546d2149809bf8f91639cca400d8dfa73c8f387c11a08000758761486141fe3c7ed5846e6ef15e6a262a6b05030220d5f0b72948d1108b99676a6907db9b7e2e58121a70dd1ea2f6b897d454fe93630fe5825125a13fd39010300652f22c2c0ed659d74d3a3cd234aa2e0ef854c49875514a7559d586d4f1e0400000cff0f0cff0fa56980a7b3cfe15c5f3bbe3fc9ab4f3d73c1cd7da9e8ae537629ebaeb2bac32c90435c5f1258cca230a6c071b5d40897389cbfa08c31efa1155df29fff6c57b2a473f9d146e99ae4fcc097e21421893c96e515a5621917183f8fbbcef6696e01acbf5d4766f067b6809dfb8aaa7a1617102cf1b83acb030fa34ce9b3475c1856057859fd2d76dda81e737d37dcef60ff2f28f0ca586e415b7e62db003fd4ebf32430cb876a3db1f1eda3c1a6599d682e921069ed2cd23e125d984567e7bfd2bd9509c89242eaed8fe832045d789b3b200a9904e5d62ab9c9e6a59067bcf147ed065c8e818abcdf329ef243512de8eebeea0ad234ecca62b1f14b1a2d0022a485d12ea87940af7aac1cddefdec1439999fd3901030065b12d2bd101c3c629859d1990ba2f5a6e1b5d1b01d1805abd71322bf7ca0100000cff0f0cff0f87fc5378cfb956fc0696c091e78fc5113a879fe46818d7f2f690a1cd9b7a6a314ef3958e99f4120d6d6ba1de3d5420ffd7ed6e52266e9f1ecea3657dee4667fdb79ed34fcc71af13baddd13116717602ae6cda610c2d283d61a2a786def53bcf16fc34d6557cb1a8c025a1fe7d5d0a4fac7177054bc00d07860391c7f03e7cae0490af7c663422a95b323161e679a19c35401ae62ddaa0a474ecd95e01125513c0f8361cf68c000459d18e31ebe72ab1838c780da07c11a6076c6459cf020d168191c89e9c5854e23e366f3a884fb5f1c5a6767cd0f5731073dbd920ddefde9614d7dfca0f5dd1cf2d888fee94099b0ef381cc10fb2381c3724837da50e7a3813865f752242c849b4232b391feff5e31fd3901030065f547145bef4d7245728145173abd82530083c426f1ab1b49c427e36d360000000cff0f0cff0f8acfdbcf8deaefb06e03cff275b3807eb81c8c92505b0f48827c6a503471ea75a0a6a37808661d7a4fc028f7a40dda5320684d9670b7129534ce31ebe2923ad4724174f76975aae0f88dca97d8fcb3c1a01f3ec095f8c879b130281f87d3f6f9a2a1691c5c6270c9fad95afa77a2b9bdbfb36683ab4ba406fcdeaff663ade56a0a8373a466c1dc243cfac88c6a89251c7c2602ae5f74c54c5e76eecd63ff1a6a9bee475cbc52a6332775660e43290c48acdcf975a98967eef69cf17ce5f9222f08c50c2e9008c7b4abe99f962c45ccc59ea088f2e2ea66f0236fcbd168d1b3a607a763351a4c60261c6b67bf8b52bcb99a88dff8ae33aa6214773cfc52fa0e8299b051b18d483871599025d92f6a4765fd39010400697fc1936a4507595755d48c39e3daf9146a608e35802416c48a3181d383210000010008ff08ffb0107d065aa3dafcea53b90f7b725e31f948367641c7c6c8b69d7cfad348a3465629975a1c92a3d99fb5a369d3a97d6bba15db82876a91a2a96bcbc75ba61c52dfe33f29885cce4055dd27ae9c55332d9385523156c0acca0b80bdcc01954aa1b98bc748317f29d67b401c19d7a6d4a449e02efce06274cc6c70250008cae8da033d6f63c659d9b2f14dfc5ea97b25a1cca5380ae30d8b6bfa5af5f3511dd0763d2a1d9ab7d7406558be1a37afa2340e91b1c8b0293b63313934374e05e114f7ab7a2ad725dffafca85f0a56ed9f4a1a69af8bd452f5e218b5ec0d1a8094b037184ac54ec66ff6944f3317a09d4c736cefa5685576103e120c0f7c62022af611da3dee4c96e814a90f9bf69255cf2492fd3901040069b12d2bd101c3c629859d1990ba2f5a6e1b5d1b01d1805abd71322bf7ca010000000008ff08ff97b8e0411dd35e02d05675c9d2f996a1675cc9bdeff4ca537fcb8f237d3e5dfad1e9e612ff13c1f7b84e4caaca349fc12d13a726fe7624c8e553c415692861747ba6b779f948c19428997b45115f8f598df857bedab3020c058572e38f9ccd9172c44b3949338ac812eb9167cf9dc349324984f7eb340631cd9fd9564fac7c91100fd679a9977de87998ba814988c3930f043145271c6b6e9b7073b2b81226f30b901cb4f6f720356e54d3bb5960d62db8208c2f150585bbf8f199724afe93253ce9b117a2cf6e838c73303fd8e3034bf779fe1d55e1ae7034e48d97d3c52053153e75d4d4cc72fdfc6829cfa7431849d7e24550b52b15c0ff3efce1eff9deb82c41f59d30707aa07d85f0e3afecd59efd390103006b1a9062a40cc2dde34af8fdccdcb091e957d5804dbc57eb207215e36df60500000cff0f0cff0f80534ec19bee96b84f0dadb28433ee0b44aefd69e6dc8e5124cdd494df15269f62f6fb51b91a5cede7d12d138625776f717f72307471790cb57018bad49cb3557ccf9efd7b233aef0088e08025dec3e9a25223b886c6e4bc7af69c8ec844042216e31ca1d7fbace828f844e53be052165846a75f7e1be96f07e9c4e8db0d165e090c7f39c28da0c4188bd9cf7ff3e777af4647289b24c31288bcb6c56a525845c72cbb31fc0d1e600af89e1f5e8301afa30e5281bfd730db1b36e56efdf2f3b8f18eed1c6f9ac8e1028b99eb1ba20adf1c30046eaedb3b94ba261fcf6166b138150afcd2dd02e48bb7bf756f07a50106f28d0389226e11d749df7871dc0d9f58444e25c5605ccaafcaa671454faa6c9cfd390103006b2f22c2c0ed659d74d3a3cd234aa2e0ef854c49875514a7559d586d4f1e0400000cff0f0cff0f88492096daf2dfe7d48fb346ab406a49833e890dfa60c9025b201848c739f5bbfa75f91d8791e84cddc6479172bc72672641231412080e6cedf10568315514f5f4b0dce002ba4132c67543e73901f31db6f0b8044d99cca1efae8372093029b963905bf467cf6263ce16ca3892c3afd12d90b5029b107ae9c7bc8bae3bf3169003f7e078a096702092b5dafc8bfa9978f23e815d3a9c3daecd2b441ab841111d696e95214d2c1a10885bfe8dfcd0ba9190661336c308e08b239f58eb9e1d5e3152c34adbba0b3eaa5f6ba8ee4f0d35f9631743e12cf08e5751ab91cd8e94723614439a493f5e7abf3344469c9c6e39586d69fab0db4232ceede57d4549484eecdbf4184c9253749a9a7defdea867bf8cfd390103006bb12d2bd101c3c629859d1990ba2f5a6e1b5d1b01d1805abd71322bf7ca0100000cff0f0cff0fa2605b9706e06332f269bc53ea02e7dcb24d47d43c541e079171544012a5eb5dd724dedc32f18b5a8726da4a0c9c22f407d0e311f5c48a7340a80efa581925147bde364f8cb6f25cc437958cdcf8206bb69798eda5c074a0374435567a1c7b70995301c148d4b56a9887edf8cc6b934dcaa89bab01f8b25a382e8f752659e95a034fefd1c5dac4de435e059c2c52ecba21a74107477eab9168e8695e4b56ead98a9748ea3914ae283928b895dddc76dcaf18dbee1ac5c5f5050b050b59e90189cbfcdbf5afcd5d2fe1d20ea04c1710bcf8a4d42c41aa9e2b40d5e3da5998931811e8fe4d97a41f27d146df52deaa589e54d0d386c57fd1e12883a296483a8738bc90e99946b5c76205b0415af805db49fd390103006bf547145bef4d7245728145173abd82530083c426f1ab1b49c427e36d360000000cff0f0cff0fada222a1b00c9653f807ea7b254d225b5f8abf3d0f7e8c84089ae96e74ebae63ab3152622e2cc3b4736d30a51b2d8e8db97821bbbcd2ec08b621b3114a85877aaed1028918cabfb17b90694f5915d9e5a2057983f37f96d94528f68d23f4a137a91ea535b53f0916f83f84a18bdfba8ef5bda7ff6c0e2c5b76c0869ee596f56e0f29da122a3bb2f8c34f8837532148104855cdb3dbbe48274b6f28f3f489e1b5b7c77cdb55e92f40099dad5da7332041900b776c89053defa7c78b8ba8fd1a8111a30cd7a9869e613412c8a748f7e9f6e8ae4a6fe18506013a8cf89f9fa18fb010d96863dcb8b84e23e6cc49d6cc760345bdf3e6dd1bc77c91a9fdca307a3fec57c3fd0d491dd4a74ea6a5a69f50087f'; +var coreDiffs = require('../fixtures/core-p2p-mnlistdiffs.json'); +// Core's RPC JSON for the same testnet diffs as coreDiffs.testnetFull and the +// one block after it +var coreDiffsJSON = require('../fixtures/core24-testnet-mnlistdiffs.json'); +var mnListDiffHexString = coreDiffs.testnetIncremental.hex; var mnListDiffJSON = SMNListFixture.getSecondDiff(); describe('SimplifiedMNListDiff', function () { @@ -92,12 +98,6 @@ describe('SimplifiedMNListDiff', function () { }); }); describe('fromBuffer', function () { - it('Should be able to parse a buffer', function () { - var buf = Buffer.from(mnListDiffHexString, 'hex'); - var smlDiff = new SimplifiedMNListDiff(buf, Networks.testnet); - var parsed = smlDiff.toObject(); - expect(parsed).to.be.deep.equal(mnListDiffJSON); - }); it('Should not modify the buffer it parses', function () { var buf = Buffer.from(mnListDiffHexString, 'hex'); @@ -105,20 +105,169 @@ describe('SimplifiedMNListDiff', function () { expect(buf.toString('hex')).to.be.equal(mnListDiffHexString); }); + it('Should throw if the buffer has data after the diff', function () { + expect(function () { + SimplifiedMNListDiff.fromHexString(mnListDiffHexString + '00', 'testnet'); + }).to.throw('MnListDiff is followed by unexpected data'); + }); + describe('with a crafted quorum bit set size', function () { + // the first new quorum is a version 3 llmq_50_60 commitment: version (2 bytes), + // llmqType (1), quorumHash (32), then the signers bit set size, 0x32 (50) + var quorumStart; + var hugeSize = 'ff0000000000002000'; // 2^53 bits + + before(function () { + var diff = SimplifiedMNListDiff.fromHexString(mnListDiffHexString, 'testnet'); + quorumStart = mnListDiffHexString.indexOf( + diff.newQuorums[0].toBufferForHashing().toString('hex') + ); + expect(mnListDiffHexString.substr(quorumStart + 4, 2)).to.be.equal('01'); + expect(mnListDiffHexString.substr(quorumStart + 70, 2)).to.be.equal('32'); + }); + + function craft(llmqType, size) { + return mnListDiffHexString.slice(0, quorumStart + 4) + llmqType + + mnListDiffHexString.slice(quorumStart + 6, quorumStart + 70) + size + + mnListDiffHexString.slice(quorumStart + 72); + } + + it('Should throw if it differs from the size of a known quorum type', function () { + this.timeout(2000); + [hugeSize, '31'].forEach(function (size) { + expect(function () { + SimplifiedMNListDiff.fromHexString(craft('01', size), 'testnet'); + }).to.throw('Expected a bit set of 50 bits, got'); + }); + }); + it('Should throw if it is longer than the data of an unknown quorum type', function () { + this.timeout(2000); + expect(function () { + SimplifiedMNListDiff.fromHexString(craft('c8', hugeSize), 'testnet'); + }).to.throw('Bit set of 9007199254740992 bits is longer than the remaining data'); + }); + }); }); describe('toBuffer', function () { - it('Should be able to serialize data', function () { + it('Should serialize a diff from Core RPC so that fromBuffer reads it back', function () { var buf = new SimplifiedMNListDiff(mnListDiffJSON).toBuffer(); - expect(buf.toString('hex')).to.be.equal(mnListDiffHexString); + var diff = SimplifiedMNListDiff.fromBuffer(buf, Networks.testnet); + + expect(diff.toBuffer()).to.be.deep.equal(buf); + expect(diff.toObject()).to.be.deep.equal( + Object.assign({}, mnListDiffJSON, { quorumsCLSigs: [] }) + ); }); }); - describe('fromHexString', function () { - it('Should be able to create an instance from a hex string', function () { - var diff = SimplifiedMNListDiff.fromHexString( - mnListDiffHexString, - Networks.testnet - ); - expect(diff.toObject()).to.be.deep.equal(mnListDiffJSON); + describe('Core P2P mnlistdiff messages', function () { + Object.keys(coreDiffs).filter(function (name) { + return name !== 'note'; + }).forEach(function (name) { + var vector = coreDiffs[name]; + + describe(name + ' (' + vector.baseHeight + ' to ' + vector.height + ')', function () { + var diff; + + before(function () { + diff = SimplifiedMNListDiff.fromHexString(vector.hex, vector.network); + }); + + it('Should serialize back to the same bytes', function () { + expect(diff.toBuffer().toString('hex')).to.be.equal(vector.hex); + }); + it('Should round-trip through toObject and fromObject', function () { + var copy = SimplifiedMNListDiff.fromObject(diff.toObject(), vector.network); + expect(copy.toBuffer().toString('hex')).to.be.equal(vector.hex); + }); + it('Should round-trip through JSON.stringify and fromJSON', function () { + var copy = SimplifiedMNListDiff.fromJSON(JSON.parse(JSON.stringify(diff))); + expect(copy.toBuffer().toString('hex')).to.be.equal(vector.hex); + }); + it('Should prove the coinbase is in the diff block', function () { + var header = BlockHeader.fromString(vector.blockHeader); + // a merkleblock message is the header followed by a partial merkle tree + var merkleBlock = new MerkleBlock( + Buffer.concat([header.toBuffer(), diff.cbTxMerkleTree.toBuffer()]) + ); + + expect(header.hash).to.be.equal(diff.blockHash); + expect(merkleBlock.validMerkleTree()).to.be.true; + expect(merkleBlock.hasTransaction(diff.cbTx)).to.be.true; + expect(diff.cbTx.extraPayload.height).to.be.equal(vector.height); + }); + }); + }); + + describe('testnetFull', function () { + var vector = coreDiffs.testnetFull; + var diff; + var rpcDiff; + + before(function () { + diff = SimplifiedMNListDiff.fromHexString(vector.hex, 'testnet'); + rpcDiff = SimplifiedMNListDiff.fromObject(coreDiffsJSON[0], 'testnet'); + }); + + it('Should hold the same entries as Core RPC reports', function () { + expect(diff.mnList.length).to.be.equal(coreDiffsJSON[0].mnList.length); + diff.mnList.forEach(function (entry, i) { + var rpcEntry = coreDiffsJSON[0].mnList[i]; + expect(entry.calculateHash()).to.be.deep.equal(rpcDiff.mnList[i].calculateHash()); + expect(entry.proRegTxHash).to.be.equal(rpcEntry.proRegTxHash); + expect(entry.confirmedHash).to.be.equal(rpcEntry.confirmedHash); + expect(entry.nVersion).to.be.equal(rpcEntry.nVersion); + expect(entry.pubKeyOperator).to.be.equal(rpcEntry.pubKeyOperator); + expect(entry.votingAddress).to.be.equal(rpcEntry.votingAddress); + expect(entry.isValid).to.be.equal(rpcEntry.isValid); + // Version 1 entries carry no type on the wire + if (entry.nVersion >= 2) { + expect(entry.nType).to.be.equal(rpcEntry.nType); + expect(entry.platformNodeID).to.be.equal(rpcEntry.platformNodeID); + expect(entry.platformHTTPPort).to.be.equal(rpcEntry.platformHTTPPort); + } + if (entry.nVersion >= 3) { + expect(entry.addresses).to.be.deep.equal(rpcEntry.addresses); + expect(entry.service).to.be.equal(rpcEntry.service); + } + }); + var v3Types = diff.mnList.filter(function (entry) { + return entry.nVersion === 3; + }).map(function (entry) { + return entry.nType; + }); + expect(v3Types).to.include.members([0, 1]); + }); + it('Should hold the same quorums and chainlock signatures as Core RPC reports', function () { + var quorums = diff.newQuorums.map(function (quorum) { + return quorum.toObject(); + }); + expect(quorums).to.be.deep.equal(coreDiffsJSON[0].newQuorums); + expect(diff.deletedQuorums).to.be.deep.equal(coreDiffsJSON[0].deletedQuorums); + expect(diff.quorumsCLSigs).to.be.deep.equal(coreDiffsJSON[0].quorumsCLSigs); + // signers that are not all set must be counted as Core's RPC does + expect(diff.newQuorums.some(function (quorum) { + return quorum.signersCount < quorum.getParams().size; + })).to.be.true; + }); + it('Should serialize Core RPC JSON to the bytes Core sends', function () { + expect(rpcDiff.toBuffer().toString('hex')).to.be.equal(vector.hex); + }); + it('Should match the merkle roots in the coinbase', function () { + var mnList = new SimplifiedMNList(diff); + expect(mnList.calculateMerkleRoot()).to.be.equal(diff.merkleRootMNList); + expect(mnList.calculateMerkleRootQuorums()).to.be.equal(diff.merkleRootQuorums); + }); + it('Should match the merkle roots after applying the incremental diff', function () { + var incremental = SimplifiedMNListDiff.fromHexString( + coreDiffs.testnetIncremental.hex, + 'testnet' + ); + var mnList = new SimplifiedMNList(diff); + + mnList.applyDiff(incremental); + + expect(mnList.calculateMerkleRoot()).to.be.equal(incremental.merkleRootMNList); + expect(mnList.calculateMerkleRootQuorums()).to.be.equal(incremental.merkleRootQuorums); + }); }); }); describe('copy', function () { diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index 5973c57e6..fbc64b7e0 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -5,6 +5,7 @@ var SimplifiedMNListEntry = require('../../lib/deterministicmnlist/SimplifiedMNL var _ = require('lodash'); var expect = require('chai').expect; var Networks = require('../../lib/networks'); +var BufferReader = require('../../lib/encoding/bufferreader'); var smlEntryJSON = { proRegTxHash: @@ -174,6 +175,45 @@ describe('SimplifiedMNListEntry', function () { ); }); }); + describe('P2P serialization', function () { + // Core sends entries as nVersion followed by the hashed fields + [ + ['version 2 evonode', smlEntryJSON, '0200' + smlEntryHex], + ['version 3 evonode', extAddrEvoEntryJSON, '0300' + extAddrEvoEntryHex], + ['version 3 regular masternode', extAddrRegularEntryJSON, '0300' + extAddrRegularEntryHex], + ].forEach(function (testCase) { + it('Should read and write a ' + testCase[0], function () { + var reader = new BufferReader(Buffer.from(testCase[2], 'hex')); + var entry = SimplifiedMNListEntry.fromP2PBufferReader(reader, Networks.testnet); + + expect(reader.finished()).to.be.true; + expect(entry.toObject()).to.be.deep.equal(testCase[1]); + expect(entry.toP2PBuffer().toString('hex')).to.be.equal(testCase[2]); + expect(new SimplifiedMNListEntry(testCase[1]).toP2PBuffer().toString('hex')) + .to.be.equal(testCase[2]); + }); + }); + it('Should read a version 1 entry, which has no type, as a regular masternode', function () { + // the version 1 layout ends after isValid + var hex = '0100' + smlEntryHex.slice(0, 151 * 2); + var reader = new BufferReader(Buffer.from(hex, 'hex')); + var entry = SimplifiedMNListEntry.fromP2PBufferReader(reader, Networks.testnet); + + expect(reader.finished()).to.be.true; + expect(entry.nVersion).to.be.equal(1); + expect(entry.nType).to.be.equal(0); + expect(entry.platformHTTPPort).to.be.undefined; + expect(entry.toP2PBuffer().toString('hex')).to.be.equal(hex); + }); + it('Should not modify the buffer it parses', function () { + var hex = '0300' + extAddrEvoEntryHex; + var buffer = Buffer.from(hex, 'hex'); + + SimplifiedMNListEntry.fromP2PBufferReader(new BufferReader(buffer), Networks.testnet); + + expect(buffer.toString('hex')).to.be.equal(hex); + }); + }); describe('fromObject', function () { it('Should create an instance from a JSONObject', function () { var entry = SimplifiedMNListEntry.fromObject(smlEntryJSON); diff --git a/test/fixtures/core-p2p-mnlistdiffs.json b/test/fixtures/core-p2p-mnlistdiffs.json new file mode 100644 index 000000000..c851a669e --- /dev/null +++ b/test/fixtures/core-p2p-mnlistdiffs.json @@ -0,0 +1,24 @@ +{ + "note": "Raw `mnlistdiff` P2P message payloads (protocol 70242) from Dash Core 24.0.0 testnet and 23.1.8 mainnet (protocol 70240) peers, with the 80-byte header of each diff's block", + "testnetFull": { + "network": "testnet", + "baseHeight": 0, + "height": 1580498, + "blockHeader": "0000002091506f6644e3da1ec6dd78d648ac13b1817a61c923ec2af6154d292f30000000b9698e99cceb2b4e6fe2f4407a3e9667d3fcb4740da7b61ce3ae65d63fe14ed45fdae06a2bd7001e12db0000", + "hex": "0100000000000000000000000000000000000000000000000000000000000000000083c1bfb8bdac446221e973c3f520626ddd4a81fec93a59aef14830b57d0000000800000004a868b2ed8d2301e11fe10af03de99739daabf69fd189ea44fb5aa1c47df56f1d389ffc77b4ade36785589c1161ed70c244ff26dbfda7830e56bad2a9300d165bb9f91e0478b64957632ae2729374a2c7c0f386052c87ac24864baf6bf0f711c70eb605f91fd18b14932cb62fa77678cfffc9ab19d7990e7283e456398d44d5b0010f03000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff037c558c03000000001976a914b489115851ca07a26a5ad8bac3cec3c7dbebd83188ac2ed5fd0300000000016a442ba706000000001976a91464f2b2b84f62d68a2cd7f7f5fb2b5aa75ef716d788ac00000000cf0400d21d1800e852020514f1c94ec6ac3c55ce6a2cb8f1199f5391ca8d22be31dd13b12adc04491ba69a17d37dee7ddf37301ced78994fcbef01bacfb2fb14758a6a7ed9587b00859ce2bf11f430492ac6c7c7b00fa1086ec856988e210899b35e280c9e55be4d7aabc69d5b5b15b3078887d62b567ad00c7378b8d6f3f92c02f0dc2e3a7c78decae6e9285354a1ec342895ad04907552c9841d946897c9950e7b1c0d609b3c417af9a1635a220000000000000000000000000000000000000000000000000000000000000000000000fd8f010100a018bc40ee20e2d5f80be412d81e25890e38ddb4e9d2b15d01e2e786852e4185c5caa6fd8dce5f123e00bd92c553c58a961d86751a02d1a02e7e1d050000000000000000000000000000ffffa747dfd44e1e80b7defb6341399f9e9b4ed7c2d627fc828d0eff9c168165b75b24e5fc6c3f5bc8a9eeaee2bc655fdaa58c0d2f3b1b944b682e8847992b8e9a2531cef5a3169ea5b80c58000100807109b23de7d601fe818e7d79b74ee3cd9199d1e07fedbdabb2e1c74a9ef1349bd6efed873f4ab25cfb1a29d4b1ebbe6e909f64bde69e059f48152e0000000000000000000000000000ffff0314461227130fb7164d86058e2b22c4a6f6917714dfa4a2cb4d54bebbf3c9300ebfe1759b33d15b0b68e32999aae19bf0dd92341e401dc640b7e408921b2c99d18ec5bfee69e61d85b6000100c071fae501f5747d2e0259cecda4e40c06ea9add0ddca036de5c69bf77730c52042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a484e1f842d732a03847819b1e2675ac48b9af4a1c92b310ecacc42c428ff902099cc47d08ecd4616da55d185463855aee99f7933fe2817ece26659df352c7abdf87f8cb7dde58b010100001eb4ffa3feaf12495a2206608116feea16b2070915d56f8ba774df7398a9bb0f854345b4041af285193843258dd3b3c170c66401291ecadc9da135d00a000000000000000000000000ffff6b16c7824e1f1059c4fdcdc32d831534403f7e6587555d74dc1624f6e2bcdba10a099e6c4f8d5f31d4c270231180ded264a009387e6cdd3b064ca33829ca65c37e911259da50d3490ef3000200c016291f16bb938f9be4dedf08c461dc67f03346a7740d9ddcd3834878eb118ee026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a174e1f8160877a911d8bb7d1e75e2320e98cc3233c1f6972cb642424bfcec7c182c56d2c0ebb59e45f788f4d5dbfa2ebff3e3a0063eb51fcfd121e6c3eae23b90b26cd1100217b010100a305357cb7f558f264c99460327d48066e197c70576d01004037472321c1aaf3fab2ea768ba4e4f0d6532aba3653361e8e1706518d787ab2042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a464e1f00eb80b32b60db5d7b03559f6e9205beac8d047689904bdda0bc50987d5f208d39b78ed90a34af7e1e9d44495ca1eb422d0ac80af443e477060c785066014d561253d22d010200a047145aafddfb3e068ec4d899ae23c9ad3ccf908446127f05d49f9a9752b9ed47cf4023b16e6b9674087806016cb4c693f3251bb08432d75592daac2000000000000000000000000000ffff57e5167e4e1f82be9e928577bd4f1050a030418b083d625dbb4ad963494ce146ca818c49879dfec6fb746cae1cda4342b9ad442937cb9764876f34204cc0b83bf6543583ecee98d4a511010100a30504f8d328a048126337270c9625819012b06504530100c0a3e8f9461c4fce0d89476083fef0f16648c3be1e838a7ab016984cb050ca6c042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a2a4e1f0ad4f577d067630f6fd15f4d2aefdb9456d648b71cb7253d47511acc81dd5ddb69a03c848322aa11e5242f66afde5a2a94d596c0f06812174b31eb104a44cb1c94563073010100608c43a6a23324860eb2d8e6212798506bebac53120c591d6b32aeb0e35edc63042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a214e1f13f411bb160a34b3d8254e7c537e1300afed010d4a245e376b81d889020854fb999fe9cbb7430ddee0faf2fe5e711ebbb190542b5c7522f4db19e48033d17c34afd71845010100607007ddbe091843236ed8045a058ecf8b12db8d9027235e92d922593f275755ba5d468e5c009dae6c9b2a16d80a22b97b25443c74d50ac600d19fa03d00000000000000000000000000ffff5fde193c4e1d08b66151b81bd6a08bad2e68810ea07014012d6d804859219958a7fbc293689aa902bd0cd6db7a4699c9e88a4ae8c2c08fd1a9502c58ab103792693e951bf39f10ee46a90001008010db04bfaed8961840df7aba51b0ebd897cd222ac58ac8b1c89570e0d62fb4042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a3e4e1f088905cc3f99e76b3a1abf714a55978d9930c2abdc77a21bd809e452e8c47c35d38e318ec3118e1944cf1a4a8df907c1b4fb462bdbb65e1dc1380a1d4723220a09f3b4dd010200803c39eddd391194e9b5b003c533098e8261a6bd6ad88a6577efc4c11070ae991d774733fb0bb8f632ed3223a21c05a704a71e086f73762d5bc4ec820700000000000000000000000000ffff592803814e1f844e036dd08f2a973014023016dfb5fe4d533be55aeb81da956becd312345aeca4a41b168a51b87ef5e42c3c6892fce06bbc79395663389c2864b7e423e111b9d73b33810000000300800b5c82dde5ef1694b17d0a93dfd03ca8a49c1c31512457e78396f79fa23c76592611046b94ce81ef960d2e026aa6deed97bd6c14ec634d2466fc231001000001030001010104a223f3554e1d0101010104a223f3558f300201010104a223f35505a3ad99da4a07542b221fa6f2335ab2a1071cad6f2dbae230fe82a619b07d039b6270632272ea7269dc5c8241eef139f57c6fc8eacf785fce62cb493c3ce335a12ff0596ff1010100c92e0ebc2cf6df05de73b3a9821e9c590420d03302008067842697bc90d881860908be2ceec1f25026a56a67ae18a3a939d730d44e609a4498ad5a1a0fc61ef0c9b3ecfb0fd1a206086c4ee6b4b9db89ecf37300000000000000000000000000ffff55d1f3034e1fa692633b3f7c4b342c44225247f5ce9358bbc4d1eac4d13a971ad6c67c151d6b8c0920d3a1e9cf8143d38a06523850ded50b0033a1380315eae2693bca07a289babeef02010100a3054c2e06cd1cae739d4fc613ced41f8f184ca8e1e2010061d48e8bd82f0fe9dba3413753c90b695ae75dccee5b3401e76df29b9d33a139744f95133255a8df4bb58e36cdc6e352c6048633caee29608346a9370c03000000000000000000000000ffffc6c74af14e1f0efda51589f86e30cc2305e7388c01ce0309c19a182cf37bced97c7da72236f660c0a395e765e6e06962ecff5a69d7de359c348a574176c210c37a25d4ffd917866fb0a3000100c1b4efa5fe55cfac7b9841af3161d7b27a208f29e5eecf6cd8059b1236b1fab9042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a424e1f0fa5377eb256323aace31b45c3e48ea110404b053cb80e8043bd1e44de1705130548e4ab28738816251ea57a7fc10324289d0729f182f703f2df4990b51e2b103515a32b01010041e5e9c68f8c61bd44136b148f8a196ec435045b41b55058f8dd5f1a8bce69053664815c5b2f45be780c5caeb2e7d3c840283097e4fdd1d499364801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005a3086c1c6e57c19d3613870421059d308d5b16f000200613da7325ea2dbcf6dcefd15c14459d5271836678ffef57166b79e7308f101ae6157be4e38ea989fab79a7c00f143af7d0f5f2f0b32b9470c041f9974900000000000000000000000000ffff8b3c5f474e1f93dd670bb8e78f365b13163ed86f37e1e794c5238b216d02e8335a07a6a30da5065fdd4219909b37c8349fce149c17ec7170c7f963f11ad0f52e65065c54d7e66ae6b1540000000100c1a555222b83dd986987ffb354f541e7696d0dd6d9f28fa9ea4cab5dad785fc92928c180cef0f3ab90e609c93db391cc46e909fb4772d36243465d100000000000000000000000000000ffff2d2056e74e1f128e20333b8b51fb8c72c2d5acafa049758b53279bf78f10a1f32995bd05d4f6313b2bd67fbc48379455d89ef869fa6e65b4b2bd9e302d32234a9bb205db6e51c08767ed00010061d2a534d50fa0c224d58bcb66aba349a11a2b016e3c08ccdf72d076d339f2236e487a6ed3d93d75da4554375469b3894319255bc718fafb62d4a3003e01000000000000000000000000ffff50f0182c4e1f94b3e11094b8781908d100194fa8b47659c2ee17720200f9fcdc2804c557d219e87eb68fb0e6db97822f1f73ab7f846d1516fc10404fc6199c750577dabb9f26800c8652000100812269339ace97eb8839f491bdd5be89fb9f5f1d455f545a1bc6113cd4f8e29fa5059b5cf433f5b3399486d9b5794d84bd2678e2e1678a762707f2010000000000000000000000000000ffff4e2ea1164e1f80174252e0f66a71b7e53f8b32dea5f97a6b39dfc1479c6e355daa415b1ff7733a4bea6bbe3fdf412fed0fb60e5b71d77ec334a85c5cf331176646c2c357732ec06eba39000100c10a2826f3e775d2d7527b131d5e3620647a6926938061a7ff16b7d0b745efb426cb9af0c2dd8555f03dccbc1f209376c344f2a89d021dad06bbe6aa2c02000000000000000000000000ffffb9bba9c14e1f0450dbbbe82df6808151b83a46f8c531cb240eccfe65f8f0b49f3717056da7268c14e45f0dd14fff8daed28fd353c1b41546085b56803db4366c2059d4755c8a28796ca300020001ffaaa508180ffcd764acd0f2f63409d60562533506a1112da5bf112e507a71dd32b3afa4a2300e5681811768ed8e0c7600bae0f6476328d0b18755bd00000000000000000000000000ffff40b032a74e1fa90fce30ea814b244dc767b5d29bf227842226705a0bd2e8589e776a4dd113ed3dada52c6a07f55be90173d6431d8f34b6e879f9bd59a349caa9d490f0bd51988bb1fc34000000010041d72a6a7c2ea07deb4c70e47dbe2842850b0298fbb6c69cba57373cb74abab4c058d2a5ffc21a38eec0bdc42158792a2b85fda96535619f28cab109000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003db6e109e8d2e2f052216a47568b8881f7df245b000100616f1a973ebca29263147f716fdb4978034079720a88350beac0bf9b9fccf838042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a334e1f897fc76b69f1ff4b06535e7a4bc7396fb66b33194effbb72214dbefc2c7cd3220ab6cc39fe4630a513879f9f8dca27e3ea1e4a53ffa1572af733c63b530860885f9c8ad801010081b39cfc075fe97a75d23fc6fc8b0bf9be906392b04b228148a5145403a229f17f41682e1c0331bb55c8df2555ca522850d657f8b5de650f08e31b030000000000000000000000000000ffffa747dfd44e1f84657bff1dbf81b2aa50e385d01549f9c3683994ab0b16d5b7e3ede8efe95992bb621ec221c5003d2f9f26fa190ffb2ae21d3baa7509a95b0a9d741549047c2e90ed8aa7000100e14bb33180ae655ddddea98116ea3246ee568a5c130e8af39631f10916c258e56bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d413476596005d1f334fabccd08847756effff3116eece973c077e3acd1aa936f4e51293fa8753de661dc7a03edde24714eb7acdcd18fdd71cb9d2d0a4cd75e08191dff37fbdf446500001002129b96ff3d6dfb88b0266a0958f3f7fcaef4c7947e8b54cb26d8ed31fe8eec6906648c5a900b80bc262dd7b0cc54ed958baccc95d135ab293bad9000000000000000000000000000000ffff68f8f27e4e1f050f3a743867bf78d2e9a3906d15d8400d8d58255771d12828922386e8685f8aeccb8d9d81153f9c2d7da0436a71fe5538130b3e02c119f53aba29a12de6be40f0c6596f00010021b1aeb48918d1500c6da4c963b6922a7ad8db83b62fb99fc755c4cea2a577bc042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a1e4e1f174de56654f2bb6417e15ff06361ea0becc00bd72a3eba0f83b60feac860570769fbf28482a706f10906a1e96dae4a8f2878a638db0682d49249ff8ca872f16653a84f9101010021d15df373dec8ce033a6d43a6f0e2369e3995d3592c6c295158e9a99e32b4c4042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a514e1f0196970badc74d068ec1226ffd4a656313decef59d792237a32e6ff56cd4e43030c436025831a4a3d0306a616f03381020b99d09dfcc0f9b69fa97738487f20cc6c57868010100419aa9fed4f35fcb986c50fbaa0c7555c68f8a9876968c63c6f92064ff06f1fe7b34629e1f9f156e4e3720ed76745a9966adb50fb513f1285263d5050000000000000000000000000000ffff2d30a8104e1f842476e8d82327adfb9b617a7ac3f62868946c0c4b6b0e365747cfb8825b8b79ba0eb1fa62e8583ae7102f59bf70c7c7ce2342a602ce6bd150809591377ecf31971558ca0001004172e5a561e36ae49358bc4c6c37ff688f54a05ae8842b496b86feb71f06b886bbaf9ff7a4ffcf3931de9233fa8e151f187bf30235000e3b5fb102b01200000000000000000000000000ffff6deb47384e1f8d1412ff39045ef39c2e19a75cb3ad986afc14c3139ed0a3392b41d471558676029a8137f95b0ba0e7315bf11c497f0fc8270f9d208c75006659cedd927f04ccf829242c00010002d4fa244fbfdb6378f87fb2078363a01305096b272031eaf2af2d1e77ac8fc7dd1c2dffd75cd9fcbbf25810aa2a3000d4333bc9ac1cf9dce747140bb1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000102a8370b2b437e8e4750ac05a712c524a6e3c5900010062f0e57a0772b9313555c77c1234aad8f8dbbba68402004950876c2c99bfa1dddd1c2dffd75cd9fcbbf25810aa2a3000d4333bc9ac1cf9dce747140bb10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e0b2a999f6d3d5180bc0a7cbfc57886b3caeace00030082607466905f4091ec6db6df160559605139b246282485a548f4ea38255da1e6c7397e9cc7e7fc3906807f77b36aa3e6389652e4f7771feafd84fbb10e000000010100010101044e1146af4dbbadad41e17d64581f58fcbc8dff2845a9a10c2e57db782dc5fd41a175729e59048dc3fb82d9f0620eeaac3eacadced01a4d29551a50fabceb0c248be984667982c4b7525d0100000100c21c409e1a11dd13d2f81877c63b0c802e3e5b860e3ae43dd929c000374f8b18c66e783d97bee0c61568799866c19bd0772fb1fd7015d3e9552565180000000000000000000000000000ffff761f230d4e21831193814d5cddb0268d276281ff7356b9cbe560bbcb6c9c55f12a53b0dfdb60ed5570c9c4bdd39d8a0728dfb2b0596fa7755e80caba03e54606c89f955d1d4b9b85d91c000100e2f8c6900028cc807e87e3c4d09f23726da5388a83e20f7aa8d23e1b80ca35f725c1addd1ad4c97ac677405576e385ec630d4167f1fdd624fbf2ac030000000000000000000000000000ffff22da81624e1f8cf9b3235f77637f144728584ca13d1d3fd47450ad392a510beb2425e0d88f6a3354f0cbdd26d4e6152d38899c025aa365802f3ece78b624b0739a8002fe35327ab85b7000010022aa9381fd5632124c682f1b498ab52bb1509d7d307e9c53b35daf1dbf9ffff54364b2b68b5020a930796eaa75567538ecd934cb5d371fa8ab9dc301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005bd941a8832f305921d4f2bdf7946d8ac1b6b379000100421684ace5bf21173abee1a68c966880bf93d60f17683e2b94c1ed515d5ae23ff3bfb0d7a45eee0eac355f2c8b58052591d53d96f193c0af1bc3380a0000000000000000000000000000ffff86d15a704e1f91386d3acee0bf9044cce40a07515289589b68fc9b8c8e5d184471ed7982106b1e11587af4c9e983883baec00b67e473d71dc86bda7e222f78c9e45f22deae9437028650000100420fad05dae1e8c22d9e09ed7d02b191b8cb8775317feb4ffcda3c73343c01176bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d4134765ba836747d419d09200e404aa3500fc5d51f044fc01fd1a9f452324c8c14ab90ccd75887b6bae1599cdd458e738a53587f25caf3605e05011b3abef72e467ab5334e74d6863000100821b492fdb5e25efbaf7dbaa82cd8aa9b5227495b3eed3e323cf92c633058dbe365d7a9de501086dbd238c8476092ad180186b88aa6616ea530bf5070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000091b4363730ea1c02de53aaade78508ee41c38f9c000100c2db98a6bff0f6044e212e766ae31421d43ccf06d9831de1d61e3ad18c0a1ffb042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a494e1f02ce863d0843ca66b4a64d94c0d84ec15980ea04e4444ac4d4188f38cc0da4d6d2360b8a2046725b682862255af6a48c083a4e9632bcb5c6490a92b3c3b05979bcc866f5010100024a3ab7c146c32be22d3a69ea33dda2df3d31c636a93ff7f6d87e78afde0a1d042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a364e1f8b7c76ec03f7ae0dc9be41fb9168906ef0d0d4de74f0ad8c5cf0a30483879b5203ae5d7c6aeee5b92998444bc10f68ec68a589741e0d3d06e5aae5ae008f2a8b11426a8c01030002769272e89a947728aebb2b1ce38a35e47ea334ee53fe29e8a6734cf439ee0c042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d8450000000101000101010444437a354e1fa80f24b5e040dcbf86c3f468dd28bf45d9e41fbcd127fad56669d9afe358dcdc26e42f0f8b19997b1741dbb99c553aa6b73068d00c048915c175450c366478f783bc455b010000010022a7834ea2416d54b0fbe309b4eb38a7fc2e5d17e434ba394ad3091ad9600ada042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a394e1f1099dcddc6560d1039b0edb91bd700e5deae0cba43163fa289a80c2bd22335b5b0e7a1fb8f5494c0e6360e73a12fe0a89c14a51ad09e0eedf4d49b11a779aebe5755a4e801010022ff95f3392f9ca21be3601f0cc61989aff67ab0f08d1f8d09ebe4f1b984c1baaa2b5033568b70cec7062265924ed182cb5606dde746099e31a2b7020000000000000000000000000000ffff03dd1d174e1f8bf25d66d63197e3144f6fa17ad92ae38cc11b143027fb91dcf5c20fde6e52bde7f46f2789e6fa84573197d8085389dd5519b11029baf89428cad6fd4e70e3bb09f356f100020023d8aab98a2e8f71a40887665f786aee6caff71abbf4743b9afcbd0a00ac84430c5abfbe03b04d75f843d51269605397a38bde4dcf0a1ba7d10abbfe0400000000000000000000000000ffff55d1f3074e1fa08769d8e414245828d46b3411da93c2c62188f6e5168b0b32e6a04e5efc4af076f6792e10dbb0b6a3896ed77f4314fb8c88081bb1e0e81ed8fc2407330caada78ed3a95010100a3052c77bd9a6313ffa39d5c2b14875eb0fcca0cf66501008384b2330fc26f5ba5ff492e15006e42521cbe351ed42fbc45e4d17320d537b03fb1af5817b316a19cd3aafe04e5c0c499c45eeb613b75ff8865a40b0000000000000000000000000000000000000000000009a5e37e3b9e556a7d7fe7cda1b54682351d4d1f6ecd331d98816db0696a5389c4a09bfc57f66f0a27120302ceadb078bf9ef9d3fee586e471698a3f70eaff9dcc96ba84000100234524f1ecf913cc326592cd1416f67ce1b4c36f58f30a6984e11d8f01e16708de8d01abe32760d948069962e3d81b37acd5933f8bf716a4411169000000000000000000000000000000ffffa516d5954e1f881cec3eab18eaeb916d3f234fae24363b68faa705f51a6efb06f83adef70f73fe28ee70bf4c8300ec4f77f017dbf7f06de6137f220482eab495340caf21f19261b1e84100020043e5f7410ba48e35d57114fcbb98372bc808948c6c25eded4efaec1b8ca69405f37d09c1ebc59238a7456c5b4ce03c408ccb096f19f2378adf9ec97cf400000000000000000000000000ffffbc7424a14e1fa4dc9f6f159977565bbde1257b7bfa52cb0e7762ca2106da425074a1fc821e1c1478f5f1989d4cddc14d0a65eafcfca6916d7ec6890496e40ec4e7903a20183628e63bc30000000200838d0c4f3580ff5602ef3e0bdb8dba2247a56cb1557d43aa1fd1f062e7b598b6af237796d18341710fd6ec7afcf10a1a9ca69e53184059bddfcac81a3100000000000000000000000000ffff12c3324e4e1fa90febb8c2b031ae7ca222debc10f12fa0a71fcef84e2d92f13c1ad192c42e371324bbb66c86e24434754e2864ba14ce2e3252f64967541fc0092b78aac3dfbb7fdfa8d20000000100a3ddf7716c62a29cf5f259446c51122a518b7b3165284ff8636b67ae65def9b469ee9f2add6f18c3496f5dd67566ce6b0f5de80e2f070382e51285bf0d00000000000000000000000000ffff010101014e1f016a16472319f62f71bb60e38038aa8cb93a301ff6c3727f75f4d770428d71d032fdbd27c5d03dc56ef1d658fefe79546960122148b84c5f044a812c35d5380281a0f02e000200c3357bf95fb8fc8628840460745358db1d007bfdd88cb2be27931e07608e3bd109e6f409a21638e9a82ce854c58593bf70c73b515becb820bc4bdf980700000000000000000000000000ffff2ee029ad4e1fb90302b52893e2ff51e468c2ef2e9f42507e71c6c9ec43689070acb15a0caa1952ed0dfa659cbf85516c794111bc3ccd9cbf4a0a2b932575285a85c8475247e5063a551a0000000100e31d716e9009884c5063f5e7c751cd3f03304cb37ee37f43f67382f3d943141f70f83517c7f6c3bdaf58572091885cc2673605cf36b247ef4db70608000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004d532ed2827d7bea5c49ac0e1cf57d6dabe11e06000100231e411943c6d4826d78f6b6f8d227f4726eb19de7edf25da1bc88a159caa94b19af5aca73f9f272c83575f05d7f7995e345f9255cacdd8da7938a11f101000000000000000000000000ffff0343bb9b4e1f8b7381340a3e266498248137c146a3c82e36c27c196bac05772dd7b22132912bcfdc1263e2761245b6871b47dba982a61f69134183fab7568ebf7eef6c97f80b3ed603ae000100c31ff5109b565396c664717a5147bd7eea9eb7ffd29541714ecc2c4f26fe51758913680eb0236b652eb1fe84b9535517ac863d4a8a2527d21b3bf4000000000000000000000000000000ffffa1bd43194e1f0becd48c0d44ca6fbff3825f55c35a6f70024f2b8f4f939260d40b5b51c11cdfff85f7d0444a1a9cb8fc45bacd237b31abf73c421d3d73583ebcd9fdf55fe8b3013a82230002000358ffd0b6835dd812312e75fb93156b95ffb4faf29542ca8c958f9220b94c27f0b601148b421813e1631d897f85b9e6436d82289d42ebe60cb2662e3f00000000000000000000000000ffffa7564b874e1f9561a686a3a58768c0792d699480035d5345b95a8030cb3d5fa5d2e7dcfbb6a8d5371c75e166a116c1c66542de2c4a6c164d2cfad6d706d26a6e5166aa600eb92eaaa993000100a305117786967d8423c40aa52b6c68aeec3b2bf2133101000378aaa2e2ec71b47ab267aa9b55ec5422740471ce1651c2aee7cad48d8f0f61042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a254e1f90ea47f22be1644834d8756793f2308f2c5b40afd16ebb98d29a3bd37e437990d4d5930ccfa56c1ea0b4e51d05a49f23bec1b55112fe6dee11593a3ec19e9d712855521801010003aa33e3c8829a5d40df1f3006d4ec8772b0dcbd1a36995364197630bd7901b0caff9aa8378b4e090614186b7f273f20ad3266498c4addeff5c72e1c0000000000000000000000000000ffffc38d8f314e1f878386a8d07cf79e1dc6963d4cdcd7a4af6ef7e350cad3e1373e45fb86fdd9390a77366ccffda6ebe1470c45b0f75910307e6b1e1ebb35dc358870ed74feb6563617e40d000100035a4baea91732fc217a20da6b7b6759e80f26f09383def9794b0cee1ed0803a85bc5faf11681e1eacd93ac4a807fbebc818350b12eea0b49b04300100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e3f6150f12df79d8bf1fa5513edcc000b93d388a000100045480c439ccaf9f38afff4a07e8a212735cdea7e7e8f2511c0883e2583e2b68ef6d852f7e1d547e18f881e4cb053d531b046c0750a8c620553524c81800000000000000000000000000ffff40c13ece4e1f05f2269374676476f00068b7cb168d124b7b780a92e8564e18edf45d77497abd9debf186ee98001a0c9a6dfccbab7a0af4aa6fd6b27d9649267b4ae48e3be5399c89d63a000100a488a1ca6fd40ae8a170928c232ff2109d11338efe218447bb58ffdeeba345163c305a87ab0207b41b271338ce0912d0e27ac116b69774f014b7b0ea1b01000000000000000000000000ffff68f8872c4e1f0a5c47983c44aa99ce5f04b32cfbdff42e7f92b1410558559dcbff3ca8aacc3c2fcaf05db6f021a9f335ba05b9af4c52dec4f1bdf91c23a8ed56738a32f2795855bc9827000100c4eca0c58c7f6be21f57ca22938b405aa7b70d1d7670a30ce00d83c145e6a0d332fb097e425849c5ad93ab25b92b58f57427416f28fac2f9c5d7fa000000000000000000000000000000000000000000000005b7d7aee629c25efc5604104a3a9af1e23663464e0505a057e68cf12317834160597fcf80287a94e98f171a8c79a2a91e88661c273d73b44bbf942d3f1cfc145539969d000100249147dc56394eea19a987b95f6088bd88e9df840e4c7989e2e6a6520bad3e1fb180a8ee02ac30f1609b32b33d9b48b93f838f353eca68fec5ede3164201000000000000000000000000ffff344820cf4e1f1641b24598bd24e49c4c2c59d027567d89f2e7315e53fabafb508e48a93b48f32525fd9dd9266b0ca4bef5d08b9605afe1aea521fd5d9c90c17704c6a7b653c6fdd42ce8000100a4f5cb0858d3831a0a4c38d391a9d9c1db1aca1cce2ce7cb1b6ed87d4929fbed2cf74e8cf4f70cbe8d4169db6e5d941815bd64edf919b434b1286d271206000000000000000000000000ffff0b7a212d4e1f8d052595c653122cccb964230a5404399634e7cc6b3fa9314b54678c28d2f9c4854baa7be02845937bb0de35e43cfbd435d72ae28122e666401fbc5f914da502815aedd3000200c4dd9a97bc37a65224b5289ff8bb875d17c2f1df4cd07b828d05f75229b1e88de026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a144e1f9502bb884b3437d65c0e025e49fb00ff6ea9f55d5bcdc36330b46c8bd18be9126b7a6d7f35f558ef8040f2c2284500a5300904176cc6023370b2462964c12c5979b9f411010100a30502b38580015433ac23013e5c8231c5d2b57f7b33010024ee4b80978891cc74cb110a92d09c6e2013b868bc0145667fc7733677cbb8eeda1a04a5298ec6d6ab7e4a3758875a820516b2ff90440c3825a6d9120000000000000000000000000000ffff030d2293271104cab5bc1d73f5f8299feeecc0bee2d76f27c3b2a56a7e2fc1f927e495ac9b2a0560b7d82fd06fa8fce4af69d0fcb10b87de47ade017f67b717616f8bd4f5fc3005dbe8500010064d23aab4726b9b743c705cf8a9349a438f43ef9cc08f93e0c36f9656b7cbb7cdf54faf1598e2513bf8b0afad7bfc08a361655c15109c02211d6c2dd0600000000000000000000000000ffff68f85c624e1f136de56a265eb21c006bd312a0353c7c3eed46f4f63c301c348fb5d5de8f965c9b60ac6a4ae805d0d241e4942821ed9af46b2001f6dd71cf0fa69aea9f5a71ff6142302400010084ca5074ce4d40b329e07eb1453915553fe207f63d79c5626cdc801746b1eef3042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a4a4e1f90051db915bd86bd938746c14440b11ee3b2801cbc6d6c1c912e8b41ea5eb1d8f852abf220ae91ecdb6da094846c1ba83ecf5e66d9c0373f20f86d3fefdce2165c19c7be010100e486f4ba0e05313dd7066a25389ecf44861451bff33cfc639517aee4b8f7b3b18def264b1ab32efeb22ff1bc8916e07072c3b2e5c26d9043b42d3ae55301000000000000000000000000ffff6ca0877f4e1f0d7a075032c423dfd82adaba63256db7c7a0ab10eecc99544fd628e76840759ce5fc0f27ad83197f464371a1b3530952c61ae923d66d3e7a9a82b85fe5eb375e966b1091000200449b34ec269e1dc2e636b0294a8ad5e0a50d1b6c66ea4b446f6cdc84113b2df8ffab1b00d68805ade7922061be4b91e529316df5ff625cf31bbca47a2301000000000000000000000000ffff037ffd564e1b8458274be8fddb6b8685d753bb151ebe32a9021fab91228611a81c3c70b287b607a81388b46aabb16518494f912777667d354fb6b06f9f41e73aef311024f576db99e6c300000001002438facfc3b253cc62375a88d338848ab45f030e882547619efb302a0211be7aff9bda40e8cf851d13a4102465ca1a74f0c741e2d48dbf290f30ce060000000000000000000000000000ffff128891a54e1e8edd5cbdd7b381c92ac7de638440bb1ad417af0e82fece69432f36930a6defd3faa0d53d79bc3347ef684eb1e470abbcd6f5f660f9cf13ff1ff56a13042be5cdb214c1c100020024e85b1daec424104cbd7808e20c11661ea20f1a2ad7c1a9938283d3315af185e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a0a4e1fac3026b3e3023db1db9ec8e3b7678761820a2a6e96e7a5d9a39b1894170f9cea7765d3d131d60fa9d17492ba560fb1f90f89b9ac5ac8f31255f692a3eb0283d0ef3be436010100a3057c771e4de42b161d0c869c38a8d11638d406c56801004431d5c73ecb095daa699363d0a19e5b6e549414e6608cecd1188dbf551a777f87642a37e81f3d63e25a4426e06087954c872fde1894ea3c937517060000000000000000000000000000ffff2f4b449a4e1f842b8e5b5cc0841de193f440d5fa3e0b4a34df7fffa798fb8c3df46fa31187162cc3b3ecf929689ae35e04cbac6e069ff0a5dd8f973cec456a2423da87b5f1aa97f282e0000100444947144b4830f61467feba8295002de6fb48bcac6eaef828bf708165a412ee66522efacbce81bc3a35ea83e03b7fd2e8ab1b1025c5bec187ae60119200000000000000000000000000ffff34ce62c44e1f121a1cade221b1eadcc0c7a02a03508551dfb97b959ae1511d4cae47b503b39ba0fb37b984e4010164378513edfaf0729a870b5d4ccfe0068e92a6cb4cd35915ef94df9c0002004451b225cc3d73931b2cf7650a72ce22fcf2db4ad465f30ca635332b7d70a30b8dcb2f4f1e6b808472e3fc481e98887117c14b2e476cff4089ebef491a00000000000000000000000000ffff2d5e3a614e1f8005472acfeff13c058fb5dbb0eb641c7e1efbdf590b61e149d5331ce5b12c10e42461b8b70f04c704557348b3b2a3e4d4788322314e747c9aa7a74bf4e08e0cd2326227000100a305d6ca01d5e8bde42836d79d397fb9cdba0b7174360100250dccf685be96396e92687a8ce39e4538751a900a346483aaa9223b778c0cb083bbfdf4db4b783ff62c2c387a1e7ab3736fcceb26fe90019ef1808b2c00000000000000000000000000ffffae22e96e4e1f926eed90600b93cd05453899e96db8dfa36d2c71c3209e1660738c6b3af11473b5169c8f8dfacd89ad1bc92a481978f7ce7f101b7f075273c892063b1b8571311737a576000100a595db826aa16bc6fd6665b7cae78966cb41f8a25f9bcc818ba944d245c1d5b5387fa9c9f93571bcdbf2a480460b3010a45d6d969f6d488a877335000000000000000000000000000000ffff336b04264e1f8f4fed7576bd1e31d45225788c1c96836dbc85b3ece3b77fbe4ede0f5f784f138ef6b32884e3345915a758364d1e58232e1496064dd3a7a8c0a5cda78f3b4a4073ffa2390002004552c7abf240fe5ebd8ecadaa434f4d814ffea5c6865483039389447302d53375874898442ad3b18469da9a1bde26a1f93cb74d06ded25f4792861136700000000000000000000000000ffffc38d008f4e1f90252ab3c93815b60fa87f1cba4726a20edd61102c6b0881170543c01af39c3d00e93c70c83d1d8a98459cbacb1457993ea17b2051a9a9c32149b2732da594398fef9258000100a30571ca4989688a2698395214c4e9fcd689a862ee060100c5ba9d7534d9ba000bf5787804c9ecdf53fd457799dfe7ed72e4bf34e76de7a69906d6e2a830c62dbecf4a41efd17923b5d8e382d0b48be90ba1ff49660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e6bbd756d65c821406c82f5d520c000f2d562e30002008527334f080d51fd2b6b82f562015701eb2d869a26413d2713235c9acc809f95a3e1f8fa60eb5dc2e599432228a114dc64b611edf0615f28439400365800000000000000000000000000ffff052d6c124e1f89faf4977b1390702a58db649bd81cd61e8f8aa0d2e1dc627031d1794da234088c18b0b4e31ff49aecb3d05635bc4943d463adb52e6944043ed8f88956e94903509b24270000000100e5d44bb510f7522960e5f5fbabac98fc5744665cd53e817fd370b3d89769905d042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a2e4e1f155b2a1aa71cbb3b5fc81b00242805a6d573826679a2b6a5c49bec714829322efe8af0c1640ba3b59811c70e17b488a5431c84baf52eb97d5546ed1ee44872c30772a30a010100e5644d115172b5b59c92b110d3aa9c816455b157fbe4edb5c04f133cdf3e6c760dfa4668c81ef4087103d204ae406f37d7a8e591c894bb06ba126f10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008fce46d5cc1aec576dbf5ba0ada049a2c2c64fb3000100a60c10dcfab0748f399153c47909dbd55e31c02aaacef2f52ef01a94f6727d61042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a244e1f067ec5f7cb5511ba2bf10aa09eac4107d76fd53edeb2fd94edef4555171dbe3ed7dc6cfe37b087390af61a6aa269182b17acdf555cdcf9dec1b41f2b44511123917d2998010100e6f0f87be3456dac022e662e98593bff81f97f0000a0d5b06d582a0513c965a57760f33a17bac779a5397b8b706c4ef23aecfa0aae9e1272884044000000000000000000000000000000ffff91efeb104e1f01e584b5723fec78495744b68b971fd654f16b016d676ffdbac01b2c64f319675eda577f5eaa5cf5379e95418c61ab10d2a33a64d0e54ed860973f6c22ad36aebba1dc9e000100260db6fd104668297f032b538d026a68ea82a592d6e8f4a11d992002c156f1696bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d4134765e68e72ce4ecb7e37c0ba5376c77ec364606b796eccd05d80583a36da42d57421c21d3ccc3b3105ab18f87901e03ce09a0076a035432f549ce35f39ae28b45f19965340d82a000100c6f98324c39dba83ba0d461f61c3dea1471debb903f83eb4d8db792fa6ce530bd1c310e4699917a51541450b5d2e7b8f56e2267df3bd1e10da2352afbe00000000000000000000000000ffff5fb3fbb64e1f90403255a5c2aef92a899cf01080a78446f07e0a25fe391a81791c37eddba6e82aee9b8b86b7aa4f44129637146221c9eaa8b45af937c2eefe766f3bc3ab753329aa44380001000682c86d9afb864697190081adb9b1c4b0268b079edc428e41368fcb1655eff8042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a204e1f015ae7a4f88fd79e4659c4b24b32f24d1e92106b867a2c23d1d084cfedd0e2766edd3f0a77f274acd4d1d53fb1ff0218baee5acad0f663430ff83c4e9b9fcb0122789520010100e6660e41ee6fab97c87e4a018aaf9b830a064aa2968c5cc597ad689e7566841be4691de8074655741a573ebe01c47f7486f7720d56dee1d291c3826b6d02000000000000000000000000ffff23f704404e1f0b0a6390cd90308c8b7adc4374fee6d4c1d0f467d543dec6e306922e7e78d06ddff70e6adf6b2410cdaaf0d7fbab39ba46d82dfecb82e98abd5b694ffa54558168fa593c0002006609326328e57347b000ec6a97404e0c9af6aaa9057ea744cdef36a05b181b6de026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a0d4e1fb8a2161c64bfdc7d621df51de569911a219f718bad4d6058dcca9bddf6696d43ddc4c1e3cf91640c93f820e5680efac3f3a83d259634fbfc3e3c4ed50cdcab6598fce20b010100a3056fee3e6e0ebaa195bbb879ab6ae761ea16f074c30100667592fd071e8e17bd8c9bff238c14a0d69b9516759083a92a6801b54923691c09ab803afcb97fa1b0dced6b11ceffc2a11a7411f4e71c5d2f9dbf833f00000000000000000000000000ffff2cf298cb4e1f0923d28fcb1fb8b90bf8281f2f50b6a109f2f5b17a4e1653e193ae58980a03b3538ceb82c6b4e1e986ad40d08c63e330a164232c5ce6f1a1448084d628145eedb09f0147000100664a0c663eede7e26fb443b4cbef28f79e5ab6874568557cb79cf75e55753e3a69f94da29881541abe9eb79ceb5ae5fce958a5d880a1250129f8ac46d400000000000000000000000000ffff675076154e1f87a55d353f1c76f34d45486140b3242762e03d9f688b7be28be4389f552b7a057e3a014f7f654cf7da7260b5ec1c15e50aeea2780fdcce25eb1ff50c0edafd09e0397ec100010066f63efc54784de714835f1199821dbbfac1c49bb5d836ad6ac7268861d861719e4f98209ac0950b0726d009a9922e8b7e553876475a191ec0e07b220f0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009676669d0daf4a6ac06239eae91b43b4469cecb4000100472419cad37dfe438ce60cfb70b9156af1d2bf6e1874fe958ba18f229b7af37b5a575b641b2b69ccc7e60e41ef3e26c390c870332f599357a13776de8900000000000000000000000000ffff090807064e1f897af9fdc7920426089efaabbae8aacd61ea4306c0a2c89b140c9d3a69a56084dabcea352d2e1ff8e1f3ae127313e989e6dd610e8d417424f55f90b5c468011307051eca00010087f8bcdc3641a1d3fe3e8776e1c123048cccb4d25615e2e6213fe62fe47f1e96da370a6260cdd4843cf2eaf1c5ce5a4d5de95cc9d6280d7ecb9e97291d01000000000000000000000000ffffa76370174e1f804fadcd7b5dade6f9f577fe663cdf86f1483b71e6fd8e7c5cc4b981c0ee086412b16c796ce8fa3f7b6445fbee866640a656c3104b5aa2a042680aef3494b62eb3ac3e7e00010047f962f47fc21cba4c4256bb383cf366089eedafe639cf909831042679a7be0fc011aefb025f52be8a8cf9c9db5e521fb8164cf7df2755d628da850400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ddba67c250a9c4121d546702cc9d29f65007c8550001008789c5ef265771c98bfe558dccee7987198a1ee0a63acfad19cf50981e4d4d44e61028f0847d8c16f92488990a062e651e50bbce2c1e6ccb7de538120000000000000000000000000000ffffcf9af29d4e1f958adbe9f954ec23983c4be5788e86e0df30fed1d6852136376b49c1a24e0fe1da1178b23dec4ea098b9e355aba8de0b3cc1fe7d3423d6a9a847723ecdee0368320171ad00010007a61deef4c6336a378b05c9af5db085afaf8daffcf97fadd141f1e6d77fabdff8ade1d5f841bd4e0ba3e48bc82ce6bdfe6e68396c8922be00ef7e000000000000000000000000000000ffff91efeb124e1f0b4282cdfe1cd639e60b6c58b2f210bfe6b57f8f247cc5b55673d188ef458270c7314f7128b286a3326b9ab6109bd2ff4c90d0d2b49179ba1311496241623928b4e9bfc6000200a75a0705a546822ec7adc984695d7232d70ad86c9ac7fd93e7a377ba60fa7cf9da8038fb786dd69546ac46fa8b5192928b22d3ebfc57bb00a7db0dea58000000000000000000000000000000000000000000b925506853bc6d953ac37321d0582ff6b2541740955eef79d0e41ea8306514931943f78cf7a5d438c0b0a6978dc87a298ae4703072003e803d0d5226320e2600dab137ea0000000100c73a8b2277538ebebeee5e6e6d8090997130ab6b6922804040bb467b86c6a88b73411e62028ef165eb00b9864ee52f374382715f92b2ab61ada34c160000000000000000000000000000ffff22d2f6b94e1f8b5d53516c0c7134efabc77f5f7d19b6e289b5e8befc35ca5d77626a252e659888fdd09a7c9bb286dd9fc4d73025bcd7b2ea896299dd86bc7c3353a40d354be43ca26372000200c707d1a3b331b8cbfa95fb879085bc7905a7a6c2cc271f52c7d8c243460b1130618d13e51fc41774dab94832b4b49e3cd31f49be1c05ed011ce5f38e7901000000000000000000000000ffffa6469a724e1f8cf6a03c7a0bd8c38c1fc536a013f93afc8224cbfc79d8b0157c7080117ec9c1c60198c3fb3886ec472aeaf80bd5fffc52bfa12608d37422d52423c3a71f9574ffa8267f0001001027b19b0191dd308e8dc1a09d86b34b9fe82239f9c3010028e886392e04e2e498d1e255f6b59d75e411554c7ac053f27180bb15f0bc3fcd64ad295e48132f2af038955df077ff2b8a8a36e1d8e74c613fd218039101000000000000000000000000ffff3ccdda054e1f91da7eb4d0d0f78e4aa5379d45c779105a0d809a04497186d53743876e31874d663f965507339259ce2b99fd3ea4c2757fffc5375fa606d4066bfd9f4fd211b3c5d59a95000200089d7f09afddce572996c8fb89fa5c8d623b07f9e4fa12b0f58f5468182c3e73b6bfd4a56a431cc10c268988b114205650f437450bbdfb955a027695be00000000000000000000000000ffff97f455b24e1fa8284d7f998de96142e7e9e78641b5889062af51fc107a094e76811d632ebf62472b2663468a8d7db497dc11ff41320ae91d119e80a59ade62644150600e5edb9e114f3b010000010068a5d395f0b4cdbaf29a120f485216bf1c012d0f0d79c56a68e6b0d18b382de6dd1c2dffd75cd9fcbbf25810aa2a3000d4333bc9ac1cf9dce747140bb1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000884338c3fb33289e04212abd9465b3cb2b7f4a06000100888dbe6df4d824c05bf806a0722965200a32b1c4e2cffe3aededd6066e21fa686761aa4d9496cde9477b1ef872738132462fc74fc06e6a8dbe99e60fa002000000000000000000000000ffff9f45480c4e1f039715a9bc06634ab10b432e3a9d446d436b4584f65c19aef93c69d07802690df0b51d81da6ff9a8de1542c40edb0b1af07b6cfed0116e02792156cfa9f595b52a3d87c8000200a84e24a5ec9c888430c2cd89a781852d3e641fd499aab23324fa44839787b605e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a184e1f80f8efb42f65ed9650078785be5d13e6e90eb9df87a99261d4de34df2b4b79a9c9b8c5e1aec7ac068ebef14636ceac4cec83a5f23ec17737f590153eb7eed16f7b2f1327010100a3055872bc65a323257b585ec95658766f4d4000eb8f01002897d9875cf221b3a813bc7021e962a49338a5e0def2259b99d17e6a0ee2200d740557665772cd580d184520f843e33956db260503e6a3f5d801630e0000000000000000000000000000ffff9f4145f54e1f14bf71456476fa02cfe3de9735eaa10513e943db4576667128051f34692ce042c90cbb9dbb268d3ebf89205e5c8e2afc3e6504110505f16171693fa5f8582c03d7e0450a000100c86353dd241e72ff1703cd03eb947b00ed5b8794db235ae4bd0ed1005fb3eeb938c2d11cbf2bcf39df66686e6bcc7ab47e71d722e593457896ae63010000000000000000000000000000ffff9de628664e1c8f420a082c56b30c9bd8492394d83066a8d03628a7c8e3eac27486377e2648bdb3cebcc4fdd16cb4cab1341e480fa439da5ea047322750daebfc463a87ec042108d63ce9000100e8cbc0cfc994f63fdc8ce785732e2d961e4b21d721610ffec2c2500263a411f3080ca6a2c87d5f5efd0a7a13d46c5c71399b5e60297597ac7cd4822a0000000000000000000000000000ffff6a334e464e1e01841a16adc73f0224e6544d0cc57057ee2508c906706307ef8561908bd476594ff1e825798faac54c8f8a66583a3dba57f73d89afcfbd3c34ff42e19f32af77dfd7a899000100685ce50d7f351dae246ab6d23033b1fb7ad8368467fb348086822fe6ce77d8a4398f99dae01aee02f174c49075e3e92422227fd6a57d4a44d7419c060000000000000000000000000000ffff5350e5d54e1f16415af54406658be9ea44d82b6b502bb90d93e32997484533a8a71a4ed98d12cea3709d84a5835b6ad8ed48d3101633d0669e929f06ac80c05365d9558ad79cbb78f2c0000100685c08be982ca2c27a877e3a552817f0c1d64e53a2ab6633bfac33d11a4ca5c22380e1134e1222e7a10ce0a9cf918ee514d04f0a69e7102ea4ea24100000000000000000000000000000ffff9de66e564e1f85f01c97f8e6d601ed269d4fd1d33b456c5c940aecc45b084c0f8d9ddac26d6fb7c5cc1eb817a41bca401e5c9c4ff85607b48a04d0a73deac14da9df36ca49ffb14233910001000827ccfdc4d571aae5cf0c6a03fa2333274c7127c597330bc3d9eeaf4dc0ea5864a8b81a46fa9ec8109fe6e6a47a7d30522c84646d2277118ec9f51d0d0a000000000000000000000000ffff0b7a212c4e1f046e3d8efdf54e1cf182c8eb894c4bc1a2845fd3e8e20a383a80435dd131e34e69ee562cc7f31628960e1ad57fdc538a44c8afafbb5c35b797e7a8876f8932829f99c6c000010008c793da5bdaf2a08c5ce7312a794e75c36a766be9416a2b381f45e7f9182c9072c143476d845183f9f4afa593ed6a5aeec8286b370e93e0db664d285b00000000000000000000000000ffff53e977834e1f198e877839e3a29d8e1e0f0f8db6d9e533902dac30db36ab1330fc4e1e45427b658fac866dd8bb65b0c68b263ebc695f747d840bce00916fa12947def52684f03a37618b00020048a311aefc2b65fce613d9f995c403cd5a8cdc088c936d34108c977b763e659bcffb90a56a936f35568172c053c3bdae89b1fb114b25664ccb49be7a4700000000000000000000000000ffff44437a044e1f80ab2701950ef7a1e068b97d02a2ed1e64a8d38dd2d0e5a0d9de5432b3e1ad2c318109d89326d48ab6e285d6ac0bb22eda3d9736bf16f34dcea4be38d0eb404335ee21c8010100a305bbe0dd0cf1785a19d29dbbf75dc018a11e888507010048f753e30613104d2eea72327fdb484a33b5a0e928f083a5d10336ff66b1d1156bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff369ad3f265a618e4c1013002f690ead979628cecef981034bf9277813035637c376ce9dd04b1b379c4abd45bc5ea969a894fa0d0ebc26b78d97d6d777cdadb430c72745d53aa169eed8f0001004978e987b1a6bcfb9d486cafc3c8e0529c46386ac96021f3105913bedf6b1d7edae5a8aa5892e3e6c8f119f214d423275c8e50f143bd61616ce296010000000000000000000000000000ffff2d4db0104e1f0d5a850d41302b179b9009a4969537c5cbf7f0145c94de4306a4e09115ec00248cb1aa76cf04249e2a5104b5cfa868792226272b80f5aa9fce78cc60704f1dbcf81715010001008914d3c03997deb1dbaeffa6531a5e185c5f2b07c5cc137cd7283b2c054e6eec38ffb43735af18d84f29f9f027eaabca7616a0df616e3d3e8112de92ce01000000000000000000000000ffffc380664b4e1f1926b68942b544b4e17347c5e0d28ba91453984294c7679965f3a1d3cdcd9f5bf80f2c28a48b503f301bada544798968cebc041331aa21d5fdce3e44b4a2a520889d4ba4000200e934256a123362fdbbd31f9089f39a513196c91820b3a6542f60b2c98f24866f09e6f409a21638e9a82ce854c58593bf70c73b515becb820bc4bdf980700000000000000000000000000ffff05a1d7f24e1fb4bf5f174b66dd12f3648347c5846697247187fe8eb43169f27e1d3247e8ec210cb36547289e2aa7ddc34222714670775db443b2ad2d4f03bd29bd302af26b13068f692f000100a30598c76fafeca892e0b6e4380fbbeb239709bc3d06010009d9ca008cb9263341c196659ea2f751507cc6b6199a9c5b6d40bd136ea3f3f7bf917bdc5b734cddb7bc5822f6846b0c99c9199a70ca6ba0ee3c050f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000954c2b52b2f86d89f81b8eb64b0944a1b1033e38000100494518f9dc4badd3b5615b35236830653f746fb17dd00c266a9014d4d80f963fb11b2330d1a9e7dad84b3e962c0992323ad4b6a2f7a3ee3175fe6c06000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001db884e38658de13612ec46d636c67f26d3c43ab000100695dbbea77cd2a5391bdd59b4b328a240c30b5dcc2ff06fea2c78d7d51956344db1ba09da6ccacb1a7d4f4b14f697fdbfd43188f58a14f7736a77b01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006af70254b26589b80f06011b3de26f7fbf715b8e000100c9adca4033fbb3fd459671d4e62585b6cfc8a313b5eec0c8ace665633d5e139b7f1fcd16244270262e3b74927d2e65145a8be0f781c80e1f9a6d2ef4b100000000000000000000000000ffff12ec1e464e1f88ec95047130c4b310db3b6585c2fbe9453c7f5ff43ee5fe844e74c25766488fae8ad62052ec76a6c2b584029e1b1b04d89c5ba79b9553a13f783b50673e15283eff3a3b000100092ed131880f8eaad47a1fb19d31c87f5f821f757eb4125977eb834e746329cd9e66ab48f787b066aedc4f9a668aad7b5c13b9b7f95f328ea03c2a995d00000000000000000000000000ffff2d200cea4e1f04ed653b556ea9d8b8391dc59ce49796b15c118e37daa2eb42326f72571b195cf21e814f93338ac9488000dc3d8b4d55f6cdfba67f9b6228a325c80d5723557d8a2006d8000200c94a4f3fe127472a4592ae88bd5c3bf0961761c5b6aed82a7a90761c2c1d26ffe026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a1b4e1f967796952922dcc5208a3848ab85a787e4592df2d8ce36a29369b0b3a9576073651075039e1377873aa8c67514ad2726b14f661da8227a41acef9f8096df2afd2a192aaf010100a3059cfcce0dd3e16f033500f2ad96c8c7521b5be06701002993a3a976afc9af823b354ed8b60b5315e6aeafb04df534e1a061d14915543a3eb42314547cd77536cd1cccf5db363de1cbc28fafe3538b0f7c9a170e00000000000000000000000000ffff8b3b23144e1f0a97f109f81f15cc0b6af0a57ec93cf9f201789fd28494baf1840594d4bb233cb790f4ba434c49ecb6a1ebba61beec0328c20f725ddcc44c5d6c728b05b886805ca125af000100497324f53dfe04f3892427a246791edaf2ade762f199589b044e4ec42002afea3074a06bd37b893444564b73fb924f6f604540ae1a4292521a1627090000000000000000000000000000ffff9fcb22634e1f0452f32ac367f352d6ef53f984667db9ad658bf940292eae2440024e5af9445f7a7a618d536c4743c9de4c3e07b6a5f916c792ef93229ad52721ad5b08249eac743fb74f000100693bbe97a5f05edd01c77233fe95f8789d02559d37f9a689f7b31267a291b2c693d30745374d676048cbc4580520eca862ca43470d953af9cb2097aeaa00000000000000000000000000ffff2e65f3544e1f83152e6489f210094f5dd558373a1ce9abf36bb8001d4f35b3dea74f7ae74baf859abd22238c682ac94cab82eeb58aab5d068e0fe2b3fd8bf7983a2d806e797c522b75a9000200895f6ad52a27fde4add081c029df4b272a81439ce612d5272eaf9cc1cf90b0d9e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a094e1f86108e551691da2642f37b68bcfbc5bbe9984ca51aca15ee24b6fa9b8690ed62c6ed722d091e04ef617cfc99341fd3587334f54da07d3b9e177f6e57c64f97bb0f982caf010100a305892d1cc512af1d3cfd0f8efb51533c85d3f034700100c9ab04081160c78f70269728c38424f2c4af2263e56529969a2aea3d88a9fe696885e557fb7046fbd7b0fe27142d01ff7a445880b5fdacd67eafc637bc01000000000000000000000000ffff22e180e44e1f18d5da073c85f04213bf2cbc10eaab55f3a2779c0f347e2fb9c869024f30afa57c7054ed6b69f2a03ce928bc9683ecadf573287084ca8cd1ca8ab21ff5436945022466c200020029993fdc71302b0d08af5d3e39ad85ecc43769f83ffdcf75e7829a80fe57e549c51df49169a376435b72aed5cda41832347e46c230899a22de22f7965000000000000000000000000000ffff44437a1d4e1f8f2df81ba65db70eaab625c5fe46f0f5e52a45b25c761686db23b4f18e547cb0d161912dd187302eb6f7c4a9a666a3236cc3fca46aae1c44ea1bab81d506e9f0a3ae1d05010100a3055d92878e4720f0b3c03b82a6419b3edf98472dfe020029b5a218ffe4842991b29dfda254d3a7ea1189d3f3a86a3806121f063bc5b44987cc0d950e8b02155d81693fea6c29a3959e475f4a23a251c1825d135100000000000000000000000000ffff034fb2cf4e1f8e7fdbe2c04139b2d0839c236ddf6640a2bc757791a23edf0eea9526c4da9a8f23f7a94462291c369277605592d756cea4e8d392208cc560a4b1f0f097bc3fba132e95ea000000010049aa692330179f95c1342715102e37777df91cc0f3a4ae7e8f9e214ee97dbb3dcb51fdb1b943d2741f362764fb0722965eb6770b421b4cb8e6d3460d3c04000000000000000000000000ffff342440944e1f139b654f0b1c031e1cf2b934c2d895178875cfe7c6a4f6758f02bc66eea7fc292d0040701acbe31f5e14a911cb061a2f6cc4a7bb877a80c11ae06b988d98305773f93b9800020049be2b744dd8fac4a3968d367ddf71019ac2199086d1c9f7911a9b776c5647f69a4498ad5a1a0fc61ef0c9b3ecfb0fd1a206086c4ee6b4b9db89ecf37300000000000000000000000000ffff55d1f3024e1fb1205eedcd5b3964096f477541d019a0a990f7bf59905eed160622128843b2d277f1c639970e429ba997037667068475d1769a7dd51d17aebb64a38e69042d5aa50a7d31010100a305c1d5bfbc4f6105b6a4e29aaf7911d3f0428b100801000a94e9a705dd8841f52cf30518332578c905573c309365217b9d797f20b441e38a4656a464a2a618e07559761aa3b6ab7cc0e66bf5c972c0533fb5350000000000000000000000000000ffff369d08914e1f029d0298b3ab58f541f566ba5ddd40e8e1e711dca26b1757fd1b707baea16ce77aaf8a836232809a5e1d301a36f204589dbb34ad2ac21a13371e4212a67150bbc0320b2f0001008abc87fbb5018b42be79851aabf0acf62e9459b9c1a64a78f44bd92dc0d1f91b042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a474e1f14e81203caa5c0cc305b5e0f3ae7a388b974a629358e4e83e50a25b2c2a387e3d114c7c82e2b23c25b65585220e63c9983564ef2fc85085dc7f2a155a64fb5d14d89dadc010200ea249c84a35c5c4d767c9effcde8455d607e8b8d9154f5d56829653b53a62f4ca87429402a00af635621bca277c7fa5e63d674140b5fe0928bed71b38900000000000000000000000000ffff97f455304e1f90fd924ac1997c50397612532e96cbe96564436e4cfe587f1cdbf95c977b0138f2183bbfe7f8db3e1fb177b2ec3b69aac80566bf24b2ed33476fc7fc4d40eb1bc7e308fc01000001000a29cc9281c956d30d365c7cab93075a3893d2960a0ed22e2380b77f5b73da3a042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a4b4e1f8c290b31d2e878c2d7235efb0c61f423aa37742a31318e61f8bb0bd6c110a892dc244512fec12a8b0fe7cbb08e12be284c58fc64d6bf36b4cd655f04a3e35ddb1ed7a48b0101002a0daa3b789866a15779d9bb3bf4c1108bbbb82af9414f7a7469b85654fec1ed042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a284e1f06e81b2ac08c3ba6308868ced5075f366013ee8598961bc84150d8dfe9085fdceafc082fd18d3a7bb1338b74584048fc0e42f45a86824089891f17af573e84512f00d8850102004abd3d9b76a34d428159456a2e9722036c2de8da9ef138e7e191d73dd81a7439e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a164e1fb6693296894820bdc3c0ae76f357e544847f10a68f0046f53745370dbe861d57e194ddaf7ff7d5e73cc3f240515c448ebd9857229d4c0509c753afc85d264fe2e4f344a3010100a305b4563e764e296ef17cbb808ca3ed72bcb9f37bec0100cac11525ce0a8c897a57495cedebba859dbae776530759d696213e957bb07d03e15fc250d94081fe2eb9ad7ddb5aab3f92f86f909421936c81e269070000000000000000000000000000ffff80c763bf4e1f8f3afb0dbbfec8610efdb4089f1b163e7f55325f6c0503470e8d49ecf439c848ff9448749e0a383980824994aa5dc50d8fa9e72582f9dc7882e0e40c932d98d302676591000100ea1d9285a4277410ad7f8a478a23e147e75dbf52de4dfa8912476c1d1ae5e5e05b06d81c7f745976b4aa6b10873ac20bc7869461d27ed52000e6f30d8b01000000000000000000000000ffff020304054e1f19224c48bde28c061c99ca2a641ed1f695546fb6f1d103e93b4d8aa5164f5fdced8073ee239baa885cf377c8c173016552b3089828ccd5c5e2929bb45057829bf2cd99000002000af23bc94eb59fae108e08949c3e128eb5a09962d984cc689867955df97856043e3ad8b685c34eb49a8e4edc05d76a10c24349f7b9d16d1ff8a6bb5dec01000000000000000000000000ffff4545444507cf85a8e547858c3772041317eac7dcd53f61fca75a5595870b44081a6bb83e78d28c397222c3c2b3015e651e163f62880b542a58cb756c3edab85a715297c77ff4e02c9bcf00000001006ad616183897963783b37d10d259a5c9c8945f6c44a9bdca811f6348c02f860d2809cdcfc375bc68668f095709bfb241618ed9ccc7d426e652c51428690b000000000000000000000000ffff74cbcc784e1f028f3d9ff027351da67047d78254333d5430c022f800ce5530835d1f048721a82d87bfe959b74aa447908e3c3cacb63aa86191b01824b41f452f494a6d62026fcfc373f7000200aa6248309136c45fc5e13dc128fdbf7c0c69883828bc794edadecc17402403f5e55625d2c966980775dcb8e352b0c9d666b207c822e685af90f3f045d200000000000000000000000000ffffa74708d2752eae85957c684df823b199c7c8f001a70efec163ec830f64787e4a0e020d2b4914434c78b6e9c173b82db346b32d651bb95f68d4c14279d08870f54e337f44afdb72d28a8600010026595b6ddd9532c5863b8bb113ef92d77f77b66f859b0100eaeafb8f8aae2883b2f2e0e023e4e2b8e4c61dbdbd8592922e9ea47a7d40ef3347fda655a6908df732c8979bcf849249abbab19afdb67c9a3d26b04b8d00000000000000000000000000ffff88f471a64e1f0032db28dfb209566356ae396255132f8f8e412d6a59dd5cde82ca347f4fb2a4713d8a49dd2dd9b8019bc2dd3a62745a4f9406e645e1d2a24f68a29124a5ed91782c92080002000a5b7f1478902ecab28b3404f5e79e9020acb9abd31a6e13928c2450d42684b6adf56996e04e36430af272b8c0b1ed1dd57b6146775532c53634ecb5b600000000000000000000000000ffff44437a064e1fb8a150e6472bc281b51537dda3d077264786f207d05c4ec8d314b5883e32b14b15077bf39ccf5d92543a6fd0ee23897802e543987843fbd195e84ff3a28637ab2231d98b010100a30559b537a5cf3ed95e48d8c006d53de9e049dbf9a101004a37c4c6a85c1ba73b84288726c1f43eda96a72f027fcd0910773209b3d0d865d6264ac48c327071e53196e3ff74e03715345b3cf43e95ff0f63ae83e100000000000000000000000000ffff23a155ea4e1f8b98c8e6620c7893f7c26acc50d4e335b74f9cc866019ff0b6af497407fd0ebdb33c2cba0a6f2c4ea9a8aee85f22bf5ffaab53c3c104da464745c096b8c79c91f47b835e000100aad381e4314f1630559db598be088232973e6c9ab61e7da181272675fa70ee72042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a434e1f91f9052f62561db112ddca7df3d914d546866b130124eccb2ae1e8419563e51f239b2efec3d1b3fd388072610939d694889e8cd2ab0cc70e36e566648e265c7536a8fcaf010100ca0bede330fead7733cf68bf2f7edf00bc7dc175b6b56fcf1758969b0402df9a6fcd6b82befa2655c42bf1c7fdff20c341485b0de4208930e822253c6e03000000000000000000000000ffff4e2eb95e4e1f0df074095e5498c6f1b76508c3ae700484d8b0d5cd12a990f3dd54e35b47d6fc943af4223d390ddaf8989d883c84b284a7889dad2c33e1f6b8b68aa5cbb7392d9e96bdee0001006ac187c54507ab1144de2acc9fea9477628be19bba18db3434ee456aa187504dcc6d42235a73f538f345c112ebea18a4f6f19f53455f5ee7d9308941d60a000000000000000000000000ffff411520564e1f8162401aae703d5bb3f53a7b9c5b65b9a94dbd1c207a8da8f1350200a960d1607e89e50b365afb14cf3b700662342a4c336788df1ce1418edfb0b8321db1611264010a7b0001006a59f9b585d75f1b2cb43b7c0fb90a294fa45e0c1f7a432f82139a3ddfbecd3ed2b9c32684bce2de8afb2ba9bc9f7547d3a43fc88bf0a33ae2dddf6e3700000000000000000000000000ffff6c3dc02f4e1f0634f8b926631cb2b14c81720c6130b3f6f5429da1c9dc9c33918b2474b7ffff239caa9b59c7b1a782565052232d052a1bba3b56ededb76c1834f3b3e02c159aba4777a6000100abe4f25c3753453abe3bfde7f0bf6109434b9c7fed47fe972547cbd32784605a255ba1c99b3221641955fdc17d936f0f021e4ed754e209d9d6fc9462b100000000000000000000000000ffffae22e9774e1f8a0ede82d78a0a8f4c2332d431c7be496c3aa09349ed3b2db30f7eb7dcc7b6e580a9d71f7d76bdaca1b3670e0cf4cd3cce7f101b7f075273c892063b1b8571311737a5760001000bd1e4e68fcb354b25185c443da6badc1d61b02f10dade4f68e1ce372d01dd6fdd1c2dffd75cd9fcbbf25810aa2a3000d4333bc9ac1cf9dce747140bb10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004146e5a5841fbed699d9a1e0666340bd1caea1a7000100abd2a494509514d4f548ccf21736f7f65d348c5c297fa91ab7271b63570677df9d63015573ee0a8596c91c15f24f8157227cf08ff013d0a2c6e1f373ca01000000000000000000000000ffffa123e12e4e1f865e0561715f28e996d2c63e1ed462c82d403ad9300369529cc8932f02a8ecd6bf5c5b316130997f2c1ad1df700ea2fb5603402752878f5c76e9f40a3091b748a731beea000100ebde30c65e5635f78ae3a41c82a78d0631b4ec775049e7ab50d41cf7f8950c8e042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a2b4e1f94a0bd3671bf20ef2a75c2e1723eda50a4d7566ceb0f5e018af7c576e11b1320b4b370e703afd4a7220a7c5688414040d28ef7142aa96b23b44a0e7a5294dc2152e5f53f0101000b4ba30ecdaff42079340cfd0be8baa00a30ee8b2d38b1ed12e7875d541beb4849fb66acec91c9305bc81c8eac64a560ac8882ef5b2d2783e5ff09080000000000000000000000000000ffff23a84ebf4e1f051b1a638ba22cba300ba0836304586ba5572a525622c4dd49e7178214985277eebad66a371253163e35e93d3b44081b27ab63981bb3d5f5b0375ebd3c709b95174ec67a000100eb6b18749ab006b0b61b4012a9e2ba65a8684867162b5149db2dd0e9e12297f33795ce7844fa98ddeb3101d348969416f7f89d1dcb7d1a6598be3c090000000000000000000000000000ffff030d2293271796395d8ca159e5ca66eae7685beb6766a6c0ae50b4569809c4ecca3e101a1f210bc35637473b5afd5e71bfbbc976277dbc97efd5f5716cff0af50478f0d383052635c7eb0001000ca0d03ea2379fd59275d0b1a5d512a1e61f17e92849c3a6a78fb3df833d29f80a2121c3c1f41cab0e6bc7ab4db694e1bcbe5120e20d862083e47b010000000000000000000000000000ffff34348bba4e21803d3e3a2593dfd56111203f3f7c562d1df639d57376d1994aff17260cfbfa576bfed870eedf234bec169e2f8e6c44da8e4960713412ea367f0a11c39808c2e77b83048b000100ccc81046e9796064a98dd3bbcfaba09d799ca2584dedb35469a62bad7592c248cf84161ccd4cbbe9497d564562ca6ef8fd7bbb64174857e5dff2f5784b00000000000000000000000000ffff88f459e24e1f90dfa669abbc6504966bf8cc2b4971db18a5052b70475fdd5e6f427349635ccd6b9c7869b52ff3133c5661412ea5ec134188469fff56b20349d06ab783d8e44b7e698a730001002c0582f3b56caa8adb8876762024b5fc5d444629e0304f24a56129263b315e44042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a524e1f12fa57a5676925e8dfe3b340df2132f5844ad9f89594b04efa28fb4fb884fe21f411fa49120ed7a60ce9381a54232a100a2d318258cf9455a414d2fbe30c69f5ce8d0929010100acc5dbb142f64d1b14455d1121c5a118385afacf17624de54b71f4709193bb846413a5c9bc7991b721a4cb96d45227c674edb4a758cb2566b4b537000000000000000000000000000000ffff6d61d62b4e1f0f4002936319c495d9557ac1bd514bc760cb8db72dd99d5d20af93dd5a7570974d75e5761fc494de28127ae02413819cdd52f6e013f0cff9cda0dae34d43317bcb66edfa000100ec99ee5137c789013867f1f3897472c3805f286bcb5abb5f31583519f817cb849e4f98209ac0950b0726d009a9922e8b7e553876475a191ec0e07b220f01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000097726f02d3aa3114065784a38c5b0c7dd66811d8000100ec02d43c95be558c4286ac61c9fa5ef23877f66306ed78a1d3c3d3fc6a0cc919dd1c2dffd75cd9fcbbf25810aa2a3000d4333bc9ac1cf9dce747140bb10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003beeb6930d8a27fe88c3af829fc6fbb3198500da0001000ccb06a1735874a9bc1bd9042547aa981b7130c5b8b4090b420b7e5b337585edc6134cd3088e1f7bb40531da5e8710173121224ab3ca5d94a439de060000000000000000000000000000ffff8b3b51aa4e1f8dd3b8d006c8ea260bc6158daf0680c5cc7cf4936458024b51ea2036a800ec6563d75135004055d94743b8341b7013589f9695e95af100e56314bd08680812a0f6256e110002004c3fd5bb504ca179596113aa8e3e27aa1488860edb81269eb18a9ca900b4a91b21a94745d870aa608ab95e20f6d075ad6c7bf73df3174eca6ba33f797500000000000000000000000000ffff58162c0b08aeb006ed30975f322c830001d1b3c2e2f14a4ef35445431356f1c5b93c2eedd9c7ea24c4429d27d45f2fd701733ffd655496375e263814e1d54fc3297fe893dfb5a9c1a9eb0001002659a5ec5f2f26d07486a5344dc4a741f5a1b0d9dbf201008c0b02af5280f5ac3c6d3ffdef14a6feb71c8a277d205a9912f72775d9fe95266bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d4134765a291c5a9a2513c46543acad374e11f69e63df9583e74e511e619355f2cb7c1b9cd7b4c1ddf3f33c28a8c046658958c3a10c33d559a563e9c22c95585c0f556727c6c5195f4000100ac3731e02bb87282b09b7d1ea9f7ca460193592160afa2e194fc2bbf5c9b87afa0d69822744ab3b6d8bc0d9e1ecabaa18371ba75453bea5ee00771818d00000000000000000000000000ffffae22e9744e1f932f6fc90c9dcaacdf9d836a2a7e60d090fe5e55b0b02f5a4f608a4b8235ba5aa7abc4e05f9387d1d942adc57c87f5b7ce7f101b7f075273c892063b1b8571311737a576000200cc533b9eea549b30d93f877a43210e8830d7a85166e995a4cb54a99bf92a516856a0649b7a6e00ce4df33ba0090f654928458509e3cda88ab7ef5f11db00000000000000000000000000ffffb92f84944e1fa28a648ac2b271105426f11a070d3f4854f812c3fd0241860afd511a7ac76e681210505ed41121b141e377a73faaeddeb2f70938b1cfaba9eab8eee718a48f292e1d5180000100a305a95ee5d97531c6e3c0e1f5c075b907e89b1f3ce301002c8411bae19c2926f8a4759da7584b66154a83ea95e78db457109dfacaa46797201ea53c7caf89e4d28761e9b41e0cfce68008ad447f77caa7b36f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025df60b3535787ed34aa614aeeea9f99236ea9940001002c905e309a34616f07e818d1d9ac77e1ffea27f5559bb0bcfe83c5e58d036c306bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d413476598052abb8468545b0593010c326d358cec47a3079a36c6f5002b2c36fb45aeaa6ef00746d0e73697fef01704d51d87049477d7ce66a12ce0c43ee6b3b9116649dba376cbeb0002006c00eafff29a3b2507a6064173c049042d3ee5b78b1718ca0d88810ec57e1020e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a1a4e1fb6e979f20241cbb73de7451779e8e059d9cb75a74b72ea6862d7ac703dc2ac07d86cec39b6e8923b55fd54dbc6177c3a04d65ec5142719f58e31f4510dda3c810a843642010100a305b39ac88b40a8cf7cdad85c3bb4057d3976ebd93501006c209e66aa96cfda6f124e23800e3e6feb830a10d6273483ce16650ac5e0720e83bbfdf4db4b783ff62c2c387a1e7ab3736fcceb26fe90019ef1808b2c00000000000000000000000000ffffae22e9704e1f8d29637a7883deac9d725e72cc5a1c366ea0dea49bb61dc118d2c2afe7f0916ac7b2516a4ad4bdbe7dfe68533867a866ce7f101b7f075273c892063b1b8571311737a576000100aca0ba03c6b25db6766e3080cfb52e7085e5e0ed3d34f63dcf4875af5ce39aad9e4f98209ac0950b0726d009a9922e8b7e553876475a191ec0e07b220f0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008a366a03dfaa43d112376f878b171f8df7289600000100acc0073487b3b1e3791a1c330cb80f1fdf4e9bc63024c226491f039dfb1872c8042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a384e1f842c53c3aa11ae4b985a52ae6a3170bdb58f88ec04c62013f9322bd5fda4417939836b6f41741dd864c348103a1155d35c0c201c793d6cb7265b6897058b0b1e75fdf5280101002cca3ea5891fa34136d4c53122d1a4f843ddd8bf062f95e1aea9e169e8e104d3b2919552febeb84511bc568cc875f5d3b4832597ebb7e2dd285059010000000000000000000000000000ffff904c42144e1f09591bf26ea6f179457440d73ebc70c44310ea56dad7539a93be903e28788cac013b646e3ed4198ca74a8325d8a721c26f39ee8ed58b56b0fdecdbf64cc71b75041ac7710001002c5eadbf6056ba14fa85c440881afcc228de1be6f07f90ca0cbefa4c68744871c74d05e7036dd4f8d239c94fac4556d412e7c8fb18f2fd020ea91f010000000000000000000000000000ffffb6327d554e1f11b8a3cdbf872f868b08b211878ef11a0f6f7a7ebd55533864aa98e53e194faa159ae2d13a7625384a3fd1572f68deb43f309e73b02fc83f88a0bf28f87d715a48678ea60001002c7e7a4bc72764adfdd7ae8c1d8296682fddb6434da75a6da930335f013028676bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d4134765f3151436dee05a55afb36dfcc21afcd193ec0852d2f2b27d86f3ce8c05e7e4d4af4e0023d0089b0ef3d41bcdaf4556b1fe0fb0af93f68351ac5e92ff99c982c664da1697020001004c82d4ce98e02fb63af8cda16c50e62e24eb0af87cf5cf6884253fabfdc61fb1dd1c2dffd75cd9fcbbf25810aa2a3000d4333bc9ac1cf9dce747140bb1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a221df490494e647eab2dd93a225a2cc62cb2a740001004c720430ae033bf0a1f8a2685ccdcf5906c2b5b62faeeb18d84b67837738e0f098f6dd6e17b89819d3ed39a8e55ef115846b8fb581a4ecf870f2c1fb9f01000000000000000000000000ffff2be54d2e4e1f88aef910c408df03f396e0f92411de096d08d4ea727fb3abf45541685d0327ee1e8d5b5a5057c92fa2360d9c0abbd11fa6986dc7a8d22b1ec6edc799608163d63eb92ff60001004d6c9b4114b070ff139d44ee9928cc5f659e4e7384effa9866b8177373dbcdcf721e858b5e357f7e3f50a4bf24dd8d18275aa2e70f7f4ea4efcee5040000000000000000000000000000ffff3415087c4e1f009a876325699d6979757ac10d6b37eb7f6690a40447f6473779eb9130975998d3a9fd6e9ada19559730bc017843ce128b7f823b0acb2fce829b7434a720075572ce6dd60002006ddc88c3c0479e2869ef1dec459349532fe6a71c26155264f4346236826319fcc5baf828866946228aaa8193b5d3ea3d5f0e41ed2608d77fca826c7b1401000000000000000000000000ffff258b0cf74e1f8d0651a04f9c243872f613901a1eeb2fa86f3e9a65168bc39941790783d9189a9a7876cedb0c6d7ff7d49fbc88a71911c9a9263ee8c8c606c449959c48978ba70d4a097b000100a305f4bbc801f55f9bf54bc102f66fcf6370f9d8481301004d35ab4dd32e0b2c60354bbc491f13abe6bbbd853994609f802a52462fa8c587de8bf74b378ce943d87ca86bdf897223fa1698fad3732eed2eb6a6070000000000000000000000000000ffff9de628664e1d84fb8f4119d367a2336982fecdcf326c56b7c09c0911994720ebe2a657d5d95252be1871889b13f81cdd16d49e15a7d33305078857e34417e861260119603fc23c67d2210001006d6d5b27f4bdb63658686fd6b2ae2a9a448f94939c33d4bad93258acffb18b29a54ef8809b4010abc2092b9a6fb2a61ec4b378ef734dc632723be4b12c00000000000000000000000000ffffae22e9764e1f925d20af1a6d0ccd3890f0aead4a05a59be22e005b6d732f855311915b351a9153b2c83d84611b2c9958f806c93f7b5fce7f101b7f075273c892063b1b8571311737a576000100ad15e693a28c0055a8610e57edb8e5e11e551da4a98278503076a7e381f9ad7595584af26d1f62a096859ba7f365b5b8064a653c200052a3ca8713000000000000000000000000000000ffffa7636e3b4e1f0fd87b62bf91162008451c1f00a1d7bd65ef581e88c153d105970ab30e451378966b6e4141e68024b3976461605e840282242bff3c046e018b795b7bc3bb8667628dc02b0001004dea1da648b4453ad1529f5fa2df35e99244302b0222503b2f3dbf2e1ec631062f662979b702a945d971fed1d3dae66930ca071b53ae4ce85b9c61010000000000000000000000000000ffff74cbc5074e1f186053ffd90c84db8fb369e1178492b3a0a3941d33c43cd84b839d92668203b6501c786486083eff2b229cec3e0a190d383f1034f0693df2fc8b166d242b00917b7fec070001004d937aac30ca900235ca2a476b48ff19bba8d0682d07bd2d5a75ca270b828a56e3489c49577523cbe901d655f1acaaf06667359bf0de39ffba06584fbd01000000000000000000000000ffff040302014e1f983bd995f3e1280858f37e33edabbd8ca4d90605e26cb6dce824a2c03f83dd498180085db4168f9e6b4df0b46a7f4f5494c7496c719925d805ad2f0a75c837fd9abc52c7000100adcb6dc4aee52790945dcdbbc9283add6c1626d6c715058966c019f3ffc7ae75042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a2f4e1f124aaa5688cb7220be4600211257ae054554583ad9233e8ca0d58abafe317129dcca9e34a1b9bbfa175b88d9fb31b55e36e3ce4c7f353ef721fc1754406cf534bab728b0010100cd576c882db7da6ec00e3f04dec63d60ed858f3b30c35cea5cdbfae973a0c02c2b599dc346b4d3f95b54cf0ddddf3f9535ad01076549d40130e72a020000000000000000000000000000ffff5fd8ae984e1f8dd3ff4dfb358ca5c5c58f5c163d73482caf44427b62751b18255a456b8edb175f887db87b3b214753045f631db58475c5871227ddeef90af4d9e891170c4407a5957ca00003008d88dad3a94111fd2a70061f58de964a745f75fce5499ce2f419e2f0f9ededdee68663ab8ac6b7e137b6b68e6db778fd51f15af506be2359fbef7e2db50000000101000101010455d1f32e4e1f9940ce35274814495ed1855e09650c016fde1dae932ec081c6a4c4e34910a38cfda65cb4eeecfcb0129158a49d1cc081ca0f2acd4e483922b1bd27de9a25cc94b7cd706301000001008da07d39f748b649b6481419678a824322f1cd0f216b4e50d87f21fe4c4fa7318a19568a11cc8fb38243dbdcd5fcae4c877613d6fd1225b2956369adbf00000000000000000000000000ffff8b3bf9414e1f82f48df5b39fac4fe299c0741cdf675eb53aa0b936ea147d4883b650596887142e4fecf91087799de85312aa47c6d601c522817f1747a9922db608af9f16503bff0779800001008dec231b4659e56e4c7a5ea1ef960f0765d7a10755fc7d397504c160aaa0101990df16d78c3bfd377c8cc70ddf3e09e6b25fcd10f119ee6b3f74735f5600000000000000000000000000ffff5350cc764e1f90a2a532b28ceb661521902ada94853f1b90c1a7d13d8ce8a40ebec6c7a62a4e22fe5c1019e278750fc93f6808f410d3a2e7118000386185b278ec3d5304db462f23cc55000100cd9489ebee1dce8c2ff96e8267ed117a1d77d3268713cc8f8afacf25fc21d05d78b0dde07ed0ab2e3f64e550bff2c682a17e4f1271bd15f2dc76a10eff00000000000000000000000000ffffb2077c284e1f0cacf46c91a350240272bcf66d48eafd77c78ab71185ca41769621116c8dae8b29732fb998cb6547f3fb27b556c660ed6d40952f2f57e3d63ad144d4171de75dbad6871c000200cdf814ab540b8e0f6dec0d5a63831cc67a28b4de54a594b73d1f50378ccc567ed8601e2a4b7054c76c6f13349dfd5015496b1ad754361dba7af5ce6ed300000000000000000000000000ffff22df662b4e1f8156eebd6b52f4146a5b3f9686b9712f48980dfa4e8f17b514ba185a31e8d5fa7568c69aa79d9533fa6efd380c27b0c06afebaad117b22669d50cf8bffc7863662b1d7e1000100bb010858d598c3f0e00717fe5155b9b4c31865ef0f390100cd788d4c33d2c9fa714353d0fcff2108ee37467ea7fe74a7e5c9c121dbb71600dd1c2dffd75cd9fcbbf25810aa2a3000d4333bc9ac1cf9dce747140bb1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b5df74817c60158e2226856e9d67d86a8e3ae06a000100ed95d56f195611e034617736e54a037b1841918e6fdb048cdcda1fbb1ab009ce042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a344e1f068631f31e13e1c29c739619b2cb58adf73724dd8c6227ab1b8f45c08c5d6934338365ed45c795e4117ddf79f1e50370c826f62669213156ccf082dc70087b8e68ff4c58010100edcd97758fe49da2cb337abf460f1167c1aba09733a286c798dcac6854ef390183bbfdf4db4b783ff62c2c387a1e7ab3736fcceb26fe90019ef1808b2c00000000000000000000000000ffffae22e9714e1f0d4b5e1f48d7a77746676f09e2b995389d0f1c18601a6f909a4b542fccce87d9f5f30695d078a9181e142602d2e93f8fce7f101b7f075273c892063b1b8571311737a5760002004ea41264559f7b224ce1d715debd3817a36ac63400b2ace3350e204509fe02c93f4ef4bab61c4fffaf9b76b49cba2320583b00676388e133abd9ec42b301000000000000000000000000ffffc2a3b3454e1fa2d4df46e1a22a707fb42351f097764dfbee1468f23d662dd11aff6d27fead20f8d2ab1c3a2ac391db6456cc2b7efdc32c2c18488d6ee04f5884ea9a9b1d1fd6c7a2f74500000002008edcebfde23e67353b5d198ec69272f74f062edbca349985e5abf7567b2557b5fa192982c30cca853affa0747097f1d0e57f9d53c3590dba75318a039200000000000000000000000000ffffc2a3a92b4e1fa0de75f40f60ea7255d3bb51ce31ca67121997accb63f75116a5458a8d2bdd45fb827a2cca65e815fe6dbfa04087091cbf0c461435639d704c0145079ae082735a626d3c00000001006eb9d0ec4f9d170dc2885de8770dba4384992de5b326e78f3b0fea38abe64a27f73a62d8e9baf81d17a0de9f753493fe7962078b3d25b3b1ab49a7aecc01000000000000000000000000ffff86d1e74f4e1f0db6da5d8ee9fb8925f0818df7553062bf35ec9d62114144bc395980c29fcd06b738beca63faf265d7480106fc6cceea7f0dddb29235681f498bb64f7bdcee4bee1f4c980001000ee2fbf2614f987d58627e6194b7310059efb793d564d9fb8f94c1a18d81f949042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a3c4e1f8c1cc0f5e0a5aea680a170ec945074f5b83d83db4d208854204f57c0de220ceb63b0121bd2a7bdb214228338c575ff6fec97fc04493eb004db432813f88f3ce6e730baf5010100aed6e6d63c6e8815af71d8c1f5f8517c0e8b2e1d023bb3e27733d79d38180690042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a2c4e1f0dc75e865b89e96560b38fae96f1d0a5438795778e68b705a506046245ca5dbbedb09e2379eea4c9bde0d0fd4fe05080e7b23e56a97aec080890987ca187cc902a4efe21010100ee6e6ce3ad98d6ac037243dcc9e6802c4cd029dab65f9ba9842ba47450ed0268af51ca8170f9e36531d5432a0aa367e2e3a2b6e19951c45f7db17d000000000000000000000000000000ffff22e098644e1f8d6bea256f36d8b92071b66fa64f4023737cbd1b5dac7c1c9bf514cae400c332a1091df0ba8cd007d641a92507d9cbbf0c5f490d930fd36cd578b0f71c053b488b42c4ed0001002e139e5b0c1d87a22539b2711296f67841a4e1f115a35bd2e4f5e6a745bfbf6e52d785d0a97c46da250133e75acb405876c10f3ea34ad0ccda5393560100000000000000000000000000ffff2d3dbaf54e1f0f57b0dd5947df31adba9480eda73a2282aeee0bace77680da718af1b91a05f878e433e8455ad56a38918b5aa262be0975246a92185e0f116ef70707a58be008da5ce1680001004e7358e6330efb9a2a3b6e26c688dffa202c0ed6975cac103e1b45568166c3cc834bf8d0b6f14ef18e63cc9ed226acceeea0e850d6ac0f7de989f4494000000000000000000000000000ffff9b8ae2534e1f057b3b0190261b1ded22b9c58550f7bf17a150de6a755a5478988b58e32bb7a53e7e6f9981bdbf416324e75ddd9853b8d144786581a774e12dacc4aa155eb7d6d73869df0001004e7f4473a1982306372733fca47baa94c2ba3866cb23506988255a90663e7f9a188d92c55ac2b53d71981657895e37833ac9c200976800ad4c9b02c95401000000000000000000000000ffff0de4d2314e1f0517910047a316c354423fedf0402dde38c90fb376dfb680e6b2b430addea140d0b6c795d90ca17f1df658a5d401c9dafcec2b5f013e92232ebfdc4f814ac10eb533a25e000100ef20363087822b4bb46dde06ef3ab677eecb59ae2a42a287e09db3d9da0e2c717976ab294ffc2cc6e8c2466a294f9abbf1cc1873b0827a5124e43cf23602000000000000000000000000ffffc910020507ce0ec57774146e447ac6cf131a40fb37664ed5f9ff45b59d22cc68f2aa9f4659cef42235b63c3f2c3ed36f8b2344399d2f57ac5622d91ea4b76a62b0e4d0a47bfc4a1fc03c0001002f15bb2a39f6e9fcf11d20875134ef8463ab4015cd66e7856d2c0cfbaf0d8500a339dd5b8e5a421220c878bbdebde5c19e890fd3af8c5c633b33e4cbc100000000000000000000000000ffff5fb3bd75752f027b568db397773d473e1e7c5a06af5254ea184be9e0e648f96969c662fb10eb6d559fcfc9ddc7ab8649e360d7db6e59b278c771d9cadb83d08cbd822125e04bf704f547000100efb5ae93b72eff5ddec13c81055e599763534c5098cc259813f2c9490e26efe03226cd27cc1c704aeb3c2ee6c9f99a44b0392413c0295689e7698bd75b03000000000000000000000000ffff8e5da3424e1f0dda9adbc22cdf89c04e8ee714da7d80dd5620c1d14e30780668d3b782b2f0acac9f00a556be1548733c1ad1abdd96ff5d3a8bd28dc7d0ea750f976b89991482da5ef8bf0002002f860df54c72a136fdf53a9c2aa5e5d62517a5d6152711ade90fb6718574b5b3e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a154e1f816ab3f50007333bcb40445130cd0e82139f8c68b592001cd686efc15e303206491fada6cf90af8f24a28b81a9b59ccfd57dc73bca0753cbd47bb33f68362ac13e6daead010100a3056e518ca89831905831557bf6399eca37f708b34101004fffa9f32d7a39239934fb2bf1110b40e1bb4f1ef6ffa1837b1ae7362db79378bc9bcd3d3984dc57ea96b5075a69a7af8ecd12556e0772dd988704240000000000000000000000000000ffff36ecd6084e1f92427cdb8c9694e0c6ba086ed7c00dd9f52ca18e335f65de8a839a378b1e040b279c05e3822e7c2fbd57fa8852d04cf443497f4ae216cc6f837b4d82ca8e6cd348b636b10002006f2b0973a0eee8f50cf181dff475f5c8f654ecbeea7d5387ebef24b1d41b2588e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a134e1faf9cd8567923fea3f6e6bbf5e1b3a76bf772f6a3c72b41be15c257af50533b32cc3923cebdeda9fce7a6bc9659123d530799f054652c1cef10d2994fb83d012d559a1faf010100a305b5d2cd7c46007006b4a9c7912e9be18a54d91f710100aff369a849f52d0a6d172a8f5c312385eb72427ba88766b9e0f6430e4acafc77861efa72d5fdfbd086065bba507377c99076358882fcc9bb777fb2040000000000000000000000000000ffff8b9fce4c4e1f914aa95d1c7d7c39e0a3b213b6497f5c8624d4f476b8043b22c4f30cd05bc037c80d02b42625d743c6a18d0562aeb579f2d2bd8ce0dcf0434c3cf661c2045c6fdd22ba57000100efdf644f2723689338340c980bacc492aaf0da81a5c9a78da28b797183a108407d8c78f497e31b2810d4f948993e8ec268e64b3ef13624250b1433000000000000000000000000000000ffffa5e30a444e1f121af4d4c49a65e1439a27ce0f39a2eca5ff751e9998c0fea7a3c2b13731cfa47fc6a56a313a38b448f3792fb60dc117463b7729308cbc30c9c199ccee5a77921bb0ace50001006f8a813df204873df003d6efc44e1906eaf6180a762513b1c91252826ce0591679d19f6d32541bcc31a3aaef426e8e50c8253c7461b32708853ea90c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000595d9f40d00a7ea5cca2502a1c5bc47706688c420002006f760f3eaea3b3779923eca5f48388b4e4382413e357c02d1297f03e0bd3b32fe37804a2f32f2637a23034f0acc97c523fdc8131b83eb3cd29f9b8916700000000000000000000000000ffff55d1f3044e1faf71f8ffeab47423d514744e6705d6720dbfbeb342c9a0e81343616e3cee9f778320b48f8bf219d83e18d760aece68592555ba22325c711e8d83fa5dba18917057462396010100a3057f236914be948b199f5a4b645453ff99a841d2b602008f8e30b83936a83eb17c86eff0408c401a3c297e11c8db335b432665939999f7997936508516d08819e5375cd95715febee60fe7457e9b4d6186d4944f00000000000000000000000000ffff55d1f3094e1fab052bc57acf5257a1c88bc1240f658bbb5661a83771eca2dd8b09338b12f8003f955790afcaebbe477377fd08338e5f04af4631be1e5a4fe6c8698698d9bf45204ac66c010100a3054bbf5c68a0aef6ae72ed036f6858ad63041d56f601008fc66927da2a1c17c7d6eade1e2455dc9ad034ed7e8cae6a794fab72e955bb53ceeed739c7ea612a9ffbfa2a36d7e17545b370c102352925ed69afb82d02000000000000000000000000ffff2269201d4e1f08af8fcaaef14df7bc8ddfae8fbcb1f239040be0ac89d43ce0f27ea3f7a00d1685ffc1075614144e70f02df385a996f7c291f09365d25227523575b9f07683a28c205a3000010030ac93fe03fcbe94a13fecd0edfbb0aaafae005f269fa989e4a47908b3b320e285db1eb27ee64885870753ee48c9533634095b8b70fbaed815c39bd67600000000000000000000000000ffff951c7f085207127147f0cd5bc3ec7c8c5704924e855d46dac21de72e2de112f7dee7c9c3f9c40d4474c0d9ae36a56b800659af62c16d27ac0bbb97d398cc78587b58ea40d92039f6a275000100d064eed9ffc9d604f1c5a7f3f2be3000732d6aca4ac2a737b3227c9bdb09b32cdb7149840d6e18729a58d1024140f58e7ac14a203367ded2a731e30e4008000000000000000000000000ffff7217368d4e1f903a0f90f0ee9ee05c0d567eaae8ca9aab5bf0d5d1e0240494f40e4bf954201e48585c5a2897ed76697b4fb95dd993f55871467000cb7d7144ece56406a6d0f8e9736039000100f0540e82b35422ce5e2959f5873b47f87ef08fec762e6a054f938cd9b3e4b3a39a0edfe4fa7f5be8ac1b6f397ee967f8ea09021d8569c7367241c8bb7400000000000000000000000000ffffd25ad25a4aff81bc001c31c71b0d4d4f9ecfb205e914fd9cdaab4e7dbaf0b320d40f0bd5b193d1be809ca34eb4979d661f487b21124bd641c8b61a2c1d3a3bd4e4c850daccffc59a4ef400020010f988bebd079ce989cb67a6f6f6564c20c6a3dd0a801e8ebb4a6936ad45d02bd7925f5236bddb172a97824a330da681835ab329b1b667a038285fb20600000000000000000000000000ffffb0df8f944e1fb4788aed0a3f9d7ed8bf4a15469c48eaf14e4a1e96b4f78b1f8c1182533d924e3b12f62fd06a67d930051aeb792b06d86cf1e49d1929342c7acf3afce26178a0c87041d600000002003089853c8d09e23a297b2a20eac6eef20893bb0054bf842749b7a8e83b5136cc0d77b77bfab342c25a21a28b0fc6495d0de7d29141f3cd7e109ad4b19801000000000000000000000000ffffa75672f44e1faaab2ee2cb7aeb52a2da6b3cc6c44536e7c811b33a5e1f7ce2bfa06ce9cf8bb5e74277fa1d61d37dedd167df52cd7b0c8d17b1cefe9a599baa66b3ddfc70fab2bf5814c900000001003076cb339b759d09a3a9e0104ca702843c38d25a652b5cd0bde69c0f074c04945957dfa835f8ddbd004e663f5628cf242d66c624facaf56046df21010000000000000000000000000000ffff6c3dbd904e1f996f5888a81b9668c16a12c87134536e3616c929a7b67b37aa06d3eb7d7e405e3d3148ce7a072128c9063e1a8042eccd4915c7d1e335096ed8afa549bd4b52c6491d0df90002009042bef1bdae84a6f2e56adf60b6c2054acf4a6e0830ad0fd2461e37aded1877e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a194e1fb675a1940be872b6a0d4e1696bb39ea38179933a1bae02ae1eaf4b47f625bd939482f8791eb38925af47f73be027a64cc0e3581e5165c63790a2b8d2670fb4a1917cd4b8010100a305575cda391cae8011920e52a7c24500c8e3ad98040200b022efb5471828c9c19989b00ed17a4ddfcc4b2005677c30334e09cd8be97df3099f4fbfc79d4db0b9d043ce40968437972297007f23312ce018b6e52800000000000000000000000000ffff44437a034e1fa96efd6e93a689473ce1d03d0c00a5d193b6f1e9e64a826f912ce335b0512446e30aff8841ce34155d2a967fe019a292c88818ac93c56d21cb72c3255e3de33d160674d3010100a30527e81486c4ae836aedc0fb9a037bca941d5092510100b0c35e14ae0acd7b69a78f79a023dde9f4fba4c2037de760665e1acbf230471697e55e5ffc3d881154859ad61de09c0555cb84a6e36b4b6e517977804202000000000000000000000000ffff5350e2eb4e1f874a5fe9d0a2d7ec3ce741fa441e2f9a2f1726fbd2d09a000a6dd4828bd4951a82b0bef934716a40d0ff36e4641f9378d593063ca9a226192be390d6841d9fc28f33818c000100f0bfa4aa5a016e0ca66339001522716a8d4282cc3a5ad655e2373028fa8a14d257355edf9513c7e9c3aec6f26475b3e103a1984e5dd65387eef0f2250000000000000000000000000000ffff03140e8f27128de1a5d67b291f75e87e20eb4b9fa7246dff5bcf4030ae26c321b1845609d50d04b240cc51862b4a7b3dc9be4aff050f4e818207ce6a7e9752c006ae4328b26c14af2a9500010090b520c63106b609f67b8127bfd3013e776bd4b4f83a3f2dc5f3323edcc6379a042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a274e1f0aeb5c2757211202b3afd2033ec1b4ef2dfe376ba5c6c07b45e6a7460afa4086423c4a704eb9a781514fbc513e190a62fd47aa98b58734af1453cdbbe5ef9396716775f8010200906d0a50fcc6324ab1f9b06f12b480343f2073cfcef022d1474246e21300b7996bb5051b05c55f903214681a5572d0d358bb6670d332f0ae9babbc093d00000000000000000000000000ffff97f455604e1f8762ab6cda7a38827e39e8dffcea30aea015ac78895cdf9a64c650f5ba71b36c25d29211bfee08619267d71971371d86400d33b0c9735c4ec53457758ed77a21455d4deb010000010090756048c6e7cd1c39b8e6bd88e7ce0b896ddc258c90acc24636da018517b40b7c11d44fbd23d92e7cefa2a5f2dbdd43ae11b3545b75a05c44a4bd000000000000000000000000000000ffff74ca448e4e1f8ef072018a444bc1f30885a199087d2e2200ea31acc4659eb0c05cca30f83e4bd940ed27873458fb605c92556883933b408fea2918afc5688ce5093b789cf08823a57ae1000100d0563e4969ed37631265d56b3a220db7bb7ac2eee8ac0070052117d8f6cf30a4452a37a4298f0adeb55cbeda2c2e7dbe77db073d8dd7d46656f7e808f100000000000000000000000000ffff8a440d6e4e1f8e845bfce2651b80e95f37e231301260449d1d89aad58729398208007ee430fc8137c323857bc75cd9bda7334838181384a12f2ef1a65969f60c6d2e2b2974c36cb309d0000200d06ac37501d8f509618ca7c43dfddda735c3d88e33fd06c8fd815736769e0c9fc1f5e900830f2bdd3ccd56f55bf78dc4d9d8be9e927bc83727015de5be00000000000000000000000000ffff23a96f934e1f8f31cd2f8220f32823bba9d844b2bdc4d5525c8d098d033058916421d6d782d29765ca45b156772f9d0f7b7b1ec40d0bf90f85389e282e08c2abcf50fe7c31b31c1873300000000100318c32ec1598fa8f38dc76e28d15c00c20553c7020161a4c64742524a18465c39b2d789571a3b223a5365fbde69b3fe7143c4f9ebc6293afb84d340f0000000000000000000000000000ffff23a3e2204e1f08b4c1a8b9c1402ea84afe7c47f7e98d657df873b9747a0e4a497120ec62c81f314ad91a6f3384648e7e60f2734554f7f4c0fe75eec22907d6b043edb0df74ccc7b85a45000100516cb4e5f54513bee842f3dfe2ff0589051959934d49b4aaf501161da8562442273b56d826b60e51d09d2227f31ac57cc7876849c1674a6e882c8f117f00000000000000000000000000ffff9b8aefd94e1f14f9192c3986e589f919f428c43770c3eca5c4ff3722d967d8f0d4b69ec3ec02fd876737fb06880a54566f56393899723d88e6f4cd4c285f48dadba815f581acc2e2df7c000200b1d84d597e2985ecd1b87beade5a07f7b584e84131951ce3664be16f7f1428043d9976e17a9b6d09c12ad956d62d86a7292d8cef56f569759dd8b8a11f01000000000000000000000000ffff40b0c6a14e1f8510cbaaeb8d82f2cf1fe1c6e6d3e7fe3d9e6ef58a5cf1c135a0d39194c457fbc8921dbcda07fce72e6aef5b54f9b060a2c6189093918db0942b8ad5c215989cef4d367c0000000100d1348f218e0f26ca214957ab38e6ce4548b38ee7bcbfa473caa087c84ef6420744d8b5d59ff010a5939f343bb8b8480fc2ebf2cc6e4d74ea6c22c8a94a01000000000000000000000000ffff4d1733044e1f12271e7f58e6f7d3e2b7bf724729808642a16369b4ad22de438ed38df62b415a92bdcbe7923a858532eeedb44bf12c129fadaadbe9448ec9a703912e80e3b90f36519de20001009159bd4b4fcaf4a32741056c96d5a1f232afbde78dc34398cc4cce95ac022edc042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a404e1f0db85a27cd589d225beff9977aa0ac32551d15bd906a899bc1ef33458d7c979118f92bf1de4ddb55144acc2f7cf6d85432eb51df8032392d44594907e5032d23df570b2d010100d101339e98725e8dbb87ce803366bb7af996c265f52387ce57fe7ce6067a46d26b037d3e63ba7f02f58d076423ad84ac06337a35f94f16764139d03d0d00000000000000000000000000ffff68ee9c6d4e1f145a212f37b991a6c050cfaaad7e83f1347486984174e8f446e59fa6c225691ea679f70613d829f536bb1a311d812cb85d63ccb848f51351e6623f5841dd00e057e46337000100f1b186e58c0e1247221f1ce97cd273f917e034ff4b66baee366ede691e0590a66348fc52b6a63a7013cde16f282347e5949d88a8f9989a46411409190000000000000000000000000000ffff2d4da7f74e1f8b165f653a3970a17f432f6c3abb8b681c71a3775f998fff322341d2994767c167c8a43b1b4661b9c01ef637763d4d814d31414b558eff2b3b489b6128ac6852e0fdb85c00010071ce9f30984ba8ff47618bdbb362be8315b11d7be903b37fb9ed2a0010090dca042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a224e1f92823797ad456d53ce1e6bde84e8a19164ff88a73ccd242ec48d9c6a479f2a049e214c7e8ec2243b7ea74ca6144ab2c5b7e99fc3ee4cf3d64c4438fc2a61cba12c0e73190101003193e57ee81ccf3c25937a5b585afa86c64d86b79f2b881b88c7302731f51292bed604587f137c806a8a21f6e1c593861deebb6283097b75d228710c0000000000000000000000000000ffff6418ef404e1f1931bdfa94f15b64ed9d09d210db9998dfa068332fee19d8e1ba4872c0acc3efc723e2fd04a64ef2da473caa4471c69e95dc83d7059a8aeca9996d6c5436db3f8c9924ff000100f19071a1fa482d2e511a78634a35fb63b29dd7895ffc76f9183d40fd9886f18b88b7d3b68eb85c3c4f1e621dba7b9372eb0876720287b28a1e694a0a0000000000000000000000000000ffff17f0e8c34e1f8041404bfd1cd4b71416116af92b7a17f42c47bf3dbc2294369fe1691eccb9ba851183a0e85a4fd728c946a582d006a704dbf4531a1c3aa937a63bb4d5a67f13a335d5c3000100f19c027c8aa2192b7bd34090aa59458b47a739e66db4ae1fde3fd5a3d864cd55108c07d959b5e8b4161be3a37591bed81162887053dc0e144fb306b0e500000000000000000000000000ffffc1758ec84e1f0a1442ae1b122649814bd8354b4daee4a289220eea514988d6cc93ec6302e346bdc77236c91967ff86362d81b18b1c7cd2f955d617b4c5687619d9030ff1b9bc39a7fb4a000100f1f0cf17436077ac9fd85ca3c5302f2e5943a55b0b53f6bda09b65d1ba3ca33aa0448ec3bb47abeaf361d19f0b4c76134baccdf2eeb87f292dacb20b0000000000000000000000000000ffff74cbc88b4e1f8130957c5939cc5aa59a9d7ebb88a03c8e60175230f87927f9485d452f6c844454148d8efdeb9e3216600b7f6645ecb8e5abcfb8f638610b4b5ea0b23d82db8e7560b4000001001192bb8a7f71b435686736d079f9c4bb5bbf321fbea8e69efa1b7643c59bd27a7c95722ae41d412c12b2c902075ccb8731e0b2e53de312bb2220b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000224cc410b74ac6206018520cac60988122bb337a00010011168fc50551f544fc72b230b6b9802337fd6123da2eae9b44806230c8fc341b106254ad9848bab5b440ddf198401771eb5d373cd00097c140da69711300000000000000000000000000ffff36cb869d4e1f1622ef0d7145ce02babddc8762f170f46d5d55c151218ffc12aab3ea9faf90dc97bf158a0fe37a3d8c1fc416bf01bffde922c71d669bf4aa1fe543345d67d0be170b91ea00020012c8d756322b07e0cf2f73ad46f5f25bf91c8b7749419d02a6ccca8da60dc488f5364ad36ce7990bab58abc9f2d480903668268322b7e5099d089ccb3d00000000000000000000000000ffff55d1f3064e1f89d3ff18d870c0b6c9605d986c2de92f44c48032408e183a1d268bad77570e30a6d057a026119d9d044c1ce29b518e91d69372e5829f2cc1c3a9882e466ae9761dfc40e5010100a3053fab9f8566e701ad9bdc75f6f0fe7aaba6008f060100d28018e798ccbd797d0b2fc33513d64d60d55c92f4b35f46db169332dae95f4d042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a454e1f87be789e5b798cf3a40ff5dc22b0384dc690acadd614067c0f7e6a933b8f0c72c67b3f4b3e666e6fc48369a8161b04e64bd7db8a067386d8bda99986fd6661186757bf9501020092a52515eb33fd2e5a41600b34da9833c8eee685f8a02c0262ebe97a846a39ceb455b53e114ebd429d9ff22901fd4491b60c18d9c0e845e370dbf29c1402000000000000000000000000ffffd4382a70752f8a5a3ac96d75c412fa90bef0255204ff5ad428c68476b1cb397e3119d30d88cccc76460f4fc3196199f0abee537bc9e59859eafad31e4785fa131f8ef731f8a6361e6891000100a305041ed8b9be96f156e0fdad65444f01a10d782c170200d239283a62d1749e03339d948d117147e71110b725ea0d4c0f7a2f24bfcefcdbe254f745fb2633a818d888bfd8e0075640788fb0e0553faa003714052f00000000000000000000000000ffff01010101752fadbcd4b1ca70e220279cb084e9fb07f910acaa647297d0953b615418e59cdd4f7d72039538cc4db69bfff684c36e973879cb6f21bc02f8d721ad4aee4fd9dd5dcd92bce60000000200522655e57bf1414ab7668098716f4c474ac22bd7f4a3ded9622258e74de01a7ae026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a024e1f93943908436a934c08582583b08cbcc50b4478bb79b7718789c25eb0ad2f3e5713ad4c152d4b1fd13cfd12bf896072e663c8d3ed082eb5d476a0940ef9c3fadafc4c5d2c010100a30579e3282f6459cf76576409be3b8e7863d1cf78740100b2b69159973619cef08b9add969ba4d1ae18dd33bd62635429d44d93b62d57533d7c98b3942c5550e3bfcaa37e37282fd4b29bb40f677a38906e59725700000000000000000000000000ffffae22e9754e1f07f818e5c2330ac4e7f0ef820f337addf8ab28b07c9d451304d807feda1d764c7074bccbbd941284b0d0276a96cf5e7fce7f101b7f075273c892063b1b8571311737a576000100724358a51f6d02818a40ba46ad2c315c9fc0481de2bc18b36586df2b0f10de47dd1c2dffd75cd9fcbbf25810aa2a3000d4333bc9ac1cf9dce747140bb1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8acd3790b09eca3c1f32c7f29e54fa62f4f8e37000100f277e7b83422ba404af92d4fde9d23050a70aa3339f62de35c9f00e2752c38f2501fa5e6a674dc90f03e715648f12c2f243306001340b7c5e59ea70a0000000000000000000000000000ffff342ad5934e1f16d49c42cf506d5687c4035fc8ea37c2bc293761412b8c28a73f674df9d3983581f53a8eeb7f1c7b6382bb0485df3814c3bd25702c4c5cabd642d5498f118ec73134c53400010052f87482f949cc953702e99a586374ec46dd519742c010d09a48d8be6e17900c23ce984b09397592dff901c68604089feb8e05a51fb93121e9fb35a57403000000000000000000000000ffffcebd93f04e1f0128cccd87ee9fa75531c9d8db129ecdec0931b57d3d521d35df8af82f6972bbca75f3861faecd3b701bf4aa678b18e5e1668a2c23b7a257d3e1acfe926e7fb5d64b24ea00010052fc431c2f463e03caf5da0cc4d393c5afa1bc779f766c854005f08ebec102cc042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a314e1f13146b3252f408f1cffc875b12b61f56c1ae02113b24c0b5aaedcda4a9b509332c8c4587450074f3e0906aaf3ceca7541d92da27f8fd00226f85d9e1c2ae33f2d57b843601010013784cc1b2111e1350cffad6499c3d9762776915e7cd851d7e9cf98134c9495d042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a4e4e1f826cbf63d989916d252cfc2cf827f34314c32c64acc0c252f1c3d42019589650c3ecb098655ba153dbc04211a1a73e88079a7d38b11c6a3e040e4e665150768b8a5faec401010053ac76891bfe83b92360aa75c5fe209a30be2de1201da1226f95b2a6421de70a4245e413a2d48a737b6f659b54702b7b7d81ccf1bb63d28737ca862f0000000000000000000000000000ffff0d3be7c527151438959163472114ebd0f4e72e984527894a871063cefbc8cc492593a7afbf4214538c0618ff8477590f40a3b2155aeeb0bd96751e06b5f3f3bb00dbbeab1576fc941f5f0001009364d20cd8691a85992cccfaade15de643fda1c01dd5a634f9fe35f9885f59e66bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff369ad3f265d88f72e69ac2373a62f14b7b1d99fb24eafdc87b74247af42a591aef0989c9a3e152197736dbc266b2535c4b4b53d8ec4aca9f4452a8988619c535d28f0b25aaf2c34b5d13000100b344a77fdaaa7bcb7d53927a255da9bb9e9afb3f2239d8c0dac672cb2357a406042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a264e1f0e9855b2ee991f988e446b60dcd637f33a782baf1e755785ca058f0398133bf3a95e4e77d4168c13c47d7e3fb1e3ecfceee87111a02be65095125160059b010ac7ca3d3001020013f125706542a3146d58cd5e4c24299ade655d0ea1881ee0fd84766b6acd3d14e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a124e1fb928fa4e127214ccb2b5de1660b5e371d2f3c9845077bc3900fc6aabe82ddd2e61530be3765cea15752e30fc761ab7307395853c0be5fb0f0348aff8128979368f52b0ee010100a3054ed83144996c18df0b97d2c2058dcf708f5ff2b5010033914802864685fd11a4d0695e6436a899fff914c78748bc71b66f31916c0ff6fb9b6198e7b7034fd9cbf6dfef92054cf5b915311c6836a89c279c66a100000000000000000000000000ffff2cea7d974e1f06af52990c96c3d5aa6d8d29ffc118f43a74c12f3dc860825818e70bbdc9548a6b62d680af91772ba9231378cb6d29254e28cb55a991fc1d6062edd80b6597f7e72d2a9200020013a65e07cdf3e391d8008c8922f4f8ea307866115f9daf78d54a0b74d10f444e65688e98145afac905339b6c3483019995fee19418be700d436f6b871101000000000000000000000000ffff5fd916b64e1990e7cc61a596c1cae0c431f22e92ad1ea142558e6e2fdfd8cba033040a4bc56e58f44ccb3380d10ce89246fb6f1c52bb21be19b26057cba9acdd22fabf0d3643e8440b650000000100530a0286e02cf5cd463c80d3645f40c0aa3d2f85bfcca5228538e49b219f35fa9906d6e2a830c62dbecf4a41efd17923b5d8e382d0b48be90ba1ff49660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c687a0e88c8907d91c576b0e64a45ad6b504caf000100930fee26405fee758046a6faf808843859ecf67ba3b8e5812a32353f5ff7562349f217d43ece8f23e9cb38449c6bd4f372030d22b5cd57d6831d4c230000000000000000000000000000ffff23ac34584e1f05d35fb7ea96c707d0bb6a9185f575596285d3578af73d43d2940e6ba7a39a60ad5d3a6f5df16449e4f5ba1cbd8306f6e37b26efc667299ef749a8ffb5a2a64bc48c5236000100b3a77b66d092aa07f53c5c176641c56733938b9b777cbc8cfcb39fbab4e495e983bbfdf4db4b783ff62c2c387a1e7ab3736fcceb26fe90019ef1808b2c00000000000000000000000000ffffae22e96f4e1f0620124f5dbe95b93bbcbab48452ba0cc47beaaf554e63db5deef90c10ca79c1e83c08a43d4316105419bccf65958023ce7f101b7f075273c892063b1b8571311737a576000100d3aba1cf7fc23882b84eff807db13920b387ce209463e48689073795d3479cb0eddfaab70cfdd909ecc0c0ab6b9f7ef49c2e188da5cdeb4733df6c090000000000000000000000000000ffff23a971884e1f1848a0024f4a1b85e18552105a7d397714bb9d16a392a29b5d6d18bba91fc880a6b20be09f1400dfe58de3ea87f919badc7ad6d9a56d9e5370352a41c4b9b5560100906400010074e11d7fe492ae20aeb94007f712e302d4f56ceff8f2af866dc143a70262053b042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a3d4e1f0d9ee7b7e66124c4b047e1f93aed5a764ed7384292737ea17f3a7e429ce3f24d602d54b97f72d181b6f093da9b3ad3f542d81ce6e9a394d9a408d1fa828a8bb7bf84d17301010094e54edf33715d857b31c66f23c19e952d80680e673afd03da2352f83471f642042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a374e1f0bde30ce81e7c6396e334eb1bd788b683535eefe9911286bb42662c46e3712ddb5c7c24d7998118ae089e804e14efee5bb34d97e1f0a6406ea00f10eaced5e3ef227a5ca01020034f208f213e6f0035ab464511b314634a746b8c701791a0d41f3186354bb1789e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a0c4e1f87d25769002af2a4f050127c73fff03a24935e48f34fecaacd69410787d0e6384b345c78e81b1cb397b43dcd635568b618d9316618b1f55977af250cc5d8783617084aac010100a305c15c2225333739ae2315c76e0c86ad0d4af3db5b020054dfd3371942cd3d802857286f333ff325c21258ef80f961f7a20f665de81297e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a0e4e1fa8dbccb130522909dc710a65728006732c18441757f12a338cf4a6d8cbd5baf1a484537a6a0542f51bb686e6e546f1a020c7f1991e06d09b1f067b049fe6d0af0a20f79c010100a30588cf6e3d68244d1ec0f88ac73aa2ce3b823111ed0100b497a9cb160b2094fccf8ae90aca774089a89406118cad3f3e36761d03952f26b41595579b1db6bfba5b323d1aa6ffb7ee300335ca0fad24f1c1e5eff90a000000000000000000000000ffff03e253694e1f8a50a9ec0042d293ca23c941142561632df2f182445a96d693b70079a085dc35073092d00761156f88a1f269a2d87e7ff98d462531cb02fdc829890da5d5f248c81f9619000100d4c79312d5f251e40bb72254875cf9dc455f29a4dcb635416b9726692436393955448270f86cbd25c837203a2b73d9c5172a9491d9462243a3310ed87d03000000000000000000000000ffff5fb3a4574e1f847383710ac1786f020769809abad8f93018338eb855c103f5239d75dc2767770eb45709c895c99c0c86d375ddbe478de8ca11e6c7d6a9e15b9676b534663298040b88b700020015081e2dce5d4c68da658ea66c17f6d055e1fe50fbc9e08c532ed2af93118d8be026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a084e1fa1749fecb407bb0e0ab9d6df65ea068dba5dc03e14dcb36abe5cb2b5c6e424683f715ff09ce290d035dbb31add0c0180b802f02882ed36279ba95674e7e190e86bd6458d010100a305c2d2297dd7a669758b34fefe84a482fe7be1a0a001003528eca40c0bd81dbec77ac49a33199c0ea7f1ea2749d783477d80b0cbf62794042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a504e1f959f6f2d48d283390b246a55b19a267f8ada326c8eb67f217839d5d1cc55377d8c1a2962cd5e80b892577454390c36b8de39cdda2efe7dde604c1965fa539df01785bcca0103007584d3a89e5de2a6e0152ca769f786bc7ee915d415ebe822c88fb6e2b98b2351dce9f6cd47402f05d1b12b8ebe1bcc23ad22fee342652e2d5d80290881000000010100010101043e3cf4ae4e1face65ce6933fbafc8686ea9e8ba72bc98f3d07905b4ccfb83162db440308e9255f708ce4f2623b27f9fc04fe11cf60571341a33990289395b5659a4cb30cc07ba002b50501000001007511221bb4a5bc3b4d984739add15f40991b6378e9b408f533af011ef2de73f74c6ed20b7bffece331c67765007c96709bf598d6ea358f2ac50797070e00000000000000000000000000ffff23b9cadb4e1f04f1a3407bf953809815243d539d316d2b055a57ff6c5412f31d98f0d5ea84f54511fa9f02ddd6d7f8751505c560eaec8a108538dca5b073f24fb8db78f06ab6a0aec27e000300952e8f1318a5475efb7dc10e164d47c2f5bcd1fc0ffc1fc2fb66c1bc53aa9ad3e68663ab8ac6b7e137b6b68e6db778fd51f15af506be2359fbef7e2db50000000101000101010455d1f32a4e1f9321b7d8d72b12d10b413673a65b81d375bd0cc1b7f05f027d974d30209452167e577eddad6a76ac737d1563c659d1e1a5f8e9b87827f988a28e63b164c52034b8ceef820100000200b536b4cc225280ef6d25db11cd63ef44a69b7c6ff0d290fbb830ca450ca42e0b48e6c98ccdc3ba7d98cfa062fbf538ecad7f3c5c8e53edcb4dd76a9b3f00000000000000000000000000ffff5a1029be4e1fa6aa4c4a277dfa3d3a42154b286c66f2eac4ae88d6434e1aec898118590f767cbbd0ebdf7362f308a67adc1bcd629511eee4c74a4dd6902ccdea0a17ec1d65fdb03819f8000100a3057ec099b40ed522bbb41032f59b25c13195f6f9040200f56ac75f5801afb0fbf248e8b39db1dd43a55f1d873beb9674989c51b53d73eed24cfd75ef1bd1889ee6999e637e43c333c7f20e4fa3a700b8da0f6fdd00000000000000000000000000ffff0238d45d4e1fb695a4757d60c472f67a7807def81ab7a52dec3bc64aa1c4188d02a4768ac3a862b404f0656ed395f1eb6470dca7bca45b44dc4b3f0ed26a8243afa46bada49c18da6b2a000100a305414289f74d7803fe50acb5188c4391373e9155830100555f6acf211a8abf3c78ab20b2071c9feeb828ecd5b1922cc111cdbaaa7741f9ac8675cc73f106b9b0341300967cbb44f827894b452ad11055c6cfac3e00000000000000000000000000ffff9de6f7db4e1f9993c900fc49b020d4050981a45281cc71274196c57c9405f7ea8d82823b2cb36c04a2aa363111d74e383bdf9fdfe254365a7fa9a52936ee5272729a4011c8a00c0351d3000200f51315cc297f031bf846dd64b7a29f4449334d6d77030501b295a3dc99db48840ae9161f20d9de016ed9fa9fc4251b2b93377a1aa894885a2a67aeb87501000000000000000000000000ffff0b16212c1a0a926c2cdc34322421c41a41ba7b3c088d69e0da3366468438b6081a7ea078bd65fb3eaf75e0b099b44f3335983086e8e0b3054e0a4b97aa63e61acdc2cf8e4481a70a6dbc0000000100f5613cfcc6fc6111d2db7d1372eca86a750a7b196454c9050409748303166f43293777edd2b121decaee02ff1886257a3507011ba587ac97b97332512700000000000000000000000000ffff03d8c6fb4e1f07bab9e0cfd301779007735cfcd14445ef09191df5a8ac39aa177abcd56d20e46f7aff4286ca6ff02f4747c0534dae9ae7cbf5568870f2e301ec84dd720ce2d71d2ef6db000100f56ddedbd69c005b9078ad7268a3b33a64ed3b0d8d1cea619e3aefdc1a3ebd5d6bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d4134765a7119d599048331efb5bd38ea0506ac51765eaddf396114dcf14fbbbab70b7a929c9227f8ec980851ad79f502d2fd1750ad0a7d804064376431fc57385721270a55fd9915400010016500388c354e3e87cb78a87e9c1b763cff8cf96b723adabe91e5957ef5d116393bf68626da222480e1076eb95397f66368bc699aabe83412e1b4c020000000000000000000000000000ffff2d4db0104e2002ede7bba4f6330aa85b22f2d20167cb529ac1334125ec439f873c0cd7d54e7c07b65bca725799f8292564af4296f06079cd5958432147b2cafde22aad47375e690a9a4d00020036f4aebd4b3e42802861a15f33932f475255928a955d7183713815667642655ce026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a104e1f86d0a2ca6f434eaa47ff6919ecafa4fc3b012b89c62a04835a24c00faf62c3d30d3f8755c33a7abc595e96fb5b79594a7ef7c15840f96c603fcd29409a70f77f878d25d8010100a305b8764fc1c19db2b84ffaace89d35576d05332a97010056a400ed0b0ef5c34858db07d9e47173b375e4ce4b461da1aded7923e7eed171c351109755e91b6ee895bac465676004fc8e5abcf8fba6acd3e6f7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000059b1ebf04d63af3ef87391a0a4a7b47f5f5e7911000100d6f87eba9c62615acae80548de4b605ecbfd92bdd09bc8b5e74ab51aacfb82c56b1de24740f3162b179e380a9f7ae0d559be917d4197cc6fc03ebf502802000000000000000000000000ffff035aa7434e1f96153a0fb857c1da0f6bb1ce2e569bebfa14e6e1f532139f9dcb720d481db17999cbcd8eb66a5ab4c0fd20c5f9695fb1ffc45339be50b5953200438ba8c71ebabe9575e9000100f690ae59eb283f2e390b9b45349dbadafca10f9df3ed955f1b631ccf306670b5ad01d34e972dd43bd5c99a6c48d887f3719128772c34adbe1bff43010000000000000000000000000000ffff9de6137f4e1f874b17058e37c39f770188dfe8e699959654d723e62e28b2760900e5284f63f6b70e077a6ea9803714bdf62d083b1d9ed157ab8fe5436f4c7a475be8a35079b3aca6c5fb00020076354b8d3ef13f96d91747b92a5f20a35170350f44e4a27de1d19977ee0c7a8a152c66931a771406885c603a92177e8ea175f76440015d897f9fab071400000000000000000000000000ffff55d1f3294e1f8770575dae922740f47bf040e13281a74e1424fbf9990edd3fb04d84f51b5edb1ed6535afcbf235d8af573b90b3a16a55345c347fa2df12693cbde46457c3ec1846bc717010000010096b19030565fc88f841184c2c8ac418c8e44e36a4420bea538b1d9b918d1db462f50e5fe5bd461c956c07a8a6280549b6decb2965f787c4e2a70c0070000000000000000000000000000ffffa516e93b4e1f97adf3867ae5155b18345e44e277ab26b9a497c7d0cf9b53bdc42362dff3642c922d1d10e277ce6bd407f48bfabac68be3bbcadcc1f89ed98a2ada3473bde7dc618cf3a7000100165a5b17d27d77e492ba93d65de0ffe941b2875f75bcb3ccbae278f4a386b40c0412db080746bf4cab8c4251d5c1a893d1fe1656ff5fb3fb73a3261a330200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200be0fcbcb2c10e968ea0b5508bc3424c9c68b000300762aeafadd5dcce21e72cb613a99ba9185f07f196a025427d8bb5d32c1b19657e68663ab8ac6b7e137b6b68e6db778fd51f15af506be2359fbef7e2db50000000101000101010455d1f32d4e1fa98f8ca3644fe1055e2604556fcc8edb9d5191116fe2dac57d12a4afa3f2ce23b1aca5bcecd726e271987b86dca91581a0699323fe85451f4e0f6fc1335515a196f4e9230100000100762fcc85ca4b2fab16e27f3756b84a69d0e34ebeac3f58647e51e9d58a4521c383bbfdf4db4b783ff62c2c387a1e7ab3736fcceb26fe90019ef1808b2c00000000000000000000000000ffffae22e9724e1f17a49cea05ca2e18f74af110c5ab52c89a43ced4e056a8af7ca8973401494bdaba26d1c56b46b018091d0dd64f244750ce7f101b7f075273c892063b1b8571311737a576000100b63b44a79eaf7e763434d0510b88dbfe73f9b589bdf08a6b53b0b7b8ebb8dcfb042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a234e1f17f2a4f37de1f78a0d356835de99c7639d8daf824b0d432b2a7c01a921f6a8cd4028272e2962f7d04abf9bfd5a77ab8e982169553d0e618637a1cc6847b185baa50a2ae4010100d6373ec9005b1c601b7c593318c109ee53eca8588543a7b277eccfa4749ac936524b13bac303ddf4f82ef48e21a8a3e59d0ae5a459d2fe9a9a21b5130000000000000000000000000000ffff23af3e6a4e1f8f2caf4cf1e01130aa6bcf27784bb36a2b4daea4ada3be553c9b709afcc752d0f34a0c35e3301e8f6a2fb3ca44656be27509e1003c6e1b4f64a954ba6297e4554f3e735900010036c97361c1b5b664eeb743d04fcff7282e94a37732b99779d027dfafb030c3fc9b69e8417d48280b521fdcfe126c56b1aef3b06128de4905ce73e9ff6d00000000000000000000000000ffff0381198e4e1f89aaf743d70a26ecd18aae71d8e2ff0bfc98a51511f83c8acda856e3f5d7c61c21ff4e19a56f02ecfb6ee014097945e37200be0fcbcb2c10e968ea0b5508bc3424c9c68b0001003675a49a423973487143a5321af768323e0bc79d0772e070a367667efc1b4d8d6d4f0ae98c4142e5ee1c93b8801608426899c0b2af3fa69212da12170000000000000000000000000000ffff2d3f68684e1f05e588704a6f6d703617081d8328c006b1173d60aa26cfe44b954f1279a1ba9a042bddc5b3a00cbc8180676d12060d62571ac9b0061529d85a340440e6fd38a3ad5633c8000200361e3d11a33d09ddf74e0fd62ddff8e87523608dc513f252da58d984b1dd2e7f75c5ec80d4226acd2151e3ba753464a443662f99cd2ab2f3dcd100951f00000000000000000000000000ffff3270e3264e1f8f7f1c9fbef0d00cf365bff2a3405c777c1cb0a89b0238208f599101611159204aec8ff5d1da136acb9cf01696c8d5b6fc512d1f32f0b80ee1b5de33e61039ae51e335dc000100a305f52f517fd9f4371ba168d88b494bf863c3c0a37a010036c6298f595939395ec930f936452726f33a311a79b2abe290ae01aad0200116042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a3b4e1f066d57a6451b7800c1c2a6c6e04fe73ec2e1c95e492bacae760ad2f79ca3c30727ec9bf0daea43c08ff1ad6c2cf07612ae2c14ab7113b1aea921177e509bc59048556b97010100d62a3f54945dda86301e5b2118e6962c79b8d6461c075d97f1b926eda4e7dcae042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a4d4e1f007b1f3f16835ebfba6f505f43c6de757bb22ecf27a89703e90e43aedafea3df353a5bd1915b27e8db397d53f0a23f60996eefc4c3da0c2c59fceec99aac79122c44d632010100d652ae25bbf28109fac407dc21370c72581140d546c8b2fd48815b890816d28769f2a50ec4d41bdf33aab17be8f06527e573f5452fb533305b4a1e000000000000000000000000000000ffff44b7a5362714974b7b4e608007f22ece8fb933fc18d66cf35cc0e5a7977279a092976b501786d4ab9108c7fda681e23978bf54b7709a419483f32fe1e48e4fb5ae37cb31cd39247ed46a000100f682389d5e0baed394a65cbdd6a666f59271943d935f85c9d05b3433978c9b28042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a3a4e1f0e2825781a496023c8be61f2bf352ad1094afd6e4f84c4ef331bc727bc149a6dd7e23d78944b8b047c03da44eef1c7961c6031c212e840f9756b8f08e3801cca20b38a62010200f6aec1912158ae25c3da6b203f7a34c519883eb54542f652753e5393d8b1e7c095c3662ed8812505152aaa218190f92a2ff7fc2da343c74d1ddeaa363000000000000000000000000000ffff55d1f3054e1fb362c9e1c662e038e334bd6c5ab36d31ec161df27439b2662acb55677815bee23929ccdeda8df9a8cb5d97f5c2119555a6b1db0a8e55b28668b0f6ae6278a0141186a417010100a3052d71c0ff1b5b842fafa51f0cf9c24c37466772c90100f7c01894c6a372069b6ef22eec5b383d59760ee487bb252b9e10cf698e8b801dd47eaaf799e52fef1ab499c11421852bfb559826e7656b4bca416f54b001000000000000000000000000ffff22aada884e1f9687b4c357c8faa53e6733e83b77b94e92d2b528047e2e4cad325810b5e4856b7ffabfdf97c825d224992838d7435d7591755d751fafdbf3c4d1e1dc94c2bc198f7b5bb500010017a635a425059e7599576f010e5d36fdfc7faf165d3884cc88b9aa24abe8aac0042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a324e1f892a3de6fe305d39d81ecc9dbc7c85bc6eb57434618903f45ac8996aecfa9a7945e2cc48c40c1540172096229780519eec74aec7dcccccfaec4c5fe65166a2d5ce0e326801010077b25d8809df68a929f68e7f144dbd1ea0a184f3c7abdca58bf7c3af647fce7a042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a4f4e1f826fc7f30c49215b98d5cb47a350f888a306c52fa42c77e765b55288e622f03859273cae7e1cac99e67f7a9a96a6aa2c139d5a4134c30bebba6a178a1c8cf91d8bc47b3301010097ba4d7a90ffc251678e861007072d23c7276026dee3c20860d5964c5e7e800c0076b3d996d069c2d5c2b8918c061ed68ba2b757b196e6d69e87e6040000000000000000000000000000ffff6b9679d94e1f088f0bea4590c29e0a8657faea9d5f2e0f79cbe8f1cae3cf9111e84ecace1443ed8dfe136c539019684d9511d1bba8070df5db4ce6161b24565f0e90a154cb4e38751eeb000100b793ac00290abfe0ae6e8e96dd228c0a209d774ecb90b1e7f384e3e975f2aade042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a444e1f17f78abcee6d2ed68bf2c82afbf56ef9af67313e2eb655ea5178850907cb3057cae0bb5a1d09f161057bf62f9d4890c676c44d67639aaa8fb877f1db08d5e582b570c25d01030057084e10bcf2851de4a8140c15fa2384c93f4e9993f2589f47b8f9a54fa078cce68663ab8ac6b7e137b6b68e6db778fd51f15af506be2359fbef7e2db50000000101000101010455d1f32b4e1fae6dba4a6b6c1fe9fb23ba8b22ff570bde267c0ad0542b1df45f64f931fcc083850c95fae1f7bfd478f6ff3dbc87385cfa7a0d39c7aa1d2f8e7141b1dc37bcd149cde1a9010000010057606f07ab5e314683cd88f7984706da8ad0b9c3c1811f7374b3653cadc0a2aee5f9035ab116e616ff9d6a2cb63a6122d36c2de2ad711f0d31f63fcea508000000000000000000000000ffff74cb570c4e1f95234e6e7d476318b4811f1daaa7e887fad24b1499b3472a3a7decdd88e8bdd14551b7b67b22ab896adb298600aee96fa17349d2777d28f0cc7d39fda9cace46c23036a0000100978c1ae2314759a97b9e36de89cdb8764ba13bd83256e4adfd0145c0619cced1cd9dbf335d3cb88e8659575f6a810a4055ef83fddf0295ad851d7cce1700000000000000000000000000ffff44b7c45d4e1f0c49037992160cb8d7f6ad7e13d778fbbfb5d10230b456bb3aca1c044e79fb15c3b1fcef7efac59899eccbd190bdc40e227ac0c292b7d209dc75272563f6e3e40bcd4b66000100975c1fdcf549fec88dba2714f9536c0fd5ab0bd0cd5149eae9c1d578266a4776bc20451b8a05c6cba10970aac3cdafc549a8b40d7c23b9481a6187100000000000000000000000000000ffff34dc3d584e1f10142d44041c90621d111283fe46fd8b2450d4b9bebad194290fce09ba080679c748b1ba70e3959623f127af0d2bc9c46dddbc8e0c2bf0803d236fcba6cc32a8123ef43e000100d794a4e51ecb0e2c53dd2135af079344bc46a5705da9eea9f119312d91a8134d7b31df0ed546c685b38d9e05e3b3144d351e5f6fc46aa0b917a4f3060000000000000000000000000000ffff36c8c8e44e1f09ca23af93ce00a95bfefe790ffca791e093a8c0e79675b103b2a4d06f930433b3f6b15c83f4e2c4b5118fe0c27ca13ae855484a6e6f72f6f4793b80cafc202a981fe8c1000100d7f0144969037c992fb98d7a5a58f108d98b2683222533321218875cdb3241906bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d4134765e1962c65927aa1616e3783ae7cdf8c3d19b4c26b477686a9f146cd9ae40eb7c0e01a1580d5df8c32d1f4c43a52f62ff5e06efa257a906f0adf9b184b0738a187a5b775df68000200579b91f33ba2a2979bbf1fd235f041739acd7cbcd52c64eec5e6f982d1a87f64130959a181252f08bbb4f7127a7ff34765da22d5f4de66aa4f2121889b00000000000000000000000000ffffa45cd8694e1fa72192989a29df2c4d9c622b642732367bebb975bc1d492ea1f29bc65a471e24c2d4727a021c55c6e2269161d3990dcb9c9af58998bd37f878f460e6190fc94af0d3cdf40001002659116e45f28f4d2cbade36dc87c12a493a5a7f5306020057f35920a1c8a292016ccc7ad12a16cf41e80ea33fbca14923d636ce0d9bff668222bf6dad2d1d743d75d1b3d30e238865b1a0f5553720cd23aaf0ba0100000000000000000000000000ffff9ea012384e1f8c2d2fdcabe70bfa6dc5fecddac62cf29a8864543efe198e21ed99ed09d2b7a023ad675d9b7f4ea2005da649533eb1a13a32d03423002944bf6b79fb9d37194011754760000100a305988e9f546a3c67157ea57d86027771ca773aaab9020057733f716783008f7615d056888ec922d584e5473b7aae240dd11a0de890b86c4ad772704a0499308575f6b94c384c88fdd6aad5a65efaeb4a1901b190000000000000000000000000000000000000000000968e86e1e7810c7064363b9e4fed111fee65852fb79bcd7123e83be67837e5cb9a368761095cfae018b30ee932d087309fd3714128ad774678e9704baad148407bb41a77000000010098ac3707cd76f8ca828c8b7a86acb0f3768ff9d89927ac4ea0b7cb43fcb3e6716bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d4134765f1975c482384c7bd4cfd5930fabac11646121d420e31883673dfb6e6e3bfa273da73a2a91b4b69cc108eff9619fdbb4cf40883903918c1154fb2b589d80caed855a157b1240002001889670d9b727e3d39b2f0018c64d47cb60c3346ce97bb423b3547ac34520787e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a074e1fa7afe7674de986aff5e2e0a173be8c29abed8b5d6f878389ea18be0d43c62ad1ba66a59e9e8d8453aa0ed1a696976758762e689e7bfecbe16bfd0e6138eea642ee0513c5010100a305e3053f65754c630ab036f1370bcc6835fedb9c9b0200b829b67fdad85ff45a551e71b759f56c42ef822abb9597cba60bf83b1edf7160c7c3fac0caa39ba258a547088c1c487ea7031dc00314ca71509ad64b7b00000000000000000000000000ffff2d20db704e1fb4e545a909b1916959139eaea845262b0aadec1c5cf555922bb6e6d1804343e6490428a382a0c01832fc70c50438fea6ff1266f2c8aa7b1f93a15b8882013af424d7d9e8000100bb01a4a889553772ed86b9adec63a244752576d1cd520100381fef4ab9f067b9782bdf19fe0a7d316a9a0ba26ce2b2761b41a1970ec549b1d8b78d6381579c07fdf1c9ed1782262d0a11c2ad1edb94c4c00c2b100000000000000000000000000000ffff342353514e1f8f60f80538f335ba0f9f5452f02d7f5527652671da80c3d1c10e31e040f9b901a53b476a9ce02b507958bc8a65acd7b02adba90fd804d6d89438ea6729608d5276687fbc000100783fd3ab928204573e8a9f1cc91ba620dbd234344c49ac22ff5bbba149e9c26fb2b5f6ad6373581fd4a3a11ee33f81a5896330d5d76cba65efa493c3ad01000000000000000000000000ffffc210020507ce032bfab78f78c968f4a1e7fb87d9b3bd75dd2a49e18b7592e4274322660c27f213b898442eb41f5db42291a2172508b6c8a4cfec45b3e5793f8413968d3bd6aa7c6bfffd000100b80f165f4331e9c2a4b10cf4aac450eac95752679aca3c32cacc20a7dd261cc8fb48c2ed60e33321f5f8e13d0e074d5145891680276d3d9bcae5bf67d701000000000000000000000000ffff36bead174e1f10d9901c0aa8f9b3e789a1413e731ff07b9b58d4f53925f53a1502f00e6ccf056dc86ffa5595a1ca5c02cbdfb38b1cb8e614deca3fce7919013e4e440fd3d4721ad3465d00010018032b1f077f0e7fe6ab2d7c9f96ef2268872bce9d6a6ab737bb2e0c0d84993c042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a4c4e1f121adeaff038746afb470b84fb3a58f1a3bf304cac771d16980845c4902e5da34d366394c8e84a46d8bc0c0a1da23cc6c682e285e7a7bd65e9268c1bc695091eb4ce5a8c01010018732fe66305e3ddaa0f40c3547f1e0a37f5c2621fc716b43ffd1a2eca81d18435972e9a83060119098c4fd59848996682de342c230b52864f8fe2080000000000000000000000000000ffff8e5d284f4e1f05b69b964d581a7659f5fcf2cf4a50a75e9cacccebc4e18d27364225eb3f9886de5472cfffbf9cf029f81b49037e27a2a4e27301bbacc1c42b5e2268bc2b4400599edcf10002001999c60acd2a8a66ca833ef65edcde933436670f70e57d693e00ff0856938df52cdd57d96576c6336413df28b773df1dc6f80b73332630e4e2a252e34900000000000000000000000000ffffcef5a73f4e1f8de9ff4dbb2fab156e5b3d8aca182a4c6807fa924307c8b6830180892cdff466f3b43ee0ef4370d3ac4828e2dd341649acdd4afc43b1275e912db55145e5947bf6735c4e010100a30502d89fd0aa31211b8e8daefc86713f7b625ef69e01007941e0458946e0dc791875aa660f454f898ec81e10c20d9229a63d4200679b86fc07fb8b877bdbdaf7019b035a32cde8c20d8c06c4122f8baab4540200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011b1a772ead943d16af20c36c7255e18ea5102700010059160c6bd0c377545d97efe36e0721538707364df39439b946efa29873de0b503ecbcf63d7038b1ecc838248ccc37e3267844ba7d4cbfcf24d79fd566701000000000000000000000000ffffa763b7374e1f822967c827427a4ea722459a6a5d007c5a14e1da4b6fd52417914cdac7dfbc9233dd046cda0c2980d1936cfa8b22920034e4669b580af3248979acd2db51c30d0794c76e000100f9aa14a66c5edcabbf6857ab16b02e1b725ff23f6fce8e9d174d87b00c1f496742d70ea01c8e792ea8f75df86a9c408511b21172b45a53c1f6c0251f0000000000000000000000000000ffff5bbe7d851a0b91e633b72726091f58e3bd1ede3a21de66abb2456c2f669be8bdcf76f3ab76aa2d75f7d03cf2f7d5761ab15e62e00613de6d2589b9cd1d134c8aee733d9ecf70a21d334700010019ff909c374dbe749d331662dead647a91d571bd0584002e4d49cc98c98cd0bdabea16ba0d55702d510a3d43c78809f37f7c7e7f8f75353c68b2e3cb3000000000000000000000000000ffff951ccbbe4e1f03d325fdc665db2900c24c0736b927b7fa7ba7068c9595991e6cfaaa0f8e1269a31f6d1da2b85fd922186a9979872cff68eb72f005d366e18db38f638716d97bf60d53c7000100597bf0c915434cd9eddcd2360804ef07c4f06ce7cfe930ece85e6da95d3a5b5a7acda8593971d282f279aa9a5bd80d87163cc920353944b2af758e80dc00000000000000000000000000ffff12de6f464e1f80f8dfef131ac329d428504e7cb89974f188f07caef0668df1daa4ca8fc5f50f6c5945f020271292dc220ce313c00f16f0c742302a89db49217e328e18811d0ab1904ac5000100b917f7685d22ec2f95db103b2d18d3bc7c280106d0c07ee21fbef4c07a22e14eb29188c03efc5303b140837097f45c1cee91599efdf9056dece7e525070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000258f7afa5a4ec575f6781fb2d91fd59f8a409d6000100d9ab0e65c1e3de4eb2213dfe0c1a01457f13e6798cd91beb15a714709c874b04042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a1f4e1f002a2e19ac4b986e20f55ddb19cdfa69cbd5f76c5e2d66ac6d9c8418aa1f0836e61b643bf48eeb004ccf3f3f0f03b82acc93037f5cdf14ebd273fc3d4999b9cdf81865d90101003988b5f83343213ac50805d34bd73f1e06c48b873e46ad299aabe92fa3cdb2f281ead6bb4f642ae5995579e3d25feaef564a3ac6314ddb406661f8561000000000000000000000000000ffff22d1d3864e1f195b44e1d553d160abfcf70b8ccfaf24480ad34fa7917fb87675f712f0795a23dd0107f5f3e39c07474697e95b15170dba7c694196934a70e11ed92a56db5766131968cc00020039c4c1a4d6dda0d3eb47b242a5919a81d45f693915953c090b5f797c538eb83931946afc2f225da2a9135e62ab8010ce3a0f6ca0dabd736e980d5bc4a300000000000000000000000000ffff82a2e1724e1f90d98ff5c2f2970197ce80ca8cba9431f7dd7cff5d9fc9c4e99c2ed06e598f273d31dad8d8d05655c8b04599495cee23bf951db34349385992e0303f21fd7240e44e4b02000100bb013c32b44d5fcb611eab1ae4705d74792f7a6c978e010099941c6692c2f238e96fa0df46f978b9013b660cae9f362f9efbebe5ec94332d1578183584f0734134246125dd61453e764fa0a4dc018eb48b470fa36d01000000000000000000000000ffff50f017c74e1f1260b9b40d6a39c14c5f52763ef6e98094a6d41ed35660ba24334c50b60cbf18a524aec8bd4d0203c3257e70e193fd308c5f76e0794a0729c41142e619663b77948748ab00010099ccd9983f3433ae1fe5d5cec96721dff60b527d0f1ee41f4e1f9867bb85fb4143c7372049ecabb23d1ced9b706f49623748f322f31b28b0cef58e000000000000000000000000000000ffff019f8feb4e1f8e53b8ab39fcc259aa22b93d1ab4e333353e6d56b9bd4d194985a59e0de5060c1225588a256569848ea421725223711b07ee5dd8d1e99e4bc8e015d94346580d7cfb1c44000100d9f20fabe28047ea6b5c426ec94868f34f5d98012c5f9549b9d259e67f71d6ef0d40092b542aeff047bb3bca3f9cc454404d8ad8475b7defcd754e18a901000000000000000000000000ffff22e0950a4e1f1560ff0ddb3c1e8bad9f2e237b3ad39c37ba804fc09d4ccd928362ee874308b29e827ea60c21b3c04787d771a16e73219d39d1cb8cd5844cb453cdb1a62b73e0ea6cde7b000100d97629f8a789f38d97356aff702276d6af3597f9c34816220b2e3fe3d2d75e0e042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a294e1f01f24418ec73b09b00514dba8fb18d6d8af1dc2ff93d594bf987911f3b98d659eb43286cd450b7e1ee5978b361660d737d0138cea5135b7e0d5ff96c57483eb5c155f144010100998f9e8a86d04b8c70580a66368bbc4c4edbb8c8cd3beed3ba3c3ba712bb4fae404404120953a8d35895ca9cfb4b61fb0d02872397d746ba3407d998900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ac3feec8ca9b54c378faf16c260b0c4c57876d100020099bf316f8cb867be68dc9bd89bf2b8b984eae2fa9fa6ac19e862459cb7cf7939a2e6fc8cfa816c2dc9869773359c7de54bbda0c587f80b10ebfd89a97900000000000000000000000000ffffadc777f24e1fa73d8c1e640d29e2257042a39bbbac8d867f69ae252e146884816b98ab0d0526ed4992d9cff22ef04878423f66583382212e07f7407be541b585ff0a8225fa1afbcc9ac8000100265901775a0fdf52eb8f479398d971aff60740c0b57101005a94cc400caa13fa0ef41df4497ee1256040467dfd48cb94bbf53460ab958113299b4cb4b026c7a1d33f0cc052f3246c99a1b0083db0290a811bd3e01b00000000000000000000000000ffff23e6538e4e1f0d554f8bc61403a96326c47b95797ff0ca91971a73f50b9c95837b912d8f0c191a85792f7ae6106fbff7e51d50818b18a8f3150f40bb0cabcfae79e98faa3d9d855d9f270001007a555964bcb9ad2701ab5af920a7d98472e489915e247d9471f4fe08a1b76e6d49f66db793dc58cf7506e10bc9f44202a71c8b99fa9686288a5af335d301000000000000000000000000ffff5fd91a8749c8955368e9fb5cce100a0ce6df64bcf624355222e19032cff0c80cbc75140173c2eb47863b189d2423b64af6544226bb50387b6ba2b25346e89278b4fae874858a8c09960f000100baf99a2b89bef2f1225f2676a00d54435fe139e7890d7ead6ab9d64e59d8b92a606c3240b152fd593db7b263fe949ee92b07c20ee42bf0097ceca6430000000000000000000000000000ffff3ada3c2a4e2104eda2a8c31489e17463ea27c0c39473afe2c9153641028de360eee8ce213d36a14ef8f8b4f85fb2cd70815a9c1f56db78a2c01c623373d2761103791484d3ad1164c06d000200da61a86f405f6f062b1bff6c84dfece7a610555f1e0c8c98cc94ae38b8a221e08cf77bb77b7650c4d89b588bb4c3d543feeae6e157e4056bad25a31a6900000000000000000000000000ffff2d8268514e1fb7178e93c29fb6205ae5672a8683ecd55f16ac9a26c3401d8ea99df4c4bfce9436b76295bf013951d5b6659e41a5177e86390b1e047a7e74910f46f107c5e897e7ec746c000100bb010c7fb041c6c2e17110f0495843f02b757628265f0100fa2ea57c1332790ed640cedcbeaa54614f7e70522477a4c04f63d903d35831db6bc82c47d2c643374a47ab74bca42e3f2033f995ebb3ea55807f5fbb3600000000000000000000000000ffff34d4134765b0989f584df6e5a359bc469a4975baccda2bc6a3fc3e89721c639f5567db7abef79f31ddeb4832b95418d49322419d3eb0f312f66e9433c75b461b481f0576d3acd398c2230001009a57659092f2e0943e75e3e5e7a767b9d8bd793c455b069f2de962ffe57ac3d0e25bbecb8cb3a1ffde3031ea275bfa8da4983388efd1a136b3076999e400000000000000000000000000ffff8c520f8f4e1f10ac5b643b6dfa29989103ad74641e5fa27626492f08b8337a8415b598ebf2bc6676c73621905a71a15d7bf9f92d6efd6150352212815041fa5dc70a94c7dfd507f486190001007b88a1cdf40dcdefbd47b1a96bf88be1580bea00597d3b99a3f8e690897f0c40f3e8069958306c30252ce3b2566cbabcee61f8bbd6a0c5d219ea13060000000000000000000000000000ffff5911296a4e1f848bfbe1bf50debe1322e14c9115adb3b96e5b8a3ae96beb7e2161281d9e56c30e43478d6f39835e3533a1c54377258b724befff5e89d92cbad3cb0cc8281020b0243bd80001009b9460f56930a95bd5f8631aa6a1ff1e8d43e4d7ca378bca46d8d0f662dd4653d02464a8dbdd1302ff3745cee38bd3c0ee6ca3be3e19489afc6956080000000000000000000000000000ffff34348bba4e1f847178ed08f0f5728dfe39ba9e3a43555b4c5e8100d825d91bf452bb7dad7bce7e8224fb665abc59cfc74d3bd1e040e1692ac6d2aff4da4de4a815b41b58369de627380f000300fb7dee56a72414f3eff6006d0f31213c842d7c3c3588dee47b793389473fd361e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f80440100000103000101010444437a054e1f010101010444437a058f30020101010444437a0505a3a6a63376eb861bda6afa09e28e39ba40cdfb877ee6f9aace10eaccd4caafe8d9243f2f2c0ef982a0766347073cc199bbb5b414e1a68e36a25df214ac6eba35782a0437da0101006ae924bc352f788d3c83a0949cb30964483eda7a01001bf266fbb2b526bf5ebab51fe983985f579c89eea40ebb2b6963ac0d7474565891a79d300a8d16b09eec921da12c58fb022e34186622a8fc1c88bc120000000000000000000000000000ffff34dc85584e1f93dac0f5d028eeddeaf4257919511991872523675ab24d1d971af3ab1900f27fc617d1d53a846c32abe1ef52a2cb26ee0e03a8899ae0f3a5771fa41ee75752713e920fd8000200fbf60e95f35aa3e6315669598bd20cafa41a0674b87afbf2b40c9c2e1a65482ee026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a114e1f943a88959611417f9e8ce4e664e1d9c6a839daae14f54ae8e78bc5ef6ec1524d116efca49ecd5c57dce31d90015a51ff63f16c50b9cd1dccd8465e1802749bd00dd49a2d010100a30524dc9affa9045729055355f5b8dc4125d3f1fb1f01009b3fc4ec99cbb7d9d47bbc9e2aeedff625d6cdf1a0a8351df569ebe98a029242042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a3f4e1f8f70ff352844250e267b31c0ddb83dffd4cac43532194bd47cbabf410ca29fa7f1ecec08c8fde8c0d13910e903016d5aecd8bcbf77d0dae7f59c750bf68d5d33a45ddfa2010100bb23dd49b698629e519eb1a09600af7b270e2673781c408ef046a7ec7618dcc0c4ea50affac7f7798f95c54ab3ef3dd487c7881e48c605df635c253e1100000000000000000000000000ffffb280576f4e1f03f959fdcb3eefebe409ee7044748f71ec8cba18a7a73df9d55d118e170d7ec2540d5c08a4cadc4bdeff3f7886265ac1febd730bedac1010a2943dff1c796cc544609f7c000100db82e4b93c64f29c1c31ad98326836f404a2143415b588a0b09c3dd2358e3bc41964152511caf8008e3fa873bb8c35afe6593d46619c5dcccad05ad90401000000000000000000000000ffff8d47264f4e1f81ace6ff9442d477f7eb80ebdaa666804cd0c1d6cb131838847f75cd83540eace2c501484adf0333b847ddaa6c087a86c42520f12ca094d0c840ec18eaaf497adda92995000200db6a9006be408fd0e8501a2e66830f891946b4af6c926fad5ce9fb35e44e3feb5def57dddfdbcb46e6322161db6dd38f71cfb2c3803436aa729c665c3e00000000000000000000000000ffff55d1f3084e1faa2442382b30d27ec5057d7dbdcec5de5d1de2c4da6821991ed897f56582818c6a392f862af14d9697b76b0ff4f5c395fd9cfd8235e4be72c7a1a11a70e7da0e6ece7773010100a3056894419cda4dbd7f03af9899f99bd239098c8a7001003ba330d521ffd8d0d7fee89e5b1222a91edd5ee077751bc171cec7b9157bcf9f042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a414e1f0b04bcb5cf6d2d6df5979234611da42854a5e69374a29e0c85128caedb53d9c818042613d2f30f3ef782ed37bd8ce161f1b18d69579c40dc8292842f02ff5f511e61609d0101003b43be0539e3780c85607d76b610a7aa57b27658c410bf852cf9daa6863631a2128cd611d3681883531d9dc3aafa7edc97ccc9026f553bea3ac2760b2200000000000000000000000000ffff68ec34d64e1f915b61f2b726d5409a26a41bb3f350c1e4bbeb5e07e808f9f68361d70ac7fc2fc33178ac9639ae7ef484a427d326f24683c1f4c02bce59c4775cec76c6ca37241429c4e20001001cac95e0a27f4a3de7ec2d4d9b1494e48c6750ea21360e5adbd236ddac7f5d995bcb2758b4ced09c16c0e5d31e27c4b688615f5c121b6595ae5e940a0000000000000000000000000000ffffcff661694e1f8c26812d38faf159f811a09c1462e07e40d6b44881114358cb5390b65778ee437018c5879fbf935fd78955899b67633e373eabed3a46c91e10415d8a7e0b36c2867b92f90003003c7ce42cb2e41faa5980ae306563df345ceb182a4b4726349bb5553749d27027ffa55c0788d7ec18bc37f2a3f9f90543d8922ab62bca18ca085bd499010000000103000101010455d1f30a4e1f010101010455d1f30a8f30020101010455d1f30a05a3b03a69e21639859a0448d9ad97ee33d9c7807adc1aba1791c759b87aa81b99d79c19ba8778cdda6533ce9f0534a8392f0c51904ec5f4eeeb8d54b4f7b43c67a9d7a202ff0101000f4f47471c554804e4ab6ec7bf6fd289c438c6e302005c14f8358952dc50d8d2f989565bc57be769fb3858cc002c1350a01b274fb09ce026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a014e1fb6ee48c7a71a9d8e0813e68ca09846245fa155285f24a62b0ce9cb0102b1994ec58af8ba2a01c09363bdcc395d41f3dfc54ac69739bf492d4328f1bf0d49581ce0b10d41010100a305cf6235366edfccb366a298edfe50b6f6a360e9620100fc24e51446787e48ea9356b09a14ee2b62eb36ea44763ce7431c1b6e6b1ce441fce726f1ae14c80c13fd5e6c275564ef99df407304e144e4cbf012050000000000000000000000000000ffff95f8331e4e1f99567cfb20c6bed5d20638c31e7e512aedda02649e82f2b955ecd3e34f73c2229b350069f6e74a4304acedddd87997fe7efda5f8279fc8c7490aa37ed57903870a1f4ee10001007cea175a485c82eb49046074e08ea41d57a058dfeec2d2fd1473b21e98b59166042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a2d4e1f00a2e66a810493a91b5ed1a8ef8ac4be41543598f5b4765a6f5d6339078ab88030817dc9c9bdb60c7c7a02d7787d6f2ef719327671f37c8f4dca62dba332ba17ba42e7e20101001cafe324bb967bc634d9e5b67ba4bbf5bac35e32605249523624d1acf7be0641c6696889c08766ef56284c63103c37c44e15c9ce0138950745fb79695f07000000000000000000000000ffffc3c913284e1f98e7dca1b8dbcfdc54faff65b94f81f2e3fce6440bb10848d434a96ebe30ccbb33aa586a2d0ddce112e38cb09bbf13c732b8da1c9e22be858fa29f61e92d6d48eb84e270000100bc973f3a17c5fa3dd59493b7d2bc41ce472be2401459ca2d96fac25bedb2b7aa042a425ae2d3289646d71765e97cfe099acdf021aaeabd52393477d84500000000000000000000000000ffff44437a304e1f018a6d23ae53d6231f7dd73a058f125340e92f6e97897f017d9d9d4e6671bbd92241170dfcdd5a4ab8ef47ef12ddcad5853fd590bbfa10868687b7c648238857248adfb70101001d044d90e90fd27861d7340ad564c41deab4311339aee5c394c4e24140eded303a82c9ab3f0d8dd27bfbcfe9d074f221e207e33a77242a538d4e4d070000000000000000000000000000ffffa747dfd407cf0d731903ca090050801af45465c96d1248532819959a5a97eacb1ce518dcd5c5a21f20676d7c893f81ba672fcfb0f805ac1a700d32a6ab3f00aebf68573e26613e62a8250002005d60fa4b85a0f1f444c6ceabeb2514c3c03576cba2c099d0e0bd4ec394cebb91e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a0f4e1f81ad0f9be5a88ae62ff54fe938dfceea71be03bd4c6a7aebf75896e8d495d310acc4146aa4820bc0e5f5b06579dedea5cf83406065cb65ce74100f3c35a0324bfc92dcb0010100a305d96b1b3cefa0553732d9d6cae76666eff869aa0201001d09e6c4660596a0cb7a714a044d41f208b514ee79147f9d73d5bcec839f4f9f83f4a966a456d795192b83bd2c866dbefee9dc9435dc3fdb42882e1c2c00000000000000000000000000ffff2d20d39b4e1f08e37b3fcba972fe0c2c0ea15f8285c8bfb262ad4d8a6741a530154f1abc4edd367a22abd0cb1934647f033913cca58aa2dbf2c2b6149412562f306841959b9cac234b730002003db1bf3af77e15437ecb63597b55627efe44a531f0c788ccfba4e53935ff433f8f0d644077c903f8015a4796855c45911841507d2ca5223a542563f0ce00000000000000000000000000ffffb29d5bbc4e1f980213c5fa8031aff096b8a3eddeefd0584a0e5c2492f631ef67941e082763250e2615975db1f1bfae631f80d41d839a62026629f37f587b1dc70ccbc6732225803db4e3000100a30586c2ddab5b510cecf14f20adca99d2e93204120001001d02c62ec31b37a00a906e0f3d29240a2a8c8f4b5fbbb86edaf8019d18c7e4b90869edbcab8a116cdab8d12560ce2d5a8e67676ed7d329a5f754250f0000000000000000000000000000ffff8b3b56924e1f98bae0f71cbb77fff1560f45680ada9492ec4c9f779df777754b54bfb3474729c269399bd3cdfc736866364d3fb011d1fc8511ca8de4f660ed57620e4537737e55622bd90001009d8df45a92c9e486da531a3dee6e38ce6d32613f0f65c3faaa4a411e31f8bcdc1291fcb624ffa2a24b22ac9f13e47743ff59ccfee42acbc7b83c300b0000000000000000000000000000ffffb23e440a4e1f89277d2620e48dcf8456cc8815aa18ad3587bbf40cf0d1718696bd126e19791bb600b22f1063d4e5e8efe85fab8f90c8d930d39369c43d62ef0fa29c85a349673712f5970001009d15d4d2f03b000755dd3f7fe619d08afe237a6c12a8cdeee78b7570149528720d02a858a529372307d8090f01b548f0ceb58bbe28c9590d4838b8761600000000000000000000000000ffff3feee5ba270e88d719278eef605d9c19037366910b59bc28d437de4a8db4d76fda6d6985dbdf10404fb9bb5cd0e8c22f4a914a6c55665fb5dd519d994f5e5706b4047d65ba1b873d9ffc0002007d46da41bec61f0fc7863700fec88b6f62c002a944926f15601c769a3f4f7840e026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f804401000000000000000000000000ffff44437a0b4e1f82f60dad4b7b498379d1c700da56d4927727eab4387a793b861a96df47bdabe5666c270acf04b5b842ab54045bbf102af37ebd7e83f4185ccc5fb617a7ac9e09c11ffc12010100a30562f9e99a3add57b380c2758dc2d6e96e64108b3e01007dea3af845240b50e269623bb46c68e1dda5146df0a7087f84f91f9df5573b069a8d36aeb1a1ba0710c884c4f6b5846ae0b437d8cf95623d01fe8cd3bc09000000000000000000000000ffff2d4dde3c4e1f0c1c54b5377e920076f2fec26824a5d15ff6144dc106185a614e60fd9722d7577609ae202168a02a50ec45e01f5b7e6b555ac4d7829d25f1fb6c3abb84a180f5cf746f3e000100bda33d9ba20f91152ed5ef65e6876f1cd67cd06d68647e269577bfdfa139472541c3681439c2680bd1213bf72e7cb062bd58a2893ebf596485def7b67101000000000000000000000000ffff47c6dc824e1f90f7f3a97069a5090c885a85aa7e8c970b5d3982718dc4394e2de8ecc2d1c38c8ef51a322e6a5e9660eb794af6c40474eafc007af0360da4457b92cbb8ad08650f20ea4c000100bd3fcd8042b7d7ec522cf00834722ed62c0853f7a634034dd46c75d61539cdac9e4f98209ac0950b0726d009a9922e8b7e553876475a191ec0e07b220f01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054e91ab69c8979d6f2b5f476f666dd09a07f099c0001007e4c78740b1b8e1e2da02fd33b9632b3fe5bf1fea5266b58ffdeaec62d5ce2f562b46ba5e245b806d2522b6bc06a17fc1aaa558c26145aebae0a4bb9af00000000000000000000000000ffff22cb31a34e1f99cec23c58cf89081e39d8862912ecd50a18b44dbe92af0378ef2a3bbbdf4a6a11a76af5b70db205cfd31323391ee64001878fc5b0bde020f35fd7dab0709c0faacba8fe0001005e8193fa6f63a29c9f74fc3546d18d7a37f419244f4d8b4e5a2ff6b8af6e12e958766fc46a8bab5eba49c6c3f625b1bb280d4a609407d672415288000000000000000000000000000000ffff3695cfc14e1f8ad9500ef26ae510e0dd8cf0568b2a89d1234697873db2fcdd11674a73caba91cd416f9ac701f4f7807d8db102bc4a39b2ea896299dd86bc7c3353a40d354be43ca263720001009efe6782f18c780d3691c0d9b5abf48e0b75635e9c8c3a38af9580dbaa334b0b0e2d142b4778d1ddab063a39ad555dccf6324784847247f6016a3b000000000000000000000000000000ffff91efeb114e1f11d05fff5f406fd207bc8984188894b6bbd32098e58244136519a51c183c70db3d713a33c9a55f8d6993f644fb34ff2d9e1a38143b4dffb85126ea5a1d78f957327ddde60001003ebb51759663be0ab997630bdbc93e622179a4dcd112aa180661f032a5461d024bce3f86ed570745882b0515d141c81a198bac0062b12cb561977d0ac601000000000000000000000000ffff47c6dc844e1f1135af6f4c73cd6f45513a9bdbf919fc9dcf76b18c0a3256b4aee0ed48360fa88637cd113502e4f6027232c2ed5de3b857124732477195239cee3a968c4923f5f8adb9610001005e43a6770a39e1b23842a18166efc352bc9c0d07dcc6be4444cfddf2f6dc846e5b8892fd835ba3a45b214442d83cd99c892a206573c67914eb357b150000000000000000000000000000ffff175b61d34e1f0064583f3f5dbb756708aa405572d2eaf3349ec2d9048c93f21a2d1e5a0da7ae1675d27d626035ce0754de1898d5cc308d83de554dcaf4e9c01608497bf4b6bc632101420003001f009f5bceffdaf1a088976509387b07b2bdd3da42fd3a28ab24a1b3cb4bca8ee026d37e13aedcfbccdc13d4d1245422d805850e0ac7adee22dc4f80440100000103000101010444437a1c4e1f010101010444437a1c8f30020101010444437a1c05a3b942e2e50c5cf9d9fe81119cc5379057c05fe15134f85847356b5d1f6a21f29f4a53f61f03338d056edc15a8c63fbbe8983259ad7ed51267fb83a8e41bbaa5ab55c632e1010100fbaae48b92890e7ebb7bbb133af936833a9975c001005f8c48db672ff4fdbb8c73916b3846aff1d93233c063cea706ae64593de4aeb0ef28c0a9d509aef19d2f67ff3167a0966d57d6d15e1e34de0e49cc100000000000000000000000000000ffff22e99bec4e1f90ea99287802a44836309be934ed63933b423580626adeb428026acde6bdb283f370ff19bb37f81b0e4775187ad006a3177f797c6205c1997ff1ffaaf6aac98c13d865dd000100df3cb4f7b5914b8cf121e25f982235b45011ec65b4334c4e8cb82f3b1cd4c34aa42908ebfc04ff050907bb5526b010149e7517fe39813ff2463f0b000000000000000000000000000000ffffb93e96c327118ebffc014c8b97d9da8841464d3c7cd09b9f679471a068666c217ec13524ad7ccafa50eb18126c99edcb43fb74e290b9208d9e4d63aa7667b7b67bebf81d9e58e2f4d464000100ffa884120266337ed03f5fa9b027a34f7900edd40285762c931842677533c2876e7557de965a623c094d402cc39b2c243b225c02f9aca6f8ac1fe2080000000000000000000000000000ffff0353f0dc07ce8074793934715bde7630f4f267a9647955ac45400792369bd3e5f88e2b9d6c809251b79428e3a8ec07bdbb7364e3c299f2b29900381739339a74d57fdbc677a6ef249f1c0001003f6da2cb03f59857012a8bf38003c81dd02430348efaefba70fe49280dd440c9f33dbae25846c3f0626f5178e379e2ac65709f26975e82abbf759ee10c00000000000000000000000000ffffcf9afaaf4e1f069dd3113d6320397e9674ba3595f46dacd562e013e9e80a2e7d1095525f35134d4a8c19f4cd4a19d3886edf603287558fe94ff326b3d3f9986fe97fb2eefa9a36562919000300bff14121e137fc00cabf32e0789977c9e839b9de537b075a72ba8098914e232ce68663ab8ac6b7e137b6b68e6db778fd51f15af506be2359fbef7e2db50000000101000101010455d1f32c4e1fb2b6553f221b16a34b996ae93343f1a0a085a106845f089c2d2258176b218ab6f5fe50acde73fbaf0f28039394bef24787e98c408cd1c718a9c4d0e37f826bf4bc4e90460100000100fff5048726710ee3995c937bf7b3e8c9f9e4fe5fc2a73ca93d0727c25b1b0973353cce03a13a81c74d3a45b3dedd17e9e159e534eedc6035360ef9d18401000000000000000000000000ffff8d47264e4e1f0fa3db9b808db89b49f91f6136cfb966288a56d731c9afd44dc8c4819ae5c286d08dc0572249f41dc919f888d562340190614a6e4cf57a7381bd81839ebce238cca8f9f10001001f0aade9021f071deead3039640d9d357370352f415951669df1c5ea48d2c0b5b64f81113ee8e6e7e704617506dbefbe925f2925693ee86096346e0d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008923f5540c3cde8ff5f57178422303c469510c480001005fb6b5cb76ac928fdd8334ec5af40c467734fd4970d51f796274ce5f650cb92af3bfb0d7a45eee0eac355f2c8b58052591d53d96f193c0af1bc3380a0000000000000000000000000000ffff9fcb15144e1f06b2e598c2a16e7394cff63bb9939e8bec49849f2520f97d8de70ed9336625ea0582889be68007e93663685d03d6996b9406f3c3f3e20632e58e419a5107a5db669cb4200001007f12b79c3583b94dd23207afa39594c528b66e4dd7953588d9faf46743676d5a7a11032d1f4965337f4543150f9d453e65221f1d64b21f00922ca3000000000000000000000000000000ffffb9c313d44e1f8f53fb19c3be85ce00e96d634221f20a06a3a50942998193004264075a70422a3305f57c0c478a70ad69f1112e2f9993a6c4b0ce143aed43b6665e086fdabc05f6797a2a000100ff92ce2d64657cc3d4d9d82547efca2206ec1d9f5dcf2776624bc0a58b12ed3bb004d1b16e8bd67540ea64db533d8b8e5fcc9bcb9d0466b4a327a6269a00000000000000000000000000ffffae22e9734e1f8c9c5c77fe321ff0a115d1ba5bf7462063ef21a82ba796415f4ee538bf9e8a6a49707530c72cbb6b60026c46ff1b9443ce7f101b7f075273c892063b1b8571311737a5760001001fb778f03688d6f966dc6c67bf2c492b04c46090fd3e65e6164ff393573f56be3d804f9b84418bea8e515e321fa7e41ae594415864fe479b4bc6ff24d50e000000000000000000000000ffffc3c925ff4e1f93612d652fabcdc0052e5bde98d276e49ea71d050a323b98c368a06742fd964d21f567e16fbda2c17b00adc470f6f614304c46d5f01a600ba66e3dbbd8bf6dc23e8c52970001001f99c15bd0e1141b1cd28843eb55b8451e2a4c7a668950bab85a9244209018f7401498319af3c883d8727b20c312db37cb5d7f17307d4b85ee6c3d8d4300000000000000000000000000ffff2e65348a4e1f0d2be4cbd0faf7a27695a4f11690ba772a32c9df368f0558998681d697e60888b7127314dfa8495096050638d8507c92a7327f6d42206d3642c4633c4b6b10919202cc8b0001001fbd564371ed64a766e5a7975ef0e7e8d40a0f18a52e201d1e4d5e48bdad3db4bfcf015ddea1a4734d4c1a059310d1125edd3f78d53629878464e9070000000000000000000000000000ffff2f6fb5cf4e2190c0e9ec9dc5f08b1d4d0211920fe5d96a225c555a4ba7dd7f6cb14e271c925f2fc72316a01282973f9ad9cf1e39e03829dcf16f7a66b832d3b84dbab400a1e9eb7f30ac000200df15cf6a2932259767ad2647c67847c7cb7043d90c2ce0866a8ce14decdfb1fc8a92fa8989e5b468e2248b264a7ec3385e8df7038148286f8e6e4cfb1700000000000000000000000000ffff55d1f3014e1f95ab47efb395f60ed95d41b2794c8e9bf58b401ebce334f5ba89fd5df5d072fd4e398e69392f1fdf3451476043285ed82475b535cf5f9cfec26ffd4f53d8ae52f83f252b010100a3053e9e9b3c72b9d3cb7474883d33549418cce8fe140200df99e35429536ad99ab6724e7bbac9a12b83249982a1557eb3f7ba73ab1c4ce090f357fc1d73429a5f45aa226d29c782ea81ad6fd118ed634b0604560700000000000000000000000000ffff22b266904e1fa58642905d5a2d568c8ea25a0a1bcb01b87a1f71be80787784072d1c384679d33c6bd1d851f0eb4ffe04a393c6da82acf5067e909f83ca46f32049d9adc1369bc35e12830100000100dfc98b1ef83c86d8fb556476721104fbc2dbc68ad4f5238c93462dcddfbbb5929cd63f26854111de1c8298f8e0e429c38249ee431b1ffc19354eb40b0000000000000000000000000000ffff6a0c494a4e1f07cff9e4c50da82722bf41fa5da01ca4bdb238d8d53fef085a56c34a432f6994c79e5bf754898499ec4dbe91eec0d00abb9bd2501e88903aa50a1eb8f36a5eb1fd0a29e200006d0300010813b0d6afdd170258ef7b3b91b7563cb99223e80b430d66e5a4d8640500000032ffffffffffff0332ffffffffffff038da0269ad49ae3d6a8bdfdb69599752becb3c0252ef31297e0d074b2b271462ffd481ef3d685437c606d5a960219aa251d8b5afca96eef1b117e7b019f1c7b63ef1f13aa1d01a751634bcb61111b95a9899c44247be8024f85977202cd327e9e238f4dff09e24edb5eccfc990e0390469d16fbbc5a79c435ee7adc0a0c8ae3cc0dd5622a5737ab6799bcc1bb83a925f19e54af804cd464cf65a8cbeabff89c6a8f0222c9b79f92d33049fe2c1063ee90957d1387ab37665dd0f978cb4d6bafb7e18cdd6a920cc72ed922b0f02629dc250a4c157b7e39c750c1914272236230d50c1abdc66a0f1c3d6c2303bf3a456d6f6013edc06456e88bdec5b819eeaa73184fd2661c1877e8e14fe4d7716e88071e030001215d56426d0b48a016626a29ce26fdeff3c96f40fe4667fe6682e35d3500000032feffffffffff0332feffffffffff038e26acaf312859dd5fa452fbf5c072fc069c99a55364c2e989eee465c32bf1d783fa54819ab35fece98b47f951876ddd888b4f258718e259e3e487aaae6bbee620d8e808a2784cc581562ff7f244280bb6efea4449d1a33beb99cf2cd6623f2b88137e92be078adbbebb07c19c5e9e679ffd041c9e5dfa270d6d6420a0393340080548e676c93387290d9b2d476cafaeb705fecb709b3cd2a477849912ec7c858c1188a8ced5c7f1fdb5fdba2a50b673a2c16a6f82a13dd9a3d87cfab79b66faf8dc6a78cab99703ab92ee65dd531f374a3c66b3d7b73fd63d8bd45275730ebf112e50b6ef95b229907bc328d31cf6b7c1b7e7055135443d6830c2c734f9ee8b5412da6ad187498e62920dfd0556d8bc03000121e72893e30f1216cc97b4920e707e4905e9bfed1f84f94809c5b5393100000032ffffffffffff0332ffffffffffff03a1add29d97bd577b65421fe0ae710a20dc76bb600fc3ffe9076e9b0a9cb20e9ed7fd47239498cfa8f0094a2dd90f0ef5c9f04331ad837af2cdcae9c01a6d236928d543d640ac5cb2f85216e308dcbe42b28c2545606086fde3e88d3975d51ae3315e09f17e69e183895c29fc725e3cec309e3abdaebff0c4e42a736c2ad7858a0cb733c1fa9c3cea4a153ba22f4b29cf79f451f361718dd4bec244eff4764bfeaf975ea8d77ca8754f37d1eaf53bd642a0f4db7e08c19b3ce5a2bdc4d18b60c9b3f4da5b67aceb477088e817e79b36282d1d3dcba6837f73ecc6c72c76d85df90c597d662079b786a3a6304f0e44403f292187ab806c0f533b69dfe77e1b41f6ae2f01d9877b63ad8c11bdf6466bfd8c0300012cc16c2804e4db62fa75400be262db71ed3abd19bc737d9f2453a6d18e00000032ffffffffffff0332ffffffffffff039267fe34682325e02ac24c5a896c0d4e013e0712b812cc5f5aa6cd31cc39251db29a9142365d377afe2f37f24540be06a5cf6d2a78fd4ee35033ef97d9f6d556577f97e91167131330d6c640385e840eb02d192c6b919703073fbff366f0d4b17fd5e151ad2cb7ff636d21965532598dee1f16540cf86e5dfbce1f9fd52b4ec6110a969bbc6b630b639c1090e636d2a1f89faf9d60f5e3d6a20ad3172c7b3573f809f36db756681b601687c5d79956e5951e4ac2f56c22a44635581608fee55d68473ff0afd09992bcedc9587464a1f73a28744bdb132f49777a804f0addaa3d030e85410292e6841bf166abf469488509e33d1f9dbd4c17ec6f213d3700f4c9e490a5eb5ec30d2f87b2a9c16a8f58cd03000132aff469aa0f661590ffd786cb8ca4fc0284e575d5ebb112d391ccfe2b01000032ffffffffffff0332ffffffffffff03a75224f0527fb260b488a5a0493faf5ec4889c3c3b3c28247412eee148c970aabac688bd61aaa36c9da8543e3585943e2b965a53ae549f13ac7948e46ee00a29d77857093f59fc948e3cd63f20305033ab30370073706c635dbf40d72af4175dc32d6c6050f97021cbeec3e3e7af960e62bafbc8ef74dd53a91e6ec929432eed180d3836269e0a5926200a1d14eb9a87d3d848e7d05c2ed616b93147166d6d444ea79652c1abacbfbb69c0012a58e3e88db47c06c8190558f10946686cc2a205938e4bfc06940887572c627e089b082eb2a25a9f913518397724983dc69bbeaa036e746e26396ceebf03dcfcc7b018d4580e6a6242b7e971334f1278bfbaa8278b5bf50e77e626a833c0f51ad379c93f03000139397fee7189036f1493b60167be26d83e7d35ea30b2a9597bb1a64f0900000032ffffffffffff0332ffffffffffff03b47c80d44a33768f89138e5ebf990e41a9d7773550d898ef211a26c7cfd0ea5e5f9c4dd3b5056db7cd413414dbd35f4e5afea824ea2d347c1f70bb5359523a6a02fb47a931837c26321eab0c54c20a4bb4d05428f5a42e53c2f07e202e8f1eb8b7cf99034c74cdc596bd4afc1789384865e54d1b99d1800ac6b06a5d8e218e5d0a04d926fcc2a14ffa9eeaba76d10fa114d494ef05389152466b5c79c086cc14da3aed6da4ea4cf0b6fb471074c5cd5e96183ed21698552732fa606b620e76b58446ad647cce903295997bc53f3350d2616f6756feb039a64c3a399eb83a4c3803ad5059d0446ea9b02139050495350672d2ca1d7b61fbf853d8269aa6d7a6e04cf7e54237a7620edb41d7539e1020280300015299ae02324687b8a92df73f82f3f660756227172103ec98fb446f077e00000032ffffffffffff0332ffffffffffff03a78dcc72d52d24124f7c82cea8132d0b6760186b1d1fb032cc2a6390224ea5a1dce6156f1086a6b9f25bfdf5d10a386d710c8a42a2ac9e9adb0467729a7a28cb6fa8b186035c023971661a5ad3e9fccda765ffe0c4f95f43154f3daa649f3316db88a898a24e24125581854eff2ed2a34619e4e375e559e5ddd9eba6724e472714a2d6bb54eadd8dda4a960712a83d0fa0bef9b2ac0b23d145da16f7609d699bf7f26b34367db0c3a8c3ec26cd0f28b2924b2483ac4829793448d52b8f4fe34dc9330a74d9b93d421608c117cc2ce9c133a13e3a6c9c76cc1c843e3eab16d2b013f6c941f142500cac3c830ab9b36f8ef4d541e2e2980bf17a4ce0b47bccf8c22f7b0a52d0c983eb233c8484d313ed160300015ac508113c51d7fd75e527bcf4c0b44a20f41ab9df669af607e603f9a500000032ffffffffffff0332ffffffffffff038d3e948bffaadf77908e28d6d6670c1da00fc2b4aa00f1a6c484984d51edb2f4d0551d1c2a03dc8931fdf7ddb0022ce0d248f0d0347e9aac373441e927402334279fe7ae03ac162750ea5cc7f2f030a481eaeca728ee24098895463d67d9f1d7676515b3a377cab3aaee3b7057814945a34db7cc3c2b0b6570d2306b0ec863b30e4396acdbb3f646aedf668e89b0fd642a2436d854c7a1fac97939bdcd36529581afc2f21c781e6ef3232fa59ae769c1b4e896d28e7217381650ddf4d27bc8ad8d3172daf523cfef122d21c52d706436e5e14102070b34c7adf6747da8a3a72f04a06915c387718b6175ebed1df4493b2baaa566d4724ee0f46bd6e3e8890317e71c303670e1700e33d2cd5a193b1b500300015e0af7ced20920e334926285b580a67a8b8138c56a3b76bfbd4ec84bee00000032ffffffffffff0332ffffffffffff03a45f8365ac32b793d51de542c0d81993ae78f02aee5e50b1914013269ca86558ad7569c347919520b9bc507561aecd7bf08fee68a777d4e143071a2d809d0552a90a33716cefbda4e965123cf3b14e08821e4b0e3a5050ff6923a446498db65d4506aed2898d8a744add10268f9620f0420468da3ac0d01bc699f51229c5d64b06cc0f0971506eacbf4c4889fe8f18a4b117bd96de3eb1cda5fc87c8de2ea45ebb34d01be2a86a470591a0c5e67aaf80a58c2b41a5f5d7f9c827d06f23a4584278e9e943cdee5aa7216a7ea5b4a93ea3305e01b9e080c0f187dd465bb9167a81021b7b6cd921b1b23047d8cb615bbf9dad16f7e23f7abd401c1b98f89c2925a7200f868bdbad53d3bd8600a8e89e982c0300017ff5b4165d7555ad6ccc0ce666026f21ccce5cc6d9f9038a910a78023a00000032ffffffffffff0332ffffffffffff0380c30cb613198ef63e130b10899e9f4d4091c50807ed2e8776c8efb3d955faca59016cf53cfe0c1351aa025d24e558965bd69190e14fc72ccf5e0fe989229b00ffae5e0e7bea12a42e1a04f948e5954d8659d13c05b8afa4084f15470ca0c82413d884a6ffc1b745cc99eb45f6a560c90d52ac4fa95335f39b2eafb360093cb11922864c3fd3c6898f0c71ef0d042f1b87a8dadabdc28593c704dd8d09e008016ef24cd175f682f26f85f90e5466bf9889ee8982aa43735a7007d318e3a75147f8ed908bf62fc346e9940b1e7d62f970e3e2a9f67c1d9a00d976ed44d19e4829126095b9f78dda15d99334bd3ed73cc102e697c6ae996b85c4fd89188a5789e756036ea758484a66db18ed4df97520b303000180d118b1d85d79d68c7920943aa776aa4a857e331d1720d3d77875cd2900000032ffffffffffff0332ffffffffffff039434bf6e190d94f17e3aebbefc82ad2251062ba995637131142c88b1f8b86bc05af92e279c5324bac91cb1dc76bd6b5298c008db18193df99346b6fc77a636ea6293ae0e55714739c175658760e49bd5b20fc363fc2b2f8018fc9f2e04fda2ce7c4761801f6ec35f9b700aa15973af679ed37b59c88c4f65455e38e02350e30d0177ba17776240af759668798e4f3e2528b6edfa35a04d0d7476877a894f10030c5fa24b954f9b50596074a8d0c2318ba0c8d3a434ca3308d5686b17dfb64539b2f7dea56c4101dfa8881a679fa9990bb792486e745e2f3e1756b50ccd1891c80d2f7d7f0d852ca6be1596e2fcd3ba6b97d1efdd1d2ac561ed05f839f9406e816ad388a09f4579d3944ad415647d394e0300018bcc929986c96aa428ac63ce4bd25520244172325ef4e28759aa95351700000032ffffffffffff0332ffffffffffff0391d53a49af51a781573bfa563817cebff72ca951c4c8d8955d5a72fd80b2dfd8f5d98828104da4446adf2f84ab7aa414f6083f6e0cd37cb1f957538cbb5087dd80032dd69002f40e8740bbf7081f4a99a90051e8fb327247feeb4120c9edeefd8cdd33b206072ca577f1d80d17fc1dfe9f0f07583c604766de84d4477c5cac4a14e0e0d49c30d110c7127af43a6f0ec6ad57eea803c5817a99de595444d11237ecaf2fea6bca9925c2bb922c243bea8183080d49465a181fc48c357a78ebfd9c487e8c8f08e7aa67714ea2884854b4f3596e0af66568bdf6bad358ee02b89f3a0caa4c2e9089661d2c2fde0bc4fb3463cee9aa8c704fe4c1e03517b159966fb3efcc48cdb5f4d75202fee4e2bba0f16c0300019c6ca671212d00f182ee9e294d196cb0e82c165bdbe6cd3ccc29aa796100000032ffffffffffff0332ffffffffffff039406d429b6c8884703d6a51f5f665e655c8e6b9c2bd6dd339987e2c2de2c8a2189f783f131a25b6b8d89982950c6e96575e6aac9c52e9fb82fa2b9a2d26b39b7ca890a775312d05cb9127350cebb3b8caa1311bdf058a3bb3da61c0f8799c250ac23b56e6574f803b9e4d704c99d1bee0669ff12ff311ec2c059901aba7389a30ca7831ab4dc273ca2bb86d203f1c88020cbd0df357b54c3905fb95092e21082defb2f04e81477f2debacbff5501691ba124a040bc67ce6874660dd3b64c520b79966f44f07c4fbf3643d657d939e4ee1ac3c833f5b7b5744a855ca749112d0a04227f09ae80769c830f8cc8198bc343dfde53107f0864a91c651b88b61737fff68dc7a774e906646587e0af2b524e9e030001a6162152b073de1d717d209a771a22712a64f3f043aaf3a344f47950b200000032ffffffffffff0332ffffffffffff0383e52d485a8897b577aa4c68cf3eb45845411f2994071cfd967f82a282e52ca0cb9bec324d46e2c7877a2a5c61f14cbff0581a4b4fb369597668d2a9e8ded079f1ca4b9a8c2e4b3b3d7e7652865952f4b98df1734a10d387d744aa50658df034efe0b513e29efd79ae61e1c9a0c7909f6e607f243f562420e6d36ec4500fb83f07d72b464b6109707a471627c9f518f21b82a030992e9d1826a98d616982d349f3bcc4ef2d933879d6e093ea3fd9ab1eb248a66e22668314e0c3c987cc336f67b7d79ef6b2e903dae1eb91552e5b700b4b60235fdefbeaa3d84ba053bfc3f5d10487cd2ee685af37334fdacef1667027a9bf71cfdff3fe068cdd21a4fd31c9dd5e5dd6d77acf2e499a42bb871d2afa5d030001a7240ddbb3b7d50ba386ca5e65e4a3250cd4147dfa43b2c77244e10d9700000032ffffffffffff0332ffffffffffff03ad6142fd9fd8866af0539198ee1a8554493b8769eaefc79b32b746b17ad4144c24b265cd0d9632e452372fa963326f2c97506a19e670e25e76bbd8dab982d5682a23630dfe3aa464c7e1629225d44ee3a131b65cb091f632840211c05b77651b39b45e4d710bf03bd71f408bc42040723ed55181adb5d9bc899051d00cb5c4bc116c9165924bb6e93a56e2eebabc48db770bcd2f3beab165e04c8c97eee66972aca243142c88512eb2a2029361f9c83fa1d0d4b9e2916ab464b9d652ceb0bf8aa45d51e6208b936bf005935c3cec1a1c93a91ccd1ac39493b66212c743869a980a8b4746640f0bf2279c5589fffe822addc2cd7917856e712898c2bf52bd6c35a900ccd70225537abd8fb79d8133a75e030001b1bbec44fcb93a84e4950fbee8d03fcd672174846bd21b1302d65a302300000032ffffffffffff0332ffffffffffff0390e55206090ff5e9cd54d837cc70b16a21858c31accfc85058b0ffb2f6845ad5b3392abf74aba5090c20279db712323f427055de2bf6ce0d63351be4fde9a8b11d5f6bd64d20d4af9c16338f0ddb5865899a0baabd115a335b471e1a399415a2152c6d293dd463c61585c7cbb4c1dae73dfe41de995f703cc5091274ba57a82f163056375762cca08337141db912fd6cf06217d2d0f9925d64f41af788224e2ae352b13f52b38dab7c7b022af9b3d622a586efc2538afaae3c4b681e8802c54406aa659f601ea25a9c7fc0786024eb1f3e50c830bda38a8f9162e119e157645003f65ef55104fc3317c5972627b51ee396364b2a58e06066f2210a8fecd0bb98812c8cb8e60330b12fc9c0147c38c96f030001b34fa2b77c3fc270abcf3bedd235995315c0fae0ef521c9aea9b21ac5900000032ffffffffffff0332ffffffffffff03915f35cf43277e1ff2a8e2ab974e0770af9d1f9fbcceb4371d9713046d3e8d79a2859e40df28cff5009c9341fd19f02f23820a3eaa6b425725c31dd800f9f9db2f774b1e9618860fbde6deab915e8b8b93cf1899aeae9e7c8b4c858ee76f16911334538476b8662dbf8c789ecfb3391e431e531aa0efc4c42e56e0e21c573d6f147ffddbd0441c6b6a265f9939dacb0b6cd5bb68c0a7f6eb1232bc250af810cfb3797f020dd81940b8e92f1bc395ff1b85b2bdff0db143cbc061e1503062f6b1821ea8e9e8872a142ddd710c00cb17af3d6e24ce5e723c5608011b861c0baf9703fce232bf2f4dc6e6ca2aeff83ba6c42237b5b9d4409624e030f9e0333bbf2c1270f0c7147746aa8c5fc4de44e8a789030001dc04d3a653a6b56f66b52aa3541a718b386d0fd43ada47962bbd010b0a01000032ffffffffffff0332ffffffffffff0382fab4032e006f2a5b8fe7aed675ac96a436cd05de3bd27a614fe66fe2438a1022dffd0f5fb28083c091fd6c4aaddca499191a1f67251afd73e3b5b1beafd8229f3691dd0c43fa972de4b56aa1672118aee8f79b08ee1a592b96fbfb6309f478b161c8b53241ce270244dcf16031eb3c42ef553bb4656e160317744d9115303d035e1df52d5597ae40e91537d1e9d07b3dcad591312812055f61b537b328f18053a6327337a913561a22b3e68c7b2f13888cfaeac604516cbb7ece79d113cde1ee46bf71dde2d428e53a21a3ad86b68e93829a79baab8adb4cd2e12e0c79fc1d0bc72cca4be253b811aba4d31aab1c2a2bfe5be9bfb8536bafa0926afd4c5527cf9a4b3683e511ac86f5d5f794ae972c030001dec9605a6e6043753620d476d1919bf586042ae0706f2d902999346a3201000032ffffffffffff0332ffffffffffff03abd7dfe7236612dedb0537ea1a2beb9e71dc4594b549e4c04c01edf3bf693575700d1731873af19006ef3d41ab5b6f05b038d462d429cd02152a3210c60d7ecb235c3934a572e798f955cad862dfb525805dc9a1eb35870bc26b494b0438d7dfd928d7db5bb7f6e7f77fa36a127a510a35a10a73271b9c332f12ab9de950d6580b5d06a8237a3960c4a8219b2c7330f5404ed5664bbc3852a6788d9553685dd312442ad58ec836db6d325e8189e1cc40885d35b84c9fb8d7802cc933219d38c90e86571c659ab847265a250a776c4a93ba1e21b97ac89af80e6b84c63f434164152330671f7751a7e3c4fd6ef8989d0716b30efc94fe56473b7fc0ad8b581a6379cabf3cb4bfb90299b4e7fe86dbf3c1030001df63c7ba777c253c097853ad177f36930345d366a6d745848da4b1b98b00000032ffffffffffff0332ffffffffffff03871fc9ff67e273b8617f4fad594baf4d53645c6f7e52e25b4d5cd025881b192f6b08fa612abaed34b2b413d7ec80f9f47968011b206edea592a9e2d5889867b4f794f4ff08192cee07aa9a78f66fe055b5fa2c62dfd08d48601ce1afaabc179c86f6db1a1792b8a1ff624e04d734239873676e80a76b6caf3f2c075bf08403a80376701149e245437b88cb614ba01ec863d36e8187e777e3d1ddc1045caffbfcbe33ed6e663cc790d5eee5afa0258ca68f89036c0f88cdfa3da8a9cef0d6620bb77c9dd0a4c731973a934b830aaf94bd02a725802c903deaeb069db639795a2601b86151671c603efdbd264d5f9df8162cdc91b2e73fb0346f180c644656cbf61d0f80de3cc7587344c49eaa213f8932030001dfdd407ea3ffae260328deb31a84c8395089c5f3078c7545c0175f224c00000032ffffffffffff0332ffffffffffff03b3cdd3252a98916d7860e182bdadb0d0840fafb9121cf03e95436a4dcb89bda3b1e6d17140244e2e7010d702bca25a5bb33f6ab8d1676150fbec803c041023d5b0e44c3d509e1e1093c629d8f05a78508cda1017f8740381512cd384c0a2c2cdc5dcd22df73a648d52268b0cfa364a271403df938e649e02a1da6a42ccbcef5a0f49efee0fde3e2413600847c807ad0e0445b96cc3255476d15b1af3ee4e97b73b0d215c74f938438152b58ec60d7b2d88fb8b492789177b456cd9088a76cf3f178c2f2500906b40b4f88e5b4fb74dd924bf60bed24af720c396d517fa8322dd035c996f034256aaa1ee3a535dc8494573953b66c54162f45bcfd628b1fb8795e7033544092a0501a438954a59a76ca5030001f1c29d792e05e96a6928cc58d6ca2b1e6525eb361e8984a6877c0968b500000032ffffffffffff0332ffffffffffff038aaa2ecb876d0a19d8ae8a1fda114b0e7a2e40e95d31876faa76599338475f9b4493830c670ac8f0b66f091fcbbb8de806ffe26f2109db9058dd7ab3b94c42d612f1910b32798e99018c7629697248fe9201c13a0dc802191ba8eeddc40cc28b55b16f254d96b4a63d131f4ea4c0e574d2877abf74c0ed68cfc2145f134feab600efda7b7a5c60f3f95837cb944dd3e5416e749f279e1c9790d4ef20153f418a8acfeb038ace0a7896a2367798381d118e1396349b5c395ef8f7f0177fa9bcea7c9e82652cd15930447d0cea672b41c32896f0aed082b0f7e4139c582e3cbc8e06bbbd31af185481bf89e45d1d5ae1c268012029b9da1323dd83851b5822c68b8b11b507d03267f6004edc098cfab969030001fa32ae75020e4ff7dd2626630c49ce57cb6aa4bb53345aa4ff7325685900000032ffffffffffff0332ffffffffffff03a3974823e7ed1916fff308ad165a9285974c7561b3a0b5234a43ac0007378207094d3ee7473a3dbd03d881ed90d4547be1577ecf123746ccfb3b4183c697a448454330f44d553756e778ea1ce019000182418c7d4b549ed241d641614398ae7c9e403a8701aa2a72542f833528e375b19a4786cb9cf41da61a4e129350a6ecbb1804e5826e6ba5a2ab123910252a63f64d48b5bd9b9677d62a6531c677e8171af8406daf830a7b8c8e4044e25376329b98e62747560e8e3a004509f7c436bab682bee3731fc94e347b088d1b0ae7c1fd9117e5de601d015a8c68c63ae69016c40d211e7e802594c57421428a0dd3aba7942dc1798759f46009e99da8c382205692f118a94437f2a2b3f4f60572c650d2030001fb626dfbe19d25e37267c321776a482ded3e172216268ebb87183fb9e200000032ffffffffffff0332ffffffffffff03afc15436226a7478caa97a3ebf355770532b3cc74208b72ab6b39a9c3bd706cff40cd30a8093a36d3c40ab4bf186bf8207b8e29e18de34922aacee8307689335aff4a877bf0dd7b2abdc9a53101dcae6b8512a52c4b7d557ec01a3bed007e596b747c27bfb4891776b7b47660f4dd42623d2b28668397f604934987dbddf249e05adc30e05b42539abdb3bd2e03ddd28211071540c8c4e7768378839a9d3f55a46826994ce5b6e3ccb8aa7c8186f2c4d8df00741a28ae4a4f23a0819d8773bf75f26548ec548dd10aa17228a11a67eafd2f4375b97cb2c602df3daa04a2d3ebc00ff51bb6f6e8e6609e7064e02d39c23399c9177dd0f9a59a35d9de4a2dcdcd9d7410af43f9b08127717fcce13bc0bcc0100021bcfd99fb740059f272122577f51db10ae460a6a45d5a6193900c9aa6f000000fd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f000000000000000000000000fd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f000000000000000000000000134a941967b2acfd46578804c3ef141c6bebad19a162741d4cc20502bb565f11686d6d1cea4cd6a3cf6f4962b16cab5b8a719639a2fcd6be9d5a694ab1715301449228df0aec5a18ee450d1060d726f39892042a4920141771a2f69b6b8de175f837b3832dc572a88ce962d0d5ed93b1ca20fcc7bbca942b2623285a13c09f0e047401f4b3163748e9748ad19a193ffa3fb7de04e0a4fb78b7e91cfe70e0bae8143414ca0de24ec7458c39d59a1ff8fd09a4d2a4c51fa1a8422f487dcc1a7d6d2ca3459f1e54a12afc515f3946e2656c253a98e62a0a08d84f36e0ed3d3bc9dc14251789c028f139d197ed977172b8758d09d42050a9977e7802ecce8e4cd76aa2f6d761d4b169cc747cc97a4c60aad90100022362a6dd5f1de53b7cb3f54f86d91142335c39c31641998a2322d707dc000000fd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f000000000000000000000000fd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f00000000000000000000000002060e7975897ce70a8921b3dbe99891e0b24835a20bdc1adfef48048ee3598901c403fffe3c9190a009726a73050f993a7b0f34b9d0e05534a58a6fbbd87e9fcb57d9274f368492f3e2363177e6a73d05fe1f69d80e421d0b737dba4dd90e13cb048b3b054c7915d3863e2b0ca13a247a4c3b835d721d6fea1315ab9548cc1f00a1dc31a92becd63894adc234d412c01b1acbbc4fa1ab79ec52f10541cdcaa1bff248b2ad55c0cc8c0c36e0bba0585007abc913dd29b9edad6e453389cc93a0d654005df0126b6a23482ee990bcd2a2c43c2187aff6b93bec543d8e2b8eb18a19f883a3db2f14f46820a91bfc7365806cc33333aeaf0d55144a3de6e4ef8107535bfde0ff77c13f24d1635607a7943801000271d03f52a14e2b7ef5b8b562977ad84303fec35302f6abd81dc23956bd000000fd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f000000000000000000000000fd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f000000000000000000000000180a133b2c886a53d1d7ec81ea07d6ec20c462dff173bb1a65be0d79a0ef99e4318d7c3e1151d85c414a5c3f3d66908f38ec80832a43aec509f7a782e2beb9059c014611f27a3794b1e488c75855ab618288c3a346b50cdcc4a05d47b890e975176ba1ea8d0195f8a5a21a61d3a966c61a22b98294e0049228722ed0538f4fa015eee01599598541d5cf17c1b9eae18e2401fd6080e675a2e6d4465e9cd5e46135c5b2fb70a4633355dcc299cd49192a89d84f81db0fb04bc959357aa7f97907c02d48f39633efa433bad88bef1849f537ad420cd9a1d7db8817ca714e875380167c03672513db51da413c250ad4971f431f06de7adcad49a8b85d592bf6eff1027ac0eb134d50e293224a11e9fa39fe0100028e14a08cb480e0c248784b83b0b2eb445c994a3c941c212518a1156d0e000000fd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f000000000000000000000000fd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f00000000000000000000000088b7c1ea6ba3f2d9b3028960122b3bb985a6be48b088acfe8e4fdb849024df5da7c0d1b331482f46891ac578c92a9e6dfe1ea48e651973343bbd9799bc22ca77b213529fc5364e996227ab0792d18c5f04d0065fa771279b4da6d1b07d24c20b0e51470ca92dc438c888c832cfca906e324a4993a7d707f4a5ccbdc58ad30463168498bddb3726f2299af07cbd35eeb57249d591e10c1eebbe283cbb454826f184fce72501cd8439557da8a0ff0cdfd7839497aa575cb104193633ea61a8c3c89b3344dff09157dd5a779caa9aea11df5693e65fdd58955ab8a6f68a7322120704d1da5557a583e4dc278bcd847d9894cd6bfce937b511ee0513473d9b2e69a13155c11473ea0aa1ffa5d7984239b646010003df4ff96a37b7e58508c3d327e1649cfb90d50fba0ccd18ee457dd99c33000000fd9001fffffffdfffff7fffffffffffdffdfbffffffffff7fff7fffffffffffffffffffffffffffffffffffffffffffffff7ffff1ffd9001fffffffdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1f002a5dfd00f393a181688b0b81cb6dee658939fee0d1163f34fbe50bf3d9b7da721e721dd2c505490730462ec0b7bb05b0536375a7a46573e8b41900d811b2aaefccd10b7a7b8f91807730fa47ce24690a76386291014fe1591564a9dfad552562d68e2291876b4e9032d3a512b98004fbcaa4ebd541a6b9ca0425feb4fc29d90b7fa748d08ab5c2c57596d9a69d61b1015082e8eb6be7e713bd421c96d43e5c0ff570463a3cc2c4b62be7774fddfdac975134cdfffcfcdd4d349b4b5f70bfc06f04de05f70b5bd9c7447342a2ea048bf160b1cc7a54aa12e8a100516ded0c7e06b04211fb1433c153d27d6755974ebdad61329e9345e8530f51a0c80f4246ef35951a05331b10a4204b1157eb8174310300040813b0d6afdd170258ef7b3b91b7563cb99223e80b430d66e5a4d8640500000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fa497ae9f03c5faf99e3836d5039652deab840b18085755526cc8ca1fd81116faabf906c3c35311a21b4219e1f1dd8f491d6bd2e188da85d995b9ead2cf68c05953d368b1033232cab0fc4fba24b779fab2f968063da9541b8fd1ec5081d5f9f1f5303fcfdb5c3373172d00f272f21b0c562330d837932e0758a8ab4f538fa65a0bb15983c2577220d89cee611e27f66cecc4f3a2d89d26dce8ec7972e2e22afb9866e5a961bad7beb6d4a5c72955b314b7fe0858095af5b6a05edae6258fc13669dca94f11ec03165b9f70fe09e2ff26a7899670fc55d6ef36e86f8947013d5316df8ffaa3d85a83391f1008f90ba2617e2d11b701be4c2ba20df3c4e4c688f77d686631a80f41d02177335dbbc88ea8030004215d56426d0b48a016626a29ce26fdeff3c96f40fe4667fe6682e35d3500000064ffffffffffffdfffffffffff0f64ffffffffffffdfffffffffff0fabee0c1f86be4fea395f70126789744e3b2b89cc13b9b51182c0e2eaa4d06071f48e472a213c16f58aa013d19e89fd6de0735c77133d3d07526a621dae095a71bfa3c7146fc937958b6add7a3b2815238abcd614c91b82328ad1db1b956f697fadf048030d0c53acb2801ee575d79efbc4cf67624a5498b776d8f9b5a7ae820515a497aa57f5bb01d2a7a2a8cc84a38345cb8c711373f00ca236b15fd00e61c7b4d9fb07d5a4b9aa2c7d976edc643c1fabc4bdb49fd74f2a3b99dc8be0363141ccbf463cfe5e08c3019ce38e03b64cfd9f804ef6495a1a37c15f4c6d05af78730d05db6a61344038fabc5f2b54801cf1c41f9181176f3141dec97edb8161afc6e01aa3e6b5942f362d4c16ccadec250a03000421e72893e30f1216cc97b4920e707e4905e9bfed1f84f94809c5b5393100000064def8ffffbbffffff5fb6ffff0f64dfffffffffffffffffbfffff0f9843e8d966f038ad379bba8600de762f13f77ae3749493f1af9c1c1f108a5f36001ea820dabb1eeb5774a25b1d6870794a5145fe64cd75720977bfa19c09fa445e5599050198531d4bc42bd26eff6f4e8ea0b0795189e9aa4f7eb0733e4b7e0c6c9e51c55629dc66858de008e73a91ae03d75755ac6a52123250978a24f43a3c1276d9e5d63fd581b18ba08bf72c42279dc6e58c9ac6e3136208d836cd48680b6ba6a2f5ae5b0fb3fb81784ff7fa2ab4a4e90a2bcb801928dd06d4745cebff2aff61bfc35f17f42608f09ef084ad064a080d9dfc245deca19ad2e262f5676f921593dc1750a65d3a44ee8b907121a2e9a706008871779a5933ca443c91a9473e3bdd00a3522f50d335a5dcf2771558c00300042cc16c2804e4db62fa75400be262db71ed3abd19bc737d9f2453a6d18e00000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fb469eb4fd0f9ed1ad1e2b322986a3dd1de51ad5a73690aed86490928110835795ee3d1e3b1624e8972e8dfa5fba6f1666cdec870904e963421ffe531026362a0492fd3d7a1110addabc9ad4fc63cf451b5fe0a04ace8e94b561c1d935977f2b4b657c5b83fee8348d47cee8a03baad0a7ca129848ff8eca49bb5fa093822d0481123d0663a8f41d6e0053ae980ff5af145119afda2a29fa2378fa24ff6721dfdca8ae94e19db01f3c3cc60d7e88e33c1999995aab38edbc216e388f8749d93b600f473d8dd776221446af1a0516a17736881a72cd355072cb7dda18e33949d530e0129416542e5f1c8e89e043c2d7e1a44705615ff3d59ac5f28d6e6d8d530a270e254ef1f8041fd0fccd56aa16fae1103000432aff469aa0f661590ffd786cb8ca4fc0284e575d5ebb112d391ccfe2b01000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0faf3ec93104f5a003433366f2839a77b169aadf341c5a4b0d27a7b18ce0e57a14e98e177437ff68a26617d2cde6c6167a1b2b09c16f85c1b018d9eaf61c666cdfb9928c3b9076b88634611723c5efcbf9ac7d1c1ee5f1d1b8fa31e104dcf9c9e826930559b22922253f972c7a76f16d5483191b31047d89f12c12964f0492b7aa02af0a46fac16f3585b2f1838a08fab77c4e4b4b92379db306695359dd061e2701fa78a6cbba660eedea66745d0af134a93ffe9de779b0ed07e322f4cc570546320be53494c1685936c139dccb2940f24873060d66be1e1cf8958a23f52a4f710fc0cba8c06479fff91c218e5eb9ac1b662fb5543098fdcaa1f690c0bdac2136fcd1b2166490e20a31f1d528d3006d0303000439397fee7189036f1493b60167be26d83e7d35ea30b2a9597bb1a64f0900000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f90ad43503cae47b57ba7fdac1287f41ed8cd3a28b24633026a719d0d857dd53099996ffd02d7b3dd17d71575da711b7a87071a83f6d1b84200636a759c3e8261684a1d7cd60b9d2b79c4a97afb056c4493a44d1f2b261beb2020dc78685da60ea95202896c98a46e20b922b9edb0b66a95dcb1c108cbc9527a59b45e838fe46e1370af7e90e534122efc9f8ed273d0e193c67b167cd290140b943255d794e9a2c9d6bd07785631485f3100fff3bac2d9a3f0c653c89704a3679aa96163c9da7830dd2f53e1cd94a98c551a8c632a7884c417a54eccb47fa465c68e753b54dbee14393f3b4fd8d81b46f0f386cf5be7cfac666861708ff46ad9ed4fb25f80b77581b8ea3905a42355b75782b16b330db30300045299ae02324687b8a92df73f82f3f660756227172103ec98fb446f077e00000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f80ddb726efc12c8440ed465f13a180174b736ba99a9f05ff4beb28e9772f865bd2fb247113f54c7ff9ff59db3d07de48231e3b0ddc62d8087b6175dd7873829884927f48654275241fad17a7c23642ec9730bd2d6950cb07bcd7aa014c4a07c627e71bedbd507ba9c7a3386d8dcc3961dac0630300221203ffd9832d2088054c163bcafb678ea887dc1787cffa3208cb94eacd106bce106ec9ff6a4022eb49c9384581d6bef661f0fe8272a53063a0e996ded6fbaaddc3d227e56567de4e7d20bf29df6a691dd049186221e1cf3b3a846b06b8cb151172be35aee6fd61a4fdeb067e9359c2f9c41bf1b46f2aa6a376cd370477919d89447a0b3d1331c297f4cb4c20fa8940f0837df6ed470141efbb220300045ac508113c51d7fd75e527bcf4c0b44a20f41ab9df669af607e603f9a500000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f88606d95fd847ba14dd44c0a2889b237b0167dc164184d0404f34cb5af33d1d1e4033bcf5635e2895185551bd30653874548392a7ab23a5ccc5fa83483574ceb4e7055d692ff636e4f7fe4e3d3042a61800bc6285e57d6b642aad2ca6819a5e042609b0d235a39c8249c1fb57f3e643ac78a9157661dcef49bdb134ac8c856c3160566f3ac9aa6a545bc025b9d0473f8d6d14e6ff3704a2b1e80e0f74d27d61e36d34b5e340ac07eedcc869138d337a083264a50ef553873a835a5fb7afa201a3b1a0220db242f6439168f2612fa29d8fcdd8ce81660c82090c709f1198e76450c43638e1820ed1d7e81fcb9e4fd6db486860d52d699ea52272155b58fed3c971c3e58b2cd33a24ad4ba5833d18a5f6d0300045e0af7ced20920e334926285b580a67a8b8138c56a3b76bfbd4ec84bee00000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f82155583056b20f582fa4687cde1aec9c6a692fed2f12f4cc6c7d89bb17a788d51a96c660a8973320f151135fc0bd3443adf3771b9ca9a409ac04271ebc2dc1f570b3a62cc7e74d02403c232443ad976b5cef3e78e456ca1d053f9fe9b0576a2b8f437f2f7cc4d7aaef267f88a3732e869cf181f6e68b93e95d9614b0fdf39d20a34c4d28eb1c81fbcc11e706800fc29b458626b90884b018de397539927640dbff3bb9ea35551d003b24301032549c9a9b9ab3d8140261ce3d7b20757cbeba5f8cb37d8554df2958ddafc70f14ab2a3d927433219f88dc45bca855be136644e0d5397ae53f2e642339c76a2580846924ceab54ed83a2f43e7438084ad493f49350f9ae173c1f1df598c72d4cbc32d6d0300047ff5b4165d7555ad6ccc0ce666026f21ccce5cc6d9f9038a910a78023a00000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f99cf7ad02a7d814623c76be3b2313913135e814825c5653a6b91791e4a20683031a872e23f534fe80c6bb8f35dbbd139ac68d3445af815adde76528bd04a8573749f8f813335e91199692a20447bbfb491275d16288ba802a199ee7fef681440e9ae6b6b35849319bf9bcb83e13e16c9d71e0df89dc8a365905db5e120adddd8120227ad9dcf4ca2aa4f9bfad40dd7a6f0615ab2f12c9b6e83e70f3014b7b8ec763f21c997bef2a2831f16070b5917a382826d7d318a7d8c3a000aa817617282cbd7ebc157691be5f0fbcf149a5c7deedbc9b8b113a7680eb250d521cfd2626e05a3949dae548c5579c41966cd87f851eec2a3c3578e461b9789c915875963cf3a7b6c130fdfc65acb4bb42d902441ee03000480d118b1d85d79d68c7920943aa776aa4a857e331d1720d3d77875cd2900000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f8e2ca8434691560e39d318cb7b31adaf098dc8c0f9051cb89e8128e095b7785613de58f1e2e51471e4fd6f8284dcb9dac00cbb03b76dcbbb6332e16b1ca8983151adde3e77fca6cea85716122fd093d19039fe662ef107f2772c8ee335fbaa4d7252b0ead5e5c5bd8043715128eb4a6be8aef3dc77537fb84b1909bdb6f57ce5190fd076cb0efa7dd6cfe30a467c896e39a0bc141054fb2dd5069b62875136da0af9dc658a2201403e211c08eb699641aeecaf05febcd1b3259335bff4846a6a5b644173cc3de2b0cfc1b06b1e4fadfde4f8961c4f440ace493dd7c72e155d9201ea2c10513a77b6dcbf871f6815e2ed5bb285c16d339b6b4b7bf31d0f826369eb259fcb77b5ea015466192a53f825c50300048bcc929986c96aa428ac63ce4bd25520244172325ef4e28759aa95351700000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fb259b22bd1bb62e7025c001cb7d9df81c22f9f7e96378ea9a7c374a90c836923ee31ce3ef5ce05a9232b60ce769d6a74bbd8926b03a6b85eb78219d80f9870f85a42e4402894e173a7caba724eb40631b4889fa23e4260f589a82ec669b1a00e65b350ced8d35b98feac46ddc03d1f5becdd2c4cd3f0424e77284d9badf3cf2d030efe5cedb3f4a951ba81519cdcc1a8cdfaa293ccb6617deeb463dcc829f59807beb9efd97c9b2db54cbf017c62fe50a4abeae9ef59e934665fb349c8baeec44e0d60c8a9e0d9a969b3c2c71183316c28babcf1f901940350ca95ca4e817936038fedf19634717f30d85af45174be698323103c8a38a73b17b660f628994eb1c6020ab8651efddccbf00f22f6fb6ef00300049c6ca671212d00f182ee9e294d196cb0e82c165bdbe6cd3ccc29aa796100000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fb7f2cfaa9ab71095293deccf4b794ed5c1cd868ba5c8595687c3f593ea1f4ba8cf9f66ad799fb399dc8eb0adeedae10dfb1e9fbff1757ffdfa26d1217491b3f2ad50c4b1478baafb6f1fd2cf932b1db0b5f641c65d7ee3fc18e7b639c0f6caf6ab22268d56b7c068db882fb7f479bad4c936eb01c7df7c43049d62f3614e08c009fb9b530bccae838f9cc832a3a20847b065b604cd7a5e074817a56cc1d23a3e608d5901da35bc66fce9f00766d1442d97c5de26430c3e75f9988e684ae5734640445d1cca1a004319c9ff15406ae6a05d4b8ec7d8c7830b841a1c3aab3b909c0db589111573461dc736b3dd558aad5296b3d063159c1654df62bd55cd90bcd296dab5cc4b3a8153c3120b53b455e32d030004a6162152b073de1d717d209a771a22712a64f3f043aaf3a344f47950b200000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f88881a0abb2e4701c4d8f01a6ef512945ea671dee28d4079d44c8f23974f806b60111a6439d3f43821e6498835a8579c830e84f9829affe735da40d9c0d647741b5ffb2cd8eaae3138bac40163c3d2afb5f81614b6f90969599a0db1e9ae6e3016ffa076e41e468f221fd6cc17a526c6b703fdc611f118b4403fc70519ff3d7305b5a8b7bfc24d8bf8713a27e7c68b79511f060d82418297504f2c31ecd6fc7f5ed0f5fc2a4128d0367027eb505a2294a340e7da4e10beef70497dc097716e8873cd177d43cea2a25e841603a45bc41556e305a1e507d23a8cd4dcee791d1a770f671616405dda0046cdfff80c822cafa78ef83d25cb39252af4716a2986af55bccd23edaeb2e4fb205c591254552680030004a7240ddbb3b7d50ba386ca5e65e4a3250cd4147dfa43b2c77244e10d9700000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f8bd313d50dbae9f3783277536ada343edf61ea7e07a217de2013d42668b4431942bd1973f1d6272f5bb517d161224e5c61510d14f0bd2cb66ce8820a4cfc06b04ec2ef800b95b08426c44929ef0944b781a35f517b9e5e9b9f57f558bd94fad333cc818cb308e075c39ea6d8043a4e8490365b9e4677f532cc019a83dcc14a1f15cc069b0534be96e20adc84eacd471930f5d458ec1725d56caf7e0bbdceeeca403f7a1e181b196757b93c1010c66b7589454872cb496a9eddd0a6886f93ae8191c9b5e1cae37094fe3f7890a2d51bb47c3703e5aa31a6bc25052c21939b0a09017fb51f994331678dc84be477b9c46344e06a6bede44e3b4e8a77852c0886144c7c532ede096da18ad2f8bed0162743030004b1bbec44fcb93a84e4950fbee8d03fcd672174846bd21b1302d65a302300000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f896ded748a9744bd2749ce289bba47d1e5eb80d7230ec93284a70f9bb9f9ff3154f1c3f7b9b76b06901bfda5a3ad1bd50f02ecf09cefc0bece37d531b7bc7d6c2605de31e313f20cd91b44ad28417b2daef68c3f3506a0bac76f2ff2534649297c36a2315b2f6f579412074038ed9bb3083e24377254aba37a175276fef624c60d22ae38c6c74960d951e774b4b28ee96f2fd547bf6695bb141398790c293e5a1d6cfdafda1f51188318dc502e7f8ee1ac3e350977a61f315ac7c37810a1e8ce18b0bc7b6a4ba9e4a2dc97e2ef8280884eaac799a5e692677f700ea28f35cc9c14395240a993beddcc43f1d74ad3d7bdba5d44e5465fa987b498acaad0207be20134f306e00a75eca52c53b6a0c6f01f030004b34fa2b77c3fc270abcf3bedd235995315c0fae0ef521c9aea9b21ac5900000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f953f132202c18b488988eb6e5e5de067f1fbb4c9f3d8f440b6904c572e145b09925cecda5013687646063dd795a4189ffe67ece5e3d1887cfbd66416d1219330ee50b8b7dbbb910dc6781e57b621835a97a47cd848212a4634cb66aa06fe287730e7e963efda35e1a4b62036f22cd7129bbd307df5145228618ca2055a8f3aa00b6aa9383c007a654de3c363a9be4e4f0e75a2398b5cdd63ba39dc7e07a321e6ee4fa4df3ce4ad7f5eb839e378e73ab3a41971ae42b092c0f4b55070c2e988526a650a27f2ea105d7c3a272379d5d808980c5f18aabc7cbdf4106a2fd317b4db13867378a8eddde158c4da2ca1984553c9344a6a6c8fbe63f3fdff78cb5d9351b869a2be146d157a6198d023c1e7bc22030004dc04d3a653a6b56f66b52aa3541a718b386d0fd43ada47962bbd010b0a01000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f91fd8243c5f16da96f15c00aa389d1ef6b49ecbfdc48f7153095c1f169277d68401732c498c169b5eea71830a9f0fa717723e492ed85db50e328ef14ca53a0c26f7a7af2ff8faeabbca6ee125ae98d728551ca4b19aed0622480bb402a5dd1ee13c21628215bf7987fa9f831671d90195ea10b69e77c2d20e7bb2ff767557fc7024aabc830c594b3fc08fc4fa81539f8773842f3df27e0ab0a396946a5c3e6ebae6f7a3c1871c0fe5b492dcc8fea65ea99e46d0b05537bf20cce456dc8ebbe346c3dfac1ede556ce92a7b595609643cafd2c3aaa5c6f1ad03299b6673d1ed09901c57946e9733aa97fded2c3b8a0bcbdba2f47fb77dbaa9da91d20e4ec06c80524b8926416e68e6f61cf04969e26e741030004dec9605a6e6043753620d476d1919bf586042ae0706f2d902999346a3201000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fb7b0accd1753b3990399df6adbeb83b7314bbbf25a85b31928c1a6d7b27aa8bddd25d927e22d8def5a59406e66ced1ba8dc84a8cb049009b74d829343a4ee591bc56211faf2ca198c9ce7b43a8fc1ec58213e3d008d7dd87a73672971730e9ab121753a811fbf6a983c1c9a9b84bf8d8113cdaa02352e4a9a4871761aeb12c6802091d3b781e2b8bb1360557c2da1ebbbe47274b528df6916df27b3dd8a861554d762f58cc65eb2f76235a6afa2621c58a3c3145a3e956a1c9c14f5a1233d23a2c56e63ff240a929b59feef760b78079ceb21a5e6b0de1a4b31bbf71ab0b5d2f0211cc8b2e7252c5ac069445337d1d4ce1be0b05a209841ffd431a49ddcecae6a5a52aa00691c29daa0eac53eb9dcda7030004df63c7ba777c253c097853ad177f36930345d366a6d745848da4b1b98b00000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fb707eb9a238e7f4c0695a4d0652624f1c1763b2c09bef5b056773a3ef141e39ea55f831b57474537372e234ec62499a294d7edcaf674bb0ff35a15fcbc6ce3547ea7d1385ded24c1218032e2a38c8e4cb2edae02551be6e716b24135f66da691f25cd1d006e67c0faa74652f7f22c72b9f07c2fc9ee7508cd6b4af5c2518bb130d150baea27ac508039ca65628886e477570a3917e9c9a9888f4b4398429a210893fef5dc7bd65a6f9759275c9b084648343db4fec6b165c3d3cfe3c59d675f27e5410ca9fbeb90f27edf4448d8cceaf6a13e87661747d3ef5483c32f26c7af40a6e659ef346786ab005c4990fcbc5948aa181da8c36b818c9d1987ef615efaef4aeae83e2e3249db268c407a4bc987d030004dfdd407ea3ffae260328deb31a84c8395089c5f3078c7545c0175f224c00000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f80cbcaf5c3b5874f0b2717dda06ccd1f18db8c0bd20202b64f6ef29e5faa19b0e2343b05213d288ce8d2ebd4df2802b849e9c395b7d14519831738fc26523fb37bb139ef42f04d6924ba069941a1a25a88a5433a465a3314655cea204599c7cddefcc8be5ad409e06c0206d9f1a19b99c7478b2be27a1c73087ce2bc1183e83407d8ff2650ac37a9ba0b2dfafd0cfcca1edf2b215a94d3b937a7802f96ed65b8f7da8efbfb089beb2bc7b6110668d2aba43ce0c089087b4ee09e7199ff5dcbdc60f14e55dade2c03b9ed41c3cc9e8b31dca84d6c79c1b444fa88c484cb3a917013970568174d4fb42fabc778d78376a2fee46bed7b54f48dd18f301e6827169439a3ef410370e81b84c56d041f2957b8030004f1c29d792e05e96a6928cc58d6ca2b1e6525eb361e8984a6877c0968b500000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f949e5cf1bbee1f0fada72db264795d650902080cb366654111d5e6cf4ad4c0ab7d8899f76bea432f878666fcf472699527860b9bc50940188c077bd249ba78c83bddef3e7de34fd972791b00c99fd8b2a887eb5e4e8167bb2b0b0233debe22efde549cfe3b138f9ba2b58f663ecf19ca30da33f43db20ec5db827f1975e184281113eb81d4b7a2d27647835842157c72a05d489286a98a9da435c0597fa89469dfb03a032fcbd62fd3047d43bc1016e88ed50480304a1c11e7fa1b63f67f0db4eca15e016c1d850b2dab5490559b2d1d4e1248f6e080e07d378c93ab55101a3d0eaf073fbb202ab4b1e0f68cd8c908cbb8f1a9fd4c707b31e6e299c241b13c6ab2ec6a8f19c7d46e36f4a9fbaa3d4197030004fa32ae75020e4ff7dd2626630c49ce57cb6aa4bb53345aa4ff7325685900000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fb7d76350ae68857cca9b83144f9c2737d3780fd29027c93f453a1ae3e461514620002387047c5ccb3ce7dbff9f495b010a52b3a57c72fe883dd197ad0ee2de1cd165374f67de79cb668195d82b373e5799a2af11f9a7042201615a74a473d345bc1aa0a30e3f21da4bd324fc11690e2a62b9bd31f45903f57a5a77d75e3d77ae16e3ba70e64e5fbd9f906705da14ad563761fba2cb466a733e3991c17bed1e4e3b9c70f6f769506744a09d70d982c95983bb17089e38012c56cc264b6b8eda14e455f9a00593453195610115c36799c632624c8dfba1e8a10c49a6f67d1eddbf175a3ad1fb33457c016394e2ee4d74a271518b04a0e863188df3a31c56b1f44c7ac9dce5ce016494dfdadc1235ca0b20030004fb626dfbe19d25e37267c321776a482ded3e172216268ebb87183fb9e200000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f9580245eb7aa7f2dfba766ce6a2a1ef9d894629b2abb642e43371841fe8a04522610f39e87b8835c6bea849d2ac43866ae421a62e9605a38d5be1de0bfccbe3eecb170d5e85c0be9cb2c95162d5418d08f0c0c2dcc0527a3b37b18e3eed3c863d8abf6d6ab3373290ad5385d80ffcfdab996ad82604168d92bff93d9d3cbd5b51290456c8f2fc7cf6bab6f8c7d284da901a0f287ac1bd330f428c63b2967bd815b17c2540d7e593b63b090984687b4b98c325361a2a18e50e6b2cf986a3d8d01014bf3a532f98ebea39f118889fece039dbd6e7779a6eac9ee1397010fc0cbc201fae7c35c645bcb7aab0b8481b5d87c5cb8a3c93af1679951c2375f860f2881b26d86803e051ec06c35ccc9c16659aa0400051d5a4b418e76e192b77e54db85edbf98aae2a47677e001e807f91dfe8200000016003cffffffffffffff0f3cffffffffffffff0fa0d77e4f829d3a5b9a831dc61119ac0294430780126be1ca0642db092053ee145041be5c7c8678374f6ff77a88e7266ab9f22fc87cf7c9b20ae72db47c4479461531ea7d7e6d31522ba2131dddbd1e13ac0026f0890c29a5f4da2bb6220bdb72da1e8ca68e2488683950a1e429855bbf7647ba5852c2e68a547ad00365085fd810fe246244469832d8ea5d8d23f879d3cbfcd1d2c5cd787d7597f7e0ace865f552e0606368c0b84d2920f22cd82fba40aa555fcdc18c8a23be39063ae391549544b3dc311138ea175ad37c429628a7e8570491fe3b5099bd51a8ef8920d4e1ff151393e57c1a3f16823a25f753a849eb0437f98da75d333ebbedd7143d228a6d152ff77ec0585a29d72986705ce0a08e040005229a3a687c87bb2ecaa9e3890963315f0c08162b9fe01316623f9fcf2b00000019003cffffffffffffff0f3cffffffffffffff0fab652e50bde48076d18d0004d3fb91369bd57ad122dc9ec61d9bdbde02fa77c7f16b7190404f099800e2d00e8c556efd21ae95986174a1e36506ea1a9dc430923aa0922f45d8654f41ebf17a52555f2d89cdbaea0de08730be47b20fd315e437b385ad6463cd6ab0fc9bb6899387907f1b3a5de2808cf238507c6af028ba89780deb1c9c1ea0238c11c02b11485e2be66099d26a6723dbb2158410712dbdf21c897e8ae6cc9f6c5f31a2bf26124a19348b1bf48473d3c90b419bc21bdbc22d599cddf4164c900eda44c7dcd4ddc866112a347a1b99b954fe50f313d699820a530ade6e1c903b3b219a42d07c48aa849b36a3b456b1b5100ff0bdaebf07730e3ca0014d1dcbe7d7c53fed9c76596e5b7d0400052609dd341dbb161ece78f7b63e29385808327a919761bd38242eb6876d0000000f003cffffffffffffff0f3cffffffffffffff0fb55f5d9188ce51d36fce536cc6800432b21c9320ad042e17ac81d499469ec694a358b493683cfeebc9b4630585bb55fe0b33eae947ffb521a38a97c5d139651b8f0367f7be483069a627b33af6ca2ae7b9d73e8868ca50627aa8d910c103477e03bd68a47df4e446bb143309521c9b89a1dbdb187ea00c91e2364c70c23840180dd790c1f229aafee07b2ddf208ced1167dee94d9416879c0261890245d1d24572e1a9fc8b722d82134ef9bb199321f1916cc39b8e73c49b545d80b46b4b83ab4873039db465ae59a82d908df3e85a14869ba8393078e84937865c2021a0a19519ec62f8a40c0d35fd0f44b4ca13f202d37a41abd943438c8ad6bdffcf8e612b4feb101a6c5384c0b44f3e4abed6a9ca04000529881ff840a38774ff5ff68d0974b9b7581e4b83bba2025ca7f3771a840000001c003cffffffffffffff0f3cffffffffffffff0fad4b9cb630e89f3667b81b8ac363a8b4722de2de14c8eb09062ca245ee50f2454ff81461db86b529d16f9a03891f08c07d06b13d0b467c9a7ec357065b2df35cf9b0fe44bf2e4c947cd31d5cf400adc9a899045f16cd5765788f409191d157d0ade15c38467798aa374864005fc25e869bf9387f92d1ff561bba188798b1041e11af18a7752ee17e5e38b1127bf7e27eb7c796add1b8dece9c14b8ae221ab8805d711dc5783537f0e4eab7a019a9caba8dec80dc2b9814c05243ced4a90dd4b77111e561cf28977ec7b60237324a3cc7ab64b55d110a338e4fa1a079d02c3770032ed047a3002e54f4d0ad8c0a0b458193a0e7049618bda1bd99ac3a7d5ffca4d945cce4a394c650d81e1818da25dde604000539397fee7189036f1493b60167be26d83e7d35ea30b2a9597bb1a64f0900000018003cffffffffffffff0f3cffffffffffffff0f967c3473f08b4c57b39d04143cc85df014c4dc74cf7c54c5e2046bf33ca5ab017afa84f3d806e07795a56dce17ff8093c4e600e6d245b9802ec85614fdf79c550f6956c4fd97678f7544bdc9d4a4f1459704a213f35c92dac928d94fd63ff52e117c8dc6dcec3249f83090751ebccffaf78714bda641197d257ebec49384ea41118040557972423e00a6640497dc116fc6fc4c508f02f963eaea6228aaafc4d0c93a1d35a95a1662ddaf6a156c774c8da19f19b4fe0956c5c94d60b11c8250c3316ce60a0c7fb91848f4a3ea6a04e50bdeb65d65a5dc3521843325d397454e3d02e015c49ad6fcde11af6ed7608283ab65a861fab2d0acb4209761dc6221b7b866925c7b8a410a75f3f06d1cd2340a5a04000554f322072b61202bfaedf4011c7aeb704730fbc5e75f4aa6286d791dfa0000001f003cffffffffffffff0f3cffffffffffffff0f882ea9c31ed68bcaa11922a2f2a8890dadf07e54f0caa9e74183ae709dd29e4dc431232e7d943b562f760b948ece39ecf6a442605c9d186b7459341abf38a1b23fcdf440a012eb0b04a7abf4790681e8a0f8e66fb4a15fab329a0a973be49d22039f37b6ced6a943f4829303ceda65ab1219759eecb45a535d5576ae3919b02e0681f1e651821c1da22dbec5c949ad214792121c432828f92287699d757e9595ae24c4e5e3b53d146eca2736eede068aa4af720441f289ea6463d3123eeff374294f8389c867e43efa8e4b99cf79c56cefb14a09a55fdec13f00910c4004476d052e44f8233be24f6ea2bef0db9aeeac04c62a9761a7a53d9b2511dcda49841c363667c4c739b22850496e5bcef215710400055688fc17c0dc6f800c89dafe0d088ac658d26746df09262ea15a3a09ba00000002003cffffffffffffff0f3cffffffffffffff0f9758d0558cbb8cfbb7dee652780732e2ae4493ef054ba80556568213f80137dc07b83d5a3334d52ac33cbfcb5de1d31097b59e3b055570f685eea936fd1ecf7ac6820f1e6e4f1fb4f96218a1ffc8020db0db51cbd96ca682b2f0b855f09c88af8c379f5e9c21b292bfbc80fb3279c7ad569576de1b577a7b125621d5601501e017a8787aca643f785a233285dee220afc92cbfa3af6cca145ec1b0cf5f6d298cf8c6a0fde39505ad6b46ffaba5e7e38ea16690ac93002b7e8127b9b97df334515eb6013f39516f3f84b3b87bd05af9aaad81c6e16ff5f33da7570666683090d00901db5018f097ad3e923ce99dfdc1ccbce1a46c656c6de707d5a27768eb6a2aab8c18829194e37556838e78e253e01704000560a9ad2deaa56320cf681393a1494d57d0b00b142dba980fc35f567bf10000000e003cffffffffffffff0f3cffffffffffffff0faafff6c3c1d7128a14cfe116ae2e8de92d6b6ff7814fadbae028b72c3e991e97bdd92a4ef780c6df051e6e5fe27894e9ecb60ba124e2954efbf301b526d4f56f50dc2ed599e91b949b3ec0a6ca55793bb84fe46d1700cf47cb0523e1612166d0c04f43bdc7509b8678b7880ed0cbe0b63c4866a2f2e0731200a9c8c5ca17c5d10f712910a5e1649bc5462c1529e54cb2bb36b9a58f12e07c53f83722023ea2fd39b3326fdfd780848e32fbd1ec5088dda2ee3c2d3d56c0d55d7c1c81db2fcdc3c56a00f96e19cbe3d536773c5dfee0e3acbb2cb4870286486e0d5d73c483a41218085b2ce8c99c15b469c20c82778c9af922f8608a13c52541d456c7953ac95d2489add14bb919973ca819ed718f1996040005663a8bc77337739edcd8590ff792e2bb689cb44587507b7ee09a42d1df00000014003cffffffffffffff0f3cffffffffffffff0f8a14c0e18df4b528d8f9e35c1e4106a60642918f516c0bd260de6ebe0232e06a5c5dada3fe4baef6f1fff9bd188a271c318167b5f3275933c79881769b45eae9afea05b1f59bc666c4601a7e5a919cf3838bcb831206f69a51985976011a47cef10ddfc4cc68bd6f3ce3d96f3803fc1f098c8484e499b7c939c571850220aca8068e059c8c3a2d30f05a6f773aeb72dd02903ff338b9d998a7bd80613cff39156acdd19b135e54df7efbd59ebfb1313e84ebfc8d8dad93bfc0d497283c8f89416aa5b5a2bcdedaef1e106fa2515a998de5614a9968068f8b667ef0a2b91a6c6317723fa192ec8141ceaaf20715a965a3424c5794f16f27867ad6870221ed2843ec790cd953bb4cbde44a2e978619db450400057c122b0060a6971e3378cd1155b0926484b9fcbe917512d2a7804fdb4100000007003cffffffffffffff0f3cffffffffffffff0f94c61c0c969e5e7b044a94a041d567e92d2de00d420ad03f19eae26a4bc7a0decabdf96ba02912a941049c8916684b253db3cc394d6408413c5d1ab526f53ad5afcfc6574f46ad871915235cd75b457daff1483719a315d9f8fdda65e019b044d54336b8b2e9bba40efd45ec662ea7a35ff6e5bf65906c5347427f1f0773cf410738745b8cb4e42856f59978562cf8708a48502920add54c2221c4ae81edc7344ad7221b1a175cadad4690989bace314b20624e546bed5d0d2c64544f597d81815f61f41a1245bae107d55b82ba89d2af3562ce4038f39b89c387aa40f70611d0fd1eabede842b60bf0d5781ecf69edb3ad11d05211ca2e3b43f1b3874b62604d839411c145bdbecd442844e4312d8f1040005869156fc0969c6055b87aebc0aecbba31d22d9eb8e74d510f475c68d340000001d003cffffffffffffff0f3cffffffffffffff0f81d248c19946b5faeeacfe48f82aa31a1ea08cf4c87fc252d8a6a1f2c2689144b419f399a5f3d491d585cc739500968ed8bb0872153f6e3c49ff90c1a6112b92a701af882b5df70de8ec0ead3b258449875ef421713b60636f5445477fa846ea340622021eaffb526a0f494b3b8310475e4e1bfc06e977c7b79583f4fd1df50003ce79bc05f6d6a0bf48ab93e2130990f422ee79fda822c06f66efe981c285b3509be95834026b22b2223181f1a41b8593e4133b130b174adfbee72ae3faf8331064744932ba4b02e763dc8eb5dcee29cc8bb8b00da2fb25e9f2a4a7013850820cd925b4c104ce9e7d8b309dd37d184e18d02155fde900aa1247cfa8b928a0c5ef4da51272fadc1e6b61b844e4933d3c0400058771a8b9f1f8d2261f123046c5999667200b3d19e5c28e42ce4780f6a700000001003cffffffffffffff0f3cffffffffffffff0f8c7ac677ccb88eb92cbf40a9880120a17cbfe7a167a0f30612fa549dd113236d8501e4184e3e18231aebe9133f7294cb6bf73efb3cd0057986c3581854c127ede3f7e01cc3f223656801e4940a35b90eb24c8a724bbf122c6a2ae7ebfe5549bbe68e6965e7dc811168233673ed177bc0079dcbdbea24d48d9b81a3a950d9046b0cbd05402e573372bd53dea90d55a8fa48e54ef945166bc2231ce44857052fc653aa2bb513175509e16a78cb5edf8d5ab3c28a34c9058e5aeedac0e2133105eaec9b3bc0f5d71a0c0eefbefaa51c302c95a564a2b93b8ef67df8b71168bf9f89172cdc7eb40473bbc49daa30af2b2c85e66ef75fcf39a16a2067fe788abcad7131e45a42d5215759acb85fdff88be204040005877c972de8adf74a3c28e4bd6bf7e5c5fa198e64cdebcd1c7e4949ad1001000005003cffffffffffffff0f3cffffffffffffff0f9693e47c4b3bed080feb3841c70815a5118c47996608acb6537541b60ce29995ea7820173644d56f3ec0bbc31f05b8d5ccd510633fe627cfba37e0114d221f77241325955b0caa89a0d538e6e33ac97c8c4bd39d549622c9e9e8f2e1fa8c59958cb2e51c8e36844181b5d271e96e14eb032beeb7aa67000ffb02acde4f1b6e6310371ff7c8368b91718fecaa20d104357ab2c93790af2b54f053cceeac2905cb59a6d51c00feb05e03a31e76b5bc4669b24d19e2934f7e1b04ce75d941dacdfb0b2d902e68d2c0b2404d4fe7f0b8807fed3a44814f31ef05c581aadcbc3b83ca10fdb03cfbd27f13441ba95f5cb033341a30a9f3f872e4053fa4a6d29df9d1df19329952a9856570806785973b8077f904000594573b8b4aa28c48ff5bafdc3f92f6689f9affe883f4f0d9d11515d25f00000017003cffffffffffffff0f3cffffffffffffff0f869ff3033eaef56409c7e731e124a78fb048988dacf5f5ff2eff5ce2a3849a46d24b482da24ad3e7bbca260c5d099c491bf84d0b8073ab81ec334a94e0e370c76ee70f6d497a1118a9a87c2edc4fdddd95bb74979058059c5791598b20c45676edc5d534469be0c5aa0c52375e2de517754a25d82ec813e0d14893360bb10e5e0d8d6a91584a83b369fcc6f1ed599044dbc3f7a0d87c338b4c1807d69535d63992a507f379801f01f918bcaac9d56f628d886a52681a7c24bcd74649b6825567a42383779d522e9b243b1d0a09c1dd0ebd10add8759a523b0f68f5c2fe6a5545072df729174783a939b9b8a3e9dc5af8c909a15ddf434159d4701c2c157321dae3806aff7cebb9173ada2ee799c2034904000594d7fade46846faf321e32e9ce4a11a41e2d519dfb880399101a4d850e00000006003cffffffffffffff0f3cffffffffffffff0f980ebc2a18ef8a8517524c510cb4962cdcd5c4a879f38c4f6ef23075f381bcc72e3312847685f394899a767e5b8cea330dfb6ac6c503253b31936816987a2028342d9653cb61e1cfa365f7fa19c67325b485a2dcc758430da2feebccdbfa6542cb047d2cd198980051ad9aa19b1a0faccd7e9fab2422d1d6acd441f400fdd17a0b81fce02e1a8f5079a26f5f8707a0025e46702a372e4e4580ac3dcf29941292dd709eb46f67d3bd1d847906c9208e1080801fb777c06bee56737514adc707a3534eb37838dd30f6fc463ed713efd3b6b550fd9bb90c9cf0b24322c94997ea620f3d92f09658dc45491498b89eae183376f96d26672acb740011911dfc67e947fa976505c4cd38e58b1e981f11d0a5ef040005997a477395e247a011c9305392cc1c01c7000a01059a945ef0c07f4a160100000d003cffffffffffffff0f3cffffffffffffff0fa7ae5a6a4d232e123c632813d7414ed89ab0e74e611381bbe603011e184eb71b42ac120ed300fa9685428bcf1f3158f633cdf9de38fba7c1ac456e7dc5ded7a63b1babde00f467d84b8143d82f82a31eae2292dc50e1fc9b5112f014dc88be336a6ec04f538c9feb4c023c39f9606718084c70e3303835df659dc3c3e36eab9f013b5c74214f9c0b94c837cd9cade6e738958f66e99166b001a028c1b72c5ca9d8be523b6be7a6a0ecdec2c73e92e11480cc694a8d0bf7749743eb1aef8ad821bcae8004c0c5e50aad84e67b98d81d3ab602f98deaf4163dcd2c1dc4ba4b42be055586c7d240feeb8272e03d934bf296815283a86ad3cacb3268504d57fd0c86f14fb0a491202aec7acac639f88b21620400059ccdc1214acb4c8640387b6a02b3b0356cf1d16e6425e08d39151a725b0100000a003cffffffffffffff0f3cffffffffffffff0fa4645753366b647ea0da709914ef22b0e5ccef531f31dfa682e8e652dd96f9e8459ff746f55a1a55265ac171be8814f32d8216017a881e9036fcacf3a6caf058f5988b0ec977218c014644ded7cd33c2b7bfb54996dd8d63f0985587dec9676fdcbbce26e06d96c06d9132ad4845a32ec9be4f1537453e3bcab0ec6cef18417a0d704a9cc972d5d734f4f9b21c6858f3ce2553e382210a5e2181ae17e3387f6b6b3e474fe111c5ac45f0fa05987680f4b290dd9350a408b116cb15912c35c2fe43a14ef1c98ce3823fafee6e75ff41c6486dcab82d9a369cbf890fece630122203e5f0c7f9fd26151ab37cf7abcdc4a3a49867694c56f5c99ce022eb2980a612e2bc94c557224cfa95b7bf1e26282fae0400059e2dff47ecf0c89c19fad38f18badd3df0531e4741dfc06dbcb5b20a3100000012003cffffffffffffff0f3cffffffffffffff0fa892dfc1003dd35fbb5c78a25af866ca6b291312c1847f8148bce08c1288cf27f8ba673de9765bda9f7c7066608d517f8caa575398911f48ef8bd3b3fbb4ceef8982bb2348d3f2c31c2d2b1da3245a18a77507d8babacca8fc8cf30e2b15f77d766e0084ddfad7d25a8a7c30d142e82be333cfe3485704a28010fb3a40a0d25d04e228da53afab623227063bc6af90fae6dedde4d4518b94eccbc9687141986a3def89803a5b7d0ecd17ae5cbdc657b3a35af6e9a2f37a957fe7ba2915a0eddabdf98c3686fc746f768251c1059c1b41998d98c4918c3ae19d30410a9c64062b1558daef427cfeea9b4258b91f6048763c23150a594302142d2ffaf2788779e1af467cace4a5b6390910a98bedbb4b02040005b1bbec44fcb93a84e4950fbee8d03fcd672174846bd21b1302d65a302300000000003cffffffffffffff0f3cffffffffffffff0f879e1368069603b46d12226e6b589825f18152c06949d0985c13851fdbeef5b783233e2806a29cab54f164da4360e9aa6a0cb58e490252b105a007b376ce301f7334712b5ae9b9a85d9575ca055284e7af02156d3c8e285143b590f806a660c42c8e16cfb3e047414e03e5699841d6c0ce258002e1f2627c2016e1b4082c1b33183b3a8819f7c04d18da396396c63f471ca287ff7c8e1bee1b4fa35c82c134da8937255a0bc8ab77d08e36d73774741c812569549ada7d117b40d3c8255cc5d7dff81e0d47ff43ebedc1ddc7f32fba40f1553342906f813c11e6d836d6ba968c1772ee78a2fbaca24788a5e212368ba906e80532aa4f4e58cbe529fa244a1af9f08c5fee21af3d918a767464ed529440040005b61d43573e1fae1b2d318b1476b373e66f175ad9b3ea116141be92e27a00000015003cffffffffffffff0f3cffffffffffffff0f95ec0158ed63989e0d4aac9453cce44e3320e428b793dd34cbd94149b1be22a6dee60a7404074d546141aebf68a69c7fb8c851ec4fac0c89bead0ad509275379787726d42eca350da9436a1421590a6584ef96728c2417b65725eb91a51075a336769a7ac58050793594ad135bdcb2d143e387ed9eff9d382015e6a63ffa843113d586dc8bad64b5c9ee23f1e302662e9efbc48d6812ffab6370040bfc37b168420490ec99800e7c0fe7ba688cca3289a8124d3a178e66f42341c62a0dd9531c427352f91cdcdd66c5de42f5ac7d8da048e72fa0811b26f2c64133f75565d941106fac8e521e3bb26d991cbf9a1fbd501046a72168ee3dca981e83bf838880bd96e05f6a76d6b92ff1040ee22e83e508040005b7c4a90870c88c2d035ca55ebcef96430b60127209ab78aeeef4cce3b200000010003cffffffffffffff0f3cffffffffffffff0fa2aa7138059d72e8ad3980cbac1341ef793c6a4e822278f79c20b65a780eac3800d1a1a6e68da1496a550277b85a9a1a3e455bc2880d947aafaa83539e8b77632b6af6c73771ece48a3662af0112f639a8cb46036a8dee87ab772a12522c07210c500fcbdff210726698d9f9df23e577f148247c0c0aa85e3cb12d99496bb6630dd4d00b0514fa913950d6b4beb9382c3c4b1391efd354a306756c1d9e1628ff885c7e21bd1929798298ec3eabc3aa2aa26fb3697b2f597636cfe2a1b9bf4dc348d3f9cab63ee5c9b22c3660c7c2085c9ef79a5048b8a107c8a1fbd2e0aacd18036db9117b479c945422b4ef7463fa59cd44a443f3ade55d61212133aa6fdbe53fceac40237d426d5f160f8b31c4b04c040005bdcc69109eed2c96770ae46f43f76906d9610973eb1433b34727657c8b0700001b003cffffffffffffff0f3cffffffffffffff0fb8ccb307127f4310a059ae2b47701b2bc6d2bee5e31e2b6d92541d010d9d2e77df40b0da6638f341ca3cfeca9bf97de6362e1fc85c48c47cedd411da45f8613f3a74e2973d7b09c22861a74fb314ba82a81da5f011c0b5045f28484d3200d019c4e9382d00ee71010cc476cc3d102cfeee44a077757864f73056112a410e1ea30e0c80a1dfbbb2cd10af57744ed2aa209bcd7fed4ffc00cfb5de8d7f206cd1b81ba131078ee712a876ffb2b78b127bfe87716f20e6018f2e3e19cb8eb09daf39e93554fea2b3d87991cb2793988bc2092e0373c06e2c1a5a5c8067065bd7eac60ed972962699c03ccbc3b61534bafbf1b56d68da61c3b0f20317436d5e4533b47e9cfcff1d9332eb58277ce9f3d5edc1040005c2ac9954e8b50f2c2eeec08de718adc9d7636dc265f97db0245759be4500000013003cffffffffffffff0f3cffffffffffffff0f80376dfd94183f491876fc460294a37a6bf8ef0e88b00540e5a6cf2ab047a05663e1828effd1b22be7e3e0cde9d137243a0939ced3e4df612682d688b54701d00ead7ff759ef5c93c8351619e18e54d99607a39978399232e58ff4a68d695a60b28f10c339c43537c86d8c6504d2ddf997d417450fafe4e6000e76f35d774b3510b02533647d40bb174f2182ea700d10a36fcdb3a23b20c1ced4a3162dfd434bfad109b73d8a2e04c4b859a0b76a7a44b92d232d479ffb0db1401b0331013edda3ef66c1f03b1084757bad56e8b296930746f937c227cabb4dd2056ad6c6f5751292ee87cb409fb1020548baee9f2d1b117d91c4969e90b7cb722ae14bc582c94ad9ee621d98f7966e3fbab18ca8cf57040005ca3cbc59bdfd8771edf8393f544adac6d94006a450623c9fa1d99ee77400000009003cffffffffffffff0f3cffffffffffffff0fa7cd8516553098e52ef65109b1f6a31beb083976ee4e3a76dd685ef978ca1c5f5927a61e9ee0f7a780df33e21add5c3e5c38e9625cc12eb2d2575e6d4a7e56128db6d1b47c71a6b61d26a88148f4a09aa6196dbdb6117ad3c79bb0f7e1a12b13c352d4a3c1dd3c366bd6b424afd7f2a4757d61ce9d5211d10b7779a38f362d6212d9a090b0b81c615a4ab806699f6b50d9dcd221b10afbdb281d7fdf71a2e85bf73c0d7fe11134b65fc2719a1752d2f483b04d6f0c9f723c0bd968adbc21451be3c88d9882a9ffbcfebdfcd2195fae1ce33aa9b5c9aa3a709efb8164e31f76690b92dba16ca9951b326744a984f62b2b4c3da078d736bdb399cd29d9538d0b1dbc6ee6062a97473df88de972ed7e3865040005d0aafe6f3665b0c586996605c0461d36e9c95c655890ee6532aa8c060c01000008003cffffffffffffff0f3cffffffffffffff0f81414b8a527b79e8de424ba60f72653158a2dc44e5a92fecbda0613afd1a657446ce53a45a0cc12d66c7d87a5a96db819805ebac6d48316d08a0ace7dc9381d46915f9eaa3bd2b29736cd86e2d3799beb7a449d7366790ba4d00c38b2e4e046996116bf390df4bd95e9f49cc2f036aae887ba7f927a95f689a56fc62abe3a87c12a2ac06f318e3de653b24ba3e1195fa51bc470446453d964d8573a67205ba67bfb59e7b1ef636f842f4af23dc1dcc7fb647ee53ef1d058cd0070644bb08450ac6d3fdda88b581e3305a52a61f8c6d94fc6c5e3fd9c9d1f62f81ddd41a20551806c9976cad42f0750abcba083b0ba87f6263feb9d905d088325c1874641d42264ac2af0d6e3a13d7574fafaa3a6485ac040005e2581022afc36ddeae07c31b8edbc386646c2c327de8391057f0d8ba790100000b003cffffffffffffff0f3cffffffffffffff0f946dca8d00fe8978b8fa9370e8655c82c1ceabd69b7fbb0c0d1fe3e36d7d73b8ac147b97e5e6a319f06f0e917a21cd25301d8b7849ba03790bda77445992cd61078efe76111247e28bba7b311171bdfcae6452845fe46296e5a40c1d73f0306e10a518a8ba6afc817f88c0faa2dd4134e6f0e7732ae1e6d526e2ff8d971a85c30ec9fc8d8045947df353f39e5c36b195f1ea9eb5fe4329de4612a7189fcd7efd65640c4e6dd41085c569d45a5c3cb7e28828972f14303af42462b5f4ad39c31ac883a8bc96fe623790937db849727070d1af4d2d0ec08b4cefacc61fbc3cddc908f6d05f64a6a1e46a7998774fe2832ee3702c96a3957ea5c9951315563404d10e482b45bfa4c6e73d2610da2e22b357040005e4daeabe9206c1afcd9643b4710f39b1966cef4298145b8e214230684e0000000c003cffffffffffffff0f3cffffffffffffff0fadb7c83d3ca32fe2f124a6650a584deb38fb53f799fb6524f12f4de3aec23bcec2bd9758b1fdae151005dfcedf408e21fb528304cf46ad11d7996f1e923cb30bbe5edbcf0e2ece897622d567488a9ff3b8c835c7e6f61f339cac79018918a02e201f965008e3406724305d57a445a566855dc7bd97ddeb5ee7368f310c42c97211082bb70d074c3aefe84e76eaa2f63dac3a400938ff51707a39c7dd836e925b970b597a808e86be8b5bd357ce21c227a5f3b0ba97541ab57da3e0940fafc203da070942267f2e98df0874347b411b6e73be1e0ce4380263ffab20582380b9fe16e7ebe45c09d112806c14e729d745f07249dabbb1d491e42c8cac658b52b528348b87dd6092c7e4d06313676618a236040005e50444eead63c53a341ef95e81017f04b70ddf3c3e19cf133bc112b07400000003003cffffffffffffff0f3cffffffffffffff0fa9fee4577dd451ed0165ee57b864fbd046479d52493f075424fc500c0dfdaf1c5e92e3c1107c3e858864df9c6721d2d0d6a2e85214ca620c37ba1641a9923f09a3a11040161f5fb33ca555d0c8b91385a860cda14532211a389780f988e8aef34d4bee6bb9fb8876f79a030659f491c8f91d23a474a30d4ec0392e9c3fad7bef037ea613cb3f780d1194c98d9a0c3a5517e2379fbd3d0e9ae38d700c0d361bfaa53dcbb36160c7f0c65e7d328fcd0601a660cfb2eec4771f74f3a493270ee96baf1f6115ba195a810c458d0c2e03cfc3b4fbf229eeb2e14ef0c7790e904aca1a06305031d29923a0436c99d3b841e1c502679395bf36f7832aeab65280a4aa19fb604615a9d14b24c87a0c301fd0c098040005e88fe222d61bdb814636013f9d197e97ebe85505ba2ec59ac29b6a25190100001e003cffffffffffffff0f3cffffffffffffff0fb59fdea4757aaf9cd26bb54c0f6e78e8a5343cebd06514edf0ba4aae1c7195bab2f512632c7520c50d95c0e057e72f9d5f2d8bc401c237fb0b396aaeec79c8c370068e6e269565a1f90b8ee096d61ad7b5da3a964c3c72273970ad1f405d52f4941f2e7d38008c8067ef3dd6a1f16e6e0919ec1acfd842bc78305ef24cfcac8c0abc8e17ce39de8229b9386f393908b44141b67934ce6317e7ef2f5965d3d78b46701f085709780931acd8402fd695beb3a653e2314d284daf4047eeefcbb8b928c7cc564f8ae43b477a5bc145251e71e37ca309848d459960b0bfa26df864f814fd7785a1049608642577ab56611c59215a9c3d60a3ea75668d711202cdb3e3cc41d1e389a64ab285900aa67da4c5b2040005eddcf6728f71e0406f460551446a42d3f8eab9b4237f1671e05ac0961e00000004003cffffffffffffff0f3cffffffffffffff0fa346f81455debd32501ac18f10108316e9623cade3fcae5616d7701b9d3cf4b4cade9e03f9e127945ad27412564a90b522042e59c59a2961fc8feffcf50e1fe46e1dcbb0bae15f13a90b1f9ebd58243085f8d94d67409469cd6fc6ced13f6760c25b6fdb143cb0622f6c714dad83a22f5a6bd94e50630460fd5ad53c73f773a81255df64121e9de4d0be3a5eca980139e6010c7d96897c9881a93b001c883d69c46944ad8fa84863380b9dc65dcd5ea9892e11546d4b6c0581b7863e241c6aec35917801d50021077fd69f820bdf67e078188b972b5694137746220a359e622a18f5fa528a5d3fb8ab6bc10f29e90a4702e17727d257145124076799c5351e0ffebaf3977ff30f02eb478c85db7349d1040005f2804fb954ddfa28728c2d3895bceb1f7866d39746ad3af46e2412314d0000001a003cffffffffffffff0f3cffffffffffffff0f8bacf4fe9f0b3b9e63e574d757384a09b62e0a5488d1c6c1959d07db57827bf9c8bb5772df9e6526cbed82e554df36dd414b1addf7059e6bdd6c23a03dccd3196b25a26661f97d4f2e88e242be54531bab52dfe0bb526785a0952e4e889f16d6ac7690c8a9bca0ea0233000199ae08e88d0d4ed1a254dde6c9a9b961dabd16cf0f80862ce1aaeccd653db2836db9cff24e9dd066ba9991ff1f8fd619bc557ef857e24ce795dbea71af4ec0c038ef108282b346f67ba2b1548d0f4c1c5fbc22e600b7aad1f7e6c810bb3ebd6df1114c2988051add8f8faf17bc089692557712020c0a8f8d357aa5e2c39f9a9e35ff97ed874d35e50f080f6a3edf3c1e670a918f4f9a8ea38214921f27be8ca5aace4781040005fd1d962414d32016c6e93a3d191a21ffea07a1617d7607423bb0b5445300000011003cffffffffffffff0f3cffffffffffffff0fa9ce5e51d96eb1801c0861b923a000f9f0149dfbadbbef210768f8eb63403080256cc41807f848feb8d9f8941c0039a66056557cfba2d64e82dee5adbdfebe68c12a7127ea7885d0c4969b9c37a208d7a89aa70a884f09e3c1626ed651b8499e433ce8c4aedb030b1410da3636462e39946af31264aac397830ad59370e7da130ceb07bb4c518a1ce2ecaf525d86de97e1201d401d451e4d8cd2200a4aa1df1534dcbf103f5eaeaba41c9437db7995ce8a220ea92d9e453daf91eec3eb4f1f8d3166b25b90895ecba565eac03f86ac7a79faeae7fd2749555223ceeed2bf107708e98a35e767fdac4b6116975caf16b59e4637f61567cf72be49ddfdded9e7e0b3f03e73ff08912ca37174caf4f3e7100300060813b0d6afdd170258ef7b3b91b7563cb99223e80b430d66e5a4d8640500000019ffffff0119ffffff01b19a90bf8bd10d5eb046992a133f9688cc7c129cd9820f3ce9fe7b6c1f70ac40d19b9f38d1958792a02c95b77c5cf8b25ab4e6c5cc214cd9c3e07a2c11939fa322ab6ec6ba5696b1a4e401b07a425913af6ce88dcf01f3d97d3c6d3257b44be76bc8a0b28f6ea860fb527b7fadcaaccc6b80d3a13bb52a9041a5af9f0c0e176d0d51466b9f0850e2e0495e8583bac692ec6c3a7da9f59a6089d187a08fdc325137762bc88a7a9ee37ce2208e671ff441a3925e4b8fe0704b1a663da60a28942800ea5476e880c0b9b3999eb40b6cb34ad041a7e1880db54e096b8b4a8eafa54e06faaf763b2a5600f4ab4ac016d3be5456f3c5b0892644103913f290f8f13e908f4a0232c4e5b45d2fe9f5ca8708c5c9030006215d56426d0b48a016626a29ce26fdeff3c96f40fe4667fe6682e35d3500000019ffffff0119ffffff0182520d7629e7a1fabafd164e36b989e201dee02456f170e9a0931847e4e0f2b266d89c5e72642e0bbfd9cc9c997f7876c41678eb9e53f5c3cee854bcab820ed189415441f3f79088640c390a9056c5c2964c7c4e288688bcffbb2e608b68d71ce07f0518e9c47900c32d12d8e9d56e3e99b1d3feb23b5432617da34f353916b50f3871595a92cb7e4ac2552d5f7f323d3e9a52675581e9475bc2042c1d6f06d5afb058f8749c41521be74dd0c45027d0a8727169619caabe68a01d00d9982cf75b156e3aaab6c4bee9d911c5d3acfc2090897e46e1705e866be88d6747a32e5906bc054e93308b614ec827bb29e157ed30fbf3f9a9f496e44524ca997122210dd43be978b53feb7b8b6db9d35ba1036203000621e72893e30f1216cc97b4920e707e4905e9bfed1f84f94809c5b5393100000019ffffff0119ffffff018aa7ece2974f9ee33ac7f6423d0a407d748bc6dbf2e4df798594bfa4ad1c4d72ac0b8be73beb351e976d416590fb3c55a1083359d0ec3a220597f634455dc8e89751d50e93b0a2603928f9c1baf8f331b7b5ccbabc9e9acec23c1408f5baaef3546912f909adbc17f51f946802c37709c3ef34adbd280f221764c39cda4663e701ab8203569f1d26cd06865ab88a3e3258af96fa8d54310d10018abf40ee91ef0000f1e1e0fb7abc8299aac476bb30c2ae87ad2b64926119b9e600406d89bbc9c473c64ef7aad90a6921a62c3b4af527c23f63050f5f87b97b1fe48652aac4bc18ed05c48ae12ee1b4ff4c8db8d819deea20a4bc96796e3b4c93ebe0a7fa9631dd5113eb0aba22682bb63ded41815a6b0300062cc16c2804e4db62fa75400be262db71ed3abd19bc737d9f2453a6d18e00000019ffffff0119ffffff0193ce0728ca389231f4eb3783f21a90593f399b7b2bc104978cdeaa7176174907f87991d805caa0d9982eaf0aced9329e0b34024371bf2b1461916c450508d42f51a324de60270e6da6793be8a20694fc99113256c086b57889b77be6464cccdc715d0926598358d7b6bca6f8c0557cd158868f92a6ea70ea2cd012a3e293539a09e2389cb9bbe2c01c6161a032665a591cb317c405f513ca6c51d6e45c38246a0bd7dbc1d48adb4dce369ffeb6b147b19879aca758caedfbcc9e15318c3f916bade31556b6c22222f50b5cd3c150d9f350dcc8c2330f667be2bd278a7d86557e08d49f0a66ac511db8f7f480a24a113cb0db45cb7e44f757e011c4452986f98fd57963fb4be7b0f488f7a71ca1fa947103000632aff469aa0f661590ffd786cb8ca4fc0284e575d5ebb112d391ccfe2b01000019ffffff0119ffffff01935409e99049b7b23d1c4b3f278e7efeddbc153e081715c3002f141dafae56664c01470fd2bd9f175587e7da7b4d8f5982831d245d1da0ece9a50698b7a160f102ee380fab8ac816ad418f5734dee23bb1de55d65b9129b99f07854583fa87a148fc8d894ec3cb06e571fab177bb541c6a848b3b42c780b1c47d6a2ef0da24b412830ffef2b6ee8f2dccae1891531079d2079300a8037de62206f3889484f8701b9fe7e6a3c7b3e11007e8bcda4744168695d443684139776991074fb326311c59d983577bef4f5165dcff50017bfb128438cd91e9966c625b08e17c2249b72a098f611cada31b058907ec357859bff0e12d963dc9c94b146bd27b4a711a763e349850e0f77b46c31dd183fb56c7589703000639397fee7189036f1493b60167be26d83e7d35ea30b2a9597bb1a64f0900000019ffffff0119ffffff01b41aaef2af4077466f5d3ac3851911e8d4dc9a765e23bbf6be6e398be0fe7e68f253522b9f279b01e39ddc7a8e5c367f3c65599b303f1c2e4790e17582694932bb47e96ea2e570648e5b5919e49add96b69f83df64cce3a1a2f334d344209764b8f263ae73229560c977c367dce11f4d71ff9fd59189d962e7ae729cb1f8863a0acc942b1e8c6bedde9fa944c8f5b4d4849cb7c5feae3426a01e1b43f862a654a075e8f81d3d184d48e93234a007c32592360dd9aa36daa7e22d025604aecbb772510fd1960d6f6993b46add5e2d3e84220a6c856e27f73a50445c331bc263580a43c0fa9153a48f43af5b3a792484457e18f1da917ebd46010a444c8c474b895a778fc4e560aba864cf9da90e11d7c90300065299ae02324687b8a92df73f82f3f660756227172103ec98fb446f077e00000019ffffff0119ffffff01b9f4e6ace00780c27a0cb3cc06005ab1cb27734cb9604973525b489a776ffb86e6347d70a5d0a3a4857ccb1ceb90c6afbfa48c64912bb7535ed59fea5ce8537ba36a0ca6cde2d2c34532fdf30c9f578ca0eb3ac83dbc43b774dada1663d28dce1f5a26e417ebf8869cb5dc43db0e8ceb0eb2f9ab60411f4304a80c37c40eda2b01df20425852eba7102efc6671b7025c6f2f751b5214360caf85dad3e56930bebe0541aaa71b45dfb39d696db380c52085df2356388e8d8347078bdd9359fa61b87b359f5930fd8569ea82d3f8c67c792a5a45e01bd1e8e5ee13936703fb27900a731c66deb0eea376a401b870e6cd32bfff15a5954201545dc093fb7833671bb18398f20bf4e1a4a06fc5bbbad14bfc0300065ac508113c51d7fd75e527bcf4c0b44a20f41ab9df669af607e603f9a500000019ffffff0119ffffff01809fb56cb286108809fe8a45da8ecb8747e103ff40bded471dbf54df732bb2f950d95f2cc9d742fd0836bdda63567509d3294250e4f978e1c7076fea45c1020dedba05577ad2a0081e8b6300f3ae21e7b794d100ca97e741f626cf7da77034153ae5acc40d814a6901cabb1fd0412a8c73a442709750465640fb165f706d8c7914de5df87b3feae96cdfff01f747a07614557fc0907e8dab4b392f3e1f365c99baad1f6907e2936431d7dce6d87340abab56662ee46d27bf24f3cf6d207df40515dd1f62faf14a04bb0b77e44e12e0d3f23f05d9167375d248ba6391e2e6da930f09f8a2b6468d1dc1ffae55de3576fd899dc66f601bed38754713be513805238c27ed6e4532c258852077c8555ef0f50300065e0af7ced20920e334926285b580a67a8b8138c56a3b76bfbd4ec84bee00000019ffffff0119ffffff01b71a656999b7e3c5a9ae886f4ccc0b0ee89c7335303155450f1c0c2305d28ef92c779eefaf495aecfea7f1a7798e4f0e3e317313cf7df4591785e9bed2bc780d51891f5ec5fa2278f4fc11c351b6806789e40ecf3a0af4d68246f45158defa550f22ed89cf3ab06f003deca21f3e219fe954a453e9c6b72ab0f5e336716545b218bd9578f6cab1409ec52e38efd9bb4816e22b2e0ae257f9908711c83ddb5ad951837feb4f76c7150ce29d2eaece144d80015fd327e3a1073f56a215bb3326ffb9abf0be02f220484244c52afe402d0d55e24a1cfdc8a3100fdf2624e964268809bfc9ebe96a6d90d127709973c7ee70f548ab0514160e2281bfbf2dd20dea6f402bb7f6b0a7c3633851c0f13c8d30b60300067ff5b4165d7555ad6ccc0ce666026f21ccce5cc6d9f9038a910a78023a00000019ffffff0119ffffff01abe070ced1f19a7dc991dd112f4c42db8cdf42047fe828d774f52eef4c32efb01bcbb0dbc7eaea9daebacbd96f0ebf6d4a20afb9fae2599ece3ca90e5a854b33d47e154f6e24c95527035407804ebacda7e1c55feee95a90105d4896ccda6d827ae485be54d0abf336e63a830ff114752fb1901f01303e9785072ab66f2106b802bb906197707cf4eac4424883e355bee8b39afeb1140200877942997f2c2bfa038b5cf8987e52008346bdb014a2062a9770c7229405ccbfaee97cd37c801bcceeaf6bd03c8b726d1d820a383be07a8ff860185a4367ca734dd1abfdb893b9401712697f0454c858c65e839172f07f437e5d8d6aa993151aafad92679eb681af0cf94eaed35c2d16f8ec7231ca6c826d03000680d118b1d85d79d68c7920943aa776aa4a857e331d1720d3d77875cd2900000019ffffff0119ffffff0193f38c3c43dffea6714285deee5d7219d9267251ffc6ec7b8c2e436768414cc7ca8bf3e65a593dfd48ff4713bcfdefd9f9a69ddb8f87de3e86c2ada45dc49c158bd56e7716f89b380a0839b398668ba0942f2eb60f027d5763cdf6e3c88126ca23d3a7ef63869f61d88d2f7ded93757cc6308832092c8cbf7be4e5f96e520a6e0de8d87413271d5af53f406e4ae658e224a0f372a2db5e4d0d1b20b719213c1b3892ee59ca27a5415a72c04dde6a5e54b16a8c12ccd282ccde0ac8d2be88abe57677307a2e2bead9c0a226e67a695e413e84604cdff3314db86100afad615b901682b74ec207931e54d39eaf5ff92be212e190e5df93d93757bc835c03a86268e7b3690cd2fe0f0d8485e8f8510f516a0300068bcc929986c96aa428ac63ce4bd25520244172325ef4e28759aa95351700000019ffffff0119ffffff01965611a52cf7e6616fe1dbf615ab690300dc7794c0293c1ab520392fb76f8a887499e7e15c3c703891022c35ffa20a9ec18c4f0db13df25488ecc103fc1c2598e1ed87fb3657f52698f948a9d1c6b5e8ad98f7720e4230da8a5557f3681a5e558b19d9119dddb62786530300c331aeab262789f60324a20f764093a27977fe851987f93cc9edabd877820d359d4d89afa59af00188d174195afe54360fec17c3433f70088c0c4a7bd6e3ea7912400a3894a8423b470f2131bba3d7a5460bd860ceb4edffb56f6a275d58edba53a9b15a95b58995134ed110aba66718ad7c01b716e6aebe1943e1caa610add6353e3a27a157fb92170012104f35af37e91fd568100563d9a8d681a10827441171c94cc20300069c6ca671212d00f182ee9e294d196cb0e82c165bdbe6cd3ccc29aa796100000019ffffff0119ffffff0198bd2f3e17d3093230b2b2e39e1f2ef08504d8737de0016f58d92c9bf4364af0d465cc6efe95b2d29ce17769e8062d65ed81eb6f48ec940fbe85c8a191c8fa76348628451cb98d509abb6bbd7c276c54a96b07623593182dbcb9d00a24cbb1ee811fc6b960dacda93488d80dce1131c4e2d35ae552d0ab14995ffed4778a10f21841fa81b1a5be872c0cc99250748d15474f33f1bb45f7971b53dde12a7b44a1cdd44aacef57520a0c92962264aeca8f8668911f6c20c487803bebdf141d4ee7735094804e88aff1d5a3b281fe404635323bf711279d423c09b27720b22d601b0dc3c6de7f5ba2c3189df118ba5d4696a2ebba667e628a5608852f28086534400cac4aa24cfe90a7364df5c875d84855030006a6162152b073de1d717d209a771a22712a64f3f043aaf3a344f47950b200000019ffffff0119ffffff01a2a9a8ba6cd66d2611f673a1505b0a3a067cae5ce7fc7e3049639c03ea1b7bee458e51586ff210d3bf79fe3d0bf1f644511b2743187ddfd558e66f2e3d208a61699d5ab43df0de92116decbcb1566681b56033bb2e49a4ad70c72f52acccb212f732ef7615624ddb0417b7eaa624360ff1d0a15f8eb4db2d85a606088d56499417be9114f284ba8933bf4b480fb9df9201fb3c20fc014bd21ffe3259a4227e54af1c9c71512fa0d4f187f4aa4ee263b28ffd57e746c8dee3ab23e55ee34fef17db8220fe5682aeff867d4565bc743a5278e4b49bae58cb08ef74b2036cb2c0710d74b27b4368529c8c97a93829257ad1911dd6ea87b8de6666901bb9292162024947832763feda1fc275642a91b422f4030006a7240ddbb3b7d50ba386ca5e65e4a3250cd4147dfa43b2c77244e10d9700000019ffffff0119ffffff01a56297148884dd919e0f2c7b881368e7fb3480194fc1d32db52e5768dd65e6b5087d8546ce0d6934f5464a28f4d772c6d544c35f8c90850d34603c808c171137acfaa6b2db73e5003124ebe16d3ffc35a29f9bd3d5b7f9980183d0c780200cc9ae5b948e08fe2c7c4cdcc9bfc21e173c7f9cf267cc3f07158633ebcd86354b9915745e33e2ef6c24de59c605d7546982964a4614d59d9e1562285547b3a3dd2cd5f02b1e8bf3782603d85522e7ba23f485d34e2e7d3f1b9e36168e2711384539df5e7fdf75556c35cde2050a449881956d1001c0e9759569f7545b537b2b1fb70921600b87594bac7dbd5170ebf7086761be0ae783c0d091018deeab067435415dcc77348dbbeae4624e303924118bf7030006b1bbec44fcb93a84e4950fbee8d03fcd672174846bd21b1302d65a302300000019ffffff0119ffffff0193719ed30073329ab7cc3c46fed6c4913c7caf6a2b7417e5115a2852e05bdfc092794c7da84bd82bab234301ff6f32358c1bbdea7a4eff0091b03069560d235a800351e084dbaae86335173b8b42a42598e20581a651595e701d5c80ac81144732a6b82110e57743a74855a799c2bfbd3cc72f423b305bd94e3fe3bdfebce15d05c042ef92441e1e280c82eb676caac4260d6a92981014e4509e61c46dab09863777614cceec47069328aab3877283bbb353d6660dbba034dd3e0941a2f1812aa0158dee0344617bab92c63925b9263a864eef775773cab086c1e790df01050001e635613f31de12e249e07ed3e2e55f6c485123fa1cf5665bcc7fa78784c785536d29a6131731c1dcf1084f72e4fb63030006b34fa2b77c3fc270abcf3bedd235995315c0fae0ef521c9aea9b21ac5900000019ffffff0119ffffff018795d7beb371df668268543759323f1227f135b9a5ca7b1e3e84f114293a3ee261266a7c0fe353e1fde37129ac7d8efdc11aef282557b97f4d776928f9cfcee1a879691f5afbbb015f10f2152eb3ab84b956347dfdef7995d24fdbb4422e95a5383fe09256b96b167854d498979b06318ccdaacac09231071d4442a7c35b106417946d398efa060adf794a0658d74d172054b60b405379e5d0645bfe7457f42df2dd95d46a9e6f383d6f23f83daaf1ec928d1953ee631c6f098ee37f2e174ab38e574f2ea3f0575b0d348cb46ee76db81b71e4a80c9855bd689ab4d8ecd9ac73189892af92a024da343aafb40a8966eacdaedf659e3cc94176ace322c6fde3479608388b2fff652073b3be40dbb3dce9030006dc04d3a653a6b56f66b52aa3541a718b386d0fd43ada47962bbd010b0a01000019ffffff0119ffffff01931f528252b01c37e4f5b14c0ca4fbff175a2899fe210c17a8041d46f6225aba40cb152c0cd63f058a0be2b373f50178ff25b08121a38dcd6da31d931758522174cbbf63ed997e39d547629e4be2a58eb5e8241d9a329c9941288e2c49df5a62a64b85dd0efde701eedc50478d61d731af403b996b8da8a4a8f308f269cc6b1e07641707d463e0580faf27bd5863d95c73620216d71a426783bcbf10dd9aff77e53f62f75cef88ccf2c8e906c47551f9a46ca51237138f548d80b7b893bd3477274e78310c31b9904869b69674bf7b12335c7d7809f0f67597c139b0d5d2d7b417a82b3ba41e9e568588b9458fcd331bc0ef7eca683d3b37763c3400eaa3dfd18a56cb536b63da7453a0b1f3df5bc6f8030006dec9605a6e6043753620d476d1919bf586042ae0706f2d902999346a3201000019ffffff0119ffffff0185c6ab76a03e128da694c569c9d452f3ddc40e55040e646563a5c2742c41b8950dea90ef496fb7b8e150963d01cbd6ffd4d795e94490624e323f695d9543eaf28b4b8c4229e5845f8f3709e3a1fc90e5a8c14d2b9a24525fc49f22f92c01e06a1e3217af0d94636852ac759d5a06f30c7e6924ef65e2807ea775e9d79b77a95b0328e643755a93a9b0ec6f2b0eb462805326d970fe22d74a954f25b02ec036631f314add70b38b294e0ae30a3761e9a3a6b03d96e193f262d9d2403eaf27278518a82ff891e1ed22a46496747dd173ee83b03f262e1ff7eb87de0eedf6030edb0bd9c37dfad0e1d1d1f956e1065e8369bdefaeefeec4efb6216207dce3a5c936fa95a6b92cb1fdc6d0f6b8f373629e1d030006df63c7ba777c253c097853ad177f36930345d366a6d745848da4b1b98b00000019ffffff0119ffffff018d92ba709cb3a16c7bae070c2734a6d46c1d6128d37dd7ef8f0eac93540d46552a33c3253e0d8e482d3fda33422a5ac89e57b5a2a01ed0b8417705dc76e210ba35b87c9d81ad0e2782e924adee2adaaba004b1531a4d0d294ebc9885000b8a1530a41c7988b0c10afb9b2c3ac2fc63d20d66ea8384847f658915473024ea49a703d03ce1f324764653cd54599b21f088ab0cd2d5714ee55d170c84adbca7381393d12d11f71b1d6d3dc08922b332fb35890819448aea3dbb26cb801e2e10969cca45bea123a197b14c7ebe57cdf8e343f32c215ea1e56f9088c3d366155a8a100d716240fbbcbe6c48b7600736ca1d001fd06e2db283a9e1acbc5aa6586238102eaca89ba6f0b320d34a64f391ba73c9030006dfdd407ea3ffae260328deb31a84c8395089c5f3078c7545c0175f224c00000019ffffff0119ffffff01b91fdf630d9a7cbcf25b09989cccdf94fa2f9103a81a681f9745f031491d233e16377b5fd1090c7b73fa56d7988c23e2bb8bd828af6caf33388c1b60c3d3f194addaa38ae1f757d9ef59c9e6b05866e782d52247f932eb8701f0d5b24c0e3222b3c9c45dff8bffb40a208ce817cd83ca6a9aa0918bead1bee534613a696a51490a4bdfb6f82bb5469e5cf3d5514d8722aab656c8d4f119e743e74c33d9e4a98d282a4fc55f6b69a498c6c5b6a12055ec944bf03824bc274f33ea00c5537d86abeb9b248e50a9de3822ce974added733bd20697137743472021535fa141b835401588876de9a3ffcd79db03fecc8f2bdcff90eb766cac311c6208c2ebf97e24ea23e025a8aa3996e02e095bd64619ac3e030006f1c29d792e05e96a6928cc58d6ca2b1e6525eb361e8984a6877c0968b500000019ffffff0119ffffff01b3b715ef58d33cf815869464852e02718f7cc33d73c2a1d8f4bcb31539a7a2794868d479078949bb1e3aa9ec7a092969f37fa848741e1e1b9f122eed263559c5b78c3c912bc03d3d228950e3eaa7de63af0fef6a58687bf692c33d113813b1c2cca971af14b1c287bebf105da0eeaac8bf3978c35744f40590c204b03a80395409cc1e2c3fd679fc49fda47bfdf04f2ac2bfc69bcd5e53e101b143667f352006705dfa46435c3bac205c041f15b008ea894a19e3320a02caf5f560e9f3a6f616c8fa4b1b967fda4b1f262bd907cf18a9c3771efac5a165e5cc0fe06a48b98d47159c1d1a4b758b27b07ff5449ff3ac160ebd59c7674c5c519109d0d812bcff56476a4e00bca36300033c2e1e348760f8030006fa32ae75020e4ff7dd2626630c49ce57cb6aa4bb53345aa4ff7325685900000019ffffff0119ffffff01b9ce2855f72f5ec46287fa3e14278a0613647a41c099a19e1d178bfb7738d5a0f308024523d2de6b93fd20d6b390e296121d6c8fadbba9ae160b742496cb854d54d61ff9e37abfb16cae49df0e8270a78aadb3e42b1319dd37ea2dcf4118105bf2f5f668f4a82e2da84d20a5a6241c65668bd719d2d9da06e7ec379104b54bc3172be2bd458d494f798fae73f55cca5da85ea46dc1b6f8d8ea079ac62ee5a888171f0a8b893f7343ba349f2a0130e989945073c973ddc631b929e4eb1f1ad91af52281a69b6d360982384718d8d4e3e6e8780334777d58ef9d6a4b2f5db022ae09fffe076523279b5c82bf04cdf92c00bb004032611fb743234b88a2683ed7bec7d6038d27cff5ced9ef19823a22f8be030006fb626dfbe19d25e37267c321776a482ded3e172216268ebb87183fb9e200000019ffffff0119ffffff018daf120606278202197f203241fb81abf2f6cb9ab1d6ef60ae6b6bf5135df80a53f9882dfc82d2409cee3c6d7bedca2a625544298336a8b6f5c0f07a35381a4797f4cd3b5ca4ae24f6fe636b8b208676b335a32f516d4f5b2e608b89bc0b2c955e27641983aaf97c3a96ed87f9d0323933e595033d2def94d124e291ca24fb7d0b097f72d00cb8c071057805ce6a367cba8c91f279c9af305c04345809c9c5c7a097872213ded5c6cc6599c353fcc83bac305ce5f8b783495f05c1b40eae213da84a0f4cff9108730560a67e6849cd3230d8fb4eb8453d5251c65622494686790e248f577605db57aaee238b2c6f822f9677d804e9eb42fd8fdc5c89d4523375e7b024f4166371eab3ff4acb06ca928219afbc9b3fd325d3efb9fac4f8fd7aa4e880ca2dac6e4002c42a1e5dd93c4ad44e85f988aa9e0cb8ddd5c298fc9879491610734a311c59558a0581a4bfc9f0ea249f286db17049119a6288261ce8a67bdc06458a19ffa4236490e487e6ce2350490301001e005600af63eeb08a205edd825bbf1871e8c741347341ab7c4656a01d2b8239474fe184c951dc6452fdd399271299715ef9082e18273aaa2250a84b8f24ddded105f90f07fdf0e84e9cc1d7f8755dfabd503388565942f3fb6947a1fde7b584ae064a2003160033006b008bfa831fd4c999453aa04819c796964f89bcde2701d9737071263652b874b38fc8d7e6210bc60b3f5c71882408ffaf4b015a75ec6c473041d2fb24b07c8dc96836da2ded8a3502cfb2500de48bfc1b9b2844f8afdf1a2a5f861a6d5f6780ceb90302001f005700a0626e00afa70022afcb83714ef4ac686c91a2abaa22b1c83677bf20ab2f08b8a020d803b05066907716847bf1605102006d65076418b524c87ea0f033a80383b3729ed23a103076353cc8007804b382d2bfc5a870c90f9b46e3c951cb9f1716030d002a006200b5cc6b4981591c92e37c50976bb825dd50b0fa594b4e456a8a37e8d8cda81d70dacaeae8e47c1eadffc9ca42b2bccf3a0f328b1707cd125e9aeb04d0ea4fba353a056ce182cd64bd7d410dc89cab39a886e625ee9d372e5c40560bed803ba9920310002d006500adcdd2200c2c6880dc960872060dc5def1ff0871fd639c85d3f6553c739eca667015f5bba3fe279421b779e5126cc921080d01d4c9581d39ca14163364d31a9ed4552064b9f35ee561a5526636423455e6d5b919ddaf853b9dbeac0c628363fe03140031006900864bdf773dbedbc0bd95eb08593d164b6be7cc6b501729fb512a9f91619fec30c65814f888a4aa70810a157438e734030421357c9a13ba27de58d68557c39dc36c1316e4a5aa875d1b3344b3b785462de823878aaf1387dc85388a55373818f90303002000580096d4522a9ca9675804e8536e92f5b3354c7a3886e60be3c7f0550dee0866a89ad863d056d6593aa7cf4e5c627272e90a05b2cb775435392b3ef26fa4abdd31446bf8932df36cdc84e8d2989d63110c81f642fba1b72aaf2c787d36953c9d350403170034006c00aea835e8d60c3075981788526e5ff2d0b01d418c3a743dd454918e00c671bec4755b55e461a6152e472ee7636d0cd3340c7bf264f16dc5c4bb3600c249f41d1a089160bdd41e21b45334414d482f028e72d09c6501e4f560661c570b02b58fed03090026005e00b6745c746cd55c9181837345848e5af6a5b4d2e83878f3cd8edcac3c20e2d4f655085888180894b6de2c2a09e2d65e4215d4256915823629aac46208f78c4fd34043a9bd1d90155e360ac9110415beccd326236d0d88978790ac71b386e10e7d0300001d005500ada499cbeca1b2bdb869e5bedddfe453ae7c67b6285c9ad92e78acea7e2bd905e53410788d7c7434cf5fee48b984126c0e5187388893fba0faba66025f7e946277bde88f18e0d7bf9ef80420c4719ca34423e527d64888f6d6ba354ed1ceab1703040021005900b01172eda03420ab5985fcb60c540a0f5b55536ab3393d40afc20890cebe4bb54480c803d6be955ea70bc271942c85740d5305370023b669b39f9de576123e3a6ec6bfe7186c1715247705e3661cf63ea47c29378f1528b6bdfa6d77fb62b31203060023005b00a84a420431a947c664abd8dbca55b36ee732c5b56676e711e7a57006b104b1d3556f8e04a9ceda0942bdd21d06603b3406435ac9224c87fb718f579896616dbdb75f7d4686e03d5e0f804bc7af1ee1f156bdc211dafc7016476107c501d08722230f002c00350036003700380039003a003b003c003d003e003f0040004100420043004400450046004700480049004a004b004c004d004e004f00500051005200530054006400b90874d09afd2163909f5964b05d525a30b25e50cf3ee186cc08d1a79bac1876f0a289cac4c0e3d9af978f1d63f6e7f8120178c9efbdc040323c54a067527078eefc175343caf5d5644c9dc7eb899d921113ab622fd277774448b6062059a985030c0029006100b3f9f9542fec225b243ba35407e641301a05981a8378a3e8b6af7e2e6619aa902fa20682f61f64e22f9374d2c724acd805da1ffc25202d233c29ad84543f85869cd53f36461db609ca215601ed10b24fdc6b367706ee9cb91368baaf646c5a4c03050022005a0090c5c77d3e5c73110da4c47993377f81b3da1cb59a3ba9321b66e7b74cf99ca1a2fe986798524559060e13303fada84b1881bba5ea0fa243e75aa9751a7ba1117a1b056086ffb2c22b43971c7484c5f03393585868bd56ea4142451b7ab9039c030a0027005f00940e1c61961954618d9765cf9a5133b7de33376e2ef92d267ea2af6daed06f97e58ab7dcfaef38b3c00c422b2984ac0f04c68a06ac7846328ee1a1bd6227295747bf5d1da93c143dc86258bd0d836dbd3dc46ce103a0f35efe42e5ce60be88db0312002f0067008d418d73787edfc2df52dfbac0b299070db617062ebd36a48eb7e341446794a544b9f6c490ef7b1240993cdf8c98517915f81d2347243c4e8503a43bebe521692349635a8ffc036c1723d38b652d40ec0a58805aa59a011121166d26082538bd030b002800600097df377019a04fe4f902347bab0d3a202ad15a6c7e3de4665f34fa6eb0e025144f8a92f51e7f7d94a14794e1458a32d5192df079c8ecc9dbddd73ff4c29f6914b17bad9a19148e283d9508e672358b7358b1bab043d7673dac4b654f6dd9625e030e002b006300b9c9bfc33e829da7ca59be3e17b7130bc9290c77a77faff0e3ae9343ec2ad966f70f238ce83de39f0bccfedbdac9e6c30d11c56153b9887433c3bd9c9cc5368c10fb5d47779fe27462d5377097dd4671cea5c30c20f4923113fb74f4ce351fa003150032006a0089d6be374e16cf79e13244efc85c429ed49d22471c2d0e839714d71ad37adc8cf6dce58f9d8b76cfeb57e639e650ccb70d2fa887c2592138850bde30502949c1b93f9dcae9cfa4358fc1f307365839358c498eb40aaf28457526dd124293ed8f03070024005c00a2a5c3c3097961ab2511e3dd8675e5a077de09fc35e144a891e0c8a5770efd4c1bbe6ed359a6c4af1c4322858b2f7e771747de86c5f05a7dfe247496b301855938dcd7edcdd3164182faf6bef72a84e6a7de1fc7d31283f9e1469516f349b7fa03080025005d00a7b933c916e2e45620220f872984b0d740e5c59ba2dda83e22ee265043dfc145bdc96188529b9a97f1de3f21a0319b30177645129969fe1412a834785de527451a575a609fbd3e03a7deed383bff32f3c78fef5dcffe72d137fd66a33606f5760311002e00660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005180019001a001b001c0097b1f027beaf26847ad0625b1a8e3d6fc378a10a2ffb2dfb27cb10ea4a263161ac7e918963a90af61ba7744aa4040a2615720c0bfee2abcb161f4d3362f3681b0417e4c1498f51aa75ee8253fc35819b6db2b7a482d951ff9a8ff6e0bcf8f26a03130030006800" + }, + "testnetIncremental": { + "network": "testnet", + "baseHeight": 1580498, + "height": 1580546, + "blockHeader": "00000020ce906c3cf652cca824e1bf48ae6b8787b237e08a7aa10a73f13cf6816100000034dba0e0f3c64a53dd7cd9eac856c060a115e347ea932cfe26988aab145c4c018bf6e06a7440011efd730100", + "hex": "010083c1bfb8bdac446221e973c3f520626ddd4a81fec93a59aef14830b57d00000053138b0dc2d6741f84c987a1d72bb3e1919ba4f53bffed03c52eae492200000010000000052a75349b6ef9c7315e77921c16e9700427c18d23bdd3cc5e3c94625cf848c7ede89d8155f6ba7352def42e555a55345067b7f312c3ebecf26145b3bf00b95d70df947e7c096ad5a62b3fbe48fdb29e34e1223cfb98c075b4b01379cd90eb4008774cdb0e6f3d4d3d2958cf4e5c517648601a978d3fd5db92830ccde46fc3419118e7a90b7802d63e0ee5bb517f7f0cb228a1dbec7ccc059cfd197914b3af256b021f0003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff03b4558c03000000001976a914b489115851ca07a26a5ad8bac3cec3c7dbebd83188ac2ed5fd0300000000016aed2ba706000000001976a91464f2b2b84f62d68a2cd7f7f5fb2b5aa75ef716d788ac00000000cf0400021e1800e852020514f1c94ec6ac3c55ce6a2cb8f1199f5391ca8d22be31dd13b12adc04b450dfdb484f142dd2813496d173d9881e3751fdf1c7426aa35d88245120f5f00086bea9f98bc98ad1afade072d3de909a15edf5adf638fdc945763a28d92838f69d3f1bc5f571698df7ec056461ea054e07ad279e13365ff2c12a1d45ce25db738caf3dd80a4d1dac4ccf91383d6023817f70c125a3ab4bfd9c776e9106a1a48e0078e6115b22000000000000000000000000000000000000000000000000000000000000000000000000060132aff469aa0f661590ffd786cb8ca4fc0284e575d5ebb112d391ccfe2b010000015ac508113c51d7fd75e527bcf4c0b44a20f41ab9df669af607e603f9a50000000432aff469aa0f661590ffd786cb8ca4fc0284e575d5ebb112d391ccfe2b010000045ac508113c51d7fd75e527bcf4c0b44a20f41ab9df669af607e603f9a50000000632aff469aa0f661590ffd786cb8ca4fc0284e575d5ebb112d391ccfe2b010000065ac508113c51d7fd75e527bcf4c0b44a20f41ab9df669af607e603f9a5000000060300014ee749ed0627862372727691b516618860ea7f13e6f0051e24ece57c4500000032ffffffffffff0332ffffffffffff03b3a62fdf54f89ea23cfc87d360a05e8899ad4f2214cbfa46cd2984d7ec6306f8b7e35d57f4d6fbafe001b9841b44a4a94a5644625f8034f5f2b86caaf2111b8c262b68cbef4dcd25145d1685047dea0fa6a2683d2ace8bfded54c8b62690a75cbc6c134bd70b29a64119297f3a6e4cef7dba3d30a14890174d4c354e6d9eed520beec35ce54bceba534b9ed709f7a95293353da2b8fa8ca7a22f432866d1cf55dcf61823050be5bc56718b78a62ed260b0b101aac8d37a09cbb128eea79cde5b746baed4a4680a79a4b9255dfb847a68132bd1b4f73db7a1cb3f0314ea82391206245c2dab21f159d8ae82071b64888ebc352c9ee1341a5707b77664322ea15aceb3ba7dd6877a0ace0d5bcc6a84360e03000195f052d987615a3c2ec2313870aa85e619584a22ca37af03e57406721800000032ffffffffffff0332ffffffffffff038ea83d362979c034aeccdcddde5dafb182e7454a868bd81c2a5ff6194d9ba12070ec4c4115e63f22de05fa49a0df304e37d61ba160ccbe91c770b3b04ca057d2c324e29e9626f1482a555c56a7b9a4288ca3cc9519c678a029be5af2e8f99d8974894b88dbcb1225229a376ed5f26c1cc49f37d4eb347a5003bb46dba422388316083f36dcec7ddf47b79591363de8bb53beb07e78de4cb8ae37ec70967e3f0f1fe9124e9dea47a12bc50ca05c3d9bef88709f8b81b66ba1abf0e525ca418bad876b14bf128b559a0220a78bb06973a0311506d8e05bd82d446e4b9a719a36c1192e5df53ae9ba417386923600a65800351445f3caa56d024c67b2eeaa0e452ef7a061cde7d81278ef49488ec3accfbc0300044ee749ed0627862372727691b516618860ea7f13e6f0051e24ece57c4500000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fb7aa306ad4de5f6a3e68908c0732f7208f4405e071f7b85c509fe1d305f79e6e8d3c441123414ee392e93e2f7d7202095810ec15caca1f2d7ee4f6fd3516a756d2405e71be5f6661da8e2ffec0844128a08230c01fe3e97d25e6136066a1739d45c108b03c3cfec33dec510857e6004d439e55e15fadf10d29d6c28cef68e5d817f0be12e43f245016c4689ab3cf8198c69b72d1a3a0e984602578aa44d588180ed88927296b5b7b8ef96e7e88fdd5ee8ea6809477cbddd0576855bedc0329f110df976fc17067d3f42d6d281b97f98eb1cdf03de480fe2e94244495f141547f032164ab19dbeb09aed1691e41ef64b832b398c2dcf40a3677aebf347326d4c1b9e108866d13e9a006b1a0adcb142b4703000495f052d987615a3c2ec2313870aa85e619584a22ca37af03e57406721800000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f8150b7be6c6f3e49579ae0a13234991de8bf081bae7ab6f396a0318c82d4d1d84f0132110e38421b9734a0fa50cd3b2f9dc224cd81810732ab9718cd1e7dc45c6ede9b3a744358e3fa69beae67f40559a608aafb6fb6b372693ae049bd017bae1810729ce9bc61f6e82198469fffb2994c1ce0494c88af057ea08e6f9cd79676174904e8b6c29abdbc8cbc4a715a58e50848e8b9895f670d4d609c653f58d284a0dcc55e63a9f217a61c0160c7ea977ab0eb9ae53de821c79c0ccff94ae6d0991c781e6fddc149e1638ffb3d188c6459c0df36f08bb90a061aa1eb47b4e151c7070cb0def81398529f50272ee00d22a1b3d42af14e455ebd73b554cdf9976449c182ef49f555ba0cfe2439cf64092e140300064ee749ed0627862372727691b516618860ea7f13e6f0051e24ece57c4500000019ffffff0119ffffff018acb7c4dacb67e55e9f9763487d9ef8e9018100241908e6af147fde914a2504c6780c3bcf38fa21ff1a20b7643eec2c3460111b52166e23dd408b12492695df0e0e9caad42b4e9f47b42d34112ef1ba39939912f9395205dbc9c3ad5cf70e2902281286edf5a5e5cd457d0d4a8bfc36194b923bc6c643f1f5800361927bf3aa80540fe6d9295f442ba3e3b1625601c1b220b661975b6e942cd8b1ca37a2580cfa695aefa3c882fd05c32f6144ffb484c99c04cb57e16de50b5223246ba4a73297b2feb52e371f0844e7ec3e0c71a5d9b4429ae2eb0bc9d15841c7a100f853a6c018ad3f63084b6964c6e701bd8756ba6449b697ba8d18f920360348dc9b6e2d3cf9844ee1a5ad772226014e9fc48300903000695f052d987615a3c2ec2313870aa85e619584a22ca37af03e57406721800000019ffffff0119ffffff0185edcac2658181751a7c7dda7f7f20500001dab2d66be222879cc0b276acdd8c0505cc04c91f6d94693ba2cbd892867efd1f9a70f8724697ba034f04de438879b8280e9b3f2ef3a9e39eaab0db5afc81b792349ebc8ed113bf7c385a5339addf8d780c3af845efb9d8c1b106afc4388a533f436a515b3c36e6f1449f5b36cf021265654162dff287edd22407dee0e59dd4c5cde6e8d9afef546a6eb8def59fef64a26b6d864094e95db168dba0554efe8cc8853d4e2d21798fa0288f71d832edca235c6fd90637b1981232c6468d8a8473177bdae62bd7d5fbfa60e7303e53200c1657d12c06da7ed46a45766071b4ea6e3aaab28d5370eca43e4fd6f051d393877608493841bca2f83213417474c11802b0076d8e6873c978eda78da2041f95b1ae4e9db9917daa9b973aeaeaa025d0cba8a707d5df3160792df1277533f0391b14dacc0aac49b3fc1e4b8e5b2060bb5f15cca2de27fcf5bdeb3c4db49d3a81303a7c39e7d3221f6da8ed00e4866ff4bd03010003000500a464e1e1584e30b735fc88872e220e09753969f89de5de0261060bc3a04acb0bca0b23e6d99f6670d7710692eaa148c51172a4e30c23b8f6da23badda303a9860ee20200fe40862b332df2331b284a4b85d36f90bf96e1886d83794f2cd1f2b903000002000400" + }, + "mainnetIncremental": { + "network": "mainnet", + "baseHeight": 2487500, + "height": 2487600, + "blockHeader": "00000020b2b085d903bbd3ce02e891ebe973d68997b9eb40b4071957220000000000000098ed71015ebf7de8d25406091a93bd0fdd3cbf7befe3ab19301ddc9c9ab32a1be8002d6aea9a3d196877bb36", + "hex": "010092b4ba5d0c744c1923f865ae94b4f7d386069e212728e49f110000000000000020d5693eeea5e42c0ce2029d20fcc587f7d0b2fec9fd1c4137000000000000000f0000000529a03bb5518bcdcab201ea2ba4e920426727713e4432bad91b34e73c101c563c1a70571bc4c1772a7ea6ee71e6c24667bb6d66e94946181f26aea727b043adec9cfb32895a9eda013ef37cee73ec2757da5bb12b3c59eb1197fd842a5fa34c189aac126e6c8e63e40a5651b1f32c1ca08326970ca9d72f58305ab41e095046795df2237b2fa16630987021c20439639143942a8bee20ca659c1022be86c67737021f0003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff2f0330f525192f5669614254432f4d696e65642062792079616e666c69702f104a04c900539552e0d22cb4ca52340200ffffffff03706ba302000000001976a914bc89d6071dabc5b4494d303af761a052a5c70d5788acbbb2f70200000000016a928ff204000000001976a9140818d69f7432325c262016eb0a38805c63ef0d1788ac00000000af030030f52500b1269d9ab59a6f8ed4ab58e1bac40574a0f69a847a63ecb099c4f8e4b92de754afd10efa141d712ce45a5844e2f37c6e3157a628b4e0db0f33b9b78cfe875e0f00b25ef25f406262be06799112d5d21625e6c63a225e899e462b6205d1aff14793c12cca831e0db62fd1e2d7502da38a0a0d1e6b45ccb82721d957766151c3329460c8f8ae6f6171dd2e5843bbf5cca9abc285dc38cd0fbc16ddb751dbdabb6b3f4b53ed355502000000070100c387202431f526002c2cb42a867580cf5a762dde21915f9ae8d66442238cf38ba9c4a7b037b73d2704f2ffa0211184535e1cdf944cf975410e0000000000000000000000000000000000ffff93b69280270f0af741440fbb7382b47b31433be23a911b41afe40a1fb8c9ebe22c6946b370d351e5cfa1db891a8a9f749e675c56f61e501bd5182e68eae0700c5a4f4e242399b649363a0101008b52e1a916cb1fcaa88b0802db3b856195ad65aa8f95611f3792a6f92fb5c90d76f738a69bdbd0d34720bc8140edcd1a49099e4805b8f482030000000000000000000000000000000000ffffa5161414270f0685bbd73cee500698c31f79fe287be2b14def46499dd8edede28534b60a1b9b17578b13fca6bd51a76e0520b42b3c0e4f52812becc0c3226f5c2d7a33f86fae7d86b724010100cd10332e29250b009b81be26bf842562f8bd0d3200f477da9e0b3200a332a23dcdc19c042a7ad5a421f5164ca0c4605266dfe87ed645949a120000000000000000000000000000000000ffff8b3bf3c2270f08cb0aa4bd876acd9865ac68224e309b48935f4e942366f1f2b0dff30cf2b03dcb26109996707604542db2cccb68c4f861393d8e7cb0576fc843cb1d3fed90b3789e2f8801020012e4aa37b03d35220c3da9e886ade81736d264aa1cce51025792746a179e6444ad7bcaecfb043c8f9950beb84880c398087bc135f81c97bb2b0000000000000000000000000000000000ffffbc28af46270f870406629d602b0696eb6d6c992e25bfef59fde8883e08ed44e9ada214fee43ce4dd57c4a9ca112c258dbda16e7712dd6995f73efd66ec646cea537bfefcdcb06ca9edc5010000010092d49ab558c9785c5bc1495d56a224dccc514a3be251b8622b8b5b5de492f61a37664cdd13ca52f96c6b7c96f722f7a52107c4463984cb47040000000000000000000000000000000000ffff8a449b17270f9699bbb4ba24b6ad92e8816112baa87ab4638bac785fc234e9de82312fc85dd80bcd91debfb5b43cd42f4e9e53320ed2ce70e0d9a323d99a9210f11340d7c4a242273a7401010055a4116977946b3956febda9c287ada48004d21f9159c65a884e7655fbe5c5ae0154fbb8c74a3b8dff418678e53bb4a5243df4b9fbd1d0e1130000000000000000000000000000000000ffffae8a0e63270f0090b8a58c3f1e3d2b9dc4530c06f3f7f96431d5dbc8acd40f7cdd501e6a520317162fe898f9ff0e44ee2d57643819f759fed6102c73874bbc81a81a0a49a938c7a39372010100d79b39521820e06b784ad7b1fcd43ffd438e77f393036d806a4415f8293315b3c733aebd97dc38f9052e0251dd2c9615b2ced05278692049120000000000000000000000000000000000ffffa5163ef5270f10a65e71faae33d9d167b20d271f9d5a50bea0a711b2601b1e40960e7197c4b3cf4abe760893c16d8e4f0cd6d10571b05d8d06b4534405df32018239781e56f5d57a87d2010404092284e2ba1799aaaddd2a46e2602843fda0d9e1d9a931e60a000000000000000490af0f8bffaedb14c4dc1a17917dce8aa1067d4cc69c49e3050000000000000004cfb5f111070ced4dd1cebf81b7f45e6ed6cfbf030e44412e020000000000000004eab4ac04a0fb4d528ef5b730c9f19101e06762247d3752ee0100000000000000040300041040dd149ed413e12f163986901cb3d5d7bb64e1a1cf9e82130000000000000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fa43b80a7e1e46cdc3c03901f8e3d0877bf0fbc673db27c89f5767a948c30d2ea299e222763ffe32af7c2a00108f2981e2c913f973f5b653fb86f4aba995580dbaaa022ee38eacca5097decf9f57c167bacf6b79cdf8746657fe07e3e9ab94ab90852c4297d9cf0c0b1c4b43cdc7a6276a3ae077fc0f375912156fe42f4102bf612edddc017c0136cce810dc58b38c188fe5c76f66cef99b94b7254e5a36b402c37921000ca3ee478ab6abe4c6992fcab960c92e7a8b491c36789469d24c7fd5de09ae87aa1ea2fb22a65eb503febbc1fa7488f7cfc1bdca75d382ec39897ba8205e16eab74003f00e2ecf7eface6f9806ac5f9d9a1bbcb7cef1ce84551f0b78604e8af1161be207e2c79a581b6f123870300042f5975eb83c020da78288341ce75e1a6c798e4da13fe54d31e0000000000000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f842d68a17c80073ea7335e8a7f5ae20d90354515b6dbb18e4b836d57821c0f7a7375f4402f8aa1ec445da4abe3709a41c3e878f6aa2e710cfe1a2824fd198c5120d7fbf78f1575f0940ed4a55e3e3754afd0359da8378fe2cd856fdc7431592399e02d78fc86782421eab559ae3617ad9fa8cbca2bd4a6eb59888d36e6706b4002b3b085a98f17a53f026a32ede7e569374e624c334814501e8e99da1cf8e1ddf3c3d397500bb07e37529c1f6b290f69b4841d5e97e936a43d26d6d359647b3a67c10ef1faba698a6bae0c16defb04b14a1b98ff83df375a52bbeb3deb6be388059f59d15a4d41ab647ed308118933d029a358697609bd1caa5473b020e62f4072d101ebaac5f8c0afc5c8e78b624dee0300043d8e3c5d32e2f441bed03af3eeaf48e51f762768b96bf0ef2f0000000000000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0fa1f1c8355b322e0697fcc636332b2c5d8fb205ea3fc74fa140d889a0b35ee77b8b8ead623fdf980bb0d3f5ed3bf840931e417568f9539f82fdc2b02f1aac7a1e6046d9785928715d992ca5115edc4d31a2993e9ee723283ac4fc9fa3615da558b99b01c50877f56c2b63af169eb0b0f36788592885435cd06880826c5b351f3a03509b17db5e00861f839afee1fc5f0ac4365b9bc8ef41a0f9497d1215c70ac2d23d0df87c9333a671c849a428dd0e2eb6e735d70d006db17c97ec11407728df1255858b960222dffddd91e0d986edc42221533811744ab0511184a0415ac96d12a533496b524f6e180daa646d86983de44d3d57cdebdd493d9c340c73179fce296d255046149484975280839c096707030004da0e9d7fc30f99c0688e2d101371da0d8534e40bcd2a89a2160000000000000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f971e4a28f2420532039d70f391ca9c19d90170be44bf2e961c0657594b011b77038b160be9ac72bc82a9028751bc331007ee245ed19e01526a843f00e49bedacf82473f017543d67b9fdfcb42a258815b33698e09075be367db3b657b0965d6f165f1aad01e2a185016b0118ece195400e91b5676124e7cfb8fd793ed9c6cfdb023b04f24d1309953cf36b4490290eb5015a8e4ea5ec4ddb99e575d247f9be243ccd67910d15ea395e0dd371e67c5cc5a8d733bbb4d151d299ed6e71c03a6e596544559220624d474ff795e19d88171e7dbd6a382b4d7e8276df32cbb6bb1a2a036f184cc6a4414545cf6c5acf5f287be248a0753ce7811e8b99c16d89128ec143dcc868205d4e4e449eaa35fdb98c7604b0c3c0c6034c234109812b66e240139bf9d20ac10d3b6b9ffbdb2beb8dd29791f19a1832100375a52bbc5887d172d1a6163d594de0bd97ef71fd8b79a75967a01086a4e953eec5fe6e62975705277d67b45e726aacdfcc535900d5a8fec734c8010200a24b1436a250cc5a54ef8b39cbd9e1cd411f5651fbb44a304749f0014e21b10040d2d3ae2e99671b55b8a3b8050f971f06213d09ea6da274412c76e29092287effc6bb6ccf94e315c4de5ab895572466a8f86333b2a450f41b827c7958132d4e010000b232ed20f72bcf1f770bd06292048bc49535543c61bc787a25be6cfe56a2beb694055b82a275f7bc0dc0f10adf28759f05791a173a0dbfc2ee056397caa71cb05d8a3641cb56a7a074158283d4a968ef1b9c80bdde39e28577659720f027ff4901030091cdd042b5c9793886586d5b74e3fd13406400f55c57b207597f6bd3b17fb1b1c89d41704d04ed9cb510374df1dffdd8171f3e09704426307212e6cfa37aa7bbafb026f6cba550db2e3874cdac7fcd22762d1bc3acc556b2b734bbd3a4f6f2d2010100" + } +} diff --git a/typings/deterministicmnlist/QuorumEntry.d.ts b/typings/deterministicmnlist/QuorumEntry.d.ts index 7806a3d35..908856162 100644 --- a/typings/deterministicmnlist/QuorumEntry.d.ts +++ b/typings/deterministicmnlist/QuorumEntry.d.ts @@ -1,5 +1,6 @@ import { SimplifiedMNList } from './SimplifiedMNList'; import { SimplifiedMNListEntry } from './SimplifiedMNListEntry'; +import { BufferReader } from '../buffer/BufferReader'; /** * @class QuorumEntry * @param {string|Object|Buffer} [arg] - A Buffer, JSON string, @@ -27,6 +28,19 @@ export class QuorumEntry { * @return {QuorumEntry} */ static fromBuffer(buffer: Buffer): QuorumEntry; + + /** + * Reads a final commitment as Core sends it in the P2P `mnlistdiff` message, + * the layout toBufferForHashing() writes. Like Core's RPC, signersCount and + * validMembersCount are the numbers of set bits. + * @param {BufferReader} bufferReader + * @param {Object} [llmqParamsOverride] + * @return {QuorumEntry} + */ + static fromP2PBufferReader( + bufferReader: BufferReader, + llmqParamsOverride?: Object + ): QuorumEntry; /** * @param {string} string * @return {QuorumEntry} diff --git a/typings/deterministicmnlist/SimplifiedMNListDiff.d.ts b/typings/deterministicmnlist/SimplifiedMNListDiff.d.ts index 23910b1ab..3aa03f9a6 100644 --- a/typings/deterministicmnlist/SimplifiedMNListDiff.d.ts +++ b/typings/deterministicmnlist/SimplifiedMNListDiff.d.ts @@ -33,7 +33,8 @@ export class SimplifiedMNListDiff { constructor(arg?: Buffer | any | string, network?: string); /** - * Creates MnListDiff from a Buffer. + * Creates MnListDiff from a Buffer holding Core's P2P `mnlistdiff` message + * (protocol 70230 and later, unchanged through 70242). * @param {Buffer} buffer * @param {string} [network] * @return {SimplifiedMNListDiff} @@ -51,7 +52,8 @@ export class SimplifiedMNListDiff { ): SimplifiedMNListDiff; /** - * Serializes mnlist diff to a Buffer + * Serializes mnlist diff as Core's P2P `mnlistdiff` message (protocol 70230 + * and later). Quorums need their LLMQ type params, for the bit set sizes. * @return {Buffer} */ toBuffer(): Buffer; @@ -83,4 +85,9 @@ export class SimplifiedMNListDiff { * merkle root of the whole mn list */ merkleRootMNList: string; + /** + * chainlock signatures used for quorum rotation, each mapped to indexes + * into newQuorums + */ + quorumsCLSigs?: Record[]; } diff --git a/typings/deterministicmnlist/SimplifiedMNListEntry.d.ts b/typings/deterministicmnlist/SimplifiedMNListEntry.d.ts index 864323428..19d8282dc 100644 --- a/typings/deterministicmnlist/SimplifiedMNListEntry.d.ts +++ b/typings/deterministicmnlist/SimplifiedMNListEntry.d.ts @@ -1,3 +1,5 @@ +import { BufferReader } from '../buffer/BufferReader'; + /** * @typedef {Object} SMLEntry * @property {string} proRegTxHash @@ -53,6 +55,18 @@ export class SimplifiedMNListEntry { */ static fromBuffer(buffer: Buffer, network?: string): SimplifiedMNListEntry; + /** + * Reads an entry as Core sends it in the P2P `mnlistdiff` message: + * nVersion followed by the hashed fields (see toP2PBuffer) + * @param {BufferReader} bufferReader + * @param {string} [network] + * @return {SimplifiedMNListEntry} + */ + static fromP2PBufferReader( + bufferReader: BufferReader, + network?: string + ): SimplifiedMNListEntry; + /** * @param {string} string * @param {string} [network] @@ -66,6 +80,13 @@ export class SimplifiedMNListEntry { */ toBuffer(): Buffer; + /** + * Serialize SML entry as Core sends it in the P2P `mnlistdiff` message + * (protocol 70228 and later): nVersion, then the fields toBuffer() hashes + * @return {Buffer} + */ + toP2PBuffer(): Buffer; + /** * Create SMLEntry from an object * @param {SMLEntry} obj From 41f44629ab269e74e52bc3357b7b5f0d573a2085 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 17:32:25 +0000 Subject: [PATCH 03/21] feat: add SimplifiedMNListEntry.getPlatformHTTPSEndpoint() getIp() reads `service`, Core's deprecated primary core_p2p address, and callers paired it with platformHTTPPort to reach Platform. A version 3 (ExtAddr) evonode can serve Platform from another host, a domain or an IPv6 address, listed in addresses.platform_https. getPlatformHTTPSEndpoint() returns { host, port } from the first platform_https address of a version 3 evonode, getIp() and platformHTTPPort for an older one, and null for regular masternodes and evonodes without an address. Like getIp(), IPv6 hosts have no brackets. Co-Authored-By: Claude Opus 5.5 --- .../SimplifiedMNListEntry.js | 28 ++++++++++ test-d/index.test-d.ts | 1 + .../SimplifiedMNListDiff.js | 14 +++++ .../SimplifiedMNListEntry.js | 55 +++++++++++++++++++ .../SimplifiedMNListEntry.d.ts | 10 ++++ 5 files changed, 108 insertions(+) diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index 7f3ac426d..5cf84cd78 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -389,6 +389,34 @@ SimplifiedMNListEntry.prototype.getIp = function getIp() { return this.service.split(':')[0]; }; +/** + * Gets the evonode's Platform HTTPS endpoint: the first `platform_https` address + * of a version 3 (ExtAddr) entry, which may be an IPv6 address or a domain, or + * the service ip with platformHTTPPort of an older one. PoSe-banned entries + * have endpoints too; check isValid before using one. + * @return {{host: string, port: number}|null} host without IPv6 brackets, like + * getIp(); null for regular masternodes and evonodes without an address + */ +SimplifiedMNListEntry.prototype.getPlatformHTTPSEndpoint = function getPlatformHTTPSEndpoint() { + if (this.nType !== MASTERNODE_TYPE_HP || typeof this.platformHTTPPort !== 'number') { + return null; + } + + // fromObject takes an ExtAddr entry's platformHTTPPort from platform_https[0] + if (this.nVersion >= PROTX_EXT_ADDR_VERSION) { + const [address] = this.addresses.platform_https; + return { + host: address.slice(0, address.lastIndexOf(':')).replace(/^\[(.*)\]$/, '$1'), + port: this.platformHTTPPort, + }; + } + + if (ipUtils.isZeroAddress(this.service)) { + return null; + } + return { host: this.getIp(), port: this.platformHTTPPort }; +}; + /** * Serialize confirmed hash with proRegTxHash for MN scores * and quorum member selection diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index 290be8a61..db2eeefb9 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -123,3 +123,4 @@ declare const smlEntry: SimplifiedMNListEntry; expectType(smlEntry.toP2PBuffer()); declare const smlDiff: SimplifiedMNListDiff; expectType[] | undefined>(smlDiff.quorumsCLSigs); +expectType<{ host: string; port: number } | null>(smlEntry.getPlatformHTTPSEndpoint()); diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index 7095f5c66..bf21d9b87 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -248,6 +248,20 @@ describe('SimplifiedMNListDiff', function () { return quorum.signersCount < quorum.getParams().size; })).to.be.true; }); + it('Should give each evonode the Platform endpoint Core RPC reports', function () { + var checked = 0; + diff.mnList.forEach(function (entry, i) { + var platformHTTPS = coreDiffsJSON[0].mnList[i].addresses.platform_https; + // Version 1 entries carry no type on the wire + if (entry.nVersion < 2 || !platformHTTPS) { + return; + } + var endpoint = entry.getPlatformHTTPSEndpoint(); + expect(endpoint.host + ':' + endpoint.port).to.be.equal(platformHTTPS[0]); + checked += 1; + }); + expect(checked).to.be.above(50); + }); it('Should serialize Core RPC JSON to the bytes Core sends', function () { expect(rpcDiff.toBuffer().toString('hex')).to.be.equal(vector.hex); }); diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index fbc64b7e0..569fe1b89 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -355,4 +355,59 @@ describe('SimplifiedMNListEntry', function () { expect(entry.getIp()).to.be.equal('2001:db8::1'); }); }); + describe('getPlatformHTTPSEndpoint', function () { + it('Should get the service ip and platformHTTPPort of a legacy evonode', function () { + var entry = new SimplifiedMNListEntry(smlEntryJSON); + expect(entry.getPlatformHTTPSEndpoint()).to.be.deep.equal({ + host: '95.183.51.146', + port: 3200, + }); + }); + it('Should get the platform_https address of a version 3 evonode', function () { + var entry = new SimplifiedMNListEntry(extAddrEvoEntryJSON); + expect(entry.getPlatformHTTPSEndpoint()).to.be.deep.equal({ + host: '162.35.243.85', + port: 1443, + }); + }); + it('Should get a platform_https host that differs from the service ip', function () { + [ + ['[2001:db8::1]:443', '2001:db8::1'], + ['evo.example.com:443', 'evo.example.com'], + ['203.0.113.7:443', '203.0.113.7'], + ].forEach(function (testCase) { + var json = Object.assign({}, extAddrEvoEntryJSON, { + addresses: Object.assign({}, extAddrEvoEntryJSON.addresses, { + platform_https: [testCase[0]], + }), + }); + var entry = new SimplifiedMNListEntry(json); + + expect(entry.getIp()).to.be.equal('162.35.243.85'); + expect(entry.getPlatformHTTPSEndpoint()).to.be.deep.equal({ + host: testCase[1], + port: 443, + }); + }); + }); + it('Should return null for an evonode without a Platform address', function () { + var json = Object.assign({}, extAddrEvoEntryJSON, { + addresses: { core_p2p: extAddrEvoEntryJSON.addresses.core_p2p }, + }); + expect(new SimplifiedMNListEntry(json).getPlatformHTTPSEndpoint()).to.be.null; + expect( + new SimplifiedMNListEntry(smlEntryWithoutAddress).getPlatformHTTPSEndpoint() + ).to.be.null; + }); + it('Should return null for a regular masternode', function () { + expect( + new SimplifiedMNListEntry(extAddrRegularEntryJSON).getPlatformHTTPSEndpoint() + ).to.be.null; + expect( + new SimplifiedMNListEntry( + Object.assign({}, smlEntryJSON, { nType: 0, platformHTTPPort: undefined }) + ).getPlatformHTTPSEndpoint() + ).to.be.null; + }); + }); }); diff --git a/typings/deterministicmnlist/SimplifiedMNListEntry.d.ts b/typings/deterministicmnlist/SimplifiedMNListEntry.d.ts index 19d8282dc..740638864 100644 --- a/typings/deterministicmnlist/SimplifiedMNListEntry.d.ts +++ b/typings/deterministicmnlist/SimplifiedMNListEntry.d.ts @@ -105,6 +105,16 @@ export class SimplifiedMNListEntry { */ getIp(): string; + /** + * Gets the evonode's Platform HTTPS endpoint: the first `platform_https` address + * of a version 3 (ExtAddr) entry, which may be an IPv6 address or a domain, or + * the service ip with platformHTTPPort of an older one. PoSe-banned entries + * have endpoints too; check isValid before using one. + * @return {{host: string, port: number}|null} host without IPv6 brackets, like + * getIp(); null for regular masternodes and evonodes without an address + */ + getPlatformHTTPSEndpoint(): { host: string; port: number } | null; + /** * Creates a copy of SimplifiedMNListEntry * @return {SimplifiedMNListEntry} From 4f2f7c4533094d39339aae52eadd797629a3d9be Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 17:33:20 +0000 Subject: [PATCH 04/21] fix: align LLMQ thresholds with Dash Core LLMQ_25_67 had a threshold of 67 (its percentage) instead of Core's 17, and LLMQ_TYPE_TEST_DIP0024 had 2 instead of 3. A new test checks the size, threshold and active quorum count of every LLMQ type against src/llmq/params.h in Dash Core 24.0.0; the other types already matched. Co-Authored-By: Claude Opus 5.5 --- lib/constants/index.js | 8 +++---- test/constants/llmqparams.js | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 test/constants/llmqparams.js diff --git a/lib/constants/index.js b/lib/constants/index.js index 47397e0f6..54116001f 100644 --- a/lib/constants/index.js +++ b/lib/constants/index.js @@ -65,7 +65,7 @@ const constants = { // 60 members, 45 (75%) threshold, one every 12 hours LLMQ_TYPE_60_75: 5, - // 25 members, 67 (67%) threshold, one per hour + // 25 members, 17 (67%) threshold, one per hour LLMQ_TYPE_25_67: 6, // 3 members, 2 (66%) threshold, one per hour (24 blocks) @@ -80,7 +80,7 @@ const constants = { // Params might differ when -llmqtestparams is used LLMQ_TYPE_TEST_V17: 102, - // 4 members, 2 (66%) threshold, one per hour. + // 4 members, 3 (75%) threshold, one per hour. // Params might differ when -llmqtestparams is used LLMQ_TYPE_TEST_DIP0024: 103, @@ -141,7 +141,7 @@ constants.LLMQ_TYPE_PARAMS= { }, [constants.LLMQ_TYPES.LLMQ_TYPE_25_67]: { size: 25, - threshold: 67, + threshold: 17, maximumActiveQuorumsCount: 24, }, [constants.LLMQ_TYPES.LLMQ_TYPE_LLMQ_TEST]: { @@ -161,7 +161,7 @@ constants.LLMQ_TYPE_PARAMS= { }, [constants.LLMQ_TYPES.LLMQ_TYPE_TEST_DIP0024]: { size: 4, - threshold: 2, + threshold: 3, maximumActiveQuorumsCount: 2, }, [constants.LLMQ_TYPES.LLMQ_TYPE_TEST_INSTANTSEND]: { diff --git a/test/constants/llmqparams.js b/test/constants/llmqparams.js new file mode 100644 index 000000000..8aae27493 --- /dev/null +++ b/test/constants/llmqparams.js @@ -0,0 +1,43 @@ +/* eslint-disable */ + +var expect = require('chai').expect; +var constants = require('../../lib/constants'); + +var LLMQ_TYPES = constants.LLMQ_TYPES; + +describe('LLMQ_TYPE_PARAMS', function () { + // size, threshold and signingActiveQuorumCount from src/llmq/params.h in Dash Core 24.0.0 + var coreParams = [ + [LLMQ_TYPES.LLMQ_TYPE_50_60, 50, 30, 24], + [LLMQ_TYPES.LLMQ_TYPE_400_60, 400, 240, 4], + [LLMQ_TYPES.LLMQ_TYPE_400_85, 400, 340, 4], + [LLMQ_TYPES.LLMQ_TYPE_100_67, 100, 67, 24], + [LLMQ_TYPES.LLMQ_TYPE_60_75, 60, 45, 32], + [LLMQ_TYPES.LLMQ_TYPE_25_67, 25, 17, 24], + [LLMQ_TYPES.LLMQ_TYPE_LLMQ_TEST, 3, 2, 2], + [LLMQ_TYPES.LLMQ_TYPE_LLMQ_DEVNET, 12, 6, 4], + [LLMQ_TYPES.LLMQ_TYPE_TEST_V17, 3, 2, 2], + [LLMQ_TYPES.LLMQ_TYPE_TEST_DIP0024, 4, 3, 2], + [LLMQ_TYPES.LLMQ_TYPE_TEST_INSTANTSEND, 3, 2, 2], + [LLMQ_TYPES.LLMQ_DEVNET_DIP0024, 8, 4, 2], + [LLMQ_TYPES.LLMQ_TEST_PLATFORM, 3, 2, 2], + [LLMQ_TYPES.LLMQ_DEVNET_PLATFORM, 12, 8, 4], + ]; + + it('Should define the same LLMQ types as Dash Core', function () { + var types = coreParams.map(function (params) { + return String(params[0]); + }); + expect(Object.keys(constants.LLMQ_TYPE_PARAMS)).to.have.members(types); + }); + + coreParams.forEach(function (params) { + it('Should match Dash Core params for LLMQ type ' + params[0], function () { + expect(constants.LLMQ_TYPE_PARAMS[params[0]]).to.be.deep.equal({ + size: params[1], + threshold: params[2], + maximumActiveQuorumsCount: params[3], + }); + }); + }); +}); From 921b5ad1ac8ebe69cfaf94f416309b6892a9f83f Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 20:46:06 +0000 Subject: [PATCH 05/21] fix: bound peer-controlled counts in masternode list diff parsing BufferReader.read() returns an empty slice past the end of its buffer, so a crafted count in a P2P mnlistdiff made fromBuffer loop on empty reads for ages, e.g. 0xffffffff deleted masternodes or merkle hashes. A new readVarintCount() util reads a count and throws unless the remaining bytes can hold that many items of their smallest encoded size. It now guards every count on the diff path: merkle hashes and flag bytes in PartialMerkleTree, deleted masternodes, entries (137 bytes at least), deleted quorums, new quorums (309 bytes at least), quorum chainlock signatures and their indexes. ExtNetInfo purposes and entries were already bounded, and each coinbase input and output does a read that throws past the end. Co-Authored-By: Claude Opus 5.5 --- lib/block/PartialMerkleTree.js | 9 ++- .../SimplifiedMNListDiff.js | 33 +++++++++-- lib/util/readVarintCount.js | 18 ++++++ test/block/PartialMerkleTree.js | 9 +++ .../SimplifiedMNListDiff.js | 55 +++++++++++++++++++ 5 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 lib/util/readVarintCount.js diff --git a/lib/block/PartialMerkleTree.js b/lib/block/PartialMerkleTree.js index 716e1b0a8..43454df77 100644 --- a/lib/block/PartialMerkleTree.js +++ b/lib/block/PartialMerkleTree.js @@ -6,6 +6,7 @@ const isHexString = require('../util/js').isHexa; const constants = require('../constants'); const MerkleTreeUtils = require('../util/merkletree'); const BitArrayUtils = require('../util/bitarray'); +const readVarintCount = require('../util/readVarintCount'); const { convertBitArrayToUInt8Array } = BitArrayUtils; @@ -63,7 +64,11 @@ PartialMerkleTree.fromBufferReader = function fromBufferReader(bufferReader) { const partialMerkleTree = new PartialMerkleTree(); partialMerkleTree.totalTransactions = bufferReader.readUInt32LE(); - const merkleHashesCount = bufferReader.readVarintNum(); + const merkleHashesCount = readVarintCount( + bufferReader, + constants.SHA256_HASH_SIZE, + 'Merkle hash' + ); partialMerkleTree.merkleHashes = []; for (let i = 0; i < merkleHashesCount; i += 1) { partialMerkleTree.merkleHashes.push( @@ -71,7 +76,7 @@ PartialMerkleTree.fromBufferReader = function fromBufferReader(bufferReader) { ); } - const merkleFlagsCount = bufferReader.readVarintNum(); + const merkleFlagsCount = readVarintCount(bufferReader, 1, 'Merkle flag byte'); partialMerkleTree.merkleFlags = []; for (let i = 0; i < merkleFlagsCount; i += 1) { partialMerkleTree.merkleFlags.push(bufferReader.readUInt8()); diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 92996d28f..96b6a5f2d 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -11,6 +11,14 @@ const PartialMerkleTree = require('../block/PartialMerkleTree'); const Transaction = require('../transaction'); const constants = require('../constants'); const Networks = require('../networks'); +const readVarintCount = require('../util/readVarintCount'); + +// nVersion, proRegTxHash, confirmedHash, an empty ExtNetInfo (version and purpose +// count), pubKeyOperator, keyIDVoting and isValid +const MIN_SML_ENTRY_SIZE = 2 + 32 + 32 + 2 + 48 + 20 + 1; +// version, llmqType, quorumHash, two empty bit sets, quorumPublicKey, +// quorumVvecHash, quorumSig and membersSig +const MIN_QUORUM_ENTRY_SIZE = 2 + 1 + 32 + 1 + 1 + 48 + 32 + 96 + 96; /** * @param {Buffer|Object|string} [arg] - A Buffer, JSON string, or Object representing a MnListDiff @@ -76,7 +84,11 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam data.cbTxMerkleTree = PartialMerkleTree.fromBufferReader(bufferReader); data.cbTx = new Transaction().fromBufferReader(bufferReader); - const deletedMNsCount = bufferReader.readVarintNum(); + const deletedMNsCount = readVarintCount( + bufferReader, + constants.SHA256_HASH_SIZE, + 'Deleted masternode' + ); data.deletedMNs = []; for (let i = 0; i < deletedMNsCount; i += 1) { data.deletedMNs.push( @@ -84,7 +96,7 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam ); } - const mnListSize = bufferReader.readVarintNum(); + const mnListSize = readVarintCount(bufferReader, MIN_SML_ENTRY_SIZE, 'Masternode list entry'); data.mnList = []; for (let i = 0; i < mnListSize; i += 1) { data.mnList.push( @@ -92,7 +104,11 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam ); } - const deletedQuorumsCount = bufferReader.readVarintNum(); + const deletedQuorumsCount = readVarintCount( + bufferReader, + 1 + constants.SHA256_HASH_SIZE, + 'Deleted quorum' + ); data.deletedQuorums = []; for (let i = 0; i < deletedQuorumsCount; i += 1) { const deletedQuorum = {}; @@ -103,7 +119,7 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam data.deletedQuorums.push(deletedQuorum); } - const newQuorumsSize = bufferReader.readVarintNum(); + const newQuorumsSize = readVarintCount(bufferReader, MIN_QUORUM_ENTRY_SIZE, 'New quorum'); data.newQuorums = []; for (let i = 0; i < newQuorumsSize; i += 1) { data.newQuorums.push( @@ -112,13 +128,18 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam } // Core's RPC form: [{ : [indexes into newQuorums] }, ...] - const quorumsCLSigsCount = bufferReader.readVarintNum(); + // a signature and its (possibly empty) index count + const quorumsCLSigsCount = readVarintCount( + bufferReader, + constants.BLS_SIGNATURE_SIZE + 1, + 'Quorum chainlock signature' + ); data.quorumsCLSigs = []; for (let i = 0; i < quorumsCLSigsCount; i += 1) { const signature = bufferReader .read(constants.BLS_SIGNATURE_SIZE) .toString('hex'); - const indexesCount = bufferReader.readVarintNum(); + const indexesCount = readVarintCount(bufferReader, 2, 'Quorum index'); const indexes = []; for (let j = 0; j < indexesCount; j += 1) { indexes.push(bufferReader.readUInt16LE()); diff --git a/lib/util/readVarintCount.js b/lib/util/readVarintCount.js new file mode 100644 index 000000000..3e11254a0 --- /dev/null +++ b/lib/util/readVarintCount.js @@ -0,0 +1,18 @@ +/** + * Reads a peer-controlled item count and checks that the remaining data can + * hold that many items. BufferReader.read() returns empty slices past the end, + * so without this check a huge count would make the caller loop for ages. + * @param {BufferReader} bufferReader + * @param {number} minItemSize - smallest serialized size of one item, in bytes + * @param {string} name - what is counted, for the error message + * @return {number} + */ +function readVarintCount(bufferReader, minItemSize, name) { + const count = bufferReader.readVarintNum(); + if (count * minItemSize > bufferReader.buf.length - bufferReader.pos) { + throw new Error(`${name} count ${count} is larger than the remaining data allows`); + } + return count; +} + +module.exports = readVarintCount; diff --git a/test/block/PartialMerkleTree.js b/test/block/PartialMerkleTree.js index 8e9910472..edd7afcfb 100644 --- a/test/block/PartialMerkleTree.js +++ b/test/block/PartialMerkleTree.js @@ -91,5 +91,14 @@ describe('PartialMerkleTree', function () { expect(buffer.toString('hex')).to.be.equal(coreTreeHex); expect(tree.toString()).to.be.equal(coreTreeHex); }); + it('Should throw quickly on a hash count larger than the data', function () { + this.timeout(2000); + // the hash count (04) follows the 4-byte transaction count + var hex = coreTreeHex.slice(0, 8) + 'feffffffff' + coreTreeHex.slice(10); + + expect(function () { + PartialMerkleTree.fromHexString(hex); + }).to.throw('Merkle hash count 4294967295 is larger than the remaining data allows'); + }); }); }); diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index bf21d9b87..6be036200 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -110,6 +110,61 @@ describe('SimplifiedMNListDiff', function () { SimplifiedMNListDiff.fromHexString(mnListDiffHexString + '00', 'testnet'); }).to.throw('MnListDiff is followed by unexpected data'); }); + describe('with a crafted count', function () { + // byte offsets of each single-byte count in mnListDiffHexString + var offsets = {}; + + before(function () { + var diff = SimplifiedMNListDiff.fromHexString(mnListDiffHexString, 'testnet'); + var tree = diff.cbTxMerkleTree; + var offset = 2 + 32 + 32 + 4; // nVersion, base and block hashes, totalTransactions + offsets['Merkle hash'] = offset; + offset += 1 + tree.merkleHashes.length * 32; + offsets['Merkle flag byte'] = offset; + offset += 1 + tree.merkleFlags.length + diff.cbTx.toBuffer().length; + offsets['Deleted masternode'] = offset; + offset += 1 + diff.deletedMNs.length * 32; + offsets['Masternode list entry'] = offset; + offset += 1; + offsets['Deleted quorum'] = offset; + offset += 1 + diff.deletedQuorums.length * 33; + offsets['New quorum'] = offset; + offset += 1; + diff.newQuorums.forEach(function (quorum) { + offset += quorum.toBufferForHashing().length; + }); + offsets['Quorum chainlock signature'] = offset; + offsets['Quorum index'] = offset + 1 + 96; + + // the vector has no masternode changes, so mnList adds no bytes above + expect(diff.mnList).to.be.empty; + Object.keys(offsets).forEach(function (name) { + expect(parseInt(mnListDiffHexString.substr(offsets[name] * 2, 2), 16)).to.be.below(0xfd); + }); + }); + + [ + 'Merkle hash', + 'Merkle flag byte', + 'Deleted masternode', + 'Masternode list entry', + 'Deleted quorum', + 'New quorum', + 'Quorum chainlock signature', + 'Quorum index', + ].forEach(function (name) { + it('Should throw quickly on a huge ' + name + ' count', function () { + this.timeout(2000); + var position = offsets[name] * 2; + var hex = mnListDiffHexString.slice(0, position) + 'feffffffff' + + mnListDiffHexString.slice(position + 2); + + expect(function () { + SimplifiedMNListDiff.fromHexString(hex, 'testnet'); + }).to.throw(name + ' count 4294967295 is larger than the remaining data allows'); + }); + }); + }); describe('with a crafted quorum bit set size', function () { // the first new quorum is a version 3 llmq_50_60 commitment: version (2 bytes), // llmqType (1), quorumHash (32), then the signers bit set size, 0x32 (50) From 280a329bdb944d79d60e814453aebebc2070ef66 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 20:46:39 +0000 Subject: [PATCH 06/21] fix: reject malformed quorumsCLSigs and zero Platform endpoints - getPlatformHTTPSEndpoint() returns null for a version 3 evonode whose platform_https is missing, empty or a zero address such as '[::]:0', as it already did for a legacy evonode with a zero service, instead of returning '::' with port 0 or throwing on a missing list. - SimplifiedMNListDiff.toBuffer() requires each quorumsCLSigs entry to be an object with exactly one 96-byte hex signature mapped to an array of indexes, instead of silently writing only its first key. Co-Authored-By: Claude Opus 5.5 --- .../SimplifiedMNListDiff.js | 11 ++++++++- .../SimplifiedMNListEntry.js | 5 +++- .../SimplifiedMNListDiff.js | 23 +++++++++++++++++++ .../SimplifiedMNListEntry.js | 15 ++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 96b6a5f2d..9ee56c139 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -217,12 +217,21 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { const quorumsCLSigs = this.quorumsCLSigs || []; bufferWriter.writeVarintNum(quorumsCLSigs.length); quorumsCLSigs.forEach((quorumsCLSig) => { - const [signature] = Object.keys(quorumsCLSig); + const signatures = _.isPlainObject(quorumsCLSig) ? Object.keys(quorumsCLSig) : []; + $.checkArgument( + signatures.length === 1, + 'Expected each quorumsCLSigs entry to hold exactly one signature' + ); + const [signature] = signatures; const indexes = quorumsCLSig[signature]; $.checkArgument( isHexStringOfSize(signature, constants.BLS_SIGNATURE_SIZE * 2), 'Expected quorumsCLSigs keys to be bls signatures' ); + $.checkArgument( + Array.isArray(indexes), + 'Expected quorumsCLSigs values to be arrays of quorum indexes' + ); bufferWriter.write(Buffer.from(signature, 'hex')); bufferWriter.writeVarintNum(indexes.length); indexes.forEach((index) => bufferWriter.writeUInt16LE(index)); diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index 5cf84cd78..5394673c2 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -404,7 +404,10 @@ SimplifiedMNListEntry.prototype.getPlatformHTTPSEndpoint = function getPlatformH // fromObject takes an ExtAddr entry's platformHTTPPort from platform_https[0] if (this.nVersion >= PROTX_EXT_ADDR_VERSION) { - const [address] = this.addresses.platform_https; + const [address] = this.addresses.platform_https || []; + if (!address || ipUtils.isZeroAddress(address)) { + return null; + } return { host: address.slice(0, address.lastIndexOf(':')).replace(/^\[(.*)\]$/, '$1'), port: this.platformHTTPPort, diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index 6be036200..f7e92a305 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -213,6 +213,29 @@ describe('SimplifiedMNListDiff', function () { ); }); }); + describe('toBuffer quorumsCLSigs', function () { + var signature = coreDiffsJSON[0].quorumsCLSigs[0]; + var signatureHex = Object.keys(signature)[0]; + + [ + ['two signatures in one entry', Object.assign({}, signature, coreDiffsJSON[0].quorumsCLSigs[1]), + 'Expected each quorumsCLSigs entry to hold exactly one signature'], + ['no signature', {}, 'Expected each quorumsCLSigs entry to hold exactly one signature'], + ['an array entry', [[1]], 'Expected each quorumsCLSigs entry to hold exactly one signature'], + ['a short signature', { abcd: [1] }, 'Expected quorumsCLSigs keys to be bls signatures'], + ['indexes that are not an array', { [signatureHex]: 1 }, + 'Expected quorumsCLSigs values to be arrays of quorum indexes'], + ].forEach(function (testCase) { + it('Should reject ' + testCase[0], function () { + var diff = new SimplifiedMNListDiff(mnListDiffJSON); + diff.quorumsCLSigs = [testCase[1]]; + + expect(function () { + diff.toBuffer(); + }).to.throw(testCase[2]); + }); + }); + }); describe('Core P2P mnlistdiff messages', function () { Object.keys(coreDiffs).filter(function (name) { return name !== 'note'; diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index 569fe1b89..01e8ec5ca 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -399,6 +399,21 @@ describe('SimplifiedMNListEntry', function () { new SimplifiedMNListEntry(smlEntryWithoutAddress).getPlatformHTTPSEndpoint() ).to.be.null; }); + it('Should return null for a version 3 evonode with a zero or missing platform_https', function () { + var json = Object.assign({}, extAddrEvoEntryJSON, { + addresses: Object.assign({}, extAddrEvoEntryJSON.addresses, { + platform_https: ['[::]:0'], + }), + }); + expect(new SimplifiedMNListEntry(json).getPlatformHTTPSEndpoint()).to.be.null; + + // platformHTTPPort left set after the addresses change + var entry = new SimplifiedMNListEntry(extAddrEvoEntryJSON); + entry.addresses.platform_https = []; + expect(entry.getPlatformHTTPSEndpoint()).to.be.null; + delete entry.addresses.platform_https; + expect(entry.getPlatformHTTPSEndpoint()).to.be.null; + }); it('Should return null for a regular masternode', function () { expect( new SimplifiedMNListEntry(extAddrRegularEntryJSON).getPlatformHTTPSEndpoint() From 053c737e1e9002f63eecb5c2b1cd23d018468577 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 28 Oct 2026 23:29:01 +0000 Subject: [PATCH 07/21] fix: check quorum bit set lengths before serializing A signers or validMembers string shorter or longer than the LLMQ size would shift every field written after it. Co-Authored-By: Claude Opus 5.5 --- lib/deterministicmnlist/QuorumEntry.js | 11 +++++++++++ test/deterministicmnlist/QuorumEntry.js | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/deterministicmnlist/QuorumEntry.js b/lib/deterministicmnlist/QuorumEntry.js index 515772e38..3843c53d6 100644 --- a/lib/deterministicmnlist/QuorumEntry.js +++ b/lib/deterministicmnlist/QuorumEntry.js @@ -271,6 +271,17 @@ QuorumEntry.prototype.toBufferForHashing = function toBufferForHashing() { this.validate(); const bufferWriter = new BufferWriter(); const fixedCounterLength = this.getParams().size; + // The bit sets are written after their declared size, so a wrong length would + // shift every field that follows + const bitSetHexLength = Math.ceil(fixedCounterLength / 8) * 2; + $.checkArgument( + this.signers.length === bitSetHexLength, + 'Expected signers to have the LLMQ bit set length' + ); + $.checkArgument( + this.validMembers.length === bitSetHexLength, + 'Expected validMembers to have the LLMQ bit set length' + ); bufferWriter.writeUInt16LE(this.version); bufferWriter.writeUInt8(this.llmqType); bufferWriter.write(Buffer.from(this.quorumHash, 'hex').reverse()); diff --git a/test/deterministicmnlist/QuorumEntry.js b/test/deterministicmnlist/QuorumEntry.js index 28fc9865e..fd25ef54b 100644 --- a/test/deterministicmnlist/QuorumEntry.js +++ b/test/deterministicmnlist/QuorumEntry.js @@ -132,6 +132,20 @@ describe('QuorumEntry', function () { var buffer = entry.toBufferForHashing(); expect(buffer).to.be.deep.equal(Buffer.from(quorumEntryHex, 'hex')); }); + + it('Should reject bit sets that do not match the LLMQ size', function () { + var short = QuorumEntry.fromBuffer(Buffer.from(quorumEntryHex, 'hex')); + short.signers = '00'; + expect(function () { + short.toBufferForHashing(); + }).to.throw('Expected signers to have the LLMQ bit set length'); + + var long = QuorumEntry.fromBuffer(Buffer.from(quorumEntryHex, 'hex')); + long.validMembers += '00'; + expect(function () { + long.toBufferForHashing(); + }).to.throw('Expected validMembers to have the LLMQ bit set length'); + }); }); describe('generate commitmentHash', function () { it('Should be able to generate a correct commitmentHash', function () { From b337fdd7eb4a8f5ac989cf9b59e944a3f01425b9 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 00:00:43 +0000 Subject: [PATCH 08/21] fix: reject quorum bit sets with bits set past their size Core's ReadFixedBitSet rejects set padding bits in the last byte; we kept and hashed them. Co-Authored-By: Claude Opus 5.5 --- lib/deterministicmnlist/QuorumEntry.js | 21 +++++++++++++++++++++ test/deterministicmnlist/QuorumEntry.js | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/lib/deterministicmnlist/QuorumEntry.js b/lib/deterministicmnlist/QuorumEntry.js index 3843c53d6..00e8af4c2 100644 --- a/lib/deterministicmnlist/QuorumEntry.js +++ b/lib/deterministicmnlist/QuorumEntry.js @@ -173,6 +173,19 @@ QuorumEntry.fromBuffer = function fromBuffer(buffer, llmqParamsOverride = {}) { * bit count to equal their size * @return {{count: number, bits: string}} number of set bits, packed bits hex */ +/** + * Whether the unused high bits of a bit set's last byte are set, which Core's + * ReadFixedBitSet rejects + * @param {Buffer} bytes + * @param {number} size - number of bits + * @return {boolean} + */ +function hasOutOfRangeBits(bytes, size) { + const unused = bytes.length * 8 - size; + // eslint-disable-next-line no-bitwise + return unused > 0 && (bytes[bytes.length - 1] & ~(0xff >>> unused) & 0xff) !== 0; +} + function readDynamicBitSet(bufferReader, params) { const size = bufferReader.readVarintNum(); if (params && size !== params.size) { @@ -183,6 +196,9 @@ function readDynamicBitSet(bufferReader, params) { throw new Error(`Bit set of ${size} bits is longer than the remaining data`); } const bytes = bufferReader.read(bytesCount); + if (hasOutOfRangeBits(bytes, size)) { + throw new Error('Out-of-range bits set'); + } let count = 0; for (let i = 0; i < size; i += 1) { // eslint-disable-next-line no-bitwise @@ -282,6 +298,11 @@ QuorumEntry.prototype.toBufferForHashing = function toBufferForHashing() { this.validMembers.length === bitSetHexLength, 'Expected validMembers to have the LLMQ bit set length' ); + $.checkArgument( + !hasOutOfRangeBits(Buffer.from(this.signers, 'hex'), fixedCounterLength) && + !hasOutOfRangeBits(Buffer.from(this.validMembers, 'hex'), fixedCounterLength), + 'Expected no bits set past the LLMQ size' + ); bufferWriter.writeUInt16LE(this.version); bufferWriter.writeUInt8(this.llmqType); bufferWriter.write(Buffer.from(this.quorumHash, 'hex').reverse()); diff --git a/test/deterministicmnlist/QuorumEntry.js b/test/deterministicmnlist/QuorumEntry.js index fd25ef54b..6d565553a 100644 --- a/test/deterministicmnlist/QuorumEntry.js +++ b/test/deterministicmnlist/QuorumEntry.js @@ -118,6 +118,15 @@ describe('QuorumEntry', function () { })); expect(entry.toBufferForHashing().toString('hex')).to.be.equal(hex); }); + + it('Should reject bits set past the bit set size like Core', function () { + // a 50-bit set uses the low 2 bits of its last byte; 0x06 sets bit 50 + var hex = quorumEntryHex.replace('0cff0f0cff0f', '32fffffffffffe0632ffffffffffff02'); + var reader = new BufferReader(Buffer.from(hex, 'hex')); + expect(function () { + QuorumEntry.fromP2PBufferReader(reader, { 107: { size: 50 } }); + }).to.throw('Out-of-range bits set'); + }); }); describe('to buffer', function () { it('Should be able to generate correct buffer', function () { @@ -146,6 +155,19 @@ describe('QuorumEntry', function () { long.toBufferForHashing(); }).to.throw('Expected validMembers to have the LLMQ bit set length'); }); + + it('Should reject bits set past the LLMQ size', function () { + var entry = QuorumEntry.fromBuffer(Buffer.from(quorumEntryHex, 'hex')); + var size = entry.getParams().size; + expect(size % 8).to.not.equal(0); + var bytes = Buffer.from(entry.signers, 'hex'); + // eslint-disable-next-line no-bitwise + bytes[bytes.length - 1] |= 0x80; + entry.signers = bytes.toString('hex'); + expect(function () { + entry.toBufferForHashing(); + }).to.throw('Expected no bits set past the LLMQ size'); + }); }); describe('generate commitmentHash', function () { it('Should be able to generate a correct commitmentHash', function () { From 002f1fcf88d8e84377df8f5aa5aadb83fa9356fb Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 00:31:38 +0000 Subject: [PATCH 09/21] fix: read masternode list diffs strictly and check quorum bit sets like Core - Every CompactSize on the P2P diff path (counts, bit set sizes) is read with strictpayloadreader.readCompactSize: shortest encoding only and at most MAX_SIZE, as Core's ReadCompactSize requires. Fixed-size fields in the diff, its merkle tree, entries and commitments use readExact or readExactReverse, so truncated data throws instead of yielding short values. - getCommitmentHash checks validMembers with the same helper as toBufferForHashing: exactly the LLMQ size's byte length and no bits set past it, since it writes the bits after that size. - readDynamicBitSet counts set bits per byte (at most 8 steps each) instead of per declared bit; readExact bounds the bytes by the remaining data for unknown quorum types. - fromJSON passes llmqParamsOverride to fromObject, and QuorumEntry.copy keeps it, so diffs with custom quorum types round-trip through JSON. - toBuffer requires quorumsCLSigs indexes to be integers in uint16 range. Co-Authored-By: Claude Opus 5.5 --- lib/block/PartialMerkleTree.js | 3 +- lib/deterministicmnlist/QuorumEntry.js | 92 +++++++++++-------- .../SimplifiedMNListDiff.js | 24 ++--- .../SimplifiedMNListEntry.js | 13 +-- lib/util/readVarintCount.js | 11 ++- test/block/PartialMerkleTree.js | 4 +- test/deterministicmnlist/QuorumEntry.js | 21 +++++ .../SimplifiedMNListDiff.js | 65 +++++++++++-- .../SimplifiedMNListEntry.js | 11 +++ 9 files changed, 169 insertions(+), 75 deletions(-) diff --git a/lib/block/PartialMerkleTree.js b/lib/block/PartialMerkleTree.js index 43454df77..e6d74dfe3 100644 --- a/lib/block/PartialMerkleTree.js +++ b/lib/block/PartialMerkleTree.js @@ -7,6 +7,7 @@ const constants = require('../constants'); const MerkleTreeUtils = require('../util/merkletree'); const BitArrayUtils = require('../util/bitarray'); const readVarintCount = require('../util/readVarintCount'); +const { readExactReverse } = require('../transaction/payload/strictpayloadreader'); const { convertBitArrayToUInt8Array } = BitArrayUtils; @@ -72,7 +73,7 @@ PartialMerkleTree.fromBufferReader = function fromBufferReader(bufferReader) { partialMerkleTree.merkleHashes = []; for (let i = 0; i < merkleHashesCount; i += 1) { partialMerkleTree.merkleHashes.push( - bufferReader.readReverse(constants.SHA256_HASH_SIZE).toString('hex') + readExactReverse(bufferReader, constants.SHA256_HASH_SIZE).toString('hex') ); } diff --git a/lib/deterministicmnlist/QuorumEntry.js b/lib/deterministicmnlist/QuorumEntry.js index 00e8af4c2..50e95a927 100644 --- a/lib/deterministicmnlist/QuorumEntry.js +++ b/lib/deterministicmnlist/QuorumEntry.js @@ -9,6 +9,11 @@ const Hash = require('../crypto/hash'); const bls = require('../crypto/bls'); const utils = require('../util/js'); const isHashQuorumIndexRequired = require('../util/isHashQuorumIndexRequired'); +const { + readExact, + readExactReverse, + readCompactSize, +} = require('../transaction/payload/strictpayloadreader'); const { isHexStringOfSize, @@ -93,7 +98,7 @@ function readCommitmentHead(bufferReader) { return { version, llmqType: bufferReader.readUInt8(), - quorumHash: bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'), + quorumHash: readExactReverse(bufferReader, SHA256_HASH_SIZE).toString('hex'), quorumIndex: isHashQuorumIndexRequired(version) ? bufferReader.readInt16LE() : 0, }; } @@ -106,10 +111,10 @@ function readCommitmentHead(bufferReader) { */ function readCommitmentTail(bufferReader) { return { - quorumPublicKey: bufferReader.read(BLS_PUBLIC_KEY_SIZE).toString('hex'), - quorumVvecHash: bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'), - quorumSig: bufferReader.read(BLS_SIGNATURE_SIZE).toString('hex'), - membersSig: bufferReader.read(BLS_SIGNATURE_SIZE).toString('hex'), + quorumPublicKey: readExact(bufferReader, BLS_PUBLIC_KEY_SIZE).toString('hex'), + quorumVvecHash: readExactReverse(bufferReader, SHA256_HASH_SIZE).toString('hex'), + quorumSig: readExact(bufferReader, BLS_SIGNATURE_SIZE).toString('hex'), + membersSig: readExact(bufferReader, BLS_SIGNATURE_SIZE).toString('hex'), }; } @@ -166,13 +171,6 @@ QuorumEntry.fromBuffer = function fromBuffer(buffer, llmqParamsOverride = {}) { return SMLQuorumEntry; }; -/** - * Reads Core's dynamic bit set: its bit count, then the bits packed into bytes - * @param {BufferReader} bufferReader - * @param {Object} [params] - LLMQ type params, if known; Core requires the - * bit count to equal their size - * @return {{count: number, bits: string}} number of set bits, packed bits hex - */ /** * Whether the unused high bits of a bit set's last byte are set, which Core's * ReadFixedBitSet rejects @@ -186,24 +184,49 @@ function hasOutOfRangeBits(bytes, size) { return unused > 0 && (bytes[bytes.length - 1] & ~(0xff >>> unused) & 0xff) !== 0; } +/** + * Checks a bit set before it is written after the LLMQ size: a wrong length + * would shift every field that follows, and Core rejects bits past the size + * @param {string} bits - packed bits hex + * @param {number} size - LLMQ size in bits + * @param {string} name + */ +function checkBitSet(bits, size, name) { + $.checkArgument( + bits.length === Math.ceil(size / 8) * 2, + `Expected ${name} to have the LLMQ bit set length` + ); + $.checkArgument( + !hasOutOfRangeBits(Buffer.from(bits, 'hex'), size), + 'Expected no bits set past the LLMQ size' + ); +} + +/** + * Reads Core's dynamic bit set: its bit count, then the bits packed into bytes + * @param {BufferReader} bufferReader + * @param {Object} [params] - LLMQ type params, if known; Core requires the + * bit count to equal their size + * @return {{count: number, bits: string}} number of set bits, packed bits hex + */ function readDynamicBitSet(bufferReader, params) { - const size = bufferReader.readVarintNum(); + // at most MAX_SIZE bits, and readExact bounds the bytes by the remaining data + const size = readCompactSize(bufferReader); if (params && size !== params.size) { throw new Error(`Expected a bit set of ${params.size} bits, got ${size}`); } - const bytesCount = Math.floor((size + 7) / 8); - if (bytesCount > bufferReader.buf.length - bufferReader.pos) { - throw new Error(`Bit set of ${size} bits is longer than the remaining data`); - } - const bytes = bufferReader.read(bytesCount); + const bytes = readExact(bufferReader, Math.ceil(size / 8)); if (hasOutOfRangeBits(bytes, size)) { throw new Error('Out-of-range bits set'); } let count = 0; - for (let i = 0; i < size; i += 1) { - // eslint-disable-next-line no-bitwise - count += (bytes[Math.floor(i / 8)] >> (i % 8)) & 1; - } + bytes.forEach((byte) => { + // clear the lowest set bit until none is left: at most 8 steps per byte + for (let bits = byte; bits !== 0; count += 1) { + // eslint-disable-next-line no-bitwise + bits &= bits - 1; + } + }); return { count, bits: bytes.toString('hex') }; } @@ -287,22 +310,8 @@ QuorumEntry.prototype.toBufferForHashing = function toBufferForHashing() { this.validate(); const bufferWriter = new BufferWriter(); const fixedCounterLength = this.getParams().size; - // The bit sets are written after their declared size, so a wrong length would - // shift every field that follows - const bitSetHexLength = Math.ceil(fixedCounterLength / 8) * 2; - $.checkArgument( - this.signers.length === bitSetHexLength, - 'Expected signers to have the LLMQ bit set length' - ); - $.checkArgument( - this.validMembers.length === bitSetHexLength, - 'Expected validMembers to have the LLMQ bit set length' - ); - $.checkArgument( - !hasOutOfRangeBits(Buffer.from(this.signers, 'hex'), fixedCounterLength) && - !hasOutOfRangeBits(Buffer.from(this.validMembers, 'hex'), fixedCounterLength), - 'Expected no bits set past the LLMQ size' - ); + checkBitSet(this.signers, fixedCounterLength, 'signers'); + checkBitSet(this.validMembers, fixedCounterLength, 'validMembers'); bufferWriter.writeUInt16LE(this.version); bufferWriter.writeUInt8(this.llmqType); bufferWriter.write(Buffer.from(this.quorumHash, 'hex').reverse()); @@ -458,10 +467,12 @@ QuorumEntry.prototype.getParams = function getParams() { * @return {Uint8Array} */ QuorumEntry.prototype.getCommitmentHash = function getCommitmentHash() { + const { size } = this.getParams(); + checkBitSet(this.validMembers, size, 'validMembers'); const bufferWriter = new BufferWriter(); bufferWriter.writeUInt8(this.llmqType); bufferWriter.write(Buffer.from(this.quorumHash, 'hex').reverse()); - bufferWriter.writeVarintNum(this.getParams().size); + bufferWriter.writeVarintNum(size); bufferWriter.write(Buffer.from(this.validMembers, 'hex')); bufferWriter.write(Buffer.from(this.quorumPublicKey, 'hex')); bufferWriter.write(Buffer.from(this.quorumVvecHash, 'hex').reverse()); @@ -633,7 +644,8 @@ QuorumEntry.prototype.calculateHash = function calculateHash() { * @return {QuorumEntry} */ QuorumEntry.prototype.copy = function copy() { - return QuorumEntry.fromBuffer(this.toBuffer()); + // fromBuffer needs the params of custom quorum types for the bit set sizes + return QuorumEntry.fromBuffer(this.toBuffer(), this.llmqParamsOverride); }; module.exports = QuorumEntry; diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 9ee56c139..d692600a3 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -12,6 +12,7 @@ const Transaction = require('../transaction'); const constants = require('../constants'); const Networks = require('../networks'); const readVarintCount = require('../util/readVarintCount'); +const { readExact, readExactReverse } = require('../transaction/payload/strictpayloadreader'); // nVersion, proRegTxHash, confirmedHash, an empty ExtNetInfo (version and purpose // count), pubKeyOperator, keyIDVoting and isValid @@ -74,12 +75,8 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam const data = {}; data.nVersion = bufferReader.readUInt16LE(); - data.baseBlockHash = bufferReader - .readReverse(constants.SHA256_HASH_SIZE) - .toString('hex'); - data.blockHash = bufferReader - .readReverse(constants.SHA256_HASH_SIZE) - .toString('hex'); + data.baseBlockHash = readExactReverse(bufferReader, constants.SHA256_HASH_SIZE).toString('hex'); + data.blockHash = readExactReverse(bufferReader, constants.SHA256_HASH_SIZE).toString('hex'); data.cbTxMerkleTree = PartialMerkleTree.fromBufferReader(bufferReader); data.cbTx = new Transaction().fromBufferReader(bufferReader); @@ -92,7 +89,7 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam data.deletedMNs = []; for (let i = 0; i < deletedMNsCount; i += 1) { data.deletedMNs.push( - bufferReader.readReverse(constants.SHA256_HASH_SIZE).toString('hex') + readExactReverse(bufferReader, constants.SHA256_HASH_SIZE).toString('hex') ); } @@ -113,8 +110,7 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam for (let i = 0; i < deletedQuorumsCount; i += 1) { const deletedQuorum = {}; deletedQuorum.llmqType = bufferReader.readUInt8(); - deletedQuorum.quorumHash = bufferReader - .readReverse(constants.SHA256_HASH_SIZE) + deletedQuorum.quorumHash = readExactReverse(bufferReader, constants.SHA256_HASH_SIZE) .toString('hex'); data.deletedQuorums.push(deletedQuorum); } @@ -136,8 +132,7 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam ); data.quorumsCLSigs = []; for (let i = 0; i < quorumsCLSigsCount; i += 1) { - const signature = bufferReader - .read(constants.BLS_SIGNATURE_SIZE) + const signature = readExact(bufferReader, constants.BLS_SIGNATURE_SIZE) .toString('hex'); const indexesCount = readVarintCount(bufferReader, 2, 'Quorum index'); const indexes = []; @@ -229,7 +224,8 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { 'Expected quorumsCLSigs keys to be bls signatures' ); $.checkArgument( - Array.isArray(indexes), + Array.isArray(indexes) + && indexes.every((index) => Number.isInteger(index) && index >= 0 && index <= 0xffff), 'Expected quorumsCLSigs values to be arrays of quorum indexes' ); bufferWriter.write(Buffer.from(signature, 'hex')); @@ -315,11 +311,11 @@ SimplifiedMNListDiff.fromJSON = function fromJSON(diffJSON, llmqParamsOverride= return SimplifiedMNListDiff.fromObject( { - llmqParamsOverride, ...diffJSON, cbTxMerkleTree, }, - Networks.get(diffJSON.network.name) + Networks.get(diffJSON.network.name), + llmqParamsOverride ); }; diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index 5394673c2..3d4c7e51c 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -8,6 +8,7 @@ const constants = require('../constants'); const utils = require('../util/js'); const ipUtils = require('../util/ip'); const { extNetInfoToBuffer, readExtNetInfo } = require('../util/netinfo'); +const { readExact, readExactReverse } = require('../transaction/payload/strictpayloadreader'); const Address = require('../address'); const Networks = require('../networks'); @@ -140,16 +141,16 @@ SimplifiedMNListEntry.fromP2PBufferReader = function fromP2PBufferReader(bufferR const object = {}; object.nVersion = bufferReader.readUInt16LE(); - object.proRegTxHash = bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'); - object.confirmedHash = bufferReader.readReverse(SHA256_HASH_SIZE).toString('hex'); + object.proRegTxHash = readExactReverse(bufferReader, SHA256_HASH_SIZE).toString('hex'); + object.confirmedHash = readExactReverse(bufferReader, SHA256_HASH_SIZE).toString('hex'); if (object.nVersion >= PROTX_EXT_ADDR_VERSION) { object.addresses = readExtNetInfo(bufferReader); } else { - object.service = parseIp(bufferReader.read(ipUtils.IP_AND_PORT_SIZE)); + object.service = parseIp(readExact(bufferReader, ipUtils.IP_AND_PORT_SIZE)); } - object.pubKeyOperator = bufferReader.read(BLS_PUBLIC_KEY_SIZE).toString('hex'); + object.pubKeyOperator = readExact(bufferReader, BLS_PUBLIC_KEY_SIZE).toString('hex'); object.votingAddress = Address.fromPublicKeyHash( - bufferReader.read(PUBKEY_ID_SIZE), + readExact(bufferReader, PUBKEY_ID_SIZE), network ).toString(); object.isValid = Boolean(bufferReader.readUInt8()); @@ -163,7 +164,7 @@ SimplifiedMNListEntry.fromP2PBufferReader = function fromP2PBufferReader(bufferR if (object.nVersion < PROTX_EXT_ADDR_VERSION) { object.platformHTTPPort = bufferReader.readUInt16LE(); } - object.platformNodeID = bufferReader.readReverse(PLATFORM_NODE_ID_SIZE).toString('hex'); + object.platformNodeID = readExactReverse(bufferReader, PLATFORM_NODE_ID_SIZE).toString('hex'); } } diff --git a/lib/util/readVarintCount.js b/lib/util/readVarintCount.js index 3e11254a0..7ffbc4562 100644 --- a/lib/util/readVarintCount.js +++ b/lib/util/readVarintCount.js @@ -1,14 +1,17 @@ +const { readCompactSize } = require('../transaction/payload/strictpayloadreader'); + /** - * Reads a peer-controlled item count and checks that the remaining data can - * hold that many items. BufferReader.read() returns empty slices past the end, - * so without this check a huge count would make the caller loop for ages. + * Reads a peer-controlled item count the way Core does (shortest CompactSize + * encoding, at most MAX_SIZE) and checks that the remaining data can hold that + * many items. BufferReader.read() returns empty slices past the end, so without + * this check a huge count would make the caller loop for ages. * @param {BufferReader} bufferReader * @param {number} minItemSize - smallest serialized size of one item, in bytes * @param {string} name - what is counted, for the error message * @return {number} */ function readVarintCount(bufferReader, minItemSize, name) { - const count = bufferReader.readVarintNum(); + const count = readCompactSize(bufferReader); if (count * minItemSize > bufferReader.buf.length - bufferReader.pos) { throw new Error(`${name} count ${count} is larger than the remaining data allows`); } diff --git a/test/block/PartialMerkleTree.js b/test/block/PartialMerkleTree.js index edd7afcfb..c72226f48 100644 --- a/test/block/PartialMerkleTree.js +++ b/test/block/PartialMerkleTree.js @@ -94,11 +94,11 @@ describe('PartialMerkleTree', function () { it('Should throw quickly on a hash count larger than the data', function () { this.timeout(2000); // the hash count (04) follows the 4-byte transaction count - var hex = coreTreeHex.slice(0, 8) + 'feffffffff' + coreTreeHex.slice(10); + var hex = coreTreeHex.slice(0, 8) + 'fe00000002' + coreTreeHex.slice(10); expect(function () { PartialMerkleTree.fromHexString(hex); - }).to.throw('Merkle hash count 4294967295 is larger than the remaining data allows'); + }).to.throw('Merkle hash count 33554432 is larger than the remaining data allows'); }); }); }); diff --git a/test/deterministicmnlist/QuorumEntry.js b/test/deterministicmnlist/QuorumEntry.js index 6d565553a..20fcafbd0 100644 --- a/test/deterministicmnlist/QuorumEntry.js +++ b/test/deterministicmnlist/QuorumEntry.js @@ -127,6 +127,13 @@ describe('QuorumEntry', function () { QuorumEntry.fromP2PBufferReader(reader, { 107: { size: 50 } }); }).to.throw('Out-of-range bits set'); }); + + it('Should throw if the commitment is cut off inside a signature', function () { + var hex = quorumEntryHex.slice(0, quorumEntryHex.length - 20); + expect(function () { + QuorumEntry.fromP2PBufferReader(new BufferReader(Buffer.from(hex, 'hex'))); + }).to.throw('expected 96 more bytes, found 86'); + }); }); describe('to buffer', function () { it('Should be able to generate correct buffer', function () { @@ -177,6 +184,20 @@ describe('QuorumEntry', function () { Buffer.from(commitmentHash, 'hex') ); }); + it('Should reject validMembers that do not fit the LLMQ size', function () { + var long = new QuorumEntry(quorumEntryJSON); + long.validMembers += '00'; + expect(function () { + long.getCommitmentHash(); + }).to.throw('Expected validMembers to have the LLMQ bit set length'); + + // a 12-member bit set uses the low 4 bits of its last byte + var outOfRange = new QuorumEntry(quorumEntryJSON); + outOfRange.validMembers = 'ff1f'; + expect(function () { + outOfRange.getCommitmentHash(); + }).to.throw('Expected no bits set past the LLMQ size'); + }); }); describe('quorum members', function () { it('Should generate the correct selectionModifier', function () { diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index f7e92a305..94c1c3816 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -153,18 +153,38 @@ describe('SimplifiedMNListDiff', function () { 'Quorum chainlock signature', 'Quorum index', ].forEach(function (name) { - it('Should throw quickly on a huge ' + name + ' count', function () { - this.timeout(2000); + function craftCount(count) { var position = offsets[name] * 2; - var hex = mnListDiffHexString.slice(0, position) + 'feffffffff' + return mnListDiffHexString.slice(0, position) + count + mnListDiffHexString.slice(position + 2); + } + it('Should throw quickly on a huge ' + name + ' count', function () { + this.timeout(2000); + // MAX_SIZE, the largest count Core reads expect(function () { - SimplifiedMNListDiff.fromHexString(hex, 'testnet'); - }).to.throw(name + ' count 4294967295 is larger than the remaining data allows'); + SimplifiedMNListDiff.fromHexString(craftCount('fe00000002'), 'testnet'); + }).to.throw(name + ' count 33554432 is larger than the remaining data allows'); + }); + it('Should reject a ' + name + ' count above MAX_SIZE or not shortest-encoded', function () { + this.timeout(2000); + expect(function () { + SimplifiedMNListDiff.fromHexString(craftCount('feffffffff'), 'testnet'); + }).to.throw('CompactSize too large'); + expect(function () { + SimplifiedMNListDiff.fromHexString(craftCount('fd0100'), 'testnet'); + }).to.throw('non-canonical CompactSize'); }); }); }); + it('Should throw if the diff is cut off inside a fixed-size field', function () { + // nVersion, baseBlockHash, then half of blockHash + var hex = mnListDiffHexString.slice(0, (2 + 32 + 16) * 2); + + expect(function () { + SimplifiedMNListDiff.fromHexString(hex, 'testnet'); + }).to.throw('expected 32 more bytes, found 16'); + }); describe('with a crafted quorum bit set size', function () { // the first new quorum is a version 3 llmq_50_60 commitment: version (2 bytes), // llmqType (1), quorumHash (32), then the signers bit set size, 0x32 (50) @@ -188,7 +208,7 @@ describe('SimplifiedMNListDiff', function () { it('Should throw if it differs from the size of a known quorum type', function () { this.timeout(2000); - [hugeSize, '31'].forEach(function (size) { + ['fe00000002', '31'].forEach(function (size) { expect(function () { SimplifiedMNListDiff.fromHexString(craft('01', size), 'testnet'); }).to.throw('Expected a bit set of 50 bits, got'); @@ -196,9 +216,30 @@ describe('SimplifiedMNListDiff', function () { }); it('Should throw if it is longer than the data of an unknown quorum type', function () { this.timeout(2000); + // MAX_SIZE bits expect(function () { - SimplifiedMNListDiff.fromHexString(craft('c8', hugeSize), 'testnet'); - }).to.throw('Bit set of 9007199254740992 bits is longer than the remaining data'); + SimplifiedMNListDiff.fromHexString(craft('c8', 'fe00000002'), 'testnet'); + }).to.throw('expected 4194304 more bytes'); + }); + it('Should round-trip a custom quorum type through fromJSON with its params', function () { + var override = { 200: { size: 50, threshold: 30, maximumActiveQuorumsCount: 24 } }; + var hex = craft('c8', '32'); + var diff = SimplifiedMNListDiff.fromHexString(hex, 'testnet', override); + var copy = SimplifiedMNListDiff.fromJSON(JSON.parse(JSON.stringify(diff)), override); + + expect(diff.newQuorums[0].llmqType).to.be.equal(200); + expect(copy.toBuffer().toString('hex')).to.be.equal(hex); + }); + it('Should reject a size above MAX_SIZE or not shortest-encoded', function () { + this.timeout(2000); + ['01', 'c8'].forEach(function (llmqType) { + expect(function () { + SimplifiedMNListDiff.fromHexString(craft(llmqType, hugeSize), 'testnet'); + }).to.throw('CompactSize too large'); + expect(function () { + SimplifiedMNListDiff.fromHexString(craft(llmqType, 'fd3200'), 'testnet'); + }).to.throw('non-canonical CompactSize'); + }); }); }); }); @@ -225,6 +266,14 @@ describe('SimplifiedMNListDiff', function () { ['a short signature', { abcd: [1] }, 'Expected quorumsCLSigs keys to be bls signatures'], ['indexes that are not an array', { [signatureHex]: 1 }, 'Expected quorumsCLSigs values to be arrays of quorum indexes'], + ['an index above 65535', { [signatureHex]: [1, 65536] }, + 'Expected quorumsCLSigs values to be arrays of quorum indexes'], + ['a negative index', { [signatureHex]: [-1] }, + 'Expected quorumsCLSigs values to be arrays of quorum indexes'], + ['a fractional index', { [signatureHex]: [1.5] }, + 'Expected quorumsCLSigs values to be arrays of quorum indexes'], + ['a string index', { [signatureHex]: ['1'] }, + 'Expected quorumsCLSigs values to be arrays of quorum indexes'], ].forEach(function (testCase) { it('Should reject ' + testCase[0], function () { var diff = new SimplifiedMNListDiff(mnListDiffJSON); diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index 01e8ec5ca..3c1ddfa7b 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -213,6 +213,17 @@ describe('SimplifiedMNListEntry', function () { expect(buffer.toString('hex')).to.be.equal(hex); }); + it('Should throw if the entry is cut off inside a fixed-size field', function () { + // drop the last 10 bytes of the platformNodeID + var hex = '0200' + smlEntryHex.slice(0, smlEntryHex.length - 20); + + expect(function () { + SimplifiedMNListEntry.fromP2PBufferReader( + new BufferReader(Buffer.from(hex, 'hex')), + Networks.testnet + ); + }).to.throw('expected 20 more bytes, found 10'); + }); }); describe('fromObject', function () { it('Should create an instance from a JSONObject', function () { From 3eee05c857c5fdde072acc22375b7eb8f41d0e40 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 00:31:45 +0000 Subject: [PATCH 10/21] fix: keep the ExtNetInfo bytes SML entries were read from A BIP155 IPv6 address in fc00::/8 (network id 2, valid on regtest) prints the same as a CJDNS address (network id 6), and extNetInfoToBuffer writes that string as CJDNS. A version 3 entry read from P2P with such an address therefore re-serialized, and hashed, differently from Core's bytes. fromP2PBufferReader now keeps the ExtNetInfo bytes with the addresses they decoded to, in a non-enumerable field that toObject() and JSON leave out and copy() (also the diff's) carries over. toBuffer and toP2PBuffer reuse those bytes while the addresses are unchanged. Core's JSON can't tell the two apart, so entries from JSON keep the string rule. Co-Authored-By: Claude Opus 5.5 --- .../SimplifiedMNListDiff.js | 5 +- .../SimplifiedMNListEntry.js | 53 +++++++++++++++++-- .../SimplifiedMNListDiff.js | 14 +++++ .../SimplifiedMNListEntry.js | 46 ++++++++++++++++ 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index d692600a3..df603f186 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -349,7 +349,10 @@ SimplifiedMNListDiff.prototype.toObject = function toObject() { }; SimplifiedMNListDiff.prototype.copy = function copy() { - return _.cloneDeep(this); + // entries copy themselves to keep the ExtNetInfo bytes they were read from + return _.cloneDeepWith(this, (value) => ( + value instanceof SimplifiedMNListEntry ? value.copy() : undefined + )); }; module.exports = SimplifiedMNListDiff; diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index 3d4c7e51c..dbc8cf652 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -87,6 +87,39 @@ function SimplifiedMNListEntry(arg, network) { } } +/** + * Keeps the ExtNetInfo bytes an entry was read from, with the addresses they + * decoded to. A BIP155 IPv6 address in fc00::/8 (valid on regtest) prints the + * same as a CJDNS one and extNetInfoToBuffer writes that string as CJDNS, so + * re-encoding could change the entry's bytes and hash. Core's JSON can't tell + * the two apart either, so entries from JSON keep the string rule. + * Non-enumerable, so toObject() and JSON.stringify() leave it out. + * @param {SimplifiedMNListEntry} entry + * @param {{buffer: Buffer, addresses: Object}} rawExtNetInfo + */ +function setRawExtNetInfo(entry, rawExtNetInfo) { + Object.defineProperty(entry, '_rawExtNetInfo', { + value: rawExtNetInfo, + writable: true, + configurable: true, + enumerable: false, + }); +} + +/** + * The entry's ExtNetInfo bytes: the ones it was read from while its addresses + * are unchanged, otherwise its addresses serialized + * @param {SimplifiedMNListEntry} entry + * @return {Buffer} + */ +function getExtNetInfoBuffer(entry) { + const raw = entry._rawExtNetInfo; + if (raw && _.isEqual(raw.addresses, entry.addresses)) { + return raw.buffer; + } + return extNetInfoToBuffer(entry.addresses); +} + /** * Parse buffer and returns SimplifiedMNListEntry * @param {Buffer} buffer @@ -143,8 +176,14 @@ SimplifiedMNListEntry.fromP2PBufferReader = function fromP2PBufferReader(bufferR object.nVersion = bufferReader.readUInt16LE(); object.proRegTxHash = readExactReverse(bufferReader, SHA256_HASH_SIZE).toString('hex'); object.confirmedHash = readExactReverse(bufferReader, SHA256_HASH_SIZE).toString('hex'); + let rawExtNetInfo; if (object.nVersion >= PROTX_EXT_ADDR_VERSION) { + const start = bufferReader.pos; object.addresses = readExtNetInfo(bufferReader); + rawExtNetInfo = { + buffer: Buffer.from(bufferReader.buf.slice(start, bufferReader.pos)), + addresses: _.cloneDeep(object.addresses), + }; } else { object.service = parseIp(readExact(bufferReader, ipUtils.IP_AND_PORT_SIZE)); } @@ -168,7 +207,11 @@ SimplifiedMNListEntry.fromP2PBufferReader = function fromP2PBufferReader(bufferR } } - return SimplifiedMNListEntry.fromObject(object); + const entry = SimplifiedMNListEntry.fromObject(object); + if (rawExtNetInfo) { + setRawExtNetInfo(entry, rawExtNetInfo); + } + return entry; }; /** @@ -192,7 +235,7 @@ SimplifiedMNListEntry.prototype.toBuffer = function toBuffer() { bufferWriter.write(Buffer.from(this.proRegTxHash, 'hex').reverse()); bufferWriter.write(Buffer.from(this.confirmedHash, 'hex').reverse()); if (hasExtAddr) { - bufferWriter.write(extNetInfoToBuffer(this.addresses)); + bufferWriter.write(getExtNetInfoBuffer(this)); } else { bufferWriter.write(serializeIp(this.service)); } @@ -439,7 +482,11 @@ SimplifiedMNListEntry.prototype.confirmedHashWithProRegTxHash = * @return {SimplifiedMNListEntry} */ SimplifiedMNListEntry.prototype.copy = function copy() { - return _.cloneDeep(this); + const entry = _.cloneDeep(this); + if (this._rawExtNetInfo) { + setRawExtNetInfo(entry, this._rawExtNetInfo); + } + return entry; }; module.exports = SimplifiedMNListEntry; diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index 94c1c3816..f2a219179 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -412,6 +412,20 @@ describe('SimplifiedMNListDiff', function () { }); }); describe('copy', function () { + it('Should keep the ExtNetInfo bytes entries were read from', function () { + var vector = coreDiffs.testnetFull; + var diff = SimplifiedMNListDiff.fromHexString(vector.hex, 'testnet'); + var copy = diff.copy(); + var extAddrEntries = copy.mnList.filter(function (entry) { + return entry.nVersion >= 3; + }); + + expect(extAddrEntries).to.not.be.empty; + extAddrEntries.forEach(function (entry) { + expect(entry._rawExtNetInfo).to.be.an('object'); + }); + expect(copy.toBuffer().toString('hex')).to.be.equal(vector.hex); + }); it('Should create a detached copy of an instance', function () { var instance = new SimplifiedMNListDiff(mnListDiffJSON); var copy = new SimplifiedMNListDiff(instance); diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index 3c1ddfa7b..24739e441 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -6,6 +6,7 @@ var _ = require('lodash'); var expect = require('chai').expect; var Networks = require('../../lib/networks'); var BufferReader = require('../../lib/encoding/bufferreader'); +var Hash = require('../../lib/crypto/hash'); var smlEntryJSON = { proRegTxHash: @@ -224,6 +225,51 @@ describe('SimplifiedMNListEntry', function () { ); }).to.throw('expected 20 more bytes, found 10'); }); + describe('a BIP155 IPv6 address in fc00::/8', function () { + // extAddrRegularEntryHex with its IPv4 core_p2p replaced by fc00::1 under + // BIP155 network id 2 (IPv6), which prints like CJDNS (network id 6) + var ipv4NetInfo = '010100010101044e1146af4dbb'; + var ipv6NetInfo = '01010001010210fc0000000000000000000000000000014e1f'; + var hashedHex = extAddrRegularEntryHex.replace(ipv4NetInfo, ipv6NetInfo); + var p2pHex = '0300' + hashedHex; + var entry; + + beforeEach(function () { + expect(hashedHex).to.not.equal(extAddrRegularEntryHex); + entry = SimplifiedMNListEntry.fromP2PBufferReader( + new BufferReader(Buffer.from(p2pHex, 'hex')), + Networks.testnet + ); + }); + + it('Should keep the bytes it was read from', function () { + expect(entry.addresses).to.be.deep.equal({ core_p2p: ['[fc00::1]:19999'] }); + expect(entry.toP2PBuffer().toString('hex')).to.be.equal(p2pHex); + expect(entry.calculateHash()).to.be.deep.equal( + Hash.sha256sha256(Buffer.from(hashedHex, 'hex')) + ); + expect(entry.copy().toP2PBuffer().toString('hex')).to.be.equal(p2pHex); + expect(new SimplifiedMNListEntry(entry).toP2PBuffer().toString('hex')) + .to.be.equal(p2pHex); + }); + it('Should leave the bytes out of toObject and JSON', function () { + expect(entry.toObject()).to.not.have.property('_rawExtNetInfo'); + expect(JSON.stringify(entry)).to.not.include('_rawExtNetInfo'); + }); + it('Should re-encode changed addresses, and JSON input, by the string rule', function () { + var cjdnsHex = '0300' + extAddrRegularEntryHex.replace( + ipv4NetInfo, + '01010001010610fc0000000000000000000000000000014e1f' + ); + expect(new SimplifiedMNListEntry(entry.toObject()).toP2PBuffer().toString('hex')) + .to.be.equal(cjdnsHex); + + entry.addresses.core_p2p = ['[fc00::1]:9999']; + expect(entry.toP2PBuffer().toString('hex')).to.be.equal( + cjdnsHex.replace('fc0000000000000000000000000000014e1f', 'fc000000000000000000000000000001270f') + ); + }); + }); }); describe('fromObject', function () { it('Should create an instance from a JSONObject', function () { From 26fdd174d7904ddd5eb302f5f38c49fd8cab5d8f Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 01:20:59 +0000 Subject: [PATCH 11/21] fix: require Core-canonical coinbase and chainlock sets in diffs The coinbase must serialize back to the bytes it was read from, and each chainlock signature's quorum indexes must be a sorted set with no repeated signature, as Core's map of sets always is. Co-Authored-By: Claude Opus 5.5 --- .../SimplifiedMNListDiff.js | 28 +++++++++++ .../SimplifiedMNListDiff.js | 50 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index df603f186..99661d9d1 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -21,6 +21,14 @@ const MIN_SML_ENTRY_SIZE = 2 + 32 + 32 + 2 + 48 + 20 + 1; // quorumVvecHash, quorumSig and membersSig const MIN_QUORUM_ENTRY_SIZE = 2 + 1 + 32 + 1 + 1 + 48 + 32 + 96 + 96; +/** + * @param {number[]} values + * @return {boolean} + */ +function isStrictlyIncreasing(values) { + return values.every((value, i) => i === 0 || values[i - 1] < value); +} + /** * @param {Buffer|Object|string} [arg] - A Buffer, JSON string, or Object representing a MnListDiff * @param {string} [network] @@ -79,7 +87,13 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam data.blockHash = readExactReverse(bufferReader, constants.SHA256_HASH_SIZE).toString('hex'); data.cbTxMerkleTree = PartialMerkleTree.fromBufferReader(bufferReader); + const cbTxStart = bufferReader.pos; data.cbTx = new Transaction().fromBufferReader(bufferReader); + // The transaction parser accepts non-canonical CompactSizes, which Core + // rejects; a canonical coinbase serializes back to exactly what was read + if (!data.cbTx.toBuffer().equals(bufferReader.buf.slice(cbTxStart, bufferReader.pos))) { + throw new Error('Non-canonical coinbase transaction encoding'); + } const deletedMNsCount = readVarintCount( bufferReader, @@ -131,14 +145,24 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam 'Quorum chainlock signature' ); data.quorumsCLSigs = []; + const seenSignatures = new Set(); for (let i = 0; i < quorumsCLSigsCount; i += 1) { const signature = readExact(bufferReader, constants.BLS_SIGNATURE_SIZE) .toString('hex'); + // Core holds these in a map of sets, so a repeated signature or an index + // out of ascending order can't come from Core and wouldn't round-trip + if (seenSignatures.has(signature)) { + throw new Error('Duplicate quorum chainlock signature'); + } + seenSignatures.add(signature); const indexesCount = readVarintCount(bufferReader, 2, 'Quorum index'); const indexes = []; for (let j = 0; j < indexesCount; j += 1) { indexes.push(bufferReader.readUInt16LE()); } + if (!isStrictlyIncreasing(indexes)) { + throw new Error('Quorum chainlock indexes are not a sorted set'); + } data.quorumsCLSigs.push({ [signature]: indexes }); } @@ -228,6 +252,10 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { && indexes.every((index) => Number.isInteger(index) && index >= 0 && index <= 0xffff), 'Expected quorumsCLSigs values to be arrays of quorum indexes' ); + $.checkArgument( + isStrictlyIncreasing(indexes), + 'Expected quorumsCLSigs indexes to be sorted and unique, as Core sends them' + ); bufferWriter.write(Buffer.from(signature, 'hex')); bufferWriter.writeVarintNum(indexes.length); indexes.forEach((index) => bufferWriter.writeUInt16LE(index)); diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index f2a219179..9c25ed91f 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -274,6 +274,10 @@ describe('SimplifiedMNListDiff', function () { 'Expected quorumsCLSigs values to be arrays of quorum indexes'], ['a string index', { [signatureHex]: ['1'] }, 'Expected quorumsCLSigs values to be arrays of quorum indexes'], + ['unsorted indexes', { [signatureHex]: [2, 1] }, + 'Expected quorumsCLSigs indexes to be sorted and unique, as Core sends them'], + ['a repeated index', { [signatureHex]: [1, 1] }, + 'Expected quorumsCLSigs indexes to be sorted and unique, as Core sends them'], ].forEach(function (testCase) { it('Should reject ' + testCase[0], function () { var diff = new SimplifiedMNListDiff(mnListDiffJSON); @@ -285,6 +289,52 @@ describe('SimplifiedMNListDiff', function () { }); }); }); + describe('fromBuffer canonical encodings', function () { + // a diff with one chainlock signature: serialize, then edit the trailing bytes + function withCLSigs(clSigs) { + var diff = new SimplifiedMNListDiff(mnListDiffJSON); + diff.quorumsCLSigs = []; + var head = diff.toBuffer().slice(0, -1); // drop the zero quorumsCLSigs count + return Buffer.concat([head, clSigs]); + } + var signature = Buffer.from(Object.keys(coreDiffsJSON[0].quorumsCLSigs[0])[0], 'hex'); + + it('Should reject chainlock indexes that are not a sorted set', function () { + [[2, 1], [1, 1]].forEach(function (pair) { + var indexes = Buffer.alloc(4); + indexes.writeUInt16LE(pair[0], 0); + indexes.writeUInt16LE(pair[1], 2); + var raw = withCLSigs(Buffer.concat([Buffer.from('01', 'hex'), signature, Buffer.from('02', 'hex'), indexes])); + expect(function () { + SimplifiedMNListDiff.fromBuffer(raw, Networks.testnet); + }).to.throw('Quorum chainlock indexes are not a sorted set'); + }); + }); + + it('Should reject a repeated chainlock signature', function () { + var entry = Buffer.concat([signature, Buffer.from('00', 'hex')]); + var raw = withCLSigs(Buffer.concat([Buffer.from('02', 'hex'), entry, entry])); + expect(function () { + SimplifiedMNListDiff.fromBuffer(raw, Networks.testnet); + }).to.throw('Duplicate quorum chainlock signature'); + }); + + it('Should reject a coinbase transaction with a non-canonical CompactSize', function () { + var diff = new SimplifiedMNListDiff(mnListDiffJSON); + diff.quorumsCLSigs = []; + var raw = diff.toBuffer(); + var cbTx = diff.cbTx.toBuffer(); + var cbStart = raw.indexOf(cbTx); + expect(cbStart).to.be.above(0); + // the input count follows the 4-byte version and type; re-encode 1 as fd0100 + expect(cbTx[4]).to.equal(1); + var nonCanonicalTx = Buffer.concat([cbTx.slice(0, 4), Buffer.from('fd0100', 'hex'), cbTx.slice(5)]); + var tampered = Buffer.concat([raw.slice(0, cbStart), nonCanonicalTx, raw.slice(cbStart + cbTx.length)]); + expect(function () { + SimplifiedMNListDiff.fromBuffer(tampered, Networks.testnet); + }).to.throw('Non-canonical coinbase transaction encoding'); + }); + }); describe('Core P2P mnlistdiff messages', function () { Object.keys(coreDiffs).filter(function (name) { return name !== 'note'; From a397e0b2b41a13fe5c38db65f1a168954d455904 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 02:38:12 +0000 Subject: [PATCH 12/21] fix: reject repeated or sparse chainlock entries and type LLMQ overrides The diff writer accepted one signature in two entries and index arrays with holes, neither of which the reader takes back; the typings now expose llmqParamsOverride and fromJSON. Co-Authored-By: Claude Opus 5.5 --- .../SimplifiedMNListDiff.js | 9 +++++- test-d/index.test-d.ts | 7 +++++ .../SimplifiedMNListDiff.js | 11 +++++++ .../SimplifiedMNListDiff.d.ts | 29 ++++++++++++++++--- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 99661d9d1..d4e508ba4 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -235,6 +235,7 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { const quorumsCLSigs = this.quorumsCLSigs || []; bufferWriter.writeVarintNum(quorumsCLSigs.length); + const writtenSignatures = new Set(); quorumsCLSigs.forEach((quorumsCLSig) => { const signatures = _.isPlainObject(quorumsCLSig) ? Object.keys(quorumsCLSig) : []; $.checkArgument( @@ -247,9 +248,15 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { isHexStringOfSize(signature, constants.BLS_SIGNATURE_SIZE * 2), 'Expected quorumsCLSigs keys to be bls signatures' ); + $.checkArgument( + !writtenSignatures.has(signature.toLowerCase()), + 'Expected each quorumsCLSigs signature to appear once' + ); + writtenSignatures.add(signature.toLowerCase()); + // Array.from turns holes into undefined, which every() would otherwise skip $.checkArgument( Array.isArray(indexes) - && indexes.every((index) => Number.isInteger(index) && index >= 0 && index <= 0xffff), + && Array.from(indexes).every((index) => Number.isInteger(index) && index >= 0 && index <= 0xffff), 'Expected quorumsCLSigs values to be arrays of quorum indexes' ); $.checkArgument( diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index db2eeefb9..076884681 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -124,3 +124,10 @@ expectType(smlEntry.toP2PBuffer()); declare const smlDiff: SimplifiedMNListDiff; expectType[] | undefined>(smlDiff.quorumsCLSigs); expectType<{ host: string; port: number } | null>(smlEntry.getPlatformHTTPSEndpoint()); +declare const SMLDiffClass: typeof SimplifiedMNListDiff; +const customLLMQ = { 200: { size: 3 } }; +expectType(SMLDiffClass.fromBuffer(Buffer.alloc(0), 'testnet', customLLMQ)); +expectType(SMLDiffClass.fromHexString('', 'testnet', customLLMQ)); +expectType(SMLDiffClass.fromObject({}, 'testnet', customLLMQ)); +expectType(SMLDiffClass.fromJSON({}, customLLMQ)); +expectType(new SMLDiffClass({}, 'testnet', customLLMQ)); diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index 9c25ed91f..44e782506 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -278,6 +278,9 @@ describe('SimplifiedMNListDiff', function () { 'Expected quorumsCLSigs indexes to be sorted and unique, as Core sends them'], ['a repeated index', { [signatureHex]: [1, 1] }, 'Expected quorumsCLSigs indexes to be sorted and unique, as Core sends them'], + // eslint-disable-next-line no-sparse-arrays + ['a sparse index array', { [signatureHex]: [0, , 2] }, + 'Expected quorumsCLSigs values to be arrays of quorum indexes'], ].forEach(function (testCase) { it('Should reject ' + testCase[0], function () { var diff = new SimplifiedMNListDiff(mnListDiffJSON); @@ -289,6 +292,14 @@ describe('SimplifiedMNListDiff', function () { }); }); }); + it('Should reject a chainlock signature that appears in two entries', function () { + var diff = new SimplifiedMNListDiff(mnListDiffJSON); + var signatureHex = Object.keys(coreDiffsJSON[0].quorumsCLSigs[0])[0]; + diff.quorumsCLSigs = [{ [signatureHex]: [0] }, { [signatureHex.toUpperCase()]: [1] }]; + expect(function () { + diff.toBuffer(); + }).to.throw('Expected each quorumsCLSigs signature to appear once'); + }); describe('fromBuffer canonical encodings', function () { // a diff with one chainlock signature: serialize, then edit the trailing bytes function withCLSigs(clSigs) { diff --git a/typings/deterministicmnlist/SimplifiedMNListDiff.d.ts b/typings/deterministicmnlist/SimplifiedMNListDiff.d.ts index 3aa03f9a6..c0fb21fd9 100644 --- a/typings/deterministicmnlist/SimplifiedMNListDiff.d.ts +++ b/typings/deterministicmnlist/SimplifiedMNListDiff.d.ts @@ -19,6 +19,7 @@ export var validMNs: SimplifiedMNListEntry[]; /** * @param {Buffer|Object|string} [arg] - A Buffer, JSON string, or Object representing a MnListDiff * @param {string} [network] + * @param {Object} [llmqParamsOverride] - params for LLMQ types dashcore-lib doesn't know * @class SimplifiedMNListDiff * @property {string} baseBlockHash - sha256 * @property {string} blockHash - sha256 @@ -30,25 +31,32 @@ export var validMNs: SimplifiedMNListEntry[]; * @property {string} merkleRootMNList - merkle root of the whole mn list */ export class SimplifiedMNListDiff { - constructor(arg?: Buffer | any | string, network?: string); + constructor(arg?: Buffer | any | string, network?: string, llmqParamsOverride?: Object); /** * Creates MnListDiff from a Buffer holding Core's P2P `mnlistdiff` message * (protocol 70230 and later, unchanged through 70242). * @param {Buffer} buffer * @param {string} [network] + * @param {Object} [llmqParamsOverride] * @return {SimplifiedMNListDiff} */ - static fromBuffer(buffer: Buffer, network?: string): SimplifiedMNListDiff; + static fromBuffer( + buffer: Buffer, + network?: string, + llmqParamsOverride?: Object + ): SimplifiedMNListDiff; /** * @param {string} hexString * @param {string} [network] + * @param {Object} [llmqParamsOverride] * @return {SimplifiedMNListDiff} */ static fromHexString( hexString: string, - network?: string + network?: string, + llmqParamsOverride?: Object ): SimplifiedMNListDiff; /** @@ -62,9 +70,22 @@ export class SimplifiedMNListDiff { * Creates MNListDiff from object * @param obj * @param {string} [network] + * @param {Object} [llmqParamsOverride] + * @return {SimplifiedMNListDiff} + */ + static fromObject( + obj: any, + network?: string, + llmqParamsOverride?: Object + ): SimplifiedMNListDiff; + + /** + * Creates MNListDiff from the JSON produced by JSON.stringify, not from toObject() + * @param diffJSON + * @param {Object} [llmqParamsOverride] * @return {SimplifiedMNListDiff} */ - static fromObject(obj: any, network?: string): SimplifiedMNListDiff; + static fromJSON(diffJSON: any, llmqParamsOverride?: Object): SimplifiedMNListDiff; /** * sha256 From dafe3409d6bf4059500bf9e1c47df0692046861d Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 03:59:35 +0000 Subject: [PATCH 13/21] fix: reject sparse quorumsCLSigs arrays when serializing diffs forEach skipped holes, so fewer entries were written than the count said. Co-Authored-By: Claude Opus 5.5 --- lib/deterministicmnlist/SimplifiedMNListDiff.js | 4 +++- test/deterministicmnlist/SimplifiedMNListDiff.js | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index d4e508ba4..987702f97 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -233,7 +233,9 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { bufferWriter.write(quorumEntry.toBufferForHashing()); }); - const quorumsCLSigs = this.quorumsCLSigs || []; + // Array.from turns holes into undefined, so a sparse array fails the entry + // check below instead of writing fewer entries than its count + const quorumsCLSigs = Array.from(this.quorumsCLSigs || []); bufferWriter.writeVarintNum(quorumsCLSigs.length); const writtenSignatures = new Set(); quorumsCLSigs.forEach((quorumsCLSig) => { diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index 44e782506..e0bf11d5d 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -292,6 +292,15 @@ describe('SimplifiedMNListDiff', function () { }); }); }); + it('Should reject a sparse quorumsCLSigs array', function () { + var diff = new SimplifiedMNListDiff(mnListDiffJSON); + var signatureHex = Object.keys(coreDiffsJSON[0].quorumsCLSigs[0])[0]; + // eslint-disable-next-line no-sparse-arrays + diff.quorumsCLSigs = [, { [signatureHex]: [0] }]; + expect(function () { + diff.toBuffer(); + }).to.throw('Expected each quorumsCLSigs entry to hold exactly one signature'); + }); it('Should reject a chainlock signature that appears in two entries', function () { var diff = new SimplifiedMNListDiff(mnListDiffJSON); var signatureHex = Object.keys(coreDiffsJSON[0].quorumsCLSigs[0])[0]; From 9544c6ebdbb02bb04ea2a8f74f639de46356f29d Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 05:04:38 +0000 Subject: [PATCH 14/21] fix: require dense arrays for every vector a diff serializes Array.from on an array-like with a huge length allocated before any check, and forEach skipped holes in the other count-prefixed vectors. Co-Authored-By: Claude Opus 5.5 --- .../SimplifiedMNListDiff.js | 22 ++++++++++++++++--- .../SimplifiedMNListDiff.js | 15 ++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 987702f97..0ef611952 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -21,6 +21,16 @@ const MIN_SML_ENTRY_SIZE = 2 + 32 + 32 + 2 + 48 + 20 + 1; // quorumVvecHash, quorumSig and membersSig const MIN_QUORUM_ENTRY_SIZE = 2 + 1 + 32 + 1 + 1 + 48 + 32 + 96 + 96; +/** + * @param {*} value + * @return {boolean} + */ +function isDenseArray(value) { + // a hole leaves a key missing, so this rejects sparse arrays (of any length) + // without materializing them + return Array.isArray(value) && Object.keys(value).length === value.length; +} + /** * @param {number[]} values * @return {boolean} @@ -211,6 +221,14 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { bufferWriter.write(this.cbTxMerkleTree.toBuffer()); bufferWriter.write(this.cbTx.toBuffer()); + ['deletedMNs', 'mnList', 'deletedQuorums', 'newQuorums'].forEach((name) => { + $.checkArgument(isDenseArray(this[name]), `Expected ${name} to be an array without empty slots`); + }); + $.checkArgument( + this.quorumsCLSigs === undefined || isDenseArray(this.quorumsCLSigs), + 'Expected quorumsCLSigs to be an array without empty slots' + ); + bufferWriter.writeVarintNum(this.deletedMNs.length); this.deletedMNs.forEach((deleteMNHash) => { bufferWriter.write(Buffer.from(deleteMNHash, 'hex').reverse()); @@ -233,9 +251,7 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { bufferWriter.write(quorumEntry.toBufferForHashing()); }); - // Array.from turns holes into undefined, so a sparse array fails the entry - // check below instead of writing fewer entries than its count - const quorumsCLSigs = Array.from(this.quorumsCLSigs || []); + const quorumsCLSigs = this.quorumsCLSigs || []; bufferWriter.writeVarintNum(quorumsCLSigs.length); const writtenSignatures = new Set(); quorumsCLSigs.forEach((quorumsCLSig) => { diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index e0bf11d5d..8d39c7c54 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -299,7 +299,20 @@ describe('SimplifiedMNListDiff', function () { diff.quorumsCLSigs = [, { [signatureHex]: [0] }]; expect(function () { diff.toBuffer(); - }).to.throw('Expected each quorumsCLSigs entry to hold exactly one signature'); + }).to.throw('Expected quorumsCLSigs to be an array without empty slots'); + }); + it('Should reject array-like or huge sparse vectors without materializing them', function () { + ['deletedMNs', 'mnList', 'deletedQuorums', 'newQuorums', 'quorumsCLSigs'].forEach(function (name) { + [{ length: 1e9 }, new Array(1e9)].forEach(function (value) { + var diff = new SimplifiedMNListDiff(mnListDiffJSON); + diff[name] = value; + var started = Date.now(); + expect(function () { + diff.toBuffer(); + }).to.throw('Expected ' + name + ' to be an array without empty slots'); + expect(Date.now() - started).to.be.below(1000); + }); + }); }); it('Should reject a chainlock signature that appears in two entries', function () { var diff = new SimplifiedMNListDiff(mnListDiffJSON); From 9711be24de7ff4d7a94bc334f20b082d9b130a3a Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 05:07:24 +0000 Subject: [PATCH 15/21] fix: drop type and platform fields from the ProUpRegTx typings The payload has none of them and its constructor rejects them. Co-Authored-By: Claude Opus 5.5 --- test-d/index.test-d.ts | 5 +++++ .../transaction/payload/ProUpRegTxPayload.d.ts | 17 ----------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index 076884681..75e131b9a 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -131,3 +131,8 @@ expectType(SMLDiffClass.fromHexString('', 'testnet', custo expectType(SMLDiffClass.fromObject({}, 'testnet', customLLMQ)); expectType(SMLDiffClass.fromJSON({}, customLLMQ)); expectType(new SMLDiffClass({}, 'testnet', customLLMQ)); +// ProUpRegTx has no type or platform fields +// @ts-expect-error +proUpRegTxPayload.platformNodeID; +// @ts-expect-error +proUpRegTxPayload.type; diff --git a/typings/transaction/payload/ProUpRegTxPayload.d.ts b/typings/transaction/payload/ProUpRegTxPayload.d.ts index 96f51363d..48c6a590b 100644 --- a/typings/transaction/payload/ProUpRegTxPayload.d.ts +++ b/typings/transaction/payload/ProUpRegTxPayload.d.ts @@ -26,7 +26,6 @@ export type ProUpRegTransactionPayloadJSON = { * @class ProUpRegTxPayload * @property {number} version uint_16 2 Upgrade Provider Transaction version number. Currently set to 1. * @property {string} proTxHash uint256 32 The hash of the provider transaction - * @property {number} [type] * @property {number} mode uint_16 2 Masternode mode * @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. @@ -35,9 +34,6 @@ export type ProUpRegTransactionPayloadJSON = { * @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 - * @property {number} [platformP2PPort] Platform P2P port - * @property {number} [platformHTTPPort] Platform HTTP port@ * @property {string} payloadSig vector Variable Signature of the hash of the ProTx fields. Signed by the Owner. * * @param {ProUpRegTransactionPayloadJSON} [payloadJSON] @@ -98,7 +94,6 @@ export class ProUpRegTxPayload { * uint_16 2 Upgrade Provider Transaction version number. Currently set to 1. */ version: number; - type?: number; /** * uint256 32 The hash of the provider transaction */ @@ -135,18 +130,6 @@ export class ProUpRegTxPayload { * compactSize uint 1-9 Size of the Signature */ payloadSigSize: number; - /* - * Platform Node ID - */ - platformNodeID?: string; - /* - * Platform P2P port - */ - platformP2PPort?: number; - /* - * Platform HTTP port - */ - platformHTTPPort?: number; /** * vector Variable Signature of the hash of the ProTx fields. Signed by the Owner. */ From ab488a72307d0528d2f4e36cf82030cd8a41b539 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 05:43:12 +0000 Subject: [PATCH 16/21] fix: check chainlock index arrays without materializing them Array.from on a huge sparse index array allocated before the check rejected it. Co-Authored-By: Claude Opus 5.5 --- lib/deterministicmnlist/SimplifiedMNListDiff.js | 5 ++--- test/deterministicmnlist/SimplifiedMNListDiff.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 0ef611952..6d06ef159 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -271,10 +271,9 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { 'Expected each quorumsCLSigs signature to appear once' ); writtenSignatures.add(signature.toLowerCase()); - // Array.from turns holes into undefined, which every() would otherwise skip $.checkArgument( - Array.isArray(indexes) - && Array.from(indexes).every((index) => Number.isInteger(index) && index >= 0 && index <= 0xffff), + isDenseArray(indexes) + && indexes.every((index) => Number.isInteger(index) && index >= 0 && index <= 0xffff), 'Expected quorumsCLSigs values to be arrays of quorum indexes' ); $.checkArgument( diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index 8d39c7c54..c7a9200ce 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -314,6 +314,16 @@ describe('SimplifiedMNListDiff', function () { }); }); }); + it('Should reject a huge sparse chainlock index array without materializing it', function () { + var diff = new SimplifiedMNListDiff(mnListDiffJSON); + var signatureHex = Object.keys(coreDiffsJSON[0].quorumsCLSigs[0])[0]; + diff.quorumsCLSigs = [{ [signatureHex]: new Array(1e9) }]; + var started = Date.now(); + expect(function () { + diff.toBuffer(); + }).to.throw('Expected quorumsCLSigs values to be arrays of quorum indexes'); + expect(Date.now() - started).to.be.below(1000); + }); it('Should reject a chainlock signature that appears in two entries', function () { var diff = new SimplifiedMNListDiff(mnListDiffJSON); var signatureHex = Object.keys(coreDiffsJSON[0].quorumsCLSigs[0])[0]; From 3dc8559d11f88acde4abb731d30b191345824ab7 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 07:27:19 +0000 Subject: [PATCH 17/21] fix: check every count-prefixed vector is dense before serializing it Serializers write a vector's length and then forEach over it, which skips empty slots, so a sparse array wrote a count with fewer items. PartialMerkleTree.toBuffer (merkleHashes, merkleFlags) and extNetInfoToBuffer (each purpose's address list) had no check, and the diff's check accepted an array whose missing index was made up for by an extra property (`a.length = 1; a.extra = true`). isDenseArray moves to lib/util and now requires the own enumerable keys to be exactly '0'..String(length - 1), still without materializing anything from length. The diff, PartialMerkleTree and extNetInfoToBuffer check with it before writing each count. Co-Authored-By: Claude Opus 5.5 --- lib/block/PartialMerkleTree.js | 11 +++++++ .../SimplifiedMNListDiff.js | 11 +------ lib/util/isDenseArray.js | 18 ++++++++++ lib/util/netinfo.js | 6 ++-- test/block/PartialMerkleTree.js | 19 +++++++++++ .../SimplifiedMNListDiff.js | 13 ++++++++ test/util/isDenseArray.js | 33 +++++++++++++++++++ test/util/netinfo.js | 15 +++++++++ 8 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 lib/util/isDenseArray.js create mode 100644 test/util/isDenseArray.js diff --git a/lib/block/PartialMerkleTree.js b/lib/block/PartialMerkleTree.js index e6d74dfe3..3414b288c 100644 --- a/lib/block/PartialMerkleTree.js +++ b/lib/block/PartialMerkleTree.js @@ -7,6 +7,8 @@ const constants = require('../constants'); const MerkleTreeUtils = require('../util/merkletree'); const BitArrayUtils = require('../util/bitarray'); const readVarintCount = require('../util/readVarintCount'); +const isDenseArray = require('../util/isDenseArray'); +const $ = require('../util/preconditions'); const { readExactReverse } = require('../transaction/payload/strictpayloadreader'); const { convertBitArrayToUInt8Array } = BitArrayUtils; @@ -112,6 +114,15 @@ PartialMerkleTree.prototype.toBuffer = function toBuffer() { bufferWriter.writeUInt32LE(this.totalTransactions); + // forEach skips empty slots, which would leave the counts wrong + $.checkArgument( + isDenseArray(this.merkleHashes), + 'Expected merkleHashes to be an array without empty slots' + ); + $.checkArgument( + isDenseArray(this.merkleFlags), + 'Expected merkleFlags to be an array without empty slots' + ); bufferWriter.writeVarintNum(this.merkleHashes.length); this.merkleHashes.forEach((hash) => { bufferWriter.write(Buffer.from(hash, 'hex').reverse()); diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 6d06ef159..646f67ba9 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -12,6 +12,7 @@ const Transaction = require('../transaction'); const constants = require('../constants'); const Networks = require('../networks'); const readVarintCount = require('../util/readVarintCount'); +const isDenseArray = require('../util/isDenseArray'); const { readExact, readExactReverse } = require('../transaction/payload/strictpayloadreader'); // nVersion, proRegTxHash, confirmedHash, an empty ExtNetInfo (version and purpose @@ -21,16 +22,6 @@ const MIN_SML_ENTRY_SIZE = 2 + 32 + 32 + 2 + 48 + 20 + 1; // quorumVvecHash, quorumSig and membersSig const MIN_QUORUM_ENTRY_SIZE = 2 + 1 + 32 + 1 + 1 + 48 + 32 + 96 + 96; -/** - * @param {*} value - * @return {boolean} - */ -function isDenseArray(value) { - // a hole leaves a key missing, so this rejects sparse arrays (of any length) - // without materializing them - return Array.isArray(value) && Object.keys(value).length === value.length; -} - /** * @param {number[]} values * @return {boolean} diff --git a/lib/util/isDenseArray.js b/lib/util/isDenseArray.js new file mode 100644 index 000000000..fd9e48c0e --- /dev/null +++ b/lib/util/isDenseArray.js @@ -0,0 +1,18 @@ +/** + * Whether value is an array whose own enumerable keys are exactly its indexes + * '0'..String(length - 1): no empty slots and no extra properties. Serializers + * check this before writing a count, since forEach skips holes. Object.keys + * lists only present keys, so a huge sparse length is never materialized. + * @param {*} value + * @return {boolean} + */ +function isDenseArray(value) { + if (!Array.isArray(value)) { + return false; + } + // an array's integer keys come first, in ascending order + const keys = Object.keys(value); + return keys.length === value.length && keys.every((key, i) => key === String(i)); +} + +module.exports = isDenseArray; diff --git a/lib/util/netinfo.js b/lib/util/netinfo.js index af7a85842..01a213128 100644 --- a/lib/util/netinfo.js +++ b/lib/util/netinfo.js @@ -3,6 +3,7 @@ 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'); +const isDenseArray = require('./isDenseArray'); // Serialization of Dash Core's ExtNetInfo (src/evo/netinfo.h), the address // format of ExtAddr (version 3) masternode entries. @@ -324,8 +325,9 @@ function extNetInfoToBuffer(addresses) { writer.writeVarintNum(purposes.length); purposes.forEach((purpose) => { const entries = addresses[purpose]; - if (!Array.isArray(entries)) { - throw new Error(`Expected ${purpose} addresses to be an array`); + // forEach skips empty slots, which would leave the count wrong + if (!isDenseArray(entries)) { + throw new Error(`Expected ${purpose} addresses to be an array without empty slots`); } if (entries.length > MAX_ENTRIES_PER_PURPOSE) { throw new Error(`Too many ${purpose} addresses`); diff --git a/test/block/PartialMerkleTree.js b/test/block/PartialMerkleTree.js index c72226f48..b2bf44e72 100644 --- a/test/block/PartialMerkleTree.js +++ b/test/block/PartialMerkleTree.js @@ -78,6 +78,25 @@ describe('PartialMerkleTree', function () { }); }); + describe('#toBuffer', function () { + it('Should reject merkle hashes or flags with empty slots or extra properties', function () { + ['merkleHashes', 'merkleFlags'].forEach(function (name) { + var extra = []; + extra.length = 1; + extra.extra = 0; + [[, 0], extra].forEach(function (value) { + var tree = PartialMerkleTree.fromHexString( + '0100000001' + '11'.repeat(32) + '0101' + ); + tree[name] = value; + expect(function () { + tree.toBuffer(); + }).to.throw('Expected ' + name + ' to be an array without empty slots'); + }); + }); + }); + }); + describe('.fromBuffer', function () { // cbTxMerkleTree of Core's testnet mnlistdiff at block 1580498 var coreTreeHex = diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index c7a9200ce..b1c86ab73 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -314,6 +314,19 @@ describe('SimplifiedMNListDiff', function () { }); }); }); + it('Should reject vectors with extra properties in place of their elements', function () { + ['deletedMNs', 'mnList', 'deletedQuorums', 'newQuorums', 'quorumsCLSigs'].forEach(function (name) { + // as many own keys as its length, but none of them an index + var value = []; + value.length = 1; + value.extra = true; + var diff = new SimplifiedMNListDiff(mnListDiffJSON); + diff[name] = value; + expect(function () { + diff.toBuffer(); + }).to.throw('Expected ' + name + ' to be an array without empty slots'); + }); + }); it('Should reject a huge sparse chainlock index array without materializing it', function () { var diff = new SimplifiedMNListDiff(mnListDiffJSON); var signatureHex = Object.keys(coreDiffsJSON[0].quorumsCLSigs[0])[0]; diff --git a/test/util/isDenseArray.js b/test/util/isDenseArray.js new file mode 100644 index 000000000..7c0fc1ee6 --- /dev/null +++ b/test/util/isDenseArray.js @@ -0,0 +1,33 @@ +const { expect } = require('chai'); +const isDenseArray = require('../../lib/util/isDenseArray'); + +describe('isDenseArray', () => { + it('should accept arrays whose keys are exactly their indexes', () => { + expect(isDenseArray([])).to.be.true; + expect(isDenseArray([1, undefined, 'a'])).to.be.true; + }); + + it('should reject non-arrays and arrays with holes', () => { + expect(isDenseArray({ length: 0 })).to.be.false; + expect(isDenseArray('ab')).to.be.false; + // eslint-disable-next-line no-sparse-arrays + expect(isDenseArray([1, , 3])).to.be.false; + }); + + it('should reject extra properties, even when they make up for a hole', () => { + const replaced = []; + replaced.length = 1; + replaced.extra = true; + expect(isDenseArray(replaced)).to.be.false; + + const added = [1]; + added.extra = true; + expect(isDenseArray(added)).to.be.false; + }); + + it('should reject a huge sparse array without materializing it', () => { + const started = Date.now(); + expect(isDenseArray(new Array(2 ** 32 - 1))).to.be.false; + expect(Date.now() - started).to.be.below(1000); + }); +}); diff --git a/test/util/netinfo.js b/test/util/netinfo.js index 13a3761bd..8594f8d1d 100644 --- a/test/util/netinfo.js +++ b/test/util/netinfo.js @@ -69,6 +69,21 @@ describe('netinfo', () => { ); }); + it('should reject address lists with empty slots or extra properties', () => { + // forEach would skip the hole and write a count of 2 with one entry + // eslint-disable-next-line no-sparse-arrays + const sparse = [, '1.2.3.4:9999']; + const extra = []; + extra.length = 1; + extra.extra = '1.2.3.4:9999'; + [sparse, extra].forEach((entries) => { + ['core_p2p', 'platform_p2p', 'platform_https'].forEach((purpose) => { + expect(() => extNetInfoToBuffer({ [purpose]: entries })) + .to.throw(`Expected ${purpose} addresses to be an array without empty slots`); + }); + }); + }); + it('should reject unknown purposes and malformed entries', () => { expect(() => extNetInfoToBuffer({ unknown: [] })).to.throw('Unknown address purpose'); expect(() => extNetInfoToBuffer({ core_p2p: '1.2.3.4:9999' })).to.throw('to be an array'); From 96002d3bb9d6c3108176e503bfaec52ab430c050 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 07:27:19 +0000 Subject: [PATCH 18/21] fix: keep quorumsCLSigs in Core's map order Core keeps quorumsCLSigs in a std::map keyed by CBLSSignature, ordered by GetHash(): sha256d of the 96 signature bytes, compared bytewise. All captured mnlistdiff messages list them in that order. toBuffer now validates the entries, sorts a copy by that key and writes it, so its output matches Core's bytes whatever order the entries are given in. fromBuffer rejects signatures that are not in strictly ascending order, which Core never sends and which would not round-trip. Co-Authored-By: Claude Opus 5.5 --- .../SimplifiedMNListDiff.js | 61 +++++++++++++------ .../SimplifiedMNListDiff.js | 39 ++++++++++++ 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListDiff.js b/lib/deterministicmnlist/SimplifiedMNListDiff.js index 646f67ba9..ad6ab95d3 100644 --- a/lib/deterministicmnlist/SimplifiedMNListDiff.js +++ b/lib/deterministicmnlist/SimplifiedMNListDiff.js @@ -13,6 +13,7 @@ const constants = require('../constants'); const Networks = require('../networks'); const readVarintCount = require('../util/readVarintCount'); const isDenseArray = require('../util/isDenseArray'); +const Hash = require('../crypto/hash'); const { readExact, readExactReverse } = require('../transaction/payload/strictpayloadreader'); // nVersion, proRegTxHash, confirmedHash, an empty ExtNetInfo (version and purpose @@ -22,6 +23,16 @@ const MIN_SML_ENTRY_SIZE = 2 + 32 + 32 + 2 + 48 + 20 + 1; // quorumVvecHash, quorumSig and membersSig const MIN_QUORUM_ENTRY_SIZE = 2 + 1 + 32 + 1 + 1 + 48 + 32 + 96 + 96; +/** + * Core keeps quorumsCLSigs in a std::map keyed by CBLSSignature, whose + * operator< compares GetHash(): sha256d of the 96 signature bytes, bytewise + * @param {Buffer} signature + * @return {Buffer} + */ +function clSigOrderKey(signature) { + return Hash.sha256sha256(signature); +} + /** * @param {number[]} values * @return {boolean} @@ -146,16 +157,24 @@ SimplifiedMNListDiff.fromBuffer = function fromBuffer(buffer, network, llmqParam 'Quorum chainlock signature' ); data.quorumsCLSigs = []; - const seenSignatures = new Set(); + let previousOrderKey = null; for (let i = 0; i < quorumsCLSigsCount; i += 1) { - const signature = readExact(bufferReader, constants.BLS_SIGNATURE_SIZE) - .toString('hex'); - // Core holds these in a map of sets, so a repeated signature or an index - // out of ascending order can't come from Core and wouldn't round-trip - if (seenSignatures.has(signature)) { - throw new Error('Duplicate quorum chainlock signature'); + const signatureBytes = readExact(bufferReader, constants.BLS_SIGNATURE_SIZE); + const signature = signatureBytes.toString('hex'); + // Core holds these in a map of sets, so a repeated signature, signatures out + // of map order or an index out of ascending order can't come from Core and + // wouldn't round-trip + const orderKey = clSigOrderKey(signatureBytes); + if (previousOrderKey) { + const order = Buffer.compare(previousOrderKey, orderKey); + if (order === 0) { + throw new Error('Duplicate quorum chainlock signature'); + } + if (order > 0) { + throw new Error('Quorum chainlock signatures are not in Core\'s map order'); + } } - seenSignatures.add(signature); + previousOrderKey = orderKey; const indexesCount = readVarintCount(bufferReader, 2, 'Quorum index'); const indexes = []; for (let j = 0; j < indexesCount; j += 1) { @@ -242,10 +261,7 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { bufferWriter.write(quorumEntry.toBufferForHashing()); }); - const quorumsCLSigs = this.quorumsCLSigs || []; - bufferWriter.writeVarintNum(quorumsCLSigs.length); - const writtenSignatures = new Set(); - quorumsCLSigs.forEach((quorumsCLSig) => { + const quorumsCLSigs = (this.quorumsCLSigs || []).map((quorumsCLSig) => { const signatures = _.isPlainObject(quorumsCLSig) ? Object.keys(quorumsCLSig) : []; $.checkArgument( signatures.length === 1, @@ -257,11 +273,6 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { isHexStringOfSize(signature, constants.BLS_SIGNATURE_SIZE * 2), 'Expected quorumsCLSigs keys to be bls signatures' ); - $.checkArgument( - !writtenSignatures.has(signature.toLowerCase()), - 'Expected each quorumsCLSigs signature to appear once' - ); - writtenSignatures.add(signature.toLowerCase()); $.checkArgument( isDenseArray(indexes) && indexes.every((index) => Number.isInteger(index) && index >= 0 && index <= 0xffff), @@ -271,7 +282,21 @@ SimplifiedMNListDiff.prototype.toBuffer = function toBuffer() { isStrictlyIncreasing(indexes), 'Expected quorumsCLSigs indexes to be sorted and unique, as Core sends them' ); - bufferWriter.write(Buffer.from(signature, 'hex')); + const signatureBytes = Buffer.from(signature, 'hex'); + return { signatureBytes, orderKey: clSigOrderKey(signatureBytes), indexes }; + }); + // write them in Core's map order, whatever order they are given in + quorumsCLSigs.sort((a, b) => Buffer.compare(a.orderKey, b.orderKey)); + quorumsCLSigs.forEach((quorumsCLSig, i) => { + $.checkArgument( + i === 0 || !quorumsCLSig.orderKey.equals(quorumsCLSigs[i - 1].orderKey), + 'Expected each quorumsCLSigs signature to appear once' + ); + }); + + bufferWriter.writeVarintNum(quorumsCLSigs.length); + quorumsCLSigs.forEach(({ signatureBytes, indexes }) => { + bufferWriter.write(signatureBytes); bufferWriter.writeVarintNum(indexes.length); indexes.forEach((index) => bufferWriter.writeUInt16LE(index)); }); diff --git a/test/deterministicmnlist/SimplifiedMNListDiff.js b/test/deterministicmnlist/SimplifiedMNListDiff.js index b1c86ab73..f9d370782 100644 --- a/test/deterministicmnlist/SimplifiedMNListDiff.js +++ b/test/deterministicmnlist/SimplifiedMNListDiff.js @@ -327,6 +327,14 @@ describe('SimplifiedMNListDiff', function () { }).to.throw('Expected ' + name + ' to be an array without empty slots'); }); }); + it('Should write quorumsCLSigs in Core\'s order whatever order they are given in', function () { + var vector = coreDiffs.testnetFull; + var diff = SimplifiedMNListDiff.fromHexString(vector.hex, 'testnet'); + expect(diff.quorumsCLSigs.length).to.be.above(2); + diff.quorumsCLSigs.reverse(); + + expect(diff.toBuffer().toString('hex')).to.be.equal(vector.hex); + }); it('Should reject a huge sparse chainlock index array without materializing it', function () { var diff = new SimplifiedMNListDiff(mnListDiffJSON); var signatureHex = Object.keys(coreDiffsJSON[0].quorumsCLSigs[0])[0]; @@ -367,6 +375,37 @@ describe('SimplifiedMNListDiff', function () { }); }); + it('Should reject chainlock signatures that are not in Core\'s map order', function () { + var diff = SimplifiedMNListDiff.fromHexString(coreDiffs.testnetFull.hex, 'testnet'); + var entries = diff.quorumsCLSigs.map(function (clSig) { + var signatureHex = Object.keys(clSig)[0]; + var indexes = clSig[signatureHex]; + var indexBytes = Buffer.alloc(indexes.length * 2); + indexes.forEach(function (index, i) { + indexBytes.writeUInt16LE(index, i * 2); + }); + return Buffer.concat([ + Buffer.from(signatureHex, 'hex'), + Buffer.from([indexes.length]), + indexBytes, + ]); + }); + var tail = Buffer.concat(entries); + var coreRaw = Buffer.from(coreDiffs.testnetFull.hex, 'hex'); + var head = coreRaw.slice(0, coreRaw.length - tail.length - 1); + expect(Buffer.concat([head, Buffer.from([entries.length]), tail])) + .to.be.deep.equal(coreRaw); + + var reversed = Buffer.concat([ + head, + Buffer.from([entries.length]), + Buffer.concat(entries.slice().reverse()), + ]); + expect(function () { + SimplifiedMNListDiff.fromBuffer(reversed, Networks.testnet); + }).to.throw('Quorum chainlock signatures are not in Core\'s map order'); + }); + it('Should reject a repeated chainlock signature', function () { var entry = Buffer.concat([signature, Buffer.from('00', 'hex')]); var raw = withCLSigs(Buffer.concat([Buffer.from('02', 'hex'), entry, entry])); From 9abfe81aa940e1a2ad7f600b6ee5c749cb75107b Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 08:21:51 +0000 Subject: [PATCH 19/21] fix: reject masternode list entries with a type Core does not have Core's MnType is Regular (0) or Evo (1); any other value read from a peer was accepted as a regular entry. Co-Authored-By: Claude Opus 5.5 --- lib/deterministicmnlist/SimplifiedMNListEntry.js | 5 +++-- test/deterministicmnlist/SimplifiedMNListEntry.js | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index dbc8cf652..5b98ddf22 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -354,9 +354,10 @@ SimplifiedMNListEntry.prototype.validate = function validate() { typeof this.isValid === 'boolean', 'Expected isValid to be a boolean' ); + // Core's MnType has only Regular (0) and Evo (1) $.checkArgument( - typeof this.nType === 'number', - 'Expected nType to be a number' + this.nType === MASTERNODE_TYPE_BASIC || this.nType === MASTERNODE_TYPE_HP, + 'Expected nType to be 0 (regular) or 1 (evo)' ); if (this.nType === constants.MASTERNODE_TYPE_HP) { diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index 24739e441..ae975c6e9 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -206,6 +206,13 @@ describe('SimplifiedMNListEntry', function () { expect(entry.platformHTTPPort).to.be.undefined; expect(entry.toP2PBuffer().toString('hex')).to.be.equal(hex); }); + it('Should reject a masternode type Core does not have', function () { + // a version 2 entry ends its hashed fields with isValid, then nType (2 here) + var hex = '0200' + smlEntryHex.slice(0, 151 * 2) + '0200'; + expect(function () { + SimplifiedMNListEntry.fromP2PBufferReader(new BufferReader(Buffer.from(hex, 'hex')), Networks.testnet); + }).to.throw('Expected nType to be 0 (regular) or 1 (evo)'); + }); it('Should not modify the buffer it parses', function () { var hex = '0300' + extAddrEvoEntryHex; var buffer = Buffer.from(hex, 'hex'); From dacfeb199990a5bfb30f098666c7b57ee4118ea6 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 10:58:22 +0000 Subject: [PATCH 20/21] fix: read a v3 evonode's Platform port from its current address getPlatformHTTPSEndpoint returned the port cached at construction, which went stale if addresses changed. Also types llmqParamsOverride on the QuorumEntry constructor and factories. Co-Authored-By: Claude Opus 5.5 --- lib/deterministicmnlist/SimplifiedMNListEntry.js | 15 ++++++++------- test-d/index.test-d.ts | 4 ++++ test/deterministicmnlist/SimplifiedMNListEntry.js | 11 +++++++++++ typings/deterministicmnlist/QuorumEntry.d.ts | 11 +++++++---- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index 5b98ddf22..b66bc0c7a 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -443,23 +443,24 @@ SimplifiedMNListEntry.prototype.getIp = function getIp() { * getIp(); null for regular masternodes and evonodes without an address */ SimplifiedMNListEntry.prototype.getPlatformHTTPSEndpoint = function getPlatformHTTPSEndpoint() { - if (this.nType !== MASTERNODE_TYPE_HP || typeof this.platformHTTPPort !== 'number') { + if (this.nType !== MASTERNODE_TYPE_HP) { return null; } - // fromObject takes an ExtAddr entry's platformHTTPPort from platform_https[0] + // ExtAddr entries: host and port both come from the current platform_https[0] if (this.nVersion >= PROTX_EXT_ADDR_VERSION) { - const [address] = this.addresses.platform_https || []; - if (!address || ipUtils.isZeroAddress(address)) { + const [address] = (this.addresses && this.addresses.platform_https) || []; + if (typeof address !== 'string' || ipUtils.isZeroAddress(address)) { return null; } + const separator = address.lastIndexOf(':'); return { - host: address.slice(0, address.lastIndexOf(':')).replace(/^\[(.*)\]$/, '$1'), - port: this.platformHTTPPort, + host: address.slice(0, separator).replace(/^\[(.*)\]$/, '$1'), + port: Number(address.slice(separator + 1)), }; } - if (ipUtils.isZeroAddress(this.service)) { + if (typeof this.platformHTTPPort !== 'number' || ipUtils.isZeroAddress(this.service)) { return null; } return { host: this.getIp(), port: this.platformHTTPPort }; diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index 75e131b9a..1049ca3ef 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -136,3 +136,7 @@ expectType(new SMLDiffClass({}, 'testnet', customLLMQ)); proUpRegTxPayload.platformNodeID; // @ts-expect-error proUpRegTxPayload.type; +expectType(QuorumEntryClass.fromBuffer(Buffer.alloc(0), customLLMQ)); +expectType(QuorumEntryClass.fromHexString('', customLLMQ)); +expectType(QuorumEntryClass.fromObject({}, customLLMQ)); +expectType(new QuorumEntryClass({}, customLLMQ)); diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index ae975c6e9..d92df5bac 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -454,6 +454,17 @@ describe('SimplifiedMNListEntry', function () { }); }); }); + it('Should follow platform_https when it changes after construction', function () { + var entry = new SimplifiedMNListEntry(extAddrEvoEntryJSON); + entry.addresses = Object.assign({}, entry.addresses, { platform_https: ['node.example:443'] }); + expect(entry.getPlatformHTTPSEndpoint()).to.be.deep.equal({ host: 'node.example', port: 443 }); + + var noPlatform = new SimplifiedMNListEntry(Object.assign({}, extAddrEvoEntryJSON, { + addresses: { core_p2p: extAddrEvoEntryJSON.addresses.core_p2p }, + })); + noPlatform.addresses = Object.assign({}, noPlatform.addresses, { platform_https: ['203.0.113.7:1443'] }); + expect(noPlatform.getPlatformHTTPSEndpoint()).to.be.deep.equal({ host: '203.0.113.7', port: 1443 }); + }); it('Should return null for an evonode without a Platform address', function () { var json = Object.assign({}, extAddrEvoEntryJSON, { addresses: { core_p2p: extAddrEvoEntryJSON.addresses.core_p2p }, diff --git a/typings/deterministicmnlist/QuorumEntry.d.ts b/typings/deterministicmnlist/QuorumEntry.d.ts index 908856162..a332b7d6c 100644 --- a/typings/deterministicmnlist/QuorumEntry.d.ts +++ b/typings/deterministicmnlist/QuorumEntry.d.ts @@ -20,14 +20,15 @@ import { BufferReader } from '../buffer/BufferReader'; * @property {string} membersSig */ export class QuorumEntry { - constructor(arg?: Buffer | any | string); + constructor(arg?: Buffer | any | string, llmqParamsOverride?: Object); /** * Parse buffer and returns QuorumEntry * @param {Buffer} buffer + * @param {Object} [llmqParamsOverride] * @return {QuorumEntry} */ - static fromBuffer(buffer: Buffer): QuorumEntry; + static fromBuffer(buffer: Buffer, llmqParamsOverride?: Object): QuorumEntry; /** * Reads a final commitment as Core sends it in the P2P `mnlistdiff` message, @@ -43,16 +44,18 @@ export class QuorumEntry { ): QuorumEntry; /** * @param {string} string + * @param {Object} [llmqParamsOverride] * @return {QuorumEntry} */ - static fromHexString(string: String): QuorumEntry; + static fromHexString(string: String, llmqParamsOverride?: Object): QuorumEntry; /** * Create SMLQuorumEntry from an object * @param {SMLQuorumEntry} obj + * @param {Object} [llmqParamsOverride] * @return {QuorumEntry} */ - static fromObject(obj: Object): QuorumEntry; + static fromObject(obj: Object, llmqParamsOverride?: Object): QuorumEntry; /** * Validate QuorumEntry structure From 71f5f1873aafe3ec43d24af4d5dba69fa6c49e11 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 29 Oct 2026 11:08:24 +0000 Subject: [PATCH 21/21] fix: return no Platform endpoint for port 0 Co-Authored-By: Claude Opus 5.5 --- lib/deterministicmnlist/SimplifiedMNListEntry.js | 13 +++++++++++-- test/deterministicmnlist/SimplifiedMNListEntry.js | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/deterministicmnlist/SimplifiedMNListEntry.js b/lib/deterministicmnlist/SimplifiedMNListEntry.js index b66bc0c7a..e90889020 100644 --- a/lib/deterministicmnlist/SimplifiedMNListEntry.js +++ b/lib/deterministicmnlist/SimplifiedMNListEntry.js @@ -454,13 +454,22 @@ SimplifiedMNListEntry.prototype.getPlatformHTTPSEndpoint = function getPlatformH return null; } const separator = address.lastIndexOf(':'); + const port = Number(address.slice(separator + 1)); + // port 0 is reserved, not a reachable HTTPS endpoint + if (port === 0) { + return null; + } return { host: address.slice(0, separator).replace(/^\[(.*)\]$/, '$1'), - port: Number(address.slice(separator + 1)), + port, }; } - if (typeof this.platformHTTPPort !== 'number' || ipUtils.isZeroAddress(this.service)) { + if ( + typeof this.platformHTTPPort !== 'number' + || this.platformHTTPPort === 0 + || ipUtils.isZeroAddress(this.service) + ) { return null; } return { host: this.getIp(), port: this.platformHTTPPort }; diff --git a/test/deterministicmnlist/SimplifiedMNListEntry.js b/test/deterministicmnlist/SimplifiedMNListEntry.js index d92df5bac..39e1e4acb 100644 --- a/test/deterministicmnlist/SimplifiedMNListEntry.js +++ b/test/deterministicmnlist/SimplifiedMNListEntry.js @@ -465,6 +465,15 @@ describe('SimplifiedMNListEntry', function () { noPlatform.addresses = Object.assign({}, noPlatform.addresses, { platform_https: ['203.0.113.7:1443'] }); expect(noPlatform.getPlatformHTTPSEndpoint()).to.be.deep.equal({ host: '203.0.113.7', port: 1443 }); }); + it('Should return null for a Platform port of 0', function () { + var legacy = new SimplifiedMNListEntry(smlEntryJSON); + legacy.platformHTTPPort = 0; + expect(legacy.getPlatformHTTPSEndpoint()).to.be.null; + + var v3 = new SimplifiedMNListEntry(extAddrEvoEntryJSON); + v3.addresses = Object.assign({}, v3.addresses, { platform_https: ['203.0.113.7:0'] }); + expect(v3.getPlatformHTTPSEndpoint()).to.be.null; + }); it('Should return null for an evonode without a Platform address', function () { var json = Object.assign({}, extAddrEvoEntryJSON, { addresses: { core_p2p: extAddrEvoEntryJSON.addresses.core_p2p },