From 391060a234beca54597e4f82e88f6b4e4c175ca8 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:38:02 +0100 Subject: [PATCH 01/90] Implement qpi.getOracleQuery() and tests --- src/contract_core/qpi_oracle_impl.h | 3 +- src/oracle_core/oracle_engine.h | 44 ++++++++++++---- src/qubic.cpp | 2 + src/ticking/ticking.h | 3 -- test/common_def.cpp | 1 + test/contract_testex.cpp | 43 +-------------- test/oracle_engine.cpp | 81 +++++++++++++++++++++++++++++ test/oracle_testing.h | 54 +++++++++++++++++++ test/test.vcxproj | 4 +- test/test.vcxproj.filters | 4 +- 10 files changed, 180 insertions(+), 59 deletions(-) create mode 100644 test/oracle_engine.cpp create mode 100644 test/oracle_testing.h diff --git a/src/contract_core/qpi_oracle_impl.h b/src/contract_core/qpi_oracle_impl.h index 14a1af5d3..0ad286816 100644 --- a/src/contract_core/qpi_oracle_impl.h +++ b/src/contract_core/qpi_oracle_impl.h @@ -85,8 +85,7 @@ inline bool QPI::QpiContextProcedureCall::unsubscribeOracle( template bool QPI::QpiContextFunctionCall::getOracleQuery(QPI::sint64 queryId, OracleInterface::OracleQuery& query) const { - // TODO - return false; + return oracleEngine.getOracleQuery(queryId, &query, sizeof(query)); } template diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index e4fc97f57..c5edd4b79 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -1,15 +1,19 @@ #pragma once -#include "network_messages/common_def.h" +#include "contract_core/pre_qpi_def.h" #include "contracts/qpi.h" +#include "oracle_core/oracle_interfaces_def.h" #include "system.h" +#include "common_buffers.h" +#include "spectrum/special_entities.h" #include "oracle_transactions.h" #include "core_om_network_messages.h" #include "platform/memory_util.h" + void enqueueResponse(Peer* peer, unsigned int dataSize, unsigned char type, unsigned int dejavu, const void* data); constexpr uint32_t MAX_ORACLE_QUERIES = (1 << 18); @@ -110,8 +114,8 @@ struct OracleReplyState uint8_t ownReplyData[MAX_ORACLE_REPLY_SIZE + 2]; uint16_t ownReplyCommitExecCount; - uint32 ownReplyCommitComputorTxTick[computorSeedsCount]; - uint32 ownReplyCommitComputorTxExecuted[computorSeedsCount]; + uint32_t ownReplyCommitComputorTxTick[computorSeedsCount]; + uint32_t ownReplyCommitComputorTxExecuted[computorSeedsCount]; m256i replyCommitDigests[NUMBER_OF_COMPUTORS]; m256i replyCommitKnowledgeProofs[NUMBER_OF_COMPUTORS]; @@ -231,7 +235,7 @@ class OracleEngine } oracleQueryCount = 0; - queryStorageBytesUsed = 1; // reserve offset 0 for "no data" + queryStorageBytesUsed = 8; // reserve offset 0 for "no data" setMem(&contractQueryIdState, sizeof(contractQueryIdState), 0); replyStatesIndex = 0; pendingQueryIndices.numValues = 0; @@ -295,7 +299,7 @@ class OracleEngine return -1; // compute timeout as absolute point in time - DateAndTime timeout = DateAndTime::now(); + auto timeout = QPI::DateAndTime::now(); if (!timeout.addMillisec(timeoutMillisec)) return -1; @@ -319,7 +323,7 @@ class OracleEngine // map ID to index ASSERT(!queryIdToIndex->contains(queryId)); - if (queryIdToIndex->set(queryId, oracleQueryCount) == NULL_INDEX) + if (queryIdToIndex->set(queryId, oracleQueryCount) == QPI::NULL_INDEX) return -1; // register index of pending query @@ -340,7 +344,7 @@ class OracleEngine // init reply state (temporary until reply is revealed) auto& replyState = replyStates[replyStateSlotIdx]; - setMemory(replyState, 0); + setMem(&replyState, sizeof(replyState), 0); replyState.queryId = queryId; replyState.notificationProcedure = notificationProcedure; replyState.notificationLocalsSize = notificationLocalsSize; @@ -356,7 +360,7 @@ class OracleEngine } // Enqueue oracle machine query message. May be called from tick processor or contract processor only (uses reorgBuffer). - void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint16_t timeoutMillisec, const void* queryData, uint16 querySize) + void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint16_t timeoutMillisec, const void* queryData, uint16_t querySize) { // Prepare message payload OracleMachineQuery* omq = reinterpret_cast(reorgBuffer); @@ -521,16 +525,36 @@ class OracleEngine // clean all queries (except for last n ticks in case of seamless transition) } - bool getOracleQuery(uint64_t queryId, const void* queryData, uint16_t querySize) const + bool getOracleQuery(int64_t queryId, void* queryData, uint16_t querySize) const { // get query index uint32_t queryIndex; if (!queryIdToIndex->get(queryId, queryIndex) || queryIndex >= oracleQueryCount) return false; + // check query size const auto& queryMetadata = queries[queryIndex]; - // TODO + ASSERT(queryMetadata.interfaceIndex < OI::oracleInterfacesCount); + if (querySize != OI::oracleInterfaces[queryMetadata.interfaceIndex].querySize) + return false; + + void* querySrcPtr = nullptr; + switch (queryMetadata.type) + { + case ORACLE_QUERY_TYPE_CONTRACT_QUERY: + { + const auto offset = queryMetadata.typeVar.contract.queryStorageOffset; + ASSERT(offset > 0 && offset < queryStorageBytesUsed && queryStorageBytesUsed <= ORACLE_QUERY_STORAGE_SIZE); + querySrcPtr = queryStorage + offset; + break; + } + // TODO: support other types + default: + return false; + } + // Return query data + copyMem(queryData, querySrcPtr, querySize); return true; } diff --git a/src/qubic.cpp b/src/qubic.cpp index 5da0dcadd..58862726a 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -59,6 +59,8 @@ #include "logging/net_msg_impl.h" #include "ticking/ticking.h" +#include "ticking/tick_storage.h" +#include "ticking/pending_txs_pool.h" #include "contract_core/qpi_ticking_impl.h" #include "vote_counter.h" #include "ticking/execution_fee_report_collector.h" diff --git a/src/ticking/ticking.h b/src/ticking/ticking.h index 4bbb0d61a..97396bed6 100644 --- a/src/ticking/ticking.h +++ b/src/ticking/ticking.h @@ -5,9 +5,6 @@ #include "network_messages/tick.h" -#include "ticking/tick_storage.h" -#include "ticking/pending_txs_pool.h" - #include "private_settings.h" GLOBAL_VAR_DECL Tick etalonTick; diff --git a/test/common_def.cpp b/test/common_def.cpp index adba975fd..4c835edc7 100644 --- a/test/common_def.cpp +++ b/test/common_def.cpp @@ -3,6 +3,7 @@ #include "contract_testing.h" #include "logging_test.h" +#include "oracle_testing.h" #include "platform/concurrency_impl.h" #include "platform/profiling.h" diff --git a/test/contract_testex.cpp b/test/contract_testex.cpp index e56e85460..1c010fe60 100644 --- a/test/contract_testex.cpp +++ b/test/contract_testex.cpp @@ -4,6 +4,7 @@ #include #include "contract_testing.h" +#include "oracle_testing.h" static const id TESTEXA_CONTRACT_ID(TESTEXA_CONTRACT_INDEX, 0, 0, 0); static const id TESTEXB_CONTRACT_ID(TESTEXB_CONTRACT_INDEX, 0, 0, 0); @@ -2101,48 +2102,6 @@ TEST(ContractTestEx, SystemCallbacksWithNegativeFeeReserve) EXPECT_LT(getContractFeeReserve(TESTEXC_CONTRACT_INDEX), 0); } -static union -{ - RequestResponseHeader header; - - struct - { - RequestResponseHeader header; - OracleMachineQuery queryMetadata; - unsigned char queryData[MAX_ORACLE_QUERY_SIZE]; - } omQuery; -} enqueuedNetworkMessage; - -template -void checkNetworkMessageOracleMachineQuery(uint64 expectedOracleQueryId, id expectedOracle, uint32 expectedTimeout) -{ - EXPECT_EQ(enqueuedNetworkMessage.header.type(), OracleMachineQuery::type()); - EXPECT_GT(enqueuedNetworkMessage.header.size(), sizeof(RequestResponseHeader) + sizeof(OracleMachineQuery)); - uint32 queryDataSize = enqueuedNetworkMessage.header.size() - sizeof(RequestResponseHeader) - sizeof(OracleMachineQuery); - EXPECT_LE(queryDataSize, (uint32)MAX_ORACLE_QUERY_SIZE); - EXPECT_EQ(queryDataSize, sizeof(typename OracleInterface::OracleQuery)); - EXPECT_EQ(enqueuedNetworkMessage.omQuery.queryMetadata.oracleInterfaceIndex, OracleInterface::oracleInterfaceIndex); - EXPECT_EQ(enqueuedNetworkMessage.omQuery.queryMetadata.oracleQueryId, expectedOracleQueryId); - EXPECT_EQ(enqueuedNetworkMessage.omQuery.queryMetadata.timeoutInMilliseconds, expectedTimeout); - const auto* q = (const OracleInterface::OracleQuery*)enqueuedNetworkMessage.omQuery.queryData; - EXPECT_EQ(q->oracle, expectedOracle); -} - -static void enqueueResponse(Peer* peer, unsigned int dataSize, unsigned char type, unsigned int dejavu, const void* data) -{ - EXPECT_EQ(peer, (Peer*)0x1); - EXPECT_LE(dataSize, sizeof(OracleMachineQuery) + MAX_ORACLE_QUERY_SIZE); - EXPECT_TRUE(enqueuedNetworkMessage.header.checkAndSetSize(sizeof(RequestResponseHeader) + dataSize)); - enqueuedNetworkMessage.header.setType(type); - enqueuedNetworkMessage.header.setDejavu(dejavu); - copyMem(&enqueuedNetworkMessage.omQuery.queryMetadata, data, dataSize); -} - -uint64 getContractOracleQueryId(uint32 tick, uint16 indexInTick) -{ - return ((uint64)tick << 31) | (indexInTick + NUMBER_OF_TRANSACTIONS_PER_TICK); -} - TEST(ContractTestEx, OracleQuery) { ContractTestingTestEx test; diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp new file mode 100644 index 000000000..548e148b6 --- /dev/null +++ b/test/oracle_engine.cpp @@ -0,0 +1,81 @@ +#define NO_UEFI + +#include "oracle_testing.h" + + +struct OracleEngineTest : public OracleEngine, LoggingTest +{ + OracleEngineTest() + { + EXPECT_TRUE(init()); + EXPECT_TRUE(initCommonBuffers()); + } + + ~OracleEngineTest() + { + deinitCommonBuffers(); + deinit(); + } +}; + +static void dummyNotificationProc(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals) +{ +} + +TEST(OracleEngine, ContractQuery) +{ + OracleEngineTest oracleEngine; + + system.tick = 1000; + etalonTick.year = 25; + etalonTick.month = 12; + etalonTick.day = 15; + etalonTick.hour = 16; + etalonTick.minute = 51; + etalonTick.second = 12; + + OI::Price::OracleQuery priceQuery; + priceQuery.oracle = m256i(1, 2, 3, 4); + priceQuery.currency1 = m256i(2, 3, 4, 5); + priceQuery.currency2 = m256i(3, 4, 5, 6); + priceQuery.timestamp = QPI::DateAndTime::now(); + QPI::uint32 interfaceIndex = 0; + QPI::uint16 contractIndex = 1; + QPI::uint32 timeout = 30000; + USER_PROCEDURE notificationProc = dummyNotificationProc; + QPI::uint32 notificationLocalsSize = 128; + + //------------------------------------------------------------------------- + // start contract query / check message to OM node + QPI::sint64 queryId = oracleEngine.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize); + EXPECT_EQ(queryId, getContractOracleQueryId(system.tick, 0)); + checkNetworkMessageOracleMachineQuery(queryId, priceQuery.oracle, timeout); + + //------------------------------------------------------------------------- + // get query contract data + OI::Price::OracleQuery priceQueryReturned; + EXPECT_TRUE(oracleEngine.getOracleQuery(queryId, &priceQueryReturned, sizeof(priceQueryReturned))); + EXPECT_EQ(memcmp(&priceQueryReturned, &priceQuery, sizeof(priceQuery)), 0); + + //------------------------------------------------------------------------- + // process simulated reply from OM node + struct + { + OracleMachineReply metatdata; + OI::Price::OracleReply data; + } priceOracleMachineReply; + + priceOracleMachineReply.metatdata.oracleMachineErrorFlags = 0; + priceOracleMachineReply.metatdata.oracleQueryId = queryId; + priceOracleMachineReply.data.numerator = 1234; + priceOracleMachineReply.data.denominator = 1; + + oracleEngine.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + + // duplicate from other node + oracleEngine.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + + // other value from other node + priceOracleMachineReply.data.numerator = 1233; + oracleEngine.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); +} \ No newline at end of file diff --git a/test/oracle_testing.h b/test/oracle_testing.h new file mode 100644 index 000000000..30ff3a005 --- /dev/null +++ b/test/oracle_testing.h @@ -0,0 +1,54 @@ +#pragma once + +// Include this first, to ensure "logging/logging.h" isn't included before the custom LOG_BUFFER_SIZE has been defined +#include "logging_test.h" + +#include "gtest/gtest.h" + +#include "oracle_core/oracle_engine.h" +#include "contract_core/qpi_ticking_impl.h" + + +union EnqueuedNetworkMessage +{ + RequestResponseHeader header; + + struct + { + RequestResponseHeader header; + OracleMachineQuery queryMetadata; + unsigned char queryData[MAX_ORACLE_QUERY_SIZE]; + } omQuery; +}; + +GLOBAL_VAR_DECL EnqueuedNetworkMessage enqueuedNetworkMessage; + +template +static void checkNetworkMessageOracleMachineQuery(QPI::uint64 expectedOracleQueryId, QPI::id expectedOracle, QPI::uint32 expectedTimeout) +{ + EXPECT_EQ(enqueuedNetworkMessage.header.type(), OracleMachineQuery::type()); + EXPECT_GT(enqueuedNetworkMessage.header.size(), sizeof(RequestResponseHeader) + sizeof(OracleMachineQuery)); + QPI::uint32 queryDataSize = enqueuedNetworkMessage.header.size() - sizeof(RequestResponseHeader) - sizeof(OracleMachineQuery); + EXPECT_LE(queryDataSize, (QPI::uint32)MAX_ORACLE_QUERY_SIZE); + EXPECT_EQ(queryDataSize, sizeof(typename OracleInterface::OracleQuery)); + EXPECT_EQ(enqueuedNetworkMessage.omQuery.queryMetadata.oracleInterfaceIndex, OracleInterface::oracleInterfaceIndex); + EXPECT_EQ(enqueuedNetworkMessage.omQuery.queryMetadata.oracleQueryId, expectedOracleQueryId); + EXPECT_EQ(enqueuedNetworkMessage.omQuery.queryMetadata.timeoutInMilliseconds, expectedTimeout); + const auto* q = (const OracleInterface::OracleQuery*)enqueuedNetworkMessage.omQuery.queryData; + EXPECT_EQ(q->oracle, expectedOracle); +} + +static void enqueueResponse(Peer* peer, unsigned int dataSize, unsigned char type, unsigned int dejavu, const void* data) +{ + EXPECT_EQ(peer, (Peer*)0x1); + EXPECT_LE(dataSize, sizeof(OracleMachineQuery) + MAX_ORACLE_QUERY_SIZE); + EXPECT_TRUE(enqueuedNetworkMessage.header.checkAndSetSize(sizeof(RequestResponseHeader) + dataSize)); + enqueuedNetworkMessage.header.setType(type); + enqueuedNetworkMessage.header.setDejavu(dejavu); + copyMem(&enqueuedNetworkMessage.omQuery.queryMetadata, data, dataSize); +} + +static inline QPI::uint64 getContractOracleQueryId(QPI::uint32 tick, QPI::uint32 indexInTick) +{ + return ((QPI::uint64)tick << 31) | (indexInTick + NUMBER_OF_TRANSACTIONS_PER_TICK); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 5979b640b..0da0c697d 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -114,6 +114,7 @@ + @@ -126,6 +127,7 @@ + @@ -190,4 +192,4 @@ - \ No newline at end of file + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 05f9b9622..91fdc25c4 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -46,6 +46,7 @@ + @@ -53,6 +54,7 @@ + @@ -67,4 +69,4 @@ core - \ No newline at end of file + From 7ca5294b0b6d1434e22daec30c63997549179d0f Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:52:15 +0100 Subject: [PATCH 02/90] Implement oracle reply commit transaction --- src/network_messages/common_def.h | 4 +- src/oracle_core/oracle_engine.h | 287 +++++++++++++++++++----- src/oracle_core/oracle_transactions.h | 2 + src/qubic.cpp | 4 +- test/contract_testex.cpp | 2 +- test/oracle_engine.cpp | 307 ++++++++++++++++++++++++-- 6 files changed, 530 insertions(+), 76 deletions(-) diff --git a/src/network_messages/common_def.h b/src/network_messages/common_def.h index e7368043e..a15a2d972 100644 --- a/src/network_messages/common_def.h +++ b/src/network_messages/common_def.h @@ -72,8 +72,8 @@ constexpr uint16_t ORACLE_FLAG_OM_ERROR_FLAGS = 0xff; ///< Mask of all error fl constexpr uint16_t ORACLE_FLAG_REPLY_RECEIVED = 0x100; ///< Oracle engine got valid reply from the oracle machine. constexpr uint16_t ORACLE_FLAG_BAD_SIZE_REPLY = 0x200; ///< Oracle engine got reply of wrong size from the oracle machine. constexpr uint16_t ORACLE_FLAG_OM_DISAGREE = 0x400; ///< Oracle engine got different replies from oracle machines. -constexpr uint16_t ORACLE_FLAG_COMP_DISAGREE = 0x800; ///< The number of reply commits is sufficient (>= 451 computors), but they disagree about the reply value. -constexpr uint16_t ORACLE_FLAG_TIMEOUT = 0x1000; ///< The weren't enough reply commit tx before timeout (< 451). +constexpr uint16_t ORACLE_FLAG_COMP_DISAGREE = 0x800; ///< The reply commits differ too much and no quorum can be reached. +constexpr uint16_t ORACLE_FLAG_TIMEOUT = 0x1000; ///< The weren't enough reply commit tx with the same digest before timeout (< 451). typedef union IPv4Address { diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index c5edd4b79..9d15a10a1 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -105,6 +105,7 @@ struct OracleSubscriptionMetadata }; // State of received OM reply and computor commits for a single oracle query +template struct OracleReplyState { int64_t queryId; @@ -113,17 +114,19 @@ struct OracleReplyState uint16_t ownReplySize; uint8_t ownReplyData[MAX_ORACLE_REPLY_SIZE + 2]; + // track state of own reply commits (when they are scheduled and when actually got executed) uint16_t ownReplyCommitExecCount; - uint32_t ownReplyCommitComputorTxTick[computorSeedsCount]; - uint32_t ownReplyCommitComputorTxExecuted[computorSeedsCount]; + uint32_t ownReplyCommitComputorTxTick[ownComputorSeedsCount]; + uint32_t ownReplyCommitComputorTxExecuted[ownComputorSeedsCount]; m256i replyCommitDigests[NUMBER_OF_COMPUTORS]; m256i replyCommitKnowledgeProofs[NUMBER_OF_COMPUTORS]; uint32_t replyCommitTicks[NUMBER_OF_COMPUTORS]; + uint16_t replyCommitHistogramIdx[NUMBER_OF_COMPUTORS]; + uint16_t replyCommitHistogramCount[NUMBER_OF_COMPUTORS]; + uint16_t mostCommitsHistIdx; uint16_t totalCommits; - uint16_t mostCommitsCount; - m256i mostCommitsDigest; uint32_t revealTick; uint32_t revealTxIndex; @@ -154,7 +157,7 @@ struct UnsortedMultiset return true; } - bool remove(unsigned int idx) + bool removeByIndex(unsigned int idx) { ASSERT(numValues <= N); if (idx >= numValues || numValues == 0) @@ -166,12 +169,28 @@ struct UnsortedMultiset } return true; } + + bool removeByValue(const T& v) + { + unsigned int idx = 0; + bool removedAny = false; + while (idx < numValues) + { + if (values[idx] == v) + removedAny = removedAny || removeByIndex(idx); + else + ++idx; + } + return removedAny; + } }; // TODO: locking, implement hash function for queryIdToIndex based on tick +template class OracleEngine { +protected: /// array of all oracle queries of the epoch with capacity for MAX_ORACLE_QUERIES elements OracleQueryMetadata* queries; @@ -192,7 +211,7 @@ class OracleEngine } contractQueryIdState; // state of received OM reply and computor commits for each oracle query (used before reveal) - OracleReplyState* replyStates; + OracleReplyState* replyStates; // index in replyStates to check next for empty slot (cyclic buffer) int32_t replyStatesIndex; @@ -203,9 +222,29 @@ class OracleEngine /// fast lookup of reply state indices for which commit tx is pending UnsortedMultiset pendingCommitReplyStateIndices; + /// fast lookup of reply state indices for which reveal tx is pending + UnsortedMultiset pendingRevealReplyStateIndices; + + /// fast lookup of query indices for which the contract should be notified + UnsortedMultiset notificationQueryIndicies; + + struct { + /// total number of successful oracle queries + unsigned long long successCount; + + /// total number of timeout oracle queries + unsigned long long timeoutCount; + + /// total number of timeout oracle queries + unsigned long long unresolvableCount; + } stats; + /// fast lookup of oracle query index (sequential position in queries array) from oracle query ID (composed of query tick and other info) QPI::HashMap* queryIdToIndex; + /// array of ownComputorSeedsCount public keys (mainly for testing, in EFI core this points to computorPublicKeys from special_entities.h) + const m256i* ownComputorPublicKeys; + /// Return empty reply state slot or max uint32 value on error uint32_t getEmptyReplyStateSlot() { @@ -222,9 +261,17 @@ class OracleEngine return 0xffffffff; } + void freeReplyStateSlot(uint32_t replyStateIdx) + { + ASSERT(replyStatesIndex < MAX_SIMULTANEOUS_ORACLE_QUERIES); + setMem(&replyStates[replyStateIdx], sizeof(*replyStates), 0); + } + public: - bool init() + bool init(const m256i* ownComputorPublicKeys) { + this->ownComputorPublicKeys = ownComputorPublicKeys; + // alloc arrays and set to 0 if (!allocPoolWithErrorLog(L"OracleEngine::queries", MAX_ORACLE_QUERIES * sizeof(*queries), (void**)&queries, __LINE__) || !allocPoolWithErrorLog(L"OracleEngine::queryStorage", ORACLE_QUERY_STORAGE_SIZE, (void**)&queryStorage, __LINE__) @@ -240,6 +287,9 @@ class OracleEngine replyStatesIndex = 0; pendingQueryIndices.numValues = 0; pendingCommitReplyStateIndices.numValues = 0; + pendingRevealReplyStateIndices.numValues = 0; + notificationQueryIndicies.numValues = 0; + setMem(&stats, sizeof(stats), 0); return true; } @@ -356,11 +406,13 @@ class OracleEngine // enqueue query message to oracle machine node enqueueOracleQuery(queryId, interfaceIndex, timeoutMillisec, queryData, querySize); + // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + return queryId; } // Enqueue oracle machine query message. May be called from tick processor or contract processor only (uses reorgBuffer). - void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint16_t timeoutMillisec, const void* queryData, uint16_t querySize) + void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint32_t timeoutMillisec, const void* queryData, uint16_t querySize) { // Prepare message payload OracleMachineQuery* omq = reinterpret_cast(reorgBuffer); @@ -411,7 +463,8 @@ class OracleEngine // get reply state const auto replyStateIdx = oqm.statusVar.pending.replyStateIndex; ASSERT(replyStateIdx < MAX_SIMULTANEOUS_ORACLE_QUERIES); - OracleReplyState& replyState = replyStates[replyStateIdx]; + auto& replyState = replyStates[replyStateIdx]; + ASSERT(replyState.queryId == replyMessage->oracleQueryId); // return if we already got a reply if (replyState.ownReplySize) @@ -432,90 +485,211 @@ class OracleEngine pendingCommitReplyStateIndices.add(replyStateIdx); } - /// Return array of reply indices and size of array (as output-by-reference parameter). To be used for getReplyCommitTransactionItem(). - const uint32_t* getPendingReplyCommitTransactionIndices(uint32_t& arraySizeOutput) const - { - arraySizeOutput = pendingCommitReplyStateIndices.numValues; - return pendingCommitReplyStateIndices.values; - } - /** - * Return commit items in OracleReplyCommitTransaction. + * Prepare OracleReplyCommitTransaction in txBuffer, setting all except signature. + * + * @param txBuffer Buffer for constructing the transaction. Size must be at least MAX_TRANSACTION_SIZE bytes. * @param computorIdx Index of computor list of computors broadcasted by arbitrator. * @param ownComputorIdx Index of computor in local array computorSeeds. - * @param replyIdx Index of reply to consider. Use getPendingReplyCommitTransactionIndices() to get an array of those. * @param txScheduleTick Tick, in which the transaction is supposed to be scheduled. - * @param commit Pointer to output buffer of commit data in transaction that is being constructed. - * @return Whether this computor/reply is supposed to be added to tx. If false, commit is untouched. + * @param startIdx Index returned by the previous call of this function if more than one tx is required. + * @return 0 if no tx needs to be sent; UINT32_MAX if all pending commits are included in the created tx; + * any value in between indicates that another tx needs to be created and should be passed as the start + * index for the next call of this function * * Called from tick processor. */ - bool getReplyCommitTransactionItem( - uint16_t computorIdx, uint16_t ownComputorIdx, - int32_t replyIdx, uint32_t txScheduleTick, - OracleReplyCommitTransactionItem* commit) + uint32_t getReplyCommitTransaction( + void* txBuffer, uint16_t computorIdx, uint16_t ownComputorIdx, + uint32_t txScheduleTick, uint32_t startIdx = 0) { // check inputs - ASSERT(commit); - if (ownComputorIdx >= computorSeedsCount || computorIdx >= NUMBER_OF_COMPUTORS || replyIdx >= MAX_SIMULTANEOUS_ORACLE_QUERIES) - return false; + ASSERT(txBuffer); + if (ownComputorIdx >= ownComputorSeedsCount || computorIdx >= NUMBER_OF_COMPUTORS || txScheduleTick <= system.tick) + return 0; + + // init data pointers and reply commit counter + auto* tx = reinterpret_cast(txBuffer); + auto* commits = reinterpret_cast(tx->inputPtr()); + uint16_t commitsCount = 0; + constexpr uint16_t maxCommitsCount = MAX_INPUT_SIZE / sizeof(OracleReplyCommitTransactionItem); + + // consider queries with pending commit tx, specifically the reply data indices of those + const unsigned int replyIdxCount = pendingCommitReplyStateIndices.numValues; + const unsigned int* replyIndices = pendingCommitReplyStateIndices.values; + unsigned int idx = startIdx; + for (; idx < replyIdxCount; ++idx) + { + // get reply state and check that oracle reply has been received + const unsigned int replyIdx = replyIndices[idx]; + if (replyIdx >= MAX_SIMULTANEOUS_ORACLE_QUERIES) + continue; + auto& replyState = replyStates[replyIdx]; + if (replyState.queryId <= 0 || replyState.ownReplySize == 0) + continue; - // get reply state and check that oracle reply has been received - OracleReplyState& replyState = replyStates[replyIdx]; - if (replyState.queryId == 0 || replyState.ownReplySize == 0) - return false; + // tx already executed or scheduled? + if (replyState.ownReplyCommitComputorTxExecuted[ownComputorIdx] || + replyState.ownReplyCommitComputorTxTick[ownComputorIdx] >= system.tick) // TODO: > or >= ? + continue; - // tx already executed or scheduled? - if (replyState.ownReplyCommitComputorTxExecuted[ownComputorIdx] || - replyState.ownReplyCommitComputorTxTick[ownComputorIdx] >= system.tick) // TODO: > or >= ? - return false; + // additional commit required -> leave loop early to finish tx + if (commitsCount == maxCommitsCount) + break; - // set known data of commit tx part - commit->queryId = replyState.queryId; - commit->replyDigest = replyState.ownReplyDigest; + // set known data of commit tx part + commits[commitsCount].queryId = replyState.queryId; + commits[commitsCount].replyDigest = replyState.ownReplyDigest; - // compute knowledge proof of commit = K12(oracle reply + computor index) - ASSERT(replyState.ownReplySize <= MAX_ORACLE_REPLY_SIZE); - *(uint16_t*)(replyState.ownReplyData + replyState.ownReplySize) = computorIdx; - KangarooTwelve(replyState.ownReplyData, replyState.ownReplySize + 2, &commit->replyKnowledgeProof, 32); + // compute knowledge proof of commit = K12(oracle reply + computor index) + ASSERT(replyState.ownReplySize <= MAX_ORACLE_REPLY_SIZE); + *(uint16_t*)(replyState.ownReplyData + replyState.ownReplySize) = computorIdx; + KangarooTwelve(replyState.ownReplyData, replyState.ownReplySize + 2, &commits[commitsCount].replyKnowledgeProof, 32); - // signal to schedule tx for given tick - replyState.ownReplyCommitComputorTxTick[ownComputorIdx] = txScheduleTick; - return true; + // signal to schedule tx for given tick + replyState.ownReplyCommitComputorTxTick[ownComputorIdx] = txScheduleTick; + + // we have compelted adding this commit + ++commitsCount; + } + + // no reply commits needed? -> signal to skip tx + if (!commitsCount) + return 0; + + // finish all of tx except for source public key and signature + tx->sourcePublicKey = ownComputorPublicKeys[ownComputorIdx]; + tx->destinationPublicKey = m256i::zero(); + tx->amount = 0; + tx->tick = txScheduleTick; + tx->inputType = OracleReplyCommitTransactionPrefix::transactionType(); + tx->inputSize = commitsCount * sizeof(OracleReplyCommitTransactionItem); + + // if we had to break from the loop early, return and signal to call this again for creating another + // tx with the start index we return here + if (idx < replyIdxCount) + return idx; + + // signal that the tx is ready and the function doesn't need to be called again for more commits + return UINT32_MAX; } // Called from tick processor. - void processTransactionOracleReplyCommit(const OracleReplyCommitTransactionPrefix* transaction) + bool processOracleReplyCommitTransaction(const OracleReplyCommitTransactionPrefix* transaction) { + // check precondition for calling with ASSERTs ASSERT(transaction != nullptr); ASSERT(transaction->checkValidity()); ASSERT(isZero(transaction->destinationPublicKey)); - ASSERT(transaction->tick == system.tick); + ASSERT(transaction->inputType == OracleReplyCommitTransactionPrefix::transactionType()); + // check size of tx if (transaction->inputSize < OracleReplyCommitTransactionPrefix::minInputSize()) - return; + return false; + // get computor index int compIdx = computorIndex(transaction->sourcePublicKey); if (compIdx < 0) - return; + return false; + // process the N commits in this tx const OracleReplyCommitTransactionItem* item = (const OracleReplyCommitTransactionItem*)transaction->inputPtr(); uint32_t size = sizeof(OracleReplyCommitTransactionItem); while (size <= transaction->inputSize) { + // get and check query_id uint32_t queryIndex; if (!queryIdToIndex->get(item->queryId, queryIndex) || queryIndex >= oracleQueryCount) continue; + // get query metadata and check state OracleQueryMetadata& oqm = queries[queryIndex]; if (oqm.status != ORACLE_QUERY_STATUS_PENDING && oqm.status != ORACLE_QUERY_STATUS_COMMITTED) continue; - // TODO + // get reply state + const auto replyStateIdx = oqm.statusVar.pending.replyStateIndex; + ASSERT(replyStateIdx < MAX_SIMULTANEOUS_ORACLE_QUERIES); + auto& replyState = replyStates[replyStateIdx]; + ASSERT(replyState.queryId == item->queryId); + + // ignore commit if we already have processed a commit by this computor + if (replyState.replyCommitTicks[compIdx] != 0) + continue; + // save reply commit of computor + replyState.replyCommitDigests[compIdx] = item->replyDigest; + replyState.replyCommitKnowledgeProofs[compIdx] = item->replyKnowledgeProof; + replyState.replyCommitTicks[compIdx] = transaction->tick; + + // if tx is from own computor, prevent rescheduling of commit tx + for (auto i = 0ull; replyState.ownReplyCommitExecCount < ownComputorSeedsCount && i < ownComputorSeedsCount; ++i) + { + if (!replyState.ownReplyCommitComputorTxExecuted[i] && ownComputorPublicKeys[i] == transaction->sourcePublicKey) + { + replyState.ownReplyCommitComputorTxExecuted[i] = transaction->tick; + ++replyState.ownReplyCommitExecCount; + break; + } + } + + // update reply commit histogram + // 1. search existing or free slot of digest in histogram array + uint16_t histIdx = 0; + while (replyState.replyCommitHistogramCount[histIdx] != 0 && + item->replyDigest != replyState.replyCommitDigests[replyState.replyCommitHistogramIdx[histIdx]]) + { + ASSERT(histIdx < NUMBER_OF_COMPUTORS); + ++histIdx; + } + // 2. update slot + if (replyState.replyCommitHistogramCount[histIdx] == 0) + { + // first time we see this commit digest + replyState.replyCommitHistogramIdx[histIdx] = compIdx; + } + ++replyState.replyCommitHistogramCount[histIdx]; + // 3. update variables that trigger reveal + ++replyState.totalCommits; + if (replyState.replyCommitHistogramCount[histIdx] > replyState.replyCommitHistogramCount[replyState.mostCommitsHistIdx]) + replyState.mostCommitsHistIdx = histIdx; + + // check if there are enough computor commits for decision + const auto mostCommitsCount = replyState.replyCommitHistogramCount[replyState.mostCommitsHistIdx]; + if (mostCommitsCount >= QUORUM) + { + // enough commits for the reply reveal transaction + // -> switch to status COMMITTED + if (oqm.status != ORACLE_QUERY_STATUS_COMMITTED) + { + oqm.status = ORACLE_QUERY_STATUS_COMMITTED; + pendingCommitReplyStateIndices.removeByValue(replyStateIdx); + pendingRevealReplyStateIndices.add(replyStateIdx); + // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + } + } + else if (replyState.totalCommits - mostCommitsCount > NUMBER_OF_COMPUTORS - QUORUM) + { + // more than 1/3 of commits don't vote for most voted digest -> getting quorum isn't possible + // -> switch to status UNRESOLVABLE and cleanup data of pending reply immediately (no info for revenue required) + oqm.status = ORACLE_QUERY_STATUS_UNRESOLVABLE; + oqm.statusFlags |= ORACLE_FLAG_COMP_DISAGREE; + oqm.statusVar.failure.agreeingCommits = mostCommitsCount; + oqm.statusVar.failure.totalCommits = replyState.totalCommits; + notificationQueryIndicies.add(queryIndex); + pendingQueryIndices.removeByValue(queryIndex); + pendingCommitReplyStateIndices.removeByValue(replyStateIdx); + freeReplyStateSlot(replyStateIdx); + ++stats.unresolvableCount; + // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + } + + // go to next commit in tx size += sizeof(OracleReplyCommitTransactionItem); ++item; } + + return true; } void beginEpoch() @@ -560,16 +734,23 @@ class OracleEngine void logStatus(CHAR16* message) const { - setText(message, L"Oracles queries: "); + setText(message, L"Oracles queries: pending "); appendNumber(message, pendingCommitReplyStateIndices.numValues, FALSE); appendText(message, " / "); + appendNumber(message, pendingRevealReplyStateIndices.numValues, FALSE); + appendText(message, " / "); appendNumber(message, pendingQueryIndices.numValues, FALSE); - appendText(message, " got replies from OM node"); + appendText(message, ", successful "); + appendNumber(message, stats.successCount, FALSE); + appendText(message, ", timeout "); + appendNumber(message, stats.timeoutCount, FALSE); + appendText(message, ", unresolvable "); + appendNumber(message, stats.unresolvableCount, FALSE); logToConsole(message); } }; -GLOBAL_VAR_DECL OracleEngine oracleEngine; +GLOBAL_VAR_DECL OracleEngine oracleEngine; /* - Handle seamless transitions? Keep state? diff --git a/src/oracle_core/oracle_transactions.h b/src/oracle_core/oracle_transactions.h index 42e9c6f48..aff48313a 100644 --- a/src/oracle_core/oracle_transactions.h +++ b/src/oracle_core/oracle_transactions.h @@ -23,6 +23,8 @@ struct OracleReplyCommitTransactionPrefix : public Transaction { return sizeof(OracleReplyCommitTransactionItem); } + + // followed by: n times OracleReplyCommitTransactionItem }; // Transaction for revealing oracle reply. The tx prefix is followed by the OracleReply data diff --git a/src/qubic.cpp b/src/qubic.cpp index 58862726a..51719feb4 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -2834,7 +2834,7 @@ static void processTickTransaction(const Transaction* transaction, const m256i& case OracleReplyCommitTransactionPrefix::transactionType(): { - oracleEngine.processTransactionOracleReplyCommit((OracleReplyCommitTransactionPrefix*)transaction); + oracleEngine.processOracleReplyCommitTransaction((OracleReplyCommitTransactionPrefix*)transaction); } break; @@ -5747,7 +5747,7 @@ static bool initialize() } } - if (!oracleEngine.init()) + if (!oracleEngine.init(computorPublicKeys)) return false; #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES diff --git a/test/contract_testex.cpp b/test/contract_testex.cpp index 1c010fe60..1ad86b6fc 100644 --- a/test/contract_testex.cpp +++ b/test/contract_testex.cpp @@ -145,7 +145,7 @@ class ContractTestingTestEx : protected ContractTesting INIT_CONTRACT(QX); callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); - EXPECT_TRUE(oracleEngine.init()); + EXPECT_TRUE(oracleEngine.init(computorPublicKeys)); checkContractExecCleanup(); diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index 548e148b6..1d69d294b 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -3,18 +3,68 @@ #include "oracle_testing.h" -struct OracleEngineTest : public OracleEngine, LoggingTest +struct OracleEngineTest : public LoggingTest { OracleEngineTest() { - EXPECT_TRUE(init()); EXPECT_TRUE(initCommonBuffers()); + EXPECT_TRUE(initSpecialEntities()); + + // init computors + for (int computorIndex = 0; computorIndex < NUMBER_OF_COMPUTORS; computorIndex++) + { + broadcastedComputors.computors.publicKeys[computorIndex] = m256i(computorIndex * 2, 42, 13, 1337); + } + + // setup tick and time + system.tick = 1000; + etalonTick.year = 25; + etalonTick.month = 12; + etalonTick.day = 15; + etalonTick.hour = 16; + etalonTick.minute = 51; + etalonTick.second = 12; } ~OracleEngineTest() { deinitCommonBuffers(); - deinit(); + } +}; + +template +struct OracleEngineWithInitAndDeinit : public OracleEngine +{ + OracleEngineWithInitAndDeinit(const m256i* ownComputorPublicKeys) + { + this->init(ownComputorPublicKeys); + } + + ~OracleEngineWithInitAndDeinit() + { + this->deinit(); + } + + void checkPendingState(int64_t queryId, uint16_t totalCommitTxExecuted, uint16_t ownCommitTxExecuted, uint8_t expectedStatus) const + { + uint32_t queryIndex; + EXPECT_TRUE(this->queryIdToIndex->get(queryId, queryIndex)); + EXPECT_LT(queryIndex, this->oracleQueryCount); + const OracleQueryMetadata& oqm = this->queries[queryIndex]; + EXPECT_EQ(oqm.status, expectedStatus); + EXPECT_TRUE(oqm.status == ORACLE_QUERY_STATUS_PENDING || oqm.status == ORACLE_QUERY_STATUS_COMMITTED); + const OracleReplyState& replyState = this->replyStates[oqm.statusVar.pending.replyStateIndex]; + EXPECT_EQ((int)totalCommitTxExecuted, (int)replyState.totalCommits); + EXPECT_EQ((int)ownCommitTxExecuted, (int)replyState.ownReplyCommitExecCount); + } + + void checkStatus(int64_t queryId, uint8_t expectedStatus) const + { + uint32_t queryIndex; + EXPECT_TRUE(this->queryIdToIndex->get(queryId, queryIndex)); + EXPECT_LT(queryIndex, this->oracleQueryCount); + const OracleQueryMetadata& oqm = this->queries[queryIndex]; + EXPECT_EQ(oqm.status, expectedStatus); } }; @@ -22,17 +72,15 @@ static void dummyNotificationProc(const QPI::QpiContextProcedureCall&, void* sta { } -TEST(OracleEngine, ContractQuery) +TEST(OracleEngine, ContractQuerySuccess) { - OracleEngineTest oracleEngine; + OracleEngineTest test; - system.tick = 1000; - etalonTick.year = 25; - etalonTick.month = 12; - etalonTick.day = 15; - etalonTick.hour = 16; - etalonTick.minute = 51; - etalonTick.second = 12; + // simulate three nodes: one with 400 computor IDs, one with 200, and one with 76 + const m256i* allCompPubKeys = broadcastedComputors.computors.publicKeys; + OracleEngineWithInitAndDeinit<400> oracleEngine1(allCompPubKeys); + OracleEngineWithInitAndDeinit<200> oracleEngine2(allCompPubKeys + 400); + OracleEngineWithInitAndDeinit<76> oracleEngine3(allCompPubKeys + 600); OI::Price::OracleQuery priceQuery; priceQuery.oracle = m256i(1, 2, 3, 4); @@ -47,14 +95,16 @@ TEST(OracleEngine, ContractQuery) //------------------------------------------------------------------------- // start contract query / check message to OM node - QPI::sint64 queryId = oracleEngine.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize); + QPI::sint64 queryId = oracleEngine1.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize); EXPECT_EQ(queryId, getContractOracleQueryId(system.tick, 0)); checkNetworkMessageOracleMachineQuery(queryId, priceQuery.oracle, timeout); + EXPECT_EQ(queryId, oracleEngine2.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize)); + EXPECT_EQ(queryId, oracleEngine3.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize)); //------------------------------------------------------------------------- // get query contract data OI::Price::OracleQuery priceQueryReturned; - EXPECT_TRUE(oracleEngine.getOracleQuery(queryId, &priceQueryReturned, sizeof(priceQueryReturned))); + EXPECT_TRUE(oracleEngine1.getOracleQuery(queryId, &priceQueryReturned, sizeof(priceQueryReturned))); EXPECT_EQ(memcmp(&priceQueryReturned, &priceQuery, sizeof(priceQuery)), 0); //------------------------------------------------------------------------- @@ -70,12 +120,233 @@ TEST(OracleEngine, ContractQuery) priceOracleMachineReply.data.numerator = 1234; priceOracleMachineReply.data.denominator = 1; - oracleEngine.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + oracleEngine1.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + oracleEngine2.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + oracleEngine3.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); // duplicate from other node - oracleEngine.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + oracleEngine1.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); // other value from other node priceOracleMachineReply.data.numerator = 1233; - oracleEngine.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); -} \ No newline at end of file + oracleEngine1.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + + //------------------------------------------------------------------------- + // create reply commit tx (with local computor index 0 / global computor index 0) + uint8_t txBuffer[MAX_TRANSACTION_SIZE]; + auto* replyCommitTx = (OracleReplyCommitTransactionPrefix*)txBuffer; + EXPECT_EQ(oracleEngine1.getReplyCommitTransaction(txBuffer, 0, 0, system.tick + 3, 0), UINT32_MAX); + { + EXPECT_EQ((int)replyCommitTx->inputType, (int)OracleReplyCommitTransactionPrefix::transactionType()); + EXPECT_EQ(replyCommitTx->sourcePublicKey, allCompPubKeys[0]); + EXPECT_TRUE(isZero(replyCommitTx->destinationPublicKey)); + EXPECT_EQ(replyCommitTx->tick, system.tick + 3); + EXPECT_EQ((int)replyCommitTx->inputSize, (int)sizeof(OracleReplyCommitTransactionItem)); + } + + // second call in the same tick: no commits for tx + EXPECT_EQ(oracleEngine1.getReplyCommitTransaction(txBuffer, 0, 0, system.tick + 3, 0), 0); + + // process commit tx + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + + //------------------------------------------------------------------------- + // create and process enough reply commit tx to trigger reval tx + + // create tx of node 3 computers and process in all nodes + for (int i = 600; i < 676; ++i) + { + EXPECT_EQ(oracleEngine3.getReplyCommitTransaction(txBuffer, i, i - 600, system.tick + 3, 0), UINT32_MAX); + EXPECT_EQ(replyCommitTx->sourcePublicKey, allCompPubKeys[i]); + const int txFromNode3 = i - 600; + oracleEngine1.checkPendingState(queryId, txFromNode3 + 1, 1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine1.checkPendingState(queryId, txFromNode3 + 2, 1, ORACLE_QUERY_STATUS_PENDING); + oracleEngine2.checkPendingState(queryId, txFromNode3 + 1, 0, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine2.checkPendingState(queryId, txFromNode3 + 2, 0, ORACLE_QUERY_STATUS_PENDING); + oracleEngine3.checkPendingState(queryId, txFromNode3 + 1, txFromNode3 + 0, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine3.checkPendingState(queryId, txFromNode3 + 2, txFromNode3 + 1, ORACLE_QUERY_STATUS_PENDING); + } + + // create tx of node 2 computers and process in all nodes + for (int i = 400; i < 600; ++i) + { + EXPECT_EQ(oracleEngine2.getReplyCommitTransaction(txBuffer, i, i - 400, system.tick + 3, 0), UINT32_MAX); + EXPECT_EQ(replyCommitTx->sourcePublicKey, allCompPubKeys[i]); + const int txFromNode2 = i - 400; + oracleEngine1.checkPendingState(queryId, txFromNode2 + 77, 1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine1.checkPendingState(queryId, txFromNode2 + 78, 1, ORACLE_QUERY_STATUS_PENDING); + oracleEngine2.checkPendingState(queryId, txFromNode2 + 77, txFromNode2 + 0, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine2.checkPendingState(queryId, txFromNode2 + 78, txFromNode2 + 1, ORACLE_QUERY_STATUS_PENDING); + oracleEngine3.checkPendingState(queryId, txFromNode2 + 77, 76, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine3.checkPendingState(queryId, txFromNode2 + 78, 76, ORACLE_QUERY_STATUS_PENDING); + } + + // create tx of node 1 computers and process in all nodes + for (int i = 1; i < 400; ++i) + { + bool expectStatusCommitted = (i + 276) >= 451; + EXPECT_EQ(oracleEngine1.getReplyCommitTransaction(txBuffer, i, i, system.tick + 3, 0), ((expectStatusCommitted) ? 0 : UINT32_MAX)); + if (!expectStatusCommitted) + { + EXPECT_EQ(replyCommitTx->sourcePublicKey, allCompPubKeys[i]); + const int txFromNode1 = i; + uint8_t newStatus = (txFromNode1 + 276 < 450) ? ORACLE_QUERY_STATUS_PENDING : ORACLE_QUERY_STATUS_COMMITTED; + oracleEngine1.checkPendingState(queryId, txFromNode1 + 276, txFromNode1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine1.checkPendingState(queryId, txFromNode1 + 277, txFromNode1 + 1, newStatus); + oracleEngine2.checkPendingState(queryId, txFromNode1 + 276, 200, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine2.checkPendingState(queryId, txFromNode1 + 277, 200, newStatus); + oracleEngine3.checkPendingState(queryId, txFromNode1 + 276, 76, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine3.checkPendingState(queryId, txFromNode1 + 277, 76, newStatus); + } + else + { + oracleEngine1.checkPendingState(queryId, 451, 175, ORACLE_QUERY_STATUS_COMMITTED); + oracleEngine2.checkPendingState(queryId, 451, 200, ORACLE_QUERY_STATUS_COMMITTED); + oracleEngine3.checkPendingState(queryId, 451, 76, ORACLE_QUERY_STATUS_COMMITTED); + } + } +} + +TEST(OracleEngine, ContractQueryUnresolvable) +{ + OracleEngineTest test; + + // simulate three nodes: two with 200 computor IDs each, one with 276 IDs + const m256i* allCompPubKeys = broadcastedComputors.computors.publicKeys; + OracleEngineWithInitAndDeinit<200> oracleEngine1(allCompPubKeys); + OracleEngineWithInitAndDeinit<200> oracleEngine2(allCompPubKeys + 200); + OracleEngineWithInitAndDeinit<276> oracleEngine3(allCompPubKeys + 400); + + + OI::Price::OracleQuery priceQuery; + priceQuery.oracle = m256i(10, 20, 30, 40); + priceQuery.currency1 = m256i(20, 30, 40, 50); + priceQuery.currency2 = m256i(30, 40, 50, 60); + priceQuery.timestamp = QPI::DateAndTime::now(); + QPI::uint32 interfaceIndex = 0; + QPI::uint16 contractIndex = 2; + QPI::uint32 timeout = 120000; + USER_PROCEDURE notificationProc = dummyNotificationProc; + QPI::uint32 notificationLocalsSize = 1024; + + //------------------------------------------------------------------------- + // start contract query / check message to OM node + QPI::sint64 queryId = oracleEngine1.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize); + EXPECT_EQ(queryId, getContractOracleQueryId(system.tick, 0)); + EXPECT_EQ(queryId, oracleEngine2.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize)); + EXPECT_EQ(queryId, oracleEngine3.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize)); + checkNetworkMessageOracleMachineQuery(queryId, priceQuery.oracle, timeout); + + //------------------------------------------------------------------------- + // get query contract data + OI::Price::OracleQuery priceQueryReturned; + EXPECT_TRUE(oracleEngine1.getOracleQuery(queryId, &priceQueryReturned, sizeof(priceQueryReturned))); + EXPECT_EQ(memcmp(&priceQueryReturned, &priceQuery, sizeof(priceQuery)), 0); + + //------------------------------------------------------------------------- + // process simulated reply from OM nodes + struct + { + OracleMachineReply metatdata; + OI::Price::OracleReply data; + } priceOracleMachineReply; + + // reply received/committed by node 1 and 2 + priceOracleMachineReply.metatdata.oracleMachineErrorFlags = 0; + priceOracleMachineReply.metatdata.oracleQueryId = queryId; + priceOracleMachineReply.data.numerator = 1234; + priceOracleMachineReply.data.denominator = 1; + oracleEngine1.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + oracleEngine2.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + + // reply received/committed by node 1 and 2 + priceOracleMachineReply.data.numerator = 1233; + priceOracleMachineReply.data.denominator = 1; + oracleEngine3.processOracleMachineReply(&priceOracleMachineReply.metatdata, sizeof(priceOracleMachineReply)); + + + //------------------------------------------------------------------------- + // create and process reply commits of node 3 computers and process in all nodes + uint8_t txBuffer[MAX_TRANSACTION_SIZE]; + auto* replyCommitTx = (OracleReplyCommitTransactionPrefix*)txBuffer; + for (int ownCompIdx = 0; ownCompIdx < 200; ++ownCompIdx) + { + int allCompIdx = ownCompIdx; + EXPECT_EQ(oracleEngine1.getReplyCommitTransaction(txBuffer, allCompIdx, ownCompIdx, system.tick + 3, 0), UINT32_MAX); + EXPECT_EQ(replyCommitTx->sourcePublicKey, allCompPubKeys[allCompIdx]); + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine1.checkPendingState(queryId, 3 * ownCompIdx + 1, ownCompIdx + 1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine2.checkPendingState(queryId, 3 * ownCompIdx + 1, ownCompIdx, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine3.checkPendingState(queryId, 3 * ownCompIdx + 1, ownCompIdx, ORACLE_QUERY_STATUS_PENDING); + + allCompIdx = ownCompIdx + 200; + EXPECT_EQ(oracleEngine2.getReplyCommitTransaction(txBuffer, allCompIdx, ownCompIdx, system.tick + 3, 0), UINT32_MAX); + EXPECT_EQ(replyCommitTx->sourcePublicKey, allCompPubKeys[allCompIdx]); + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine1.checkPendingState(queryId, 3 * ownCompIdx + 2, ownCompIdx + 1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine2.checkPendingState(queryId, 3 * ownCompIdx + 2, ownCompIdx + 1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine3.checkPendingState(queryId, 3 * ownCompIdx + 2, ownCompIdx, ORACLE_QUERY_STATUS_PENDING); + + allCompIdx = ownCompIdx + 400; + EXPECT_EQ(oracleEngine3.getReplyCommitTransaction(txBuffer, allCompIdx, ownCompIdx, system.tick + 3, 0), UINT32_MAX); + EXPECT_EQ(replyCommitTx->sourcePublicKey, allCompPubKeys[allCompIdx]); + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine1.checkPendingState(queryId, 3 * ownCompIdx + 3, ownCompIdx + 1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine2.checkPendingState(queryId, 3 * ownCompIdx + 3, ownCompIdx + 1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine3.checkPendingState(queryId, 3 * ownCompIdx + 3, ownCompIdx + 1, ORACLE_QUERY_STATUS_PENDING); + } + + // create/process transcations that contradict with majority digest and turn status into unresolvable + for (int allCompIdx = 600; allCompIdx < 676; ++allCompIdx) + { + int ownCompIdx = allCompIdx - 400; + int unknownVotes = 676 - allCompIdx; + bool moreTxExpected = (unknownVotes > 450 - 400); + EXPECT_EQ(oracleEngine3.getReplyCommitTransaction(txBuffer, allCompIdx, ownCompIdx, system.tick + 3, 0), moreTxExpected ? UINT32_MAX : 0); + if (moreTxExpected) + { + EXPECT_EQ(replyCommitTx->sourcePublicKey, allCompPubKeys[allCompIdx]); + + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + } + + if (unknownVotes > 451 - 400) + { + oracleEngine1.checkPendingState(queryId, allCompIdx + 1, 200, ORACLE_QUERY_STATUS_PENDING); + oracleEngine2.checkPendingState(queryId, allCompIdx + 1, 200, ORACLE_QUERY_STATUS_PENDING); + oracleEngine3.checkPendingState(queryId, allCompIdx + 1, ownCompIdx + 1, ORACLE_QUERY_STATUS_PENDING); + } + else + { + oracleEngine1.checkStatus(queryId, ORACLE_QUERY_STATUS_UNRESOLVABLE); + oracleEngine2.checkStatus(queryId, ORACLE_QUERY_STATUS_UNRESOLVABLE); + oracleEngine3.checkStatus(queryId, ORACLE_QUERY_STATUS_UNRESOLVABLE); + } + } +} + +/* +Tests: +- oracleEngine.getReplyCommitTransaction() with more than 1 commit / tx +- processOracleReplyCommitTransaction wihtout get getReplyCommitTransaction +- trigger failure +*/ \ No newline at end of file From aea387ccdc06c885cc1a5a26b34ab8d408524c90 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:00:09 +0100 Subject: [PATCH 03/90] OracleEngine: add lock for mutal exclusion --- src/oracle_core/oracle_engine.h | 34 +++++++++++++++++++++++++++++++-- src/platform/concurrency.h | 17 +++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 9d15a10a1..948c868c5 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -186,7 +186,6 @@ struct UnsortedMultiset }; -// TODO: locking, implement hash function for queryIdToIndex based on tick template class OracleEngine { @@ -245,6 +244,9 @@ class OracleEngine /// array of ownComputorSeedsCount public keys (mainly for testing, in EFI core this points to computorPublicKeys from special_entities.h) const m256i* ownComputorPublicKeys; + /// lock for preventing race conditions in concurrent execution + mutable volatile char lock; + /// Return empty reply state slot or max uint32 value on error uint32_t getEmptyReplyStateSlot() { @@ -268,9 +270,11 @@ class OracleEngine } public: + /// Initialize object, passing array of own computor public keys (with number of elements given by template param ownComputorSeedsCount). bool init(const m256i* ownComputorPublicKeys) { this->ownComputorPublicKeys = ownComputorPublicKeys; + lock = 0; // alloc arrays and set to 0 if (!allocPoolWithErrorLog(L"OracleEngine::queries", MAX_ORACLE_QUERIES * sizeof(*queries), (void**)&queries, __LINE__) @@ -307,11 +311,13 @@ class OracleEngine void save() const { + LockGuard lockGuard(lock); // save state (excluding queryIdToIndex and unused parts of large buffers) } void load() { + LockGuard lockGuard(lock); // load state (excluding queryIdToIndex and unused parts of large buffers) // init queryIdToIndex } @@ -327,6 +333,10 @@ class OracleEngine // ASSERT that tx is in tick storage at tx->tick, txIndex. // check interface index // check size of payload vs expected query of given interface + + // lock for accessing engine data + LockGuard lockGuard(lock); + // add to query storage // send query to oracle machine node } @@ -339,6 +349,9 @@ class OracleEngine if (contractIndex >= MAX_NUMBER_OF_CONTRACTS || interfaceIndex >= OI::oracleInterfacesCount || querySize != OI::oracleInterfaces[interfaceIndex].querySize) return -1; + // lock for accessing engine data + LockGuard lockGuard(lock); + // check that still have free capacity for the query if (oracleQueryCount >= MAX_ORACLE_QUERIES || pendingQueryIndices.numValues >= MAX_SIMULTANEOUS_ORACLE_QUERIES || queryStorageBytesUsed + querySize > ORACLE_QUERY_STORAGE_SIZE) return -1; @@ -411,8 +424,9 @@ class OracleEngine return queryId; } +protected: // Enqueue oracle machine query message. May be called from tick processor or contract processor only (uses reorgBuffer). - void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint32_t timeoutMillisec, const void* queryData, uint16_t querySize) + static void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint32_t timeoutMillisec, const void* queryData, uint16_t querySize) { // Prepare message payload OracleMachineQuery* omq = reinterpret_cast(reorgBuffer); @@ -425,6 +439,7 @@ class OracleEngine enqueueResponse((Peer*)0x1, sizeof(*omq) + querySize, OracleMachineQuery::type(), 0, omq); } +public: // CAUTION: Called from request processor, requires locking! void processOracleMachineReply(const OracleMachineReply* replyMessage, uint32_t replyMessageSize) { @@ -433,6 +448,9 @@ class OracleEngine if (replyMessageSize < sizeof(OracleMachineReply)) return; + // lock for accessing engine data + LockGuard lockGuard(lock); + // get query index uint32_t queryIndex; if (!queryIdToIndex->get(replyMessage->oracleQueryId, queryIndex) || queryIndex >= oracleQueryCount) @@ -514,6 +532,9 @@ class OracleEngine uint16_t commitsCount = 0; constexpr uint16_t maxCommitsCount = MAX_INPUT_SIZE / sizeof(OracleReplyCommitTransactionItem); + // lock for accessing engine data + LockGuard lockGuard(lock); + // consider queries with pending commit tx, specifically the reply data indices of those const unsigned int replyIdxCount = pendingCommitReplyStateIndices.numValues; const unsigned int* replyIndices = pendingCommitReplyStateIndices.values; @@ -592,6 +613,9 @@ class OracleEngine if (compIdx < 0) return false; + // lock for accessing engine data + LockGuard lockGuard(lock); + // process the N commits in this tx const OracleReplyCommitTransactionItem* item = (const OracleReplyCommitTransactionItem*)transaction->inputPtr(); uint32_t size = sizeof(OracleReplyCommitTransactionItem); @@ -694,6 +718,9 @@ class OracleEngine void beginEpoch() { + // lock for accessing engine data + LockGuard lockGuard(lock); + // TODO // clean all subscriptions // clean all queries (except for last n ticks in case of seamless transition) @@ -701,6 +728,9 @@ class OracleEngine bool getOracleQuery(int64_t queryId, void* queryData, uint16_t querySize) const { + // lock for accessing engine data + LockGuard lockGuard(lock); + // get query index uint32_t queryIndex; if (!queryIdToIndex->get(queryId, queryIndex) || queryIndex >= oracleQueryCount) diff --git a/src/platform/concurrency.h b/src/platform/concurrency.h index 4cdf5d3df..9bdeb8ff7 100644 --- a/src/platform/concurrency.h +++ b/src/platform/concurrency.h @@ -43,6 +43,23 @@ class BusyWaitingTracker // Release lock #define RELEASE(lock) lock = 0 +// Create an object of this class to lock until the end of the life-time of this object. +// Usually used on stack for making sure that the lock is released, no matter which way the function is left. +struct LockGuard +{ + LockGuard(volatile char& lock) : _lock(lock) + { + ACQUIRE(_lock); + } + + ~LockGuard() + { + RELEASE(_lock); + } + + volatile char& _lock; +}; + #ifdef NDEBUG From 95745e5c83edf671af0a851cd332ce86b9b83d01 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:39:14 +0100 Subject: [PATCH 04/90] Refactor: move global tick storage instance to header --- src/oracle_core/oracle_engine.h | 1 + src/qubic.cpp | 1 - src/ticking/tick_storage.h | 3 + test/tick_storage.cpp | 165 ++++++++++++++++---------------- 4 files changed, 86 insertions(+), 84 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 948c868c5..d64382306 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -7,6 +7,7 @@ #include "system.h" #include "common_buffers.h" #include "spectrum/special_entities.h" +#include "ticking/tick_storage.h" #include "oracle_transactions.h" #include "core_om_network_messages.h" diff --git a/src/qubic.cpp b/src/qubic.cpp index 51719feb4..999a86038 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -138,7 +138,6 @@ static unsigned short numberOfOwnComputorIndices; static unsigned short ownComputorIndices[computorSeedsCount]; static unsigned short ownComputorIndicesMapping[computorSeedsCount]; -static TickStorage ts; static VoteCounter voteCounter; static ExecutionFeeReportCollector executionFeeReportCollector; static TickData nextTickData; diff --git a/src/ticking/tick_storage.h b/src/ticking/tick_storage.h index 3c9116e87..7dde48944 100644 --- a/src/ticking/tick_storage.h +++ b/src/ticking/tick_storage.h @@ -7,6 +7,7 @@ #include "platform/concurrency.h" #include "platform/console_logging.h" #include "platform/debugging.h" +#include "platform/global_var.h" #include "public_settings.h" @@ -1037,3 +1038,5 @@ class TickStorage } } transactionsDigestAccess; }; + +GLOBAL_VAR_DECL TickStorage ts; diff --git a/test/tick_storage.cpp b/test/tick_storage.cpp index 5876ace7f..103390c99 100644 --- a/test/tick_storage.cpp +++ b/test/tick_storage.cpp @@ -39,101 +39,100 @@ class TestTickStorage : public TickStorage nextTickTransactionOffset += transactionSize; } } -}; - -TestTickStorage ts; - -void addTick(unsigned int tick, unsigned int seed, unsigned short maxTransactions) -{ - // use pseudo-random sequence - std::mt19937 gen32(seed); - - // add tick data - TickData& td = ts.tickData.getByTickInCurrentEpoch(tick); - td.epoch = 1234; - td.tick = tick; - // add computor ticks - Tick* computorTicks = ts.ticks.getByTickInCurrentEpoch(tick); - for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) + void addTick(unsigned int tick, unsigned int seed, unsigned short maxTransactions) { - computorTicks[i].epoch = 1234; - computorTicks[i].computorIndex = i; - computorTicks[i].tick = tick; - computorTicks[i].prevResourceTestingDigest = gen32(); - } + // use pseudo-random sequence + std::mt19937 gen32(seed); - // add transactions of tick - unsigned int transactionNum = gen32() % (maxTransactions + 1); - unsigned int orderMode = gen32() % 2; - unsigned int transactionSlot; - for (unsigned int transaction = 0; transaction < transactionNum; ++transaction) - { - if (orderMode == 0) - transactionSlot = transaction; // standard order - else if (orderMode == 1) - transactionSlot = transactionNum - 1 - transaction; // backward order - ts.addTransaction(tick, transactionSlot, gen32() % MAX_INPUT_SIZE); - } - ts.checkStateConsistencyWithAssert(); -} - -void checkTick(unsigned int tick, unsigned int seed, unsigned short maxTransactions, bool previousEpoch = false) -{ - // only last ticks of previous epoch are kept in storage -> check okay - if (previousEpoch && !ts.tickInPreviousEpochStorage(tick)) - return; + // add tick data + TickData& td = tickData.getByTickInCurrentEpoch(tick); + td.epoch = 1234; + td.tick = tick; - // use pseudo-random sequence - std::mt19937 gen32(seed); - - // check tick data - TickData& td = previousEpoch ? ts.tickData.getByTickInPreviousEpoch(tick) : ts.tickData.getByTickInCurrentEpoch(tick); - EXPECT_EQ((int)td.epoch, (int)1234); - EXPECT_EQ(td.tick, tick); - - // check computor ticks - Tick* computorTicks = previousEpoch ? ts.ticks.getByTickInPreviousEpoch(tick) : ts.ticks.getByTickInCurrentEpoch(tick); - for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) - { - EXPECT_EQ((int)computorTicks[i].epoch, (int)1234); - EXPECT_EQ((int)computorTicks[i].computorIndex, (int)i); - EXPECT_EQ(computorTicks[i].tick, tick); - EXPECT_EQ(computorTicks[i].prevResourceTestingDigest, gen32()); - } + // add computor ticks + Tick* computorTicks = ticks.getByTickInCurrentEpoch(tick); + for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) + { + computorTicks[i].epoch = 1234; + computorTicks[i].computorIndex = i; + computorTicks[i].tick = tick; + computorTicks[i].prevResourceTestingDigest = gen32(); + } - // check transactions of tick - { - const auto* offsets = previousEpoch ? ts.tickTransactionOffsets.getByTickInPreviousEpoch(tick) : ts.tickTransactionOffsets.getByTickInCurrentEpoch(tick); + // add transactions of tick unsigned int transactionNum = gen32() % (maxTransactions + 1); unsigned int orderMode = gen32() % 2; unsigned int transactionSlot; - for (unsigned int transaction = 0; transaction < transactionNum; ++transaction) { - int expectedInputSize = (int)(gen32() % MAX_INPUT_SIZE); - if (orderMode == 0) transactionSlot = transaction; // standard order else if (orderMode == 1) transactionSlot = transactionNum - 1 - transaction; // backward order + addTransaction(tick, transactionSlot, gen32() % MAX_INPUT_SIZE); + } + checkStateConsistencyWithAssert(); + } + + void checkTick(unsigned int tick, unsigned int seed, unsigned short maxTransactions, bool previousEpoch = false) + { + // only last ticks of previous epoch are kept in storage -> check okay + if (previousEpoch && !tickInPreviousEpochStorage(tick)) + return; - // If previousEpoch, some transactions at the beginning may not have fit into the storage and are missing -> check okay - // If current epoch, some may be missing at he end due to limited storage -> check okay - if (!offsets[transactionSlot]) - continue; + // use pseudo-random sequence + std::mt19937 gen32(seed); - Transaction* tp = ts.tickTransactions(offsets[transactionSlot]); - EXPECT_TRUE(tp->checkValidity()); - EXPECT_EQ(tp->tick, tick); - EXPECT_EQ((int)tp->inputSize, expectedInputSize); + // check tick data + TickData& td = previousEpoch ? tickData.getByTickInPreviousEpoch(tick) : tickData.getByTickInCurrentEpoch(tick); + EXPECT_EQ((int)td.epoch, (int)1234); + EXPECT_EQ(td.tick, tick); + + // check computor ticks + Tick* computorTicks = previousEpoch ? ticks.getByTickInPreviousEpoch(tick) : ticks.getByTickInCurrentEpoch(tick); + for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) + { + EXPECT_EQ((int)computorTicks[i].epoch, (int)1234); + EXPECT_EQ((int)computorTicks[i].computorIndex, (int)i); + EXPECT_EQ(computorTicks[i].tick, tick); + EXPECT_EQ(computorTicks[i].prevResourceTestingDigest, gen32()); } - } -} + // check transactions of tick + { + const auto* offsets = previousEpoch ? tickTransactionOffsets.getByTickInPreviousEpoch(tick) : tickTransactionOffsets.getByTickInCurrentEpoch(tick); + unsigned int transactionNum = gen32() % (maxTransactions + 1); + unsigned int orderMode = gen32() % 2; + unsigned int transactionSlot; + + for (unsigned int transaction = 0; transaction < transactionNum; ++transaction) + { + int expectedInputSize = (int)(gen32() % MAX_INPUT_SIZE); + + if (orderMode == 0) + transactionSlot = transaction; // standard order + else if (orderMode == 1) + transactionSlot = transactionNum - 1 - transaction; // backward order + + // If previousEpoch, some transactions at the beginning may not have fit into the storage and are missing -> check okay + // If current epoch, some may be missing at he end due to limited storage -> check okay + if (!offsets[transactionSlot]) + continue; + + Transaction* tp = tickTransactions(offsets[transactionSlot]); + EXPECT_TRUE(tp->checkValidity()); + EXPECT_EQ(tp->tick, tick); + EXPECT_EQ((int)tp->inputSize, expectedInputSize); + } + } + } +}; -TEST(TestCoreTickStorage, EpochTransition) { +TEST(TestCoreTickStorage, EpochTransition) +{ + TestTickStorage ts; unsigned int seed = 42; // use pseudo-random sequence @@ -170,11 +169,11 @@ TEST(TestCoreTickStorage, EpochTransition) { // add ticks for (int i = 0; i < firstEpochTicks; ++i) - addTick(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + ts.addTick(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); // check ticks for (int i = 0; i < firstEpochTicks; ++i) - checkTick(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + ts.checkTick(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); // Epoch transistion ts.beginEpoch(secondEpochTick0); @@ -182,14 +181,14 @@ TEST(TestCoreTickStorage, EpochTransition) { // add ticks for (int i = 0; i < secondEpochTicks; ++i) - addTick(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions); + ts.addTick(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions); // check ticks for (int i = 0; i < secondEpochTicks; ++i) - checkTick(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions); + ts.checkTick(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions); bool previousEpoch = true; for (int i = 0; i < firstEpochTicks; ++i) - checkTick(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions, previousEpoch); + ts.checkTick(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions, previousEpoch); // Epoch transistion ts.beginEpoch(thirdEpochTick0); @@ -197,13 +196,13 @@ TEST(TestCoreTickStorage, EpochTransition) { // add ticks for (int i = 0; i < thirdEpochTicks; ++i) - addTick(thirdEpochTick0 + i, thirdEpochSeeds[i], maxTransactions); + ts.addTick(thirdEpochTick0 + i, thirdEpochSeeds[i], maxTransactions); // check ticks for (int i = 0; i < thirdEpochTicks; ++i) - checkTick(thirdEpochTick0 + i, thirdEpochSeeds[i], maxTransactions); + ts.checkTick(thirdEpochTick0 + i, thirdEpochSeeds[i], maxTransactions); for (int i = 0; i < secondEpochTicks; ++i) - checkTick(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions, previousEpoch); + ts.checkTick(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions, previousEpoch); ts.deinit(); } From a90d5673840df112c61120d643d163572b35c5e7 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:23:25 +0100 Subject: [PATCH 05/90] Create/process reveal tx + notify contract --- src/contract_core/contract_exec.h | 2 +- src/contracts/qpi.h | 2 +- src/network_messages/common_def.h | 5 +- src/oracle_core/oracle_engine.h | 269 ++++++++++++++++++++++++-- src/oracle_core/oracle_transactions.h | 2 + test/oracle_engine.cpp | 19 ++ test/oracle_testing.h | 1 + 7 files changed, 280 insertions(+), 20 deletions(-) diff --git a/src/contract_core/contract_exec.h b/src/contract_core/contract_exec.h index 659d430b6..2f8bcf673 100644 --- a/src/contract_core/contract_exec.h +++ b/src/contract_core/contract_exec.h @@ -1282,7 +1282,7 @@ struct UserProcedureNotification // - oracle notifications (managed by oracleEngine) struct QpiContextUserProcedureNotificationCall : public QPI::QpiContextProcedureCall { - QpiContextUserProcedureNotificationCall(const UserProcedureNotification& notification) : QPI::QpiContextProcedureCall(notif.contractIndex, NULL_ID, 0, USER_PROCEDURE_NOTIFICATION_CALL), notif(notification) + QpiContextUserProcedureNotificationCall(const UserProcedureNotification& notification) : QPI::QpiContextProcedureCall(notification.contractIndex, NULL_ID, 0, USER_PROCEDURE_NOTIFICATION_CALL), notif(notification) { contractActionTracker.init(); } diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index 1fef17fbd..294273041 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -2472,7 +2472,7 @@ namespace QPI { sint64 queryId; ///< ID of the oracle query that led to this notification. uint32 subscriptionId; ///< ID of the oracle subscription or 0 in case of a pure oracle query. - uint8 status; ///< Oracle query status as defined in `network_messages/common_def.h` + uint32 status; ///< Oracle query status as defined in `network_messages/common_def.h` typename OracleInterface::OracleReply reply; ///< Oracle reply if status == ORACLE_QUERY_STATUS_SUCCESS }; diff --git a/src/network_messages/common_def.h b/src/network_messages/common_def.h index a15a2d972..e8580e0e7 100644 --- a/src/network_messages/common_def.h +++ b/src/network_messages/common_def.h @@ -47,8 +47,8 @@ typedef union m256i #endif -constexpr uint16_t MAX_ORACLE_QUERY_SIZE = MAX_INPUT_SIZE - 8; -constexpr uint16_t MAX_ORACLE_REPLY_SIZE = MAX_INPUT_SIZE - 8; +constexpr uint16_t MAX_ORACLE_QUERY_SIZE = MAX_INPUT_SIZE - 16; +constexpr uint16_t MAX_ORACLE_REPLY_SIZE = MAX_INPUT_SIZE - 16; constexpr uint8_t ORACLE_QUERY_TYPE_CONTRACT_QUERY = 0; constexpr uint8_t ORACLE_QUERY_TYPE_CONTRACT_SUBSCRIPTION = 1; @@ -74,6 +74,7 @@ constexpr uint16_t ORACLE_FLAG_BAD_SIZE_REPLY = 0x200; ///< Oracle engine got re constexpr uint16_t ORACLE_FLAG_OM_DISAGREE = 0x400; ///< Oracle engine got different replies from oracle machines. constexpr uint16_t ORACLE_FLAG_COMP_DISAGREE = 0x800; ///< The reply commits differ too much and no quorum can be reached. constexpr uint16_t ORACLE_FLAG_TIMEOUT = 0x1000; ///< The weren't enough reply commit tx with the same digest before timeout (< 451). +constexpr uint16_t ORACLE_FLAG_BAD_SIZE_REVEAL = 0x200; ///< Reply in a reveal tx had wrong size. typedef union IPv4Address { diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index d64382306..d2fa30d7f 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -8,6 +8,7 @@ #include "common_buffers.h" #include "spectrum/special_entities.h" #include "ticking/tick_storage.h" +#include "contract_core/contract_exec.h" #include "oracle_transactions.h" #include "core_om_network_messages.h" @@ -83,6 +84,8 @@ struct OracleSubscriptionContractStatus uint16_t contractIndex; uint16_t notificationPeriodMinutes; QPI::DateAndTime nextNotification; + USER_PROCEDURE notificationProcedure; + uint32_t notificationLocalsSize; }; struct OracleSubscriptionMetadata @@ -129,6 +132,7 @@ struct OracleReplyState uint16_t mostCommitsHistIdx; uint16_t totalCommits; + uint32_t expectedRevealTxTick; uint32_t revealTick; uint32_t revealTxIndex; @@ -210,8 +214,11 @@ class OracleEngine uint32_t queryIndexInTick; } contractQueryIdState; + // data type of state of received OM reply and computor commits for single oracle query (used before reveal) + typedef OracleReplyState ReplyState; + // state of received OM reply and computor commits for each oracle query (used before reveal) - OracleReplyState* replyStates; + ReplyState* replyStates; // index in replyStates to check next for empty slot (cyclic buffer) int32_t replyStatesIndex; @@ -225,9 +232,6 @@ class OracleEngine /// fast lookup of reply state indices for which reveal tx is pending UnsortedMultiset pendingRevealReplyStateIndices; - /// fast lookup of query indices for which the contract should be notified - UnsortedMultiset notificationQueryIndicies; - struct { /// total number of successful oracle queries unsigned long long successCount; @@ -245,6 +249,9 @@ class OracleEngine /// array of ownComputorSeedsCount public keys (mainly for testing, in EFI core this points to computorPublicKeys from special_entities.h) const m256i* ownComputorPublicKeys; + /// buffer used to store input for contract notifications + uint8_t contractNotificationInputBuffer[16 + MAX_ORACLE_REPLY_SIZE]; + /// lock for preventing race conditions in concurrent execution mutable volatile char lock; @@ -293,7 +300,6 @@ class OracleEngine pendingQueryIndices.numValues = 0; pendingCommitReplyStateIndices.numValues = 0; pendingRevealReplyStateIndices.numValues = 0; - notificationQueryIndicies.numValues = 0; setMem(&stats, sizeof(stats), 0); return true; @@ -407,7 +413,7 @@ class OracleEngine queryMetadata.statusVar.pending.replyStateIndex = replyStateSlotIdx; // init reply state (temporary until reply is revealed) - auto& replyState = replyStates[replyStateSlotIdx]; + ReplyState& replyState = replyStates[replyStateSlotIdx]; setMem(&replyState, sizeof(replyState), 0); replyState.queryId = queryId; replyState.notificationProcedure = notificationProcedure; @@ -440,6 +446,41 @@ class OracleEngine enqueueResponse((Peer*)0x1, sizeof(*omq) + querySize, OracleMachineQuery::type(), 0, omq); } + void notifyContractsIfAny(const OracleQueryMetadata& oqm, const ReplyState& replyState, const void* replyData = nullptr) + { + ASSERT(oqm.queryId == replyState.queryId); + if (!replyState.notificationProcedure || oqm.type == ORACLE_QUERY_TYPE_USER_QUERY) + return; + + const auto replySize = OI::oracleInterfaces[oqm.interfaceIndex].replySize; + ASSERT(16 + replySize < 0xffff); + + UserProcedureNotification notification; + if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) + { + // setup notification + notification.contractIndex = oqm.typeVar.contract.queryingContract; + notification.procedure = replyState.notificationProcedure; + notification.inputSize = (uint16_t)(16 + replySize); + notification.inputPtr = contractNotificationInputBuffer; + notification.localsSize = replyState.notificationLocalsSize; + setMem(contractNotificationInputBuffer, notification.inputSize, 0); + *(int64_t*)(contractNotificationInputBuffer + 0) = oqm.queryId; + *(uint32_t*)(contractNotificationInputBuffer + 8) = 0; + *(uint32_t*)(contractNotificationInputBuffer + 12) = oqm.status; + if (replyData) + { + copyMem(contractNotificationInputBuffer + 16, replyData, replySize); + } + + // run notification + QpiContextUserProcedureNotificationCall qpiContext(notification); + qpiContext.call(); + } + + // TODO: handle subscriptions + } + public: // CAUTION: Called from request processor, requires locking! void processOracleMachineReply(const OracleMachineReply* replyMessage, uint32_t replyMessageSize) @@ -482,7 +523,7 @@ class OracleEngine // get reply state const auto replyStateIdx = oqm.statusVar.pending.replyStateIndex; ASSERT(replyStateIdx < MAX_SIMULTANEOUS_ORACLE_QUERIES); - auto& replyState = replyStates[replyStateIdx]; + ReplyState& replyState = replyStates[replyStateIdx]; ASSERT(replyState.queryId == replyMessage->oracleQueryId); // return if we already got a reply @@ -546,7 +587,7 @@ class OracleEngine const unsigned int replyIdx = replyIndices[idx]; if (replyIdx >= MAX_SIMULTANEOUS_ORACLE_QUERIES) continue; - auto& replyState = replyStates[replyIdx]; + ReplyState& replyState = replyStates[replyIdx]; if (replyState.queryId <= 0 || replyState.ownReplySize == 0) continue; @@ -571,7 +612,7 @@ class OracleEngine // signal to schedule tx for given tick replyState.ownReplyCommitComputorTxTick[ownComputorIdx] = txScheduleTick; - // we have compelted adding this commit + // we have completed adding this commit ++commitsCount; } @@ -579,7 +620,7 @@ class OracleEngine if (!commitsCount) return 0; - // finish all of tx except for source public key and signature + // finish all of tx except for signature tx->sourcePublicKey = ownComputorPublicKeys[ownComputorIdx]; tx->destinationPublicKey = m256i::zero(); tx->amount = 0; @@ -610,7 +651,7 @@ class OracleEngine return false; // get computor index - int compIdx = computorIndex(transaction->sourcePublicKey); + const int compIdx = computorIndex(transaction->sourcePublicKey); if (compIdx < 0) return false; @@ -622,7 +663,7 @@ class OracleEngine uint32_t size = sizeof(OracleReplyCommitTransactionItem); while (size <= transaction->inputSize) { - // get and check query_id + // get and check query index uint32_t queryIndex; if (!queryIdToIndex->get(item->queryId, queryIndex) || queryIndex >= oracleQueryCount) continue; @@ -635,7 +676,7 @@ class OracleEngine // get reply state const auto replyStateIdx = oqm.statusVar.pending.replyStateIndex; ASSERT(replyStateIdx < MAX_SIMULTANEOUS_ORACLE_QUERIES); - auto& replyState = replyStates[replyStateIdx]; + ReplyState& replyState = replyStates[replyStateIdx]; ASSERT(replyState.queryId == item->queryId); // ignore commit if we already have processed a commit by this computor @@ -696,16 +737,21 @@ class OracleEngine else if (replyState.totalCommits - mostCommitsCount > NUMBER_OF_COMPUTORS - QUORUM) { // more than 1/3 of commits don't vote for most voted digest -> getting quorum isn't possible - // -> switch to status UNRESOLVABLE and cleanup data of pending reply immediately (no info for revenue required) + // -> switch to status UNRESOLVABLE oqm.status = ORACLE_QUERY_STATUS_UNRESOLVABLE; oqm.statusFlags |= ORACLE_FLAG_COMP_DISAGREE; oqm.statusVar.failure.agreeingCommits = mostCommitsCount; oqm.statusVar.failure.totalCommits = replyState.totalCommits; - notificationQueryIndicies.add(queryIndex); pendingQueryIndices.removeByValue(queryIndex); + ++stats.unresolvableCount; + + // run contract notification(s) if needed + notifyContractsIfAny(oqm, replyState); + + // cleanup data of pending reply immediately (no info for revenue required) pendingCommitReplyStateIndices.removeByValue(replyStateIdx); freeReplyStateSlot(replyStateIdx); - ++stats.unresolvableCount; + // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status } @@ -717,6 +763,197 @@ class OracleEngine return true; } + /** + * Prepare OracleReplyRevealTransaction in txBuffer, setting all except signature. + * + * @param txBuffer Buffer for constructing the transaction. Size must be at least MAX_TRANSACTION_SIZE bytes. + * @param ownComputorIdx Index of computor in local array computorSeeds. + * @param txScheduleTick Tick, in which the transaction is supposed to be scheduled. + * @param startIdx Index returned by the previous call of this function if more than one tx is required. + * @return 0 if no tx needs to be sent; any other value indicates that another tx needs to be created and it + * should be passed as the start index for the next call of this function + * + * Called from tick processor. + */ + uint32_t getReplyRevealTransaction(void* txBuffer, uint16_t ownComputorIdx, uint32_t txScheduleTick, uint32_t startIdx = 0) + { + // check inputs + ASSERT(txBuffer); + if (ownComputorIdx >= ownComputorSeedsCount || txScheduleTick <= system.tick) + return 0; + + // init data pointer + auto* tx = reinterpret_cast(txBuffer); + void* txReplyData = tx + 1; + + // lock for accessing engine data + LockGuard lockGuard(lock); + + // consider queries with pending reveal tx, specifically the reply data indices of those + const unsigned int replyIdxCount = pendingRevealReplyStateIndices.numValues; + const unsigned int* replyIndices = pendingRevealReplyStateIndices.values; + unsigned int idx = startIdx; + for (; idx < replyIdxCount; ++idx) + { + // get reply state and check that oracle reply has been received and a quorum has formed about the value + const unsigned int replyIdx = replyIndices[idx]; + if (replyIdx >= MAX_SIMULTANEOUS_ORACLE_QUERIES) + continue; + ReplyState& replyState = replyStates[replyIdx]; + if (replyState.queryId <= 0 || replyState.ownReplySize == 0) + continue; + const uint16_t mostCommitsCount = replyState.replyCommitHistogramCount[replyState.mostCommitsHistIdx]; + ASSERT(replyState.mostCommitsHistIdx < NUMBER_OF_COMPUTORS && mostCommitsCount <= NUMBER_OF_COMPUTORS); + if (mostCommitsCount < QUORUM) + continue; + + // tx already scheduled or seen? + if (replyState.expectedRevealTxTick >= system.tick) // TODO: > or >= ? + continue; + + // check if local view is the quorum view + const m256i quorumCommitDigest = replyState.replyCommitDigests[replyState.replyCommitHistogramIdx[replyState.mostCommitsHistIdx]]; + if (quorumCommitDigest != replyState.ownReplyDigest) + continue; + + // set all of tx except for signature + tx->sourcePublicKey = ownComputorPublicKeys[ownComputorIdx]; + tx->destinationPublicKey = m256i::zero(); + tx->amount = 0; + tx->tick = txScheduleTick; + tx->inputType = OracleReplyRevealTransactionPrefix::transactionType(); + tx->inputSize = sizeof(tx->queryId) + replyState.ownReplySize; + tx->queryId = replyState.queryId; + copyMem(txReplyData, replyState.ownReplyData, replyState.ownReplySize); + + // remember that we have scheduled reveal of this reply + replyState.expectedRevealTxTick = txScheduleTick; + + // return non-zero in order instruct caller to call this function again with the returned startIdx + return idx + 1; + } + + // currently no reply reveal needed -> signal to skip tx + return 0; + } + +protected: + // Check oracle reply reveal transaction. Returns reply state if okay or NULL otherwise. Also sets output param queryIndexOutput. + // Caller is responsible for locking. + ReplyState* checkReplyRevealTransaction(const OracleReplyRevealTransactionPrefix* transaction, uint32_t* queryIndexOutput = nullptr) const + { + // check precondition for calling with ASSERTs + ASSERT(transaction != nullptr); + ASSERT(transaction->checkValidity()); + ASSERT(transaction->inputType == OracleReplyRevealTransactionPrefix::transactionType()); + ASSERT(isZero(transaction->destinationPublicKey)); + + // check size of tx + if (transaction->inputSize < OracleReplyRevealTransactionPrefix::minInputSize()) + return nullptr; + + // check that tx source is computor + if (computorIndex(transaction->sourcePublicKey) < 0) + return nullptr; + + // get and check query index + uint32_t queryIndex; + if (!queryIdToIndex->get(transaction->queryId, queryIndex) || queryIndex >= oracleQueryCount) + return nullptr; + + // get query metadata and check state + OracleQueryMetadata& oqm = queries[queryIndex]; + if (oqm.status != ORACLE_QUERY_STATUS_COMMITTED) + return nullptr; + + // check reply size vs size expected by interface + ASSERT(oqm.interfaceIndex < OI::oracleInterfacesCount); + const uint16_t replySize = transaction->inputSize - sizeof(transaction->queryId); + if (replySize != OI::oracleInterfaces[oqm.interfaceIndex].replySize) + { + oqm.statusFlags |= ORACLE_FLAG_BAD_SIZE_REVEAL; + return nullptr; + } + + // get reply state + const auto replyStateIdx = oqm.statusVar.pending.replyStateIndex; + ASSERT(replyStateIdx < MAX_SIMULTANEOUS_ORACLE_QUERIES); + ReplyState& replyState = replyStates[replyStateIdx]; + ASSERT(replyState.queryId == transaction->queryId); + + // compute digest of reply in reveal tx + const void* replyData = transaction + 1; + m256i revealDigest; + KangarooTwelve(replyData, replySize, revealDigest.m256i_u8, 32); + + // check that revealed reply matches the quorum digest + const m256i quorumCommitDigest = replyState.replyCommitDigests[replyState.replyCommitHistogramIdx[replyState.mostCommitsHistIdx]]; + ASSERT(!isZero(quorumCommitDigest)); + if (revealDigest != quorumCommitDigest) + return nullptr; + + // set output param + if (queryIndexOutput) + *queryIndexOutput = queryIndex; + + return &replyState; + } + +public: + // Called by request processor when a tx is received in order to minimize sending of reveal tx. + void announceExpectedRevealTransaction(const OracleReplyRevealTransactionPrefix* transaction) + { + // lock for accessing engine data + LockGuard lockGuard(lock); + + // check tx and get reply state + ReplyState* replyState = checkReplyRevealTransaction(transaction); + if (!replyState) + return; + + // update tick when reveal is expected + if (!replyState->expectedRevealTxTick || replyState->expectedRevealTxTick > transaction->tick) + replyState->expectedRevealTxTick = transaction->tick; + } + + // Called from tick processor. + bool processOracleReplyRevealTransaction(const OracleReplyRevealTransactionPrefix* transaction, uint32_t txSlotInTickData) + { + ASSERT(txSlotInTickData < NUMBER_OF_TRANSACTIONS_PER_TICK); + ASSERT(transaction->tick == system.tick); + + // lock for accessing engine data + LockGuard lockGuard(lock); + + // check tx and get reply state + query metadata + uint32_t queryIndex; + ReplyState* replyState = checkReplyRevealTransaction(transaction, &queryIndex); + if (!replyState) + return false; + OracleQueryMetadata& oqm = queries[queryIndex]; + const auto replyStateIdx = oqm.statusVar.pending.replyStateIndex; + + // TODO: check knowledge proofs of all computors and add revenue points for computors who sent correct commit tx fastest + + // update state + oqm.statusVar.success.revealTick = transaction->tick; + oqm.statusVar.success.revealTxIndex = txSlotInTickData; + oqm.status = ORACLE_QUERY_STATUS_SUCCESS; + pendingQueryIndices.removeByValue(queryIndex); + ++stats.successCount; + + // run contract notification(s) if needed + const void* replyData = transaction + 1; + notifyContractsIfAny(oqm, *replyState, replyData); + + // cleanup data of pending reply + pendingRevealReplyStateIndices.removeByValue(replyStateIdx); + freeReplyStateSlot(replyStateIdx); + + return true; + } + + void beginEpoch() { // lock for accessing engine data diff --git a/src/oracle_core/oracle_transactions.h b/src/oracle_core/oracle_transactions.h index aff48313a..5baf3f3ab 100644 --- a/src/oracle_core/oracle_transactions.h +++ b/src/oracle_core/oracle_transactions.h @@ -42,6 +42,8 @@ struct OracleReplyRevealTransactionPrefix : public Transaction } unsigned long long queryId; + + // followed by: oracle reply }; // Transaction for querying oracle. The tx prefix is followed by the OracleQuery data diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index 1d69d294b..e540e7453 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -9,6 +9,7 @@ struct OracleEngineTest : public LoggingTest { EXPECT_TRUE(initCommonBuffers()); EXPECT_TRUE(initSpecialEntities()); + EXPECT_TRUE(initContractExec()); // init computors for (int computorIndex = 0; computorIndex < NUMBER_OF_COMPUTORS; computorIndex++) @@ -29,6 +30,7 @@ struct OracleEngineTest : public LoggingTest ~OracleEngineTest() { deinitCommonBuffers(); + deinitContractExec(); } }; @@ -152,6 +154,9 @@ TEST(OracleEngine, ContractQuerySuccess) EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + // no reveal yet + EXPECT_EQ(oracleEngine1.getReplyRevealTransaction(txBuffer, 0, system.tick + 3, 0), 0); + //------------------------------------------------------------------------- // create and process enough reply commit tx to trigger reval tx @@ -216,6 +221,20 @@ TEST(OracleEngine, ContractQuerySuccess) oracleEngine3.checkPendingState(queryId, 451, 76, ORACLE_QUERY_STATUS_COMMITTED); } } + + //------------------------------------------------------------------------- + // reply reveal tx + + // success for one tx + EXPECT_EQ(oracleEngine1.getReplyRevealTransaction(txBuffer, 0, system.tick + 3, 0), 1); + EXPECT_EQ(oracleEngine1.getReplyRevealTransaction(txBuffer, 0, system.tick + 3, 1), 0); + + // second call does not provide the same tx again + EXPECT_EQ(oracleEngine1.getReplyRevealTransaction(txBuffer, 0, system.tick + 3, 0), 0); + + system.tick += 3; + auto* replyRevealTx = (OracleReplyRevealTransactionPrefix*)txBuffer; + oracleEngine1.processOracleReplyRevealTransaction(replyRevealTx, 0); } TEST(OracleEngine, ContractQueryUnresolvable) diff --git a/test/oracle_testing.h b/test/oracle_testing.h index 30ff3a005..eed680217 100644 --- a/test/oracle_testing.h +++ b/test/oracle_testing.h @@ -7,6 +7,7 @@ #include "oracle_core/oracle_engine.h" #include "contract_core/qpi_ticking_impl.h" +#include "contract_core/qpi_spectrum_impl.h" union EnqueuedNetworkMessage From 38f2be1f443eb1b7efcf26a569ab96ec92a335d4 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:42:44 +0100 Subject: [PATCH 06/90] Change contract notifications to use registry Using plain function pointers for referencing user procedures for notifications may cause failures when snapshots are saved and loaded, because function pointer may change when the node is restarted with a snapshot in the middle of an epoch. As a solution, notification contract procedures must be registered at node startup. The function pointer and related data is stored in a registry and referenced by a procedure ID that is defined by the contract index and the line of the procedure in the source code (which stay constant between restarts and thus can be stored in the oracle engine state snapshot file). --- src/contract_core/contract_def.h | 45 +++++++++ src/contract_core/contract_exec.h | 21 +++- src/contract_core/pre_qpi_def.h | 3 + src/contract_core/qpi_oracle_impl.h | 24 +++-- src/contracts/TestExampleC.h | 8 +- src/contracts/qpi.h | 149 ++++++++++++++++------------ src/oracle_core/oracle_engine.h | 20 ++-- test/oracle_engine.cpp | 20 ++-- 8 files changed, 192 insertions(+), 98 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 1f6643ecb..434512f76 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -445,3 +445,48 @@ static void initializeContracts() #endif } +// Class for registering and looking up user procedures independently of input type, for example for notifications +class UserProcedureRegistry +{ +public: + struct UserProcedureData + { + USER_PROCEDURE procedure; + unsigned int contractIndex; + unsigned int localsSize; + unsigned short inputSize; + unsigned short outputSize; + }; + + void init() + { + setMemory(*this, 0); + } + + bool add(unsigned int procedureId, const UserProcedureData& data) + { + const unsigned int cnt = (unsigned int)idToIndex.population(); + if (cnt >= idToIndex.capacity()) + return false; + + copyMemory(userProcData[cnt], data); + idToIndex.set(procedureId, cnt); + + return true; + } + + const UserProcedureData* get(unsigned int procedureId) const + { + unsigned int idx; + if (!idToIndex.get(procedureId, idx)) + return nullptr; + return userProcData + idx; + } + +protected: + UserProcedureData userProcData[MAX_CONTRACT_PROCEDURES_REGISTERED]; + QPI::HashMap idToIndex; +}; + +// For registering and looking up user procedures independently of input type (for notifications), initialized by initContractExec() +GLOBAL_VAR_DECL UserProcedureRegistry* userProcedureRegistry GLOBAL_VAR_INIT(nullptr); diff --git a/src/contract_core/contract_exec.h b/src/contract_core/contract_exec.h index 2f8bcf673..9358c355d 100644 --- a/src/contract_core/contract_exec.h +++ b/src/contract_core/contract_exec.h @@ -192,6 +192,12 @@ static bool initContractExec() if (!contractActionTracker.allocBuffer()) return false; + if (!allocPoolWithErrorLog(L"userProcedureRegistry", sizeof(*userProcedureRegistry), (void**)&userProcedureRegistry, __LINE__)) + { + return false; + } + userProcedureRegistry->init(); + return true; } @@ -218,6 +224,9 @@ static void deinitContractExec() freePool(contractStateChangeFlags); } + if (userProcedureRegistry) + freePool(userProcedureRegistry); + contractActionTracker.freeBuffer(); } @@ -917,6 +926,16 @@ void QPI::QpiContextForInit::__registerUserProcedure(USER_PROCEDURE userProcedur contractUserProcedureLocalsSizes[_currentContractIndex][inputType] = localsSize; } +void QPI::QpiContextForInit::__registerUserProcedureNotification(USER_PROCEDURE userProcedure, unsigned int procedureId, unsigned short inputSize, unsigned short outputSize, unsigned int localsSize) const +{ + ASSERT(userProcedureRegistry); + if (!userProcedureRegistry->add(procedureId, { userProcedure, _currentContractIndex, localsSize, inputSize, outputSize })) + { +#if !defined(NDEBUG) + addDebugMessage(L"__registerUserProcedureNotification() failed. You should increase MAX_CONTRACT_PROCEDURES_REGISTERED."); +#endif + } +} // QPI context used to call contract system procedure from qubic core (contract processor) @@ -1279,7 +1298,7 @@ struct UserProcedureNotification // The procedure pointer, the expected inputSize, and the expected localsSize, which are passed via // UserProcedureNotification, must be consistent. The code using notifications is responible for ensuring that. // Use cases: -// - oracle notifications (managed by oracleEngine) +// - oracle notifications (managed by oracleEngine and userProcedureRegistry) struct QpiContextUserProcedureNotificationCall : public QPI::QpiContextProcedureCall { QpiContextUserProcedureNotificationCall(const UserProcedureNotification& notification) : QPI::QpiContextProcedureCall(notification.contractIndex, NULL_ID, 0, USER_PROCEDURE_NOTIFICATION_CALL), notif(notification) diff --git a/src/contract_core/pre_qpi_def.h b/src/contract_core/pre_qpi_def.h index 7a5aabd49..e1370a512 100644 --- a/src/contract_core/pre_qpi_def.h +++ b/src/contract_core/pre_qpi_def.h @@ -27,6 +27,9 @@ constexpr unsigned short MAX_NESTED_CONTRACT_CALLS = 10; // Size of the contract action tracker, limits the number of transfers that one contract call can execute. constexpr unsigned long long CONTRACT_ACTION_TRACKER_SIZE = 16 * 1024 * 1024; +// Maximum number of contract procedures that may be registered, e.g. for user procedure notifications +constexpr unsigned int MAX_CONTRACT_PROCEDURES_REGISTERED = 16 * 1024; + static void __beginFunctionOrProcedure(const unsigned int); // TODO: more human-readable form of function ID? static void __endFunctionOrProcedure(const unsigned int); diff --git a/src/contract_core/qpi_oracle_impl.h b/src/contract_core/qpi_oracle_impl.h index 0ad286816..ac6b9fcaf 100644 --- a/src/contract_core/qpi_oracle_impl.h +++ b/src/contract_core/qpi_oracle_impl.h @@ -6,9 +6,10 @@ template -QPI::sint64 QPI::QpiContextProcedureCall::queryOracle( +QPI::sint64 QPI::QpiContextProcedureCall::__qpiQueryOracle( const OracleInterface::OracleQuery& query, - void (*notificationCallback)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, QPI::OracleNotificationInput& input, QPI::NoData& output, LocalsType& locals), + void (*notificationProcPtr)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), + unsigned int notificationProcId, uint32 timeoutMillisec ) const { @@ -28,9 +29,16 @@ QPI::sint64 QPI::QpiContextProcedureCall::queryOracle( const QPI::uint16 contractIndex = static_cast(this->_currentContractIndex); // check callback - if (!notificationCallback || ContractStateType::__contract_index != contractIndex) + if (!notificationProcPtr || ContractStateType::__contract_index != contractIndex) return -1; + // check vs registry of user procedures for notification + const UserProcedureRegistry::UserProcedureData* procData; + if (!userProcedureRegistry || !(procData = userProcedureRegistry->get(notificationProcId)) || procData->procedure != (USER_PROCEDURE)notificationProcPtr) + return -1; + ASSERT(procData->inputSize == sizeof(OracleNotificationInput)); + ASSERT(procData->localsSize == sizeof(LocalsType)); + // get and destroy fee (not adding to contracts execution fee reserve) sint64 fee = OracleInterface::getQueryFee(query); int contractSpectrumIdx = ::spectrumIndex(this->_currentContractId); @@ -39,8 +47,7 @@ QPI::sint64 QPI::QpiContextProcedureCall::queryOracle( // try to start query QPI::sint64 queryId = oracleEngine.startContractQuery( contractIndex, OracleInterface::oracleInterfaceIndex, - &query, sizeof(query), timeoutMillisec, - (USER_PROCEDURE)notificationCallback, sizeof(LocalsType)); + &query, sizeof(query), timeoutMillisec, notificationProcId); if (queryId >= 0) { // success @@ -55,17 +62,18 @@ QPI::sint64 QPI::QpiContextProcedureCall::queryOracle( input->queryId = -1; QPI::NoData output; auto* locals = (LocalsType*)__qpiAllocLocals(sizeof(LocalsType)); - notificationCallback(*this, *state, *input, output, *locals); + notificationProcPtr(*this, *state, *input, output, *locals); __qpiFreeLocals(); __qpiFreeLocals(); return -1; } template -inline QPI::sint32 QPI::QpiContextProcedureCall::subscribeOracle( +inline QPI::sint32 QPI::QpiContextProcedureCall::__qpiSubscribeOracle( const OracleInterface::OracleQuery& query, - void (*notificationCallback)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), + void (*notificationProcPtr)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), QPI::uint32 notificationIntervalInMilliseconds, + unsigned int notificationProcId, bool notifyWithPreviousReply ) const { diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index a75f1e25d..b8ad49346 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -211,7 +211,7 @@ struct TESTEXC : public ContractBase PUBLIC_PROCEDURE(QueryPriceOracle) { - output.oracleQueryId = qpi.queryOracle(input.priceOracleQuery, NotifyPriceOracleReply, input.timeoutMilliseconds); + output.oracleQueryId = QUERY_ORACLE(OI::Price, input.priceOracleQuery, NotifyPriceOracleReply, input.timeoutMilliseconds); if (output.oracleQueryId < 0) { // error @@ -234,7 +234,7 @@ struct TESTEXC : public ContractBase PUBLIC_PROCEDURE(SubscribePriceOracle) { - output.oracleSubscriptionId = qpi.subscribeOracle(input.priceOracleQuery, NotifyPriceOracleReply, input.subscriptionIntervalMinutes); + output.oracleSubscriptionId = SUBSCRIBE_ORACLE(OI::Price, input.priceOracleQuery, NotifyPriceOracleReply, input.subscriptionIntervalMinutes, true); if (output.oracleSubscriptionId < 0) { // error @@ -282,7 +282,7 @@ struct TESTEXC : public ContractBase // Query oracle if (qpi.tick() % 2 == 0) { - locals.oracleQueryId = qpi.queryOracle(locals.priceOracleQuery, NotifyPriceOracleReply, 20000); + locals.oracleQueryId = QUERY_ORACLE(OI::Price, locals.priceOracleQuery, NotifyPriceOracleReply, 20000); } } @@ -300,5 +300,7 @@ struct TESTEXC : public ContractBase REGISTER_USER_PROCEDURE(QpiBidInIpo, 30); REGISTER_USER_PROCEDURE(QueryPriceOracle, 100); + + REGISTER_USER_PROCEDURE_NOTIFICATION(NotifyPriceOracleReply); } }; diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index 294273041..0e5aa119d 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -2519,32 +2519,6 @@ namespace QPI uint32 quantity ) const; - /** - * @brief Initiate oracle query that will lead to notification later. - * @param query Details about which oracle to query for which information, as defined by a specific oracle interface. - * @param notificationCallback User procedure that shall be executed when the oracle reply is available or an error occurs. - * @param timeoutMillisec Maximum number of milliseconds to wait for reply. - * @return Oracle query ID that can be used to get the status of the query, or 0 on error. - * - * This will automatically burn the oracle query fee as defined by the oracle interface (burning without - * adding to the contract's execution fee reserve). It will fail if the contract doesn't have enough QU. - * - * The notification callback will be executed when the reply is available or on error. - * The callback must be a user procedure of the contract calling qpi.queryOracle() with the procedure input type - * OracleNotificationInput and NoData as output. - * Success is indicated by input.status == ORACLE_QUERY_STATUS_SUCCESS. - * If an error happened before the query has been created and sent, input.status is ORACLE_QUERY_STATUS_UNKNOWN - * and input.queryID is -1 (invalid). - * Other errors that may happen with valid input.queryID are input.status == ORACLE_QUERY_STATUS_TIMEOUT and - * input.status == ORACLE_QUERY_STATUS_UNRESOLVABLE. - */ - template - inline sint64 queryOracle( - const OracleInterface::OracleQuery& query, - void (*notificationCallback)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), - uint32 timeoutMillisec = 60000 - ) const; - inline sint64 releaseShares( const Asset& asset, const id& owner, @@ -2569,43 +2543,6 @@ namespace QPI sint64 invocationReward ) const; - /** - * @brief Subscribe for regularly querying an oracle. - * @param query The regular query, which must have a member `DateAndTime timestamp`. - * @param notificationCallback User procedure that shall be executed when the oracle reply is available or an error occurs. - * @param notificationIntervalInMilliseconds Number of milliseconds between consecutive queries/replies. - * This is also used as a timeout. Currently, only multiples of 60000 are supported and other - * values are rejected with an error. - * @param notifyWithPreviousReply Whether to immediately notify this contract with the most up-to-date value if any is available. - * @return Oracle subscription ID that can be used to get the status of the subscription, or -1 on error. - * - * Subscriptions automatically expire at the end of each epoch. So, a common pattern is to call qpi.subscribeOracle() - * in BEGIN_EPOCH. - * - * Subscriptions facilitate shareing common oracle queries among multiple contracts. This saves network ressources and allows - * to provide a fixed-price subscription for the whole epoch, which is usually much cheaper than the equivalent series of - * individual qpi.queryOracle() calls. - * - * The qpi.subscribeOracle() call will automatically burn the oracle subscription fee as defined by the oracle interface - * (burning without adding to the contract's execution fee reserve). It will fail if the contract doesn't have enough QU. - * - * The notification callback will be executed when the reply is available or on error. - * The callback must be a user procedure of the contract calling qpi.subscribeOracle() with the procedure input type - * OracleNotificationInput and NoData as output. - * Success is indicated by input.status == ORACLE_QUERY_STATUS_SUCCESS. - * If an error happened before the query has been created and sent, input.status is ORACLE_QUERY_STATUS_UNKNOWN - * and input.queryID is -1 (invalid). - * Other errors that may happen with valid input.queryID are input.status == ORACLE_QUERY_STATUS_TIMEOUT and - * input.status == ORACLE_QUERY_STATUS_UNRESOLVABLE. - */ - template - inline sint32 subscribeOracle( - const OracleInterface::OracleQuery& query, - void (*notificationCallback)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), - uint32 notificationIntervalInMilliseconds = 60000, - bool notifyWithPreviousReply = true - ) const; - /** * @brief Add/change/cancel shareholder vote(s) in another contract. * @param contractIndex Index of the other contract, that SELF is shareholder of and that the proposal is about. @@ -2654,6 +2591,25 @@ namespace QPI bool __qpiCallSystemProc(unsigned int otherContractIndex, InputType& input, OutputType& output, sint64 invocationReward) const; inline void __qpiNotifyPostIncomingTransfer(const id& source, const id& dest, sint64 amount, uint8 type) const; + // Internal version of QUERY_ORACLE (macro ensures that proc pointer and id match) + template + inline sint64 __qpiQueryOracle( + const OracleInterface::OracleQuery& query, + void (*notificationProcPtr)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), + unsigned int notificationProcId, + uint32 timeoutMillisec + ) const; + + // Internal version of SUBSCRIBE_ORACLE (macro ensures that proc pointer and id match) + template + inline sint32 __qpiSubscribeOracle( + const OracleInterface::OracleQuery& query, + void (*notificationProcPtr)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), + unsigned int notificationProcId, + uint32 notificationIntervalInMilliseconds = 60000, + bool notifyWithPreviousReply = true + ) const; + // Internal version of transfer() that takes the TransferType as additional argument. inline sint64 __transfer( // Attempts to transfer energy from this qubic const id& destination, // Destination to transfer to, use NULL_ID to destroy the transferred energy @@ -2671,6 +2627,7 @@ namespace QPI { inline void __registerUserFunction(USER_FUNCTION, unsigned short, unsigned short, unsigned short, unsigned int) const; inline void __registerUserProcedure(USER_PROCEDURE, unsigned short, unsigned short, unsigned short, unsigned int) const; + inline void __registerUserProcedureNotification(USER_PROCEDURE, unsigned int, unsigned short, unsigned short, unsigned int) const; // Construction is done in core, not allowed in contracts inline QpiContextForInit(unsigned int contractIndex); @@ -2941,7 +2898,7 @@ namespace QPI #define PRIVATE_PROCEDURE_WITH_LOCALS(procedure) \ private: \ - enum { __is_function_##procedure = false }; \ + enum { __is_function_##procedure = false, __id_##procedure = (CONTRACT_INDEX << 22) | __LINE__ }; \ inline static void procedure(const QPI::QpiContextProcedureCall& qpi, CONTRACT_STATE_TYPE& state, procedure##_input& input, procedure##_output& output, procedure##_locals& locals) { ::__FunctionOrProcedureBeginEndGuard<(CONTRACT_INDEX << 22) | __LINE__> __prologueEpilogueCaller; __impl_##procedure(qpi, state, input, output, locals); } \ static void __impl_##procedure(const QPI::QpiContextProcedureCall& qpi, CONTRACT_STATE_TYPE& state, procedure##_input& input, procedure##_output& output, procedure##_locals& locals) @@ -2963,7 +2920,7 @@ namespace QPI #define PUBLIC_PROCEDURE_WITH_LOCALS(procedure) \ public: \ - enum { __is_function_##procedure = false }; \ + enum { __is_function_##procedure = false, __id_##procedure = (CONTRACT_INDEX << 22) | __LINE__ }; \ inline static void procedure(const QPI::QpiContextProcedureCall& qpi, CONTRACT_STATE_TYPE& state, procedure##_input& input, procedure##_output& output, procedure##_locals& locals) { ::__FunctionOrProcedureBeginEndGuard<(CONTRACT_INDEX << 22) | __LINE__> __prologueEpilogueCaller; __impl_##procedure(qpi, state, input, output, locals); } \ static void __impl_##procedure(const QPI::QpiContextProcedureCall& qpi, CONTRACT_STATE_TYPE& state, procedure##_input& input, procedure##_output& output, procedure##_locals& locals) @@ -2989,6 +2946,14 @@ namespace QPI static_assert(sizeof(userProcedure##_locals) <= MAX_SIZE_OF_CONTRACT_LOCALS, #userProcedure "_locals size too large"); \ qpi.__registerUserProcedure((USER_PROCEDURE)userProcedure, inputType, sizeof(userProcedure##_input), sizeof(userProcedure##_output), sizeof(userProcedure##_locals)); + // Register procedure for notifications (such as oracle reply notification) + #define REGISTER_USER_PROCEDURE_NOTIFICATION(userProcedure) \ + static_assert(!__is_function_##userProcedure, #userProcedure " is function"); \ + static_assert(sizeof(userProcedure##_output) <= 65535, #userProcedure "_output size too large"); \ + static_assert(sizeof(userProcedure##_input) <= 65535, #userProcedure "_input size too large"); \ + static_assert(sizeof(userProcedure##_locals) <= MAX_SIZE_OF_CONTRACT_LOCALS, #userProcedure "_locals size too large"); \ + qpi.__registerUserProcedureNotification((USER_PROCEDURE)userProcedure, __id_##userProcedure, sizeof(userProcedure##_input), sizeof(userProcedure##_output), sizeof(userProcedure##_locals)); + // Call function or procedure of current contract (without changing invocation reward) // WARNING: input may be changed by called function #define CALL(functionOrProcedure, input, output) \ @@ -3052,7 +3017,59 @@ namespace QPI #define INVOKE_OTHER_CONTRACT_PROCEDURE(contractStateType, procedure, input, output, invocationReward) \ INVOKE_OTHER_CONTRACT_PROCEDURE_E(contractStateType, procedure, input, output, invocationReward, interContractCallError) - #define QUERY_ORACLE(oracle, query) // TODO + /** + * @brief Initiate oracle query that will lead to notification later. + * @param query Details about which oracle to query for which information, as defined by a specific oracle interface. + * @param userProcNotification User procedure that shall be executed when the oracle reply is available or an error occurs. + * @param timeoutMillisec Maximum number of milliseconds to wait for reply. + * @return Oracle query ID that can be used to get the status of the query, or 0 on error. + * + * This will automatically burn the oracle query fee as defined by the oracle interface (burning without + * adding to the contract's execution fee reserve). It will fail if the contract doesn't have enough QU. + * + * The notification callback will be executed when the reply is available or on error. + * The callback must be a user procedure of the contract calling qpi.queryOracle() with the procedure input type + * OracleNotificationInput and NoData as output. The procedure must be registered with + * REGISTER_USER_PROCEDURE_NOTIFICATION() in REGISTER_USER_FUNCTIONS_AND_PROCEDURES(). + * Success is indicated by input.status == ORACLE_QUERY_STATUS_SUCCESS. + * If an error happened before the query has been created and sent, input.status is ORACLE_QUERY_STATUS_UNKNOWN + * and input.queryID is -1 (invalid). + * Other errors that may happen with valid input.queryID are input.status == ORACLE_QUERY_STATUS_TIMEOUT and + * input.status == ORACLE_QUERY_STATUS_UNRESOLVABLE. + */ + #define QUERY_ORACLE(OracleInterface, query, userProcNotification, timeoutMillisec) qpi.__qpiQueryOracle(query, userProcNotification, __id_##userProcNotification, timeoutMillisec) + + /** + * @brief Subscribe for regularly querying an oracle. + * @param query The regular query, which must have a member `DateAndTime timestamp`. + * @param notificationCallback User procedure that shall be executed when the oracle reply is available or an error occurs. + * @param notificationIntervalInMilliseconds Number of milliseconds between consecutive queries/replies. + * This is also used as a timeout. Currently, only multiples of 60000 are supported and other + * values are rejected with an error. + * @param notifyWithPreviousReply Whether to immediately notify this contract with the most up-to-date value if any is available. + * @return Oracle subscription ID that can be used to get the status of the subscription, or -1 on error. + * + * Subscriptions automatically expire at the end of each epoch. So, a common pattern is to call qpi.subscribeOracle() + * in BEGIN_EPOCH. + * + * Subscriptions facilitate shareing common oracle queries among multiple contracts. This saves network ressources and allows + * to provide a fixed-price subscription for the whole epoch, which is usually much cheaper than the equivalent series of + * individual qpi.queryOracle() calls. + * + * The qpi.subscribeOracle() call will automatically burn the oracle subscription fee as defined by the oracle interface + * (burning without adding to the contract's execution fee reserve). It will fail if the contract doesn't have enough QU. + * + * The notification callback will be executed when the reply is available or on error. + * The callback must be a user procedure of the contract calling qpi.subscribeOracle() with the procedure input type + * OracleNotificationInput and NoData as output. The procedure must be registered with + * REGISTER_USER_PROCEDURE_NOTIFICATION() in REGISTER_USER_FUNCTIONS_AND_PROCEDURES(). + * Success is indicated by input.status == ORACLE_QUERY_STATUS_SUCCESS. + * If an error happened before the query has been created and sent, input.status is ORACLE_QUERY_STATUS_UNKNOWN + * and input.queryID is -1 (invalid). + * Other errors that may happen with valid input.queryID are input.status == ORACLE_QUERY_STATUS_TIMEOUT and + * input.status == ORACLE_QUERY_STATUS_UNRESOLVABLE. + */ + #define SUBSCRIBE_ORACLE(OracleInterface, query, userProcNotification, notificationIntervalInMilliseconds, notifyWithPreviousReply) qpi.__qpiSubscribeOracle(query, userProcNotification, __id_##userProcNotification, notificationIntervalInMilliseconds, notifyWithPreviousReply) #define SELF id(CONTRACT_INDEX, 0, 0, 0) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index d2fa30d7f..94ec6c95f 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -43,6 +43,7 @@ struct OracleQueryMetadata struct { uint64_t queryStorageOffset; + uint32_t notificationProcId; uint16_t queryingContract; } contract; @@ -135,9 +136,6 @@ struct OracleReplyState uint32_t expectedRevealTxTick; uint32_t revealTick; uint32_t revealTxIndex; - - USER_PROCEDURE notificationProcedure; - uint32_t notificationLocalsSize; }; // @@ -350,7 +348,7 @@ class OracleEngine int64_t startContractQuery(uint16_t contractIndex, uint32_t interfaceIndex, const void* queryData, uint16_t querySize, uint32_t timeoutMillisec, - USER_PROCEDURE notificationProcedure, uint32_t notificationLocalsSize) + unsigned int notificationProcId) { // check inputs if (contractIndex >= MAX_NUMBER_OF_CONTRACTS || interfaceIndex >= OI::oracleInterfacesCount || querySize != OI::oracleInterfaces[interfaceIndex].querySize) @@ -410,14 +408,13 @@ class OracleEngine queryMetadata.timeout = timeout; queryMetadata.typeVar.contract.queryingContract = contractIndex; queryMetadata.typeVar.contract.queryStorageOffset = queryStorageBytesUsed; + queryMetadata.typeVar.contract.notificationProcId = notificationProcId; queryMetadata.statusVar.pending.replyStateIndex = replyStateSlotIdx; // init reply state (temporary until reply is revealed) ReplyState& replyState = replyStates[replyStateSlotIdx]; setMem(&replyState, sizeof(replyState), 0); replyState.queryId = queryId; - replyState.notificationProcedure = notificationProcedure; - replyState.notificationLocalsSize = notificationLocalsSize; // copy oracle query data to permanent storage copyMem(queryStorage + queryStorageBytesUsed, queryData, querySize); @@ -446,10 +443,12 @@ class OracleEngine enqueueResponse((Peer*)0x1, sizeof(*omq) + querySize, OracleMachineQuery::type(), 0, omq); } - void notifyContractsIfAny(const OracleQueryMetadata& oqm, const ReplyState& replyState, const void* replyData = nullptr) + void notifyContractsIfAny(const OracleQueryMetadata& oqm, const void* replyData = nullptr) { + /* + // TODO: change to run in contract prcocessor -> move parts to qubic.cpp ASSERT(oqm.queryId == replyState.queryId); - if (!replyState.notificationProcedure || oqm.type == ORACLE_QUERY_TYPE_USER_QUERY) + if (oqm.type == ORACLE_QUERY_TYPE_USER_QUERY) return; const auto replySize = OI::oracleInterfaces[oqm.interfaceIndex].replySize; @@ -479,6 +478,7 @@ class OracleEngine } // TODO: handle subscriptions + */ } public: @@ -746,7 +746,7 @@ class OracleEngine ++stats.unresolvableCount; // run contract notification(s) if needed - notifyContractsIfAny(oqm, replyState); + notifyContractsIfAny(oqm); // cleanup data of pending reply immediately (no info for revenue required) pendingCommitReplyStateIndices.removeByValue(replyStateIdx); @@ -944,7 +944,7 @@ class OracleEngine // run contract notification(s) if needed const void* replyData = transaction + 1; - notifyContractsIfAny(oqm, *replyState, replyData); + notifyContractsIfAny(oqm, replyData); // cleanup data of pending reply pendingRevealReplyStateIndices.removeByValue(replyStateIdx); diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index e540e7453..f5c3111bf 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -92,16 +92,16 @@ TEST(OracleEngine, ContractQuerySuccess) QPI::uint32 interfaceIndex = 0; QPI::uint16 contractIndex = 1; QPI::uint32 timeout = 30000; - USER_PROCEDURE notificationProc = dummyNotificationProc; - QPI::uint32 notificationLocalsSize = 128; + const QPI::uint32 notificationProcId = 12345; + EXPECT_TRUE(userProcedureRegistry->add(notificationProcId, { dummyNotificationProc, 1, 128, 128, 1 })); //------------------------------------------------------------------------- // start contract query / check message to OM node - QPI::sint64 queryId = oracleEngine1.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize); + QPI::sint64 queryId = oracleEngine1.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProcId); EXPECT_EQ(queryId, getContractOracleQueryId(system.tick, 0)); checkNetworkMessageOracleMachineQuery(queryId, priceQuery.oracle, timeout); - EXPECT_EQ(queryId, oracleEngine2.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize)); - EXPECT_EQ(queryId, oracleEngine3.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize)); + EXPECT_EQ(queryId, oracleEngine2.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProcId)); + EXPECT_EQ(queryId, oracleEngine3.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProcId)); //------------------------------------------------------------------------- // get query contract data @@ -256,15 +256,15 @@ TEST(OracleEngine, ContractQueryUnresolvable) QPI::uint32 interfaceIndex = 0; QPI::uint16 contractIndex = 2; QPI::uint32 timeout = 120000; - USER_PROCEDURE notificationProc = dummyNotificationProc; - QPI::uint32 notificationLocalsSize = 1024; + const QPI::uint32 notificationProcId = 12345; + EXPECT_TRUE(userProcedureRegistry->add(notificationProcId, { dummyNotificationProc, 1, 1024, 128, 1 })); //------------------------------------------------------------------------- // start contract query / check message to OM node - QPI::sint64 queryId = oracleEngine1.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize); + QPI::sint64 queryId = oracleEngine1.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProcId); EXPECT_EQ(queryId, getContractOracleQueryId(system.tick, 0)); - EXPECT_EQ(queryId, oracleEngine2.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize)); - EXPECT_EQ(queryId, oracleEngine3.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProc, notificationLocalsSize)); + EXPECT_EQ(queryId, oracleEngine2.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProcId)); + EXPECT_EQ(queryId, oracleEngine3.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProcId)); checkNetworkMessageOracleMachineQuery(queryId, priceQuery.oracle, timeout); //------------------------------------------------------------------------- From defb91b7ba58feadbe783cd7782a1ce0343a2165 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:04:33 +0100 Subject: [PATCH 07/90] Implement notifications in oracleEngine --- src/oracle_core/oracle_engine.h | 134 ++++++++++++++++++++------------ test/oracle_engine.cpp | 44 ++++++++++- test/oracle_testing.h | 19 +++++ 3 files changed, 146 insertions(+), 51 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 94ec6c95f..d5865eacb 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -8,7 +8,6 @@ #include "common_buffers.h" #include "spectrum/special_entities.h" #include "ticking/tick_storage.h" -#include "contract_core/contract_exec.h" #include "oracle_transactions.h" #include "core_om_network_messages.h" @@ -144,6 +143,14 @@ struct OracleRevenueCounter uint32_t computorRevPoints[NUMBER_OF_COMPUTORS]; }; +struct OracleNotificationData +{ + uint32_t procedureId; + uint16_t contractIndex; + uint16_t inputSize; + uint8_t inputBuffer[16 + MAX_ORACLE_REPLY_SIZE]; +}; + // Array with fast insert and remove, for which order of entries does not matter. Entry duplicates are possible. template struct UnsortedMultiset @@ -230,6 +237,9 @@ class OracleEngine /// fast lookup of reply state indices for which reveal tx is pending UnsortedMultiset pendingRevealReplyStateIndices; + // fast lookup of query indices for which the contract should be notified + UnsortedMultiset notificationQueryIndicies; + struct { /// total number of successful oracle queries unsigned long long successCount; @@ -247,8 +257,8 @@ class OracleEngine /// array of ownComputorSeedsCount public keys (mainly for testing, in EFI core this points to computorPublicKeys from special_entities.h) const m256i* ownComputorPublicKeys; - /// buffer used to store input for contract notifications - uint8_t contractNotificationInputBuffer[16 + MAX_ORACLE_REPLY_SIZE]; + /// buffer used to store output of getNotification() + OracleNotificationData notificationOutputBuffer; /// lock for preventing race conditions in concurrent execution mutable volatile char lock; @@ -298,6 +308,7 @@ class OracleEngine pendingQueryIndices.numValues = 0; pendingCommitReplyStateIndices.numValues = 0; pendingRevealReplyStateIndices.numValues = 0; + notificationQueryIndicies.numValues = 0; setMem(&stats, sizeof(stats), 0); return true; @@ -443,44 +454,6 @@ class OracleEngine enqueueResponse((Peer*)0x1, sizeof(*omq) + querySize, OracleMachineQuery::type(), 0, omq); } - void notifyContractsIfAny(const OracleQueryMetadata& oqm, const void* replyData = nullptr) - { - /* - // TODO: change to run in contract prcocessor -> move parts to qubic.cpp - ASSERT(oqm.queryId == replyState.queryId); - if (oqm.type == ORACLE_QUERY_TYPE_USER_QUERY) - return; - - const auto replySize = OI::oracleInterfaces[oqm.interfaceIndex].replySize; - ASSERT(16 + replySize < 0xffff); - - UserProcedureNotification notification; - if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) - { - // setup notification - notification.contractIndex = oqm.typeVar.contract.queryingContract; - notification.procedure = replyState.notificationProcedure; - notification.inputSize = (uint16_t)(16 + replySize); - notification.inputPtr = contractNotificationInputBuffer; - notification.localsSize = replyState.notificationLocalsSize; - setMem(contractNotificationInputBuffer, notification.inputSize, 0); - *(int64_t*)(contractNotificationInputBuffer + 0) = oqm.queryId; - *(uint32_t*)(contractNotificationInputBuffer + 8) = 0; - *(uint32_t*)(contractNotificationInputBuffer + 12) = oqm.status; - if (replyData) - { - copyMem(contractNotificationInputBuffer + 16, replyData, replySize); - } - - // run notification - QpiContextUserProcedureNotificationCall qpiContext(notification); - qpiContext.call(); - } - - // TODO: handle subscriptions - */ - } - public: // CAUTION: Called from request processor, requires locking! void processOracleMachineReply(const OracleMachineReply* replyMessage, uint32_t replyMessageSize) @@ -745,13 +718,14 @@ class OracleEngine pendingQueryIndices.removeByValue(queryIndex); ++stats.unresolvableCount; - // run contract notification(s) if needed - notifyContractsIfAny(oqm); - // cleanup data of pending reply immediately (no info for revenue required) pendingCommitReplyStateIndices.removeByValue(replyStateIdx); freeReplyStateSlot(replyStateIdx); + // schedule contract notification(s) if needed + if (oqm.type != ORACLE_QUERY_TYPE_USER_QUERY) + notificationQueryIndicies.add(queryIndex); + // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status } @@ -899,6 +873,19 @@ class OracleEngine return &replyState; } + const void* getReplyDataFromTickTransactionStorage(const OracleQueryMetadata& queryMetadata) const + { + const uint32_t tick = queryMetadata.statusVar.success.revealTick; + const uint32_t txSlotInTickData = queryMetadata.statusVar.success.revealTxIndex; + ASSERT(txSlotInTickData < NUMBER_OF_TRANSACTIONS_PER_TICK); + const unsigned long long* tsTickTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(tick); + const auto* transaction = (OracleReplyRevealTransactionPrefix*)ts.tickTransactions.ptr(tsTickTransactionOffsets[txSlotInTickData]); + ASSERT(queryMetadata.queryId == transaction->queryId); + ASSERT(queryMetadata.interfaceIndex < OI::oracleInterfacesCount); + ASSERT(transaction->inputSize - sizeof(transaction->queryId) == OI::oracleInterfaces[queryMetadata.interfaceIndex].replySize); + return transaction + 1; + } + public: // Called by request processor when a tx is received in order to minimize sending of reveal tx. void announceExpectedRevealTransaction(const OracleReplyRevealTransactionPrefix* transaction) @@ -935,24 +922,71 @@ class OracleEngine // TODO: check knowledge proofs of all computors and add revenue points for computors who sent correct commit tx fastest - // update state + // update state to SUCCESS oqm.statusVar.success.revealTick = transaction->tick; oqm.statusVar.success.revealTxIndex = txSlotInTickData; oqm.status = ORACLE_QUERY_STATUS_SUCCESS; pendingQueryIndices.removeByValue(queryIndex); ++stats.successCount; - // run contract notification(s) if needed - const void* replyData = transaction + 1; - notifyContractsIfAny(oqm, replyData); - - // cleanup data of pending reply + // cleanup reply state pendingRevealReplyStateIndices.removeByValue(replyStateIdx); freeReplyStateSlot(replyStateIdx); + // schedule contract notification(s) if needed + if (oqm.type != ORACLE_QUERY_TYPE_USER_QUERY) + notificationQueryIndicies.add(queryIndex); + return true; } + /** + * @brief Get info for notfying contracts. Call until nullptr is returned. + * @return Pointer to notification info or nullptr if no notifications are needed. + * + * Only to be used in tick processor! No concurrent use supported. Uses one internal buffer for returned data. + */ + const OracleNotificationData* getNotification() + { + // currently no notifications needed? + if (!notificationQueryIndicies.numValues) + return nullptr; + + // lock for accessing engine data + LockGuard lockGuard(lock); + + // get index and update list + const uint32_t queryIndex = notificationQueryIndicies.values[0]; + notificationQueryIndicies.removeByIndex(0); + + // get query metadata + const OracleQueryMetadata& oqm = queries[queryIndex]; + ASSERT(oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY); + + const auto replySize = OI::oracleInterfaces[oqm.interfaceIndex].replySize; + ASSERT(16 + replySize < 0xffff); + + if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) + { + // setup notification + notificationOutputBuffer.contractIndex = oqm.typeVar.contract.queryingContract; + notificationOutputBuffer.procedureId = oqm.typeVar.contract.notificationProcId; + notificationOutputBuffer.inputSize = (uint16_t)(16 + replySize); + setMem(notificationOutputBuffer.inputBuffer, notificationOutputBuffer.inputSize, 0); + *(int64_t*)(notificationOutputBuffer.inputBuffer + 0) = oqm.queryId; + *(uint32_t*)(notificationOutputBuffer.inputBuffer + 8) = 0; + *(uint32_t*)(notificationOutputBuffer.inputBuffer + 12) = oqm.status; + if (oqm.status == ORACLE_QUERY_STATUS_SUCCESS) + { + const void* replySrcPtr = getReplyDataFromTickTransactionStorage(oqm); + copyMem(notificationOutputBuffer.inputBuffer + 16, replySrcPtr, replySize); + } + } + + // TODO: handle subscriptions + + return ¬ificationOutputBuffer; + } void beginEpoch() { diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index f5c3111bf..2a477c995 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -10,6 +10,7 @@ struct OracleEngineTest : public LoggingTest EXPECT_TRUE(initCommonBuffers()); EXPECT_TRUE(initSpecialEntities()); EXPECT_TRUE(initContractExec()); + EXPECT_TRUE(ts.init()); // init computors for (int computorIndex = 0; computorIndex < NUMBER_OF_COMPUTORS; computorIndex++) @@ -25,12 +26,14 @@ struct OracleEngineTest : public LoggingTest etalonTick.hour = 16; etalonTick.minute = 51; etalonTick.second = 12; + ts.beginEpoch(system.tick); } ~OracleEngineTest() { deinitCommonBuffers(); deinitContractExec(); + ts.deinit(); } }; @@ -157,6 +160,9 @@ TEST(OracleEngine, ContractQuerySuccess) // no reveal yet EXPECT_EQ(oracleEngine1.getReplyRevealTransaction(txBuffer, 0, system.tick + 3, 0), 0); + // no notifications + EXPECT_EQ(oracleEngine1.getNotification(), nullptr); + //------------------------------------------------------------------------- // create and process enough reply commit tx to trigger reval tx @@ -234,7 +240,26 @@ TEST(OracleEngine, ContractQuerySuccess) system.tick += 3; auto* replyRevealTx = (OracleReplyRevealTransactionPrefix*)txBuffer; - oracleEngine1.processOracleReplyRevealTransaction(replyRevealTx, 0); + const unsigned int txIndex = 10; + addOracleTransactionToTickStorage(replyRevealTx, txIndex); + oracleEngine1.processOracleReplyRevealTransaction(replyRevealTx, txIndex); + + //------------------------------------------------------------------------- + // notifications + const OracleNotificationData* notification = oracleEngine1.getNotification(); + EXPECT_NE(notification, nullptr); + EXPECT_EQ((int)notification->contractIndex, (int)contractIndex); + EXPECT_EQ(notification->procedureId, notificationProcId); + EXPECT_EQ((int)notification->inputSize, sizeof(OracleNotificationInput)); + const auto* notificationInput = (const OracleNotificationInput*) & notification->inputBuffer; + EXPECT_EQ(notificationInput->queryId, replyRevealTx->queryId); + EXPECT_EQ(notificationInput->status, ORACLE_QUERY_STATUS_SUCCESS); + EXPECT_EQ(notificationInput->subscriptionId, 0); + EXPECT_EQ(notificationInput->reply.numerator, 1234); + EXPECT_EQ(notificationInput->reply.denominator, 1); + + // no additional notifications + EXPECT_EQ(oracleEngine1.getNotification(), nullptr); } TEST(OracleEngine, ContractQueryUnresolvable) @@ -361,6 +386,23 @@ TEST(OracleEngine, ContractQueryUnresolvable) oracleEngine3.checkStatus(queryId, ORACLE_QUERY_STATUS_UNRESOLVABLE); } } + + //------------------------------------------------------------------------- + // notifications + const OracleNotificationData* notification = oracleEngine1.getNotification(); + EXPECT_NE(notification, nullptr); + EXPECT_EQ((int)notification->contractIndex, (int)contractIndex); + EXPECT_EQ(notification->procedureId, notificationProcId); + EXPECT_EQ((int)notification->inputSize, sizeof(OracleNotificationInput)); + const auto* notificationInput = (const OracleNotificationInput*) & notification->inputBuffer; + EXPECT_EQ(notificationInput->queryId, queryId); + EXPECT_EQ(notificationInput->status, ORACLE_QUERY_STATUS_UNRESOLVABLE); + EXPECT_EQ(notificationInput->subscriptionId, 0); + EXPECT_EQ(notificationInput->reply.numerator, 0); + EXPECT_EQ(notificationInput->reply.denominator, 0); + + // no additional notifications + EXPECT_EQ(oracleEngine1.getNotification(), nullptr); } /* diff --git a/test/oracle_testing.h b/test/oracle_testing.h index eed680217..c84ac3116 100644 --- a/test/oracle_testing.h +++ b/test/oracle_testing.h @@ -3,6 +3,12 @@ // Include this first, to ensure "logging/logging.h" isn't included before the custom LOG_BUFFER_SIZE has been defined #include "logging_test.h" +#undef MAX_NUMBER_OF_TICKS_PER_EPOCH +#define MAX_NUMBER_OF_TICKS_PER_EPOCH 50 +#undef TICKS_TO_KEEP_FROM_PRIOR_EPOCH +#define TICKS_TO_KEEP_FROM_PRIOR_EPOCH 5 +#include "ticking/tick_storage.h" + #include "gtest/gtest.h" #include "oracle_core/oracle_engine.h" @@ -53,3 +59,16 @@ static inline QPI::uint64 getContractOracleQueryId(QPI::uint32 tick, QPI::uint32 { return ((QPI::uint64)tick << 31) | (indexInTick + NUMBER_OF_TRANSACTIONS_PER_TICK); } + +static void addOracleTransactionToTickStorage(const Transaction* tx, unsigned int txIndex) +{ + const unsigned int txSize = tx->totalSize(); + auto* offsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(tx->tick); + if (ts.nextTickTransactionOffset + txSize <= ts.tickTransactions.storageSpaceCurrentEpoch) + { + EXPECT_EQ(offsets[txIndex], 0); + offsets[txIndex] = ts.nextTickTransactionOffset; + copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), tx, txSize); + ts.nextTickTransactionOffset += txSize; + } +} From 2e58367fecf64685795bf18da402dbf820705651 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:42:50 +0100 Subject: [PATCH 08/90] Implement timeouts in oracle engine --- src/oracle_core/oracle_engine.h | 51 +++++++++++++++++++++++++++++++ test/oracle_engine.cpp | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index d5865eacb..a9394710d 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -940,6 +940,57 @@ class OracleEngine return true; } + // Called once per tick from the tick processor. + void processTimeouts() + { + // lock for accessing engine data + LockGuard lockGuard(lock); + + // consider peinding queries + const uint32_t queryIdxCount = pendingQueryIndices.numValues; + const uint32_t* queryIndices = pendingQueryIndices.values; + const QPI::DateAndTime now = QPI::DateAndTime::now(); + for (uint32_t i = 0; i < queryIdxCount; ++i) + { + // get query data + const uint32_t queryIndex = queryIndices[i]; + ASSERT(queryIndex < oracleQueryCount); + OracleQueryMetadata& oqm = queries[queryIndex]; + ASSERT(oqm.status == ORACLE_QUERY_STATUS_PENDING || oqm.status == ORACLE_QUERY_STATUS_COMMITTED); + + // check for timeout + if (oqm.timeout < now) + { + // get reply state + const auto replyStateIdx = oqm.statusVar.pending.replyStateIndex; + ASSERT(replyStateIdx < MAX_SIMULTANEOUS_ORACLE_QUERIES); + ReplyState& replyState = replyStates[replyStateIdx]; + ASSERT(replyState.queryId == oqm.queryId); + const uint16_t mostCommitsCount = replyState.replyCommitHistogramCount[replyState.mostCommitsHistIdx]; + ASSERT(replyState.mostCommitsHistIdx < NUMBER_OF_COMPUTORS && mostCommitsCount <= NUMBER_OF_COMPUTORS); + + // update state to TIMEOUT + oqm.status = ORACLE_QUERY_STATUS_TIMEOUT; + oqm.statusFlags |= ORACLE_FLAG_TIMEOUT; + oqm.statusVar.failure.agreeingCommits = mostCommitsCount; + oqm.statusVar.failure.totalCommits = replyState.totalCommits; + pendingQueryIndices.removeByValue(queryIndex); + ++stats.timeoutCount; + + // cleanup reply state + pendingCommitReplyStateIndices.removeByValue(replyStateIdx); + pendingRevealReplyStateIndices.removeByValue(replyStateIdx); + freeReplyStateSlot(replyStateIdx); + + // schedule contract notification(s) if needed + if (oqm.type != ORACLE_QUERY_TYPE_USER_QUERY) + notificationQueryIndicies.add(queryIndex); + + // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + } + } + } + /** * @brief Get info for notfying contracts. Call until nullptr is returned. * @return Pointer to notification info or nullptr if no notifications are needed. diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index 2a477c995..e3406402b 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -405,6 +405,60 @@ TEST(OracleEngine, ContractQueryUnresolvable) EXPECT_EQ(oracleEngine1.getNotification(), nullptr); } +TEST(OracleEngine, ContractQueryTimeout) +{ + OracleEngineTest test; + + // simulate one node + const m256i* allCompPubKeys = broadcastedComputors.computors.publicKeys; + OracleEngineWithInitAndDeinit<676> oracleEngine1(allCompPubKeys); + + OI::Price::OracleQuery priceQuery; + priceQuery.oracle = m256i(10, 20, 30, 40); + priceQuery.currency1 = m256i(20, 30, 40, 50); + priceQuery.currency2 = m256i(30, 40, 50, 60); + priceQuery.timestamp = QPI::DateAndTime::now(); + QPI::uint32 interfaceIndex = 0; + QPI::uint16 contractIndex = 2; + QPI::uint32 timeout = 10000; + const QPI::uint32 notificationProcId = 12345; + EXPECT_TRUE(userProcedureRegistry->add(notificationProcId, { dummyNotificationProc, 1, 1024, 128, 1 })); + + //------------------------------------------------------------------------- + // start contract query / check message to OM node + QPI::sint64 queryId = oracleEngine1.startContractQuery(contractIndex, interfaceIndex, &priceQuery, sizeof(priceQuery), timeout, notificationProcId); + checkNetworkMessageOracleMachineQuery(queryId, priceQuery.oracle, timeout); + + //------------------------------------------------------------------------- + // get query contract data + OI::Price::OracleQuery priceQueryReturned; + EXPECT_TRUE(oracleEngine1.getOracleQuery(queryId, &priceQueryReturned, sizeof(priceQueryReturned))); + EXPECT_EQ(memcmp(&priceQueryReturned, &priceQuery, sizeof(priceQuery)), 0); + + //------------------------------------------------------------------------- + // timeout: no response from OM node + ++system.tick; + ++etalonTick.hour; + oracleEngine1.processTimeouts(); + + //------------------------------------------------------------------------- + // notifications + const OracleNotificationData* notification = oracleEngine1.getNotification(); + EXPECT_NE(notification, nullptr); + EXPECT_EQ((int)notification->contractIndex, (int)contractIndex); + EXPECT_EQ(notification->procedureId, notificationProcId); + EXPECT_EQ((int)notification->inputSize, sizeof(OracleNotificationInput)); + const auto* notificationInput = (const OracleNotificationInput*) & notification->inputBuffer; + EXPECT_EQ(notificationInput->queryId, queryId); + EXPECT_EQ(notificationInput->status, ORACLE_QUERY_STATUS_TIMEOUT); + EXPECT_EQ(notificationInput->subscriptionId, 0); + EXPECT_EQ(notificationInput->reply.numerator, 0); + EXPECT_EQ(notificationInput->reply.denominator, 0); + + // no additional notifications + EXPECT_EQ(oracleEngine1.getNotification(), nullptr); +} + /* Tests: - oracleEngine.getReplyCommitTransaction() with more than 1 commit / tx From dac80f7af8ff1569d9526bdd20c0e0bcb2136b24 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:51:09 +0100 Subject: [PATCH 09/90] Integrate oracle engine in qubic.cpp --- src/contract_core/contract_exec.h | 17 +---- src/logging/logging.h | 1 + src/qubic.cpp | 118 +++++++++++++++++++++++------- 3 files changed, 97 insertions(+), 39 deletions(-) diff --git a/src/contract_core/contract_exec.h b/src/contract_core/contract_exec.h index 9358c355d..d2626f33c 100644 --- a/src/contract_core/contract_exec.h +++ b/src/contract_core/contract_exec.h @@ -1281,15 +1281,6 @@ struct QpiContextUserFunctionCall : public QPI::QpiContextFunctionCall }; -struct UserProcedureNotification -{ - unsigned int contractIndex; - USER_PROCEDURE procedure; - const void* inputPtr; - unsigned short inputSize; - unsigned int localsSize; -}; - // QPI context used to call contract user procedure as a notification from qubic core (contract processor). // This means, it isn't triggered by a transaction, but following an event after having setup the notification // callback in the contract code. @@ -1301,13 +1292,13 @@ struct UserProcedureNotification // - oracle notifications (managed by oracleEngine and userProcedureRegistry) struct QpiContextUserProcedureNotificationCall : public QPI::QpiContextProcedureCall { - QpiContextUserProcedureNotificationCall(const UserProcedureNotification& notification) : QPI::QpiContextProcedureCall(notification.contractIndex, NULL_ID, 0, USER_PROCEDURE_NOTIFICATION_CALL), notif(notification) + QpiContextUserProcedureNotificationCall(const UserProcedureRegistry::UserProcedureData& notification) : QPI::QpiContextProcedureCall(notification.contractIndex, NULL_ID, 0, USER_PROCEDURE_NOTIFICATION_CALL), notif(notification) { contractActionTracker.init(); } // Run user procedure notification - void call() + void call(const void* inputPtr) { ASSERT(_currentContractIndex < contractCount); @@ -1347,7 +1338,7 @@ struct QpiContextUserProcedureNotificationCall : public QPI::QpiContextProcedure __qpiAbort(ContractErrorAllocInputOutputFailed); } char* locals = input + notif.inputSize; - copyMem(input, notif.inputPtr, notif.inputSize); + copyMem(input, inputPtr, notif.inputSize); setMem(locals, notif.localsSize, 0); // call user procedure @@ -1370,5 +1361,5 @@ struct QpiContextUserProcedureNotificationCall : public QPI::QpiContextProcedure } private: - const UserProcedureNotification& notif; + const UserProcedureRegistry::UserProcedureData& notif; }; diff --git a/src/logging/logging.h b/src/logging/logging.h index c0ed6f6d7..d22512e8b 100644 --- a/src/logging/logging.h +++ b/src/logging/logging.h @@ -374,6 +374,7 @@ class qLogger static constexpr unsigned int SC_BEGIN_TICK_TX = NUMBER_OF_TRANSACTIONS_PER_TICK + 2; static constexpr unsigned int SC_END_TICK_TX = NUMBER_OF_TRANSACTIONS_PER_TICK + 3; static constexpr unsigned int SC_END_EPOCH_TX = NUMBER_OF_TRANSACTIONS_PER_TICK + 4; + static constexpr unsigned int SC_NOTIFICATION_TX = NUMBER_OF_TRANSACTIONS_PER_TICK + 5; #if ENABLED_LOGGING // Struct to map log buffer from log id diff --git a/src/qubic.cpp b/src/qubic.cpp index 999a86038..b3ea9f3c2 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -158,7 +158,8 @@ static unsigned int contractProcessorPhase; static const Transaction* contractProcessorTransaction = 0; // does not have signature in some cases, see notifyContractOfIncomingTransfer() static int contractProcessorTransactionMoneyflew = 0; static unsigned char contractProcessorPostIncomingTransferType = 0; -static const UserProcedureNotification* contractProcessorUserProcedureNotification = 0; +static const UserProcedureRegistry::UserProcedureData* contractProcessorUserProcedureNotificationProc = 0; +static const void* contractProcessorUserProcedureNotificationInput = 0; static EFI_EVENT contractProcessorEvent; static m256i contractStateDigests[MAX_NUMBER_OF_CONTRACTS * 2 - 1]; const unsigned long long contractStateDigestsSizeInBytes = sizeof(contractStateDigests); @@ -995,6 +996,14 @@ static void processBroadcastTransaction(Peer* peer, RequestResponseHeader* heade } } ts.tickData.releaseLock(); + + // shortcut: oracle reply reveal transactions are analyzed immediately after receiving them (before execution of the tx), + // in order to minimize the number of reveal transaction (one per oracle query is enough, so no reveal tx is generated + // after one has been seen) + if (isZero(request->destinationPublicKey) && request->inputType == OracleReplyRevealTransactionPrefix::transactionType()) + { + oracleEngine.announceExpectedRevealTransaction((OracleReplyRevealTransactionPrefix*)request); + } } } } @@ -2412,15 +2421,17 @@ static void contractProcessor(void*) case USER_PROCEDURE_NOTIFICATION_CALL: { - const auto* notification = contractProcessorUserProcedureNotification; - ASSERT(notification && notification->procedure && notification->inputPtr); + const auto* notification = contractProcessorUserProcedureNotificationProc; + ASSERT(notification && notification->procedure); ASSERT(notification->inputSize <= MAX_INPUT_SIZE); ASSERT(notification->localsSize <= MAX_SIZE_OF_CONTRACT_LOCALS); + ASSERT(contractProcessorUserProcedureNotificationInput); QpiContextUserProcedureNotificationCall qpiContext(*notification); - qpiContext.call(); + qpiContext.call(contractProcessorUserProcedureNotificationInput); - contractProcessorUserProcedureNotification = 0; + contractProcessorUserProcedureNotificationProc = 0; + contractProcessorUserProcedureNotificationInput = 0; } break; } @@ -2732,28 +2743,17 @@ static void processTickTransactionSolution(const MiningSolutionTransaction* tran } } - -static void processTickTransactionOracleReplyReveal(const OracleReplyRevealTransactionPrefix* transaction) +static void processTickTransaction(const Transaction* transaction, unsigned int transactionIndex, unsigned long long processorNumber) { PROFILE_SCOPE(); ASSERT(nextTickData.epoch == system.epoch); ASSERT(transaction != nullptr); ASSERT(transaction->checkValidity()); - ASSERT(isZero(transaction->destinationPublicKey)); ASSERT(transaction->tick == system.tick); - // TODO -} - -static void processTickTransaction(const Transaction* transaction, const m256i& transactionDigest, const m256i& dataLock, unsigned long long processorNumber) -{ - PROFILE_SCOPE(); - - ASSERT(nextTickData.epoch == system.epoch); - ASSERT(transaction != nullptr); - ASSERT(transaction->checkValidity()); - ASSERT(transaction->tick == system.tick); + const m256i& transactionDigest = nextTickData.transactionDigests[transactionIndex]; + const m256i& dataLock = nextTickData.timelock; // Record the tx with digest ts.transactionsDigestAccess.acquireLock(); @@ -2839,12 +2839,7 @@ static void processTickTransaction(const Transaction* transaction, const m256i& case OracleReplyRevealTransactionPrefix::transactionType(): { - if (computorIndex(transaction->sourcePublicKey) >= 0 - && transaction->inputSize >= OracleReplyRevealTransactionPrefix::minInputSize()) - { - // TODO: fix size check by defining minInputSize - processTickTransactionOracleReplyReveal((OracleReplyRevealTransactionPrefix*)transaction); - } + oracleEngine.processOracleReplyRevealTransaction((OracleReplyRevealTransactionPrefix*)transaction, transactionIndex); } break; @@ -3227,7 +3222,7 @@ static void processTick(unsigned long long processorNumber) { Transaction* transaction = ts.tickTransactions(tsCurrentTickTransactionOffsets[transactionIndex]); logger.registerNewTx(transaction->tick, transactionIndex); - processTickTransaction(transaction, nextTickData.transactionDigests[transactionIndex], nextTickData.timelock, processorNumber); + processTickTransaction(transaction, transactionIndex, processorNumber); } else { @@ -3241,6 +3236,28 @@ static void processTick(unsigned long long processorNumber) PROFILE_SCOPE_END(); } + // Check for oracle query timeouts (may schedule notification) + oracleEngine.processTimeouts(); + + // Notify contracts about successfully obtained oracle replies and about errors (using contract processor) + const OracleNotificationData* oracleNotification = oracleEngine.getNotification(); + while (oracleNotification) + { + PROFILE_NAMED_SCOPE("processTick(): run oracle contract notification"); + logger.registerNewTx(system.tick, logger.SC_NOTIFICATION_TX); + contractProcessorUserProcedureNotificationProc = userProcedureRegistry->get(oracleNotification->procedureId); + contractProcessorUserProcedureNotificationInput = oracleNotification->inputBuffer; + ASSERT(contractProcessorUserProcedureNotificationProc); + ASSERT(contractProcessorUserProcedureNotificationProc->contractIndex == oracleNotification->contractIndex); + ASSERT(contractProcessorUserProcedureNotificationProc->inputSize == oracleNotification->inputSize); + ASSERT(contractProcessorUserProcedureNotificationInput); + contractProcessorPhase = USER_PROCEDURE_NOTIFICATION_CALL; + contractProcessorState = 1; + WAIT_WHILE(contractProcessorState); + + oracleNotification = oracleEngine.getNotification(); + } + // The last executionFeeReport for the previous phase is published by comp (0-indexed) in the last tick t1 of the // previous phase (t1 % NUMBER_OF_COMPUTORS == NUMBER_OF_COMPUTORS - 1) for inclusion in tick t2 = t1 + TICK_TRANSACTIONS_PUBLICATION_OFFSET. // Tick t2 corresponds to tick of the current phase. @@ -3455,6 +3472,55 @@ static void processTick(unsigned long long processorNumber) } } + // Publish oracle reply commit and reveal transactions (uses reorgBuffer for constructing packets) + if (isMainMode()) + { + const auto txTick = system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET; + unsigned char digest[32]; + { + PROFILE_NAMED_SCOPE("processTick(): broadcast oracle reply transactions"); + auto* tx = (OracleReplyCommitTransactionPrefix*)reorgBuffer; + for (unsigned int i = 0; i < numberOfOwnComputorIndices; i++) + { + const auto ownCompIdx = ownComputorIndicesMapping[i]; + const auto overallCompIdx = ownComputorIndices[i]; + unsigned int retCode = 0; + do + { + // create reply commit transaction in tx (without signature), returning: + // - 0 if no tx was created (no need to send reply commits) + // - UINT32_MAX if we all pending reply commits fitted into this one tx + // - otherwise, an index value that has to be passed to the next call for building another tx + retCode = oracleEngine.getReplyCommitTransaction(tx, overallCompIdx, ownCompIdx, txTick, retCode); + if (!retCode) + break; + + // sign and broadcast tx + KangarooTwelve(tx, sizeof(Transaction) + tx->inputSize, digest, sizeof(digest)); + sign(computorSubseeds[i].m256i_u8, computorPublicKeys[i].m256i_u8, digest, tx->signaturePtr()); + enqueueResponse(NULL, tx->totalSize(), BROADCAST_TRANSACTION, 0, tx); + } + while (retCode != UINT32_MAX); + } + } + + { + PROFILE_NAMED_SCOPE("processTick(): broadcast oracle reveal transactions"); + auto* tx = (OracleReplyRevealTransactionPrefix*)reorgBuffer; + // create reply reveal transaction in tx (without signature), returning: + // - 0 if no tx was created (no need to send reply commits) + // - otherwise, an index value that has to be passed to the next call for building another tx + unsigned int retCode = 0; + while ((retCode = oracleEngine.getReplyRevealTransaction(tx, 0, txTick, retCode)) != 0) + { + // sign and broadcast tx + KangarooTwelve(tx, sizeof(Transaction) + tx->inputSize, digest, sizeof(digest)); + sign(computorSubseeds[0].m256i_u8, computorPublicKeys[0].m256i_u8, digest, tx->signaturePtr()); + enqueueResponse(NULL, tx->totalSize(), BROADCAST_TRANSACTION, 0, tx); + } + } + } + if (isMainMode()) { // Publish solutions that were sent via BroadcastMessage as MiningSolutionTransaction From 54fe7cc4914cb476b22dbb15a7dbe919e7a41a9a Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:29:11 +0100 Subject: [PATCH 10/90] Reduce OracleQueryMetadata size from 80 to 72 bytes --- src/oracle_core/oracle_engine.h | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index a9394710d..93052ecce 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -21,35 +21,36 @@ constexpr uint32_t MAX_ORACLE_QUERIES = (1 << 18); constexpr uint64_t ORACLE_QUERY_STORAGE_SIZE = MAX_ORACLE_QUERIES * 512; constexpr uint32_t MAX_SIMULTANEOUS_ORACLE_QUERIES = 1024; +#pragma pack(push, 4) struct OracleQueryMetadata { int64_t queryId; ///< bits 31-62 encode index in tick, bits 0-30 are index in tick, negative values indicate error uint8_t type; ///< contract query, user query, subscription (may be by multiple contracts) uint8_t status; ///< overall status (pending -> success or timeout) uint16_t statusFlags; ///< status and error flags (especially as returned by oracle machine connected to this node) - uint32_t interfaceIndex; uint32_t queryTick; QPI::DateAndTime timeout; + uint32_t interfaceIndex; union { struct { - m256i queryingEntity; uint32_t queryTxIndex; // query tx index in tick + m256i queryingEntity; } user; struct { - uint64_t queryStorageOffset; uint32_t notificationProcId; + uint64_t queryStorageOffset; uint16_t queryingContract; } contract; struct { - uint64_t queryStorageOffset; uint32_t subscriptionId; ///< 0 is reserved for "no subscription" + uint64_t queryStorageOffset; } subscription; } typeVar; @@ -76,6 +77,9 @@ struct OracleQueryMetadata } failure; } statusVar; }; +#pragma pack(pop) + +static_assert(sizeof(OracleQueryMetadata) == 72, "Unexpected struct size"); struct OracleSubscriptionContractStatus From f3c85d5f28981e64950c91a2828519eddbdf7685 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:33:19 +0100 Subject: [PATCH 11/90] Implement first RequestOracleData features --- src/network_messages/oracles.h | 12 +-- src/oracle_core/net_msg_impl.h | 152 ++++++++++++++++++++++++++++++++ src/oracle_core/oracle_engine.h | 2 + src/qubic.cpp | 9 ++ 4 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 src/oracle_core/net_msg_impl.h diff --git a/src/network_messages/oracles.h b/src/network_messages/oracles.h index bb7861375..1ff3f4407 100644 --- a/src/network_messages/oracles.h +++ b/src/network_messages/oracles.h @@ -31,7 +31,7 @@ struct RequestOracleData unsigned int _padding; // tick, query ID, or subscription ID (depending on reqType) - unsigned long long reqTickOrId; + long long reqTickOrId; }; static_assert(sizeof(RequestOracleData) == 16, "Something is wrong with the struct size."); @@ -79,23 +79,25 @@ struct RespondOracleDataQueryMetadata uint8_t type; ///< contract query, user query, subscription (may be by multiple contracts) uint8_t status; ///< overall status (pending -> success or timeout) uint16_t statusFlags; ///< status and error flags (especially as returned by oracle machine connected to this node) - uint32_t interfaceIndex; uint32_t queryTick; - uint64_t timeout; ///< Timeout in QPI::DateAndTime format m256i queryingEntity; + uint64_t timeout; ///< Timeout in QPI::DateAndTime format + uint32_t interfaceIndex; int32_t subscriptionId; ///< -1 is reserved for "no subscription" uint32_t revealTick; ///< Tick of reveal tx. Only available if status is success. uint16_t totalCommits; ///< Total number of commit tx. Only available if status isn't success. uint16_t agreeingCommits; ///< Number of agreeing commit tx (biggest group with same digest). Only available if status isn't success. }; +static_assert(sizeof(RespondOracleDataQueryMetadata) == 72, "Unexpected struct size"); + struct RespondOracleDataSubscriptionMetadata { - uint16_t queryIntervalMinutes; - uint16_t queryTimestampOffset; int64_t lastQueryQueryId; int64_t lastRevealQueryId; uint64_t nextQueryTimestamp; + uint16_t queryIntervalMinutes; + uint16_t queryTimestampOffset; }; struct RespondOracleDataSubscriptionContractMetadata diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h new file mode 100644 index 000000000..03b7ea76b --- /dev/null +++ b/src/oracle_core/net_msg_impl.h @@ -0,0 +1,152 @@ +#pragma once + +#include "oracle_core/oracle_engine.h" +#include "network_messages/oracles.h" +#include "network_core/peers.h" + +template +void OracleEngine::processRequestOracleData(Peer* peer, RequestResponseHeader* header) const +{ + // check input + ASSERT(header && peer); + ASSERT(header->type() == RequestOracleData::type()); + if (!header->checkPayloadSize(sizeof(RequestOracleData))) + return; + + // prepare buffer + constexpr int maxQueryIdCount = 2; + constexpr int payloadBufferSize = math_lib::max((int)math_lib::max(MAX_ORACLE_QUERY_SIZE, MAX_ORACLE_REPLY_SIZE), maxQueryIdCount * 8); + static_assert(payloadBufferSize >= sizeof(RespondOracleDataQueryMetadata), "Buffer too small."); + static_assert(payloadBufferSize < 32 * 1024, "Large alloc in stack may need reconsideration."); + uint8_t responseBuffer[sizeof(RespondOracleData) + payloadBufferSize]; + RespondOracleData* response = (RespondOracleData*)responseBuffer; + void* payload = responseBuffer + sizeof(RespondOracleData); + int64_t* payloadQueryIds = (int64_t*)(responseBuffer + sizeof(RespondOracleData)); + + // lock for accessing engine data + LockGuard lockGuard(lock); + + // process request + const RequestOracleData* request = header->getPayload(); + switch (request->reqType) + { + + case RequestOracleData::requestAllQueryIdsByTick: + { + // TODO + break; + } + + case RequestOracleData::requestUserQueryIdsByTick: + // TODO + break; + + case RequestOracleData::requestContractDirectQueryIdsByTick: + // TODO + break; + + case RequestOracleData::requestContractSubscriptionQueryIdsByTick: + // TODO + break; + + case RequestOracleData::requestPendingQueryIds: + { + response->resType = RespondOracleData::respondQueryIds; + const unsigned int numMessages = (pendingQueryIndices.numValues + maxQueryIdCount - 1) / maxQueryIdCount; + unsigned int idIdx = 0; + for (unsigned int msgIdx = 0; msgIdx < numMessages; ++msgIdx) + { + unsigned int idxInMsg = 0; + for (; idxInMsg < maxQueryIdCount && idIdx < pendingQueryIndices.numValues; ++idxInMsg) + { + payloadQueryIds[idxInMsg] = pendingQueryIndices.values[idIdx]; + } + enqueueResponse(peer, sizeof(RespondOracleData) + idxInMsg * 8, + RespondOracleData::type(), header->dejavu(), response); + } + break; + } + + case RequestOracleData::requestQueryAndResponse: + { + // get query metadata + const int64_t queryId = request->reqTickOrId; + uint32_t queryIndex; + if (queryId < 0 || !queryIdToIndex->get(queryId, queryIndex)) + break; + const OracleQueryMetadata& oqm = queries[queryIndex]; + + // prepare metadata response + response->resType = RespondOracleData::respondQueryMetadata; + auto* payloadOqm = (RespondOracleDataQueryMetadata*)payload; + setMemory(*payloadOqm, 0); + payloadOqm->queryId = oqm.queryId; + payloadOqm->type = oqm.type; + payloadOqm->status = oqm.status; + payloadOqm->statusFlags = oqm.statusFlags; + payloadOqm->queryTick = oqm.queryTick; + if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) + { + payloadOqm->queryingEntity = m256i(oqm.typeVar.contract.queryingContract, 0, 0, 0); + } + else if (oqm.type == ORACLE_QUERY_TYPE_USER_QUERY) + { + payloadOqm->queryingEntity = oqm.typeVar.user.queryingEntity; + } + payloadOqm->timeout = *(uint64_t*)&oqm.timeout; + payloadOqm->interfaceIndex = oqm.interfaceIndex; + if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_SUBSCRIPTION) + { + payloadOqm->subscriptionId = oqm.typeVar.subscription.subscriptionId; + } + if (oqm.status == ORACLE_QUERY_STATUS_SUCCESS) + { + payloadOqm->revealTick = oqm.statusVar.success.revealTick; + } + if (oqm.status == ORACLE_QUERY_STATUS_PENDING || oqm.status == ORACLE_QUERY_STATUS_COMMITTED) + { + const ReplyState& replyState = replyStates[oqm.statusVar.pending.replyStateIndex]; + payloadOqm->agreeingCommits = replyState.replyCommitHistogramCount[replyState.mostCommitsHistIdx]; + payloadOqm->totalCommits = replyState.totalCommits; + } + else if (oqm.status == ORACLE_QUERY_STATUS_TIMEOUT || oqm.status == ORACLE_QUERY_STATUS_UNRESOLVABLE) + { + payloadOqm->agreeingCommits = oqm.statusVar.failure.agreeingCommits; + payloadOqm->totalCommits = oqm.statusVar.failure.totalCommits; + } + + // send metadata response + enqueueResponse(peer, sizeof(RespondOracleData) + sizeof(RespondOracleDataQueryMetadata), + RespondOracleData::type(), header->dejavu(), response); + + // get and send query data + const uint16_t querySize = (uint16_t)OI::oracleInterfaces[oqm.interfaceIndex].querySize; + ASSERT(querySize <= payloadBufferSize); + if (getOracleQuery(queryId, payload, querySize)) + { + response->resType = RespondOracleData::respondQueryData; + enqueueResponse(peer, sizeof(RespondOracleData) + querySize, + RespondOracleData::type(), header->dejavu(), response); + } + + // get and send reply data + if (oqm.status == ORACLE_QUERY_STATUS_SUCCESS) + { + const uint16_t replySize = (uint16_t)OI::oracleInterfaces[oqm.interfaceIndex].replySize; + const void* replyData = getReplyDataFromTickTransactionStorage(oqm); + response->resType = RespondOracleData::respondReplyData; + enqueueResponse(peer, sizeof(RespondOracleData) + replySize, + RespondOracleData::type(), header->dejavu(), response); + } + + break; + } + + case RequestOracleData::requestSubscription: + // TODO + break; + + } + + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), nullptr); +} diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 93052ecce..d88ea61d2 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -1105,6 +1105,8 @@ class OracleEngine appendNumber(message, stats.unresolvableCount, FALSE); logToConsole(message); } + + void processRequestOracleData(Peer* peer, RequestResponseHeader* header) const; }; GLOBAL_VAR_DECL OracleEngine oracleEngine; diff --git a/src/qubic.cpp b/src/qubic.cpp index b3ea9f3c2..98a15e2c5 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -2182,11 +2182,13 @@ static void requestProcessor(void* ProcedureArgument) processRequestAssets(peer, header); } break; + case RequestCustomMiningSolutionVerification::type(): { processRequestedCustomMiningSolutionVerificationRequest(peer, header); } break; + case RequestCustomMiningData::type(): { processCustomMiningDataRequest(peer, processorNumber, header); @@ -2203,6 +2205,13 @@ static void requestProcessor(void* ProcedureArgument) { processOracleMachineReply(peer, header); } + break; + + case RequestOracleData::type(): + { + oracleEngine.processRequestOracleData(peer, header); + } + break; #if ADDON_TX_STATUS_REQUEST /* qli: process RequestTxStatus message */ From a1c4cfe86e2d03c0e9903eb1c1ac55de91ea5ccf Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Wed, 7 Jan 2026 08:58:02 +0100 Subject: [PATCH 12/90] Revert "virtual memory test: add deinit filesystem" This reverts commit ef41b57eec680d43dbe3519c7652de0373bc6351. --- test/virtual_memory.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/virtual_memory.cpp b/test/virtual_memory.cpp index 306aea3a8..50794b981 100644 --- a/test/virtual_memory.cpp +++ b/test/virtual_memory.cpp @@ -55,8 +55,6 @@ TEST(TestVirtualMemory, TestVirtualMemory_NativeChar) { EXPECT_TRUE(test_vm[index] == arr[index]); } test_vm.deinit(); - - deInitFileSystem(); } #define IMAX_BITS(m) ((m)/((m)%255+1) / 255%255*8 + 7-86/((m)%255+12)) @@ -118,8 +116,6 @@ TEST(TestVirtualMemory, TestVirtualMemory_NativeU64) { EXPECT_TRUE(test_vm[index] == arr[index]); } test_vm.deinit(); - - deInitFileSystem(); } TickData randTick() @@ -185,8 +181,6 @@ TEST(TestVirtualMemory, TestVirtualMemory_TickStruct) { EXPECT_TRUE(tickEqual(test_vm[index], arr[index])); } test_vm.deinit(); - - deInitFileSystem(); } TEST(TestVirtualMemory, TestVirtualMemory_SpecialCases) { @@ -242,6 +236,4 @@ TEST(TestVirtualMemory, TestVirtualMemory_SpecialCases) { } test_vm.deinit(); - - deInitFileSystem(); } \ No newline at end of file From 017c75f68ed64b90adcce29e5d9ba15210fee07c Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:53:08 +0100 Subject: [PATCH 13/90] Add oracle interface Mock --- src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 5 ++- src/oracle_core/oracle_interfaces_def.h | 4 +++ src/oracle_interfaces/Mock.h | 44 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/oracle_interfaces/Mock.h diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 2afd551db..1e766ae71 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -96,6 +96,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index d76cdb246..2c3df626f 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -332,6 +332,9 @@ contract_core + + oracle_interfaces + @@ -382,4 +385,4 @@ platform - + \ No newline at end of file diff --git a/src/oracle_core/oracle_interfaces_def.h b/src/oracle_core/oracle_interfaces_def.h index 23c2ff8e1..cb91fb596 100644 --- a/src/oracle_core/oracle_interfaces_def.h +++ b/src/oracle_core/oracle_interfaces_def.h @@ -8,7 +8,10 @@ namespace OI #define ORACLE_INTERFACE_INDEX 0 #include "oracle_interfaces/Price.h" #undef ORACLE_INTERFACE_INDEX + #define ORACLE_INTERFACE_INDEX 1 +#include "oracle_interfaces/Mock.h" +#undef ORACLE_INTERFACE_INDEX #define REGISTER_ORACLE_INTERFACE(Interface) {sizeof(Interface::OracleQuery), sizeof(Interface::OracleReply)} @@ -17,6 +20,7 @@ namespace OI unsigned long long replySize; } oracleInterfaces[] = { REGISTER_ORACLE_INTERFACE(Price), + REGISTER_ORACLE_INTERFACE(Mock), }; static constexpr uint32_t oracleInterfacesCount = sizeof(oracleInterfaces) / sizeof(oracleInterfaces[0]); diff --git a/src/oracle_interfaces/Mock.h b/src/oracle_interfaces/Mock.h new file mode 100644 index 000000000..4ad135cad --- /dev/null +++ b/src/oracle_interfaces/Mock.h @@ -0,0 +1,44 @@ +using namespace QPI; + +/** +* Oracle interface "Mock" (see Price.h for general documentation about oracle interfaces). +* +* This is for useful testing the oracle machine and the core logic without involving external services. +*/ +struct Mock +{ + //------------------------------------------------------------------------- + // Mandatory oracle interface definitions + + /// Oracle interface index + static constexpr uint32 oracleInterfaceIndex = ORACLE_INTERFACE_INDEX; + + /// Oracle query data / input to the oracle machine + struct OracleQuery + { + /// Value that processed + uint64 value; + }; + + /// Oracle reply data / output of the oracle machine + struct OracleReply + { + /// Value given in query + uint64 echoedValue; + + // 2 * value given in query + uint64 doubledValue; + }; + + /// Return query fee, which may depend on the specific query (for example on the oracle). + static sint64 getQueryFee(const OracleQuery& query) + { + return 10; + } + + /// Return subscription fee, which may depend on query and interval. + static sint64 getSubscriptionFee(const OracleQuery& query, uint16 notifyIntervalInMinutes) + { + return 1000; + } +}; From 6f020e221dcb3772b026d976e52f3d66bce2d2a0 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:25:52 +0100 Subject: [PATCH 14/90] set START_NETWORK_FROM_SCRATCH to 1 --- src/public_settings.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/public_settings.h b/src/public_settings.h index a41a65282..9dd31046f 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -57,7 +57,7 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // If this flag is 1, it indicates that the whole network (all 676 IDs) will start from scratch and agree that the very first tick time will be set at (2022-04-13 Wed 12:00:00.000UTC). // If this flag is 0, the node will try to fetch data of the initial tick of the epoch from other nodes, because the tick's timestamp may differ from (2022-04-13 Wed 12:00:00.000UTC). // If you restart your node after seamless epoch transition, make sure EPOCH and TICK are set correctly for the currently running epoch. -#define START_NETWORK_FROM_SCRATCH 0 +#define START_NETWORK_FROM_SCRATCH 1 // Addons: If you don't know it, leave it 0. #define ADDON_TX_STATUS_REQUEST 0 @@ -130,3 +130,4 @@ static unsigned int gFullExternalComputationTimes[][2] = // Use values like (numerator 1, denominator 10) for division by 10. GLOBAL_VAR_DECL unsigned long long executionTimeMultiplierNumerator GLOBAL_VAR_INIT(1ULL); GLOBAL_VAR_DECL unsigned long long executionTimeMultiplierDenominator GLOBAL_VAR_INIT(1ULL); + From 674639da4fdb11b600cce97689f73d6c5c291ffc Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:10:32 +0100 Subject: [PATCH 15/90] Add char enum to conveniently init id from string --- src/contracts/qpi.h | 16 ++++++++++++++++ src/platform/m256.h | 11 +++++++++++ test/qpi.cpp | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index 0e5aa119d..98a94d4ce 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -70,6 +70,22 @@ namespace QPI constexpr sint64 INVALID_AMOUNT = 0x8000000000000000; + // Characters for building strings (for example in constructor of id / m256i) + namespace Ch + { + enum : char + { + null = 0, + space = ' ', slash = '/', backslash = '\\', dot = '.', comma = ',', colon = ':', semicolon = ';', + a = 'a', b = 'b', c = 'c', d = 'd', e = 'e', f = 'f', g = 'g', h = 'h', i = 'i', j = 'j', k = 'k', l = 'l', m = 'm', + n = 'n', o = 'o', p = 'p', q = 'q', r = 'r', s = 's', t = 't', u = 'u', v = 'v', w = 'w', x = 'x', y = 'y', z = 'z', + A = 'A', B = 'B', C = 'C', D = 'D', E = 'E', F = 'F', G = 'G', H = 'H', I = 'I', J = 'J', K = 'K', L = 'L', M = 'M', + N = 'N', O = 'O', P = 'P', Q = 'Q', R = 'R', S = 'S', T = 'T', U = 'U', V = 'V', W = 'W', X = 'X', Y = 'Y', Z = 'Z', + _0 = '0', _1 = '1', _2 = '2', _3 = '3', _4 = '4', _5 = '5', _6 = '6', _7 = '7', _8 = '8', _9 = '9', + }; + } + + // Letters for defining identity with ID function constexpr long long _A = 0; constexpr long long _B = 1; constexpr long long _C = 2; diff --git a/src/platform/m256.h b/src/platform/m256.h index 09a57af9e..fa78adba4 100644 --- a/src/platform/m256.h +++ b/src/platform/m256.h @@ -62,6 +62,17 @@ union m256i _mm256_set_epi64x(ull3, ull2, ull1, ull0)); } + m256i( + char c0, char c1, char c2, char c3, char c4, char c5 = 0, char c6 = 0, char c7 = 0, char c8 = 0, char c9 = 0, + char c10 = 0, char c11 = 0, char c12 = 0, char c13 = 0, char c14 = 0, char c15 = 0, char c16 = 0, char c17 = 0, + char c18 = 0, char c19 = 0, char c20 = 0, char c21 = 0, char c22 = 0, char c23 = 0, char c24 = 0, char c25 = 0, + char c26 = 0, char c27 = 0, char c28 = 0, char c29 = 0, char c30 = 0, char c31 = 0) + { + _mm256_storeu_si256(reinterpret_cast<__m256i*>(this), + _mm256_set_epi8(c31, c30, c29, c28, c27, c26, c25, c24, c23, c22, c21, c20, c19, c18, c17, c16, c15, c14, c13, c12, + c11, c10, c9, c8, c7, c6, c5, c4, c3, c2, c1, c0)); + } + m256i(const unsigned char value[32]) { _mm256_storeu_si256(reinterpret_cast<__m256i*>(this), diff --git a/test/qpi.cpp b/test/qpi.cpp index 9ea0b67fe..6b7fc1468 100644 --- a/test/qpi.cpp +++ b/test/qpi.cpp @@ -343,6 +343,32 @@ TEST(TestCoreQPI, Mod) { EXPECT_EQ(QPI::mod(2, -1), 0); } +TEST(TestCoreQPI, IdFromCharacters) +{ + using namespace QPI::Ch; + + QPI::id test('t', 'e', s, t, '!'); + EXPECT_EQ(std::string((char*)test.m256i_i8), std::string("test!")); + + test = QPI::id(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, space, dot, comma, colon, semicolon, null); + EXPECT_EQ(std::string((char*)test.m256i_i8), std::string("abcdefghijklmnopqrstuvwxyz .,:;")); + + test = QPI::id(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, slash, backslash); + EXPECT_EQ(std::string((char*)test.m256i_i8), std::string("ABCDEFGHIJKLMNOPQRSTUVWXYZ/\\")); + + test = QPI::id(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9); + EXPECT_EQ(std::string((char*)test.m256i_i8), std::string("0123456789")); + + test = QPI::id(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + EXPECT_TRUE(isZero(test)); + + test = QPI::id(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, space); + EXPECT_EQ(test.i8._31, ' '); + test.m256i_i8[31] = 0; + EXPECT_TRUE(isZero(test)); +} + + struct ContractExecInitDeinitGuard { ContractExecInitDeinitGuard() From c195c22dec62112eacba35651df06a61b867d3e2 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:13:40 +0100 Subject: [PATCH 16/90] Set TESTEXC regular price query to value supported by OM --- src/contracts/TestExampleC.h | 9 +++++++++ src/oracle_interfaces/Price.h | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index b8ad49346..43074ac9f 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -282,6 +282,15 @@ struct TESTEXC : public ContractBase // Query oracle if (qpi.tick() % 2 == 0) { + // Setup query (in extra scope limit scope of using namespace Ch + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getMockOracleId(); + locals.priceOracleQuery.currency1 = id(B, T, C, null, null); + locals.priceOracleQuery.currency2 = id(U, S, D, null, null); + locals.priceOracleQuery.timestamp = qpi.now(); + } + locals.oracleQueryId = QUERY_ORACLE(OI::Price, locals.priceOracleQuery, NotifyPriceOracleReply, 20000); } } diff --git a/src/oracle_interfaces/Price.h b/src/oracle_interfaces/Price.h index fba931343..70bc92a48 100644 --- a/src/oracle_interfaces/Price.h +++ b/src/oracle_interfaces/Price.h @@ -96,4 +96,18 @@ struct Price // TODO: // implement and test currency conversion (including using uint128 on the way in order to support large quantities) // provide character enum / id constructor for convenient setting of oracle/currency IDs + + /// Get oracle ID of mock oracle + static id getMockOracleId() + { + using namespace Ch; + return id(m, o, c, k, null); + } + + /// Get oracle ID of coingecko oracle + static id getCoingeckoOracleId() + { + using namespace Ch; + return id(c, o, i, n, g, e, c, k, o); + } }; From ea7bef473a72047dae0aaad1ab6e6b593e40ba0f Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:51:44 +0100 Subject: [PATCH 17/90] TESTEXC: Add query/notification to mack oracle interface --- src/contracts/TestExampleC.h | 40 ++++++++++++++++++++++++++++++++++-- test/contract_testex.cpp | 2 +- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index 43074ac9f..33b4aa501 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -271,16 +271,46 @@ struct TESTEXC : public ContractBase } } + typedef OracleNotificationInput NotifyMockOracleReply_input; + typedef NoData NotifyMockOracleReply_output; + struct NotifyMockOracleReply_locals + { + OI::Mock::OracleQuery query; + uint32 queryExtraData; + }; + + PRIVATE_PROCEDURE_WITH_LOCALS(NotifyMockOracleReply) + { + if (input.status == ORACLE_QUERY_STATUS_SUCCESS) + { + // get and use query info if needed + if (!qpi.getOracleQuery(input.queryId, locals.query)) + return; + + ASSERT(locals.query.value == input.reply.echoedValue); + ASSERT(locals.query.value == input.reply.doubledValue / 2); + + // TODO: log + } + else + { + // handle failure ... + + // TODO: log + } + } + struct END_TICK_locals { OI::Price::OracleQuery priceOracleQuery; + OI::Mock::OracleQuery mockOracleQuery; sint64 oracleQueryId; }; END_TICK_WITH_LOCALS() { - // Query oracle - if (qpi.tick() % 2 == 0) + // Query oracles + if (qpi.tick() % 10 == 0) { // Setup query (in extra scope limit scope of using namespace Ch { @@ -293,6 +323,11 @@ struct TESTEXC : public ContractBase locals.oracleQueryId = QUERY_ORACLE(OI::Price, locals.priceOracleQuery, NotifyPriceOracleReply, 20000); } + if (qpi.tick() % 2 == 1) + { + locals.mockOracleQuery.value = qpi.tick(); + QUERY_ORACLE(OI::Mock, locals.mockOracleQuery, NotifyMockOracleReply, 8000); + } } //--------------------------------------------------------------- @@ -311,5 +346,6 @@ struct TESTEXC : public ContractBase REGISTER_USER_PROCEDURE(QueryPriceOracle, 100); REGISTER_USER_PROCEDURE_NOTIFICATION(NotifyPriceOracleReply); + REGISTER_USER_PROCEDURE_NOTIFICATION(NotifyMockOracleReply); } }; diff --git a/test/contract_testex.cpp b/test/contract_testex.cpp index 1ad86b6fc..8bd206441 100644 --- a/test/contract_testex.cpp +++ b/test/contract_testex.cpp @@ -2124,7 +2124,7 @@ TEST(ContractTestEx, OracleQuery) expectedOracleQueryId = getContractOracleQueryId(system.tick, 2); test.endTick(); ++system.tick; - checkNetworkMessageOracleMachineQuery(expectedOracleQueryId, id(0, 0, 0, 0), 20000); + checkNetworkMessageOracleMachineQuery(expectedOracleQueryId, OI::Price::getMockOracleId(), 20000); expectedOracleQueryId = getContractOracleQueryId(system.tick, 0); EXPECT_EQ(test.queryPriceOracle(USER1, id(2, 3, 4, 5), 13), expectedOracleQueryId); From 90b92f91ad1f10e3c0eb94b8b03676da6c48b5d1 Mon Sep 17 00:00:00 2001 From: TakaYuPP Date: Wed, 7 Jan 2026 07:06:17 -0800 Subject: [PATCH 18/90] QIP fix: bug on moving the remaining amount for the next epoch (#712) --- src/contracts/QIP.h | 20 +++++++++++++++++--- test/contract_qip.cpp | 8 ++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/contracts/QIP.h b/src/contracts/QIP.h index d583b48f4..722d94817 100644 --- a/src/contracts/QIP.h +++ b/src/contracts/QIP.h @@ -464,6 +464,21 @@ struct QIP : public ContractBase state.transferRightsFee = 100; } + struct BEGIN_EPOCH_locals + { + ICOInfo ico; + }; + + BEGIN_EPOCH_WITH_LOCALS() + { + if (qpi.epoch() == 196) + { + locals.ico = state.icos.get(0); + locals.ico.remainingAmountForPhase3 = qpi.numberOfPossessedShares(locals.ico.assetName, locals.ico.issuer, SELF, SELF, SELF_INDEX, SELF_INDEX); + state.icos.set(0, locals.ico); + } + } + struct END_EPOCH_locals { ICOInfo ico; @@ -477,20 +492,19 @@ struct QIP : public ContractBase locals.ico = state.icos.get(locals.idx); if (locals.ico.startEpoch == qpi.epoch() && locals.ico.remainingAmountForPhase1 > 0) { + locals.ico.remainingAmountForPhase2 += locals.ico.remainingAmountForPhase1; locals.ico.remainingAmountForPhase1 = 0; - locals.ico.remainingAmountForPhase2 += locals.ico.remainingAmountForPhase1; state.icos.set(locals.idx, locals.ico); } if (locals.ico.startEpoch + 1 == qpi.epoch() && locals.ico.remainingAmountForPhase2 > 0) { - locals.ico.remainingAmountForPhase2 = 0; locals.ico.remainingAmountForPhase3 += locals.ico.remainingAmountForPhase2; + locals.ico.remainingAmountForPhase2 = 0; state.icos.set(locals.idx, locals.ico); } if (locals.ico.startEpoch + 2 == qpi.epoch() && locals.ico.remainingAmountForPhase3 > 0) { qpi.transferShareOwnershipAndPossession(locals.ico.assetName, locals.ico.issuer, SELF, SELF, locals.ico.remainingAmountForPhase3, locals.ico.creatorOfICO); - locals.ico.remainingAmountForPhase3 = 0; state.icos.set(locals.idx, state.icos.get(state.numberOfICO - 1)); state.numberOfICO--; } diff --git a/test/contract_qip.cpp b/test/contract_qip.cpp index 3126a5bfd..3a667c7c9 100644 --- a/test/contract_qip.cpp +++ b/test/contract_qip.cpp @@ -1144,9 +1144,7 @@ TEST(ContractQIP, END_EPOCH_Phase1Rollover) // Check that Phase 1 remaining was set to 0 icoInfo = QIP.getICOInfo(0); EXPECT_EQ(icoInfo.remainingAmountForPhase1, 0); - // Note: Due to bug in contract (sets Phase1 to 0 before adding), Phase2 doesn't increase - // Phase2 should remain unchanged (since Phase1 was already 0 when added) - EXPECT_EQ(icoInfo.remainingAmountForPhase2, initialPhase2); + EXPECT_EQ(icoInfo.remainingAmountForPhase2, initialPhase2 + initialPhase1); } TEST(ContractQIP, END_EPOCH_Phase2Rollover) @@ -1223,9 +1221,7 @@ TEST(ContractQIP, END_EPOCH_Phase2Rollover) // Check that Phase 2 remaining was set to 0 icoInfo = QIP.getICOInfo(0); EXPECT_EQ(icoInfo.remainingAmountForPhase2, 0); - // Note: Due to bug in contract (sets Phase2 to 0 before adding), Phase3 doesn't increase - // Phase3 should remain unchanged (since Phase2 was already 0 when added) - EXPECT_EQ(icoInfo.remainingAmountForPhase3, initialPhase3); + EXPECT_EQ(icoInfo.remainingAmountForPhase3, initialPhase3 + initialPhase2); } TEST(ContractQIP, END_EPOCH_Phase3ReturnToCreator) From 507c30c3368999ae197163efa94fff90c668a8e1 Mon Sep 17 00:00:00 2001 From: mundusakhan Date: Mon, 12 Jan 2026 08:31:49 -0500 Subject: [PATCH 19/90] Add Smart Contract qRWA for QMINE ecosystem (#703) * Add qRWA SC * Add qRWA unit test * Fix payout logic * Fix typos and renames * Fix Bug - Wrong treasury update after release when the SC does not have enough funds to pay for releaseShares (to QX) * Add a comprehensive test case * Exclude the SC from the snapshot * Fix init addresses * Fix init addresses for gtest * Add get active poll functions * Add function to get general asset balances * Add function to get a list of general assets * Adapt to new DateAndTime object * Fix Pool B extra funds after revoking management rights * Use internal Asset object in qRWA * Add checks for dividend transfer * Address comments * Snapshot QMINE shares in all SC * Fix SC code compliance * Remove unused comments --- src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 3 + src/contract_core/contract_def.h | 12 + src/contracts/qRWA.h | 2015 ++++++++++++++++++++++++++++++ test/contract_qrwa.cpp | 1750 ++++++++++++++++++++++++++ test/test.vcxproj | 1 + test/test.vcxproj.filters | 1 + 7 files changed, 3783 insertions(+) create mode 100644 src/contracts/qRWA.h create mode 100644 test/contract_qrwa.cpp diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 480756ef7..78634bba1 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -24,6 +24,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 0afb6f340..2c0da5fb5 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -312,6 +312,9 @@ contract_core + + contracts + diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 12a13b5c9..44652f12d 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -201,6 +201,16 @@ #define CONTRACT_STATE2_TYPE QRAFFLE2 #include "contracts/QRaffle.h" +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QRWA_CONTRACT_INDEX 20 +#define CONTRACT_INDEX QRWA_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QRWA +#define CONTRACT_STATE2_TYPE QRWA2 +#include "contracts/qRWA.h" + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -308,6 +318,7 @@ constexpr struct ContractDescription {"QBOND", 182, 10000, sizeof(QBOND)}, // proposal in epoch 180, IPO in 181, construction and first use in 182 {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 + {"QRWA", 198, 10000, sizeof(QRWA)}, // proposal in epoch 196, IPO in 197, construction and first use in 198 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA)}, @@ -423,6 +434,7 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QBOND); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRWA); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/contracts/qRWA.h b/src/contracts/qRWA.h new file mode 100644 index 000000000..0d41020b1 --- /dev/null +++ b/src/contracts/qRWA.h @@ -0,0 +1,2015 @@ +using namespace QPI; + +/***************************************************/ +/******************* CONSTANTS *********************/ +/***************************************************/ + +constexpr uint64 QRWA_MAX_QMINE_HOLDERS = 1048576 * 2 * X_MULTIPLIER; // 2^21 +constexpr uint64 QRWA_MAX_GOV_POLLS = 64; // 8 active polls * 8 epochs = 64 slots +constexpr uint64 QRWA_MAX_ASSET_POLLS = 64; // 8 active polls * 8 epochs = 64 slots +constexpr uint64 QRWA_MAX_ASSETS = 1024; // 2^10 + +constexpr uint64 QRWA_MAX_NEW_GOV_POLLS_PER_EPOCH = 8; +constexpr uint64 QRWA_MAX_NEW_ASSET_POLLS_PER_EPOCH = 8; + +// Dividend percentage constants +constexpr uint64 QRWA_QMINE_HOLDER_PERCENT = 900; // 90.0% +constexpr uint64 QRWA_QRWA_HOLDER_PERCENT = 100; // 10.0% +constexpr uint64 QRWA_PERCENT_DENOMINATOR = 1000; // 100.0% + +// Payout Timing Constants +constexpr uint64 QRWA_PAYOUT_DAY = FRIDAY; // Friday +constexpr uint64 QRWA_PAYOUT_HOUR = 12; // 12:00 PM UTC +constexpr uint64 QRWA_MIN_PAYOUT_INTERVAL_MS = 6 * 86400000LL; // 6 days in milliseconds + +// STATUS CODES for Procedures +constexpr uint64 QRWA_STATUS_SUCCESS = 1; +constexpr uint64 QRWA_STATUS_FAILURE_GENERAL = 0; +constexpr uint64 QRWA_STATUS_FAILURE_INSUFFICIENT_FEE = 2; +constexpr uint64 QRWA_STATUS_FAILURE_INVALID_INPUT = 3; +constexpr uint64 QRWA_STATUS_FAILURE_NOT_AUTHORIZED = 4; +constexpr uint64 QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE = 5; +constexpr uint64 QRWA_STATUS_FAILURE_LIMIT_REACHED = 6; +constexpr uint64 QRWA_STATUS_FAILURE_TRANSFER_FAILED = 7; +constexpr uint64 QRWA_STATUS_FAILURE_NOT_FOUND = 8; +constexpr uint64 QRWA_STATUS_FAILURE_ALREADY_VOTED = 9; +constexpr uint64 QRWA_STATUS_FAILURE_POLL_INACTIVE = 10; +constexpr uint64 QRWA_STATUS_FAILURE_WRONG_STATE = 11; + +constexpr uint64 QRWA_POLL_STATUS_EMPTY = 0; +constexpr uint64 QRWA_POLL_STATUS_ACTIVE = 1; // poll is live, can be voted +constexpr uint64 QRWA_POLL_STATUS_PASSED_EXECUTED = 2; // poll inactive, result is YES +constexpr uint64 QRWA_POLL_STATUS_FAILED_VOTE = 3; // poll inactive, result is NO +constexpr uint64 QRWA_POLL_STATUS_PASSED_FAILED_EXECUTION = 4; // poll inactive, result is YES but failed to release asset + +// QX Fee for releasing management rights +constexpr sint64 QRWA_RELEASE_MANAGEMENT_FEE = 100; + +// LOG TYPES +constexpr uint64 QRWA_LOG_TYPE_DISTRIBUTION = 1; +constexpr uint64 QRWA_LOG_TYPE_GOV_VOTE = 2; +constexpr uint64 QRWA_LOG_TYPE_ASSET_POLL_CREATED = 3; +constexpr uint64 QRWA_LOG_TYPE_ASSET_VOTE = 4; +constexpr uint64 QRWA_LOG_TYPE_ASSET_POLL_EXECUTED = 5; +constexpr uint64 QRWA_LOG_TYPE_TREASURY_DONATION = 6; +constexpr uint64 QRWA_LOG_TYPE_ADMIN_ACTION = 7; +constexpr uint64 QRWA_LOG_TYPE_ERROR = 8; +constexpr uint64 QRWA_LOG_TYPE_INCOMING_REVENUE_A = 9; +constexpr uint64 QRWA_LOG_TYPE_INCOMING_REVENUE_B = 10; + + +/***************************************************/ +/**************** CONTRACT STATE *******************/ +/***************************************************/ + +struct QRWA : public ContractBase +{ + friend class ContractTestingQRWA; + /***************************************************/ + /******************** STRUCTS **********************/ + /***************************************************/ + + struct QRWAAsset + { + id issuer; + uint64 assetName; + + operator Asset() const + { + return { issuer, assetName }; + } + + bool operator==(const QRWAAsset other) const + { + return issuer == other.issuer && assetName == other.assetName; + } + + bool operator!=(const QRWAAsset other) const + { + return issuer != other.issuer || assetName != other.assetName; + } + + inline void setFrom(const Asset& asset) + { + issuer = asset.issuer; + assetName = asset.assetName; + } + }; + + // votable governance parameters for the contract. + struct QRWAGovParams + { + // Addresses + id mAdminAddress; // Only the admin can create release polls + // Addresses to receive the MINING FEEs + id electricityAddress; + id maintenanceAddress; + id reinvestmentAddress; + id qmineDevAddress; // Address to receive rewards for moved QMINE during epoch + + // MINING FEE Percentages + uint64 electricityPercent; + uint64 maintenancePercent; + uint64 reinvestmentPercent; + }; + + // Represents a governance poll in a rotating buffer + struct QRWAGovProposal + { + uint64 proposalId; // The unique, increasing ID + uint64 status; // 0=Empty, 1=Active, 2=Passed, 3=Failed + uint64 score; // Final score, count at END_EPOCH + QRWAGovParams params; // The actual proposal data + }; + + // Represents a poll to release assets from the treasury or dividend pool. + struct AssetReleaseProposal + { + uint64 proposalId; + id proposalName; + Asset asset; + uint64 amount; + id destination; + uint64 status; // 0=Empty, 1=Active, 2=Passed_Executed, 3=Failed, 4=Passed_Failed_Execution + uint64 votesYes; // Final score, count at END_EPOCH + uint64 votesNo; // Final score, count at END_EPOCH + }; + + // Logger for general contract events. + struct QRWALogger + { + uint64 contractId; + uint64 logType; + id primaryId; // voter, asset issuer, proposal creator + uint64 valueA; + uint64 valueB; + sint8 _terminator; + }; + +protected: + Asset mQmineAsset; + + // QMINE Shareholder Tracking + HashMap mBeginEpochBalances; + HashMap mEndEpochBalances; + uint64 mTotalQmineBeginEpoch; // Total QMINE shares at the start of the current epoch + + // PAYOUT SNAPSHOTS (for distribution) + // These hold the data from the last epoch, saved at END_EPOCH + HashMap mPayoutBeginBalances; + HashMap mPayoutEndBalances; + uint64 mPayoutTotalQmineBegin; // Total QMINE shares from the last epoch's beginning + + // Votable Parameters + QRWAGovParams mCurrentGovParams; // The live, active parameters + + // Voting state for governance parameters (voted by QMINE holders) + Array mGovPolls; + HashMap mShareholderVoteMap; // Maps QMINE holder -> Gov Poll slot index + uint64 mCurrentGovProposalId; + uint64 mNewGovPollsThisEpoch; + + // Asset Release Polls + Array mAssetPolls; + HashMap mAssetProposalVoterMap; // (Voter -> bitfield of poll slot indices) + HashMap mAssetVoteOptions; // (Voter -> bitfield of options (0=No, 1=Yes)) + uint64 mCurrentAssetProposalId; // Counter for creating new proposal ID + uint64 mNewAssetPollsThisEpoch; + + // Treasury & Asset Release + uint64 mTreasuryBalance; // QMINE token balance holds by SC + HashMap mGeneralAssetBalances; // Balances for other assets (e.g., SC shares) + + // Payouts and Dividend Accounting + DateAndTime mLastPayoutTime; // Tracks the last payout time + + // Dividend Pools + uint64 mRevenuePoolA; // Mined funds from Qubic farm (from SCs) + uint64 mRevenuePoolB; // Other dividend funds (from user wallets) + + // Processed dividend pools awaiting distribution + uint64 mQmineDividendPool; // QUs for QMINE holders + uint64 mQRWADividendPool; // QUs for qRWA shareholders + + // Total distributed tracking + uint64 mTotalQmineDistributed; + uint64 mTotalQRWADistributed; + +public: + /***************************************************/ + /**************** PUBLIC PROCEDURES ****************/ + /***************************************************/ + + // Treasury + struct DonateToTreasury_input + { + uint64 amount; + }; + struct DonateToTreasury_output + { + uint64 status; + }; + struct DonateToTreasury_locals + { + sint64 transferResult; + sint64 balance; + QRWALogger logger; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(DonateToTreasury) + { + // NOTE: This procedure transfers QMINE from the invoker's *managed* balance (managed by this SC) + // to the SC's internal treasury. + // A one-time setup by the donor is required: + // 1. Call QX::TransferShareManagementRights to give this SC management rights over the QMINE. + // 2. Call this DonateToTreasury procedure to transfer ownership to the SC. + + // This procedure has no fee, refund any invocation reward + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.status = QRWA_STATUS_FAILURE_GENERAL; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_TREASURY_DONATION; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = input.amount; + + if (state.mQmineAsset.issuer == NULL_ID) + { + output.status = QRWA_STATUS_FAILURE_WRONG_STATE; // QMINE asset not set + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + if (input.amount == 0) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Check if user has granted management rights to this SC + locals.balance = qpi.numberOfShares(state.mQmineAsset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX }); + + if (locals.balance < static_cast(input.amount)) + { + output.status = QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE; // Not enough managed shares + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Transfer QMINE from invoker (managed by SELF) to SELF (owned by SELF) + locals.transferResult = qpi.transferShareOwnershipAndPossession( + state.mQmineAsset.assetName, + state.mQmineAsset.issuer, + qpi.invocator(), // current owner + qpi.invocator(), // current possessor + input.amount, + SELF // new owner and possessor + ); + + if (locals.transferResult >= 0) // Transfer successful + { + state.mTreasuryBalance = sadd(state.mTreasuryBalance, input.amount); + output.status = QRWA_STATUS_SUCCESS; + } + else + { + output.status = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + } + + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + } + + // Governance: Param Voting + struct VoteGovParams_input + { + QRWAGovParams proposal; + }; + struct VoteGovParams_output + { + uint64 status; + }; + struct VoteGovParams_locals + { + uint64 currentBalance; + uint64 i; + uint64 foundProposal; + uint64 proposalIndex; + QRWALogger logger; + QRWAGovProposal poll; + sint64 rawBalance; + QRWAGovParams existing; + uint64 status; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(VoteGovParams) + { + output.status = QRWA_STATUS_FAILURE_GENERAL; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_GOV_VOTE; + locals.logger.primaryId = qpi.invocator(); + + // Get voter's current QMINE balance + locals.rawBalance = qpi.numberOfShares(state.mQmineAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), AssetPossessionSelect::byPossessor(qpi.invocator())); + locals.currentBalance = (locals.rawBalance > 0) ? static_cast(locals.rawBalance) : 0; + + if (locals.currentBalance <= 0) + { + output.status = QRWA_STATUS_FAILURE_NOT_AUTHORIZED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Validate proposal percentages + if (sadd(sadd(input.proposal.electricityPercent, input.proposal.maintenancePercent), input.proposal.reinvestmentPercent) > QRWA_PERCENT_DENOMINATOR) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + if (input.proposal.mAdminAddress == NULL_ID) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Now process the new/updated vote + locals.foundProposal = 0; + locals.proposalIndex = NULL_INDEX; + + // Check if the current proposal matches any existing unique proposal + for (locals.i = 0; locals.i < QRWA_MAX_GOV_POLLS; locals.i++) + { + locals.existing = state.mGovPolls.get(locals.i).params; + locals.status = state.mGovPolls.get(locals.i).status; + + if (locals.status == QRWA_POLL_STATUS_ACTIVE && + locals.existing.electricityAddress == input.proposal.electricityAddress && + locals.existing.maintenanceAddress == input.proposal.maintenanceAddress && + locals.existing.reinvestmentAddress == input.proposal.reinvestmentAddress && + locals.existing.qmineDevAddress == input.proposal.qmineDevAddress && + locals.existing.mAdminAddress == input.proposal.mAdminAddress && + locals.existing.electricityPercent == input.proposal.electricityPercent && + locals.existing.maintenancePercent == input.proposal.maintenancePercent && + locals.existing.reinvestmentPercent == input.proposal.reinvestmentPercent) + { + locals.foundProposal = 1; + locals.proposalIndex = locals.i; // This is the proposal we are voting for + break; + } + } + + // If proposal not found, create it in a new slot + if (locals.foundProposal == 0) + { + if (state.mNewGovPollsThisEpoch >= QRWA_MAX_NEW_GOV_POLLS_PER_EPOCH) + { + output.status = QRWA_STATUS_FAILURE_LIMIT_REACHED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.proposalIndex = mod(state.mCurrentGovProposalId, QRWA_MAX_GOV_POLLS); + + // Clear old data at this slot + locals.poll = state.mGovPolls.get(locals.proposalIndex); + locals.poll.proposalId = state.mCurrentGovProposalId; + locals.poll.params = input.proposal; + locals.poll.score = 0; // Will be count at END_EPOCH + locals.poll.status = QRWA_POLL_STATUS_ACTIVE; + + state.mGovPolls.set(locals.proposalIndex, locals.poll); + + state.mCurrentGovProposalId++; + state.mNewGovPollsThisEpoch++; + } + + state.mShareholderVoteMap.set(qpi.invocator(), locals.proposalIndex); + output.status = QRWA_STATUS_SUCCESS; + + locals.logger.valueA = locals.proposalIndex; // Log the index voted for/added + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + + + // Governance: Asset Release + struct CreateAssetReleasePoll_input + { + id proposalName; + Asset asset; + uint64 amount; + id destination; + }; + struct CreateAssetReleasePoll_output + { + uint64 status; + uint64 proposalId; + }; + struct CreateAssetReleasePoll_locals + { + uint64 newPollIndex; + AssetReleaseProposal newPoll; + QRWALogger logger; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(CreateAssetReleasePoll) + { + output.status = QRWA_STATUS_FAILURE_GENERAL; + output.proposalId = -1; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ASSET_POLL_CREATED; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = 0; + + if (qpi.invocator() != state.mCurrentGovParams.mAdminAddress) + { + output.status = QRWA_STATUS_FAILURE_NOT_AUTHORIZED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Check poll limit + if (state.mNewAssetPollsThisEpoch >= QRWA_MAX_NEW_ASSET_POLLS_PER_EPOCH) + { + output.status = QRWA_STATUS_FAILURE_LIMIT_REACHED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + if (input.amount == 0 || input.destination == NULL_ID || input.asset.issuer == NULL_ID) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.newPollIndex = mod(state.mCurrentAssetProposalId, QRWA_MAX_ASSET_POLLS); + + // Create and store the new poll, overwriting the oldest one + locals.newPoll.proposalId = state.mCurrentAssetProposalId; + locals.newPoll.proposalName = input.proposalName; + locals.newPoll.asset = input.asset; + locals.newPoll.amount = input.amount; + locals.newPoll.destination = input.destination; + locals.newPoll.status = QRWA_POLL_STATUS_ACTIVE; + locals.newPoll.votesYes = 0; + locals.newPoll.votesNo = 0; + + state.mAssetPolls.set(locals.newPollIndex, locals.newPoll); + + output.proposalId = state.mCurrentAssetProposalId; + output.status = QRWA_STATUS_SUCCESS; + state.mCurrentAssetProposalId++; // Increment for the next proposal + state.mNewAssetPollsThisEpoch++; + + locals.logger.valueA = output.proposalId; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + + + struct VoteAssetRelease_input + { + uint64 proposalId; + uint64 option; /* 0=No, 1=Yes */ + }; + struct VoteAssetRelease_output + { + uint64 status; + }; + struct VoteAssetRelease_locals + { + uint64 currentBalance; + AssetReleaseProposal poll; + uint64 pollIndex; + QRWALogger logger; + uint64 foundPoll; + bit_64 voterBitfield; + bit_64 voterOptions; + sint64 rawBalance; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(VoteAssetRelease) + { + output.status = QRWA_STATUS_FAILURE_GENERAL; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ASSET_VOTE; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = input.proposalId; + locals.logger.valueB = input.option; + + if (input.option > 1) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; // Overwrite valueB with status + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Get voter's current QMINE balance + locals.rawBalance = qpi.numberOfShares(state.mQmineAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), AssetPossessionSelect::byPossessor(qpi.invocator())); + locals.currentBalance = (locals.rawBalance > 0) ? static_cast(locals.rawBalance) : 0; + + + if (locals.currentBalance <= 0) + { + output.status = QRWA_STATUS_FAILURE_NOT_AUTHORIZED; // Not a QMINE holder or balance is zero + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Find the poll + locals.pollIndex = mod(input.proposalId, QRWA_MAX_ASSET_POLLS); + locals.poll = state.mAssetPolls.get(locals.pollIndex); + + if (locals.poll.proposalId != input.proposalId) + { + locals.foundPoll = 0; + } + else { + locals.foundPoll = 1; + } + + if (locals.foundPoll == 0) + { + output.status = QRWA_STATUS_FAILURE_NOT_FOUND; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + if (locals.poll.status != QRWA_POLL_STATUS_ACTIVE) // Check if poll is active + { + output.status = QRWA_STATUS_FAILURE_POLL_INACTIVE; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Now process the new vote + state.mAssetProposalVoterMap.get(qpi.invocator(), locals.voterBitfield); // Get or default (all 0s) + state.mAssetVoteOptions.get(qpi.invocator(), locals.voterOptions); + + // Record vote + locals.voterBitfield.set(locals.pollIndex, 1); + locals.voterOptions.set(locals.pollIndex, (input.option == 1) ? 1 : 0); + + // Update voter's maps + state.mAssetProposalVoterMap.set(qpi.invocator(), locals.voterBitfield); + state.mAssetVoteOptions.set(qpi.invocator(), locals.voterOptions); + + output.status = QRWA_STATUS_SUCCESS; + locals.logger.valueB = output.status; // Log final status + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + + // deposit general assets + struct DepositGeneralAsset_input + { + Asset asset; + uint64 amount; + }; + struct DepositGeneralAsset_output + { + uint64 status; + }; + struct DepositGeneralAsset_locals + { + sint64 transferResult; + sint64 balance; + uint64 currentAssetBalance; + QRWALogger logger; + QRWAAsset wrapper; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(DepositGeneralAsset) + { + // This procedure has no fee + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.status = QRWA_STATUS_FAILURE_GENERAL; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ADMIN_ACTION; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = input.asset.assetName; + + if (qpi.invocator() != state.mCurrentGovParams.mAdminAddress) + { + output.status = QRWA_STATUS_FAILURE_NOT_AUTHORIZED; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + if (input.amount == 0 || input.asset.issuer == NULL_ID) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Check if admin has granted management rights to this SC + locals.balance = qpi.numberOfShares(input.asset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX }); + + if (locals.balance < static_cast(input.amount)) + { + output.status = QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE; // Not enough managed shares + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Transfer asset from admin (managed by SELF) to SELF (owned by SELF) + locals.transferResult = qpi.transferShareOwnershipAndPossession( + input.asset.assetName, + input.asset.issuer, + qpi.invocator(), // current owner + qpi.invocator(), // current possessor + input.amount, + SELF // new owner and possessor + ); + + if (locals.transferResult >= 0) // Transfer successful + { + locals.wrapper.setFrom(input.asset); + state.mGeneralAssetBalances.get(locals.wrapper, locals.currentAssetBalance); // 0 if not exist + locals.currentAssetBalance = sadd(locals.currentAssetBalance, input.amount); + state.mGeneralAssetBalances.set(locals.wrapper, locals.currentAssetBalance); + output.status = QRWA_STATUS_SUCCESS; + } + else + { + output.status = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + } + + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + } + + struct RevokeAssetManagementRights_input + { + Asset asset; + sint64 numberOfShares; + }; + struct RevokeAssetManagementRights_output + { + sint64 transferredNumberOfShares; + uint64 status; + }; + struct RevokeAssetManagementRights_locals + { + QRWALogger logger; + sint64 managedBalance; + sint64 result; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(RevokeAssetManagementRights) + { + // This procedure allows a user to revoke asset management rights from qRWA + // and transfer them back to QX, which is the default manager for trading + // Ref: MSVAULT + + output.status = QRWA_STATUS_FAILURE_GENERAL; + output.transferredNumberOfShares = 0; + + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = input.asset.assetName; + locals.logger.valueB = input.numberOfShares; + + if (qpi.invocationReward() < (sint64)QRWA_RELEASE_MANAGEMENT_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.transferredNumberOfShares = 0; + output.status = QRWA_STATUS_FAILURE_INSUFFICIENT_FEE; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)QRWA_RELEASE_MANAGEMENT_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)QRWA_RELEASE_MANAGEMENT_FEE); + } + + // must transfer a positive number of shares. + if (input.numberOfShares <= 0) + { + // Refund the fee if params are invalid + qpi.transfer(qpi.invocator(), (sint64)QRWA_RELEASE_MANAGEMENT_FEE); + output.transferredNumberOfShares = 0; + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Check if qRWA actually manages the specified number of shares for the caller. + locals.managedBalance = qpi.numberOfShares( + input.asset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX } + ); + + if (locals.managedBalance < input.numberOfShares) + { + // The user is trying to revoke more shares than are managed by qRWA. + qpi.transfer(qpi.invocator(), (sint64)QRWA_RELEASE_MANAGEMENT_FEE); + output.transferredNumberOfShares = 0; + output.status = QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + } + else + { + // The balance check passed. Proceed to release the management rights to QX. + locals.result = qpi.releaseShares( + input.asset, + qpi.invocator(), // owner + qpi.invocator(), // possessor + input.numberOfShares, + QX_CONTRACT_INDEX, // destination ownership managing contract + QX_CONTRACT_INDEX, // destination possession managing contract + QRWA_RELEASE_MANAGEMENT_FEE // offered fee to QX + ); + + if (locals.result < 0) + { + // Transfer failed + output.transferredNumberOfShares = 0; + output.status = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + } + else + { + // Success, the fee was spent. + output.transferredNumberOfShares = input.numberOfShares; + output.status = QRWA_STATUS_SUCCESS; + locals.logger.logType = QRWA_LOG_TYPE_ADMIN_ACTION; // Or a more specific type + + // Since the invocation reward (100 QU) was added to mRevenuePoolB + // via POST_INCOMING_TRANSFER, but we just spent it in releaseShares, + // we must remove it from the pool to keep the accountant in sync + // with the actual balance. + if (state.mRevenuePoolB >= QRWA_RELEASE_MANAGEMENT_FEE) + { + state.mRevenuePoolB -= QRWA_RELEASE_MANAGEMENT_FEE; + } + } + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + } + } + + /***************************************************/ + /***************** PUBLIC FUNCTIONS ****************/ + /***************************************************/ + + // Governance: Param Voting + struct GetGovParams_input {}; + struct GetGovParams_output + { + QRWAGovParams params; + }; + PUBLIC_FUNCTION(GetGovParams) + { + output.params = state.mCurrentGovParams; + } + + struct GetGovPoll_input + { + uint64 proposalId; + }; + struct GetGovPoll_output + { + QRWAGovProposal proposal; + uint64 status; // 0=NotFound, 1=Found + }; + struct GetGovPoll_locals + { + uint64 pollIndex; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetGovPoll) + { + output.status = QRWA_STATUS_FAILURE_NOT_FOUND; + + locals.pollIndex = mod(input.proposalId, QRWA_MAX_GOV_POLLS); + output.proposal = state.mGovPolls.get(locals.pollIndex); + + if (output.proposal.proposalId == input.proposalId) + { + output.status = QRWA_STATUS_SUCCESS; + } + else + { + // Clear output if not the poll we're looking for + setMemory(output.proposal, 0); + } + } + + // Governance: Asset Release + struct GetAssetReleasePoll_input + { + uint64 proposalId; + }; + struct GetAssetReleasePoll_output + { + AssetReleaseProposal proposal; + uint64 status; // 0=NotFound, 1=Found + }; + struct GetAssetReleasePoll_locals + { + uint64 pollIndex; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetAssetReleasePoll) + { + output.status = QRWA_STATUS_FAILURE_NOT_FOUND; + + locals.pollIndex = mod(input.proposalId, QRWA_MAX_ASSET_POLLS); + output.proposal = state.mAssetPolls.get(locals.pollIndex); + + if (output.proposal.proposalId == input.proposalId) + { + output.status = QRWA_STATUS_SUCCESS; + } + else + { + // Clear output if not the poll we're looking for + setMemory(output.proposal, 0); + } + } + + // Balances & Info + struct GetTreasuryBalance_input {}; + struct GetTreasuryBalance_output + { + uint64 balance; + }; + PUBLIC_FUNCTION(GetTreasuryBalance) + { + output.balance = state.mTreasuryBalance; + } + + struct GetDividendBalances_input {}; + struct GetDividendBalances_output + { + uint64 revenuePoolA; + uint64 revenuePoolB; + uint64 qmineDividendPool; + uint64 qrwaDividendPool; + }; + PUBLIC_FUNCTION(GetDividendBalances) + { + output.revenuePoolA = state.mRevenuePoolA; + output.revenuePoolB = state.mRevenuePoolB; + output.qmineDividendPool = state.mQmineDividendPool; + output.qrwaDividendPool = state.mQRWADividendPool; + } + + struct GetTotalDistributed_input {}; + struct GetTotalDistributed_output + { + uint64 totalQmineDistributed; + uint64 totalQRWADistributed; + }; + PUBLIC_FUNCTION(GetTotalDistributed) + { + output.totalQmineDistributed = state.mTotalQmineDistributed; + output.totalQRWADistributed = state.mTotalQRWADistributed; + } + + struct GetActiveAssetReleasePollIds_input {}; + + struct GetActiveAssetReleasePollIds_output + { + uint64 count; + Array ids; + }; + + struct GetActiveAssetReleasePollIds_locals + { + uint64 i; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetActiveAssetReleasePollIds) + { + output.count = 0; + for (locals.i = 0; locals.i < QRWA_MAX_ASSET_POLLS; locals.i++) + { + if (state.mAssetPolls.get(locals.i).status == QRWA_POLL_STATUS_ACTIVE) + { + output.ids.set(output.count, state.mAssetPolls.get(locals.i).proposalId); + output.count++; + } + } + } + + struct GetActiveGovPollIds_input {}; + struct GetActiveGovPollIds_output + { + uint64 count; + Array ids; + }; + struct GetActiveGovPollIds_locals + { + uint64 i; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetActiveGovPollIds) + { + output.count = 0; + for (locals.i = 0; locals.i < QRWA_MAX_GOV_POLLS; locals.i++) + { + if (state.mGovPolls.get(locals.i).status == QRWA_POLL_STATUS_ACTIVE) + { + output.ids.set(output.count, state.mGovPolls.get(locals.i).proposalId); + output.count++; + } + } + } + + struct GetGeneralAssetBalance_input + { + Asset asset; + }; + struct GetGeneralAssetBalance_output + { + uint64 balance; + uint64 status; + }; + struct GetGeneralAssetBalance_locals + { + uint64 balance; + QRWAAsset wrapper; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetGeneralAssetBalance) { + locals.balance = 0; + locals.wrapper.setFrom(input.asset); + if (state.mGeneralAssetBalances.get(locals.wrapper, locals.balance)) { + output.balance = locals.balance; + output.status = 1; + } + else { + output.balance = 0; + output.status = 0; + } + } + + struct GetGeneralAssets_input {}; + struct GetGeneralAssets_output + { + uint64 count; + Array assets; + Array balances; + }; + struct GetGeneralAssets_locals + { + sint64 iterIndex; + QRWAAsset currentAsset; + uint64 currentBalance; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetGeneralAssets) + { + output.count = 0; + locals.iterIndex = NULL_INDEX; + + while (true) + { + locals.iterIndex = state.mGeneralAssetBalances.nextElementIndex(locals.iterIndex); + + if (locals.iterIndex == NULL_INDEX) + { + break; + } + + locals.currentAsset = state.mGeneralAssetBalances.key(locals.iterIndex); + locals.currentBalance = state.mGeneralAssetBalances.value(locals.iterIndex); + + // Only return "active" assets (balance > 0) + if (locals.currentBalance > 0) + { + output.assets.set(output.count, locals.currentAsset); + output.balances.set(output.count, locals.currentBalance); + output.count++; + + if (output.count >= QRWA_MAX_ASSETS) + { + break; + } + } + } + } + + /***************************************************/ + /***************** SYSTEM PROCEDURES ***************/ + /***************************************************/ + + INITIALIZE() + { + // QMINE Asset Constant + // Issuer: QMINEQQXYBEGBHNSUPOUYDIQKZPCBPQIIHUUZMCPLBPCCAIARVZBTYKGFCWM + // Name: 297666170193 ("QMINE") + state.mQmineAsset.assetName = 297666170193ULL; + state.mQmineAsset.issuer = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); + state.mTreasuryBalance = 0; + state.mCurrentAssetProposalId = 0; + setMemory(state.mLastPayoutTime, 0); + + // Initialize default governance parameters + state.mCurrentGovParams.mAdminAddress = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); // Admin set to QMINE Issuer by default, subject to change via Gov Voting + state.mCurrentGovParams.electricityAddress = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); // Electricity address set to QMINE Issuer by default, subject to change via Gov Voting + state.mCurrentGovParams.maintenanceAddress = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); // Maintenance address set to QMINE Issuer by default, subject to change via Gov Voting + state.mCurrentGovParams.reinvestmentAddress = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); // Reinvestment address set to QMINE Issuer by default, subject to change via Gov Voting + + // QMINE DEV's Address for receiving rewards from moved QMINE tokens + // ZOXXIDCZIMGCECCFAXDDCMBBXCDAQJIHGOOATAFPSBFIOFOYECFKUFPBEMWC + state.mCurrentGovParams.qmineDevAddress = ID( + _Z, _O, _X, _X, _I, _D, _C, _Z, _I, _M, _G, _C, _E, _C, _C, _F, + _A, _X, _D, _D, _C, _M, _B, _B, _X, _C, _D, _A, _Q, _J, _I, _H, + _G, _O, _O, _A, _T, _A, _F, _P, _S, _B, _F, _I, _O, _F, _O, _Y, + _E, _C, _F, _K, _U, _F, _P, _B + ); // Default QMINE_DEV address + state.mCurrentGovParams.electricityPercent = 350; + state.mCurrentGovParams.maintenancePercent = 50; + state.mCurrentGovParams.reinvestmentPercent = 100; + + state.mCurrentGovProposalId = 0; + state.mCurrentAssetProposalId = 0; + state.mNewGovPollsThisEpoch = 0; + state.mNewAssetPollsThisEpoch = 0; + + // Initialize revenue pools + state.mRevenuePoolA = 0; + state.mRevenuePoolB = 0; + state.mQmineDividendPool = 0; + state.mQRWADividendPool = 0; + + // Initialize total distributed + state.mTotalQmineDistributed = 0; + state.mTotalQRWADistributed = 0; + + // Initialize maps/arrays + state.mBeginEpochBalances.reset(); + state.mEndEpochBalances.reset(); + state.mPayoutBeginBalances.reset(); + state.mPayoutEndBalances.reset(); + state.mGeneralAssetBalances.reset(); + state.mShareholderVoteMap.reset(); + state.mAssetProposalVoterMap.reset(); + state.mAssetVoteOptions.reset(); + + setMemory(state.mAssetPolls, 0); + setMemory(state.mGovPolls, 0); + } + + struct BEGIN_EPOCH_locals + { + AssetPossessionIterator iter; + uint64 balance; + QRWALogger logger; + id holder; + uint64 existingBalance; + }; + BEGIN_EPOCH_WITH_LOCALS() + { + // Reset new poll counters + state.mNewGovPollsThisEpoch = 0; + state.mNewAssetPollsThisEpoch = 0; + + state.mEndEpochBalances.reset(); + + // Take snapshot of begin balances for QMINE holders + state.mBeginEpochBalances.reset(); + state.mTotalQmineBeginEpoch = 0; + + if (state.mQmineAsset.issuer != NULL_ID) + { + for (locals.iter.begin(state.mQmineAsset); !locals.iter.reachedEnd(); locals.iter.next()) + { + // Exclude SELF (Treasury) from dividend snapshot + if (locals.iter.possessor() == SELF) + { + continue; + } + locals.balance = locals.iter.numberOfPossessedShares(); + locals.holder = locals.iter.possessor(); + + if (locals.balance > 0) + { + // Check if holder already exists in the map (e.g. from a different manager) + // If so, add to existing balance. + locals.existingBalance = 0; + state.mBeginEpochBalances.get(locals.holder, locals.existingBalance); + + locals.balance = sadd(locals.existingBalance, locals.balance); + + if (state.mBeginEpochBalances.set(locals.holder, locals.balance) != NULL_INDEX) + { + state.mTotalQmineBeginEpoch = sadd(state.mTotalQmineBeginEpoch, (uint64)locals.iter.numberOfPossessedShares()); + } + else + { + // Log error - Max holders reached for snapshot + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = locals.holder; + locals.logger.valueA = 11; // Error code: Begin Epoch Snapshot full + locals.logger.valueB = state.mBeginEpochBalances.population(); + LOG_INFO(locals.logger); + } + } + } + } + } + + struct END_EPOCH_locals + { + AssetPossessionIterator iter; + uint64 balance; + + sint64 iterIndex; + id iterVoter; + uint64 beginBalance; + uint64 endBalance; + uint64 votingPower; + uint64 proposalIndex; + uint64 currentScore; + bit_64 voterBitfield; + bit_64 voterOptions; + uint64 i_asset; + + // Gov Params Voting + uint64 i; + uint64 topScore; + uint64 topProposalIndex; + uint64 totalQminePower; + Array govPollScores; + uint64 govPassed; + uint64 quorumThreshold; + + // Asset Release Voting + uint64 pollIndex; + AssetReleaseProposal poll; + uint64 yesVotes; + uint64 noVotes; + uint64 totalVotes; + uint64 transferSuccess; + sint64 transferResult; + uint64 currentAssetBalance; + Array assetPollVotesYes; + Array assetPollVotesNo; + uint64 assetPassed; + + sint64 releaseFeeResult; // For releaseShares fee + + uint64 ownershipTransferred; + uint64 managementTransferred; + uint64 feePaid; + uint64 sufficientFunds; + + QRWALogger logger; + uint64 epoch; + + sint64 copyIndex; + id copyHolder; + uint64 copyBalance; + + QRWAAsset wrapper; + + QRWAGovProposal govPoll; + + id holder; + uint64 existingBalance; + }; + END_EPOCH_WITH_LOCALS() + { + locals.epoch = qpi.epoch(); // Get current epoch for history records + + // Take snapshot of end balances for QMINE holders + if (state.mQmineAsset.issuer != NULL_ID) + { + for (locals.iter.begin(state.mQmineAsset); !locals.iter.reachedEnd(); locals.iter.next()) + { + // Exclude SELF (Treasury) from dividend snapshot + if (locals.iter.possessor() == SELF) + { + continue; + } + locals.balance = locals.iter.numberOfPossessedShares(); + locals.holder = locals.iter.possessor(); + + if (locals.balance > 0) + { + // Check if holder already exists (multiple SC management) + locals.existingBalance = 0; + state.mEndEpochBalances.get(locals.holder, locals.existingBalance); + + locals.balance = sadd(locals.existingBalance, locals.balance); + + if (state.mEndEpochBalances.set(locals.holder, locals.balance) == NULL_INDEX) + { + // Log error - Max holders reached for snapshot + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = locals.holder; + locals.logger.valueA = 12; // Error code: End Epoch Snapshot full + locals.logger.valueB = state.mEndEpochBalances.population(); + LOG_INFO(locals.logger); + } + } + } + } + + // Process Governance Parameter Voting (voted by QMINE holders) + // Recount all votes from scratch using snapshots + + locals.totalQminePower = state.mTotalQmineBeginEpoch; + locals.govPollScores.setAll(0); // Reset scores to zero. + + locals.iterIndex = NULL_INDEX; // Iterate all voters + while (true) + { + locals.iterIndex = state.mShareholderVoteMap.nextElementIndex(locals.iterIndex); + if (locals.iterIndex == NULL_INDEX) + { + break; + } + + locals.iterVoter = state.mShareholderVoteMap.key(locals.iterIndex); + + // Get true voting power from snapshots + locals.beginBalance = 0; + locals.endBalance = 0; + state.mBeginEpochBalances.get(locals.iterVoter, locals.beginBalance); + state.mEndEpochBalances.get(locals.iterVoter, locals.endBalance); + + locals.votingPower = (locals.beginBalance < locals.endBalance) ? locals.beginBalance : locals.endBalance; // min(begin, end) + + if (locals.votingPower > 0) // Apply voting power + { + state.mShareholderVoteMap.get(locals.iterVoter, locals.proposalIndex); + if (locals.proposalIndex < QRWA_MAX_GOV_POLLS) + { + if (state.mGovPolls.get(locals.proposalIndex).status == QRWA_POLL_STATUS_ACTIVE) + { + locals.currentScore = locals.govPollScores.get(locals.proposalIndex); + locals.govPollScores.set(locals.proposalIndex, sadd(locals.currentScore, locals.votingPower)); + } + } + } + } + + // Find the winning proposal (max votes) + locals.topScore = 0; + locals.topProposalIndex = NULL_INDEX; + + for (locals.i = 0; locals.i < QRWA_MAX_GOV_POLLS; locals.i++) + { + if (state.mGovPolls.get(locals.i).status == QRWA_POLL_STATUS_ACTIVE) + { + locals.currentScore = locals.govPollScores.get(locals.i); + if (locals.currentScore > locals.topScore) + { + locals.topScore = locals.currentScore; + locals.topProposalIndex = locals.i; + } + } + } + + // Calculate 2/3 quorum threshold + locals.quorumThreshold = 0; + if (locals.totalQminePower > 0) + { + locals.quorumThreshold = div(sadd(smul(locals.totalQminePower, 2ULL), 2ULL), 3ULL); + } + + // Finalize Gov Vote (check against 2/3 quorum) + locals.govPassed = 0; + if (locals.topScore >= locals.quorumThreshold && locals.topProposalIndex != NULL_INDEX) + { + // Proposal passes + locals.govPassed = 1; + state.mCurrentGovParams = state.mGovPolls.get(locals.topProposalIndex).params; + + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_GOV_VOTE; + locals.logger.primaryId = NULL_ID; // System event + locals.logger.valueA = state.mGovPolls.get(locals.topProposalIndex).proposalId; + locals.logger.valueB = QRWA_STATUS_SUCCESS; // Indicate params updated + LOG_INFO(locals.logger); + } + + // Update status for all active gov polls (for history) + for (locals.i = 0; locals.i < QRWA_MAX_GOV_POLLS; locals.i++) + { + locals.govPoll = state.mGovPolls.get(locals.i); + if (locals.govPoll.status == QRWA_POLL_STATUS_ACTIVE) + { + locals.govPoll.score = locals.govPollScores.get(locals.i); + if (locals.govPassed == 1 && locals.i == locals.topProposalIndex) + { + locals.govPoll.status = QRWA_POLL_STATUS_PASSED_EXECUTED; + } + else + { + locals.govPoll.status = QRWA_POLL_STATUS_FAILED_VOTE; + } + state.mGovPolls.set(locals.i, locals.govPoll); + } + } + + // Reset governance voter map for the next epoch + state.mShareholderVoteMap.reset(); + + // --- Process Asset Release Voting --- + locals.assetPollVotesYes.setAll(0); + locals.assetPollVotesNo.setAll(0); + + locals.iterIndex = NULL_INDEX; + while (true) + { + locals.iterIndex = state.mAssetProposalVoterMap.nextElementIndex(locals.iterIndex); + if (locals.iterIndex == NULL_INDEX) + { + break; + } + + locals.iterVoter = state.mAssetProposalVoterMap.key(locals.iterIndex); + + // Get true voting power + locals.beginBalance = 0; + locals.endBalance = 0; + state.mBeginEpochBalances.get(locals.iterVoter, locals.beginBalance); + state.mEndEpochBalances.get(locals.iterVoter, locals.endBalance); + locals.votingPower = (locals.beginBalance < locals.endBalance) ? locals.beginBalance : locals.endBalance; // min(begin, end) + + if (locals.votingPower > 0) // Apply voting power + { + state.mAssetProposalVoterMap.get(locals.iterVoter, locals.voterBitfield); + state.mAssetVoteOptions.get(locals.iterVoter, locals.voterOptions); + + for (locals.i_asset = 0; locals.i_asset < QRWA_MAX_ASSET_POLLS; locals.i_asset++) + { + if (state.mAssetPolls.get(locals.i_asset).status == QRWA_POLL_STATUS_ACTIVE && + locals.voterBitfield.get(locals.i_asset) == 1) + { + if (locals.voterOptions.get(locals.i_asset) == 1) // Voted Yes + { + locals.yesVotes = locals.assetPollVotesYes.get(locals.i_asset); + locals.assetPollVotesYes.set(locals.i_asset, sadd(locals.yesVotes, locals.votingPower)); + } + else // Voted No + { + locals.noVotes = locals.assetPollVotesNo.get(locals.i_asset); + locals.assetPollVotesNo.set(locals.i_asset, sadd(locals.noVotes, locals.votingPower)); + } + } + } + } + } + + // Finalize Asset Polls + for (locals.pollIndex = 0; locals.pollIndex < QRWA_MAX_ASSET_POLLS; ++locals.pollIndex) + { + locals.poll = state.mAssetPolls.get(locals.pollIndex); + + if (locals.poll.status == QRWA_POLL_STATUS_ACTIVE) + { + locals.yesVotes = locals.assetPollVotesYes.get(locals.pollIndex); + locals.noVotes = locals.assetPollVotesNo.get(locals.pollIndex); + + // Store final scores in the poll struct + locals.poll.votesYes = locals.yesVotes; + locals.poll.votesNo = locals.noVotes; + + locals.ownershipTransferred = 0; + locals.managementTransferred = 0; + locals.feePaid = 0; + locals.sufficientFunds = 0; + + + if (locals.yesVotes >= locals.quorumThreshold) // YES wins + { + // Check if asset is QMINE treasury + if (locals.poll.asset.issuer == state.mQmineAsset.issuer && locals.poll.asset.assetName == state.mQmineAsset.assetName) + { + // Release from treasury + if (state.mTreasuryBalance >= locals.poll.amount) + { + locals.sufficientFunds = 1; + locals.transferResult = qpi.transferShareOwnershipAndPossession( + state.mQmineAsset.assetName, state.mQmineAsset.issuer, + SELF, SELF, locals.poll.amount, locals.poll.destination); + + if (locals.transferResult >= 0) // ownership transfer succeeded + { + locals.ownershipTransferred = 1; + // Decrement internal balance + state.mTreasuryBalance = (state.mTreasuryBalance > locals.poll.amount) ? (state.mTreasuryBalance - locals.poll.amount) : 0; + + if (state.mRevenuePoolA >= QRWA_RELEASE_MANAGEMENT_FEE) + { + // Release management rights from this SC to QX + locals.releaseFeeResult = qpi.releaseShares( + locals.poll.asset, + locals.poll.destination, // new owner + locals.poll.destination, // new possessor + locals.poll.amount, + QX_CONTRACT_INDEX, + QX_CONTRACT_INDEX, + QRWA_RELEASE_MANAGEMENT_FEE + ); + + if (locals.releaseFeeResult >= 0) // management transfer succeeded + { + locals.managementTransferred = 1; + locals.feePaid = 1; + state.mRevenuePoolA = (state.mRevenuePoolA > (uint64)locals.releaseFeeResult) ? (state.mRevenuePoolA - (uint64)locals.releaseFeeResult) : 0; + } + // else: Management transfer failed (shares are "stuck"). + // The destination ID still owns the transferred asset, but the SC management is currently under qRWA. + // The destination ID must use revokeAssetManagementRights to transfer the asset's SC management to QX + } + } + } + } + else // Asset is from mGeneralAssetBalances + { + locals.wrapper.setFrom(locals.poll.asset); + if (state.mGeneralAssetBalances.get(locals.wrapper, locals.currentAssetBalance)) + { + if (locals.currentAssetBalance >= locals.poll.amount) + { + locals.sufficientFunds = 1; + // Ownership Transfer + locals.transferResult = qpi.transferShareOwnershipAndPossession( + locals.poll.asset.assetName, locals.poll.asset.issuer, + SELF, SELF, locals.poll.amount, locals.poll.destination); + + if (locals.transferResult >= 0) // Ownership transfer tucceeded + { + locals.ownershipTransferred = 1; + // Decrement internal balance + locals.currentAssetBalance = (locals.currentAssetBalance > locals.poll.amount) ? (locals.currentAssetBalance - locals.poll.amount) : 0; + state.mGeneralAssetBalances.set(locals.wrapper, locals.currentAssetBalance); + + if (state.mRevenuePoolA >= QRWA_RELEASE_MANAGEMENT_FEE) + { + // Management Transfer + locals.releaseFeeResult = qpi.releaseShares( + locals.poll.asset, + locals.poll.destination, // new owner + locals.poll.destination, // new possessor + locals.poll.amount, + QX_CONTRACT_INDEX, + QX_CONTRACT_INDEX, + QRWA_RELEASE_MANAGEMENT_FEE + ); + + if (locals.releaseFeeResult >= 0) // management transfer succeeded + { + locals.managementTransferred = 1; + locals.feePaid = 1; + state.mRevenuePoolA = (state.mRevenuePoolA > (uint64)locals.releaseFeeResult) ? (state.mRevenuePoolA - (uint64)locals.releaseFeeResult) : 0; + } + } + } + } + } + } + + locals.transferSuccess = locals.ownershipTransferred & locals.managementTransferred & locals.feePaid; + if (locals.transferSuccess == 1) // All steps succeeded + { + locals.logger.logType = QRWA_LOG_TYPE_ASSET_POLL_EXECUTED; + locals.logger.valueB = QRWA_STATUS_SUCCESS; + locals.poll.status = QRWA_POLL_STATUS_PASSED_EXECUTED; + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + if (locals.sufficientFunds == 0) + { + locals.logger.valueB = QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE; + } + else if (locals.ownershipTransferred == 0) + { + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + } + else + { + // This is the stuck shares case + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + } + locals.poll.status = QRWA_POLL_STATUS_PASSED_FAILED_EXECUTION; + } + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.primaryId = locals.poll.destination; + locals.logger.valueA = locals.poll.proposalId; + LOG_INFO(locals.logger); + } + else // Vote failed (NO wins or < quorum) + { + locals.poll.status = QRWA_POLL_STATUS_FAILED_VOTE; + } + state.mAssetPolls.set(locals.pollIndex, locals.poll); + } + } + // Reset voter tracking map for asset polls + state.mAssetProposalVoterMap.reset(); + state.mAssetVoteOptions.reset(); + + // Copy the finalized epoch snapshots to the payout buffers + state.mPayoutBeginBalances.reset(); + state.mPayoutEndBalances.reset(); + state.mPayoutTotalQmineBegin = state.mTotalQmineBeginEpoch; + + // Copy mBeginEpochBalances -> mPayoutBeginBalances + locals.copyIndex = NULL_INDEX; + while (true) + { + locals.copyIndex = state.mBeginEpochBalances.nextElementIndex(locals.copyIndex); + if (locals.copyIndex == NULL_INDEX) + { + break; + } + locals.copyHolder = state.mBeginEpochBalances.key(locals.copyIndex); + locals.copyBalance = state.mBeginEpochBalances.value(locals.copyIndex); + state.mPayoutBeginBalances.set(locals.copyHolder, locals.copyBalance); + } + + // Copy mEndEpochBalances -> mPayoutEndBalances + locals.copyIndex = NULL_INDEX; + while (true) + { + locals.copyIndex = state.mEndEpochBalances.nextElementIndex(locals.copyIndex); + if (locals.copyIndex == NULL_INDEX) + { + break; + } + locals.copyHolder = state.mEndEpochBalances.key(locals.copyIndex); + locals.copyBalance = state.mEndEpochBalances.value(locals.copyIndex); + state.mPayoutEndBalances.set(locals.copyHolder, locals.copyBalance); + } + } + + + struct END_TICK_locals + { + DateAndTime now; + uint64 durationMicros; + uint64 msSinceLastPayout; + + uint64 totalGovPercent; + uint64 totalFeeAmount; + uint64 electricityPayout; + uint64 maintenancePayout; + uint64 reinvestmentPayout; + uint64 Y_revenue; + uint64 totalDistribution; + uint64 qminePayout; + uint64 qrwaPayout; + uint64 amountPerQRWAShare; + uint64 distributedAmount; + + sint64 qminePayoutIndex; + id holder; + uint64 beginBalance; + uint64 endBalance; + uint64 eligibleBalance; + // Use uint128 for all payout accounting + uint128 scaledPayout_128; + uint128 eligiblePayout_128; + uint128 totalEligiblePaid_128; + uint128 movedSharesPayout_128; + uint128 qmineDividendPool_128; + uint64 payout_u64; + uint64 foundEnd; + QRWALogger logger; + }; + END_TICK_WITH_LOCALS() + { + locals.now = qpi.now(); + + // Check payout conditions: Correct day, correct hour, and enough time passed + if (qpi.dayOfWeek((uint8)mod(locals.now.getYear(), (uint16)100), locals.now.getMonth(), locals.now.getDay()) == QRWA_PAYOUT_DAY && + locals.now.getHour() == QRWA_PAYOUT_HOUR) + { + // check if mLastPayoutTime is 0 (never initialized) + if (state.mLastPayoutTime.getYear() == 0) + { + // If never paid, treat as if enough time has passed + locals.msSinceLastPayout = QRWA_MIN_PAYOUT_INTERVAL_MS; + } + else + { + locals.durationMicros = state.mLastPayoutTime.durationMicrosec(locals.now); + + if (locals.durationMicros != UINT64_MAX) + { + locals.msSinceLastPayout = div(locals.durationMicros, 1000); + } + else + { + // If it's invalid but NOT zero, something is wrong, so we prevent payout + locals.msSinceLastPayout = 0; + } + } + + if (locals.msSinceLastPayout >= QRWA_MIN_PAYOUT_INTERVAL_MS) + { + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_DISTRIBUTION; + + // Calculate and pay out governance fees from Pool A (mined funds) + // gov_percentage = electricity_percent + maintenance_percent + reinvestment_percent + locals.totalGovPercent = sadd(sadd(state.mCurrentGovParams.electricityPercent, state.mCurrentGovParams.maintenancePercent), state.mCurrentGovParams.reinvestmentPercent); + locals.totalFeeAmount = 0; + + if (locals.totalGovPercent > 0 && locals.totalGovPercent <= QRWA_PERCENT_DENOMINATOR && state.mRevenuePoolA > 0) + { + locals.electricityPayout = div(smul(state.mRevenuePoolA, state.mCurrentGovParams.electricityPercent), QRWA_PERCENT_DENOMINATOR); + if (locals.electricityPayout > 0 && state.mCurrentGovParams.electricityAddress != NULL_ID) + { + if (qpi.transfer(state.mCurrentGovParams.electricityAddress, locals.electricityPayout) >= 0) + { + locals.totalFeeAmount = sadd(locals.totalFeeAmount, locals.electricityPayout); + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = state.mCurrentGovParams.electricityAddress; + locals.logger.valueA = locals.electricityPayout; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + + locals.maintenancePayout = div(smul(state.mRevenuePoolA, state.mCurrentGovParams.maintenancePercent), QRWA_PERCENT_DENOMINATOR); + if (locals.maintenancePayout > 0 && state.mCurrentGovParams.maintenanceAddress != NULL_ID) + { + if (qpi.transfer(state.mCurrentGovParams.maintenanceAddress, locals.maintenancePayout) >= 0) + { + locals.totalFeeAmount = sadd(locals.totalFeeAmount, locals.maintenancePayout); + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = state.mCurrentGovParams.maintenanceAddress; + locals.logger.valueA = locals.maintenancePayout; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + + locals.reinvestmentPayout = div(smul(state.mRevenuePoolA, state.mCurrentGovParams.reinvestmentPercent), QRWA_PERCENT_DENOMINATOR); + if (locals.reinvestmentPayout > 0 && state.mCurrentGovParams.reinvestmentAddress != NULL_ID) + { + if (qpi.transfer(state.mCurrentGovParams.reinvestmentAddress, locals.reinvestmentPayout) >= 0) + { + locals.totalFeeAmount = sadd(locals.totalFeeAmount, locals.reinvestmentPayout); + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = state.mCurrentGovParams.reinvestmentAddress; + locals.logger.valueA = locals.reinvestmentPayout; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + state.mRevenuePoolA = (state.mRevenuePoolA > locals.totalFeeAmount) ? (state.mRevenuePoolA - locals.totalFeeAmount) : 0; + } + + // Calculate total distribution pool + locals.Y_revenue = state.mRevenuePoolA; // Remaining Pool A after fees + locals.totalDistribution = sadd(locals.Y_revenue, state.mRevenuePoolB); + + // Allocate to QMINE and qRWA pools + if (locals.totalDistribution > 0) + { + locals.qminePayout = div(smul(locals.totalDistribution, QRWA_QMINE_HOLDER_PERCENT), QRWA_PERCENT_DENOMINATOR); + locals.qrwaPayout = locals.totalDistribution - locals.qminePayout; // Avoid potential rounding errors + + state.mQmineDividendPool = sadd(state.mQmineDividendPool, locals.qminePayout); + state.mQRWADividendPool = sadd(state.mQRWADividendPool, locals.qrwaPayout); + + // Reset revenue pools after allocation + state.mRevenuePoolA = 0; + state.mRevenuePoolB = 0; + } + + // Distribute QMINE rewards + if (state.mQmineDividendPool > 0 && state.mPayoutTotalQmineBegin > 0) + { + locals.totalEligiblePaid_128 = 0; + locals.qminePayoutIndex = NULL_INDEX; // Start iteration + locals.qmineDividendPool_128 = state.mQmineDividendPool; // Create 128-bit copy for accounting + + // pay eligible holders + while (true) + { + locals.qminePayoutIndex = state.mPayoutBeginBalances.nextElementIndex(locals.qminePayoutIndex); + if (locals.qminePayoutIndex == NULL_INDEX) + { + break; + } + + locals.holder = state.mPayoutBeginBalances.key(locals.qminePayoutIndex); + locals.beginBalance = state.mPayoutBeginBalances.value(locals.qminePayoutIndex); + + locals.foundEnd = state.mPayoutEndBalances.get(locals.holder, locals.endBalance) ? 1 : 0; + if (locals.foundEnd == 0) + { + locals.endBalance = 0; + } + + locals.eligibleBalance = (locals.beginBalance < locals.endBalance) ? locals.beginBalance : locals.endBalance; + + if (locals.eligibleBalance > 0) + { + // Payout = (EligibleBalance * DividendPool) / PayoutBase + locals.scaledPayout_128 = (uint128)locals.eligibleBalance * (uint128)state.mQmineDividendPool; + locals.eligiblePayout_128 = div(locals.scaledPayout_128, state.mPayoutTotalQmineBegin); + + if (locals.eligiblePayout_128 > (uint128)0 && locals.eligiblePayout_128 <= locals.qmineDividendPool_128) + { + // Cast to uint64 ONLY at the moment of transfer + locals.payout_u64 = locals.eligiblePayout_128.low; + + // Check if the cast truncated the value (if high part was set) + if (locals.eligiblePayout_128.high == 0 && locals.payout_u64 > 0) + { + if (qpi.transfer(locals.holder, (sint64)locals.payout_u64) >= 0) + { + locals.qmineDividendPool_128 -= locals.eligiblePayout_128; + state.mTotalQmineDistributed = sadd(state.mTotalQmineDistributed, locals.payout_u64); + locals.totalEligiblePaid_128 += locals.eligiblePayout_128; + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = locals.holder; + locals.logger.valueA = locals.payout_u64; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + } + else if (locals.eligiblePayout_128 > locals.qmineDividendPool_128) + { + // Payout is larger than the remaining pool + locals.payout_u64 = locals.qmineDividendPool_128.low; // Get remaining pool + + if (locals.qmineDividendPool_128.high == 0 && locals.payout_u64 > 0) + { + if (qpi.transfer(locals.holder, (sint64)locals.payout_u64) >= 0) + { + state.mTotalQmineDistributed = sadd(state.mTotalQmineDistributed, locals.payout_u64); + locals.totalEligiblePaid_128 += locals.qmineDividendPool_128; + locals.qmineDividendPool_128 = 0; // Pool exhausted + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = locals.holder; + locals.logger.valueA = locals.payout_u64; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + break; + } + } + } + + // Pay QMINE DEV the entire remainder of the pool + locals.movedSharesPayout_128 = locals.qmineDividendPool_128; + if (locals.movedSharesPayout_128 > (uint128)0 && state.mCurrentGovParams.qmineDevAddress != NULL_ID) + { + locals.payout_u64 = locals.movedSharesPayout_128.low; + if (locals.movedSharesPayout_128.high == 0 && locals.payout_u64 > 0) + { + if (qpi.transfer(state.mCurrentGovParams.qmineDevAddress, (sint64)locals.payout_u64) >= 0) + { + state.mTotalQmineDistributed = sadd(state.mTotalQmineDistributed, locals.payout_u64); + locals.qmineDividendPool_128 = 0; + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = state.mCurrentGovParams.qmineDevAddress; + locals.logger.valueA = locals.payout_u64; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + } + + // Update the 64-bit state variable from the 128-bit local + // If transfers failed, funds remain in qmineDividendPool_128 and will be preserved here. + state.mQmineDividendPool = locals.qmineDividendPool_128.low; + } // End QMINE distribution + + // Distribute qRWA shareholder rewards + if (state.mQRWADividendPool > 0) + { + locals.amountPerQRWAShare = div(state.mQRWADividendPool, NUMBER_OF_COMPUTORS); + if (locals.amountPerQRWAShare > 0) + { + if (qpi.distributeDividends(static_cast(locals.amountPerQRWAShare))) + { + locals.distributedAmount = smul(locals.amountPerQRWAShare, static_cast(NUMBER_OF_COMPUTORS)); + state.mQRWADividendPool -= locals.distributedAmount; + state.mTotalQRWADistributed = sadd(state.mTotalQRWADistributed, locals.distributedAmount); + } + } + } + + // Update last payout time + state.mLastPayoutTime = qpi.now(); + locals.logger.logType = QRWA_LOG_TYPE_DISTRIBUTION; + locals.logger.primaryId = NULL_ID; + locals.logger.valueA = 1; // Indicate success + locals.logger.valueB = 0; + LOG_INFO(locals.logger); + } + } + } + + struct POST_INCOMING_TRANSFER_locals + { + QRWALogger logger; + }; + POST_INCOMING_TRANSFER_WITH_LOCALS() + { + // Differentiate revenue streams based on source type + if (input.sourceId.u64._1 == 0 && input.sourceId.u64._2 == 0 && input.sourceId.u64._3 == 0 && input.sourceId.u64._0 != 0) + { + // Source is likely a contract (e.g., QX transfer) -> Pool A + state.mRevenuePoolA = sadd(state.mRevenuePoolA, static_cast(input.amount)); + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_INCOMING_REVENUE_A; + locals.logger.primaryId = input.sourceId; + locals.logger.valueA = input.amount; + locals.logger.valueB = input.type; + LOG_INFO(locals.logger); + } + else if (input.sourceId != NULL_ID) + { + // Source is likely a user (EOA) -> Pool B + state.mRevenuePoolB = sadd(state.mRevenuePoolB, static_cast(input.amount)); + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_INCOMING_REVENUE_B; + locals.logger.primaryId = input.sourceId; + locals.logger.valueA = input.amount; + locals.logger.valueB = input.type; + LOG_INFO(locals.logger); + } + } + + PRE_ACQUIRE_SHARES() + { + // Allow any entity to transfer asset management rights to this contract + output.requestedFee = 0; + output.allowTransfer = true; + } + + POST_ACQUIRE_SHARES() + { + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + // PROCEDURES + REGISTER_USER_PROCEDURE(DonateToTreasury, 3); + REGISTER_USER_PROCEDURE(VoteGovParams, 4); + REGISTER_USER_PROCEDURE(CreateAssetReleasePoll, 5); + REGISTER_USER_PROCEDURE(VoteAssetRelease, 6); + REGISTER_USER_PROCEDURE(DepositGeneralAsset, 7); + REGISTER_USER_PROCEDURE(RevokeAssetManagementRights, 8); + + // FUNCTIONS + REGISTER_USER_FUNCTION(GetGovParams, 1); + REGISTER_USER_FUNCTION(GetGovPoll, 2); + REGISTER_USER_FUNCTION(GetAssetReleasePoll, 3); + REGISTER_USER_FUNCTION(GetTreasuryBalance, 4); + REGISTER_USER_FUNCTION(GetDividendBalances, 5); + REGISTER_USER_FUNCTION(GetTotalDistributed, 6); + REGISTER_USER_FUNCTION(GetActiveAssetReleasePollIds, 7); + REGISTER_USER_FUNCTION(GetActiveGovPollIds, 8); + REGISTER_USER_FUNCTION(GetGeneralAssetBalance, 9); + REGISTER_USER_FUNCTION(GetGeneralAssets, 10); + } +}; diff --git a/test/contract_qrwa.cpp b/test/contract_qrwa.cpp new file mode 100644 index 000000000..374075951 --- /dev/null +++ b/test/contract_qrwa.cpp @@ -0,0 +1,1750 @@ +#define NO_UEFI + +#include "contract_testing.h" +#include "test_util.h" + +#define ENABLE_BALANCE_DEBUG 0 + +// Pseudo IDs (for testing only) + +// QMINE_ISSUER is is also the ADMIN_ADDRESS +static const id QMINE_ISSUER = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G +); +static const id ADMIN_ADDRESS = QMINE_ISSUER; + +// temporary holder for the initial 150M QMINE supply +static const id TREASURY_HOLDER = id::randomValue(); + +// Addresses for governance-set fees +static const id FEE_ADDR_E = id::randomValue(); // Electricity fees address +static const id FEE_ADDR_M = id::randomValue(); // Maintenance fees address +static const id FEE_ADDR_R = id::randomValue(); // Reinvestment fees address + +// pseudo test address for QMINE developer +static const id QMINE_DEV_ADDR_TEST = ID( + _Z, _O, _X, _X, _I, _D, _C, _Z, _I, _M, _G, _C, _E, _C, _C, _F, + _A, _X, _D, _D, _C, _M, _B, _B, _X, _C, _D, _A, _Q, _J, _I, _H, + _G, _O, _O, _A, _T, _A, _F, _P, _S, _B, _F, _I, _O, _F, _O, _Y, + _E, _C, _F, _K, _U, _F, _P, _B +); + +// Test accounts for holders and users +static const id HOLDER_A = id::randomValue(); +static const id HOLDER_B = id::randomValue(); +static const id HOLDER_C = id::randomValue(); +static const id USER_D = id::randomValue(); // no-share user +static const id DESTINATION_ADDR = id::randomValue(); // dest for asset releases + +// Test QMINE Asset (using the random issuer for testing only) +static const Asset QMINE_ASSET = { QMINE_ISSUER, 297666170193ULL }; + +// Fees for dependent contracts +static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ull; +static constexpr uint64 QX_TRANSFER_FEE = 100ull; // Fee for transfering assets back to QX +static constexpr uint64 QX_MGT_TRANSFER_FEE = 0ull; // Fee for QX::TransferShareManagementRights +static constexpr sint64 QUTIL_STM1_FEE = 10LL; // QUTIL SendToManyV1 fee (QUTIL_STM1_INVOCATION_FEE) + + +enum qRWAFunctionIds +{ + QRWA_FUNC_GET_GOV_PARAMS = 1, + QRWA_FUNC_GET_GOV_POLL = 2, + QRWA_FUNC_GET_ASSET_RELEASE_POLL = 3, + QRWA_FUNC_GET_TREASURY_BALANCE = 4, + QRWA_FUNC_GET_DIVIDEND_BALANCES = 5, + QRWA_FUNC_GET_TOTAL_DISTRIBUTED = 6 +}; + +enum qRWAProcedureIds +{ + QRWA_PROC_DONATE_TO_TREASURY = 3, + QRWA_PROC_VOTE_GOV_PARAMS = 4, + QRWA_PROC_CREATE_ASSET_RELEASE_POLL = 5, + QRWA_PROC_VOTE_ASSET_RELEASE = 6, + QRWA_PROC_DEPOSIT_GENERAL_ASSET = 7, + QRWA_PROC_REVOKE_ASSET = 8, +}; + +enum QxProcedureIds +{ + QX_PROC_ISSUE_ASSET = 1, + QX_PROC_TRANSFER_SHARES = 2, + QX_PROC_TRANSFER_MANAGEMENT = 9 +}; + +enum QutilProcedureIds +{ + QUTIL_PROC_SEND_TO_MANY_V1 = 1 +}; + + +class ContractTestingQRWA : protected ContractTesting +{ + // Grant access to protected/private members for setup + friend struct QRWA; + +public: + ContractTestingQRWA() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QRWA); + callSystemProcedure(QRWA_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QUTIL); + callSystemProcedure(QUTIL_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QSWAP); + callSystemProcedure(QSWAP_CONTRACT_INDEX, INITIALIZE); + + // Custom Initialization for qRWA State + // (Overrides defaults from INITIALIZE() for testing purposes) + QRWA* state = getState(); + + // Fee addresses + // Note: We want to check these Fee Addresses separately, + // we use different addresses instead of same address as the Admin Address + state->mCurrentGovParams.electricityAddress = FEE_ADDR_E; + state->mCurrentGovParams.maintenanceAddress = FEE_ADDR_M; + state->mCurrentGovParams.reinvestmentAddress = FEE_ADDR_R; + } + + QRWA* getState() + { + return (QRWA*)contractStates[QRWA_CONTRACT_INDEX]; + } + + void beginEpoch(bool expectSuccess = true) + { + callSystemProcedure(QRWA_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QRWA_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + void endTick(bool expectSuccess = true) + { + callSystemProcedure(QRWA_CONTRACT_INDEX, END_TICK, expectSuccess); + } + + // manually reset the last payout time for testing. + void resetPayoutTime() + { + getState()->mLastPayoutTime = { 0, 0, 0, 0, 0, 0, 0 }; + } + + // QX/QUTIL Contract Wrappers + + void issueAsset(const id& issuer, uint64 assetName, sint64 shares) + { + QX::IssueAsset_input input{ assetName, shares, 0, 0 }; + QX::IssueAsset_output output; + increaseEnergy(issuer, QX_ISSUE_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, QX_PROC_ISSUE_ASSET, input, output, issuer, QX_ISSUE_ASSET_FEE); + } + + // Transfers asset ownership and possession on QX. + void transferAsset(const id& from, const id& to, const Asset& asset, sint64 shares) + { + QX::TransferShareOwnershipAndPossession_input input{ asset.issuer, to, asset.assetName, shares }; + QX::TransferShareOwnershipAndPossession_output output; + increaseEnergy(from, QX_TRANSFER_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, QX_PROC_TRANSFER_SHARES, input, output, from, QX_TRANSFER_FEE); + } + + // Transfers management rights of an asset to another contract + void transferManagementRights(const id& from, const Asset& asset, sint64 shares, uint32 toContract) + { + QX::TransferShareManagementRights_input input{ asset, shares, toContract }; + QX::TransferShareManagementRights_output output; + increaseEnergy(from, QX_MGT_TRANSFER_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, QX_PROC_TRANSFER_MANAGEMENT, input, output, from, QX_MGT_TRANSFER_FEE); + } + + // Simulates a dividend payout from QLI pool using QUTIL::SendToManyV1. + void sendToMany(const id& from, const id& to, sint64 amount) + { + QUTIL::SendToManyV1_input input = {}; + input.dst0 = to; + input.amt0 = amount; + QUTIL::SendToManyV1_output output; + increaseEnergy(from, amount + QUTIL_STM1_FEE); + invokeUserProcedure(QUTIL_CONTRACT_INDEX, QUTIL_PROC_SEND_TO_MANY_V1, input, output, from, amount + QUTIL_STM1_FEE); + } + + // QRWA Procedure Wrappers + + uint64 donateToTreasury(const id& from, uint64 amount) + { + QRWA::DonateToTreasury_input input{ amount }; + QRWA::DonateToTreasury_output output; + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_DONATE_TO_TREASURY, input, output, from, 0); + return output.status; + } + + uint64 voteGovParams(const id& from, const QRWA::QRWAGovParams& params) + { + QRWA::VoteGovParams_input input{ params }; + QRWA::VoteGovParams_output output; + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_VOTE_GOV_PARAMS, input, output, from, 0); + return output.status; + } + + QRWA::CreateAssetReleasePoll_output createAssetReleasePoll(const id& from, const QRWA::CreateAssetReleasePoll_input& input) + { + QRWA::CreateAssetReleasePoll_output output; + memset(&output, 0, sizeof(output)); + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_CREATE_ASSET_RELEASE_POLL, input, output, from, 0); + return output; + } + + uint64 voteAssetRelease(const id& from, uint64 pollId, uint64 option) + { + QRWA::VoteAssetRelease_input input{ pollId, option }; + QRWA::VoteAssetRelease_output output; + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_VOTE_ASSET_RELEASE, input, output, from, 0); + return output.status; + } + + uint64 depositGeneralAsset(const id& from, const Asset& asset, uint64 amount) + { + QRWA::DepositGeneralAsset_input input{ asset, amount }; + QRWA::DepositGeneralAsset_output output; + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_DEPOSIT_GENERAL_ASSET, input, output, from, 0); + return output.status; + } + + QRWA::RevokeAssetManagementRights_output revokeAssetManagementRights(const id& from, const Asset& asset, sint64 numberOfShares) + { + QRWA::RevokeAssetManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + + QRWA::RevokeAssetManagementRights_output output; + memset(&output, 0, sizeof(output)); + + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_REVOKE_ASSET, input, output, from, QRWA_RELEASE_MANAGEMENT_FEE); + return output; + } + + // QRWA Wrappers + + QRWA::QRWAGovParams getGovParams() + { + QRWA::GetGovParams_input input; + QRWA::GetGovParams_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_GOV_PARAMS, input, output); + return output.params; + } + + QRWA::GetGovPoll_output getGovPoll(uint64 pollId) + { + QRWA::GetGovPoll_input input{ pollId }; + QRWA::GetGovPoll_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_GOV_POLL, input, output); + return output; + } + + QRWA::GetAssetReleasePoll_output getAssetReleasePoll(uint64 pollId) + { + QRWA::GetAssetReleasePoll_input input{ pollId }; + QRWA::GetAssetReleasePoll_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_ASSET_RELEASE_POLL, input, output); + return output; + } + + uint64 getTreasuryBalance() + { + QRWA::GetTreasuryBalance_input input; + QRWA::GetTreasuryBalance_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_TREASURY_BALANCE, input, output); + return output.balance; + } + + QRWA::GetDividendBalances_output getDividendBalances() + { + QRWA::GetDividendBalances_input input; + QRWA::GetDividendBalances_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_DIVIDEND_BALANCES, input, output); + return output; + } + + QRWA::GetTotalDistributed_output getTotalDistributed() + { + QRWA::GetTotalDistributed_input input; + QRWA::GetTotalDistributed_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_TOTAL_DISTRIBUTED, input, output); + return output; + } + +}; + + +TEST(ContractQRWA, Initialization) +{ + ContractTestingQRWA qrwa; + + // Check gov params (set in test constructor) + auto params = qrwa.getGovParams(); + EXPECT_EQ(params.mAdminAddress, ADMIN_ADDRESS); + EXPECT_EQ(params.qmineDevAddress, QMINE_DEV_ADDR_TEST); + EXPECT_EQ(params.electricityAddress, FEE_ADDR_E); + EXPECT_EQ(params.maintenanceAddress, FEE_ADDR_M); + EXPECT_EQ(params.reinvestmentAddress, FEE_ADDR_R); + EXPECT_EQ(params.electricityPercent, 350); + EXPECT_EQ(params.maintenancePercent, 50); + EXPECT_EQ(params.reinvestmentPercent, 100); + + // Check pools and balances via public functions + EXPECT_EQ(qrwa.getTreasuryBalance(), 0); + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 0); + EXPECT_EQ(divBalances.revenuePoolB, 0); + EXPECT_EQ(divBalances.qmineDividendPool, 0); + EXPECT_EQ(divBalances.qrwaDividendPool, 0); + + auto distTotals = qrwa.getTotalDistributed(); + EXPECT_EQ(distTotals.totalQmineDistributed, 0); + EXPECT_EQ(distTotals.totalQRWADistributed, 0); +} + + +TEST(ContractQRWA, RevenueAccounting_POST_INCOMING_TRANSFER) +{ + ContractTestingQRWA qrwa; + + // Pool A from SC QUTIL + // We simulate this by calling QUTIL's SendToMany + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 1000000); + // We cannot test pool B as the test environment does not support standard transfer + // as noted in contract_testex.cpp + EXPECT_EQ(divBalances.revenuePoolB, 0); +} + +TEST(ContractQRWA, Governance_VoteGovParams_And_EndEpochCount) +{ + ContractTestingQRWA qrwa; + + // Issue QMINE, distribute, and run BEGIN_EPOCH + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1000000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 400000); // 40% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 400000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 600000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); // 30% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 300000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 300000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 200000); // 20% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 200000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 100000); + + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(HOLDER_C, 1000000); + increaseEnergy(USER_D, 1000000); + + qrwa.beginEpoch(); + // Quorum should be 2/3 of 900,000 = 600,000 + + // Not a holder + EXPECT_EQ(qrwa.voteGovParams(USER_D, {}), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + // Invalid params (Admin NULL_ID) + QRWA::QRWAGovParams invalidParams = qrwa.getGovParams(); + invalidParams.mAdminAddress = NULL_ID; + EXPECT_EQ(qrwa.voteGovParams(HOLDER_A, invalidParams), QRWA_STATUS_FAILURE_INVALID_INPUT); + + // Create new poll and vote for it + QRWA::QRWAGovParams paramsA = qrwa.getGovParams(); + paramsA.electricityPercent = 100; // Change one param + + EXPECT_EQ(qrwa.voteGovParams(HOLDER_A, paramsA), QRWA_STATUS_SUCCESS); // Poll 0 + EXPECT_EQ(qrwa.voteGovParams(HOLDER_B, paramsA), QRWA_STATUS_SUCCESS); // Vote for Poll 0 + + // Change vote + QRWA::QRWAGovParams paramsB = qrwa.getGovParams(); + paramsB.maintenancePercent = 100; // Change another param + + EXPECT_EQ(qrwa.voteGovParams(HOLDER_A, paramsB), QRWA_STATUS_SUCCESS); // Poll 1 + + // Mid-epoch sale + qrwa.transferAsset(HOLDER_B, USER_D, QMINE_ASSET, 150000); // B's balance is now 150k + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 150000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 150000); + + + // Accountant at END_EPOCH + qrwa.endEpoch(); + + // Check results: + // Poll 0 (ParamsA): HOLDER_B voted. Begin=300k, End=150k. VotingPower = 150k. + // Poll 1 (ParamsB): HOLDER_A voted. Begin=400k, End=400k. VotingPower = 400k. + // Total power = 900k. Quorum = 600k. + + // Poll 0 (ParamsA) failed. + auto poll0 = qrwa.getGovPoll(0); + EXPECT_EQ(poll0.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(poll0.proposal.score, 150000); + EXPECT_EQ(poll0.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + + // Poll 1 (ParamsB) failed. + auto poll1 = qrwa.getGovPoll(1); + EXPECT_EQ(poll1.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(poll1.proposal.score, 400000); + EXPECT_EQ(poll1.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + + // Params should be unchanged (still 50 from init) + EXPECT_EQ(qrwa.getGovParams().maintenancePercent, 50); + + // New Epoch: Test successful vote + qrwa.beginEpoch(); // New snapshot total: A(400k) + B(150k) + C(200k) = 750k. Quorum = 500k. + + // All holders vote for ParamsB + EXPECT_EQ(qrwa.voteGovParams(HOLDER_A, paramsB), QRWA_STATUS_SUCCESS); // Creates Poll 2 + EXPECT_EQ(qrwa.voteGovParams(HOLDER_B, paramsB), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(HOLDER_C, paramsB), QRWA_STATUS_SUCCESS); + + qrwa.endEpoch(); + + // Check results: + // Poll 2 (ParamsB): A(400k) + B(150k) + C(200k) = 750k vote power. + // Vote passes. + auto poll2 = qrwa.getGovPoll(2); + EXPECT_EQ(poll2.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(poll2.proposal.score, 750000); + EXPECT_EQ(poll2.proposal.status, QRWA_POLL_STATUS_PASSED_EXECUTED); + + // Verify params were updated + EXPECT_EQ(qrwa.getGovParams().maintenancePercent, 100); +} + +TEST(ContractQRWA, Governance_AssetReleasePolls) +{ + ContractTestingQRWA qrwa; + + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(USER_D, 1000000); + increaseEnergy(DESTINATION_ADDR, 1000000); + + // Issue QMINE, distribute, and run BEGIN_EPOCH + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000 + 1000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1001000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 700000); // 70% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 700000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 301000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); // 30% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 300000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1000); + // QMINE_ISSUER (ADMIN_ADDRESS) now holds 1000 + + // Give SC 1000 QMINE for its treasury + qrwa.transferManagementRights(QMINE_ISSUER, QMINE_ASSET, 1000, QRWA_CONTRACT_INDEX); + EXPECT_EQ(qrwa.donateToTreasury(QMINE_ISSUER, 1000), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.getTreasuryBalance(), 1000); + + qrwa.beginEpoch(); + + // Not Admin + QRWA::CreateAssetReleasePoll_input pollInput = {}; + pollInput.proposalName = id::randomValue(); + pollInput.asset = QMINE_ASSET; + pollInput.amount = 100; + pollInput.destination = DESTINATION_ADDR; + + auto pollOut = qrwa.createAssetReleasePoll(HOLDER_A, pollInput); // HOLDER_A is not admin + EXPECT_EQ(pollOut.status, QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + // Admin creates poll + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + EXPECT_EQ(pollOut.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(pollOut.proposalId, 0); + + // Not a holder + EXPECT_EQ(qrwa.voteAssetRelease(USER_D, 0, 1), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + // Holders vote + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_A, 0, 1), QRWA_STATUS_SUCCESS); // 700k YES + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_B, 0, 0), QRWA_STATUS_SUCCESS); // 300k NO + + // Add revenue to Pool A so the contract can pay the release fee + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + EXPECT_EQ(qrwa.getDividendBalances().revenuePoolA, 1000000); + + // Count at end epoch (Pass) + qrwa.endEpoch(); + + auto poll = qrwa.getAssetReleasePoll(0); + EXPECT_EQ(poll.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(poll.proposal.status, QRWA_POLL_STATUS_PASSED_EXECUTED); // Should pass now + EXPECT_EQ(poll.proposal.votesYes, 700000); + EXPECT_EQ(poll.proposal.votesNo, 300000); + + // Verify balances + EXPECT_EQ(qrwa.getTreasuryBalance(), 900); // 1000 - 100 + EXPECT_EQ(numberOfShares(QMINE_ASSET, { DESTINATION_ADDR, QX_CONTRACT_INDEX }, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }), 100); // Should be 100 now + + // Count at end epoch (Fail Vote) + qrwa.beginEpoch(); + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); // Poll 1 + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_A, 1, 0), QRWA_STATUS_SUCCESS); // 700k NO + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_B, 1, 1), QRWA_STATUS_SUCCESS); // 300k YES + qrwa.endEpoch(); + + poll = qrwa.getAssetReleasePoll(1); + EXPECT_EQ(poll.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + EXPECT_EQ(qrwa.getTreasuryBalance(), 900); // Unchanged + + // Count at end epoch (Fail Execution - Insufficient) + qrwa.beginEpoch(); + pollInput.amount = 1000; // Try to release 1000 (only 900 left) + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); // Poll 2 + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_A, 2, 1), QRWA_STATUS_SUCCESS); // 700k YES + qrwa.endEpoch(); + + poll = qrwa.getAssetReleasePoll(2); + EXPECT_EQ(poll.proposal.status, QRWA_POLL_STATUS_PASSED_FAILED_EXECUTION); + EXPECT_EQ(qrwa.getTreasuryBalance(), 900); // Unchanged +} + +TEST(ContractQRWA, Governance_AssetRelease_FailAndRevoke) +{ + ContractTestingQRWA qrwa; + + const sint64 initialEnergy = 1000000000; + increaseEnergy(HOLDER_A, initialEnergy); + increaseEnergy(HOLDER_B, initialEnergy); + increaseEnergy(ADMIN_ADDRESS, initialEnergy + QX_ISSUE_ASSET_FEE); + increaseEnergy(DESTINATION_ADDR, initialEnergy); + + const sint64 treasuryAmount = 1000; + const sint64 voterShares = 1000000; + const sint64 releaseAmount = 500; + + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, voterShares + treasuryAmount); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 700000); + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); + + // Give qRWA management rights over the treasury shares + qrwa.transferManagementRights(QMINE_ISSUER, QMINE_ASSET, treasuryAmount, QRWA_CONTRACT_INDEX); + + // Verify management rights were transferred + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QRWA_CONTRACT_INDEX }, + { QMINE_ISSUER, QRWA_CONTRACT_INDEX }), treasuryAmount); + + // Donate the shares to the treasury + EXPECT_EQ(qrwa.donateToTreasury(QMINE_ISSUER, treasuryAmount), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.getTreasuryBalance(), treasuryAmount); + + // Verify Revenue Pool A (for fees) is empty. + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 0); + + qrwa.beginEpoch(); + // Total voting power = 1,000,000 (HOLDER_A + HOLDER_B) + // Quorum = (1,000,000 * 2 / 3) + 1 = 666,667 + + QRWA::CreateAssetReleasePoll_input pollInput = {}; + pollInput.proposalName = id::randomValue(); + pollInput.asset = QMINE_ASSET; + pollInput.amount = releaseAmount; + pollInput.destination = DESTINATION_ADDR; + + // create poll + auto pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + EXPECT_EQ(pollOut.status, QRWA_STATUS_SUCCESS); + uint64 pollId = pollOut.proposalId; + + // HOLDER_A votes YES, passing the poll (700k > 666k quorum) + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_A, pollId, 1), QRWA_STATUS_SUCCESS); + + qrwa.endEpoch(); + + // Check poll status + // It should have passed the vote but failed execution (due to lack of 100 QUs fee for QX management transfer) + auto poll = qrwa.getAssetReleasePoll(pollId); + EXPECT_EQ(poll.proposal.status, QRWA_POLL_STATUS_PASSED_FAILED_EXECUTION); + EXPECT_EQ(poll.proposal.votesYes, 700000); + + // Check SC asset state + // Asserts the INTERNAL counter is now decreased + EXPECT_EQ(qrwa.getTreasuryBalance(), treasuryAmount - releaseAmount); // 1000 - 500 = 500 + + // the SC balance is decreased + sint64 scOwnedBalance = numberOfShares(QMINE_ASSET, + { id(QRWA_CONTRACT_INDEX, 0, 0, 0), QRWA_CONTRACT_INDEX }, + { id(QRWA_CONTRACT_INDEX, 0, 0, 0), QRWA_CONTRACT_INDEX }); + EXPECT_EQ(scOwnedBalance, treasuryAmount - releaseAmount); // 1000 - 500 = 500 + + // DESTINATION_ADDR should now owns the shares, but they are MANAGED by qRWA + sint64 destManagedByQrwa = numberOfShares(QMINE_ASSET, + { DESTINATION_ADDR, QRWA_CONTRACT_INDEX }, + { DESTINATION_ADDR, QRWA_CONTRACT_INDEX }); + EXPECT_EQ(destManagedByQrwa, releaseAmount); // 500 shares are stuck + + // DESTINATION_ADDR should have 0 shares managed by QX + sint64 destManagedByQx = numberOfShares(QMINE_ASSET, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }); + EXPECT_EQ(destManagedByQx, 0); + + // Test Revoke + qrwa.beginEpoch(); + + // Fund DESTINATION_ADDR with the fee for the revoke procedure + increaseEnergy(DESTINATION_ADDR, QRWA_RELEASE_MANAGEMENT_FEE); + sint64 destBalanceBeforeRevoke = getBalance(DESTINATION_ADDR); + + // DESTINATION_ADDR calls revokeAssetManagementRights + auto revokeOut = qrwa.revokeAssetManagementRights(DESTINATION_ADDR, QMINE_ASSET, releaseAmount); + + // check the outcome + EXPECT_EQ(revokeOut.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(revokeOut.transferredNumberOfShares, releaseAmount); + + // check final on-chain asset state + // DESTINATION_ADDR should be no longer have shares managed by qRWA + destManagedByQrwa = numberOfShares(QMINE_ASSET, + { DESTINATION_ADDR, QRWA_CONTRACT_INDEX }, + { DESTINATION_ADDR, QRWA_CONTRACT_INDEX }); + EXPECT_EQ(destManagedByQrwa, 0); + + // DESTINATION_ADDR's shares should now be managed by QX + destManagedByQx = numberOfShares(QMINE_ASSET, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }); + EXPECT_EQ(destManagedByQx, releaseAmount); + + // check if the fee was paid by the user + sint64 destBalanceAfterRevoke = getBalance(DESTINATION_ADDR); + EXPECT_EQ(destBalanceAfterRevoke, destBalanceBeforeRevoke - QRWA_RELEASE_MANAGEMENT_FEE); + + // Critical check: + // Verify that the fee sent to the SC was NOT permanently added to Pool B. + // The POST_INCOMING_TRANSFER adds 100 QU to Pool B. + // The procedure executes, spends 100 QU to QX, and logic must subtract 100 from Pool B. + // Net result for Pool B must be 0. + auto finalDivBalances = qrwa.getDividendBalances(); + EXPECT_EQ(finalDivBalances.revenuePoolB, 0); +} + +TEST(ContractQRWA, Treasury_Donation) +{ + ContractTestingQRWA qrwa; + + // Issue QMINE to the temporary treasury holder + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 150000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 150000000); + + qrwa.transferAsset(QMINE_ISSUER, TREASURY_HOLDER, QMINE_ASSET, 150000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { TREASURY_HOLDER, QX_CONTRACT_INDEX }, + { TREASURY_HOLDER, QX_CONTRACT_INDEX }), 150000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 0); + + increaseEnergy(TREASURY_HOLDER, 1000000); + + // Fail (No Management Rights) + EXPECT_EQ(qrwa.donateToTreasury(TREASURY_HOLDER, 1000), QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE); + + // Success (With Management Rights) + // Give SC management rights + qrwa.transferManagementRights(TREASURY_HOLDER, QMINE_ASSET, 150000000, QRWA_CONTRACT_INDEX); + + // Verify rights + sint64 managedBalance = numberOfShares(QMINE_ASSET, + { TREASURY_HOLDER, QRWA_CONTRACT_INDEX }, + { TREASURY_HOLDER, QRWA_CONTRACT_INDEX }); + EXPECT_EQ(managedBalance, 150000000); + + // Donate + EXPECT_EQ(qrwa.donateToTreasury(TREASURY_HOLDER, 150000000), QRWA_STATUS_SUCCESS); + + // Verify treasury balance in SC + EXPECT_EQ(qrwa.getTreasuryBalance(), 150000000); + + // Verify SC now owns the shares + sint64 scOwnedBalance = numberOfShares(QMINE_ASSET, + { id(QRWA_CONTRACT_INDEX, 0, 0, 0), QRWA_CONTRACT_INDEX }, + { id(QRWA_CONTRACT_INDEX, 0, 0, 0), QRWA_CONTRACT_INDEX }); + EXPECT_EQ(scOwnedBalance, 150000000); +} + +TEST(ContractQRWA, Payout_FullDistribution) +{ + ContractTestingQRWA qrwa; + + // Issue QMINE, distribute, and run BEGIN_EPOCH + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1000000); + + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(HOLDER_C, 1000000); + increaseEnergy(USER_D, 1000000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 200000); // Holder A + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 200000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 800000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); // Holder B + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 300000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 500000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 100000); // Holder C + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 100000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 400000); + + qrwa.beginEpoch(); + // mTotalQmineBeginEpoch = 1,000,000 + + // Mid-epoch transfers + qrwa.transferAsset(HOLDER_A, USER_D, QMINE_ASSET, 50000); // Holder A ends with 150k + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 50000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 150000); + + qrwa.transferAsset(HOLDER_C, USER_D, QMINE_ASSET, 100000); // Holder C ends with 0 + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 150000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 0); + + // Deposit revenue + // Pool A (from SC) + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + // Pool B (from User) - Untestable. We will proceed using only Pool A. + + qrwa.endEpoch(); + + // Set time to payout day + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 7; // A Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + + // Use helper to reset payout time + qrwa.resetPayoutTime(); // Reset time to allow payout + + // Call END_TICK to trigger DistributeRewards + qrwa.endTick(); + + // Verification + // Fees: Pool A = 1M + // Elec (35%) = 350,000 + // Maint (5%) = 50,000 + // Reinv (10%) = 100,000 + // Total Fees = 500,000 + EXPECT_EQ(getBalance(FEE_ADDR_E), 350000); + EXPECT_EQ(getBalance(FEE_ADDR_M), 50000); + EXPECT_EQ(getBalance(FEE_ADDR_R), 100000); + + // Distribution Pool + // Y_revenue = 1,000,000 - 500,000 = 500,000 + // totalDistribution = 500,000 (Y) + 0 (B) = 500,000 + // mQmineDividendPool = 500k * 90% = 450,000 + // mQRWADividendPool = 500k * 10% = 50,000 + + // qRWA Payout (50,000 QUs) + uint64 qrwaPerShare = 50000 / NUMBER_OF_COMPUTORS; // 73 + auto distTotals = qrwa.getTotalDistributed(); + EXPECT_EQ(distTotals.totalQRWADistributed, qrwaPerShare * NUMBER_OF_COMPUTORS); // 73 * 676 = 49328 + + // QMINE Payout (450,000 QUs) + // mPayoutTotalQmineBegin = 1,000,000 + + // Eligible Balances: + // H1: min(200k, 150k) = 150,000 + // H2: min(300k, 300k) = 300,000 + // H3: min(100k, 0) = 0 + // Issuer: min(400k, 400k) = 400,000 + // Total Eligible = 850,000 + + // Payouts: + // H1 Payout: (150,000 * 450,000) / 1,000,000 = 67,500 + // H2 Payout: (300,000 * 450,000) / 1,000,000 = 135,000 + // H3 Payout: 0 + // Issuer Payout: (400,000 * 450,000) / 1,000,000 = 180,000 + // Total Eligible Paid = 67,500 + 135,000 + 180,000 = 382,500 + // QMINE_DEV Payout (Remainder) = 450,000 - 382,500 = 67,500 + + EXPECT_EQ(getBalance(HOLDER_A), 1000000 + 67500); + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 135000); + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 0); + EXPECT_EQ(getBalance(QMINE_DEV_ADDR_TEST), 67500); + + // Re-check balances + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 135000); + + // Check pools are empty (or contain only dust from integer division) + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 0); + EXPECT_EQ(divBalances.revenuePoolB, 0); + EXPECT_EQ(divBalances.qmineDividendPool, 0); + EXPECT_EQ(divBalances.qrwaDividendPool, 50000 - (qrwaPerShare * NUMBER_OF_COMPUTORS)); // Dust +} + +TEST(ContractQRWA, Payout_SnapshotLogic) +{ + ContractTestingQRWA qrwa; + + // Give energy to all participants + increaseEnergy(QMINE_ISSUER, 1000000000); + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(HOLDER_C, 1000000); + increaseEnergy(USER_D, 1000000); + + // Issue 3500 QMINE + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 3500); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, { QMINE_ISSUER, QX_CONTRACT_INDEX }), 3500); + + // Epoch 1 Setup: Distribute initial shares + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 1000); + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 1000); + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 1000); + // QMINE_ISSUER keeps 500 + + qrwa.beginEpoch(); + // Snapshot (Begin Epoch 1): + // Total: 3500 (A, B, C, Issuer) + // A: 1000 + // B: 1000 + // C: 1000 + // D: 0 + // Issuer: 500 + + // Epoch 1 Mid-Epoch Transfers + qrwa.transferAsset(HOLDER_A, USER_D, QMINE_ASSET, 500); // A: 500, D: 500 + qrwa.transferAsset(HOLDER_B, USER_D, QMINE_ASSET, 1000); // B: 0, D: 1500 + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 500); // C: 1500, Issuer: 0 + + // Deposit 1M QUs into Pool A + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + qrwa.endEpoch(); + // Payout Snapshots (Epoch 1): + // mPayoutTotalQmineBegin: 3500 + // Eligible: + // A: min(1000, 500) = 500 + // B: min(1000, 0) = 0 + // C: min(1000, 1500) = 1000 + // D: (not in begin map) = 0 + // Issuer: min(500, 0) = 0 + // Total Eligible: 1500 + + // Payout Calculation (Epoch 1): + // Pool A: 1,000,000 -> Fees (50%) = 500,000 -> Y_revenue = 500,000 + // mQmineDividendPool (90%): 450,000 + // mQRWADividendPool (10%): 50,000 + + // Payouts: + // A: (500 * 450,000) / 3,500 = 64,285 + // B: 0 + // C: (1000 * 450,000) / 3,500 = 128,571 + // D: 0 + // Issuer: 0 + // totalEligiblePaid = 192,856 + // movedSharesPayout (QMINE_DEV) = 450,000 - 192,856 = 257,144 + + // Trigger Payout + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 14; // Next Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + qrwa.resetPayoutTime(); + qrwa.endTick(); + + // Verify Payout 1 + EXPECT_EQ(getBalance(HOLDER_A), 1000000 + 64285); + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 0); + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 128571); + EXPECT_EQ(getBalance(USER_D), 1000000 + 0); + EXPECT_EQ(getBalance(QMINE_DEV_ADDR_TEST), 257144); + + // Check C's balance again + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 128571); + + + // Epoch 2 + qrwa.beginEpoch(); + // Snapshot (Begin Epoch 2): + // Total: 3500 + // A: 500, B: 0, C: 1500, D: 1500, Issuer: 0 + + // Epoch 2 Mid-Epoch Transfers + qrwa.transferAsset(USER_D, HOLDER_A, QMINE_ASSET, 500); // A: 1000, D: 1000 + qrwa.transferAsset(HOLDER_C, HOLDER_B, QMINE_ASSET, 1000); // C: 500, B: 1000 + + // Deposit 1M QUs into Pool A + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + qrwa.endEpoch(); + // Snapshot (End Epoch 2): + // A: 1000, B: 1000, C: 500, D: 1000, Issuer: 0 + // + // Payout Snapshots (Epoch 2): + // mPayoutTotalQmineBegin: 3500 + // Eligible: + // A: min(500, 1000) = 500 + // B: min(0, 1000) = 0 + // C: min(1500, 500) = 500 + // D: min(1500, 1000) = 1000 + // Total Eligible: 2000 + + // Payout Calculation (Epoch 2): + // Pool A: 1,000,000 -> Fees (50%) = 500,000 -> Y_revenue = 500,000 + // mQmineDividendPool (90%): 450,000 + // Payouts: + // A: (500 * 450,000) / 3,500 = 64,285 + // B: 0 + // C: (500 * 450,000) / 3,500 = 64,285 + // D: (1000 * 450,000) / 3,500 = 128,571 + // totalEligiblePaid = 257,141 + // movedSharesPayout (QMINE_DEV) = 450,000 - 257,141 = 192,859 + + // Trigger Payout 2 + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 21; // Next Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + qrwa.resetPayoutTime(); + qrwa.endTick(); + + // Verify Payout 2 (Cumulative) + // A: Base + payout1 + payout2 + EXPECT_EQ(getBalance(HOLDER_A), 1000000 + 64285 + 64285); + // B: Base + payout1 + payout2 + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 0 + 0); + // C: Base + payout1 + payout2 + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 128571 + 64285); + // D: Base + payout1 + payout2 + EXPECT_EQ(getBalance(USER_D), 1000000 + 0 + 128571); + // QMINE dev: payout1 + payout2 + EXPECT_EQ(getBalance(QMINE_DEV_ADDR_TEST), 257144 + 192859); +} + +TEST(ContractQRWA, Payout_FullDistribution2) +{ + ContractTestingQRWA qrwa; + + // Issue QMINE, distribute, and run BEGIN_EPOCH + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1000000); + + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(HOLDER_C, 1000000); + increaseEnergy(USER_D, 1000000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 200000); // Holder A + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 200000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 800000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); // Holder B + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 300000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 500000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 100000); // Holder C + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 100000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 400000); + + qrwa.beginEpoch(); + // mTotalQmineBeginEpoch = 1,000,000 (A:200k, B:300k, C:100k, Issuer:400k) + + // Mid-epoch transfers + qrwa.transferAsset(HOLDER_A, USER_D, QMINE_ASSET, 50000); // Holder A ends with 150k + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 50000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 150000); + + qrwa.transferAsset(HOLDER_C, USER_D, QMINE_ASSET, 100000); // Holder C ends with 0 + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 150000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 0); + + // Deposit revenue + // Pool A (from SC) + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 3000000); // Increased revenue + + // Pool B (from User): Untestable. We will proceed using only Pool A. + + qrwa.endEpoch(); + + // Set time to payout day + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 7; // A Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + + // Use helper to reset payout time + qrwa.resetPayoutTime(); // Reset time to allow payout + + // Call END_TICK to trigger DistributeRewards + qrwa.endTick(); + + // Verification + // Fees: Pool A = 3M + // Elec (35%) = 1,050,000 + // Maint (5%) = 150,000 + // Reinv (10%) = 300,000 + // Total Fees = 1,500,000 + EXPECT_EQ(getBalance(FEE_ADDR_E), 1050000); + EXPECT_EQ(getBalance(FEE_ADDR_M), 150000); + EXPECT_EQ(getBalance(FEE_ADDR_R), 300000); + + // Distribution Pool + // Y_revenue = 3,000,000 - 1,500,000 = 1,500,000 + // totalDistribution = 1,500,000 (Y) + 0 (B) = 1,500,000 + // mQmineDividendPool = 1.5M * 90% = 1,350,000 + // mQRWADividendPool = 1.5M * 10% = 150,000 + + // qRWA Payout (150,000 QUs) + uint64 qrwaPerShare = 150000 / NUMBER_OF_COMPUTORS; // 150000 / 676 = 221 + auto distTotals = qrwa.getTotalDistributed(); + EXPECT_EQ(distTotals.totalQRWADistributed, qrwaPerShare * NUMBER_OF_COMPUTORS); // 221 * 676 = 149416 + + // QMINE Payout (1,350,000 QUs) + // mPayoutTotalQmineBegin = 1,000,000 (A:200k, B:300k, C:100k, Issuer:400k) + + // Eligible: + // H1: min(200k, 150k) = 150,000 + // H2: min(300k, 300k) = 300,000 + // H3: min(100k, 0) = 0 + // Issuer: min(400k, 400k) = 400,000 + // Total Eligible = 850,000 + + // Payouts: + // H1 Payout: (150,000 * 1,350,000) / 1,000,000 = 202,500 + // H2 Payout: (300,000 * 1,350,000) / 1,000,000 = 405,000 + // H3 Payout: 0 + // Issuer Payout: (400,000 * 1,350,000) / 1,000,000 = 540,000 + // Total Eligible Paid = 202,500 + 405,000 + 540,000 = 1,147,500 + // QMINE dev Payout (Remainder) = 1,350,000 - 1,147,500 = 202,500 + + EXPECT_EQ(getBalance(HOLDER_A), 1000000 + 202500); + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 405000); + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 0); + EXPECT_EQ(getBalance(QMINE_DEV_ADDR_TEST), 202500); + + // Re-check B's balance + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 405000); + + + // Check pools are empty (or contain only dust from integer division) + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 0); + EXPECT_EQ(divBalances.revenuePoolB, 0); + EXPECT_EQ(divBalances.qmineDividendPool, 0); // QMINE dev gets the remainder + EXPECT_EQ(divBalances.qrwaDividendPool, 150000 - (qrwaPerShare * NUMBER_OF_COMPUTORS)); // Dust (584) +} + +TEST(ContractQRWA, FullScenario_DividendsAndGovernance) +{ + ContractTestingQRWA qrwa; + + /* --- SETUP --- */ + + etalonTick.year = 25; // 2025 + etalonTick.month = 11; // November + etalonTick.day = 7; // 7th (Friday) + etalonTick.hour = 12; + etalonTick.minute = 1; + etalonTick.second = 0; + etalonTick.millisecond = 0; + + // Helper to handle month rollovers for this test + auto advanceTime7Days = [&]() + { + etalonTick.day += 7; + // Simple logic for Nov/Dec 2025 + if (etalonTick.month == 11 && etalonTick.day > 30) { + etalonTick.day -= 30; + etalonTick.month++; + } + else if (etalonTick.month == 12 && etalonTick.day > 31) { + etalonTick.day -= 31; + etalonTick.month = 1; + etalonTick.year++; + } + }; + + // Constants + const sint64 TOTAL_SUPPLY = 1000000000000LL; // 1,000,000,000,000 = 1 Trillion + const sint64 TREASURY_INIT = 150000000000LL; // 150 Billion + const sint64 SHAREHOLDERS_TOTAL = 850000000000LL; // 850 Billion + const sint64 SHAREHOLDER_AMT = SHAREHOLDERS_TOTAL / 5; // 170 Billion each + const sint64 REVENUE_AMT = 10000000LL; // 10 Million QUs per epoch revenue + + // Known Pool Amounts derived from REVENUE_AMT and 50% total fees + // Revenue 10M -> Fees 5M -> Net 5M + const sint64 QMINE_POOL_AMT = 4500000LL; // 90% of 5M + const sint64 QRWA_POOL_AMT_BASE = 500000LL; // 10% of 5M + + const sint64 QRWA_TOTAL_SHARES = 676LL; + + // Track dust for qRWA pool to calculate accurate rates per epoch + sint64 currentQrwaDust = 0; + sint64 currentQXReleaseFee = 0; + + auto getQrwaRateForEpoch = [&](sint64 poolAmount) -> sint64 { + sint64 totalPool = poolAmount + currentQrwaDust; + sint64 rate = totalPool / QRWA_TOTAL_SHARES; + currentQrwaDust = totalPool % QRWA_TOTAL_SHARES; // Update dust for next epoch + return rate; + }; + + // Entities + const id S1 = id::randomValue(); // Hybrid: Holds QMINE + qRWA shares + const id S2 = id::randomValue(); // Control QMINE: Holds only QMINE + const id S3 = id::randomValue(); // QMINE only + const id S4 = id::randomValue(); // QMINE only + const id S5 = id::randomValue(); // QMINE only + const id Q1 = id::randomValue(); // Control qRWA: Holds only qRWA shares + const id Q2 = id::randomValue(); // qRWA only + + // Energy Funding + increaseEnergy(QMINE_ISSUER, QX_ISSUE_ASSET_FEE * 2 + 100000000); + increaseEnergy(TREASURY_HOLDER, 100000000); + increaseEnergy(S1, 100000000); + increaseEnergy(S2, 100000000); + increaseEnergy(S3, 100000000); + increaseEnergy(S4, 100000000); + increaseEnergy(S5, 100000000); + increaseEnergy(Q1, 100000000); + increaseEnergy(Q2, 100000000); + increaseEnergy(DESTINATION_ADDR, 1000000); + increaseEnergy(ADMIN_ADDRESS, 1000000); + + // Issue QMINE + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, TOTAL_SUPPLY); + + // Distribute to Treasury Holder + qrwa.transferAsset(QMINE_ISSUER, TREASURY_HOLDER, QMINE_ASSET, TREASURY_INIT); + + // Distribute to 5 Shareholders (170B each) + qrwa.transferAsset(QMINE_ISSUER, S1, QMINE_ASSET, SHAREHOLDER_AMT); + qrwa.transferAsset(QMINE_ISSUER, S2, QMINE_ASSET, SHAREHOLDER_AMT); + qrwa.transferAsset(QMINE_ISSUER, S3, QMINE_ASSET, SHAREHOLDER_AMT); + qrwa.transferAsset(QMINE_ISSUER, S4, QMINE_ASSET, SHAREHOLDER_AMT); + qrwa.transferAsset(QMINE_ISSUER, S5, QMINE_ASSET, SHAREHOLDER_AMT); + + // Issue and Distribute qrwa Contract Shares + std::vector> qrwaShares{ + {S1, 200}, + {Q1, 200}, + {Q2, 276} + }; + issueContractShares(QRWA_CONTRACT_INDEX, qrwaShares); + + // Snapshot balances + std::map prevBalances; + auto snapshotBalances = [&]() { + prevBalances[S1] = getBalance(S1); + prevBalances[S2] = getBalance(S2); + prevBalances[S3] = getBalance(S3); + prevBalances[S4] = getBalance(S4); + prevBalances[S5] = getBalance(S5); + prevBalances[Q1] = getBalance(Q1); + prevBalances[Q2] = getBalance(Q2); + prevBalances[DESTINATION_ADDR] = getBalance(DESTINATION_ADDR); + }; + snapshotBalances(); + + // Helper to calculate exact QMINE payout matching contract logic + // Payout = (EligibleBalance * DividendPool) / PayoutBase + auto calculateQminePayout = [&](sint64 balance, sint64 payoutBase, sint64 poolAmount) -> sint64 { + if (payoutBase == 0) return 0; + // Contract uses: div((uint128)balance * pool, totalEligible) + // We mimic that integer math here + uint128 res = (uint128)balance * (uint128)poolAmount; + res = res / (uint128)payoutBase; + return (sint64)res.low; + }; + + // Helper that uses the calculated rate for the current epoch + auto calculateQrwaPayout = [&](sint64 shares, sint64 currentRate) -> sint64 { + return shares * currentRate; + }; + +#if ENABLE_BALANCE_DEBUG + auto print_balances = [&]() + { + std::cout << "\n--- Current Balances ---" << std::endl; + std::cout << "S1: " << getBalance(S1) << std::endl; + std::cout << "S2: " << getBalance(S2) << std::endl; + std::cout << "S3: " << getBalance(S3) << std::endl; + std::cout << "S4: " << getBalance(S4) << std::endl; + std::cout << "S5: " << getBalance(S5) << std::endl; + std::cout << "Q1: " << getBalance(Q1) << std::endl; + std::cout << "Q2: " << getBalance(Q2) << std::endl; + std::cout << "Dest: " << getBalance(DESTINATION_ADDR) << std::endl; + std::cout << "Treasury: " << getBalance(TREASURY_HOLDER) << std::endl; + std::cout << "Dev: " << getBalance(QMINE_DEV_ADDR_TEST) << std::endl; + std::cout << "------------------------\n" << std::endl; + }; + + std::cout << "PRE-EPOCH 1\n"; + print_balances(); +#endif + // epoch 1 + qrwa.beginEpoch(); + + //Shareholders Exchange + qrwa.transferAsset(S1, S2, QMINE_ASSET, 10000000000LL); + qrwa.transferAsset(S3, S4, QMINE_ASSET, 5000000000LL); + + // Treasury Donation + qrwa.transferManagementRights(TREASURY_HOLDER, QMINE_ASSET, 10, QRWA_CONTRACT_INDEX); + EXPECT_EQ(qrwa.donateToTreasury(TREASURY_HOLDER, 10), QRWA_STATUS_SUCCESS); + + //Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + qrwa.endEpoch(); + + // Checks Ep 1 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << "END-EPOCH 1\n"; + print_balances(); +#endif + + // Contract holds 10 shares. Base = Total Supply - 10 + sint64 payoutBaseEp1 = TOTAL_SUPPLY - 10; + sint64 qrwaRateEp1 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); // Standard pool for Ep 1 + + sint64 divS1 = calculateQminePayout(160000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + sint64 divS2 = calculateQminePayout(170000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + sint64 divS3 = calculateQminePayout(165000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + sint64 divS4 = calculateQminePayout(170000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + sint64 divS5 = calculateQminePayout(170000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + + sint64 divQS1 = calculateQrwaPayout(200, qrwaRateEp1); + sint64 divQQ1 = calculateQrwaPayout(200, qrwaRateEp1); + sint64 divQQ2 = calculateQrwaPayout(276, qrwaRateEp1); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << "PRE-EPOCH 2\n"; + print_balances(); +#endif + + // epoch 2 + qrwa.beginEpoch(); + + // Treasury Donation (Remaining) + sint64 treasuryRemaining = TREASURY_INIT - 10; + qrwa.transferManagementRights(TREASURY_HOLDER, QMINE_ASSET, treasuryRemaining, QRWA_CONTRACT_INDEX); + EXPECT_EQ(qrwa.donateToTreasury(TREASURY_HOLDER, treasuryRemaining), QRWA_STATUS_SUCCESS); + + // Exchange + qrwa.transferAsset(S1, S2, QMINE_ASSET, 10000000000LL); + qrwa.transferAsset(S2, S3, QMINE_ASSET, 10000000000LL); + qrwa.transferAsset(S3, S4, QMINE_ASSET, 10000000000LL); + qrwa.transferAsset(S4, S5, QMINE_ASSET, 10000000000LL); + + // Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Release Poll + QRWA::CreateAssetReleasePoll_input pollInput; + pollInput.proposalName = id::randomValue(); + pollInput.asset = QMINE_ASSET; + pollInput.amount = 1000; + pollInput.destination = DESTINATION_ADDR; + + auto pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + uint64 pollIdEp2 = pollOut.proposalId; + + EXPECT_EQ(qrwa.voteAssetRelease(S1, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S2, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S3, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S4, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S5, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(Q1, pollIdEp2, 1), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + qrwa.endEpoch(); + + // Checks Ep 2 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << "END-EPOCH 2\n"; + print_balances(); +#endif + + auto pollResultEp2 = qrwa.getAssetReleasePoll(pollIdEp2); + EXPECT_EQ(pollResultEp2.proposal.status, QRWA_POLL_STATUS_PASSED_EXECUTED); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { DESTINATION_ADDR, QX_CONTRACT_INDEX }), 1000); + + // Calculate Pools based on Revenue - 100 QU Fee + sint64 netRevenueEp2 = REVENUE_AMT - 100; + sint64 feeAmtEp2 = (netRevenueEp2 * 500) / 1000; // 50% fees + sint64 distributableEp2 = netRevenueEp2 - feeAmtEp2; + sint64 qminePoolEp2 = (distributableEp2 * 900) / 1000; + sint64 qrwaPoolEp2 = distributableEp2 - qminePoolEp2; + + // Correct Base: TOTAL_SUPPLY - 10 (Shares held by SC at START of epoch) + sint64 payoutBaseEp2 = TOTAL_SUPPLY - 10; + + sint64 qrwaRateEp2 = getQrwaRateForEpoch(qrwaPoolEp2); + + divS1 = calculateQminePayout(150000000000LL, payoutBaseEp2, qminePoolEp2); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp2, qminePoolEp2); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp2, qminePoolEp2); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp2, qminePoolEp2); + divS5 = calculateQminePayout(170000000000LL, payoutBaseEp2, qminePoolEp2); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp2); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp2); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp2); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 3\n"; + print_balances(); +#endif + + // epoch 3 + qrwa.beginEpoch(); + + // Exchange + qrwa.transferAsset(S1, S2, QMINE_ASSET, 5000000000LL); + qrwa.transferAsset(S2, S3, QMINE_ASSET, 5000000000LL); + qrwa.transferAsset(S3, S4, QMINE_ASSET, 5000000000LL); + qrwa.transferAsset(S4, S5, QMINE_ASSET, 5000000000LL); + + // Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Release Poll + pollInput.amount = 500; + pollInput.proposalName = id::randomValue(); + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + uint64 pollIdEp3 = pollOut.proposalId; + + EXPECT_EQ(qrwa.voteAssetRelease(S1, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S2, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S3, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S4, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S5, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(Q1, pollIdEp3, 1), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + // Gov Vote + QRWA::QRWAGovParams newParams = qrwa.getGovParams(); + newParams.electricityPercent = 300; + newParams.maintenancePercent = 100; + + newParams.mAdminAddress = ADMIN_ADDRESS; + newParams.qmineDevAddress = QMINE_DEV_ADDR_TEST; + newParams.electricityAddress = FEE_ADDR_E; + newParams.maintenanceAddress = FEE_ADDR_M; + newParams.reinvestmentAddress = FEE_ADDR_R; + + EXPECT_EQ(qrwa.voteGovParams(S1, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(S2, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(S3, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(S4, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(S5, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(Q1, newParams), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + qrwa.endEpoch(); + + // Checks Ep 3 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 3\n"; + print_balances(); +#endif + + auto pollResultEp3 = qrwa.getAssetReleasePoll(pollIdEp3); + EXPECT_EQ(pollResultEp3.proposal.status, QRWA_POLL_STATUS_PASSED_EXECUTED); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { DESTINATION_ADDR, QX_CONTRACT_INDEX }), 1000 + 500); + + auto activeParams = qrwa.getGovParams(); + EXPECT_EQ(activeParams.electricityPercent, 300); + EXPECT_EQ(activeParams.maintenancePercent, 100); + + // Calculate Pools based on Revenue - 100 QU Fee + sint64 netRevenueEp3 = REVENUE_AMT - 100; + sint64 feeAmtEp3 = (netRevenueEp3 * 500) / 1000; // 50% fees still (params update next epoch) + sint64 distributableEp3 = netRevenueEp3 - feeAmtEp3; + sint64 qminePoolEp3 = (distributableEp3 * 900) / 1000; + sint64 qrwaPoolEp3 = distributableEp3 - qminePoolEp3; + + // Contract released 1000 + 500. Balance = 150B - 1500. + sint64 payoutBaseEp3 = TOTAL_SUPPLY - (TREASURY_INIT - 1500); + + sint64 qrwaRateEp3 = getQrwaRateForEpoch(qrwaPoolEp3); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp3, qminePoolEp3); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp3, qminePoolEp3); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp3, qminePoolEp3); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp3, qminePoolEp3); + divS5 = calculateQminePayout(180000000000LL, payoutBaseEp3, qminePoolEp3); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp3); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp3); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp3); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 4\n"; + print_balances(); +#endif + + // epoch 4 (no transfers) + qrwa.beginEpoch(); + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + qrwa.endEpoch(); + + // Checks Ep 4 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 4\n"; + print_balances(); +#endif + + // Payout base remains same as previous epoch (no new releases) + sint64 payoutBaseEp4 = payoutBaseEp3; + // Revenue is full 10M (no releases) + sint64 qminePoolEp4 = QMINE_POOL_AMT; + + sint64 qrwaRateEp4 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp4, qminePoolEp4); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp4, qminePoolEp4); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp4, qminePoolEp4); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp4, qminePoolEp4); + divS5 = calculateQminePayout(185000000000LL, payoutBaseEp4, qminePoolEp4); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp4); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp4); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp4); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 5\n"; + print_balances(); +#endif + + // epoch 5 + qrwa.beginEpoch(); + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Release Poll + pollInput.amount = 100; + pollInput.proposalName = id::randomValue(); + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + uint64 pollIdEp5 = pollOut.proposalId; + + // Vote NO (3/5 Majority) + EXPECT_EQ(qrwa.voteAssetRelease(S1, pollIdEp5, 0), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S2, pollIdEp5, 0), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S3, pollIdEp5, 0), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S4, pollIdEp5, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S5, pollIdEp5, 1), QRWA_STATUS_SUCCESS); + + qrwa.endEpoch(); + + // Checks Ep 5 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 5\n"; + print_balances(); +#endif + + auto pollResultEp5 = qrwa.getAssetReleasePoll(pollIdEp5); + EXPECT_EQ(pollResultEp5.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { DESTINATION_ADDR, QX_CONTRACT_INDEX }), 1500); // Unchanged + + // Failed vote = No release = No fee = Full Revenue. Base unchanged. + sint64 qrwaRateEp5 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp4, qminePoolEp4); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp4, qminePoolEp4); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp4, qminePoolEp4); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp4, qminePoolEp4); + divS5 = calculateQminePayout(185000000000LL, payoutBaseEp4, qminePoolEp4); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp5); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp5); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp5); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 6\n"; + print_balances(); +#endif + + // epoch 6 + qrwa.beginEpoch(); + + // Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Create Gov Proposal + QRWA::QRWAGovParams failParams = qrwa.getGovParams(); + failParams.reinvestmentPercent = 200; + + // Only S1 votes (< 20% supply). Quorum fail + EXPECT_EQ(qrwa.voteGovParams(S1, failParams), QRWA_STATUS_SUCCESS); + + qrwa.endEpoch(); + + // Checks Ep 6 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 6\n"; + print_balances(); +#endif + + auto paramsEp6 = qrwa.getGovParams(); + EXPECT_EQ(paramsEp6.reinvestmentPercent, 100); + EXPECT_NE(paramsEp6.reinvestmentPercent, 200); + + sint64 qrwaRateEp6 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp4, qminePoolEp4); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp4, qminePoolEp4); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp4, qminePoolEp4); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp4, qminePoolEp4); + divS5 = calculateQminePayout(185000000000LL, payoutBaseEp4, qminePoolEp4); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp6); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp6); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp6); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 7\n"; + print_balances(); +#endif + + // epoch 7 + qrwa.beginEpoch(); + + // Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Create poll, no votes + pollInput.amount = 100; + pollInput.proposalName = id::randomValue(); + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + uint64 pollIdEp7 = pollOut.proposalId; + + qrwa.endEpoch(); + + // Checks Ep 7 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 7\n"; + print_balances(); +#endif + + auto pollResultEp7 = qrwa.getAssetReleasePoll(pollIdEp7); + EXPECT_EQ(pollResultEp7.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + + sint64 qrwaRateEp7 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp4, qminePoolEp4); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp4, qminePoolEp4); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp4, qminePoolEp4); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp4, qminePoolEp4); + divS5 = calculateQminePayout(185000000000LL, payoutBaseEp4, qminePoolEp4); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp7); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp7); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp7); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); +} + +TEST(ContractQRWA, Payout_MultiContractManagement) +{ + ContractTestingQRWA qrwa; + + const sint64 totalShares = 1000000; + const sint64 qxManagedShares = 700000; + const sint64 qswapManagedShares = 300000; // 30% moved to QSWAP management + + // Issue QMINE and give to HOLDER_A + // Initially, all 1M shares are managed by QX (default for transfers via QX) + increaseEnergy(QMINE_ISSUER, 1000000000); + increaseEnergy(HOLDER_A, 1000000); // For fees + + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, totalShares); + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, totalShares); + + // Verify initial state managed by QX + EXPECT_EQ(numberOfPossessedShares(QMINE_ASSET.assetName, QMINE_ASSET.issuer, HOLDER_A, HOLDER_A, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), totalShares); + EXPECT_EQ(numberOfPossessedShares(QMINE_ASSET.assetName, QMINE_ASSET.issuer, HOLDER_A, HOLDER_A, QSWAP_CONTRACT_INDEX, QSWAP_CONTRACT_INDEX), 0); + + // Transfer management rights of 300k shares to QSWAP + // The user (HOLDER_A) remains the Possessor. + qrwa.transferManagementRights(HOLDER_A, QMINE_ASSET, qswapManagedShares, QSWAP_CONTRACT_INDEX); + + // Verify the split in management rights + // 700k should remain under QX + EXPECT_EQ(numberOfPossessedShares(QMINE_ASSET.assetName, QMINE_ASSET.issuer, HOLDER_A, HOLDER_A, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), qxManagedShares); + // 300k should now be under QSWAP + EXPECT_EQ(numberOfPossessedShares(QMINE_ASSET.assetName, QMINE_ASSET.issuer, HOLDER_A, HOLDER_A, QSWAP_CONTRACT_INDEX, QSWAP_CONTRACT_INDEX), qswapManagedShares); + + qrwa.beginEpoch(); + + // Generate Revenue + // pool A revenue: 1,000,000 QUs + // fees (50%): 500,000 + // net revenue: 500,000 + // QMINE pool (90%): 450,000 + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + qrwa.endEpoch(); + + // trigger Payout + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 7; // Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + qrwa.resetPayoutTime(); + + // snapshot balances for check + sint64 balanceBefore = getBalance(HOLDER_A); + + qrwa.endTick(); + + // Calculate Expected Payout + // Payout = (UserTotalShares * PoolAmount) / TotalSupply + // UserTotalShares = 1,000,000 (regardless of manager) + // PoolAmount = 450,000 + // TotalSupply = 1,000,000 + // Expected = 450,000 + sint64 expectedPayout = (totalShares * 450000) / totalShares; + + sint64 balanceAfter = getBalance(HOLDER_A); + + // If qRWA only counted QX shares, the payout would be (700k/1M * 450k) = 315,000. + // If qRWA counts ALL shares, the payout is 450,000. + EXPECT_EQ(balanceAfter - balanceBefore, expectedPayout); + EXPECT_EQ(balanceAfter - balanceBefore, 450000); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 7eda0af5b..5132f62d4 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -121,6 +121,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 391098eb9..725c90fc9 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -47,6 +47,7 @@ + From 129010719a9395db02f2700b71c6a7965787caeb Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:49:06 +0100 Subject: [PATCH 20/90] qRWA: change construction epoch from 198 to 197 (as confirmed with Eko) --- src/contract_core/contract_def.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 44652f12d..65139e477 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -318,7 +318,7 @@ constexpr struct ContractDescription {"QBOND", 182, 10000, sizeof(QBOND)}, // proposal in epoch 180, IPO in 181, construction and first use in 182 {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 - {"QRWA", 198, 10000, sizeof(QRWA)}, // proposal in epoch 196, IPO in 197, construction and first use in 198 + {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA)}, From e9293e7cce0d7f18e7db87aa11d762260deb72e7 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:53:13 +0100 Subject: [PATCH 21/90] add NO_QRWA toggle --- src/contract_core/contract_def.h | 8 ++++++++ src/qubic.cpp | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 65139e477..a0bc113bb 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -201,6 +201,8 @@ #define CONTRACT_STATE2_TYPE QRAFFLE2 #include "contracts/QRaffle.h" +#ifndef NO_QRWA + #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE @@ -211,6 +213,8 @@ #define CONTRACT_STATE2_TYPE QRWA2 #include "contracts/qRWA.h" +#endif + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -318,7 +322,9 @@ constexpr struct ContractDescription {"QBOND", 182, 10000, sizeof(QBOND)}, // proposal in epoch 180, IPO in 181, construction and first use in 182 {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 +#ifndef NO_QRWA {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 +#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA)}, @@ -434,7 +440,9 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QBOND); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE); +#ifndef NO_QRWA REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRWA); +#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/qubic.cpp b/src/qubic.cpp index b148b224f..b5ce9d40f 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,5 +1,7 @@ #define SINGLE_COMPILE_UNIT +// #define NO_QRWA + // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" From 9e53369467bd9d8a57b7c5f6bbf778183c74a928 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:58:35 +0100 Subject: [PATCH 22/90] update params for epoch 196 / v1.274.0 --- src/public_settings.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/public_settings.h b/src/public_settings.h index a41a65282..7f073ca74 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -57,7 +57,7 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // If this flag is 1, it indicates that the whole network (all 676 IDs) will start from scratch and agree that the very first tick time will be set at (2022-04-13 Wed 12:00:00.000UTC). // If this flag is 0, the node will try to fetch data of the initial tick of the epoch from other nodes, because the tick's timestamp may differ from (2022-04-13 Wed 12:00:00.000UTC). // If you restart your node after seamless epoch transition, make sure EPOCH and TICK are set correctly for the currently running epoch. -#define START_NETWORK_FROM_SCRATCH 0 +#define START_NETWORK_FROM_SCRATCH 1 // Addons: If you don't know it, leave it 0. #define ADDON_TX_STATUS_REQUEST 0 @@ -66,12 +66,12 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // Config options that should NOT be changed by operators #define VERSION_A 1 -#define VERSION_B 273 +#define VERSION_B 274 #define VERSION_C 0 // Epoch and initial tick for node startup -#define EPOCH 195 -#define TICK 41622000 +#define EPOCH 196 +#define TICK 42232000 #define TICK_IS_FIRST_TICK_OF_EPOCH 1 // Set to 0 if the network is restarted during the EPOCH with a new initial TICK #define ARBITRATOR "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" From feb6a05b1b8465e8fead7cbd9ff4a1ec0a08d693 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:44:44 +0100 Subject: [PATCH 23/90] Fix bug in processRequestOracleData() --- src/oracle_core/net_msg_impl.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index 03b7ea76b..94934891f 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -134,6 +134,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R { const uint16_t replySize = (uint16_t)OI::oracleInterfaces[oqm.interfaceIndex].replySize; const void* replyData = getReplyDataFromTickTransactionStorage(oqm); + copyMem(payload, replyData, replySize); response->resType = RespondOracleData::respondReplyData; enqueueResponse(peer, sizeof(RespondOracleData) + replySize, RespondOracleData::type(), header->dejavu(), response); From 57cd01cedd26506ce9cee5f5ea4a0117c2d47d92 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:44:02 +0100 Subject: [PATCH 24/90] enable contract execution fee deduction --- src/ticking/execution_fee_report_collector.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ticking/execution_fee_report_collector.h b/src/ticking/execution_fee_report_collector.h index a186e9e9c..7a0192864 100644 --- a/src/ticking/execution_fee_report_collector.h +++ b/src/ticking/execution_fee_report_collector.h @@ -144,8 +144,7 @@ class ExecutionFeeReportCollector if (quorumValue > 0) { - // TODO: enable subtraction after mainnet testing phase - // subtractFromContractFeeReserve(contractIndex, quorumValue); + subtractFromContractFeeReserve(contractIndex, quorumValue); ContractReserveDeduction message = { quorumValue, getContractFeeReserve(contractIndex), contractIndex }; logger.logContractReserveDeduction(message); } From 87d2cdb6dcc2cffba8fe5eb866d79d4e8a4bbf24 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:52:32 +0100 Subject: [PATCH 25/90] Revert "add NO_QRWA toggle" This reverts commit e9293e7cce0d7f18e7db87aa11d762260deb72e7. --- src/contract_core/contract_def.h | 8 -------- src/qubic.cpp | 2 -- 2 files changed, 10 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index a0bc113bb..65139e477 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -201,8 +201,6 @@ #define CONTRACT_STATE2_TYPE QRAFFLE2 #include "contracts/QRaffle.h" -#ifndef NO_QRWA - #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE @@ -213,8 +211,6 @@ #define CONTRACT_STATE2_TYPE QRWA2 #include "contracts/qRWA.h" -#endif - // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -322,9 +318,7 @@ constexpr struct ContractDescription {"QBOND", 182, 10000, sizeof(QBOND)}, // proposal in epoch 180, IPO in 181, construction and first use in 182 {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 -#ifndef NO_QRWA {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 -#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA)}, @@ -440,9 +434,7 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QBOND); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE); -#ifndef NO_QRWA REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRWA); -#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/qubic.cpp b/src/qubic.cpp index b5ce9d40f..b148b224f 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,7 +1,5 @@ #define SINGLE_COMPILE_UNIT -// #define NO_QRWA - // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" From 9ca8c9c6206230fdf14acbde73fd01e9a638616b Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:55:12 +0100 Subject: [PATCH 26/90] QIP: remove one-time fix for epoch 196 --- src/contracts/QIP.h | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/contracts/QIP.h b/src/contracts/QIP.h index 722d94817..870f50775 100644 --- a/src/contracts/QIP.h +++ b/src/contracts/QIP.h @@ -464,21 +464,6 @@ struct QIP : public ContractBase state.transferRightsFee = 100; } - struct BEGIN_EPOCH_locals - { - ICOInfo ico; - }; - - BEGIN_EPOCH_WITH_LOCALS() - { - if (qpi.epoch() == 196) - { - locals.ico = state.icos.get(0); - locals.ico.remainingAmountForPhase3 = qpi.numberOfPossessedShares(locals.ico.assetName, locals.ico.issuer, SELF, SELF, SELF_INDEX, SELF_INDEX); - state.icos.set(0, locals.ico); - } - } - struct END_EPOCH_locals { ICOInfo ico; From 6ed5a5198ca9e57694938d17523ea6920d07aaf3 Mon Sep 17 00:00:00 2001 From: TakaYuPP Date: Thu, 15 Jan 2026 03:48:27 -0800 Subject: [PATCH 27/90] QRaffle fix: incorrect shareholder info (#717) --- src/contracts/QRaffle.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h index 114de9551..4c5fc6ccb 100644 --- a/src/contracts/QRaffle.h +++ b/src/contracts/QRaffle.h @@ -1372,7 +1372,7 @@ struct QRAFFLE : public ContractBase while (locals.idx != NULL_INDEX) { locals.shareholder = state.shareholdersList.key(locals.idx); - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, div(locals.shareholderRevenue, 676) * qpi.numberOfShares(locals.acTokenRaffle.token, AssetOwnershipSelect::byOwner(locals.shareholder), AssetPossessionSelect::byPossessor(locals.shareholder)), locals.shareholder); + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, div(locals.shareholderRevenue, 676) * qpi.numberOfShares(locals.QraffleAsset, AssetOwnershipSelect::byOwner(locals.shareholder), AssetPossessionSelect::byPossessor(locals.shareholder)), locals.shareholder); locals.idx = state.shareholdersList.nextElementIndex(locals.idx); } From 2b90e0614cb2480ff8d9e69c95a0444c1c885627 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:55:55 +0100 Subject: [PATCH 28/90] Fix compiler compatibility issue --- src/contracts/qpi.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index 98a94d4ce..df3825b4b 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -2434,7 +2434,7 @@ namespace QPI * @return Whether queryId is found and matches the oracle interface. */ template - inline bool getOracleQuery(sint64 queryId, OracleInterface::OracleQuery& query) const; + inline bool getOracleQuery(sint64 queryId, typename OracleInterface::OracleQuery& query) const; /** * @brief Get oracle reply by queryId. @@ -2443,7 +2443,7 @@ namespace QPI * @return Whether queryId is found, matches the oracle interface, and a valid reply is available. */ template - inline bool getOracleReply(sint64 queryId, OracleInterface::OracleReply& reply) const; + inline bool getOracleReply(sint64 queryId, typename OracleInterface::OracleReply& reply) const; /** * @brief Get status of oracle query by queryId. @@ -2610,7 +2610,7 @@ namespace QPI // Internal version of QUERY_ORACLE (macro ensures that proc pointer and id match) template inline sint64 __qpiQueryOracle( - const OracleInterface::OracleQuery& query, + const typename OracleInterface::OracleQuery& query, void (*notificationProcPtr)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), unsigned int notificationProcId, uint32 timeoutMillisec @@ -2619,7 +2619,7 @@ namespace QPI // Internal version of SUBSCRIBE_ORACLE (macro ensures that proc pointer and id match) template inline sint32 __qpiSubscribeOracle( - const OracleInterface::OracleQuery& query, + const typename OracleInterface::OracleQuery& query, void (*notificationProcPtr)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput& input, NoData& output, LocalsType& locals), unsigned int notificationProcId, uint32 notificationIntervalInMilliseconds = 60000, From 0678ff4992224421c171325592e863971bcb8f6e Mon Sep 17 00:00:00 2001 From: cyber-pc <165458555+cyber-pc@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:23:41 +0700 Subject: [PATCH 29/90] OracleMock: show stats. --- src/contracts/TestExampleC.h | 6 +- src/oracle_core/oracle_engine.h | 84 +++++++++++++++++++++++++ src/oracle_core/oracle_interfaces_def.h | 2 + src/oracle_interfaces/Mock.h | 6 ++ src/qubic.cpp | 2 +- 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index 33b4aa501..4a60b8b42 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -271,6 +271,7 @@ struct TESTEXC : public ContractBase } } + // MOCK ORACLE TESTING typedef OracleNotificationInput NotifyMockOracleReply_input; typedef NoData NotifyMockOracleReply_output; struct NotifyMockOracleReply_locals @@ -290,12 +291,15 @@ struct TESTEXC : public ContractBase ASSERT(locals.query.value == input.reply.echoedValue); ASSERT(locals.query.value == input.reply.doubledValue / 2); + if (!OI::Mock::replyIsValid(locals.query, input.reply)) + { + return; + } // TODO: log } else { // handle failure ... - // TODO: log } } diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index d88ea61d2..42c97f895 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -255,6 +255,19 @@ class OracleEngine unsigned long long unresolvableCount; } stats; +#if ENABLE_ORACLE_STATS_RECORD + struct + { + unsigned long long queryCount; + unsigned long long replyCount; + unsigned long long validCount; + + // Extra data buffer + unsigned long long extraData[4]; + + } oracleStats[OI::oracleInterfacesCount]; // stats per oracle. +#endif + /// fast lookup of oracle query index (sequential position in queries array) from oracle query ID (composed of query tick and other info) QPI::HashMap* queryIdToIndex; @@ -315,6 +328,10 @@ class OracleEngine notificationQueryIndicies.numValues = 0; setMem(&stats, sizeof(stats), 0); +#if ENABLE_ORACLE_STATS_RECORD + setMem(&oracleStats, sizeof(oracleStats), 0); +#endif + return true; } @@ -440,6 +457,11 @@ class OracleEngine // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + // Debug logging +#if ENABLE_ORACLE_STATS_RECORD + oracleStats[queryMetadata.interfaceIndex].queryCount++; +#endif + return queryId; } @@ -520,6 +542,43 @@ class OracleEngine // add reply state to set of indices with pending commit tx pendingCommitReplyStateIndices.add(replyStateIdx); + +#if ENABLE_ORACLE_STATS_RECORD + // Update the stats for each type of oracles + const uint32_t ifaceIdx = oqm.interfaceIndex; + oracleStats[ifaceIdx].replyCount++; + // Now only record contract querry + if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) + { + const void* queryData = queryStorage + oqm.typeVar.contract.queryStorageOffset; + + bool valid = false; + switch (ifaceIdx) + { + case 0: // Price + valid = OI::Price::replyIsValid(*(const OI::Price::OracleReply*)replyData); + oracleStats[ifaceIdx].extraData[0] = ((const OI::Price::OracleReply*)replyData)->denominator; + oracleStats[ifaceIdx].extraData[1] = ((const OI::Price::OracleReply*)replyData)->numerator; + break; + case 1: // Mock + valid = OI::Mock::replyIsValid( + *(const OI::Mock::OracleQuery*)queryData, + *(const OI::Mock::OracleReply*)replyData); + // Get some extra data + oracleStats[ifaceIdx].extraData[0] = ((const OI::Mock::OracleQuery*)queryData)->value; + oracleStats[ifaceIdx].extraData[1] = ((const OI::Mock::OracleReply*)replyData)->echoedValue; + oracleStats[ifaceIdx].extraData[2] = ((const OI::Mock::OracleReply*)replyData)->doubledValue; + break; + default: // unknown + break; + } + if (valid) + { + oracleStats[ifaceIdx].validCount++; + } + } +#endif + } /** @@ -1104,6 +1163,31 @@ class OracleEngine appendText(message, ", unresolvable "); appendNumber(message, stats.unresolvableCount, FALSE); logToConsole(message); + +#if ENABLE_ORACLE_STATS_RECORD + // Show per-interface oracle stats + for (uint32_t i = 0; i < OI::oracleInterfacesCount; i++) + { + setText(message, L"Oracle["); + appendNumber(message, i, FALSE); + appendText(message, L"]: queries="); + appendNumber(message, oracleStats[i].queryCount, FALSE); + appendText(message, L", replies="); + appendNumber(message, oracleStats[i].replyCount, FALSE); + appendText(message, L", valid="); + appendNumber(message, oracleStats[i].validCount, FALSE); + appendText(message, L", data="); + appendNumber(message, oracleStats[i].extraData[0], TRUE); + if (i == 1) + { + appendText(message, L", "); + appendNumber(message, oracleStats[i].extraData[1], TRUE); + appendText(message, L", "); + appendNumber(message, oracleStats[i].extraData[2], TRUE); + } +#endif + logToConsole(message); + } } void processRequestOracleData(Peer* peer, RequestResponseHeader* header) const; diff --git a/src/oracle_core/oracle_interfaces_def.h b/src/oracle_core/oracle_interfaces_def.h index cb91fb596..1e6c72793 100644 --- a/src/oracle_core/oracle_interfaces_def.h +++ b/src/oracle_core/oracle_interfaces_def.h @@ -27,4 +27,6 @@ namespace OI #undef REGISTER_ORACLE_INTERFACE +#define ENABLE_ORACLE_STATS_RECORD 1 + } diff --git a/src/oracle_interfaces/Mock.h b/src/oracle_interfaces/Mock.h index 4ad135cad..131bdfab0 100644 --- a/src/oracle_interfaces/Mock.h +++ b/src/oracle_interfaces/Mock.h @@ -41,4 +41,10 @@ struct Mock { return 1000; } + + /// Check if the passed oracle reply is valid + static bool replyIsValid(const OracleQuery& querry, const OracleReply& reply) + { + return (reply.echoedValue == querry.value) && (reply.doubledValue == 2 * querry.value); + } }; diff --git a/src/qubic.cpp b/src/qubic.cpp index b7d63f5d9..4b663274e 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -6330,7 +6330,7 @@ static void logInfo() #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES oracleEngine.logStatus(message); - logToConsole(message); + //logToConsole(message); #endif } From 3fe22d5ec4db92a68bf1b9c4ccc083878ff8ef15 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:16:09 +0100 Subject: [PATCH 30/90] Add logging of oracle query status changes --- src/logging/logging.h | 20 +++++++++++++++++++- src/oracle_core/oracle_engine.h | 33 ++++++++++++++++++++++++++++----- src/private_settings.h | 2 ++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/logging/logging.h b/src/logging/logging.h index d22512e8b..1e3f7f844 100644 --- a/src/logging/logging.h +++ b/src/logging/logging.h @@ -18,7 +18,7 @@ struct Peer; #define LOG_CONTRACTS (LOG_CONTRACT_ERROR_MESSAGES | LOG_CONTRACT_WARNING_MESSAGES | LOG_CONTRACT_INFO_MESSAGES | LOG_CONTRACT_DEBUG_MESSAGES) -#if LOG_SPECTRUM | LOG_UNIVERSE | LOG_CONTRACTS | LOG_CUSTOM_MESSAGES +#if LOG_SPECTRUM | LOG_UNIVERSE | LOG_CONTRACTS | LOG_CUSTOM_MESSAGES | LOG_ORACLES #define ENABLED_LOGGING 1 #else #define ENABLED_LOGGING 0 @@ -57,6 +57,7 @@ struct Peer; #define ASSET_OWNERSHIP_MANAGING_CONTRACT_CHANGE 11 #define ASSET_POSSESSION_MANAGING_CONTRACT_CHANGE 12 #define CONTRACT_RESERVE_DEDUCTION 13 +#define ORACLE_QUERY_STATUS_CHANGE 14 #define CUSTOM_MESSAGE 255 #define CUSTOM_MESSAGE_OP_START_DISTRIBUTE_DIVIDENDS 6217575821008262227ULL // STA_DDIV @@ -239,6 +240,16 @@ struct ContractReserveDeduction unsigned int contractIndex; }; +struct OracleQueryStatusChange +{ + m256i queryingEntity; + long long queryId; + unsigned int interfaceIndex; + unsigned char type; + unsigned char status; + + char _terminator; // Only data before "_terminator" are logged +}; /* * LOGGING IMPLEMENTATION @@ -863,6 +874,13 @@ class qLogger #endif } + void logOracleQueryStatusChange(const OracleQueryStatusChange& message) + { +#if LOG_ORACLES + logMessage(offsetof(OracleQueryStatusChange, _terminator), ORACLE_QUERY_STATUS_CHANGE, &message); +#endif + } + template void logCustomMessage(T message) { diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 42c97f895..1db0ab77a 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -8,6 +8,7 @@ #include "common_buffers.h" #include "spectrum/special_entities.h" #include "ticking/tick_storage.h" +#include "logging/logging.h" #include "oracle_transactions.h" #include "core_om_network_messages.h" @@ -455,7 +456,9 @@ class OracleEngine // enqueue query message to oracle machine node enqueueOracleQuery(queryId, interfaceIndex, timeoutMillisec, queryData, querySize); - // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + // log status change + OracleQueryStatusChange logEvent{ m256i(contractIndex, 0, 0, 0), queryId, interfaceIndex, queryMetadata.type, queryMetadata.status }; + logger.logOracleQueryStatusChange(logEvent); // Debug logging #if ENABLE_ORACLE_STATS_RECORD @@ -480,6 +483,19 @@ class OracleEngine enqueueResponse((Peer*)0x1, sizeof(*omq) + querySize, OracleMachineQuery::type(), 0, omq); } + void logQueryStatusChange(const OracleQueryMetadata& oqm) const + { + m256i queryingEntitiy = m256i::zero(); + if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) + queryingEntitiy.u64._0 = oqm.typeVar.contract.queryingContract; + else if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_SUBSCRIPTION) + queryingEntitiy.u64._0 = oqm.typeVar.subscription.subscriptionId; + else if (oqm.type == ORACLE_QUERY_TYPE_USER_QUERY) + queryingEntitiy = oqm.typeVar.user.queryingEntity; + OracleQueryStatusChange logEvent{ queryingEntitiy, oqm.queryId, oqm.interfaceIndex, oqm.type, oqm.status }; + logger.logOracleQueryStatusChange(logEvent); + } + public: // CAUTION: Called from request processor, requires locking! void processOracleMachineReply(const OracleMachineReply* replyMessage, uint32_t replyMessageSize) @@ -761,13 +777,15 @@ class OracleEngine if (mostCommitsCount >= QUORUM) { // enough commits for the reply reveal transaction - // -> switch to status COMMITTED if (oqm.status != ORACLE_QUERY_STATUS_COMMITTED) { + // -> switch to status COMMITTED oqm.status = ORACLE_QUERY_STATUS_COMMITTED; pendingCommitReplyStateIndices.removeByValue(replyStateIdx); pendingRevealReplyStateIndices.add(replyStateIdx); - // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + + // log status change + logQueryStatusChange(oqm); } } else if (replyState.totalCommits - mostCommitsCount > NUMBER_OF_COMPUTORS - QUORUM) @@ -789,7 +807,8 @@ class OracleEngine if (oqm.type != ORACLE_QUERY_TYPE_USER_QUERY) notificationQueryIndicies.add(queryIndex); - // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + // log status change + logQueryStatusChange(oqm); } // go to next commit in tx @@ -1000,6 +1019,9 @@ class OracleEngine if (oqm.type != ORACLE_QUERY_TYPE_USER_QUERY) notificationQueryIndicies.add(queryIndex); + // log status change + logQueryStatusChange(oqm); + return true; } @@ -1049,7 +1071,8 @@ class OracleEngine if (oqm.type != ORACLE_QUERY_TYPE_USER_QUERY) notificationQueryIndicies.add(queryIndex); - // TODO: send log event ORACLE_QUERY with queryId, query starter, interface, type, status + // log status change + logQueryStatusChange(oqm); } } } diff --git a/src/private_settings.h b/src/private_settings.h index ce257c2b2..b8737a8d3 100644 --- a/src/private_settings.h +++ b/src/private_settings.h @@ -48,6 +48,7 @@ static const unsigned char oracleMachineIPs[][4] = { #define LOG_CONTRACT_INFO_MESSAGES 1 #define LOG_CONTRACT_DEBUG_MESSAGES 1 #define LOG_CUSTOM_MESSAGES 1 +#define LOG_ORACLES 1 #else #define LOG_UNIVERSE 0 #define LOG_SPECTRUM 0 @@ -56,6 +57,7 @@ static const unsigned char oracleMachineIPs[][4] = { #define LOG_CONTRACT_INFO_MESSAGES 0 #define LOG_CONTRACT_DEBUG_MESSAGES 0 #define LOG_CUSTOM_MESSAGES 0 +#define LOG_ORACLES 0 #endif static unsigned long long logReaderPasscodes[4] = { From 030f9933a44ae83f327e01e221d18f94e21e064b Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:14:56 +0100 Subject: [PATCH 31/90] Fix uninitialized padding in log items --- src/logging/logging.h | 2 ++ src/spectrum/spectrum.h | 1 + src/ticking/execution_fee_report_collector.h | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/logging/logging.h b/src/logging/logging.h index 1e3f7f844..2d0d87177 100644 --- a/src/logging/logging.h +++ b/src/logging/logging.h @@ -231,6 +231,7 @@ struct SpectrumStats unsigned long long dustThresholdBurnHalf; unsigned int numberOfEntities; unsigned int entityCategoryPopulations[48]; + unsigned int _padding; }; struct ContractReserveDeduction @@ -238,6 +239,7 @@ struct ContractReserveDeduction unsigned long long deductedAmount; long long remainingAmount; unsigned int contractIndex; + unsigned int _padding; }; struct OracleQueryStatusChange diff --git a/src/spectrum/spectrum.h b/src/spectrum/spectrum.h index e1a57dd2a..0423577d1 100644 --- a/src/spectrum/spectrum.h +++ b/src/spectrum/spectrum.h @@ -105,6 +105,7 @@ static void logSpectrumStats() spectrumStats.dustThresholdBurnHalf = dustThresholdBurnHalf; spectrumStats.numberOfEntities = spectrumInfo.numberOfEntities; copyMem(spectrumStats.entityCategoryPopulations, entityCategoryPopulations, sizeof(entityCategoryPopulations)); + spectrumStats._padding = 0; logger.logSpectrumStats(spectrumStats); } diff --git a/src/ticking/execution_fee_report_collector.h b/src/ticking/execution_fee_report_collector.h index a186e9e9c..e4905dd73 100644 --- a/src/ticking/execution_fee_report_collector.h +++ b/src/ticking/execution_fee_report_collector.h @@ -146,7 +146,7 @@ class ExecutionFeeReportCollector { // TODO: enable subtraction after mainnet testing phase // subtractFromContractFeeReserve(contractIndex, quorumValue); - ContractReserveDeduction message = { quorumValue, getContractFeeReserve(contractIndex), contractIndex }; + ContractReserveDeduction message = { quorumValue, getContractFeeReserve(contractIndex), contractIndex, 0 }; logger.logContractReserveDeduction(message); } } From 851dc98b3fd4fb12a38c262aa0b2889889520ab8 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:29:27 +0100 Subject: [PATCH 32/90] TESTEXC: Log in oracle notification procedures --- src/contracts/TestExampleC.h | 37 +++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index 4a60b8b42..5425950f2 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -194,6 +194,17 @@ struct TESTEXC : public ContractBase //--------------------------------------------------------------- // ORACLE TESTING + struct NotificationLog + { + uint32 contractIndex; + uint8 interface; + uint8 status; + uint16 dataCheck; + sint64 data; + sint64 queryId; + sint8 _terminator; // Only data before "_terminator" are logged + }; + // optional: additional of contract data associated with oracle query HashMap oracleQueryExtraData; @@ -247,10 +258,14 @@ struct TESTEXC : public ContractBase { OI::Price::OracleQuery query; uint32 queryExtraData; + NotificationLog notificationLog; }; PRIVATE_PROCEDURE_WITH_LOCALS(NotifyPriceOracleReply) { + locals.notificationLog = NotificationLog{CONTRACT_INDEX, OI::Price::oracleInterfaceIndex, input.status, OI::Price::replyIsValid(input.reply), input.reply.numerator, input.queryId }; + LOG_INFO(locals.notificationLog); + if (input.status == ORACLE_QUERY_STATUS_SUCCESS) { // get and use query info if needed @@ -278,30 +293,30 @@ struct TESTEXC : public ContractBase { OI::Mock::OracleQuery query; uint32 queryExtraData; + NotificationLog notificationLog; }; PRIVATE_PROCEDURE_WITH_LOCALS(NotifyMockOracleReply) { + locals.notificationLog = NotificationLog{ CONTRACT_INDEX, OI::Mock::oracleInterfaceIndex, input.status, 0, (sint64)input.reply.echoedValue, input.queryId }; + if (input.status == ORACLE_QUERY_STATUS_SUCCESS) { - // get and use query info if needed - if (!qpi.getOracleQuery(input.queryId, locals.query)) - return; - - ASSERT(locals.query.value == input.reply.echoedValue); - ASSERT(locals.query.value == input.reply.doubledValue / 2); - - if (!OI::Mock::replyIsValid(locals.query, input.reply)) + // success + if (qpi.getOracleQuery(input.queryId, locals.query)) { - return; + ASSERT(locals.query.value == input.reply.echoedValue); + ASSERT(locals.query.value == input.reply.doubledValue / 2); + + locals.notificationLog.dataCheck = OI::Mock::replyIsValid(locals.query, input.reply); } - // TODO: log } else { // handle failure ... - // TODO: log } + + LOG_INFO(locals.notificationLog); } struct END_TICK_locals From 8d2ec92d08ef933d6a95cf94df46a3297283c820 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:30:41 +0100 Subject: [PATCH 33/90] Fix status type in OracleNotificationInpuut --- src/contracts/qpi.h | 4 +++- src/oracle_core/oracle_engine.h | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index df3825b4b..c5f42e5a2 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -2488,7 +2488,9 @@ namespace QPI { sint64 queryId; ///< ID of the oracle query that led to this notification. uint32 subscriptionId; ///< ID of the oracle subscription or 0 in case of a pure oracle query. - uint32 status; ///< Oracle query status as defined in `network_messages/common_def.h` + uint8 status; ///< Oracle query status as defined in `network_messages/common_def.h` + uint8 __reserved0; + uint16 __reserved1; typename OracleInterface::OracleReply reply; ///< Oracle reply if status == ORACLE_QUERY_STATUS_SUCCESS }; diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 1db0ab77a..978ce8a61 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -1112,7 +1112,7 @@ class OracleEngine setMem(notificationOutputBuffer.inputBuffer, notificationOutputBuffer.inputSize, 0); *(int64_t*)(notificationOutputBuffer.inputBuffer + 0) = oqm.queryId; *(uint32_t*)(notificationOutputBuffer.inputBuffer + 8) = 0; - *(uint32_t*)(notificationOutputBuffer.inputBuffer + 12) = oqm.status; + *(uint8_t*)(notificationOutputBuffer.inputBuffer + 12) = oqm.status; if (oqm.status == ORACLE_QUERY_STATUS_SUCCESS) { const void* replySrcPtr = getReplyDataFromTickTransactionStorage(oqm); From b118bc1ef25ec5783c1a5176b58485607cfacba4 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:29:13 +0100 Subject: [PATCH 34/90] Fix ASSERT in logging --- src/network_messages/logging.h | 4 ++-- src/qubic.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/network_messages/logging.h b/src/network_messages/logging.h index 43b7584b2..21bbce3c3 100644 --- a/src/network_messages/logging.h +++ b/src/network_messages/logging.h @@ -2,8 +2,8 @@ #include "common_def.h" -#define LOG_TX_NUMBER_OF_SPECIAL_EVENT 5 -#define LOG_TX_PER_TICK (NUMBER_OF_TRANSACTIONS_PER_TICK + LOG_TX_NUMBER_OF_SPECIAL_EVENT)// +5 special events +#define LOG_TX_NUMBER_OF_SPECIAL_EVENT 6 +#define LOG_TX_PER_TICK (NUMBER_OF_TRANSACTIONS_PER_TICK + LOG_TX_NUMBER_OF_SPECIAL_EVENT) // normal tx + special events // Fetches log struct RequestLog diff --git a/src/qubic.cpp b/src/qubic.cpp index 4b663274e..84ca7827d 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -3273,6 +3273,7 @@ static void processTick(unsigned long long processorNumber) while (oracleNotification) { PROFILE_NAMED_SCOPE("processTick(): run oracle contract notification"); + // TODO: discuss this extension with steakholders (changes event log communication protocol messages) logger.registerNewTx(system.tick, logger.SC_NOTIFICATION_TX); contractProcessorUserProcedureNotificationProc = userProcedureRegistry->get(oracleNotification->procedureId); contractProcessorUserProcedureNotificationInput = oracleNotification->inputBuffer; From 3a955c3991f6e1d79bad0f901b206d2a8e53ad49 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:51:36 +0100 Subject: [PATCH 35/90] Fix loop in processOracleReplyCommitTransaction() --- src/oracle_core/oracle_engine.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 978ce8a61..28be811d6 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -713,7 +713,7 @@ class OracleEngine // process the N commits in this tx const OracleReplyCommitTransactionItem* item = (const OracleReplyCommitTransactionItem*)transaction->inputPtr(); uint32_t size = sizeof(OracleReplyCommitTransactionItem); - while (size <= transaction->inputSize) + for (; size <= transaction->inputSize; size += sizeof(OracleReplyCommitTransactionItem), ++item) { // get and check query index uint32_t queryIndex; @@ -810,10 +810,6 @@ class OracleEngine // log status change logQueryStatusChange(oqm); } - - // go to next commit in tx - size += sizeof(OracleReplyCommitTransactionItem); - ++item; } return true; From 6138676f54a5bd8ed8bd54efdc7ef3ec2dcf1d0e Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:25:03 +0100 Subject: [PATCH 36/90] Fix bug in processRequestOracleData() --- src/oracle_core/net_msg_impl.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index 94934891f..1a1f8f977 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -57,9 +57,10 @@ void OracleEngine::processRequestOracleData(Peer* peer, R for (unsigned int msgIdx = 0; msgIdx < numMessages; ++msgIdx) { unsigned int idxInMsg = 0; - for (; idxInMsg < maxQueryIdCount && idIdx < pendingQueryIndices.numValues; ++idxInMsg) + for (; idxInMsg < maxQueryIdCount && idIdx < pendingQueryIndices.numValues; ++idxInMsg, ++idIdx) { - payloadQueryIds[idxInMsg] = pendingQueryIndices.values[idIdx]; + const uint32_t queryIndex = pendingQueryIndices.values[idIdx]; + payloadQueryIds[idxInMsg] = queries[queryIndex].queryId; } enqueueResponse(peer, sizeof(RespondOracleData) + idxInMsg * 8, RespondOracleData::type(), header->dejavu(), response); From 79464bab12b3459b123cdff258bb610975ae2c3a Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:02:03 +0100 Subject: [PATCH 37/90] Fix deadlock in processRequestOracleData() --- src/oracle_core/net_msg_impl.h | 2 +- src/oracle_core/oracle_engine.h | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index 1a1f8f977..a1e3889ea 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -123,7 +123,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R // get and send query data const uint16_t querySize = (uint16_t)OI::oracleInterfaces[oqm.interfaceIndex].querySize; ASSERT(querySize <= payloadBufferSize); - if (getOracleQuery(queryId, payload, querySize)) + if (getOracleQueryWithoutLocking(queryId, payload, querySize)) { response->resType = RespondOracleData::respondQueryData; enqueueResponse(peer, sizeof(RespondOracleData) + querySize, diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 28be811d6..6762eeded 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -951,6 +951,7 @@ class OracleEngine return &replyState; } + // Caller is responsible for locking. const void* getReplyDataFromTickTransactionStorage(const OracleQueryMetadata& queryMetadata) const { const uint32_t tick = queryMetadata.statusVar.success.revealTick; @@ -1131,11 +1132,10 @@ class OracleEngine // clean all queries (except for last n ticks in case of seamless transition) } - bool getOracleQuery(int64_t queryId, void* queryData, uint16_t querySize) const +protected: + // Caller is responsible for locking. + bool getOracleQueryWithoutLocking(int64_t queryId, void* queryData, uint16_t querySize) const { - // lock for accessing engine data - LockGuard lockGuard(lock); - // get query index uint32_t queryIndex; if (!queryIdToIndex->get(queryId, queryIndex) || queryIndex >= oracleQueryCount) @@ -1167,6 +1167,16 @@ class OracleEngine return true; } +public: + bool getOracleQuery(int64_t queryId, void* queryData, uint16_t querySize) const + { + // lock for accessing engine data + LockGuard lockGuard(lock); + + return getOracleQueryWithoutLocking(queryId, queryData, querySize); + } + + void logStatus(CHAR16* message) const { setText(message, L"Oracles queries: pending "); From 7f1770f7ab7263bbac50f36e9643e2a24a8c9328 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:25:21 +0100 Subject: [PATCH 38/90] Add important reminder about debug logging --- src/platform/debugging.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/platform/debugging.h b/src/platform/debugging.h index 0e15dee8c..42eed1d6f 100644 --- a/src/platform/debugging.h +++ b/src/platform/debugging.h @@ -119,7 +119,9 @@ static void printDebugMessages() RELEASE(debugLogLock); } -// Add a message for logging from arbitrary thread +// Add a message for logging from arbitrary thread. +// CAUTION: Adding many messages may change the overall behavior, because it slows down the main thread +// significantly (printDebugMessages() function) and it may slow down other threads too due to locking. static void addDebugMessage(const CHAR16* msg) { ACQUIRE_WITHOUT_DEBUG_LOGGING(debugLogLock); From 0a3182acd58976dcca09d1555e084731e90cccbf Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:35:25 +0100 Subject: [PATCH 39/90] Implement more oracle query QPI functions --- src/contract_core/qpi_oracle_impl.h | 7 ++--- src/contracts/TestExampleC.h | 9 +++++++ src/contracts/qpi.h | 5 ---- src/oracle_core/oracle_engine.h | 42 +++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/contract_core/qpi_oracle_impl.h b/src/contract_core/qpi_oracle_impl.h index ac6b9fcaf..a9244d938 100644 --- a/src/contract_core/qpi_oracle_impl.h +++ b/src/contract_core/qpi_oracle_impl.h @@ -99,13 +99,10 @@ bool QPI::QpiContextFunctionCall::getOracleQuery(QPI::sint64 queryId, OracleInte template bool QPI::QpiContextFunctionCall::getOracleReply(QPI::sint64 queryId, OracleInterface::OracleReply& reply) const { - // TODO - return false; + return oracleEngine.getOracleReply(queryId, &reply, sizeof(reply)); } -template inline QPI::uint8 QPI::QpiContextFunctionCall::getOracleQueryStatus(sint64 queryId) const { - // TODO - return ORACLE_QUERY_STATUS_UNKNOWN; + return oracleEngine.getOracleReplygetOracleQueryStatus(queryId); } diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index 5425950f2..c2e434cd5 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -292,6 +292,7 @@ struct TESTEXC : public ContractBase struct NotifyMockOracleReply_locals { OI::Mock::OracleQuery query; + OI::Mock::OracleReply reply; uint32 queryExtraData; NotificationLog notificationLog; }; @@ -300,6 +301,7 @@ struct TESTEXC : public ContractBase { locals.notificationLog = NotificationLog{ CONTRACT_INDEX, OI::Mock::oracleInterfaceIndex, input.status, 0, (sint64)input.reply.echoedValue, input.queryId }; + ASSERT(qpi.getOracleQueryStatus(input.queryId) == input.status); if (input.status == ORACLE_QUERY_STATUS_SUCCESS) { // success @@ -310,10 +312,16 @@ struct TESTEXC : public ContractBase locals.notificationLog.dataCheck = OI::Mock::replyIsValid(locals.query, input.reply); } + ASSERT(qpi.getOracleQueryStatus(input.queryId) == ORACLE_QUERY_STATUS_SUCCESS); + ASSERT(qpi.getOracleReply(input.queryId, locals.reply)); + ASSERT(locals.reply.echoedValue == input.reply.echoedValue); + ASSERT(locals.reply.doubledValue == input.reply.doubledValue); } else { // handle failure ... + ASSERT(qpi.getOracleQueryStatus(input.queryId) == ORACLE_QUERY_STATUS_TIMEOUT || qpi.getOracleQueryStatus(input.queryId) == ORACLE_QUERY_STATUS_UNRESOLVABLE); + ASSERT(!qpi.getOracleReply(input.queryId, locals.reply)); } LOG_INFO(locals.notificationLog); @@ -341,6 +349,7 @@ struct TESTEXC : public ContractBase } locals.oracleQueryId = QUERY_ORACLE(OI::Price, locals.priceOracleQuery, NotifyPriceOracleReply, 20000); + ASSERT(qpi.getOracleQueryStatus(locals.oracleQueryId) == ORACLE_QUERY_STATUS_PENDING); } if (qpi.tick() % 2 == 1) { diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index c5f42e5a2..c57b735c1 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -2457,7 +2457,6 @@ namespace QPI * - ORACLE_QUERY_STATUS_UNRESOLVABLE: No valid oracle reply is available, because computors disagreed about the value. * - ORACLE_QUERY_STATUS_TIMEOUT: No valid oracle reply is available and timeout has hit. */ - template inline uint8 getOracleQueryStatus(sint64 queryId) const; // Access proposal functions with qpi(proposalVotingObject).func(). @@ -2749,10 +2748,6 @@ namespace QPI static void __expand(const QpiContextProcedureCall& qpi, void*, void*) {} }; - struct OracleBase - { - }; - // Internal macro for defining the system procedure macros #define NO_IO_SYSTEM_PROC(CapLetterName, FuncName, InputType, OutputType) \ public: \ diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 6762eeded..a12297933 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -1176,6 +1176,48 @@ class OracleEngine return getOracleQueryWithoutLocking(queryId, queryData, querySize); } + bool getOracleReply(int64_t queryId, void* replyData, uint16_t replySize) const + { + // lock for accessing engine data + LockGuard lockGuard(lock); + + // get query index + uint32_t queryIndex; + if (!queryIdToIndex->get(queryId, queryIndex) || queryIndex >= oracleQueryCount) + return false; + + // check status + const auto& queryMetadata = queries[queryIndex]; + if (queryMetadata.status != ORACLE_QUERY_STATUS_SUCCESS) + return false; + + // check query size + ASSERT(queryMetadata.interfaceIndex < OI::oracleInterfacesCount); + if (replySize != OI::oracleInterfaces[queryMetadata.interfaceIndex].replySize) + return false; + + // get reply data from tick transaction storage + const void* replySrcPtr = getReplyDataFromTickTransactionStorage(queryMetadata); + + // return reply data + copyMem(replyData, replySrcPtr, replySize); + return true; + } + + uint8_t getOracleReplygetOracleQueryStatus(int64_t queryId) const + { + // lock for accessing engine data + LockGuard lockGuard(lock); + + // get query index + uint32_t queryIndex; + if (!queryIdToIndex->get(queryId, queryIndex) || queryIndex >= oracleQueryCount) + return ORACLE_QUERY_STATUS_UNKNOWN; + + // return status + const auto& queryMetadata = queries[queryIndex]; + return queryMetadata.status; + } void logStatus(CHAR16* message) const { From b2b03bbd4275d509396e04fec77d7b0cc93091d0 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:48:55 +0100 Subject: [PATCH 40/90] Log avg tick count until oracle reply tx get executed --- src/oracle_core/oracle_engine.h | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index a12297933..a7936d6c3 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -249,11 +249,20 @@ class OracleEngine /// total number of successful oracle queries unsigned long long successCount; + /// sum of ticks that were required to reach the success state + unsigned long long successTicksSum; + /// total number of timeout oracle queries unsigned long long timeoutCount; - /// total number of timeout oracle queries + /// total number of unresolvable oracle queries unsigned long long unresolvableCount; + + /// total number of oracle queries that reached commit state + unsigned long long commitCount; + + /// sum of ticks that were required to reach the commit state + unsigned long long commitTicksSum; } stats; #if ENABLE_ORACLE_STATS_RECORD @@ -783,6 +792,8 @@ class OracleEngine oqm.status = ORACLE_QUERY_STATUS_COMMITTED; pendingCommitReplyStateIndices.removeByValue(replyStateIdx); pendingRevealReplyStateIndices.add(replyStateIdx); + ++stats.commitCount; + stats.commitTicksSum += (system.tick - oqm.queryTick); // log status change logQueryStatusChange(oqm); @@ -1007,6 +1018,7 @@ class OracleEngine oqm.status = ORACLE_QUERY_STATUS_SUCCESS; pendingQueryIndices.removeByValue(queryIndex); ++stats.successCount; + stats.successTicksSum += (oqm.statusVar.success.revealTick - oqm.queryTick); // cleanup reply state pendingRevealReplyStateIndices.removeByValue(replyStateIdx); @@ -1223,9 +1235,22 @@ class OracleEngine { setText(message, L"Oracles queries: pending "); appendNumber(message, pendingCommitReplyStateIndices.numValues, FALSE); - appendText(message, " / "); + appendText(message, " ("); + // print how many ticks it takes on average until the commit status is reached (until 451 commit tx got executed) + unsigned long long ticks10perCommit = (stats.commitCount) ? (stats.commitTicksSum * 10 / stats.commitCount) : 0; + appendNumber(message, ticks10perCommit / 10, FALSE); + appendText(message, "."); + appendNumber(message, ticks10perCommit % 10, FALSE); + appendText(message, " ticks) / "); appendNumber(message, pendingRevealReplyStateIndices.numValues, FALSE); - appendText(message, " / "); + appendText(message, " ("); + // print how many ticks it takes on average between commit and success (ticks until 1 reveal tx got executed) + unsigned long long ticks10perSuccess = (stats.successCount) ? (stats.successTicksSum * 10 / stats.successCount) : 0; + unsigned long long ticks10perReveal = ticks10perSuccess - ticks10perCommit; + appendNumber(message, ticks10perReveal / 10, FALSE); + appendText(message, "."); + appendNumber(message, ticks10perReveal % 10, FALSE); + appendText(message, " ticks) / "); appendNumber(message, pendingQueryIndices.numValues, FALSE); appendText(message, ", successful "); appendNumber(message, stats.successCount, FALSE); From cebab8fcd81cf1a5779a9a782114ed149b764d73 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:08:08 +0100 Subject: [PATCH 41/90] Set oracle reply tx publication offsets --- src/qubic.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/qubic.cpp b/src/qubic.cpp index 84ca7827d..8387a63cd 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -95,6 +95,8 @@ #define SYSTEM_DATA_SAVING_PERIOD 300000ULL #define TICK_TRANSACTIONS_PUBLICATION_OFFSET 2 // Must be only 2 #define MIN_MINING_SOLUTIONS_PUBLICATION_OFFSET 3 // Must be 3+ +#define ORACLE_REPLY_COMMIT_PUBLICATION_OFFSET 5 +#define ORACLE_REPLY_REVEAL_PUBLICATION_OFFSET 3 #define TIME_ACCURACY 5000 constexpr unsigned long long TARGET_MAINTHREAD_LOOP_DURATION = 30; // mcs, it is the target duration of the main thread loop @@ -3505,10 +3507,10 @@ static void processTick(unsigned long long processorNumber) // Publish oracle reply commit and reveal transactions (uses reorgBuffer for constructing packets) if (isMainMode()) { - const auto txTick = system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET; unsigned char digest[32]; { PROFILE_NAMED_SCOPE("processTick(): broadcast oracle reply transactions"); + const auto txTick = system.tick + ORACLE_REPLY_COMMIT_PUBLICATION_OFFSET; auto* tx = (OracleReplyCommitTransactionPrefix*)reorgBuffer; for (unsigned int i = 0; i < numberOfOwnComputorIndices; i++) { @@ -3537,6 +3539,7 @@ static void processTick(unsigned long long processorNumber) { PROFILE_NAMED_SCOPE("processTick(): broadcast oracle reveal transactions"); auto* tx = (OracleReplyRevealTransactionPrefix*)reorgBuffer; + const auto txTick = system.tick + ORACLE_REPLY_REVEAL_PUBLICATION_OFFSET; // create reply reveal transaction in tx (without signature), returning: // - 0 if no tx was created (no need to send reply commits) // - otherwise, an index value that has to be passed to the next call for building another tx From 9c47cc2cfd5f57c5f0044f3cdd04202216efe255 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:44:04 +0100 Subject: [PATCH 42/90] Add debug output to oracle query pipeline --- .../execution_time_accumulator.h | 2 +- src/network_core/peers.h | 10 ++ src/oracle_core/oracle_engine.h | 154 ++++++++++++++++++ src/platform/file_io.h | 2 + src/qubic.cpp | 116 +++++++++++++ src/text_output.h | 43 ++++- src/ticking/pending_txs_pool.h | 19 +++ 7 files changed, 338 insertions(+), 8 deletions(-) diff --git a/src/contract_core/execution_time_accumulator.h b/src/contract_core/execution_time_accumulator.h index efff5cf87..94357e6f9 100644 --- a/src/contract_core/execution_time_accumulator.h +++ b/src/contract_core/execution_time_accumulator.h @@ -59,7 +59,7 @@ class ExecutionTimeAccumulator math_lib::sadd(contractExecutionTimePerPhase[contractExecutionTimeActiveArrayIndex][contractIndex], timeMicroSeconds); RELEASE(lock); -#if !defined(NDEBUG) && !defined(NO_UEFI) +#if !defined(NDEBUG) && !defined(NO_UEFI) && 0 CHAR16 dbgMsgBuf[128]; setText(dbgMsgBuf, L"Execution time added for contract "); appendNumber(dbgMsgBuf, contractIndex, FALSE); diff --git a/src/network_core/peers.h b/src/network_core/peers.h index 9c570f9d9..78d89267b 100644 --- a/src/network_core/peers.h +++ b/src/network_core/peers.h @@ -369,6 +369,10 @@ static void pushToFullNodes(RequestResponseHeader* requestResponseHeader, int nu static void pushToOracleMachineNodes(RequestResponseHeader* requestResponseHeader) { +#if !defined(NDEBUG) + // TODO: cleanup this debug code when OM connection is fully stable + setText(::message, L"pushToOracleMachineNodes(): "); +#endif if (NUMBER_OF_OM_NODE_CONNECTIONS > 0) { unsigned short numberOfSuitablePeers = 0; @@ -379,11 +383,17 @@ static void pushToOracleMachineNodes(RequestResponseHeader* requestResponseHeade && peers[i].isConnectedAccepted && !peers[i].isClosing) { +#if !defined(NDEBUG) + appendIPv4Address(::message, peers[i].address); +#endif push(&peers[i], requestResponseHeader); numberOfSuitablePeers++; } } } +#if !defined(NDEBUG) + addDebugMessage(::message); +#endif } // Add message to response queue of specific peer. If peer is NULL, it will be sent to random peers. Can be called from any thread. diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index a7936d6c3..93944f7a9 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -9,6 +9,7 @@ #include "spectrum/special_entities.h" #include "ticking/tick_storage.h" #include "logging/logging.h" +#include "text_output.h" #include "oracle_transactions.h" #include "core_om_network_messages.h" @@ -474,6 +475,21 @@ class OracleEngine oracleStats[queryMetadata.interfaceIndex].queryCount++; #endif +#if !defined(NDEBUG) + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"oracleEngine.startContractQuery(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", queryId "); + appendNumber(dbgMsg, queryId, FALSE); + appendText(dbgMsg, ", interfaceIndex "); + appendNumber(dbgMsg, interfaceIndex, FALSE); + appendText(dbgMsg, ", timeout "); + appendDateAndTime(dbgMsg, timeout); + appendText(dbgMsg, ", now "); + appendDateAndTime(dbgMsg, QPI::DateAndTime::now()); + addDebugMessage(dbgMsg); +#endif + return queryId; } @@ -604,6 +620,16 @@ class OracleEngine } #endif +#if !defined(NDEBUG) + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"oracleEngine.processOracleMachineReply(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", queryId "); + appendNumber(dbgMsg, oqm.queryId, FALSE); + appendText(dbgMsg, ", interfaceIndex "); + appendNumber(dbgMsg, oqm.interfaceIndex, FALSE); + addDebugMessage(dbgMsg); +#endif } /** @@ -689,6 +715,25 @@ class OracleEngine tx->inputType = OracleReplyCommitTransactionPrefix::transactionType(); tx->inputSize = commitsCount * sizeof(OracleReplyCommitTransactionItem); +#if !defined(NDEBUG) && 0 + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"oracleEngine.getReplyCommitTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", txScheduleTick "); + appendNumber(dbgMsg, txScheduleTick, FALSE); + appendText(dbgMsg, ", computorIdx "); + appendNumber(dbgMsg, computorIdx, FALSE); + appendText(dbgMsg, ", commitsCount "); + appendNumber(dbgMsg, commitsCount, FALSE); + appendText(dbgMsg, ", queryId"); + for (int i = 0; i < commitsCount; ++i) + { + appendText(dbgMsg, " "); + appendNumber(dbgMsg, commits[i].queryId, FALSE); + } + addDebugMessage(dbgMsg); +#endif + // if we had to break from the loop early, return and signal to call this again for creating another // tx with the start index we return here if (idx < replyIdxCount) @@ -714,7 +759,25 @@ class OracleEngine // get computor index const int compIdx = computorIndex(transaction->sourcePublicKey); if (compIdx < 0) + { +#if !defined(NDEBUG) + CHAR16 dbgMsg[120]; + setText(dbgMsg, L"oracleEngine.processOracleReplyCommitTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", source is no computor!"); + addDebugMessage(dbgMsg); +#endif return false; + } + +#if !defined(NDEBUG) && 0 + CHAR16 dbgMsg[800]; + setText(dbgMsg, L"oracleEngine.processOracleReplyCommitTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", computorIdx "); + appendNumber(dbgMsg, compIdx, FALSE); + appendText(dbgMsg, ", queryId "); +#endif // lock for accessing engine data LockGuard lockGuard(lock); @@ -797,6 +860,20 @@ class OracleEngine // log status change logQueryStatusChange(oqm); + +#if !defined(NDEBUG) && 1 + CHAR16 dbgMsg1[200]; + setText(dbgMsg1, L"oracleEngine.processOracleReplyCommitTransaction(), tick "); + appendNumber(dbgMsg1, system.tick, FALSE); + appendText(dbgMsg1, ", queryId "); + appendNumber(dbgMsg1, oqm.queryId, FALSE); + appendText(dbgMsg1, " ("); + appendNumber(dbgMsg1, mostCommitsCount, FALSE); + appendText(dbgMsg1, ":"); + appendNumber(dbgMsg1, replyState.totalCommits - mostCommitsCount, FALSE); + appendText(dbgMsg1, ") -> COMMITTED"); + addDebugMessage(dbgMsg1); +#endif } } else if (replyState.totalCommits - mostCommitsCount > NUMBER_OF_COMPUTORS - QUORUM) @@ -820,9 +897,36 @@ class OracleEngine // log status change logQueryStatusChange(oqm); + +#if !defined(NDEBUG) && 1 + CHAR16 dbgMsg1[200]; + setText(dbgMsg1, L"oracleEngine.processOracleReplyCommitTransaction(), tick "); + appendNumber(dbgMsg1, system.tick, FALSE); + appendText(dbgMsg1, ", queryId "); + appendNumber(dbgMsg1, oqm.queryId, FALSE); + appendText(dbgMsg1, " ("); + appendNumber(dbgMsg1, mostCommitsCount, FALSE); + appendText(dbgMsg1, ":"); + appendNumber(dbgMsg1, replyState.totalCommits - mostCommitsCount, FALSE); + appendText(dbgMsg1, ") -> UNRESOLVABLE"); + addDebugMessage(dbgMsg1); +#endif } + +#if !defined(NDEBUG) && 0 + appendNumber(dbgMsg, item->queryId, FALSE); + appendText(dbgMsg, " ("); + appendNumber(dbgMsg, mostCommitsCount, FALSE); + appendText(dbgMsg, ":"); + appendNumber(dbgMsg, replyState.totalCommits - mostCommitsCount, FALSE); + appendText(dbgMsg, ")"); +#endif } +#if !defined(NDEBUG) && 0 + addDebugMessage(dbgMsg); +#endif + return true; } @@ -892,6 +996,19 @@ class OracleEngine // remember that we have scheduled reveal of this reply replyState.expectedRevealTxTick = txScheduleTick; +#if !defined(NDEBUG) + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"oracleEngine.getReplyRevealTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", txScheduleTick "); + appendNumber(dbgMsg, txScheduleTick, FALSE); + appendText(dbgMsg, ", queryId "); + appendNumber(dbgMsg, replyState.queryId, FALSE); + appendText(dbgMsg, ", computorIdx "); + appendNumber(dbgMsg, computorIndex(tx->sourcePublicKey), FALSE); + addDebugMessage(dbgMsg); +#endif + // return non-zero in order instruct caller to call this function again with the returned startIdx return idx + 1; } @@ -991,6 +1108,19 @@ class OracleEngine // update tick when reveal is expected if (!replyState->expectedRevealTxTick || replyState->expectedRevealTxTick > transaction->tick) replyState->expectedRevealTxTick = transaction->tick; + +#if !defined(NDEBUG) + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"oracleEngine.announceExpectedRevealTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", queryId "); + appendNumber(dbgMsg, replyState->queryId, FALSE); + appendText(dbgMsg, ", computorIdx "); + appendNumber(dbgMsg, computorIndex(transaction->sourcePublicKey), FALSE); + appendText(dbgMsg, ", expectedRevealTxTick "); + appendNumber(dbgMsg, replyState->expectedRevealTxTick, FALSE); + addDebugMessage(dbgMsg); +#endif } // Called from tick processor. @@ -1031,6 +1161,17 @@ class OracleEngine // log status change logQueryStatusChange(oqm); +#if !defined(NDEBUG) + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"oracleEngine.processOracleReplyRevealTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", queryId "); + appendNumber(dbgMsg, oqm.queryId, FALSE); + appendText(dbgMsg, ", computorIdx "); + appendNumber(dbgMsg, computorIndex(transaction->sourcePublicKey), FALSE); + addDebugMessage(dbgMsg); +#endif + return true; } @@ -1082,6 +1223,19 @@ class OracleEngine // log status change logQueryStatusChange(oqm); + +#if !defined(NDEBUG) + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"oracleEngine.processTimeouts(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", queryId "); + appendNumber(dbgMsg, oqm.queryId, FALSE); + appendText(dbgMsg, ", timeout "); + appendDateAndTime(dbgMsg, oqm.timeout); + appendText(dbgMsg, ", now "); + appendDateAndTime(dbgMsg, now); + addDebugMessage(dbgMsg); +#endif } } } diff --git a/src/platform/file_io.h b/src/platform/file_io.h index 1616bdec3..e4a568576 100644 --- a/src/platform/file_io.h +++ b/src/platform/file_io.h @@ -34,7 +34,9 @@ static EFI_FILE_PROTOCOL* root = NULL; class AsyncFileIO; static AsyncFileIO* gAsyncFileIO = NULL; +#ifndef NDEBUG static void addDebugMessage(const CHAR16* msg); +#endif static long long getFileSize(CHAR16* fileName, CHAR16* directory = NULL) { diff --git a/src/qubic.cpp b/src/qubic.cpp index 8387a63cd..c1f74eb02 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -903,6 +903,25 @@ static void processBroadcastFutureTickData(Peer* peer, RequestResponseHeader* he enqueueResponse(NULL, header); } +#if !defined(NDEBUG) && 1 + unsigned int txCount = 0; + for (unsigned int transactionIndex = 0; transactionIndex < NUMBER_OF_TRANSACTIONS_PER_TICK; transactionIndex++) + { + if (!isZero(request->tickData.transactionDigests[transactionIndex])) + { + txCount++; + } + } + CHAR16 dbgMsg1[200]; + setText(dbgMsg1, L"processBroadcastFutureTickData(), current tick "); + appendNumber(dbgMsg1, system.tick, FALSE); + appendText(dbgMsg1, ", tickData.tick "); + appendNumber(dbgMsg1, request->tickData.tick, FALSE); + appendText(dbgMsg1, ", tx count "); + appendNumber(dbgMsg1, txCount, FALSE); + addDebugMessage(dbgMsg1); +#endif + ts.tickData.acquireLock(); TickData& td = ts.tickData.getByTickInCurrentEpoch(request->tickData.tick); if (td.epoch != INVALIDATED_TICK_DATA) @@ -959,12 +978,26 @@ static void processBroadcastTransaction(Peer* peer, RequestResponseHeader* heade { Transaction* request = header->getPayload(); const unsigned int transactionSize = request->totalSize(); + +#if !defined(NDEBUG) && 1 + // TODO: remove this debug code when the OM pipeline is fully stable + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"processBroadcastTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); +#endif + if (request->checkValidity() && transactionSize == header->size() - sizeof(RequestResponseHeader)) { +#if !defined(NDEBUG) && 1 + appendText(dbgMsg, L" valid"); +#endif unsigned char digest[32]; KangarooTwelve(request, transactionSize - SIGNATURE_SIZE, digest, sizeof(digest)); if (verify(request->sourcePublicKey.m256i_u8, digest, request->signaturePtr())) { +#if !defined(NDEBUG) && 1 + appendText(dbgMsg, L" verified"); +#endif if (header->isDejavuZero()) { enqueueResponse(NULL, header); @@ -1005,10 +1038,26 @@ static void processBroadcastTransaction(Peer* peer, RequestResponseHeader* heade // after one has been seen) if (isZero(request->destinationPublicKey) && request->inputType == OracleReplyRevealTransactionPrefix::transactionType()) { +#if !defined(NDEBUG) && 1 + appendText(dbgMsg, L" reveal"); + addDebugMessage(dbgMsg); +#endif oracleEngine.announceExpectedRevealTransaction((OracleReplyRevealTransactionPrefix*)request); } + + if (isZero(request->destinationPublicKey) && request->inputType == OracleReplyCommitTransactionPrefix::transactionType()) + { +#if !defined(NDEBUG) && 0 + appendText(dbgMsg, L" commit"); + addDebugMessage(dbgMsg); +#endif + } } } + +#if !defined(NDEBUG) && 0 + addDebugMessage(dbgMsg); +#endif } static void processRequestComputors(Peer* peer, RequestResponseHeader* header) @@ -2791,6 +2840,27 @@ static void processTickTransaction(const Transaction* transaction, unsigned int ts.transactionsDigestAccess.insertTransaction(transactionDigest, transaction); ts.transactionsDigestAccess.releaseLock(); +#if !defined(NDEBUG) + if (isZero(transaction->destinationPublicKey)) + { + CHAR16 dbgMsg[200]; + /* + if (transaction->inputType == OracleReplyCommitTransactionPrefix::transactionType()) + { + setText(dbgMsg, L"OracleReplyCommitTransaction found in processTickTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + addDebugMessage(dbgMsg); + } + */ + if (transaction->inputType == OracleReplyRevealTransactionPrefix::transactionType()) + { + setText(dbgMsg, L"OracleReplyRevealTransaction found in processTickTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + addDebugMessage(dbgMsg); + } + } +#endif + const int spectrumIndex = ::spectrumIndex(transaction->sourcePublicKey); if (spectrumIndex >= 0) { @@ -3342,6 +3412,21 @@ static void processTick(unsigned long long processorNumber) getUniverseDigest(etalonTick.saltedUniverseDigest); getComputerDigest(etalonTick.saltedComputerDigest); +#if !defined(NDEBUG) && 1 + { + CHAR16 dbgMsg[500]; + setText(dbgMsg, L"pending tx: tick/count"); + for (unsigned int i = system.tick; i < system.tick + 10; ++i) + { + appendText(dbgMsg, " "); + appendNumber(dbgMsg, i, FALSE); + appendText(dbgMsg, "/"); + appendNumber(dbgMsg, pendingTxsPool.getNumberOfPendingTickTxs(i), FALSE); + } + addDebugMessage(dbgMsg); + } +#endif + // prepare custom mining shares packet ONCE if (isMainMode()) { @@ -3512,6 +3597,7 @@ static void processTick(unsigned long long processorNumber) PROFILE_NAMED_SCOPE("processTick(): broadcast oracle reply transactions"); const auto txTick = system.tick + ORACLE_REPLY_COMMIT_PUBLICATION_OFFSET; auto* tx = (OracleReplyCommitTransactionPrefix*)reorgBuffer; + unsigned int txCount = 0; for (unsigned int i = 0; i < numberOfOwnComputorIndices; i++) { const auto ownCompIdx = ownComputorIndicesMapping[i]; @@ -3531,9 +3617,39 @@ static void processTick(unsigned long long processorNumber) KangarooTwelve(tx, sizeof(Transaction) + tx->inputSize, digest, sizeof(digest)); sign(computorSubseeds[i].m256i_u8, computorPublicKeys[i].m256i_u8, digest, tx->signaturePtr()); enqueueResponse(NULL, tx->totalSize(), BROADCAST_TRANSACTION, 0, tx); + ++txCount; } while (retCode != UINT32_MAX); } + +#if !defined(NDEBUG) + if (txCount) + { + CHAR16 dbgMsg[300]; + setText(dbgMsg, L"oracleEngine.getReplyCommitTransaction(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", txScheduleTick "); + appendNumber(dbgMsg, txTick, FALSE); + for (unsigned int i = 0; i < numberOfOwnComputorIndices; i++) + { + if (txTick % NUMBER_OF_COMPUTORS == ownComputorIndices[i]) + { + appendText(dbgMsg, " (I am tick leader)"); + } + } + appendText(dbgMsg, ", total number of tx "); + appendNumber(dbgMsg, txCount, FALSE); + appendText(dbgMsg, ", last tx queryId"); + unsigned short commitCount = tx->inputSize / sizeof(OracleReplyCommitTransactionItem); + auto* commits = reinterpret_cast(tx->inputPtr()); + for (unsigned short i = 0; i < commitCount; ++i) + { + appendText(dbgMsg, " "); + appendNumber(dbgMsg, commits[i].queryId, FALSE); + } + addDebugMessage(dbgMsg); + } +#endif } { diff --git a/src/text_output.h b/src/text_output.h index 2195b930f..fd9099264 100644 --- a/src/text_output.h +++ b/src/text_output.h @@ -5,6 +5,8 @@ #include "network_messages/common_def.h" +#include "contracts/qpi.h" + static void appendQubicVersion(CHAR16* dst) { @@ -17,11 +19,38 @@ static void appendQubicVersion(CHAR16* dst) static void appendIPv4Address(CHAR16* dst, const IPv4Address& address) { - appendNumber(message, address.u8[0], FALSE); - appendText(message, L"."); - appendNumber(message, address.u8[1], FALSE); - appendText(message, L"."); - appendNumber(message, address.u8[2], FALSE); - appendText(message, L"."); - appendNumber(message, address.u8[3], FALSE); + appendNumber(dst, address.u8[0], FALSE); + appendText(dst, L"."); + appendNumber(dst, address.u8[1], FALSE); + appendText(dst, L"."); + appendNumber(dst, address.u8[2], FALSE); + appendText(dst, L"."); + appendNumber(dst, address.u8[3], FALSE); +} + +static void appendDateAndTime(CHAR16* dst, const QPI::DateAndTime& dt, bool microsec = false) +{ + appendNumber(dst, dt.getYear(), FALSE); + appendText(dst, L"-"); + appendNumber(dst, dt.getMonth(), FALSE); + appendText(dst, L"-"); + appendNumber(dst, dt.getDay(), FALSE); + appendText(dst, L" "); + appendNumber(dst, dt.getHour(), FALSE); + appendText(dst, L":"); + appendNumber(dst, dt.getMinute(), FALSE); + appendText(dst, L":"); + appendNumber(dst, dt.getSecond(), FALSE); + appendText(dst, L"."); + const uint16_t millisec = dt.getMillisec(); + appendNumber(dst, millisec / 100, FALSE); + appendNumber(dst, (millisec % 100) / 10, FALSE); + appendNumber(dst, millisec % 10, FALSE); + if (microsec) + { + const uint16_t microsec = dt.getMicrosecDuringMillisec(); + appendNumber(dst, microsec / 100, FALSE); + appendNumber(dst, (microsec % 100) / 10, FALSE); + appendNumber(dst, microsec % 10, FALSE); + } } diff --git a/src/ticking/pending_txs_pool.h b/src/ticking/pending_txs_pool.h index 41b565540..f5d8bff5f 100644 --- a/src/ticking/pending_txs_pool.h +++ b/src/ticking/pending_txs_pool.h @@ -351,6 +351,25 @@ class PendingTxsPool } #endif } +#if !defined(NDEBUG) && !defined(NO_UEFI) + else + { + CHAR16 dbgMsgBuf[250]; + setText(dbgMsgBuf, L"tx failed (tx->checkValidity() && tickInStorage(tx->tick)): tick "); + appendNumber(dbgMsgBuf, tx->tick, FALSE); + appendText(dbgMsgBuf, L", amount "); + appendNumber(dbgMsgBuf, tx->amount, FALSE); + appendText(dbgMsgBuf, L", inputType "); + appendNumber(dbgMsgBuf, tx->inputType, FALSE); + appendText(dbgMsgBuf, L", inputSize "); + appendNumber(dbgMsgBuf, tx->inputSize, FALSE); + appendText(dbgMsgBuf, L", dst[0] "); + appendNumber(dbgMsgBuf, tx->destinationPublicKey.u64._0, FALSE); + appendText(dbgMsgBuf, L", src[0] "); + appendNumber(dbgMsgBuf, tx->sourcePublicKey.u64._0, FALSE); + addDebugMessage(dbgMsgBuf); + } +#endif end_add_function: RELEASE(lock); From f902702208ab7a9d7b7b77e86faa0f9184b387e9 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 19 Jan 2026 08:31:59 +0100 Subject: [PATCH 43/90] More oracle query stats and request/response for those --- src/network_messages/oracles.h | 37 +++++++++++++ src/oracle_core/net_msg_impl.h | 29 ++++++++++ src/oracle_core/oracle_engine.h | 94 ++++++++++++++++++++++++++------- 3 files changed, 140 insertions(+), 20 deletions(-) diff --git a/src/network_messages/oracles.h b/src/network_messages/oracles.h index 1ff3f4407..2cdbda2e2 100644 --- a/src/network_messages/oracles.h +++ b/src/network_messages/oracles.h @@ -11,6 +11,7 @@ // - all oracle query IDs that are pending (status is neither success nor failure) // - for given oracle query ID: metadata, query, and response if available // - subscription info for given oracle subscription ID +// - query statistics of this node (counts and durations) struct RequestOracleData { static constexpr unsigned char type() @@ -26,6 +27,7 @@ struct RequestOracleData static constexpr unsigned int requestPendingQueryIds = 4; static constexpr unsigned int requestQueryAndResponse = 5; static constexpr unsigned int requestSubscription = 6; + static constexpr unsigned int requestQueryStatistics = 7; unsigned int reqType; unsigned int _padding; @@ -67,6 +69,9 @@ struct RespondOracleData // The payload is RespondOracleDataSubscriptionContractMetadata. static constexpr unsigned int respondSubscriptionContractMetadata = 6; + // The payload is RespondOracleDataQueryStatistics. + static constexpr unsigned int respondQueryStatistics = 7; + // type of oracle response unsigned int resType; }; @@ -107,3 +112,35 @@ struct RespondOracleDataSubscriptionContractMetadata uint16_t notificationIntervalMinutes; uint64_t nextQueryNotificationTimestamp; ///< Timeout in QPI::DateAndTime format }; + +struct RespondOracleDataQueryStatistics +{ + uint64_t pendingCount; + uint64_t pendingOracleMachineCount; + uint64_t pendingCommitCount; + uint64_t pendingReplyCount; + + uint64_t successfulCount; + + uint64_t unresolvableCount; + + uint64_t timeoutCount; + uint64_t timeoutNoReplyCount; + uint64_t timeoutNoCommitCount; + uint64_t timeoutNoRevealCount; + + /// For how many queries multiple OM nodes connected to this Core node sent differing replies + uint64_t oracleMachineRepliesDisagreeCount; + + /// How many thousandth ticks it takes on average until the OM reply is received + uint64_t oracleMachineReplyAvgMilliTicksPerQuery; + + /// How many thousandth ticks it takes on average until the commit status is reached (until 451 commit tx got executed) + uint64_t commitAvgMilliTicksPerQuery; + + /// How many thousandth ticks it takes on average until until success (ticks until 1 reveal tx got executed) + uint64_t successAvgMilliTicksPerQuery; + + /// How many thousandth ticks it takes on average until until timeout (only considering cases in which timeout happened) + uint64_t timeoutAvgMilliTicksPerQuery; +}; diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index a1e3889ea..e9643385b 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -148,6 +148,35 @@ void OracleEngine::processRequestOracleData(Peer* peer, R // TODO break; + case RequestOracleData::requestQueryStatistics: + { + // prepare response + response->resType = RespondOracleData::respondQueryStatistics; + auto* p = (RespondOracleDataQueryStatistics*)payload; + setMemory(*p, 0); + p->pendingCount = pendingQueryIndices.numValues; + p->pendingOracleMachineCount = pendingQueryIndices.numValues - pendingCommitReplyStateIndices.numValues - pendingRevealReplyStateIndices.numValues; + p->pendingCommitCount = pendingCommitReplyStateIndices.numValues; + p->pendingReplyCount = pendingRevealReplyStateIndices.numValues; + p->successfulCount = stats.successCount; + p->unresolvableCount = stats.unresolvableCount; + const uint64_t totalTimeouts = stats.timeoutNoReplyCount + stats.timeoutNoCommitCount + stats.timeoutNoReplyCount; + p->timeoutCount = totalTimeouts; + p->timeoutNoReplyCount = stats.timeoutNoReplyCount; + p->timeoutNoCommitCount = stats.timeoutNoCommitCount; + p->timeoutNoRevealCount = stats.timeoutNoRevealCount; + p->oracleMachineRepliesDisagreeCount = stats.oracleMachineRepliesDisagreeCount; + p->oracleMachineReplyAvgMilliTicksPerQuery = (stats.oracleMachineReplyCount) ? stats.oracleMachineReplyTicksSum * 1000 / stats.oracleMachineReplyCount : 0; + p->commitAvgMilliTicksPerQuery = (stats.commitCount) ? stats.successTicksSum * 1000 / stats.successCount : 0; + p->successAvgMilliTicksPerQuery = (stats.successCount) ? stats.successTicksSum * 1000 / stats.successCount : 0; + p->timeoutAvgMilliTicksPerQuery = (totalTimeouts) ? stats.timeoutTicksSum * 1000 / totalTimeouts : 0; + + // send response + enqueueResponse(peer, sizeof(RespondOracleData) + sizeof(RespondOracleDataQueryStatistics), + RespondOracleData::type(), header->dejavu(), response); + break; + } + } enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), nullptr); diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 93944f7a9..26ea6253c 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -253,17 +253,35 @@ class OracleEngine /// sum of ticks that were required to reach the success state unsigned long long successTicksSum; - /// total number of timeout oracle queries - unsigned long long timeoutCount; + /// total number of timeout oracle queries without reply from oracle machine + unsigned long long timeoutNoReplyCount; + + /// total number of timeout oracle queries without commit quorum + unsigned long long timeoutNoCommitCount; + + /// total number of timeout oracle queries without reveal + unsigned long long timeoutNoRevealCount; + + /// sum of ticks until timeout of all timeout cases + unsigned long long timeoutTicksSum; /// total number of unresolvable oracle queries unsigned long long unresolvableCount; + /// total number of oracle queries that got oracle machine reply locally + unsigned long long oracleMachineReplyCount; + + /// sum of ticks that were required to get oracle machine reply locally + unsigned long long oracleMachineReplyTicksSum; + /// total number of oracle queries that reached commit state unsigned long long commitCount; /// sum of ticks that were required to reach the commit state unsigned long long commitTicksSum; + + /// total number of oracle machin replies that disagree with the first reply received for a query + unsigned long long oracleMachineRepliesDisagreeCount; } stats; #if ENABLE_ORACLE_STATS_RECORD @@ -571,7 +589,10 @@ class OracleEngine { // if the digests don't match, set an error flag if (replyDigest != replyState.ownReplyDigest) + { oqm.statusFlags |= ORACLE_FLAG_OM_DISAGREE; + ++stats.oracleMachineRepliesDisagreeCount; + } return; } @@ -581,6 +602,10 @@ class OracleEngine replyState.ownReplySize = replySize; oqm.statusFlags |= ORACLE_FLAG_REPLY_RECEIVED; + // update statistics + ++stats.oracleMachineReplyCount; + stats.oracleMachineReplyTicksSum += (system.tick - oqm.queryTick); + // add reply state to set of indices with pending commit tx pendingCommitReplyStateIndices.add(replyStateIdx); @@ -1204,13 +1229,21 @@ class OracleEngine const uint16_t mostCommitsCount = replyState.replyCommitHistogramCount[replyState.mostCommitsHistIdx]; ASSERT(replyState.mostCommitsHistIdx < NUMBER_OF_COMPUTORS && mostCommitsCount <= NUMBER_OF_COMPUTORS); + // update statistics + stats.timeoutTicksSum += (system.tick - oqm.queryTick); + if (oqm.status == ORACLE_QUERY_STATUS_COMMITTED) + ++stats.timeoutNoRevealCount; + else if (oqm.statusFlags & ORACLE_FLAG_REPLY_RECEIVED) + ++stats.timeoutNoCommitCount; + else + ++stats.timeoutNoReplyCount; + // update state to TIMEOUT oqm.status = ORACLE_QUERY_STATUS_TIMEOUT; oqm.statusFlags |= ORACLE_FLAG_TIMEOUT; oqm.statusVar.failure.agreeingCommits = mostCommitsCount; oqm.statusVar.failure.totalCommits = replyState.totalCommits; pendingQueryIndices.removeByValue(queryIndex); - ++stats.timeoutCount; // cleanup reply state pendingCommitReplyStateIndices.removeByValue(replyStateIdx); @@ -1387,31 +1420,52 @@ class OracleEngine void logStatus(CHAR16* message) const { + auto appendQuotientWithOneDecimal = [](CHAR16* message, uint64_t dividend, uint64_t divisor) + { + unsigned long long quotient10 = (divisor) ? (dividend * 10 / divisor) : 0; + appendText(message, ", "); + appendNumber(message, quotient10 / 10, FALSE); + appendText(message, "."); + appendNumber(message, quotient10 % 10, FALSE); + }; + + setText(message, L"Oracles queries: pending "); + // print total number of pending queries + appendNumber(message, pendingQueryIndices.numValues, FALSE); + // print number of pending queries currently waiting for OM reply + appendText(message, " (OM "); + appendNumber(message, pendingQueryIndices.numValues - pendingCommitReplyStateIndices.numValues - pendingRevealReplyStateIndices.numValues, FALSE); + // print how many ticks it takes on average until the OM reply is received + appendQuotientWithOneDecimal(message, stats.oracleMachineReplyTicksSum, stats.oracleMachineReplyCount); + appendText(message, " ticks; commit "); + // print number of pending queries currently waiting for commit status appendNumber(message, pendingCommitReplyStateIndices.numValues, FALSE); - appendText(message, " ("); // print how many ticks it takes on average until the commit status is reached (until 451 commit tx got executed) - unsigned long long ticks10perCommit = (stats.commitCount) ? (stats.commitTicksSum * 10 / stats.commitCount) : 0; - appendNumber(message, ticks10perCommit / 10, FALSE); - appendText(message, "."); - appendNumber(message, ticks10perCommit % 10, FALSE); - appendText(message, " ticks) / "); + appendQuotientWithOneDecimal(message, stats.commitTicksSum, stats.commitCount); + appendText(message, " ticks; reveal "); + // print number of pending queries currently waiting for reveal transaction appendNumber(message, pendingRevealReplyStateIndices.numValues, FALSE); - appendText(message, " ("); - // print how many ticks it takes on average between commit and success (ticks until 1 reveal tx got executed) - unsigned long long ticks10perSuccess = (stats.successCount) ? (stats.successTicksSum * 10 / stats.successCount) : 0; - unsigned long long ticks10perReveal = ticks10perSuccess - ticks10perCommit; - appendNumber(message, ticks10perReveal / 10, FALSE); - appendText(message, "."); - appendNumber(message, ticks10perReveal % 10, FALSE); - appendText(message, " ticks) / "); - appendNumber(message, pendingQueryIndices.numValues, FALSE); + // print how many ticks it takes on average until success (ticks until 1 reveal tx got executed) + appendQuotientWithOneDecimal(message, stats.successTicksSum, stats.successCount); + appendText(message, " ticks), "); appendText(message, ", successful "); appendNumber(message, stats.successCount, FALSE); - appendText(message, ", timeout "); - appendNumber(message, stats.timeoutCount, FALSE); appendText(message, ", unresolvable "); appendNumber(message, stats.unresolvableCount, FALSE); + appendText(message, ", timeout "); + const uint64_t totalTimeouts = stats.timeoutNoReplyCount + stats.timeoutNoCommitCount + stats.timeoutNoReplyCount; + appendNumber(message, totalTimeouts, FALSE); + appendText(message, " (OM "); + appendNumber(message, stats.timeoutNoReplyCount, FALSE); + appendText(message, " ; commit "); + appendNumber(message, stats.timeoutNoCommitCount, FALSE); + appendText(message, " ; reveal "); + appendNumber(message, stats.timeoutNoRevealCount, FALSE); + appendText(message, " ; ticks "); + appendQuotientWithOneDecimal(message, stats.timeoutTicksSum, totalTimeouts); + appendText(message, "), conflicting OM replies "); + appendNumber(message, stats.oracleMachineRepliesDisagreeCount, FALSE); logToConsole(message); #if ENABLE_ORACLE_STATS_RECORD From 565af823b9878bb17dcecab9e3d6ba32a1de58b8 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:01:44 +0100 Subject: [PATCH 44/90] Fix typo in oracle query stats struct --- src/network_messages/oracles.h | 2 +- src/oracle_core/net_msg_impl.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network_messages/oracles.h b/src/network_messages/oracles.h index 2cdbda2e2..16fc3d2e8 100644 --- a/src/network_messages/oracles.h +++ b/src/network_messages/oracles.h @@ -118,7 +118,7 @@ struct RespondOracleDataQueryStatistics uint64_t pendingCount; uint64_t pendingOracleMachineCount; uint64_t pendingCommitCount; - uint64_t pendingReplyCount; + uint64_t pendingRevealCount; uint64_t successfulCount; diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index e9643385b..19225a076 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -157,7 +157,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R p->pendingCount = pendingQueryIndices.numValues; p->pendingOracleMachineCount = pendingQueryIndices.numValues - pendingCommitReplyStateIndices.numValues - pendingRevealReplyStateIndices.numValues; p->pendingCommitCount = pendingCommitReplyStateIndices.numValues; - p->pendingReplyCount = pendingRevealReplyStateIndices.numValues; + p->pendingRevealCount = pendingRevealReplyStateIndices.numValues; p->successfulCount = stats.successCount; p->unresolvableCount = stats.unresolvableCount; const uint64_t totalTimeouts = stats.timeoutNoReplyCount + stats.timeoutNoCommitCount + stats.timeoutNoReplyCount; From b612d062e69d4c3483382b0af4c583250aab83d6 Mon Sep 17 00:00:00 2001 From: TakaYuPP Date: Mon, 19 Jan 2026 01:28:57 -0800 Subject: [PATCH 45/90] QIP: not transfer the share management during the ICO & fixing the bug in the end of 3 phase of ICO (#718) --- src/contracts/QIP.h | 29 ++++++++++++++++++++++++----- test/contract_qip.cpp | 12 ++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/contracts/QIP.h b/src/contracts/QIP.h index 870f50775..c0ec8a12a 100644 --- a/src/contracts/QIP.h +++ b/src/contracts/QIP.h @@ -410,13 +410,28 @@ struct QIP : public ContractBase LOG_INFO(locals.log); } - PUBLIC_PROCEDURE(TransferShareManagementRights) + struct TransferShareManagementRights_locals + { + ICOInfo ico; + uint32 i; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) { if (qpi.invocationReward() < state.transferRightsFee) { return ; } + for (locals.i = 0 ; locals.i < state.numberOfICO; locals.i++) + { + locals.ico = state.icos.get(locals.i); + if (locals.ico.issuer == input.asset.issuer && locals.ico.assetName == input.asset.assetName) + { + return ; + } + } + if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) { // not enough shares available @@ -467,12 +482,12 @@ struct QIP : public ContractBase struct END_EPOCH_locals { ICOInfo ico; - uint32 idx; + sint32 idx; }; END_EPOCH_WITH_LOCALS() { - for(locals.idx = 0; locals.idx < state.numberOfICO; locals.idx++) + for(locals.idx = 0; locals.idx < (sint32)state.numberOfICO; locals.idx++) { locals.ico = state.icos.get(locals.idx); if (locals.ico.startEpoch == qpi.epoch() && locals.ico.remainingAmountForPhase1 > 0) @@ -487,11 +502,15 @@ struct QIP : public ContractBase locals.ico.remainingAmountForPhase2 = 0; state.icos.set(locals.idx, locals.ico); } - if (locals.ico.startEpoch + 2 == qpi.epoch() && locals.ico.remainingAmountForPhase3 > 0) + if (locals.ico.startEpoch + 2 == qpi.epoch()) { - qpi.transferShareOwnershipAndPossession(locals.ico.assetName, locals.ico.issuer, SELF, SELF, locals.ico.remainingAmountForPhase3, locals.ico.creatorOfICO); + if (locals.ico.remainingAmountForPhase3 > 0) + { + qpi.transferShareOwnershipAndPossession(locals.ico.assetName, locals.ico.issuer, SELF, SELF, locals.ico.remainingAmountForPhase3, locals.ico.creatorOfICO); + } state.icos.set(locals.idx, state.icos.get(state.numberOfICO - 1)); state.numberOfICO--; + locals.idx--; } } } diff --git a/test/contract_qip.cpp b/test/contract_qip.cpp index 3a667c7c9..034fd9b1e 100644 --- a/test/contract_qip.cpp +++ b/test/contract_qip.cpp @@ -1426,11 +1426,19 @@ TEST(ContractQIP, TransferShareManagementRights) // Transfer management rights sint64 transferAmount = 100000; - increaseEnergy(creator, QIP_TRANSFER_RIGHTS_FEE); + increaseEnergy(creator, QIP_TRANSFER_RIGHTS_FEE * 2); + QIP.endEpoch(); + system.epoch += 1; + QIP.endEpoch(); + system.epoch += 1; sint64 transferred = QIP.transferShareManagementRights(creator, asset, transferAmount, QX_CONTRACT_INDEX, QIP_TRANSFER_RIGHTS_FEE); + EXPECT_EQ(transferred, 0); + QIP.endEpoch(); + transferred = QIP.transferShareManagementRights(creator, asset, transferAmount, QX_CONTRACT_INDEX, QIP_TRANSFER_RIGHTS_FEE); EXPECT_EQ(transferred, transferAmount); // Verify shares were transferred - EXPECT_EQ(numberOfPossessedShares(assetName, issuer, QIP_CONTRACT_ID, QIP_CONTRACT_ID, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), totalShares - transferAmount); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, QIP_CONTRACT_ID, QIP_CONTRACT_ID, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), 0); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, creator, creator, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), totalShares - transferAmount); } From 211f36a7147b8b8341311ca821bd198cc5d2f430 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:04:20 +0100 Subject: [PATCH 46/90] Extend and fix stats --- src/network_messages/oracles.h | 1 + src/oracle_core/net_msg_impl.h | 2 +- src/oracle_core/oracle_engine.h | 25 +++++++++++++++++++------ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/network_messages/oracles.h b/src/network_messages/oracles.h index 16fc3d2e8..a8c6f2a09 100644 --- a/src/network_messages/oracles.h +++ b/src/network_messages/oracles.h @@ -121,6 +121,7 @@ struct RespondOracleDataQueryStatistics uint64_t pendingRevealCount; uint64_t successfulCount; + uint64_t revealTxCount; uint64_t unresolvableCount; diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index 19225a076..b11ef47ad 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -167,7 +167,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R p->timeoutNoRevealCount = stats.timeoutNoRevealCount; p->oracleMachineRepliesDisagreeCount = stats.oracleMachineRepliesDisagreeCount; p->oracleMachineReplyAvgMilliTicksPerQuery = (stats.oracleMachineReplyCount) ? stats.oracleMachineReplyTicksSum * 1000 / stats.oracleMachineReplyCount : 0; - p->commitAvgMilliTicksPerQuery = (stats.commitCount) ? stats.successTicksSum * 1000 / stats.successCount : 0; + p->commitAvgMilliTicksPerQuery = (stats.commitCount) ? stats.commitTicksSum * 1000 / stats.commitCount : 0; p->successAvgMilliTicksPerQuery = (stats.successCount) ? stats.successTicksSum * 1000 / stats.successCount : 0; p->timeoutAvgMilliTicksPerQuery = (totalTimeouts) ? stats.timeoutTicksSum * 1000 / totalTimeouts : 0; diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 26ea6253c..5a97c8c13 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -282,6 +282,9 @@ class OracleEngine /// total number of oracle machin replies that disagree with the first reply received for a query unsigned long long oracleMachineRepliesDisagreeCount; + + /// total number of reply reveal transactions + unsigned long long revealTxCount; } stats; #if ENABLE_ORACLE_STATS_RECORD @@ -1157,6 +1160,10 @@ class OracleEngine // lock for accessing engine data LockGuard lockGuard(lock); + // track how many reveal tx are processed in total for later optimizing code to achieve minimal overall + // oracle reply latency with low number of reveal tx + ++stats.revealTxCount; + // check tx and get reply state + query metadata uint32_t queryIndex; ReplyState* replyState = checkReplyRevealTransaction(transaction, &queryIndex); @@ -1448,24 +1455,30 @@ class OracleEngine appendNumber(message, pendingRevealReplyStateIndices.numValues, FALSE); // print how many ticks it takes on average until success (ticks until 1 reveal tx got executed) appendQuotientWithOneDecimal(message, stats.successTicksSum, stats.successCount); - appendText(message, " ticks), "); - appendText(message, ", successful "); + appendText(message, " ticks), successful "); appendNumber(message, stats.successCount, FALSE); appendText(message, ", unresolvable "); appendNumber(message, stats.unresolvableCount, FALSE); appendText(message, ", timeout "); - const uint64_t totalTimeouts = stats.timeoutNoReplyCount + stats.timeoutNoCommitCount + stats.timeoutNoReplyCount; + const uint64_t totalTimeouts = stats.timeoutNoReplyCount + stats.timeoutNoCommitCount + stats.timeoutNoRevealCount; appendNumber(message, totalTimeouts, FALSE); appendText(message, " (OM "); appendNumber(message, stats.timeoutNoReplyCount, FALSE); - appendText(message, " ; commit "); + appendText(message, "; commit "); appendNumber(message, stats.timeoutNoCommitCount, FALSE); - appendText(message, " ; reveal "); + appendText(message, "; reveal "); appendNumber(message, stats.timeoutNoRevealCount, FALSE); - appendText(message, " ; ticks "); + appendText(message, "; ticks "); appendQuotientWithOneDecimal(message, stats.timeoutTicksSum, totalTimeouts); appendText(message, "), conflicting OM replies "); appendNumber(message, stats.oracleMachineRepliesDisagreeCount, FALSE); + appendText(message, "), query slots occupied "); + appendNumber(message, oracleQueryCount * 100 / MAX_ORACLE_QUERIES, FALSE); + appendText(message, "%, query storage occupied "); + appendNumber(message, queryStorageBytesUsed * 100 / ORACLE_QUERY_STORAGE_SIZE, FALSE); + appendText(message, "%"); + appendQuotientWithOneDecimal(message, stats.revealTxCount, stats.successCount); + appendText(message, " reveal tx per success"); logToConsole(message); #if ENABLE_ORACLE_STATS_RECORD From c2cf9c102506e6f5108f7f3e151a38e0d22500ad Mon Sep 17 00:00:00 2001 From: cyber-pc <165458555+cyber-pc@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:36:33 +0700 Subject: [PATCH 47/90] Add addition mining algorithm (#716) * Remove memory leak of score reference. * Update score generation tool. * Add more data for testing * Reduce complexity of clean up neurons. * Adapt score reference hyperidentity and addition score. * Separate score engine to qubic's score. * Add score addition algorithm. * Change score test with new addition score. * Score use INVALID_THRESHOLD as invalid score. * Add dummy getLastOuput for qpi's mining. * Add dummy qubic's solution threshold. * Init memory for compute buffer. * ScoreAddition: Process each 64 samples with SIMD. * Fix score computation match with Qiner version. * Update score addtion test cases. * Update sample file with header. * Minimal config for test score. * Change score's algo enum. * Support multiple score thresholds. * Get last output for mining only run the hyperidentity algo. * Replace memcpy with save/load with SIMD register. * Remove threshold related comments. --- src/Qubic.vcxproj | 4 + src/Qubic.vcxproj.filters | 14 +- src/contract_core/qpi_mining_impl.h | 17 +- src/mining/score_addition.h | 1327 ++++++++++++++ src/mining/score_common.h | 542 ++++++ src/mining/score_engine.h | 70 + src/mining/score_hyperidentity.h | 1214 +++++++++++++ src/network_messages/special_command.h | 2 + src/network_messages/system_info.h | 2 +- src/public_settings.h | 24 +- src/qubic.cpp | 70 +- src/score.h | 1528 +---------------- test/data/samples_20240815.csv | 769 +++++++++ test/data/scores_addition.csv | 1025 +++++++++++ test/data/scores_hyperidentity.csv | 1025 +++++++++++ test/data/scores_v4.csv | 257 --- test/data/scores_v5.csv | 257 --- test/score.cpp | 822 ++++++--- test/score_addition_reference.h | 869 ++++++++++ test/score_common_reference.h | 108 ++ test/score_hyperidentity_reference.h | 785 +++++++++ test/score_params.h | 73 +- test/score_reference.h | 852 +-------- test/test.vcxproj | 5 +- test/test.vcxproj.filters | 3 + .../score_test_generator.vcxproj | 2 + tools/score_test_generator/test_generator.cpp | 165 +- 27 files changed, 8603 insertions(+), 3228 deletions(-) create mode 100644 src/mining/score_addition.h create mode 100644 src/mining/score_common.h create mode 100644 src/mining/score_engine.h create mode 100644 src/mining/score_hyperidentity.h create mode 100644 test/data/scores_addition.csv create mode 100644 test/data/scores_hyperidentity.csv delete mode 100644 test/data/scores_v4.csv delete mode 100644 test/data/scores_v5.csv create mode 100644 test/score_addition_reference.h create mode 100644 test/score_common_reference.h create mode 100644 test/score_hyperidentity_reference.h diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 78634bba1..2128d5fa1 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -70,6 +70,10 @@ + + + + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 2c0da5fb5..edc604737 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -312,6 +312,18 @@ contract_core + + mining + + + mining + + + mining + + + mining + contracts @@ -362,4 +374,4 @@ platform - \ No newline at end of file + diff --git a/src/contract_core/qpi_mining_impl.h b/src/contract_core/qpi_mining_impl.h index be4cf796c..bd2196d39 100644 --- a/src/contract_core/qpi_mining_impl.h +++ b/src/contract_core/qpi_mining_impl.h @@ -3,16 +3,7 @@ #include "contracts/qpi.h" #include "score.h" -static ScoreFunction< - NUMBER_OF_INPUT_NEURONS, - NUMBER_OF_OUTPUT_NEURONS, - NUMBER_OF_TICKS*2, - NUMBER_OF_NEIGHBORS, - POPULATION_THRESHOLD, - NUMBER_OF_MUTATIONS, - SOLUTION_THRESHOLD_DEFAULT, - 1 ->* score_qpi = nullptr; // NOTE: SC is single-threaded +static ScoreFunction<1>* score_qpi = nullptr; // NOTE: SC is single-threaded m256i QPI::QpiContextFunctionCall::computeMiningFunction(const m256i miningSeed, const m256i publicKey, const m256i nonce) const { @@ -22,6 +13,10 @@ m256i QPI::QpiContextFunctionCall::computeMiningFunction(const m256i miningSeed, { score_qpi->initMiningData(miningSeed); } - (*score_qpi)(0, publicKey, miningSeed, nonce); + m256i hyperIdentityNonce = nonce; + // Currently, only hyperidentity scoring support the last output + hyperIdentityNonce.m256i_u8[0] = (hyperIdentityNonce.m256i_u8[0] & 0xFE); + ASSERT((hyperIdentityNonce.m256i_u8[0] & 1) == 0); + (*score_qpi)(0, publicKey, miningSeed, hyperIdentityNonce); return score_qpi->getLastOutput(0); } diff --git a/src/mining/score_addition.h b/src/mining/score_addition.h new file mode 100644 index 000000000..3d217526b --- /dev/null +++ b/src/mining/score_addition.h @@ -0,0 +1,1327 @@ +#pragma once + +#include "score_common.h" + +namespace score_engine +{ +template +struct ScoreAddition +{ + + // Convert params for easier usage + static constexpr unsigned long long numberOfInputNeurons = Params::numberOfInputNeurons; + static constexpr unsigned long long numberOfOutputNeurons = Params::numberOfOutputNeurons; + static constexpr unsigned long long numberOfTicks = Params::numberOfTicks; + static constexpr unsigned long long maxNumberOfNeighbors = Params::numberOfNeighbors; + static constexpr unsigned long long populationThreshold = Params::populationThreshold; + static constexpr unsigned long long numberOfMutations = Params::numberOfMutations; + static constexpr unsigned int solutionThreshold = Params::solutionThreshold; + + static constexpr unsigned long long numberOfNeurons = + numberOfInputNeurons + numberOfOutputNeurons; + static constexpr unsigned long long maxNumberOfNeurons = populationThreshold; + static constexpr unsigned long long maxNumberOfSynapses = + populationThreshold * maxNumberOfNeighbors; + static constexpr unsigned long long trainingSetSize = 1ULL << numberOfInputNeurons; // 2^K + static constexpr unsigned long long paddingNumberOfSynapses = + (maxNumberOfSynapses + 31) / 32 * 32; // padding to multiple of 32 + +#if defined(__AVX512F__) + static constexpr unsigned long long BATCH_SIZE = 64; +#else // AVX2 path + static constexpr unsigned long long BATCH_SIZE = 32; +#endif + static constexpr unsigned long long PADDED_SAMPLES = + ((trainingSetSize + BATCH_SIZE - 1) / BATCH_SIZE) * BATCH_SIZE; + + static_assert( + maxNumberOfSynapses <= (0xFFFFFFFFFFFFFFFF << 1ULL), + "maxNumberOfSynapses must less than or equal MAX_UINT64/2"); + static_assert(maxNumberOfNeighbors % 2 == 0, "maxNumberOfNeighbors must divided by 2"); + static_assert( + populationThreshold > numberOfNeurons, + "populationThreshold must be greater than numberOfNeurons"); + static_assert( + PADDED_SAMPLES% BATCH_SIZE == 0, + "PADDED_SAMPLES must be a multiple of BATCH_SIZE"); + static_assert( + trainingSetSize <= 0xFFFFFFFF, + "trainingSetSize must fit in unsigned int for sampleMapping"); + + typedef char Synapse; + typedef char Neuron; + + // Data for roll back + struct ANN + { + unsigned char neuronTypes[maxNumberOfNeurons]; + Synapse synapses[maxNumberOfSynapses]; + unsigned long long population; + }; + + // Intermediate data + struct InitValue + { + unsigned long long outputNeuronPositions[numberOfOutputNeurons]; + unsigned long long synapseWeight[paddingNumberOfSynapses / 32]; // each 64bits elements will decide value of 32 synapses + unsigned long long synpaseMutation[numberOfMutations]; + }; + static constexpr unsigned long long paddingInitValueSizeInBytes = (sizeof(InitValue) + 64 - 1) / 64 * 64; + + unsigned char paddingInitValue[paddingInitValueSizeInBytes]; + + // Training set + alignas(64) char trainingInputs[numberOfInputNeurons * PADDED_SAMPLES]; + alignas(64) char trainingOutputs[numberOfOutputNeurons * PADDED_SAMPLES]; + + // For accessing neuron values of multiple samples + char* neuronValues; + char* prevNeuronValues; + alignas(64) char neuronValuesBuffer0[maxNumberOfNeurons * PADDED_SAMPLES]; + alignas(64) char neuronValuesBuffer1[maxNumberOfNeurons * PADDED_SAMPLES]; + + // Incoming synapses + alignas(64) unsigned int incomingSource[maxNumberOfNeurons * maxNumberOfNeighbors]; + alignas(64) Synapse incomingSynapses[maxNumberOfNeurons * maxNumberOfNeighbors]; + unsigned int incomingCount[maxNumberOfNeurons]; + + // For tracking/compacting sample after each tick + alignas(64) unsigned int sampleMapping[PADDED_SAMPLES]; + alignas(64) unsigned int sampleScores[PADDED_SAMPLES]; + unsigned long long activeCount; + + // Indices caching look up + unsigned long long neuronIndices[numberOfNeurons]; + unsigned long long outputNeuronIndices[numberOfOutputNeurons]; + unsigned long long outputNeuronIdxCache[numberOfOutputNeurons]; + unsigned long long numCachedOutputs; + + // Buffers for cleaning up + unsigned long long removalNeurons[maxNumberOfNeurons]; + unsigned long long numberOfRedundantNeurons; + + // ANN structure + ANN bestANN; + ANN currentANN; + + // Temp buffers + char inputBits[numberOfInputNeurons]; + char outputBits[numberOfOutputNeurons]; + + void initMemory() + { + generateTrainingSet(); + } + + + void initialize(unsigned char miningSeed[32]) + { + } + + unsigned long long getActualNeighborCount() const + { + unsigned long long population = currentANN.population; + unsigned long long maxNeighbors = population - 1; // Exclude self + unsigned long long actual = maxNumberOfNeighbors > maxNeighbors ? maxNeighbors : maxNumberOfNeighbors; + + return actual; + } + + unsigned long long getLeftNeighborCount() const + { + unsigned long long actual = getActualNeighborCount(); + // For odd number, we add extra for the left + return (actual + 1) / 2; + } + + unsigned long long getRightNeighborCount() const + { + return getActualNeighborCount() - getLeftNeighborCount(); + } + + // Get the starting index in synapse buffer (left side start) + unsigned long long getSynapseStartIndex() const + { + constexpr unsigned long long synapseBufferCenter = maxNumberOfNeighbors / 2; + return synapseBufferCenter - getLeftNeighborCount(); + } + + // Get the ending index in synapse buffer (exclusive) + unsigned long long getSynapseEndIndex() const + { + constexpr unsigned long long synapseBufferCenter = maxNumberOfNeighbors / 2; + return synapseBufferCenter + getRightNeighborCount(); + } + + // Convert buffer index to neighbor offset + long long bufferIndexToOffset(unsigned long long bufferIdx) const + { + constexpr long long synapseBufferCenter = maxNumberOfNeighbors / 2; + if (bufferIdx < synapseBufferCenter) + { + return (long long)bufferIdx - synapseBufferCenter; // Negative (left) + } + else + { + return (long long)bufferIdx - synapseBufferCenter + 1; // Positive (right), skip 0 + } + } + + // Convert neighbor offset to buffer index + long long offsetToBufferIndex(long long offset) const + { + constexpr long long synapseBufferCenter = maxNumberOfNeighbors / 2; + if (offset == 0) + { + return -1; // Invalid, exclude self + } + else if (offset < 0) + { + return synapseBufferCenter + offset; + } + else + { + return synapseBufferCenter + offset - 1; + } + } + + long long getIndexInSynapsesBuffer(long long neighborOffset) const + { + long long leftCount = (long long)getLeftNeighborCount(); + long long rightCount = (long long)getRightNeighborCount(); + + if (neighborOffset == 0 || + neighborOffset < -leftCount || + neighborOffset > rightCount) + { + return -1; + } + + return offsetToBufferIndex(neighborOffset); + } + + void cacheOutputNeuronIndices() + { + numCachedOutputs = 0; + for (unsigned long long i = 0; i < currentANN.population; i++) + { + if (currentANN.neuronTypes[i] == OUTPUT_NEURON_TYPE) + { + outputNeuronIdxCache[numCachedOutputs++] = i; + } + } + } + + void mutate(unsigned long long mutateStep) + { + // Mutation + unsigned long long population = currentANN.population; + unsigned long long actualNeighbors = getActualNeighborCount(); + Synapse* synapses = currentANN.synapses; + InitValue* initValue = (InitValue*)paddingInitValue; + + // Randomly pick a synapse, randomly increase or decrease its weight by 1 or -1 + unsigned long long synapseMutation = initValue->synpaseMutation[mutateStep]; + unsigned long long totalValidSynapses = population * actualNeighbors; + unsigned long long flatIdx = (synapseMutation >> 1) % totalValidSynapses; + + // Convert flat index to (neuronIdx, local synapse index within valid range) + unsigned long long neuronIdx = flatIdx / actualNeighbors; + unsigned long long localSynapseIdx = flatIdx % actualNeighbors; + + // Convert to synapse buffer index that have bigger range + unsigned long long synapseIndex = localSynapseIdx + getSynapseStartIndex(); + unsigned long long synapseFullBufferIdx = neuronIdx * maxNumberOfNeighbors + synapseIndex; + + // Randomly increase or decrease its value + char weightChange = 0; + if ((synapseMutation & 1ULL) == 0) + { + weightChange = -1; + } + else + { + weightChange = 1; + } + + char newWeight = synapses[synapseFullBufferIdx] + weightChange; + + // Valid weight. Update it + if (newWeight >= -1 && newWeight <= 1) + { + synapses[synapseFullBufferIdx] = newWeight; + } + else // Invalid weight. Insert a neuron + { + // Insert the neuron + insertNeuron(neuronIdx, synapseIndex); + } + + // Clean the ANN + while (scanRedundantNeurons() > 0) + { + cleanANN(); + } + } + + // Get the pointer to all outgoing synapse of a neurons + Synapse* getSynapses(unsigned long long neuronIndex) + { + return ¤tANN.synapses[neuronIndex * maxNumberOfNeighbors]; + } + + // Calculate the new neuron index that is reached by moving from the given `neuronIdx` `value` + // neurons to the right or left. Negative `value` moves to the left, positive `value` moves to + // the right. The return value is clamped in a ring buffer fashion, i.e. moving right of the + // rightmost neuron continues at the leftmost neuron. + unsigned long long clampNeuronIndex(long long neuronIdx, long long value) + { + return clampCirculatingIndex((long long)currentANN.population, neuronIdx, value); + } + + + // Remove a neuron and all synapses relate to it + void removeNeuron(unsigned long long neuronIdx) + { + long long leftCount = (long long)getLeftNeighborCount(); + long long rightCount = (long long)getRightNeighborCount(); + unsigned long long startSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long endSynapseBufferIdx = getSynapseEndIndex(); + + // Scan all its neighbor to remove their outgoing synapse point to the neuron + for (long long neighborOffset = -leftCount; neighborOffset <= rightCount; neighborOffset++) + { + if (neighborOffset == 0) continue; + + unsigned long long nnIdx = clampNeuronIndex(neuronIdx, neighborOffset); + Synapse* pNNSynapses = getSynapses(nnIdx); + + long long synapseIndexOfNN = getIndexInSynapsesBuffer(-neighborOffset); + if (synapseIndexOfNN < 0) + { + continue; + } + + // The synapse array need to be shifted regard to the remove neuron + // Also neuron need to have 2M neighbors, the addtional synapse will be set as zero + // weight Case1 [S0 S1 S2 - SR S5 S6]. SR is removed, [S0 S1 S2 S5 S6 0] Case2 [S0 S1 SR + // - S3 S4 S5]. SR is removed, [0 S0 S1 S3 S4 S5] + constexpr unsigned long long halfMax = maxNumberOfNeighbors / 2; + if (synapseIndexOfNN >= (long long)halfMax) + { + for (long long k = synapseIndexOfNN; k < (long long)endSynapseBufferIdx - 1; ++k) + { + pNNSynapses[k] = pNNSynapses[k + 1]; + } + pNNSynapses[endSynapseBufferIdx - 1] = 0; + } + else + { + for (long long k = synapseIndexOfNN; k > (long long)startSynapseBufferIdx; --k) + { + pNNSynapses[k] = pNNSynapses[k - 1]; + } + pNNSynapses[startSynapseBufferIdx] = 0; + } + } + + // Shift the synapse array and the neuron array + for (unsigned long long shiftIdx = neuronIdx; shiftIdx < currentANN.population - 1; shiftIdx++) + { + currentANN.neuronTypes[shiftIdx] = currentANN.neuronTypes[shiftIdx + 1]; + + // Also shift the synapses + copyMem( + getSynapses(shiftIdx), + getSynapses(shiftIdx + 1), + maxNumberOfNeighbors * sizeof(Synapse)); + } + currentANN.population--; + } + + unsigned long long + getNeighborNeuronIndex(unsigned long long neuronIndex, unsigned long long neighborOffset) + { + const unsigned long long leftNeighbors = getLeftNeighborCount(); + unsigned long long nnIndex = 0; + if (neighborOffset < leftNeighbors) + { + nnIndex = clampNeuronIndex( + neuronIndex + neighborOffset, -(long long)leftNeighbors); + } + else + { + nnIndex = clampNeuronIndex( + neuronIndex + neighborOffset + 1, -(long long)leftNeighbors); + } + return nnIndex; + } + + void insertNeuron(unsigned long long neuronIndex, unsigned long long synapseIndex) + { + unsigned long long synapseFullBufferIdx = neuronIndex * maxNumberOfNeighbors + synapseIndex; + // Old value before insert neuron + unsigned long long oldStartSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long oldEndSynapseBufferIdx = getSynapseEndIndex(); + unsigned long long oldActualNeighbors = getActualNeighborCount(); + long long oldLeftCount = (long long)getLeftNeighborCount(); + long long oldRightCount = (long long)getRightNeighborCount(); + + constexpr unsigned long long halfMax = maxNumberOfNeighbors / 2; + + // Validate synapse index is within valid range + ASSERT(synapseIndex >= oldStartSynapseBufferIdx && synapseIndex < oldEndSynapseBufferIdx); + + Synapse* synapses = currentANN.synapses; + unsigned char* neuronTypes = currentANN.neuronTypes; + unsigned long long& population = currentANN.population; + + // Copy original neuron to the inserted one and set it as Neuron::kEvolution type + unsigned long long insertedNeuronIdx = neuronIndex + 1; + + char originalWeight = synapses[synapseFullBufferIdx]; + + // Insert the neuron into array, population increased one, all neurons next to original one + // need to shift right + for (unsigned long long i = population; i > neuronIndex; --i) + { + neuronTypes[i] = neuronTypes[i - 1]; + + // Also shift the synapses to the right + copyMem(getSynapses(i), getSynapses(i - 1), maxNumberOfNeighbors * sizeof(Synapse)); + } + neuronTypes[insertedNeuronIdx] = EVOLUTION_NEURON_TYPE; + population++; + + // Recalculate after population change + unsigned long long newActualNeighbors = getActualNeighborCount(); + unsigned long long newStartSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long newEndSynapseBufferIdx = getSynapseEndIndex(); + + // Try to update the synapse of inserted neuron. All outgoing synapse is init as zero weight + Synapse* pInsertNeuronSynapse = getSynapses(insertedNeuronIdx); + for (unsigned long long synIdx = 0; synIdx < maxNumberOfNeighbors; ++synIdx) + { + pInsertNeuronSynapse[synIdx] = 0; + } + + // Copy the outgoing synapse of original neuron + if (synapseIndex < halfMax) + { + // The synapse is going to a neuron to the left of the original neuron. + // Check if the incoming neuron is still contained in the neighbors of the inserted + // neuron. This is the case if the original `synapseIndex` is > 0, i.e. + // the original synapse if not going to the leftmost neighbor of the original neuron. + if (synapseIndex > newStartSynapseBufferIdx) + { + // Decrease idx by one because the new neuron is inserted directly to the right of + // the original one. + pInsertNeuronSynapse[synapseIndex - 1] = originalWeight; + } + // If the incoming neuron of the original synapse if not contained in the neighbors of + // the inserted neuron, don't add the synapse. + } + else + { + // The synapse is going to a neuron to the right of the original neuron. + // In this case, the incoming neuron of the synapse is for sure contained in the + // neighbors of the inserted neuron and has the same idx (right side neighbors of + // inserted neuron = right side neighbors of original neuron before insertion). + pInsertNeuronSynapse[synapseIndex] = originalWeight; + } + + // The change of synapse only impact neuron in [originalNeuronIdx - actualNeighbors / 2 + // + 1, originalNeuronIdx + actualNeighbors / 2] In the new index, it will be + // [originalNeuronIdx + 1 - actualNeighbors / 2, originalNeuronIdx + 1 + + // actualNeighbors / 2] [N0 N1 N2 original inserted N4 N5 N6], M = 2. + for (long long delta = -oldLeftCount; delta <= oldRightCount; ++delta) + { + // Only process the neighbors + if (delta == 0) + { + continue; + } + unsigned long long updatedNeuronIdx = clampNeuronIndex(insertedNeuronIdx, delta); + + // Generate a list of neighbor index of current updated neuron NN + // Find the location of the inserted neuron in the list of neighbors + long long insertedNeuronIdxInNeigborList = -1; + for (unsigned long long k = 0; k < newActualNeighbors; k++) + { + unsigned long long nnIndex = getNeighborNeuronIndex(updatedNeuronIdx, k); + if (nnIndex == insertedNeuronIdx) + { + insertedNeuronIdxInNeigborList = (long long)(newStartSynapseBufferIdx + k); + } + } + + ASSERT(insertedNeuronIdxInNeigborList >= 0); + + Synapse* pUpdatedSynapses = getSynapses(updatedNeuronIdx); + // [N0 N1 N2 original inserted N4 N5 N6], M = 2. + // Case: neurons in range [N0 N1 N2 original], right synapses will be affected + if (delta < 0) + { + // Left side is kept as it is, only need to shift to the right side + for (long long k = (long long)newEndSynapseBufferIdx - 1; k >= insertedNeuronIdxInNeigborList; --k) + { + // Updated synapse + pUpdatedSynapses[k] = pUpdatedSynapses[k - 1]; + } + + // Incomming synapse from original neuron -> inserted neuron must be zero + if (delta == -1) + { + pUpdatedSynapses[insertedNeuronIdxInNeigborList] = 0; + } + } + else // Case: neurons in range [inserted N4 N5 N6], left synapses will be affected + { + // Right side is kept as it is, only need to shift to the left side + for (long long k = (long long)newStartSynapseBufferIdx; k < insertedNeuronIdxInNeigborList; ++k) + { + // Updated synapse + pUpdatedSynapses[k] = pUpdatedSynapses[k + 1]; + } + } + } + } + + + // Check which neurons/synapse need to be removed after mutation + unsigned long long scanRedundantNeurons() + { + unsigned long long population = currentANN.population; + Synapse* synapses = currentANN.synapses; + unsigned char* neuronTypes = currentANN.neuronTypes; + + unsigned long long startSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long endSynapseBufferIdx = getSynapseEndIndex(); + long long leftCount = (long long)getLeftNeighborCount(); + long long rightCount = (long long)getRightNeighborCount(); + + numberOfRedundantNeurons = 0; + // After each mutation, we must verify if there are neurons that do not affect the ANN + // output. These are neurons that either have all incoming synapse weights as 0, or all + // outgoing synapse weights as 0. Such neurons must be removed. + for (unsigned long long i = 0; i < population; i++) + { + if (neuronTypes[i] == EVOLUTION_NEURON_TYPE) + { + bool allOutGoingZeros = true; + bool allIncommingZeros = true; + + // Loop though its synapses for checkout outgoing synapses + for (unsigned long long m = startSynapseBufferIdx; m < endSynapseBufferIdx; m++) + { + char synapseW = synapses[i * maxNumberOfNeighbors + m]; + if (synapseW != 0) + { + allOutGoingZeros = false; + break; + } + } + + // Loop through the neighbor neurons to check all incoming synapses + for (long long offset = -leftCount; offset <= rightCount; offset++) + { + if (offset == 0) continue; + + unsigned long long nnIdx = clampNeuronIndex(i, offset); + long long synapseIdx = getIndexInSynapsesBuffer(-offset); + if (synapseIdx < 0) + { + continue; + } + char synapseW = getSynapses(nnIdx)[synapseIdx]; + + if (synapseW != 0) + { + allIncommingZeros = false; + break; + } + } + if (allOutGoingZeros || allIncommingZeros) + { + removalNeurons[numberOfRedundantNeurons] = i; + numberOfRedundantNeurons++; + } + } + } + return numberOfRedundantNeurons; + } + + // Remove neurons and synapses that do not affect the ANN + void cleanANN() + { + unsigned long long& population = currentANN.population; + + // Scan and remove neurons/synapses + for (unsigned long long i = 0; i < numberOfRedundantNeurons; i++) + { + // Remove it from the neuron list. Overwrite data + // Remove its synapses in the synapses array + removeNeuron(removalNeurons[i]); + // The index is sorted, just reduce the index + for (unsigned long long j = i + 1; j < numberOfRedundantNeurons; j++) + { + removalNeurons[j]--; + } + } + } + + void processTick() + { + + unsigned long long population = currentANN.population; + unsigned char* neuronTypes = currentANN.neuronTypes; + + unsigned long long activeSamplePad = ((activeCount + BATCH_SIZE - 1) / BATCH_SIZE) * BATCH_SIZE; + +#if defined(__AVX512F__) + + const __m512i one16 = _mm512_set1_epi16(1); + const __m512i negOne16 = _mm512_set1_epi16(-1); + const __m512i packLoc = _mm512_setr_epi64(0, 2, 4, 6, 1, 3, 5, 7); + + for (unsigned long long targetNeuron = 0; targetNeuron < population; targetNeuron++) + { + if (neuronTypes[targetNeuron] == INPUT_NEURON_TYPE) + continue; + + unsigned int numIncoming = incomingCount[targetNeuron]; + const unsigned long long targetNeuronOffset = targetNeuron * maxNumberOfNeighbors; + for (unsigned long long s = 0; s < activeSamplePad; s += BATCH_SIZE) + { + __m512i acc0 = _mm512_setzero_si512(); // samples 0-31 (int16) + __m512i acc1 = _mm512_setzero_si512(); // samples 32-63 (int16) + for (unsigned int i = 0; i < numIncoming; i++) + { + unsigned int srcNeuron = incomingSource[targetNeuronOffset + i]; + __m512i weight512 = _mm512_set1_epi8(incomingSynapses[targetNeuronOffset + i]); + + // Load 64 source neuron values + //__m512i srcValues = _mm512_load_si512((__m512i*)&neuronValues[srcNeuron][s]); + __m512i srcValues = _mm512_loadu_si512((__m512i*)&prevNeuronValues[srcNeuron * PADDED_SAMPLES + s]); + + __mmask64 negMask = _mm512_movepi8_mask(weight512); + __m512i negated = _mm512_sub_epi8(_mm512_setzero_si512(), srcValues); + __m512i product = _mm512_mask_blend_epi8(negMask, srcValues, negated); + + // Sign-extend char to int16 and accumulate + __m256i lo = _mm512_extracti64x4_epi64(product, 0); + __m256i hi = _mm512_extracti64x4_epi64(product, 1); + + acc0 = _mm512_add_epi16(acc0, _mm512_cvtepi8_epi16(lo)); + acc1 = _mm512_add_epi16(acc1, _mm512_cvtepi8_epi16(hi)); + } + + // Clamp to [-1, 1] + acc0 = _mm512_max_epi16(acc0, negOne16); + acc0 = _mm512_min_epi16(acc0, one16); + acc1 = _mm512_max_epi16(acc1, negOne16); + acc1 = _mm512_min_epi16(acc1, one16); + + // Pack int16 back to int8 + __m512i packed = _mm512_packs_epi16(acc0, acc1); + + // Fix lane ordering after packs + packed = _mm512_permutexvar_epi64(packLoc, packed); + + _mm512_storeu_si512((__m512i*)&neuronValues[targetNeuron * PADDED_SAMPLES + s], packed); + } + } + +#else + + const __m256i one16 = _mm256_set1_epi16(1); + const __m256i negOne16 = _mm256_set1_epi16(-1); + + for (unsigned long long targetNeuron = 0; targetNeuron < population; targetNeuron++) + { + if (neuronTypes[targetNeuron] == INPUT_NEURON_TYPE) + continue; + + unsigned int numIncoming = incomingCount[targetNeuron]; + const unsigned long long targetNeuronOffset = targetNeuron * maxNumberOfNeighbors; + for (unsigned long long s = 0; s < activeSamplePad; s += BATCH_SIZE) + { + __m256i acc0 = _mm256_setzero_si256(); // samples 0-15 (int16) + __m256i acc1 = _mm256_setzero_si256(); // samples 16-31 (int16) + for (unsigned int i = 0; i < numIncoming; i++) + { + unsigned int srcNeuron = incomingSource[targetNeuronOffset + i]; + __m256i weight256 = _mm256_set1_epi8(incomingSynapses[targetNeuronOffset + i]); + + // Load 32 source neuron values + __m256i srcValues = _mm256_loadu_si256((__m256i*)&prevNeuronValues[srcNeuron * PADDED_SAMPLES + s]); + + __m256i negated = _mm256_sub_epi8(_mm256_setzero_si256(), srcValues); + __m256i product = _mm256_blendv_epi8(srcValues, negated, weight256); + + // Sign-extend char to int16 and accumulate + __m128i lo = _mm256_castsi256_si128(product); + __m128i hi = _mm256_extracti128_si256(product, 1); + + acc0 = _mm256_add_epi16(acc0, _mm256_cvtepi8_epi16(lo)); + acc1 = _mm256_add_epi16(acc1, _mm256_cvtepi8_epi16(hi)); + } + + // Clamp to [-1, 1] + acc0 = _mm256_max_epi16(acc0, negOne16); + acc0 = _mm256_min_epi16(acc0, one16); + acc1 = _mm256_max_epi16(acc1, negOne16); + acc1 = _mm256_min_epi16(acc1, one16); + + // Pack int16 back to int8 + __m256i packed = _mm256_packs_epi16(acc0, acc1); + + // Fix lane ordering after packs. (3, 1, 2, 0) + packed = _mm256_permute4x64_epi64(packed, 0xD8); // 0xD8: (3 << 6) | (1 << 4) | (2 << 2) | 0 + + _mm256_storeu_si256((__m256i*)&neuronValues[targetNeuron * PADDED_SAMPLES + s], packed); + } + } +#endif + + } + void loadTrainingData() + { + unsigned long long population = currentANN.population; + + // Load input neuron values from training data + unsigned long long inputIdx = 0; + for (unsigned long long n = 0; n < population; n++) + { + if (currentANN.neuronTypes[n] == INPUT_NEURON_TYPE) + { + copyMem(&neuronValues[n * PADDED_SAMPLES], &trainingInputs[inputIdx * PADDED_SAMPLES], PADDED_SAMPLES); + inputIdx++; + } + else + { + // Non-input neurons: clear to zero + setMem(&neuronValues[n * PADDED_SAMPLES], PADDED_SAMPLES, 0); + } + } + + } + + // Convert the outgoing synapse to incomming style + void convertToIncomingSynapses() + { + unsigned long long population = currentANN.population; + Synapse* synapses = currentANN.synapses; + + unsigned long long startIdx = getSynapseStartIndex(); + unsigned long long endIdx = getSynapseEndIndex(); + + // Clear counts + setMem(incomingCount, sizeof(incomingCount), 0); + + // Convert outgoing synapses to incoming ones + for (unsigned long long n = 0; n < population; n++) + { + const Synapse* kSynapses = getSynapses(n); + for (unsigned long long synIdx = startIdx; synIdx < endIdx; synIdx++) + { + char weight = kSynapses[synIdx]; + if (weight == 0) continue; + + long long offset = bufferIndexToOffset(synIdx); + unsigned long long nnIndex = clampNeuronIndex((long long)n, offset); + + unsigned int idx = incomingCount[nnIndex]++; + incomingSynapses[nnIndex * maxNumberOfNeighbors + idx] = weight; + + // Cache the incomming neuron + incomingSource[nnIndex * maxNumberOfNeighbors + idx] = (unsigned int)n; + } + } + } + + // Compute the score of a sample + void computeSampleScore(unsigned long long sampleLocation) + { + unsigned int origSample = sampleMapping[sampleLocation]; + unsigned int score = 0; + + for (unsigned long long i = 0; i < numCachedOutputs; i++) + { + unsigned long long neuronIdx = outputNeuronIdxCache[i]; + char actual = neuronValues[neuronIdx * PADDED_SAMPLES + sampleLocation]; + char expected = trainingOutputs[i * PADDED_SAMPLES + origSample]; + + if (actual == expected) + { + score++; + } + } + + sampleScores[origSample] = score; + } + + void compactActiveSamplesWithScoring(unsigned long long& s, unsigned long long& writePos, unsigned long long population) + { + while (s < activeCount) + { + bool allUnchanged = true; + for (unsigned long long n = 0; n < population && allUnchanged; n++) + { + if (neuronValues[n * PADDED_SAMPLES + s] != prevNeuronValues[n * PADDED_SAMPLES + s]) + { + allUnchanged = false; + } + } + + bool allOutputsNonZero = true; + for (unsigned long long i = 0; i < numCachedOutputs && allOutputsNonZero; i++) + { + unsigned long long n = outputNeuronIdxCache[i]; + if (neuronValues[n * PADDED_SAMPLES + s] == 0) + { + allOutputsNonZero = false; + } + } + + bool isDone = allUnchanged || allOutputsNonZero; + + if (isDone) + { + // Score this sample + unsigned int origSample = sampleMapping[s]; + for (unsigned long long i = 0; i < numCachedOutputs; i++) + { + unsigned long long n = outputNeuronIdxCache[i]; + if (neuronValues[n * PADDED_SAMPLES + s] == trainingOutputs[i * PADDED_SAMPLES + origSample]) + { + sampleScores[origSample]++; + } + } + } + else + { + // Compact + if (writePos != s) + { + sampleMapping[writePos] = sampleMapping[s]; + for (unsigned long long n = 0; n < population; n++) + { + neuronValues[n * PADDED_SAMPLES + writePos] = neuronValues[n * PADDED_SAMPLES + s]; + prevNeuronValues[n * PADDED_SAMPLES + writePos] = prevNeuronValues[n * PADDED_SAMPLES + s]; + } + } + writePos++; + } + s++; + } + } + + bool compactActiveSamplesWithScoringSIMD() + { + unsigned long long population = currentANN.population; + unsigned long long writePos = 0; + unsigned long long s = 0; +#if defined(__AVX512F__) + const __m512i allOnes = _mm512_set1_epi8(-1); + while (s + BATCH_SIZE <= activeCount) + { + // Check 1: All neurons unchanged (curr == prev) + // unchangedMask = 1 if unchanged + __m512i unchangedVec = allOnes; + for (unsigned long long n = 0; n < population; n++) + { + __m512i curr = _mm512_loadu_si512((__m512i*)&neuronValues[n * PADDED_SAMPLES + s]); + __m512i prev = _mm512_loadu_si512((__m512i*)&prevNeuronValues[n * PADDED_SAMPLES + s]); + __m512i diff = _mm512_xor_si512(curr, prev); + unchangedVec = _mm512_andnot_si512(diff, unchangedVec); + } + unsigned long long unchangedMask = (unsigned long long)_mm512_cmpeq_epi8_mask(unchangedVec, allOnes); + + // Check 2: All output neurons non-zero + unsigned long long allOutputsNonZeroMask = ~0ULL; + for (unsigned long long i = 0; i < numCachedOutputs; i++) + { + unsigned long long n = outputNeuronIdxCache[i]; + __m512i val = _mm512_loadu_si512((__m512i*)&neuronValues[n * PADDED_SAMPLES + s]); + __mmask64 nonZeroMask = _mm512_test_epi8_mask(val, val); + allOutputsNonZeroMask &= (unsigned long long)nonZeroMask; + } + + // exitMask: 1 = sample is done, 0 = sample still need to continue ticking + unsigned long long exitMask = unchangedMask | allOutputsNonZeroMask; + // activeMask: 1 = sample still active + unsigned long long activeMask = ~exitMask; + + // There is sample should be stopped and score + if (exitMask != 0) + { + for (unsigned long long i = 0; i < numCachedOutputs; i++) + { + unsigned long long n = outputNeuronIdxCache[i]; + // Score each exiting sample + unsigned long long tempExitMask = exitMask; + while (tempExitMask != 0) + { + unsigned long long bitPos = countTrailingZerosAssumeNonZero64(tempExitMask); + unsigned int origSample = sampleMapping[s + bitPos]; + char actualVal = neuronValues[n * PADDED_SAMPLES + s + bitPos]; + char expectedVal = trainingOutputs[i * PADDED_SAMPLES + origSample]; + if (actualVal == expectedVal) + { + sampleScores[origSample]++; + } + tempExitMask = _blsr_u64(tempExitMask); // Clear processed bit + } + } + } + + // Compact samples, move the unscore/active to the front + unsigned int activeInBatch = popcnt64(activeMask); + // All samples are active, just copy if needed + if (activeInBatch == BATCH_SIZE) + { + // Positions are adjusted + if (writePos != s) + { + // Each copy 16 samples, need 4 times to copy full 64 samples + static_assert(BATCH_SIZE / 16 == 4, "Compress loop assumes BATCH_SIZE / 16 == 4"); + __m512i map0 = _mm512_loadu_si512((__m512i*)&sampleMapping[s + 0]); + __m512i map1 = _mm512_loadu_si512((__m512i*)&sampleMapping[s + 16]); + __m512i map2 = _mm512_loadu_si512((__m512i*)&sampleMapping[s + 32]); + __m512i map3 = _mm512_loadu_si512((__m512i*)&sampleMapping[s + 48]); + _mm512_storeu_si512((__m512i*)&sampleMapping[writePos + 0], map0); + _mm512_storeu_si512((__m512i*)&sampleMapping[writePos + 16], map1); + _mm512_storeu_si512((__m512i*)&sampleMapping[writePos + 32], map2); + _mm512_storeu_si512((__m512i*)&sampleMapping[writePos + 48], map3); + + // Neuron is char, 1 copy is enough + for (unsigned long long n = 0; n < population; n++) + { + unsigned long long baseIdx = n * PADDED_SAMPLES; + + __m512i curr = _mm512_loadu_si512((__m512i*)&neuronValues[baseIdx + s]); + __m512i prev = _mm512_loadu_si512((__m512i*)&prevNeuronValues[baseIdx + s]); + + _mm512_storeu_si512((__m512i*)&neuronValues[baseIdx + writePos], curr); + _mm512_storeu_si512((__m512i*)&prevNeuronValues[baseIdx + writePos], prev); + } + } + writePos += BATCH_SIZE; + } + else if (activeInBatch > 0) // mixing inactive an avtive + { + __mmask64 kActive = (__mmask64)activeMask; + + // Load 4 blocks of 16 samples to adjust the sample index + static_assert(BATCH_SIZE / 16 == 4, "Compress loop assumes BATCH_SIZE / 16 == 4"); + int offset = 0; + for (unsigned long long blockId = 0; blockId < BATCH_SIZE; blockId +=16) + { + __mmask16 k16 = (__mmask16)((activeMask >> blockId) & 0xFFFF); + __m512i map = _mm512_loadu_si512((__m512i*)&sampleMapping[s + blockId]); + __m512i compressed = _mm512_maskz_compress_epi32(k16, map); + _mm512_storeu_si512((__m512i*)&sampleMapping[writePos + offset], compressed); + offset += popcnt32(k16); + } + + + // Adjust the neurons and previous neuron values + unsigned long long baseIdx = 0; + for (unsigned long long n = 0; n < population; n++, baseIdx += PADDED_SAMPLES) + { + __m512i curr = _mm512_loadu_si512((__m512i*)&neuronValues[baseIdx + s]); + __m512i prev = _mm512_loadu_si512((__m512i*)&prevNeuronValues[baseIdx + s]); + + __m512i compCurr = _mm512_maskz_compress_epi8(kActive, curr); + __m512i compPrev = _mm512_maskz_compress_epi8(kActive, prev); + + _mm512_storeu_si512((__m512i*)&neuronValues[baseIdx + writePos], compCurr); + _mm512_storeu_si512((__m512i*)&prevNeuronValues[baseIdx + writePos], compPrev); + } + + writePos += activeInBatch; + } + // activeInBatch == 0, all samples done, no compaction needed + s += BATCH_SIZE; + } +#else + const __m256i allOnes = _mm256_set1_epi8(-1); + const __m256i allZeros = _mm256_setzero_si256(); + while (s + BATCH_SIZE <= activeCount) + { + // Check 1: All neurons unchanged (curr == prev) + // unchangedMask = 1 if unchanged + __m256i unchangedVec = allOnes; + for (unsigned long long n = 0; n < population; n++) + { + __m256i curr = _mm256_loadu_si256((__m256i*)&neuronValues[n * PADDED_SAMPLES + s]); + __m256i prev = _mm256_loadu_si256((__m256i*)&prevNeuronValues[n * PADDED_SAMPLES + s]); + __m256i diff = _mm256_xor_si256(curr, prev); + unchangedVec = _mm256_andnot_si256(diff, unchangedVec); + } + unsigned int unchangedMask = (unsigned int)_mm256_movemask_epi8(_mm256_cmpeq_epi8(unchangedVec, allOnes)); + + // Check 2: All output neurons non-zero + unsigned int allOutputsNonZeroMask = ~0U; + for (unsigned long long i = 0; i < numCachedOutputs; i++) + { + unsigned long long n = outputNeuronIdxCache[i]; + __m256i val = _mm256_loadu_si256((__m256i*) & neuronValues[n * PADDED_SAMPLES + s]); + __m256i isZero = _mm256_cmpeq_epi8(val, allZeros); + unsigned int zeroMask = (unsigned int)_mm256_movemask_epi8(isZero); + allOutputsNonZeroMask &= ~zeroMask; + } + + // exitMask: 1 = sample is done, 0 = sample still need to continue ticking + unsigned int exitMask = unchangedMask | allOutputsNonZeroMask; + // activeMask: 1 = sample still active + unsigned int activeMask = ~exitMask; + + // There is sample should be stopped and score + if (exitMask != 0) + { + for (unsigned long long i = 0; i < numCachedOutputs; i++) + { + unsigned long long n = outputNeuronIdxCache[i]; + // Score each exiting sample + unsigned int tempExitMask = exitMask; + while (tempExitMask != 0) + { + unsigned int bitPos = countTrailingZerosAssumeNonZero32(tempExitMask); + unsigned int origSample = sampleMapping[s + bitPos]; + char actualVal = neuronValues[n * PADDED_SAMPLES + s + bitPos]; + char expectedVal = trainingOutputs[i * PADDED_SAMPLES + origSample]; + if (actualVal == expectedVal) + { + sampleScores[origSample]++; + } + tempExitMask = _blsr_u32(tempExitMask); // Clear processed bit + } + } + } + + // Compact samples, move the unscore/active to the front + int activeInBatch = popcnt32(activeMask); + // All samples are active, just copy if needed + if (activeInBatch == BATCH_SIZE) + { + // Positions are adjusted + if (writePos != s) + { + // Each copy 8 samples, need 4 times to copy full 32 samples + static_assert(BATCH_SIZE / 8 == 4, "Compress loop assumes BATCH_SIZE / 8 == 4"); + __m256i map0 = _mm256_loadu_si256((__m256i*) & sampleMapping[s + 0]); + __m256i map1 = _mm256_loadu_si256((__m256i*) & sampleMapping[s + 8]); + __m256i map2 = _mm256_loadu_si256((__m256i*) & sampleMapping[s + 16]); + __m256i map3 = _mm256_loadu_si256((__m256i*) & sampleMapping[s + 24]); + _mm256_storeu_si256((__m256i*)& sampleMapping[writePos + 0], map0); + _mm256_storeu_si256((__m256i*)& sampleMapping[writePos + 8], map1); + _mm256_storeu_si256((__m256i*)& sampleMapping[writePos + 16], map2); + _mm256_storeu_si256((__m256i*)& sampleMapping[writePos + 24], map3); + + // Neuron is char, 1 copy is enough + for (unsigned long long n = 0; n < population; n++) + { + unsigned long long baseIdx = n * PADDED_SAMPLES; + + __m256i curr = _mm256_loadu_si256((__m256i*) & neuronValues[baseIdx + s]); + __m256i prev = _mm256_loadu_si256((__m256i*) & prevNeuronValues[baseIdx + s]); + + _mm256_storeu_si256((__m256i*) & neuronValues[baseIdx + writePos], curr); + _mm256_storeu_si256((__m256i*) & prevNeuronValues[baseIdx + writePos], prev); + } + + } + writePos += BATCH_SIZE; + } + else if (activeInBatch > 0) // mixing inactive an avtive + { + unsigned int mask = (unsigned int)activeMask; + unsigned int writeOffset = 0; + while (mask != 0) + { + uint32_t i = countTrailingZerosAssumeNonZero32(mask); + sampleMapping[writePos + writeOffset] = sampleMapping[s + i]; + writeOffset++; + mask = _blsr_u32(mask); // clear processed bit + } + + // Separate update for better caching + for (unsigned long long n = 0; n < population; n++) + { + unsigned long long baseIdx = n * PADDED_SAMPLES; + mask = (unsigned int)activeMask; + writeOffset = 0; + while (mask != 0) + { + unsigned int i = countTrailingZerosAssumeNonZero32(mask); + neuronValues[baseIdx + writePos + writeOffset] = neuronValues[baseIdx + s + i]; + prevNeuronValues[baseIdx + writePos + writeOffset] = prevNeuronValues[baseIdx + s + i]; + writeOffset++; + mask = _blsr_u32(mask); + } + } + + writePos += activeInBatch; + } + // activeInBatch == 0, all samples done, no compaction needed + s += BATCH_SIZE; + } +#endif + + // Process remained samples with scalar version + compactActiveSamplesWithScoring(s, writePos, population); + + activeCount = writePos; + return (activeCount == 0); + } + + // Tick simulation only runs on one ANN + void runTickSimulation() + { + // PROFILE_NAMED_SCOPE("runTickSimulation"); + + unsigned long long population = currentANN.population; + //Neuron* neurons = currentANN.neurons; + unsigned char* neuronTypes = currentANN.neuronTypes; + + { + // PROFILE_NAMED_SCOPE("runTickSimulation:PrepareData"); + for (unsigned long long i = 0; i < trainingSetSize; i++) + { + sampleMapping[i] = (unsigned int)i; + sampleScores[i] = 0; + } + activeCount = trainingSetSize; + + // Load the training set and fill ANN value + loadTrainingData(); + copyMem(prevNeuronValues, neuronValues, population * PADDED_SAMPLES); + + // Cache the location of ouput neurons + cacheOutputNeuronIndices(); + + // Transpose the synpase + convertToIncomingSynapses(); + } + + { + // PROFILE_NAMED_SCOPE("runTickSimulation:Ticking"); + for (unsigned long long tick = 0; tick < numberOfTicks; ++tick) + { + // No more ANN to infer aka stop condition of all samples are hit + if (activeCount == 0) + { + break; + } + + swapCurrentPreviousNeuronPointers(); + + { + // PROFILE_NAMED_SCOPE("Ticking:processTick"); + processTick(); + } + + // Move samples that not exit to suitable consition + { + // PROFILE_NAMED_SCOPE("Ticking:compactActiveSamplesWithScoring"); + if (compactActiveSamplesWithScoringSIMD()) + { + break; + } + } + } + } + + // Full tick simulation finish, compute score of each remained samples + for (unsigned long long i = 0; i < activeCount; i++) + { + computeSampleScore(i); + } + } + + // Generate all 2^K possible (A, B, C) pairs + void generateTrainingSet() + { + static constexpr long long boundValue = (1LL << (numberOfInputNeurons / 2)) / 2; + setMem(trainingInputs, sizeof(trainingInputs), 0); + setMem(trainingOutputs, sizeof(trainingOutputs), 0); + unsigned long long sampleIdx = 0; + for (long long A = -boundValue; A < boundValue; A++) + { + for (long long B = -boundValue; B < boundValue; B++) + { + long long C = A + B; + + toTenaryBits(A, inputBits); + toTenaryBits(B, inputBits + numberOfInputNeurons / 2); + toTenaryBits(C, outputBits); + + for (unsigned long long n = 0; n < numberOfInputNeurons; n++) + { + trainingInputs[n * PADDED_SAMPLES + sampleIdx] = inputBits[n]; + } + + for (unsigned long long n = 0; n < numberOfOutputNeurons; n++) + { + trainingOutputs[n * PADDED_SAMPLES + sampleIdx] = outputBits[n]; + } + sampleIdx++; + } + } + + } + + unsigned int getTotalSamplesScore() + { + unsigned int total = 0; + for (unsigned long long i = 0; i < trainingSetSize; i++) + { + total += sampleScores[i]; + } + return total; + } + + unsigned int inferANN() + { + unsigned int score = 0; + runTickSimulation(); + score = getTotalSamplesScore(); + + return score; + } + + void swapCurrentPreviousNeuronPointers() + { + char* tmp = neuronValues; + neuronValues = prevNeuronValues; + prevNeuronValues = tmp; + } + + unsigned int initializeANN(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool) + { + unsigned char hash[32]; + unsigned char combined[64]; + copyMem(combined, publicKey, 32); + copyMem(combined + 32, nonce, 32); + KangarooTwelve(combined, 64, hash, 32); + + unsigned long long& population = currentANN.population; + Synapse* synapses = currentANN.synapses; + unsigned char* neuronTypes = currentANN.neuronTypes; + + // Initialization + population = numberOfNeurons; + neuronValues = neuronValuesBuffer0; + prevNeuronValues = neuronValuesBuffer1; + + // Generate all 2^K possible (A, B, C) pairs + //generateTrainingSet(); + + // Initalize with nonce and public key + random2(hash, randomPool, (unsigned char*)&paddingInitValue, sizeof(paddingInitValue)); + + // Randomly choose the positions of neurons types + for (unsigned long long i = 0; i < population; ++i) + { + neuronIndices[i] = i; + neuronTypes[i] = INPUT_NEURON_TYPE; + } + + InitValue* initValue = (InitValue*)paddingInitValue; + unsigned long long neuronCount = population; + for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) + { + unsigned long long outputNeuronIdx = initValue->outputNeuronPositions[i] % neuronCount; + + // Fill the neuron type + neuronTypes[neuronIndices[outputNeuronIdx]] = OUTPUT_NEURON_TYPE; + outputNeuronIndices[i] = neuronIndices[outputNeuronIdx]; + + // This index is used, copy the end of indices array to current position and decrease + // the number of picking neurons + neuronCount = neuronCount - 1; + neuronIndices[outputNeuronIdx] = neuronIndices[neuronCount]; + } + + // Synapse weight initialization + auto extractWeight = [](unsigned long long packedValue, unsigned long long position) -> char + { + unsigned char extractValue = static_cast((packedValue >> (position * 2)) & 0b11); + switch (extractValue) + { + case 2: + return -1; + case 3: + return 1; + default: + return 0; + } + }; + for (unsigned long long i = 0; i < (maxNumberOfSynapses / 32); ++i) + { + for (unsigned long long j = 0; j < 32; ++j) + { + synapses[32 * i + j] = extractWeight(initValue->synapseWeight[i], j); + } + } + + // Handle remaining synapses (if maxNumberOfSynapses not divisible by 32) + unsigned long long remainder = maxNumberOfSynapses % 32; + if (remainder > 0) + { + unsigned long long lastBlock = maxNumberOfSynapses / 32; + for (unsigned long long j = 0; j < remainder; ++j) + { + synapses[32 * lastBlock + j] = extractWeight(initValue->synapseWeight[lastBlock], j); + } + } + + // Run the first inference to get starting point before mutation + unsigned int score = inferANN(); + + return score; + } + + // Main function for mining + unsigned int computeScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool) + { + // Initialize + unsigned int bestR = initializeANN(publicKey, nonce, randomPool); + copyMem(&bestANN, ¤tANN, sizeof(bestANN)); + + for (unsigned long long s = 0; s < numberOfMutations; ++s) + { + // Do the mutation + mutate(s); + + // Exit if the number of population reaches the maximum allowed + if (currentANN.population >= populationThreshold) + { + break; + } + + // Ticks simulation + unsigned int R = inferANN(); + + // Roll back if neccessary + if (R >= bestR) + { + bestR = R; + // Better R. Save the state + copyMem(&bestANN, ¤tANN, sizeof(bestANN)); + } + else + { + // Roll back + copyMem(¤tANN, &bestANN, sizeof(bestANN)); + } + + ASSERT(bestANN.population <= populationThreshold); + } + return bestR; + } +}; + +} diff --git a/src/mining/score_common.h b/src/mining/score_common.h new file mode 100644 index 000000000..10f00d67f --- /dev/null +++ b/src/mining/score_common.h @@ -0,0 +1,542 @@ +#pragma once + +#include "platform/profiling.h" +#include "platform/memory_util.h" +#include "platform/m256.h" +#include "platform/concurrency.h" +#include "kangaroo_twelve.h" + +namespace score_engine +{ + +enum AlgoType +{ + HyperIdentity = 0, + Addition = 1, + MaxAlgoCount // for counting current supported ago +}; + +// ============================================================================= +// Algorithm 0: HyperIdentity Parameters +// ============================================================================= +template< + unsigned long long inputNeurons, // numberOfInputNeurons + unsigned long long outputNeurons, // numberOfOutputNeurons + unsigned long long ticks, // numberOfTicks + unsigned long long neighbor, // numberOfNeighbors + unsigned long long population, // populationThreshold + unsigned long long mutations, // numberOfMutations + unsigned int threshold // solutionThreshold +> +struct HyperIdentityParams +{ + static constexpr unsigned long long numberOfInputNeurons = inputNeurons; + static constexpr unsigned long long numberOfOutputNeurons = outputNeurons; + static constexpr unsigned long long numberOfTicks = ticks; + static constexpr unsigned long long numberOfNeighbors = neighbor; + static constexpr unsigned long long populationThreshold = population; + static constexpr unsigned long long numberOfMutations = mutations; + static constexpr unsigned int solutionThreshold = threshold; + + static constexpr AlgoType algoType = AlgoType::HyperIdentity; + static constexpr unsigned int paramsCount = 7; +}; + +// ============================================================================= +// Algorithm 1: Addition Parameters +// ============================================================================= +template< + unsigned long long inputNeurons, // numberOfInputNeurons + unsigned long long outputNeurons, // numberOfOutputNeurons + unsigned long long ticks, // numberOfTicks + unsigned long long neighbor, // maxNumberOfNeigbor + unsigned long long population, // populationThreshold + unsigned long long mutations, // numberOfMutations + unsigned int threshold // solutionThreshold +> +struct AdditionParams +{ + static constexpr unsigned long long numberOfInputNeurons = inputNeurons; + static constexpr unsigned long long numberOfOutputNeurons = outputNeurons; + static constexpr unsigned long long numberOfTicks = ticks; + static constexpr unsigned long long numberOfNeighbors = neighbor; + static constexpr unsigned long long populationThreshold = population; + static constexpr unsigned long long numberOfMutations = mutations; + static constexpr unsigned int solutionThreshold = threshold; + + static constexpr AlgoType algoType = AlgoType::Addition; + static constexpr unsigned int paramsCount = 7; +}; + +//================================================================================================= +// Defines and constants +static constexpr unsigned int DEFAUL_SOLUTION_THRESHOLD[AlgoType::MaxAlgoCount] = { + HYPERIDENTITY_SOLUTION_THRESHOLD_DEFAULT, + ADDITION_SOLUTION_THRESHOLD_DEFAULT}; +static constexpr unsigned int INVALID_SCORE_VALUE = 0xFFFFFFFFU; +static constexpr long long NEURON_VALUE_LIMIT = 1LL; + +constexpr unsigned char INPUT_NEURON_TYPE = 0; +constexpr unsigned char OUTPUT_NEURON_TYPE = 1; +constexpr unsigned char EVOLUTION_NEURON_TYPE = 2; + +constexpr unsigned long long POOL_VEC_SIZE = (((1ULL << 32) + 64)) >> 3; // 2^32+64 bits ~ 512MB +constexpr unsigned long long POOL_VEC_PADDING_SIZE = (POOL_VEC_SIZE + 200 - 1) / 200 * 200; // padding for multiple of 200 +constexpr unsigned long long STATE_SIZE = 200; + +//================================================================================================= +// Helper functions + + + +#if defined(_MSC_VER) + +#define popcnt32(x) static_cast(__popcnt(static_cast(x))) +#define popcnt64(x) static_cast(__popcnt64(static_cast(x))) + +// Does not handle the x = 0. Expect check zeros before usage +static inline unsigned long long countTrailingZerosAssumeNonZero64(unsigned long long x) +{ + unsigned long index; + _BitScanForward64(&index, x); + return index; +} + +// Does not handle the x = 0. Expect check zeros before usage +static inline unsigned int countTrailingZerosAssumeNonZero32(unsigned int x) +{ + unsigned long index; + _BitScanForward(&index, x); + return index; +} + +#else + +#define popcnt32(x) __builtin_popcount (static_cast(x)) +#define popcnt64(x) __builtin_popcountll(static_cast(x)) + +// Does not handle the x = 0. Expect check zeros before usage +static inline unsigned long long countTrailingZerosAssumeNonZero64(unsigned long long x) +{ + return __builtin_ctzll(x); +} + +// Does not handle the x = 0. Expect check zeros before usage +static inline unsigned int countTrailingZerosAssumeNonZero32(unsigned int x) +{ + return __builtin_ctz(x); +} + +#endif + +#if !(defined (__AVX512F__) || defined(__AVX2__)) +static_assert(false, "Either AVX2 or AVX512 is required."); +#endif + +#if defined (__AVX512F__) +static constexpr int BATCH_SIZE = 64; +static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; +static inline int popcnt512(__m512i v) +{ + __m512i pc = _mm512_popcnt_epi64(v); + return (int)_mm512_reduce_add_epi64(pc); +} +#elif defined(__AVX2__) +static constexpr int BATCH_SIZE = 32; +static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; +static inline unsigned popcnt256(__m256i v) +{ + return popcnt64(_mm256_extract_epi64(v, 0)) + + popcnt64(_mm256_extract_epi64(v, 1)) + + popcnt64(_mm256_extract_epi64(v, 2)) + + popcnt64(_mm256_extract_epi64(v, 3)); +} + +#endif + +static void generateRandom2Pool(const unsigned char* miningSeed, unsigned char* state, unsigned char* pool) +{ + // same pool to be used by all computors/candidates and pool content changing each phase + copyMem(&state[0], miningSeed, 32); + setMem(&state[32], STATE_SIZE - 32, 0); + + for (unsigned int i = 0; i < POOL_VEC_PADDING_SIZE; i += STATE_SIZE) + { + KeccakP1600_Permute_12rounds(state); + copyMem(&pool[i], state, STATE_SIZE); + } +} + +static void random2( + unsigned char* seed, // 32 bytes + const unsigned char* pool, + unsigned char* output, + unsigned long long outputSizeInByte // must be divided by 64 +) +{ + ASSERT(outputSizeInByte % 64 == 0); + + unsigned long long segments = outputSizeInByte / 64; + unsigned int x[8] = { 0 }; + for (int i = 0; i < 8; i++) + { + x[i] = ((unsigned int*)seed)[i]; + } + + for (int j = 0; j < segments; j++) + { + // Each segment will have 8 elements. Each element have 8 bytes + for (int i = 0; i < 8; i++) + { + unsigned int base = (x[i] >> 3) >> 3; + unsigned int m = x[i] & 63; + + unsigned long long u64_0 = ((unsigned long long*)pool)[base]; + unsigned long long u64_1 = ((unsigned long long*)pool)[base + 1]; + + // Move 8 * 8 * j to the current segment. 8 * i to current 8 bytes element + if (m == 0) + { + // some compiler doesn't work with bit shift 64 + *((unsigned long long*) & output[j * 8 * 8 + i * 8]) = u64_0; + } + else + { + *((unsigned long long*) & output[j * 8 * 8 + i * 8]) = (u64_0 >> m) | (u64_1 << (64 - m)); + } + + // Increase the positions in the pool for each element. + x[i] = x[i] * 1664525 + 1013904223; // https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use + } + } + +} + + +// Clamp the neuron value +template +static char clampNeuron(T val) +{ + if (val > 1) + { + return 1; + } + else if (val < -1) + { + return -1; + } + return static_cast(val); +} + +static void extract64Bits(unsigned long long number, char* output) +{ + int count = 0; + for (int i = 0; i < 64; ++i) + { + output[i] = ((number >> i) & 1); + } +} + +static void setBitValue(unsigned char* data, unsigned long long bitIdx, unsigned char bitValue) +{ + // (data[bitIdx >> 3] & ~(1u << (bitIdx & 7u))). Set the bit at data[bitIdx >> 3] byte become zeros + // then set it with the bit value + data[bitIdx >> 3] = (data[bitIdx >> 3] & ~(1u << (bitIdx & 7u))) | + (bitValue << (bitIdx & 7u)); +} + +static unsigned char getBitValue(const unsigned char* data, unsigned long long bitIdx) +{ + // data[bitIdx >> 3]: get the byte idx + // (bitIdx & 7u) get the bit index in byte + // (data[bitIdx >> 3] >> (bitIdx & 7u)) move the required bits to the end + return ((data[bitIdx >> 3] >> (bitIdx & 7u)) & 1u); +} + +template +static void paddingDatabits( + unsigned char* data, + unsigned long long dataSizeInBits) +{ + const unsigned long long head = paddedSizeInBits; + const unsigned long long tail = dataSizeInBits - paddedSizeInBits; + + for (unsigned r = 0; r < paddedSizeInBits; ++r) + { + unsigned long long src1 = tail + r + paddedSizeInBits; + unsigned long long dst1 = (unsigned long long)r; + + unsigned char bit; + + // Copy the end to the head + bit = getBitValue(data, src1); + setBitValue(data, dst1, bit); + + // copy the head to the end + unsigned long long src2 = head + r; + unsigned long long dst2 = tail + 2 * paddedSizeInBits + r; + bit = getBitValue(data, src2); + setBitValue(data, dst2, bit); + } +} + +static void orShiftedMask64(unsigned long long* dst, unsigned int idx, unsigned int shift, unsigned long long mask) +{ + if (shift == 0) + { + dst[idx] |= mask; + } + else + { + dst[idx] |= mask << shift; + dst[idx + 1] |= mask >> (64 - shift); + } +} + +static void orShiftedMask32(unsigned int* dst, unsigned int idx, unsigned int shift, unsigned int mask) +{ + if (shift == 0) + { + dst[idx] |= mask; + } + else + { + dst[idx] |= mask << shift; + dst[idx + 1] |= mask >> (32 - shift); + } +} + +static void packNegPosWithPadding(const char* data, + unsigned long long dataSizeInBits, + unsigned long long paddedSizeInBits, + unsigned char* negMask, + unsigned char* posMask) +{ + const unsigned long long totalBits = dataSizeInBits + 2ULL * paddedSizeInBits; + const unsigned long long totalBytes = (totalBits + 8 - 1) >> 3; + setMem(negMask, totalBytes, 0); + setMem(posMask, totalBytes, 0); + +#if defined (__AVX512F__) + auto* neg64 = reinterpret_cast(negMask); + auto* pos64 = reinterpret_cast(posMask); + const __m512i vMinus1 = _mm512_set1_epi8(-1); + const __m512i vPlus1 = _mm512_set1_epi8(+1); + unsigned long long k = 0; + for (; k + BATCH_SIZE <= dataSizeInBits; k += BATCH_SIZE) + { + __m512i v = _mm512_loadu_si512(reinterpret_cast(data + k)); + __mmask64 mNeg = _mm512_cmpeq_epi8_mask(v, vMinus1); + __mmask64 mPos = _mm512_cmpeq_epi8_mask(v, vPlus1); + + // Start to fill data from the offset + unsigned long long bitPos = paddedSizeInBits + k; + unsigned int wordIdx = static_cast(bitPos >> 6); // /64 + unsigned int offset = static_cast(bitPos & 63); // %64 + orShiftedMask64(neg64, wordIdx, offset, static_cast(mNeg)); + orShiftedMask64(pos64, wordIdx, offset, static_cast(mPos)); + } +#else + auto* neg32 = reinterpret_cast(negMask); + auto* pos32 = reinterpret_cast(posMask); + const __m256i vMinus1 = _mm256_set1_epi8(-1); + const __m256i vPlus1 = _mm256_set1_epi8(+1); + unsigned long long k = 0; + for (; k + BATCH_SIZE <= dataSizeInBits; k += BATCH_SIZE) + { + __m256i v = _mm256_loadu_si256(reinterpret_cast(data + k)); + + // Compare for -1 and +1 + __m256i isNeg = _mm256_cmpeq_epi8(v, vMinus1); + __m256i isPos = _mm256_cmpeq_epi8(v, vPlus1); + + unsigned int mNeg = static_cast(_mm256_movemask_epi8(isNeg)); + unsigned int mPos = static_cast(_mm256_movemask_epi8(isPos)); + + // Start to fill data from the offset + unsigned long long bitPos = paddedSizeInBits + k; + unsigned int wordIdx = static_cast(bitPos >> 5); // / 32 + unsigned int offset = static_cast(bitPos & 31); // % 32 + + orShiftedMask32(neg32, wordIdx, offset, static_cast(mNeg)); + orShiftedMask32(pos32, wordIdx, offset, static_cast(mPos)); + + } +#endif + // Process the remained data + for (; k < dataSizeInBits; ++k) + { + char v = data[k]; + if (v == 0) continue; /* nothing to set */ + + unsigned long long bitPos = paddedSizeInBits + k; /* logical bit index */ + unsigned long long byteIdx = bitPos >> 3; /* byte containing it */ + unsigned shift = bitPos & 7U; /* bit %8 */ + + unsigned char mask = (unsigned char)(1U << shift); + + if (v == -1) + negMask[byteIdx] |= mask; + else /* v == +1 */ + posMask[byteIdx] |= mask; + } +} + +// Load 256/512 values start from a bit index into a m512 or m256 register +#if defined (__AVX512F__) +static inline __m512i load512Bits(const unsigned char* array, unsigned long long bitLocation) +{ + const unsigned long long byteIndex = bitLocation >> 3; // /8 + const unsigned int bitOffset = (unsigned)(bitLocation & 7ULL); // %8 + + __m512i v0 = _mm512_loadu_si512((const void*)(array + byteIndex)); + if (bitOffset == 0) + return v0; + + __m512i v1 = _mm512_loadu_si512((const void*)(array + byteIndex + 1)); + __m512i right = _mm512_srli_epi64(v0, bitOffset); // low part + __m512i left = _mm512_slli_epi64(v1, 8u - bitOffset); // carry bits + + return _mm512_or_si512(right, left); +} +#else +static inline __m256i load256Bits(const unsigned char* array, unsigned long long bitLocation) +{ + const unsigned long long byteIndex = bitLocation >> 3; + const int bitOffset = (int)(bitLocation & 7ULL); + + // Load a 256-bit (32-byte) vector starting at the byte index. + const __m256i v = _mm256_loadu_si256(reinterpret_cast(array + byteIndex)); + + if (bitOffset == 0) + { + return v; + } + + // Perform the right shift within each 64-bit lane. + const __m256i right_shifted = _mm256_srli_epi64(v, bitOffset); + + // Left-shift the +1 byte vector to align the carry bits. + const __m256i v_shifted_by_one_byte = _mm256_loadu_si256( + reinterpret_cast(array + byteIndex + 1) + ); + const __m256i left_shifted_carry = _mm256_slli_epi64(v_shifted_by_one_byte, 8 - bitOffset); + + // Combine the two parts with a bitwise OR to get the final result. + return _mm256_or_si256(right_shifted, left_shifted_carry); + +} +#endif + +template +static void toTenaryBits(long long A, char* bits) +{ + for (unsigned long long i = 0; i < bitCount; ++i) + { + char bitValue = static_cast((A >> i) & 1); + bits[i] = (bitValue == 0) ? -1 : bitValue; + } +} + +static int extractLastOutput( + const unsigned char* data, + const unsigned char* dataType, + const unsigned long long dataSize, + unsigned char* requestedOutput, + int requestedSizeInBytes) +{ + setMem(requestedOutput, requestedSizeInBytes, 0); + + int byteCount = 0; + int bitCount = 0; + unsigned char currentByte = 0; + + for (unsigned long long i = 0; i < dataSize && byteCount < requestedSizeInBytes; i++) + { + if (dataType[i] == OUTPUT_NEURON_TYPE) + { + // Skip zero data + if (data[i] == 0) + { + continue; + } + + // Pack sign bit: 1 if positive, 0 if negative + unsigned char bit = (data[i] > 0) ? 1 : 0; + currentByte |= (bit << (7 - bitCount)); + bitCount++; + + // Byte complete + if (bitCount == 8) + { + requestedOutput[byteCount++] = currentByte; + currentByte = 0; + bitCount = 0; + } + } + } + + // Write final partial byte if any bits were set + if (bitCount > 0 && byteCount < requestedSizeInBytes) + { + requestedOutput[byteCount++] = currentByte; + } + + return byteCount; +} + +// Clamp the index in cirulate +// This only work with |value| < population +static inline unsigned long long clampCirculatingIndex( + long long population, + long long neuronIdx, + long long value) +{ + long long nnIndex = neuronIdx + value; + + // Get the signed bit and decide if we should increase population + nnIndex += (population & (nnIndex >> 63)); + + // Subtract population if idx >= population + long long over = nnIndex - population; + nnIndex -= (population & ~(over >> 63)); + return (unsigned long long)nnIndex; +} + +// Get the solution threshold depend on nonce +// In case of not provided threholdBuffer, just return the default value +static AlgoType getAlgoType(const unsigned char* nonce) +{ + AlgoType selectedAgo = ((nonce[0] & 1) == 0) ? AlgoType::HyperIdentity : AlgoType::Addition; + return selectedAgo; +} + +// Verify if the solution threshold is valid +static bool checkAlgoThreshold(int threshold, AlgoType algo) +{ + if (threshold <= 0) + { + return false; + } + + switch (algo) + { + case AlgoType::HyperIdentity: + if (threshold > HYPERIDENTITY_NUMBER_OF_OUTPUT_NEURONS) + { + return false; + } + break; + case AlgoType::Addition: + if (threshold > ADDITION_NUMBER_OF_OUTPUT_NEURONS * (1U << ADDITION_NUMBER_OF_INPUT_NEURONS)) + { + return false; + } + break; + default: + return false; + } + return true; +} + +} diff --git a/src/mining/score_engine.h b/src/mining/score_engine.h new file mode 100644 index 000000000..2b4cb4b2c --- /dev/null +++ b/src/mining/score_engine.h @@ -0,0 +1,70 @@ +#pragma once + + +#include "score_hyperidentity.h" +#include "score_addition.h" + +namespace score_engine +{ + +template +struct ScoreEngine +{ + ScoreHyperIdentity _hyperIdentityScore; + ScoreAddition _additionScore; + unsigned char lastNonceByte0; + + void initMemory() + { + setMem(&_hyperIdentityScore, sizeof(ScoreHyperIdentity), 0); + setMem(&_additionScore, sizeof(ScoreAddition), 0); + + _hyperIdentityScore.initMemory(); + _additionScore.initMemory(); + } + + // Unused function + void initMiningData(const unsigned char* randomPool) + { + + } + + unsigned int computeHyperIdentityScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool) + { + return _hyperIdentityScore.computeScore(publicKey, nonce, randomPool); + } + + unsigned int computeAdditionScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool) + { + return _additionScore.computeScore(publicKey, nonce, randomPool); + } + + unsigned int computeScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool) + { + lastNonceByte0 = nonce[0]; + if ((nonce[0] & 1) == 0) + { + return computeHyperIdentityScore(publicKey, nonce, randomPool); + } + else + { + return computeAdditionScore(publicKey, nonce, randomPool); + } + } + + // returns last computed output neurons, only returns 256 non-zero neurons, neuron values are compressed to bit + m256i getLastOutput() + { + // Only hyperidentity score support + m256i result; + result = m256i::zero(); + if ((lastNonceByte0 & 1) == 0) + { + _hyperIdentityScore.getLastOutput(result.m256i_u8, 32); + } + return result; + } + +}; + +} diff --git a/src/mining/score_hyperidentity.h b/src/mining/score_hyperidentity.h new file mode 100644 index 000000000..720847914 --- /dev/null +++ b/src/mining/score_hyperidentity.h @@ -0,0 +1,1214 @@ +#pragma once + +#include "score_common.h" + +namespace score_engine +{ + +template +struct ScoreHyperIdentity +{ + // Convert params for easier usage + static constexpr unsigned long long numberOfInputNeurons = Params::numberOfInputNeurons; + static constexpr unsigned long long numberOfOutputNeurons = Params::numberOfOutputNeurons; + static constexpr unsigned long long numberOfTicks = Params::numberOfTicks; + static constexpr unsigned long long numberOfNeighbors = Params::numberOfNeighbors; + static constexpr unsigned long long populationThreshold = Params::populationThreshold; + static constexpr unsigned long long numberOfMutations = Params::numberOfMutations; + static constexpr unsigned int solutionThreshold = Params::solutionThreshold; + + // Computation params + static constexpr unsigned long long numberOfNeurons = numberOfInputNeurons + numberOfOutputNeurons; + static constexpr unsigned long long maxNumberOfNeurons = populationThreshold; + static constexpr unsigned long long maxNumberOfSynapses = populationThreshold * numberOfNeighbors; + static constexpr unsigned long long initNumberOfSynapses = numberOfNeurons * numberOfNeighbors; + static constexpr long long radius = (long long)numberOfNeighbors / 2; + static constexpr long long paddingNeuronsCount = (maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE; + static constexpr unsigned long long incommingSynapsesPitch = ((numberOfNeighbors + 1) + BATCH_SIZE_X8 - 1) / BATCH_SIZE_X8 * BATCH_SIZE_X8; + static constexpr unsigned long long incommingSynapseBatchSize = incommingSynapsesPitch >> 3; + + static_assert(numberOfInputNeurons % 64 == 0, "numberOfInputNeurons must be divided by 64"); + static_assert(numberOfOutputNeurons % 64 == 0, "numberOfOutputNeurons must be divided by 64"); + static_assert(maxNumberOfSynapses <= (0xFFFFFFFFFFFFFFFF << 1ULL), "maxNumberOfSynapses must less than or equal MAX_UINT64/2"); + static_assert(initNumberOfSynapses % 32 == 0, "initNumberOfSynapses must be divided by 32"); + static_assert(numberOfNeighbors % 2 == 0, "numberOfNeighbors must be divided by 2"); + static_assert(populationThreshold > numberOfNeurons, "populationThreshold must be greater than numberOfNeurons"); + static_assert(numberOfNeurons > numberOfNeighbors, "Number of neurons must be greater than the number of neighbors"); + static_assert(numberOfNeighbors < ((1ULL << 63) - 1), "numberOfNeighbors must be in long long range"); + static_assert(BATCH_SIZE_X8 % 8 == 0, "BATCH_SIZE must be be divided by 8"); + + + // Intermediate data + struct InitValue + { + unsigned long long outputNeuronPositions[numberOfOutputNeurons]; + unsigned long long synapseWeight[initNumberOfSynapses / 32]; // each 64bits elements will decide value of 32 synapses + unsigned long long synpaseMutation[numberOfMutations]; + }; + + struct MiningData + { + unsigned long long inputNeuronRandomNumber[numberOfInputNeurons / 64]; // each bit will use for generate input neuron value + unsigned long long outputNeuronRandomNumber[numberOfOutputNeurons / 64]; // each bit will use for generate expected output neuron value + }; + static constexpr unsigned long long paddingInitValueSizeInBytes = (sizeof(InitValue) + 64 - 1) / 64 * 64; + + void initMemory() {} + + typedef char Synapse; + typedef char Neuron; + typedef unsigned char NeuronType; + + // Data for roll back + struct ANN + { + void init() + { + neurons = paddingNeurons + radius; + } + void prepareData() + { + // Padding start and end of neuron array + neurons = paddingNeurons + radius; + } + + void copyDataTo(ANN& rOther) + { + copyMem(rOther.neurons, neurons, population * sizeof(Neuron)); + copyMem(rOther.neuronTypes, neuronTypes, population * sizeof(NeuronType)); + copyMem(rOther.synapses, synapses, maxNumberOfSynapses * sizeof(Synapse)); + rOther.population = population; + } + + Neuron* neurons; + // Padding start and end of neurons so that we can reduce the condition checking + Neuron paddingNeurons[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE]; + NeuronType neuronTypes[(maxNumberOfNeurons + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE]; + Synapse synapses[maxNumberOfSynapses]; + + // Encoded data + unsigned char neuronPlus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE + BATCH_SIZE_X8]; + unsigned char neuronMinus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE + BATCH_SIZE_X8]; + + unsigned char nextNeuronPlus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE + BATCH_SIZE_X8]; + unsigned char nextneuronMinus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE + BATCH_SIZE_X8]; + + unsigned char synapsePlus1s[incommingSynapsesPitch * populationThreshold]; + unsigned char synapseMinus1s[incommingSynapsesPitch * populationThreshold]; + + + unsigned long long population; + }; + ANN bestANN; + ANN currentANN; + + // Intermediate data + unsigned char paddingInitValue[paddingInitValueSizeInBytes]; + MiningData miningData; + + unsigned long long neuronIndices[numberOfNeurons]; + Neuron previousNeuronValue[maxNumberOfNeurons]; + + Neuron outputNeuronExpectedValue[numberOfOutputNeurons]; + + Neuron neuronValueBuffer[maxNumberOfNeurons]; + unsigned char hash[32]; + unsigned char combined[64]; + + unsigned long long removalNeuronsCount; + + // Contain incomming synapse of neurons. The center one will be zeros + Synapse incommingSynapses[maxNumberOfNeurons * incommingSynapsesPitch]; + + // Padding to fix bytes for each row + Synapse paddingIncommingSynapses[populationThreshold * incommingSynapsesPitch]; + + unsigned char nextNeuronPlus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE]; + unsigned char nextNeuronMinus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE]; + + unsigned char keptNeurons[maxNumberOfNeurons]; + unsigned long long affectedNeurons[maxNumberOfNeurons]; + unsigned long long nextAffectedNeurons[maxNumberOfNeurons]; + + void mutate(unsigned long long mutateStep) + { + // Mutation + unsigned long long population = currentANN.population; + unsigned long long synapseCount = population * numberOfNeighbors; + Synapse* synapses = currentANN.synapses; + InitValue* initValue = (InitValue*)paddingInitValue; + + // Randomly pick a synapse, randomly increase or decrease its weight by 1 or -1 + unsigned long long synapseMutation = initValue->synpaseMutation[mutateStep]; + unsigned long long synapseIdx = (synapseMutation >> 1) % synapseCount; + // Randomly increase or decrease its value + char weightChange = 0; + if ((synapseMutation & 1ULL) == 0) + { + weightChange = -1; + } + else + { + weightChange = 1; + } + + char newWeight = synapses[synapseIdx] + weightChange; + + // Valid weight. Update it + if (newWeight >= -1 && newWeight <= 1) + { + synapses[synapseIdx] = newWeight; + } + else // Invalid weight. Insert a neuron + { + // Insert the neuron + insertNeuron(synapseIdx); + } + // Clean the ANN + cleanANN(); + } + + // Get the pointer to all outgoing synapse of a neurons + Synapse* getSynapses(unsigned long long neuronIndex) + { + return ¤tANN.synapses[neuronIndex * numberOfNeighbors]; + } + + // Circulate the neuron index + unsigned long long clampNeuronIndex(long long neuronIdx, long long value) + { + const long long population = (long long)currentANN.population; + long long nnIndex = neuronIdx + value; + + // Get the signed bit and decide if we should increase population + nnIndex += (population & (nnIndex >> 63)); + + // Subtract population if idx >= population + long long over = nnIndex - population; + nnIndex -= (population & ~(over >> 63)); + return (unsigned long long)nnIndex; + } + + // Remove a neuron and all synapses relate to it + void removeNeuron(unsigned long long neuronIdx) + { + // Scan all its neigbor to remove their outgoing synapse point to the neuron + for (long long neighborOffset = -(long long)numberOfNeighbors / 2; neighborOffset <= (long long)numberOfNeighbors / 2; neighborOffset++) + { + unsigned long long nnIdx = clampNeuronIndex(neuronIdx, neighborOffset); + Synapse* pNNSynapses = getSynapses(nnIdx); + + long long synapseIndexOfNN = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); + if (synapseIndexOfNN < 0) + { + continue; + } + + // The synapse array need to be shifted regard to the remove neuron + // Also neuron need to have 2M neighbors, the addtional synapse will be set as zero weight + // Case1 [S0 S1 S2 - SR S5 S6]. SR is removed, [S0 S1 S2 S5 S6 0] + // Case2 [S0 S1 SR - S3 S4 S5]. SR is removed, [0 S0 S1 S3 S4 S5] + if (synapseIndexOfNN >= numberOfNeighbors / 2) + { + for (long long k = synapseIndexOfNN; k < numberOfNeighbors - 1; ++k) + { + pNNSynapses[k] = pNNSynapses[k + 1]; + } + pNNSynapses[numberOfNeighbors - 1] = 0; + } + else + { + for (long long k = synapseIndexOfNN; k > 0; --k) + { + pNNSynapses[k] = pNNSynapses[k - 1]; + } + pNNSynapses[0] = 0; + } + } + + // Shift the synapse array and the neuron array, also reduce the current ANN population + currentANN.population--; + for (unsigned long long shiftIdx = neuronIdx; shiftIdx < currentANN.population; shiftIdx++) + { + currentANN.neurons[shiftIdx] = currentANN.neurons[shiftIdx + 1]; + currentANN.neuronTypes[shiftIdx] = currentANN.neuronTypes[shiftIdx + 1]; + keptNeurons[shiftIdx] = keptNeurons[shiftIdx + 1]; + + // Also shift the synapses + copyMem(getSynapses(shiftIdx), getSynapses(shiftIdx + 1), numberOfNeighbors * sizeof(Synapse)); + } + } + + unsigned long long getNeighborNeuronIndex(unsigned long long neuronIndex, unsigned long long neighborOffset) + { + unsigned long long nnIndex = 0; + if (neighborOffset < (numberOfNeighbors / 2)) + { + nnIndex = clampNeuronIndex(neuronIndex + neighborOffset, -(long long)numberOfNeighbors / 2); + } + else + { + nnIndex = clampNeuronIndex(neuronIndex + neighborOffset + 1, -(long long)numberOfNeighbors / 2); + } + return nnIndex; + } + + void updateSynapseOfInsertedNN(unsigned long long insertedNeuronIdx) + { + // The change of synapse only impact neuron in [originalNeuronIdx - numberOfNeighbors / 2 + 1, originalNeuronIdx + numberOfNeighbors / 2] + // In the new index, it will be [originalNeuronIdx + 1 - numberOfNeighbors / 2, originalNeuronIdx + 1 + numberOfNeighbors / 2] + // [N0 N1 N2 original inserted N4 N5 N6], M = 2. + for (long long delta = -(long long)numberOfNeighbors / 2; delta <= (long long)numberOfNeighbors / 2; ++delta) + { + // Only process the neigbors + if (delta == 0) + { + continue; + } + unsigned long long updatedNeuronIdx = clampNeuronIndex(insertedNeuronIdx, delta); + + // Generate a list of neighbor index of current updated neuron NN + // Find the location of the inserted neuron in the list of neighbors + long long insertedNeuronIdxInNeigborList = -1; + for (long long k = 0; k < numberOfNeighbors; k++) + { + unsigned long long nnIndex = getNeighborNeuronIndex(updatedNeuronIdx, k); + if (nnIndex == insertedNeuronIdx) + { + insertedNeuronIdxInNeigborList = k; + } + } + + ASSERT(insertedNeuronIdxInNeigborList >= 0); + + Synapse* pUpdatedSynapses = getSynapses(updatedNeuronIdx); + // [N0 N1 N2 original inserted N4 N5 N6], M = 2. + // Case: neurons in range [N0 N1 N2 original], right synapses will be affected + if (delta < 0) + { + // Left side is kept as it is, only need to shift to the right side + for (long long k = numberOfNeighbors - 1; k >= insertedNeuronIdxInNeigborList; --k) + { + // Updated synapse + pUpdatedSynapses[k] = pUpdatedSynapses[k - 1]; + } + + // Incomming synapse from original neuron -> inserted neuron must be zero + if (delta == -1) + { + pUpdatedSynapses[insertedNeuronIdxInNeigborList] = 0; + } + } + else // Case: neurons in range [inserted N4 N5 N6], left synapses will be affected + { + // Right side is kept as it is, only need to shift to the left side + for (long long k = 0; k < insertedNeuronIdxInNeigborList; ++k) + { + // Updated synapse + pUpdatedSynapses[k] = pUpdatedSynapses[k + 1]; + } + } + + } + } + + void insertNeuron(unsigned long long synapseIdx) + { + // A synapse have incomingNeighbor and outgoingNeuron, direction incomingNeuron -> outgoingNeuron + unsigned long long incomingNeighborSynapseIdx = synapseIdx % numberOfNeighbors; + unsigned long long outgoingNeuron = synapseIdx / numberOfNeighbors; + + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + NeuronType* neuronTypes = currentANN.neuronTypes; + unsigned long long& population = currentANN.population; + + // Copy original neuron to the inserted one and set it as EVOLUTION_NEURON_TYPE type + Neuron insertNeuron = neurons[outgoingNeuron]; + unsigned long long insertedNeuronIdx = outgoingNeuron + 1; + + Synapse originalWeight = synapses[synapseIdx]; + + // Insert the neuron into array, population increased one, all neurons next to original one need to shift right + for (unsigned long long i = population; i > outgoingNeuron; --i) + { + neurons[i] = neurons[i - 1]; + neuronTypes[i] = neuronTypes[i - 1]; + + // Also shift the synapses to the right + copyMem(getSynapses(i), getSynapses(i - 1), numberOfNeighbors * sizeof(Synapse)); + } + neurons[insertedNeuronIdx] = insertNeuron; + neuronTypes[insertedNeuronIdx] = EVOLUTION_NEURON_TYPE; + population++; + + // Try to update the synapse of inserted neuron. All outgoing synapse is init as zero weight + Synapse* pInsertNeuronSynapse = getSynapses(insertedNeuronIdx); + for (unsigned long long synIdx = 0; synIdx < numberOfNeighbors; ++synIdx) + { + pInsertNeuronSynapse[synIdx] = 0; + } + + // Copy the outgoing synapse of original neuron + // Outgoing points to the left + if (incomingNeighborSynapseIdx < numberOfNeighbors / 2) + { + if (incomingNeighborSynapseIdx > 0) + { + // Decrease by one because the new neuron is next to the original one + pInsertNeuronSynapse[incomingNeighborSynapseIdx - 1] = originalWeight; + } + // Incase of the outgoing synapse point too far, don't add the synapse + } + else + { + // No need to adjust the added neuron but need to remove the synapse of the original neuron + pInsertNeuronSynapse[incomingNeighborSynapseIdx] = originalWeight; + } + + updateSynapseOfInsertedNN(insertedNeuronIdx); + } + + long long getIndexInSynapsesBuffer(unsigned long long neuronIdx, long long neighborOffset) + { + // Skip the case neuron point to itself and too far neighbor + if (neighborOffset == 0 + || neighborOffset < -(long long)numberOfNeighbors / 2 + || neighborOffset >(long long)numberOfNeighbors / 2) + { + return -1; + } + + long long synapseIdx = (long long)numberOfNeighbors / 2 + neighborOffset; + if (neighborOffset >= 0) + { + synapseIdx = synapseIdx - 1; + } + + return synapseIdx; + } + + bool isAllOutgoingSynapsesZeros(unsigned long long neuronIdx) + { + Synapse* synapse = getSynapses(neuronIdx); + for (unsigned long long n = 0; n < numberOfNeighbors; n++) + { + Synapse synapseW = synapse[n]; + if (synapseW != 0) + { + return false; + } + } + return true; + } + + bool isAllIncomingSynapsesZeros(unsigned long long neuronIdx) + { + // Loop through the neighbor neurons to check all incoming synapses + for (long long neighborOffset = -(long long)numberOfNeighbors / 2; neighborOffset <= (long long)numberOfNeighbors / 2; neighborOffset++) + { + unsigned long long nnIdx = clampNeuronIndex(neuronIdx, neighborOffset); + Synapse* nnSynapses = getSynapses(nnIdx); + + long long synapseIdx = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); + if (synapseIdx < 0) + { + continue; + } + Synapse synapseW = nnSynapses[synapseIdx]; + + if (synapseW != 0) + { + return false; + } + } + return true; + } + + // Check which neurons/synapse need to be removed after mutation + bool scanRedundantNeurons() + { + bool isStructureChanged = false; + unsigned long long population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + NeuronType* neuronTypes = currentANN.neuronTypes; + + unsigned long long affectedCount = 0; + unsigned long long nextCount = 0; + setMem(keptNeurons, sizeof(keptNeurons), 255); + + // First scan + for (unsigned long long i = 0; i < population; i++) + { + if (neuronTypes[i] != EVOLUTION_NEURON_TYPE) + { + continue; + } + + if (isAllOutgoingSynapsesZeros(i) || isAllIncomingSynapsesZeros(i)) + { + keptNeurons[i] = 0; + affectedNeurons[affectedCount++] = i; + isStructureChanged = true; + } + } + + while (affectedCount > 0) + { + nextCount = 0; + for (unsigned long long affectedIndex = 0; affectedIndex < affectedCount; affectedIndex++) + { + unsigned long long i = affectedNeurons[affectedIndex]; + // Mark the neuron for removal and set all its incoming and outgoing synapse weights to zero. + // This action isolates the neuron, allowing adjacent neurons to be considered for removal in the next iteration. + + // Remove outgoing synapse + setMem(getSynapses(i), numberOfNeighbors * sizeof(Synapse), 0); + + // Scan all its neigbor to remove their outgoing synapse point to the neuron aka incomming synapses of this neuron + for (long long neighborOffset = -(long long)numberOfNeighbors / 2; neighborOffset <= (long long)numberOfNeighbors / 2; neighborOffset++) + { + unsigned long long nnIdx = clampNeuronIndex(i, neighborOffset); + Synapse* pNNSynapses = getSynapses(nnIdx); + + long long synapseIndexOfNN = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); + if (synapseIndexOfNN < 0) + { + continue; + } + + // Synapse to this i neurons is marked as zero/aka disconnected + if (pNNSynapses[synapseIndexOfNN] != 0) + { + pNNSynapses[synapseIndexOfNN] = 0; + + // This neuron is not marked as removal yet, record it + if (keptNeurons[nnIdx]) + { + nextAffectedNeurons[nextCount++] = nnIdx; + } + } + } + + // Mark the neurons as removal + affectedCount = 0; + for (unsigned long long k = 0; k < nextCount; ++k) + { + unsigned long long idx = nextAffectedNeurons[k]; + + // Skip already removed or non-evolution neurons + if (!keptNeurons[idx] || neuronTypes[idx] != EVOLUTION_NEURON_TYPE) + continue; + + // Check if these neurons are needed to be removed + if (isAllOutgoingSynapsesZeros(idx) || isAllIncomingSynapsesZeros(idx)) + { + keptNeurons[idx] = 0; + affectedNeurons[affectedCount++] = idx; + isStructureChanged = true; + } + } + } + } + + return isStructureChanged; + } + + // Remove neurons and synapses that do not affect the ANN + void cleanANN() + { + // No removal. First scan and probagate to be removed neurons + if (!scanRedundantNeurons()) + { + return; + } + + // Remove neurons + unsigned long long neuronIdx = 0; + while (neuronIdx < currentANN.population) + { + if (keptNeurons[neuronIdx] == 0) + { + // Remove it from the neuron list. Overwrite data + // Remove its synapses in the synapses array + removeNeuron(neuronIdx); + } + else + { + neuronIdx++; + } + } + } + + void processTick() + { + unsigned long long population = currentANN.population; + + unsigned char* pPaddingNeuronMinus = currentANN.neuronMinus1s; + unsigned char* pPaddingNeuronPlus = currentANN.neuronPlus1s; + + unsigned char* pPaddingSynapseMinus = currentANN.synapseMinus1s; + unsigned char* pPaddingSynapsePlus = currentANN.synapsePlus1s; + + paddingDatabits(pPaddingNeuronMinus, population); + paddingDatabits(pPaddingNeuronPlus, population); + + +#if defined (__AVX512F__) + constexpr unsigned long long chunks = incommingSynapsesPitch >> 9; + __m512i minusBlock[chunks]; + __m512i minusNext[chunks]; + __m512i plusBlock[chunks]; + __m512i plusNext[chunks]; + + constexpr unsigned long long blockSizeNeurons = 64ULL; + constexpr unsigned long long bytesPerWord = 8ULL; + + unsigned long long n = 0; + const unsigned long long lastBlock = (population / blockSizeNeurons) * blockSizeNeurons; + for (; n < lastBlock; n += blockSizeNeurons) + { + // byteIndex = start byte for word containing neuron n + unsigned long long byteIndex = ((n >> 6) << 3); // (n / 64) * 8 + unsigned long long curIdx = byteIndex; + unsigned long long nextIdx = byteIndex + bytesPerWord; // +8 bytes + + // Load the neuron windows once per block for all chunks + unsigned long long loadCur = curIdx; + unsigned long long loadNext = nextIdx; + for (unsigned blk = 0; blk < chunks; ++blk, loadCur += BATCH_SIZE, loadNext += BATCH_SIZE) + { + plusBlock[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + loadCur)); + plusNext[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + loadNext)); + minusBlock[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + loadCur)); + minusNext[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + loadNext)); + } + + __m512i sh = _mm512_setzero_si512(); + __m512i sh64 = _mm512_set1_epi64(64); + const __m512i ones512 = _mm512_set1_epi64(1); + + // For each neuron inside this 64-neuron block + for (unsigned int lane = 0; lane < 64; ++lane) + { + const unsigned long long current_n = n + lane; + // synapse pointers for this neuron + unsigned char* pSynapsePlus = pPaddingSynapsePlus + current_n * incommingSynapseBatchSize; + unsigned char* pSynapseMinus = pPaddingSynapseMinus + current_n * incommingSynapseBatchSize; + + __m512i plusPopulation = _mm512_setzero_si512(); + __m512i minusPopulation = _mm512_setzero_si512(); + + for (unsigned blk = 0; blk < chunks; ++blk) + { + const __m512i synP = _mm512_loadu_si512((const void*)(pSynapsePlus + blk * BATCH_SIZE)); + const __m512i synM = _mm512_loadu_si512((const void*)(pSynapseMinus + blk * BATCH_SIZE)); + + // stitch 64-bit lanes: cur >> s | next << (64 - s) + __m512i neuronPlus = _mm512_or_si512(_mm512_srlv_epi64(plusBlock[blk], sh), _mm512_sllv_epi64(plusNext[blk], sh64)); + __m512i neuronMinus = _mm512_or_si512(_mm512_srlv_epi64(minusBlock[blk], sh), _mm512_sllv_epi64(minusNext[blk], sh64)); + + __m512i tmpP = _mm512_and_si512(neuronMinus, synM); + const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synP, tmpP, 234); + + __m512i tmpM = _mm512_and_si512(neuronMinus, synP); + const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synM, tmpM, 234); + + plusPopulation = _mm512_add_epi64(plusPopulation, _mm512_popcnt_epi64(plus)); + minusPopulation = _mm512_add_epi64(minusPopulation, _mm512_popcnt_epi64(minus)); + } + sh = _mm512_add_epi64(sh, ones512); + sh64 = _mm512_sub_epi64(sh64, ones512); + + // Reduce to scalar and compute neuron value + int score = (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); + char neuronValue = (score > 0) - (score < 0); + neuronValueBuffer[current_n] = neuronValue; + + // Update the neuron positive and negative bitmaps + unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; + unsigned char nNextPos = neuronValue > 0 ? 1 : 0; + setBitValue(currentANN.nextneuronMinus1s, current_n + radius, nNextNeg); + setBitValue(currentANN.nextNeuronPlus1s, current_n + radius, nNextPos); + } + } + + for (; n < population; ++n) + { + char neuronValue = 0; + int score = 0; + unsigned char* pSynapsePlus = pPaddingSynapsePlus + n * incommingSynapseBatchSize; + unsigned char* pSynapseMinus = pPaddingSynapseMinus + n * incommingSynapseBatchSize; + + const unsigned long long byteIndex = n >> 3; + const unsigned int bitOffset = (n & 7U); + const unsigned int bitOffset_8 = (8u - bitOffset); + __m512i sh = _mm512_set1_epi64((long long)bitOffset); + __m512i sh8 = _mm512_set1_epi64((long long)bitOffset_8); + + __m512i plusPopulation = _mm512_setzero_si512(); + __m512i minusPopulation = _mm512_setzero_si512(); + + for (unsigned blk = 0; blk < chunks; ++blk, pSynapsePlus += BATCH_SIZE, pSynapseMinus += BATCH_SIZE) + { + const __m512i synapsePlus = _mm512_loadu_si512((const void*)(pSynapsePlus)); + const __m512i synapseMinus = _mm512_loadu_si512((const void*)(pSynapseMinus)); + + __m512i neuronPlus = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + byteIndex + blk * BATCH_SIZE)); + __m512i neuronPlusNext = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + byteIndex + blk * BATCH_SIZE + 1)); + __m512i neuronMinus = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + byteIndex + blk * BATCH_SIZE)); + __m512i neuronMinusNext = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + byteIndex + blk * BATCH_SIZE + 1)); + + neuronPlus = _mm512_or_si512(_mm512_srlv_epi64(neuronPlus, sh), _mm512_sllv_epi64(neuronPlusNext, sh8)); + neuronMinus = _mm512_or_si512(_mm512_srlv_epi64(neuronMinus, sh), _mm512_sllv_epi64(neuronMinusNext, sh8)); + + __m512i tempP = _mm512_and_si512(neuronMinus, synapseMinus); + const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synapsePlus, tempP, 234); + + __m512i tempM = _mm512_and_si512(neuronMinus, synapsePlus); + const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synapseMinus, tempM, 234); + + tempP = _mm512_popcnt_epi64(plus); + tempM = _mm512_popcnt_epi64(minus); + plusPopulation = _mm512_add_epi64(tempP, plusPopulation); + minusPopulation = _mm512_add_epi64(tempM, minusPopulation); + } + + score = (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); + neuronValue = (score > 0) - (score < 0); + neuronValueBuffer[n] = neuronValue; + + unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; + unsigned char nNextPos = neuronValue > 0 ? 1 : 0; + setBitValue(currentANN.nextneuronMinus1s, n + radius, nNextNeg); + setBitValue(currentANN.nextNeuronPlus1s, n + radius, nNextPos); + } +#else + constexpr unsigned long long chunks = incommingSynapsesPitch >> 8; + for (unsigned long long n = 0; n < population; ++n, pPaddingSynapsePlus += incommingSynapseBatchSize, pPaddingSynapseMinus += incommingSynapseBatchSize) + { + char neuronValue = 0; + int score = 0; + unsigned char* pSynapsePlus = pPaddingSynapsePlus; + unsigned char* pSynapseMinus = pPaddingSynapseMinus; + + int synapseBlkIdx = 0; // blk index of synapse + int neuronBlkIdx = 0; + for (unsigned blk = 0; blk < chunks; ++blk, synapseBlkIdx += BATCH_SIZE, neuronBlkIdx += BATCH_SIZE_X8) + { + // Process 256bits at once, neigbor shilf 64 bytes = 256 bits + const __m256i synapsePlus = _mm256_loadu_si256((const __m256i*)(pPaddingSynapsePlus + synapseBlkIdx)); + const __m256i synapseMinus = _mm256_loadu_si256((const __m256i*)(pPaddingSynapseMinus + synapseBlkIdx)); + + __m256i neuronPlus = load256Bits(pPaddingNeuronPlus, n + neuronBlkIdx); + __m256i neuronMinus = load256Bits(pPaddingNeuronMinus, n + neuronBlkIdx); + + // Compare the negative and possitive parts + __m256i plus = _mm256_or_si256(_mm256_and_si256(neuronPlus, synapsePlus), + _mm256_and_si256(neuronMinus, synapseMinus)); + __m256i minus = _mm256_or_si256(_mm256_and_si256(neuronPlus, synapseMinus), + _mm256_and_si256(neuronMinus, synapsePlus)); + + score += popcnt256(plus) - popcnt256(minus); + } + + neuronValue = (score > 0) - (score < 0); + neuronValueBuffer[n] = neuronValue; + + // Update the neuron positive and negative + unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; + unsigned char nNextPos = neuronValue > 0 ? 1 : 0; + setBitValue(currentANN.nextneuronMinus1s, n + radius, nNextNeg); + setBitValue(currentANN.nextNeuronPlus1s, n + radius, nNextPos); + } +#endif + + + + copyMem(currentANN.neurons, neuronValueBuffer, population * sizeof(Neuron)); + copyMem(currentANN.neuronMinus1s, currentANN.nextneuronMinus1s, sizeof(currentANN.neuronMinus1s)); + copyMem(currentANN.neuronPlus1s, currentANN.nextNeuronPlus1s, sizeof(currentANN.neuronPlus1s)); + } + + void runTickSimulation() + { + unsigned long long population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + NeuronType* neuronTypes = currentANN.neuronTypes; + + // Save the neuron value for comparison + copyMem(previousNeuronValue, neurons, population * sizeof(Neuron)); + { + //PROFILE_NAMED_SCOPE("convertSynapse"); + // Compute the incomming synapse of each neurons + setMem(paddingIncommingSynapses, sizeof(paddingIncommingSynapses), 0); + for (unsigned long long n = 0; n < population; ++n) + { + const Synapse* kSynapses = getSynapses(n); + // Scan through all neighbor neurons and sum all connected neurons. + // The synapses are arranged as neuronIndex * numberOfNeighbors + for (long long m = 0; m < radius; m++) + { + Synapse synapseWeight = kSynapses[m]; + unsigned long long nnIndex = clampNeuronIndex(n + m, -radius); + paddingIncommingSynapses[nnIndex * incommingSynapsesPitch + (numberOfNeighbors - m)] = synapseWeight; + } + + //paddingIncommingSynapses[n * incommingSynapsesPitch + radius] = 0; + + for (long long m = radius; m < numberOfNeighbors; m++) + { + Synapse synapseWeight = kSynapses[m]; + unsigned long long nnIndex = clampNeuronIndex(n + m + 1, -radius); + paddingIncommingSynapses[nnIndex * incommingSynapsesPitch + (numberOfNeighbors - m - 1)] = synapseWeight; + } + } + } + + // Prepare masks + { + //PROFILE_NAMED_SCOPE("prepareMask"); + packNegPosWithPadding(currentANN.neurons, + population, + radius, + currentANN.neuronMinus1s, + currentANN.neuronPlus1s); + + packNegPosWithPadding(paddingIncommingSynapses, + incommingSynapsesPitch * population, + 0, + currentANN.synapseMinus1s, + currentANN.synapsePlus1s); + } + + { + //PROFILE_NAMED_SCOPE("processTickLoop"); + for (unsigned long long tick = 0; tick < numberOfTicks; ++tick) + { + processTick(); + // Check exit conditions: + // - N ticks have passed (already in for loop) + // - All neuron values are unchanged + // - All output neurons have non-zero values + + if (areAllNeuronsUnchanged((const char*)previousNeuronValue, (const char*)neurons, population) + || areAllNeuronsZeros((const char*)neurons, (const char*)neuronTypes, population)) + { + break; + } + + // Copy the neuron value + copyMem(previousNeuronValue, neurons, population * sizeof(Neuron)); + } + } + } + + bool areAllNeuronsZeros( + const char* neurons, + const char* neuronTypes, + unsigned long long population) + { + +#if defined (__AVX512F__) + const __m512i zero = _mm512_setzero_si512(); + const __m512i typeOutput = _mm512_set1_epi8(OUTPUT_NEURON_TYPE); + + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m512i cur = _mm512_loadu_si512((const void*)(neurons + i)); + __m512i types = _mm512_loadu_si512((const void*)(neuronTypes + i)); + + __mmask64 type_mask = _mm512_cmpeq_epi8_mask(types, typeOutput); + __mmask64 zero_mask = _mm512_cmpeq_epi8_mask(cur, zero); + + if (type_mask & zero_mask) + return false; + } +#else + const __m256i zero = _mm256_setzero_si256(); + const __m256i typeOutput = _mm256_set1_epi8(OUTPUT_NEURON_TYPE); + + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m256i cur = _mm256_loadu_si256((const __m256i*)(neurons + i)); + __m256i types = _mm256_loadu_si256((const __m256i*)(neuronTypes + i)); + + // Compare for type == OUTPUT + __m256i type_cmp = _mm256_cmpeq_epi8(types, typeOutput); + int type_mask = _mm256_movemask_epi8(type_cmp); + + // Compare for neuron == 0 + __m256i zero_cmp = _mm256_cmpeq_epi8(cur, zero); + int zero_mask = _mm256_movemask_epi8(zero_cmp); + + // If both masks overlap → some output neuron is zero + if (type_mask & zero_mask) + { + return false; + } + } + +#endif + for (; i < population; i++) + { + // Neuron unchanged check + if (neuronTypes[i] == OUTPUT_NEURON_TYPE && neurons[i] == 0) + { + return false; + } + } + + return true; + } + + bool areAllNeuronsUnchanged( + const char* previousNeuronValue, + const char* neurons, + unsigned long long population) + { + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + +#if defined (__AVX512F__) + __m512i prev = _mm512_loadu_si512((const void*)(previousNeuronValue + i)); + __m512i cur = _mm512_loadu_si512((const void*)(neurons + i)); + + __mmask64 neq_mask = _mm512_cmpneq_epi8_mask(prev, cur); + if (neq_mask) + { + return false; + } +#else + __m256i v_prev = _mm256_loadu_si256((const __m256i*)(previousNeuronValue + i)); + __m256i v_curr = _mm256_loadu_si256((const __m256i*)(neurons + i)); + __m256i cmp = _mm256_cmpeq_epi8(v_prev, v_curr); + + int mask = _mm256_movemask_epi8(cmp); + + // -1 means all bytes equal + if (mask != -1) + { + return false; + } +#endif + } + + for (; i < population; i++) + { + // Neuron unchanged check + if (previousNeuronValue[i] != neurons[i]) + { + return false; + } + } + + return true; + } + + unsigned int computeNonMatchingOutput() + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + NeuronType* neuronTypes = currentANN.neuronTypes; + + // Compute the non-matching value R between output neuron value and initial value + // Because the output neuron order never changes, the order is preserved + unsigned int R = 0; + unsigned long long outputIdx = 0; + unsigned long long i = 0; +#if defined (__AVX512F__) + const __m512i typeOutputAVX = _mm512_set1_epi8(OUTPUT_NEURON_TYPE); + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + // Load 64 neuron types and compare with OUTPUT_NEURON_TYPE + __m512i types = _mm512_loadu_si512((const void*)(neuronTypes + i)); + __mmask64 type_mask = _mm512_cmpeq_epi8_mask(types, typeOutputAVX); + + if (type_mask == 0) + { + continue; // no output neurons in this 64-wide block, just skip + } + + // Output neuron existed in this block + for (int k = 0; k < BATCH_SIZE; ++k) + { + if (type_mask & (1ULL << k)) + { + char neuronVal = neurons[i + k]; + if (neuronVal != outputNeuronExpectedValue[outputIdx]) + { + R++; + } + outputIdx++; + } + } + } +#else + const __m256i typeOutputAVX = _mm256_set1_epi8(OUTPUT_NEURON_TYPE); + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m256i types_vec = _mm256_loadu_si256((const __m256i*)(neuronTypes + i)); + __m256i cmp_vec = _mm256_cmpeq_epi8(types_vec, typeOutputAVX); + unsigned int type_mask = _mm256_movemask_epi8(cmp_vec); + + if (type_mask == 0) + { + continue; // no output neurons in this 32-wide block, just skip + } + for (int k = 0; k < BATCH_SIZE; ++k) + { + if (type_mask & (1U << k)) + { + char neuronVal = neurons[i + k]; + if (neuronVal != outputNeuronExpectedValue[outputIdx]) + R++; + outputIdx++; + } + } + } +#endif + + // remainder loop + for (; i < population; i++) + { + if (neuronTypes[i] == OUTPUT_NEURON_TYPE) + { + if (neurons[i] != outputNeuronExpectedValue[outputIdx]) + { + R++; + } + outputIdx++; + } + } + + return R; + } + + void initInputNeuron() + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + NeuronType* neuronTypes = currentANN.neuronTypes; + Neuron neuronArray[64] = { 0 }; + unsigned long long inputNeuronInitIndex = 0; + for (unsigned long long i = 0; i < population; ++i) + { + // Input will use the init value + if (neuronTypes[i] == INPUT_NEURON_TYPE) + { + // Prepare new pack + if (inputNeuronInitIndex % 64 == 0) + { + extract64Bits(miningData.inputNeuronRandomNumber[inputNeuronInitIndex / 64], neuronArray); + } + char neuronValue = neuronArray[inputNeuronInitIndex % 64]; + + // Convert value of neuron to trits (keeping 1 as 1, and changing 0 to -1.). + neurons[i] = (neuronValue == 0) ? -1 : neuronValue; + + inputNeuronInitIndex++; + } + } + } + + void initNeuronValue() + { + initInputNeuron(); + + // Starting value of output neuron is zero + Neuron* neurons = currentANN.neurons; + NeuronType* neuronTypes = currentANN.neuronTypes; + unsigned long long population = currentANN.population; + for (unsigned long long i = 0; i < population; ++i) + { + if (neuronTypes[i] == OUTPUT_NEURON_TYPE) + { + neurons[i] = 0; + } + } + } + + void initNeuronType() + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + NeuronType* neuronTypes = currentANN.neuronTypes; + InitValue* initValue = (InitValue*)paddingInitValue; + + // Randomly choose the positions of neurons types + for (unsigned long long i = 0; i < population; ++i) + { + neuronIndices[i] = i; + neuronTypes[i] = INPUT_NEURON_TYPE; + } + unsigned long long neuronCount = population; + for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) + { + unsigned long long outputNeuronIdx = initValue->outputNeuronPositions[i] % neuronCount; + + // Fill the neuron type + neuronTypes[neuronIndices[outputNeuronIdx]] = OUTPUT_NEURON_TYPE; + + // This index is used, copy the end of indices array to current position and decrease the number of picking neurons + neuronCount = neuronCount - 1; + neuronIndices[outputNeuronIdx] = neuronIndices[neuronCount]; + } + } + + void initExpectedOutputNeuron() + { + Neuron neuronArray[64] = { 0 }; + for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) + { + // Prepare new pack + if (i % 64 == 0) + { + extract64Bits(miningData.outputNeuronRandomNumber[i / 64], neuronArray); + } + char neuronValue = neuronArray[i % 64]; + // Convert value of neuron (keeping 1 as 1, and changing 0 to -1.). + outputNeuronExpectedValue[i] = (neuronValue == 0) ? -1 : neuronValue; + } + } + + void initializeRandom2( + const unsigned char* publicKey, + const unsigned char* nonce, + const unsigned char* pRandom2Pool) + { + copyMem(combined, publicKey, 32); + copyMem(combined + 32, nonce, 32); + KangarooTwelve(combined, 64, hash, 32); + + // Initalize with nonce and public key + { + random2(hash, pRandom2Pool, paddingInitValue, paddingInitValueSizeInBytes); + + copyMem((unsigned char*)&miningData, pRandom2Pool, sizeof(MiningData)); + } + + } + + unsigned int initializeANN() + { + currentANN.init(); + currentANN.population = numberOfNeurons; + bestANN.init(); + bestANN.population = numberOfNeurons; + + unsigned long long& population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + InitValue* initValue = (InitValue*)paddingInitValue; + + // Initialization + population = numberOfNeurons; + removalNeuronsCount = 0; + + // Synapse weight initialization + for (unsigned long long i = 0; i < (initNumberOfSynapses / 32); ++i) + { + const unsigned long long mask = 0b11; + for (int j = 0; j < 32; ++j) + { + int shiftVal = j * 2; + unsigned char extractValue = (unsigned char)((initValue->synapseWeight[i] >> shiftVal) & mask); + switch (extractValue) + { + case 2: synapses[32 * i + j] = -1; break; + case 3: synapses[32 * i + j] = 1; break; + default: synapses[32 * i + j] = 0; + } + } + } + + // Init the neuron type positions in ANN + initNeuronType(); + + // Init input neuron value and output neuron + initNeuronValue(); + + // Init expected output neuron + initExpectedOutputNeuron(); + + // Ticks simulation + runTickSimulation(); + + // Copy the state for rollback later + currentANN.copyDataTo(bestANN); + + // Compute R and roll back if neccessary + unsigned int R = computeNonMatchingOutput(); + + return R; + + } + + // Main function for mining + unsigned int computeScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* pRandom2Pool) + { + // Setup the random starting point + initializeRandom2(publicKey, nonce, pRandom2Pool); + + // Initialize + unsigned int bestR = initializeANN(); + + for (unsigned long long s = 0; s < numberOfMutations; ++s) + { + + // Do the mutation + mutate(s); + + // Exit if the number of population reaches the maximum allowed + if (currentANN.population >= populationThreshold) + { + break; + } + + // Ticks simulation + runTickSimulation(); + + // Compute R and roll back if neccessary + unsigned int R = computeNonMatchingOutput(); + if (R > bestR) + { + // Roll back + //copyMem(¤tANN, &bestANN, sizeof(ANN)); + bestANN.copyDataTo(currentANN); + } + else + { + bestR = R; + + // Better R. Save the state + //copyMem(&bestANN, ¤tANN, sizeof(ANN)); + currentANN.copyDataTo(bestANN); + } + + //ASSERT(bestANN.population <= populationThreshold); + } + + unsigned int score = numberOfOutputNeurons - bestR; + return score; + } + + // returns last computed output neurons, only returns non-zero neurons, + // non-zero neurons will be packed in bits until requestedSizeInBytes is hitted or no more output neurons + int getLastOutput(unsigned char* requestedOutput, int requestedSizeInBytes) + { + return extractLastOutput( + (const unsigned char*)bestANN.neurons, + bestANN.neuronTypes, + bestANN.population, + requestedOutput, + requestedSizeInBytes); + } + +}; + +} diff --git a/src/network_messages/special_command.h b/src/network_messages/special_command.h index 517754aeb..7eaa882ce 100644 --- a/src/network_messages/special_command.h +++ b/src/network_messages/special_command.h @@ -23,6 +23,8 @@ struct SpecialCommandSetSolutionThresholdRequestAndResponse unsigned long long everIncreasingNonceAndCommandType; unsigned int epoch; int threshold; + int algoType; + int padding; }; diff --git a/src/network_messages/system_info.h b/src/network_messages/system_info.h index 48c6933df..ce65eee11 100644 --- a/src/network_messages/system_info.h +++ b/src/network_messages/system_info.h @@ -46,7 +46,7 @@ struct RespondSystemInfo unsigned int targetTickVoteSignature; unsigned long long computorPacketSignature; - unsigned long long _reserve1; + unsigned long long solutionAdditionalThreshold; // solution threshold for additional mining algorithm unsigned long long _reserve2; unsigned long long _reserve3; unsigned long long _reserve4; diff --git a/src/public_settings.h b/src/public_settings.h index 7f073ca74..b1525d0bf 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -88,14 +88,24 @@ static unsigned short CUSTOM_MINING_CACHE_FILE_NAME[] = L"custom_mining_cache.?? static unsigned short CONTRACT_EXEC_FEES_ACC_FILE_NAME[] = L"contract_exec_fees_acc.???"; static unsigned short CONTRACT_EXEC_FEES_REC_FILE_NAME[] = L"contract_exec_fees_rec.???"; -static constexpr unsigned long long NUMBER_OF_INPUT_NEURONS = 512; // K -static constexpr unsigned long long NUMBER_OF_OUTPUT_NEURONS = 512; // L -static constexpr unsigned long long NUMBER_OF_TICKS = 1000; // N -static constexpr unsigned long long NUMBER_OF_NEIGHBORS = 728; // 2M. Must be divided by 2 -static constexpr unsigned long long NUMBER_OF_MUTATIONS = 150; -static constexpr unsigned long long POPULATION_THRESHOLD = NUMBER_OF_INPUT_NEURONS + NUMBER_OF_OUTPUT_NEURONS + NUMBER_OF_MUTATIONS; // P +static constexpr unsigned long long HYPERIDENTITY_NUMBER_OF_INPUT_NEURONS = 512; // K +static constexpr unsigned long long HYPERIDENTITY_NUMBER_OF_OUTPUT_NEURONS = 512; // L +static constexpr unsigned long long HYPERIDENTITY_NUMBER_OF_TICKS = 1000; // N +static constexpr unsigned long long HYPERIDENTITY_NUMBER_OF_NEIGHBORS = 728; // 2M. Must be divided by 2 +static constexpr unsigned long long HYPERIDENTITY_NUMBER_OF_MUTATIONS = 150; +static constexpr unsigned long long HYPERIDENTITY_POPULATION_THRESHOLD = HYPERIDENTITY_NUMBER_OF_INPUT_NEURONS + HYPERIDENTITY_NUMBER_OF_OUTPUT_NEURONS + HYPERIDENTITY_NUMBER_OF_MUTATIONS; // P +static constexpr unsigned int HYPERIDENTITY_SOLUTION_THRESHOLD_DEFAULT = 321; + +static constexpr unsigned long long ADDITION_NUMBER_OF_INPUT_NEURONS = 14; // K +static constexpr unsigned long long ADDITION_NUMBER_OF_OUTPUT_NEURONS = 8; // L +static constexpr unsigned long long ADDITION_NUMBER_OF_TICKS = 1000; // N +static constexpr unsigned long long ADDITION_NUMBER_OF_NEIGHBORS = 728; // 2M. Must be divided by 2 +static constexpr unsigned long long ADDITION_NUMBER_OF_MUTATIONS = 150; +static constexpr unsigned long long ADDITION_POPULATION_THRESHOLD = ADDITION_NUMBER_OF_INPUT_NEURONS + ADDITION_NUMBER_OF_OUTPUT_NEURONS + ADDITION_NUMBER_OF_MUTATIONS; // P +static constexpr unsigned int ADDITION_SOLUTION_THRESHOLD_DEFAULT = 87381; // 8 * ( 2^14 ) * 2 / 3 + static constexpr long long NEURON_VALUE_LIMIT = 1LL; -static constexpr unsigned int SOLUTION_THRESHOLD_DEFAULT = 321; + #define SOLUTION_SECURITY_DEPOSIT 1000000 diff --git a/src/qubic.cpp b/src/qubic.cpp index b148b224f..211923447 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -182,13 +182,6 @@ static int nContractProcessorIDs = 0; static int nSolutionProcessorIDs = 0; static ScoreFunction< - NUMBER_OF_INPUT_NEURONS, - NUMBER_OF_OUTPUT_NEURONS, - NUMBER_OF_TICKS, - NUMBER_OF_NEIGHBORS, - POPULATION_THRESHOLD, - NUMBER_OF_MUTATIONS, - SOLUTION_THRESHOLD_DEFAULT, NUMBER_OF_SOLUTION_PROCESSORS > * score = nullptr; static volatile char solutionsLock = 0; @@ -200,7 +193,7 @@ static m256i competitorPublicKeys[(NUMBER_OF_COMPUTORS - QUORUM) * 2]; static unsigned int competitorScores[(NUMBER_OF_COMPUTORS - QUORUM) * 2]; static bool competitorComputorStatuses[(NUMBER_OF_COMPUTORS - QUORUM) * 2]; static unsigned int minimumComputorScore = 0, minimumCandidateScore = 0; -static int solutionThreshold[MAX_NUMBER_EPOCH] = { -1 }; +static int solutionThreshold[MAX_NUMBER_EPOCH][score_engine::AlgoType::MaxAlgoCount]; static unsigned long long solutionTotalExecutionTicks = 0; static unsigned long long K12MeasurementsCount = 0; static unsigned long long K12MeasurementsSum = 0; @@ -695,10 +688,13 @@ static void processBroadcastMessage(const unsigned long long processorNumber, Re if (k == system.numberOfSolutions) { unsigned int solutionScore = (*score)(processorNumber, request->destinationPublicKey, solution_miningSeed, solution_nonce); - const int threshold = (system.epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[system.epoch] : SOLUTION_THRESHOLD_DEFAULT; + score_engine::AlgoType selectedAlgo = score_engine::getAlgoType(solution_nonce.m256i_u8); + const int threshold = (system.epoch < MAX_NUMBER_EPOCH) ? + solutionThreshold[system.epoch][selectedAlgo] + : score_engine::DEFAUL_SOLUTION_THRESHOLD[selectedAlgo]; if (system.numberOfSolutions < MAX_NUMBER_OF_SOLUTIONS - && score->isValidScore(solutionScore) - && score->isGoodScore(solutionScore, threshold)) + && score->isValidScore(solutionScore, selectedAlgo) + && score->isGoodScore(solutionScore, threshold, selectedAlgo)) { ACQUIRE(solutionsLock); @@ -1330,7 +1326,8 @@ static void processRequestSystemInfo(Peer* peer, RequestResponseHeader* header) respondedSystemInfo.numberOfTransactions = numberOfTransactions; respondedSystemInfo.randomMiningSeed = score->currentRandomSeed; - respondedSystemInfo.solutionThreshold = (system.epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[system.epoch] : SOLUTION_THRESHOLD_DEFAULT; + respondedSystemInfo.solutionThreshold = (system.epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[system.epoch][score_engine::AlgoType::HyperIdentity] : HYPERIDENTITY_SOLUTION_THRESHOLD_DEFAULT; + respondedSystemInfo.solutionAdditionalThreshold = (system.epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[system.epoch][score_engine::AlgoType::Addition] : ADDITION_SOLUTION_THRESHOLD_DEFAULT; respondedSystemInfo.totalSpectrumAmount = spectrumInfo.totalAmount; respondedSystemInfo.currentEntityBalanceDustThreshold = (dustThresholdBurnAll > dustThresholdBurnHalf) ? dustThresholdBurnAll : dustThresholdBurnHalf; @@ -1555,12 +1552,36 @@ static void processSpecialCommand(Peer* peer, RequestResponseHeader* header) // can only set future epoch if (_request->epoch > system.epoch && _request->epoch < MAX_NUMBER_EPOCH) { - solutionThreshold[_request->epoch] = _request->threshold; + if (_request->algoType == score_engine::AlgoType::HyperIdentity) + { + solutionThreshold[_request->epoch][score_engine::AlgoType::HyperIdentity] = _request->threshold; + } + else if (_request->algoType == score_engine::AlgoType::Addition) + { + solutionThreshold[_request->epoch][score_engine::AlgoType::Addition] = _request->threshold; + } + else // unknown algo, don't do anything + { + + } } SpecialCommandSetSolutionThresholdRequestAndResponse response; response.everIncreasingNonceAndCommandType = _request->everIncreasingNonceAndCommandType; response.epoch = _request->epoch; - response.threshold = (_request->epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[_request->epoch] : SOLUTION_THRESHOLD_DEFAULT; + response.algoType = _request->algoType; + if (_request->algoType == score_engine::AlgoType::HyperIdentity) + { + response.threshold = (_request->epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[_request->epoch][score_engine::AlgoType::HyperIdentity] : HYPERIDENTITY_SOLUTION_THRESHOLD_DEFAULT; + } + else if (_request->algoType == score_engine::AlgoType::Addition) + { + response.threshold = (_request->epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[_request->epoch][score_engine::AlgoType::Addition] : ADDITION_SOLUTION_THRESHOLD_DEFAULT; + } + else // unknown algo, respond with an invalid number + { + response.algoType = -1; + } + enqueueResponse(peer, sizeof(SpecialCommandSetSolutionThresholdRequestAndResponse), SpecialCommand::type(), header->dejavu(), &response); } break; @@ -2555,13 +2576,15 @@ static void processTickTransactionSolution(const MiningSolutionTransaction* tran minerSolutionFlags[flagIndex >> 6] |= (1ULL << (flagIndex & 63)); unsigned int solutionScore = (*::score)(processorNumber, transaction->sourcePublicKey, transaction->miningSeed, transaction->nonce); - if (score->isValidScore(solutionScore)) + score_engine::AlgoType selectedAlgo = score_engine::getAlgoType(transaction->nonce.m256i_u8); + if (score->isValidScore(solutionScore, selectedAlgo)) { resourceTestingDigest ^= solutionScore; KangarooTwelve(&resourceTestingDigest, sizeof(resourceTestingDigest), &resourceTestingDigest, sizeof(resourceTestingDigest)); - - const int threshold = (system.epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[system.epoch] : SOLUTION_THRESHOLD_DEFAULT; - if (score->isGoodScore(solutionScore, threshold)) + const int threshold = (system.epoch < MAX_NUMBER_EPOCH) ? + solutionThreshold[system.epoch][selectedAlgo] + : score_engine::DEFAUL_SOLUTION_THRESHOLD[selectedAlgo]; + if (score->isGoodScore(solutionScore, threshold, selectedAlgo)) { // Solution deposit return { @@ -3659,8 +3682,13 @@ static void beginEpoch() minimumComputorScore = 0; minimumCandidateScore = 0; - if (system.epoch < MAX_NUMBER_EPOCH && (solutionThreshold[system.epoch] <= 0 || solutionThreshold[system.epoch] > NUMBER_OF_OUTPUT_NEURONS)) { // invalid threshold - solutionThreshold[system.epoch] = SOLUTION_THRESHOLD_DEFAULT; + if (system.epoch < MAX_NUMBER_EPOCH && !score_engine::checkAlgoThreshold(solutionThreshold[system.epoch][score_engine::AlgoType::HyperIdentity], score_engine::AlgoType::HyperIdentity)) + { + solutionThreshold[system.epoch][score_engine::AlgoType::HyperIdentity] = HYPERIDENTITY_SOLUTION_THRESHOLD_DEFAULT; + } + if (system.epoch < MAX_NUMBER_EPOCH && !score_engine::checkAlgoThreshold(solutionThreshold[system.epoch][score_engine::AlgoType::Addition], score_engine::AlgoType::Addition)) + { + solutionThreshold[system.epoch][score_engine::AlgoType::Addition] = ADDITION_SOLUTION_THRESHOLD_DEFAULT; } system.latestOperatorNonce = 0; @@ -5614,7 +5642,7 @@ static bool initialize() } setMem(score_qpi, sizeof(*score_qpi), 0); - setMem(solutionThreshold, sizeof(int) * MAX_NUMBER_EPOCH, 0); + setMem(&solutionThreshold[0][0], sizeof(int) * MAX_NUMBER_EPOCH * score_engine::AlgoType::MaxAlgoCount, 0); if (!allocPoolWithErrorLog(L"minserSolutionFlag", NUMBER_OF_MINER_SOLUTION_FLAGS / 8, (void**)&minerSolutionFlags, __LINE__)) { return false; diff --git a/src/score.h b/src/score.h index 0921cfbf2..3e171185f 100644 --- a/src/score.h +++ b/src/score.h @@ -8,1492 +8,42 @@ static unsigned long long top_of_stack; #include "platform/profiling.h" #include "public_settings.h" #include "score_cache.h" +#include "mining/score_engine.h" -#if defined(_MSC_VER) - -#define popcnt32(x) static_cast(__popcnt(static_cast(x))) -#define popcnt64(x) static_cast(__popcnt64(static_cast(x))) - -#else - -#define popcnt32(x) __builtin_popcount (static_cast(x)) -#define popcnt64(x) __builtin_popcountll(static_cast(x)) - -#endif - - -////////// Scoring algorithm \\\\\\\\\\ - -constexpr unsigned char INPUT_NEURON_TYPE = 0; -constexpr unsigned char OUTPUT_NEURON_TYPE = 1; -constexpr unsigned char EVOLUTION_NEURON_TYPE = 2; - -#if !(defined (__AVX512F__) || defined(__AVX2__)) -static_assert(false, "Either AVX2 or AVX512 is required."); -#endif - -#if defined (__AVX512F__) -static constexpr int BATCH_SIZE = 64; -static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; -static inline int popcnt512(__m512i v) -{ - __m512i pc = _mm512_popcnt_epi64(v); - return (int)_mm512_reduce_add_epi64(pc); -} -#elif defined(__AVX2__) -static constexpr int BATCH_SIZE = 32; -static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; -static inline unsigned popcnt256(__m256i v) -{ - return popcnt64(_mm256_extract_epi64(v, 0)) + - popcnt64(_mm256_extract_epi64(v, 1)) + - popcnt64(_mm256_extract_epi64(v, 2)) + - popcnt64(_mm256_extract_epi64(v, 3)); -} - -#endif - -constexpr unsigned long long POOL_VEC_SIZE = (((1ULL << 32) + 64)) >> 3; // 2^32+64 bits ~ 512MB -constexpr unsigned long long POOL_VEC_PADDING_SIZE = (POOL_VEC_SIZE + 200 - 1) / 200 * 200; // padding for multiple of 200 -constexpr unsigned long long STATE_SIZE = 200; - -static void generateRandom2Pool(const unsigned char* miningSeed, unsigned char* state, unsigned char* pool) -{ - // same pool to be used by all computors/candidates and pool content changing each phase - copyMem(&state[0], miningSeed, 32); - setMem(&state[32], STATE_SIZE - 32, 0); - - for (unsigned int i = 0; i < POOL_VEC_PADDING_SIZE; i += STATE_SIZE) - { - KeccakP1600_Permute_12rounds(state); - copyMem(&pool[i], state, STATE_SIZE); - } -} - -static void random2( - unsigned char* seed, // 32 bytes - const unsigned char* pool, - unsigned char* output, - unsigned long long outputSizeInByte // must be divided by 64 -) -{ - ASSERT(outputSizeInByte % 64 == 0); - - unsigned long long segments = outputSizeInByte / 64; - unsigned int x[8] = { 0 }; - for (int i = 0; i < 8; i++) - { - x[i] = ((unsigned int*)seed)[i]; - } - - for (int j = 0; j < segments; j++) - { - // Each segment will have 8 elements. Each element have 8 bytes - for (int i = 0; i < 8; i++) - { - unsigned int base = (x[i] >> 3) >> 3; - unsigned int m = x[i] & 63; - - unsigned long long u64_0 = ((unsigned long long*)pool)[base]; - unsigned long long u64_1 = ((unsigned long long*)pool)[base + 1]; - - // Move 8 * 8 * j to the current segment. 8 * i to current 8 bytes element - if (m == 0) - { - // some compiler doesn't work with bit shift 64 - *((unsigned long long*) & output[j * 8 * 8 + i * 8]) = u64_0; - } - else - { - *((unsigned long long*) & output[j * 8 * 8 + i * 8]) = (u64_0 >> m) | (u64_1 << (64 - m)); - } - - // Increase the positions in the pool for each element. - x[i] = x[i] * 1664525 + 1013904223; // https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use - } - } - -} - -// Clamp the neuron value -template -static T clampNeuron(T val) -{ - if (val > NEURON_VALUE_LIMIT) - { - return NEURON_VALUE_LIMIT; - } - else if (val < -NEURON_VALUE_LIMIT) - { - return -NEURON_VALUE_LIMIT; - } - return val; -} - -static void extract64Bits(unsigned long long number, char* output) -{ - int count = 0; - for (int i = 0; i < 64; ++i) - { - output[i] = ((number >> i) & 1); - } -} - -static void setBitValue(unsigned char* data, unsigned long long bitIdx, unsigned char bitValue) -{ - // (data[bitIdx >> 3] & ~(1u << (bitIdx & 7u))). Set the bit at data[bitIdx >> 3] byte become zeros - // then set it with the bit value - data[bitIdx >> 3] = (data[bitIdx >> 3] & ~(1u << (bitIdx & 7u))) | - (bitValue << (bitIdx & 7u)); -} - -static unsigned char getBitValue(const unsigned char* data, unsigned long long bitIdx) -{ - // data[bitIdx >> 3]: get the byte idx - // (bitIdx & 7u) get the bit index in byte - // (data[bitIdx >> 3] >> (bitIdx & 7u)) move the required bits to the end - return ((data[bitIdx >> 3] >> (bitIdx & 7u)) & 1u); -} - -template -static void paddingDatabits( - unsigned char* data, - unsigned long long dataSizeInBits) -{ - const unsigned long long head = paddedSizeInBits; - const unsigned long long tail = dataSizeInBits - paddedSizeInBits; - - for (unsigned r = 0; r < paddedSizeInBits; ++r) - { - unsigned long long src1 = tail + r + paddedSizeInBits; - unsigned long long dst1 = (unsigned long long)r; - - unsigned char bit; - - // Copy the end to the head - bit = getBitValue(data, src1); - setBitValue(data, dst1, bit); - - // copy the head to the end - unsigned long long src2 = head + r; - unsigned long long dst2 = tail + 2 * paddedSizeInBits + r; - bit = getBitValue(data, src2); - setBitValue(data, dst2, bit); - } -} - -static void orShiftedMask64(unsigned long long* dst, unsigned int idx, unsigned int shift, unsigned long long mask) -{ - if (shift == 0) - { - dst[idx] |= mask; - } - else - { - dst[idx] |= mask << shift; - dst[idx + 1] |= mask >> (64 - shift); - } -} - -static void orShiftedMask32(unsigned int* dst, unsigned int idx, unsigned int shift, unsigned int mask) -{ - if (shift == 0) - { - dst[idx] |= mask; - } - else - { - dst[idx] |= mask << shift; - dst[idx + 1] |= mask >> (32 - shift); - } -} - -static void packNegPosWithPadding(const char* data, - unsigned long long dataSizeInBits, - unsigned long long paddedSizeInBits, - unsigned char* negMask, - unsigned char* posMask) -{ - const unsigned long long totalBits = dataSizeInBits + 2ULL * paddedSizeInBits; - const unsigned long long totalBytes = (totalBits + 8 - 1) >> 3; - setMem(negMask, totalBytes, 0); - setMem(posMask, totalBytes, 0); - -#if defined (__AVX512F__) - auto* neg64 = reinterpret_cast(negMask); - auto* pos64 = reinterpret_cast(posMask); - const __m512i vMinus1 = _mm512_set1_epi8(-1); - const __m512i vPlus1 = _mm512_set1_epi8(+1); - unsigned long long k = 0; - for (; k + BATCH_SIZE <= dataSizeInBits; k += BATCH_SIZE) - { - __m512i v = _mm512_loadu_si512(reinterpret_cast(data + k)); - __mmask64 mNeg = _mm512_cmpeq_epi8_mask(v, vMinus1); - __mmask64 mPos = _mm512_cmpeq_epi8_mask(v, vPlus1); - - // Start to fill data from the offset - unsigned long long bitPos = paddedSizeInBits + k; - unsigned int wordIdx = static_cast(bitPos >> 6); // /64 - unsigned int offset = static_cast(bitPos & 63); // %64 - orShiftedMask64(neg64, wordIdx, offset, static_cast(mNeg)); - orShiftedMask64(pos64, wordIdx, offset, static_cast(mPos)); - } -#else - auto* neg32 = reinterpret_cast(negMask); - auto* pos32 = reinterpret_cast(posMask); - const __m256i vMinus1 = _mm256_set1_epi8(-1); - const __m256i vPlus1 = _mm256_set1_epi8(+1); - unsigned long long k = 0; - for (; k + BATCH_SIZE <= dataSizeInBits; k += BATCH_SIZE) - { - __m256i v = _mm256_loadu_si256(reinterpret_cast(data + k)); - - // Compare for -1 and +1 - __m256i isNeg = _mm256_cmpeq_epi8(v, vMinus1); - __m256i isPos = _mm256_cmpeq_epi8(v, vPlus1); - - unsigned int mNeg = static_cast(_mm256_movemask_epi8(isNeg)); - unsigned int mPos = static_cast(_mm256_movemask_epi8(isPos)); - - // Start to fill data from the offset - unsigned long long bitPos = paddedSizeInBits + k; - unsigned int wordIdx = static_cast(bitPos >> 5); // / 32 - unsigned int offset = static_cast(bitPos & 31); // % 32 - - orShiftedMask32(neg32, wordIdx, offset, static_cast(mNeg)); - orShiftedMask32(pos32, wordIdx, offset, static_cast(mPos)); - - } -#endif - // Process the remained data - for (; k < dataSizeInBits; ++k) - { - char v = data[k]; - if (v == 0) continue; /* nothing to set */ - - unsigned long long bitPos = paddedSizeInBits + k; /* logical bit index */ - unsigned long long byteIdx = bitPos >> 3; /* byte containing it */ - unsigned shift = bitPos & 7U; /* bit %8 */ - - unsigned char mask = (unsigned char)(1U << shift); - - if (v == -1) - negMask[byteIdx] |= mask; - else /* v == +1 */ - posMask[byteIdx] |= mask; - } -} - -// Load 256/512 values start from a bit index into a m512 or m256 register -#if defined (__AVX512F__) -static inline __m512i load512Bits(const unsigned char* array, unsigned long long bitLocation) -{ - const unsigned long long byteIndex = bitLocation >> 3; // /8 - const unsigned int bitOffset = (unsigned)(bitLocation & 7ULL); // %8 - - __m512i v0 = _mm512_loadu_si512((const void*)(array + byteIndex)); - if (bitOffset == 0) - return v0; - - __m512i v1 = _mm512_loadu_si512((const void*)(array + byteIndex + 1)); - __m512i right = _mm512_srli_epi64(v0, bitOffset); // low part - __m512i left = _mm512_slli_epi64(v1, 8u - bitOffset); // carry bits - - return _mm512_or_si512(right, left); -} -#else -static inline __m256i load256Bits(const unsigned char* array, unsigned long long bitLocation) -{ - const unsigned long long byteIndex = bitLocation >> 3; - const int bitOffset = (int)(bitLocation & 7ULL); - - // Load a 256-bit (32-byte) vector starting at the byte index. - const __m256i v = _mm256_loadu_si256(reinterpret_cast(array + byteIndex)); - - if (bitOffset == 0) - { - return v; - } - - // Perform the right shift within each 64-bit lane. - const __m256i right_shifted = _mm256_srli_epi64(v, bitOffset); - - // Left-shift the +1 byte vector to align the carry bits. - const __m256i v_shifted_by_one_byte = _mm256_loadu_si256( - reinterpret_cast(array + byteIndex + 1) - ); - const __m256i left_shifted_carry = _mm256_slli_epi64(v_shifted_by_one_byte, 8 - bitOffset); - - // Combine the two parts with a bitwise OR to get the final result. - return _mm256_or_si256(right_shifted, left_shifted_carry); - -} -#endif - -template < - unsigned long long numberOfInputNeurons, // K - unsigned long long numberOfOutputNeurons,// L - unsigned long long numberOfTicks, // N - unsigned long long numberOfNeighbors, // 2M - unsigned long long populationThreshold, // P - unsigned long long numberOfMutations, // S - unsigned int solutionThreshold, - unsigned long long solutionBufferCount -> +template struct ScoreFunction { - static constexpr unsigned long long numberOfNeurons = numberOfInputNeurons + numberOfOutputNeurons; - static constexpr unsigned long long maxNumberOfNeurons = populationThreshold; - static constexpr unsigned long long maxNumberOfSynapses = populationThreshold * numberOfNeighbors; - static constexpr unsigned long long initNumberOfSynapses = numberOfNeurons * numberOfNeighbors; - static constexpr long long radius = (long long)numberOfNeighbors / 2; - static constexpr long long paddingNeuronsCount = (maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE; - static constexpr unsigned long long incommingSynapsesPitch = ((numberOfNeighbors + 1) + BATCH_SIZE_X8 - 1) / BATCH_SIZE_X8 * BATCH_SIZE_X8; - static constexpr unsigned long long incommingSynapseBatchSize = incommingSynapsesPitch >> 3; - - static_assert(numberOfInputNeurons % 64 == 0, "numberOfInputNeurons must be divided by 64"); - static_assert(numberOfOutputNeurons % 64 == 0, "numberOfOutputNeurons must be divided by 64"); - static_assert(maxNumberOfSynapses <= (0xFFFFFFFFFFFFFFFF << 1ULL), "maxNumberOfSynapses must less than or equal MAX_UINT64/2"); - static_assert(initNumberOfSynapses % 32 == 0, "initNumberOfSynapses must be divided by 32"); - static_assert(numberOfNeighbors % 2 == 0, "numberOfNeighbors must be divided by 2"); - static_assert(populationThreshold > numberOfNeurons, "populationThreshold must be greater than numberOfNeurons"); - static_assert(numberOfNeurons > numberOfNeighbors, "Number of neurons must be greater than the number of neighbors"); - static_assert(numberOfNeighbors < ((1ULL << 63) - 1), "numberOfNeighbors must be in long long range"); - static_assert(BATCH_SIZE_X8 % 8 == 0, "BATCH_SIZE must be be divided by 8"); - - // Intermediate data - struct InitValue - { - unsigned long long outputNeuronPositions[numberOfOutputNeurons]; - unsigned long long synapseWeight[initNumberOfSynapses / 32]; // each 64bits elements will decide value of 32 synapses - unsigned long long synpaseMutation[numberOfMutations]; - }; - - struct MiningData - { - unsigned long long inputNeuronRandomNumber[numberOfInputNeurons / 64]; // each bit will use for generate input neuron value - unsigned long long outputNeuronRandomNumber[numberOfOutputNeurons / 64]; // each bit will use for generate expected output neuron value - }; - static constexpr unsigned long long paddingInitValueSizeInBytes = (sizeof(InitValue) + 64 - 1) / 64 * 64; + score_engine::ScoreEngine< + score_engine::HyperIdentityParams< + HYPERIDENTITY_NUMBER_OF_INPUT_NEURONS, + HYPERIDENTITY_NUMBER_OF_OUTPUT_NEURONS, + HYPERIDENTITY_NUMBER_OF_TICKS, + HYPERIDENTITY_NUMBER_OF_NEIGHBORS, + HYPERIDENTITY_POPULATION_THRESHOLD, + HYPERIDENTITY_NUMBER_OF_MUTATIONS, + HYPERIDENTITY_SOLUTION_THRESHOLD_DEFAULT>, + + score_engine::AdditionParams< + ADDITION_NUMBER_OF_INPUT_NEURONS, + ADDITION_NUMBER_OF_OUTPUT_NEURONS, + ADDITION_NUMBER_OF_TICKS, + ADDITION_NUMBER_OF_NEIGHBORS, + ADDITION_POPULATION_THRESHOLD, + ADDITION_NUMBER_OF_MUTATIONS, + ADDITION_SOLUTION_THRESHOLD_DEFAULT> + > _computeBuffer[solutionBufferCount]; volatile char random2PoolLock; - unsigned char state[STATE_SIZE]; - unsigned char externalPoolVec[POOL_VEC_PADDING_SIZE]; - unsigned char poolVec[POOL_VEC_PADDING_SIZE]; + unsigned char state[score_engine::STATE_SIZE]; + unsigned char externalPoolVec[score_engine::POOL_VEC_PADDING_SIZE]; + unsigned char poolVec[score_engine::POOL_VEC_PADDING_SIZE]; void initPool(const unsigned char* miningSeed) { // Init random2 pool with mining seed - generateRandom2Pool(miningSeed, state, externalPoolVec); + score_engine::generateRandom2Pool(miningSeed, state, externalPoolVec); } - struct computeBuffer - { - typedef char Synapse; - typedef char Neuron; - typedef unsigned char NeuronType; - - - // Data for roll back - struct ANN - { - void init() - { - neurons = paddingNeurons + radius; - } - void prepareData() - { - // Padding start and end of neuron array - neurons = paddingNeurons + radius; - } - - void copyDataTo(ANN& rOther) - { - copyMem(rOther.neurons, neurons, population * sizeof(Neuron)); - copyMem(rOther.neuronTypes, neuronTypes, population * sizeof(NeuronType)); - copyMem(rOther.synapses, synapses, maxNumberOfSynapses * sizeof(Synapse)); - rOther.population = population; - } - - Neuron* neurons; - // Padding start and end of neurons so that we can reduce the condition checking - Neuron paddingNeurons[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE]; - NeuronType neuronTypes[(maxNumberOfNeurons + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE]; - Synapse synapses[maxNumberOfSynapses]; - - // Encoded data - unsigned char neuronPlus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE + BATCH_SIZE_X8]; - unsigned char neuronMinus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE + BATCH_SIZE_X8]; - - unsigned char nextNeuronPlus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE + BATCH_SIZE_X8]; - unsigned char nextneuronMinus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE + BATCH_SIZE_X8]; - - unsigned char synapsePlus1s[incommingSynapsesPitch * populationThreshold]; - unsigned char synapseMinus1s[incommingSynapsesPitch * populationThreshold]; - - - unsigned long long population; - }; - ANN bestANN; - ANN currentANN; - - // Intermediate data - unsigned char paddingInitValue[paddingInitValueSizeInBytes]; - MiningData miningData; - - unsigned long long neuronIndices[numberOfNeurons]; - Neuron previousNeuronValue[maxNumberOfNeurons]; - - Neuron outputNeuronExpectedValue[numberOfOutputNeurons]; - - Neuron neuronValueBuffer[maxNumberOfNeurons]; - unsigned char hash[32]; - unsigned char combined[64]; - - unsigned long long removalNeuronsCount; - - // Contain incomming synapse of neurons. The center one will be zeros - Synapse incommingSynapses[maxNumberOfNeurons * incommingSynapsesPitch]; - - // Padding to fix bytes for each row - Synapse paddingIncommingSynapses[populationThreshold * incommingSynapsesPitch]; - - unsigned char nextNeuronPlus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE]; - unsigned char nextNeuronMinus1s[(maxNumberOfNeurons + numberOfNeighbors + BATCH_SIZE - 1) / BATCH_SIZE * BATCH_SIZE]; - - void mutate(unsigned long long mutateStep) - { - // Mutation - unsigned long long population = currentANN.population; - unsigned long long synapseCount = population * numberOfNeighbors; - Synapse* synapses = currentANN.synapses; - InitValue* initValue = (InitValue*)paddingInitValue; - - // Randomly pick a synapse, randomly increase or decrease its weight by 1 or -1 - unsigned long long synapseMutation = initValue->synpaseMutation[mutateStep]; - unsigned long long synapseIdx = (synapseMutation >> 1) % synapseCount; - // Randomly increase or decrease its value - char weightChange = 0; - if ((synapseMutation & 1ULL) == 0) - { - weightChange = -1; - } - else - { - weightChange = 1; - } - - char newWeight = synapses[synapseIdx] + weightChange; - - // Valid weight. Update it - if (newWeight >= -1 && newWeight <= 1) - { - synapses[synapseIdx] = newWeight; - } - else // Invalid weight. Insert a neuron - { - // Insert the neuron - insertNeuron(synapseIdx); - } - - // Clean the ANN - while (scanRedundantNeurons() > 0) - { - cleanANN(); - } - } - - // Get the pointer to all outgoing synapse of a neurons - Synapse* getSynapses(unsigned long long neuronIndex) - { - return ¤tANN.synapses[neuronIndex * numberOfNeighbors]; - } - - // Circulate the neuron index - unsigned long long clampNeuronIndex(long long neuronIdx, long long value) - { - const long long population = (long long)currentANN.population; - long long nnIndex = neuronIdx + value; - - // Get the signed bit and decide if we should increase population - nnIndex += (population & (nnIndex >> 63)); - - // Subtract population if idx >= population - long long over = nnIndex - population; - nnIndex -= (population & ~(over >> 63)); - return (unsigned long long)nnIndex; - } - - // Remove a neuron and all synapses relate to it - void removeNeuron(unsigned long long neuronIdx) - { - // Scan all its neigbor to remove their outgoing synapse point to the neuron - for (long long neighborOffset = -(long long)numberOfNeighbors / 2; neighborOffset <= (long long)numberOfNeighbors / 2; neighborOffset++) - { - unsigned long long nnIdx = clampNeuronIndex(neuronIdx, neighborOffset); - Synapse* pNNSynapses = getSynapses(nnIdx); - - long long synapseIndexOfNN = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); - if (synapseIndexOfNN < 0) - { - continue; - } - - // The synapse array need to be shifted regard to the remove neuron - // Also neuron need to have 2M neighbors, the addtional synapse will be set as zero weight - // Case1 [S0 S1 S2 - SR S5 S6]. SR is removed, [S0 S1 S2 S5 S6 0] - // Case2 [S0 S1 SR - S3 S4 S5]. SR is removed, [0 S0 S1 S3 S4 S5] - if (synapseIndexOfNN >= numberOfNeighbors / 2) - { - for (long long k = synapseIndexOfNN; k < numberOfNeighbors - 1; ++k) - { - pNNSynapses[k] = pNNSynapses[k + 1]; - } - pNNSynapses[numberOfNeighbors - 1] = 0; - } - else - { - for (long long k = synapseIndexOfNN; k > 0; --k) - { - pNNSynapses[k] = pNNSynapses[k - 1]; - } - pNNSynapses[0] = 0; - } - } - - // Shift the synapse array and the neuron array, also reduce the current ANN population - currentANN.population--; - for (unsigned long long shiftIdx = neuronIdx; shiftIdx < currentANN.population; shiftIdx++) - { - currentANN.neurons[shiftIdx] = currentANN.neurons[shiftIdx + 1]; - currentANN.neuronTypes[shiftIdx] = currentANN.neuronTypes[shiftIdx + 1]; - - // Also shift the synapses - copyMem(getSynapses(shiftIdx), getSynapses(shiftIdx + 1), numberOfNeighbors * sizeof(Synapse)); - } - } - - unsigned long long getNeighborNeuronIndex(unsigned long long neuronIndex, unsigned long long neighborOffset) - { - unsigned long long nnIndex = 0; - if (neighborOffset < (numberOfNeighbors / 2)) - { - nnIndex = clampNeuronIndex(neuronIndex + neighborOffset, -(long long)numberOfNeighbors / 2); - } - else - { - nnIndex = clampNeuronIndex(neuronIndex + neighborOffset + 1, -(long long)numberOfNeighbors / 2); - } - return nnIndex; - } - - void updateSynapseOfInsertedNN(unsigned long long insertedNeuronIdx) - { - // The change of synapse only impact neuron in [originalNeuronIdx - numberOfNeighbors / 2 + 1, originalNeuronIdx + numberOfNeighbors / 2] - // In the new index, it will be [originalNeuronIdx + 1 - numberOfNeighbors / 2, originalNeuronIdx + 1 + numberOfNeighbors / 2] - // [N0 N1 N2 original inserted N4 N5 N6], M = 2. - for (long long delta = -(long long)numberOfNeighbors / 2; delta <= (long long)numberOfNeighbors / 2; ++delta) - { - // Only process the neigbors - if (delta == 0) - { - continue; - } - unsigned long long updatedNeuronIdx = clampNeuronIndex(insertedNeuronIdx, delta); - - // Generate a list of neighbor index of current updated neuron NN - // Find the location of the inserted neuron in the list of neighbors - long long insertedNeuronIdxInNeigborList = -1; - for (long long k = 0; k < numberOfNeighbors; k++) - { - unsigned long long nnIndex = getNeighborNeuronIndex(updatedNeuronIdx, k); - if (nnIndex == insertedNeuronIdx) - { - insertedNeuronIdxInNeigborList = k; - } - } - - ASSERT(insertedNeuronIdxInNeigborList >= 0); - - Synapse* pUpdatedSynapses = getSynapses(updatedNeuronIdx); - // [N0 N1 N2 original inserted N4 N5 N6], M = 2. - // Case: neurons in range [N0 N1 N2 original], right synapses will be affected - if (delta < 0) - { - // Left side is kept as it is, only need to shift to the right side - for (long long k = numberOfNeighbors - 1; k >= insertedNeuronIdxInNeigborList; --k) - { - // Updated synapse - pUpdatedSynapses[k] = pUpdatedSynapses[k - 1]; - } - - // Incomming synapse from original neuron -> inserted neuron must be zero - if (delta == -1) - { - pUpdatedSynapses[insertedNeuronIdxInNeigborList] = 0; - } - } - else // Case: neurons in range [inserted N4 N5 N6], left synapses will be affected - { - // Right side is kept as it is, only need to shift to the left side - for (long long k = 0; k < insertedNeuronIdxInNeigborList; ++k) - { - // Updated synapse - pUpdatedSynapses[k] = pUpdatedSynapses[k + 1]; - } - } - - } - } - - void insertNeuron(unsigned long long synapseIdx) - { - // A synapse have incomingNeighbor and outgoingNeuron, direction incomingNeuron -> outgoingNeuron - unsigned long long incomingNeighborSynapseIdx = synapseIdx % numberOfNeighbors; - unsigned long long outgoingNeuron = synapseIdx / numberOfNeighbors; - - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - NeuronType* neuronTypes = currentANN.neuronTypes; - unsigned long long& population = currentANN.population; - - // Copy original neuron to the inserted one and set it as EVOLUTION_NEURON_TYPE type - Neuron insertNeuron = neurons[outgoingNeuron]; - unsigned long long insertedNeuronIdx = outgoingNeuron + 1; - - Synapse originalWeight = synapses[synapseIdx]; - - // Insert the neuron into array, population increased one, all neurons next to original one need to shift right - for (unsigned long long i = population; i > outgoingNeuron; --i) - { - neurons[i] = neurons[i - 1]; - neuronTypes[i] = neuronTypes[i - 1]; - - // Also shift the synapses to the right - copyMem(getSynapses(i), getSynapses(i - 1), numberOfNeighbors * sizeof(Synapse)); - } - neurons[insertedNeuronIdx] = insertNeuron; - neuronTypes[insertedNeuronIdx] = EVOLUTION_NEURON_TYPE; - population++; - - // Try to update the synapse of inserted neuron. All outgoing synapse is init as zero weight - Synapse* pInsertNeuronSynapse = getSynapses(insertedNeuronIdx); - for (unsigned long long synIdx = 0; synIdx < numberOfNeighbors; ++synIdx) - { - pInsertNeuronSynapse[synIdx] = 0; - } - - // Copy the outgoing synapse of original neuron - // Outgoing points to the left - if (incomingNeighborSynapseIdx < numberOfNeighbors / 2) - { - if (incomingNeighborSynapseIdx > 0) - { - // Decrease by one because the new neuron is next to the original one - pInsertNeuronSynapse[incomingNeighborSynapseIdx - 1] = originalWeight; - } - // Incase of the outgoing synapse point too far, don't add the synapse - } - else - { - // No need to adjust the added neuron but need to remove the synapse of the original neuron - pInsertNeuronSynapse[incomingNeighborSynapseIdx] = originalWeight; - } - - updateSynapseOfInsertedNN(insertedNeuronIdx); - } - - long long getIndexInSynapsesBuffer(unsigned long long neuronIdx, long long neighborOffset) - { - // Skip the case neuron point to itself and too far neighbor - if (neighborOffset == 0 - || neighborOffset < -(long long)numberOfNeighbors / 2 - || neighborOffset >(long long)numberOfNeighbors / 2) - { - return -1; - } - - long long synapseIdx = (long long)numberOfNeighbors / 2 + neighborOffset; - if (neighborOffset >= 0) - { - synapseIdx = synapseIdx - 1; - } - - return synapseIdx; - } - - bool isAllOutgoingSynapsesZeros(unsigned long long neuronIdx) - { - Synapse* synapse = getSynapses(neuronIdx); - for (unsigned long long n = 0; n < numberOfNeighbors; n++) - { - Synapse synapseW = synapse[n]; - if (synapseW != 0) - { - return false; - } - } - return true; - } - - bool isAllIncomingSynapsesZeros(unsigned long long neuronIdx) - { - // Loop through the neighbor neurons to check all incoming synapses - for (long long neighborOffset = -(long long)numberOfNeighbors / 2; neighborOffset <= (long long)numberOfNeighbors / 2; neighborOffset++) - { - unsigned long long nnIdx = clampNeuronIndex(neuronIdx, neighborOffset); - Synapse* nnSynapses = getSynapses(nnIdx); - - long long synapseIdx = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); - if (synapseIdx < 0) - { - continue; - } - Synapse synapseW = nnSynapses[synapseIdx]; - - if (synapseW != 0) - { - return false; - } - } - return true; - } - - // Check which neurons/synapse need to be removed after mutation - unsigned long long scanRedundantNeurons() - { - unsigned long long population = currentANN.population; - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - NeuronType* neuronTypes = currentANN.neuronTypes; - - removalNeuronsCount = 0; - // After each mutation, we must verify if there are neurons that do not affect the ANN output. - // These are neurons that either have all incoming synapse weights as 0, - // or all outgoing synapse weights as 0. Such neurons must be removed. - for (unsigned long long i = 0; i < population; i++) - { - if (neuronTypes[i] == EVOLUTION_NEURON_TYPE) - { - if (isAllOutgoingSynapsesZeros(i) || isAllIncomingSynapsesZeros(i)) - { - neuronIndices[removalNeuronsCount] = i; - removalNeuronsCount++; - } - } - } - return removalNeuronsCount; - } - - // Remove neurons and synapses that do not affect the ANN - void cleanANN() - { - // Scan and remove neurons/synapses - for (unsigned long long i = 0; i < removalNeuronsCount; i++) - { - unsigned long long neuronIdx = neuronIndices[i]; - // Remove it from the neuron list. Overwrite data - // Remove its synapses in the synapses array - removeNeuron(neuronIdx); - } - removalNeuronsCount = 0; - } - - void processTick() - { - unsigned long long population = currentANN.population; - - unsigned char* pPaddingNeuronMinus = currentANN.neuronMinus1s; - unsigned char* pPaddingNeuronPlus = currentANN.neuronPlus1s; - - unsigned char* pPaddingSynapseMinus = currentANN.synapseMinus1s; - unsigned char* pPaddingSynapsePlus = currentANN.synapsePlus1s; - - paddingDatabits(pPaddingNeuronMinus, population); - paddingDatabits(pPaddingNeuronPlus, population); - - -#if defined (__AVX512F__) - constexpr unsigned long long chunks = incommingSynapsesPitch >> 9; - __m512i minusBlock[chunks]; - __m512i minusNext[chunks]; - __m512i plusBlock[chunks]; - __m512i plusNext[chunks]; - - constexpr unsigned long long blockSizeNeurons = 64ULL; - constexpr unsigned long long bytesPerWord = 8ULL; - - unsigned long long n = 0; - const unsigned long long lastBlock = (population / blockSizeNeurons) * blockSizeNeurons; - for (; n < lastBlock; n += blockSizeNeurons) - { - // byteIndex = start byte for word containing neuron n - unsigned long long byteIndex = ((n >> 6) << 3); // (n / 64) * 8 - unsigned long long curIdx = byteIndex; - unsigned long long nextIdx = byteIndex + bytesPerWord; // +8 bytes - - // Load the neuron windows once per block for all chunks - unsigned long long loadCur = curIdx; - unsigned long long loadNext = nextIdx; - for (unsigned blk = 0; blk < chunks; ++blk, loadCur += BATCH_SIZE, loadNext += BATCH_SIZE) - { - plusBlock[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + loadCur)); - plusNext[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + loadNext)); - minusBlock[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + loadCur)); - minusNext[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + loadNext)); - } - - __m512i sh = _mm512_setzero_si512(); - __m512i sh64 = _mm512_set1_epi64(64); - const __m512i ones512 = _mm512_set1_epi64(1); - - // For each neuron inside this 64-neuron block - for (unsigned int lane = 0; lane < 64; ++lane) - { - const unsigned long long current_n = n + lane; - // synapse pointers for this neuron - unsigned char* pSynapsePlus = pPaddingSynapsePlus + current_n * incommingSynapseBatchSize; - unsigned char* pSynapseMinus = pPaddingSynapseMinus + current_n * incommingSynapseBatchSize; - - __m512i plusPopulation = _mm512_setzero_si512(); - __m512i minusPopulation = _mm512_setzero_si512(); - - for (unsigned blk = 0; blk < chunks; ++blk) - { - const __m512i synP = _mm512_loadu_si512((const void*)(pSynapsePlus + blk * BATCH_SIZE)); - const __m512i synM = _mm512_loadu_si512((const void*)(pSynapseMinus + blk * BATCH_SIZE)); - - // stitch 64-bit lanes: cur >> s | next << (64 - s) - __m512i neuronPlus = _mm512_or_si512(_mm512_srlv_epi64(plusBlock[blk], sh), _mm512_sllv_epi64(plusNext[blk], sh64)); - __m512i neuronMinus = _mm512_or_si512(_mm512_srlv_epi64(minusBlock[blk], sh), _mm512_sllv_epi64(minusNext[blk], sh64)); - - __m512i tmpP = _mm512_and_si512(neuronMinus, synM); - const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synP, tmpP, 234); - - __m512i tmpM = _mm512_and_si512(neuronMinus, synP); - const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synM, tmpM, 234); - - plusPopulation = _mm512_add_epi64(plusPopulation, _mm512_popcnt_epi64(plus)); - minusPopulation = _mm512_add_epi64(minusPopulation, _mm512_popcnt_epi64(minus)); - } - sh = _mm512_add_epi64(sh, ones512); - sh64 = _mm512_sub_epi64(sh64, ones512); - - // Reduce to scalar and compute neuron value - int score = (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); - char neuronValue = (score > 0) - (score < 0); - neuronValueBuffer[current_n] = neuronValue; - - // Update the neuron positive and negative bitmaps - unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; - unsigned char nNextPos = neuronValue > 0 ? 1 : 0; - setBitValue(currentANN.nextneuronMinus1s, current_n + radius, nNextNeg); - setBitValue(currentANN.nextNeuronPlus1s, current_n + radius, nNextPos); - } - } - - for (; n < population; ++n) - { - char neuronValue = 0; - int score = 0; - unsigned char* pSynapsePlus = pPaddingSynapsePlus + n * incommingSynapseBatchSize; - unsigned char* pSynapseMinus = pPaddingSynapseMinus + n * incommingSynapseBatchSize; - - const unsigned long long byteIndex = n >> 3; - const unsigned int bitOffset = (n & 7U); - const unsigned int bitOffset_8 = (8u - bitOffset); - __m512i sh = _mm512_set1_epi64((long long)bitOffset); - __m512i sh8 = _mm512_set1_epi64((long long)bitOffset_8); - - __m512i plusPopulation = _mm512_setzero_si512(); - __m512i minusPopulation = _mm512_setzero_si512(); - - for (unsigned blk = 0; blk < chunks; ++blk, pSynapsePlus += BATCH_SIZE, pSynapseMinus += BATCH_SIZE) - { - const __m512i synapsePlus = _mm512_loadu_si512((const void*)(pSynapsePlus)); - const __m512i synapseMinus = _mm512_loadu_si512((const void*)(pSynapseMinus)); - - __m512i neuronPlus = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + byteIndex + blk * BATCH_SIZE)); - __m512i neuronPlusNext = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + byteIndex + blk * BATCH_SIZE + 1)); - __m512i neuronMinus = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + byteIndex + blk * BATCH_SIZE)); - __m512i neuronMinusNext = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + byteIndex + blk * BATCH_SIZE + 1)); - - neuronPlus = _mm512_or_si512(_mm512_srlv_epi64(neuronPlus, sh), _mm512_sllv_epi64(neuronPlusNext, sh8)); - neuronMinus = _mm512_or_si512(_mm512_srlv_epi64(neuronMinus, sh), _mm512_sllv_epi64(neuronMinusNext, sh8)); - - __m512i tempP = _mm512_and_si512(neuronMinus, synapseMinus); - const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synapsePlus, tempP, 234); - - __m512i tempM = _mm512_and_si512(neuronMinus, synapsePlus); - const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synapseMinus, tempM, 234); - - tempP = _mm512_popcnt_epi64(plus); - tempM = _mm512_popcnt_epi64(minus); - plusPopulation = _mm512_add_epi64(tempP, plusPopulation); - minusPopulation = _mm512_add_epi64(tempM, minusPopulation); - } - - score = (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); - neuronValue = (score > 0) - (score < 0); - neuronValueBuffer[n] = neuronValue; - - unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; - unsigned char nNextPos = neuronValue > 0 ? 1 : 0; - setBitValue(currentANN.nextneuronMinus1s, n + radius, nNextNeg); - setBitValue(currentANN.nextNeuronPlus1s, n + radius, nNextPos); - } -#else - constexpr unsigned long long chunks = incommingSynapsesPitch >> 8; - for (unsigned long long n = 0; n < population; ++n, pPaddingSynapsePlus += incommingSynapseBatchSize, pPaddingSynapseMinus += incommingSynapseBatchSize) - { - char neuronValue = 0; - int score = 0; - unsigned char* pSynapsePlus = pPaddingSynapsePlus; - unsigned char* pSynapseMinus = pPaddingSynapseMinus; - - int synapseBlkIdx = 0; // blk index of synapse - int neuronBlkIdx = 0; - for (unsigned blk = 0; blk < chunks; ++blk, synapseBlkIdx += BATCH_SIZE, neuronBlkIdx += BATCH_SIZE_X8) - { - // Process 256bits at once, neigbor shilf 64 bytes = 256 bits - const __m256i synapsePlus = _mm256_loadu_si256((const __m256i*)(pPaddingSynapsePlus + synapseBlkIdx)); - const __m256i synapseMinus = _mm256_loadu_si256((const __m256i*)(pPaddingSynapseMinus + synapseBlkIdx)); - - __m256i neuronPlus = load256Bits(pPaddingNeuronPlus, n + neuronBlkIdx); - __m256i neuronMinus = load256Bits(pPaddingNeuronMinus, n + neuronBlkIdx); - - // Compare the negative and possitive parts - __m256i plus = _mm256_or_si256(_mm256_and_si256(neuronPlus, synapsePlus), - _mm256_and_si256(neuronMinus, synapseMinus)); - __m256i minus = _mm256_or_si256(_mm256_and_si256(neuronPlus, synapseMinus), - _mm256_and_si256(neuronMinus, synapsePlus)); - - score += popcnt256(plus) - popcnt256(minus); - } - - neuronValue = (score > 0) - (score < 0); - neuronValueBuffer[n] = neuronValue; - - // Update the neuron positive and negative - unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; - unsigned char nNextPos = neuronValue > 0 ? 1 : 0; - setBitValue(currentANN.nextneuronMinus1s, n + radius, nNextNeg); - setBitValue(currentANN.nextNeuronPlus1s, n + radius, nNextPos); - } -#endif - - - - copyMem(currentANN.neurons, neuronValueBuffer, population * sizeof(Neuron)); - copyMem(currentANN.neuronMinus1s, currentANN.nextneuronMinus1s, sizeof(currentANN.neuronMinus1s)); - copyMem(currentANN.neuronPlus1s, currentANN.nextNeuronPlus1s, sizeof(currentANN.neuronPlus1s)); - } - - void runTickSimulation() - { - unsigned long long population = currentANN.population; - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - NeuronType* neuronTypes = currentANN.neuronTypes; - - // Save the neuron value for comparison - copyMem(previousNeuronValue, neurons, population * sizeof(Neuron)); - { - //PROFILE_NAMED_SCOPE("convertSynapse"); - // Compute the incomming synapse of each neurons - setMem(paddingIncommingSynapses, sizeof(paddingIncommingSynapses), 0); - for (unsigned long long n = 0; n < population; ++n) - { - const Synapse* kSynapses = getSynapses(n); - // Scan through all neighbor neurons and sum all connected neurons. - // The synapses are arranged as neuronIndex * numberOfNeighbors - for (long long m = 0; m < radius; m++) - { - Synapse synapseWeight = kSynapses[m]; - unsigned long long nnIndex = clampNeuronIndex(n + m, -radius); - paddingIncommingSynapses[nnIndex * incommingSynapsesPitch + (numberOfNeighbors - m)] = synapseWeight; - } - - //paddingIncommingSynapses[n * incommingSynapsesPitch + radius] = 0; - - for (long long m = radius; m < numberOfNeighbors; m++) - { - Synapse synapseWeight = kSynapses[m]; - unsigned long long nnIndex = clampNeuronIndex(n + m + 1, -radius); - paddingIncommingSynapses[nnIndex * incommingSynapsesPitch + (numberOfNeighbors - m - 1)] = synapseWeight; - } - } - } - - // Prepare masks - { - //PROFILE_NAMED_SCOPE("prepareMask"); - packNegPosWithPadding(currentANN.neurons, - population, - radius, - currentANN.neuronMinus1s, - currentANN.neuronPlus1s); - - packNegPosWithPadding(paddingIncommingSynapses, - incommingSynapsesPitch * population, - 0, - currentANN.synapseMinus1s, - currentANN.synapsePlus1s); - } - - { - //PROFILE_NAMED_SCOPE("processTickLoop"); - for (unsigned long long tick = 0; tick < numberOfTicks; ++tick) - { - processTick(); - // Check exit conditions: - // - N ticks have passed (already in for loop) - // - All neuron values are unchanged - // - All output neurons have non-zero values - - if (areAllNeuronsUnchanged((const char*)previousNeuronValue, (const char*)neurons, population) - || areAllNeuronsZeros((const char*)neurons, (const char*)neuronTypes, population)) - { - break; - } - - // Copy the neuron value - copyMem(previousNeuronValue, neurons, population * sizeof(Neuron)); - } - } - } - - bool areAllNeuronsZeros( - const char* neurons, - const char* neuronTypes, - unsigned long long population) - { - -#if defined (__AVX512F__) - const __m512i zero = _mm512_setzero_si512(); - const __m512i typeOutput = _mm512_set1_epi8(OUTPUT_NEURON_TYPE); - - unsigned long long i = 0; - for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) - { - __m512i cur = _mm512_loadu_si512((const void*)(neurons + i)); - __m512i types = _mm512_loadu_si512((const void*)(neuronTypes + i)); - - __mmask64 type_mask = _mm512_cmpeq_epi8_mask(types, typeOutput); - __mmask64 zero_mask = _mm512_cmpeq_epi8_mask(cur, zero); - - if (type_mask & zero_mask) - return false; - } -#else - const __m256i zero = _mm256_setzero_si256(); - const __m256i typeOutput = _mm256_set1_epi8(OUTPUT_NEURON_TYPE); - - unsigned long long i = 0; - for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) - { - __m256i cur = _mm256_loadu_si256((const __m256i*)(neurons + i)); - __m256i types = _mm256_loadu_si256((const __m256i*)(neuronTypes + i)); - - // Compare for type == OUTPUT - __m256i type_cmp = _mm256_cmpeq_epi8(types, typeOutput); - int type_mask = _mm256_movemask_epi8(type_cmp); - - // Compare for neuron == 0 - __m256i zero_cmp = _mm256_cmpeq_epi8(cur, zero); - int zero_mask = _mm256_movemask_epi8(zero_cmp); - - // If both masks overlap → some output neuron is zero - if (type_mask & zero_mask) - { - return false; - } - } - -#endif - for (; i < population; i++) - { - // Neuron unchanged check - if (neuronTypes[i] == OUTPUT_NEURON_TYPE && neurons[i] == 0) - { - return false; - } - } - - return true; - } - - bool areAllNeuronsUnchanged( - const char* previousNeuronValue, - const char* neurons, - unsigned long long population) - { - unsigned long long i = 0; - for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) - { - -#if defined (__AVX512F__) - __m512i prev = _mm512_loadu_si512((const void*)(previousNeuronValue + i)); - __m512i cur = _mm512_loadu_si512((const void*)(neurons + i)); - - __mmask64 neq_mask = _mm512_cmpneq_epi8_mask(prev, cur); - if (neq_mask) - { - return false; - } -#else - __m256i v_prev = _mm256_loadu_si256((const __m256i*)(previousNeuronValue + i)); - __m256i v_curr = _mm256_loadu_si256((const __m256i*)(neurons + i)); - __m256i cmp = _mm256_cmpeq_epi8(v_prev, v_curr); - - int mask = _mm256_movemask_epi8(cmp); - - // -1 means all bytes equal - if (mask != -1) - { - return false; - } -#endif - } - - for (; i < population; i++) - { - // Neuron unchanged check - if (previousNeuronValue[i] != neurons[i]) - { - return false; - } - } - - return true; - } - - unsigned int computeNonMatchingOutput() - { - unsigned long long population = currentANN.population; - Neuron* neurons = currentANN.neurons; - NeuronType* neuronTypes = currentANN.neuronTypes; - - // Compute the non-matching value R between output neuron value and initial value - // Because the output neuron order never changes, the order is preserved - unsigned int R = 0; - unsigned long long outputIdx = 0; - unsigned long long i = 0; -#if defined (__AVX512F__) - const __m512i typeOutputAVX = _mm512_set1_epi8(OUTPUT_NEURON_TYPE); - for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) - { - // Load 64 neuron types and compare with OUTPUT_NEURON_TYPE - __m512i types = _mm512_loadu_si512((const void*)(neuronTypes + i)); - __mmask64 type_mask = _mm512_cmpeq_epi8_mask(types, typeOutputAVX); - - if (type_mask == 0) - { - continue; // no output neurons in this 64-wide block, just skip - } - - // Output neuron existed in this block - for (int k = 0; k < BATCH_SIZE; ++k) - { - if (type_mask & (1ULL << k)) - { - char neuronVal = neurons[i + k]; - if (neuronVal != outputNeuronExpectedValue[outputIdx]) - { - R++; - } - outputIdx++; - } - } - } -#else - const __m256i typeOutputAVX = _mm256_set1_epi8(OUTPUT_NEURON_TYPE); - for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) - { - __m256i types_vec = _mm256_loadu_si256((const __m256i*)(neuronTypes + i)); - __m256i cmp_vec = _mm256_cmpeq_epi8(types_vec, typeOutputAVX); - unsigned int type_mask = _mm256_movemask_epi8(cmp_vec); - - if (type_mask == 0) - { - continue; // no output neurons in this 32-wide block, just skip - } - for (int k = 0; k < BATCH_SIZE; ++k) - { - if (type_mask & (1U << k)) - { - char neuronVal = neurons[i + k]; - if (neuronVal != outputNeuronExpectedValue[outputIdx]) - R++; - outputIdx++; - } - } - } -#endif - - // remainder loop - for (; i < population; i++) - { - if (neuronTypes[i] == OUTPUT_NEURON_TYPE) - { - if (neurons[i] != outputNeuronExpectedValue[outputIdx]) - { - R++; - } - outputIdx++; - } - } - - return R; - } - - void initInputNeuron() - { - unsigned long long population = currentANN.population; - Neuron* neurons = currentANN.neurons; - NeuronType* neuronTypes = currentANN.neuronTypes; - Neuron neuronArray[64] = { 0 }; - unsigned long long inputNeuronInitIndex = 0; - for (unsigned long long i = 0; i < population; ++i) - { - // Input will use the init value - if (neuronTypes[i] == INPUT_NEURON_TYPE) - { - // Prepare new pack - if (inputNeuronInitIndex % 64 == 0) - { - extract64Bits(miningData.inputNeuronRandomNumber[inputNeuronInitIndex / 64], neuronArray); - } - char neuronValue = neuronArray[inputNeuronInitIndex % 64]; - - // Convert value of neuron to trits (keeping 1 as 1, and changing 0 to -1.). - neurons[i] = (neuronValue == 0) ? -1 : neuronValue; - - inputNeuronInitIndex++; - } - } - } - - void initNeuronValue() - { - initInputNeuron(); - - // Starting value of output neuron is zero - Neuron* neurons = currentANN.neurons; - NeuronType* neuronTypes = currentANN.neuronTypes; - unsigned long long population = currentANN.population; - for (unsigned long long i = 0; i < population; ++i) - { - if (neuronTypes[i] == OUTPUT_NEURON_TYPE) - { - neurons[i] = 0; - } - } - } - - void initNeuronType() - { - unsigned long long population = currentANN.population; - Neuron* neurons = currentANN.neurons; - NeuronType* neuronTypes = currentANN.neuronTypes; - InitValue* initValue = (InitValue*)paddingInitValue; - - // Randomly choose the positions of neurons types - for (unsigned long long i = 0; i < population; ++i) - { - neuronIndices[i] = i; - neuronTypes[i] = INPUT_NEURON_TYPE; - } - unsigned long long neuronCount = population; - for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) - { - unsigned long long outputNeuronIdx = initValue->outputNeuronPositions[i] % neuronCount; - - // Fill the neuron type - neuronTypes[neuronIndices[outputNeuronIdx]] = OUTPUT_NEURON_TYPE; - - // This index is used, copy the end of indices array to current position and decrease the number of picking neurons - neuronCount = neuronCount - 1; - neuronIndices[outputNeuronIdx] = neuronIndices[neuronCount]; - } - } - - void initExpectedOutputNeuron() - { - Neuron neuronArray[64] = { 0 }; - for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) - { - // Prepare new pack - if (i % 64 == 0) - { - extract64Bits(miningData.outputNeuronRandomNumber[i / 64], neuronArray); - } - char neuronValue = neuronArray[i % 64]; - // Convert value of neuron (keeping 1 as 1, and changing 0 to -1.). - outputNeuronExpectedValue[i] = (neuronValue == 0) ? -1 : neuronValue; - } - } - - void initializeRandom2( - const unsigned char* publicKey, - const unsigned char* nonce, - const unsigned char* pRandom2Pool) - { - copyMem(combined, publicKey, 32); - copyMem(combined + 32, nonce, 32); - KangarooTwelve(combined, 64, hash, 32); - - // Initalize with nonce and public key - { - random2(hash, pRandom2Pool, paddingInitValue, paddingInitValueSizeInBytes); - - copyMem((unsigned char*)&miningData, pRandom2Pool, sizeof(MiningData)); - } - - } - - unsigned int initializeANN() - { - currentANN.init(); - currentANN.population = numberOfNeurons; - bestANN.init(); - bestANN.population = numberOfNeurons; - - unsigned long long& population = currentANN.population; - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - InitValue* initValue = (InitValue*)paddingInitValue; - - // Initialization - population = numberOfNeurons; - removalNeuronsCount = 0; - - // Synapse weight initialization - for (unsigned long long i = 0; i < (initNumberOfSynapses / 32); ++i) - { - const unsigned long long mask = 0b11; - for (int j = 0; j < 32; ++j) - { - int shiftVal = j * 2; - unsigned char extractValue = (unsigned char)((initValue->synapseWeight[i] >> shiftVal) & mask); - switch (extractValue) - { - case 2: synapses[32 * i + j] = -1; break; - case 3: synapses[32 * i + j] = 1; break; - default: synapses[32 * i + j] = 0; - } - } - } - - // Init the neuron type positions in ANN - initNeuronType(); - - // Init input neuron value and output neuron - initNeuronValue(); - - // Init expected output neuron - initExpectedOutputNeuron(); - - // Ticks simulation - runTickSimulation(); - - // Copy the state for rollback later - currentANN.copyDataTo(bestANN); - - // Compute R and roll back if neccessary - unsigned int R = computeNonMatchingOutput(); - - return R; - - } - - // Main function for mining - unsigned int computeScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* pRandom2Pool) - { - // Setup the random starting point - initializeRandom2(publicKey, nonce, pRandom2Pool); - - // Initialize - unsigned int bestR = initializeANN(); - - for (unsigned long long s = 0; s < numberOfMutations; ++s) - { - - // Do the mutation - mutate(s); - - // Exit if the number of population reaches the maximum allowed - if (currentANN.population >= populationThreshold) - { - break; - } - - // Ticks simulation - runTickSimulation(); - - // Compute R and roll back if neccessary - unsigned int R = computeNonMatchingOutput(); - if (R > bestR) - { - // Roll back - //copyMem(¤tANN, &bestANN, sizeof(ANN)); - bestANN.copyDataTo(currentANN); - } - else - { - bestR = R; - - // Better R. Save the state - //copyMem(&bestANN, ¤tANN, sizeof(ANN)); - currentANN.copyDataTo(bestANN); - } - - //ASSERT(bestANN.population <= populationThreshold); - } - - unsigned int score = numberOfOutputNeurons - bestR; - return score; - } - - // returns last computed output neurons, only returns 256 non-zero neurons, neuron values are compressed to bit - m256i getLastOutput() - { - unsigned long long population = bestANN.population; - Neuron* neurons = bestANN.neurons; - NeuronType* neuronTypes = bestANN.neuronTypes; - int count = 0; - int byteCount = 0; - uint8_t A = 0; - m256i result; - result = m256i::zero(); - - for (unsigned long long i = 0; i < population; i++) - { - if (neuronTypes[i] == OUTPUT_NEURON_TYPE) - { - if (neurons[i]) - { - uint8_t v = (neurons[i] > 0); - v = v << (7 - count); - A |= v; - if (++count == 8) - { - result.m256i_u8[byteCount++] = A; - A = 0; - count = 0; - if (byteCount >= 32) - { - break; - } - } - } - } - } - - return result; - } - - } _computeBuffer[solutionBufferCount]; m256i currentRandomSeed; volatile char solutionEngineLock[solutionBufferCount]; @@ -1514,7 +64,7 @@ struct ScoreFunction currentRandomSeed = randomSeed; // persist the initial random seed to be able to send it back on system info response ACQUIRE(random2PoolLock); - copyMem(poolVec, externalPoolVec, POOL_VEC_PADDING_SIZE); + copyMem(poolVec, externalPoolVec, score_engine::POOL_VEC_PADDING_SIZE); RELEASE(random2PoolLock); } @@ -1533,6 +83,10 @@ struct ScoreFunction // Make sure all padding data is set as zeros setMem(_computeBuffer, sizeof(_computeBuffer), 0); + for (int i = 0; i < solutionBufferCount; i++) + { + _computeBuffer[i].initMemory(); + } for (int i = 0; i < solutionBufferCount; i++) { @@ -1575,13 +129,25 @@ struct ScoreFunction return success; } - bool isValidScore(unsigned int solutionScore) + bool isValidScore(unsigned int solutionScore, score_engine::AlgoType selectedAlgo) { - return (solutionScore >= 0 && solutionScore <= numberOfOutputNeurons); + if (selectedAlgo == score_engine::AlgoType::HyperIdentity) + { + return (solutionScore >= 0) + && (solutionScore <= HYPERIDENTITY_NUMBER_OF_OUTPUT_NEURONS) + && (solutionScore != score_engine::INVALID_SCORE_VALUE); + } + else if (selectedAlgo == score_engine::AlgoType::Addition) + { + return (solutionScore >= 0 ) + && (solutionScore <= ADDITION_NUMBER_OF_OUTPUT_NEURONS * (1ULL << ADDITION_NUMBER_OF_INPUT_NEURONS)) + && (solutionScore != score_engine::INVALID_SCORE_VALUE); + } + return false; } - bool isGoodScore(unsigned int solutionScore, int threshold) + bool isGoodScore(unsigned int solutionScore, int threshold, score_engine::AlgoType selectedAlgo) { - return (threshold <= numberOfOutputNeurons) && (solutionScore >= (unsigned int)threshold); + return checkAlgoThreshold(threshold, selectedAlgo) && (solutionScore >= (unsigned int)threshold); } unsigned int computeScore(const unsigned long long solutionBufIdx, const m256i& publicKey, const m256i& nonce) @@ -1605,7 +171,7 @@ struct ScoreFunction if (isZero(miningSeed) || miningSeed != currentRandomSeed) { - return numberOfOutputNeurons + 1; // return invalid score + return score_engine::INVALID_SCORE_VALUE; } int score = 0; diff --git a/test/data/samples_20240815.csv b/test/data/samples_20240815.csv index 110cfc493..77188bea6 100644 --- a/test/data/samples_20240815.csv +++ b/test/data/samples_20240815.csv @@ -1,3 +1,4 @@ +seed, publickey, nonce 80f531580b97c8de305ee20b7c87624b022c65badf37863f75360d9b94c8b748, ec8dc8f960ce120cfc670b71120eb3756fc77039f81f7e9f763820c1d185ee78, 59039e870f2626a33bbc24821b9c2ca6bf11aa89149d3f726fdf8e3bdef41b03 33599e8c36c33829a77ffde84b0c9a63fb6eb1fd984a7202b27d8e98a198d87f, 10833f675abafcda6ed8962325b5492d570f04f028c2c928a613ad8f8ab4b59e, cf2a3e440731066c9554203f364597e8e169306b16bf5dbed16bead466db9afa 8c53fb63add5ba46d7d35f3e884436c75d99141fbf8cecb2ceb4a49bec39b453, 45e4cc860d917fd83e2fbe418036ff8299b045ea8ff390c4e67f226ce809a703, 57ba057d57a2353e562eb11e3cd06184660e68e5a8295b0077cf65cda8455032 @@ -254,3 +255,771 @@ a4de543258a6485e2412678c1f54eca3b33c9b596f283d1768e61501320cc0b5, 58201e3ee015ab 2832d5d056c3251cc58585e1ec46fee83b7066422a7e48a00471462e21551d6a, 1b0f74bcdc25c92e0d10753d33f124f8c2cb0c52e09936702ef9536a8bd6a4fe, 3af3f48534e741fe77128a6c1eaef02fac0f2d97aecae4cee890f38e9f83b042 f7f68849ede71cf678063f66f6e1ae38f1854f3084f4f4cc2846ae2e69450cf4, 201cf906865c23154017b987461a4600ce30b48f640d465ca676868e4f8db586, eb75fcafc15af498c1a7e6110fc1e60246f0737742fd37d8e2a0ffa954ac911c 48570d90ce59afe7590fb6ce1a28d8dffcd9432bb6ec3cd13d96a05c102d3268, 08309d4085c5a9e3efa5b8a238213b9bc173de61deec4922b9aaf9e3b5e8b52f, 750335c4c77e10cce1edb328aad6d427156f9ce891d459cf03ebc983549e60f0 +56c9c53dd5365d08624e42718e5834347b6ae6ad61f267a2da17f2909e8389b5, 5f6092f086457e6f51895a120c472cb159e25ee6be5b3daf04a68ed88e82688f, 1ce95bc347c64191ced6b4e59ae73f19069edd3d512bdcb42891e3a9fca7cb52 +cf5c5e18485c927dcdb928fab02504b425b066f442d39e9a31d12e2aa0adb39a, ab46f54ffc2ee8f82e6cd465bd48d7a3a58b660afec9a0be44a7c891ec613d4f, 07a7c88301a55efcdbc0e4122007d0a3ad466b235832fa62c28254448b978d25 +f95cd1f41b27d17bee8ddf1503ab29b44111e04e2ea6f5e28fc80133b23effed, e5da5d1c08a796a0d94ab838dcc1dcd8bb58a32e33c6c9a248fa68c496437c98, f1cd2fc8f434cef688212488f1142b1672dd680f859770cbf2f18b9cf232aa12 +8e5242c6d5869682e16a733dc5d46d9603b2e933c4819429c3a8a313fc63835b, 418798d1e1d1aced31830c818f088907bb918fb6a6faeb31bdcc16d0919446ee, ab5425bbf430ab987e7b4a245a2e79a020327cfadfbc3adcc3279cb9f7cf16c6 +7324ab063d6c3db9b79ca8f3cede21cf779a7525caaac3cfd122de33e2a53ec1, ceca8b0bce0579317dc3e61b308f78b51137b22e9ebce2b24fbfb288425b141a, a555fac95b96e94dc1b589f9a40368a960e95ba8780fd71b6af98131b92e9139 +69fb18860f208e77402b79981f1c9ecc2339f73214beb06a3e7e1d95b58d2c55, 11520f2f262818c0931ce835bff1bc1db40b08ec07ea536a16cf79f12a9490c0, 2515a753064788efa6edf3019961b6257055917912f11cac7bbacf3cb74effa3 +b4fb8ce3b698ef092144e266168a4873298bf49e43f9f1c896ab720c730a2f91, f4c9f3f764a5fe8ef0a7dfedd6763feed4af6c8265c33dd02062cb59f8e527e0, 6123820e051507abba4155d1ff8c6a531289961e58e74828a0025865fb8b3e64 +a81a3de5f01243f42463e807c7640f01dbfdb7249ee853b1120299f76e796ff8, 13ccf9b8d14989143d6db39f78db7f2241502b55db845364122c2cdef1c02c24, abd7e2f0ec93eb83a3ff315cdf73ef975c976357df6c17532cec90b23ddb955a +4fa5bf35337d77fb387f5ba80230ba3388a35fc9217764961a2e3ef1475e7f17, 0a90abf612976ff3e2d57ba66024d8bd9870a6d1c1cb3a32d7f8a0874ed952cd, fc730a27f018d85aaad887038fafeef9300437f147d1465cb74fe2b729cb4165 +611111f9230f1bff602760c2d8b97ba244817efc00b803fc3e44a0d5f1431eca, 0fc383507c6e9fb6399872cdd7bd47fe7aa15b784f5d8ab64d27988792d02fe7, c94f2ef299ede9440fbf526a80359d06b6bae337273ce10b30bc5729934f984f +af06b165653d9e73b8c21e15f7f17c784c111086b0c712375b891f651503bc20, 33bd3fd02d2147d810c517497335f2898750357853be3cb23577694e6b0e0127, db4dad3e40d74553602a36cb2d0a6288b901149ad982eedf4e7b9b75d6c19d48 +58d3eba71ee7f0313aad4286cbc68967d028e607be9f1e649fc6462ba94b1367, 9b4068458a4c70b018fe35af2ee1224297f4e7cfa1ba67d02c2531dae564f523, e57f7ffbcb5ed8638f25b7bf6d2002498718d4b37146695216104a5fa9398fce +89081fd95382c0dab6fd97f8b55c4751ec6b8b6933509b046ed511ae4f55e241, 20d605e541f551908322b7abb4fe9e93e730ed4aa5074b51562fa1a64d700868, 2e8c50b88914937dd99ece2bee0d9cb28dabb0dc287a88f9793dfd6ce0d0e16f +a49cd8fb9f54b998946229dd4522e2c4882ed9c9dbc8a6c34265b92128a62cd6, c2f20d42fbdb15348cd28693ff84c4dd724ce88e53791b0cf34a08f4570c4290, fc0eb2a5b418bf22d6fe9383bda5c50d00cf5683f15312852c14c39686ae99e4 +a843a18c135bbd5e357494024e74fe3f78b8acea215adf1ebfbc1394ebc315bf, fb830c864f7c0f44277231e8f90b9fad6b18a9ed93e6f22ccb47b994c871fcfb, 39273c2e51527db3373e7a9f1c12dcb481fe96b0f7e3aba0a25ac3e9bfa9bf6e +788513cda60fb4d4d79978770fa2a0e668fc2bdde5b31438caf7904005d9c4f9, e039f2dd844f4fa15bcb58434c638b7581b78417d8f194fa892159de37629671, 3012515bc6e96cbe12b39ee682ae820c5f0ef28000e0a6115eaf999398be9946 +f6b5caafc27c1480e815f83efae6d59036e43b5ed3461825b8ed1a97c9035c90, 1c018c9dc29540525fd6839fa612fdc653267788a1d348bc5fcaed67b4786337, f7e454a08bbba49131f2a0b92a186fa206ce34a89e7611e30ac71e985ecefc54 +b6c664bbfd57bbc11dc06b9f3386d0fba9d9365ed3b355566a84df527bb476fe, 9e00a499d535c74ec6f9b5518c10d89666eea7278fd136fbd8698ece3fa121ae, 5af73b067b082937d8da429bc79c8deff9516bc8ecbdddbbd86cc093c71fd483 +7a2f60dfce65bb8b5e1da528dab2f1d9afddf1a4eed7c42444c46c6d375333b1, b56379bd6ba7d12adcd5475cbd187c2ccb3d04a9812612f10bcc55b0255ef48e, 9d7f6aada8dd42d4a77c3cb72a4c3a154f52d0bf521984f95c7d7dc7ea5effd4 +020a3e31fbcbdb141d331f58b22de82caeeec8e45e10d24d9cee6abdf3cb4c97, d9f5d5d3688c461939f8543bda3f1d7f08d8633c837edda6fad460f786409fa4, 44bc0645c7f018fdd9637db6d6081ce0f05345c1224a77778b1269895cedb81c +deb8723fdce92cc7560ff7b86a9e8cfb9c4ad5e230bf6851867eb6dd20907aef, 470c325f0d8462c6ecb62027ab95d8167cc14f82c7a3c37a4bc955e53671f4e6, c31cc2dc1ae2faf13fcf42f243948e0296a7bcfd3028c67b768f09ec713d4c48 +effdf94de930978c17bafd1aa739af3a6a7f2a41bdb4d8bd1d7d1da617318803, f07d705f17ea414d4175f677b5418a2f8fa932d59056d8a6f3a1f1e0ab137fe9, 24dec37c04fae5490dfc019f27018d84bcd469e869c05e1d3200829acf96fd88 +390316e9f19808a5c455f0f12bc60da93761fbae37e7a888929a09692439d233, d58f6ee143efb1e63a5b5a57539ed4c72e6a3af509d28c01bfd731c2b4f74e4f, ae112962be47b89064e049fb0a1ad7938124b9aa8904934b45531cc218da6428 +85fc8c83b8ac38baee564bdcfb38bb6ec3e0374310eb1433bd4173af0cecf445, 94e122de3b8e3305a50723e9f99820c4b65040fcff75116e1a874fa4476c673d, 06b5b168e9ac774393119103235b1713942fba1267597c7f5d7988f5a726f1c8 +7360664603aed1ad9c65602983eb7b67f1590cc82ef7f25ff66274d222365c10, 2850a81dc5e02a6065944e06e309bdab0fb3fc9c36684eb2b188b91b77519f12, 991c3eadbe12f3b62417f233b88bc9c01ebd72ab813ddc46bd8d8fb4b14f20fe +92b694a02c91655d09b2009da20d815a117ffc5e961f60ff0979a7d546159bd3, 0bd57ad96104235083fc28cace48a1172e24bbaae22e1703ae850b156767459d, ff6a6857937ec0d28cf136de502205feecc5adc5292d97faaacf43267b57a498 +ff76be3da28265d978343c1dae12e5280590dc8bc1104d5ea426dbed3e9c9b79, 8c8273008011b2b8db1f3ed0fc4f6170796488063fae7c9a3799db11b76bb792, fa891cfd0ae9b18f70cd88524f60f9f21f67e99ba7fa82eb7b773f1d60af684a +3ca75a3b38fc38f828284018b766cfc550893804a7f02768e57f75b9108c6318, e4d8f5e4968860c267b46333130a6e715f06c7076b0645bcd8632143be6c966e, 9d4e67d85d2b4a9151e0c39ab09b7bab4b30def80ae842d76f4ddeea7a4e6f4c +aa3792702d9ea1b9bd856d7e219f8bd60e7f592a9f18272a1755045311cecac7, b298a23a308b66bc6c47081e54d16edc0df2a62b5414c8dbddc4f3bbe89996de, 2fa29c00fc00eee115ae40792c57f12cb0d67ba1817c4bc62e2de41ac09ae641 +66edc4b15c50309e1ff00e4149c7b866bd87bfb37955ef72154d6e0b11a98123, 125d810863a4fa29d4ee15dbb0de9cde10d1ddcc92bd428a160e62e348f2f0d2, 95ae367420d47ad63c0c8203a1f823610c4053e4aa5d98e0bf6bf3363b92b157 +96c44d7adb091c8964eee8b7a9d027f5aabeb870d65175f603a23b3a99bcdfad, bc0d26ee4446e2c476e3ae722d458b5bd7bd80f0c6269b0bebf357f41b9e2ee1, 12d0fa7baa38fc1fccc1fa803cd34a30d2915650f3964302d922f3328142c1ae +51c31c928fb63d8cc0356e037ae08058e5c78013caff5e4cb7e4b6bef669e1e6, 7b0190fdb37e5fbbaf448aee5cd57fdcafd4f948bfabc7aa77bbb6b4500e8e2b, 8cc11e1166a8807e462e435b6592d2ef711318d791ff04a02e874041ad889222 +4db1dc26a268bcb20965a09aa5c4e8785480cffebfdedf66769b278e4e22a692, e5d570c6c4544657bbc76e3ce6908628a110d00a20bccbc78cde0eb1b07ad4ce, 2ca991e45e050246410bbff55e0d5a114d299a4277ef76538e2a56a67a421e80 +3eb7c5d1e8c19e8160439c39c7fe85ae2790b98c29751894c06dda33d4a99736, 64117d02c50e1ef08652aeeb3401766c0e4e98214effdff6eceea3ccc4ad4f4e, 3c52582e25419fc856f3d610782f65d40812c3b0e032d18cfac388a2f93c32fb +c1f2e34040c58af0a2afe512eefd2344ef8d5340472c07c2248f9a93d9df5eed, 36fb5649129d2c95856382e6e79c32fb962035a2693b84a9204c972af0e6d471, 63e9067d8be99e43041abbc6892a34c71e40410801e9d8184173647e44210a24 +abe91214cdf98c6c091716c5234b6335b0b705291ca1dbe815de4362e86ac7b2, 2872d3ba5db72cc40e6d5301ebd18f6163987028f2a6adec51c1eebb36632813, 3e551c41429b64929ab5a4be7b4efa71f19f16e0f876300ff9aa24aa0efe15f5 +8573f710b185747626aa59f01f595849c816f16a7938f6d355149e20b6ed9bb3, 38ad06dd657ad5c3cc02f23178a6f9e945c9f9feff4c243693191170d95aea4b, e7eacbb5c2ac9f4a478f7ba94c440694c834bbc2711f7bbd3138ece7a9476c05 +6859690dbcc8faed26c4277f319649fca59139bc79deaaf08a1eedbabaecc09e, b1e94848bd60cbe662179b56b12a70ded1fcb700de35832cf060396366cac545, d3747f9c833081bddd127bda78a4566595e49a931d104f53e096b3df564d0270 +6b94598d0aa9f5bdbe420b2e70c924b88214c7bcb0eba8129d23732d486d5033, fa3ec579bc83c6e0ec586b0aa832e2658f58f214d5a79f39c51e73db2ba10dbe, 937750fa1403876c8fc285b7a69d16e4278d7688c8d6e2662892882d4b56644f +44b93a544a5faa697309fbad41a6e504336fc938f6b41e2ed9d8eb0e65617d3f, 05953dafaaa79abb5173ace90a589393fe5c31115f17f1149964d4d1ebbdaeeb, 4a15b0dc684dde8363dc3445cdc1802044dd3adc3476f544ba584ee548a69ea0 +5ed0fe551f0e63a22c57e24b87d1608d58b51b6b883faba8d57c0d6eb3bf2bcc, 67832e184ef95d184a49f72a026fc5eef5cd4b0d9f67c7ace51e3ab4caf342db, ac552bca1361ba9a6b2ec1d613b54b3fd6245ea3bdf53752d3def4c7d05bbf11 +fc6c408947870901ffdc04994342a51be8baad4717c24e7130f957126b4d3194, eec931e012e37518236012e7001f9b503ed3aa82c0af99c540de1cc3fa127102, 92ada1eb41a87ed9566006f715d10e35dc61c928863334872c04bec573d3260b +af83730fed2141036542390d74124765d7dcf8c4320f1e439f96c1622540c7c3, ab658dd4ebcf765a913793083de0960156e0e2ab876ba7cbc716d619f232473d, 4f2f54d7da8f3d160151d9d57d2806aee4a87855741a9d3dd56c0cd5c3feb843 +9e92ee5cda38cc8bcc8ec45b8ccbbd9348785a7d0b7de96552aab78e4387d2a9, 390b33b7313349c342d9446e9b33e8333011c5fcf80fb4b4db3122003b90c0fd, c706c6c47ab7d407bcb52219d5be9d0a60bdb7b74cbe63fb78275d385dad9989 +b2983032db9a48d43448b85a1009109377c6a62e69012e0985ea79af7b03357d, 03999281a51d106e484ff3ebc0a54b5e40eddd0347c4ce98ac49e26627775a74, d2985f1e64cef76aad360d600e8a808087c774bcd26ea15a89ba80ec03e6b00e +7f2c1c290c3d1d9278fdbc9455048eb45cbc3c68dae466db096e60ecce1f817b, 039cbdf935942e700ee5acb97b8c2c3d7cdffea610d0ccde886e10229270d884, 6acb9b35c1c5849529c548930fca6b1cdb28ba83ee847381607309c61ff93714 +d6e43186c0a2f76e08f6b1f4b0f6e32b5fd07f26beda8ec4cd43e3c1d3cc754c, a934e9b151cf89247414b14e0c33cf1753b701427a0c2f88398ac623e6f71602, 93183156167ef2993da1b3f8f020cd33650965d30d05648593d841b8ae1f5c0b +9bace85b82dc043a9238b47ae091456a7435ec62f9ab03e8127d74a0ce127849, 17c12597f778b881def7ac31c350a4a97657fc495f0caf3198f84b6cbf2ce565, 130b2d55976439f64ddac690bf93a465072bad00f11b6f110a56ff39dab93ee4 +5aadcfb02c1776a0442e5fb4ba4e71f0b1679aaef89426364b4421a7485cca8e, 6b428e5dc62a462e736c608a668cfe6160148456176075716502abc5bc5c47d4, 0541a9a2d1260e8f5eb5781cfc96e330f3c496980faf4d8a4b933d8a6e1fdf17 +54a04fce072d2de00a2c096c2ac3f9240a958b8a20b9f42c72e0a054494233df, 9d529150abbfad05a43c31a7f4cba6100b4fc7f996e6eb5cae20684e05b557af, 6f06cb5b3b2bb2eff37f4cca922b9002ab305ae3c4da9df734590accf9b8fd37 +ca3cf1f77cbb6aecf5c8960996cc069c3161a8c66f3085e1df233a6cdc341a7c, febceefa10a344254785d7ec3450a23bf48cf9d99c6fcefeb5a336c859444535, 88ca45323379921f9a21960b6c669e8747ab483bd8902100ea18526f138de5c4 +02692909c9207dea17c4f2c75772f05e88a7a5834922caf2ea2817c9e96e6c96, b6ee9e5a13d3ed89fa496dcade61765a4833656aa5a37b8b954ac821870a3cdd, 388d76d08f2f705655c9ca601be6e3b4818e23f4ae3fcc2c672f71853e709644 +d19de2015d79061bf45c3d6ce36315064fa8f0b0f12c457cc2036ead743ca5ee, 77cb5a56cd07fec4b9a2a7ef620974abbebe6f5d171285670a52a94f14cbe1a1, a60ec3d2e3b8449535c235f96708863c66e90a12e2ecfe2b4eb8e2439501091a +745fc3714d4049c8160096f88e5664c685e5636a9ce4fc8d2dde99dafcb05b86, c64462d58638704e6bfea5c47887b1a89b596ff03de81676fa138bef590cfe45, 2af32888a2b52de4fa1a2569ad293544e8a92c23b5304d81c799aa641185757e +19811bad7098c30704db46dff71f3d2600a41d9512e8007d367502fcef4a575a, 9a86356c29de8bf2ca7b3807176fafe1342754dad9c12fb4f608f9af566c8d83, a8bf8823b0fa34beaa74ad830962f4a2cd63b1c1cda52db88d2f471548b93ffb +4c3e87b4f0e70a828295308df6e33f2da59f87e12040ed9cf3609bf8cc377fea, 776e66a38d6c096f633502ea0208883b9ca5edfbb0a36f7278d53a5c5758cb85, eee223451d0fb73eb538c899f3e6fb45dfd7b242cd1212815cf4086dcf8b3f82 +98d1589d25b81cf76bfef6fa10d05ee0abba04dde6adda866981691d711bc4cd, 17cdecf22ddca6cc6da3f07812dff0864a46c9a78ca4de3cae649673e394866f, 950e5ccbe6ecd040bd7caa264540e2d2c6b034e95c66ca81016edf16fe50bed5 +a0aa0b1e81f5e34c4ba997caa725a588538c0cf7a5b91a119a3b8512a0a9e672, 1a429e26ed67bdecc373dab6c1987c0a131283f0285080a401e1e1a49d4fdc22, 9b8e55a914092654217343df0e2223adf39b376609e74a9b43ed760c21f88cfc +97fb9d5c6106e38ae1337818ab84e67f413a22bb8dd7d27ed5b35284400cd4af, 5a89fb5692832964aa04b3d69234191c6a539d33e292e01a16c16482f49c8619, 2c9222d6a68621812e11e150a3cf67eafe9b6013b48c1d5d368381b0574bf95e +cecd182286b3b4ef1d134a3f3a5984397937d43c077d2868855f022d661366d8, a8e33b796496a702ba70788dc56f80affa3023763191c05d31911a37af6b6b14, 0b240c7b3a2e84d112864d3bc36841eaf82dfda633a625f9ee5320e4c37c9dcb +b181d274cdb8d6b2e941929f84c8d1ebefdfb8473b2941855367bba11f498ad9, 62c9c29a2c54a3c45cb7d685005fa4cb4fbd428f6c1194644533b47f0fd8ec8c, d2b348c04fbf426fbd9b45c4387a5cbcb3fc55006e3f30524f17f5c881d0e734 +4d7d3efbe571b5e5a28d43b4062108f8dd7c7173b2820c447d5faeda40ec6aa3, c79f6a527481aea82674625809e2767859b85469d3fe061ebae1f3a37f214867, a4b33fc3de7c7da0145fd004118daebcc4a532b01026ec2df5dde32b790a6a8f +89eadb841a962cc2804bfbb3c5596d91bf23268946278bfddb0d8a131691b5c2, d659fb2b0a5c909fa5b8a5d330880781f8cdfe501c6fb1f61d909f84ef94d759, b6744bc23d838611d0d62bee78445e8f42c6f07ea488c446c3809dc0dc65bc57 +f2654acab4d56dd356c627a417f32eae94710fdf1743f5c4094e64280613340d, 49b460cce8b5332bd591e0fd802138c906739507564011c5cc46ce75114f4863, 563c6fbda56e4b632a20e67e29bc6cc1a8b0abc185de1d42ef894a83a11d03c2 +d60e3903e877eacede78214cab8a4f438ad325a431f5546d974d17fa59d37325, c8bed219ac91cf3b5cd37d64aa3e97a0ccad0088f62179375473d1bdb344ef35, b4ab1740d21ad0d0cdefa7242c0e6d24e2f937a20ea30dbb614a0f165423f12a +91ee7a0a76142aa4a90093dd8438421425b423f3edfc6c9edf9baf88cf812736, 718ea9fd12b79855aea360d687749d10e6bbd83f0808f45e641cea9485c9c59f, c52d911a47bb2a1221b9b99db010fc2f32902acf64093649b6eefc718a83033e +4e43c451a1da9c7a9ff3e647e1ff3d482dfb2f8c0896c72b1cc5e3c6aae6d768, 76c6fb46bd0908122886866e3100ad0d66beadbe17d09b4a106bd43f72cb4448, 79b8279c949c450e963cdafbca8d17065593ba08b01f29f7f0660a717f712bc6 +e7587ed1a28c3b21130cebce8b18a68449c3c60dc199ec409803d26748b392c5, 871bea98c3ccd580b5adc276fdc0965826ef498453989cb8bb24fcc7a6ddb7c4, 8b28a9ad40a13ce70191a129eb4a91caa6ae569fe42dba2b50f03f1bd5e3230b +66a6fe30b321e974e0187c8f581855efe7226e01720df11977415a61d486feff, ad62901f7644a7d589f709bba23596965920632d7944ef7dc4af844976d5ee82, 8897daaa3fb648625b18aad6ca8d4354c1c759a65998a1711232b004ae08afc0 +6cc65cc5e89922379b531a4a75a87335cf8ff6d7192cde374778434b8de24fa5, f54c1211f48d85f7612d825eb7c441bc1ba234cd1a1f0208a2d39ff6b19a303d, 9a09a532c49c62fa9ab15ee03307824c3cfaf6c9ee8f8ee48d34ca4612793fff +0f3aae2b990b876a38ea684fa6ba1d9b70115875c0f13f88e25e63ecfe625481, fb1d76732fade77381001cdec91a7c61194713afd317ba0e870eed3e55d00d90, 119d558a2e9fa5be2c7e4a72873fe4b29492a796b9373dccdf904e96afbc6a77 +c02224448aaa7d172804e33b03b2a3b02e308d2e5092fd5782a0ec7a00a50686, b9517b83535695e2206e4f5db978fcb6221b49091ebb1fdfd2ec8cfcffb4493d, 75c18deb5d37fab1160de90ccec02cab3012a5e849ca81b2fbad7016305c905f +904e1a981e43fe8dc6d9d44399b98922ab9a171a7183cf9aaaa8731d0ebd78e4, 6840e17f4c709a2093f2b6a2bc439d7b778c0556c0fad0be4766934604c50f99, d020979be777c2b461ec097effc87fbe8985b5b62e4bc9f5d6af62feb2e7da52 +d8ee43c866887c55eb8097ebf8b1d21e32a67bd26f6cdde52b4d707b5e6ec9b1, c1e4f0125eb89db1b7bd733b359ab6fb7be47ed9adb3c4528666e0958c89874e, f5ce32c074e33d30eb3ebca68d7b8be3f335f3597460f7980c25c87d3ef5620d +9b136b40b2d8c82e652add35f8ddd6f235eb6c21c86d98cc8575a1afee821661, 97782c2da4b4d54936668e7fc1c07d425219256a2c504a7bdb0d742f1506a8e9, 8cffabcba8b0e5e85d1d148ee9d20540672e84cc22270c9193c9d6d0a1c9f6e8 +09097139e45f4bf12f8d51ef4ccdfe35709c57b9dcb293460441c65dc4d5664f, 824c00b3d3977e9abc4117b755053a42348cf403f9e45fa53c383f0c666dbf61, cf1934545531f11bf2ab60bf96567df83a2e35609e20af3861abe6de733b6046 +eb45f24e78505df5fa40ee4fd53ced00a2089b3c447014d052d5c3730d0cab2f, 3029e6025c04d2fea31ac9a959bc5ba91826490d5254d61b720f0d16068578a2, d6bd17b8a7303cd3dd68f814571332ee82a7848627d2606434a159692a2f5bff +8ffebd740fabdd74b0f2995e2ba6f3555dd93d0143baab454e57a923e5072c4c, 85ebab13178cd1bdac0dc50e2875e27b5bf81d0e49bf1a2164fd936ce4491ec9, 20ca606a6f8302d667a01781a14dc69a67dd1e1104f3101c54ba4427e64634a2 +7e9f1954328e942a8e97da5db18c9973fe5aa4a6ba2d5d398d34cd10461de8f8, 3393db3ff5d2b08794d55b63017bd165266baa62da584c418145ac60a9692a7e, 6537c6b49d21cbe2c967a6bf18907b509a1c0125b4a686abb53673789381656d +b94fa02e3c6530273a8f59f25213dd60ce28f3f62f586de1796b99411c59e785, f45f6c6bb06178ea7e60a77a405e92d2524acb4ef51562057dd851fa70e88d0c, cba0cb6c27370c4408458bb54cd0c5dd8d7bd800f1fad926e3d4b13a865f7200 +31fccb251a7b4e3be1a5105d3c8d10d28accff42bece10cb8eed77f1342073c1, b42a564170326ce44045284843ba9093d885b9e2c8de774eea543050a018da81, 73db78c2227d51031673e4eb648204fb104a8380dccbba1a162d6806dace9201 +2264bbf7772edc5f8ae4a6b6afb96f70c3a651e42cf04c85dcca0de761005f4e, d84c9867d2cabb75feb8e4f48045281bec0d1ccb5c277f97d49d77a20339cb56, 8099cc346a4a1fbd59194cc0cb64b6041ea7e462cedbfe3e23e8eb893ed3161a +ceda63d7a796cf2088086095a1584881e98dc3e6eab8b29ae9ad2b73740d4008, 82da86b1e4a17cf1373b85510560a89ddfb7752b5c7d074f1405601a868a57f7, be295496181533f9cdafdc6a052fc37b7eb0fdeaa92b20efef749a43d0e9319d +058f37dcb55d1fef2a2c80285fbfa7d5b3a0c26e4cbea092275304a3e23b4a8f, 61e05614ce75d0f043759d552f39d8d628926c9721ba9a67908efdd444af97fc, 3346b12499be05109d819632734792c02dcc622e25fc34fcba1694765117dce0 +31a8d330844cbd5232678db507c8c307b52a5a3cd8fe99727d1bf5ab8ff593c4, c268b2e75ea2b48fd3404702ae7bd695dc0394f04e3ac959070b7f1ab5b36616, 4dda5fe28a0ca361f8b57bbe042f6494d076810005ff574414e5773b11871e77 +43c27087df6f54a419d8708612902a7524575d6aa04427613449576c74b6d4c4, 7f0b4d51cb88b39ea277efad0ea1fd131633a0c3a7ee3bd19daf316e9cd6e4dc, 9297f658efa46f178c44b2467fdd8af261bc465b53d2b4a3316ffe5c13402388 +d5322f6e70a8307827d09f44ccd2a518351c968f5e2e91866cfee3646ad99b0a, a3e874dad0db055379339ab1f1c99e79aba6412713378c92daab5202984eaec4, 9710434e6ce188337015eeceb71c8de11709c0e9a5e03a16651d704b8f1f5f46 +cfccb0369ad5c38a6245666c789c20f01dcd27acbed6d7f4497eabb5c4a58711, 4d7a0eff04cdae99d5d40dfa883db5b466d115a62a9cfe55708e7f152bb080a1, 278a587e83fa55f222d3d0a79295fffe01d44a95b38784a3273daac33265340a +612143b81a9eb94bfc5d2b957873e2068f63494254d1383aa53ebfb7cc1a7b19, bb08e225e2dd4dda24d825af88a137b92f318a2388e27453b152c05c63b47343, cdaa899e021198880e3504a44a8a9f890a8878d3bcddf7ab51cda12c4977ee99 +d5b8ba4bed1f33b7f6972dbd4cd4fee4f08ac51483ece715a9a48a5489213e9b, f9f3b06271697ebd61cc49c7bc156a80addb9fa7396066f165c0eafd23396b26, 9d82f95e4bdf527573f5829b511a8bffccce02d3a376b26a708b05e38266bb00 +5b6b322dbf49d62af13c01e17688bea3db573a37084b9c9d01ff88282bdd3ffe, d402a37a3bc76e9bda73a9f52d1f87cd230927a71e050c5547763e8af093ffe7, c06a005b669559343aba233e632bcca19c575dcefd163556896414a84576630c +f752c7fd5a03ac805af9fb31b34323806621bd73ea0fa6845268c90d7361c677, 40830d095f0cbad61ecf8547f48c4a716fe4502afb6f2ae81a757e40713c95ce, ff00935ced29a609fa7a42d48aa902b582f2ab0ec09c443c3750236731096c7f +fed6482ff749ab302d3c327ccb5770d04896c51fc690361d97c745703a7a542c, bff03901f4c92938aa6fd0926aba318f33102b89c3a7b56c0c856640119c706c, 075cc18e4c818ae6b4ca70a49643c381a61d5bb06062fbfeea7aa19cb90e1c52 +603f14ce233ac8216b87f61198aaa2ef184e2cd603b9018e338f12b9464838c3, df72ee92ed57bcca5d578ace54f2ffbd6a8b4ab3b3e73f9ce2208bb83a060d7e, be6f836d82fdf6bfb06c048ac4d655d091c700dd2c9dba4e27ebe9282d5d8f35 +249467fe22a33afbccc531ae379e1503b47ce92b2e8d6109f1ed7c81f1713e0b, c682a6a7f46f2153243dc263900db374d8370a3087547eaa09cf551da05ad5f1, a39da45a187aa264b7fc2c42804467923154294df8aa4396cf90029fb3a0eee6 +95e54a50b13bf09fa0fdeec9528216cb1427532cf047a9b71314ec1ffc508daf, 8afbf1ec6a224e501caeeb33def3b74d25cffab29aa28db324c20eea452ebc30, cdd13c1d83277945e07369c1a705e3e2b3bbc17fa5ee21748e79ba8ced006904 +6008ec9d1e3ae1cb6ef51976bc90b697b72013801863ab2a5baa940d3f96f0f4, 2415d537ee5b154c6bf6e92d0ab6595f6d95f8c70de40093497a71ac4ee0d493, b165640e4bf5a78840912a5e2602b590a3354b4cd973adbf81b997e91423e265 +17a161e669326737a55ddf40e623fa1c23fa9d2e015ccdb8ff1ead398fb25b4f, 69a3b9757d57e4fbbde90cef0e2bacecb35b6e037d7882c0a8a4f05fe783e051, 223c61f9f5a6e6f2930f04662ed52392692a69ee4b1ed787e799219ac838b397 +4f6cf1acd616b993e3861e2b1256d46a68432628718d04a6c9b480f738127d39, 9a63d472e2bc8f0ab20433ed359e4c1bf54dcd143d2613117685e959bcae0166, 43b8d4cf6cad67fb21efb1a829762bb8b1f4490313b020fb20e1cf9e602733d8 +a2dc382409b5ab60c359bd2a3f3b4aa64aaf63bb2215be73584a323e9ef4d019, eaf5bdbae8d7b8fa838bc6b7f8dde33b109e1cc5bc390f7284020af2016c21a1, c46c7514a655468aae7f423db0ff9d5070948101ba823194bdadbf7bfaa4c9f6 +6d3048a4cb1bee61c59cfbb2150ae9e31b5383080f34b2f3b4b5b61341df895a, afe2f43a2132977a9af02660beefc0c309a0f21e6c0961b91d25f8ffa9fdadac, beec92796c197ec46d7d9b3bf1f971800cd947c80d76d0be3c11cb48387e812b +8c20618bda061e9b54e2e4d844eb56af7537e83be01ff3e444d2855ab66308ce, b3d2335f0513e55fdfe3ae7200a69c12db9d9ab114c11bb298cdbc566cf262f0, b3374e60b1f11ed1aeb0b4740a531b6ed0bb1e37564d28ed523455d2825c09d2 +aad27f9d4c23118b0fb42de4b28b703e271ec6ad0bf043b7000874a8bf65700e, 48f5167467a999536da8ad289f95095daf91b057a53ece8d8e6a41a75bffaa15, 335db0143e7ddaf57223436641af6965193971373bbbcc403a3f7ada2b570113 +1799d17002eb69b537c0a894420e6942feeca7adb5ae0a56d6a5cc416e1d341a, 331e0331d348519b0014c3fcbbcef17953c9e2f06402c3d58cd10e3ecc18dffd, cd566686f72bd69576d577d24e243153ed844322e269141ac639633424bfc358 +e20d14b0c4004b884070ca20743d0984b02e24798d6265f9bffc091260f64738, b6510f4c57cb2b678bee9b70afdf9f03843bd59c32ad1ab1a80f7d8bc00a33ce, 966bea8315231695f11a787345421100ec7a8c8bf84651e6c6dd750fb94f2d8f +86a7f5426ab0fb2cafe482a4582ed448682609405527c0a71f40de6031e987d2, 4164c512b62d2fbfa08a63636e4cbb73f84941167829d9ec6c5c586a6002a0f8, c982582a6e2d77990ff22b2c9b460b6e1a66e2b7e1a8c21fba519755629ddbc1 +22767701d4863cf0fdafaaf78afab72c831a343ec7c636e2eac60cd88f57f8ec, cf2af935b972b0304c58707752dcb2fde55b9124549fd297649596e3c0b69be4, c28f1428fc8a1ffe9dea6ca9eb91447b91f928a1bde711c8e7f6b922c78ca7c5 +f5c9d2e5bd9587cc4be70a2683a5687718ca348fe20bcc4c55e96caf48f9dcf9, 7dfe6042c3f9ad9aea16f9cdda05a654fe3140a4a5eceaa81aae9fee6f42b951, 1871f11d451889c14d51e7f50a19a4c133141d60e126a7081b21d1a953005be3 +8ad3cd007ab2fc86d461b3f1c944f3dc414f3566fd1e39e5a7d481bd40ce1f82, c84cae4e234d399479f0b8a8b7f68ba3d2a18542b9be722a3856535f133cac80, ad54773788217d5779cf4a43548e11b359b2655b76ec51296807b6e358c97189 +2dc3f4e11460d9cc059fc528ba16f5c912c7dba4ee0920f1dd62d7810bc4cf8b, 371b322efa2f3f2e41827f8ced2ede62dee30940cf59ec9aaa089619785f4896, d61f6fc6af169debce3c1737baa22a2c24e2d0b85b3e0108092786de8b40ad2d +4eb8e114820a0eeeb1463ddb160f28671180417e537690cc54b25cbff2469884, 9bb83ce53d5b7439d074c6a6a59e70be74cc2e2abde708e1f823604be79f87b3, f0806997dcf99e81d7b0ff7468c9bea42913a6226837f13217e8347309fd70ab +25831dd8ea723cc0de8ff13793c7aff994b55aedf70e3bedea92c68696b88104, 550f8f0444f6384dff070d8543f643911b3d499443cc4874b4c353665f8196f9, 8f2648aa0b7b9725f6dfa2680050e51cdbbcd4310c1e54599325bd529de80457 +ae1b77168acd5792bac9ae8dac20a530224a9ced758ed77e25e31f507c39dac0, b5561c1d2ede6105a75bc1df5f5e46edcdf5b52af5063498b6cf395b412775c1, 506493f3a0b2665bad6b2536afde39c4653b753cb9ac327fba696ed5a278fc47 +cac226c1ac4249ba001c2c2059e3fa9760e786bd17af6aa26f9558af8fb43db1, 36a7a810a754805a4a34f2ef7879d25907e5ced9a9b581db466f9d309505a070, 62ed9397cbdf33ca7a73d5bcff8b4a8500115a3d914e2a4c9a15033060d4053a +d2f495a37e41595697edeea98decbe760a7228c26aa7cd4a7333eb695f04401f, f510c11a29560f16c210de88d0cda9e91ffed267cd070db76e21a59f6d99be84, 58b5591ab3c8b592af69e7a0467bdcbbc49e8cd17e44ffb31ef46f4f26018949 +f9b8449534dd8170fbf55e5326cb0c35429ae7eb49fe906c1fe1a63f840ec4e8, b7a0e2238215e017a4bfa36bf0e5f645e48253d2d243455c8a3c23cdfbcbbc58, 5681e23138eee67bf2daa8da093d9aaf7c86ba1677a83989e9a084488e1cab3f +6be36aa617a09f3d0f9b48fedbc83c1405e25a4ee23e5c2bb9f50fe78abe7462, 14add37465d8766be8292effed9720498501fa7c046c89f89a5778def66e2d0b, 01d511618422d4c360b9a39171fa26a0be319f0f0fe2f172e3db46d4a76833d1 +070e7a11e4c9dfdef4634bdcc5d5e21802c36a94fb3fa5944a5773ef933af336, 4922f59bb9a7e4aed28742fea242040eb7a3a1150e15eda0a1bb9dc2da2a18a3, 07a69c912d4b5e706df9f4144ac93c9531508957fa13b4d74499b46b093e1bc3 +1a23402c59d348eeceab3925a42a67a3360c36a904ccbca3d07bbe57038ad20f, 288afbdbfad8ca6d000584084034d973511e22ebe5cf5640dcbeb4fb93def5ef, d47371643f5a1dbeb84c1479aaaac26014af402678330188ba518de87252fcd5 +a6c22219274b18940c6f246876c11796ab5881089cb2389044d170c9b4db5ee6, 81a4613d8ef278a0a14b9f4e5af013e194ce463879167d8ccb91f530b3edea6d, 3fbde245ba96b25a24682d8e911fc4cf7e735ac2148dc1bec90ba59c5bd0a880 +01510551379faaf967e15daed5eee59f32f14a4882e1cdfaad1c5176185909af, d1e65e5a326f051b574f18c243d87049416b662f5379dcfc198678d85e859887, b4421641b225f0037190cef2f7f6bb521b5cde8480d00a0375db9de402944cdb +83331ee5a9d94eed4953996028bce779a2a1df5683e6c65c8a91918891f952e4, b9ac9dbc3a086fcb58dc14d01659709b354ba85f114ee7acf53f3f733c5a7587, d41bea4599b2e71e37900cef8a6d69a21bc40df1b7039825fb10c9eef8bfe387 +21ae89ba63499eb5413051241c023280807001afc799d9dc6fe56789d8f21324, 50da7ab02243c410659a351fb304f8ccc90249e648d8d7e495ca4e613007729a, 66e7d4399218e701af0dae04f88211c0b30143ed2add6548ce76e2d5555785f4 +b86bad6dad1a31f660e2d0458ff7203da69bfe85afebd2197f5769a77728263e, aaa66d5b663e8ccc509c2dddf03be1f8ad85144b88f270d6c5250d31dc39d1ff, 067fc0e2d7188bee01459c13fbbfbe8881058d66fe4942086f07a17dd18c39ca +ec67dd6168d63c6c34fcc0cceeac8d31038f1b46b0f953773afcff281a72db8f, 3a7bc0292fb683646bb7800a8e7cac89f6704691b0646901df9fe1f10e3907c3, 55d1f51ba65333e23d75c9299652ae606a4225a0781e65fd64948f25d99f6c38 +97b8b9a1f7c8ba6be10d8cc3280d9111bf6c55bf09a5c32bdcaf926fc72ca016, c645a298eb170088da7601c90f7d1d8843b14ad6a697ba0a1414c92eeff18b6c, 235f648eee5c00b40a0d97abb75087dc4ef94274f52fcf9d4928b11badae1e7f +5e422707099b03963eff74cd9f4c96c6690b0e5df2dcb631933dd15a4db1217b, 6041899fb1e2048a9115c6b2333a3ccc176ff4dfdefc3f3d30ddbda547275502, eaafdc4f50c181254196cb7c7ca9265019285e99817b48edc3dc86c7634178b9 +c05cefd7f48ac78803221f790f17f38a401960033ca9ecd6f90d7b568f61d236, 9df66accf9486ef15d5c5a4ed20e2f786742ed429878fd17c7b34d831e938cea, 6f080c05f8304847f5d24789f2d1de22e6da0dc8cc3f75c62f50638af574270e +47966afce233da40c6f0389a064be3aa084a4446582e6e31b1d6510d2cf5575b, 34bc6dd6e02f42e23e568620df49759641c0da270ca493be022112fa7e178790, 071fe44543334c6b3dc651a256cc0d8c733d9ba990913c4cea170e7e45ba0993 +65167976723e1794b0a71d5fd44f4c81957e59772f6a48c69342ecc4c30ed8a2, c88b179402b5c402c60ae2cec80c994052a1e93d4d3424d39da05d96f9d5a32a, 034fe3b28a9cd41bc42d048587b400798e1235b3839a2875f3a50c327adb35f7 +08ef4507229fc0a765cebe4ebd7834dab51dbec0dc01a0447a117744561999b3, cebc3b273453c4d0505754b364ae20dab9119ef45ac7b0bdd442ca167883e97a, 5f1026649f7111debc076c4aeccc1c80c2361f769c0c7571667168e293bd1e7c +1519c8a194bc36acd78d93b45a8642b24405f3601c0735b138eebc6f77f1bfff, dc4156e9d38078aaba1e1cdf69018d76450f15881fa860873686fbe51186b300, 72e0dd2b6b21b0c19b7db17b2ab1a62a23b49474651a78b7b072a0e64fe89375 +667ce1ec401d2f63611aea5fa20c7505763366ec692b425629b5aa1924abd725, ea3bcbc67f9260bb38757326e846b5715f9832eb29d2447faab854e15cb42f6e, 5920bd03c80a16a5f4bc9578747950352cb1fd4b5d7c90002cbd4a6c667f2d90 +a198b3f483f580117b8f5e756f63437cc1f1893ea523d38ca10cbc29f3febc2f, 4f0eca2ea748c94d56fa99991e158ef7d0fa201762d3e66f27b34c40b53e18aa, 551a9c1a56a530920a35b3d4e045ca5fb8d319f363eab410287c7781c1755808 +8c0cc6bb9ff332845f1ad854d8ce984ecdf3d18bd8d1d8e5e89f6f18af61aa80, 62f92a1aeec61f45acbb48f397e7fbbf8f67b1c3fb51794369375af471995c9d, 7ec5b11027d042c7953512e3e74e3611563858f466d52463865197168c1b66d8 +d20b721c97a1e5661ff2dab50f3021e73a27c76602448f5abc82b32ad6bf242d, 232063d17132a9b42cb2c6abd23a8d0c508fb5b9d7a518e8ff29960ffc2ad320, 7b18fce578779f36b71f866cdce2c70801888c6335d0d525a69867375cb6eaab +1d9b2b2349ac4723897ea54baa6f03bb52c1c185642bd0bc5c08958eda37c416, 40e50853dee913b7fd3bbf8b6b41ca4a803ed2fbc397a758e50c42a2e2f7ac18, d0f9cc11f7bd27361b2887f1e3411caacb4badd4bde88930cd78117c3623337b +d9956a8b24e646400f9886fcc5e73a4e4b09230c9d93b451e98adbedc13b8d8c, f757b2376d1a43a30aa4413cb3134782873a7df4656fd2deaa7b8affaefab79c, 5b736f489c5331b81b0ce5acaf0f188f0dde186958a11a97a57b381e3854c34a +265cdf7c953d85aaf65ab42f3d02bf5a455108851c6f040d0a49d2dda424f54f, 903e9483f84808fe2fede8f956af7f58245a7e0c5f04c0afba1eafa717f6c646, 2fdf21c547f1cfea97395a938d966b07e9e2e1e9bd7fc701433b095af4e09b7d +bb652d54f2b2b4dc2a832a125ed692a8614be973bd61c91ac5bff04cb5d2adaf, 0aa241b1de7f800ea73620f4b7beea20f0ec3568e7cf1c45dda720d4ff0da6a8, 68d04065863bf519df9709732bc1b7010b0a568dc47f5e8b371ee4a681832ad9 +2b92119cf8e35d165c89157ba365e3de1c3ce2413cd523bccfc62fc2bad8b5d5, b048c5b80184fd0f47d5a6616e11bcaf2a10c29287289ca377fb0b130f534316, adc4c711dba12f5198222896302e9a0086b00d3d6b8324cefb3da57bdf65c1b2 +3b5daa7f94445754deb91f6b20ff78ffd45e779ac26b723fa1ae3d0f69e6d5ce, 7fe349e4f7866d7dac6e67a431f3d36b5c71e39ec01da66adde657792659b6c6, dbc348a4e5a87e7683a1a0ea45a5f0dcc583c66cceb16b0075747be02434bef7 +69c91b51e34d889f8dd12347772fc4ec4c2958c9a6e99980bc9fed7d14dccc2c, 6807cc036d7a3d7307a1f5af2e2b46b90fb3a355f64c51f5b07e559d414a5db3, fb00f80fd33c06ff4b465f1f51f4dd34b0323e12f68e100f4e245a2e7e7ae16e +1ef349d1a77790a0ef89683c43b8e1ae349a3b16702798cfe465ce3562dc735b, 57b94df75deaedefdadad0038ca65dabf5d821ccc7ac3c008877e62f16e48755, 032c5cbc00c7045291f9478e01f4ea808ef65732f9ae898a8357d123881c5c77 +d7c7d05f98559a96837ec0586593d9cf6331600d375b3b2183278d0b5a481bbe, 63a5073ada2c71423665ba174f09685e956ee49cc2076667c1d09e0cda932f52, df74471882a5896affad3954aec119b321977eea87bde9981d2398b091103c2e +671d7863e5c38fdcb0949df4be78f63b9a3e5c033a4cc438f31c868b01fb4969, 06a4bf45fd91204cb63adef90ca63e1b34ea09d4330ff636594171299e4c2cf0, 944766f1b9c7db67f4dca1b70eb77bbe655134b4f5a6d783061323ffce0179a1 +d48b819124a6b6b63ed10eeb83230837e03af9f933860495b87d2e12da85a058, 76bf22593dc5f04bf829f3298944f88abc1cc4eec7800273ec36f0dbf927f4ed, 01bd711c9e07496c13c52847cc16881b323051c40a6fd4a1512471401b56c6a3 +c85560c1647756f46f51d9d84f2e435f62b969f78514084ff68444e5aaacf708, 9bc4aaea63e993911ecf80a3571cb045c43d6f87fb36c71064000a4df1d40cc5, 9087bae7d500f72f3fe3ef6bcb86cc72695ab1f1d4f49d83be2f4d38a9ca4b1b +f1997d4dfb24e6a9590a2f8a4e7d2f32bebb33194a15d8de0a18fad104b66073, b57b5dbe4ea3df9c25408accb70189046d701e05190b3f682c8de71e46b596c0, abbd0a285d7083b14cd9c096372812d73ae68e0b31468deb0139ce1423739cff +3b442740da33573c78096bd7910d1b4798085ed9af804472028638192ca558a9, 83ac8a0845fce2c8e1063e4d3116a545ba41246d045dcd9fab4c3ce3c4a757dd, bfbdf733d27f0d0909ff0970947a407519ef22819813e8c0be4705db105fc6bb +66c25f8dc223447ac9ff9ab177674b943c5b1047addfc4b1170380f36ad62161, 68cd1a1d070e331c7c2b2c2d3c8a05e98ffe80f82e66a3308ba582e32aad2e85, e52bf82629746aed5469a4b4800d8172b3839cad1e2dd070220b748779f15e50 +6437cd6386f22defbdf98181441c312007b16301c914a2084e32d240fabf1056, e1a4ce711e89a4af7ebb494098f8a262e0c97974bee31d13f1302d061a4d018c, 9372804bf22743984c8bee5ea58fdbfb2a829ebe5c458b96ab02eb8d297abc7e +3e8c474f99c9bcbdc8dc869772fc4644c64c3bbe6e152966d82fd1a67d45a432, 41cfd6289c52e4b6c7e8115502d69fcaee12c9eabcdefa607f02de4017871d76, cbfdb22bffee2068ffa2d8582399d00985e19039b7bee50b9409dab32feacacc +2190d82a7613b6e08bf5931b2c7e8bbd75df264ff3adc5a6310513e635ac88ac, a6e99d41b00344a468aa73fdff6b06923ca288d56ef05d8c649975ab019aa2cd, c4e3c57101957e0fc445545e1ab5f931ec2fa96cfd6cb107eff230f2af6eba9a +a985de9c06d73a7d4c0394dca3ba6816cbb9e6ee8e623c9160ef2be3a7423b46, 8e8edde2b6915209619beed37a1b68b5b6a4ab8c21c1b4846666db63ee66a750, b8c9de97cd1176e34e38468b4e68ed92573b6ee886dc427fa544b6ee79336045 +8d237e125f86941f76285d170dadb07e99f94f4d8a98b249ef58a7b3e8d7c32b, 014412134645b1af15457e08fef9a0c55b8464ea0f6c291cbbbc86613d7f8e3f, 9beb3186f23e4882d9a83589e44671add3b827699ec3c199f4daa17159dd4a66 +2c19f29b5cacabe28421c225d9b3bb579d41572d3f59d308cc3815803e540033, e46cfb0e0fa60f8d3ed03b6e50135d2fafb3725bf7ca4f801c3497017c7a1cf6, a71a475611d24d09cf08165c7dc31a87b7d382b40ee0e1cf13793fb740fddad2 +38135a8e98165436bd461aafb3d437109dca6434ca21769ee02974a36508c169, d0798ce03fa9dbe41a130f746e87bd554dd0267dc7408ca995768b14cb86cc52, fd1ac22a8a8b984fb78f4eb2dc4c93e4a6994dc7b4ee82e5a7994faa947a8942 +40e04951b7f274e0d0399fa13223228a55a7f93edd1e83bedbc23f9a8f7ca4bd, 1a1c4baeabfa07fa59ac5b6c4a6ba02ff3ab045123ab3828f803f322b458ad64, fd89278cfd7a612e5b833e683862b39e74addc6913101b7dec7fecb2434c73d9 +16ee42b64c00acc08eda8ace95c0a3b176a27c7267da4bd2d4b46ae8d9253576, 04b544136e1d651a22e7036fbea9cf04d74ca5b5319fd1dfcec20001e605da2b, 2beb8c0a4b7d8f3ab9ba071542da69ce5bac46058993be28620c628b4242c4ea +edfc59d3b550d57989ed565414251d1a1b05414125941c97d0d03a79c6f4430f, 27e742a9a544bfe0178001e6f7b3edf820b7dbda4bad2f73e5bfb525ad1b4752, 04e238047d2308e1a73a7308fe10b6a642b1a20d8ee5cd6adc805dc2730b2b93 +ffae0dd033c6aa4be74ef3c4e6b04a71acfc14bb540d421a16bbbf059449db74, 04457f05b2fc3ba713e8bd1f76ca526267cab6d2ec9522d0047a3090310568d9, c6b95f8581f767f611f970c03d9fa85a0c45b4d63933679491dae06af8823918 +7ba3f93b8ca7e9e1b6ab6d984db7c0a05dad1619e4fb91850c4cce8bb950900f, a1d1811c98f06f3f0e75d8aabfc1eb7abdcc2a71ba6a32ee749b6a316b346e3c, e10ef3f5fec9cc9561c86b1e93a58f604429b14c1ee523250b98c49dec558e56 +6ed99eb8402c44f3d5a03f4ce6e3543fba71cfe42f8f652fd68b4515e9f629a6, ea19acf000f2413b850bb7d5c8e6005b91c4dcf3700bc7df032928fd9416661a, 4bae703e5dbf4c2bcddc1d6c5547c71436516b04f976e58a2bf30510ba96749b +4e132ddd4373f9e0eae8ec4ed5bac6fc2f43b93a2c6c0aea4ac66174cdb021fb, ca5c5bde6c95fc79aed8da6883bec403ce0cbfcf08363d1a59101ec4641b4b6f, 6f8cd98c30f94c5b455ed2a3ad975bc13649b755030186ee6dd9c64c99ef0bf2 +d5fcefc5c435f4e1e70227eb34bb5af1953e71cabc3f646b192979dc03a41085, db055a0af87a9c703ba827b3f8c64736c112f4ea108f6c35205848c579a4adb2, e1423d2385914c0a4ddecb70b97bdd9b843bc9effbaf64481cac738d3ed4b7df +612009446f4c20312fe0df3637e3072cdd90a065fb4aeac4a3d86c6f0f4fb67a, 8c67ebb73154e252a6b77a9a471fa8a9352c2903e34053cd083a3e4c9fb4c5b1, 9098b300e33613643b837a558bfca72550e0d1133822dcf23508bb59e7224c54 +d91fb416ea1f1197bb728b32692fd645e5dc8bec41d360e9761d914d99490ea0, 32291a6e92e9126a6782e2c6e2d0b8223b4bdb0fdd3598dea08bbe29b9e586a5, 8fe51dfa1f8120f7da7ba7b406431f4a2a1639851fef36946d1a6cf7e0daa2f2 +ddd8791ea954db6680be03b6a089f572fb8b22065ac0870e18299b9552cf5ca3, c412cef8fadd10b4f0cd832219a22c79a743aba82e26e90e06a9b511264985e2, 387a605553e78493eb71afd9450bb1547303c6b1e38452b83d0900a7c8c238bb +18d915aaa40de409ec6a74ffc968bb9a41b97b90961e7cb53cea87ae0a1b5d24, 7c98d539771e9a04b5b4434b0dffebb2dc7e8e23a07e29ef96ab0f7402812f55, ee3a087741191640edb4f69ad329ecb77eded29c571eca82fe34837e2821174b +3e42df1404b84fedb7dd5029b87bc1c7a5c3427d0fd3e90a35f15891e6bb451f, 30e99ff1734f01d021f21e8c5106e4ef3f92a56d02bdb33a997ea5b4a82f224e, f0006fa01d1892f9d7fc5f785d540beee1def097f24ec306d28fd15a63f5b358 +fffb261df2fde26b9d2cfcaef870b9f9799add62e4cfaa790799ee867a624c05, d7712465a51ab82ee3fe67022efd142e1bbfb819bf4c3ded2e756a752b52d25e, 67c49ff22e5a9cb3839a7d7760280bb8c3219c1409b3978232b2cb04920c243f +e5db5055f53f58721535749a7eb3fcb5b6e2c48feeae0b8ed2d93e09e2e22b1b, 3774183b8854462f1e911a6d8da910bf2ffb91d7f8af7c11db66c95b58dd9cd3, 0570c9425fee18e5609b6a39b589f7998c4770e4a453a4364a5369fb58f90d2a +b206953348b8785c450534310c5cfc351c4ecdc02ec408b425e248efaffa475b, cbe474ae2cad669b24d10225972248b4ffe0be3a39186df10bb9e9ec25cad21f, c88a4f10b22973528ce7f6e24c9f04936f43d6c5d84fbe8b1ef7086421ce8745 +aea0fa925794d1eb2a21f7b84a556f76837d6046db11a9a855325521c70173d3, 5c33ba82050d2e51684616df8c5bb82d4908819fd3aab874bbdceb3071af0cc3, fff2c20907d32486f39b08fda0cde331cb2dc6793214cc48b5f09a27960e0c44 +e7320a78f3a447e0b409669136fcd937eb6478d953729c951d2cc7919506be6b, 0253a18d19f02ac5d2bff024f7dd814e3c38ae629bda0a98a5ed91713ccf05de, d657ae399033a5273c785bdb664ab970787b4206e55d31fb7ac3796624a6114d +7b726637913482546d96aa8d4556c523020c5b4d9e9e45d0197459a07a24d11a, 688f378b72cb6073687248f4e86701373ea156b861a15feda7c6503c625193e9, 9c01baaf226d072cbcbe8c61dce11129a7845015c09e403ef1cb4200cfbad970 +00c9659344c2e04979805a7d7dc4b4be7f7273a2f86a07052328bf3da8ccfc77, f527084c3fd7c25d8d59a2989ec7b707dae9941d73fe08ca06c436770bfea677, e3edfd20273a4c5fff5ea7e2613a1146b2415c8f57e1268b0a91d2b7e87e658d +a286f98a00ee27b018bda51a8f15930d36c6f01f8508de26e84e0ee99c736fef, 74c74307b22b9202be81a19e9ee8035b77e8353b650c91fa3bc7952c575f7fed, 8b3ea1be92c9400ecb0b7dbc72cea9a89ad338570b8110e3f874a13dca020516 +2d95aeda6886174094a284e5947a354c219db0974e4d10e03c04d9a13fb58080, ba2ce98ebab47879f4ab38bc0969be400e347a546f547124bb1c96908edb587d, 90b89d99b9c1b38258a5822cbce1f1bf199c5289f0a745b804ca02567e600ce7 +8b28956d345aae6794463f52550b5f0ec19dcea544534acfb78f507f3b1cac9e, 10516002620df4f9bcb0cb2ef7ff765a6c2a31cb5a0d93714bfa54df62a47421, 1e7062e9ca9d24d23cd44fdd8084a9588daed6458b31031e2e9fd135167f823b +91a2634c96dc25ffc0c20849ec0279c6aa4f8548f663444d5ec03b669801b51e, 2033f553159fb40034982fcb57b502767ba892f204fbf4e63205315f9e0a45b0, f2d1acd248dff0dc24f866644f6018eec6527c43829fa9bfea46b23e60a47865 +8fb38e48e61fc9b25da9cf85aa7e5059d97b1f260d2a4bbb3c104b20048ef8d9, 3f31635048b0548c87d5de5f1b3559e3a5c78baf833fea04deedb1ee22e17170, 59ecfa0f6260f8c56f56a126f37c39fd63ef4e5e163d65711536221cccbadfbc +a3fba6fdd1c9e2f8c5d2b688884a042edd375a83176a1f088f55349197eba526, cfb79b5bc201d36b2b1d9db43b6573be4bddef16da25416af66703cf41779a28, c1b27fa7b78e9e7e0f7ce4a0436b1d0e582138ac545a286bb6b53b1fb046b419 +08a4b12dde32a0fa7b723e68babda5b72e52bd4f97c2a18d7a49254484b0f589, da5b1bed232e9b3b03743ccf2d1af84ff92c5301e10ed90efcc41c9552ff3b32, dddd736b6fb4b5d53b4b05d3536ff43736f0e93240ef1bf6a90635a8a3f9a83e +8b63e70d4b3f2d44929134ab520da543571ac993e61c3f63107555af7b2b6f7d, 7a09c7295134255198c1aa0380444f08079248eb9592ddf165f3d75f7d4a1126, 5ac21c27b4ceebe8da1eb63a30636251423f62c5b5a46f17348ece9c4582d1e3 +f33701ab7e14a617f519ec8e08aa59c9e62e3e1fa36d869786ee8394beca0438, 3e1c7893e55763c300c565cdb972a3aaa3de19e0fb2a5c413493d58f0341a9c3, c3af42a0bcb94e141add35c6a92ef46aa8635f04120f1b30e0e26e964d4d197a +031ac5ef9d2514076686cf8f5ab6b4c1f0fb3d8ab56a6ce8fa7c9bb51c07e397, 400fcf5de07d6ba01004413ec81e10bec19e61966dc72221d367e14b350f2960, 1e2488587be279dfdab5b98717565c6f8848661360ee6bcc41f8dd82c2f1bf89 +0233ae87eb9cd9ee0d4a0acc8a9de4c90046cb8aaf69d53d40e852dec95a1b2e, 46da515069d2fa2c3e246e28fe7babaa0b9f41ce883055fea18bfae830ee37bd, a336b9d5787d5fae904cb804b295c2b4a331cd7df64235d59dbd612a21eb5adb +74fc94d08dffd453b33b010f685347cd40d49ba055e6508721c5298dca22118b, 194ee590a21f7d5cf13f2e49944d10bc3b1da098b85cab1720afcbb52dfb137f, d6563ea2f94f7f7dfd6b5dbb258e18c1935efc8ba89403317f51ccc60effcafe +d95fa74737926cf8615fe4866afc82603beb91681d36994da75f25e518a0f8e7, 50815b9992ae5f18c90e049c86b5669ade316950c2c8f29c2e5b591f0af34171, 45d3e704861f7d3d701bfd6a5b9c1d6c4f1cf2d7fd0d56f7b6652a04675010f5 +f754b45b3e40c1f69f20c916d1ec18db5c214b3d4aa8d0a214e87573d240d790, 167c5e41814fdca93c06c442fc816b86c9962f9d6bb82e7970aa301d486b3797, def836380127d3989d414f6b25aef0fe570baceccaf644aefb44e5760c120381 +33c69f3f444bc7e6cba597068b0e47aca608dd0dfc83afbeba54df250e979488, c56f6d0f92aca6e1b05aca5c80f51fb5a89f0eecc7f239297eb2bfed13ace515, 09e0f8f19d307848de26f9397774fdb3b8d8f0fbf54735f4838e2a031f09dfc5 +c4c7252ba51cb819e3fe6ff60db6386389bad50a7b7372bc47b13f9c6dff8c25, 4d0d06381cccf27f4f64ace2c9213c01dc2841759ec632be68a543353e910a5b, 1ec9b0d4be3a7048323855b4ecce1a4b38737faffbdb8f6bfc839e9b6de18e94 +4e2df28699e7a75681dbc8aee002996ce6b23d600c8ed95895b5443345000343, 7a63cbee05611cf86e0daef7fa8140dff8dc930f3ed021602a9a9dfaa94895e1, edfca78db50f73ec0c3c94e678e9407416fb940ce2fa349c1ce6d2bab652e8fd +365a8cf690154f641fec749ea161f072e418df8373d547d13455abc5d42edc55, 7b19592d64a49687042a2bac0aac5f529c1edddd1a8be2af73a47dc61478c9b3, 6c817990d837cb18d105dddf31fba923f5e71d6302e6cb7fda6b120fbc643ca8 +2e085510c9534106cd71515797f8e54177bcb6bb74cd0623076372300f4d53d2, 0bab06f6367729d80a6e1c788b8c85302388486b00af55b7c055a616b77997d6, 0094443b044a0037cb7ff68a29f8a6b3597550383b705d003fb511de585caced +fc1888612220e847af32302b27bb24a8ca2995329dd3aeeee069bfb27bdcbad8, 8f7098230b7c6384887dde532e1c86c91c0251f5de24c8f646ee45486bd1ad4b, 072bf5952577ccd64ab47b4521d9a8f25e034042f61ce2439c0e2bc5fb097940 +9586350d9c716fd4cc7c0aac521b6c7e9c1cfbb1876bc6d0674bbbd3db32b4d3, d420d9cd010e1e4bf108d7cfe2162b7b4966b3546f100df3798b078ede59c14f, 8066e889751490cc108ef9b30067fb76639f1a7d4651952d15f2e6217675380a +2d492c4043080ba92fb9260027562cb63bbb30da2b68fdcf40b7cd362fe50298, e4a129a536832eb203df1d5270d0660cab78444fbc4ab1246d305b6b0887301a, 5adcdac69d8391d9f83e88c08965588775dd84593c3f438906ea7518b61097ab +caf617923211e211a42b6457c91e33bdf8bf01c6136de35cc60d2132af47f238, 8e890ac754bdb3f31ad67081517c898c55a264fe50bb2ec1e12be20330437115, 42369f8e87421fa50a2fa2bba84e6bdd95c33e31a82f3d6a197baca290d8d0dc +ebd4b6ec53b0319f8b4a6eeac2cd3bc69ec2a9d49f82b2a8714fc5883b2c1846, b81d9d3f8ec1c23a688c9eefe0bc1ffcff2ed9a3386620e3f15d45167c649b8d, 145daee3fb0d6512367a74aeaee1e3ec155d6e4b545bc95f2eb06a428702bab7 +394e6d7dd8fc91efbe4989186c82561d26ef9b84d585aaaeec34bfbadb678b45, 492bdc4b8159ee576bcdb6d6aafc790a29b767579f9f6b078b235574dc316ede, db6134e5ecc765c7c8d28ae4a275b562183d7395c694257db718019d3b5a20a3 +fbc2a815abe0d3355870d460e2a27565bfbedb5c6037bc2b20fbd77d2accecef, ca6f6e9c6a77a1291ed524036486acf0a692fbced8cccac50ed9640bf8b091e8, bbebafb312ccc5166cfd2610b17cf77de6a60f15507afe4a23189e46fc04a950 +426eda11cd697b540e07e51a7091d5fc43a37a495c1d79cb9a859184472ba210, 3e60aebf040a74627827ed5e73f38a165b5d23195c5db020b4b8c11b78a31b81, 538aa5488839ee0a84947a92b353e0bae367980a61fa8556a7d391e9cc4dab1b +6fa4c7f84fed8b4cc1f329b53ffa9e334ec9bc904b7baa8f98b18bc940c39b54, 5851419774b1b646dc7bddb747af46e7065cb861aa983c0619326b79eb3889d7, f276c4c2ad411f52338986b8fb552bb7aa015a92053be69bea41859e28e53bb9 +c4ed29415a85afa311cc83d8bcb1ee5ae546c43375ad87163feaea22efdb3e99, 7ac48e14f3761a7d4af8397390f4981d9a17e689f675dab087589747d7bdfd5e, 63a8970c1db4b5eca0b90bc514aef207eee55f4edd2545a72b03a0f444f03bac +a1a2200e6c911d082a01edfdf93fa6454124b65d7ed1d11caf2764f09917122d, 58bad1f03326675b8c2a9988cc8b1fd01a9bc82e750f164faf4c61aa71f6b5af, 4e6fc5eedce6a7f5e73d20b416ca7f90e463775f5c5d3b4c2124874203590da9 +2e34a3b67f5e3e9759a40bfc3fa6a7f25a356b9437d94327e3f392f5600d0829, 0bc71cc04d418ea25fa0fe038e02c124ab9123a7bffe9cc182e0be29859a8a03, def995f773281ad36f9b3949f2c16f196e4d1d062408ee8d8bd5cc1d9ce4588e +0735c897bf79cbd78f922938e7814e8d102f3051e4600addf528b7730bb0d03f, e28da2625cf1c60be14c81eb258058a8b64c5fd7b19c8e4e533fabbfe21fd0c2, d1aa80b8ab1b9283c9b377925468f3044928df788b798ef9e52a85accf403b35 +619d6a9225ff1c06d8a4ae9501a4c719b315c312a753d42c9b84bcc612ec6ef6, 1b1ec878207e9c2b6535def957f940c66b34c5b01766f9ee88ade9c6702371d7, 4cbc70775126793bc4b445e5f3a76042be177e532ffc5dee69f6e42e9587d540 +334938005d6a2ba88ca49cdb767a70c596429479b01935f293b5467b3c7c3812, 12debba46841b8d84510b32921b94f8b9d3cc9380078b1df32d92e0b9239e8fa, d8e9743f12962443d2b5e0dfbca6b6298b21b265b6130c59f22f400226802fd5 +282b0f2f37adc23dd63cd37938f733246debcca0641f04469767c6bf0783de99, ba868bcb4cc13853cf67f55bd285ef51d3ff9e27fc679c00c13c411a62786b58, 6423e3b0c6432c46c2880ca9fc349e155d14119e8472a1c746de8a0bda44f02a +4ef5d58ddb8467b2223f340b700735676c44586ae21172de8ce62f2854c1ae2f, 2d354728f94b8dbe2fe4474c01643c32ea16fe3bd81a62fe8e81c4488c35a2a1, ea189caec57aab58f304a1a4bb473ff050ee8d37645e6d5e1ba4de9797447122 +61181e9d743e120931aaea370c74593c11826e1021df69bc2a985b4f51f4aedc, 6ef33875bdc4961ec0ba9ad35f392389f3e7e8e0af9cabd7bbb13b7bce503303, e39ea27dab83cea1487f805f3c6d2032521da772968f4a21c983718cafc2327c +e46978dc9010dedff0958148f248342238bded7c671e4220595d1bfa7db40722, 23d9a35aa98ab12beab8b1ffd4ac43fb9858e27e5cc7869ab8b5be136a715adf, c720d6ed1bd6f98c8c87b05aeeff7021b3f9bfb531e0b64f8380a5271a2fd5d8 +8f2434a73c24ddda7ee5d721edb4811bfff781d837f0909200cac02cb0d2ad84, b9afaedefbeb7bdd494e23ff9079277c8942c864d9bed73fea6e0d6fc1724a3a, e0e7a989816ea07ebd38776e5cb32e34e912f81f8668e9a47d5e569cf182f87f +4eca7a6981024e68b7b30ad6c4e370a38975407a38eae1ebc5b674c7c5f085d7, 3753dd2f21e6e0b1967f5aa515ca272f26f005067f8057dec34b72868df79b11, d9a197d2aaa037b2355717d5a971d44ea50259e6194dd0135e4caa95c73c244c +c7a43367e10eb1489ecbcb370ca91f50e8ee388713886cec0ca990ceb1a69dc8, 1f5535267274d828ffcad61fd177e36cef4e35e03ad0e2a5cd48a9cf32b6e48d, d1b1bc00523ca871f860489bff5151e8800fdd46fbafcb0a72e2c18bba001ea6 +f07544436356d2714033361761462cf5f6a07701b987e5829c3f848c9c270f8f, 660743be83554fe25e825ed150ab71206b912175b841328f74b0ff1dfe7d52b1, 062db1aac0c015093a1e71a865ba4bb1e3d3383f1fbe32961cb39e609e39b021 +300425c842629b528f788c86bacef74951f08277500ed12a5c9b028f075ac87b, b9ca7ea7be2ed8f2f4077b1ace6870d11d012d3328ae370d6dc86c2ccfdd8dfa, bd066cdbeed5fe770cb14af422d6bc7ed6354692d51a23581bce5930d0895192 +5296cb953b06dd5394a5394a1ec32ae04e820ffba7b9f112cca22089dff6ec4e, d9b93138516a4bd4535af2fb3557e9f30d0e0b3bf72157c7a5a9f1f23ef5ba33, 31ceadc59ceffdeb4ca02e4f50d65032e9ce0411eeab9151c854ab87c3adf562 +8014700aa3976c4ecfb592807a1aa538f50f275f597a5c6380f0a45826b0dfd7, 90f1bba00230dda4e05b1238ded5fac83dc8f27f46ea2d576829645e6775be91, 2e2e0178cd1e98d8ef76fa6b569523ebde0247396b8c7d1d707fe3281874bebb +92095bb4e83ce23d1c987de87f37744b562a57f080b0886db445cab7b3a243d3, 7818d7c8ba25f5d982657e3b3ec2b6a95011d6fc9bea96199d5f60edd0e230d3, c40d9fe26a6f80aaf4fb405f9bd69a52c6838831f1e81fece20c73edc04969fa +eb79a1a035e42015526b1c10670f88acf1adb4d96d6a34d7319a196639810a31, f457d88eee0121a8ad6c5084ab625b7ec1a9f2db7aff8a1893a29c7d32d55c14, 959e92622a733c5ba24b32da65eeea2e28728b7d24cc66f9dbb2bcc00b9cb106 +e84aea96d2f72ed7b8c1ecf8d95e1f4a5d4fb04874809e544ecf011cb7f333f7, d8d73ce6d2aaf6cd263f45da60172bf4360651b245d4ad46e105d6bc1effc756, 14db2c8cb9fc3dbac49ef7c77ad85f2516e3049c8fd3de6ed7a96f9406fd59d9 +0ba5e93577da4bb0898e3f922484385828a1688232c4eead3b5be632654e76aa, 9ee5e7fcea86045fe38154f531098f4953ad26aa4fb0e74e3617ceb7f42754de, 64069e17ef1671d9a5d50f61ca2737b113d97ec32c2c763a26784bf8d90e6ce5 +30673a3dba01a0c1ccd57dfadae3bbbdb5c5496348759b30c11c379741d5ef06, 650a84518dabc60266ad5a14cefa7db34f0e6a694c71e38b1ce55b980fd8e1cc, dc9720b617ae9529ac18e00f0262949d5186f218637924949c96afcf73e5ec35 +f4c4affbb72ffe03852d08064cc4f3e2e9e826af25e930973dc7019d11550ba1, 63b250af7abb6062c2385a025ec57eb929091204eb419adfce7030d33bee3859, d9e4ee29a00bf4dad7250f7739dee68d2bc2ee3d8302ff0c94f0fba976c4e08c +9ca9ff652ab43f98f5050af83b54ed94faf9ece57a82486c5dd1cea0f97a0e90, 965eccddbcc7a1f732c5f9018ae7b8cf281b8e72b14f71a964a90118c1e65026, 5f140e5f14dcbe12ff6283a4f7d0408ab8e87342f29c8848b488579060690039 +5be1693cf213ac3f0229a5de0ede913193ebf283e368f932eb52ba377c82ab18, cb55e9d16acb1195f63d40d496f9dc90642043667ae33958b2f6ea10510f3c75, ffb296ab8c16631e569afeeddb48f9ccbd5d2377ec3b95d8e0211d3df09ca452 +505d86f6a370e6631dc1bb86da9345205181f76cdf3d47b5905dfc503c4449de, 84f6f8094cd4e6d9fdbff34ba2eebe4c47b20d9a728412d4867e3c6b0027ebfa, c537f91cf2176785e6a687324e3c73dbfca5db70696432a0aebe07428ba475d6 +820cebe4b605852fccad49e89ccc86a352570ca1a324dd9a78631d237652fe7d, cb4583b7e20ba096706ce67a245f5ac29a83b706bc50715bc42c72f629da02ff, e2943d4d3c4672aa25750179e21c62ceb6a9314834d77753f65b6ffdd46c86b5 +1fee6c610b44df77cf8557deb5b7944248077411b23f38b40049fb7e8a60f438, e3ae538520c127315e45fb0a9d252f5fd10beae05df12fca6f7a779986e4710c, f2a6b6d10e82fe2eb9ad71164cf4003be2e34510c45a977349ee3600c387ea00 +d3949770c54f874687fe99aeabb170a4c60f15ab7297b0b21a0580767765cd09, 125cf82db1ad008c7bf212ae95d01cff58d6bf856a4c3e6e873919ebda8a290b, b5754e94d10c7636ab82f58e9af98b1c4206f3bf1baf27aee758103f4b04bc8e +2e72e73a96550d8c6ff0d2dd7e0a1ace0519922e15accfb7828cde87bc7e6fd0, fdd4ea21f55350e245590dab339942ce95ea2278e7988ab27a4c66e531f0e5b3, e328156a094fb1bce191e2e04100c161789195a5cfa536c9df68d5532ab07b21 +f6779fee18551108f61f108022353c51017b0b3898deb802482f21f9e717e6d7, 1f68db435c25f06aa2ca44ce670b249a9eec5ce43b69988bcbc28247a558a6ff, 23d1ef85687bb78eb9491b28a7fb1a5709f85fcdd61295058a4ad5ccb45a87d2 +5d19a19603f2f7b676ddc0572233b4659a2831facfedd1857f9d89144f26bc83, 44a7dd57190a7f74184a126a9de6c8c698497b6ebb2f70d75f99ba22aab55764, 2cbff5d0967655b69e49d6117616511a833c531c56017d58e816a6c919fd34ed +a6164d2605d3c5a76c17a81faf9e716c9c718ad6ec7f224d74d1a9842790158d, 79f51c516fa7e73e52b1c5baaf84c201a3f548ad99e0c3c6780459e4f8787871, c379e470fb7badef23e576709314201a025eaab49e97cf8f19d9a9caaec5f874 +0480bffd2334432f65da56d975189947a1c889028307b32fba08b2da06361fa1, 60f6b69bcd44e5ff31e3cf000fd9b80bcba9d255db4c39718412e62fcaf9f79a, 6511de28d7c070e4d547d27f0020298695101cce37b1c3119af1f23ca6f9ffb2 +57fbfe1dd44f954520cd38a301e77426b963536f39ea25ef9ffb0f924b9bb38d, 3a46a05690b880a09e1a5e8ce809abae3a66faba9fec589308469acc2aa81b7d, 9ff4d970b784f47145659ac1dc0be1a22ec5269355aa74e9a3e0f0f83737ee47 +a3371a9a82c3fa055458b73e789f40991207abbbd8d710aa96ad39c035fd4f39, 2d12223e30b9807e897ff984c9118031ced0e6632b98392eee1203c7925b1f14, 193ff8cdb817dc377ceb2f05ba9824013a403b00bbcd9d1b85fed8eeda452cba +3179ae84d6f8b35d796823e07570fd8b249246772fa6de00d79da7ca805ed2a2, 182f49bf739db7099f7271dede0814cd3a31e687313294ff028e73546cd5f126, e72eea743dfd933012b266da9ababdded34b8133998b0eb18e6177ef1750f6c9 +0f033754c8db71856995feb6266fd7b87260ed8b62f90479a9aaff496416e7a4, 3e1fb24dcfdc0c0771bc064312cb423a17ce0973ffa36445bac23974baf01ad8, aab125ef16c876eebb2e4dc1227fefd841baf14937294a29f4d3e9487acfdd0c +5d22ed58ac04aac43dd29a9b1b6bcf01abcdce46c0478b4c8219b1152fa952c0, a26d7e934d663fc01b21a7f2a87f9d6c4e37e5622a0518eaf3f74d20d9396dbf, c334d41a01ac9b3c89058e824b3d9a7f73ba622b6ab6da12be50afe8e5aad161 +d42190e6a46905ead29989e2846b39711770078bff4267442a5478272385ffd7, a4a1273f5d1daeaab49748a20a32b20a4df406952c36c5feabee53b2803559b4, f61584f3994c0ecfd0cd6692802dab68285f145e5562db2033202f1c24ab81a0 +6244042b8241aca12b0713b6b4f6130363f3339a292241723b05661da58e38bf, b6b8e9c79ed467e88523ece1a033ab3e9ab588014488154a0eec7af8d82778e3, 1905edcd02554e04aa7d982d933f138b257115ff2d11f9e53eef8ecdb8969839 +a8c9b906d8181f93990be961c3794980da9f43f2be2091e6a802e5b362579ef3, 7aae2bd149ede04c6d51d7b170850d62b7f0b7af8347497d0a33473bbeab2d18, e30711b0855f76550177348546f3f320e5760037bbf47e87d682dc9e907f47de +96ed0fcd5efee98b48c8c693e6e90f060454486b3827b7172ee59aeded9e97a4, 896152b00d3bd047a0a3a035b96c9874e26633243e945b0bfa42d1a88b4f400a, 087c7113ecfa43ab960fa760f0c8fcc699bac16438e8857853c6669260bde102 +25914744539f4633d221009f44f790bad23d021d72405a8ca981d82cdae429e2, b2ce79118037d0e96cb0265ea70556632fe23e6cf45d01c572b02216c6b2cc49, fa3f12aeca52f0862799ae46b30875ab44e155987f00e59b27ad6a17b2e56a20 +c43caf10826ff2cbd37ef25343041e36654d97071f7408f5952aa31a691265d1, b9540d1ecd88515a8815414f757e1d8cdd144242fa336daaa48b97044d629982, cc63a6b375092c9325bfb5ef7595b73861a5a714a5621f8e2cb76ac5d6a3aaf3 +28acc4acfa7861447dc0172ee7865058e4b0650e807ccd5d90619021221e145a, aade4c93c4cf893f05d0001af8e838d1460b9b2d731d097aafe8a03cae16bbca, 915950e205d758bbc7db92506f403a60f1cb01f3c4eec356540f5358a02389d5 +9a0f49950cba7712df8a4bdd8af656f057595d1cea80724501dbcdf2178b8fa4, 3eb4293787d40f7eea2bfaeafb79b7d73afe8c525a24d24faa0cfdebb815d0d0, aca77831c277c838b91b707ad4f1fa7e944608254529d69a798667d9d57e3ea4 +acd7984fa5578709b8defcd8624e0437fc1e4556ed6e635cd39dca8e070981aa, 6869ccc989fabb82421ea3f2a9f3fc3d84483d423545d348640cee1be4ae286a, 218ea5570f3d1e72000a8ce5d5b1572c576510717a86c2dfd7de6b5ba9f7a906 +17556c0da80581de3d4aaf527088400993bcab8c433b969fdb50ae00429f2cf5, 3cebb5421cc94cd9188cf2946fc8dab13e0397d6c94d3376c2c672422d9b064a, cf47b7cf47c06204ef6036e1ed28a26ae4303cbddc236bddbde8547743bf91a0 +9ad27082884403fa12889c89287c267cef0dc3553c7106d937931dbeafa23314, 3538160306662046f4e8e3761ffd4fde7b08e4859785d2e7462ce8bce14428f0, 3eb2996200c4fbbf206a09f28fc1b030e637e15979d72be6c154ac2685b43b10 +0050831b490db7d800f6974d8ee02ed8c01f5d495bd63b7503b2ea66adc4713f, 16da58aebe62b23b54e9dd3c7fecf6d88cfb44d61bf0e4a6128dd3c837b48218, 8d833c0a0da01bcc0c83131fe43695b0fc22e19446e20adac7a0270b6ce791f3 +459a6689f07312c67bf19df5f6e8e17ba7fdea8766f47ee651ed7e8bbe942891, 7902dc6c43e72d60d83ac6e5da6cd46d06f43a52cf77fc62545d3274d66f89e9, 1812476f801fdd1844c8a695827edd4473eee13b39bf5b1bd23b598d6e3cd8c2 +6fae52ad40bb44f65c26098628b0c4e07e380fb915d85ed144d4c37d224cf141, eee63ae8310ca61e6dd1bc62af1bbe6229c62de719a381e02738dbaf3cc04824, fdeab6e0f003e72a0106412f3129c3af09e22a6611c5c39424838e01fb82f309 +37bb97cbb35e3c62b36256213e6167afd484070605affe61a8c2ca4e7401a791, 79ab02eb919cc2b23c9ea35d4f2ed50f9773b3ae02e819ee449a5e97ca1bc21c, d165917daeec366fa2d104db9d3e59d3ace9eb6288b23a9dcec55221b4f4d8eb +543e09d6ae60717230f38c190779bff9b42eff4a183e4b6cd36c5a58396489f9, 2b6a52c794e84b57f07d8015c8a937116caf78d5817337d3a01e6088183694e9, de7b34b6fa7581b2cf90e48be298e477f709ce6ba995318a3892016deddbaaad +db4ac20bb0287e4942286836cfd5dfa5b623a1f8e2ed2e5eec4012898823e63f, 1173ec2d291f894d35f917a8a09c61a61440acc562ad9e35fbdb7744dd52c093, 4858e75294b2721078443ac0f41948962d48625dde7ee7687dec1b4d5a269229 +5ffeea10d9b35db9fcd56a588008bc092d3ef4bcccfdc69a2019605bc06d2115, c46005eba6d2764bfef3cd9d06ba6d2c6609008598cc0d09d2cc8568e55a3d05, 106e9bb7b9b3564073bd68a54d0a71e23f7c647c4c7d832df3eefa02b542fa08 +fa65ad48a9378d16306123be50084f02221088612e8459abc5643f8729e9641a, c54e7b26ac578a37840d43b075811f21ac752222a3fa97887de9e04c5b166594, a008d22b131e9ac1e2f68f65704ddef0ab70177eaa219ccf964c64efe1c4e99d +df5422b3bb4b3273a5ee44fa48423e27a04fd2ef3cdc9fd66c11c89175040efb, 35029d7e88839176ff40e0c25d1a9de4e8792944b4cab1475aa669f19f26f658, 467f2aa7cd3d46c88cf0f1a7aa81e6c7dec5ae43bdc840104854936e5a6a2059 +23d568dd7db2dc313122ddd5e05b025e28993b61a70aae704ee4ca16a62b71d3, 2ea09e6143aa43842b46b4721951475489199ed0b261f7aa677760a3a39cc95c, 1458ecdd053115c2a08902800d1071172d778a8d77cfb0b62674548cee862df1 +362f98ecad56c72c71eeb27d6ed5100e89901c7430b5c1fcffe81d536de56df8, 18637d9c4d3a77ff610cbf1b646c5b249f167d2f2db08129384c163b236af18d, f925e7881f601b4294edb9a94eed85ddc463553bd850a07a20c8768fbfbeacc6 +0d18259fbbd74b59f0300a9499e6cb920b88dd3d628a683ee0a7fe6bac475437, 08fc6feec5ac0dc2e3d8b5ba6154de176f53e818984485f1b1f4e96c426e6cfa, d29af198c241ae391fb5fe7b83bf168031010c030202ca76996ae17298487aef +bbedd62627b4dc692e6139ff02457ba44a7fb7b315143de2308be310b3e14929, ffa7b456057eae291af3ac33bad6f9079bdb73c69b534d811b952adc9feee727, 3e05c7bb1142e632659fdeb3133b9f8c48a02de7f4d01ca3087d75714aec929f +fdb058f869ba0e617a4f8ae00df5fc195e4441c6e1f0476d787a2f61235dc696, 059b892d62476b217215065d991969420f81579cee7f20ee3a54dcc695a45a99, c066cd670bc01d28236247f0a81146c5788e6ce38dc0e2de1e1ecf23c62c0b5f +74394aec20b53636f4eed20c95b9ccaeea3fa38b89d61f137c90e17b56407d5e, 58a866a04d00e8d2be7bf2af338efdad24483265452185d34d91e212ca16dcd2, 109ee356cea3cfaeab4d905712cb16aba419cfa778b3bb4ec6bda923a0a19e6f +e55f5763b84825bc507851a76ddb07974c7b87b1fc247377eb27f711ceb12b95, 512ce80786d31722630dacf8632179718f453cb572b84ce93548469f985b9f79, ba090360bf533cf299ef28297d03ac13465fb190f879298e93dd8850bbcc1c72 +cc4c0b04af33a92da24b75ffb5876aa31e09661f7011010ea5da3f57383784d9, 9a143db95872f1eff4d179da4f34e388e003d394d1709eea734221188fed1bd1, dc3199b00057d182b651acde2d4209827ad9203a20f2a05003eb7ef8dc0f0709 +0436f5fe5dcf4b0723d214f775552a22bf4ac8c1b2f1e05bef50f75619539c89, 68c48fb0bd0eb7c3252bc31caad79969329c0c8eba00959338d487ca699fd9bf, 771388fc04391956bf3e555f67a897fc68b1325767c40e2ec88632f922285705 +7e4f27cdfd257a307791d88791d6f40fabbf40c68e5ac43dc0d102ad6bdf0583, a152b4338d5ab02ab4be35b4f76632b40d78baf5fbcad74af0bae67f8ca5eaa1, ee52e9149fc7496429068e626ede82a070440be8151ddfeac1cca337910bf3da +4a717a0baf634d9e011d1b3506d0333101c251e23de82225251a966d8290cce4, 671c757537f9e35b1185caefb5be6e75f85cb0d94c57b764da322694c69f4a5f, 07d52a4ca4f881bbabc03005036466e987524f6689f44fc00e1cae7f8620492c +b7eb84805ba9dbb7652dcd9037f676b18d6e5f696d3e69cfae3f92a3a94ba37a, 2314292a9a65d9c6abe6621263464409418117c41927b0b58784b0616803087e, f0a59c979b872ca273d5df9368153a7c269833a0508439d63e2fe55755947a57 +6d5ae4f99b5aa7fe284e9faef6d306d14810398a1a307e72077b8a40c3e14169, fde52d5706dc4066b0dcf410823f36454cb71700873eaaab7267dbf2567e643e, 89b807868f5b3d798a583d53d816e50520ba333da9b0cf59e8a0aeb8126360a6 +3181d9ce692aa1cd028ff711a86240895076831022984aed9dd0e54fe1cb5fdc, 3eb9a0e03709290661a546003616719667619f6612e8ae319bbacc63dc90e4c9, bdb06f0b8a95c0f532241798246c714d947f5d66aae44ca9d27d5071eee3dbe1 +00cca082ef77854cd01e7687359e0d8af296b8af92992c71f8cbcfeed8429639, 5a56716bd294095a953cefeae30dafab67b6342a44f9d6d06072e4ab7079351e, 20482d37f805f68590a24fe14aeae6c24c3cc6bd350ce9846f9fb6d531e0f63c +b9eb6cf2624dbfcda78d693418aafac6d9439dc98248edaab979ac4369cdcd1e, c933c6d92a6f8b4d3019f484d80cc7ab502eb124b7e85a979e62fe1e0721a216, 11299696075019cc2a16a63eb0d3097a875f1c1a7b580bebb16e33a87aab2bc1 +9673cb86a894f8c043e5ea88e752263cc026a02e363f2e9560e1dd5a19586a41, f2f7d5154b35e5bba24c62d36a68f0c56b74bce9e21d0b0dcc1af0e7cfb4f6ba, bf48b95295d2b7be354ce6216514a79b80a993f2a050a9c487e1ba1e0eca972f +70fb81619914506fcb9e79c6c15c9bdd578005aa5d307f3844bf46ffb48402a1, f69aa89ef1630a21335a41d640346bad29f450bdfe626c0fb6944040fc0c88bd, 304b48ce10c3e4a0c4723d3230f966472bd22df8425b741248d709a9d4655dff +e94430ec7a2c7a2eb256c57035bc1a6172928041879c1423c1f472fd85e39e7a, 8d65ac79f5755be70a88f9b365a925bb0c591dbba0c2364d730cf9d60a7e38f5, 954287497ec1ca9cd665798a4020c92acb6a534c3a7f01a8d67d3539c8907ef4 +fbaf30dae64eb14b90abbed3ceb853456bc1935f70c37fded5af1b4d2ec0c8b4, 184a2ad94decbde0c8dabae37957083794e3f31efaed1c44cdd0cdb5330071c3, aecd0f48a508f6dd22145445d359e43f656bdf7c6ec3f5f8feb23a2369a604ab +597d315206d204dcdc04cec0edd9c45530c901d6e5b1b033173c6e3bd16b581a, a83a8304f560c18011eef0d8a970b3264adb9969e1e952bb0007dbdf213fe9a4, fc89bc3565e09c26a963a8bf455a5d8398dc279e1a85bad8e2096ea966ee8586 +3a53f1d1de15ea116e4cfdee39a98ef1fc62354a6952f9d9323507db428c94cc, 402b2f36fffa3097f0e25dd7c22edaf12e23e062a3748b134c687b07fa1c5f1b, 7fe1721bebebd91877e4ff6447d76373f0e56558c39b09fce693c93d2ad165ca +51a7f31dbc1c9f6475310c93cb87fb44070b11490d691b9ce2546d755c59d5d0, 8ba264bec75d258f59c5e33b7bade85413b805db2adb615be2e742f06f2a298b, a0a8d9e7c46d8cc264e210ae2eef80307bd8cf9be8f54e625055a8bd3de49929 +8e460d24e705803fd9f068cdca2c5c472267b7b8bd3f2011ef0de00baf530008, 7c41c41b83da1779f343ff352f9eecc59caa5d50eaf2cd6957eb454d5ed7ddcd, 161a369f09c75b050b0d017ade6cf7b6417c6bc60d34f5b63b810ce73598be06 +9f23ea4bb7ceebd23b8ffa5987d983496980a2e497b6b132c0fe6a7d05c3ec9f, 4921b76c555d6ba02e67136e3a5f32d3c8cf6996a27f8f02a21b72d346d7dfd5, 088b37e720faaa60dba3e3fc8c0d8de060ef52b3c20fafbd95e4483cf3a7e909 +5968c4de23160b1b58fd045f46d7fdf13f946244bd8fca57b8f185e50911f1ba, 0303d31561b4eee05b9486663c0c85bd45c95b0597002b317c660a1bd7f48b9e, 378e0fe1948d67ed4266326ba7946f4801dacd390c9cbb09570aa8d0a78bbfdc +52507d1d8290d10e321fe2d751a254200b87340f37b6dd01f3c312f421ae223f, 622af0aaf462767c4d97bc07761a184a97bcb60f266983bb931962e9819bce99, 70257b70d9b0f3d688134d36ae07f5a7247eee70ebd8847baa8c49044a476184 +1b5f0076a48028ae80bd4a54001fbac6f65568cbb4e86928e0aa99e4336de6df, f61329b71e74b509257c15e5e242524ca6933badfec64eb46fb4956032d9ee05, 79cd24534899c582e0590513adbd60a2cfda2041ec0a436fe4d0c89e45f37376 +633e0186b3d0ff9f7090cca02ff3844d0b07028db1d3bdc71ff10b2f876ee6d6, b0fd8d8340b9e93c7098d9251b014882bd005d85fce2e57e0a906d191b5c3fba, e0da694a1f763ed64288cd1f014b09c8bf130cd9b08f86010fb505c0b1cdb328 +5418b360d1f69a66b38c482f3335e6bc75d36f44ec03976f47982162feddbcdc, c3e03bcb3aab0aee5c434a85e32c4a203e8b3eef9e8c14c685d9696d67d2bf6d, ab565d731ef678b03ec8719c062cffc7acbb0124d9e2c7550ec3113c05133f6d +d89c082eb4ca0357a2d6c818a9ff7c68b40c4614ccd963ef4ac3c7f34021c190, 591f500c9f0dea33b70dd77593b4f8dfc9b57e4045b48d6f7ecd62c239fd63d4, 8c823ff17948c6d6fcf5d0b8fb6294a23971753e5e45c412c16a08e23fb93095 +c1beaca255712399a2900641e9fef783e5b75bbb8252c9a07753baaf11130835, 9db95b5e09e42936ceb9549455c42c5867da36c0c39d5a5b68583d1f83cbca75, be5a980ababeb83decc6fc6f2acf90eddcab9080f281c7ef5a5cd4c128458738 +8070a828a7fb1a5430fc7bd17849a47a52cf917bb8655cff43a4cf9b681f25ca, 8da403682f842a8dde66c43b9466881ce0ef375efae37e61d36ff161049c04a0, 3b842aa990430ae92544ab042e525e9724c2dfa4aa6c309239c39b3e09b9bfe9 +28e0f690aabccb40fc1645d1bc823afafc6cf3ffa8799e091942448f0827c697, abd3118b4562c414ebbb17bb9394c7c8a64211d488a1e03bf7c9889988a5a850, 0322c9335d98556a01cc687ef4a35f843f491428a5195b70253dc7d928c9e8e8 +eeea29e26e8e80f5f3a56d920f9425c3ebd9c67a947c34bf6a5901d7da89675a, f68f26b6a17c309f5bf24c0b94eb200bd9de1820728fb27dbebe9fe3bf88ecd9, 95cb406f329aca9fa08d3c3ad6a24d29b25ded3386b54eac132f9622d14103b3 +72c016f8a5f4a51e9712c0484c855e3741096c432913df3993a0c801003da600, b574ad0be01921963d95e6393f5912a3de5e6a4902b08bc60aab3c8ea2a6e6f0, 3c77be98844c0e84cd4100fd3013478c0bd86c5276a3efdf6926b821beac98fd +7275afef0649a86a05ed4712bf0a1dc41dfdb72cb6bd6bd938e52ccb90c968e2, 3bbf2f9cab4b20d80a045badd49594dbcc0580d18f2e20f74a9fdd31cae50c4e, 49ef88621a690536dceb8d6c90b2ca638388286172684c20d6ef2f81b9747ad6 +c24edd02b9b0a6ebef234552cee4edf71581904054cca7f9643eb09aa9480e69, 9e645ed997fc2e5a7bec0649f493d40bce63cd1d0af78fb0bf696cf9b5a3fb78, 7a1ad377fddcf0cbb689496348f3c17ccfcb4f1117b5bcee2bd22ad0dd104fdb +4ea58c8dc3a2bb21d73b9d6abe248e4a448e7d537e3ceee22e13c7c155e3eb61, 17a2988a2c62a1bf9da60ffdfdb02e8824bd8e8ec0f7f47bc5b3c10e3c7d2f7d, bb75c34d78616c3b196fc2dc2306567b9d1fc17fa4b73b224791fbaaf2bb061e +68add6b03a43d32f0e8145792c82e0d9373cef6731555932b19540edbd674282, 0518eb13d2701b13a02ce0f3dbbf2c06129a45bc1fac2c4c2ed9af380b0ed48d, 6234d3729d42108beafe14e3e22ff155927e50cb45cfc6981bbd478758ac8726 +de7d84ac20b9390a746e8c3776da618fbe2338796bba1743523c4e95a27e83d8, 4bf2b17772f32d1ba39affe17aaba0b7af0d6b41e0be94e225a95567075b5280, 701e88c41146ad9a0b1ff20cbf69e20630fd47d6e656076fac9148675ef6a8c5 +1472e2f4e39d3c02bf9e460b532654bc9045d3a27b5714bb63a03ec7ead1a429, 19cab0cb64b0ee7b43e2db9649e9409ae50a5fb613b26e5d1f89ba9fc2793e70, 97851c49701d1cd707f3b406435c2eeebcbd69d56e42d421ead0482c95506b83 +41bc084940ca6d380c0d47ddf6c1421e22d665ebd8f7af08e6a99319f4661800, d44e20af955a477c1bc1d56a1a0132ca17fbf71003ef02f181b03cb38915e5d3, 85a1530a267851886b2abbe3ac9e40c7a6ba23d350e9de5342cd84fd2682edad +b980de4e495af58c8dd16a7cf12aa0e5063dfdf87ae9ce183a0ee26eb7e47385, e9c2dc046e8b9ac75a48f30eaecee5dfc86588b402073a44b69226c3fac90f7a, 197082fdc651c3876b8f1869f41df380a1300a369973037329a093a4a41baa8a +a01f586bf35ad81e20a8e47bb476ddc33af6c0dc823457dda5ce7a8d137b39e5, aa754362197b1c40d915d6ed7f828a39caa6db842a149235cdc3153cdf01f578, b7a32f7e268c7cb56249334650e7eb54f48ce9a5e4e83df1dcf3f7e053fe5886 +485556be8cd49267519524b0d83a44b970275b528fbf793dca8d256da3f2ca41, 2827c58a110854fd9006831d1200aadbb340efea30e447ede7157edc2badd9e7, 7dd2ffef503101c626177e0debe07a42ceba568e1695d5fcd71ecbff64fae7e4 +abf6921a45b2b582e3734fcdbf0d62c149761d62854404405c917b46f73ee90d, 6153d42edd3cee9fe3a53065ce88abf2ef6fc0e108f759dd65b2f55ecc2f2d7c, d45a45030b8b0e40a4dc74194dfb552ec37c4e783d52d7f630b845f349abeaf2 +88669b27048cf175e2cabd100e9ff8dde9670b9ec37446467e4f2a580c47826b, 6a4e1d1478069b7680365a8a9bd85b814bca16a8d5c6f865d740f5ececc8185f, eb59d311bf23419874a202b54c46461bfffe4e8bd6c15926f89628c2e490d84d +b326d1bd335cf51d518e89442539b1d7a318f3df383d53f9a82a7292c19cb02e, 6e688c2e0394f442036cc20ba28ff013f2b476f62e927ec7dc7fbefbeba10ba4, 9c69db61203fcd59faf74ae95d827e072f719cd3038e3702eda266b534be61ed +5e41838874ef79cb7de85b9b48ec99605af65213cfc82d7b80ec9c647ac1b10d, 5c330a16b81124e37aee8357226ed1d0fdedff9162f2377dbde03863720a4657, 44f67e4c63abe2ed6ded455f47778924333dca0f69b0e1de95879a930956b411 +af24c594268434537d3767ab8f67cc37af95c6ad5021860948c6713b925f1660, 79b2fb407a16787945ea8e1a5524dbbec192bd834e799b994bdf1b6d07191d33, d660eadc2c18768bed26d927c914b72064c072e07e4b8e8779928a3f2c6a2926 +ea7b4e6354b01488e3258a6982ef2feb9c12070b39d8f2edbaccc9d536ea80cc, 3a2b57ab6202f81a9c07773e3958ab9d70436a6f732f3c9579e9f59b8c70341f, deba5b6859bfa26569ee62d0eeb6ffa1449ba31683d974e2f7f7c6c72a28aca1 +8cf852a30ab9cd45d94ca420a9fa02204e1b5c2494b0976df92b1fbd8f8516bb, 0643a54ca470436c07ae3d67e236dc6fe31efc83cc4721673b5491c18b924e04, 92fb3eb56464a6078f4eaf5dbcfbed038ba472d37e6f75bd485fe8a0dc8d37d5 +fcbfb7d7cdaf676e18ee4f843817d293419cdedaa77efbe03fd62a2999c2d76a, 76d988ac4082afac59f5265ca3f706fafbeefe0658e143aa5f2b15bcdacd876b, e4ed43fe179677ad6d4c0d32e2d85b3906b5ec9076fb7feeb5bca8ba5b993cd2 +402a3f285db283d0e3c59e4450486eb64d1238e0af589f4e4c8ebacdbd1a56b5, f87e645b9ff1b1632c6b7548c04dd5afbea1f2f560521b2139f4dc1b10005179, 7380bf93c84196d154dddde22240ceb09d98ffbe113f6bb738728216bbdaed64 +ae1cfffe8bc4d282cea653dab13201a04f4698572b8a02d326828d3e06dd166d, 929935e23bd41e40aae8895f245033d16e5729fa3fdce74d9d174d263c79bac5, 3540ade4e9f5bf9ad7f60e17a89991e42e4233d2ba6e994287b822497096b6ba +eadbd91d5d9d9f096df439cca2e6f172518eb2b5d5060ef71690aee7e3223b2e, 50561ea3a6d31c2eb779c178611bd293e34eb23b7db895dfef60e9549581b4eb, e03b13c51e3250be5c2e0e55bf8b6ec6359aabc0789516db14549976a5071c5c +7fdb47965480997476070af1db270a87d60d55569296e1a1e421719b45b74768, d46586be6e875fb6bcedbfc8327310cd7a6c0b56e956d32dbf9a742c61f01f0e, 75a8737f8b80f267368c2cca4e20a48e9530abb9fcf115e6dd6d9f94916b3c07 +08bbff7c8ef00d2e076501be16d0922d3b129f3ab0b8d741c261c7dd6c35b2b8, 025b8716b09a1719573f9c97ffe06f041268665b133926570727389e1440ad03, f91cb9ef6c9982ab77c5b646fc60bbf8d49edf00f5bf53605691ca935a79ade8 +ddae3c06cb4c8ccf3ca262858ac4481a529d686fee05169d8799fc0e8ec17a3f, 9bf2a2b6aec3e5fc0783be7bbe5361f601550e08ed29df6b7da13773b44dce50, b775cfa3e317eedf933763421ff5dea62f40f60cb9520687a1c978acf24095e7 +21acab552441b39702c6a89b91aff56eeb42b9931b63e70e194f52ad8be0ae66, d2bd1210079a9a666267576677d08625f63c939b982f898f7fd55d8903caac57, 1bda1aa998fef34758dcfd66e2495dce73ad29f7fd2b4d9aa8160e1c9ed1e240 +cb2f3f06acb1c4bbcf841d724da705b87e81023a91827feaba22f7404fc6cf47, e2500d3ed028060b6e6db2688420d8e31e1ff3b60f9b385af95ea9add5a8b46f, e3b1d14331f60c2016a5de295c93b8932f67fdfe2132c17006168286b6a42b0b +30a47f9bf46dcb8e0fdcab70e5c5cedb6ab27a64b08e8fb226447d9f44d7936a, 2db59f34a4102fa62ec265e0d5eeec0c141f3cde6474a9c290049581f7d37764, 40298e44cc533e7610435aa258c52c20239f95f220c0cae9c4e5dc461eb308d8 +8a888a86c294557d369453b021c31f35f5543d2a9a3769385bc9417e640b1099, 656681f41b1fe353bf9398511ddb01130e05c37e3ddaaa6b2953a74a2a5001a7, fda8d0cb52a75334956333e0b8e0185e76cdd953079b3fcf21e71e23329753f9 +5f1fdd6e66c926c48c33a5984e0a5412e07e417007a039b330a36fb0f57daea0, c1ca35147ca722f34d0cff157932ba3fe91f5b9e5677bf485910874d33ea88b0, 5bc0ae2453ffd52cc3b5a4b810f88c1ed3741e98aef2d467c55c98135235b35c +26b53d01748c35a4758ff07268f0dace6cfc414db546521956427715e7c8f895, f2287fdf0f58fdfadb0b9e8e0dc07354a35e9bc15f502e16d6260eca8f381bef, 13bdd36cfe7ce512b4c1a00ba18267bb3c475e50b21f9219cc4433f1c7080317 +c8f629fffb6b62c9cf01f34821686ea71095223c3bcdb01f80c30206829f33ec, 7be24ba6a3d06718a4bbf85cd751452d1253a2b95e1299e3a537ba41fbe0b32d, c7b93428fe69fd53b550320e25d397f2477d1e98d1a405d897f699c894ac1df2 +b506a4156b669500c6a7c99baf289cf2abd1ca3c446afbf74c63a49e5974f4bd, 3b1e3d59d41a06adb865ae0b37fd367fb88e743a72f45e7f60a03e169271d2a7, 14442bc5416630672a45f11fbb8931502bb8764266d62c13169e1668aea8bc3d +45bef5607684b17ca33e72bd74996f077b2fba8e7d17e17a2a80af5aff341420, e397e3a0c5b237184c948165f843ec7a1143c9ec9a58a8c31c4043425051ab55, 48b4b38563f2cf8da6e8143e2b48350c7ad8206569585b88e28b673e0e9ab5db +d4ade68696428e5c813cef1f12118127e4b1c43506bc4e6c0d8f9d4f7e0d4f00, e25e504ea4b59a5c351cec908d6b3b32ecc3ba65bca5656d91ae98c02e0b14a7, 7dec9645b0e486d05039baa69d90230cfc4abd6f9c471b2e45a6da2fef5909a3 +1d945dbdc26b23e1f8a1fb7390d8e4dbf8e130e05505fbc975d11c43ecf5085f, 528970e1cc786d2a488f72f9c9b88797cbcd6d28479061fe55d89e79c5926e52, 002759cf969e823688f1150378b3527da6ed0e84cbb47e33c428d3c3d2bad754 +647ffb6a245794fef2940302e06c0760059e5198b51b034001241df3e98d2fc0, 0bc92a5f83cab25ead777bcfeb6a247da60b785fde2d18d3b96a145490a28656, 8aea9c10776e3a341dc0fcf989286ff6fd2e7fc4057e72d27ca330e961892a5a +0d05d5871dd29f66fa48bb69e2bb3e797a0789413298ae4eedad1f0a6cba5019, a404e449148cecae4a9014baa8e2cd772e96dc61c13484eb285f0f7b25125609, 1d4c59bd377a2c8fe24e556a081c646b69fef8f86b862b1113d8f007f67d16de +0a019a626d26a5902c0d9c160746c2ec341355c341b7bcf84d269ebf420f570b, a4b996e3cb88403e0725db0b7925b5ffbd857fb016fa9b73081d540c3f94de01, 015c84622eb5cc4124f97541c5322aa8aced7739bc234aaf946a6f010157ed9e +f121c2798a64d11d3cc546d30c0e2ccd8e25ae9e3d03132dc7866e8953ff00df, fd835e3a5ae70e02a2da07d093776faf2f89f8de900eafad384232d09f968aac, b1f80e8e88caf08d0ebac44e239b3cc765a040040be5c0be38e6a1fed714659b +7ebffa3c2ca187bdb028c7ade214697a16078399f2629e172cf7d8db560a92da, 55331b2a8cbde1132ed4959279f47df2b13c8697a9b8282cd8cdb93124c0f2b4, 4329bb9ee05dd211f371d83f7a3e74c0b0ca8846addcaaa877aacd976b3bdfdf +04f9e8e5665f3ac7c1ed17c5d0ca1ee660d1b92f8c6d9dc5c458c08724be9a5e, 14e1d71ca8a81e11c8e9d6ebfe5b36e3ffe3a082f4b6cd0a14b54b26eb0487e4, 3c7da7e8557ad6d49092b94f55be5dbf57966ec7835c5094b43b81657a5f5f25 +4a221e734f55a12d0e0bf837462843f52710529dd521017c693f16a70efa7a49, 038911cf62c3351d83df54cf036ff44b5b301c724f8b540e7ab489aed493d2bc, 1a933afc814259feecd12f3b4cc919610fc859cf1fe040456c8818c8062b3b9b +c47bec7c3c28c1b8ef39236129d9df48ec2feb8a6a97a382a6c05a9ce2ca5afc, fe1bb9e15fe99593611559be9b4fb06177d3f38b882901c67aff17c6279e44ec, 05b5a996fc1c9646891bcf8ca38e4e5f18f1192930da42d4fd44903095d593ed +d9f46859b1e4168e1ea16047143c662006b566e7d9d69e3068c2caf591ca4de2, 8b4467e140a7bd5b24ee9afa571f331dccd0f586d5849b5dd5a744833b3582d6, 58ea2a30109226a40f69ba211556bbf990cbac1d921519eecc183e22fe5cb0a1 +afbb27c301368fc286eed61e12db2b085d0e9abff37d4aaa4793347807f7ede5, 1b2a19219f371a487de07e7b87118e6a9d46257728d18fe0ea062dd4ec2f05cc, 5229c4f0f0747b5bf44a0da6524b6e4ee46f0e3e65e811d78f8063f6c16a84ed +75e73a56dfca1b7e590dab1765183c9f061daa2e91926abbb85e3268b0db3398, 984b7a4af0e7a3058018e9f284789386d51fa9ab33bd98fa1123f681878c4f40, 2e8e9e38bd0c2bbac69b9465327117cac7a6d443db6d24669f93f096f5708687 +32bc273001d41d08c88d6829d26fb8b3de339862ae66f6a480051a8b01792eb4, e1677f2b2869073be9f1f42a07a9140d4937da4bfdbfbc3357a0f145e3e6777b, a372c1a8bd277616d11caa60ca1986592c82a95ba963fc508d91d568cb5a3566 +fba46c806acb376cc06fcc03569e7bbd0e68105fd217041803984bdc1aa48ee2, 65073bdc69465bf2f352b4db5b5950d259e8c48b6a616fa5d2f4b98bf51fda4e, 6d6ef46f0b6901aa2c360d8f67ff97fbc437fa0bf934d5e54e0fa33c336a6358 +7b2017d23ee485e9bfc6fa11f4fc05c7d8653409e58c354ab3b8dd591a0829e7, 9471160b96737f09d61c91dfac0f7059715ab559a5ca35604ac2f380a6e553bf, f5cd520f7c868d06709234327aefed65e127a48b52e8c3cca2b3664acd6634ba +faa0abe2fa27a4c4f45e66e0ffa80754d86a44d80f339ba9496525aaec89bd1a, 41bd4e444c083a86b8571a079ab78736be3e73083768dd60e67a4ebf9a35703f, f44153ac1b3d5b18c3755b005aee159494b77304a573ccce67daccc6db6af52a +934e209c9b090c63e544dc1f421f89cd2c87d8fd44f8d2a4b5da090db9ae2da8, ff6d4903a2edc1ab341a90e7fb12c1b5a0731d1f5bd7d26b214b10dd7167c169, a530d1ed0261408195a198f7ab7ee534c77b033600c6245ad3f73afab7ae34b5 +cdaf09fa4a5c39098137cffc002dd62a2e7167216eb527192642e03192413480, c9841498395cb0a73b31daa8fd538c17ea3e2d448c0806288e7f85226e2931be, 660ad4f6668f71eb6c4ac0f485156c0459538a7858211f92688f4b5393fc7a8c +c8321157921042d07f86def117732e1c1c4c9ade6e81a2a833ed06f3a28fb7b5, 2ae274f4ae84a36cf834e112eea1db380d852005cf2875dd29876db3e5842f32, 0590563557deabb7e918ed0b7277d83a1d41bdd9a91c4fa42136e624df374280 +bda3f35f8255cff405a43623f4af37c683bb4dcb6660c0a218de967211d7cd40, 7030ac7b653b2fe141a283ae08f43594c74c0752788085376eeb2b13529fb64c, a83cf90e3fdb6093e4f57631365f7b160bfbf7551f892c7f9b16d0cb70e885d8 +a2c9d44d02c933ce70ddeea7b035e13c7577deea00586fd9125baa3178376efd, 929049ae9b3c94deeef4814404bda33fc43c1935b51b58dc514eabee987d7288, c3d34c90c3756f36edc1ff16da16d00ae55b539b3602c664abb1a6c46069c482 +59513dc4562f73a23506495d6b71742989fc8f50415632136209680fce45c97a, 033f77f25fc1602990a554b5509d9ca1b58de612369dd605785074ca3c3b95ed, a5da3dfeddbb716a54cbf1f90f1c60ae6f572f57c545ae599101c6baff14964d +f4588b9ed1a03e37fbbc53fa0fdfcabc639bb0ed67d4583b428a4bab44e56a40, 305d8e4e74ba94c879cf93d31743ccc047d8163e9c772c63565a934f9957cad7, a96805e7ec9981d92d8cc6180a664d22651fdc763de2dcf311f7b02f3622972e +529f51fba8275d3fb729c5ee13ba7a8fd3943f8d075c0c54022c973c7cc6e8da, 1561ef8152b029b7531ea1d72bfae9bd1572f60878703dea5288880294bd6744, a72f86d5f63191b879ca027dcb3dfe3cb00878ebc2d1836416e6ec1660fbf1cf +712d3dc0fa21ece04b67392d797a985785efa04c2adffe9787572e44a7588098, ca558bf1e7915a4855419b9719add4f1302850c857e01459145464137848ac3d, 1a155a4483c84117c80aa1fc7b541ea33b660e639a62cd398cd7aede03728d3f +0b21dae0f056a1d550a020d30e621cf68034580a2ba252ea150fe1d23a64623f, bdb5cf1cb2ad7f8a0eeb83262afb346d741b84f993b612b6246b8c7b996931f5, b793a41dee59858518be64568ed1574eb10dd742d4811ad42a8c4802f8787dd1 +5afee848741c114856c5f62b84d33448e154d05c0ea7394e3fdb694cecd4267a, 7f9994df6767bf87afac9831dcd5797463c1af6b9264ed6e16323e6db4da6e44, 20e996a751075201d9e63f9cc9fff39102d94f60d13752ee44c0ad19c57bddd9 +6ad392694652213ea251a74577957397ec692252cca1a6ecd9c24adf528d85f0, d3b4dd153b5c9b6045c1dba99008f8822a7c8da983a7538415872aff9a066861, 3b111a97e43f1fe8d1e29897b2e6d2cb365499b8447c8bea14a69f94a31713fe +93f40d01475ce24aab5a323b5613a1cf3bda486bb5841e969ca783e6ba0e8eac, 7934556b461cd8a7618b64e5678c29d30cd08a547b4f445294288c7749b77239, 808979ed4c982abbce7064d55cea1edbf8fc407d3b2ab38ceaea5113e550e4a8 +d5aceed6dc04d050af40ede0d7649509d2b8bc5e1fa07506c1adf4b54ba33508, 664e653fe696b8b0d84bd72b74db422142af300b6e5dfd935d7d4b4b4e7a7c00, 471880e2b194dcc470c8dcfd11a39fa436653e7711dec6f3d93ba7849719998a +d26158582e47ee60bcff376841cbbc7a12895531de747d3f7484dd198d9f4903, dc652b53b9ed346aa1d37615cd494ff4c0375211884e48b37c8eafc1a8d3cdac, d23ac8ac2d8adfc332003764e33c17c8391d549f41d381276e06859f6ce7f57d +63c1e043bfb2903d82d974f14d9618dbc79edc0ded2a45eee119229ba6cda4b0, 87bedeee778ced1b24c0286c9e55dcb8e8a83176cd27b5c7e7a8d14bd209f09d, 58283210754ba4b8845f8def8f3938baa0f99e4da07c18d5721feafd2a0edd56 +d1736712ddb4ad99435e6f1f38a59ca727f28293a72391f03e3ab119b3f15fa5, acafb7f163d33cd127efbba1df6763ed8ec75e6036627cf35e30aa7cce6cabaa, 11ba2ccc8682506c4bf9d9028ba95c3e21ea7648374e7d5a277379d3266446b5 +702f3c165706158695c2dc3057a4e56f8475943b9f687380f4468bfbf36adcad, 34999a7e30d40e22c51d167c5faa1ff9b7828614c41acc46aa2d7354ef59a3d4, e50cde49232d7e2912467f426ed026678e8d76e31fd4dcdca0db8014946568c0 +0872942614e024e06ba3a560635252f7b840e1b4a471d17483bdb85bc9b51610, aeb6647d58a3b0fed04a84b3183e295ceefd92be7519e856b18a914c8f4e6cdf, bb03b0aee1d24c7570c42e41a732b1793527331b9a00d2d77038a8e24d2072cc +ae218ec74cd1feae5f41be0cf1673995ff3e58a1b9c767b0356288b2bb101ff4, ccce0a708f4b9d898ee2b9e31bad941489323c5b149979c2bc8a0e7a05794ed9, 194cb6340e4d0072bb6311b391b53e7933b1d808c59285e237b1e9746eb2e8be +ce913c952a3703439082c9af9e2da37d102b9845b2aa5f9d5544a41b7b0e26cc, d3201d98f0dc164a38f3c3f7e9d9d5bb3a3768ed84f91512881e33997cb0a089, bb8a9baf082cf8fa2b88b104ea923f270b1e5a37c58929f7f4c2f55c4a55c3ee +699a97c1d9c554d5cdb5bb5671e70c7c84d5223af3de7494e70b1e8464264789, d8e4209067cf0c302c68145260fc710290b131e8d5914ffd22372d8fb0ca8e52, c00f6d0afba9a3e5fe6f4751f58cf2898a28a79052ef2dd3c876b65cfbb3b167 +aaee6338871c4b68bcdb864648eab6b8927ad94662a309d102df8927dbce8797, 5e4721304d5e27be7f955b804da52b10d7dd9d0912576bf9b196ccf1d2a83a2c, 310a225d5107f8ba226356c95fa3011d1a5ec6f19395cf3be9bc0b436f67d96c +6dec773d0f39256e3c92d51fe097bf9d3d5d8d33c8972b3e943f2bbfda41573c, 1b1a895d75f98fa046f8abf29dbb26ec8825375e90894e112674d269d8f32977, c8666e340b055c73a7f350dcce9b674630b7aa809079867e7c38928e77787edb +1b4624c833cdbab11a04045ba8860e51eb0d56bf98776152c7d710cc5a96b321, 2b22b27f0e34d599d01b41101bc482bc64958e49a4679f00b0fcb287833c3baf, 2b633a7917f819b3ac8eb8b1819bf71293b58ee8a0436a4f73b74c3149d7eab0 +15fe677b3bcd2bbc844a808b853323dfa3fc207d00c550872d67e2b5c046e4e8, 35959c9f4c5904232b99f137db33b015b7b04808e7cc63917ea25bf38b52b517, 01fb46d1dcccfe68a7b85674354cf9af91a05396dcdce34ef4ade7fc4f029c3d +1375e0388b861db01088cf568e435af7a4be600677267a8e4ef2f914e85c2dca, 2ae22d36c7e32d2d5d8150fcf62e24b8f13b1ea2b443c5d5ffa03ee443edfeb3, 52ad6e58a7bd775ae53e69bba1a1b1c8fd76141d28e8f63791e23e2d9f04efdb +e4dda0c911809d1a87e734692f482b0915d2c642f31b4ce4a1d2babacd7f659b, d433ea97d94cf2ee1b360302d91d8214371ee4b5639ced44086c4476161b9a4f, 2c8086188b87927fcd37484ead1b1c764f6654d67d0d6e60eeaa6e25266a018c +5f7d5a266058495da66ccd00053ff54fce52099885406241aba7acef59e61c1b, d396ddb6c43276f9462ed525d5e20c9e2b971affa71e3c545458cfd4d186e540, 508102349054305b0499344d618573bb659426f020fe56cf4ab9c05ba0fa075f +2df2d80d35549baa645f1d00b7c807703b7d674a87b42eb5947ec53c4326f2f2, 9c13de37e48a44862fcb674030a4e583aa063227fd69e733a7fea85d2fe79a76, 526649e7572b39929703331c20ce2f7ab4a573815aa87c9af549e482afc65169 +942e59c122c3e9eb78a89116c4af3334b30783ed6f2208428bb28dfc5b884ef6, f41dea01f312b14e8696b09f3c992f848e376bfcac02e3eb81e7a6837d7619d7, 7926013dce33ad48d8ce88a0515ff85ed5be96fb0fcc507ec0487e334d211394 +77d7582120c340dd37ece7e3266abd92cb911769b28c7df5b4947e2e9a476d42, 5104463706807551de63be1f625a9dd563e2ce05cf778cd33c7731c8025861f8, 98cf6291477bf62c2e1c2e983d0b533941040090809ffbd25eff48e70e8e2452 +90ece7a45a0f68e7f7940beb2f4bb372cefe67e26976500647f67bfb725fe454, afd3b99deeb04dcbfcfa336d34c9305cd7ddd7a08fd262a5ba0c3cf795f3f8ac, 04651e1544a0bb6f60e01a29aafcf8def53468a9dd9b616316ea814ac2a9b1b9 +15276c0a5c6d087a02dc9014d20c49a94299e3e604f2eb1f9ce45646fc6d29f0, c798aa61370b5c450605c159d0e16054009b0a8c2fdaa3cf422d862671766844, c31fc1c9b6c0dcf68a33b6928fed0e477d3cf85d4d32eac3d707234375a7683b +431d1b7fb7dbc4a505ddfa9e5c5dfdcef474ab916503b42c7b477e86cbc31303, a761ab23fc207f6bf9c160f10bc7fcf343093023f8e9f6560b84dd198cebcf43, 68b1b778e7975b97ee9dd32bd733f00ce1a1d59206b7077bb0ea3d3aeb4345f6 +226112a180634d338fbcce761c3d23aa7a1e9fb2db41bc2123baf2f281ba8ea7, 6ddcc6d743d6ad877c8b89b8c5b3f572f971a1a7e3b2fefc12ca06994640e565, f65f2dc844e9a40f737459d46bdd48792572668a201da68b2b8503491db874fb +59da1469827eb0424a2d52e9e36328a508cc603fc80b588f97768d249155d991, ace02e2ddf2c426d4ca68fe516aa9d20d99e3af50687c1f5860a0b657cd2923f, 795eabdc88ea5dda82e1ed929a33910df0b6dc258b196c828d4aad998e90f193 +f125e767c6de6496b07ac7792d60d966316d9eb7750a182764b98be22c94e8af, 20ec39dee134a628f8f1cf5d58e1393bb3872ad05178f103b0a64accd2180300, 0f3881d4449a85fd1e036a5139c26de960d74c4e7f5888d3c07edceeb9303e82 +ae6f4124a53d6d8ada514ec9e0910aa41d1dbcbbbb69be0fbe7dcc53f8f05e36, 16a33169d5c5141f06b64d2017e8c4b7ee2d4114ee2e9db5252d8f6cd857d9af, 62722507184889c50f789b6f50620a5d3bdb747d01fd07e050b84b18eac0e876 +57c4f2308e837d39e49eac0cd011ede8a81093cb259d3e31cccc6f3d2ab0de2e, 03747aa040ccbbcbdbdda10dcf821a220718a3365a31ff2845a5888869c54497, 189c1a6e9a665cbc230998731911380bd2b16530049d0d54a63ab31a985a89b8 +614f8b913d12bcb6330da78c448ed7a75d6c9c09a81693c07aa37694cfe784d4, 7e0385f510d6aed52d8e6790a2051b9f2d457109171cebfb2674101190749436, 732019d9c8c2b0d4da2c15d90d88823e93a4b77c2f6cb34a1840afbf75995e35 +b7ed20307effe2a799824969bc1af0d1623d7c6758625764e36c93d0efec986e, 40f8d7f774b13b773ff070a05bca8fc516a4e3ee17cb300c4b9485d472cd8b91, d289e375fdbd885baced7f614f4f38cdd76a3b2a63f04f06b3ff782cc5367cb9 +e7d1f25b6033b28e11b821b53535d9bdbe996863d11d4bb5ff81895b1a7b8963, 4a41b8405a40c24847e2fe6bef93c5fa74b980db4d377387dd6ce5e44ffce940, b23e0d5bbf83643e8f3d4ebb7db897b000e11ba427d086253e5699a3b412ccfb +435b9115f6114e76ad27936cdcecf4780c9918a1b228ec8e4fd400b6697f0f7e, bf7ef95b4d2b3e15f7b7df0f23cba86dfc9ced13d0aeef60da80fad691385d9d, a7f9b7dc3c1ec2d7f0892d1eee31427bcff90846dc2835bf847fcabfce9413c0 +ad64da2e4d7cd5752abf10e5998f794abf3119a5ba23413f3f1c410404eec112, 546733cd48808d70efdb6cba3b6cf22bcf2b69540b88d476c62c8d24b23a0a6c, ce6714cfe02b5182571e1a8d0a2828ea83e76ed30b1f142e3a6b7076de7aec81 +63ed580717ad3ece2864cce83b89cffc3dba36f09444f3591eb956e03af4f735, f0c5ba7102bbf8f653452c318d80ffc4a67786985510ae748548e1988d384fa9, 0654eef367138319e462389a0c6004c1843564c5df6ae190f4c3eaee501919bc +f048210201ad9aed711f0a1006d978b7c02ede5c6fb120fa789605806e77e4cf, 92e913399ef60e9fb6c4ada6500f1f3ab607430dbb59638ead8be7b6ddf5c20a, e60504021df667a5262431c87e322028c9dbc078433c82b3184e3835f061a693 +c52b1d309e76e8c0c498f29fc2462c5721cd928b21ea91766700be50a5dfca46, ea6e05ee310348d53b5171361808e01919b62966775bc18abf87652bdd158c4e, 70a44c4f5decdf2571650d72c198ff26da48fec964adc3bfee8730916a848617 +cfa39508004f34ee02cb8956e89f29b0870ed6cf0ee9c60ac818501305c3fe9c, 08511cd8c898513c1cdd4438bb0c13564f0a8c21bd19268f6943153b49e4b2c3, 02d8854c5f98f4e329ae6dbc0d47f2a09fa722947e26438bb7ff73725d399112 +92b51bde822a1a47ad423e9a7ccaa698bdc844920cc53eefb4f7488907daeeab, 5b32f0f96445a16227056ce45d81181ef82dab56427a844eca1afc024065aafa, 8719a1a17ca6d492a4742984fe4ba5b22e3e6751e68db1d40aecf67065974fc5 +46bf55d5bfd3924fc0f57c1401a0a4914aa767aabdd2c508997f5a549ca283a7, 4f1ef2f2b1376dc8bd9ff172688c94efd886961c731cc09929b6ef6d7958576a, 16beea8e33de5e4302cfbcba4ee3e1583e2ee4257944d60e05907cbfa7999e9a +074393e2a53f05b3c78cc354ab8aa126d8d33fb42f56b2c0a7d906e9cecbf3d8, 0676d1ee7e9a57fcd1c0eb8b1e41e22e125198a26b889b741666b8f28cc36f98, cc769e2e1a6bb2c67b28deb9c4e63a043527d25a87b4215e086080d1f7128b17 +33bf1970c979f8b2a4dcb2da1940a8446744b6f437f040f09a265cb223222824, fcc438a1125293d832336b4ad930c09756c6973c0ce0b9150e2ae64c8b4993dd, a7f90e72a1e0e17d6b1fac9750bb3b698c03daa0d04bf435bbf4f20fcec3b297 +196525eb836c5c1d68d25e2b830fe1a24ca90ae0dabf756f220c1b47b650de20, 2d35e12c4c624a6e1d0076b2b0aa31657bcf8d422d2600de5023e388f87ae106, 6eb718910473f987a22295aef0716fd62a3cdccbd05e26ef5c9c094093f70e8e +907e1977cd95f8765cbf336fb180f5e44cd0122198c5509e59c0fc0742fa58c8, 09bd5ce91a75d3e834783d2da2790df12c554a3060a12454289d57c5ffacf05c, 0dea3ebebf0ed186e93fdcfacf8284c5ec81b81c777d2ac6b3ff5c658cb46591 +c7b4e43d43e017bfca5017fe5ae58b111ef0ac65a35a735265fbd9ac611fe7a6, 8c9fcf4d9b47e9ed373b3780c799b781f145056443eec02cd8016ed4e3e37ca1, fe54a854da38077b58ed135d35ad0a4d084f9f897c22cd3679351d38c85364b1 +d285577438b2c0dd98a908a1b848a7edfeb2db39aefb82ecd0ff1c69e4dd17f1, 6a7ab0c70140117f4b84034c7d3b89e87ab8e3132431da34c7004dd03ba4bccb, cdb3f8db507d477d57dc9781ea066ccd056483d412968f51552bfedeac15aa24 +fa9af55676d31480b640bbbf7b3aa89620c2b4de11ddc1c0b7ce680f0a092670, f410ed90b3fc2d62a8b83bfd4bb9dcad74d67c19c8a4e440ff7f1ac0d6661cb4, 59452e5853a5f274fba657b35a709394d4baf73a6f37d1c320b4bc368f051af9 +b638de5ff00b1fe372db0597173aeeffa2ecfadad390a6cd3dcf3c43a8a27d96, 03f6c00e240d22e2073535e61e5d8521dbfe7fce342065a418d51745f32c1363, c7436381489438729b475feb463bca8b5b27a2fe65faa05a59f586dc9987ea06 +fd6a262c224f6cf54ceef450937905f3cfba2633481d4ac7f45d25b7bb6939fa, e3778a541393b0af5e7698e810ac0059d6f5b6b4012ecaacc07357357cb6bd80, 89533680da34c5b0a0fcecf00f730b21ce6bc60d788f3d024ab8cbf270544b92 +66123db9b938d6e506a8298511487f1b90630a716fbab99792a83d20108edcfe, 68eaa64117ab06680030dbbe443daa1cc974d865ed874758244de693379efdae, dc58d31a46e441ae5caf48d8c711e785f8d57f89e3d8b81eb4018cde0e652b00 +f1daa52d44f55bbe1c2cd42a3dbaf16799b397e3b1cf631849362de4e1e490ac, 6351dd93864580aeadb4561b7fb4309d03d8c1ba3a401194b58995758c52cae1, d60341e754bcc15c5e2849ca7947dacbb9084b4971308493f5f5cef0d2642e83 +c6d2518f5bd2b1c8b1f7db101950ed5876763afe68ec09ab803857c80da1723c, 429319b259156e707bc6cca16e1986f85bd68d9493c2fbdbb189bd318c42456e, 0ccfeb890714e599bd5702d4b15acf32a62d83e015732bbe303da32a35e909ba +8fed604776e7de59e95b529c591e6b46808ccb3cdb903debc36bd58b39e9a935, e0ef77a70289440e3a2bd3776c362541ce14177df48a36af94a4c7a5476d86bf, c0ed578d481dd3591ffa6512a61b0d9d218e48a7cdb3037a064e43944b2f50ca +5b218a99dd9395f36df96a9b9afe1f912113458b63977e96cf3044a405b75c56, 63ae448796c07370e17b2e3367e672866538b589828b4f91255c612422f60724, 640d23feb36f123215f03917f2d10b39aa7f5fd571efd47899dbad0e3181173a +f66e816bc83468cdd134e5965c4cfaee514d9344b68e814f1de5363f53377caf, 6e5fcfa132be502a85f303b3d6682ea545eaa29b3d339a49182c30117d51176e, e5ec79687bf8020b22ec5b5637ec1618bd47fba0ad441e72aaf33d32579bde2f +655ce06048945bd81d6e7b79eca3f34c5c99483d35e86bda2ebf153005422f94, 040314cd7f80fd5decabf959515af6c35af8324eed5aa7b5a1d5dc885f69a71b, db85a39f6370a42c2f0553147be09963c648f703069d5b01c6e076cbbffd8a61 +2b473bdbcfbc91147b8eb523110638ad4b0b236c3b741a52a0fd76e250af9d9e, 0747f52738ba188c4d914cede9e78e4a74c31306ed80065fcf433fb43ed82801, 8ae14ec0313c8b6abc3fe93201e9dd12335871a44ead03f839d7f5274fe1d1e4 +d18d7694c82d3fcc908fc13819386ba1c843425fbed5da3f864e1cdf6a476dce, 864482741f1d122fc06330462552336de13d9aad41594ec7c8fc1a8c4a1ca510, 9d0387697c669cc239e4c0e8ebf112b13b6214d704fc3aa8b7bfbb95b9505598 +c6dc04c901afe199dddfb731b1e78d0c35ac6d02be7aaeab38ec2bb181c08832, 5607ed80437b3f68f7631299d2b4a34675a19bf375680677e636135cb08d4921, 8d44a4f0ef5476d18dc25670b1b862e57c3eb726b8c3f387c7adef39383e6294 +c03cab306872cabfa52c499cdcc1fe1f2366a9f4b4ddffeda4ec64f69dbf9403, c1192d6a82212edc4f47502e1363d6465bcdf7ebc61727b1bbebdfac35d5eabd, 53f63dfef4b1298643df488bde21a5fb2e1d67d8433812791507abed2da2cabb +b359d7e920f1879fb92406ea416a6e30b6d0ea0c8afa28b21c6a730d4d32a07b, 160749431526f814194216c21d344ee69e340c1d7c0d1dc72f519587b91dbf84, c87703816c90d17d3e7e0cc173389b110830a00a4e5ef6ac810ae44e1cccbf28 +fd1661342b5468be77d13c81b23fa895f367ff905abfefdd4301902eae3f2a86, 96fdf4a2d570ac89189cd3f33bf19bdde98511d5327011c461e9afe552272139, 63ae7c6a8dd63f5c2305a2ac6cadb0eeeadaba97a1692c3431109c92c275e516 +6a29e50db5692f13ee3fa89d3b6be22f4de212548744b92ff9c4939ed07b708c, 3f5a130b9cff2344e164b672214b2b7c9cb743fdf5929f446bea5ca7dcd65037, 78e9bff304547a8c293ec888fd5d5402569f12218fd40ecab98d43faf128dbb6 +c8499efede52f1d9d691f765dc968aaa1d0a46426b4371af98f0b088dabf8b1a, 49255d84bc9925626a8634c7a87d50981c4ef30b03d2937a8fc36282f4fbd57b, b733286f05e26b8a69bdaface83a98c776437904aff747999d03dbcb1ea7b2df +895392a14cf8807bcd35cbd77201d21469d558fab43945dbee0b4e017b646fc6, a1c30958914108f814e45797d21b5df5aee55f30298828eeea95baffcbf076bf, 3eb54b480efafaa0d88fc8d592def96ee6cbd25e05c655991d1ab39421b2bd95 +8f9e9790d4812b5f8c340b76dc2f4abbadf036e5c281d9d9b107b5da88fa6e3c, 98fae71777f31b6aa81992c58efdaca55b76fcd9ec7fa74969af3ef32d8f54a0, 058084f0110ccada030ea0f33a88163fa29865ef5fff71ad1d54d34f7f34410a +298e3afe813478aa29f189326e92fe7cf4ea38fcb4fc76e39f1074a0f0567d3d, a9da4de1b352b852ac3890f5fae059f20ccf7ebdb815e0a82c7a465d177b917b, 03bd119db55d53a588f5f10287d220359469bf455b84c32d731918e18fb009b6 +9afa5427298c00fe6351271ba51ed7bd7b7d8be88e10f832158c7a3f7fc0297a, e7fba61662dca347217877d039b85f3672c0b1f32b080a8f56cc5ec8d5698c4f, 3005ba08279f36d05d5f9b4599748e07d5a91b331463d60576ddc80c07f4c6fe +fee59f1566363bdbb68d791c008508a026dc44e7fbb32d29b384c139028a9d8a, d85f6f4c9b58c96c8649eaf32a4400bdfc0eea4fa8b42d22ea559ef91293d6ee, fd57c49c5eef4c07540fade485247ba7dfed669eac257a8d9bd3cd21ce199611 +d3ca209fee80aae05f589c8977647ca52e97f6d00a0423e019d988296ed95aa8, 49cea8fd63affccc17e6063922b363eec390898bf74e2dda825b790d1af61487, b2a97943834c6facdddd2713f40e963d78aedde2c4423627bafefcd49899b057 +34e06033f8495bfe9d67792a68242731ebef1cf6e8c7c068dd28ffbb2aa6c7e6, 26e30853ca889dbe1dc1e33a2e31d1958b988ff3e3147b18b0f54e5dc5b92911, 3d9925ce20fdbf78dca6e52aee9a68c399eedc6e67fba0e944a5a17c6fb0a5a9 +ea1318a2c593e4ae22d01d05d89301f2d96b55a8290f21c9209f11de1aa4a63b, 3b3b44ffac4b680f231d7da4dda260789093e556d9301c22c4d401b719bc1be3, c5267d552e6b6b9c4928cc1ab3e2e9bbc199fb100d030cc0112e76a1850976ce +e7a621d49c438049c360edb084f9da053a04a042889a42cb735975108dbed3d0, 058173575412bdc44d98c966766c74526533af47067bcacff11c109eefb7871c, a65d42856569492a31d0602fae1eff1c64071d616820f6dadebca69e8ab71b23 +fb3be171a6f9e3b2b012228e63958f7e2eeaafbac739ea3b724f11b04cea6802, a59229676a2546c4c2a842bb115de97d8e8eb2dc7701868f22ecdbfb5da91515, 1ab6659e0dadc397f84a6ea14d1c2886b41746ceb9ae99dc992a5cd4112a199d +1ba6eaafe8dfc8a8defdc1a9aafaad14072a6375cf1f97408c0e2c91f9b913ce, 6e9871c9a3f9a59c72ab52b8a8ba1a136b0662a08c60f6a45fbec76b1ea6ceab, a553c4b49176b42f862a009514b85ce2af961d942ae33af2f449e3d664ccb230 +f17d38980c5aa79e9298057547f98b72d69bc6731985d530256a8e50f7900d16, e019ca45cc1f355804075f65d08f0d7b911101f73e6187b28aaed218e25e66c2, da73177005b812481395f4b7419a30bdc7a3d4f560ba816e3e19162c3fa4651d +b665a20bb2dea0f1ed9aa77236ddad2f02a516ccddb9f0e91089f7a9d885470d, 70320c8ebfd02c78fb068515e1646b9581c4c3e158fddc1cd9b88807c6bd04be, 772fe3a6f3f9c10d23493afd549024839484f665e1f4a78b5b90a01f2809a116 +f6e954cb33d57a130769d9f2e1d5da53b76ba844b25da132bba8425615d5f2be, 98dc157d2d6d6e2e8de48bd766365441062a6d6983cfcabf5aa67bd6df64ec42, d360bfb08f09378245f9a5eb93d847a26c78f845b5966ae4d6b5c4c0f9a44c27 +5193ed440c4b628152a46842d1e2e3f5287892f34b1d907183064d303fec5fae, 7c8050bdba8a3a7c91cf52aad68426d4b6e28a10a3650440c8902a823f60c34b, c8d2d72686003f83606efc60d7bcd86eba7659df766289590872752810132342 +8603f0ffff4416ea8b4bb0068efbef2eecc896204d7569ca78813a71f924520f, 593bcd1b1415478832cfb30f7c1a33a306a3d1ab7f5ea14e429593e1068a0cfb, d9f17776b907d94aff6e4b1ca5c86494739787065a61addb73b784cc98cdc42b +b17eb0cf55fe1d5fbfe19ec43f73c819de46fcb862f577b775d4ab3bde726563, dc5973e70ed28dfdcdb556d794f73f45d101b421dc99f6541ddaee1928cedc65, bca48020a8af744e247dcd2309b65044f8e23ee19f69745ef35eb432c962d206 +7f1d7cf72220a062a3eee77b27146eb1369394b312e8c66b2c0556fbeaad5e7e, 3ee7a3f55b51184fe7fae2e0b7020e3e0be6671335ac6c570b820fe5b002b7ac, b3da8eafdaae4402677fb492d2cc68d1aea78916c3b7b5ab0001c810ac493b34 +2ce772a743573f232e668af9da977ccf050d8274402c03aa9973d26366c5b2c9, ef5f0ab5f8f9c844b049f376b68dbd7f8761c5591b5cdb808c1c47c5da127bc3, e8f38889b62d030e7893aa2c06713349a8f85bd496b38ae56bda0e032311c3c4 +26242ae52d1ef96ad42f71c29fe8872eac885f7acece150877cd199419d3f367, 0102da202bc67396ce74067653fba176d60bdceda07b1c0adfab614ec2ff40d3, ada8567e2d64308ff7f72ac2ba16b5134e04e9f7ef047afa7dcdc1df75cdb683 +4175ee2c90926e07927792c96000c3c133f502f5f85a204e5f156939eacf8f41, 5106a729a70b2c3a73c7f1a3f79d2e66912cfe365fcc5167b5a7d789fee024c6, 4599d90f8c47661096e1aaa3829ade04455f60bc9b2faa46c5fec3ddff986fbe +ed315221b8b4086f2a1132235eab7331c0fb7cda01e756457d67bbb6d15d37da, 3e6ea549e66dc7e233b81beb2af9881768979c24139e1106917c5256920cbdb5, f7b058f43e04debfb6a38a0ea17be350dc8a7562decad90da4ce728e30855fd5 +b33870fea08a896ff4e387dac04bacbdb0206bd4e54281d8678b0788b92541fb, 88ea42e0bfc97bf10677c1bdf8370b4389f263c4b145bd7013bddcd6c3fe4418, 68d06cd3869b2653319117f167d36699cc8edd24475dc135282ede536fa9f7b3 +99e0395b298baddfad60d7ed9e5904f4bb3b521142f955e1b4713e5db8b6b569, d1c6b5266376956eaecb043636c3f7b388932c5ef3c979940a05f02124a2719f, ccf47788ce187b19b13f3c6865ba7f442ee55b7105cddf2c17feef39f2f877fa +ada2943265ecbc90e3c2a10a8bc011ed42ace94f8f008c243fbb4515759addfe, 0e0a01c783ef2b82b9562cac4836217aae3077d41ffe7c0da5d9bf3b0e7d7ae0, f6c58f113637d1086b756cb821f72a53607933627f1489911fcce1eb349fe930 +fd5b142b80d2817d3360cd190233d7093b3d89bc5e3eba4bce60c9652d64f3d9, 9a424259d4504c4542d76be5a6239d6d9876cb9d89c2c0889b9a5df698392bc1, 857e61c10bba505e8b9728bc052169b0e561f63c9d1b8c5ca2a946a8f8e1da31 +3a8a0562e3b95c28fbf95ae1bd2e7d961fcc0780f24303f68c37dc2e533db21e, e965d302ca135a5fee8260a04eed9f58eab04ad01589d23e1fb95d25f55b788d, cea8065ed50ba40964d890745b0bdbfa22543ab2ea5864715264503675b89ca7 +c47304b059fdb79c406d12df4f840b2a0f063bd4c984786e73da61c14e459907, f39e5bf575b4c7bdb95abac15b0972012f955c2f5c60eba7610e8589c59a033a, db60eeee94c1227be8074819f4fb5be25fb45401da6df5f93e1181d336af54cb +83ed5549737709a2e3d3eec9d5a5fcd1baaa2669a3022a987723f2caabc41de1, 4ffca285f2a03644598d6fbb8063151de23e5d1a3a261a088dec3fc36381a1b5, 0a9f25a51835bde58936f3161876d0d0ec5e74c24d7bb45bcf88e8d9be8add49 +c32ef02d6237d8ff75e541eedfe43c5559b7eb7bb9a54bd5b214924ee210ea96, 4ba59fd17832df0f140cf291f331129ed0cb672508419b4187882c4cf6a122c4, c40d29791d60252920ad378f81de67afe13b7ae86c46428fa3f139a7ef299751 +743c92f32616f6a6ba6057175ca286fcada696fae8d99fef18a590caaa4c33b8, 119a2db9b9849ec898e6b734dc5df745e691e5d53949e5a29befe5706f1a1173, 39dd4dbe9df46b63af3b98168308a5e63629221594faf917ff7d812467b4c839 +7650b9d39844034ae820141e9066b11620efe969a2e5da015b044614e928bd47, 5c56fddf88d68d23fcc0b13d1f7b823db5b9d1bc5bc133f54cc5751582d80a7a, ac6dc120ef62bc4efc5f4947a38dda80baf56c5d91bfe2d9ceea85f28b061788 +db1e0428a7b891ed3a1e57b79a9acea64b2362232e353a4cdb5263cf9660d966, 660b0f9f204b3f10e9572abec82a8f365abe5c31242547e9c766d09c13e25677, a15ec0f89255e4d63436d0a636de650f4c387cf50b9421e84a87da2d2a686548 +d1e36d6175b88d0bdb9dd3a01490cf3722f7c795c86051436befcc0ef9b63bac, 3bbf0662375d2083bce3d1eb9f2ddac1f0f3499dcc89a7f5242f3af0d038a498, c0c0bd8a0dcf89f6705db78660dafbc1a5d968c997eebebf79bb7ed814c883a9 +41057151c15e07fcbcb71121ec64043019ebf1ecfed3fd945d08c716bbbc0646, de858375b5a60ec6530b21a2caf9654618cf5c6d67b8046497e030e3dfc80ee1, 418ef2d7a7c500bd0c7edcb44718afa5f070afc19ee7f6a4c90762ef9e5cfe44 +7264e44a766a36f969b1ee0ff1dd76033ed116c066713ddde7e20beea7252a6e, 9fc6aeae2b2ee304dd218ea2fab801a8109e56d1e69836095e08ae9d677e03f4, 21d93c0b10bed0442043195796fa241ae2c34510ed69eeb450967bd2ccb69d04 +94d4e80296adf1d2457a8e8eba7ca0f9b04af7a60963dd1c8d194c5b21dc11c5, 1c5408c7dde98aef66f11c96749434ed257ba61583421f5cf4cc6d534a6992d0, 3431c6c768640eb3cdfa7469d65051932aa7409bb5191c82b68ecf4e48eb3b29 +9c357152ff9bfd5e7af96fdf76a989cf65b0305cb483b8ffe0e598081d2b6e3d, edae309894cf38ea4b1ad23b9d7b89bd60ac0e4a76951dbb3207aa00705dac56, 2b54c30e0432666dbf47316dc660394dbff5566c5813354eef1a3c506e0de667 +e927405f7acb922ef58a8ac42bdf1e121eb9bfe6e87b5f20cfa1727c4c8aacc4, 4aa0b60675dc855de44553678b2336488b8274833b9ecb8fe481c417ff5c31b6, 60d0797e4f8a3551c5b9d7261f735d57c752bde04b736ff6e0fc3a39a0ad75b3 +ded7b14119a2bba1fa7bd01267eef137b268647dd92f08bafca111985b4d58dc, 85cc87989b11688ec326da243cc8c52ca4fdd43862d7b9ba78587840358df825, 4be674731e225548c1aeac1717dfb45f7a80903f212c6f96aa3adb424a7df6a3 +030bed7be1adcab133a7f628ab031632039537f6bd4715558e05e23fbcb4a35e, 2ee82a6a89a3cc104c2bbe0024ab27ae714cc7bcff005612690fab2150945419, 61427d8a66897a3781b43ba0196c2358a5742c33e3c53e39b3262a7f604d0f35 +c4225c52a2581e858b4b43e54d879c243760c69abc2948653fb56f813c447e3f, 351e3b0cf5d241e9df6ba88222c873dd87a10d861abe642b784c2ad8152493b8, 849d974f0e006e0b10cacf18c8afe95c1184fd53fa325376a2216dde0e255fb0 +3cc4afdd22d31d1ec1cbf7d737417a065f1251f4d63dad5f24b42b6d04d2b25d, 7d8f2f07b2285d18b57bc78b0c3872bafc12d4a3516a586dd22775a598e96aa1, ea4fae363365ad3bc1914f17471213b32218ed0c0a8ad5154f01a852fbe231c5 +a187339102c09f69fa682afe59308efd5dd8f9d59ecfba7d3d9989d51343cc41, 905197e1bb022e48c47e4707ddde5d4437d4526ab340f1790c8a44d5cbdc014d, d6742e500b944f60b2ab83f3ed04c256e8a8eeda9405cacf254911c9e870a4d3 +643d4de094c6252e22dcf606352026da68b078d8f111f3c08a098333c01c1c0a, e65ef52fd9498ed3f050a0fb06249875bc1ec677fcddf1a072110ae83298b770, eac0dd5a2356cb7549e1ecdd28ba173bacb550fdf1dfea55e5acae1bf6731b91 +23a0044c210c71f4b4103fb6c5d92801bff6b583f78d65330ed28b50fca15339, 3f5c09ca91ed23dadce3986743567eeedc88bc4ed7c775abf8d8fdd2f28df1ee, 365a789fb42a8512234eb1183cb9cf9d4dfc9830b22e3d05f9e30b8350f6a511 +f77d887e5c6dbd75fb40bdb150e5e34e121033e6b716999d6018c54d72de483c, 43dc046476135a5d812661f4b9fddad49797013749041c575e9609c4713f8116, 531bed28231af68114e2dcce46241fcb714cc07934a58535b1c72bee372fd9ba +46fa044797745feeba9e8b868fc51f37c45cdfb4a1c360972fa48018f28aa1c2, e7f3f14e9cd96c93e7f560ceb75d753b816b98f5c0188ebdaa57443f3e170036, af5f3bfb8915596d37a55bb02402c460a7b4d43f1842d75c6d6a74212d39421f +29e0cfb8e3fdf905dd359fa4db3333679b3d21d49b42a0f06b686e30d59a37dd, 2ed5076aedf9156c5d90bff9eeac3811a1019da35e584d6832d204597882fb61, 3dce65fd2f9a383e45286ac14e10ed434401215f7800d1e7ae0f4453541de1b7 +4dd448c246230bc3f11be1a301fbfec02b60fb7fd38b1a04bd3f0ca7870116fc, f48240f0fbcf926f260be5b7cf68dd7776e117c41eff3c4d43a2d5b3121210e7, 7fc84b31b390655a947b7476b055cc5ce286b49f097889cedac9712509cfba32 +63ceed5b765f4b51ba23db3943924654ae8058e0ec8644eb4f05ec93ddb64935, 3718988df681796c5919837c92cd8d6c894387deffeb4be147582355c8214988, 6b9c6566a40f4c8d6313c1593531c3f95e48a562a30aed9751fb2e3f08036c0f +34c5ffa58fd5050dabd2a26b57aa12c6e897645f041331eac298618597024ce9, 16d89875d663a3708272fcefa0ffb73c1e4d93c4d3baa2015cad1e67bdc09542, ba4e9dbb7e8ad7f86cafb9f4de18038b223260d39cfc70af72db3724b6db047e +1c504fe2820f2271fa22d7f4900e7c6847a9adc69ea5e16df2f98aa2c77739e2, 982d5c7101b10c2af008fb29b072c185d7d4d79415484ea7955a66498f109ea0, 845cd014f802445d474038994453332d2dda40469f91913fbdfe78b18c78406d +a8912024d2d19b63b21875db68a1809f03f2c90bbd042ba7f5a12eff1419ded3, f9a6d177bcf8cf4599e3004e1ed3644ec2c45d854edcb9df0988a466ad6cc83a, 5d7e0023704eca645ae8d0dcb0317e1be4a7b91710fa2782902b8a44eb9b559a +6b7a7ab7cb19aeb27473dc778414e19e5e300ab37517e81d547d7b03251d57cd, e66f3e18d1e7fabb0a83dcf8074b57f90d529f3f7f5192292f6ca10266139816, 4d23a39a5935527b9cb6e03baec756bea2e88eff0b2630d0edd164c27df97753 +05eded4550f09869ec75b4e5106d06cb65e28c84a02b999c3d9a932e5da34951, 5ad04f3e38b3ab49fed60c151f090ce8d4812ba4752932548109f5605c62ed7e, daddc53bdfb790434e9c9d482fec8ea35c2c1af4b3d21c9c42d26c110b35bb2a +558c22719f4e7d11945b97a5c76f9fc438ac57e31343ead0c32cbe3f9bb63b16, dc1b8c0848ef11dcd44c6ccc5874bcfeb98684b649a26cec8881aed9f7f674e4, bfdbdc6f94d04d21837e8ad52b91ffaf512915eb8e400f245142b1b92f8913da +8caa196aca6eea7e1a9ffa8fecec6d831f6f7baa3b5f43e72686121edaf1a185, 6ac73dd34494fdc673d0341273a3ec3c233bdba8e29c37ab7511c3455d6f9b79, 13c917efca6ca014ce9223c154f376b52f38d69d774116250592853656556c6a +543d9996f124168630d5218c635ab04b51c5edfe4add36895dc8ba17fe591d1c, 9ee486e0ab297f9dd244ac9863212bef5ac9a04d442afcbdf29f7dfac7f6c04b, 011e4ab3ee33401fb993ae42ace726e503ca00481f929bcfda9737dae15b8e71 +7b6ae543d0ef6ea352b0ee0615c116cf71bd2bbdee919230810c7fd70732b37f, 93b005b2a39e239e0dfcdd58443bf12824a16f34551e06a065a88bec295ebd38, c7aaff013090dc03e5fd640fdffc53506681634bc4b7f8d66076aceff71f2b1b +5c903f4d9c9a40a7a8fe893ba012162ab463e6c953f77556fd14b3e8b22167af, c98e24852d6db1f823fa6e8103228285f4b231fe4f360d63ad18787dccda6cf1, e4417bfa86a83ee6fbb54f97568ea71079373b890024a4728fc9595693c07269 +3b36f28bb22b971285a6bc7cc26431f99d2419e54bba14c5eeeaa539e7e92e79, 2d162b7f8a135835d9f941d48984fba8866e35fc32940f0f207c29e4d58847db, d3f9c1a3a3d57764647b8e5421c08f1d57aa2c589b38a77ef3935b208ab78399 +a1109eaa8323d1675f84d0ac562c4c79c114a75b92192a375290240d5090b8f1, a9956a40eccea973e3c815614ca5e0efb86fcbc1b1bf07610b109127c9ae9482, e7dec54080b4368576ff366000c9f787506e5fb0f76c47039cf59e4e1118f433 +a3271e5380ef1fb53a7d5bcf891728c0c6f6e0ef0acd56eabb18699c0b16a66b, 3a0a797deab4c9523c9fb6a6630101af0c8fdd7498604cca74a13e0bd2ee366b, 856ac08f92e0db1c8dc4a640fac823fd048d7705db856b61c4b690c13b4d411d +c83192dc0e4f9dd527c076c5001c27241ebc21659ef86c859464982f1118adde, 8c9a6d40b8e8a4c18039755baf2a1f7fe22ae290a59686d1aef46b34b3ced665, aa040a19abd33d1c3e11931c23f0f0dcd9cf6e8cbc6e3557a59af565ab7f2c0d +5ff50d1d8a56af42c4b5a72787f49cc090f59e9f8f69ad1d5db842be33cff9c5, e80f9f6dba9b171856cbaa51a253ac69147defaa5d162adf049fd00bef1d38b2, 6032efb102c7aae2f84f7d4bcc9bad306c27eab7a8bdcf1d91f7a330ba55e329 +1bd2028f4e6030972aa78793d8cda498d08662d8c5d075bd1d22cff3b95944b3, 2814ae876ae86b7b9ecf89e893a38756c147fcf7a577f3fbbee1a0b7cb80ff3e, 6321797c554664eb31229564ed1068d590b5526e46449b4c96f555fdfb587e47 +08236565e69a4fcd88a62d8c44d78b95629c46f5690e5ae28fac657797c6711a, f15c831e99b497555a6b5881e143455c31cfa04e598c4626b9ade318f2f55765, 182fd58b52b52950eddc28975b141724d47cea488d65381e29d39d3c29926ec8 +0f6a4437c4e4aebf4803a036915e9908f82380b5bcd401fc674a4f944445baaf, b626fa4e09a9c6b3ba9cc2631da6a5179828024198f1bb0d240ed045f6a4b6a1, 40e1af0a1ab20d23369084ac30ed1aa1beb067eb9f15754fb1c202246fdb1035 +ba717a06996b11bd87ec9ca5e93a0231f3e455ea2fa8ade9253ad4eca574a6f7, aa23e4528e300097e0a9dfe2b7e6e7db71f902a2c768296f9eb15a30b44ce8ff, 2fdc327ef51e8e074a5ace52b610bfbab0ada652092073c7e4cbdc11326c51f8 +5cad8e962c4a0d7b00765e1c6f4ebbd17468d44deab07ec600515151ac3ab7c8, 7579a5185a34c39a3a928147add251f011a7c621872e444e6ecdf6a006775beb, 091b8603feb97c48c75ded60cf38bc12e21662c143380565d9af4d1545fd415a +3b36db409e70b1267c768d431152be6a8312a0ddfca88e565e09ea5c85ef6fda, 2fc0f040a45c8c3140f005caa3cff9cb2e3c5efd788a214a3eb731ac8d082575, f769e7a57ad73ca3ae1d4b3d6b5d00122f0db96ecdb0302befe98140afe3019d +aabdc34f6a79d9637ac1726b1fc5e8c35a5e55db1ff2e0b21252c3553224cb72, eb7f52715b1e66f699bd3a116f797e0a12d30ffbf947539d26badce3015c7e40, 5f042c9bd4d18c0e7e67bc61d9b62ff025830bf0cc75d64a3192d04276405dca +70dcd66a7dab955ccaa265ad5219482c9e9721c22bea95100d75935988569ab6, c889f45fc8e2c996591d5fcd19a5b83651015a260cccc95da344a88ece7baedf, c3b76507ef15d27d96d7e22d17b56f3e8eee359a492ec3f4dbd73e02bc2a565a +1f4b6ee8735b02bf6ee2c02851e99f3541d0e73930201635861440ec78d5306f, 0e8bf9734f71731b7fbe633f590abc84cf49e8f9b57d2984d4bfbe76d546b33f, 83444048204aae392adab314bfbdfd35a0a5c76ad93317c9b9eec7d728d301c1 +0166a11056b1d820143cc4a4ada13e1b3a5f0dbe198be25ce5423590ad6618af, 1cff13cd82b0689f1e2ac91a6a5bc6aeed6515a78b61cfd4532daba15e5a1c08, 9672e23777af26913d8a6e433af1a4a8bc6f837da63b1a76918bcb152878c031 +cf54ef27b6711fbab47144309b6e90fc3e72203c83153703506617f4ddc283a8, fbd29c2187168967ee46e56751fb22be376fd55b8f689323656e37f6bd5d78e1, 33961812dee929de388bcf04e517391e87ab8d6cd92ce4e9d46c69b046011bcb +d4065d4d6bc5b13fa486a9146626174900d4b3828fd954fff4d431500bc9bf13, f22afcea9cbaf36a6c2ccb5ab7d5026a571af0ebe9cf104c410e14a009be2ae0, e1b94f1bec56bee3ea3f04e2770a3c3090baf12c9be828e184b66ce098fbf3a1 +393e1d55746f1bb98c5bb7d2131643f37bc4edd3d6bc5d07e02564fa37cb549f, c924ce7c3b97f9824edaf88161075660b51a9ed30ba37812c6aefcb3fab7d86b, 82b1438dd13b572fcc8505e6cc225c9f7e1e60ffae7af599de91ce1bb0a6fb7e +62bf6c30ea482b6a9a17f26de679f4d36300a683e3c82d19d7a7e507c3ec1807, 99b5d9f294b724999dfa436acf62bf711a49edb5351df982b84ae880596af50a, 932981258d722c9fdb0da862ff3ba495cfa5c7a2c6da18e0b9687507ae7d3291 +4bb7ac3ee04340812b4b92b1d9bcf8f8c1ba683958702f59a93c98474211def5, d5d9ca675f68c661693d5cb5431a740a36f8bf8eab66da82b4a3bbd4e40f86ac, 323a958c5ea0a6b2f4a351a1a1a27bf315b4e40bf63041d1e96b4110cf690302 +54f2cee88a6d4d735056b8b7eca1aec1c34f229c01a25bb6c34df75ceecc6722, 3598cfdae171e8714d820d8dfaad21553f38711924a7e2fc0bc65f8930512e9e, c620f5e5fa41f8acde5e37bdc543631b749886eb840c5a2bb51bd04f1ff9524e +241ef6d53ea7b83fa0c9aa81bc7cdde94875f4c054cddb9fb31cd07304e9d005, b49df9f3c62871ba67961d9cdfb9e7633bd960a52b5781148412301c515d88b2, 44a5ab8725bea6339991357eb10a4c7370ee5f818527f685ef1c1ca5c3a3db48 +1d1d52dee61db17a01738410e280cb6bf472d72137111cac509f05e85042795a, 95ff4f33bce14fa9ea25ca6b7ab41a7c90adf291dcd0579b69ac11aa89039af3, 699d1c402b4666fc6bf145a2a1a52c90911d0d2d95b3cb8c8566fa99f00b2f62 +f743134f15be64cbea9d286091441f3fcb23705d0d0e7bc483776774c9f03ce4, bfae0c28c07d30714ed7bfa1847e3d32666a795d6e7d909ce6ad930f05f1d23e, f663c18c86b49910725241efc602f6d6a7afcf23ba3a2c26f6e7f9ba5c93d83b +6b83d27ff0dd0a52ee32c5ab5917ade346c39fd8b6e2151e2b679cfd6123ee60, 82cbbbceeed6e6cbee5c1ea2e6a03d112c7a302f548c36c805bc24e3ba2d8ba1, 8e4750478d2d2411b1b0a74df66863fc7ca55f897302a757d061a626eaf46863 +a4c063984e850269ceb421fc1a8834d2525ad137be67bdc134f0a2f2950c7d15, 77944936c17700dddd03fbbc5554fbb99b0fbb179fce87504e92d4318cc5d36c, 941dc6331cacfd787060566959e13126f9543a7aaecc8b32014a8fae0c012470 +0c2a4fd179c9901a73fe0cc06ef0aca6ada875660b134c575a4e21ad3810b501, de73c2f7ff1405c32b1f3abbde49762d971834e4ff7cb0129e3af65b9f4bbaa9, da26ba88594b1651766dd8cc259cbc76afff56d9fa96e3251bd314a1a9295da3 +7c15265cb5eb653c2ff23dbbb96d07cc04ec558e9319a14cc95643370cf02e58, 3218dcda1e655c74ff2ff00cfa58347b49e128f9cd2827e449d8f8953cc177d6, 75868aded4a3c4df6a6f6f60aeade0d3cbe494fbf2752de483a363fc8491197c +2ea0f6cacf829eadb9c24756b0f91194491d0518c9646fd77746ef40235413de, 459d21d494c893ba50644e2af8443784bc9040bd93fe1067f5748252305b58ad, 569dd3476b890049e641e8530bc25a734c984902a7249f1980fec414b4cbb257 +220251966a0a4a3a5247409dd85a36d264ea4a4d52e330c211a617ab232fe448, 8e6265ee6927b610ded261b98ffea44b26154b66e82e9fe9ebb20c973fb1f81a, 27b2d947326e4ad3dfa7e9363bbb03cc01a5e5c98af622afcefa262a6f2cba56 +db10ed0c306243746cc4ca398fb9cae16c21be93cbed67558fd61ffe5c68da5c, 0b2b8ae06d15ccffb23775ef57e559c223e30058a6ef39fe7525b31deb1fa369, 4ac94a81a71f51b459bd8f028c2fdc05f667231af219aabcbe1a2e62604f27e4 +c68bb78bf14c175623dbf65ea9ba72d35310867e989f591ca594782c60a295ad, 15b5913605f4e155bac6a87c28ca0eae5a890ebcd300aba956fdddfc31235bcf, f39d0ef5ea107b3e85fda47a50e331629268e62f28c1d0536aeb7510fc8ade26 +d2ab9938cc63dc2b33a62bbdff448200272020855d931b8bb6ffd1532b143a54, 6583ce731b6bc12753be76049d2fef5b32fc260ae2fff5dab815be3188174306, e1fc9f1f8846cc02d39976e069ca939e327e74f390aae9b393df413a0a1e5dda +e1b45eb5d28306e5d5451266a1829ba3dbd88e63cdec61465e618dcc7ab389b1, 5781cc82ffef86199b3d32c9b3a06d83a1a86726498095cc92550ed31a5b26c7, 0c6e98dd3389ddfbd0b74432d6d8242e7588011f02637991bff1d4ba16015cb4 +2791dd0c7212c8e0b5fff87cdcdad19365e7a1fdd9e745b45ba28f6d57aabc55, 83cee4ffac53f23be86c55c47a1dd7dd0160c8b35860df33499abf59d1499ec2, 4ba8642d57646de179f2bc2d974a89535ddc3c2506e677e2d42a2d93b0a5a891 +89099a83c2084c393d0cb043bfd38bea3b21ed0e6cc6a19a9f2f3fc4ae88ac8a, 5df36c4e0e2163cebdb5f168531daf42338634295d9f22f5146c6c613599d5a8, 4af209c3cc1b69290ceb0c3c1964fd502a422391a92cd5f4bf07ff8194d48bcd +480c1f7c2d24f5e246ed4ed0fd5192a796fb768c8fe0b053f3446f1ca15a8ee4, 3a4de30c0cae2be71810e96cca76daedb90fb727ae8e45b83e03e129b99a23f1, 7b4c7290890ef7bcc4598340eb3ad5c35ac3607dd96ba2cbf28749cb14b7c6e1 +9568488a4b0b5ec9bce29b298373db23b87f4dc358f1632260692c95378d6d40, f58837e87d4268c8e68bb0e266580d1d3ba87e0f2301b79acbf86dd5251d6b33, 2043417b7ea26a0fa15f70c5ce71eeefb5296099a2798a0b0309d487eabeabf1 +f93f2e0c440e73f59eb793305b8067368567be86eaecdbbcedc66d20d767987b, 7889d83ab945110f4d01cd6a07b197d297e68b16ea75ea7131b04b0659bd9efb, 00cf3b0b3f98f95685b8e756595744aaed2bb03c5a0108fc530cc699ded246e0 +f140f54ffbdd10d1fa0baef2274b9fdab712467a3fb550d15bbb62685b6f62bc, a13fc89ad924f884ed284ca53d56f9cbd580bd80c323c40b6368bff357498f7a, e46699bfd36144b8a06a562d20577967d889c349e78dab709c398ac27b2fe2b7 +b8dc36657dadeb95bca8f5ef2d4be3e7c25cf53d8f21608b96be1ade01509f20, f98c7be277e63c8c652bff69dd47450b092aa5f57adf692c92c8794b4d41a61d, ec47e382c7e3e5457b40b7df422c77a400b1a286ebf0e137e8dd00ce7680606b +fdb5520269d6dcf9e51a6fc0f039351bd40574c11b5d5684a5f12ec6a88f67a3, bdf57317ab92d00785d96d3c98f42238bb5821254853ad9b1714c2f12a64920e, a509f5c850467cc89ed48474a7e322396095ecb4b34f2975595578fbdeecac91 +1446539c6b8aeb8fdd4786fb49ed0ef79c043502c5ae5b5a1b8dee8077ec36a8, 052e2da1e6a1be3b4bf60566a699207756cfa6f23d2125d0d1e49de0480efb58, 8dc5de46c0fe09c5ddd649d9e6a59f4231568fb121d38108a7e174f9257da2f4 +7b78daa73045414ddb10c8d9cd092f3e6240d64a1c98b71a6cc7b6818b98ac65, d44d4cb916abaae9fbf1aebb81c1fd1d914047c30a9005accd72925e611332f4, 1ccae304c1241b6020d36b1c77608dfa15b54fedb60bed63ca3f89f1e4e25d0a +e43daa06d74cb9d91b4df806ba730fd4dba793c722000d8d0195a628e585d338, d5f1e3f71a23a5b1e045b94557d19cbb7d2a64633096b4481d2ef8e01c26bf3c, 78c918fe31056cb65fa206d3eb2e150bb85896d05ed54e4a8a3433268586fa71 +709600e730c9f0d8ce84f8d4619dee97f4be3870ec1cca2faa949e5a30d31fcc, 3ecd551246d9cd0cbd0a3178eb070480c1c4ea7dff0dc4d7e2e215fb96be1d5f, b9252f82e3c5f759f9cf44705f4e5c699d26117928f3352033ce39959656e4c5 +1c5c239953d8e8a962b34fb041895fa9af3c64a9e53307f35c2988a85eb50fc7, 29bcfd8230d28f53ea70ced0f99c3fb3fde045b0597730428de4adb297453350, 3cf8ae2d5a22c164ac262715d8e05f43b3c0cbfa1885a469c5b33e33ef3a4ed2 +bbee804d1fc609813811310bd258bcded4cd782bce321fb784e24b1313352c0f, fdc05083b3b67c830d5ff2026792fddee53b190c00751be347f0c28d2cb7a5f1, b5d5ca781d60bbf7fbc1949f39bf4a0e36bb0acbc9738f0f75eda064a127992d +faba7c9fe6ad755b01fe92bd6fc631a367ea377c22e8fe6b21b9fed86cb3afb3, 8411113e01ff799bc5491b2c68a2513f5a911ac8dcaafb8a037cd748706e3039, 777c4f3d7a716e9e69adec815995580497d499faff335bec1b7fc85f12bd74cd +8c980cd2466cc24fccba044576618ca7793f13ce0cb8c4fc4aa74b1f11e53e21, 89a4de9b1b8d7f03d6f4f6c2079c2d31ab35ad72a0de0d831d35c9c961215b6b, b1c478de71f22dfa169aaa21d53726a9b6a3efb4c2abb672beb9e2f33b8c4c4f +2b7b495b8a6d483a895d9d3db173f9f9570d5bf156315ce436ec38bab04b652a, 9c14d052b18da101b3dcfd2503577149ef1f6ff19e32771ca4cf6cf7c968ddf4, 43a1d8699d57da78e33c73ae1f90894310888f79865c0b0d5aec43abe3f7bc3e +a176690ac98dad32894887dde853e8186c92e91bc99262939deaf4be79fca3db, c4b9488ad3e755ff3df72e73660ae2a9d2e94b24e1feb7337cb840b11ebe3bcc, 8811e5eca771b0ff3450f912b1a601b3e0b5be78730aa9dc52830a75c8c7989b +1b637a78c9d2f43196f98303b08884a1b45f17fd29cceb6f39bab4db0491b5b2, 8e59b26ca567269919d9dece6aaaef17e0e067bc40415f279da0adc1b507c4bf, 6bf952ff73202f54c884522dabb8580756e370cfd390b8f26d4282d87541aff9 +afa2f83fd3f74364a6e4b70f1736ac15d4dbdf0793036b3e6c4c1b36427fc471, 952856c49105344a1e51e1c60700e8992a0055ba2c3a9c8edf8336e2e4d9f4cb, 94c97c81ef89ab87698666c274aac145e25712e69878f8ffb0d3a536729dec34 +a404bcf7c9ad7a8a1caec3b67682f21ab12108fd62731ced1f7913f9898ba9f3, 6d1902de673d54ab7cd25616bee3511813022077031a7605a9acad1d8351a43d, b1d5ebfb1209994217ec64f1d26d784832e198b686503220388def7a5a3db0ce +b5d395cb4583f470d388d8355bd062db6753b8405e41d8e19824911ef1f789a3, aeee22fb9901a3358944ec1bb275330c1d71fdf34972a75afce7ec7496af335f, aa291f94a82362823210c0e3c4378f1017d576e6f95a91390c3b9d5b41ddb6f4 +55af59d5b12e127582d95b301854d33d324d0a2df9e90b90b9644182df98e49d, ea1f0c914ff4ea68e7b52de56d33c9b4ecf2f5e5102f5cef49663f86cfc02720, 8e33d68c4a91e500db2472960cc996a63802cec8eecd877ab4af095de4b104c4 +2ae79ebf07a1a33a3df94f29a0528f1893c1550278a12cc997b80c8502eec967, 22ec1831d7c15b09b45a0ff709c36d2d7fa768bc2b0dfd7931d2f36ef4b062b6, 2c9b495d65375d5e41789734186ba60d03b37dd1b7764e59513ec15370e82e73 +1d72138cf9e1a7b7e92da3e08866c3e88cd5126c7a79a63cbea0a820d1a7db6d, 8bebd8c514e4d3deb8f453bd650885a971d4002b576c4876b0a383d343c0bbb7, 5c137e50b52c9895467c2ebc5b17a7af391f946b42ea54dec388baf7fcbf21d8 +a14619bb8d3edf3d9b2bfea2303a18869fe3b58b91db4de74fb8305972d93956, 7f546a8b97600c3c93961e83328f704b716f6c560f7e866ad03b501a320be871, 0b703c4474095582b5c359ea9584c2711184521fb823cdc2b68468927f2d677a +a315c0bb11a9ec759df49e388d6f02ebef328db2808455cc6f971b94e94dbeee, 0a41453f00155db70d571d8b5133299fbf805aee53e5d383a6f369d5cb9ce309, b3552996cbb116485d033bde7f6aa7d8ce776844e6aacbce83ba81ea209b1287 +49532675b676356de6f2dcc5ac299daba228d8e5c9d0188e324db1e83b14bbdc, ced70fe6c67a609f5c63d21e1869c46afc8262f79aa2fe198b3cb15293e2c8f9, 54e651162f46a6d0859c33c714d8b7ec9e6810d5c98d9cd600b46bbf592a4798 +4c043654b67235c41de77858491b74d5b32964b85884be45ec338602f56760b9, 75740e61ccd6ef81623830a3c09133f80db70dac4e402585f6d5409c957db44b, fc2fe9fa828ba1e9ddad03d4f4ed984c3b4bf7b28618d30b5b2c95c76c58fa08 +a7a6975ef3e4d375b09d8c8291632acc56b7ce3211063336256e44580fe85dce, 5ee86c2d2dab6b437e4805fd9fc4a818bcd42ff4457cfce2dc50719939e7a7f9, 1c923c8af753d2d24afc5bee94d1f350bdcaaefc603a77ed36066eb4e69a9573 +485bf2a8fc11d690e3be651258a61fdc87a650791c7b0782a43b4cef99d4a4a1, 6b8c6c57e94e27890851e71afd687486b0f8aa9a5504dd22bcb7dbc06a48400e, 9824bae3dcd3dc48050b32a193c41d4b5e5d1d21155fde839af5828be2090630 +eeaee5149657b70f8f57856e418283b3730fd7463371f2abd1e530f75005e442, 6a7a2317a7614a43423ee132e0de9bfde462beb940d85a4d171325a1e004bd22, b195a6d7514a5a4edd0cd579e928c5a35dd51ed786f934ead29f71a7f5323b68 +b5541d2c73f347595f60f64202de530059c5a3d9addf81642429264b2d92f678, 5b3f4984f40d18b985200cf2aa1990d611a86bc46dbc3f7b16aad0f028e0edb2, 82df9e32c89e91d6d2d7a471dda9e60909defc58b044a6ac2787e02b7eae6e5d +962c914eed8380fe39c93022c3ef5dc551b7293efeffeb80f99ce3d016e02fbc, 4306eaf8e72a0a8efa53f347220059e3a1261f749fca5c1760cdb341e8bf49e9, b42ce84f22b5874f415ac3ce2c7e1ea8e1e2ef7dec37b25c0b3e68060a3c71a1 +f544ed01e23649841824aa2339c66874531e8639107e8408b0fbd797215ee0a2, 768e571a3f5288e58167af8a2b9d9be5ac14bbad46f26fe2c25d28b31b4d6b36, fa0197aa9ca10a15885cd5821961fed14801fcbad271c0c2f45625ddeb155d7b +e919529cabd35bdc2ab892b198b6a4856bbc7457489eccba19a17c1dd79df72c, 27ced58831fa2197117f570fe108c4a9846d2213faff74f1c1fcd3f2b3280ded, e50d0cbf42faa7b45b2239885b458aa459089027bd7525bac47dbb5652df8eed +cf62e2ce4929821d604c0aa068aed2f1adfd3fc923a90e1cfa87f6681ba0eb39, a5fc8e0ed905cfec2dbd0a39f6535af08c355c76ec06acc3e00909edb4b2554f, 74797cae7ca3c1e17b44167dd4ff268c3f07abad802cb0c2c05018b1faeed73e +4297efbf5226e2d49260f1ec6881cb0b3c574835afd28ed57feeb305d5584a29, 8e1821efe3163a9aab1a1735dda065bd694bab11750f2bc0105581fa6c29d4c9, 0231b729929c556cb3fce169e88379885e8b57c31085d39b43a9ddab579b9b66 +df7f21dddb99807141985eee6a5d85d049203c3d87a653eff65478481f4dfc3b, 5136de5e155aee0dd8c256f57de4a72cc88936ab90d2682f1c17742811cbb6a0, 210ab8d6bf090250d9d9f73367685c0b958e7daace83748076236593ee35409b +494d3030ebd3ac12a605fbaeb570899704dfd2fce6203c77dfad8471e1eec190, 09f7775c2c1c53e4e36c2808581c4e91c215f730dd4e72b08c19c5b3b59a85f1, 4be01e70457fac5d8cab386a03e54c5a7df3f6a6db1e099a2d74097e5ff0de48 +83c5be1e6bd56fdffd699a0a6ba5848c07d27fb6a7958db198d6e92dc4aea62a, 1ea615debf3824439046c91b2452f5d66f6b38f99a3a8a2d1eca70e4249585cb, b9043a1cab196fe6f308aaf046bea32afa0b69ee30ead8da290c5777dc594e91 +8041200bb038632c68569fb3427d6628bdf5965657c4ce321f4a653fe789fb58, 60f5c78dfc27fe03ac805e9520f6c21c4eda8b8583bf6f9b11ec2e89d0e97097, 2e33092e38ecdc551f5b1bd9fdf10361f558e38b7b7e720907816a6f676d26fb +c8482e966578aa318d32137049dc56b48af5a55c0b6f26ee7aeefe0c28457420, 6e40f0ee21779ae909656ecf979f16652edc32d374ae2444c528c61b85dd2b64, 95cd5e0303e23f38ceb6cf15ae2012c988758278eb923b9b9afcb1ae17184477 +fa88a17bd0833dc26363e4f3b8779a55c0bc100d802016b5e0a585ce0a33f666, 7f2d690dd240df7f182e2a52993cb18ffc8dc987217d8ee01b3ce6fe3345c61a, aa855c9bf190138ee46c41b52547891085adb207a409714430e272a548e3fcbe +510a9a03b006db6bd6f1a20893d8838ee9da62ba4013dca60d29aa97636226d2, e128bfcfd6cb085f097364887e635a9bad9d24ce056e6023be4ee24c22441a27, d2c0f9f5e3fc0c992690c8cf2b9c76c0389a2ad421b82ed9118f0f3e0349050d +8c6c2072517e0db75c5d0a27ba996a074fd481a9ba084677607220da9dcb329d, 41e94f5d87eca6f53e30faa0847ccecedc53a780d5d39163d985588f524e1585, 67e9b3f53690c3cdb314c112404fc40a2ed11c7a7041ee2803cb16d815691196 +8773afe2dfcb9e41d3dddc280ebaebbb29a03b4b3b0107ee30a833fd4a16bb78, d2dec1b9697d7640743699876bf368efc87d64aeb35c2b107b20277e88f7c492, 306c51ce6bac40f80ccc790b5420c00a2085e923b37f77218fc4ff61f2d1b904 +5b4a86006e54fe0cde72297ff8dd78873200fb9a34916ab8ba7dd600294ba637, 2f679212dbe8bf6b7981ff37a0da9259fc2bcf4ae2430511a5c0d5644c2ed551, 5f930e6e75fce9c85c02f8bd98c5d6d802069fb95fa544a4da1f6839638ab1d5 +07d87d19c500b4e59ef6750247d7ac917f6fb3fedf779afb21588387b5f553ec, 2503037a3b41b76b55b68daa96c082822085958ca9b00923d451666e1f73eba7, 802b8f28b6e95ad43c4e741e8afaf67301f443a8fada54be707e7877a5f9bd5a +63500927b9683b878a3e9f50407b50ebe31164eb45cee26ba66feca4c889ccba, b20ddfd0ca76da8d38054a4116de5b37a4f825bd63d429df39b6c8275fe1feb3, 23e98a3fb3bdd79e38b2fbc034271037a2468e1da2359e386db2a354af4292c9 +66fe4bed24f9aef590e2dc6fc4642605ccfc153b727821758f5e6f5bdeb9aaa1, d140bf06bfb905ba6af7f9bd37cdc058aa4de79e6f9c3e97467b9ece787a8558, 50e8b2ceba232a51631a224b495b8fdfdc8e36c75fe6a7719c67bca6584cf56c +03641c880e60cc789ae13a526d4ca1e50ceb3273970fc441c2e958a7c40a93ad, fc1ee4b911faf9510ab9dc0938e30db3235b5a186251a75f875ff570f9ab4f8c, e755c393d4003f5a7ef8e6854793afb02e635716ed8ad3e21f20658d2a25ca40 +4d8d77e86f61b7633232648476fc0699bf12a79999fbafb130cb4b74876d4ecd, 172b93e50092fec99b3b752d183962798db4353f6ea734feaa73bab23d6e1f39, 96335a8aba41803094a70b438d2b54e17480e3afa8446081776c9ac2b64d11e3 +b0afc196864c9399cb5dbc77e34b9c66e1b8e375106ce09358b3375d99e8ab4d, 6de81dd98c7b62c37a52bd7565232843a154ac2085d76624546b7ab1df5f4352, 6042ed47bac29b576d5f084772b8374c23f068e0c0dd21c319620a6db5a1a415 +df789811436e1cded18da4536dbf4e1b2d1280037003ee6ea4ce679683654185, 100ef2f7393abbcd755190bfb58189bd1537b66d9c3a12c810a220f4444617c3, 13930428bfb879e675c8427dc22c89349128c9b3686b4e7d23c6d7d2c3f8e142 +9a3087ba71f10f7bb33458a8a7ff52a2f57fef37ebf14464b7f2d3874e0715e2, 67a6f63fe58b0687f036e45d55ada7d080fb486fd58fc3f0a15144ca83ab0167, 95d92429e259e87d434dd74cfda10c5f199e19aea0be83a30cefc818fd48e258 +a2121f71d2e3f0e2331af0b5831538fcdfb242ba523a8abee0693d16a4ae9343, b2104b8428847da79fb479c7168a2a88b8a328c5927c7f6ae534d93d0f7cf70a, 25664a34378c5e0cbbdbe88af9fd41e565a9c781ad32ea48a01b2a91e6b31278 +7a04368b80e539dbb100e5113c8b9a8c5d9352ca4b04aa7bf47a421af97915a7, 17d617dad5c17d81a9aad9df33998e6d10d23314157657440719d78325410f78, 49591b418285cf528e2f17af335d415eadd37512415fffe554e527e6f07fcfe8 +ec859bbe5ba1bad3fb029f73dca26b98a208e7d1a7a2887ec2ddbfa8dc454ebf, 9d48e28e17866c09ba9eb4fffcb61f11cbbfe688324f3c2095ccea91b2836c44, 80181615514dd0cb79711c5b6804999fb2f31135669acdd794eaee7476f66224 +97e22cf712f5eefee411407bb57229e16e28388d4b3293fc2e9a9854c39d7404, a05c8c852ebcba0792fc2151d8df1f1fa5b22cab63d13a2c4a994dccdc063bd3, 997be930d0d631130b5453bf35c4227f6ba35e6565e4ddc42c48e1e4ca4a09ab +fb956a0899726ef43bb72607522e671528671184adbb674d68d374581f72805c, 912c626c1c1f1ee35c684aa742f23c3ef51ab0e6d7ad1a17ebdb5cefa1428d90, 2b49b5fed7a3ed1739e7daa610d2e13ef7bb125ec24ebfaf62ab81d1ea70533b +ba46b79dc316204f934eb238e5731f8d37c8cdca03b74cfa7f4e8d6fcd1d91fa, 84ee417b50fb735bb76bcfc2476b3615b00254b0385432d332767f2bb5fddde3, 438c5507c28aa3d3365b3c62bad3c09a7b8aa3fa56e9be2257fca75c06024cd8 +68cb6b13b7869e364c9991b80b6c83a4aefb046c5a887a0ee3f65fec32d82fe0, d992000841e78146d6d737d48358acb3cf94222717f397f292246e3c0903b59e, ab09dc20a529a26dd4dd679ac70aa8bba1bd5d1571a706305bcbdadc7b2bab33 +a771fb668e1746e1be75888c4543c7119bd4bdc36462f4d75cdbb4ae5fca87fc, 144ec2196f0b83fb327fab6e8dcc6890bd88a3481adaa3777b5772ecac6ec13a, b89fa2ee6c5a9a671b6c7e4098a4fb7eb7c725e75f276cf4ad9338accc5ffdf9 +578d27d5029b1515433c4a908d185a1e45548f72beb9fdfd7f34bd2bb616917c, 92d9343584545f70dcb4ea0700395099c0da1b3f9b305538f7e7ce4cffe18ec9, 17c5a302a02646d2ff5bb851948604e2486b69f69d53dbb3f266635b7f6c96aa +1577c32a24632369fa3079c256d04ef09336e1da7a988ae1b792270395411d36, 58ceb0521d266cd7c50f5013a41e590f730d58249d91e7794de77e32a4e2ae1c, 74c1eef55d15f6ce5ac34e014cd33a704df39e8dab152b3718181ef2783c4f03 +9452fa8e98c51ee4af4333f262a6e27693d406e8dcfd856cc25a0170fa5358fb, 90c55c78375e7de9305fa2fe1f5a3a004014a457fde1b7d4ebb8cc437b7e6495, 0200dcd0b492d924adb85483c4bb9dfcdd5d49c6391fd12420a9b5099364895a +c49f69a8f9054b39bc61af83c8f583ffff7f23109af45a5055352b57203748b1, f8751ee4af6f68c40cf11a0f4e1b021e026e1e048b2d513785d42e9f888cf051, 5a72b29c3f4bb257f2fadd35c935d539d488d160b3d2fcf1427d43b25f3da4ba +73c9e636b40f48d40885de931c4303a3226b899d5babff66ab5c2c5183713722, 2d25fcb8c74c1bac673dad09af824107829373d87d91016de16426a069ef408d, 14af5979d0b32ad9f803a719f1f3e952fe588974ce983656965d30ae2080a7e6 +7dc9ff16b0e13c99dd0aa62cfa7c2f0b29bf25a12771f1b3236191dc7dcc98e1, 1a0f6a2fb31bf98f1669f26cdb7189ac2102d3e6fc018de5e8648a0fb69ecd31, 1dd04b68e88cecbe7a1bdc83666fb907d82097ce193fd6aac0010bae07506c56 +1b39981d2f96af24fcfae7fb947461a6d1595a998b9d78306e6a474b3fb34af6, 2a9b092fe4d4c50c495db13faf8eabfcb7de88f403724c9e47aa64e69c3fb7fa, 5b6f21ef9f740d08762549058b19694f7f9d8f370d06d33b74d8f6e98dc8cdb6 +0cd75d16e1547150f4b9c424e42e8bd14fdc2bdf50b139b10ad40b9d9b73f825, 4745dca6f6ffbc482069ab10dee7785c53ab0c187478bdb3f574e75a3b4a4be6, 219009aff5cf35fc9e78ebda0b8b850633af1953f7b5785ea976628fcda7f148 +fe50eb4d4d4886782bac81100c0676ba2e72ecff8d802ef1d9c71691acfd5b5c, 52e670f2260d4a95317a71013a951fda207e2978d2cdf967c2b2ee516bd12866, 256f737c569edd5dd87db1427ed7b831e13d495120aa3ba162e3960f94673a78 +4c9220e2cc51fd5e0cbd91f3b846b1aad7eed16e1ec88502914136cee6e0206d, 78d01eea0c2938210e053f2e6c82f92b2e93818392f224cab0a551eae471a6c8, cb6eb0ecc6510b6c1d24d612bcd94cd867a0333e6e57f25d066bcd7c30e32045 +ea491875a404200a8ac0949aeb426834f8682fc80ca60bf3399b2ab1470be0c1, 18b36cd4614d40cd4f755af697630d6ce601f85268fd4ade4d27dff524871e13, a895001457db58924b3f574392b75382878d3d2be6025bf67da43472e83e5b77 +fdae785d86cb3cc41b12aa8031b340072cdddb562601f4c7e3963318e0908fea, fa170dfa7b46b69638121af04c3992ca3336413143970ec2178596cd4ec23c69, 85850af8fa4bfc011db39a7a1f5c4ddb1a3ba0c61bdc74976fb09a7cfdf5ca65 +4a7cd0e026c41ac54ba688ad4ae30194338756dcc92bb8d7412d7a9a7366d1fd, 09d771de71399fb74dc7556b4b07fb29a36d814caa5ba0e77941cc776e7304a6, 9000a3702fac0b8ff51a7a9ea2f316f50960d0abcaf5515a3d683afcb66b432c +27ca5bbb78925da94f6eed74c398dd718879593b36d462ed6f2e09a2fbfbb1a7, 04f49a4a0d0fa282f12d340bbf406794fe48b0c373b042cb0703fefef64bae72, 54209faaf8c53611bd51e0d9c36a4c7074ea3a001b87f5b3946b9be5a37e5621 +92c43dc32d14d29316ba4f15969be4745be0b2358ad2f69484916f6da3beaf90, 868cfb33be2d95a483dec4a4e9f62ab26ac8d75790d0b49f407bc7d8f0290ca3, 5a3639390ab550b514cc17746268ce609bca006ef5374355784c0330989ca6a8 +21d633eaeb698e090f74a849720aadefb10b304c92140b893479ba3d50d99cb4, 99c0a3f7ed4126f29a60ddbf7f25454b87770823b6dd68a34ef1e0c7da5805a1, 7ccecd87b3a40161401997eb8a660d962a8cfcebd2881df6367cd6641b60d9a7 +3a4a2d36ee8848bd9f34887f194a2da656e0fc670db8a5157cd11a2192645a35, f149f567cd8713214113618f6afb0bd060c706dae52d87a071302fc7b3c5cfce, 097b2881b7bc560f9d10c2b4e67b69fc300c15afc8f60f888df9188982bd24bb +d48d45e5867fd6f259f0b09a1b4837d15e6f7562af93921816c97a1563475892, bf1fa8742cd733831b2e514dd5406fd9706a8e43ac7e79d338a17f0cb2c2c226, 28b11b91ba9630cf55167cd52a5078d63cfab6aa4ec887d2f78925b02eb4567c +007c33b64fec946b35b8454f573d1fe640701d216cc1251f0173873114e178df, 4ddf0497df1bdc8dd85f152fae240ac5d508e6315005a5fb6e45bf7f2ee1fe12, c3ae3b8e042ebafd53bf433e819e50fbdb3c4c5f3731e238a170e07ecba02056 +92bc97f06f43fe23ca449b310d42c8eac8b6ca3a4d4ec08e22fd9bb9c336242c, 29ed8d29138f576ab0daa0ce53c12b7435302d2f6cd34c868c8fa5263d81f942, 50ceaa961753363dc71cb7c37485c2823cf20470ea1dbacb26f328120ec64f2a +278b86a910953c61d6c1c280e505933c1e62ed320e388b6357fd6d8491d0f2ab, 73eaa50733532133831260d36d328890d025e0ddf4e80753fd1f0fd75e26105d, 954680249dd10cb69c83dcfce640db95c6374bb8b2acd5f49436b4edc57b486a +122d53bc4133f91eaa8c0cf970fdfeb35f14e68a0b1c1ad00b583194ea09b5b2, 32fa2faa17b12332bd06f755c1b089246e327e987925e4e1bc1a185e23c53dcc, 4653824976f1c38fe20bb9326afe40d296c75c032499edb1d5be9cee08da446d +8040b21ea2e78254c5527527d813ace0b051791c51c927f7706b5f5f0122c513, c668f503541995285f3fdfe7d908bb8e8f59a45498948ecf611d7b8a030f059c, 8acdec94adc97cbd4cc07bb99a7ea3800c42ca47d95ab1c63f064229689e0eff +c3c0b20a6ca8ef71325cdb72a092e010de87a4a4c5d306c29a7538521b998283, 41f58a061b698abb2cb65f1728a1ebb20acaa678174c2069eca35018c7eb296a, 7d3536ab695ad1d95ac21951eb68a6629ac58512722116383a1edf5eec004831 +bf5a77cc2fa598d0b7ef199010a48fbb924dd68fa82c9b05e1cc8b9e84d11e1a, b1b20ee0607329dc28d1cd54b2a3aa86526048d53c5accb45049a6fd5544824d, 82a5f79d0c1d390c1fbdfcfbc9d6305ed7b6eaa6f4ced45e79303ce1bcfac408 +333cb2db1d3b33d2c627b5876a672974f8a551cd633803460ff990cd470e466f, ff6349784fd87b04a923b5e3b1e0517324069634ba01725bf755810f5f251c84, 5608860ce02957d48a1e7ec28cfaec7fff64227555e69de18328544bdccaca39 +92bca0d1eb4d20305a23745a6390ce02bd6e117b19ef4fe259f25c5a8bf569f2, f3418378b547a1c15dc52e1fff2809b022d3c47e971c9747b64c24c4e9595628, 49055a7f8865111a4e18b471005c54ce2c950b11670c359284d5c8c7b3908311 +e38ad32f5c40ee44a70ca75c447f7a48ffbb5f22cfd7bb89adb68b1afe71c38d, 837c107f71d7d2c0eeda12bf8767b3c4fef2ab8e5c6d745fbe18592a458f5851, 59a37e2567aa80e63a57e19974eb5f7e84801fd0d18377cd6d96b3c065330b95 +a78e313c740cf1fb68d2c9519062b45a0afb65241f77015557c5508a1c46df4a, b0b1395a3cec97db670762a1b1c0cf5b722732ed5be0854c6bad09f7eb33ce87, 1205f1f0108729fef7d4fdc0860c50542a2e07c05e1d3e84e83da9010c49b524 +d66ae7e05ada20b03d0a47f8ef7d39d0f99506ba42ebafb748dd3cb47fbd1153, f1d92f213d03d044062d133840cd380575a4b1f0e83f879678933562169c0145, c6a0c075145614273cd7064b7c149741190c594e08b373bf7faa13e604d5de81 +3a96fd3981a74c05b1b3b8976e7673ff3a2e53af49d7c486429b75e179b2da85, a93d71934b9bb6b752e45e7a15c2eab928557754e9794cc039a16145f792cdb4, 0583b600bb9343634a63e9e9f01cb5f88af197cd9169604c6439e5a7698893e9 +32396fc1c4dcf0cd3c055eff536fd719102a31830642d09dedd11115f4feff4e, f9d46680e8e7475741069561cf99cc0370c4e6b6bc98a26a974b4e0c512b1ab5, 3034e2bfd664069ba292ff75feb3f4f6924222ad642d2e6172d153813e682996 +d802f5dc04fa78f4a0ad928befaba5a0dcf2b6133fca96ebb32332c760a8e187, def85ad79ba4c6a58888d83bf403e8b935e5246f29845a3f24a2869a30269263, 6d7b245912aef1c1ee8b8a07d1185fee12a3f9f5fbbf7b89a4918e7137221dd8 +78e36910d28762d7292b08506d0bae4e6b19f1786787f849ad08d0220a7193bc, 8ecdcf60d70c830ceb6f381a7fb3fe211d9a125b2f884bbc3b6d30ca8aaa081f, 2bd203e7dfb3440eb21bc63cc86bb0f17994b12e401bf619d56d1b713ee10d8c +4aa7d9ec3f34a01c684b640a7fd66cfd91baeb1f69b641fc768f10afdf0baa52, dda5a3931f24edf650ebdfdf9b466840fff4cb647589fbde6a4f7386f76ad723, ef31fd5eb808a2ed9fd32a1ab2648e5df6c6ce55c51809ada921e98c9af04615 +538383c109030c8f498be19671caf88d7879578adba546d6adb5a864fac04e9f, 65a594efe8edfa6bce02aae1cd13acbc54cdf1b7e790ae64806a043bec3b36dd, d535557d78e9622496b50324f034527e342ff1a4ae93675493c924c8c319f36a +d5aa73009b803602f8cb5ffd8b25517ef21f682f5e2e2e76063085f9b8817112, 8705504d84ef8ca170d7a2a05b16ebd73b28349561c35cf94263dee1b8c52ce9, 77c962d39bb66de539b9de60b68babb318fdaaf3fde8157faaa3f3a5bf46950b +730a8e8795a9a6218fd08d4d9c105f5720f972af6979a7551a24a0044393587d, 7c1207600de1aa9128998af53b4ede9766e97640adaa7cf44b2607732f41683b, 2af80977ad369b2e87bfc801f309e729df35b452574e306de1ec34d10821d986 +8fa34c107bb1c90c65e6652c37ecf192b5cb690b868d69c9d05c1bf94bfb7e59, f75ed5e1c7637799f032179d25bb8df9359b91c7d1917e7b3f9c4e98557f3a83, d1cccb623261f4ca568cfa1b63de67055cb13dae19a75e435f9f6efda5e2bde4 +f8644d4e4f90d6512511d3cdbf1cc605593243065f773d2ca1e3e179fd96f820, 5ad5306220b687270cdab6d1c02afb03cb7bf304fbdb3762882b6e726d8d04a9, b699d235a505d02e65b1f422e8bedc385ce77d784c46ead72e5a23ec34abb432 +e755596b8aa5c015132fb79ffb5b6b117337e67fe6cba9db8cf88621055ac4ff, f53e99fee14313e1148f9bab50b698f1608429b5ecdd9a4570ccdb651400794b, c098375d688d819b4eaf5b4aef89f9aa5bf388b07b88c55c589935c58c675806 +b0eb184d3cf442b9d8baf1123f047fd969c4236634a4a852107efe48711e00be, 1197f080bc924bfdd4dd1f0c8e33b6dcebd0d9705ecaf11ec216f5529ad60222, ce67b1b10ae7b3929f3f324f6515d1b5cd1227990ec9761a5e58ae987a8698fa +88d4455e87a15e0e7a3487d6a991adcf9c8cff58087df0d6a3b6c9423694ccf3, 20be381711e7e5ca58bbc0e5d31373bf10a314b84378a8aee3f4ac30752f5725, e019189d45336008459ff95b46133ac968ce95f6e7a2d40100d81d1450195190 +55efccc9c24d64d211d9d21f76a7e93883f2a3abaf5536d03c5fd4b2ea156143, 1272529d14ffabad6fadb18e590d68653e2d8c565f805a7a546cc66890e117f3, a17016ef22a22713a9b45ad6837f0ee644e92dd27967b84a938e5ae90a54ab15 +6161f50bf2d0d41c67273f36cb5cdfff490530133ae5ea5f36d33c591aca11e9, d374a988835424b079ad24e3272e42075f94833d8570c8f9be8890f40bee8122, 4d741aed5b4d1f952896643efce8b901f38a428dc3592bfdcf151415c6d80958 +07eeef65fbfdcfe8539e8c341026e02b51f8fcf0d411770a7540b77e84a3f759, fe3545333c3b556d086ee11695b6b45f27ef5adaefdcf5499f4e3c5df7a9add0, ccf3b756db33c91ab020a3e9892bd77378a073e7fcbfea64db00ab494bb43363 +d6466f348e57bacdd9a970193cecd327984da221538c97f1b5d54a828f54f99c, acb2dd560d14f60c34cf8fc48c4ef0a6d58403656b065652d91f103f74bf7b03, e7e16d7cf9a584606ab2dddf10dff774d03f7e6544082800c6f1442ecd6cca4b +ac45d3bb363ceedb9c46c737adad992d04fb00db8983f2bfc5fa44c0b40e1614, dd34cc29cd80b00a0f1ccc040fd38d8a2e8ea42889cd799c6996b1d8b2c14ae3, f3ed23574ab149e73495c384449591f55199db11f387b4cd994ade57a0c4767d +8872e3fda4361ff9776e43f6011622e73b5a5e8eb856b175835cc89933208df9, f80f689e1d0d2d30970fc80fcd6a8cef8e4e85c790b4123cd678ab5ab28d1c1f, f350ae1254de39eff6b602e23ab58578dab2fa77c4f80a1b938bf71d0fdf2a90 +c40796a69c57ada52d591e4d44b0bc209d6c986ff6fb91b4a2921165b699c8c2, dd8a41fc5004720366e5a61a7007474d994d0a4cba2157de74bb07a31771f24e, b149fab2d7d0725bf6f0b9a0618c53670f409c50887e70818b29a110ef278dba +8a4a465a524e6ce25e658a30b9f1bac240cc8cd7579713a634fc0c14df279c2e, d19e120b6a76bf0a568ca19fa159aaa80564d5fe84067208009b0dacbc17bed9, 2be368f62d5d2bbe6604102c484cfdf7dcb4c9624cdea54699e3a6ab13dd3169 +d31d0ee7c3a4f33ac2475f7cb68d84d454af126547dc9b4abaa8d3242bc64fc6, 82ab5074e654a0a0ace5c61d6a5ecdbdfb1d70b11877359a463a47ec0bb777df, 3a66436978424e99acc966b18dc80db3300422aac44c95bbdc1f88a9eb7acb5b +62477cef7453cc6cf6ea2f3d2e360b6d0df0257e610a10db3cd35b5d26ffa130, 11ca5eee5e04927ca270a10372830915a33ca49de6dadadbc310cb3619a52324, f1254f639c851d5bb77fd2fdcdc5f0a413a4459c3b64cbd9b6757d53e6657cb2 +80d5f1e25740872b1fec0b895b3658fdbed0f2b08ce0055088cdceec8cb14feb, 4a6caa27dc9fda079f471e24cf5ad836b2b4135f6bd643e39ca673c6ec99666b, 649f0b881bef646ca5d68c50fc6d9269acd09728219f6d5ca9536811e79ce224 +907e17cbde23c6538cae55d2013130264d97d704053287d4fe555fe31d11fd39, 8d0b98e18d6eb1b006901acf6e4699548bd96d28482bb3a805aab6c03bd450bc, 464205f7bb7c8012e5623786e2f467d13f0d856b658ceb0930265cb82db75509 +89e5a7ba2af7079869cb6911ceb879e83b56eaa1e0a92893fba1f61e2948df52, 61a616104af65f40eeea515ab40abbb9caa93958346d53f3bc62ccb805a5881a, a23b97685af5267fe794885925fc28b04960c62762c1ec2323b238039abd0181 +cef17c4afefe6d8ee7c4494c9e8e918ea9e30794862e2e3fbe7c143cc52ead8c, 44c68caaa2f151d49c18eea8759061e60109e1cc5afffcda98706123eaaeb29e, 4c90b6d15f752ea448b5aeac8e235e6a7a55bf6263b558e12818fdf6ea7d0fc7 +4bf45a8769d6e5fecb33cb0c4da477818c49561c039b6041924f31ed146445a4, 9bb5b229c2ddf8152f1bd6cf3734cf335b7d86eff6826ff80d51d923863d3294, cbd6bdaccb8f49254c2ca2d62936ba9d34ac18ca283c42eb38af8ecce8b35c27 +3bfb4e5a4c1aea39126a8a3cdbff21c7ddd07e34cce5adaba72f1c40828ae491, 22e97192f4a0ce7c50841654a731a3fe3a3c0161efa10973acf6aa0647ce6dd9, 8f6a271ebd0b63a489f544a352566ac7fe10c858f89dd5ba382b09383e09819b +47a87674d860be2caa8eb706a10e4dd84e5ff324c52a581ae5e4bdd30e327b4a, 01f4ce4466684df92adeb49dd3ac0a7c30ab1d69a34f513f2918f590319175be, 832b354ee78a7efc29160a988c5673b9ee7795400e0c6bcdee5494e5cfc60d59 +2f4d855c542996674997b352fd1efc9bf332e5c09fd8e65713c396a17ad7ca15, 305c57a28728e221cef70499e49a7fc9ae742225d5ca25df82e942527089f559, 5c359a4e45496831c6f9a3496ca93448e0f18e13dbc0def3e3cfa8fa5be61b39 +5d7485b280513ad3c0d57af2bbff321527f46d92f213b431312293aef0aa13f9, da042de761379b5db4e862db421a6e90390687f474fc1db649b09e2a0f3a4632, 235d9d964b4ad9c7d328791c1766969ebec651615fdf164571f4c0b0d87bf410 +b0e068bd269772f4abbcfd8e00b886750b4ef4834b5aaeb0e76ab2d3e2a33e76, 29380f02b7ceb617509268141a3b5bf809331c89ee8a4aedd3e8a97345ba6245, 02c00e5c14b7e93b00f35570b790374b86c8d5b658271f06a86578245914484e +6d5a58b1c052abbe27ade1f8e209d49eb97e65bbfed52791dcde9b70cde6eece, e6b6d92ac86bb32ebc0c91ad831439b596f3aca180b80419718d0aa015f1affb, 62e0623cf313f38c6c741c6415f069f12b749d5c6007f305229f5f94f6d73915 +3a5df40ce2f9615d6d29cb5e2497f59e97240e7028572afdd7ca8dab75aaf39d, 0fd438bc89d4c4abfcaba6a9b1c8d783534c576cd6ba976c5893d24b1b97154a, 56f085abac0600d339695b54876eebcc4c36dd87f2ee2bf95775984675bfaf9c +04b6f0da7548fbce49e7a4c552dde847a4ac2918a57d26ea6536f26003aa8622, 2ea8dc2f03894f0262643685102bf038d229e208dcd5aaa85a5d4784a54b471e, 31226364fd0d6a6241f7884f0a7719f7b4eab4a66e49680c6cf35e91f9741f8b +b34abbf74e38e476439ea10dfc9ca93f1f5346647c6da65b8ffcdae721d5e918, 7ad61a12f7d0aef01b1df392b08b289e59972a69ba7306ccef66f0a59113e4aa, d5fb5a2c92646cbd4af891187d07ee707ecbbd65065808b0acef20962e06c929 +98cb427b06acc9c3b9142c2d998c2afe9b25510c1776dcdef3ef0fcea2a97b10, 5bd1e69723c32aa37c583ebbaf494d28562c4289d845f828d07e1e351f2b8e5e, 8890bba8acc1f70ac02646c8722e2d44fc4a6a8501e056d26d96fa40f7993dbd +58fd00ddd47086831cd219b09bef2066d8a6d278f8a7ce3d7e4117aaa822f9a2, da81398ed4b28e949d0bcd88f497649dec3b51913198f74891d0a902d7f6e0c6, 3819dca4d04fc7adca4bb64858dea119b4e549258fc020a52c4250413e4e279c +707c65f7fda2e109fb8567000613827dcc46352d613dc88560b33e3bb5dafb01, 8a920cdd2a9ca2829b7534e648822f05991c5e3a10b57dcd95baeef9a38204de, 0e749b3a62d648aa5fed220255cdd604d4b694c1b27e96b82980b782ca0a1c03 +c454f62ae9194b44b1b896f63581a01fc95b21405cf5551fbfa13471da2a319c, 477ccdd6e24e6ef4f24c84a153f0e46b1342f8315d23cbe03751ba0c0455fc62, 76137057c4c3b9323bca401ea8ae66f971e94ffac07c7d071ff225543cda8b55 +5bc1cbd21a54c567827295a0b845e2aa4ca13e2ea2c0a3a74e087b8214fa092b, b67130e2afc8adbb84d1f555674a2b515a3a4a5d9c146c96c5c9b9544bc75f74, 8bfed72ebafbee1a1ac5caff60197df22acaff232f85ddeef2e1eeb3d4b9e40a +2cce12835daeb93e52f2b78eb79307ed4867fa923838b92a8bbc211dca658fbd, a354399db860de6d94f8d892e28f11c065695891fc48246c6daa4205f30526a1, 1ba1654894df4a396dbc068dd9d0d0fa6c085e7281689f2a89ecd95fa226be0e +ac5acd51e6da3d2d1ea3416d13496b75087e2bce7f6cf3ad5c116597f4d8e819, 792af8dd9bca67f62f6039dbee3fb2f9e5166dbec0412b71e7f6645fb692d376, e6ca3f88d3439cd0ca383f2c0501bf9de89073b8baf3c867af9ec466c38c009d +49a262b96078a552437ccf9bcc31215fbd7a117d8bca151b5fefb1b2ae689896, a9a9c9d46abf3baf8ffa648d48499bd9f2495ce4a0359a7b90b66751d484ea7e, cce70f2378ee4c2a9be79d249ee225322348f2a88273c8e7db1be36409c8f8e5 +7ee408f96531baebd5217391e57805c7affd1dd2c84dc8442634794a1ea828ce, d2289846186035e3dc85d2be57e347818f4426389df791730f9310a95d4de285, 03b54ba429fd7c92a27ba43356bfc80c0bf22379e012f6666d05c2013a73202f +0cf6374dc48272d9bfed40681943910ed88a03a10546e9c5630f7132c8bb9bf1, 76b409b4acd48cedc756170940cfafcc061a12719a24f7955400d9929a87423a, 692730cb854e66e9201bf87b1df3ce72e27797d87fe98ca1e8277f4dad68b3dc +1bdc39b18cf0089c21a34c0b66018a51f6e2e071d3c7e87ccea8905888b619fe, 5747ef0d20d68623231d698572b8deb731efed176fe62ea1f6ceb7dd1c359151, a0cce673d23e6784bf934a1cec46cc301ff07a2e7f8f173104da5d433988d2f5 +c043e9f28f8201bd0045860fe4a8e4ba8668399d65f7725ce3372351a48edd46, de2db83dfa06eb82f5c5a0e5567de653d2b0885ff810d1507a540d0edacb7720, 80faf6c4e122a18e762714937fcbfd11bfee46b0abaaeb3ff72fc9b35d457de8 +06b4ae91a7a59ca73c5413cce0545a0030a09a999f23bc388d983a11d24a3e53, 912333cfb964152e5b93b61c39ad9c3f64aaf555228859e4c5f370acd6af05bb, 21d7bf363ce657ba61e4cbc83fa20fa8796551db907bd27728600f0ef9bb59ec +e9031ff6688694095fe865b87d6dcec648e6a0f435d701c36f362e6885aeef7d, 04d58f65f032a6f3580ec530a34a0d2ec388b38d6c8062ac1316bddc787facbb, 2049ac59c80badc9441f2736317572068b2dfd60b0c5f1bebe1da9c03c2cb875 +4210aa958d91728c7d35c68cdb5aae91d6a5ca8831e83167bba7e1b975d781a1, 186d4f42adb0242c89f4d0464d548e04db0d5477b469afa5b5d22aebd5a16811, df758350f63754f8264177575faf171f510bc5fa5ffed101c918a3396ea34581 +7330c55f2f023eaf768f0c904000816aaeb6af1a4f49ac801d0a6ba00435399c, 5d0aea68c5f1fd72c341446d8e2b03cc29077988daa269cef3d75c7493b3ab19, 6ddea0680bcc3b950c720551aaccc20f6d2e5a96b86efcc34e7daf61fbd5a57f +cbc93f8c8229e3e03893fa4913b7a17fa6c68efdaad22ba1c1d7de191adc6683, a4d009a556301beb04acc92fc2b7d06969881961b468828bbbe99f55aa3d7fba, c83163684bf5dc392a733fce91c133ce64931f0e5fe16fdc0eb44d21c8e1f715 +77d8402d5cca78c4706a3976d89ffe1c7e4ea92201411da2bf979d8ee80717bd, 79a3d1ac6a704283da997a36d246245bbcc243377063c7758edcbb15749adeab, 0c099d8af878aa34c75a3381a651300be34eccf42160bdf056aa7f421e3f607b +52ec890636c38659937e02a3326a212f1576d6ffaafbe1025af3d614dd3d5fa2, 3f096e169eb8abd19400ee245b6f276e4f9b078ef7f5f876abcb209a4c4fea31, ee6b42259cc730afb4e106a4887efbc21fa740031b3ce42814c9a33b0af23fcc +b26d78ff51df02cae29d00400934090070c815a95c6ad310cabdf7b968888d82, 2691e19c0e15ebf91518d5185985ac347ea279e9430649bb152c0c524e4ffba2, 9465c3ce6a02a9291a045957268795782cbab595d831c30e859178f9d95bfa7c +704777c9eaafdea9aaf428b765b67a48aac4743b417d27fe3284e6e608e06107, 37172fa0b88328b59c6ad5055c44201937134a3fbdc12ff5e4e7b53e822f5f75, 51a15bc1e7c32096e938a179715ef8a9f99047a0ce48a58d17c23a50c01e7503 +5be72e6b29093346b282bb3eaf4e430f59772d8b207daf27a10a75579af61a64, 76d5615d90af56bb1fc97bec545c90b5a5956ac1c96d20127945ed8a5856682f, 32b7673b06226516be198a533c2f60f482830f0d360971900e42df5393538223 +da98b4deabcad59e77c65d5ae4a4893382806404f2ed6bedb3afbb846af222ee, 31030f7d385af05aa01a852af697ce7a7499321d2e701bc5678139c94c88ab57, 36a1cd1a39c538d26f175a4e0c27411b2e7d7ad5ec0193a98c2b2b769459812a +476480323bb112e434baa19ba600581c1fcb38a987c6bea8496fdbb68ebb92ad, e31b244f01df0a8ab24cf6e1f556f571fc406f6c4701e6c3ccd3102952b493b4, 2f27b0023302f2df659324ed90a090628484b2ff3808f03d93b00b9713cf8f07 +66607e2896e4273869a1f3b3d6439b360645a941e773ee8d648857effd2c2aa4, 7d550424183cd2589819b56345a0800fbbfb3f979cfe2589bac64776e9c8803d, e1136e713dbaf3ef97c62eca26a7d2041e339b0410c012cefb3be3d8f702eaea +8b93377b5fc408c3648ba5c4eb91d77eff778eab50f4ec98edbb92c58c09b6df, 80595fae3fea5524a7da31dffe364322beb752a284dd375fd1818f94ad43f8c8, 49c25453c7934b5f1c95126aca646cb7e2e985457fe09374efbd9185d108609a +3ee566dcaa019f6afffc5ac76daebf2da9743b43e8b32778d645df140420847f, 15e26f126585351acdb93b63adc462bcf9d6cd82470209f0017036934a3f3718, 41cd2e8434a798b83909089fbda6b355727ade1f2f692afff393e0ceb9665889 +d870d0da4d44a718e64b6207a54fe44a92791fe7f546672c74b5c7ffc03c5baf, 85792ea127eda8db6e41abd601b911fd1c5e1725278399f8aae6c7a92a15238f, 154049ad9532c71a4d8ffc444dc10f69ddb696fdf391b1e2d004e56d60d3ee2f +73e0107e351022dd208620fbf9b4639c9c39283644815c73c74eadf491cbe93b, d971a4b0308c8d99d3c72d7ab59d6f240bc9a49ed413a7355655162553fa5dd8, d051b89a655ba42c0c88c1c1d41dd89e46bec470fbbf99227fff84a86fdeb308 +8ef060cda043c8c9b1d50082e0b5865614982631934ada510e5f889f2fe6f6e2, c3de387f59404b6658a0a70f62fb86e36ecc5b4914de4f6ec105fb400854d297, 3e03e7e10f77614fc5834954ce697ae9077dc2db744ff5520f0586d65965172d +19d9858aae7ec5724a618b67d27a6dd448677a2f5a1b3184900c95c95cb2c104, 477ea4243782c65524a506eb612f0aab32a2ae41ea2f204f75634f7f77ab9d86, eaf8d51417e9748918fb97c47f5a6160c6142778e15abc8cb372bf710336d801 +12ad67df1ba88e7ea6217efedc505b0e42d1b1fd58603889b180b07d283c5759, adff9cc4c6e54fccb3a9247e385168ee58a4bcf131db3d30da6720e2a6af34dd, b871b0f8bdb13bc8b81ab055f68819cc0c5ccb95989a50be4aac1232526257cb +f668f8b88faafc806ef8160ce4992f428ac5287f381e20bd3909752541aae357, c8f247e0b003c7e9d6171d66ae581037065dbe0975bd0aa6ebd1396a60857d87, 8f1ab371070101825137edf4b9162ef247b92d651947c4058a9ad177d83b7edc +609c71325afbcf644c23283c9ad94c843eb4656750910caa10b832a5493de8b4, 1637ce001f23f0416147834c88fb6195281b7bfd38d5d3c7c47928f1a0bd801f, f0fa06710efa88b3ae9a03d8777095f98dddcb05ebff2374eacb3a0a3d065a6e +b45a8717b47e34cc1c159a76c30bea499255b21a59d11ff4200c726b9f9bf998, f3aa9fb0c8c589934a3d0050e887ee7e6b62fccdfffd4d0ec0f706ab1317bb57, 79af8a2eb95b64353de25a7935ca511feaee9b87f231cc79345b52462a3bf8a6 +47bcddd12ac3c4fa381148170c8f8c92b7dfa45d0edb4e47446c7bbca91391ec, 0eb28a4694ea9b3ba12365e110145ed068b76e9d15681725270687de4358bba3, 1d42b9dc4af6823c270b3002237d4e48f231470dc793233dadbfda0c35f1409d +57353a3327445a3e558edb0eda678e88341cc8a19ddd03b4a222fe24775619eb, 070ee911a686f991e1fc61b29cf4cf6fad2cdc74cc1d2f3ae4f4b291e658aaf9, 581311c62d50b89797f928ac7c7a38feba36d9e66b72161025b0fadcc4f72845 +b14e671295b77c3aa4a81af0aa5ae851ad9ef01ab298ff89430dd68bd02f2942, 61194db888be5ea26edd2edad27573430169e2e2cf91d34273fc0d4c7d2db17a, 4abf33712e5d59fa4c83099ea5374c72a1f2aed408cbef8f186b5a21f169b134 +7326cd36d75ca090e19e0c00324c4ca5dd9840ddba01e7c75ba4b7f297031966, 9d197c2c6bc16373eac42b2de6ac7514b55d193f7807641ae2b602454b214d2e, ec97ba5790dd77076e3c54de03e3fac812bfbdbff16ee8bbd6212b7aba1d7d2b +abf5732c9f9a4899b6a665432efd5483311d52542d027d0b9b369cb161493943, 3c501dcc66265f49edc14302523c7248e00e021e0daa7479138945eb779a0273, 456e115dd0ad656b9934ab7997c87b928a8924d20570e5d9bd7072e3b1149faf +cfb0e6561ddecb93ff86ef2415e46787c05a04316ffe60cede0675f89d4dc0ee, 5b91183611fb4f4fa6d2472611264d54ae2e945af36d201aa5ac4895d5d4e36d, 35fbe945a8aec6d6baa1a031c3505689cb9f28f637d8d619adc3ff8fe378be54 +56a3946c34e82528e36af5a184f8122553e06951282f23df1e5461fa4a4546c0, 76199001a2402e2d636210b1634dbabef26e1f290f3840dc7a5d7fa0c13eb3bc, 26c8b006f3b92aeac7888ee46fa625ffb44b679789c8d1331c1ac5c00ffc9457 +a64ecc0b90d3f56e291d8aa3864e16860bd45e293065793b521f07f69b48d808, 8b8437af86d8d30b1b24effc1fbac7f8c16af7bd41f9c2a8c8ed50bdcf5ea58c, e65c2381e273a13d9006fbe3ca28ac5d71023e8d07577f94d1402a6d09872b1a +04600e2d1cf8bbef2d3cc045aade4edfde895ad6021103eee732a18d712ef664, 9361c368eac01766114141fa53ca57484dce0126c1005c053b135a5c38fdab9c, 15dd541c7b220e303a18d711de96e779886cf0f7fd05f8a1a26207933874a6c7 +1f8246b2485ea617fc067719ac0b2e151af9dafbc423ef614bc40a8b29443561, 68c72c7db152d38232b140aff8ecae60ff5887100000737130f005e4c4894bad, df8f8683c8b4c33461d5809d3eb8b9aeec84009270a5114a4f714e08b9f1520e +e1e5c2e423567dd129dc7a6e399786729ff48bd7d8f9c45a2bbe8864ceca5213, a5f1d4b510eacdb614cf60e8f903d3724937ddee548ecde29a9e5fff37b28df5, 54b6b771059fbd4007d723d5264d269eaabb6929581df5b9c766c45e60107ba1 +74ec2db09a4c36092d6e89182a7d3cc75fbc07a0a40b5fd561843d7cd8ac59ce, c04e6b44d9a1a7f84fb2fe8ba3140dba32cc876936646983c176deff16ec4bd5, ecf5098edebecf7b22b7032da3956527018eb8caa94980498808283330752cfc +0d73c6863966e53602a5cc0b4ed716f9a4d924d8369b90bb631632f5503bf500, 797e30b8bdac0ff20296b16adb5f079b7af43b79fb692a2b02401a71b8114246, 75c0546e62ab9b1e31449548c279c5ae56c36413a32c9d9809455ef177fcd728 +1ded08743fd1cab7a8c940e18b1c4c46486dde4d7eef808802520ca5ac590b09, 6245ab37910c644afe752e9ae6909b49383e13c62377a5a7e3a7a0bf5c0b03cc, e37234a0cb35e29cb89eb47332325dbf2ad68f6354ce19054d26854dfd64ae59 +d6089e2153c5625159111d63b5761ea48de24c8f3ed09af6a8e9f1456a03c965, 2acff1fc203a44a40fba7cafddb342a549c5448f4fb5d59abd99aa149da26386, 5d468d6b034c85988a79adbb56248764cdfe4dd23d812d152a7de6f50abcfd9a +2aad0822991dc0a584b390e5b62e972a007f344e7d6c3fc7da609c581f9a3d30, e5f3d761a257597253699ba4525ebb8b02d40a4cb55cd97246cf67ee69bc7c32, 1637ed8a6dc28c74cbf9698c961581b8daed104f61874d446074dab332c71b10 +121fdd8ffe44451a83fa8a39d508238fad1d877b68dbfe5c862c791ec2699c6b, 010fba950a44c44d41bea3120f61bdededcdaa70d124be40514935cda0ad5ffe, bff90137591ccdbfdfb05752d3d2d1bcf13f98cd8f021346c148ffb10e71c3ee +d879e60301bcedb25a760cbca12dbcd0680cc91e6783087d752dd2ff0f623391, a90cc24e1c069e60a8960b4edca71253fe064082d767e4701113099e0c27df13, fb077e43c8157c846fb1ccfa3393b643be306f9719ba05eae49fd01400e4f93a +5d3818e8be093f45f28cbc5cbdda16245489985aa200fbc61e425c254933708f, e68b884fad760c0167662c98987c6b0b1b114a573e580a04608f84cbc00abcff, 8ee1196406ef4fbd77fc0b705c8a0ea80d970a7f4b5104a7cdf14901f436bf10 +e05a834b27b07e886ed5910f906768b4f9889edf3fa2c737d9e0e39a68c40059, 8ee8c935f573eb88f42c6bbf64b6fa4cfd608d3ef6084946f91161f6ba33f45b, 8c2ec737277742045884bb706b2485e59db07b0addf511e23c677bab60727015 +1ffc20942eadfec2c9372923be6ede74a1c24e2dbfe56c479e84f02b7455e408, 0fcc1eaba8e68e44b9250f43dd4c4d7f04f5b15dba4a8609fca1fc089cb47c30, 2e47c1fa85e51041e8208f52e22fa7f2b403ec28b314c0f77aba87cec0fa2f27 +7543795b1b381a6a024d914a65189c2050bf81d3fcc0fa854535af740bfd9c14, fb94d99c164fe43445670afbcfce2ae4a3c207f9129d596c591c5df9bb5fc8e5, e5101bd41cc86c9d025f127e80c4fd6a74c61cac7a4ce577dc9f4f47daaf9bc0 +8cefacf5b798f1a7d8afc551d6b4be8c7abd9b0cadc19d2ec1fc1f192ec047af, 55316291731a7851d451dea4287148f6da2dddffabba752822b0cceb413c4fa3, 2baccc9adcf09634caa795e25511ff0b45e51002dad7fb1806c8dac1b4defebb +69bbf83c1e01d5bb2f9b0fa16b3082b08e3ccb53142771f600f37b14ee5a1304, a55b50736838ecbcb4921e419de81d6afea99e91938d5c8c83b53cfb4885328f, 475b7e5cd98fc10c2d5f82359114310ed7c0fb89b496633aada7f21c9e545886 +1109e7bc9717fa4364dde4ed2ce56b1fcb163cb7a48a5a044235e75af0a3f0b4, 30a7655a304c11fc31e539d1344af235b5848d7e6d26c4f2d94be5e68de76dfd, 99969f9dafb8bf81a02e7e4557289709c923c934ae8c55aa691cef498915a5d9 +5f124d9c5c4774befde56d4b5bc8bc554d138263e05b042abfbf492e9b53e72b, 45e6a457fa8f1bb45f41c61ea0aa33c2ecbb00fcb2668aeae9d20d728d6309b8, 96329113d1a5c717eaa79859ed163ba9d5d263d5151aa8ddbb972adfdef9100d +d8bf73bdc89ead65d6e94f2f27484604bd8838caf4cae997a03e2c0164bc9bc1, f8767802ccc7c01f016c6b170fe043c20d2072e598e15dfb83d70ef1479c45b6, f175f5f066fdbf73500e0fd908ca3b29fb4f6b31e3c012e6fbb7fd18ee73446f +be5139390ef9d741f1a3fd2ac22cd046205a22caf9d6335d20a2f01ea6122c6a, 16a5af81c671dac13ea4114788e2322a33e20d2af8554df300b7d8fdb356044b, 6145844a3d2b49e60ffae0405bd5a6f2f5596e08be09f6f8c9046a547d146fc9 +a18d72accd6000cb704a3f61874794336bd4793cc1c3b8b921e4b07d42e26140, dc14b211159e7ceca46c9e1a0f606ef20181c5942c0ae35961f48fedf54e0f2a, 65151595c298bf6b1660c65f4d45d75628fb65b1ac6d5081ceb2780770fd18d2 +1fbfc8fc104ae695e38d8773117d3a92b2b1a0e522e6a2b2378203c76b03d3a2, de3710675def99170f95eeac502d48449dbbfb93a8cb8e72f3bc55968bfd842b, f3e2c043a441e4796543777b6f2476666fba7dad755ccbc91c8454c8dee8de60 +a1f20b6d4baf8194f43dcc4396efb67c9c76f10aea5bbdd5a3d5e4556ca2aab2, 814f35583f8d64cd5a523d41094d22515ef6b91e0bda3318a4f6734719725743, c1ecc37c12f05cd2649200bb3d9911d4ac7fe400d45c6e27148ca95a38973024 +942fec10b70a7bb7daf9613a71f4fa7742cae623ef735f29c706bbd871f8b955, 30cedcefcd5a84a004b358235ea549896a576cdd83e5dd2a1780a467802b25c8, d096b485667219d5331ebe93c7e16f8bb9f73ecb1e795e98310cded15c687f0e +1cf6c86007a75e6e51e8ff387a1d7a7a276650306706b25c6fc0eb546f2a430b, 969bf270082e0cecdd26d88efa1c5e592ea23376ee82c020d74e316c0015740b, ab530af80b803fb3ccf5f93177a6288de6420fea6cee44ce9859d745ebfd6c19 +3e62337fe7e2faf71d2806d6aff95e754bfcbeb49a137238f259bd13e067b007, b031f59721613a5b8299a1652738675d810563dd0b3ab33f5f3451180af69981, d40806f96b9cc3f5f4e2f6db626ac8c507022470eb7181f3ff4bcfa012a07bba +76ec758aee3c439ad7f1daca3ad52dcd60b16be5fd534079ee2266adaaa8dc37, a94e9611690756ddf490dedacc6a2565a000f52354d23d2761d3e2c119d8fe1e, 275acfa8e6a37686cf6b48da1d02c8633a68718900a6be45766cf181727a2312 +6f549772df13ec15f440791da9104e967d33aa59307474d551a5da4f99db9664, e5d8974420d7d1c253d5228bb0a2dfa6db654aa639cfec38fb7311bec46e34f9, ce0d37bd05d3ec1f7708d23df02b5add4926b9485cb7bf301996208e3e48157a +6f2b37b78c47f8770f8829c6809a9afddac3e0c172c13ca98a2e2638abc613c1, 1f7908203b08d3b6d69b9b6b0ae5bbef89f8fe2ab0a2e631210b18bbf5ec2ad3, cefc62331c73489013303a5360ffb0ec3a116d2bb69e12304f93e6b8228a6f7c +ac6a05390df8870bf22f0e2d68e979a7a4cd602e8d11a04f881b4d65b97d8c0a, 50bfb3bd5dff41bd65734447d48286b05fadb29dbb9ec727824b7394033564a1, 86af5daffd89f35d808caf3ae5166f34a43d0252e17dea1ce99963ecddfed2aa +e4cf5f736c1c18c2a2182108d9fd361213c7e4afb1021a212f3bc68cd66a4465, a5df7b7612f8154630c0d633de228527cc9eab4088a5915ac9c8f367cb74d5b9, 48931d482856aaa6a04415b89fcdeb41cf35eb3b7a972211c4970d14933f1367 +605603eaba49c11511bbdce8cad426d69c718ca4d0978e03c55df02c04fcaba5, 6eea20696cdbcab6147936bc1d229dddf40ce923608d853caf7356f0d8d6b840, 0879090b9067abba3fd8b9216b95a42bef903b460f2c452da2844f6eb18d0576 +c0f33ea34bb9735268eefbb3cc315706020a6589ffe9ea4025845cdcbca5574e, ddb874e5f90a5018e87973326cc741f7baf1bb56d913cbedc77a4771eac6d406, 64d8e522d949bcfb3c6ab0010f33216693d2c7b6e795734990978785377f5a76 +0a390136231ec5926ad849242bb3e93a5c69ae4afd29718b48b56ac14ef705b3, 06ffbd0b5fe1c467d7bb74cf2a6276e173b97163d7a3f1ed458589e7e82dd5e3, d0c621f607b7a6c3f8e7fd4fef6f6e25cd276761238f467a0b37232ef20423f4 +740e485a0dc2f4399d8a838c6a79f79a79525cbaa303c36e920cf1951b85afba, 43eba3e92f9ca37d7ab5793dfdb136896f82e732676ba9f36f6e8da1fadb4414, 262d0b685787fac92aacce2cd9fba79a49c9665c430552c1aac6619e8cf5b3f6 +76783fa6a19be2a7bcb65a9e13b8dc60c04e394467aba20f0eaca02dcf7f646a, 6f9b05cb7e52e78585c0d950b7debdca8d483926b5836ef0612d2806d3605ebc, 1cd0f4c9d41191d16220f73b1a68173d8e9eb8eb41a0f7f2813822721c4af406 +9989f0d3c1bd981e88eaf9a741cd007f5cc6a9af705c9c85e911d45c76a3fc99, 66b864d545d00bb40159d3dfbc6874cb49c99138d965eb3bf13e3cc78d07965b, 2c6172f08c08da11dc1a9efcb15f5379764ebcc93da733b2475497cbf300d451 +6389e877c74bf1ef4d0527341d080f47cf0f486115256b2e90d12e9ce5d1bec8, 38e98a016b7e495f68ad593f753318510fe567903e2efe88965fda62b681be51, 6951ce3d9f99f27ed07608edc9401ff27eb075289b99d57d6a1704df56ed88c2 +ea18d5e46eb70628e7906dbf4f169afe4bc5abe876d100c2ca40f09f2c2c2837, 2477467b39501ed65cab5296a138d8c0dc1e4b1d4eee4825b86bc652888e2fc8, f270be53ba5928da2f6379a12c826e4ba78a5c5d06d668839c682a750feebddc +6b340d1167a387e2239ef7c9d6fb92e56b9dd7e70dc59843584fd8cc5072bb48, ee395934b3147c5d0c95ab5e84bcc5f8176f53b0f566459817258144ac04e771, c370e1b52ae9908ced79f9b7f83dfecc8e431509f86bea368058f520f06e6f04 +9a72b26852e51cce8377a2c4058346a760ab824bd3f2055732e70a9adb597f52, d2c2c210eb71cec68ecb8eb5763b48c73426545bc11c244f3679e399fb06aa4d, 906f9fd06d7e5256926e087d55aaae229796e57ebec013a6df703f4a7e4ba2bf +88dc10906c6bebc22bca3241427fcb090cd1b5b14790a16c9a7bb1cd735d8f55, 9a2b1509e855879310095f3528891127db71d59da54bb7906fbcec82c18761c7, 7bf6fe0c68a45b98e4868b1e604ac61467c32e6445df7f61e0fa8c26232673d0 +ed5813590470914c2fe3f69ac67f85f424060b406da58dd868ff4b64fcd589aa, 1bb189b511858270dbe10a5996e83bdf95ca162805d1e82b81d67f3d01f037ea, 0d052eec1b3a2a2265af54fd415cd3352fdfebaf7bf57a7a3b0965c4f8d10511 +e5cb6a10359814aed137a987f7ff29b9b79e13161367858c325ff4e3cf09675d, 329334af14a1c97a1a98730cf172590239f44a065c446b31e1dbb5032f2bd6c9, edaa088182ddc36e15f49dc6c9541aa5e3a0826e0d234f2c604f359ae2b78320 +30c5dd294a49abe4b74a101640845ec029f94ef1d00c22c9e624196cc95a4d53, 4de03dbf26077c929ddae08e31a6cebc093da6756fabf61cfa500ab95fa8866e, 8bfb8ffd3a314c88fcba94c180bc7adce5abf84763c1078857157f3caefa0aff +a3830eb3f04f7361481caac17488bbf6b00d49031bd7ae5cbb691b6081e88f28, 866cd264547e5c3fcc7ae06c14fbfcd40e773088ad4783563c45b74390d4e4fd, d78dcffe89d52307b08e5fac364693f220e4cc01c85d1a88df880cca104becbd +9919aa5cee30d867a187ad687846442058e8ad616a52f48578e59e87993c409a, c556d1f7f59a0d62bebf1b36e4b6935312216dc64cb7d1546334e8db245ca930, bf57b1d097537b39ee8e83318a7a69043ddaf75428245e71ceb2cdbb05773789 +39610c4433b295fb61877039122e40353d41d483c2caffcad979c60b21d51928, 4c5cd01ccc4e741774fb74092a1e3432cda16b9906d722a92ca2ba57aae97694, 6b0305a8c4cf240e3a621f281df142ff1dccccf277ff879d95497c51a033f88e +09dd9f1a244b03032379a9b8dd89d8fcf4e296fe300f7a237595932898351533, 1a0859ed9c9499a5f168a4cd03673340780c446a5b0a03f93635b19a76956922, 67592d8260773c4c8e003e104d844583e65f4a3e517edfb736c4cd0170116ad0 +a34f5e1d0ffc653c05e14967830d1a35afb9d516229260404f1d583b3fe54451, 62d8d7d5ec708414e38aeb42075ab3d9d8ff204fc65ae17acc8021ed7830081a, 06e843ea8f6ca0fedf2819e053f84269b695041f53af744667a1fd16a13ecb0f +fb288aa8b0b342d4df3b4bd1cadb71d7181ae697b3922bf9bd67a8ba78b03ef3, 94e7ba899ec6ef40ed851f24db3d0c9fd5f8afdc35e5dbeb97fd7b5bbef8faa5, 44b0af56703ba02c8318815e601e0e27943162b2484b6951de4b248220fa0bf1 +96c42493b136f39f734484bef918b055b662c88d0569d0972ebc340d6e431f6f, 09bbed24ab46fbf9eba629c44ca02a8dfb3d927dd9d639e75c24a1f896e89ffb, 0cbf9f0e61a5e31da16787243bdd29036367fcfb6c67ff8aed343058096e3fce +d1b5fb7d8b35ee6fa235aa76be0d67fb3f8e3eb66f00496350bea43fe5b84604, 15d3a73e7271b1811708a1840c3804f84866d95eadb59b3c5f810ab00f14b2ca, 3ca42232b07a2d5cc24dbc933525f3d5dce5f5489229a4ebcd37d17bc72e2780 +38a4507fd8c05e2c8dcbddf9f11e8e02e1303f8e6af0b1ecd0840510bce6e94b, 1231645d0b136810bad8f610c1b60ac1bf10fdfb9da2cd4a8991a88b765b66c8, eda9c2a2635996ca8db64fd94885580cc85731e2e8fcdd2435fee8bdf8561670 +1ef1e927822aec3fb082e001cde47190c3c2e5b386783dfe2da3dddd388a40d0, f31ec8156dd1b4855e13bbdf3c5e36c7897257e4c1f68b3289d0aa496e655767, 6ee3b5c862d9c54560d1b6478ea7775baa29e3a27f2bb9afd578d370a9eb2caa +747d64c0f3df3c0bebec1b613b7837c0fd7a4e1613ebe0dd47f4f0a3b7ba86f3, 4a8d69c0ed8cc7db1c66c125b11071f713f1e9264d6b30166f84d7cc2c756ef5, 5007362c4afda612f7aff0d2191bfb64436834bd5c407a3bb2300a54fc88e87b +a5bb197ba47605873487ac1f17ba5fc26349e3f1eb72bdd400c33671d91f0cb3, 0693facca3e470793786c55e5ed16b17ce4ea28ea3b9aac71adfe93dbb45a78d, be9efde18b1c557926d1bb83de68f3f132c542eda7ef7752fa380110f870df9b +5eaacd8809df848778e17fc836ba70ce97c7e2f2cdd9fc398524711dc4685621, d43b24a33afb37382cea46d8ca34210676035c1170a6bcf11d81dcc4e126549a, 065529d4aa1a7b0057ba8c0fb5cf49b5d103989cd9dfce9f563313aabd4a0608 +76233f983f5fbfdc9034515383e0a05aedbe26819a11dfc0646b225a8e3916ce, e37c44ed9fe67b0c9c21cada3612130d8a23679bb15a1c735defa09addefbaa4, 0be6d49ab7c145771f9c96e2ce52dc7cf89bfe3de9b157699d7a30425ad05630 +3370e3eaa2ab6507745c0134d43a280558093e8bd64edbe549a1ed9494dfedba, 0d829439c3f15a9734e1e74436e0dac7a758fff116f6625ce99a98bf539bf9c0, 5f200565dec0b3443c60667b6280fef8fb8711dc80c1d972ece1583f0eb58b0a +a97ad0e703f0ffb017a464f9ad9ccd2e9377c902c831d6b0bffaa49b6b7af548, 81957929d85999c0beea37fef876f08dc030bdef2a07664c9280b4622518aac5, ca0f0326128c0aa46cae04610a08c0e36474ad052fabaf7cc888ee443b09c559 +800b3a3bc73573a6802d79bd1609a57f88ab16df9e0d8fb9741a2f576d046d13, 2f4c56116f1496ee24703dfcf021d49a0093dedd8a9d1a692b8318813c4345f4, 0ef9c1f225f180d8bf8ec3340deab2f5fb5f0bb5fe2bc57fb4c9bfcfad0625d0 +bc30737dbdf21dfea2b3fc09da649fdbc0232f2411c192322dc41087e2b5684f, 0d7cb63e218ed54dcd82eba02385b5acaac0b3db463197bf32bdbb313b0b7f58, 4ef1f2e30f02da171093f742c7db6ab1808bbdd60851808ecec921d377aa5a66 +75e7fc921cfaa4633cd04e7210354d40e7bb594d64536c38bdf9c320e283eb09, d6f069952a64023e8a30a8a01880220d2c2fc287f346743b8be79a7671101825, 7dcd3bcba0395c01b68a235bf5fa6d542e1a6d77f2ccc9c1b1e084b004c1d8f3 +3b9bad9421464824b66220da821f925a51ac838c5ea9f61dd37ffb639338fe65, 55e2775c0837cccf90d49aac62f11936cf43b6a6dc8b9bdccbf0974a9895b860, df7a5ef5d9d36889be44abbeffd7f9df279a8301c1ca154b2cb5f6bc28c6f983 +84557eecc6ea62237c2085deac96c74a0f06462f46a4a17398a2bef1093049ad, 0321d16e6099548e59d7c2f8a68a7d66270539c9cc4419e12759f89220ce835b, 0e55b11f1b9f6f2b852fec36a06c6579d60cf7cbe60612a14a01d1609761f4d8 +1ba126ccd1b3f7b1b26262b6ee9706ec6dfc870271ae379597086dfec205fbc2, 401761f50703f1a7e1497668e77ad4c95d6247f1bee168704dbc9e87fa926eda, 17c876f11e4d2cf3be58944c8dc63d38c2cfd3db1925201db826240818b8df2a +8224f769799a40e2e17087710695576456bbb346ff39bd23afc75a6f2596a4f0, 179cc98c1ff592cf4d446b95664c5533711800ca9f33801e474f037ac36a52ff, 98c9198bb56297dcc7842a2b498a1ba8bccc2c0acfc6e2d59798c195e50c8717 +5b1dc56ec8ce89191e9c5f6d06ffe4b2310a1790e113c55ed8fbd9d2b66f4b87, 70501a6071777bbf4b22bfe42b6deb74b67a0f286e97e077888a891335c620eb, 88bb08213efa2a4fc6780b5df0dcc4c79485cf85a407d858f676e2ca356132f6 +a83e26856a4e4c3400e8fd28ec61a87801054be560203164d3e54ece579d229b, 904a4ce1df032cb5bd581567a08bd548de2b7ef83dd6097a14dcd0a3ead32920, c5780e0bc53bacb1d2874344f2095b560babd2bf6faf4a662d8e23ca724b43f1 +a6f4cd59810c590fb39b897eda9b4756e2c700355b34d005ff234422e3d91b18, d94c35faf30263099df9bfd87ae8af055eec50ff6d784589376a4b9ed059c878, dafd7a25524d31f42998fc377c9e078568381dfc7ea853fa6a365e7bb87fe9ff +1bfadfb7429ed9aa7c3e4523dcf5a828632ce5faae9a8efda8d6ffdd74c6fa24, bb7f38f4444c4a75819fa3d46ff4abe42179b516ddc2b5790b888e92b7aeacef, f43881d19c5d772618c017bdd7e998c948559a21ed2ccd0a104b215a282b6f02 +5e41d7d408c8358a1426f29e0a764b60223aec2351864d84dd3a779d0aaa09f3, 1b0fef36cab9f932f3fc4adf3924c08faf1928fae0cf75e549d4df0e19d40e67, 78f041e8e458c0fd266be2c70f50be02346aebdec1a6df7cad76490fd5992ece diff --git a/test/data/scores_addition.csv b/test/data/scores_addition.csv new file mode 100644 index 000000000..5aa5769d2 --- /dev/null +++ b/test/data/scores_addition.csv @@ -0,0 +1,1025 @@ +4-3-50-64-100-50-36, 8-5-120-256-113-100-171, 14-8-150-512-172-150-600, 18-10-200-1024-228-200-600 +28, 682, 66416, 1283571 +30, 702, 63538, 1301601 +26, 647, 66304, 1314976 +24, 667, 64428, 1288193 +28, 634, 66832, 1321928 +28, 715, 65520, 1292861 +30, 668, 65772, 1285605 +28, 637, 64367, 1308512 +24, 718, 65062, 1316256 +24, 640, 65127, 1329976 +26, 660, 67472, 1309517 +30, 610, 64966, 1361152 +26, 748, 64768, 1325100 +28, 704, 66802, 1317252 +28, 652, 68400, 1309717 +32, 616, 65867, 1334816 +22, 692, 64736, 1328712 +24, 633, 64318, 1305683 +22, 668, 68104, 1340880 +24, 688, 68512, 1291670 +28, 674, 65135, 1330138 +28, 630, 66020, 1309504 +24, 702, 68616, 1276059 +28, 644, 66430, 1274860 +26, 720, 64960, 1331512 +26, 660, 62840, 1355506 +28, 665, 65582, 1333088 +22, 665, 67209, 1329600 +28, 669, 67076, 1331636 +28, 664, 67783, 1271267 +30, 648, 65610, 1289288 +26, 660, 66116, 1335335 +24, 644, 66144, 1356608 +20, 642, 68192, 1313408 +26, 658, 66678, 1270965 +26, 662, 67488, 1352880 +28, 678, 67660, 1311585 +24, 589, 65756, 1296210 +32, 624, 66956, 1295984 +28, 644, 65360, 1296090 +28, 716, 64944, 1297520 +26, 640, 64938, 1263843 +24, 697, 66358, 1306240 +26, 693, 64672, 1320208 +30, 674, 68043, 1331591 +28, 688, 68304, 1285812 +24, 640, 65958, 1321510 +24, 615, 64064, 1349360 +28, 638, 67016, 1336864 +30, 716, 67032, 1331360 +28, 665, 66190, 1300828 +30, 641, 64121, 1336409 +22, 644, 66416, 1325336 +30, 637, 61786, 1329824 +24, 698, 63098, 1268192 +28, 680, 65904, 1328354 +28, 644, 64969, 1310384 +26, 640, 66786, 1286465 +30, 720, 65615, 1286803 +28, 704, 66700, 1322128 +30, 710, 65182, 1323520 +26, 683, 67902, 1311200 +25, 686, 64152, 1292614 +28, 640, 66817, 1341696 +22, 636, 67785, 1288112 +28, 679, 63725, 1355736 +30, 683, 63496, 1300480 +30, 640, 64635, 1296379 +28, 633, 66668, 1285018 +24, 642, 67584, 1344814 +26, 660, 67816, 1279632 +28, 657, 64990, 1330496 +28, 664, 65312, 1355520 +30, 654, 63792, 1314839 +24, 622, 64280, 1277120 +28, 611, 67904, 1299509 +26, 701, 65456, 1309256 +28, 690, 64445, 1324198 +30, 660, 66384, 1336576 +22, 684, 67680, 1334012 +20, 608, 67312, 1326864 +25, 665, 66928, 1317492 +32, 670, 66379, 1313410 +28, 652, 65936, 1318056 +32, 706, 65258, 1323340 +30, 693, 67085, 1320795 +24, 706, 64473, 1315072 +30, 741, 66448, 1307425 +28, 678, 67320, 1357312 +24, 690, 66110, 1337766 +28, 660, 66295, 1300768 +24, 668, 67704, 1282720 +30, 632, 65168, 1326864 +24, 661, 66784, 1362480 +22, 662, 63968, 1346904 +26, 669, 64837, 1332920 +28, 636, 65000, 1322202 +30, 685, 66038, 1339904 +22, 624, 69920, 1297477 +28, 669, 65551, 1312064 +28, 602, 67421, 1331200 +18, 634, 65659, 1325885 +26, 643, 66211, 1311028 +24, 648, 67160, 1312804 +28, 716, 65011, 1321747 +24, 663, 65088, 1336800 +30, 685, 66396, 1305792 +30, 694, 66160, 1297696 +28, 655, 67024, 1313608 +26, 667, 66216, 1342480 +30, 686, 66235, 1307299 +28, 645, 64017, 1271930 +28, 668, 63439, 1347473 +30, 676, 68374, 1320624 +26, 666, 64619, 1287665 +26, 675, 66556, 1318736 +28, 688, 65739, 1312320 +30, 662, 63618, 1338750 +28, 634, 68230, 1310240 +28, 754, 66714, 1322152 +26, 628, 67774, 1328303 +30, 602, 67088, 1329712 +26, 630, 65680, 1313256 +28, 665, 64136, 1290353 +22, 692, 68728, 1307660 +26, 671, 65542, 1314176 +24, 662, 66976, 1312974 +29, 684, 63687, 1309184 +24, 712, 66352, 1328192 +26, 724, 66556, 1306086 +29, 642, 64880, 1321571 +28, 700, 65454, 1348512 +26, 676, 68400, 1342765 +28, 634, 64893, 1326384 +24, 657, 67184, 1311423 +28, 648, 64302, 1314368 +30, 648, 66784, 1344064 +28, 693, 65728, 1349531 +20, 681, 63160, 1291217 +28, 640, 63952, 1365037 +25, 616, 66120, 1340416 +26, 665, 65429, 1290752 +25, 706, 65198, 1311920 +26, 672, 66052, 1358488 +28, 673, 65772, 1312587 +24, 693, 62752, 1300959 +30, 670, 67121, 1307448 +28, 674, 65806, 1312008 +28, 667, 66144, 1346328 +28, 664, 68690, 1306856 +20, 652, 63280, 1320213 +22, 688, 66384, 1323688 +26, 661, 66364, 1314343 +23, 662, 66732, 1311360 +26, 660, 63776, 1324176 +28, 608, 66576, 1301148 +24, 672, 66969, 1351712 +30, 683, 66633, 1307308 +22, 657, 67940, 1330816 +32, 710, 67726, 1316264 +26, 656, 65680, 1317648 +26, 637, 64736, 1324552 +28, 670, 66775, 1301842 +29, 664, 65872, 1354304 +26, 701, 65880, 1341368 +28, 656, 65771, 1317742 +22, 674, 62987, 1297944 +30, 662, 67445, 1290324 +30, 713, 65698, 1359376 +26, 625, 64336, 1309728 +24, 593, 66818, 1272161 +28, 704, 67128, 1368890 +26, 666, 68048, 1304116 +22, 656, 67312, 1309436 +26, 634, 64815, 1326717 +20, 680, 65104, 1285574 +30, 640, 65999, 1333792 +26, 640, 64672, 1299211 +30, 670, 66559, 1276638 +30, 681, 64010, 1285968 +26, 737, 64112, 1337368 +31, 716, 69626, 1308288 +28, 681, 64801, 1328387 +32, 685, 66176, 1337004 +24, 692, 67128, 1317696 +24, 736, 68607, 1329814 +22, 688, 65536, 1311247 +28, 691, 66960, 1305565 +28, 710, 64997, 1303087 +22, 681, 66380, 1311448 +26, 666, 66734, 1340811 +20, 646, 64941, 1337184 +28, 658, 65412, 1292424 +24, 690, 66323, 1340528 +30, 682, 69472, 1283726 +30, 633, 65199, 1300671 +28, 668, 69205, 1357319 +26, 611, 65690, 1302360 +28, 664, 67088, 1312256 +28, 648, 66039, 1340208 +30, 682, 66240, 1291776 +24, 680, 65479, 1317206 +28, 685, 66885, 1296433 +26, 663, 64624, 1337392 +24, 709, 65987, 1305092 +28, 710, 65048, 1287764 +28, 682, 66592, 1309147 +28, 647, 65739, 1294067 +30, 644, 61816, 1299511 +28, 688, 64698, 1339034 +26, 624, 67161, 1311498 +26, 692, 68112, 1340220 +22, 648, 65551, 1309456 +26, 642, 64264, 1300172 +22, 650, 65664, 1326612 +26, 681, 65794, 1328768 +26, 631, 67168, 1320280 +28, 683, 67024, 1290723 +24, 614, 65321, 1335120 +28, 648, 69600, 1327232 +32, 703, 62888, 1343732 +28, 656, 68544, 1297363 +28, 678, 65917, 1336952 +28, 655, 65472, 1307044 +26, 677, 64440, 1304342 +26, 662, 64362, 1312107 +24, 703, 68192, 1311392 +26, 668, 67152, 1324818 +28, 685, 66816, 1331028 +26, 642, 65440, 1310056 +26, 700, 66414, 1313568 +28, 727, 66186, 1341517 +28, 646, 67408, 1276080 +30, 685, 65895, 1313088 +28, 654, 67139, 1316065 +30, 712, 63584, 1328974 +30, 655, 65807, 1306208 +22, 716, 63684, 1309237 +29, 676, 66370, 1324852 +32, 638, 65364, 1293264 +32, 686, 64536, 1343696 +26, 684, 67376, 1302826 +24, 598, 67156, 1328906 +28, 716, 64008, 1308640 +28, 698, 61862, 1292850 +30, 728, 67377, 1291264 +28, 692, 66016, 1316160 +30, 670, 66457, 1264827 +28, 669, 67509, 1303250 +26, 730, 67013, 1344368 +24, 632, 66844, 1319520 +26, 644, 65040, 1302131 +22, 696, 66117, 1348096 +20, 640, 65508, 1285365 +30, 678, 66207, 1325198 +28, 718, 66825, 1331536 +32, 661, 66011, 1325036 +30, 636, 68067, 1321532 +28, 653, 68320, 1298386 +28, 750, 66536, 1317253 +26, 657, 64897, 1320184 +24, 704, 63946, 1330492 +28, 609, 65860, 1347648 +24, 664, 64320, 1316896 +28, 654, 65059, 1305703 +22, 628, 66403, 1301408 +26, 646, 65686, 1300938 +30, 693, 68288, 1302463 +26, 643, 64453, 1312439 +30, 679, 66407, 1315888 +30, 668, 64488, 1354256 +22, 633, 66668, 1334736 +24, 624, 66010, 1331235 +28, 616, 66190, 1322031 +30, 628, 67680, 1268868 +30, 629, 66700, 1323096 +24, 662, 65200, 1308803 +30, 656, 65372, 1320808 +30, 683, 65532, 1312535 +24, 641, 65010, 1311367 +28, 636, 66463, 1311226 +26, 654, 66688, 1293520 +26, 712, 65241, 1304717 +26, 643, 65559, 1323416 +30, 667, 64768, 1314248 +28, 682, 64452, 1366592 +30, 580, 67124, 1289632 +26, 644, 67152, 1287616 +30, 648, 67312, 1353856 +24, 660, 64798, 1315888 +20, 669, 66312, 1311272 +26, 710, 66544, 1312544 +26, 644, 66464, 1326382 +32, 737, 64927, 1328880 +28, 659, 65451, 1317906 +26, 647, 64840, 1269152 +24, 654, 63544, 1324288 +32, 636, 66776, 1331398 +22, 625, 66699, 1303145 +28, 703, 66463, 1327736 +22, 656, 63719, 1356452 +30, 726, 65666, 1302528 +32, 646, 69056, 1326720 +26, 697, 65476, 1305343 +28, 599, 67061, 1304532 +18, 676, 64337, 1316116 +26, 673, 67440, 1314953 +28, 714, 66057, 1327440 +26, 660, 65411, 1334688 +32, 748, 64142, 1351609 +26, 629, 63225, 1314360 +26, 686, 65808, 1327577 +30, 664, 67392, 1302806 +30, 728, 64347, 1307276 +26, 720, 64920, 1299448 +28, 663, 65889, 1277056 +30, 616, 62103, 1321252 +30, 704, 67320, 1317344 +25, 662, 67061, 1293696 +26, 666, 64398, 1331736 +28, 680, 64301, 1335127 +28, 700, 67816, 1306408 +30, 652, 65878, 1349028 +32, 688, 66483, 1326125 +26, 646, 67885, 1316823 +26, 674, 65209, 1323296 +28, 704, 65070, 1345203 +26, 651, 65564, 1309248 +27, 634, 64682, 1267840 +28, 650, 65012, 1323755 +28, 693, 65768, 1341185 +26, 660, 64639, 1337656 +26, 606, 65314, 1304716 +26, 644, 64448, 1301376 +24, 642, 65778, 1294720 +26, 688, 66000, 1330152 +26, 672, 67472, 1349233 +28, 668, 65088, 1324544 +24, 652, 63534, 1335152 +28, 640, 64625, 1352064 +20, 705, 63000, 1326656 +28, 659, 66211, 1313964 +28, 674, 69388, 1300224 +28, 685, 67165, 1321456 +30, 644, 66506, 1320720 +24, 718, 67869, 1307547 +28, 639, 67731, 1327881 +28, 662, 68992, 1322792 +24, 682, 66336, 1300876 +26, 640, 63568, 1306918 +30, 684, 64011, 1319835 +30, 668, 65259, 1298921 +24, 647, 65728, 1300960 +27, 682, 65872, 1316464 +28, 634, 63434, 1312082 +20, 672, 65840, 1292032 +28, 636, 65968, 1324095 +22, 692, 65858, 1326464 +30, 694, 63633, 1317792 +26, 686, 65947, 1298048 +30, 641, 65778, 1291440 +28, 660, 67688, 1322386 +20, 668, 68224, 1283870 +30, 605, 67073, 1299376 +22, 691, 64726, 1312764 +30, 664, 65563, 1315290 +30, 682, 63374, 1345368 +26, 669, 64219, 1318720 +28, 648, 67056, 1309259 +26, 672, 67588, 1296179 +22, 648, 64756, 1306165 +26, 688, 66924, 1319320 +30, 737, 65391, 1337248 +32, 654, 64851, 1291823 +28, 669, 66561, 1294298 +26, 653, 66244, 1285015 +26, 668, 66605, 1309461 +26, 647, 63345, 1358016 +22, 671, 67445, 1303242 +22, 672, 70144, 1267699 +30, 636, 67392, 1286212 +26, 624, 66896, 1334367 +30, 708, 65616, 1324695 +24, 620, 65013, 1333579 +30, 739, 65536, 1314490 +24, 656, 65524, 1278698 +22, 713, 67904, 1303368 +22, 642, 65460, 1237428 +24, 609, 64918, 1308564 +28, 608, 65712, 1323744 +28, 655, 65608, 1288824 +24, 667, 65470, 1314592 +28, 672, 64375, 1308343 +30, 672, 65793, 1313108 +22, 690, 65896, 1354272 +28, 643, 64656, 1284424 +26, 630, 64904, 1319035 +24, 670, 66192, 1316832 +22, 680, 65736, 1312764 +22, 693, 64128, 1299834 +20, 692, 66122, 1319088 +28, 680, 65470, 1338127 +32, 649, 63862, 1283968 +20, 684, 68852, 1285184 +32, 654, 65711, 1301472 +26, 592, 67210, 1295675 +26, 654, 66498, 1306733 +28, 685, 66133, 1329248 +26, 671, 64652, 1323936 +22, 640, 66444, 1328000 +28, 699, 65408, 1302612 +26, 589, 65965, 1303686 +28, 716, 67455, 1311520 +30, 688, 68288, 1292575 +22, 612, 63736, 1317920 +24, 642, 65994, 1304192 +22, 634, 65553, 1321312 +26, 693, 67334, 1314544 +28, 689, 67703, 1302919 +22, 738, 65554, 1338427 +28, 678, 67881, 1333554 +26, 658, 66176, 1306384 +24, 673, 64800, 1307547 +26, 654, 65394, 1344912 +26, 659, 65592, 1308002 +28, 591, 65336, 1321224 +28, 678, 65952, 1326442 +22, 663, 66588, 1321410 +26, 679, 66451, 1343000 +30, 680, 68432, 1308985 +30, 627, 64448, 1294334 +28, 638, 65445, 1294016 +28, 599, 63873, 1344960 +22, 648, 65011, 1274979 +22, 668, 66613, 1297450 +30, 692, 67519, 1312453 +24, 676, 64388, 1292581 +28, 696, 64928, 1319044 +20, 660, 64760, 1289750 +32, 692, 65248, 1324313 +30, 666, 65004, 1310768 +28, 704, 66080, 1307488 +26, 682, 64464, 1288163 +26, 659, 67632, 1326576 +24, 641, 65616, 1315874 +24, 692, 63467, 1293110 +20, 644, 67213, 1292791 +32, 646, 66368, 1296032 +32, 690, 63506, 1295536 +22, 684, 65453, 1310518 +30, 676, 64105, 1322127 +30, 660, 65029, 1318697 +26, 619, 66672, 1327040 +18, 666, 65068, 1295090 +26, 674, 65136, 1288681 +22, 571, 64201, 1311096 +29, 648, 67823, 1329890 +28, 630, 64994, 1293728 +26, 664, 67056, 1303616 +28, 676, 66214, 1331520 +28, 685, 62528, 1293254 +28, 659, 65745, 1321984 +24, 648, 64032, 1316341 +24, 662, 64841, 1314868 +26, 635, 65521, 1314264 +26, 664, 67712, 1285522 +28, 687, 64296, 1320048 +28, 674, 67152, 1336505 +30, 672, 64284, 1285004 +30, 699, 64832, 1319266 +24, 766, 63840, 1329728 +26, 609, 64846, 1300928 +30, 668, 65584, 1303315 +26, 680, 65840, 1303864 +26, 610, 65156, 1356672 +26, 692, 67664, 1293472 +28, 658, 65428, 1336506 +32, 658, 65790, 1313920 +22, 689, 67131, 1307671 +24, 636, 64636, 1323928 +26, 688, 65448, 1328128 +30, 672, 64865, 1332944 +32, 645, 64552, 1265540 +26, 694, 66336, 1309437 +30, 665, 65216, 1301536 +32, 640, 67216, 1271936 +26, 748, 68800, 1304736 +24, 700, 66568, 1339536 +32, 600, 63486, 1289922 +28, 660, 63869, 1319657 +32, 600, 64206, 1312320 +24, 731, 65733, 1284667 +26, 645, 67672, 1319858 +30, 678, 65800, 1310448 +22, 684, 65479, 1336964 +26, 698, 65126, 1323408 +28, 684, 62431, 1343008 +28, 697, 64968, 1307178 +24, 633, 65212, 1339296 +24, 676, 65488, 1343436 +20, 666, 63839, 1356512 +28, 618, 64848, 1331499 +28, 724, 66589, 1291508 +30, 666, 64861, 1326369 +28, 644, 65117, 1295992 +26, 626, 67571, 1299104 +30, 675, 65851, 1309641 +24, 609, 66090, 1304263 +28, 670, 66881, 1328919 +28, 711, 64982, 1343584 +26, 652, 63612, 1290011 +24, 645, 66637, 1318981 +30, 689, 68303, 1322891 +28, 712, 66408, 1324200 +26, 687, 66632, 1344258 +29, 640, 64168, 1300352 +28, 604, 65870, 1316292 +28, 659, 66656, 1294938 +22, 692, 65271, 1321728 +30, 616, 63582, 1301568 +24, 622, 63265, 1331920 +26, 652, 65224, 1338573 +26, 678, 64344, 1323704 +28, 612, 67800, 1316632 +32, 694, 65102, 1331856 +23, 668, 63826, 1335776 +30, 690, 66033, 1280382 +28, 704, 63907, 1342492 +28, 653, 66904, 1309408 +26, 704, 67776, 1313984 +22, 698, 67312, 1343616 +26, 675, 69111, 1342470 +28, 653, 66206, 1285752 +30, 676, 66492, 1305407 +28, 656, 65113, 1336872 +26, 626, 66128, 1270154 +24, 602, 67501, 1289828 +28, 661, 63556, 1315933 +32, 724, 66540, 1318980 +30, 646, 64672, 1326709 +30, 695, 65030, 1288736 +28, 706, 65424, 1319025 +26, 670, 68272, 1313197 +28, 656, 65828, 1331924 +28, 632, 64882, 1309412 +24, 620, 63895, 1289301 +28, 686, 63593, 1341440 +28, 721, 67437, 1340280 +28, 647, 64728, 1330000 +20, 667, 67815, 1323808 +24, 637, 63721, 1322752 +28, 639, 67184, 1301240 +30, 680, 66348, 1314400 +30, 659, 65576, 1303364 +26, 661, 66393, 1315720 +27, 650, 64477, 1295265 +28, 676, 64928, 1284141 +28, 697, 64064, 1293926 +24, 658, 67328, 1286464 +26, 682, 66914, 1294903 +28, 637, 66960, 1299233 +24, 646, 64911, 1320163 +32, 688, 66722, 1313078 +26, 629, 66771, 1314592 +24, 695, 66424, 1347008 +30, 672, 64906, 1319513 +30, 664, 66093, 1310208 +24, 660, 64758, 1325580 +28, 660, 64009, 1323136 +30, 697, 66808, 1291358 +30, 611, 65965, 1342136 +26, 679, 64583, 1312688 +30, 674, 63756, 1305553 +30, 675, 63856, 1307584 +28, 654, 66538, 1282384 +28, 716, 66208, 1312684 +22, 660, 64408, 1308704 +22, 640, 66040, 1330413 +28, 652, 66258, 1341698 +24, 669, 66786, 1324864 +24, 624, 66388, 1334623 +26, 654, 66960, 1333764 +26, 676, 66960, 1312836 +28, 647, 65265, 1295777 +28, 656, 65848, 1334656 +26, 670, 62988, 1311660 +30, 664, 64426, 1263358 +22, 615, 65099, 1291614 +30, 704, 63831, 1324561 +28, 700, 65486, 1280555 +28, 640, 67165, 1330931 +26, 632, 66384, 1336424 +26, 674, 68704, 1262880 +26, 670, 65682, 1300528 +28, 646, 65900, 1323216 +24, 704, 68276, 1309440 +32, 688, 63864, 1300105 +30, 603, 67845, 1312448 +22, 691, 67784, 1312890 +28, 695, 67376, 1342194 +28, 663, 67947, 1314626 +28, 661, 65156, 1349873 +24, 616, 68400, 1319200 +26, 709, 66567, 1329648 +26, 644, 67178, 1306449 +28, 664, 66024, 1334950 +26, 576, 67266, 1288800 +28, 678, 67128, 1338591 +30, 629, 64296, 1319756 +28, 656, 66566, 1294832 +30, 661, 67960, 1314055 +30, 686, 66136, 1353366 +28, 666, 63736, 1304115 +28, 690, 67303, 1302304 +30, 678, 67203, 1336498 +24, 714, 64956, 1340960 +30, 614, 63114, 1328608 +20, 648, 66513, 1338688 +24, 680, 65517, 1306593 +26, 637, 65552, 1299681 +28, 612, 64674, 1319344 +28, 663, 64052, 1335804 +24, 630, 64725, 1326008 +22, 638, 63560, 1321440 +30, 776, 66600, 1338912 +30, 648, 65526, 1292499 +26, 656, 65344, 1324352 +22, 716, 64996, 1310304 +26, 722, 65772, 1296656 +26, 676, 64209, 1280113 +24, 649, 66099, 1300992 +22, 634, 63664, 1290171 +27, 663, 64771, 1306633 +30, 654, 63994, 1306640 +30, 665, 66820, 1311527 +26, 674, 65879, 1298284 +26, 684, 66704, 1271339 +28, 694, 66193, 1313280 +28, 703, 68338, 1314261 +26, 676, 64084, 1311862 +22, 670, 66016, 1310839 +24, 658, 64172, 1335899 +28, 665, 64945, 1305368 +26, 638, 62842, 1337386 +32, 668, 66368, 1325857 +24, 692, 67432, 1313216 +24, 676, 62900, 1316352 +26, 606, 66110, 1299170 +30, 691, 64741, 1318601 +28, 674, 66626, 1313098 +26, 628, 65827, 1315298 +22, 704, 63802, 1302169 +30, 706, 65984, 1314381 +30, 669, 62273, 1329536 +30, 640, 64160, 1323680 +28, 661, 64298, 1307174 +26, 656, 62519, 1306480 +28, 663, 66196, 1340976 +30, 620, 63257, 1356356 +26, 666, 66071, 1282464 +30, 658, 65549, 1349434 +30, 661, 64825, 1333864 +28, 676, 65572, 1315805 +24, 672, 63623, 1322944 +28, 650, 66389, 1318779 +30, 612, 68792, 1302896 +26, 651, 66223, 1334736 +26, 638, 68032, 1349556 +28, 687, 66377, 1332413 +28, 710, 67824, 1302086 +26, 652, 66646, 1346056 +22, 590, 66963, 1299591 +26, 626, 64822, 1303484 +28, 620, 67448, 1285794 +26, 654, 66527, 1315888 +22, 594, 65728, 1346240 +30, 640, 65523, 1291260 +28, 669, 64373, 1314405 +28, 632, 67424, 1311164 +24, 660, 66871, 1328520 +30, 640, 65574, 1322270 +28, 667, 64208, 1349696 +28, 644, 63648, 1288331 +26, 640, 67288, 1330288 +30, 674, 67010, 1327868 +26, 667, 67264, 1312033 +24, 685, 66722, 1324189 +28, 688, 65998, 1287840 +30, 675, 66406, 1307854 +20, 642, 66603, 1292024 +24, 655, 64995, 1287256 +30, 633, 64219, 1291112 +22, 682, 65742, 1296304 +28, 644, 67037, 1309712 +30, 669, 67069, 1305271 +24, 684, 66564, 1337940 +31, 629, 64249, 1295616 +26, 680, 67558, 1325696 +26, 694, 64764, 1293365 +28, 644, 67045, 1341968 +24, 646, 64668, 1285095 +28, 699, 66239, 1323614 +26, 691, 64505, 1316897 +31, 626, 66184, 1340416 +20, 708, 66232, 1293824 +26, 687, 64528, 1306272 +24, 688, 65024, 1292816 +28, 701, 66142, 1297247 +24, 636, 66352, 1333184 +26, 680, 65736, 1333976 +26, 689, 65090, 1316534 +28, 688, 65168, 1299604 +26, 677, 65307, 1331200 +22, 648, 64789, 1337888 +26, 680, 66255, 1314560 +28, 697, 65396, 1317616 +26, 613, 65721, 1331509 +30, 674, 66330, 1310897 +20, 707, 64533, 1322478 +24, 690, 67712, 1318775 +20, 666, 63713, 1302036 +26, 622, 64281, 1347008 +26, 628, 65226, 1317776 +22, 698, 65700, 1318871 +30, 608, 65283, 1305890 +24, 632, 68386, 1316864 +30, 650, 65120, 1292549 +26, 626, 67504, 1293766 +28, 736, 67336, 1339577 +25, 674, 63728, 1283847 +25, 649, 66556, 1293120 +24, 655, 66340, 1311834 +28, 696, 66551, 1327120 +30, 684, 65692, 1321872 +32, 666, 65308, 1315246 +28, 704, 64348, 1319760 +26, 580, 66826, 1336192 +28, 620, 66496, 1302991 +20, 692, 68848, 1316104 +28, 678, 66198, 1329913 +30, 672, 63229, 1302704 +28, 717, 64559, 1305463 +32, 672, 62947, 1313504 +28, 696, 64544, 1325664 +32, 672, 66328, 1318528 +28, 668, 66819, 1294272 +30, 676, 65664, 1325904 +32, 642, 67364, 1289720 +24, 646, 69888, 1318544 +28, 664, 66064, 1308451 +30, 660, 66544, 1308736 +26, 662, 64084, 1291424 +28, 698, 67981, 1342329 +26, 667, 66495, 1339215 +22, 688, 67393, 1333040 +26, 642, 69280, 1316860 +30, 604, 67635, 1327232 +30, 695, 62056, 1264616 +22, 663, 68057, 1330156 +26, 631, 65378, 1297264 +30, 600, 66960, 1282624 +22, 724, 67432, 1332056 +26, 634, 68461, 1271384 +26, 648, 65953, 1306934 +28, 639, 66560, 1296321 +32, 646, 65104, 1297640 +30, 646, 66410, 1320860 +28, 634, 63407, 1318080 +28, 654, 65583, 1289244 +28, 650, 65788, 1280174 +28, 673, 63868, 1307620 +26, 606, 65429, 1346864 +26, 646, 67584, 1331400 +22, 654, 69320, 1312047 +30, 617, 67604, 1303504 +26, 636, 65637, 1298266 +30, 632, 68398, 1323375 +30, 634, 64232, 1291456 +32, 662, 65406, 1314321 +26, 740, 65230, 1302288 +24, 616, 66430, 1310715 +26, 672, 64849, 1332331 +32, 647, 64453, 1307591 +20, 659, 66089, 1271593 +26, 681, 67904, 1326286 +28, 605, 65160, 1328408 +28, 760, 65919, 1304211 +22, 624, 62823, 1268006 +26, 691, 67477, 1304094 +28, 727, 65678, 1318919 +26, 740, 66480, 1349213 +30, 697, 64031, 1339328 +30, 694, 66359, 1287558 +22, 656, 67253, 1286386 +30, 648, 65487, 1303649 +30, 683, 66360, 1344771 +20, 680, 67137, 1327023 +28, 620, 68771, 1306665 +32, 668, 66623, 1319938 +32, 664, 64759, 1340612 +26, 654, 66560, 1286118 +28, 649, 65942, 1335552 +24, 664, 66616, 1281240 +24, 692, 65362, 1316818 +30, 618, 62688, 1301296 +24, 654, 67711, 1329440 +28, 685, 66445, 1298216 +28, 716, 65931, 1275161 +24, 680, 62635, 1303767 +28, 696, 66816, 1353541 +28, 708, 66323, 1300584 +28, 661, 66659, 1337272 +32, 660, 64658, 1353216 +22, 672, 65418, 1301728 +30, 693, 62000, 1321863 +28, 650, 65284, 1314853 +28, 670, 64908, 1354000 +30, 707, 66211, 1314094 +26, 684, 66768, 1308377 +24, 692, 64384, 1301584 +18, 690, 68914, 1338334 +28, 654, 66384, 1251521 +26, 686, 66268, 1310187 +26, 644, 63788, 1308304 +22, 634, 62820, 1336096 +30, 682, 66518, 1312719 +22, 689, 65311, 1328448 +26, 664, 65381, 1317760 +28, 711, 66714, 1326618 +22, 698, 63200, 1327552 +26, 631, 65174, 1309352 +26, 674, 66498, 1306976 +26, 666, 66093, 1290827 +30, 599, 67392, 1326418 +24, 652, 65782, 1306560 +24, 613, 65318, 1314762 +28, 678, 66192, 1326584 +28, 648, 68480, 1297708 +28, 660, 65235, 1352640 +26, 704, 63960, 1305806 +28, 679, 66553, 1322578 +30, 648, 66098, 1311366 +28, 650, 69089, 1330112 +24, 608, 66944, 1352512 +28, 662, 65128, 1298820 +24, 650, 66805, 1328303 +28, 672, 64331, 1333184 +26, 748, 68568, 1310408 +28, 576, 66351, 1307740 +26, 686, 66182, 1299796 +30, 668, 66168, 1281887 +28, 727, 66388, 1288356 +24, 642, 65826, 1319256 +22, 672, 64475, 1310609 +22, 672, 66274, 1323536 +28, 618, 65521, 1302739 +22, 640, 69275, 1288653 +22, 682, 65302, 1315069 +22, 630, 66104, 1330832 +26, 625, 66040, 1304934 +30, 672, 66124, 1314641 +26, 662, 64697, 1300209 +28, 656, 64051, 1317649 +28, 678, 66717, 1349147 +30, 710, 65760, 1313709 +28, 703, 65728, 1273418 +26, 694, 67888, 1300448 +26, 666, 67638, 1313657 +24, 651, 63271, 1271767 +28, 670, 64633, 1329348 +26, 624, 67424, 1338506 +28, 660, 65232, 1315772 +30, 702, 64715, 1342976 +28, 708, 67072, 1325637 +26, 670, 64216, 1294687 +24, 693, 67307, 1287024 +22, 685, 65374, 1325833 +24, 702, 65553, 1290374 +20, 676, 64678, 1343720 +30, 692, 65453, 1343300 +28, 698, 67507, 1327040 +28, 666, 63376, 1319392 +24, 644, 64792, 1316668 +28, 672, 65496, 1315511 +20, 690, 67076, 1303640 +32, 692, 68348, 1306087 +24, 664, 63661, 1321846 +30, 642, 62988, 1314257 +24, 692, 65448, 1292841 +24, 676, 69405, 1326592 +22, 624, 67688, 1340985 +22, 608, 65272, 1298976 +30, 578, 67468, 1311040 +28, 600, 63862, 1313830 +30, 684, 64188, 1286719 +30, 664, 65013, 1321722 +29, 680, 67584, 1312332 +28, 676, 64170, 1286696 +28, 681, 65472, 1316663 +32, 666, 65816, 1352249 +28, 676, 67284, 1332001 +26, 672, 63222, 1288224 +28, 688, 62114, 1292348 +30, 704, 68096, 1323209 +30, 680, 66156, 1306624 +28, 664, 64022, 1325292 +28, 605, 64795, 1302732 +30, 636, 64744, 1295386 +28, 626, 65042, 1312352 +28, 702, 66884, 1327649 +26, 628, 64991, 1342144 +32, 676, 65572, 1281384 +24, 660, 65296, 1327063 +26, 674, 65844, 1326836 +28, 686, 63189, 1294764 +30, 674, 64173, 1321309 +28, 678, 66648, 1278308 +30, 638, 69232, 1311256 +30, 723, 63743, 1322544 +26, 650, 66607, 1346367 +28, 658, 65027, 1294768 +30, 632, 66456, 1338336 +26, 653, 68126, 1302325 +30, 719, 67119, 1265909 +26, 670, 64099, 1308208 +26, 640, 66160, 1331840 +22, 635, 67936, 1318792 +29, 660, 65161, 1333607 +26, 684, 64290, 1307072 +28, 663, 66356, 1316507 +24, 664, 66912, 1304832 +24, 652, 65444, 1291098 +24, 688, 67296, 1299377 +22, 671, 67609, 1306990 +32, 672, 64557, 1341099 +28, 679, 66079, 1329352 +28, 672, 67110, 1289236 +22, 634, 65672, 1304028 +26, 708, 66808, 1342079 +28, 714, 65876, 1330848 +32, 686, 67164, 1309656 +24, 709, 66404, 1329808 +22, 674, 67309, 1326240 +28, 715, 67181, 1285238 +28, 714, 66934, 1341351 +26, 697, 65912, 1305800 +30, 652, 64728, 1348032 +20, 698, 67065, 1280378 +30, 681, 64970, 1290124 +30, 712, 64818, 1296336 +32, 639, 66459, 1317366 +26, 620, 66916, 1353351 +28, 700, 66757, 1326812 +26, 704, 68961, 1337664 +28, 662, 65304, 1300089 +21, 692, 65296, 1319392 +30, 700, 64015, 1339328 +28, 694, 68640, 1294491 +26, 653, 65352, 1299075 +30, 646, 65744, 1322232 +30, 664, 68144, 1317376 +24, 650, 64647, 1311066 +24, 646, 65878, 1341320 +24, 663, 64390, 1324455 +28, 626, 65064, 1320640 +26, 620, 65872, 1334928 +28, 693, 66473, 1305664 +26, 628, 67200, 1293216 +30, 667, 66572, 1296400 +30, 674, 67362, 1327235 +26, 697, 65808, 1325768 +32, 680, 62112, 1344960 +30, 632, 67315, 1295665 +26, 685, 65689, 1310048 +28, 621, 65180, 1344951 +26, 668, 68928, 1315316 +24, 636, 65046, 1333804 +28, 616, 65536, 1333376 +28, 692, 66088, 1315072 +26, 696, 64992, 1285901 +26, 704, 64602, 1290424 +30, 724, 65728, 1350546 +23, 664, 66732, 1315376 +26, 688, 65099, 1270464 +28, 647, 64656, 1325259 +24, 647, 66664, 1287502 +20, 712, 67080, 1348608 +26, 650, 66807, 1365376 +24, 646, 65414, 1308049 +24, 645, 63111, 1324702 +28, 700, 66410, 1280960 +28, 674, 66415, 1330872 +20, 704, 65536, 1292065 +26, 684, 64560, 1298346 +27, 684, 67707, 1322756 +22, 691, 64613, 1292844 +30, 647, 66716, 1258181 +26, 672, 66756, 1318086 +26, 692, 66673, 1332342 +22, 694, 65623, 1320553 +28, 691, 66595, 1302226 +30, 632, 67685, 1289583 +22, 650, 64852, 1296906 +28, 627, 63819, 1291399 +32, 643, 64076, 1305598 +28, 650, 67461, 1338880 +25, 734, 66487, 1309223 +30, 677, 65915, 1332695 +28, 673, 65340, 1309269 +30, 614, 67870, 1334193 +24, 668, 65568, 1324434 +32, 629, 65517, 1335292 +28, 679, 66816, 1258780 +30, 690, 68018, 1331661 +30, 665, 64583, 1328110 +26, 703, 65366, 1284483 +28, 664, 67072, 1342368 +22, 655, 64601, 1328639 +32, 676, 67910, 1346684 +26, 648, 64302, 1335128 +28, 630, 65835, 1310994 +26, 654, 66997, 1318680 +26, 720, 65660, 1325616 +24, 597, 67014, 1342725 diff --git a/test/data/scores_hyperidentity.csv b/test/data/scores_hyperidentity.csv new file mode 100644 index 000000000..c4df5ffda --- /dev/null +++ b/test/data/scores_hyperidentity.csv @@ -0,0 +1,1025 @@ +64-64-50-64-178-50-36, 256-256-120-256-612-100-171, 512-512-150-512-1174-150-300, 1024-1024-200-1024-3000-200-600 +38, 145, 277, 553 +42, 144, 281, 556 +39, 140, 279, 544 +40, 138, 285, 541 +39, 148, 276, 542 +41, 149, 282, 539 +37, 147, 282, 546 +40, 147, 278, 549 +39, 145, 285, 541 +45, 142, 274, 538 +38, 141, 280, 541 +44, 157, 275, 544 +42, 141, 282, 540 +42, 143, 279, 543 +37, 143, 290, 548 +41, 147, 279, 550 +38, 145, 279, 541 +41, 145, 287, 555 +37, 149, 275, 543 +41, 147, 278, 544 +40, 139, 279, 558 +43, 143, 279, 548 +41, 142, 281, 542 +40, 140, 278, 544 +39, 139, 270, 544 +43, 147, 281, 553 +41, 142, 277, 563 +37, 143, 287, 541 +41, 143, 276, 548 +37, 145, 280, 546 +39, 145, 283, 550 +36, 142, 294, 538 +40, 144, 281, 549 +42, 144, 274, 545 +39, 146, 276, 547 +36, 144, 272, 555 +36, 140, 273, 544 +41, 142, 279, 548 +36, 146, 276, 548 +42, 143, 274, 554 +38, 139, 278, 546 +37, 155, 279, 549 +40, 144, 275, 555 +40, 147, 277, 547 +37, 144, 276, 554 +39, 142, 273, 538 +38, 141, 295, 545 +41, 144, 276, 545 +37, 143, 279, 545 +41, 143, 272, 550 +42, 140, 294, 547 +40, 145, 286, 580 +40, 142, 280, 542 +38, 136, 284, 546 +40, 144, 276, 550 +42, 140, 287, 546 +44, 143, 282, 534 +38, 147, 284, 553 +38, 141, 276, 549 +39, 142, 283, 553 +39, 144, 280, 545 +40, 149, 279, 560 +41, 144, 278, 544 +36, 144, 276, 538 +38, 142, 275, 537 +42, 142, 279, 543 +38, 137, 278, 538 +41, 144, 286, 545 +40, 146, 283, 549 +40, 143, 281, 552 +41, 142, 289, 534 +43, 139, 279, 552 +40, 144, 280, 548 +40, 139, 274, 546 +39, 143, 271, 540 +41, 144, 281, 541 +40, 143, 274, 546 +40, 141, 272, 557 +40, 144, 280, 538 +37, 146, 282, 549 +41, 145, 286, 547 +39, 140, 280, 550 +39, 149, 289, 559 +43, 147, 278, 546 +43, 143, 283, 544 +40, 142, 285, 558 +42, 141, 278, 540 +43, 142, 281, 559 +42, 147, 279, 549 +37, 143, 289, 546 +39, 138, 283, 549 +40, 147, 280, 561 +39, 144, 278, 543 +37, 142, 276, 546 +43, 142, 275, 545 +37, 145, 274, 539 +40, 140, 279, 541 +38, 150, 284, 546 +39, 145, 290, 540 +41, 141, 281, 538 +37, 139, 283, 547 +42, 140, 279, 540 +40, 149, 280, 552 +42, 143, 269, 542 +40, 143, 280, 541 +38, 139, 295, 542 +39, 140, 276, 543 +39, 150, 283, 542 +39, 138, 274, 537 +37, 140, 272, 546 +40, 143, 280, 543 +42, 139, 289, 549 +36, 140, 285, 538 +41, 141, 281, 555 +40, 140, 287, 554 +39, 140, 274, 544 +39, 141, 274, 547 +37, 144, 282, 558 +37, 149, 282, 545 +37, 140, 279, 574 +40, 140, 282, 561 +38, 142, 280, 551 +39, 139, 279, 538 +46, 144, 283, 538 +40, 146, 282, 555 +41, 147, 280, 541 +40, 146, 286, 540 +38, 142, 283, 544 +35, 149, 275, 550 +38, 143, 272, 548 +41, 146, 280, 555 +40, 152, 279, 551 +37, 143, 294, 544 +40, 147, 278, 560 +44, 137, 273, 550 +41, 143, 274, 543 +38, 150, 283, 545 +40, 142, 275, 554 +38, 141, 281, 544 +38, 141, 276, 549 +37, 138, 277, 568 +40, 149, 279, 560 +39, 145, 283, 545 +37, 139, 277, 548 +42, 147, 280, 539 +43, 159, 278, 545 +38, 143, 278, 548 +39, 142, 273, 551 +38, 146, 276, 544 +42, 143, 279, 543 +37, 140, 271, 554 +38, 146, 274, 543 +38, 146, 278, 542 +38, 143, 282, 545 +39, 146, 277, 551 +39, 139, 278, 543 +39, 143, 286, 553 +40, 143, 279, 544 +41, 147, 281, 548 +43, 141, 281, 544 +39, 143, 284, 544 +38, 141, 284, 548 +40, 141, 281, 550 +43, 144, 281, 547 +38, 144, 286, 551 +41, 144, 280, 545 +42, 144, 270, 553 +39, 141, 280, 545 +37, 147, 290, 555 +39, 145, 279, 542 +44, 142, 279, 546 +38, 143, 278, 553 +42, 140, 287, 548 +38, 148, 280, 546 +39, 148, 272, 538 +37, 140, 274, 553 +41, 143, 278, 547 +39, 145, 274, 542 +40, 139, 282, 539 +38, 143, 275, 542 +40, 141, 279, 550 +42, 144, 282, 549 +38, 153, 286, 541 +37, 140, 278, 545 +38, 143, 279, 555 +36, 142, 282, 547 +41, 140, 275, 547 +38, 142, 274, 545 +37, 148, 274, 550 +39, 146, 279, 548 +38, 145, 274, 549 +39, 143, 282, 539 +40, 144, 280, 551 +40, 148, 280, 565 +39, 146, 281, 549 +40, 142, 275, 569 +40, 144, 279, 565 +40, 147, 275, 545 +39, 148, 276, 542 +44, 139, 281, 547 +39, 143, 286, 543 +39, 142, 284, 550 +42, 151, 276, 535 +37, 139, 285, 545 +42, 141, 284, 543 +40, 141, 279, 546 +38, 141, 285, 536 +37, 143, 278, 542 +40, 150, 284, 556 +45, 141, 285, 541 +39, 144, 282, 548 +37, 142, 280, 554 +41, 141, 284, 545 +39, 138, 280, 544 +38, 147, 286, 543 +39, 141, 279, 544 +39, 145, 281, 547 +41, 145, 280, 547 +37, 140, 290, 545 +36, 139, 280, 540 +38, 142, 276, 544 +41, 144, 279, 545 +41, 142, 282, 548 +42, 141, 279, 547 +42, 144, 278, 548 +42, 144, 276, 535 +40, 137, 274, 549 +39, 142, 279, 543 +43, 143, 279, 538 +39, 140, 280, 563 +38, 143, 273, 552 +39, 146, 285, 555 +39, 139, 278, 542 +38, 140, 283, 541 +37, 143, 275, 537 +38, 147, 268, 549 +40, 146, 274, 542 +40, 141, 277, 552 +45, 146, 276, 542 +44, 144, 285, 542 +40, 153, 276, 540 +39, 146, 281, 542 +37, 141, 276, 561 +42, 148, 283, 550 +39, 144, 280, 547 +41, 139, 273, 537 +40, 145, 272, 540 +39, 146, 276, 538 +36, 144, 283, 548 +35, 154, 282, 555 +42, 140, 276, 547 +40, 137, 276, 537 +42, 141, 281, 548 +40, 149, 279, 551 +40, 143, 280, 547 +40, 144, 276, 536 +39, 147, 281, 549 +39, 142, 274, 545 +42, 149, 280, 547 +41, 143, 281, 542 +40, 141, 275, 552 +37, 140, 275, 542 +39, 137, 278, 543 +42, 140, 280, 538 +40, 143, 278, 537 +41, 141, 278, 551 +38, 146, 277, 554 +37, 140, 286, 545 +41, 144, 272, 544 +40, 141, 276, 546 +40, 146, 279, 544 +37, 147, 282, 549 +41, 141, 281, 544 +38, 149, 278, 546 +38, 145, 275, 542 +41, 148, 272, 549 +39, 144, 282, 543 +39, 141, 290, 551 +42, 144, 284, 546 +39, 140, 295, 544 +38, 140, 279, 543 +40, 149, 278, 551 +38, 144, 279, 542 +40, 145, 282, 553 +38, 140, 286, 549 +37, 147, 286, 541 +39, 140, 280, 546 +39, 144, 277, 540 +42, 141, 280, 557 +41, 144, 279, 541 +39, 142, 289, 551 +39, 138, 276, 541 +39, 145, 276, 553 +37, 144, 281, 557 +38, 144, 285, 544 +40, 146, 280, 546 +39, 141, 276, 547 +39, 141, 278, 551 +42, 145, 285, 546 +40, 142, 285, 548 +36, 148, 275, 541 +41, 142, 282, 543 +40, 143, 285, 552 +40, 143, 274, 554 +40, 139, 284, 552 +39, 138, 280, 541 +41, 150, 287, 545 +43, 145, 276, 536 +38, 141, 285, 552 +42, 144, 276, 552 +38, 146, 292, 548 +41, 151, 279, 554 +39, 141, 281, 549 +38, 148, 278, 546 +40, 143, 277, 539 +40, 138, 281, 541 +40, 145, 288, 545 +36, 143, 283, 545 +40, 146, 277, 540 +41, 144, 282, 550 +39, 145, 283, 543 +42, 145, 278, 543 +38, 142, 273, 552 +42, 140, 281, 543 +39, 150, 283, 560 +37, 137, 277, 545 +39, 142, 276, 544 +38, 148, 285, 556 +39, 148, 273, 537 +44, 147, 273, 551 +38, 143, 288, 538 +38, 144, 288, 542 +41, 140, 276, 554 +40, 141, 275, 550 +40, 141, 274, 542 +39, 139, 279, 546 +41, 151, 276, 541 +40, 142, 281, 548 +42, 140, 289, 558 +41, 148, 279, 552 +42, 142, 280, 564 +40, 145, 278, 550 +38, 147, 282, 571 +39, 145, 279, 558 +37, 142, 288, 551 +40, 144, 277, 551 +37, 148, 283, 545 +37, 138, 276, 551 +38, 147, 288, 550 +38, 144, 280, 550 +41, 139, 281, 544 +40, 145, 277, 538 +38, 149, 276, 556 +43, 140, 268, 548 +40, 142, 275, 556 +40, 139, 282, 548 +38, 141, 280, 562 +38, 145, 274, 547 +40, 145, 279, 547 +39, 142, 276, 553 +39, 138, 280, 558 +43, 146, 279, 546 +42, 141, 276, 541 +39, 147, 281, 545 +42, 143, 274, 545 +38, 143, 281, 542 +39, 143, 282, 544 +44, 139, 276, 551 +41, 151, 285, 540 +39, 144, 281, 543 +41, 147, 283, 554 +40, 147, 276, 562 +39, 143, 278, 552 +40, 145, 285, 559 +38, 143, 285, 554 +41, 138, 281, 557 +40, 138, 281, 554 +40, 143, 282, 539 +39, 142, 282, 552 +41, 139, 283, 537 +40, 143, 278, 543 +40, 142, 276, 541 +37, 139, 282, 550 +38, 147, 275, 550 +39, 147, 284, 545 +39, 143, 279, 539 +43, 143, 271, 547 +38, 144, 273, 543 +35, 142, 282, 574 +39, 140, 274, 540 +42, 146, 275, 555 +36, 144, 295, 539 +45, 142, 271, 541 +41, 151, 279, 541 +38, 140, 280, 560 +36, 140, 282, 549 +40, 147, 279, 543 +39, 147, 290, 553 +39, 145, 285, 548 +40, 143, 276, 560 +37, 143, 276, 548 +42, 144, 274, 544 +38, 142, 295, 540 +40, 147, 281, 549 +41, 143, 280, 556 +40, 146, 283, 553 +38, 139, 284, 551 +39, 145, 282, 534 +40, 145, 277, 540 +38, 145, 280, 556 +39, 145, 282, 551 +38, 144, 282, 551 +40, 143, 276, 544 +36, 143, 288, 542 +38, 139, 281, 559 +38, 142, 279, 543 +40, 144, 299, 542 +43, 139, 279, 542 +38, 142, 280, 538 +39, 142, 276, 547 +35, 149, 283, 554 +41, 139, 278, 543 +38, 139, 277, 540 +39, 145, 279, 554 +38, 145, 283, 546 +39, 143, 275, 544 +40, 139, 278, 540 +38, 150, 282, 551 +38, 151, 280, 545 +41, 143, 279, 551 +38, 152, 274, 545 +41, 146, 278, 538 +36, 146, 282, 554 +37, 144, 282, 543 +36, 145, 282, 542 +40, 146, 281, 552 +39, 140, 279, 541 +40, 145, 275, 546 +39, 147, 276, 547 +40, 145, 281, 548 +39, 139, 286, 548 +42, 145, 278, 547 +38, 144, 274, 540 +38, 142, 277, 548 +39, 156, 274, 542 +42, 140, 277, 536 +40, 139, 282, 544 +41, 144, 274, 542 +37, 142, 282, 551 +41, 145, 290, 553 +39, 146, 281, 541 +41, 146, 280, 553 +41, 143, 284, 542 +37, 146, 277, 566 +38, 139, 280, 545 +39, 142, 281, 548 +39, 139, 275, 544 +39, 144, 272, 549 +38, 143, 285, 541 +42, 144, 281, 546 +38, 157, 276, 552 +40, 141, 277, 545 +38, 141, 275, 548 +39, 142, 278, 539 +39, 146, 277, 542 +39, 147, 283, 551 +38, 151, 276, 549 +39, 146, 273, 544 +38, 139, 280, 549 +42, 149, 278, 550 +39, 144, 282, 543 +38, 143, 285, 549 +42, 145, 280, 563 +39, 146, 279, 548 +44, 145, 288, 539 +39, 139, 291, 546 +39, 145, 282, 550 +36, 145, 279, 553 +40, 140, 276, 546 +41, 143, 289, 550 +39, 145, 283, 553 +38, 142, 280, 546 +41, 145, 276, 542 +41, 145, 276, 550 +40, 141, 278, 543 +41, 141, 284, 554 +45, 153, 276, 539 +37, 145, 292, 539 +40, 143, 286, 551 +37, 140, 276, 545 +39, 141, 280, 561 +41, 145, 284, 571 +43, 146, 278, 559 +38, 141, 276, 561 +40, 145, 280, 547 +42, 150, 283, 541 +39, 142, 282, 551 +42, 143, 283, 551 +41, 142, 283, 551 +40, 144, 275, 541 +39, 147, 276, 544 +40, 154, 280, 555 +39, 142, 283, 560 +37, 144, 275, 532 +40, 147, 293, 550 +42, 138, 281, 541 +39, 141, 280, 538 +40, 138, 282, 543 +40, 146, 286, 550 +43, 147, 278, 544 +39, 147, 281, 546 +39, 150, 277, 561 +38, 143, 282, 540 +37, 143, 277, 544 +43, 149, 275, 565 +40, 143, 275, 551 +42, 139, 278, 550 +43, 149, 282, 567 +36, 145, 286, 552 +38, 144, 283, 553 +40, 144, 274, 552 +40, 145, 281, 538 +38, 145, 278, 546 +40, 143, 285, 551 +41, 139, 279, 559 +39, 146, 277, 546 +38, 140, 278, 545 +43, 142, 281, 545 +40, 139, 284, 552 +36, 138, 274, 554 +39, 144, 292, 550 +39, 143, 276, 555 +38, 143, 280, 549 +40, 142, 283, 539 +37, 146, 280, 540 +41, 146, 280, 553 +40, 152, 290, 552 +40, 145, 275, 546 +40, 145, 279, 543 +39, 144, 289, 545 +43, 149, 271, 547 +38, 141, 285, 537 +39, 139, 282, 552 +43, 145, 280, 546 +38, 145, 286, 539 +41, 148, 279, 553 +40, 137, 281, 548 +38, 139, 285, 538 +38, 144, 286, 541 +40, 143, 278, 540 +38, 138, 294, 544 +39, 145, 281, 541 +38, 143, 272, 546 +38, 146, 288, 560 +39, 146, 293, 542 +39, 146, 286, 542 +43, 148, 279, 552 +39, 141, 277, 540 +38, 141, 286, 548 +38, 147, 277, 548 +42, 138, 280, 550 +43, 148, 277, 543 +41, 148, 281, 550 +38, 152, 283, 550 +39, 139, 285, 553 +37, 141, 275, 556 +37, 141, 301, 542 +38, 143, 279, 540 +42, 142, 274, 553 +38, 146, 283, 545 +40, 142, 276, 540 +37, 144, 278, 546 +36, 144, 286, 547 +39, 145, 278, 559 +41, 144, 282, 559 +39, 144, 280, 541 +43, 147, 275, 556 +37, 139, 282, 542 +38, 147, 291, 545 +41, 140, 283, 540 +39, 142, 280, 547 +37, 142, 276, 545 +41, 143, 278, 546 +40, 145, 274, 543 +37, 146, 282, 547 +41, 146, 273, 546 +38, 145, 277, 542 +39, 142, 274, 548 +39, 143, 279, 536 +38, 144, 281, 543 +42, 149, 278, 549 +40, 143, 284, 540 +45, 136, 278, 552 +40, 147, 278, 540 +40, 147, 277, 549 +40, 145, 279, 554 +41, 145, 277, 548 +36, 144, 277, 544 +37, 141, 278, 544 +39, 144, 285, 544 +38, 140, 277, 542 +36, 148, 278, 549 +41, 137, 275, 551 +43, 144, 275, 541 +40, 143, 279, 542 +44, 144, 278, 547 +42, 146, 285, 548 +40, 143, 275, 549 +39, 149, 278, 548 +40, 149, 280, 546 +47, 142, 282, 556 +41, 140, 285, 546 +37, 139, 278, 547 +38, 143, 276, 549 +39, 144, 279, 551 +40, 139, 280, 539 +41, 138, 282, 540 +42, 142, 276, 549 +40, 144, 278, 555 +50, 142, 276, 540 +41, 140, 280, 546 +40, 142, 282, 569 +39, 144, 276, 544 +39, 148, 281, 548 +40, 146, 283, 551 +38, 152, 284, 543 +37, 151, 276, 541 +43, 149, 279, 549 +38, 140, 280, 552 +41, 147, 280, 551 +38, 141, 285, 543 +44, 141, 277, 553 +41, 143, 277, 540 +37, 142, 272, 547 +43, 138, 283, 545 +40, 146, 280, 542 +42, 142, 276, 550 +40, 143, 277, 551 +41, 143, 273, 553 +39, 144, 274, 535 +38, 144, 286, 537 +39, 139, 276, 555 +41, 140, 280, 542 +41, 143, 282, 547 +39, 140, 273, 555 +37, 148, 283, 552 +42, 160, 276, 543 +41, 147, 276, 552 +37, 140, 287, 555 +37, 140, 275, 544 +40, 143, 275, 542 +39, 149, 284, 558 +42, 144, 280, 550 +39, 139, 281, 554 +37, 146, 278, 545 +42, 144, 276, 541 +41, 137, 282, 546 +37, 143, 279, 539 +38, 139, 279, 542 +41, 140, 275, 544 +38, 146, 278, 545 +43, 139, 281, 538 +38, 142, 287, 545 +45, 138, 274, 548 +36, 146, 283, 559 +41, 138, 277, 539 +39, 146, 275, 548 +43, 143, 277, 562 +37, 143, 276, 544 +42, 151, 276, 543 +40, 143, 276, 540 +40, 144, 280, 543 +36, 141, 287, 547 +42, 144, 280, 536 +39, 145, 279, 546 +37, 150, 286, 551 +37, 141, 279, 551 +40, 147, 287, 550 +39, 144, 273, 542 +42, 154, 286, 538 +43, 143, 278, 541 +39, 146, 281, 543 +38, 140, 287, 540 +40, 144, 280, 555 +41, 142, 279, 544 +37, 151, 275, 553 +39, 141, 274, 550 +39, 139, 277, 552 +39, 143, 274, 547 +41, 140, 284, 557 +41, 143, 279, 545 +37, 143, 286, 550 +38, 140, 278, 550 +39, 151, 277, 547 +39, 148, 274, 544 +42, 144, 281, 547 +37, 146, 288, 543 +38, 143, 279, 555 +40, 153, 279, 552 +37, 139, 283, 549 +37, 148, 278, 551 +39, 144, 285, 543 +41, 148, 275, 536 +38, 143, 270, 559 +39, 142, 278, 543 +38, 143, 285, 534 +38, 146, 274, 545 +37, 145, 277, 562 +40, 149, 277, 555 +39, 144, 278, 545 +39, 139, 278, 537 +41, 139, 278, 554 +40, 142, 280, 557 +41, 148, 288, 547 +42, 140, 279, 544 +38, 147, 276, 538 +41, 143, 276, 539 +40, 141, 278, 549 +39, 145, 275, 544 +39, 139, 280, 541 +40, 141, 271, 541 +37, 141, 272, 545 +42, 140, 278, 551 +40, 141, 284, 539 +38, 148, 277, 549 +38, 148, 273, 543 +38, 142, 273, 539 +40, 143, 279, 534 +41, 144, 277, 541 +43, 143, 283, 546 +38, 148, 273, 542 +40, 147, 273, 549 +37, 147, 284, 538 +42, 144, 282, 545 +41, 142, 284, 552 +38, 147, 284, 543 +44, 141, 285, 554 +40, 139, 281, 538 +42, 144, 278, 543 +41, 140, 273, 548 +40, 144, 284, 539 +39, 140, 278, 540 +43, 143, 279, 554 +37, 144, 293, 541 +40, 149, 290, 547 +38, 151, 276, 551 +39, 151, 285, 549 +38, 146, 272, 540 +40, 141, 281, 557 +40, 145, 286, 547 +37, 142, 286, 551 +40, 143, 283, 551 +39, 143, 277, 543 +39, 142, 288, 542 +40, 146, 282, 546 +37, 145, 280, 557 +38, 144, 279, 549 +41, 144, 282, 541 +39, 145, 280, 546 +41, 146, 281, 545 +39, 146, 284, 541 +43, 144, 277, 551 +39, 150, 285, 567 +42, 145, 289, 541 +39, 147, 279, 538 +42, 144, 282, 556 +41, 144, 277, 539 +41, 143, 278, 539 +41, 141, 277, 541 +40, 151, 278, 547 +37, 140, 280, 555 +38, 144, 278, 550 +42, 142, 300, 545 +43, 143, 275, 547 +37, 144, 278, 548 +44, 144, 295, 541 +40, 146, 286, 558 +40, 144, 273, 547 +41, 146, 282, 568 +40, 144, 273, 545 +37, 141, 277, 552 +38, 140, 277, 548 +37, 141, 279, 538 +38, 152, 273, 540 +37, 146, 277, 545 +39, 142, 279, 548 +39, 142, 270, 540 +38, 145, 273, 546 +40, 147, 280, 541 +38, 140, 283, 539 +38, 140, 288, 553 +40, 150, 274, 543 +41, 144, 287, 541 +38, 139, 283, 545 +40, 140, 282, 554 +38, 143, 281, 540 +41, 146, 283, 554 +41, 148, 277, 548 +41, 142, 277, 542 +38, 150, 278, 556 +38, 144, 283, 545 +39, 142, 291, 552 +37, 143, 284, 542 +40, 147, 270, 569 +39, 139, 289, 547 +41, 150, 278, 561 +41, 140, 272, 556 +40, 140, 274, 546 +40, 144, 275, 550 +40, 140, 275, 538 +41, 148, 283, 539 +42, 139, 273, 538 +38, 145, 276, 551 +38, 146, 270, 564 +40, 138, 282, 548 +44, 143, 289, 535 +40, 146, 281, 552 +41, 141, 276, 550 +36, 147, 282, 556 +38, 146, 278, 545 +41, 147, 293, 544 +37, 138, 273, 547 +41, 143, 279, 557 +37, 142, 273, 547 +38, 142, 288, 550 +41, 141, 279, 541 +47, 146, 277, 547 +36, 145, 276, 539 +39, 140, 284, 539 +38, 149, 280, 546 +45, 141, 271, 545 +43, 144, 276, 558 +42, 140, 277, 548 +38, 150, 281, 545 +40, 140, 276, 550 +39, 146, 287, 557 +39, 143, 282, 543 +40, 147, 281, 547 +39, 141, 294, 540 +43, 146, 278, 546 +39, 139, 275, 541 +42, 148, 289, 547 +40, 142, 271, 543 +36, 143, 279, 548 +38, 147, 275, 548 +38, 139, 280, 544 +43, 140, 274, 560 +43, 154, 280, 541 +37, 141, 281, 546 +42, 143, 286, 546 +38, 142, 278, 545 +41, 148, 275, 550 +41, 141, 279, 550 +40, 142, 278, 545 +41, 143, 277, 546 +41, 147, 278, 539 +38, 142, 278, 551 +40, 137, 274, 547 +40, 141, 280, 537 +38, 141, 285, 555 +37, 143, 294, 547 +43, 143, 280, 551 +35, 143, 280, 547 +37, 137, 272, 544 +40, 139, 277, 561 +43, 146, 279, 549 +41, 149, 275, 552 +35, 142, 273, 547 +38, 143, 281, 552 +40, 139, 280, 562 +37, 150, 282, 543 +41, 146, 280, 547 +38, 144, 280, 537 +40, 140, 272, 557 +40, 145, 288, 540 +38, 145, 281, 550 +40, 144, 277, 546 +39, 147, 279, 548 +36, 141, 289, 558 +41, 141, 284, 551 +39, 140, 277, 547 +42, 145, 273, 552 +40, 142, 274, 545 +37, 144, 279, 538 +40, 140, 281, 552 +39, 148, 277, 541 +40, 152, 287, 548 +43, 155, 277, 543 +37, 153, 284, 548 +43, 143, 288, 545 +41, 140, 282, 546 +38, 145, 280, 536 +37, 145, 275, 548 +38, 143, 279, 547 +39, 138, 281, 547 +37, 140, 275, 554 +36, 148, 279, 539 +39, 139, 279, 541 +41, 142, 284, 535 +38, 145, 279, 540 +38, 145, 278, 554 +39, 148, 277, 540 +40, 139, 273, 540 +39, 139, 283, 549 +39, 139, 284, 546 +40, 143, 277, 557 +39, 138, 281, 543 +36, 139, 277, 552 +40, 142, 277, 551 +41, 144, 280, 541 +39, 147, 279, 564 +43, 144, 282, 564 +40, 142, 287, 539 +38, 143, 290, 542 +41, 144, 274, 541 +41, 145, 283, 547 +41, 143, 273, 560 +39, 140, 280, 540 +37, 143, 276, 540 +37, 148, 282, 539 +39, 150, 279, 558 +40, 141, 280, 548 +41, 142, 274, 539 +41, 145, 276, 544 +43, 149, 271, 542 +38, 143, 276, 552 +37, 148, 276, 541 +38, 147, 287, 534 +40, 145, 276, 560 +41, 146, 275, 551 +36, 143, 275, 536 +41, 144, 281, 550 +39, 142, 285, 548 +38, 148, 279, 542 +40, 140, 278, 556 +38, 146, 283, 549 +40, 142, 281, 540 +40, 144, 275, 544 +38, 141, 282, 548 +40, 142, 283, 546 +38, 140, 286, 548 +40, 142, 279, 545 +35, 142, 271, 535 +40, 142, 273, 549 +39, 145, 277, 549 +39, 141, 278, 543 +37, 143, 279, 537 +39, 143, 277, 548 +40, 143, 278, 549 +38, 144, 276, 542 +41, 142, 283, 544 +38, 145, 277, 550 +41, 142, 280, 554 +40, 141, 281, 537 +39, 145, 284, 541 +41, 142, 276, 539 +37, 145, 275, 546 +39, 145, 285, 544 +41, 143, 276, 545 +39, 142, 275, 548 +43, 140, 277, 544 +42, 142, 284, 538 +38, 144, 284, 551 +40, 143, 281, 553 +37, 144, 277, 545 +40, 139, 277, 538 +42, 142, 276, 558 +38, 141, 282, 540 +37, 144, 282, 540 +42, 143, 278, 540 +40, 143, 278, 552 +36, 147, 282, 550 +38, 141, 281, 559 +39, 142, 280, 537 +37, 148, 284, 553 +40, 141, 281, 550 +40, 144, 274, 547 +39, 140, 279, 546 +38, 142, 277, 542 +41, 148, 279, 544 +39, 140, 277, 544 +40, 145, 282, 550 +37, 142, 280, 537 +42, 144, 273, 545 +40, 144, 272, 544 +40, 142, 280, 554 +40, 139, 284, 558 +36, 146, 281, 541 +40, 148, 295, 539 +41, 139, 275, 548 +45, 145, 289, 551 +41, 142, 279, 544 +40, 141, 274, 541 +42, 144, 277, 545 +44, 143, 275, 540 +38, 150, 275, 547 +41, 150, 282, 534 +40, 140, 281, 545 +37, 143, 277, 550 +40, 143, 277, 547 +39, 146, 280, 547 +38, 147, 275, 536 +37, 148, 284, 539 +41, 142, 281, 535 +37, 142, 279, 562 +40, 142, 275, 547 +43, 146, 283, 552 +45, 138, 285, 553 +42, 141, 282, 541 +39, 146, 272, 537 +43, 139, 274, 541 +38, 142, 278, 549 +42, 142, 278, 552 +38, 147, 285, 539 +39, 140, 273, 540 +40, 142, 275, 546 +38, 149, 289, 548 +43, 139, 283, 541 +41, 145, 287, 546 +39, 143, 277, 547 +42, 142, 273, 544 +41, 144, 285, 560 +38, 142, 278, 550 +37, 145, 273, 560 diff --git a/test/data/scores_v4.csv b/test/data/scores_v4.csv deleted file mode 100644 index 827e0dd14..000000000 --- a/test/data/scores_v4.csv +++ /dev/null @@ -1,257 +0,0 @@ -512-256-100000-10, 856-520-120000-20, 1000-1000-1000000-30, 2844-2160-9000000-89,3000-3000-9000000-30, 3000-3000-9000000-100 -75,96,91,103,90,90 -94,94,90,102,95,98 -84,88,86,97,96,96 -84,94,87,100,84,91 -87,96,102,100,81,92 -79,87,91,99,99,99 -85,95,88,105,102,102 -91,98,89,89,92,97 -89,80,94,99,92,94 -90,89,101,89,82,85 -83,93,88,94,87,89 -92,86,93,103,95,100 -109,101,77,101,96,96 -98,83,96,105,106,106 -99,99,87,93,105,105 -85,83,95,92,102,105 -86,88,83,95,93,95 -90,98,84,102,92,93 -78,101,83,100,91,99 -92,94,99,95,91,102 -82,95,97,94,98,101 -86,98,94,96,102,102 -90,90,84,93,99,99 -91,91,98,94,96,96 -92,93,101,96,88,98 -82,82,92,98,92,97 -91,93,99,101,94,102 -92,92,88,99,99,101 -81,83,94,103,94,94 -77,88,88,96,93,108 -83,92,91,99,86,88 -81,87,94,102,89,89 -97,94,103,99,91,102 -90,84,97,103,96,96 -72,95,85,99,94,98 -86,95,93,87,93,100 -97,104,87,103,88,93 -96,93,94,93,101,101 -86,89,92,96,85,89 -88,86,84,106,86,89 -87,98,86,82,88,91 -76,82,91,96,89,92 -83,94,78,99,103,103 -85,80,96,93,86,88 -78,83,89,93,103,103 -95,98,88,97,91,98 -85,92,99,104,98,98 -93,95,84,92,85,95 -89,93,75,95,94,94 -96,99,88,98,95,96 -85,98,79,95,97,106 -91,91,81,95,97,97 -90,96,95,98,89,91 -90,97,93,97,105,105 -88,110,92,100,87,90 -106,97,92,92,85,93 -67,88,96,89,102,102 -87,84,96,90,93,96 -90,88,91,100,92,99 -80,84,83,101,82,96 -79,89,95,99,101,101 -91,97,103,81,104,112 -91,97,106,91,82,90 -78,91,101,96,97,102 -81,91,96,94,95,97 -98,93,93,104,94,96 -86,94,89,107,88,97 -96,94,95,90,90,106 -93,90,94,96,91,92 -89,93,87,103,90,94 -83,87,90,95,86,96 -94,95,88,98,97,99 -88,94,95,108,104,106 -83,94,87,86,103,103 -92,88,87,100,90,99 -98,86,87,91,90,90 -90,92,94,90,95,95 -87,99,85,99,95,107 -92,90,102,95,98,98 -87,79,94,93,110,110 -99,88,84,93,92,99 -95,79,90,92,91,96 -93,95,89,94,84,84 -92,87,79,99,93,101 -89,85,80,88,88,90 -100,89,94,95,89,96 -96,88,99,105,87,95 -93,85,98,97,95,95 -84,87,104,93,87,89 -89,105,91,115,92,99 -98,94,98,101,88,88 -94,90,85,100,94,98 -94,82,97,93,104,104 -96,83,82,98,99,99 -91,82,89,97,98,109 -94,104,92,96,93,99 -94,86,92,88,94,98 -85,93,100,87,106,106 -85,90,84,95,89,105 -75,87,93,100,98,102 -88,95,92,91,99,99 -84,86,107,102,97,97 -102,76,95,92,93,100 -88,102,80,98,88,98 -80,100,91,95,89,94 -85,92,84,98,96,97 -91,82,91,91,101,101 -79,100,102,98,110,110 -92,91,95,98,89,97 -87,98,85,97,102,102 -83,86,77,92,100,100 -85,79,93,95,99,99 -94,96,98,93,86,97 -96,92,91,95,89,89 -83,114,96,101,87,93 -74,89,97,89,98,100 -98,92,84,103,93,93 -96,99,83,97,93,93 -101,93,90,91,101,108 -88,92,81,105,92,92 -80,93,92,99,102,102 -88,83,94,95,93,99 -88,98,88,95,81,86 -106,80,98,104,98,101 -86,91,91,99,96,99 -88,98,95,94,85,90 -88,81,98,105,85,107 -96,91,91,101,100,105 -94,92,92,105,99,105 -88,93,89,103,89,93 -77,86,85,95,100,100 -76,92,89,91,101,101 -91,75,106,101,97,103 -81,85,93,95,92,95 -85,93,96,94,87,94 -88,90,92,95,91,98 -80,108,93,93,82,101 -89,85,88,93,99,108 -88,90,94,92,93,94 -95,93,101,105,90,96 -86,97,79,100,90,93 -86,89,84,102,101,101 -91,92,91,89,100,100 -92,84,100,105,90,92 -88,94,103,102,93,99 -82,92,88,92,94,94 -81,93,97,100,106,106 -91,95,94,89,92,98 -88,87,103,90,98,98 -77,88,88,90,82,87 -95,99,91,96,84,87 -89,93,99,96,109,109 -100,95,88,97,91,94 -90,83,102,91,87,102 -86,93,90,101,83,94 -86,82,82,97,97,97 -106,92,91,89,99,110 -85,88,80,99,92,93 -84,96,78,96,95,96 -93,84,104,93,100,104 -87,87,94,111,94,99 -91,88,91,104,102,102 -92,97,93,100,90,102 -95,92,86,89,92,94 -96,91,102,99,93,93 -81,96,79,102,90,92 -83,91,97,98,93,102 -76,101,83,102,98,98 -84,81,91,96,91,93 -88,87,79,88,95,105 -78,92,95,98,98,101 -83,89,82,98,88,90 -93,101,92,100,104,104 -87,80,85,96,89,89 -108,90,107,102,96,110 -96,81,91,95,101,101 -92,77,92,101,105,105 -90,86,86,102,93,93 -91,93,98,93,85,98 -86,90,101,93,101,109 -95,100,88,96,96,96 -86,101,80,94,92,93 -91,85,89,92,98,98 -100,81,88,93,87,95 -96,101,93,94,89,97 -89,91,89,95,100,100 -99,82,86,91,87,90 -85,89,84,98,99,99 -100,96,96,98,87,93 -109,94,87,109,87,94 -84,83,97,94,86,93 -95,99,95,89,96,98 -83,79,96,103,95,95 -92,94,98,86,101,101 -86,100,95,89,89,93 -92,98,79,94,87,102 -83,100,93,97,91,102 -81,92,96,97,87,87 -96,98,85,98,82,85 -97,93,91,92,101,101 -92,93,97,97,89,89 -92,84,98,90,90,92 -92,83,94,106,105,107 -83,97,89,99,94,94 -101,95,95,88,108,108 -84,99,102,93,100,100 -92,91,95,98,89,98 -89,82,90,100,94,104 -90,91,103,103,91,98 -91,86,98,103,84,84 -98,84,84,89,96,96 -89,89,82,96,89,95 -83,84,85,101,98,102 -92,105,96,87,93,93 -96,85,93,95,97,97 -76,90,96,86,91,93 -82,81,101,93,94,107 -86,82,94,102,104,104 -86,91,96,102,91,96 -93,97,91,89,87,92 -81,80,91,99,91,91 -83,96,97,93,94,96 -83,87,94,89,104,104 -96,81,90,99,92,92 -89,98,103,89,94,99 -87,86,89,98,92,109 -86,91,86,108,94,98 -85,106,84,91,110,110 -87,95,96,103,94,94 -78,92,93,93,92,92 -74,96,101,94,92,98 -79,96,84,97,85,90 -86,92,95,96,85,90 -95,83,87,105,92,100 -97,85,87,90,101,101 -87,94,101,86,83,97 -94,98,96,103,102,102 -95,92,102,100,93,97 -95,82,88,95,98,100 -91,95,96,78,97,97 -82,97,91,96,88,96 -98,99,86,96,92,96 -103,91,96,93,94,95 -95,93,89,97,96,107 -70,92,91,102,97,99 -94,88,92,98,104,104 -79,87,90,102,93,93 -95,96,94,91,92,103 -93,95,102,94,93,96 -90,94,93,97,102,111 -90,87,90,93,89,94 -88,105,97,88,91,105 -93,88,101,103,100,100 -76,81,102,94,95,95 -91,95,84,97,98,99 -83,103,80,97,87,91 diff --git a/test/data/scores_v5.csv b/test/data/scores_v5.csv deleted file mode 100644 index e319a31c5..000000000 --- a/test/data/scores_v5.csv +++ /dev/null @@ -1,257 +0,0 @@ -64-64-50-64-178-50-36, 256-256-120-256-612-100-171, 512-512-150-512-1174-150-300, 1024-1024-200-1024-3000-200-600 -38, 145, 277, 553 -42, 144, 281, 556 -39, 140, 279, 544 -40, 138, 285, 541 -39, 148, 276, 542 -41, 149, 282, 539 -37, 147, 282, 546 -40, 147, 278, 549 -39, 145, 285, 541 -45, 142, 274, 538 -38, 141, 280, 541 -44, 157, 275, 544 -42, 141, 282, 540 -42, 143, 279, 543 -37, 143, 290, 548 -41, 147, 279, 550 -38, 145, 279, 541 -41, 145, 287, 555 -37, 149, 275, 543 -41, 147, 278, 544 -40, 139, 279, 558 -43, 143, 279, 548 -41, 142, 281, 542 -40, 140, 278, 544 -39, 139, 270, 544 -43, 147, 281, 553 -41, 142, 277, 563 -37, 143, 287, 541 -41, 143, 276, 548 -37, 145, 280, 546 -39, 145, 283, 550 -36, 142, 294, 538 -40, 144, 281, 549 -42, 144, 274, 545 -39, 146, 276, 547 -36, 144, 272, 555 -36, 140, 273, 544 -41, 142, 279, 548 -36, 146, 276, 548 -42, 143, 274, 554 -38, 139, 278, 546 -37, 155, 279, 549 -40, 144, 275, 555 -40, 147, 277, 547 -37, 144, 276, 554 -39, 142, 273, 538 -38, 141, 295, 545 -41, 144, 276, 545 -37, 143, 279, 545 -41, 143, 272, 550 -42, 140, 294, 547 -40, 145, 286, 580 -40, 142, 280, 542 -38, 136, 284, 546 -40, 144, 276, 550 -42, 140, 287, 546 -44, 143, 282, 534 -38, 147, 284, 553 -38, 141, 276, 549 -39, 142, 283, 553 -39, 144, 280, 545 -40, 149, 279, 560 -41, 144, 278, 544 -36, 144, 276, 538 -38, 142, 275, 537 -42, 142, 279, 543 -38, 137, 278, 538 -41, 144, 286, 545 -40, 146, 283, 549 -40, 143, 281, 552 -41, 142, 289, 534 -43, 139, 279, 552 -40, 144, 280, 548 -40, 139, 274, 546 -39, 143, 271, 540 -41, 144, 281, 541 -40, 143, 274, 546 -40, 141, 272, 557 -40, 144, 280, 538 -37, 146, 282, 549 -41, 145, 286, 547 -39, 140, 280, 550 -39, 149, 289, 559 -43, 147, 278, 546 -43, 143, 283, 544 -40, 142, 285, 558 -42, 141, 278, 540 -43, 142, 281, 559 -42, 147, 279, 549 -37, 143, 289, 546 -39, 138, 283, 549 -40, 147, 280, 561 -39, 144, 278, 543 -37, 142, 276, 546 -43, 142, 275, 545 -37, 145, 274, 539 -40, 140, 279, 541 -38, 150, 284, 546 -39, 145, 290, 540 -41, 141, 281, 538 -37, 139, 283, 547 -42, 140, 279, 540 -40, 149, 280, 552 -42, 143, 269, 542 -40, 143, 280, 541 -38, 139, 295, 542 -39, 140, 276, 543 -39, 150, 283, 542 -39, 138, 274, 537 -37, 140, 272, 546 -40, 143, 280, 543 -42, 139, 289, 549 -36, 140, 285, 538 -41, 141, 281, 555 -40, 140, 287, 554 -39, 140, 274, 544 -39, 141, 274, 547 -37, 144, 282, 558 -37, 149, 282, 545 -37, 140, 279, 574 -40, 140, 282, 561 -38, 142, 280, 551 -39, 139, 279, 538 -46, 144, 283, 538 -40, 146, 282, 555 -41, 147, 280, 541 -40, 146, 286, 540 -38, 142, 283, 544 -35, 149, 275, 550 -38, 143, 272, 548 -41, 146, 280, 555 -40, 152, 279, 551 -37, 143, 294, 544 -40, 147, 278, 560 -44, 137, 273, 550 -41, 143, 274, 543 -38, 150, 283, 545 -40, 142, 275, 554 -38, 141, 281, 544 -38, 141, 276, 549 -37, 138, 277, 568 -40, 149, 279, 560 -39, 145, 283, 545 -37, 139, 277, 548 -42, 147, 280, 539 -43, 159, 278, 545 -38, 143, 278, 548 -39, 142, 273, 551 -38, 146, 276, 544 -42, 143, 279, 543 -37, 140, 271, 554 -38, 146, 274, 543 -38, 146, 278, 542 -38, 143, 282, 545 -39, 146, 277, 551 -39, 139, 278, 543 -39, 143, 286, 553 -40, 143, 279, 544 -41, 147, 281, 548 -43, 141, 281, 544 -39, 143, 284, 544 -38, 141, 284, 548 -40, 141, 281, 550 -43, 144, 281, 547 -38, 144, 286, 551 -41, 144, 280, 545 -42, 144, 270, 553 -39, 141, 280, 545 -37, 147, 290, 555 -39, 145, 279, 542 -44, 142, 279, 546 -38, 143, 278, 553 -42, 140, 287, 548 -38, 148, 280, 546 -39, 148, 272, 538 -37, 140, 274, 553 -41, 143, 278, 547 -39, 145, 274, 542 -40, 139, 282, 539 -38, 143, 275, 542 -40, 141, 279, 550 -42, 144, 282, 549 -38, 153, 286, 541 -37, 140, 278, 545 -38, 143, 279, 555 -36, 142, 282, 547 -41, 140, 275, 547 -38, 142, 274, 545 -37, 148, 274, 550 -39, 146, 279, 548 -38, 145, 274, 549 -39, 143, 282, 539 -40, 144, 280, 551 -40, 148, 280, 565 -39, 146, 281, 549 -40, 142, 275, 569 -40, 144, 279, 565 -40, 147, 275, 545 -39, 148, 276, 542 -44, 139, 281, 547 -39, 143, 286, 543 -39, 142, 284, 550 -42, 151, 276, 535 -37, 139, 285, 545 -42, 141, 284, 543 -40, 141, 279, 546 -38, 141, 285, 536 -37, 143, 278, 542 -40, 150, 284, 556 -45, 141, 285, 541 -39, 144, 282, 548 -37, 142, 280, 554 -41, 141, 284, 545 -39, 138, 280, 544 -38, 147, 286, 543 -39, 141, 279, 544 -39, 145, 281, 547 -41, 145, 280, 547 -37, 140, 290, 545 -36, 139, 280, 540 -38, 142, 276, 544 -41, 144, 279, 545 -41, 142, 282, 548 -42, 141, 279, 547 -42, 144, 278, 548 -42, 144, 276, 535 -40, 137, 274, 549 -39, 142, 279, 543 -43, 143, 279, 538 -39, 140, 280, 563 -38, 143, 273, 552 -39, 146, 285, 555 -39, 139, 278, 542 -38, 140, 283, 541 -37, 143, 275, 537 -38, 147, 268, 549 -40, 146, 274, 542 -40, 141, 277, 552 -45, 146, 276, 542 -44, 144, 285, 542 -40, 153, 276, 540 -39, 146, 281, 542 -37, 141, 276, 561 -42, 148, 283, 550 -39, 144, 280, 547 -41, 139, 273, 537 -40, 145, 272, 540 -39, 146, 276, 538 -36, 144, 283, 548 -35, 154, 282, 555 -42, 140, 276, 547 -40, 137, 276, 537 -42, 141, 281, 548 -40, 149, 279, 551 -40, 143, 280, 547 -40, 144, 276, 536 diff --git a/test/score.cpp b/test/score.cpp index cc7c19f53..58d1dba26 100644 --- a/test/score.cpp +++ b/test/score.cpp @@ -5,6 +5,8 @@ #define ENABLE_PROFILING 0 // current optimized implementation +#include "../src/public_settings.h" +#include "../src/mining/score_engine.h" #include "../src/score.h" // reference implementation @@ -33,13 +35,22 @@ using namespace test_utils; static const std::string COMMON_TEST_SAMPLES_FILE_NAME = "data/samples_20240815.csv"; -static const std::string COMMON_TEST_SCORES_FILE_NAME = "data/scores_v5.csv"; +static const std::string COMMON_TEST_SCORES_HYPERIDENTITY_FILE_NAME = "data/scores_hyperidentity.csv"; +static const std::string COMMON_TEST_SCORES_ADDITION_FILE_NAME = "data/scores_addition.csv"; + static constexpr bool PRINT_DETAILED_INFO = false; +// Variable control the algo tested +// AllAlgo: run the score that alg is retermined by nonce +static std::vector> TEST_ALGOS = { + {score_engine::AlgoType::HyperIdentity, "HyperIdentity"}, + {score_engine::AlgoType::Addition, "Addition"}, + {score_engine::AlgoType::MaxAlgoCount, "Mixed"}, +}; // set to 0 for run all available samples // For profiling enable, run all available samples -static constexpr unsigned long long COMMON_TEST_NUMBER_OF_SAMPLES = 32; -static constexpr unsigned long long PROFILING_NUMBER_OF_SAMPLES = 32; +static constexpr unsigned long long COMMON_TEST_NUMBER_OF_SAMPLES = 16; +static constexpr unsigned long long PROFILING_NUMBER_OF_SAMPLES = 48; // set 0 for run maximum number of threads of the computer. @@ -52,108 +63,186 @@ static bool gCompareReference = false; std::vector filteredSamples;// = { 0 }; std::vector filteredSettings;// = { 0 }; -std::vector> gScoresGroundTruth; +std::vector> gScoresHyperIdentityGroundTruth; +std::vector> gScoresHyperAdditionGroundTruth; std::map gScoreProcessingTime; -std::map gScoreIndexMap; +std::map gScoreHyperIdentityIndexMap; +std::map gScoreAdditionIndexMap; + +struct ScoreResult +{ + unsigned int score; + long long elapsedMs; +}; + +template class ScoreType, typename CurrentConfig> +ScoreResult computeScore( + const unsigned char* miningSeed, + const unsigned char* publicKey, + const unsigned char* nonce, + score_engine::AlgoType algo, + unsigned char* externalRandomPool) +{ + std::unique_ptr> scoreEngine = + std::make_unique>(); + + scoreEngine->initMemory(); + if (nullptr == externalRandomPool) + { + scoreEngine->initMiningData(miningSeed); + } + + unsigned int scoreValue = 0; + + auto t0 = std::chrono::high_resolution_clock::now(); + + switch (algo) + { + case score_engine::AlgoType::HyperIdentity: + scoreValue = scoreEngine->computeHyperIdentityScore(publicKey, nonce, externalRandomPool); + break; + case score_engine::AlgoType::Addition: + scoreValue = scoreEngine->computeAdditionScore(publicKey, nonce, externalRandomPool); + break; + default: + scoreValue = scoreEngine->computeScore(publicKey, nonce, externalRandomPool); + break; + } + + auto t1 = std::chrono::high_resolution_clock::now(); + auto elapsedMs = std::chrono::duration_cast(t1 - t0).count(); + + return { scoreValue, elapsedMs }; + +} + +void processQubicScore(const unsigned char* miningSeed, const unsigned char* publicKey, const unsigned char* nonce, int sampleIndex) +{ + // Core use the external random pool + std::unique_ptr> pScore = std::make_unique>(); + pScore->initMemory(); + pScore->initMiningData(miningSeed); + + unsigned int scoreValue = (*pScore)(0, publicKey, miningSeed, nonce); + + // Determine which algo to use to get the correct ground truth + int gtIndex = -1; + unsigned int gtScore = 0; + score_engine::AlgoType effectiveAlgo = (nonce[0] & 1) == 0 ? score_engine::AlgoType::HyperIdentity : score_engine::AlgoType::Addition; + + std::vector state(score_engine::STATE_SIZE); + std::vector externalPoolVec(score_engine::POOL_VEC_PADDING_SIZE); + score_engine::generateRandom2Pool(miningSeed, state.data(), externalPoolVec.data()); + ScoreResult scoreResult = computeScore, + score_engine::AdditionParams< + ADDITION_NUMBER_OF_INPUT_NEURONS, + ADDITION_NUMBER_OF_OUTPUT_NEURONS, + ADDITION_NUMBER_OF_TICKS, + ADDITION_NUMBER_OF_NEIGHBORS, + ADDITION_POPULATION_THRESHOLD, + ADDITION_NUMBER_OF_MUTATIONS, + ADDITION_SOLUTION_THRESHOLD_DEFAULT> + >>( + miningSeed, publicKey, nonce, effectiveAlgo, externalPoolVec.data()); + unsigned int refScore = scoreResult.score; + +#pragma omp critical + { + EXPECT_EQ(refScore, scoreValue); + } +} + -// Recursive template to process each element in scoreSettings template -static void processElement(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex) +static void processElement(const unsigned char* miningSeed, const unsigned char* publicKey, const unsigned char* nonce, + int sampleIndex, score_engine::AlgoType algo) { + // Skip filter settings if (!filteredSettings.empty() && std::find(filteredSettings.begin(), filteredSettings.end(), i) == filteredSettings.end()) { return; } - auto pScore = std::make_unique>(); - pScore->initMemory(); - pScore->initMiningData(miningSeed); - int x = 0; - top_of_stack = (unsigned long long)(&x); - auto t0 = std::chrono::high_resolution_clock::now(); - unsigned int score_value = (*pScore)(0, publicKey, miningSeed, nonce); - auto t1 = std::chrono::high_resolution_clock::now(); - auto d = t1 - t0; - auto elapsed = std::chrono::duration_cast(d).count(); + // Get the current config + using CurrentConfig = std::tuple_element_t; + + // Core use the external random pool + std::vector state(score_engine::STATE_SIZE); + std::vector externalPoolVec(score_engine::POOL_VEC_PADDING_SIZE); + score_engine::generateRandom2Pool(miningSeed, state.data(), externalPoolVec.data()); + ScoreResult scoreResult = computeScore( + miningSeed, publicKey, nonce, algo, externalPoolVec.data()); + unsigned int scoreValue = scoreResult.score; + + // Determine which algo to use to get the correct ground truth + int gtIndex = -1; + unsigned int gtScore = 0; + score_engine::AlgoType effectiveAlgo = algo; + if (algo != score_engine::AlgoType::HyperIdentity && algo != score_engine::AlgoType::Addition) + { + // Default/Mixed mode: select based on nonce + effectiveAlgo = (nonce[0] & 1) == 0 ? score_engine::AlgoType::HyperIdentity : score_engine::AlgoType::Addition; + } + if (effectiveAlgo == score_engine::AlgoType::HyperIdentity) + { + if (gScoreHyperIdentityIndexMap.count(i) > 0) + { + gtIndex = gScoreHyperIdentityIndexMap[i]; + gtScore = gScoresHyperIdentityGroundTruth[sampleIndex][gtIndex]; + } + } + else if (effectiveAlgo == score_engine::AlgoType::Addition) + { + if (gScoreAdditionIndexMap.count(i) > 0) + { + gtIndex = gScoreAdditionIndexMap[i]; + gtScore = gScoresHyperAdditionGroundTruth[sampleIndex][gtIndex]; + } + } unsigned int refScore = 0; if (gCompareReference) { - score_reference::ScoreReferenceImplementation< - kSettings[i][score_params::NUMBER_OF_INPUT_NEURONS], - kSettings[i][score_params::NUMBER_OF_OUTPUT_NEURONS], - kSettings[i][score_params::NUMBER_OF_TICKS], - kSettings[i][score_params::NUMBER_OF_NEIGHBORS], - kSettings[i][score_params::POPULATION_THRESHOLD], - kSettings[i][score_params::NUMBER_OF_MUTATIONS], - kSettings[i][score_params::SOLUTION_THRESHOLD], - 1 - > score; - score.initMemory(); - score.initMiningData(miningSeed); - refScore = score(0, publicKey, nonce); + // Reference score always re-compute the pools + ScoreResult scoreRefResult = computeScore( + miningSeed, publicKey, nonce, algo, nullptr); + refScore = scoreRefResult.score; } + #pragma omp critical if (gCompareReference) { - EXPECT_EQ(refScore, score_value); + EXPECT_EQ(refScore, scoreValue); } else { - long long gtIndex = -1; - if (gScoreIndexMap.count(i) > 0) - { - gtIndex = gScoreIndexMap[i]; - } - - if (PRINT_DETAILED_INFO || gtIndex < 0 || (score_value != gScoresGroundTruth[sampleIndex][gtIndex])) + if (PRINT_DETAILED_INFO || gtIndex < 0 || (scoreValue != gtScore)) { - if (gScoreProcessingTime.count(i) == 0) - { - gScoreProcessingTime[i] = elapsed; - } - else + std::cout << " score " << scoreValue; + if (gtIndex >= 0) { - gScoreProcessingTime[i] += elapsed; + std::cout << " vs gt " << gtScore << std::endl; } + else // No mapping from ground truth { - std::cout << "[sample " << sampleIndex - << "; setting " << i << ": " - << kSettings[i][score_params::NUMBER_OF_INPUT_NEURONS] << ", " - << kSettings[i][score_params::NUMBER_OF_OUTPUT_NEURONS] << ", " - << kSettings[i][score_params::NUMBER_OF_TICKS] << ", " - << kSettings[i][score_params::POPULATION_THRESHOLD] << ", " - << kSettings[i][score_params::NUMBER_OF_MUTATIONS] << ", " - << kSettings[i][score_params::SOLUTION_THRESHOLD] - << "]" - << std::endl; - std::cout << " score " << score_value; - if (gtIndex >= 0) - { - std::cout << " vs gt " << gScoresGroundTruth[sampleIndex][gtIndex] << std::endl; - } - else // No mapping from ground truth - { - std::cout << " vs gt NA" << std::endl; - } - std::cout << " time " << elapsed << " ms " << std::endl; + std::cout << " vs gt NA" << std::endl; } } { - EXPECT_GT(gScoreIndexMap.count(i), 0); + EXPECT_GE(gtIndex, 0); if (gtIndex >= 0) { - EXPECT_EQ(gScoresGroundTruth[sampleIndex][gtIndex], score_value); + EXPECT_EQ(gtScore, scoreValue); } } } @@ -161,29 +250,17 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, // Recursive template to process each element in scoreSettings template -static void processElementWithPerformance(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex) +static void processElementPerf(const unsigned char* miningSeed, const unsigned char* publicKey, const unsigned char* nonce, + int sampleIndex, score_engine::AlgoType algo) { - auto pScore = std::make_unique>(); - - pScore->initMemory(); - pScore->initMiningData(miningSeed); - int x = 0; - top_of_stack = (unsigned long long)(&x); - auto t0 = std::chrono::high_resolution_clock::now(); - unsigned int score_value = (*pScore)(0, publicKey, miningSeed, nonce); - auto t1 = std::chrono::high_resolution_clock::now(); - auto d = t1 - t0; - auto elapsed = std::chrono::duration_cast(d).count(); - + using CurrentConfig = std::tuple_element_t; + + std::vector state(score_engine::STATE_SIZE); + std::vector externalPoolVec(score_engine::POOL_VEC_PADDING_SIZE); + score_engine::generateRandom2Pool(miningSeed, state.data(), externalPoolVec.data()); + ScoreResult scoreRefResult = computeScore( + miningSeed, publicKey, nonce, algo, externalPoolVec.data()); + auto elapsed = scoreRefResult.elapsedMs; #pragma omp critical { if (gScoreProcessingTime.count(i) == 0) @@ -199,23 +276,247 @@ static void processElementWithPerformance(unsigned char* miningSeed, unsigned ch // Main processing function template -static void processHelper(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex, std::index_sequence) +static void processHelper(const unsigned char* miningSeed, const unsigned char* publicKey, const unsigned char* nonce, + int sampleIndex, score_engine::AlgoType algo, std::index_sequence) { if constexpr (profiling) { - (processElementWithPerformance(miningSeed, publicKey, nonce, sampleIndex), ...); + (processElementPerf(miningSeed, publicKey, nonce, sampleIndex, algo), ...); } else { - (processElement(miningSeed, publicKey, nonce, sampleIndex), ...); + (processElement(miningSeed, publicKey, nonce, sampleIndex, algo), ...); } + } // Recursive template to process each element in scoreSettings template -static void process(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex) +static void process(const unsigned char* miningSeed, const unsigned char* publicKey, const unsigned char* nonce, + int sampleIndex = 0, score_engine::AlgoType algo = score_engine::AlgoType::AllAlgo) +{ + processHelper(miningSeed, publicKey, nonce, sampleIndex, algo, std::make_index_sequence{}); +} + +template +void writeParams(std::ostream& os, const std::string& sep = ",") +{ + // Because currently 2 params set shared the same things, incase of new algo have different params + // need to make a separate check + if constexpr (P::algoType == score_engine::AlgoType::HyperIdentity) + { + os << "InputNeurons: " << P::numberOfInputNeurons << sep + << " OutputNeurons: " << P::numberOfOutputNeurons << sep + << " Ticks: " << P::numberOfTicks << sep + << " Neighbor: " << P::numberOfNeighbors << sep + << " Population: " << P::populationThreshold << sep + << " Mutate: " << P::numberOfMutations << sep + << " Threshold: " << P::solutionThreshold; + } + else if constexpr (P::algoType == score_engine::AlgoType::Addition) + { + os << "InputNeurons: " << P::numberOfInputNeurons << sep + << " OutputNeurons: " << P::numberOfOutputNeurons << sep + << " Ticks: " << P::numberOfTicks << sep + << " Neighbor: " << P::numberOfNeighbors << sep + << " Population: " << P::populationThreshold << sep + << " Mutate: " << P::numberOfMutations << sep + << " Threshold: " << P::solutionThreshold; + } + else + { + std::cerr << "UNKNOWN ALGO !" << std::endl; + } +} + +template +void printConfigProfileImpl(score_engine::AlgoType algo) { - processHelper(miningSeed, publicKey, nonce, sampleIndex, std::make_index_sequence{}); + using CurrentConfig = std::tuple_element_t; + + if (algo & score_engine::AlgoType::HyperIdentity) + { + writeParams(std::cout); + } + else if (algo & score_engine::AlgoType::Addition) + { + writeParams(std::cout); + } +} + +template +void printConfigImpl(score_engine::AlgoType algo) +{ + using CurrentConfig = std::tuple_element_t; + + if (algo & score_engine::AlgoType::HyperIdentity) + { + writeParams(std::cout); + } + else if (algo & score_engine::AlgoType::Addition) + { + writeParams(std::cout); + } +} + +template +void printConfigByIndex(std::size_t index, score_engine::AlgoType algo, std::index_sequence) +{ + if constexpr (profiling) + { + ((Is == index ? (printConfigProfileImpl(algo), 0) : 0), ...); + } + else + { + ((Is == index ? (printConfigImpl(algo), 0) : 0), ...); + } +} + +template +void printConfig(std::size_t index, score_engine::AlgoType algo) +{ + if constexpr (profiling) + { + printConfigByIndex(index, algo, std::make_index_sequence>{}); + } + else + { + printConfigByIndex(index, algo, std::make_index_sequence>{}); + } +} + +template +bool compareParams(const std::vector& values) +{ + // Because currently 2 params set shared the same things, incase of new algo have different params + // need to make a separate check + if constexpr (P::algoType == score_engine::AlgoType::HyperIdentity) + { + return values[0] == P::numberOfInputNeurons + && values[1] == P::numberOfOutputNeurons + && values[2] == P::numberOfTicks + && values[3] == P::numberOfNeighbors + && values[4] == P::populationThreshold + && values[5] == P::numberOfMutations + && values[6] == P::solutionThreshold; + } + else if constexpr (P::algoType == score_engine::AlgoType::Addition) + { + return values[0] == P::numberOfInputNeurons + && values[1] == P::numberOfOutputNeurons + && values[2] == P::numberOfTicks + && values[3] == P::numberOfNeighbors + && values[4] == P::populationThreshold + && values[5] == P::numberOfMutations + && values[6] == P::solutionThreshold; + } + return false; +} + +template +bool checkConfig(const std::vector& values, score_engine::AlgoType algo) +{ + using CurrentConfig = std::tuple_element_t; + switch (algo) + { + case score_engine::AlgoType::HyperIdentity: + // HyperIdentity + return compareParams(values); + break; + case score_engine::AlgoType::Addition: + // Addition + return compareParams(values); + break; + default: + return false; + break; + } +} + +template +int findMatchingConfigImpl(const std::vector& values, score_engine::AlgoType algo, std::index_sequence) +{ + int result = -1; + ((checkConfig(values, algo) ? (result = Is, false) : true) && ...); + return result; +} + +int findMatchingConfig(const std::vector& values, score_engine::AlgoType algo) +{ + return findMatchingConfigImpl(values, algo, std::make_index_sequence>{}); +} + +void loadSamples( + const std::vector>& sampleString, + unsigned long long numberOfSamples, + unsigned long long numberOfSamplesReadFromFile, + std::vector& miningSeeds, + std::vector& publicKeys, + std::vector& nonces) +{ + miningSeeds.resize(numberOfSamples); + publicKeys.resize(numberOfSamples); + nonces.resize(numberOfSamples); + + // Reading the input samples + for (unsigned long long i = 0; i < numberOfSamples; ++i) + { + if (i < numberOfSamplesReadFromFile) + { + miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); + publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); + nonces[i] = hexTo32Bytes(sampleString[i][2], 32); + } + else // Samples from files are not enough, randomly generate more + { + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[24]); + + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[24]); + + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[24]); + + } + } +} + +template +void runTest( + const std::string& testName, + const std::vector& samples, + int numberOfThreads, + ProcessFunc processFunc) +{ + std::cout << "Test " << testName << " ..." << std::endl; + int proccessedSamples = 0; +#pragma omp parallel for num_threads(numberOfThreads) + for (int i = 0; i < static_cast(samples.size()); ++i) + { + int index = samples[i]; + processFunc(index); +#pragma omp critical + proccessedSamples++; + std::cout << "\r-Processed: " << proccessedSamples << " / " << static_cast(samples.size()) << " " << std::flush; + } + std::cout << std::endl; +} + +static std::vector> readSampleAsStr(const std::string& filename) +{ + std::vector> sampleString = readCSV(filename); + + // Remove header + sampleString.erase(sampleString.begin()); + + return sampleString; } void runCommonTests() @@ -224,13 +525,10 @@ void runCommonTests() #if defined (__AVX512F__) && !GENERIC_K12 initAVX512KangarooTwelveConstants(); #endif - constexpr unsigned long long numberOfGeneratedSetting = sizeof(score_params::kSettings) / sizeof(score_params::kSettings[0]); + constexpr unsigned long long numberOfGeneratedSetting = CONFIG_COUNT; // Read the parameters and results - auto sampleString = readCSV(COMMON_TEST_SAMPLES_FILE_NAME); - auto scoresString = readCSV(COMMON_TEST_SCORES_FILE_NAME); - ASSERT_FALSE(sampleString.empty()); - ASSERT_FALSE(scoresString.empty()); + auto sampleString = readSampleAsStr(COMMON_TEST_SAMPLES_FILE_NAME); // Convert the raw string and do the data verification unsigned long long numberOfSamplesReadFromFile = sampleString.size(); @@ -262,102 +560,92 @@ void runCommonTests() } } + // Reading the input samples std::vector miningSeeds(numberOfSamples); std::vector publicKeys(numberOfSamples); std::vector nonces(numberOfSamples); + loadSamples(sampleString, numberOfSamples, numberOfSamplesReadFromFile, miningSeeds, publicKeys, nonces); - // Reading the input samples - for (unsigned long long i = 0; i < numberOfSamples; ++i) + // Reading the header of score and verification + if (!gCompareReference) { - if (i < numberOfSamplesReadFromFile) + auto scoresStringHyperidentity = readCSV(COMMON_TEST_SCORES_HYPERIDENTITY_FILE_NAME); + auto scoresStringAddition = readCSV(COMMON_TEST_SCORES_ADDITION_FILE_NAME); + + if (scoresStringAddition.size() == 0 || scoresStringAddition.size() == 0) { - miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); - publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); - nonces[i] = hexTo32Bytes(sampleString[i][2], 32); + ASSERT_GT(scoresStringHyperidentity.size(), 0); + ASSERT_GT(scoresStringAddition.size(), 0); + std::cout << "Number of Hyperidentity and Addition settings must greater than zero." << std::endl; + return; } - else // Samples from files are not enough, randomly generate more + if (scoresStringAddition.size() != scoresStringAddition.size()) { - _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[0]); - _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[8]); - _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[16]); - _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[24]); - - _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[0]); - _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[8]); - _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[16]); - _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[24]); - - _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[0]); - _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[8]); - _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[16]); - _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[24]); - + ASSERT_EQ(scoresStringHyperidentity.size(), scoresStringAddition.size()); + std::cout << "Number of Hyperidentity and Addition settings must be equal." << std::endl; + return; } - } - - // Reading the header of score and verification - if (!gCompareReference) - { - auto scoreHeader = scoresString[0]; - std::cout << "Testing " << numberOfGeneratedSetting << " param combinations on " << scoreHeader.size() << " ground truth settings." << std::endl; - for (unsigned long long i = 0; i < numberOfGeneratedSetting; ++i) - { - long long foundIndex = -1; - for (unsigned long long gtIdx = 0; gtIdx < scoreHeader.size(); ++gtIdx) + // + auto buildIndexMap = []( + std::vector& header, + score_engine::AlgoType algo, + std::map& indexMap) { - auto scoresSettingHeader = convertULLFromString(scoreHeader[gtIdx]); - - // Check matching between number of parameters types - if (scoresSettingHeader.size() != score_params::MAX_PARAM_TYPE) + for (int gtIdx = 0; gtIdx < (int)header.size(); ++gtIdx) { - std::cout << "Mismatched the number of params (NEURONS, DURATION ...) and MAX_PARAM_TYPE" << std::endl; - EXPECT_EQ(scoresSettingHeader.size(), score_params::MAX_PARAM_TYPE); - return; - } - - // Check the value matching between ground truth file and score params - // Only record the current available score params - int count = 0; - for (unsigned long long j = 0; j < score_params::MAX_PARAM_TYPE; ++j) - { - if (scoresSettingHeader[j] == kSettings[i][j]) + auto scoresSettingHeader = convertULLFromString(header[gtIdx]); + int foundIndex = findMatchingConfig(scoresSettingHeader, algo); + if (foundIndex >= 0) { - count++; + indexMap[foundIndex] = gtIdx; } } - if (count == score_params::MAX_PARAM_TYPE) - { - foundIndex = gtIdx; - break; - } - } - if (foundIndex >= 0) - { - gScoreIndexMap[i] = foundIndex; - } + }; + buildIndexMap(scoresStringHyperidentity[0], score_engine::AlgoType::HyperIdentity, gScoreHyperIdentityIndexMap); + buildIndexMap(scoresStringAddition[0], score_engine::AlgoType::Addition, gScoreAdditionIndexMap); + + if (gScoreHyperIdentityIndexMap.size() != gScoreHyperIdentityIndexMap.size()) + { + ASSERT_EQ(gScoreHyperIdentityIndexMap.size(), gScoreHyperIdentityIndexMap.size()); + std::cout << "Number of tested Hyperidentity and Addition must be equal." << std::endl; + return; } + + std::cout << "Testing " << CONFIG_COUNT << " param combinations on " << scoresStringHyperidentity[0].size() << " Hyperidentity and Addition ground truth settings." << std::endl; // In case of number of setting is lower than the ground truth. Consider we are in experiement, still run but expect the test failed - if (gScoreIndexMap.size() < numberOfGeneratedSetting) + if (gScoreHyperIdentityIndexMap.size() < CONFIG_COUNT) { std::cout << "WARNING: Number of provided ground truth settings is lower than tested settings. Only test with available ones." << std::endl; - EXPECT_EQ(gScoreIndexMap.size(), numberOfGeneratedSetting); + EXPECT_EQ(gScoreHyperIdentityIndexMap.size(), CONFIG_COUNT); } - - // Read the groudtruth scores and init result scores - numberOfSamples = std::min(numberOfSamples, scoresString.size() - 1); - gScoresGroundTruth.resize(numberOfSamples); - for (size_t i = 0; i < numberOfSamples; ++i) - { - auto scoresStr = scoresString[i + 1]; - size_t scoreSize = scoresStr.size(); - for (size_t j = 0; j < scoreSize; ++j) + auto loadGroundTruth = []( + const std::vector>& scoresString, + std::vector>& groundTruth, + int numberOfSamples) -> int { - gScoresGroundTruth[i].push_back(std::stoi(scoresStr[j])); - } - } + int numberOfGTSetting = (int)scoresString.size() - 1; + numberOfSamples = std::min(numberOfSamples, numberOfGTSetting); + groundTruth.resize(numberOfSamples); + + for (size_t i = 0; i < numberOfSamples; ++i) + { + auto& scoresStr = scoresString[i + 1]; + for (const auto& str : scoresStr) + { + groundTruth[i].push_back(std::stoi(str)); + } + } + + return numberOfSamples; + }; + + int numHI = loadGroundTruth(scoresStringHyperidentity, gScoresHyperIdentityGroundTruth, (int)numberOfSamples); + int numAdd = loadGroundTruth(scoresStringAddition, gScoresHyperAdditionGroundTruth, (int)numberOfSamples); + + std::cout << "There are " << numHI << " Hyperidentity results and " << numAdd << " Addition results." << std::endl; } @@ -395,49 +683,84 @@ void runCommonTests() } std::cout << "Processing " << samples.size() << " samples " << compTerm << "..." << std::endl; + + for (const auto& [algoType, algoName] : TEST_ALGOS) + { + runTest(algoName, samples, numberOfThreads, [&](int index) + { + process<0, CONFIG_COUNT>( + miningSeeds[index].m256i_u8, + publicKeys[index].m256i_u8, + nonces[index].m256i_u8, + index, + algoType); + }); + } + + // Test Qubic score vs internal engine (always runs) + runTest("Qubic's score vs internal score engine on active config", samples, numberOfThreads, [&](int index) + { + processQubicScore( + miningSeeds[index].m256i_u8, + publicKeys[index].m256i_u8, + nonces[index].m256i_u8, + index); + }); + +} + +template +void profileAlgo( + score_engine::AlgoType algo, + const std::string& algoName, + const std::vector& samples, + const std::vector& miningSeeds, + const std::vector& publicKeys, + const std::vector& nonces, + const std::vector& filteredSamples, + int numberOfThreads, + unsigned long long numberOfSamples) +{ + std::cout << "Profile " << algoName << " ... " << std::endl; gScoreProcessingTime.clear(); + + int numSamples = static_cast(samples.size()); + int proccessedSamples = 0; #pragma omp parallel for num_threads(numberOfThreads) - for (int i = 0; i < samples.size(); ++i) + for (int i = 0; i < numSamples; ++i) { int index = samples[i]; - process<0, numberOfGeneratedSetting>(miningSeeds[index].m256i_u8, publicKeys[index].m256i_u8, nonces[index].m256i_u8, index); + process( + miningSeeds[index].m256i_u8, + publicKeys[index].m256i_u8, + nonces[index].m256i_u8, + index, + algo); #pragma omp critical - std::cout << i << ", "; + proccessedSamples++; + std::cout << "\r-Processed: " << proccessedSamples << " / " << numSamples << " " << std::flush; } std::cout << std::endl; - // Print the average processing time - if (PRINT_DETAILED_INFO) + std::size_t sampleCount = filteredSamples.empty() ? numberOfSamples : filteredSamples.size(); + for (const auto& [settingIndex, totalTime] : gScoreProcessingTime) { - for (auto scoreTime : gScoreProcessingTime) - { - unsigned long long processingTime = filteredSamples.empty() ? scoreTime.second / numberOfSamples : scoreTime.second / filteredSamples.size(); - std::cout << "Avg processing time [setting " << scoreTime.first << " " - << kSettings[scoreTime.first][score_params::NUMBER_OF_INPUT_NEURONS] << ", " - << kSettings[scoreTime.first][score_params::NUMBER_OF_OUTPUT_NEURONS] << ", " - << kSettings[scoreTime.first][score_params::NUMBER_OF_TICKS] << ", " - << kSettings[scoreTime.first][score_params::NUMBER_OF_NEIGHBORS] << ", " - << kSettings[scoreTime.first][score_params::POPULATION_THRESHOLD] << ", " - << kSettings[scoreTime.first][score_params::NUMBER_OF_MUTATIONS] << ", " - << kSettings[scoreTime.first][score_params::SOLUTION_THRESHOLD] - << "]: " << processingTime << " ms" << std::endl; - } + unsigned long long avgTime = totalTime / sampleCount; + std::cout << "Avg time [setting " << settingIndex << "]: "; + printConfig(settingIndex, algo); + std::cout << " - " << avgTime << " ms" << std::endl; } } void runPerformanceTests() { - #if defined (__AVX512F__) && !GENERIC_K12 initAVX512KangarooTwelveConstants(); #endif - constexpr unsigned long long numberOfGeneratedSetting = sizeof(score_params::kProfileSettings) / sizeof(score_params::kProfileSettings[0]); + constexpr unsigned long long numberOfGeneratedSetting = PROFILE_CONFIG_COUNT; // Read the parameters and results - auto sampleString = readCSV(COMMON_TEST_SAMPLES_FILE_NAME); - auto scoresString = readCSV(COMMON_TEST_SCORES_FILE_NAME); - ASSERT_FALSE(sampleString.empty()); - ASSERT_FALSE(scoresString.empty()); + auto sampleString = readSampleAsStr(COMMON_TEST_SAMPLES_FILE_NAME); // Convert the raw string and do the data verification unsigned long long numberOfSamplesReadFromFile = sampleString.size(); @@ -455,38 +778,11 @@ void runPerformanceTests() } } + // Loading samples std::vector miningSeeds(numberOfSamples); std::vector publicKeys(numberOfSamples); std::vector nonces(numberOfSamples); - - // Reading the input samples - for (unsigned long long i = 0; i < numberOfSamples; ++i) - { - if (i < numberOfSamplesReadFromFile) - { - miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); - publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); - nonces[i] = hexTo32Bytes(sampleString[i][2], 32); - } - else // Samples from files are not enough, randomly generate more - { - _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[0]); - _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[8]); - _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[16]); - _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[24]); - - _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[0]); - _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[8]); - _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[16]); - _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[24]); - - _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[0]); - _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[8]); - _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[16]); - _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[24]); - - } - } + loadSamples(sampleString, numberOfSamples, numberOfSamplesReadFromFile, miningSeeds, publicKeys, nonces); std::cout << "Profiling " << numberOfGeneratedSetting << " param combinations. " << std::endl; @@ -512,37 +808,20 @@ void runPerformanceTests() std::string compTerm = "for profiling, don't compare any result."; std::cout << "Processing " << samples.size() << " samples " << compTerm << "..." << std::endl; - gScoreProcessingTime.clear(); -#pragma omp parallel for num_threads(numberOfThreads) - for (int i = 0; i < samples.size(); ++i) - { - int index = samples[i]; - process<1, numberOfGeneratedSetting>(miningSeeds[index].m256i_u8, publicKeys[index].m256i_u8, nonces[index].m256i_u8, index); -#pragma omp critical - std::cout << i << ", "; - } - std::cout << std::endl; - // Print the average processing time - for (auto scoreTime : gScoreProcessingTime) - { - unsigned long long processingTime = filteredSamples.empty() ? scoreTime.second / numberOfSamples : scoreTime.second / filteredSamples.size(); - std::cout << "Avg processing time [setting " << scoreTime.first << " " - << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_INPUT_NEURONS] << ", " - << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_OUTPUT_NEURONS] << ", " - << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_TICKS] << ", " - << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_NEIGHBORS] << ", " - << kProfileSettings[scoreTime.first][score_params::POPULATION_THRESHOLD] << ", " - << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_MUTATIONS] << ", " - << kProfileSettings[scoreTime.first][score_params::SOLUTION_THRESHOLD] - << "]: " << processingTime << " ms" << std::endl; - } - gProfilingDataCollector.writeToFile(); -} + profileAlgo<1, PROFILE_CONFIG_COUNT>( + score_engine::AlgoType::HyperIdentity, "HyperIdentity", + samples, miningSeeds, publicKeys, nonces, filteredSamples, numberOfThreads, numberOfSamples); -TEST(TestQubicScoreFunction, CommonTests) -{ - runCommonTests(); + profileAlgo<1, PROFILE_CONFIG_COUNT>( + score_engine::AlgoType::Addition, "Addition", + samples, miningSeeds, publicKeys, nonces, filteredSamples, numberOfThreads, numberOfSamples); + + profileAlgo<1, PROFILE_CONFIG_COUNT>( + score_engine::AlgoType::MaxAlgoCount, "Mixed", + samples, miningSeeds, publicKeys, nonces, filteredSamples, numberOfThreads, numberOfSamples); + + gProfilingDataCollector.writeToFile(); } #if ENABLE_PROFILING @@ -553,6 +832,11 @@ TEST(TestQubicScoreFunction, PerformanceTests) } #endif +TEST(TestQubicScoreFunction, CommonTests) +{ + runCommonTests(); +} + #if not ENABLE_PROFILING TEST(TestQubicScoreFunction, TestDeterministic) { @@ -561,8 +845,7 @@ TEST(TestQubicScoreFunction, TestDeterministic) constexpr int NUMBER_OF_SAMPLES = 4; // Read the parameters and results - auto sampleString = readCSV(COMMON_TEST_SAMPLES_FILE_NAME); - ASSERT_FALSE(sampleString.empty()); + auto sampleString = readSampleAsStr(COMMON_TEST_SAMPLES_FILE_NAME); // Convert the raw string and do the data verification unsigned long long numberOfSamples = sampleString.size(); @@ -583,16 +866,7 @@ TEST(TestQubicScoreFunction, TestDeterministic) nonces[i] = hexTo32Bytes(sampleString[i][2], 32); } - auto pScore = std::make_unique>(); + std::unique_ptr> pScore = std::make_unique>(); pScore->initMemory(); // Run with 4 mining seeds, each run 4 separate threads and the result need to matched diff --git a/test/score_addition_reference.h b/test/score_addition_reference.h new file mode 100644 index 000000000..8b95c17ed --- /dev/null +++ b/test/score_addition_reference.h @@ -0,0 +1,869 @@ +#pragma once + +#include "score_common_reference.h" + +#include +#include + +namespace score_addition_reference +{ + +template +struct Miner +{ + // Convert params for easier usage + static constexpr unsigned long long numberOfInputNeurons = Params::numberOfInputNeurons; + static constexpr unsigned long long numberOfOutputNeurons = Params::numberOfOutputNeurons; + static constexpr unsigned long long numberOfTicks = Params::numberOfTicks; + static constexpr unsigned long long maxNumberOfNeighbors = Params::numberOfNeighbors; + static constexpr unsigned long long populationThreshold = Params::populationThreshold; + static constexpr unsigned long long numberOfMutations = Params::numberOfMutations; + static constexpr unsigned int solutionThreshold = Params::solutionThreshold; + + static constexpr unsigned long long numberOfNeurons = + numberOfInputNeurons + numberOfOutputNeurons; + static constexpr unsigned long long maxNumberOfNeurons = populationThreshold; + static constexpr unsigned long long maxNumberOfSynapses = + populationThreshold * maxNumberOfNeighbors; + static constexpr unsigned long long trainingSetSize = 1ULL << numberOfInputNeurons; // 2^K + static constexpr unsigned long long paddingNumberOfSynapses = + (maxNumberOfSynapses + 31 ) / 32 * 32; // padding to multiple of 32 + + static_assert( + maxNumberOfSynapses <= (0xFFFFFFFFFFFFFFFF << 1ULL), + "maxNumberOfSynapses must less than or equal MAX_UINT64/2"); + static_assert(maxNumberOfNeighbors % 2 == 0, "maxNumberOfNeighbors must divided by 2"); + static_assert( + populationThreshold > numberOfNeurons, + "populationThreshold must be greater than numberOfNeurons"); + + std::vector poolVec; + + void initialize(const unsigned char miningSeed[32]) + { + // Init random2 pool with mining seed + poolVec.resize(score_reference::POOL_VEC_PADDING_SIZE); + score_reference::generateRandom2Pool(miningSeed, poolVec.data()); + } + + // Training set + struct TraningPair + { + char input[numberOfInputNeurons]; // numberOfInputNeurons / 2 bits of A , and B (values: -1 or +1) + char output[numberOfOutputNeurons]; // numberOfOutputNeurons bits of C (values: -1 or +1) + } trainingSet[trainingSetSize]; // training set size: 2^K + + struct Synapse + { + char weight; + }; + + // Data for running the ANN + struct Neuron + { + enum Type + { + kInput, + kOutput, + kEvolution, + }; + Type type; + char value; + bool markForRemoval; + }; + + // Data for roll back + struct ANN + { + Neuron neurons[maxNumberOfNeurons]; + Synapse synapses[maxNumberOfSynapses]; + unsigned long long population; + }; + ANN bestANN; + ANN currentANN; + + // Intermediate data + struct InitValue + { + unsigned long long outputNeuronPositions[numberOfOutputNeurons]; + unsigned long long synapseWeight[paddingNumberOfSynapses / 32]; // each 64bits elements will + // decide value of 32 synapses + unsigned long long synpaseMutation[numberOfMutations]; + } initValue; + + unsigned long long neuronIndices[numberOfNeurons]; + char previousNeuronValue[maxNumberOfNeurons]; + + unsigned long long outputNeuronIndices[numberOfOutputNeurons]; + char outputNeuronExpectedValue[numberOfOutputNeurons]; + + long long neuronValueBuffer[maxNumberOfNeurons]; + + unsigned long long getActualNeighborCount() const + { + unsigned long long population = currentANN.population; + unsigned long long maxNeighbors = population - 1; // Exclude self + unsigned long long actual = std::min(maxNumberOfNeighbors, maxNeighbors); + + return actual; + } + + unsigned long long getLeftNeighborCount() const + { + unsigned long long actual = getActualNeighborCount(); + // For odd number, we add extra for the left + return (actual + 1) / 2; + } + + unsigned long long getRightNeighborCount() const + { + return getActualNeighborCount() - getLeftNeighborCount(); + } + + // Get the starting index in synapse buffer (left side start) + unsigned long long getSynapseStartIndex() const + { + constexpr unsigned long long synapseBufferCenter = maxNumberOfNeighbors / 2; + return synapseBufferCenter - getLeftNeighborCount(); + } + + // Get the ending index in synapse buffer (exclusive) + unsigned long long getSynapseEndIndex() const + { + constexpr unsigned long long synapseBufferCenter = maxNumberOfNeighbors / 2; + return synapseBufferCenter + getRightNeighborCount(); + } + + // Convert buffer index to neighbor offset + long long bufferIndexToOffset(unsigned long long bufferIdx) const + { + constexpr long long synapseBufferCenter = maxNumberOfNeighbors / 2; + if (bufferIdx < synapseBufferCenter) + { + return (long long)bufferIdx - synapseBufferCenter; // Negative (left) + } + else + { + return (long long)bufferIdx - synapseBufferCenter + 1; // Positive (right), skip 0 + } + } + + // Convert neighbor offset to buffer index + long long offsetToBufferIndex(long long offset) const + { + constexpr long long synapseBufferCenter = maxNumberOfNeighbors / 2; + if (offset == 0) + { + return -1; // Invalid, exclude self + } + else if (offset < 0) + { + return synapseBufferCenter + offset; + } + else + { + return synapseBufferCenter + offset - 1; + } + } + + long long getIndexInSynapsesBuffer(long long neighborOffset) const + { + long long leftCount = (long long)getLeftNeighborCount(); + long long rightCount = (long long)getRightNeighborCount(); + + if (neighborOffset == 0 || + neighborOffset < -leftCount || + neighborOffset > rightCount) + { + return -1; + } + + return offsetToBufferIndex(neighborOffset); + } + + + + void mutate(unsigned long long mutateStep) + { + // Mutation + unsigned long long population = currentANN.population; + unsigned long long actualNeighbors = getActualNeighborCount(); + Synapse* synapses = currentANN.synapses; + + // Randomly pick a synapse, randomly increase or decrease its weight by 1 or -1 + unsigned long long synapseMutation = initValue.synpaseMutation[mutateStep]; + unsigned long long totalValidSynapses = population * actualNeighbors; + unsigned long long flatIdx = (synapseMutation >> 1) % totalValidSynapses; + + // Convert flat index to (neuronIdx, local synapse index within valid range) + unsigned long long neuronIdx = flatIdx / actualNeighbors; + unsigned long long localSynapseIdx = flatIdx % actualNeighbors; + + // Convert to synapse buffer index that have bigger range + unsigned long long synapseIndex = localSynapseIdx + getSynapseStartIndex(); + unsigned long long synapseFullBufferIdx = neuronIdx * maxNumberOfNeighbors + synapseIndex; + + // Randomly increase or decrease its value + char weightChange = 0; + if ((synapseMutation & 1ULL) == 0) + { + weightChange = -1; + } + else + { + weightChange = 1; + } + + char newWeight = synapses[synapseFullBufferIdx].weight + weightChange; + + // Valid weight. Update it + if (newWeight >= -1 && newWeight <= 1) + { + synapses[synapseFullBufferIdx].weight = newWeight; + } + else // Invalid weight. Insert a neuron + { + // Insert the neuron + insertNeuron(neuronIdx, synapseIndex); + } + + // Clean the ANN + while (scanRedundantNeurons() > 0) + { + cleanANN(); + } + } + + // Get the pointer to all outgoing synapse of a neurons + Synapse* getSynapses(unsigned long long neuronIndex) + { + return ¤tANN.synapses[neuronIndex * maxNumberOfNeighbors]; + } + + // Calculate the new neuron index that is reached by moving from the given `neuronIdx` `value` + // neurons to the right or left. Negative `value` moves to the left, positive `value` moves to + // the right. The return value is clamped in a ring buffer fashion, i.e. moving right of the + // rightmost neuron continues at the leftmost neuron. + unsigned long long clampNeuronIndex(long long neuronIdx, long long value) + { + unsigned long long population = currentANN.population; + assert(value > -(long long)population && value < (long long)population + && "clampNeuronIndex: |value| must be less than population"); + + long long nnIndex = 0; + // Calculate the neuron index (ring structure) + if (value >= 0) + { + nnIndex = neuronIdx + value; + } + else + { + nnIndex = neuronIdx + population + value; + } + nnIndex = nnIndex % population; + return (unsigned long long)nnIndex; + } + + + // Remove a neuron and all synapses relate to it + void removeNeuron(unsigned long long neuronIdx) + { + long long leftCount = (long long)getLeftNeighborCount(); + long long rightCount = (long long)getRightNeighborCount(); + unsigned long long startSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long endSynapseBufferIdx = getSynapseEndIndex(); + + // Scan all its neighbor to remove their outgoing synapse point to the neuron + for (long long neighborOffset = -leftCount; neighborOffset <= rightCount; neighborOffset++) + { + if (neighborOffset == 0) continue; + + unsigned long long nnIdx = clampNeuronIndex(neuronIdx, neighborOffset); + Synapse* pNNSynapses = getSynapses(nnIdx); + + long long synapseIndexOfNN = getIndexInSynapsesBuffer(-neighborOffset); + if (synapseIndexOfNN < 0) + { + continue; + } + + // The synapse array need to be shifted regard to the remove neuron + // Also neuron need to have 2M neighbors, the addtional synapse will be set as zero + // weight Case1 [S0 S1 S2 - SR S5 S6]. SR is removed, [S0 S1 S2 S5 S6 0] Case2 [S0 S1 SR + // - S3 S4 S5]. SR is removed, [0 S0 S1 S3 S4 S5] + constexpr unsigned long long halfMax = maxNumberOfNeighbors / 2; + if (synapseIndexOfNN >= (long long)halfMax) + { + for (long long k = synapseIndexOfNN; k < (long long)endSynapseBufferIdx - 1; ++k) + { + pNNSynapses[k] = pNNSynapses[k + 1]; + } + pNNSynapses[endSynapseBufferIdx - 1].weight = 0; + } + else + { + for (long long k = synapseIndexOfNN; k > (long long)startSynapseBufferIdx; --k) + { + pNNSynapses[k] = pNNSynapses[k - 1]; + } + pNNSynapses[startSynapseBufferIdx].weight = 0; + } + } + + // Shift the synapse array and the neuron array + for (unsigned long long shiftIdx = neuronIdx; shiftIdx < currentANN.population - 1; shiftIdx++) + { + currentANN.neurons[shiftIdx] = currentANN.neurons[shiftIdx + 1]; + + // Also shift the synapses + memcpy( + getSynapses(shiftIdx), + getSynapses(shiftIdx + 1), + maxNumberOfNeighbors * sizeof(Synapse)); + } + currentANN.population--; + } + + unsigned long long + getNeighborNeuronIndex(unsigned long long neuronIndex, unsigned long long neighborOffset) + { + const unsigned long long leftNeighbors = getLeftNeighborCount(); + unsigned long long nnIndex = 0; + if (neighborOffset < leftNeighbors) + { + nnIndex = clampNeuronIndex( + neuronIndex + neighborOffset, -(long long)leftNeighbors); + } + else + { + nnIndex = clampNeuronIndex( + neuronIndex + neighborOffset + 1, -(long long)leftNeighbors); + } + return nnIndex; + } + + void insertNeuron(unsigned long long neuronIndex, unsigned long long synapseIndex) + { + unsigned long long synapseFullBufferIdx = neuronIndex * maxNumberOfNeighbors + synapseIndex; + // Old value before insert neuron + unsigned long long oldStartSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long oldEndSynapseBufferIdx = getSynapseEndIndex(); + unsigned long long oldActualNeighbors = getActualNeighborCount(); + long long oldLeftCount = (long long)getLeftNeighborCount(); + long long oldRightCount = (long long)getRightNeighborCount(); + + constexpr unsigned long long halfMax = maxNumberOfNeighbors / 2; + + // Validate synapse index is within valid range + assert(synapseIndex >= oldStartSynapseBufferIdx && synapseIndex < oldEndSynapseBufferIdx); + + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + unsigned long long& population = currentANN.population; + + // Copy original neuron to the inserted one and set it as Neuron::kEvolution type + Neuron insertNeuron; + insertNeuron = neurons[neuronIndex]; + insertNeuron.type = Neuron::kEvolution; + unsigned long long insertedNeuronIdx = neuronIndex + 1; + + char originalWeight = synapses[synapseFullBufferIdx].weight; + + // Insert the neuron into array, population increased one, all neurons next to original one + // need to shift right + for (unsigned long long i = population; i > neuronIndex; --i) + { + neurons[i] = neurons[i - 1]; + + // Also shift the synapses to the right + memcpy(getSynapses(i), getSynapses(i - 1), maxNumberOfNeighbors * sizeof(Synapse)); + } + neurons[insertedNeuronIdx] = insertNeuron; + population++; + + // Recalculate after population change + unsigned long long newActualNeighbors = getActualNeighborCount(); + unsigned long long newStartSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long newEndSynapseBufferIdx = getSynapseEndIndex(); + + // Try to update the synapse of inserted neuron. All outgoing synapse is init as zero weight + Synapse* pInsertNeuronSynapse = getSynapses(insertedNeuronIdx); + for (unsigned long long synIdx = 0; synIdx < maxNumberOfNeighbors; ++synIdx) + { + pInsertNeuronSynapse[synIdx].weight = 0; + } + + // Copy the outgoing synapse of original neuron + if (synapseIndex < halfMax) + { + // The synapse is going to a neuron to the left of the original neuron. + // Check if the incoming neuron is still contained in the neighbors of the inserted + // neuron. This is the case if the original `synapseIndex` is > 0, i.e. + // the original synapse if not going to the leftmost neighbor of the original neuron. + if (synapseIndex > newStartSynapseBufferIdx) + { + // Decrease idx by one because the new neuron is inserted directly to the right of + // the original one. + pInsertNeuronSynapse[synapseIndex - 1].weight = originalWeight; + } + // If the incoming neuron of the original synapse if not contained in the neighbors of + // the inserted neuron, don't add the synapse. + } + else + { + // The synapse is going to a neuron to the right of the original neuron. + // In this case, the incoming neuron of the synapse is for sure contained in the + // neighbors of the inserted neuron and has the same idx (right side neighbors of + // inserted neuron = right side neighbors of original neuron before insertion). + pInsertNeuronSynapse[synapseIndex].weight = originalWeight; + } + + // The change of synapse only impact neuron in [originalNeuronIdx - actualNeighbors / 2 + // + 1, originalNeuronIdx + actualNeighbors / 2] In the new index, it will be + // [originalNeuronIdx + 1 - actualNeighbors / 2, originalNeuronIdx + 1 + + // actualNeighbors / 2] [N0 N1 N2 original inserted N4 N5 N6], M = 2. + for (long long delta = -oldLeftCount; delta <= oldRightCount; ++delta) + { + // Only process the neighbors + if (delta == 0) + { + continue; + } + unsigned long long updatedNeuronIdx = clampNeuronIndex(insertedNeuronIdx, delta); + + // Generate a list of neighbor index of current updated neuron NN + // Find the location of the inserted neuron in the list of neighbors + long long insertedNeuronIdxInNeigborList = -1; + for (unsigned long long k = 0; k < newActualNeighbors; k++) + { + unsigned long long nnIndex = getNeighborNeuronIndex(updatedNeuronIdx, k); + if (nnIndex == insertedNeuronIdx) + { + insertedNeuronIdxInNeigborList = (long long)(newStartSynapseBufferIdx + k); + } + } + + assert(insertedNeuronIdxInNeigborList >= 0); + + Synapse* pUpdatedSynapses = getSynapses(updatedNeuronIdx); + // [N0 N1 N2 original inserted N4 N5 N6], M = 2. + // Case: neurons in range [N0 N1 N2 original], right synapses will be affected + if (delta < 0) + { + // Left side is kept as it is, only need to shift to the right side + for (long long k = (long long)newEndSynapseBufferIdx - 1; k >= insertedNeuronIdxInNeigborList; --k) + { + // Updated synapse + pUpdatedSynapses[k] = pUpdatedSynapses[k - 1]; + } + + // Incomming synapse from original neuron -> inserted neuron must be zero + if (delta == -1) + { + pUpdatedSynapses[insertedNeuronIdxInNeigborList].weight = 0; + } + } + else // Case: neurons in range [inserted N4 N5 N6], left synapses will be affected + { + // Right side is kept as it is, only need to shift to the left side + for (long long k = (long long)newStartSynapseBufferIdx; k < insertedNeuronIdxInNeigborList; ++k) + { + // Updated synapse + pUpdatedSynapses[k] = pUpdatedSynapses[k + 1]; + } + } + } + } + + + // Check which neurons/synapse need to be removed after mutation + unsigned long long scanRedundantNeurons() + { + unsigned long long population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + + unsigned long long startSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long endSynapseBufferIdx = getSynapseEndIndex(); + long long leftCount = (long long)getLeftNeighborCount(); + long long rightCount = (long long)getRightNeighborCount(); + + unsigned long long numberOfRedundantNeurons = 0; + // After each mutation, we must verify if there are neurons that do not affect the ANN + // output. These are neurons that either have all incoming synapse weights as 0, or all + // outgoing synapse weights as 0. Such neurons must be removed. + for (unsigned long long i = 0; i < population; i++) + { + neurons[i].markForRemoval = false; + if (neurons[i].type == Neuron::kEvolution) + { + bool allOutGoingZeros = true; + bool allIncommingZeros = true; + + // Loop though its synapses for checkout outgoing synapses + for (unsigned long long m = startSynapseBufferIdx; m < endSynapseBufferIdx; m++) + { + char synapseW = synapses[i * maxNumberOfNeighbors + m].weight; + if (synapseW != 0) + { + allOutGoingZeros = false; + break; + } + } + + // Loop through the neighbor neurons to check all incoming synapses + for (long long offset = -leftCount; offset <= rightCount; offset++) + { + if (offset == 0) continue; + + unsigned long long nnIdx = clampNeuronIndex(i, offset); + long long synapseIdx = getIndexInSynapsesBuffer(-offset); + if (synapseIdx < 0) + { + continue; + } + char synapseW = getSynapses(nnIdx)[synapseIdx].weight; + + if (synapseW != 0) + { + allIncommingZeros = false; + break; + } + } + if (allOutGoingZeros || allIncommingZeros) + { + neurons[i].markForRemoval = true; + numberOfRedundantNeurons++; + } + } + } + return numberOfRedundantNeurons; + } + + // Remove neurons and synapses that do not affect the ANN + void cleanANN() + { + Neuron* neurons = currentANN.neurons; + unsigned long long& population = currentANN.population; + + // Scan and remove neurons/synapses + unsigned long long neuronIdx = 0; + while (neuronIdx < population) + { + if (neurons[neuronIdx].markForRemoval) + { + // Remove it from the neuron list. Overwrite data + // Remove its synapses in the synapses array + removeNeuron(neuronIdx); + } + else + { + neuronIdx++; + } + } + } + + void processTick() + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + + // Memset value of current one + memset(neuronValueBuffer, 0, sizeof(neuronValueBuffer)); + + // Loop though all neurons + unsigned long long startSynapseBufferIdx = getSynapseStartIndex(); + unsigned long long endSynapseBufferIdx = getSynapseEndIndex(); + + for (unsigned long long n = 0; n < population; ++n) + { + const Synapse* kSynapses = getSynapses(n); + long long neuronValue = neurons[n].value; + // Scan through all neighbor neurons and sum all connected neurons. + for (unsigned long long m = startSynapseBufferIdx; m < endSynapseBufferIdx; m++) + { + char synapseWeight = kSynapses[m].weight; + long long offset = bufferIndexToOffset(m); + unsigned long long nnIndex = clampNeuronIndex(static_cast(n), offset); + + // Weight-sum + neuronValueBuffer[nnIndex] += synapseWeight * neuronValue; + } + } + + // Clamp the neuron value + for (unsigned long long n = 0; n < population; ++n) + { + // Only non input neurons are updated + if (Neuron::kInput != neurons[n].type) + { + char neuronValue = score_reference::clampNeuron(neuronValueBuffer[n]); + neurons[n].value = neuronValue; + } + } + } + void loadTrainingData(unsigned long long trainingIndex) + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + + const auto& data = trainingSet[trainingIndex]; + // Load the input neuron value + unsigned long long inputIndex = 0; + for (unsigned long long n = 0; n < population; ++n) + { + // Init as zeros + neurons[n].value = 0; + if (Neuron::kInput == neurons[n].type) + { + neurons[n].value = data.input[inputIndex]; + inputIndex++; + } + } + + // Load the expected output value + memcpy(outputNeuronExpectedValue, data.output, sizeof(outputNeuronExpectedValue[0]) * numberOfOutputNeurons); + } + // Tick simulation only runs on one ANN + void runTickSimulation(unsigned long long trainingIndex) + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + + // Load the training set and fill ANN value + loadTrainingData(trainingIndex); + + // Save the neuron value for comparison + for (unsigned long long i = 0; i < population; ++i) + { + // Backup the neuron value + previousNeuronValue[i] = neurons[i].value; + } + + for (unsigned long long tick = 0; tick < numberOfTicks; ++tick) + { + processTick(); + // Check exit conditions: + // - N ticks have passed (already in for loop) + // - All neuron values are unchanged + // - All output neurons have non-zero values + bool allNeuronsUnchanged = true; + bool allOutputNeuronsIsNonZeros = true; + for (unsigned long long n = 0; n < population; ++n) + { + // Neuron unchanged check + if (previousNeuronValue[n] != neurons[n].value) + { + allNeuronsUnchanged = false; + } + + // Ouput neuron value check + if (neurons[n].type == Neuron::kOutput && neurons[n].value == 0) + { + allOutputNeuronsIsNonZeros = false; + } + } + + if (allOutputNeuronsIsNonZeros || allNeuronsUnchanged) + { + break; + } + + // Copy the neuron value + for (unsigned long long n = 0; n < population; ++n) + { + previousNeuronValue[n] = neurons[n].value; + } + } + } + + unsigned int computeMatchingOutput() + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + + // Compute the non-matching value R between output neuron value and initial value + // Because the output neuron order never changes, the order is preserved + unsigned int R = 0; + unsigned long long outputIdx = 0; + for (unsigned long long i = 0; i < population; i++) + { + if (neurons[i].type == Neuron::kOutput) + { + if (neurons[i].value == outputNeuronExpectedValue[outputIdx]) + { + R++; + } + outputIdx++; + } + } + return R; + } + + // Generate all 2^K possible (A, B, C) pairs + void generateTrainingSet() + { + static constexpr long long boundValue = (1LL << (numberOfInputNeurons / 2)) / 2; + unsigned long long index = 0; + for (long long A = -boundValue; A < boundValue; A++) + { + for (long long B = -boundValue; B < boundValue; B++) + { + long long C = A + B; + + score_reference::toTenaryBits(A, trainingSet[index].input); + score_reference::toTenaryBits( + B, trainingSet[index].input + numberOfInputNeurons / 2); + score_reference::toTenaryBits(C, trainingSet[index].output); + index++; + } + } + } + + unsigned int inferANN() + { + unsigned int score = 0; + for (unsigned long long i = 0; i < trainingSetSize; ++i) + { + // Ticks simulation + runTickSimulation(i); + + // Compute R + unsigned int R = computeMatchingOutput(); + score += R; + } + return score; + } + + unsigned int initializeANN(const unsigned char* publicKey, const unsigned char* nonce) + { + unsigned char hash[32]; + unsigned char combined[64]; + memcpy(combined, publicKey, 32); + memcpy(combined + 32, nonce, 32); + KangarooTwelve(combined, 64, hash, 32); + + unsigned long long& population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + + // Initialization + population = numberOfNeurons; + + // Generate all 2^K possible (A, B, C) pairs + generateTrainingSet(); + + // Initalize with nonce and public key + score_reference::random2(hash, poolVec.data(), (unsigned char*)&initValue, sizeof(InitValue)); + + // Randomly choose the positions of neurons types + for (unsigned long long i = 0; i < population; ++i) + { + neuronIndices[i] = i; + neurons[i].type = Neuron::kInput; + } + unsigned long long neuronCount = population; + for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) + { + unsigned long long outputNeuronIdx = initValue.outputNeuronPositions[i] % neuronCount; + + // Fill the neuron type + neurons[neuronIndices[outputNeuronIdx]].type = Neuron::kOutput; + outputNeuronIndices[i] = neuronIndices[outputNeuronIdx]; + + // This index is used, copy the end of indices array to current position and decrease + // the number of picking neurons + neuronCount = neuronCount - 1; + neuronIndices[outputNeuronIdx] = neuronIndices[neuronCount]; + } + + // Synapse weight initialization + auto extractWeight = [](unsigned long long packedValue, unsigned long long position) -> char { + unsigned char extractValue = static_cast((packedValue >> (position * 2)) & 0b11); + switch (extractValue) + { + case 2: + return -1; + case 3: + return 1; + default: + return 0; + } + }; + for (unsigned long long i = 0; i < (maxNumberOfSynapses / 32); ++i) + { + for (unsigned long long j = 0; j < 32; ++j) + { + synapses[32 * i + j].weight = extractWeight(initValue.synapseWeight[i], j); + } + } + + // Handle remaining synapses (if maxNumberOfSynapses not divisible by 32) + unsigned long long remainder = maxNumberOfSynapses % 32; + if (remainder > 0) + { + unsigned long long lastBlock = maxNumberOfSynapses / 32; + for (unsigned long long j = 0; j < remainder; ++j) + { + synapses[32 * lastBlock + j].weight = extractWeight(initValue.synapseWeight[lastBlock], j); + } + } + + // Run the first inference to get starting point before mutation + unsigned int score = inferANN(); + + return score; + } + + // Main function for mining + unsigned int computeScore(const unsigned char* publicKey, const unsigned char* nonce) + { + // Initialize + unsigned int bestR = initializeANN(publicKey, nonce); + memcpy(&bestANN, ¤tANN, sizeof(bestANN)); + + for (unsigned long long s = 0; s < numberOfMutations; ++s) + { + // Do the mutation + mutate(s); + + // Exit if the number of population reaches the maximum allowed + if (currentANN.population >= populationThreshold) + { + break; + } + + // Ticks simulation + unsigned int R = inferANN(); + + // Roll back if neccessary + if (R >= bestR) + { + bestR = R; + // Better R. Save the state + memcpy(&bestANN, ¤tANN, sizeof(bestANN)); + } + else + { + // Roll back + memcpy(¤tANN, &bestANN, sizeof(bestANN)); + } + + assert(bestANN.population <= populationThreshold); + } + return bestR; + } + + bool findSolution(const unsigned char* publicKey, const unsigned char* nonce) + { + unsigned int score = computeScore(publicKey, nonce); + if (score >= solutionThreshold) + { + return true; + } + + return false; + } +}; + +} // namespace score_addition diff --git a/test/score_common_reference.h b/test/score_common_reference.h new file mode 100644 index 000000000..62edf5d1b --- /dev/null +++ b/test/score_common_reference.h @@ -0,0 +1,108 @@ +#pragma once + +#include "kangaroo_twelve.h" + +#include + +namespace score_reference +{ + +constexpr unsigned long long POOL_VEC_SIZE = (((1ULL << 32) + 64)) >> 3; // 2^32+64 bits ~ 512MB +constexpr unsigned long long POOL_VEC_PADDING_SIZE = (POOL_VEC_SIZE + 200 - 1) / 200 * 200; // padding for multiple of 200 + +void generateRandom2Pool(const unsigned char miningSeed[32], unsigned char* pool) +{ + unsigned char state[200]; + // same pool to be used by all computors/candidates and pool content changing each phase + memcpy(&state[0], miningSeed, 32); + memset(&state[32], 0, sizeof(state) - 32); + + for (unsigned int i = 0; i < POOL_VEC_PADDING_SIZE; i += sizeof(state)) + { + KeccakP1600_Permute_12rounds(state); + memcpy(&pool[i], state, sizeof(state)); + } +} + +void random2( + unsigned char seed[32], + const unsigned char* pool, + unsigned char* output, + unsigned long long outputSizeInByte) +{ + unsigned long long paddingOutputSize = (outputSizeInByte + 64 - 1) / 64; + paddingOutputSize = paddingOutputSize * 64; + std::vector paddingOutputVec(paddingOutputSize); + unsigned char* paddingOutput = paddingOutputVec.data(); + + unsigned long long segments = paddingOutputSize / 64; + unsigned int x[8] = { 0 }; + for (int i = 0; i < 8; i++) + { + x[i] = ((unsigned int*)seed)[i]; + } + + for (int j = 0; j < segments; j++) + { + // Each segment will have 8 elements. Each element have 8 bytes + for (int i = 0; i < 8; i++) + { + unsigned int base = (x[i] >> 3) >> 3; + unsigned int m = x[i] & 63; + + unsigned long long u64_0 = ((unsigned long long*)pool)[base]; + unsigned long long u64_1 = ((unsigned long long*)pool)[base + 1]; + + // Move 8 * 8 * j to the current segment. 8 * i to current 8 bytes element + if (m == 0) + { + // some compiler doesn't work with bit shift 64 + *((unsigned long long*) & paddingOutput[j * 8 * 8 + i * 8]) = u64_0; + } + else + { + *((unsigned long long*) & paddingOutput[j * 8 * 8 + i * 8]) = (u64_0 >> m) | (u64_1 << (64 - m)); + } + + // Increase the positions in the pool for each element. + x[i] = x[i] * 1664525 + 1013904223; // https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use + } + } + + memcpy(output, paddingOutput, outputSizeInByte); +} + +// Clamp the neuron value +template +char clampNeuron(T neuronValue) +{ + if (neuronValue > 1) + { + return 1; + } + + if (neuronValue < -1) + { + return -1; + } + return static_cast(neuronValue); +} + +void extract64Bits(unsigned long long number, char* output) +{ + for (int i = 0; i < 64; ++i) + { + output[i] = ((number >> i) & 1); + } +} + +template +void toTenaryBits(long long A, char* bits) +{ + for (unsigned long long i = 0; i < bitCount; ++i) + { + char bitValue = static_cast((A >> i) & 1); + bits[i] = (bitValue == 0) ? -1 : bitValue; + } +} +} diff --git a/test/score_hyperidentity_reference.h b/test/score_hyperidentity_reference.h new file mode 100644 index 000000000..3be6f924f --- /dev/null +++ b/test/score_hyperidentity_reference.h @@ -0,0 +1,785 @@ +#pragma once + +#include "../src/mining/score_common.h" +#include "score_common_reference.h" + +namespace score_hyberidentity_reference +{ +template +struct Miner +{ + // Convert params for easier usage + static constexpr unsigned long long numberOfInputNeurons = Params::numberOfInputNeurons; + static constexpr unsigned long long numberOfOutputNeurons = Params::numberOfOutputNeurons; + static constexpr unsigned long long numberOfTicks = Params::numberOfTicks; + static constexpr unsigned long long numberOfNeighbors = Params::numberOfNeighbors; + static constexpr unsigned long long populationThreshold = Params::populationThreshold; + static constexpr unsigned long long numberOfMutations = Params::numberOfMutations; + static constexpr unsigned int solutionThreshold = Params::solutionThreshold; + + // Condition and predifined + static constexpr unsigned long long numberOfNeurons = + numberOfInputNeurons + numberOfOutputNeurons; + static constexpr unsigned long long maxNumberOfNeurons = populationThreshold; + static constexpr unsigned long long maxNumberOfSynapses = + populationThreshold * numberOfNeighbors; + static constexpr unsigned long long initNumberOfSynapses = numberOfNeurons * numberOfNeighbors; + + static_assert(numberOfInputNeurons % 64 == 0, "numberOfInputNeurons must be divided by 64"); + static_assert(numberOfOutputNeurons % 64 == 0, "numberOfOutputNeurons must be divided by 64"); + static_assert( + maxNumberOfSynapses <= (0xFFFFFFFFFFFFFFFF << 1ULL), + "maxNumberOfSynapses must less than or equal MAX_UINT64/2"); + static_assert(initNumberOfSynapses % 32 == 0, "initNumberOfSynapses must be divided by 32"); + static_assert(numberOfNeighbors % 2 == 0, "numberOfNeighbors must divided by 2"); + static_assert( + populationThreshold > numberOfNeurons, + "populationThreshold must be greater than numberOfNeurons"); + static_assert( + numberOfNeurons > numberOfNeighbors, + "Number of neurons must be greater than the number of neighbors"); + + std::vector poolVec; + + void initialize(const unsigned char miningSeed[32]) + { + // Init random2 pool with mining seed + poolVec.resize(score_reference::POOL_VEC_PADDING_SIZE); + score_reference::generateRandom2Pool(miningSeed, poolVec.data()); + } + + struct Synapse + { + char weight; + }; + + // Data for running the ANN + struct Neuron + { + enum Type + { + kInput, + kOutput, + kEvolution, + }; + Type type; + char value; + bool markForRemoval; + }; + + // Data for roll back + struct ANN + { + Neuron neurons[maxNumberOfNeurons]; + Synapse synapses[maxNumberOfSynapses]; + unsigned long long population; + }; + ANN bestANN; + ANN currentANN; + + // Intermediate data + struct InitValue + { + unsigned long long outputNeuronPositions[numberOfOutputNeurons]; + unsigned long long synapseWeight[initNumberOfSynapses / 32]; // each 64bits elements will + // decide value of 32 synapses + unsigned long long synpaseMutation[numberOfMutations]; + } initValue; + + struct MiningData + { + unsigned long long + inputNeuronRandomNumber[numberOfInputNeurons / 64]; // each bit will use for generate + // input neuron value + unsigned long long + outputNeuronRandomNumber[numberOfOutputNeurons / 64]; // each bit will use for generate + // expected output neuron value + } miningData; + + unsigned long long neuronIndices[numberOfNeurons]; + char previousNeuronValue[maxNumberOfNeurons]; + + unsigned long long outputNeuronIndices[numberOfOutputNeurons]; + char outputNeuronExpectedValue[numberOfOutputNeurons]; + + long long neuronValueBuffer[maxNumberOfNeurons]; + + void mutate(const unsigned char nonce[32], unsigned long long mutateStep) + { + // Mutation + unsigned long long population = currentANN.population; + unsigned long long synapseCount = population * numberOfNeighbors; + Synapse* synapses = currentANN.synapses; + + // Randomly pick a synapse, randomly increase or decrease its weight by 1 or -1 + unsigned long long synapseMutation = initValue.synpaseMutation[mutateStep]; + unsigned long long synapseIdx = (synapseMutation >> 1) % synapseCount; + // Randomly increase or decrease its value + char weightChange = 0; + if ((synapseMutation & 1ULL) == 0) + { + weightChange = -1; + } + else + { + weightChange = 1; + } + + char newWeight = synapses[synapseIdx].weight + weightChange; + + // Valid weight. Update it + if (newWeight >= -1 && newWeight <= 1) + { + synapses[synapseIdx].weight = newWeight; + } + else // Invalid weight. Insert a neuron + { + // Insert the neuron + insertNeuron(synapseIdx); + } + + // Clean the ANN + while (scanRedundantNeurons() > 0) + { + cleanANN(); + } + } + + // Get the pointer to all outgoing synapse of a neurons + Synapse* getSynapses(unsigned long long neuronIndex) + { + return ¤tANN.synapses[neuronIndex * numberOfNeighbors]; + } + + // Calculate the new neuron index that is reached by moving from the given `neuronIdx` `value` + // neurons to the right or left. Negative `value` moves to the left, positive `value` moves to + // the right. The return value is clamped in a ring buffer fashion, i.e. moving right of the + // rightmost neuron continues at the leftmost neuron. + unsigned long long clampNeuronIndex(long long neuronIdx, long long value) + { + unsigned long long population = currentANN.population; + long long nnIndex = 0; + // Calculate the neuron index (ring structure) + if (value >= 0) + { + nnIndex = neuronIdx + value; + } + else + { + nnIndex = neuronIdx + population + value; + } + nnIndex = nnIndex % population; + return (unsigned long long)nnIndex; + } + + // Remove a neuron and all synapses relate to it + void removeNeuron(unsigned long long neuronIdx) + { + // Scan all its neigbor to remove their outgoing synapse point to the neuron + for (long long neighborOffset = -(long long)numberOfNeighbors / 2; + neighborOffset <= (long long)numberOfNeighbors / 2; + neighborOffset++) + { + unsigned long long nnIdx = clampNeuronIndex(neuronIdx, neighborOffset); + Synapse* pNNSynapses = getSynapses(nnIdx); + + long long synapseIndexOfNN = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); + if (synapseIndexOfNN < 0) + { + continue; + } + + // The synapse array need to be shifted regard to the remove neuron + // Also neuron need to have 2M neighbors, the addtional synapse will be set as zero + // weight Case1 [S0 S1 S2 - SR S5 S6]. SR is removed, [S0 S1 S2 S5 S6 0] Case2 [S0 S1 SR + // - S3 S4 S5]. SR is removed, [0 S0 S1 S3 S4 S5] + if (synapseIndexOfNN >= numberOfNeighbors / 2) + { + for (long long k = synapseIndexOfNN; k < numberOfNeighbors - 1; ++k) + { + pNNSynapses[k] = pNNSynapses[k + 1]; + } + pNNSynapses[numberOfNeighbors - 1].weight = 0; + } + else + { + for (long long k = synapseIndexOfNN; k > 0; --k) + { + pNNSynapses[k] = pNNSynapses[k - 1]; + } + pNNSynapses[0].weight = 0; + } + } + + // Shift the synapse array and the neuron array + for (unsigned long long shiftIdx = neuronIdx; shiftIdx < currentANN.population; shiftIdx++) + { + currentANN.neurons[shiftIdx] = currentANN.neurons[shiftIdx + 1]; + + // Also shift the synapses + memcpy( + getSynapses(shiftIdx), + getSynapses(shiftIdx + 1), + numberOfNeighbors * sizeof(Synapse)); + } + currentANN.population--; + } + + unsigned long long + getNeighborNeuronIndex(unsigned long long neuronIndex, unsigned long long neighborOffset) + { + unsigned long long nnIndex = 0; + if (neighborOffset < (numberOfNeighbors / 2)) + { + nnIndex = + clampNeuronIndex(neuronIndex + neighborOffset, -(long long)numberOfNeighbors / 2); + } + else + { + nnIndex = clampNeuronIndex( + neuronIndex + neighborOffset + 1, -(long long)numberOfNeighbors / 2); + } + return nnIndex; + } + + void insertNeuron(unsigned long long synapseIdx) + { + // A synapse have incomingNeighbor and outgoingNeuron, direction incomingNeuron -> + // outgoingNeuron + unsigned long long incomingNeighborSynapseIdx = synapseIdx % numberOfNeighbors; + unsigned long long outgoingNeuron = synapseIdx / numberOfNeighbors; + + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + unsigned long long& population = currentANN.population; + + // Copy original neuron to the inserted one and set it as Neuron::kEvolution type + Neuron insertNeuron; + insertNeuron = neurons[outgoingNeuron]; + insertNeuron.type = Neuron::kEvolution; + unsigned long long insertedNeuronIdx = outgoingNeuron + 1; + + char originalWeight = synapses[synapseIdx].weight; + + // Insert the neuron into array, population increased one, all neurons next to original one + // need to shift right + for (unsigned long long i = population; i > outgoingNeuron; --i) + { + neurons[i] = neurons[i - 1]; + + // Also shift the synapses to the right + memcpy(getSynapses(i), getSynapses(i - 1), numberOfNeighbors * sizeof(Synapse)); + } + neurons[insertedNeuronIdx] = insertNeuron; + population++; + + // Try to update the synapse of inserted neuron. All outgoing synapse is init as zero weight + Synapse* pInsertNeuronSynapse = getSynapses(insertedNeuronIdx); + for (unsigned long long synIdx = 0; synIdx < numberOfNeighbors; ++synIdx) + { + pInsertNeuronSynapse[synIdx].weight = 0; + } + + // Copy the outgoing synapse of original neuron + if (incomingNeighborSynapseIdx < numberOfNeighbors / 2) + { + // The synapse is going to a neuron to the left of the original neuron. + // Check if the incoming neuron is still contained in the neighbors of the inserted + // neuron. This is the case if the original `incomingNeighborSynapseIdx` is > 0, i.e. + // the original synapse if not going to the leftmost neighbor of the original neuron. + if (incomingNeighborSynapseIdx > 0) + { + // Decrease idx by one because the new neuron is inserted directly to the right of + // the original one. + pInsertNeuronSynapse[incomingNeighborSynapseIdx - 1].weight = originalWeight; + } + // If the incoming neuron of the original synapse if not contained in the neighbors of + // the inserted neuron, don't add the synapse. + } + else + { + // The synapse is going to a neuron to the right of the original neuron. + // In this case, the incoming neuron of the synapse is for sure contained in the + // neighbors of the inserted neuron and has the same idx (right side neighbors of + // inserted neuron = right side neighbors of original neuron before insertion). + pInsertNeuronSynapse[incomingNeighborSynapseIdx].weight = originalWeight; + } + + // The change of synapse only impact neuron in [originalNeuronIdx - numberOfNeighbors / 2 + + // 1, originalNeuronIdx + numberOfNeighbors / 2] In the new index, it will be + // [originalNeuronIdx + 1 - numberOfNeighbors / 2, originalNeuronIdx + 1 + numberOfNeighbors + // / 2] [N0 N1 N2 original inserted N4 N5 N6], M = 2. + for (long long delta = -(long long)numberOfNeighbors / 2; + delta <= (long long)numberOfNeighbors / 2; + ++delta) + { + // Only process the neigbors + if (delta == 0) + { + continue; + } + unsigned long long updatedNeuronIdx = clampNeuronIndex(insertedNeuronIdx, delta); + + // Generate a list of neighbor index of current updated neuron NN + // Find the location of the inserted neuron in the list of neighbors + long long insertedNeuronIdxInNeigborList = -1; + for (long long k = 0; k < numberOfNeighbors; k++) + { + unsigned long long nnIndex = getNeighborNeuronIndex(updatedNeuronIdx, k); + if (nnIndex == insertedNeuronIdx) + { + insertedNeuronIdxInNeigborList = k; + } + } + + assert(insertedNeuronIdxInNeigborList >= 0); + + Synapse* pUpdatedSynapses = getSynapses(updatedNeuronIdx); + // [N0 N1 N2 original inserted N4 N5 N6], M = 2. + // Case: neurons in range [N0 N1 N2 original], right synapses will be affected + if (delta < 0) + { + // Left side is kept as it is, only need to shift to the right side + for (long long k = numberOfNeighbors - 1; k >= insertedNeuronIdxInNeigborList; --k) + { + // Updated synapse + pUpdatedSynapses[k] = pUpdatedSynapses[k - 1]; + } + + // Incomming synapse from original neuron -> inserted neuron must be zero + if (delta == -1) + { + pUpdatedSynapses[insertedNeuronIdxInNeigborList].weight = 0; + } + } + else // Case: neurons in range [inserted N4 N5 N6], left synapses will be affected + { + // Right side is kept as it is, only need to shift to the left side + for (long long k = 0; k < insertedNeuronIdxInNeigborList; ++k) + { + // Updated synapse + pUpdatedSynapses[k] = pUpdatedSynapses[k + 1]; + } + } + } + } + + long long getIndexInSynapsesBuffer(unsigned long long neuronIdx, long long neighborOffset) + { + // Skip the case neuron point to itself and too far neighbor + if (neighborOffset == 0 || neighborOffset < -(long long)numberOfNeighbors / 2 || + neighborOffset > (long long)numberOfNeighbors / 2) + { + return -1; + } + + long long synapseIdx = (long long)numberOfNeighbors / 2 + neighborOffset; + if (neighborOffset >= 0) + { + synapseIdx = synapseIdx - 1; + } + + return synapseIdx; + } + + // Check which neurons/synapse need to be removed after mutation + unsigned long long scanRedundantNeurons() + { + unsigned long long population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + + unsigned long long numberOfRedundantNeurons = 0; + // After each mutation, we must verify if there are neurons that do not affect the ANN + // output. These are neurons that either have all incoming synapse weights as 0, or all + // outgoing synapse weights as 0. Such neurons must be removed. + for (unsigned long long i = 0; i < population; i++) + { + neurons[i].markForRemoval = false; + if (neurons[i].type == Neuron::kEvolution) + { + bool allOutGoingZeros = true; + bool allIncommingZeros = true; + + // Loop though its synapses for checkout outgoing synapses + for (unsigned long long n = 0; n < numberOfNeighbors; n++) + { + char synapseW = synapses[i * numberOfNeighbors + n].weight; + if (synapseW != 0) + { + allOutGoingZeros = false; + break; + } + } + + // Loop through the neighbor neurons to check all incoming synapses + for (long long neighborOffset = -(long long)numberOfNeighbors / 2; + neighborOffset <= (long long)numberOfNeighbors / 2; + neighborOffset++) + { + unsigned long long nnIdx = clampNeuronIndex(i, neighborOffset); + Synapse* nnSynapses = getSynapses(nnIdx); + + long long synapseIdx = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); + if (synapseIdx < 0) + { + continue; + } + char synapseW = nnSynapses[synapseIdx].weight; + + if (synapseW != 0) + { + allIncommingZeros = false; + break; + } + } + if (allOutGoingZeros || allIncommingZeros) + { + neurons[i].markForRemoval = true; + numberOfRedundantNeurons++; + } + } + } + return numberOfRedundantNeurons; + } + + // Remove neurons and synapses that do not affect the ANN + void cleanANN() + { + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + unsigned long long& population = currentANN.population; + + // Scan and remove neurons/synapses + unsigned long long neuronIdx = 0; + while (neuronIdx < population) + { + if (neurons[neuronIdx].markForRemoval) + { + // Remove it from the neuron list. Overwrite data + // Remove its synapses in the synapses array + removeNeuron(neuronIdx); + } + else + { + neuronIdx++; + } + } + } + + void processTick() + { + unsigned long long population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + + // Memset value of current one + memset(neuronValueBuffer, 0, sizeof(neuronValueBuffer)); + + // Loop though all neurons + for (unsigned long long n = 0; n < population; ++n) + { + const Synapse* kSynapses = getSynapses(n); + long long neuronValue = neurons[n].value; + // Scan through all neighbor neurons and sum all connected neurons. + // The synapses are arranged as neuronIndex * numberOfNeighbors + for (long long m = 0; m < numberOfNeighbors; m++) + { + char synapseWeight = kSynapses[m].weight; + unsigned long long nnIndex = 0; + if (m < numberOfNeighbors / 2) + { + nnIndex = clampNeuronIndex(static_cast(n + m), -static_cast(numberOfNeighbors / 2)); + } + else + { + nnIndex = clampNeuronIndex(static_cast(n + m + 1), -static_cast(numberOfNeighbors / 2)); + } + + // Weight-sum + neuronValueBuffer[nnIndex] += synapseWeight * neuronValue; + } + } + + // Clamp the neuron value + for (unsigned long long n = 0; n < population; ++n) + { + char neuronValue = score_reference::clampNeuron(neuronValueBuffer[n]); + neurons[n].value = neuronValue; + } + } + + void runTickSimulation() + { + unsigned long long population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + + // Save the neuron value for comparison + for (unsigned long long i = 0; i < population; ++i) + { + // Backup the neuron value + previousNeuronValue[i] = neurons[i].value; + } + + for (unsigned long long tick = 0; tick < numberOfTicks; ++tick) + { + processTick(); + // Check exit conditions: + // - N ticks have passed (already in for loop) + // - All neuron values are unchanged + // - All output neurons have non-zero values + bool shouldExit = true; + bool allNeuronsUnchanged = true; + bool allOutputNeuronsIsNonZeros = true; + for (unsigned long long n = 0; n < population; ++n) + { + // Neuron unchanged check + if (previousNeuronValue[n] != neurons[n].value) + { + allNeuronsUnchanged = false; + } + + // Ouput neuron value check + if (neurons[n].type == Neuron::kOutput && neurons[n].value == 0) + { + allOutputNeuronsIsNonZeros = false; + } + } + + if (allOutputNeuronsIsNonZeros || allNeuronsUnchanged) + { + break; + } + + // Copy the neuron value + for (unsigned long long n = 0; n < population; ++n) + { + previousNeuronValue[n] = neurons[n].value; + } + } + } + + unsigned int computeNonMatchingOutput() + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + + // Compute the non-matching value R between output neuron value and initial value + // Because the output neuron order never changes, the order is preserved + unsigned int R = 0; + unsigned long long outputIdx = 0; + for (unsigned long long i = 0; i < population; i++) + { + if (neurons[i].type == Neuron::kOutput) + { + if (neurons[i].value != outputNeuronExpectedValue[outputIdx]) + { + R++; + } + outputIdx++; + } + } + return R; + } + + void initInputNeuron() + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + unsigned long long inputNeuronInitIndex = 0; + + char neuronArray[64] = {0}; + for (unsigned long long i = 0; i < population; ++i) + { + // Input will use the init value + if (neurons[i].type == Neuron::kInput) + { + // Prepare new pack + if (inputNeuronInitIndex % 64 == 0) + { + score_reference::extract64Bits( + miningData.inputNeuronRandomNumber[inputNeuronInitIndex / 64], neuronArray); + } + char neuronValue = neuronArray[inputNeuronInitIndex % 64]; + + // Convert value of neuron to trits (keeping 1 as 1, and changing 0 to -1.). + neurons[i].value = (neuronValue == 0) ? -1 : neuronValue; + + inputNeuronInitIndex++; + } + } + } + + void initOutputNeuron() + { + unsigned long long population = currentANN.population; + Neuron* neurons = currentANN.neurons; + for (unsigned long long i = 0; i < population; ++i) + { + if (neurons[i].type == Neuron::kOutput) + { + neurons[i].value = 0; + } + } + } + + void initExpectedOutputNeuron() + { + char neuronArray[64] = {0}; + for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) + { + // Prepare new pack + if (i % 64 == 0) + { + score_reference::extract64Bits(miningData.outputNeuronRandomNumber[i / 64], neuronArray); + } + char neuronValue = neuronArray[i % 64]; + // Convert value of neuron (keeping 1 as 1, and changing 0 to -1.). + outputNeuronExpectedValue[i] = (neuronValue == 0) ? -1 : neuronValue; + } + } + + unsigned int initializeANN(const unsigned char* publicKey, const unsigned char* nonce) + { + unsigned char hash[32]; + unsigned char combined[64]; + memcpy(combined, publicKey, 32); + memcpy(combined + 32, nonce, 32); + KangarooTwelve(combined, 64, hash, 32); + + unsigned long long& population = currentANN.population; + Synapse* synapses = currentANN.synapses; + Neuron* neurons = currentANN.neurons; + + // Initialization + population = numberOfNeurons; + + // Initalize with nonce and public key + score_reference::random2(hash, poolVec.data(), (unsigned char*)&initValue, sizeof(InitValue)); + + // Randomly choose the positions of neurons types + for (unsigned long long i = 0; i < population; ++i) + { + neuronIndices[i] = i; + neurons[i].type = Neuron::kInput; + } + unsigned long long neuronCount = population; + for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) + { + unsigned long long outputNeuronIdx = initValue.outputNeuronPositions[i] % neuronCount; + + // Fill the neuron type + neurons[neuronIndices[outputNeuronIdx]].type = Neuron::kOutput; + outputNeuronIndices[i] = neuronIndices[outputNeuronIdx]; + + // This index is used, copy the end of indices array to current position and decrease + // the number of picking neurons + neuronCount = neuronCount - 1; + neuronIndices[outputNeuronIdx] = neuronIndices[neuronCount]; + } + + // Synapse weight initialization + for (unsigned long long i = 0; i < (initNumberOfSynapses / 32); ++i) + { + const unsigned long long mask = 0b11; + + for (int j = 0; j < 32; ++j) + { + int shiftVal = j * 2; + unsigned char extractValue = + (unsigned char)((initValue.synapseWeight[i] >> shiftVal) & mask); + switch (extractValue) + { + case 2: + synapses[32 * i + j].weight = -1; + break; + case 3: + synapses[32 * i + j].weight = 1; + break; + default: + synapses[32 * i + j].weight = 0; + } + } + } + + // Init the neuron input and expected output value + memcpy((unsigned char*)&miningData, poolVec.data(), sizeof(miningData)); + + // Init input neuron value and output neuron + initInputNeuron(); + initOutputNeuron(); + + // Init expected output neuron + initExpectedOutputNeuron(); + + // Ticks simulation + runTickSimulation(); + + // Copy the state for rollback later + memcpy(&bestANN, ¤tANN, sizeof(ANN)); + + // Compute R and roll back if neccessary + unsigned int R = computeNonMatchingOutput(); + + return R; + } + + unsigned int computeScore(const unsigned char* publicKey, const unsigned char* nonce) + { + // Initialize + unsigned int bestR = initializeANN(publicKey, nonce); + + for (unsigned long long s = 0; s < numberOfMutations; ++s) + { + + // Do the mutation + mutate(nonce, s); + + // Exit if the number of population reaches the maximum allowed + if (currentANN.population >= populationThreshold) + { + break; + } + + // Ticks simulation + runTickSimulation(); + + // Compute R and roll back if neccessary + unsigned int R = computeNonMatchingOutput(); + if (R > bestR) + { + // Roll back + memcpy(¤tANN, &bestANN, sizeof(bestANN)); + } + else + { + bestR = R; + + // Better R. Save the state + memcpy(&bestANN, ¤tANN, sizeof(bestANN)); + } + + assert(bestANN.population <= populationThreshold); + } + + // Compute score + unsigned int score = numberOfOutputNeurons - bestR; + return score; + } + + // Main function for mining + bool findSolution(const unsigned char* publicKey, const unsigned char* nonce) + { + // Check score + unsigned int score = computeScore(publicKey, nonce); + if (score >= solutionThreshold) + { + return true; + } + + return false; + } +}; + +} // namespace score_hyberidentity \ No newline at end of file diff --git a/test/score_params.h b/test/score_params.h index 7ef8b8910..1b7eb564d 100644 --- a/test/score_params.h +++ b/test/score_params.h @@ -1,30 +1,65 @@ #pragma once +#include "../src/mining/score_common.h" + namespace score_params { -enum ParamType + +static constexpr unsigned int MAX_PARAM_TYPE = 7; + +template +struct ConfigPair { - NUMBER_OF_INPUT_NEURONS, // K - NUMBER_OF_OUTPUT_NEURONS,// L - NUMBER_OF_TICKS, // N - NUMBER_OF_NEIGHBORS, // 2M - POPULATION_THRESHOLD, // P - NUMBER_OF_MUTATIONS, // S - SOLUTION_THRESHOLD, - MAX_PARAM_TYPE + using HyperIdentity = HI; + using Addition = ADD; }; +// All configurations +using Config0 = ConfigPair< + score_engine::HyperIdentityParams<64, 64, 50, 64, 178, 50, 36>, + score_engine::AdditionParams<2 * 2, 3, 50, 64, 100, 50, 36> +>; -// Comment out when we want to reduce the number of running test -static constexpr unsigned long long kSettings[][MAX_PARAM_TYPE] = { - {64, 64, 50, 64, 178, 50, 36}, - {256, 256, 120, 256, 612, 100, 171}, - {512, 512, 150, 512, 1174, 150, 300}, - {1024, 1024, 200, 1024, 3000, 200, 600} -}; +using Config1 = ConfigPair< + score_engine::HyperIdentityParams<256, 256, 120, 256, 612, 100, 171>, + score_engine::AdditionParams<4 * 2, 5, 120, 256, 100 + 8 + 5, 100, 171> +>; + +using Config2 = ConfigPair< + score_engine::HyperIdentityParams<512, 512, 150, 512, 1174, 150, 300>, + score_engine::AdditionParams<7 * 2, 8, 150, 512, 150 + 14 + 8, 150, 600> +>; + +using Config3 = ConfigPair< + score_engine::HyperIdentityParams<1024, 1024, 200, 1024, 3000, 200, 600>, + score_engine::AdditionParams<9 * 2, 10, 200, 1024, 200 + 18 + 10, 200, 600> +>; + +using ConfigProfile = ConfigPair< + score_engine::HyperIdentityParams< + HYPERIDENTITY_NUMBER_OF_INPUT_NEURONS, + HYPERIDENTITY_NUMBER_OF_OUTPUT_NEURONS, + HYPERIDENTITY_NUMBER_OF_TICKS, + HYPERIDENTITY_NUMBER_OF_NEIGHBORS, + HYPERIDENTITY_POPULATION_THRESHOLD, + HYPERIDENTITY_NUMBER_OF_MUTATIONS, + HYPERIDENTITY_SOLUTION_THRESHOLD_DEFAULT>, + score_engine::AdditionParams< + ADDITION_NUMBER_OF_INPUT_NEURONS, + ADDITION_NUMBER_OF_OUTPUT_NEURONS, + ADDITION_NUMBER_OF_TICKS, + ADDITION_NUMBER_OF_NEIGHBORS, + ADDITION_POPULATION_THRESHOLD, + ADDITION_NUMBER_OF_MUTATIONS, + ADDITION_SOLUTION_THRESHOLD_DEFAULT> +>; + +using ConfigList = std::tuple; + +static constexpr std::size_t CONFIG_COUNT = std::tuple_size_v; + +using ProfileConfigList = std::tuple; +static constexpr std::size_t PROFILE_CONFIG_COUNT = std::tuple_size_v; -static constexpr unsigned long long kProfileSettings[][MAX_PARAM_TYPE] = { - {::NUMBER_OF_INPUT_NEURONS, ::NUMBER_OF_OUTPUT_NEURONS, ::NUMBER_OF_TICKS, ::NUMBER_OF_NEIGHBORS, ::POPULATION_THRESHOLD, ::NUMBER_OF_MUTATIONS, ::SOLUTION_THRESHOLD_DEFAULT}, -}; } diff --git a/test/score_reference.h b/test/score_reference.h index 89142de0c..6d55c3f32 100644 --- a/test/score_reference.h +++ b/test/score_reference.h @@ -2,6 +2,8 @@ #include "../src/platform/memory_util.h" #include "../src/four_q.h" +#include "score_hyperidentity_reference.h" +#include "score_addition_reference.h" #include ////////// Original (reference) scoring algorithm \\\\\\\\\\ @@ -9,854 +11,48 @@ namespace score_reference { -constexpr unsigned long long POOL_VEC_SIZE = (((1ULL << 32) + 64)) >> 3; // 2^32+64 bits ~ 512MB -constexpr unsigned long long POOL_VEC_PADDING_SIZE = (POOL_VEC_SIZE + 200 - 1) / 200 * 200; // padding for multiple of 200 - -static void generateRandom2Pool(unsigned char miningSeed[32], unsigned char* pool) -{ - unsigned char state[200]; - // same pool to be used by all computors/candidates and pool content changing each phase - memcpy(&state[0], miningSeed, 32); - memset(&state[32], 0, sizeof(state) - 32); - - for (unsigned int i = 0; i < POOL_VEC_PADDING_SIZE; i += sizeof(state)) - { - KeccakP1600_Permute_12rounds(state); - memcpy(&pool[i], state, sizeof(state)); - } -} - -static void random2( - unsigned char seed[32], - const unsigned char* pool, - unsigned char* output, - unsigned long long outputSizeInByte) -{ - unsigned long long paddingOutputSize = (outputSizeInByte + 64 - 1) / 64; - paddingOutputSize = paddingOutputSize * 64; - std::vector paddingOutputVec(paddingOutputSize); - unsigned char* paddingOutput = paddingOutputVec.data(); - - unsigned long long segments = paddingOutputSize / 64; - unsigned int x[8] = { 0 }; - for (int i = 0; i < 8; i++) - { - x[i] = ((unsigned int*)seed)[i]; - } - - for (int j = 0; j < segments; j++) - { - // Each segment will have 8 elements. Each element have 8 bytes - for (int i = 0; i < 8; i++) - { - unsigned int base = (x[i] >> 3) >> 3; - unsigned int m = x[i] & 63; - - unsigned long long u64_0 = ((unsigned long long*)pool)[base]; - unsigned long long u64_1 = ((unsigned long long*)pool)[base + 1]; - - // Move 8 * 8 * j to the current segment. 8 * i to current 8 bytes element - if (m == 0) - { - // some compiler doesn't work with bit shift 64 - *((unsigned long long*) & paddingOutput[j * 8 * 8 + i * 8]) = u64_0; - } - else - { - *((unsigned long long*) & paddingOutput[j * 8 * 8 + i * 8]) = (u64_0 >> m) | (u64_1 << (64 - m)); - } - - // Increase the positions in the pool for each element. - x[i] = x[i] * 1664525 + 1013904223; // https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use - } - } - - memcpy(output, paddingOutput, outputSizeInByte); -} - -// Clamp the neuron value -template -T clampNeuron(T neuronValue) -{ - if (neuronValue > 1) - { - return 1; - } - - if (neuronValue < -1) - { - return -1; - } - return neuronValue; -} - -void extract64Bits(unsigned long long number, char* output) -{ - int count = 0; - for (int i = 0; i < 64; ++i) - { - output[i] = ((number >> i) & 1); - } -} - -template < - unsigned long long numberOfInputNeurons, // K - unsigned long long numberOfOutputNeurons,// L - unsigned long long numberOfTicks, // N - unsigned long long numberOfNeighbors, // 2M - unsigned long long populationThreshold, // P - unsigned long long numberOfMutations, // S - unsigned int solutionThreshold, - unsigned long long solutionBufferCount -> +template struct ScoreReferenceImplementation { + using HyperIdentityScore = ::score_hyberidentity_reference::Miner; + using AdditionScore = ::score_addition_reference::Miner; + + std::unique_ptr _hyperIdentityScore; + std::unique_ptr _additionScore; void initMemory() { - allocPoolWithErrorLog(L"ComputeBuffer", sizeof(ComputeInstance) * solutionBufferCount, (void**)&(_computeBuffer), __LINE__); + _hyperIdentityScore = std::make_unique(); + _additionScore = std::make_unique(); } - - void freeMemory() + void initMiningData(const unsigned char* miningSeed) { - freePool(_computeBuffer); + _hyperIdentityScore->initialize(miningSeed); + _additionScore->initialize(miningSeed); } - ~ScoreReferenceImplementation() + unsigned int computeHyperIdentityScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool = nullptr) { - freeMemory(); + return _hyperIdentityScore->computeScore(publicKey, nonce); } - void initMiningData(m256i randomSeed) + unsigned int computeAdditionScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool = nullptr) { - + return _additionScore->computeScore(publicKey, nonce); } - void initMiningData(unsigned char* seed) + // Return score depend on the nonce + unsigned int computeScore(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool = nullptr) { - for (int i = 0; i < solutionBufferCount; i++) - { - _computeBuffer[i].initialize(seed); - } - } - - struct ComputeInstance - { - static constexpr unsigned long long numberOfNeurons = numberOfInputNeurons + numberOfOutputNeurons; - static constexpr unsigned long long maxNumberOfNeurons = populationThreshold; - static constexpr unsigned long long maxNumberOfSynapses = populationThreshold * numberOfNeighbors; - static constexpr unsigned long long initNumberOfSynapses = numberOfNeurons * numberOfNeighbors; - - static_assert(numberOfInputNeurons % 64 == 0, "numberOfInputNeurons must be divided by 64"); - static_assert(numberOfOutputNeurons % 64 == 0, "numberOfOutputNeurons must be divided by 64"); - static_assert(maxNumberOfSynapses <= (0xFFFFFFFFFFFFFFFF << 1ULL), "maxNumberOfSynapses must less than or equal MAX_UINT64/2"); - static_assert(initNumberOfSynapses % 32 == 0, "initNumberOfSynapses must be divided by 32"); - static_assert(numberOfNeighbors % 2 == 0, "numberOfNeighbors must be divided by 2"); - static_assert(populationThreshold > numberOfNeurons, "populationThreshold must be greater than numberOfNeurons"); - static_assert(numberOfNeurons > numberOfNeighbors, "Number of neurons must be greater than the number of neighbors"); - - - std::vector poolVec; - - void initialize(unsigned char miningSeed[32]) - { - // Init random2 pool with mining seed - poolVec.resize(POOL_VEC_PADDING_SIZE); - generateRandom2Pool(miningSeed, poolVec.data()); - } - - struct Synapse - { - char weight; - }; - - // Data for running the ANN - struct Neuron - { - enum Type - { - kInput, - kOutput, - kEvolution, - }; - Type type; - char value; - bool markForRemoval; - }; - - // Data for roll back - struct ANN - { - Neuron neurons[maxNumberOfNeurons]; - Synapse synapses[maxNumberOfSynapses]; - unsigned long long population; - }; - ANN bestANN; - ANN currentANN; - - // Intermediate data - struct InitValue - { - unsigned long long outputNeuronPositions[numberOfOutputNeurons]; - unsigned long long synapseWeight[initNumberOfSynapses / 32]; // each 64bits elements will decide value of 32 synapses - unsigned long long synpaseMutation[numberOfMutations]; - } initValue; - - struct MiningData - { - unsigned long long inputNeuronRandomNumber[numberOfInputNeurons / 64]; // each bit will use for generate input neuron value - unsigned long long outputNeuronRandomNumber[numberOfOutputNeurons / 64]; // each bit will use for generate expected output neuron value - } miningData; - - unsigned long long neuronIndices[numberOfNeurons]; - char previousNeuronValue[maxNumberOfNeurons]; - - unsigned long long outputNeuronIndices[numberOfOutputNeurons]; - char outputNeuronExpectedValue[numberOfOutputNeurons]; - - long long neuronValueBuffer[maxNumberOfNeurons]; - - void mutate(unsigned char nonce[32], int mutateStep) - { - // Mutation - unsigned long long population = currentANN.population; - unsigned long long synapseCount = population * numberOfNeighbors; - Synapse* synapses = currentANN.synapses; - - - // Randomly pick a synapse, randomly increase or decrease its weight by 1 or -1 - unsigned long long synapseMutation = initValue.synpaseMutation[mutateStep]; - unsigned long long synapseIdx = (synapseMutation >> 1) % synapseCount; - // Randomly increase or decrease its value - char weightChange = 0; - if ((synapseMutation & 1ULL) == 0) - { - weightChange = -1; - } - else - { - weightChange = 1; - } - - char newWeight = synapses[synapseIdx].weight + weightChange; - - // Valid weight. Update it - if (newWeight >= -1 && newWeight <= 1) - { - synapses[synapseIdx].weight = newWeight; - } - else // Invalid weight. Insert a neuron - { - // Insert the neuron - insertNeuron(synapseIdx); - } - - // Clean the ANN - while (scanRedundantNeurons() > 0) - { - cleanANN(); - } - } - - // Get the pointer to all outgoing synapse of a neurons - Synapse* getSynapses(unsigned long long neuronIndex) - { - return ¤tANN.synapses[neuronIndex * numberOfNeighbors]; - } - - // Circulate the neuron index - unsigned long long clampNeuronIndex(long long neuronIdx, long long value) - { - unsigned long long population = currentANN.population; - long long nnIndex = 0; - // Calculate the neuron index (ring structure) - if (value >= 0) - { - nnIndex = neuronIdx + value; - } - else - { - nnIndex = neuronIdx + population + value; - } - nnIndex = nnIndex % population; - return (unsigned long long)nnIndex; - } - - - // Remove a neuron and all synapses relate to it - void removeNeuron(unsigned long long neuronIdx) - { - // Scan all its neigbor to remove their outgoing synapse point to the neuron - for (long long neighborOffset = -(long long)numberOfNeighbors / 2; neighborOffset <= (long long)numberOfNeighbors / 2; neighborOffset++) - { - unsigned long long nnIdx = clampNeuronIndex(neuronIdx, neighborOffset); - Synapse* pNNSynapses = getSynapses(nnIdx); - - long long synapseIndexOfNN = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); - if (synapseIndexOfNN < 0) - { - continue; - } - - // The synapse array need to be shifted regard to the remove neuron - // Also neuron need to have 2M neighbors, the addtional synapse will be set as zero weight - // Case1 [S0 S1 S2 - SR S5 S6]. SR is removed, [S0 S1 S2 S5 S6 0] - // Case2 [S0 S1 SR - S3 S4 S5]. SR is removed, [0 S0 S1 S3 S4 S5] - if (synapseIndexOfNN >= numberOfNeighbors / 2) - { - for (long long k = synapseIndexOfNN; k < numberOfNeighbors - 1; ++k) - { - pNNSynapses[k] = pNNSynapses[k + 1]; - } - pNNSynapses[numberOfNeighbors - 1].weight = 0; - } - else - { - for (long long k = synapseIndexOfNN; k > 0; --k) - { - pNNSynapses[k] = pNNSynapses[k - 1]; - } - pNNSynapses[0].weight = 0; - } - } - - // Shift the synapse array and the neuron array - for (unsigned long long shiftIdx = neuronIdx; shiftIdx < currentANN.population; shiftIdx++) - { - currentANN.neurons[shiftIdx] = currentANN.neurons[shiftIdx + 1]; - - // Also shift the synapses - memcpy(getSynapses(shiftIdx), getSynapses(shiftIdx + 1), numberOfNeighbors * sizeof(Synapse)); - } - currentANN.population--; - } - - unsigned long long getNeighborNeuronIndex(unsigned long long neuronIndex, unsigned long long neighborOffset) - { - unsigned long long nnIndex = 0; - if (neighborOffset < (numberOfNeighbors / 2)) - { - nnIndex = clampNeuronIndex(neuronIndex + neighborOffset, -(long long)numberOfNeighbors / 2); - } - else - { - nnIndex = clampNeuronIndex(neuronIndex + neighborOffset + 1, -(long long)numberOfNeighbors / 2); - } - return nnIndex; - } - - void insertNeuron(unsigned long long synapseIdx) - { - // A synapse have incomingNeighbor and outgoingNeuron, direction incomingNeuron -> outgoingNeuron - unsigned long long incomingNeighborSynapseIdx = synapseIdx % numberOfNeighbors; - unsigned long long outgoingNeuron = synapseIdx / numberOfNeighbors; - - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - unsigned long long& population = currentANN.population; - - // Copy original neuron to the inserted one and set it as Neuron::kEvolution type - Neuron insertNeuron; - insertNeuron = neurons[outgoingNeuron]; - insertNeuron.type = Neuron::kEvolution; - unsigned long long insertedNeuronIdx = outgoingNeuron + 1; - - char originalWeight = synapses[synapseIdx].weight; - - // Insert the neuron into array, population increased one, all neurons next to original one need to shift right - for (unsigned long long i = population; i > outgoingNeuron; --i) - { - neurons[i] = neurons[i - 1]; - - // Also shift the synapses to the right - memcpy(getSynapses(i), getSynapses(i - 1), numberOfNeighbors * sizeof(Synapse)); - } - neurons[insertedNeuronIdx] = insertNeuron; - population++; - - // Try to update the synapse of inserted neuron. All outgoing synapse is init as zero weight - Synapse* pInsertNeuronSynapse = getSynapses(insertedNeuronIdx); - for (unsigned long long synIdx = 0; synIdx < numberOfNeighbors; ++synIdx) - { - pInsertNeuronSynapse[synIdx].weight = 0; - } - - // Copy the outgoing synapse of original neuron - // Outgoing points to the left - if (incomingNeighborSynapseIdx < numberOfNeighbors / 2) - { - if (incomingNeighborSynapseIdx > 0) - { - // Decrease by one because the new neuron is next to the original one - pInsertNeuronSynapse[incomingNeighborSynapseIdx - 1].weight = originalWeight; - } - // Incase of the outgoing synapse point too far, don't add the synapse - } - else - { - // No need to adjust the added neuron but need to remove the synapse of the original neuron - pInsertNeuronSynapse[incomingNeighborSynapseIdx].weight = originalWeight; - } - - // The change of synapse only impact neuron in [originalNeuronIdx - numberOfNeighbors / 2 + 1, originalNeuronIdx + numberOfNeighbors / 2] - // In the new index, it will be [originalNeuronIdx + 1 - numberOfNeighbors / 2, originalNeuronIdx + 1 + numberOfNeighbors / 2] - // [N0 N1 N2 original inserted N4 N5 N6], M = 2. - for (long long delta = -(long long)numberOfNeighbors / 2; delta <= (long long)numberOfNeighbors / 2; ++delta) - { - // Only process the neigbors - if (delta == 0) - { - continue; - } - unsigned long long updatedNeuronIdx = clampNeuronIndex(insertedNeuronIdx, delta); - - // Generate a list of neighbor index of current updated neuron NN - // Find the location of the inserted neuron in the list of neighbors - long long insertedNeuronIdxInNeigborList = -1; - for (long long k = 0; k < numberOfNeighbors; k++) - { - unsigned long long nnIndex = getNeighborNeuronIndex(updatedNeuronIdx, k); - if (nnIndex == insertedNeuronIdx) - { - insertedNeuronIdxInNeigborList = k; - } - } - - assert(insertedNeuronIdxInNeigborList >= 0); - - Synapse* pUpdatedSynapses = getSynapses(updatedNeuronIdx); - // [N0 N1 N2 original inserted N4 N5 N6], M = 2. - // Case: neurons in range [N0 N1 N2 original], right synapses will be affected - if (delta < 0) - { - // Left side is kept as it is, only need to shift to the right side - for (long long k = numberOfNeighbors - 1; k >= insertedNeuronIdxInNeigborList; --k) - { - // Updated synapse - pUpdatedSynapses[k] = pUpdatedSynapses[k - 1]; - } - - // Incomming synapse from original neuron -> inserted neuron must be zero - if (delta == -1) - { - pUpdatedSynapses[insertedNeuronIdxInNeigborList].weight = 0; - } - } - else // Case: neurons in range [inserted N4 N5 N6], left synapses will be affected - { - // Right side is kept as it is, only need to shift to the left side - for (long long k = 0; k < insertedNeuronIdxInNeigborList; ++k) - { - // Updated synapse - pUpdatedSynapses[k] = pUpdatedSynapses[k + 1]; - } - } - - } - } - - long long getIndexInSynapsesBuffer(unsigned long long neuronIdx, long long neighborOffset) + if ((nonce[0] & 1) == 0) { - // Skip the case neuron point to itself and too far neighbor - if (neighborOffset == 0 - || neighborOffset < -(long long)numberOfNeighbors / 2 - || neighborOffset >(long long)numberOfNeighbors / 2) - { - return -1; - } - - long long synapseIdx = (long long)numberOfNeighbors / 2 + neighborOffset; - if (neighborOffset >= 0) - { - synapseIdx = synapseIdx - 1; - } - - return synapseIdx; + return computeHyperIdentityScore(publicKey, nonce); } - - // Check which neurons/synapse need to be removed after mutation - unsigned long long scanRedundantNeurons() + else { - unsigned long long population = currentANN.population; - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - - unsigned long long numberOfRedundantNeurons = 0; - // After each mutation, we must verify if there are neurons that do not affect the ANN output. - // These are neurons that either have all incoming synapse weights as 0, - // or all outgoing synapse weights as 0. Such neurons must be removed. - for (unsigned long long i = 0; i < population; i++) - { - neurons[i].markForRemoval = false; - if (neurons[i].type == Neuron::kEvolution) - { - bool allOutGoingZeros = true; - bool allIncommingZeros = true; - - // Loop though its synapses for checkout outgoing synapses - for (unsigned long long n = 0; n < numberOfNeighbors; n++) - { - char synapseW = synapses[i * numberOfNeighbors + n].weight; - if (synapseW != 0) - { - allOutGoingZeros = false; - break; - } - } - - // Loop through the neighbor neurons to check all incoming synapses - for (long long neighborOffset = -(long long)numberOfNeighbors / 2; neighborOffset <= (long long)numberOfNeighbors / 2; neighborOffset++) - { - unsigned long long nnIdx = clampNeuronIndex(i, neighborOffset); - Synapse* nnSynapses = getSynapses(nnIdx); - - long long synapseIdx = getIndexInSynapsesBuffer(nnIdx, -neighborOffset); - if (synapseIdx < 0) - { - continue; - } - char synapseW = nnSynapses[synapseIdx].weight; - - if (synapseW != 0) - { - allIncommingZeros = false; - break; - } - } - if (allOutGoingZeros || allIncommingZeros) - { - neurons[i].markForRemoval = true; - numberOfRedundantNeurons++; - } - } - } - return numberOfRedundantNeurons; - } - - // Remove neurons and synapses that do not affect the ANN - void cleanANN() - { - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - unsigned long long& population = currentANN.population; - - // Scan and remove neurons/synapses - unsigned long long neuronIdx = 0; - while (neuronIdx < population) - { - if (neurons[neuronIdx].markForRemoval) - { - // Remove it from the neuron list. Overwrite data - // Remove its synapses in the synapses array - removeNeuron(neuronIdx); - } - else - { - neuronIdx++; - } - } - } - - void processTick() - { - unsigned long long population = currentANN.population; - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - - // Memset value of current one - memset(neuronValueBuffer, 0, sizeof(neuronValueBuffer)); - - // Loop though all neurons - for (unsigned long long n = 0; n < population; ++n) - { - const Synapse* kSynapses = getSynapses(n); - long long neuronValue = neurons[n].value; - // Scan through all neighbor neurons and sum all connected neurons. - // The synapses are arranged as neuronIndex * numberOfNeighbors - for (long long m = 0; m < numberOfNeighbors; m++) - { - char synapseWeight = kSynapses[m].weight; - unsigned long long nnIndex = 0; - if (m < numberOfNeighbors / 2) - { - nnIndex = clampNeuronIndex(n + m, -(long long)numberOfNeighbors / 2); - } - else - { - nnIndex = clampNeuronIndex(n + m + 1, -(long long)numberOfNeighbors / 2); - } - - // Weight-sum - neuronValueBuffer[nnIndex] += synapseWeight * neuronValue; - } - } - - // Clamp the neuron value - for (unsigned long long n = 0; n < population; ++n) - { - char neuronValue = (char)clampNeuron(neuronValueBuffer[n]); - neurons[n].value = neuronValue; - } - } - - void runTickSimulation() - { - unsigned long long population = currentANN.population; - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - - // Save the neuron value for comparison - for (unsigned long long i = 0; i < population; ++i) - { - // Backup the neuron value - previousNeuronValue[i] = neurons[i].value; - } - - for (unsigned long long tick = 0; tick < numberOfTicks; ++tick) - { - processTick(); - // Check exit conditions: - // - N ticks have passed (already in for loop) - // - All neuron values are unchanged - // - All output neurons have non-zero values - bool shouldExit = true; - bool allNeuronsUnchanged = true; - bool allOutputNeuronsIsNonZeros = true; - for (unsigned long long n = 0; n < population; ++n) - { - // Neuron unchanged check - if (previousNeuronValue[n] != neurons[n].value) - { - allNeuronsUnchanged = false; - } - - // Ouput neuron value check - if (neurons[n].type == Neuron::kOutput && neurons[n].value == 0) - { - allOutputNeuronsIsNonZeros = false; - } - } - - if (allOutputNeuronsIsNonZeros || allNeuronsUnchanged) - { - break; - } - - // Copy the neuron value - for (unsigned long long n = 0; n < population; ++n) - { - previousNeuronValue[n] = neurons[n].value; - } - } - } - - unsigned int computeNonMatchingOutput() - { - unsigned long long population = currentANN.population; - Neuron* neurons = currentANN.neurons; - - // Compute the non-matching value R between output neuron value and initial value - // Because the output neuron order never changes, the order is preserved - unsigned int R = 0; - unsigned long long outputIdx = 0; - for (unsigned long long i = 0; i < population; i++) - { - if (neurons[i].type == Neuron::kOutput) - { - if (neurons[i].value != outputNeuronExpectedValue[outputIdx]) - { - R++; - } - outputIdx++; - } - } - return R; + return computeAdditionScore(publicKey, nonce); } - - void initInputNeuron() - { - unsigned long long population = currentANN.population; - Neuron* neurons = currentANN.neurons; - unsigned long long inputNeuronInitIndex = 0; - - char neuronArray[64] = { 0 }; - for (unsigned long long i = 0; i < population; ++i) - { - // Input will use the init value - if (neurons[i].type == Neuron::kInput) - { - // Prepare new pack - if (inputNeuronInitIndex % 64 == 0) - { - extract64Bits(miningData.inputNeuronRandomNumber[inputNeuronInitIndex / 64], neuronArray); - } - char neuronValue = neuronArray[inputNeuronInitIndex % 64]; - - // Convert value of neuron to trits (keeping 1 as 1, and changing 0 to -1.). - neurons[i].value = (neuronValue == 0) ? -1 : neuronValue; - - inputNeuronInitIndex++; - } - } - } - - void initOutputNeuron() - { - unsigned long long population = currentANN.population; - Neuron* neurons = currentANN.neurons; - for (unsigned long long i = 0; i < population; ++i) - { - if (neurons[i].type == Neuron::kOutput) - { - neurons[i].value = 0; - } - } - } - - void initExpectedOutputNeuron() - { - char neuronArray[64] = { 0 }; - for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) - { - // Prepare new pack - if (i % 64 == 0) - { - extract64Bits(miningData.outputNeuronRandomNumber[i / 64], neuronArray); - } - char neuronValue = neuronArray[i % 64]; - // Convert value of neuron (keeping 1 as 1, and changing 0 to -1.). - outputNeuronExpectedValue[i] = (neuronValue == 0) ? -1 : neuronValue; - } - } - - unsigned int initializeANN(unsigned char* publicKey, unsigned char* nonce) - { - unsigned char hash[32]; - unsigned char combined[64]; - memcpy(combined, publicKey, 32); - memcpy(combined + 32, nonce, 32); - KangarooTwelve(combined, 64, hash, 32); - - unsigned long long& population = currentANN.population; - Synapse* synapses = currentANN.synapses; - Neuron* neurons = currentANN.neurons; - - // Initialization - population = numberOfNeurons; - - // Initalize with nonce and public key - random2(hash, poolVec.data(), (unsigned char*)&initValue, sizeof(InitValue)); - - // Randomly choose the positions of neurons types - for (unsigned long long i = 0; i < population; ++i) - { - neuronIndices[i] = i; - neurons[i].type = Neuron::kInput; - } - unsigned long long neuronCount = population; - for (unsigned long long i = 0; i < numberOfOutputNeurons; ++i) - { - unsigned long long outputNeuronIdx = initValue.outputNeuronPositions[i] % neuronCount; - - // Fill the neuron type - neurons[neuronIndices[outputNeuronIdx]].type = Neuron::kOutput; - outputNeuronIndices[i] = neuronIndices[outputNeuronIdx]; - - // This index is used, copy the end of indices array to current position and decrease the number of picking neurons - neuronCount = neuronCount - 1; - neuronIndices[outputNeuronIdx] = neuronIndices[neuronCount]; - } - - // Synapse weight initialization - for (unsigned long long i = 0; i < (initNumberOfSynapses / 32); ++i) - { - const unsigned long long mask = 0b11; - - for (int j = 0; j < 32; ++j) - { - int shiftVal = j * 2; - unsigned char extractValue = (unsigned char)((initValue.synapseWeight[i] >> shiftVal) & mask); - switch (extractValue) - { - case 2: synapses[32 * i + j].weight = -1; break; - case 3: synapses[32 * i + j].weight = 1; break; - default: synapses[32 * i + j].weight = 0; - } - } - } - - // Init the neuron input and expected output value - memcpy((unsigned char*)&miningData, poolVec.data(), sizeof(miningData)); - - // Init input neuron value and output neuron - initInputNeuron(); - initOutputNeuron(); - - // Init expected output neuron - initExpectedOutputNeuron(); - - // Ticks simulation - runTickSimulation(); - - // Copy the state for rollback later - memcpy(&bestANN, ¤tANN, sizeof(ANN)); - - // Compute R and roll back if neccessary - unsigned int R = computeNonMatchingOutput(); - - return R; - } - - // Main function for mining - unsigned int computeScore(unsigned char* publicKey, unsigned char* nonce) - { - // Initialize - unsigned int bestR = initializeANN(publicKey, nonce); - - for (unsigned long long s = 0; s < numberOfMutations; ++s) - { - - // Do the mutation - mutate(nonce, (int)s); - - // Exit if the number of population reaches the maximum allowed - if (currentANN.population >= populationThreshold) - { - break; - } - - // Ticks simulation - runTickSimulation(); - - // Compute R and roll back if neccessary - unsigned int R = computeNonMatchingOutput(); - if (R > bestR) - { - // Roll back - memcpy(¤tANN, &bestANN, sizeof(bestANN)); - } - else - { - bestR = R; - - // Better R. Save the state - memcpy(&bestANN, ¤tANN, sizeof(bestANN)); - } - - assert(bestANN.population <= populationThreshold); - } - - unsigned int score = numberOfOutputNeurons - bestR; - return score; - } - }; - - ComputeInstance* _computeBuffer; - - unsigned int operator()(unsigned long long processorNumber, unsigned char* publicKey, unsigned char* nonce) - { - processorNumber %= solutionBufferCount; - return _computeBuffer[processorNumber].computeScore(publicKey, nonce); + return 0; } }; diff --git a/test/test.vcxproj b/test/test.vcxproj index 5132f62d4..04d56ccf1 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -41,7 +41,7 @@ false stdcpp20 ../src;$(MSBuildProjectDirectory);$(MSBuildProjectDirectory)\..\;%(AdditionalIncludeDirectories) - AdvancedVectorExtensions2 + AdvancedVectorExtensions512 true true @@ -114,6 +114,9 @@ + + + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 725c90fc9..6634e070c 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -55,6 +55,9 @@ + + + diff --git a/tools/score_test_generator/score_test_generator.vcxproj b/tools/score_test_generator/score_test_generator.vcxproj index 5cc475a24..eb21f3f19 100644 --- a/tools/score_test_generator/score_test_generator.vcxproj +++ b/tools/score_test_generator/score_test_generator.vcxproj @@ -57,6 +57,7 @@ true false stdcpp20 + AdvancedVectorExtensions512 Console @@ -76,6 +77,7 @@ Speed false true + AdvancedVectorExtensions512 Console diff --git a/tools/score_test_generator/test_generator.cpp b/tools/score_test_generator/test_generator.cpp index 71fa171d0..863ef583d 100644 --- a/tools/score_test_generator/test_generator.cpp +++ b/tools/score_test_generator/test_generator.cpp @@ -28,50 +28,99 @@ std::vector publicKeys; std::vector nonces; std::vector> scoreResults; std::vector> scoreProcessingTimes; +unsigned int processedSamplesCount = 0; +int gSelectedAlgorithm = 0; + +template +void writeParamsHeader(std::ostream& os, const std::string& sep = "-") +{ + // Because currently 2 params set shared the same things, incase of new algo have different params + // need to make a separate check + if constexpr (P::algoType == score_engine::AlgoType::HyperIdentity) + { + os << P::numberOfInputNeurons << sep + << P::numberOfOutputNeurons << sep + << P::numberOfTicks << sep + << P::numberOfNeighbors << sep + << P::populationThreshold << sep + << P::numberOfMutations << sep + << P::solutionThreshold; + } + else if constexpr (P::algoType == score_engine::AlgoType::Addition) + { + os << P::numberOfInputNeurons << sep + << P::numberOfOutputNeurons << sep + << P::numberOfTicks << sep + << P::numberOfNeighbors << sep + << P::populationThreshold << sep + << P::numberOfMutations << sep + << P::solutionThreshold; + } + else + { + std::cerr << "UNKNOWN ALGO !" << std::endl; + } +} + + + +template +void writeConfigs(std::ostream& oFile, std::index_sequence) +{ + constexpr std::size_t N = sizeof...(Is); + + switch (gSelectedAlgorithm) + { + case 0: + // HyperIdentity + ((writeParamsHeader::HyperIdentity>(oFile), + (Is < N - 1 ? (oFile << ", ", 0) : 0)), ...); + break; + case 1: + // Addition + ((writeParamsHeader::Addition>(oFile), + (Is < N - 1 ? (oFile << ", ", 0) : 0)), ...); + break; + default: + break; + } +} // Recursive template to process each element in scoreSettings template static void processElement(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int threadId, bool writeFile) { - score_reference::ScoreReferenceImplementation< - kSettings[i][score_params::NUMBER_OF_INPUT_NEURONS], - kSettings[i][score_params::NUMBER_OF_OUTPUT_NEURONS], - kSettings[i][score_params::NUMBER_OF_TICKS], - kSettings[i][score_params::NUMBER_OF_NEIGHBORS], - kSettings[i][score_params::POPULATION_THRESHOLD], - kSettings[i][score_params::NUMBER_OF_MUTATIONS], - kSettings[i][score_params::SOLUTION_THRESHOLD], - 1> score; + using CurrentConfig = std::tuple_element_t; + score_reference::ScoreReferenceImplementation < + typename CurrentConfig::HyperIdentity, + typename CurrentConfig::Addition + > score; + score.initMemory(); score.initMiningData(miningSeed); auto t0 = std::chrono::high_resolution_clock::now(); - unsigned int score_value = score(0, publicKey, nonce); + unsigned int score_value = 0; + + switch (gSelectedAlgorithm) + { + case 0: + score_value = score.computeHyperIdentityScore(publicKey, nonce); + break; + case 1: + score_value = score.computeAdditionScore(publicKey, nonce); + break; + default: + score_value = 0; + break; + } auto t1 = std::chrono::high_resolution_clock::now(); auto d = t1 - t0; auto elapsed = std::chrono::duration_cast(d); scoreResults[threadId][i] = score_value; scoreProcessingTimes[threadId][i] = elapsed.count(); - - // Write the result - if (writeFile) - { - std::string fileName = "score_" + std::to_string(threadId) + ".txt"; - std::ofstream output_file(fileName, std::ios::app); - if (output_file.is_open()) - { - output_file << kSettings[i][score_params::NUMBER_OF_INPUT_NEURONS] - << "-" << kSettings[i][score_params::NUMBER_OF_OUTPUT_NEURONS] - << "-" << kSettings[i][score_params::NUMBER_OF_TICKS] - << "-" << kSettings[i][score_params::NUMBER_OF_NEIGHBORS] - << "-" << kSettings[i][score_params::POPULATION_THRESHOLD] - << "-" << kSettings[i][score_params::NUMBER_OF_MUTATIONS] - << ", " << score_value << std::endl; - output_file.close(); - } - } } // Main processing function @@ -171,11 +220,11 @@ int generateSamples(std::string sampleFileName, unsigned int numberOfSamples, bo } else { - miningSeeds[i] = hexToByte(sampleString[i][0], 32); + hexToByte(sampleString[i][0], 32, miningSeeds[i].m256i_u8); } - publicKeys[i] = hexToByte(sampleString[i][1], 32); - nonces[i] = hexToByte(sampleString[i][2], 32); + hexToByte(sampleString[i][1], 32, publicKeys[i].m256i_u8); + hexToByte(sampleString[i][2], 32, nonces[i].m256i_u8); } std::cout << "Read sample file DONE " << std::endl; } @@ -215,22 +264,10 @@ void generateScore( } // Number of params settings - constexpr unsigned long long numberOfGeneratedSetting = sizeof(kSettings) / sizeof(kSettings[0]); - for (unsigned long long i = 0; i < numberOfGeneratedSetting; i++) - { - for (int j = 0; j < MAX_PARAM_TYPE; j++) - { - scoreFile << kSettings[i][j]; - if (j < MAX_PARAM_TYPE - 1) - { - scoreFile << "-"; - } - } - if (i < numberOfGeneratedSetting - 1) - { - scoreFile << ", "; - } - } + constexpr unsigned long long numberOfGeneratedSetting = CONFIG_COUNT; + + // Write the header config + writeConfigs(scoreFile, std::make_index_sequence>{}); scoreFile << std::endl; if (scoreFile.is_open()) { @@ -242,7 +279,7 @@ void generateScore( scoreResults.resize(totalSamples); scoreProcessingTimes.resize(totalSamples); - bool writeFilePerSample = true; + bool writeFilePerSample = false; #pragma omp parallel for num_threads(threadsCount) for (int i = 0; i < totalSamples; ++i) { @@ -260,12 +297,16 @@ void generateScore( process(miningSeeds[i].m256i_u8, publicKeys[i].m256i_u8, nonces[i].m256i_u8, i, writeFilePerSample); #pragma omp critical { - std::cout << "Processed sample " << i << "." << std::endl; + processedSamplesCount++; + if (processedSamplesCount % 16 == 0) + { + std::cout << "\rProcessed " << processedSamplesCount << " / " << totalSamples; + } } } // Write to a general file - std::cout << "Generate scores DONE. Collect all into a file..." << std::endl; + std::cout << "\nGenerate scores DONE. Collect all into a file..." << std::endl; scoreFile.open(outputFile, std::ios::app); if (!scoreFile.is_open()) { @@ -284,26 +325,6 @@ void generateScore( scoreFile << std::endl; } scoreFile.close(); - - // Print out the processing time in case of - for (int j = 0; j < numberOfGeneratedSetting; j++) - { - unsigned long long processingTime = 0; - for (int i = 0; i < totalSamples; i++) - { - processingTime += scoreProcessingTimes[i][j]; - } - processingTime = processingTime / totalSamples; - std::cout << "Setting " << j - << "NUMBER_OF_INPUT_NEURONS " << kSettings[j][score_params::NUMBER_OF_INPUT_NEURONS] << ", " - << "NUMBER_OF_OUTPUT_NEURONS " << kSettings[j][score_params::NUMBER_OF_OUTPUT_NEURONS] << ", " - << "NUMBER_OF_NEIGHBORS " << kSettings[j][score_params::NUMBER_OF_NEIGHBORS] << ", " - << "NUMBER_OF_TICKS " << kSettings[j][score_params::NUMBER_OF_TICKS] << ", " - << "POPULATION_THRESHOLD " << kSettings[j][score_params::POPULATION_THRESHOLD] << ", " - << "NUMBER_OF_MUTATIONS " << kSettings[j][score_params::NUMBER_OF_MUTATIONS] << ", " - << ": time " << processingTime << " ms" - << std::endl; - } } void print_random_test_case() @@ -383,6 +404,10 @@ int main(int argc, char* argv[]) { scoreFile = std::string(argv[++i]); } + else if (arg == "--algo" || arg == "-a") + { + gSelectedAlgorithm = std::atoi(argv[++i]); + } else { std::cout << "Unknown argument: " << arg << "\n"; From 5967b25748a272e1187b173b4611bfa7b35c02e4 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:56:51 +0100 Subject: [PATCH 48/90] update params for epoch 197 / v1.275.0 --- src/public_settings.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/public_settings.h b/src/public_settings.h index b1525d0bf..7c43c3ff9 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -66,12 +66,12 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // Config options that should NOT be changed by operators #define VERSION_A 1 -#define VERSION_B 274 +#define VERSION_B 275 #define VERSION_C 0 // Epoch and initial tick for node startup -#define EPOCH 196 -#define TICK 42232000 +#define EPOCH 197 +#define TICK 42702000 #define TICK_IS_FIRST_TICK_OF_EPOCH 1 // Set to 0 if the network is restarted during the EPOCH with a new initial TICK #define ARBITRATOR "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" @@ -140,3 +140,4 @@ static unsigned int gFullExternalComputationTimes[][2] = // Use values like (numerator 1, denominator 10) for division by 10. GLOBAL_VAR_DECL unsigned long long executionTimeMultiplierNumerator GLOBAL_VAR_INIT(1ULL); GLOBAL_VAR_DECL unsigned long long executionTimeMultiplierDenominator GLOBAL_VAR_INIT(1ULL); + From d2cd5542a26f11e9dc277400150bd1453f55bb8c Mon Sep 17 00:00:00 2001 From: TakaYuPP Date: Mon, 19 Jan 2026 07:20:37 -0800 Subject: [PATCH 49/90] QBay fix: qbay gtest bug (#719) --- test/contract_qbay.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/contract_qbay.cpp b/test/contract_qbay.cpp index 1f0f256dc..213cfc121 100644 --- a/test/contract_qbay.cpp +++ b/test/contract_qbay.cpp @@ -1105,7 +1105,11 @@ TEST(TestContractQBAY, testingAllProceduresAndFunctions) EXPECT_EQ(getBalance(id(QBAY_CONTRACT_INDEX, 0, 0, 0)), earnedQubic + collectedShareHolderFee); // createTraditionalAuction - updateTime(); + setMemory(utcTime, 0); + utcTime.Year = 2025; + utcTime.Month = 12; + utcTime.Day = 31; + utcTime.Hour = 0; updateQpiTime(); pfp.createTraditionalAuction(users[4], 10000000, 0, 0, 26, 1, 1, 0, 26, 1, 5, 0); pfp.getState()->createAuctionChecker(0, 10000000, 1, 0, users[4], 26, 1, 1, 0, 26, 1, 5, 0); From bd18b89046cdf45af9899e91b1a9007e7cbe8854 Mon Sep 17 00:00:00 2001 From: cyber-pc <165458555+cyber-pc@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:16:22 +0700 Subject: [PATCH 50/90] Update ADDITION_SOLUTION_THRESHOLD_DEFAULT. --- src/public_settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public_settings.h b/src/public_settings.h index 7c43c3ff9..834084796 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -102,7 +102,7 @@ static constexpr unsigned long long ADDITION_NUMBER_OF_TICKS = 1000; static constexpr unsigned long long ADDITION_NUMBER_OF_NEIGHBORS = 728; // 2M. Must be divided by 2 static constexpr unsigned long long ADDITION_NUMBER_OF_MUTATIONS = 150; static constexpr unsigned long long ADDITION_POPULATION_THRESHOLD = ADDITION_NUMBER_OF_INPUT_NEURONS + ADDITION_NUMBER_OF_OUTPUT_NEURONS + ADDITION_NUMBER_OF_MUTATIONS; // P -static constexpr unsigned int ADDITION_SOLUTION_THRESHOLD_DEFAULT = 87381; // 8 * ( 2^14 ) * 2 / 3 +static constexpr unsigned int ADDITION_SOLUTION_THRESHOLD_DEFAULT = 74200; static constexpr long long NEURON_VALUE_LIMIT = 1LL; From d769317a698a59b8227f5b8c6c4878f20ed214dc Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Wed, 21 Jan 2026 08:57:04 +0100 Subject: [PATCH 51/90] update new initial tick --- src/public_settings.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/public_settings.h b/src/public_settings.h index 834084796..2b100542b 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -71,7 +71,7 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // Epoch and initial tick for node startup #define EPOCH 197 -#define TICK 42702000 +#define TICK 42715000 #define TICK_IS_FIRST_TICK_OF_EPOCH 1 // Set to 0 if the network is restarted during the EPOCH with a new initial TICK #define ARBITRATOR "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" @@ -141,3 +141,4 @@ static unsigned int gFullExternalComputationTimes[][2] = GLOBAL_VAR_DECL unsigned long long executionTimeMultiplierNumerator GLOBAL_VAR_INIT(1ULL); GLOBAL_VAR_DECL unsigned long long executionTimeMultiplierDenominator GLOBAL_VAR_INIT(1ULL); + From e89a2f5747c6b4aa6837257193b30543f1eac4e8 Mon Sep 17 00:00:00 2001 From: mundusakhan Date: Fri, 23 Jan 2026 04:44:51 -0500 Subject: [PATCH 52/90] qRWA: Fix dividend routing (#722) --- src/contracts/qRWA.h | 8 ++-- test/contract_qrwa.cpp | 102 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/contracts/qRWA.h b/src/contracts/qRWA.h index 0d41020b1..59e892f68 100644 --- a/src/contracts/qRWA.h +++ b/src/contracts/qRWA.h @@ -1955,9 +1955,11 @@ struct QRWA : public ContractBase POST_INCOMING_TRANSFER_WITH_LOCALS() { // Differentiate revenue streams based on source type - if (input.sourceId.u64._1 == 0 && input.sourceId.u64._2 == 0 && input.sourceId.u64._3 == 0 && input.sourceId.u64._0 != 0) + // Only deposit to Pool A if source is QUTIL + // All other transfers (users or other contracts) go to Pool B + if (input.sourceId == id(QUTIL_CONTRACT_INDEX, 0, 0, 0)) { - // Source is likely a contract (e.g., QX transfer) -> Pool A + // Source is explicitly QUTIL -> Pool A state.mRevenuePoolA = sadd(state.mRevenuePoolA, static_cast(input.amount)); locals.logger.contractId = CONTRACT_INDEX; locals.logger.logType = QRWA_LOG_TYPE_INCOMING_REVENUE_A; @@ -1968,7 +1970,7 @@ struct QRWA : public ContractBase } else if (input.sourceId != NULL_ID) { - // Source is likely a user (EOA) -> Pool B + // Source is NOT QUTIL (User or other Contract) -> Pool B state.mRevenuePoolB = sadd(state.mRevenuePoolB, static_cast(input.amount)); locals.logger.contractId = CONTRACT_INDEX; locals.logger.logType = QRWA_LOG_TYPE_INCOMING_REVENUE_B; diff --git a/test/contract_qrwa.cpp b/test/contract_qrwa.cpp index 374075951..07cfe379c 100644 --- a/test/contract_qrwa.cpp +++ b/test/contract_qrwa.cpp @@ -283,8 +283,110 @@ class ContractTestingQRWA : protected ContractTesting return output; } + void issueContractSharesHelper(unsigned int contractIndex, std::vector>& shares) + { + issueContractShares(contractIndex, shares); + } + + void createQswapPool(const id& source, const id& assetIssuer, uint64 assetName, sint64 fee) + { + QSWAP::CreatePool_input input{ assetIssuer, assetName }; + QSWAP::CreatePool_output output; + invokeUserProcedure(QSWAP_CONTRACT_INDEX, 3, input, output, source, fee); + } + + void getQswapFees(QSWAP::Fees_output& output) + { + QSWAP::Fees_input input; + callFunction(QSWAP_CONTRACT_INDEX, 1, input, output); + } + + void runQswapEndTick() + { + callSystemProcedure(QSWAP_CONTRACT_INDEX, END_TICK); + } + }; +TEST(ContractQRWA, QswapDividend_PoolB) +{ + ContractTestingQRWA qrwa; + + // Create QRWA Shareholders + const id QRWA_SH1 = id::randomValue(); + increaseEnergy(QRWA_SH1, 100000000); + + std::vector> qrwaShares{ + {QRWA_SH1, NUMBER_OF_COMPUTORS} + }; + + qrwa.issueContractSharesHelper(QRWA_CONTRACT_INDEX, qrwaShares); + + //create QMINE Shareholders + const id QMINE_HOLDER = id::randomValue(); + increaseEnergy(QMINE_HOLDER, 100000000); + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000); + qrwa.transferAsset(QMINE_ISSUER, QMINE_HOLDER, QMINE_ASSET, 1000000); + + + // Create QSWAP Shares and deposit them to QRWA + // QRWA owns 100 shares. Random holder owns the rest (576) + const id QSWAP_OTHER_HOLDER = id::randomValue(); + std::vector> qswapShares{ + {id(QRWA_CONTRACT_INDEX, 0, 0, 0), 100}, + {QSWAP_OTHER_HOLDER, 576} + }; + qrwa.issueContractSharesHelper(QSWAP_CONTRACT_INDEX, qswapShares); + + // now generate Revenue in QSWAP + + const id TRADER = id::randomValue(); + const id ASSET_ISSUER = id::randomValue(); + const uint64 ASSET_NAME = assetNameFromString("TSTCOIN"); + + increaseEnergy(TRADER, 10000000000); + increaseEnergy(ASSET_ISSUER, 10000000000); + + QSWAP::Fees_output qswapFees; + qrwa.getQswapFees(qswapFees); + + // issue asset on QX + qrwa.issueAsset(ASSET_ISSUER, ASSET_NAME, 1000000000); + + // Create Pool on QSWAP + // This generates 'poolCreationFee' for QSWAP shareholders, generating substantial revenue + qrwa.createQswapPool(ASSET_ISSUER, ASSET_ISSUER, ASSET_NAME, qswapFees.poolCreationFee); + + // We skip AddLiquidity/Swap expectations as the pool creation fee + // alone is sufficient to test dividend routing + + uint64 totalShareholderRevenue = qswapFees.poolCreationFee; + + // Dividend Distribution + + // Get QRWA dividend balances BEFORE + auto qrwaDivsBefore = qrwa.getDividendBalances(); + EXPECT_EQ(qrwaDivsBefore.revenuePoolA, 0); + EXPECT_EQ(qrwaDivsBefore.revenuePoolB, 0); + + // Run END_TICK for QSWAP to distribute dividends + qrwa.runQswapEndTick(); + + // Calculate expected dividend for QRWA (100 shares) + // (TotalRevenue / 676) * 100 + uint64 expectedDividend = totalShareholderRevenue / NUMBER_OF_COMPUTORS * 100; + + // Get QRWA Dividend Balances AFTER + auto qrwaDivsAfter = qrwa.getDividendBalances(); + + // Verify Dividend Routing + // Pool A should be 0 (Only QUTIL transfers go here) + EXPECT_EQ(qrwaDivsAfter.revenuePoolA, 0); + + // Pool B should contain the dividend from QSWAP + EXPECT_EQ(qrwaDivsAfter.revenuePoolB, expectedDividend); +} + TEST(ContractQRWA, Initialization) { From 4c1b8209235baba979257051f39e5d3b84d9e1f9 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 26 Jan 2026 18:14:08 +0300 Subject: [PATCH 53/90] IPO QDuel (#705) * QDuel * transfer dividends to RL shareholders * Remove magic numbers * Move test file * Changes public to protected * Adds lock mechanism * Deposit * Return qus after end epoch * Adds check state in tick * Transferring rooms and players to the next epoch * Create room in tick * Transferring rooms and players to the next epoch * Base tests * Adds tests * Fixes * Add new test * Adds cleanup for HashMaps * Update constructionEpoch --- src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 3 + src/contract_core/contract_def.h | 12 + src/contracts/QDuel.h | 1089 +++++++++++++++++++++++++++++ src/contracts/RandomLottery.h | 6 +- test/contract_qduel.cpp | 1122 ++++++++++++++++++++++++++++++ test/test.vcxproj | 3 +- test/test.vcxproj.filters | 1 + 8 files changed, 2233 insertions(+), 4 deletions(-) create mode 100644 src/contracts/QDuel.h create mode 100644 test/contract_qduel.cpp diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 2128d5fa1..2bbe06053 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -27,6 +27,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index edc604737..1a182d796 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -303,6 +303,9 @@ contracts + + contracts + contract_core diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 65139e477..a34b81086 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -211,6 +211,16 @@ #define CONTRACT_STATE2_TYPE QRWA2 #include "contracts/qRWA.h" +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QDUEL_CONTRACT_INDEX 21 +#define CONTRACT_INDEX QDUEL_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QDUEL +#define CONTRACT_STATE2_TYPE QDUEL2 +#include "contracts/QDuel.h" + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -319,6 +329,7 @@ constexpr struct ContractDescription {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 + {"QDUEL", 199, 10000, sizeof(QDUEL)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA)}, @@ -435,6 +446,7 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRWA); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/contracts/QDuel.h b/src/contracts/QDuel.h new file mode 100644 index 000000000..44f20da99 --- /dev/null +++ b/src/contracts/QDuel.h @@ -0,0 +1,1089 @@ +using namespace QPI; + +constexpr uint32 QDUEL_MAX_NUMBER_OF_ROOMS = 512; +constexpr uint64 QDUEL_MINIMUM_DUEL_AMOUNT = 10000; +constexpr uint8 QDUEL_DEV_FEE_PERCENT_BPS = 15; // 0.15% * QDUEL_PERCENT_SCALE +constexpr uint8 QDUEL_BURN_FEE_PERCENT_BPS = 30; // 0.3% * QDUEL_PERCENT_SCALE +constexpr uint8 QDUEL_SHAREHOLDERS_FEE_PERCENT_BPS = 55; // 0.55% * QDUEL_PERCENT_SCALE +constexpr uint8 QDUEL_PERCENT_SCALE = 1000; +constexpr uint8 QDUEL_TTL_HOURS = 3; +constexpr uint8 QDUEL_TICK_UPDATE_PERIOD = 100; // Process TICK logic once per this many ticks +constexpr uint64 QDUEL_RANDOM_LOTTERY_ASSET_NAME = 19538; // RL +constexpr uint64 QDUEL_ROOMS_REMOVAL_THRESHOLD_PERCENT = 75; + +struct QDUEL2 +{ +}; + +struct QDUEL : public ContractBase +{ +public: + enum class EState : uint8 + { + NONE = 0, + WAIT_TIME = 1 << 0, + + LOCKED = WAIT_TIME + }; + + friend EState operator|(const EState& a, const EState& b) { return static_cast(static_cast(a) | static_cast(b)); } + friend EState operator&(const EState& a, const EState& b) { return static_cast(static_cast(a) & static_cast(b)); } + friend EState operator~(const EState& a) { return static_cast(~static_cast(a)); } + template friend bool operator==(const EState& a, const T& b) { return static_cast(a) == b; } + template friend bool operator!=(const EState& a, const T& b) { return !(a == b); } + + static EState removeStateFlag(EState state, EState flag) { return state & ~flag; } + static EState addStateFlag(EState state, EState flag) { return state | flag; } + + enum class EReturnCode : uint8 + { + SUCCESS, + ACCESS_DENIED, + INVALID_VALUE, + USER_ALREADY_EXISTS, + USER_NOT_FOUND, + INSUFFICIENT_FREE_DEPOSIT, + + // Room + ROOM_INSUFFICIENT_DUEL_AMOUNT, + ROOM_NOT_FOUND, + ROOM_FULL, + ROOM_FAILED_CREATE, + ROOM_FAILED_GET_WINNER, + ROOM_ACCESS_DENIED, + ROOM_FAILED_CALCULATE_REVENUE, + + STATE_LOCKED, + + UNKNOWN_ERROR = UINT8_MAX + }; + + static constexpr uint8 toReturnCode(EReturnCode code) { return static_cast(code); } + + struct RoomInfo + { + id roomId; + id owner; + id allowedPlayer; // If zero, anyone can join + uint64 amount; + uint64 closeTimer; + DateAndTime lastUpdate; + }; + + struct UserData + { + id userId; + id roomId; + id allowedPlayer; + uint64 depositedAmount; + uint64 locked; + uint64 stake; + uint64 raiseStep; + uint64 maxStake; + }; + + struct AddUserData_input + { + id userId; + id roomId; + id allowedPlayer; + uint64 depositedAmount; + uint64 stake; + uint64 raiseStep; + uint64 maxStake; + }; + + struct AddUserData_output + { + uint8 returnCode; + }; + + struct AddUserData_locals + { + UserData newUserData; + }; + + struct CreateRoom_input + { + id allowedPlayer; // If zero, anyone can join + uint64 stake; + uint64 raiseStep; + uint64 maxStake; + }; + + struct CreateRoom_output + { + uint8 returnCode; + }; + + struct CreateRoomRecord_input + { + id owner; + id allowedPlayer; + uint64 amount; + }; + + struct CreateRoomRecord_output + { + id roomId; + uint8 returnCode; + }; + + struct CreateRoomRecord_locals + { + RoomInfo newRoom; + id roomId; + uint64 attempt; + }; + + struct ComputeNextStake_input + { + uint64 stake; + uint64 raiseStep; + uint64 maxStake; + }; + + struct ComputeNextStake_output + { + uint64 nextStake; + uint8 returnCode; + }; + + struct CreateRoom_locals + { + AddUserData_input addUserInput; + AddUserData_output addUserOutput; + CreateRoomRecord_input createRoomInput; + CreateRoomRecord_output createRoomOutput; + }; + + struct GetWinnerPlayer_input + { + id player1; + id player2; + }; + + struct GetWinnerPlayer_output + { + id winner; + }; + + struct GetWinnerPlayer_locals + { + m256i randomValue; + m256i minPlayerId; + m256i maxPlayerId; + }; + + struct CalculateRevenue_input + { + uint64 amount; + }; + + struct CalculateRevenue_output + { + uint64 devFee; + uint64 burnFee; + uint64 shareholdersFee; + uint64 winner; + }; + + struct TransferToShareholders_input + { + uint64 amount; + }; + + struct TransferToShareholders_output + { + uint64 remainder; + }; + + struct TransferToShareholders_locals + { + Entity entity; + uint64 shareholdersCount; + uint64 perShareholderAmount; + uint64 remainder; + sint64 index; + Asset rlAsset; + uint64 dividendPerShare; + AssetPossessionIterator rlIter; + uint64 rlShares; + uint64 transferredAmount; + sint64 toTransfer; + }; + + struct FinalizeRoom_input + { + id roomId; + id owner; + uint64 roomAmount; + bit includeLocked; + }; + + struct FinalizeRoom_output + { + uint8 returnCode; + }; + + struct FinalizeRoom_locals + { + UserData userData; + uint64 availableDeposit; + CreateRoomRecord_input createRoomInput; + CreateRoomRecord_output createRoomOutput; + ComputeNextStake_input nextStakeInput; + ComputeNextStake_output nextStakeOutput; + }; + + struct ConnectToRoom_input + { + id roomId; + }; + + struct ConnectToRoom_output + { + uint8 returnCode; + }; + + struct ConnectToRoom_locals + { + RoomInfo room; + GetWinnerPlayer_input getWinnerPlayer_input; + GetWinnerPlayer_output getWinnerPlayer_output; + CalculateRevenue_input calculateRevenue_input; + CalculateRevenue_output calculateRevenue_output; + TransferToShareholders_input transferToShareholders_input; + TransferToShareholders_output transferToShareholders_output; + FinalizeRoom_input finalizeInput; + FinalizeRoom_output finalizeOutput; + id winner; + uint64 returnAmount; + uint64 amount; + bit failedGetWinner; + }; + + struct GetPercentFees_input + { + }; + + struct GetPercentFees_output + { + uint8 devFeePercentBps; + uint8 burnFeePercentBps; + uint8 shareholdersFeePercentBps; + uint8 percentScale; + uint64 returnCode; + }; + + struct SetPercentFees_input + { + uint8 devFeePercentBps; + uint8 burnFeePercentBps; + uint8 shareholdersFeePercentBps; + }; + + struct SetPercentFees_output + { + uint8 returnCode; + }; + + struct SetPercentFees_locals + { + uint16 totalPercent; + }; + + struct GetRooms_input + { + }; + + struct GetRooms_output + { + Array rooms; + + uint8 returnCode; + }; + + struct GetRooms_locals + { + sint64 hashSetIndex; + uint64 arrayIndex; + }; + + struct SetTTLHours_input + { + uint8 ttlHours; + }; + + struct SetTTLHours_output + { + uint8 returnCode; + }; + + struct GetTTLHours_input + { + }; + + struct GetTTLHours_output + { + uint8 ttlHours; + uint8 returnCode; + }; + + struct GetUserProfile_input + { + }; + + struct GetUserProfile_output + { + id roomId; + uint64 depositedAmount; + uint64 locked; + uint64 stake; + uint64 raiseStep; + uint64 maxStake; + uint8 returnCode; + }; + + struct GetUserProfile_locals + { + UserData userData; + }; + + struct Deposit_input + { + }; + + struct Deposit_output + { + uint8 returnCode; + }; + + struct Deposit_locals + { + UserData userData; + }; + + struct Withdraw_input + { + uint64 amount; + }; + + struct Withdraw_output + { + uint8 returnCode; + }; + + struct Withdraw_locals + { + UserData userData; + uint64 freeAmount; + }; + + struct END_TICK_locals + { + UserData userData; + RoomInfo room; + DateAndTime now; + sint64 roomIndex; + uint32 currentTimestamp; + uint64 elapsedSeconds; + FinalizeRoom_input finalizeInput; + FinalizeRoom_output finalizeOutput; + }; + +public: + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(CreateRoom, 1); + REGISTER_USER_PROCEDURE(ConnectToRoom, 2); + REGISTER_USER_PROCEDURE(SetPercentFees, 3); + REGISTER_USER_PROCEDURE(SetTTLHours, 4); + REGISTER_USER_PROCEDURE(Deposit, 5); + REGISTER_USER_PROCEDURE(Withdraw, 6); + + REGISTER_USER_FUNCTION(GetPercentFees, 1); + REGISTER_USER_FUNCTION(GetRooms, 2); + REGISTER_USER_FUNCTION(GetTTLHours, 3); + REGISTER_USER_FUNCTION(GetUserProfile, 4); + } + + INITIALIZE() + { + state.teamAddress = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, + _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + + state.minimumDuelAmount = QDUEL_MINIMUM_DUEL_AMOUNT; + + // Fee + state.devFeePercentBps = QDUEL_DEV_FEE_PERCENT_BPS; + state.burnFeePercentBps = QDUEL_BURN_FEE_PERCENT_BPS; + state.shareholdersFeePercentBps = QDUEL_SHAREHOLDERS_FEE_PERCENT_BPS; + + state.ttlHours = QDUEL_TTL_HOURS; + } + + BEGIN_EPOCH() + { + state.firstTick = true; + state.currentState = EState::LOCKED; + } + + END_EPOCH() + { + state.rooms.cleanup(); + state.users.cleanup(); + } + + END_TICK_WITH_LOCALS() + { + if (mod(qpi.tick(), QDUEL_TICK_UPDATE_PERIOD) != 0) + { + return; + } + + if ((state.currentState & EState::WAIT_TIME) != EState::NONE) + { + RL::makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentTimestamp); + if (RL_DEFAULT_INIT_TIME < locals.currentTimestamp) + { + state.currentState = removeStateFlag(state.currentState, EState::WAIT_TIME); + } + } + + if ((state.currentState & EState::LOCKED) != EState::NONE) + { + return; + } + + locals.roomIndex = state.rooms.nextElementIndex(NULL_INDEX); + while (locals.roomIndex != NULL_INDEX) + { + locals.room = state.rooms.value(locals.roomIndex); + locals.now = qpi.now(); + + /** + * The interval between the end of the epoch and the first valid tick can be large. + * To do this, we restore the time before the room was closed. + */ + if (state.firstTick) + { + locals.room.lastUpdate = locals.now; + } + + locals.elapsedSeconds = div(locals.room.lastUpdate.durationMicrosec(locals.now), 1000000ULL); + if (locals.elapsedSeconds >= locals.room.closeTimer) + { + locals.finalizeInput.roomId = locals.room.roomId; + locals.finalizeInput.owner = locals.room.owner; + locals.finalizeInput.roomAmount = locals.room.amount; + locals.finalizeInput.includeLocked = true; + CALL(FinalizeRoom, locals.finalizeInput, locals.finalizeOutput); + } + else + { + locals.room.closeTimer = usubSatu64(locals.room.closeTimer, locals.elapsedSeconds); + locals.room.lastUpdate = locals.now; + state.rooms.set(locals.room.roomId, locals.room); + } + + locals.roomIndex = state.rooms.nextElementIndex(locals.roomIndex); + } + + state.firstTick = false; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(CreateRoom) + { + if ((state.currentState & EState::LOCKED) != EState::NONE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::STATE_LOCKED); + return; + } + + if (qpi.invocationReward() < state.minimumDuelAmount) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::ROOM_INSUFFICIENT_DUEL_AMOUNT); // insufficient duel amount + return; + } + + if (input.stake < state.minimumDuelAmount || (input.maxStake > 0 && input.maxStake < input.stake)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + return; + } + + if (qpi.invocationReward() < input.stake) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::ROOM_INSUFFICIENT_DUEL_AMOUNT); + return; + } + + if (state.users.contains(qpi.invocator())) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::USER_ALREADY_EXISTS); + return; + } + + locals.createRoomInput.owner = qpi.invocator(); + locals.createRoomInput.allowedPlayer = input.allowedPlayer; + locals.createRoomInput.amount = input.stake; + CALL(CreateRoomRecord, locals.createRoomInput, locals.createRoomOutput); + if (locals.createRoomOutput.returnCode != toReturnCode(EReturnCode::SUCCESS)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.returnCode = locals.createRoomOutput.returnCode; + return; + } + + locals.addUserInput.userId = qpi.invocator(); + locals.addUserInput.roomId = locals.createRoomOutput.roomId; + locals.addUserInput.allowedPlayer = input.allowedPlayer; + if (qpi.invocationReward() > input.stake) + { + locals.addUserInput.depositedAmount = qpi.invocationReward() - input.stake; + } + else + { + locals.addUserInput.depositedAmount = 0; + } + locals.addUserInput.stake = input.stake; + locals.addUserInput.raiseStep = input.raiseStep; + locals.addUserInput.maxStake = input.maxStake; + + CALL(AddUserData, locals.addUserInput, locals.addUserOutput); + if (locals.addUserOutput.returnCode != toReturnCode(EReturnCode::SUCCESS)) + { + state.rooms.removeByKey(locals.createRoomOutput.roomId); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = locals.addUserOutput.returnCode; + return; + } + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(ConnectToRoom) + { + if ((state.currentState & EState::LOCKED) != EState::NONE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::STATE_LOCKED); + return; + } + + if (!state.rooms.get(input.roomId, locals.room)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::ROOM_NOT_FOUND); + + return; + } + + if (locals.room.allowedPlayer != NULL_ID) + { + if (locals.room.allowedPlayer != qpi.invocator()) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::ROOM_ACCESS_DENIED); + return; + } + } + + if (qpi.invocationReward() < locals.room.amount) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + output.returnCode = toReturnCode(EReturnCode::ROOM_INSUFFICIENT_DUEL_AMOUNT); + return; + } + + if (qpi.invocationReward() > locals.room.amount) + { + locals.returnAmount = qpi.invocationReward() - locals.room.amount; + qpi.transfer(qpi.invocator(), locals.returnAmount); + } + + locals.amount = (qpi.invocationReward() - locals.returnAmount) + locals.room.amount; + + locals.getWinnerPlayer_input.player1 = locals.room.owner; + locals.getWinnerPlayer_input.player2 = qpi.invocator(); + + CALL(GetWinnerPlayer, locals.getWinnerPlayer_input, locals.getWinnerPlayer_output); + locals.winner = locals.getWinnerPlayer_output.winner; + + if (locals.winner == id::zero() || + (locals.winner != locals.getWinnerPlayer_input.player1 && locals.winner != locals.getWinnerPlayer_input.player2)) + { + // Return fund to player1 + qpi.transfer(locals.getWinnerPlayer_input.player1, locals.room.amount); + // Return fund to player2 + qpi.transfer(locals.getWinnerPlayer_input.player2, locals.room.amount); + + state.rooms.removeByKey(input.roomId); + locals.amount = 0; + locals.failedGetWinner = true; + locals.room.amount = 0; + } + + if (locals.amount > 0) + { + locals.calculateRevenue_input.amount = locals.amount; + CALL(CalculateRevenue, locals.calculateRevenue_input, locals.calculateRevenue_output); + } + + if (locals.calculateRevenue_output.winner > 0) + { + qpi.transfer(locals.winner, locals.calculateRevenue_output.winner); + } + else if (!locals.failedGetWinner) + { + // Return fund to player1 + qpi.transfer(locals.getWinnerPlayer_input.player1, locals.room.amount); + // Return fund to player2 + qpi.transfer(locals.getWinnerPlayer_input.player2, locals.room.amount); + + state.rooms.removeByKey(input.roomId); + } + + if (locals.calculateRevenue_output.devFee > 0) + { + qpi.transfer(state.teamAddress, locals.calculateRevenue_output.devFee); + } + if (locals.calculateRevenue_output.burnFee > 0) + { + qpi.burn(locals.calculateRevenue_output.burnFee); + } + if (locals.calculateRevenue_output.shareholdersFee > 0) + { + locals.transferToShareholders_input.amount = locals.calculateRevenue_output.shareholdersFee; + + CALL(TransferToShareholders, locals.transferToShareholders_input, locals.transferToShareholders_output); + + if (locals.transferToShareholders_output.remainder > 0) + { + qpi.burn(locals.transferToShareholders_output.remainder); + } + } + + locals.finalizeInput.roomId = input.roomId; + locals.finalizeInput.owner = locals.room.owner; + locals.finalizeInput.roomAmount = 0; + locals.finalizeInput.includeLocked = false; + CALL(FinalizeRoom, locals.finalizeInput, locals.finalizeOutput); + output.returnCode = locals.finalizeOutput.returnCode; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(SetPercentFees) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.teamAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + locals.totalPercent = static_cast(input.devFeePercentBps) + static_cast(input.burnFeePercentBps) + + static_cast(input.shareholdersFeePercentBps); + locals.totalPercent = div(locals.totalPercent, static_cast(QDUEL_PERCENT_SCALE)); + + if (locals.totalPercent >= 100) + { + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + return; + } + + state.devFeePercentBps = input.devFeePercentBps; + state.burnFeePercentBps = input.burnFeePercentBps; + state.shareholdersFeePercentBps = input.shareholdersFeePercentBps; + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE(SetTTLHours) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.teamAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + if (input.ttlHours == 0) + { + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + return; + } + + state.ttlHours = input.ttlHours; + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_FUNCTION(GetPercentFees) + { + output.devFeePercentBps = state.devFeePercentBps; + output.burnFeePercentBps = state.burnFeePercentBps; + output.shareholdersFeePercentBps = state.shareholdersFeePercentBps; + output.percentScale = QDUEL_PERCENT_SCALE; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetRooms) + { + locals.hashSetIndex = state.rooms.nextElementIndex(NULL_INDEX); + while (locals.hashSetIndex != NULL_INDEX) + { + output.rooms.set(locals.arrayIndex++, state.rooms.value(locals.hashSetIndex)); + + locals.hashSetIndex = state.rooms.nextElementIndex(locals.hashSetIndex); + } + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_FUNCTION(GetTTLHours) + { + output.ttlHours = state.ttlHours; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetUserProfile) + { + if (!state.users.get(qpi.invocator(), locals.userData)) + { + output.returnCode = toReturnCode(EReturnCode::USER_NOT_FOUND); + return; + } + + output.roomId = locals.userData.roomId; + output.depositedAmount = locals.userData.depositedAmount; + output.locked = locals.userData.locked; + output.stake = locals.userData.stake; + output.raiseStep = locals.userData.raiseStep; + output.maxStake = locals.userData.maxStake; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(Deposit) + { + if (qpi.invocationReward() == 0) + { + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + return; + } + + if (!state.users.get(qpi.invocator(), locals.userData)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.returnCode = toReturnCode(EReturnCode::USER_NOT_FOUND); + return; + } + + locals.userData.depositedAmount += qpi.invocationReward(); + state.users.set(locals.userData.userId, locals.userData); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(Withdraw) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (!state.users.get(qpi.invocator(), locals.userData)) + { + output.returnCode = toReturnCode(EReturnCode::USER_NOT_FOUND); + return; + } + + locals.freeAmount = locals.userData.depositedAmount; + + if (input.amount == 0 || input.amount > locals.freeAmount) + { + output.returnCode = toReturnCode(EReturnCode::INSUFFICIENT_FREE_DEPOSIT); + return; + } + + locals.userData.depositedAmount -= input.amount; + state.users.set(locals.userData.userId, locals.userData); + qpi.transfer(qpi.invocator(), input.amount); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + +protected: + HashMap rooms; + HashMap users; + id teamAddress; + uint64 minimumDuelAmount; + uint8 devFeePercentBps; + uint8 burnFeePercentBps; + uint8 shareholdersFeePercentBps; + uint8 ttlHours; + uint8 firstTick; + EState currentState; + +protected: + template static constexpr const T& min(const T& a, const T& b) { return (a < b) ? a : b; } + template static constexpr const T& max(const T& a, const T& b) { return (a > b) ? a : b; } + static constexpr const m256i& max(const m256i& a, const m256i& b) { return (a < b) ? b : a; } + + static void computeNextStake(const ComputeNextStake_input& input, ComputeNextStake_output& output) + { + output.nextStake = input.stake; + + if (input.raiseStep > 1) + { + if (input.maxStake > 0 && input.stake > 0 && input.raiseStep > div(input.maxStake, input.stake)) + { + output.nextStake = input.maxStake; + } + else + { + output.nextStake = smul(input.stake, input.raiseStep); + } + } + + if (input.maxStake > 0 && output.nextStake > input.maxStake) + { + output.nextStake = input.maxStake; + } + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + static uint64_t usubSatu64(uint64 a, uint64 b) { return (a < b) ? 0 : (a - b); } + +private: + PRIVATE_PROCEDURE_WITH_LOCALS(CreateRoomRecord) + { + if (state.rooms.population() >= state.rooms.capacity()) + { + output.returnCode = toReturnCode(EReturnCode::ROOM_FULL); + output.roomId = id::zero(); + return; + } + + locals.attempt = 0; + while (locals.attempt < 8) + { + locals.roomId = qpi.K12(m256i(qpi.tick() ^ state.rooms.population() ^ input.owner.u64._0 ^ locals.attempt, + input.owner.u64._1 ^ input.allowedPlayer.u64._0 ^ (locals.attempt << 1), + input.owner.u64._2 ^ input.allowedPlayer.u64._1 ^ (locals.attempt << 2), + input.owner.u64._3 ^ input.amount ^ (locals.attempt << 3))); + if (!state.rooms.contains(locals.roomId)) + { + break; + } + ++locals.attempt; + } + if (locals.attempt >= 8) + { + output.returnCode = toReturnCode(EReturnCode::ROOM_FAILED_CREATE); + output.roomId = id::zero(); + return; + } + + locals.newRoom.roomId = locals.roomId; + locals.newRoom.owner = input.owner; + locals.newRoom.allowedPlayer = input.allowedPlayer; + locals.newRoom.amount = input.amount; + locals.newRoom.closeTimer = static_cast(state.ttlHours) * 3600U; + locals.newRoom.lastUpdate = qpi.now(); + + if (state.rooms.set(locals.roomId, locals.newRoom) == NULL_INDEX) + { + output.returnCode = toReturnCode(EReturnCode::ROOM_FAILED_CREATE); + output.roomId = id::zero(); + return; + } + + output.roomId = locals.roomId; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeRoom) + { + state.rooms.removeByKey(input.roomId); + + if (!state.users.get(input.owner, locals.userData)) + { + if (input.roomAmount > 0) + { + qpi.transfer(input.owner, input.roomAmount); + } + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + return; + } + + locals.availableDeposit = locals.userData.depositedAmount; + if (input.includeLocked) + { + locals.availableDeposit += locals.userData.locked; + } + + if (locals.availableDeposit == 0) + { + state.users.removeByKey(locals.userData.userId); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + return; + } + + locals.nextStakeInput.stake = locals.userData.stake; + locals.nextStakeInput.raiseStep = locals.userData.raiseStep; + locals.nextStakeInput.maxStake = locals.userData.maxStake; + computeNextStake(locals.nextStakeInput, locals.nextStakeOutput); + if (locals.nextStakeOutput.returnCode != toReturnCode(EReturnCode::SUCCESS)) + { + qpi.transfer(locals.userData.userId, locals.availableDeposit); + state.users.removeByKey(locals.userData.userId); + output.returnCode = locals.nextStakeOutput.returnCode; + return; + } + + if (locals.nextStakeOutput.nextStake > locals.availableDeposit) + { + locals.nextStakeOutput.nextStake = locals.availableDeposit; + } + + if (locals.nextStakeOutput.nextStake < state.minimumDuelAmount) + { + qpi.transfer(locals.userData.userId, locals.availableDeposit); + state.users.removeByKey(locals.userData.userId); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + return; + } + + locals.createRoomInput.owner = locals.userData.userId; + locals.createRoomInput.allowedPlayer = locals.userData.allowedPlayer; + locals.createRoomInput.amount = locals.nextStakeOutput.nextStake; + CALL(CreateRoomRecord, locals.createRoomInput, locals.createRoomOutput); + if (locals.createRoomOutput.returnCode != toReturnCode(EReturnCode::SUCCESS)) + { + qpi.transfer(locals.userData.userId, locals.availableDeposit); + state.users.removeByKey(locals.userData.userId); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + return; + } + + locals.userData.roomId = locals.createRoomOutput.roomId; + locals.userData.depositedAmount = locals.availableDeposit - locals.nextStakeOutput.nextStake; + locals.userData.locked = locals.nextStakeOutput.nextStake; + locals.userData.stake = locals.nextStakeOutput.nextStake; + state.users.set(locals.userData.userId, locals.userData); + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + + state.rooms.cleanupIfNeeded(QDUEL_ROOMS_REMOVAL_THRESHOLD_PERCENT); + state.users.cleanupIfNeeded(QDUEL_ROOMS_REMOVAL_THRESHOLD_PERCENT); + } + +private: + PRIVATE_FUNCTION_WITH_LOCALS(GetWinnerPlayer) + { + locals.minPlayerId = min(input.player1, input.player2); + locals.maxPlayerId = max(input.player1, input.player2); + + locals.randomValue = qpi.getPrevSpectrumDigest(); + + locals.randomValue.u64._0 ^= locals.minPlayerId.u64._0 ^ locals.maxPlayerId.u64._0 ^ qpi.tick(); + locals.randomValue.u64._1 ^= locals.minPlayerId.u64._1 ^ locals.maxPlayerId.u64._1; + locals.randomValue.u64._2 ^= locals.minPlayerId.u64._2 ^ locals.maxPlayerId.u64._2; + locals.randomValue.u64._3 ^= locals.minPlayerId.u64._3 ^ locals.maxPlayerId.u64._3; + + locals.randomValue = qpi.K12(locals.randomValue); + + output.winner = locals.randomValue.u64._0 & 1 ? locals.maxPlayerId : locals.minPlayerId; + } + + PRIVATE_FUNCTION(CalculateRevenue) + { + output.devFee = div(smul(input.amount, static_cast(state.devFeePercentBps)), QDUEL_PERCENT_SCALE); + output.burnFee = div(smul(input.amount, static_cast(state.burnFeePercentBps)), QDUEL_PERCENT_SCALE); + output.shareholdersFee = + smul(div(div(smul(input.amount, static_cast(state.shareholdersFeePercentBps)), QDUEL_PERCENT_SCALE), 676ULL), 676ULL); + output.winner = input.amount - (output.devFee + output.burnFee + output.shareholdersFee); + } + + PRIVATE_PROCEDURE_WITH_LOCALS(TransferToShareholders) + { + if (input.amount == 0) + { + return; + } + + locals.rlAsset.issuer = id::zero(); + locals.rlAsset.assetName = QDUEL_RANDOM_LOTTERY_ASSET_NAME; + + locals.dividendPerShare = div(input.amount, NUMBER_OF_COMPUTORS); + if (locals.dividendPerShare == 0) + { + return; + } + + locals.rlIter.begin(locals.rlAsset); + while (!locals.rlIter.reachedEnd()) + { + locals.rlShares = static_cast(locals.rlIter.numberOfPossessedShares()); + if (locals.rlShares > 0) + { + locals.toTransfer = static_cast(smul(locals.rlShares, locals.dividendPerShare)); + if (qpi.transfer(locals.rlIter.possessor(), locals.toTransfer) >= 0) + { + locals.transferredAmount += locals.toTransfer; + } + } + locals.rlIter.next(); + } + + output.remainder = input.amount - locals.transferredAmount; + } + + PRIVATE_PROCEDURE_WITH_LOCALS(AddUserData) + { + // Already Exist + if (state.users.contains(input.userId)) + { + output.returnCode = toReturnCode(EReturnCode::USER_ALREADY_EXISTS); + return; + } + + locals.newUserData.userId = input.userId; + locals.newUserData.roomId = input.roomId; + locals.newUserData.allowedPlayer = input.allowedPlayer; + locals.newUserData.depositedAmount = input.depositedAmount; + locals.newUserData.locked = input.stake; + locals.newUserData.stake = input.stake; + locals.newUserData.raiseStep = input.raiseStep; + locals.newUserData.maxStake = input.maxStake; + if (state.users.set(input.userId, locals.newUserData) == NULL_INDEX) + { + output.returnCode = toReturnCode(EReturnCode::ROOM_FAILED_CREATE); + return; + } + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } +}; diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 2765a4537..7de4979c4 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -778,6 +778,9 @@ struct RL : public ContractBase output.returnCode = toReturnCode(EReturnCode::SUCCESS); } + // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } + private: /** * @brief Internal: records a winner into the cyclic winners array. @@ -952,9 +955,6 @@ struct RL : public ContractBase static void getWinnerCounter(const RL& state, uint64& outCounter) { outCounter = mod(state.winnersCounter, state.winners.capacity()); } - // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. - static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } - // Reads current net on-chain balance of SELF (incoming - outgoing). static void getSCRevenue(const Entity& entity, uint64& revenue) { revenue = entity.incomingAmount - entity.outgoingAmount; } diff --git a/test/contract_qduel.cpp b/test/contract_qduel.cpp new file mode 100644 index 000000000..d36856662 --- /dev/null +++ b/test/contract_qduel.cpp @@ -0,0 +1,1122 @@ +#define NO_UEFI +#define _ALLOW_KEYWORD_MACROS +#define private protected +#include "contract_testing.h" +#undef private +#undef _ALLOW_KEYWORD_MACROS + +constexpr uint16 PROCEDURE_INDEX_CREATE_ROOM = 1; +constexpr uint16 PROCEDURE_INDEX_CONNECT_ROOM = 2; +constexpr uint16 PROCEDURE_INDEX_SET_PERCENT_FEES = 3; +constexpr uint16 PROCEDURE_INDEX_SET_TTL_HOURS = 4; +constexpr uint16 PROCEDURE_INDEX_DEPOSIT = 5; +constexpr uint16 PROCEDURE_INDEX_WITHDRAW = 6; +constexpr uint16 FUNCTION_INDEX_GET_PERCENT_FEES = 1; +constexpr uint16 FUNCTION_INDEX_GET_ROOMS = 2; +constexpr uint16 FUNCTION_INDEX_GET_TTL_HOURS = 3; +constexpr uint16 FUNCTION_INDEX_GET_USER_PROFILE = 4; + +static const id QDUEL_TEAM_ADDRESS = + ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, _W, _E, _T, _H, _N, _G, + _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + +class QpiContextUserFunctionCallWithInvocator : public QpiContextFunctionCall +{ +public: + QpiContextUserFunctionCallWithInvocator(unsigned int contractIndex, const id& invocator) + : QpiContextFunctionCall(contractIndex, invocator, 0, USER_FUNCTION_CALL) + {} +}; + +class QDuelChecker : public QDUEL +{ +public: + // Expose read-only accessors for internal state so tests can assert without + // modifying contract storage directly. + uint64 roomCount() const { return rooms.population(); } + id team() const { return teamAddress; } + uint8 ttl() const { return ttlHours; } + uint8 devFee() const { return devFeePercentBps; } + uint8 burnFee() const { return burnFeePercentBps; } + uint8 shareholdersFee() const { return shareholdersFeePercentBps; } + uint64 minDuelAmount() const { return minimumDuelAmount; } + void setState(EState newState) { currentState = newState; } + EState getState() const { return currentState; } + // Helper to fetch user record without exposing contract internals. + bool getUserData(const id& user, UserData& data) const { return users.get(user, data); } + // Directly set a user record to simulate edge-case storage edits. + void setUserData(const UserData& data) { users.set(data.userId, data); } + + RoomInfo firstRoom() const + { + // Map storage can be sparse; walk to first element. + const sint64 index = rooms.nextElementIndex(NULL_INDEX); + if (index == NULL_INDEX) + { + return RoomInfo{}; + } + return rooms.value(index); + } + + bool hasRoom(const id& roomId) const { return rooms.contains(roomId); } + + id computeWinner(const id& player1, const id& player2) const + { + // Run the same winner function as the contract to keep tests deterministic. + QpiContextUserFunctionCall qpi(QDUEL_CONTRACT_INDEX); + GetWinnerPlayer_input input{player1, player2}; + GetWinnerPlayer_output output{}; + GetWinnerPlayer_locals locals{}; + GetWinnerPlayer(qpi, *this, input, output, locals); + return output.winner; + } + + void calculateRevenue(uint64 amount, CalculateRevenue_output& output) const + { + QpiContextUserFunctionCall qpi(QDUEL_CONTRACT_INDEX); + + // Contract helpers require zeroed outputs and locals. + output = {}; + CalculateRevenue_input revenueInput{amount}; + CalculateRevenue_locals revenueLocals{}; + CalculateRevenue(qpi, *this, revenueInput, output, revenueLocals); + } + + GetUserProfile_output getUserProfileFor(const id& user) const + { + QpiContextUserFunctionCallWithInvocator qpi(QDUEL_CONTRACT_INDEX, user); + GetUserProfile_input input{}; + GetUserProfile_output output{}; + GetUserProfile_locals locals{}; + GetUserProfile(qpi, *this, input, output, locals); + return output; + } +}; + +class ContractTestingQDuel : protected ContractTesting +{ +public: + ContractTestingQDuel() + { + // Build an empty chain state and deploy the contract under test. + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QDUEL); + system.epoch = contractDescriptions[QDUEL_CONTRACT_INDEX].constructionEpoch; + callSystemProcedure(QDUEL_CONTRACT_INDEX, INITIALIZE); + } + + // Access helper for the underlying contract state. + QDuelChecker* state() { return reinterpret_cast(contractStates[QDUEL_CONTRACT_INDEX]); } + + QDUEL::CreateRoom_output createRoom(const id& user, const id& allowedPlayer, uint64 stake, uint64 raiseStep, uint64 maxStake, sint64 reward) + { + QDUEL::CreateRoom_input input{allowedPlayer, stake, raiseStep, maxStake}; + QDUEL::CreateRoom_output output; + // Route through contract procedure to keep call path identical to production. + if (!invokeUserProcedure(QDUEL_CONTRACT_INDEX, PROCEDURE_INDEX_CREATE_ROOM, input, output, user, reward)) + { + output.returnCode = QDUEL::toReturnCode(QDUEL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + QDUEL::ConnectToRoom_output connectToRoom(const id& user, const id& roomId, sint64 reward) + { + QDUEL::ConnectToRoom_input input{roomId}; + QDUEL::ConnectToRoom_output output; + // Call the user procedure so validation and state updates are exercised. + if (!invokeUserProcedure(QDUEL_CONTRACT_INDEX, PROCEDURE_INDEX_CONNECT_ROOM, input, output, user, reward)) + { + output.returnCode = QDUEL::toReturnCode(QDUEL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + QDUEL::SetPercentFees_output setPercentFees(const id& user, uint8 devFee, uint8 burnFee, uint8 shareholdersFee, sint64 reward = 0) + { + QDUEL::SetPercentFees_input input{devFee, burnFee, shareholdersFee}; + QDUEL::SetPercentFees_output output; + // System procedures are tested via normal user invocation. + if (!invokeUserProcedure(QDUEL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_PERCENT_FEES, input, output, user, reward)) + { + output.returnCode = QDUEL::toReturnCode(QDUEL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + QDUEL::SetTTLHours_output setTtlHours(const id& user, uint8 ttlHours, sint64 reward = 0) + { + QDUEL::SetTTLHours_input input{ttlHours}; + QDUEL::SetTTLHours_output output; + // Ensure contract state gets updated through procedure validation. + if (!invokeUserProcedure(QDUEL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_TTL_HOURS, input, output, user, reward)) + { + output.returnCode = QDUEL::toReturnCode(QDUEL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + QDUEL::GetPercentFees_output getPercentFees() + { + QDUEL::GetPercentFees_input input{}; + QDUEL::GetPercentFees_output output; + // Read-only function call for fee snapshot. + callFunction(QDUEL_CONTRACT_INDEX, FUNCTION_INDEX_GET_PERCENT_FEES, input, output); + return output; + } + + QDUEL::GetRooms_output getRooms() + { + QDUEL::GetRooms_input input{}; + QDUEL::GetRooms_output output; + // Read-only function call for rooms snapshot. + callFunction(QDUEL_CONTRACT_INDEX, FUNCTION_INDEX_GET_ROOMS, input, output); + return output; + } + + QDUEL::GetTTLHours_output getTtlHours() + { + QDUEL::GetTTLHours_input input{}; + QDUEL::GetTTLHours_output output; + // Read-only function call for TTL configuration. + callFunction(QDUEL_CONTRACT_INDEX, FUNCTION_INDEX_GET_TTL_HOURS, input, output); + return output; + } + + QDUEL::GetUserProfile_output getUserProfile() + { + QDUEL::GetUserProfile_input input{}; + QDUEL::GetUserProfile_output output; + // Read-only function call for caller profile. + callFunction(QDUEL_CONTRACT_INDEX, FUNCTION_INDEX_GET_USER_PROFILE, input, output); + return output; + } + + QDUEL::Deposit_output deposit(const id& user, sint64 reward) + { + QDUEL::Deposit_input input{}; + QDUEL::Deposit_output output; + // Deposit is a user procedure that mutates balance and state. + if (!invokeUserProcedure(QDUEL_CONTRACT_INDEX, PROCEDURE_INDEX_DEPOSIT, input, output, user, reward)) + { + output.returnCode = QDUEL::toReturnCode(QDUEL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + QDUEL::Withdraw_output withdraw(const id& user, uint64 amount, sint64 reward = 0) + { + QDUEL::Withdraw_input input{amount}; + QDUEL::Withdraw_output output; + // Withdraw uses user procedure to enforce validations and limits. + if (!invokeUserProcedure(QDUEL_CONTRACT_INDEX, PROCEDURE_INDEX_WITHDRAW, input, output, user, reward)) + { + output.returnCode = QDUEL::toReturnCode(QDUEL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + // Helpers that dispatch system procedures during lifecycle tests. + void endTick() { callSystemProcedure(QDUEL_CONTRACT_INDEX, END_TICK); } + + void endEpoch() { callSystemProcedure(QDUEL_CONTRACT_INDEX, END_EPOCH); } + + void beginEpoch() { callSystemProcedure(QDUEL_CONTRACT_INDEX, BEGIN_EPOCH); } + + // Control time and tick for deterministic tests. + void setTick(uint32 tick) { system.tick = tick; } + uint32 getTick() const { return system.tick; } + + void forceEndTick() + { + // Align tick to update period so END_TICK work executes. + system.tick = system.tick + (QDUEL_TICK_UPDATE_PERIOD - mod(system.tick, static_cast(QDUEL_TICK_UPDATE_PERIOD))); + + endTick(); + } + + void setDeterministicTime(uint16 year = 2025, uint8 month = 1, uint8 day = 1, uint8 hour = 0) + { + // Set a fixed time and reset etalon tick so tests are stable. + setMemory(utcTime, 0); + utcTime.Year = year; + utcTime.Month = month; + utcTime.Day = day; + utcTime.Hour = hour; + utcTime.Minute = 0; + utcTime.Second = 0; + utcTime.Nanosecond = 0; + updateQpiTime(); + etalonTick.prevSpectrumDigest = m256i::zero(); + } +}; + +namespace +{ + bool findPlayersForWinner(ContractTestingQDuel& qduel, bool wantPlayer1Win, id& player1, id& player2) + { + // Brute-force deterministic ids until winner matches desired side. + for (uint64 i = 1; i < 10000; ++i) + { + const id candidate1(i, 0, 0, 0); + const id candidate2(i + 1, 0, 0, 0); + const id winner = qduel.state()->computeWinner(candidate1, candidate2); + if (winner == (wantPlayer1Win ? candidate1 : candidate2)) + { + player1 = candidate1; + player2 = candidate2; + return true; + } + } + return false; + } + + void runFullGameCycleWithFees(ContractTestingQDuel& qduel, const id& player1, const id& player2, const id& expectedWinner) + { + // Setup shareholders so revenue distribution can be validated. + const id shareholder1 = id::randomValue(); + const id shareholder2 = id::randomValue(); + constexpr unsigned int rlSharesOwner1 = 100; + constexpr unsigned int rlSharesOwner2 = 576; + std::vector> rlShares{ + {shareholder1, rlSharesOwner1}, + {shareholder2, rlSharesOwner2}, + }; + issueContractShares(RL_CONTRACT_INDEX, rlShares); + + // Set fees as the team address (contract owner). + constexpr uint8 devFee = 15; + constexpr uint8 burnFee = 30; + constexpr uint8 shareholdersFee = 55; + increaseEnergy(qduel.state()->team(), 1); + EXPECT_EQ(qduel.setPercentFees(qduel.state()->team(), devFee, burnFee, shareholdersFee).returnCode, + QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + // Setup: give both players enough balance to cover the duel. + constexpr uint64 duelAmount = 100000ULL; + increaseEnergy(player1, duelAmount); + increaseEnergy(player2, duelAmount); + const uint64 player1Before = getBalance(player1); + const uint64 player2Before = getBalance(player2); + + const uint64 teamBefore = getBalance(qduel.state()->team()); + const uint64 shareholder1Before = getBalance(shareholder1); + const uint64 shareholder2Before = getBalance(shareholder2); + + // Create room and keep initial balance snapshots for payout assertions. + EXPECT_EQ(qduel.createRoom(player1, NULL_ID, duelAmount, 1, duelAmount, duelAmount).returnCode, + QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const uint64 player1AfterCreateRoom = getBalance(player1); + + const id winner = qduel.state()->computeWinner(player1, player2); + EXPECT_EQ(winner, expectedWinner); + + // Calculate expected revenue distribution for fees and winner. + QDUEL::CalculateRevenue_output revenueOutput{}; + qduel.state()->calculateRevenue(duelAmount * 2, revenueOutput); + + // Player 2 joins and triggers finalize logic. + EXPECT_EQ(qduel.connectToRoom(player2, qduel.state()->firstRoom().roomId, duelAmount).returnCode, + QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const uint64 player2AfterConnectToRoom = getBalance(player2); + + // Check fee distribution for team and shareholders. + EXPECT_EQ(getBalance(qduel.state()->team()), teamBefore + revenueOutput.devFee); + + // Check shareholder dividends across the full set of computors. + const uint64 dividendPerShare = revenueOutput.shareholdersFee / NUMBER_OF_COMPUTORS; + EXPECT_EQ(getBalance(shareholder1), shareholder1Before + dividendPerShare * rlSharesOwner1); + EXPECT_EQ(getBalance(shareholder2), shareholder2Before + dividendPerShare * rlSharesOwner2); + + // Check winner receives the remainder and loser only pays entry. + if (winner == player1) + { + EXPECT_EQ(getBalance(player1), player1AfterCreateRoom + revenueOutput.winner); + EXPECT_EQ(getBalance(player2), player2Before - duelAmount); + } + else + { + EXPECT_EQ(getBalance(player1), player1Before - duelAmount); + EXPECT_EQ(getBalance(player2), (player2Before - duelAmount) + revenueOutput.winner); + } + } +} // namespace + +TEST(ContractQDuel, EndEpochKeepsDepositWhileRoomsRecreatedEachEpoch) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2025, 1, 1, 0); + + const id owner(40, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + const uint64 epochs = 3; + const uint64 reward = stake + (stake * epochs); + increaseEnergy(owner, reward); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, reward).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + QDUEL::UserData ownerData{}; + ASSERT_TRUE(qduel.state()->getUserData(owner, ownerData)); + uint64 expectedDeposit = ownerData.depositedAmount; + id currentRoomId = ownerData.roomId; + + for (uint32 epoch = 0; epoch < epochs; ++epoch) + { + qduel.beginEpoch(); + qduel.endEpoch(); + qduel.setTick(qduel.getTick() + 1); + + QDUEL::UserData afterEndEpoch{}; + ASSERT_TRUE(qduel.state()->getUserData(owner, afterEndEpoch)); + EXPECT_EQ(afterEndEpoch.depositedAmount, expectedDeposit); + EXPECT_EQ(afterEndEpoch.roomId, currentRoomId); + + qduel.state()->setState(QDUEL::EState::NONE); + + const id opponent(200 + epoch, 0, 0, 0); + increaseEnergy(opponent, stake); + EXPECT_EQ(qduel.connectToRoom(opponent, currentRoomId, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + ASSERT_TRUE(qduel.state()->getUserData(owner, ownerData)); + EXPECT_NE(ownerData.roomId, currentRoomId); + EXPECT_EQ(ownerData.locked, stake); + expectedDeposit -= stake; + EXPECT_EQ(ownerData.depositedAmount, expectedDeposit); + currentRoomId = ownerData.roomId; + } +} + +TEST(ContractQDuel, BeginEpochKeepsRoomsAndUsers) +{ + ContractTestingQDuel qduel; + // Start from a deterministic time and unlocked state. + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2022, 4, 13, 0); + + const id owner(1, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + // Give the owner enough balance to create a room. + increaseEnergy(owner, stake); + + // Create a room and verify it survives epoch transition. + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + const QDUEL::RoomInfo roomBefore = qduel.state()->firstRoom(); + QDUEL::UserData userBefore{}; + EXPECT_TRUE(qduel.state()->getUserData(owner, userBefore)); + + // Begin epoch should not wipe persistent data. + qduel.beginEpoch(); + + // Room and user record should still exist after epoch transition. + EXPECT_TRUE(qduel.state()->hasRoom(roomBefore.roomId)); + QDUEL::UserData userAfter{}; + EXPECT_TRUE(qduel.state()->getUserData(owner, userAfter)); + EXPECT_EQ(userAfter.roomId, roomBefore.roomId); +} + +TEST(ContractQDuel, FirstTickAfterUnlockResetsTimerStart) +{ + ContractTestingQDuel qduel; + // Start from a deterministic time and unlocked state. + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2022, 4, 13, 0); + + const id owner(2, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + // Fund owner so the room creation succeeds. + increaseEnergy(owner, stake); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + const QDUEL::RoomInfo roomBefore = qduel.state()->firstRoom(); + const uint64 initialCloseTimer = roomBefore.closeTimer; + const DateAndTime initialLastUpdate = roomBefore.lastUpdate; + + // Locking occurs at epoch start; timers should not advance while locked. + qduel.beginEpoch(); + + // Still locked: no timer or lastUpdate changes. + qduel.setDeterministicTime(2022, 4, 13, 1); + qduel.forceEndTick(); + + const QDUEL::RoomInfo lockedRoom = qduel.state()->firstRoom(); + EXPECT_EQ(lockedRoom.closeTimer, initialCloseTimer); + EXPECT_EQ(lockedRoom.lastUpdate, initialLastUpdate); + + // First unlocked tick: reset lastUpdate to "now" without reducing timer. + qduel.setDeterministicTime(2022, 4, 14, 2); + qduel.forceEndTick(); + + const QDUEL::RoomInfo unlockedRoom = qduel.state()->firstRoom(); + EXPECT_EQ(unlockedRoom.closeTimer, initialCloseTimer); + const DateAndTime expectedNow(2022, 4, 14, 2, 0, 0); + EXPECT_EQ(unlockedRoom.lastUpdate, expectedNow); +} + +TEST(ContractQDuel, EndTickExpiresRoomCreatesNewWhenDepositAvailable) +{ + ContractTestingQDuel qduel; + // Start from a deterministic time and unlocked state. + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2025, 1, 1, 0); + + const id owner(3, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + // Fund owner with enough to re-create room after finalize. + increaseEnergy(owner, stake); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + const QDUEL::RoomInfo roomBefore = qduel.state()->firstRoom(); + EXPECT_EQ(qduel.state()->roomCount(), 1ULL); + + // Advance beyond TTL to trigger finalize and auto room creation. + qduel.setDeterministicTime(2025, 1, 1, 3); + qduel.forceEndTick(); + + // A new room should replace the expired one. + EXPECT_EQ(qduel.state()->roomCount(), 1ULL); + const QDUEL::RoomInfo roomAfter = qduel.state()->firstRoom(); + EXPECT_NE(roomAfter.roomId, roomBefore.roomId); + + QDUEL::UserData userAfter{}; + EXPECT_TRUE(qduel.state()->getUserData(owner, userAfter)); + // User should be re-bound to the new room with locked stake. + EXPECT_EQ(userAfter.roomId, roomAfter.roomId); + EXPECT_EQ(userAfter.locked, stake); +} + +TEST(ContractQDuel, EndTickExpiresRoomWithoutAvailableDepositRemovesUser) +{ + ContractTestingQDuel qduel; + // Start from a deterministic time and unlocked state. + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2025, 1, 1, 0); + + const id owner(4, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + // Fund owner just enough to create the initial room. + increaseEnergy(owner, stake); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + QDUEL::UserData userData{}; + ASSERT_TRUE(qduel.state()->getUserData(owner, userData)); + // Remove available balance so finalize cannot recreate the room. + userData.depositedAmount = 0; + userData.locked = 0; + qduel.state()->setUserData(userData); + + // Expire room and expect cleanup. + qduel.setDeterministicTime(2025, 1, 1, 3); + qduel.forceEndTick(); + + // Room and user data should be removed when deposit is insufficient. + EXPECT_EQ(qduel.state()->roomCount(), 0ULL); + QDUEL::UserData userAfter{}; + EXPECT_FALSE(qduel.state()->getUserData(owner, userAfter)); +} + +TEST(ContractQDuel, EndTickSkipsNonPeriodTicks) +{ + ContractTestingQDuel qduel; + // Start from a deterministic time and unlocked state. + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2025, 1, 1, 0); + + const id owner(5, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + // Fund owner to create a room. + increaseEnergy(owner, stake); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + const QDUEL::RoomInfo roomBefore = qduel.state()->firstRoom(); + qduel.setDeterministicTime(2025, 1, 1, 1); + qduel.setTick(1); + // Non-period tick: no updates expected. + qduel.endTick(); + + const QDUEL::RoomInfo roomAfterSkipped = qduel.state()->firstRoom(); + EXPECT_EQ(roomAfterSkipped.closeTimer, roomBefore.closeTimer); + EXPECT_EQ(roomAfterSkipped.lastUpdate, roomBefore.lastUpdate); + + // Period tick: updates should apply. + qduel.setTick(QDUEL_TICK_UPDATE_PERIOD); + qduel.endTick(); + + const QDUEL::RoomInfo roomAfterProcessed = qduel.state()->firstRoom(); + // Close timer should have decreased by one hour and lastUpdate bumped. + EXPECT_EQ(roomAfterProcessed.closeTimer, roomBefore.closeTimer - 3600ULL); + const DateAndTime expectedNow(2025, 1, 1, 1, 0, 0); + EXPECT_EQ(roomAfterProcessed.lastUpdate, expectedNow); +} + +TEST(ContractQDuel, LockedStateBlocksCreateAndConnect) +{ + ContractTestingQDuel qduel; + // Start from a deterministic time and unlocked state. + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2025, 1, 1, 0); + + const id owner(6, 0, 0, 0); + const id other(7, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + // Fund owner to create the baseline room. + increaseEnergy(owner, stake); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const QDUEL::RoomInfo roomBefore = qduel.state()->firstRoom(); + + // Lock contract and verify user procedures are blocked. + qduel.state()->setState(QDUEL::EState::LOCKED); + // Fund the other user so only the lock gate can fail. + increaseEnergy(other, stake); + + EXPECT_EQ(qduel.createRoom(other, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::STATE_LOCKED)); + EXPECT_EQ(qduel.connectToRoom(other, roomBefore.roomId, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::STATE_LOCKED)); + // Existing room should remain unchanged. + EXPECT_TRUE(qduel.state()->hasRoom(roomBefore.roomId)); +} + +TEST(ContractQDuel, EndTickRecreatesRoomWithUpdatedStake) +{ + ContractTestingQDuel qduel; + // Start from a deterministic time and unlocked state. + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2025, 1, 1, 0); + + const id owner(8, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + // Fund owner so next stake can be doubled. + increaseEnergy(owner, stake * 2); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 2, 0, stake * 2).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const QDUEL::RoomInfo roomBefore = qduel.state()->firstRoom(); + + // Expire the room and expect a new one using computed next stake. + qduel.setDeterministicTime(2025, 1, 1, 3); + qduel.forceEndTick(); + + const QDUEL::RoomInfo roomAfter = qduel.state()->firstRoom(); + EXPECT_NE(roomAfter.roomId, roomBefore.roomId); + // Amount should reflect the raiseStep applied to the original stake. + EXPECT_EQ(roomAfter.amount, stake * 2); + + QDUEL::UserData userAfter{}; + EXPECT_TRUE(qduel.state()->getUserData(owner, userAfter)); + // User should be locked into the new room with the updated stake. + EXPECT_EQ(userAfter.roomId, roomAfter.roomId); + EXPECT_EQ(userAfter.locked, stake * 2); + EXPECT_EQ(userAfter.depositedAmount, 0ULL); +} + +TEST(ContractQDuel, ConnectFinalizeIgnoresLockedAmount) +{ + ContractTestingQDuel qduel; + // Start from a deterministic time and unlocked state. + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2025, 1, 1, 0); + + const id owner(9, 0, 0, 0); + const id opponent(10, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + // Fund both players so creation and join can proceed. + increaseEnergy(owner, stake); + increaseEnergy(opponent, stake); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const QDUEL::RoomInfo roomBefore = qduel.state()->firstRoom(); + + // On connect, finalize uses includeLocked=false, so owner data is cleared. + EXPECT_EQ(qduel.connectToRoom(opponent, roomBefore.roomId, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + // Room is removed and owner record should be purged after finalize. + EXPECT_FALSE(qduel.state()->hasRoom(roomBefore.roomId)); + QDUEL::UserData ownerAfter{}; + EXPECT_FALSE(qduel.state()->getUserData(owner, ownerAfter)); +} + +TEST(ContractQDuel, InitializeDefaults) +{ + ContractTestingQDuel qduel; + + EXPECT_EQ(qduel.state()->team(), QDUEL_TEAM_ADDRESS); + EXPECT_EQ(qduel.state()->minDuelAmount(), QDUEL_MINIMUM_DUEL_AMOUNT); + EXPECT_EQ(qduel.state()->devFee(), QDUEL_DEV_FEE_PERCENT_BPS); + EXPECT_EQ(qduel.state()->burnFee(), QDUEL_BURN_FEE_PERCENT_BPS); + EXPECT_EQ(qduel.state()->shareholdersFee(), QDUEL_SHAREHOLDERS_FEE_PERCENT_BPS); + EXPECT_EQ(qduel.state()->ttl(), QDUEL_TTL_HOURS); + EXPECT_EQ(qduel.state()->getState(), QDUEL::EState::NONE); + EXPECT_EQ(qduel.state()->roomCount(), 0ULL); +} + +TEST(ContractQDuel, CreateRoomStoresRoomAndUser) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + qduel.setDeterministicTime(2025, 1, 1, 0); + + const id owner(11, 0, 0, 0); + const id allowed(12, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + const uint64 reward = stake + 5000; + increaseEnergy(owner, reward); + + EXPECT_EQ(qduel.createRoom(owner, allowed, stake, 2, stake * 3, reward).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + EXPECT_EQ(qduel.state()->roomCount(), 1ULL); + const QDUEL::RoomInfo room = qduel.state()->firstRoom(); + EXPECT_EQ(room.owner, owner); + EXPECT_EQ(room.allowedPlayer, allowed); + EXPECT_EQ(room.amount, stake); + EXPECT_EQ(room.closeTimer, static_cast(qduel.state()->ttl()) * 3600ULL); + const DateAndTime expectedNow(2025, 1, 1, 0, 0, 0); + EXPECT_EQ(room.lastUpdate, expectedNow); + + QDUEL::UserData user{}; + EXPECT_TRUE(qduel.state()->getUserData(owner, user)); + EXPECT_EQ(user.roomId, room.roomId); + EXPECT_EQ(user.allowedPlayer, allowed); + EXPECT_EQ(user.depositedAmount, reward - stake); + EXPECT_EQ(user.locked, stake); + EXPECT_EQ(user.stake, stake); + EXPECT_EQ(user.raiseStep, 2ULL); + EXPECT_EQ(user.maxStake, stake * 3); +} + +TEST(ContractQDuel, CreateRoomRejectsStakeBelowMinimum) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner(13, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount() - 1; + const uint64 reward = qduel.state()->minDuelAmount(); + increaseEnergy(owner, reward); + const uint64 balanceBefore = getBalance(owner); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, reward).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::INVALID_VALUE)); + EXPECT_EQ(getBalance(owner), balanceBefore); + EXPECT_EQ(qduel.state()->roomCount(), 0ULL); +} + +TEST(ContractQDuel, CreateRoomRejectsMaxStakeBelowStake) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner(14, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + const uint64 reward = stake; + increaseEnergy(owner, reward); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake - 1, reward).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::INVALID_VALUE)); + EXPECT_EQ(qduel.state()->roomCount(), 0ULL); +} + +TEST(ContractQDuel, CreateRoomRejectsRewardBelowMinimum) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner(15, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + const uint64 reward = qduel.state()->minDuelAmount() - 1; + increaseEnergy(owner, reward); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, reward).returnCode, + QDUEL::toReturnCode(QDUEL::EReturnCode::ROOM_INSUFFICIENT_DUEL_AMOUNT)); + EXPECT_EQ(qduel.state()->roomCount(), 0ULL); +} + +TEST(ContractQDuel, CreateRoomRejectsRewardBelowStake) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner(16, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount() + 1000; + const uint64 reward = stake - 1; + increaseEnergy(owner, reward); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, reward).returnCode, + QDUEL::toReturnCode(QDUEL::EReturnCode::ROOM_INSUFFICIENT_DUEL_AMOUNT)); + EXPECT_EQ(qduel.state()->roomCount(), 0ULL); +} + +TEST(ContractQDuel, CreateRoomRejectsDuplicateUser) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner(17, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(owner, stake * 2); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::USER_ALREADY_EXISTS)); + EXPECT_EQ(qduel.state()->roomCount(), 1ULL); +} + +TEST(ContractQDuel, CreateRoomRejectsWhenRoomsFull) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const uint64 stake = qduel.state()->minDuelAmount(); + for (uint32 i = 0; i < QDUEL_MAX_NUMBER_OF_ROOMS; ++i) + { + const id owner(100 + i, 0, 0, 0); + qduel.setTick(i); + increaseEnergy(owner, stake); + const auto output = qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake); + EXPECT_EQ(output.returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)) << "at[" << i << "]"; + } + + EXPECT_EQ(qduel.state()->roomCount(), static_cast(QDUEL_MAX_NUMBER_OF_ROOMS)); + + const id extraOwner(9999, 0, 0, 0); + increaseEnergy(extraOwner, stake); + EXPECT_EQ(qduel.createRoom(extraOwner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::ROOM_FULL)); +} + +TEST(ContractQDuel, ConnectToRoomRejectsMissingRoom) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id player(18, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(player, stake); + + EXPECT_EQ(qduel.connectToRoom(player, id(999, 0, 0, 0), stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::ROOM_NOT_FOUND)); +} + +TEST(ContractQDuel, ConnectToRoomRejectsNotAllowedPlayer) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner(19, 0, 0, 0); + const id allowed(20, 0, 0, 0); + const id other(21, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(owner, stake); + increaseEnergy(other, stake); + + EXPECT_EQ(qduel.createRoom(owner, allowed, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const QDUEL::RoomInfo room = qduel.state()->firstRoom(); + + EXPECT_EQ(qduel.connectToRoom(other, room.roomId, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::ROOM_ACCESS_DENIED)); +} + +TEST(ContractQDuel, ConnectToRoomRejectsInsufficientReward) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner(22, 0, 0, 0); + const id opponent(23, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(owner, stake); + increaseEnergy(opponent, stake - 1); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const QDUEL::RoomInfo room = qduel.state()->firstRoom(); + + EXPECT_EQ(qduel.connectToRoom(opponent, room.roomId, stake - 1).returnCode, + QDUEL::toReturnCode(QDUEL::EReturnCode::ROOM_INSUFFICIENT_DUEL_AMOUNT)); +} + +TEST(ContractQDuel, ConnectToRoomRefundsExcessRewardForLoser) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + id owner; + id opponent; + ASSERT_TRUE(findPlayersForWinner(qduel, true, owner, opponent)); + + const uint64 stake = qduel.state()->minDuelAmount(); + const uint64 reward = stake + 5000; + increaseEnergy(owner, stake); + increaseEnergy(opponent, reward); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const uint64 opponentBefore = getBalance(opponent); + + EXPECT_EQ(qduel.connectToRoom(opponent, qduel.state()->firstRoom().roomId, reward).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + EXPECT_EQ(getBalance(opponent), opponentBefore - stake); +} + +TEST(ContractQDuel, ConnectFinalizeCreatesRoomFromDeposit) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner(24, 0, 0, 0); + const id opponent(25, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(owner, stake * 2); + increaseEnergy(opponent, stake); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, 0, stake * 2).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const QDUEL::RoomInfo roomBefore = qduel.state()->firstRoom(); + + qduel.setTick(10); + + EXPECT_EQ(qduel.connectToRoom(opponent, roomBefore.roomId, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + EXPECT_EQ(qduel.state()->roomCount(), 1ULL); + const QDUEL::RoomInfo roomAfter = qduel.state()->firstRoom(); + EXPECT_NE(roomAfter.roomId, roomBefore.roomId); + EXPECT_EQ(roomAfter.owner, owner); + EXPECT_EQ(roomAfter.amount, stake); + + QDUEL::UserData userAfter{}; + EXPECT_TRUE(qduel.state()->getUserData(owner, userAfter)); + EXPECT_EQ(userAfter.roomId, roomAfter.roomId); + EXPECT_EQ(userAfter.locked, stake); + EXPECT_EQ(userAfter.depositedAmount, 0ULL); +} + +TEST(ContractQDuel, GetWinnerPlayerIsOrderInvariant) +{ + ContractTestingQDuel qduel; + qduel.setTick(1234); + + const id player1(26, 0, 0, 0); + const id player2(27, 0, 0, 0); + + const id winnerForward = qduel.state()->computeWinner(player1, player2); + const id winnerReverse = qduel.state()->computeWinner(player2, player1); + EXPECT_EQ(winnerForward, winnerReverse); + EXPECT_TRUE(winnerForward == player1 || winnerForward == player2); +} + +TEST(ContractQDuel, CalculateRevenueMatchesExpectedSplits) +{ + ContractTestingQDuel qduel; + + constexpr uint64 amount = 1000000ULL; + QDUEL::CalculateRevenue_output output{}; + qduel.state()->calculateRevenue(amount, output); + + const uint64 expectedDev = (amount * qduel.state()->devFee()) / QDUEL_PERCENT_SCALE; + const uint64 expectedBurn = (amount * qduel.state()->burnFee()) / QDUEL_PERCENT_SCALE; + const uint64 expectedShareholders = ((amount * qduel.state()->shareholdersFee()) / QDUEL_PERCENT_SCALE) / 676ULL * 676ULL; + const uint64 expectedWinner = amount - (expectedDev + expectedBurn + expectedShareholders); + + EXPECT_EQ(output.devFee, expectedDev); + EXPECT_EQ(output.burnFee, expectedBurn); + EXPECT_EQ(output.shareholdersFee, expectedShareholders); + EXPECT_EQ(output.winner, expectedWinner); +} + +TEST(ContractQDuel, SetPercentFeesAccessDeniedAndGetPercentFees) +{ + ContractTestingQDuel qduel; + const auto before = qduel.getPercentFees(); + + const id user(28, 0, 0, 0); + increaseEnergy(user, 10); + const uint64 balanceBefore = getBalance(user); + + EXPECT_EQ(qduel.setPercentFees(user, 1, 2, 3, 10).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::ACCESS_DENIED)); + EXPECT_EQ(getBalance(user), balanceBefore); + + const auto after = qduel.getPercentFees(); + EXPECT_EQ(after.devFeePercentBps, before.devFeePercentBps); + EXPECT_EQ(after.burnFeePercentBps, before.burnFeePercentBps); + EXPECT_EQ(after.shareholdersFeePercentBps, before.shareholdersFeePercentBps); + EXPECT_EQ(after.percentScale, before.percentScale); +} + +TEST(ContractQDuel, SetPercentFeesUpdatesState) +{ + ContractTestingQDuel qduel; + + increaseEnergy(qduel.state()->team(), 1); + EXPECT_EQ(qduel.setPercentFees(qduel.state()->team(), 1, 2, 3, 1).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + const auto output = qduel.getPercentFees(); + EXPECT_EQ(output.devFeePercentBps, 1); + EXPECT_EQ(output.burnFeePercentBps, 2); + EXPECT_EQ(output.shareholdersFeePercentBps, 3); + EXPECT_EQ(output.percentScale, QDUEL_PERCENT_SCALE); + EXPECT_EQ(output.returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); +} + +TEST(ContractQDuel, SetTTLHoursAccessDeniedAndInvalid) +{ + ContractTestingQDuel qduel; + const uint8 ttlBefore = qduel.state()->ttl(); + + const id user(29, 0, 0, 0); + increaseEnergy(user, 5); + const uint64 balanceBefore = getBalance(user); + + EXPECT_EQ(qduel.setTtlHours(user, 5, 5).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::ACCESS_DENIED)); + EXPECT_EQ(getBalance(user), balanceBefore); + EXPECT_EQ(qduel.state()->ttl(), ttlBefore); + + increaseEnergy(qduel.state()->team(), 1); + EXPECT_EQ(qduel.setTtlHours(qduel.state()->team(), 0, 1).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::INVALID_VALUE)); + EXPECT_EQ(qduel.state()->ttl(), ttlBefore); +} + +TEST(ContractQDuel, SetTTLHoursUpdatesState) +{ + ContractTestingQDuel qduel; + increaseEnergy(qduel.state()->team(), 1); + + EXPECT_EQ(qduel.setTtlHours(qduel.state()->team(), 6, 1).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + const auto output = qduel.getTtlHours(); + EXPECT_EQ(output.ttlHours, 6); + EXPECT_EQ(output.returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); +} + +TEST(ContractQDuel, GetRoomsReturnsActiveRooms) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id owner1(30, 0, 0, 0); + const id owner2(31, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(owner1, stake); + increaseEnergy(owner2, stake); + + EXPECT_EQ(qduel.createRoom(owner1, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + EXPECT_EQ(qduel.createRoom(owner2, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + const auto output = qduel.getRooms(); + EXPECT_EQ(output.returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + uint64 count = 0; + bool foundOwner1 = false; + bool foundOwner2 = false; + for (uint32 i = 0; i < QDUEL_MAX_NUMBER_OF_ROOMS; ++i) + { + const QDUEL::RoomInfo room = output.rooms.get(i); + if (room.roomId != id::zero()) + { + ++count; + EXPECT_TRUE(qduel.state()->hasRoom(room.roomId)); + if (room.owner == owner1) + { + foundOwner1 = true; + } + if (room.owner == owner2) + { + foundOwner2 = true; + } + } + } + EXPECT_EQ(count, qduel.state()->roomCount()); + EXPECT_TRUE(foundOwner1); + EXPECT_TRUE(foundOwner2); +} + +TEST(ContractQDuel, GetUserProfileReportsUserData) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const QDUEL::GetUserProfile_output& missing = qduel.getUserProfile(); + EXPECT_EQ(missing.returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::USER_NOT_FOUND)); + + const id owner(32, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(owner, stake + 200); + + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 2, stake * 2, stake + 200).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + const auto profile = qduel.state()->getUserProfileFor(owner); + EXPECT_EQ(profile.returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + EXPECT_EQ(profile.depositedAmount, 200ULL); + EXPECT_EQ(profile.locked, stake); + EXPECT_EQ(profile.stake, stake); + EXPECT_EQ(profile.raiseStep, 2ULL); + EXPECT_EQ(profile.maxStake, stake * 2); + EXPECT_NE(profile.roomId, id::zero()); +} + +TEST(ContractQDuel, DepositValidationsAndUpdatesBalance) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id missingUser(33, 0, 0, 0); + increaseEnergy(missingUser, 1); + EXPECT_EQ(qduel.deposit(missingUser, 0).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::INVALID_VALUE)); + + increaseEnergy(missingUser, 100); + const uint64 missingBefore = getBalance(missingUser); + EXPECT_EQ(qduel.deposit(missingUser, 100).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::USER_NOT_FOUND)); + EXPECT_EQ(getBalance(missingUser), missingBefore); + + const id owner(34, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(owner, stake); + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + QDUEL::UserData before{}; + ASSERT_TRUE(qduel.state()->getUserData(owner, before)); + increaseEnergy(owner, 500); + EXPECT_EQ(qduel.deposit(owner, 500).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + QDUEL::UserData after{}; + ASSERT_TRUE(qduel.state()->getUserData(owner, after)); + EXPECT_EQ(after.depositedAmount, before.depositedAmount + 500); +} + +TEST(ContractQDuel, WithdrawValidationsAndTransfers) +{ + ContractTestingQDuel qduel; + qduel.state()->setState(QDUEL::EState::NONE); + + const id missingUser(35, 0, 0, 0); + increaseEnergy(missingUser, 1); + EXPECT_EQ(qduel.withdraw(missingUser, 1).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::USER_NOT_FOUND)); + + const id owner(36, 0, 0, 0); + const uint64 stake = qduel.state()->minDuelAmount(); + increaseEnergy(owner, stake + 1000); + EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake + 1000).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + + EXPECT_EQ(qduel.withdraw(owner, 0).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::INSUFFICIENT_FREE_DEPOSIT)); + EXPECT_EQ(qduel.withdraw(owner, 2000).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::INSUFFICIENT_FREE_DEPOSIT)); + + const uint64 balanceBefore = getBalance(owner); + EXPECT_EQ(qduel.withdraw(owner, 500).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); + EXPECT_EQ(getBalance(owner), balanceBefore + 500); + + QDUEL::UserData userAfter{}; + ASSERT_TRUE(qduel.state()->getUserData(owner, userAfter)); + EXPECT_EQ(userAfter.depositedAmount, 500ULL); +} + +TEST(ContractQDuel, ConnectToRoomDistributesFeesPlayer1Wins) +{ + ContractTestingQDuel qduel; + + id player1; + id player2; + ASSERT_TRUE(findPlayersForWinner(qduel, true, player1, player2)); + runFullGameCycleWithFees(qduel, player1, player2, player1); +} + +TEST(ContractQDuel, ConnectToRoomDistributesFeesPlayer2Wins) +{ + ContractTestingQDuel qduel; + + id player1; + id player2; + ASSERT_TRUE(findPlayersForWinner(qduel, false, player1, player2)); + runFullGameCycleWithFees(qduel, player1, player2, player2); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 04d56ccf1..145d717bc 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -144,6 +144,7 @@ + @@ -195,4 +196,4 @@ - \ No newline at end of file + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 6634e070c..c58434225 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -28,6 +28,7 @@ + From 70ab46a3d0e583008b7e537a81f4910f10f2d704 Mon Sep 17 00:00:00 2001 From: fnordspace Date: Mon, 26 Jan 2026 16:22:18 +0100 Subject: [PATCH 54/90] Adjust SWATCH burn mechanism for infrastructure contracts (#723) * Adjust SWATCH to burn for all infrastructure contracts * Change burn in SWATCH to balance reserve of the infrastructure SC * Change divisions to use QPI::div --------- Co-authored-by: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> --- src/contracts/SupplyWatcher.h | 42 ++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/contracts/SupplyWatcher.h b/src/contracts/SupplyWatcher.h index fd525b1ad..009d94437 100644 --- a/src/contracts/SupplyWatcher.h +++ b/src/contracts/SupplyWatcher.h @@ -14,17 +14,53 @@ struct SWATCH : public ContractBase { Entity ownEntity; sint64 ownBalance; + sint64 reserve6, reserve7, reserve8; + sint64 target, adjustedTotal; + sint64 burn6, burn7, burn8; + sint64 leftover; + sint64 count; }; BEGIN_EPOCH_WITH_LOCALS() { - // Burn all coins of this contract. According to agreement of the quorum, a part of the - // computor revenue is donated to this contract for burning. + // Burn to balance fee reserves of GQMPROP (6), SWATCH (7), and CCF (8) if (qpi.getEntity(SELF, locals.ownEntity)) { locals.ownBalance = locals.ownEntity.incomingAmount - locals.ownEntity.outgoingAmount; if (locals.ownBalance > 0) - qpi.burn(locals.ownBalance); + { + locals.reserve6 = qpi.queryFeeReserve(6); + locals.reserve7 = qpi.queryFeeReserve(7); + locals.reserve8 = qpi.queryFeeReserve(8); + + locals.target = QPI::div(locals.reserve6 + locals.reserve7 + locals.reserve8 + locals.ownBalance, 3LL); + + // Exclude reserves already above target and recalculate + locals.count = 3; + locals.adjustedTotal = locals.ownBalance; + if (locals.reserve6 < locals.target) locals.adjustedTotal += locals.reserve6; else locals.count--; + if (locals.reserve7 < locals.target) locals.adjustedTotal += locals.reserve7; else locals.count--; + if (locals.reserve8 < locals.target) locals.adjustedTotal += locals.reserve8; else locals.count--; + + if (locals.count > 0) + locals.target = QPI::div(locals.adjustedTotal, locals.count); + + locals.burn6 = (locals.target > locals.reserve6) ? (locals.target - locals.reserve6) : 0; + locals.burn7 = (locals.target > locals.reserve7) ? (locals.target - locals.reserve7) : 0; + locals.burn8 = (locals.target > locals.reserve8) ? (locals.target - locals.reserve8) : 0; + + locals.leftover = locals.ownBalance - locals.burn6 - locals.burn7 - locals.burn8; + if (locals.leftover > 0) + { + locals.burn6 += QPI::div(locals.leftover, 3LL); + locals.burn7 += QPI::div(locals.leftover, 3LL); + locals.burn8 += QPI::div(locals.leftover, 3LL); + } + + if (locals.burn6 > 0) qpi.burn(locals.burn6, 6); + if (locals.burn7 > 0) qpi.burn(locals.burn7, 7); + if (locals.burn8 > 0) qpi.burn(locals.burn8, 8); + } } } }; From 9ce65cbf72027b749f6eea6e6a2e08ef522791d9 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:29:47 +0100 Subject: [PATCH 55/90] add OLD_SWATCH toggle --- src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 5 ++++- src/contract_core/contract_def.h | 4 ++++ src/contracts/SupplyWatcher_old.h | 30 ++++++++++++++++++++++++++++++ src/qubic.cpp | 2 ++ 5 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/contracts/SupplyWatcher_old.h diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 2bbe06053..3ad3d446d 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -46,6 +46,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 1a182d796..9f1b082a0 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -330,6 +330,9 @@ contracts + + contracts + @@ -377,4 +380,4 @@ platform - + \ No newline at end of file diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index a34b81086..bd01b4a4e 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -79,7 +79,11 @@ #define CONTRACT_INDEX SWATCH_CONTRACT_INDEX #define CONTRACT_STATE_TYPE SWATCH #define CONTRACT_STATE2_TYPE SWATCH2 +#ifdef OLD_SWATCH +#include "contracts/SupplyWatcher_old.h" +#else #include "contracts/SupplyWatcher.h" +#endif #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE diff --git a/src/contracts/SupplyWatcher_old.h b/src/contracts/SupplyWatcher_old.h new file mode 100644 index 000000000..087291c9e --- /dev/null +++ b/src/contracts/SupplyWatcher_old.h @@ -0,0 +1,30 @@ +using namespace QPI; + +struct SWATCH2 +{ +}; + +struct SWATCH : public ContractBase +{ + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + } + + struct BEGIN_EPOCH_locals + { + Entity ownEntity; + sint64 ownBalance; + }; + + BEGIN_EPOCH_WITH_LOCALS() + { + // Burn all coins of this contract. According to agreement of the quorum, a part of the + // computor revenue is donated to this contract for burning. + if (qpi.getEntity(SELF, locals.ownEntity)) + { + locals.ownBalance = locals.ownEntity.incomingAmount - locals.ownEntity.outgoingAmount; + if (locals.ownBalance > 0) + qpi.burn(locals.ownBalance); + } + } +}; diff --git a/src/qubic.cpp b/src/qubic.cpp index 211923447..9ae7031aa 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,5 +1,7 @@ #define SINGLE_COMPILE_UNIT +// #define OLD_SWATCH + // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" From 28ddf3271a87f219eafc2c3e0117009cec0768b7 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:41:36 +0100 Subject: [PATCH 56/90] add NO_QDUEL toggle --- src/contract_core/contract_def.h | 8 ++++++++ src/qubic.cpp | 1 + 2 files changed, 9 insertions(+) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index bd01b4a4e..0d6844c19 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -215,6 +215,8 @@ #define CONTRACT_STATE2_TYPE QRWA2 #include "contracts/qRWA.h" +#ifndef NO_QDUEL + #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE @@ -225,6 +227,8 @@ #define CONTRACT_STATE2_TYPE QDUEL2 #include "contracts/QDuel.h" +#endif + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -333,7 +337,9 @@ constexpr struct ContractDescription {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 +#ifndef NO_QDUEL {"QDUEL", 199, 10000, sizeof(QDUEL)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 +#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA)}, @@ -450,7 +456,9 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRWA); +#ifndef NO_QDUEL REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL); +#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/qubic.cpp b/src/qubic.cpp index 9ae7031aa..7821ff493 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,6 +1,7 @@ #define SINGLE_COMPILE_UNIT // #define OLD_SWATCH +// #define NO_QDUEL // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" From 141b7d39945de8f2c1c56d193296db93d0cf05a2 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 26 Jan 2026 22:36:14 +0300 Subject: [PATCH 57/90] IPO QThirtyFour (#684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement QThirtyFour contract with ticket purchasing and winner selection logic * Add QReservePool and QTF contract definitions with related functionalities * Refactor QThirtyFour.h: reposition QTF2 struct definition for clarity * Test version for QThirtyFour * CalcReserveTopUp * New structs * Fees * Refactoring * Remove profit transfer from k3 to k2 with FR enabled * Adds tests * Updates testes * Does not block the purchase of a ticket at a higher price * - src/contracts/QThirtyFour.h: ensure k4 reseed uses up-to-date QRP reserve and don’t consume reseed budget with tier top-ups on k4 rounds - test/contract_qtf.cpp: expose private/protected internals for unit tests, add exact-match k2/k3 ticket generators (unique), fund jackpot balance in k4 test, and force FR off in baseline k2/k3 revenue-split test * Removes SettlementLocals and aligned_storage_t * Updates tests * Update tests * Update tests * Updates index * Updates inde• Fix k=4 settlement: protect jackpot reseed from k2/k3 reserve top-ups; correct schedule bitmask in specx * Fixes ContractVerify * Use RL::max * Fixes ContractVerify * QDuel * transfer dividends to RL shareholders * Remove magic numbers * Move test file * Changes public to protected * Adds lock mechanism * Deposit * Return qus after end epoch * Adds check state in tick * Transferring rooms and players to the next epoch * Create room in tick * Transferring rooms and players to the next epoch * Base tests * Adds tests * Fixes * Add new test * Adds cleanup for HashMaps * Renames Available to Allowed for SC Auto-update QTF index in QRP Adds cleanup for allowedSmartContracts in QRP * Fixes comment * Update constructionEpoch * Update constructionEpoch * Fixes build and warnings * Fixes warnings, order --------- Co-authored-by: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> --- src/Qubic.vcxproj | 2 + src/Qubic.vcxproj.filters | 6 + src/contract_core/contract_def.h | 30 +- src/contracts/QDuel.h | 62 +- src/contracts/QReservePool.h | 204 ++ src/contracts/QThirtyFour.h | 2011 +++++++++++++++++++ src/contracts/RandomLottery.h | 14 +- test/contract_qduel.cpp | 60 +- test/contract_qrp.cpp | 207 ++ test/contract_qtf.cpp | 3121 ++++++++++++++++++++++++++++++ test/test.vcxproj | 2 + test/test.vcxproj.filters | 4 +- 12 files changed, 5650 insertions(+), 73 deletions(-) create mode 100644 src/contracts/QReservePool.h create mode 100644 src/contracts/QThirtyFour.h create mode 100644 test/contract_qrp.cpp create mode 100644 test/contract_qtf.cpp diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 3ad3d446d..f4cfb3115 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -47,6 +47,8 @@ + + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 9f1b082a0..e80136b24 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -303,6 +303,12 @@ contracts + + contracts + + + contracts + contracts diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 0d6844c19..e3b877276 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -215,19 +215,37 @@ #define CONTRACT_STATE2_TYPE QRWA2 #include "contracts/qRWA.h" -#ifndef NO_QDUEL +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QRP_CONTRACT_INDEX 21 +#define CONTRACT_INDEX QRP_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QRP +#define CONTRACT_STATE2_TYPE QRP2 +#include "contracts/QReservePool.h" #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QDUEL_CONTRACT_INDEX 21 +#define QTF_CONTRACT_INDEX 22 +#define CONTRACT_INDEX QTF_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QTF +#define CONTRACT_STATE2_TYPE QTF2 +#include "contracts/QThirtyFour.h" + +#ifndef NO_QDUEL +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QDUEL_CONTRACT_INDEX 23 #define CONTRACT_INDEX QDUEL_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QDUEL #define CONTRACT_STATE2_TYPE QDUEL2 #include "contracts/QDuel.h" - -#endif +#endif // NO_QDUEL // new contracts should be added above this line @@ -337,6 +355,8 @@ constexpr struct ContractDescription {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 + {"QRP", 199, 10000, sizeof(IPO)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 + {"QTF", 199, 10000, sizeof(QTF)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 #ifndef NO_QDUEL {"QDUEL", 199, 10000, sizeof(QDUEL)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 #endif @@ -459,6 +479,8 @@ static void initializeContracts() #ifndef NO_QDUEL REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL); #endif + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRP); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QTF); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/contracts/QDuel.h b/src/contracts/QDuel.h index 44f20da99..3a35d59ab 100644 --- a/src/contracts/QDuel.h +++ b/src/contracts/QDuel.h @@ -1,15 +1,15 @@ using namespace QPI; -constexpr uint32 QDUEL_MAX_NUMBER_OF_ROOMS = 512; -constexpr uint64 QDUEL_MINIMUM_DUEL_AMOUNT = 10000; +constexpr uint16 QDUEL_MAX_NUMBER_OF_ROOMS = 512; +constexpr uint16 QDUEL_MINIMUM_DUEL_AMOUNT = 10000; constexpr uint8 QDUEL_DEV_FEE_PERCENT_BPS = 15; // 0.15% * QDUEL_PERCENT_SCALE constexpr uint8 QDUEL_BURN_FEE_PERCENT_BPS = 30; // 0.3% * QDUEL_PERCENT_SCALE constexpr uint8 QDUEL_SHAREHOLDERS_FEE_PERCENT_BPS = 55; // 0.55% * QDUEL_PERCENT_SCALE -constexpr uint8 QDUEL_PERCENT_SCALE = 1000; +constexpr uint16 QDUEL_PERCENT_SCALE = 1000; constexpr uint8 QDUEL_TTL_HOURS = 3; constexpr uint8 QDUEL_TICK_UPDATE_PERIOD = 100; // Process TICK logic once per this many ticks -constexpr uint64 QDUEL_RANDOM_LOTTERY_ASSET_NAME = 19538; // RL -constexpr uint64 QDUEL_ROOMS_REMOVAL_THRESHOLD_PERCENT = 75; +constexpr uint16 QDUEL_RANDOM_LOTTERY_ASSET_NAME = 19538; // RL +constexpr uint16 QDUEL_ROOMS_REMOVAL_THRESHOLD_PERCENT = 75; struct QDUEL2 { @@ -65,7 +65,7 @@ struct QDUEL : public ContractBase id roomId; id owner; id allowedPlayer; // If zero, anyone can join - uint64 amount; + sint64 amount; uint64 closeTimer; DateAndTime lastUpdate; }; @@ -75,11 +75,11 @@ struct QDUEL : public ContractBase id userId; id roomId; id allowedPlayer; - uint64 depositedAmount; - uint64 locked; - uint64 stake; - uint64 raiseStep; - uint64 maxStake; + sint64 depositedAmount; + sint64 locked; + sint64 stake; + sint64 raiseStep; + sint64 maxStake; }; struct AddUserData_input @@ -87,10 +87,10 @@ struct QDUEL : public ContractBase id userId; id roomId; id allowedPlayer; - uint64 depositedAmount; - uint64 stake; - uint64 raiseStep; - uint64 maxStake; + sint64 depositedAmount; + sint64 stake; + sint64 raiseStep; + sint64 maxStake; }; struct AddUserData_output @@ -106,9 +106,9 @@ struct QDUEL : public ContractBase struct CreateRoom_input { id allowedPlayer; // If zero, anyone can join - uint64 stake; - uint64 raiseStep; - uint64 maxStake; + sint64 stake; + sint64 raiseStep; + sint64 maxStake; }; struct CreateRoom_output @@ -120,7 +120,7 @@ struct QDUEL : public ContractBase { id owner; id allowedPlayer; - uint64 amount; + sint64 amount; }; struct CreateRoomRecord_output @@ -138,14 +138,14 @@ struct QDUEL : public ContractBase struct ComputeNextStake_input { - uint64 stake; - uint64 raiseStep; - uint64 maxStake; + sint64 stake; + sint64 raiseStep; + sint64 maxStake; }; struct ComputeNextStake_output { - uint64 nextStake; + sint64 nextStake; uint8 returnCode; }; @@ -229,7 +229,7 @@ struct QDUEL : public ContractBase struct FinalizeRoom_locals { UserData userData; - uint64 availableDeposit; + sint64 availableDeposit; CreateRoomRecord_input createRoomInput; CreateRoomRecord_output createRoomOutput; ComputeNextStake_input nextStakeInput; @@ -272,7 +272,7 @@ struct QDUEL : public ContractBase uint8 devFeePercentBps; uint8 burnFeePercentBps; uint8 shareholdersFeePercentBps; - uint8 percentScale; + uint16 percentScale; uint64 returnCode; }; @@ -366,7 +366,7 @@ struct QDUEL : public ContractBase struct Withdraw_input { - uint64 amount; + sint64 amount; }; struct Withdraw_output @@ -377,7 +377,7 @@ struct QDUEL : public ContractBase struct Withdraw_locals { UserData userData; - uint64 freeAmount; + sint64 freeAmount; }; struct END_TICK_locals @@ -703,7 +703,7 @@ struct QDUEL : public ContractBase locals.totalPercent = static_cast(input.devFeePercentBps) + static_cast(input.burnFeePercentBps) + static_cast(input.shareholdersFeePercentBps); - locals.totalPercent = div(locals.totalPercent, static_cast(QDUEL_PERCENT_SCALE)); + locals.totalPercent = div(locals.totalPercent, QDUEL_PERCENT_SCALE); if (locals.totalPercent >= 100) { @@ -838,7 +838,7 @@ struct QDUEL : public ContractBase HashMap rooms; HashMap users; id teamAddress; - uint64 minimumDuelAmount; + sint64 minimumDuelAmount; uint8 devFeePercentBps; uint8 burnFeePercentBps; uint8 shareholdersFeePercentBps; @@ -855,9 +855,9 @@ struct QDUEL : public ContractBase { output.nextStake = input.stake; - if (input.raiseStep > 1) + if (input.raiseStep > 1LL) { - if (input.maxStake > 0 && input.stake > 0 && input.raiseStep > div(input.maxStake, input.stake)) + if (input.maxStake > 0LL && input.stake > 0LL && input.raiseStep > div(input.maxStake, input.stake)) { output.nextStake = input.maxStake; } diff --git a/src/contracts/QReservePool.h b/src/contracts/QReservePool.h new file mode 100644 index 000000000..5e537e0f4 --- /dev/null +++ b/src/contracts/QReservePool.h @@ -0,0 +1,204 @@ +using namespace QPI; + +// Number of available smart contracts in the QRP contract. +constexpr uint16 QRP_ALLOWED_SC_NUM = 128; +constexpr uint64 QRP_QTF_INDEX = QRP_CONTRACT_INDEX + 1; +constexpr uint64 QRP_REMOVAL_THRESHOLD_PERCENT = 75; + +struct QRP2 +{ +}; + +struct QRP : ContractBase +{ + enum class EReturnCode : uint8 + { + SUCCESS = 0, + ACCESS_DENIED = 1, + INSUFFICIENT_RESERVE = 2, + + MAX_VALUE = UINT8_MAX + }; + + static constexpr uint8 toReturnCode(const EReturnCode& code) { return static_cast(code); }; + + // Withdraw Reserve + struct WithdrawReserve_input + { + uint64 revenue; + }; + + struct WithdrawReserve_output + { + // How much revenue is allocated to SC + uint64 allocatedRevenue; + uint8 returnCode; + }; + + struct WithdrawReserve_locals + { + Entity entity; + uint64 checkAmount; + }; + + // Add Allowed Smart Contract + struct AddAllowedSC_input + { + uint64 scIndex; + }; + + struct AddAllowedSC_output + { + uint8 returnCode; + }; + + // Remove Allowed Smart Contract + struct RemoveAllowedSC_input + { + uint64 scIndex; + }; + + struct RemoveAllowedSC_output + { + uint8 returnCode; + }; + + // Get Available Reserve + struct GetAvailableReserve_input + { + }; + + struct GetAvailableReserve_output + { + uint64 availableReserve; + }; + + struct GetAvailableReserve_locals + { + Entity entity; + }; + + // Get Allowed Smart Contract + struct GetAllowedSC_input + { + }; + + struct GetAllowedSC_output + { + Array allowedSC; + }; + + struct GetAllowedSC_locals + { + sint64 nextIndex; + uint64 arrayIndex; + }; + + INITIALIZE() + { + // Set team/developer address (owner and team are the same for now) + state.teamAddress = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, + _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + state.ownerAddress = state.teamAddress; + + // Adds QTF to the list of allowed smart contracts. + state.allowedSmartContracts.add(id(QRP_QTF_INDEX, 0, 0, 0)); + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + // Procedures + REGISTER_USER_PROCEDURE(WithdrawReserve, 1); + REGISTER_USER_PROCEDURE(AddAllowedSC, 2); + REGISTER_USER_PROCEDURE(RemoveAllowedSC, 3); + // Functions + REGISTER_USER_FUNCTION(GetAvailableReserve, 1); + REGISTER_USER_FUNCTION(GetAllowedSC, 2); + } + + END_EPOCH() { state.allowedSmartContracts.cleanup(); } + + PUBLIC_PROCEDURE_WITH_LOCALS(WithdrawReserve) + { + if (!state.allowedSmartContracts.contains(qpi.invocator())) + { + output.allocatedRevenue = 0; + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + qpi.getEntity(SELF, locals.entity); + locals.checkAmount = RL::max(locals.entity.incomingAmount - locals.entity.outgoingAmount, 0i64); + if (locals.checkAmount == 0 || input.revenue > locals.checkAmount) + { + output.allocatedRevenue = 0; + output.returnCode = toReturnCode(EReturnCode::INSUFFICIENT_RESERVE); + return; + } + + output.allocatedRevenue = input.revenue; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + + qpi.transfer(qpi.invocator(), output.allocatedRevenue); + } + + PUBLIC_PROCEDURE(AddAllowedSC) + { + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + state.allowedSmartContracts.add(id(input.scIndex, 0, 0, 0)); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE(RemoveAllowedSC) + { + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + state.allowedSmartContracts.remove(id(input.scIndex, 0, 0, 0)); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + + state.allowedSmartContracts.cleanupIfNeeded(QRP_REMOVAL_THRESHOLD_PERCENT); + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAvailableReserve) + { + qpi.getEntity(SELF, locals.entity); + output.availableReserve = RL::max(locals.entity.incomingAmount - locals.entity.outgoingAmount, 0i64); + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAllowedSC) + { + locals.arrayIndex = 0; + locals.nextIndex = -1; + + locals.nextIndex = state.allowedSmartContracts.nextElementIndex(locals.nextIndex); + while (locals.nextIndex != NULL_INDEX) + { + output.allowedSC.set(locals.arrayIndex++, state.allowedSmartContracts.key(locals.nextIndex)); + locals.nextIndex = state.allowedSmartContracts.nextElementIndex(locals.nextIndex); + } + } + +protected: + /** + * @brief Address of the team managing the lottery contract. + * Initialized to a zero address. + */ + id teamAddress; + + /** + * @brief Address of the owner of the lottery contract. + * Initialized to a zero address. + */ + id ownerAddress; + + HashSet allowedSmartContracts; +}; diff --git a/src/contracts/QThirtyFour.h b/src/contracts/QThirtyFour.h new file mode 100644 index 000000000..77d179978 --- /dev/null +++ b/src/contracts/QThirtyFour.h @@ -0,0 +1,2011 @@ +using namespace QPI; + +// --- Core game parameters ---------------------------------------------------- +constexpr uint64 QTF_MAX_NUMBER_OF_PLAYERS = 1024; +constexpr uint64 QTF_RANDOM_VALUES_COUNT = 4; +constexpr uint64 QTF_MAX_RANDOM_VALUE = 30; +constexpr uint64 QTF_TICKET_PRICE = 1000000; + +// Baseline split for k2/k3 when FR is OFF (per spec: k3=40%, k2=28% of Winners block). +// Initial 32% of Winners block is unallocated; overflow will also include unawarded k2/k3 funds. +constexpr uint64 QTF_BASE_K3_SHARE_BP = 4000; // 40% of winners block to k3 +constexpr uint64 QTF_BASE_K2_SHARE_BP = 2800; // 28% of winners block to k2 + +// --- Fast-Recovery (FR) parameters (spec defaults) -------------------------- +// Fast-Recovery base redirect percentages (applied when FR=ON, capped at available amounts) +constexpr uint64 QTF_FR_DEV_REDIRECT_BP = 100; // 1.00% of R (base redirect) +constexpr uint64 QTF_FR_DIST_REDIRECT_BP = 100; // 1.00% of R (base redirect) + +// Deficit-driven extra redirect parameters (dynamic, no hard N threshold) +// The extra redirect is calculated based on: +// - Deficit: Δ = max(0, targetJackpot - currentJackpot) +// - Expected rounds to k=4: E_k4(N) = 1 / (1 - (1-p4)^N) +// - Horizon: H = min(E_k4(N), R_goal_rounds_cap) +// - Needed gain: g_need = max(0, Δ/H - baseGain) +// - Extra percentage: extra_pp = clamp(g_need / R, 0, extra_max) +// - Split equally: dev_extra = dist_extra = extra_pp / 2 +constexpr uint64 QTF_FR_EXTRA_MAX_BP = 70; // Maximum extra redirect: 0.70% of R total (0.35% each Dev/Dist) +constexpr uint64 QTF_FR_GOAL_ROUNDS_CAP = 50; // Cap on expected rounds horizon H for deficit calculation +constexpr uint64 QTF_FIXED_POINT_SCALE = 1000000; // Scale for fixed-point arithmetic (6 decimals precision) + +// Probability constants for k=4 win (exact combinatorics: 4-of-30) +// p4 = C(4,4) * C(26,0) / C(30,4) = 1 / 27405 +constexpr uint64 QTF_P4_DENOMINATOR = 27405; // Denominator for k=4 probability (1/27405) +constexpr uint64 QTF_FR_WINNERS_RAKE_BP = 500; // 5% of winners block from k3 +constexpr uint64 QTF_FR_ALPHA_BP = 500; // alpha = 0.05 -> 95% overflow to jackpot +constexpr uint8 QTF_FR_POST_K4_WINDOW_ROUNDS = 50; +constexpr uint8 QTF_FR_HYSTERESIS_ROUNDS = 3; + +// --- Floors and reserve safety ---------------------------------------------- +constexpr uint64 QTF_K2_FLOOR_MULT = 1; // numerator for 0.5 * P (we divide by 2) +constexpr uint64 QTF_K2_FLOOR_DIV = 2; +constexpr uint64 QTF_K3_FLOOR_MULT = 5; // 5 * P +constexpr uint64 QTF_TOPUP_PER_WINNER_CAP_MULT = 25; // 25 * P +constexpr uint64 QTF_TOPUP_RESERVE_PCT_BP = 1000; // 10% of reserve per round +constexpr uint64 QTF_RESERVE_SOFT_FLOOR_MULT = 20; // keep at least 20 * P in reserve + +// Baseline overflow split (reserve share in basis points). If spec is updated, adjust here. +constexpr uint64 QTF_BASELINE_OVERFLOW_ALPHA_BP = 5000; // 50% reserve / 50% jackpot + +// Default fee percentages (fallback if RL::GetFees fails) +constexpr uint8 QTF_DEFAULT_DEV_PERCENT = 10; +constexpr uint8 QTF_DEFAULT_DIST_PERCENT = 20; +constexpr uint8 QTF_DEFAULT_BURN_PERCENT = 2; +constexpr uint8 QTF_DEFAULT_WINNERS_PERCENT = 68; + +// Maximum attempts to generate unique random value before fallback +constexpr uint8 QTF_MAX_RANDOM_GENERATION_ATTEMPTS = 100; + +constexpr uint64 QTF_DEFAULT_TARGET_JACKPOT = 1000000000ULL; // 1 billion QU (1B) +constexpr uint8 QTF_DEFAULT_SCHEDULE = 1 << SATURDAY | 1u << WEDNESDAY; +constexpr uint8 QTF_DEFAULT_DRAW_HOUR = 11; // 11:00 UTC +constexpr uint32 QTF_DEFAULT_INIT_TIME = 22u << 9 | 4u << 5 | 13u; // RL_DEFAULT_INIT_TIME + +const id QTF_ADDRESS_DEV_TEAM = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, + _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); +const id QTF_RANDOM_LOTTERY_CONTRACT_ID = id(RL_CONTRACT_INDEX, 0, 0, 0); +constexpr uint64 QTF_RANDOM_LOTTERY_ASSET_NAME = 19538; // RL +const id QTF_RESERVE_POOL_CONTRACT_ID = id(QRP_CONTRACT_INDEX, 0, 0, 0); + +struct QTF2 +{ +}; + +struct QTF : ContractBase +{ + enum class EReturnCode : uint8 + { + SUCCESS, + INVALID_TICKET_PRICE, + MAX_PLAYERS_REACHED, + ACCESS_DENIED, + INVALID_NUMBERS, + INVALID_VALUE, + TICKET_SELLING_CLOSED, + + MAX_VALUE = UINT8_MAX + }; + + static constexpr uint8 toReturnCode(const EReturnCode& code) { return static_cast(code); }; + + enum EState : uint8 + { + STATE_NONE = 0, + STATE_SELLING = 1 << 0 + }; + + struct PlayerData + { + id player; + Array randomValues; + }; + + struct WinnerData + { + Array winners; + Array winnerValues; + uint64 winnerCounter; + uint16 epoch; + }; + + struct NextEpochData + { + void clear() + { + newTicketPrice = 0; + newTargetJackpot = 0; + newSchedule = 0; + newDrawHour = 0; + } + + void apply(QTF& state) const + { + if (newTicketPrice > 0) + { + state.ticketPrice = newTicketPrice; + } + if (newTargetJackpot > 0) + { + state.targetJackpot = newTargetJackpot; + } + if (newSchedule > 0) + { + state.schedule = newSchedule; + } + if (newDrawHour > 0) + { + state.drawHour = newDrawHour; + } + } + + uint64 newTicketPrice; + uint64 newTargetJackpot; + uint8 newSchedule; + uint8 newDrawHour; + }; + + struct PoolsSnapshot + { + uint64 jackpot; + uint64 reserve; // Available reserve from QRP (not including locked amounts) + uint64 targetJackpot; + uint8 frActive; + uint16 roundsSinceK4; + }; + + struct PoolsSnapshot_input + { + }; + + struct PoolsSnapshot_output + { + PoolsSnapshot pools; + }; + + // ValidateNumbers: Check if all numbers are valid [1..30] and unique + struct ValidateNumbers_input + { + Array numbers; // Numbers to validate + }; + struct ValidateNumbers_output + { + bit isValid; // true if all numbers valid and unique + }; + struct ValidateNumbers_locals + { + HashSet seen; + uint8 idx; + uint8 value; + }; + + // Buy Ticket + struct BuyTicket_input + { + Array randomValues; + }; + struct BuyTicket_output + { + uint8 returnCode; + }; + struct BuyTicket_locals + { + // CALL parameters for ValidateNumbers + ValidateNumbers_input validateInput; + ValidateNumbers_output validateOutput; + uint64 excess; + }; + + // Set Price + struct SetPrice_input + { + uint64 newPrice; + }; + struct SetPrice_output + { + uint8 returnCode; + }; + + // Set Schedule + struct SetSchedule_input + { + uint8 newSchedule; + }; + struct SetSchedule_output + { + uint8 returnCode; + }; + + // Set draw hour + struct SetDrawHour_input + { + uint8 newDrawHour; + }; + struct SetDrawHour_output + { + uint8 returnCode; + }; + + // Set Target Jackpot + struct SetTargetJackpot_input + { + uint64 newTargetJackpot; + }; + struct SetTargetJackpot_output + { + uint8 returnCode; + }; + + // Return All Tickets (refund all players) + struct ReturnAllTickets_input + { + }; + struct ReturnAllTickets_output + { + }; + struct ReturnAllTickets_locals + { + uint64 i; // Loop counter for mass-refund + }; + + // Check Contract Balance + struct CheckContractBalance_input + { + uint64 expectedRevenue; // Expected revenue to compare against balance + }; + struct CheckContractBalance_output + { + bit hasEnough; // true if balance >= expectedRevenue + uint64 actualBalance; // Current contract balance + }; + struct CheckContractBalance_locals + { + Entity entity; + }; + + // Calculate Base Gain (FR base carry growth estimation, excluding extra deficit-driven redirect) + struct CalculateBaseGain_input + { + uint64 revenue; // Round revenue (N * ticketPrice) + uint64 winnersBlock; // 68% of revenue allocated to winners + }; + struct CalculateBaseGain_output + { + uint64 baseGain; // Estimated carry gain in qu + }; + struct CalculateBaseGain_locals + { + uint64 devRedirect; + uint64 distRedirect; + uint64 winnersRake; + uint64 estimatedOverflow; + uint64 overflowToCarry; + }; + + // PowerFixedPoint: Computes (base^exp) in fixed-point arithmetic + struct PowerFixedPoint_input + { + uint64 base; // Base value in fixed-point (scaled by QTF_FIXED_POINT_SCALE) + uint64 exp; // Exponent (integer) + }; + struct PowerFixedPoint_output + { + uint64 result; // base^exp in fixed-point + }; + struct PowerFixedPoint_locals + { + uint64 tmpBase; + uint64 expCopy; // Copy of exp (modified during computation) + }; + + // CalculateExpectedRoundsToK4: E_k4(N) = 1 / (1 - (1-p4)^N) + struct CalculateExpectedRoundsToK4_input + { + uint64 N; // Number of tickets + }; + struct CalculateExpectedRoundsToK4_output + { + uint64 expectedRounds; // E_k4(N) in fixed-point + }; + struct CalculateExpectedRoundsToK4_locals + { + uint64 oneMinusP4; + uint64 pow1mP4N; + uint64 denomFP; + PowerFixedPoint_input pfInput; + PowerFixedPoint_output pfOutput; + }; + + // Calculate Extra Redirect BP (deficit-driven) + struct CalculateExtraRedirectBP_input + { + uint64 N; // Number of tickets + uint64 delta; // Deficit to target jackpot + uint64 revenue; // Round revenue + uint64 baseGain; // Base carry gain per round (without extra) + }; + struct CalculateExtraRedirectBP_output + { + uint64 extraBP; // Extra redirect in basis points (total, to be split 50/50 Dev/Dist) + }; + struct CalculateExtraRedirectBP_locals + { + uint64 horizonFP; + uint64 horizon; + uint64 requiredGainPerRound; + uint64 gNeed; + uint64 extraBPTemp; + CalculateExpectedRoundsToK4_input calcE4Input; + CalculateExpectedRoundsToK4_output calcE4Output; + }; + + // GetRandomValues: Generate 4 unique random values from [1..30] + struct GetRandomValues_input + { + uint64 seed; // Random seed from K12 + }; + struct GetRandomValues_output + { + Array values; // 4 unique random values [1..30] + }; + struct GetRandomValues_locals + { + uint64 tempValue; + uint8 index; + uint8 candidate; + uint8 attempts; + uint8 fallback; + HashSet used; + }; + + // CalcReserveTopUp: Calculate safe reserve top-up amount + struct CalcReserveTopUp_input + { + uint64 totalQRPBalance; // Actual QRP balance (for 10% limit and soft floor) + uint64 needed; + uint64 perWinnerCapTotal; + uint64 ticketPrice; + }; + struct CalcReserveTopUp_output + { + uint64 topUpAmount; + }; + struct CalcReserveTopUp_locals + { + uint64 softFloor; + uint64 availableAboveFloor; + uint64 maxPerRound; + }; + + // ProcessTierPayout: Unified tier payout processing (k2/k3) + struct ProcessTierPayout_input + { + uint64 floorPerWinner; // Floor payout per winner (0.5*P for k2, 5*P for k3) + uint64 winnerCount; // Number of winners in this tier + uint64 payoutPool; // Initial payout pool for this tier + uint64 perWinnerCap; // Per-winner cap (25*P) + uint64 totalQRPBalance; // QRP balance for safety limits + uint64 ticketPrice; // Current ticket price + }; + struct ProcessTierPayout_output + { + uint64 perWinnerPayout; // Calculated per-winner payout + uint64 overflow; // Overflow amount (unused funds) + uint64 topUpReceived; // Amount received from QRP top-up + }; + struct ProcessTierPayout_locals + { + uint64 floorTotalNeeded; + uint64 finalPool; + uint64 qrpRequested; + CalcReserveTopUp_input calcTopUpInput; + CalcReserveTopUp_output calcTopUpOutput; + QRP::WithdrawReserve_input qrpGetReserveInput; + QRP::WithdrawReserve_output qrpGetReserveOutput; + }; + + // Ticket Price + struct GetTicketPrice_input + { + }; + struct GetTicketPrice_output + { + uint64 ticketPrice; + }; + + // Next Epoch Data + struct GetNextEpochData_input + { + }; + struct GetNextEpochData_output + { + NextEpochData nextEpochData; + }; + + // Schedule + struct GetSchedule_input + { + }; + struct GetSchedule_output + { + uint8 schedule; + }; + + // Winner Data + struct GetWinnerData_input + { + }; + + struct GetWinnerData_output + { + WinnerData winnerData; + }; + + // Pools + struct GetPools_input + { + }; + struct GetPools_output + { + PoolsSnapshot pools; + }; + struct GetPools_locals + { + QRP::GetAvailableReserve_input qrpInput; + QRP::GetAvailableReserve_output qrpOutput; + }; + + // Draw hour + struct GetDrawHour_input + { + }; + struct GetDrawHour_output + { + uint8 drawHour; + }; + + struct GetState_input + { + }; + struct GetState_output + { + uint8 currentState; + }; + + struct SettleEpoch_input + { + }; + + struct SettleEpoch_output + { + }; + + struct CountMatches_input + { + Array playerValues; + Array winningValues; + }; + + struct CountMatches_output + { + uint8 matches; + }; + struct CountMatches_locals + { + uint64 i; + uint32 maskA; + uint32 maskB; + uint8 randomValue; + }; + + struct GetFees_input + { + }; + + struct GetFees_output + { + uint8 teamFeePercent; // Team share in percent + uint8 distributionFeePercent; // Distribution/shareholders share in percent + uint8 winnerFeePercent; // Winner share in percent + uint8 burnPercent; // Burn share in percent + uint8 returnCode; + }; + + struct GetFees_locals + { + RL::GetFees_input feesInput; + RL::GetFees_output feesOutput; + }; + + // EstimateFRJackpotGrowth: Calculate minimum expected jackpot growth in FR mode + // Used for testing to verify 95% overflow bias is working correctly + struct EstimateFRJackpotGrowth_input + { + uint64 revenue; // Total revenue (ticketPrice * numPlayers) + uint64 winnersPercent; // Winners block percentage (typically 68) + }; + struct EstimateFRJackpotGrowth_output + { + uint64 minJackpotGrowth; // Minimum expected jackpot growth + uint64 winnersRake; // 5% of winners block + uint64 overflowToJackpot; // 95% of overflow + uint64 devRedirect; // 1% of revenue + uint64 distRedirect; // 1% of revenue + }; + struct EstimateFRJackpotGrowth_locals + { + uint64 winnersBlock; + uint64 winnersBlockAfterRake; + uint64 k3Pool; + uint64 k2Pool; + uint64 winnersOverflow; + uint64 reserveAdd; + }; + + // CalculatePrizePools: Calculate k2/k3 prize pools from revenue + // Reusable function for both settlement and estimation + struct CalculatePrizePools_input + { + uint64 revenue; // Total revenue (ticketPrice * numberOfPlayers) + bit applyFRRake; // Whether to apply 5% FR rake + }; + struct CalculatePrizePools_output + { + uint64 winnersBlock; // Winners block after fees + uint64 winnersRake; // 5% rake (if FR active) + uint64 k2Pool; // 28% of winners block (after rake) + uint64 k3Pool; // 40% of winners block (after rake) + }; + struct CalculatePrizePools_locals + { + GetFees_input feesInput; + GetFees_output feesOutput; + uint64 winnersBlockBeforeRake; + }; + + // EstimatePrizePayouts: Calculate estimated prize payouts for k=2 and k=3 tiers + // Based on current ticket sales and number of winners per tier + struct EstimatePrizePayouts_input + { + uint64 k2WinnerCount; // Number of k=2 winners (estimated or actual) + uint64 k3WinnerCount; // Number of k=3 winners (estimated or actual) + }; + struct EstimatePrizePayouts_output + { + uint64 k2PayoutPerWinner; // Estimated payout per k=2 winner + uint64 k3PayoutPerWinner; // Estimated payout per k=3 winner + uint64 k2MinFloor; // Minimum guaranteed payout for k=2 (0.5*P) + uint64 k3MinFloor; // Minimum guaranteed payout for k=3 (5*P) + uint64 perWinnerCap; // Maximum payout per winner (25*P) + uint64 totalRevenue; // Total revenue from ticket sales + uint64 k2Pool; // Total pool for k=2 tier + uint64 k3Pool; // Total pool for k=3 tier + }; + struct EstimatePrizePayouts_locals + { + uint64 revenue; + uint64 k2FloorTotal; + uint64 k3FloorTotal; + uint64 k2PayoutPoolEffective; + uint64 k3PayoutPoolEffective; + CalculatePrizePools_input calcPoolsInput; + CalculatePrizePools_output calcPoolsOutput; + }; + + struct SettleEpoch_locals + { + Array winningValues; + ReturnAllTickets_input returnAllTicketsInput; + ReturnAllTickets_output returnAllTicketsOutput; + ReturnAllTickets_locals returnAllTicketsLocals; + CheckContractBalance_input checkBalanceInput; + CheckContractBalance_output checkBalanceOutput; + CheckContractBalance_locals checkBalanceLocals; + CountMatches_input countMatchesInput; + CountMatches_output countMatchesOutput; + uint16 currentEpoch; + uint64 revenue; // ticketPrice * players count + uint64 winnersBlock; + uint64 k2Pool; + uint64 k3Pool; + uint64 carryAdd; + uint64 reserveAdd; + uint64 winnersOverflow; + uint64 devPayout; // Dev after redirects + uint64 distPayout; // Distribution after redirects + uint64 burnAmount; + uint64 devRedirect; + uint64 distRedirect; + uint64 winnersRake; + uint64 k2PayoutPool; + uint64 k3PayoutPool; + uint64 k2PerWinner; + uint64 k3PerWinner; + uint64 countK2; + uint64 countK3; + uint64 countK4; + uint64 totalDevRedirectBP; // Total dev redirect in basis points (base + extra) + uint64 totalDistRedirectBP; // Total dist redirect in basis points (base + extra) + uint64 perWinnerCap; // Per-winner payout cap (25*P) + uint64 jackpotPerK4Winner; // Jackpot share per k4 winner + uint64 totalJackpotContribution; // Total amount to add to jackpot + uint64 i; + uint8 matches; + bit shouldActivateFR; + // Deficit-driven extra redirect calculation + uint64 delta; // Deficit: max(0, targetJackpot - jackpot) + uint64 devExtraBP; // Dev share of extra: extraRedirectBP / 2 + uint64 distExtraBP; // Dist share of extra: extraRedirectBP / 2 + // CALL parameters for CalculatePrizePools (shared function) + CalculatePrizePools_input calcPoolsInput; + CalculatePrizePools_output calcPoolsOutput; + // CALL parameters for CalculateBaseGain + CalculateBaseGain_input calcBaseGainInput; + CalculateBaseGain_output calcBaseGainOutput; + // CALL parameters for CalculateExtraRedirectBP + CalculateExtraRedirectBP_input calcExtraInput; + CalculateExtraRedirectBP_output calcExtraOutput; + // CALL parameters for GetRandomValues + GetRandomValues_input getRandomInput; + GetRandomValues_output getRandomOutput; + // CALL parameters for ProcessTierPayout (unified k2/k3 processing) + ProcessTierPayout_input tierPayoutInput; + ProcessTierPayout_output tierPayoutOutput; + // CALL_OTHER_CONTRACT parameters for QRP (external reserve pool) + QRP::WithdrawReserve_input qrpGetReserveInput; + QRP::WithdrawReserve_output qrpGetReserveOutput; + QRP::GetAvailableReserve_input qrpGetAvailableInput; + QRP::GetAvailableReserve_output qrpGetAvailableOutput; + uint64 qrpRequested; // Amount requested from QRP + uint64 qrpReceived; // Amount actually received from QRP + uint64 totalQRPBalance; // Total balance in QRP (for safety limits) + GetFees_input feesInput; + GetFees_output feesOutput; + uint64 dividendPerShare; + Asset rlAsset; + AssetPossessionIterator rlIter; + uint64 rlTotalShares; + uint64 rlPayback; + uint64 rlShares; + // Cache for countMatches results to avoid redundant calculations + Array cachedMatches; + }; + + struct END_EPOCH_locals + { + SettleEpoch_locals settlement; + SettleEpoch_input settleInput; + SettleEpoch_output settleOutput; + }; + + struct BEGIN_TICK_locals + { + SettleEpoch_input settleInput; + SettleEpoch_output settleOutput; + uint32 currentDateStamp; + uint8 currentDayOfWeek; + uint8 currentHour; + bit isWednesday; + bit isScheduledToday; + }; + + // Contract lifecycle methods + INITIALIZE() + { + state.teamAddress = QTF_ADDRESS_DEV_TEAM; + state.ownerAddress = state.teamAddress; + state.ticketPrice = QTF_TICKET_PRICE; + state.targetJackpot = QTF_DEFAULT_TARGET_JACKPOT; + state.overflowAlphaBP = QTF_BASELINE_OVERFLOW_ALPHA_BP; + state.schedule = QTF_DEFAULT_SCHEDULE; + state.drawHour = QTF_DEFAULT_DRAW_HOUR; + state.lastDrawDateStamp = QTF_DEFAULT_INIT_TIME; + state.currentState = STATE_NONE; + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(BuyTicket, 1); + REGISTER_USER_PROCEDURE(SetPrice, 2); + REGISTER_USER_PROCEDURE(SetSchedule, 3); + REGISTER_USER_PROCEDURE(SetTargetJackpot, 4); + REGISTER_USER_PROCEDURE(SetDrawHour, 5); + REGISTER_USER_FUNCTION(GetTicketPrice, 1); + REGISTER_USER_FUNCTION(GetNextEpochData, 2); + REGISTER_USER_FUNCTION(GetWinnerData, 3); + REGISTER_USER_FUNCTION(GetPools, 4); + REGISTER_USER_FUNCTION(GetSchedule, 5); + REGISTER_USER_FUNCTION(GetDrawHour, 6); + REGISTER_USER_FUNCTION(GetState, 7); + REGISTER_USER_FUNCTION(GetFees, 8); + REGISTER_USER_FUNCTION(EstimatePrizePayouts, 9); + } + + BEGIN_EPOCH() + { + applyNextEpochData(state); + + if (state.schedule == 0) + { + state.schedule = QTF_DEFAULT_SCHEDULE; + } + if (state.drawHour == 0) + { + state.drawHour = QTF_DEFAULT_DRAW_HOUR; + } + RL::makeDateStamp(qpi.year(), qpi.month(), qpi.day(), state.lastDrawDateStamp); + clearEpochState(state); + enableBuyTicket(state, state.lastDrawDateStamp != RL_DEFAULT_INIT_TIME); + } + + // Settle and reset at epoch end (uses locals buffer) + END_EPOCH_WITH_LOCALS() + { + enableBuyTicket(state, false); + CALL(SettleEpoch, locals.settleInput, locals.settleOutput); + clearEpochState(state); + } + + // Scheduled draw processor + BEGIN_TICK_WITH_LOCALS() + { + // Throttle: run logic only once per RL_TICK_UPDATE_PERIOD ticks + if (mod(qpi.tick(), static_cast(RL_TICK_UPDATE_PERIOD)) != 0) + { + return; + } + + locals.currentHour = qpi.hour(); + if (locals.currentHour < state.drawHour) + { + return; + } + + locals.currentDayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + RL::makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + + // Wait for valid time initialization + if (locals.currentDateStamp == QTF_DEFAULT_INIT_TIME) + { + enableBuyTicket(state, false); + state.lastDrawDateStamp = QTF_DEFAULT_INIT_TIME; + return; + } + + // First valid date after init: just record and exit + if (state.lastDrawDateStamp == QTF_DEFAULT_INIT_TIME && locals.currentDateStamp != QTF_DEFAULT_INIT_TIME) + { + enableBuyTicket(state, true); + if (locals.currentDayOfWeek == WEDNESDAY) + { + state.lastDrawDateStamp = locals.currentDateStamp; + } + else + { + state.lastDrawDateStamp = 0; + } + + return; + } + + if (locals.currentDateStamp == state.lastDrawDateStamp) + { + return; + } + + locals.isWednesday = (locals.currentDayOfWeek == WEDNESDAY); + locals.isScheduledToday = ((state.schedule & (1u << locals.currentDayOfWeek)) != 0); + + // Always draw on Wednesday; otherwise require schedule bit. + if (!locals.isWednesday && !locals.isScheduledToday) + { + return; + } + + state.lastDrawDateStamp = locals.currentDateStamp; + + // Pause selling during draw/settlement. + enableBuyTicket(state, false); + + CALL(SettleEpoch, locals.settleInput, locals.settleOutput); + clearEpochState(state); + applyNextEpochData(state); + enableBuyTicket(state, !locals.isWednesday); + } + + POST_INCOMING_TRANSFER() + { + switch (input.type) + { + case TransferType::standardTransaction: + // Return any funds sent via standard transaction + if (input.amount > 0) + { + qpi.transfer(input.sourceId, input.amount); + } + break; + default: break; + } + } + + // Procedures + PUBLIC_PROCEDURE_WITH_LOCALS(BuyTicket) + { + if ((state.currentState & STATE_SELLING) == 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.returnCode = toReturnCode(EReturnCode::TICKET_SELLING_CLOSED); + return; + } + + if (state.numberOfPlayers >= QTF_MAX_NUMBER_OF_PLAYERS) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.returnCode = toReturnCode(EReturnCode::MAX_PLAYERS_REACHED); + return; + } + + if (qpi.invocationReward() < static_cast(state.ticketPrice)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.returnCode = toReturnCode(EReturnCode::INVALID_TICKET_PRICE); + return; + } + + locals.validateInput.numbers = input.randomValues; + CALL(ValidateNumbers, locals.validateInput, locals.validateOutput); + if (!locals.validateOutput.isValid) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = toReturnCode(EReturnCode::INVALID_NUMBERS); + return; + } + + addPlayerInfo(state, qpi.invocator(), input.randomValues); + + // If overpaid, accept ticket and return excess to invocator. + // Important: refund excess ONLY after validation, otherwise invalid tickets could be over-refunded. + if (qpi.invocationReward() > static_cast(state.ticketPrice)) + { + locals.excess = qpi.invocationReward() - state.ticketPrice; + if (locals.excess > 0) + { + qpi.transfer(qpi.invocator(), locals.excess); + } + } + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE(SetPrice) + { + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + if (input.newPrice == 0) + { + output.returnCode = toReturnCode(EReturnCode::INVALID_TICKET_PRICE); + return; + } + + state.nextEpochData.newTicketPrice = input.newPrice; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE(SetSchedule) + { + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + if (input.newSchedule == 0) + { + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + return; + } + + state.nextEpochData.newSchedule = input.newSchedule; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE(SetTargetJackpot) + { + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + if (input.newTargetJackpot == 0) + { + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + return; + } + + state.nextEpochData.newTargetJackpot = input.newTargetJackpot; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE(SetDrawHour) + { + if (qpi.invocator() != state.ownerAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + if (input.newDrawHour == 0 || input.newDrawHour > 23) + { + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + return; + } + + state.nextEpochData.newDrawHour = input.newDrawHour; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + // Functions + PUBLIC_FUNCTION(GetTicketPrice) { output.ticketPrice = state.ticketPrice; } + PUBLIC_FUNCTION(GetNextEpochData) { output.nextEpochData = state.nextEpochData; } + PUBLIC_FUNCTION(GetWinnerData) { output.winnerData = state.lastWinnerData; } + PUBLIC_FUNCTION_WITH_LOCALS(GetPools) + { + output.pools.jackpot = state.jackpot; + CALL_OTHER_CONTRACT_FUNCTION(QRP, GetAvailableReserve, locals.qrpInput, locals.qrpOutput); + output.pools.reserve = locals.qrpOutput.availableReserve; + output.pools.targetJackpot = state.targetJackpot; + output.pools.frActive = state.frActive; + output.pools.roundsSinceK4 = state.frRoundsSinceK4; + } + PUBLIC_FUNCTION(GetSchedule) { output.schedule = state.schedule; } + PUBLIC_FUNCTION(GetDrawHour) { output.drawHour = state.drawHour; } + PUBLIC_FUNCTION(GetState) { output.currentState = static_cast(state.currentState); } + PUBLIC_FUNCTION_WITH_LOCALS(GetFees) + { + CALL_OTHER_CONTRACT_FUNCTION(RL, GetFees, locals.feesInput, locals.feesOutput); + output.teamFeePercent = locals.feesOutput.teamFeePercent; + output.distributionFeePercent = locals.feesOutput.distributionFeePercent; + output.burnPercent = locals.feesOutput.burnPercent; + output.winnerFeePercent = locals.feesOutput.winnerFeePercent; + + // Sanity check: if RL returns invalid fees, use defaults + if (output.teamFeePercent == 0 || output.distributionFeePercent == 0 || output.winnerFeePercent == 0) + { + output.teamFeePercent = QTF_DEFAULT_DEV_PERCENT; + output.distributionFeePercent = QTF_DEFAULT_DIST_PERCENT; + output.burnPercent = QTF_DEFAULT_BURN_PERCENT; + output.winnerFeePercent = QTF_DEFAULT_WINNERS_PERCENT; + } + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_FUNCTION_WITH_LOCALS(EstimatePrizePayouts) + { + // Calculate total revenue from current ticket sales + locals.revenue = smul(state.ticketPrice, state.numberOfPlayers); + output.totalRevenue = locals.revenue; + + // Set minimum floors and cap + output.k2MinFloor = div(smul(state.ticketPrice, QTF_K2_FLOOR_MULT), QTF_K2_FLOOR_DIV); // 0.5*P + output.k3MinFloor = smul(state.ticketPrice, QTF_K3_FLOOR_MULT); // 5*P + output.perWinnerCap = smul(state.ticketPrice, QTF_TOPUP_PER_WINNER_CAP_MULT); // 25*P + + if (locals.revenue == 0 || state.numberOfPlayers == 0) + { + // No tickets sold, no payouts + output.k2PayoutPerWinner = 0; + output.k3PayoutPerWinner = 0; + output.k2Pool = 0; + output.k3Pool = 0; + return; + } + + // Use shared CalculatePrizePools function to compute pools + locals.calcPoolsInput.revenue = locals.revenue; + locals.calcPoolsInput.applyFRRake = state.frActive; + CALL(CalculatePrizePools, locals.calcPoolsInput, locals.calcPoolsOutput); + + output.k2Pool = locals.calcPoolsOutput.k2Pool; + output.k3Pool = locals.calcPoolsOutput.k3Pool; + + // Calculate k2 payout per winner + if (input.k2WinnerCount > 0) + { + locals.k2FloorTotal = smul(output.k2MinFloor, input.k2WinnerCount); + locals.k2PayoutPoolEffective = output.k2Pool; + + // Note: This is an estimate - actual implementation may top up from reserve + // If pool insufficient, we show floor; otherwise calculate actual per-winner amount + if (locals.k2PayoutPoolEffective >= locals.k2FloorTotal) + { + output.k2PayoutPerWinner = RL::min(output.perWinnerCap, div(locals.k2PayoutPoolEffective, input.k2WinnerCount)); + } + else + { + // Pool insufficient, would need reserve top-up - show floor as estimate + output.k2PayoutPerWinner = output.k2MinFloor; + } + } + else + { + // No winners - show what a single winner would get + output.k2PayoutPerWinner = RL::min(output.perWinnerCap, output.k2Pool); + } + + // Calculate k3 payout per winner + if (input.k3WinnerCount > 0) + { + locals.k3FloorTotal = smul(output.k3MinFloor, input.k3WinnerCount); + locals.k3PayoutPoolEffective = output.k3Pool; + + // Note: This is an estimate - actual implementation may top up from reserve + if (locals.k3PayoutPoolEffective >= locals.k3FloorTotal) + { + output.k3PayoutPerWinner = RL::min(output.perWinnerCap, div(locals.k3PayoutPoolEffective, input.k3WinnerCount)); + } + else + { + // Pool insufficient, would need reserve top-up - show floor as estimate + output.k3PayoutPerWinner = output.k3MinFloor; + } + } + else + { + // No winners - show what a single winner would get + output.k3PayoutPerWinner = RL::min(output.perWinnerCap, output.k3Pool); + } + } + +protected: + static void clearEpochState(QTF& state) { clearPlayerData(state); } + + static void applyNextEpochData(QTF& state) + { + state.nextEpochData.apply(state); + state.nextEpochData.clear(); + } + + static void enableBuyTicket(QTF& state, bool bEnable) + { + if (bEnable) + { + state.currentState = static_cast(state.currentState | STATE_SELLING); + } + else + { + state.currentState = static_cast(state.currentState & static_cast(~STATE_SELLING)); + } + } + + // ========== Helper static functions ========== + + static void mix64(const uint64& x, uint64& outValue) + { + outValue = x; + outValue ^= outValue >> 30; + outValue *= 0xbf58476d1ce4e5b9ULL; + outValue ^= outValue >> 27; + outValue *= 0x94d049bb133111ebULL; + outValue ^= outValue >> 31; + } + + static void deriveOne(const uint64& r, const uint64& idx, uint64& outValue) { mix64(r + 0x9e3779b97f4a7c15ULL * (idx + 1), outValue); } + + static void addPlayerInfo(QTF& state, const id& playerId, const Array& randomValues) + { + state.players.set(state.numberOfPlayers++, {playerId, randomValues}); + } + + static uint8 bitcount32(uint32 v) + { + v = v - ((v >> 1) & 0x55555555u); + v = (v & 0x33333333u) + ((v >> 2) & 0x33333333u); + v = (v + (v >> 4)) & 0x0F0F0F0Fu; + v = v + (v >> 8); + v = v + (v >> 16); + return static_cast(v & 0x3Fu); + } + + static void clearPlayerData(QTF& state) + { + if (state.numberOfPlayers > 0) + { + setMemory(state.players, 0); + state.numberOfPlayers = 0; + } + } + + static void clearWinerData(QTF& state) { setMemory(state.lastWinnerData, 0); } + + static void fillWinnerData(QTF& state, const PlayerData& playerData, const Array& winnerValues, + const uint16& epoch) + { + if (!isZero(playerData.player)) + { + if (state.lastWinnerData.winnerCounter < state.lastWinnerData.winners.capacity()) + { + state.lastWinnerData.winners.set(state.lastWinnerData.winnerCounter++, playerData); + } + } + + state.lastWinnerData.winnerValues = winnerValues; + state.lastWinnerData.epoch = epoch; + } + + WinnerData lastWinnerData; // last winners snapshot + + NextEpochData nextEpochData; // queued config (ticket price) + + Array players; // current epoch tickets + + id teamAddress; // Dev/team payout address + + id ownerAddress; // config authority + + uint64 numberOfPlayers; // tickets count in epoch + + uint64 ticketPrice; // active ticket price + + uint64 jackpot; // jackpot balance + + uint64 targetJackpot; // FR target jackpot + + uint64 overflowAlphaBP; // baseline reserve share of overflow (bp) + + uint8 schedule; // bitmask of draw days + + uint8 drawHour; // draw hour UTC + + uint32 lastDrawDateStamp; // guard to avoid multiple draws per day + + bit frActive; // FR flag + + uint16 frRoundsSinceK4; // rounds since last jackpot hit + + uint16 frRoundsAtOrAboveTarget; // hysteresis counter for FR off + + uint8 currentState; // bitmask of STATE_* flags (e.g., STATE_SELLING) + +private: + // Core settlement pipeline for one epoch: fees, FR redirects, payouts, jackpot/reserve updates. + PRIVATE_PROCEDURE_WITH_LOCALS(SettleEpoch) + { + if (state.numberOfPlayers == 0) + { + return; + } + + locals.currentEpoch = qpi.epoch(); + locals.revenue = smul(state.ticketPrice, state.numberOfPlayers); + if (locals.revenue == 0) + { + CALL(ReturnAllTickets, locals.returnAllTicketsInput, locals.returnAllTicketsOutput); + clearPlayerData(state); + + return; + } + + // Check if contract has sufficient balance for settlement + locals.checkBalanceInput.expectedRevenue = locals.revenue; + CALL(CheckContractBalance, locals.checkBalanceInput, locals.checkBalanceOutput); + if (!locals.checkBalanceOutput.hasEnough) + { + // Insufficient balance: refund all players and abort settlement + CALL(ReturnAllTickets, locals.returnAllTicketsInput, locals.returnAllTicketsOutput); + clearPlayerData(state); + + return; + } + + CALL(GetFees, locals.feesInput, locals.feesOutput); + + locals.devPayout = div(smul(locals.revenue, static_cast(locals.feesOutput.teamFeePercent)), 100); + locals.distPayout = div(smul(locals.revenue, static_cast(locals.feesOutput.distributionFeePercent)), 100); + locals.burnAmount = div(smul(locals.revenue, static_cast(locals.feesOutput.burnPercent)), 100); + + // FR detection and hysteresis logic. + // Update hysteresis counter BEFORE activation check so deactivation can occur + // immediately when reaching the threshold (3 consecutive rounds at/above target). + if (state.jackpot >= state.targetJackpot) + { + state.frRoundsAtOrAboveTarget = sadd(state.frRoundsAtOrAboveTarget, 1); + } + else + { + state.frRoundsAtOrAboveTarget = 0; + } + + // FR Activation/Deactivation logic (deficit-driven, no hard N threshold) + // Activation: when carry < target AND within post-k4 window (adaptive) + // Deactivation (hysteresis): after carry >= target for 3+ rounds + locals.shouldActivateFR = (state.jackpot < state.targetJackpot) && (state.frRoundsSinceK4 < QTF_FR_POST_K4_WINDOW_ROUNDS); + if (locals.shouldActivateFR) + { + state.frActive = true; + } + else if (state.frRoundsSinceK4 >= QTF_FR_POST_K4_WINDOW_ROUNDS) + { + // Outside post-k4 window: FR must be OFF. + state.frActive = false; + } + else if (state.frRoundsAtOrAboveTarget >= QTF_FR_HYSTERESIS_ROUNDS) + { + // Deactivate FR after target held for hysteresis rounds + state.frActive = false; + } + + // Calculate prize pools using shared function (handles FR rake if active) + locals.calcPoolsInput.revenue = locals.revenue; + locals.calcPoolsInput.applyFRRake = state.frActive; + CALL(CalculatePrizePools, locals.calcPoolsInput, locals.calcPoolsOutput); + + locals.winnersBlock = locals.calcPoolsOutput.winnersBlock; + locals.winnersRake = locals.calcPoolsOutput.winnersRake; + locals.k2Pool = locals.calcPoolsOutput.k2Pool; + locals.k3Pool = locals.calcPoolsOutput.k3Pool; + + // Calculate initial overflow: unallocated funds after k2/k3 allocation (32% baseline) + // Additional unawarded k2/k3 funds will be added to this after tier processing + locals.winnersOverflow = locals.winnersBlock - locals.k3Pool - locals.k2Pool; + + // Fast-Recovery (FR) mode: redirect portions of Dev/Distribution to jackpot with deficit-driven extra. + // Base redirect is always 1% Dev + 1% Dist when FR=ON. + // Extra redirect is calculated dynamically based on deficit, expected k4 timing, and ticket volume. + if (state.frActive) + { + // Calculate deficit to target jackpot + locals.delta = (state.jackpot < state.targetJackpot) ? (state.targetJackpot - state.jackpot) : 0; + + // Estimate base gain from existing FR mechanisms (without extra) + locals.calcBaseGainInput.revenue = locals.revenue; + locals.calcBaseGainInput.winnersBlock = locals.calcPoolsOutput.winnersBlock; + CALL(CalculateBaseGain, locals.calcBaseGainInput, locals.calcBaseGainOutput); + + // Calculate deficit-driven extra redirect in basis points + locals.calcExtraInput.N = state.numberOfPlayers; + locals.calcExtraInput.delta = locals.delta; + locals.calcExtraInput.revenue = locals.revenue; + locals.calcExtraInput.baseGain = locals.calcBaseGainOutput.baseGain; + CALL(CalculateExtraRedirectBP, locals.calcExtraInput, locals.calcExtraOutput); + + // Split extra equally between Dev and Dist + locals.devExtraBP = div(locals.calcExtraOutput.extraBP, 2); + locals.distExtraBP = locals.calcExtraOutput.extraBP - locals.devExtraBP; // Handle odd remainder + + // Total redirect BP = base + extra + locals.totalDevRedirectBP = sadd(QTF_FR_DEV_REDIRECT_BP, locals.devExtraBP); + locals.totalDistRedirectBP = sadd(QTF_FR_DIST_REDIRECT_BP, locals.distExtraBP); + + // Calculate redirect amounts + locals.devRedirect = div(smul(locals.revenue, locals.totalDevRedirectBP), 10000); + locals.distRedirect = div(smul(locals.revenue, locals.totalDistRedirectBP), 10000); + + // Deduct redirects from payouts (capped at available amounts) + if (locals.devPayout > locals.devRedirect) + { + locals.devPayout -= locals.devRedirect; + } + else + { + locals.devRedirect = locals.devPayout; + locals.devPayout = 0; + } + + if (locals.distPayout > locals.distRedirect) + { + locals.distPayout -= locals.distRedirect; + } + else + { + locals.distRedirect = locals.distPayout; + locals.distPayout = 0; + } + } + + locals.k2PayoutPool = locals.k2Pool; // mutable pools after top-ups + locals.k3PayoutPool = locals.k3Pool; + + // Reset last-winner snapshot for this settlement (per-round view). + clearWinerData(state); + + // Generate winning random values using CALL + locals.getRandomInput.seed = qpi.K12(qpi.getPrevSpectrumDigest()).u64._0; + CALL(GetRandomValues, locals.getRandomInput, locals.getRandomOutput); + locals.winningValues = locals.getRandomOutput.values; + + // First pass: count matches and cache results for second pass + locals.i = 0; + while (locals.i < state.numberOfPlayers) + { + locals.countMatchesInput.playerValues = state.players.get(locals.i).randomValues; + locals.countMatchesInput.winningValues = locals.winningValues; + CALL(CountMatches, locals.countMatchesInput, locals.countMatchesOutput); + + locals.cachedMatches.set(locals.i, locals.countMatchesOutput.matches); // Cache result + + if (locals.countMatchesOutput.matches == 2) + { + ++locals.countK2; + } + else if (locals.countMatchesOutput.matches == 3) + { + ++locals.countK3; + } + else if (locals.countMatchesOutput.matches == 4) + { + ++locals.countK4; + } + ++locals.i; + } + + // Minimum payout floors: ensure k2 winners get >= 0.5*P, k3 winners get >= 5*P. + // Top up from Reserve.General if pool insufficient (subject to safety limits). + // First, get total QRP balance for safety limit calculations (10% of total reserve per round). + CALL_OTHER_CONTRACT_FUNCTION(QRP, GetAvailableReserve, locals.qrpGetAvailableInput, locals.qrpGetAvailableOutput); + locals.totalQRPBalance = locals.qrpGetAvailableOutput.availableReserve; + // If a k=4 win happened this round, the jackpot reseed must not be blocked by floor top-ups. + // We emulate reserve partitioning by limiting k2/k3 top-ups to the portion of QRP above targetJackpot. + // This guarantees that if QRP had >= targetJackpot before settlement, reseed can still reach target after payouts. + if (locals.countK4 > 0) + { + if (locals.totalQRPBalance > state.targetJackpot) + { + locals.totalQRPBalance -= state.targetJackpot; + } + else + { + locals.totalQRPBalance = 0; + } + } + locals.perWinnerCap = smul(state.ticketPrice, QTF_TOPUP_PER_WINNER_CAP_MULT); // 25*P + + // Process k2 tier payout + locals.tierPayoutInput.floorPerWinner = div(smul(state.ticketPrice, QTF_K2_FLOOR_MULT), QTF_K2_FLOOR_DIV); + locals.tierPayoutInput.winnerCount = locals.countK2; + locals.tierPayoutInput.payoutPool = locals.k2PayoutPool; + locals.tierPayoutInput.perWinnerCap = locals.perWinnerCap; + locals.tierPayoutInput.totalQRPBalance = locals.totalQRPBalance; + locals.tierPayoutInput.ticketPrice = state.ticketPrice; + CALL(ProcessTierPayout, locals.tierPayoutInput, locals.tierPayoutOutput); + locals.k2PerWinner = locals.tierPayoutOutput.perWinnerPayout; + locals.winnersOverflow = sadd(locals.winnersOverflow, locals.tierPayoutOutput.overflow); + + // Process k3 tier payout + locals.tierPayoutInput.floorPerWinner = smul(state.ticketPrice, QTF_K3_FLOOR_MULT); + locals.tierPayoutInput.winnerCount = locals.countK3; + locals.tierPayoutInput.payoutPool = locals.k3PayoutPool; + locals.tierPayoutInput.perWinnerCap = locals.perWinnerCap; + locals.tierPayoutInput.totalQRPBalance = locals.totalQRPBalance; + locals.tierPayoutInput.ticketPrice = state.ticketPrice; + CALL(ProcessTierPayout, locals.tierPayoutInput, locals.tierPayoutOutput); + locals.k3PerWinner = locals.tierPayoutOutput.perWinnerPayout; + locals.winnersOverflow = sadd(locals.winnersOverflow, locals.tierPayoutOutput.overflow); + + locals.carryAdd = sadd(locals.carryAdd, locals.winnersRake); + + // Compute k=4 jackpot payout per winner once. + locals.jackpotPerK4Winner = 0; + if (locals.countK4 > 0) + { + locals.jackpotPerK4Winner = div(state.jackpot, locals.countK4); + } + + // Second pass: payout loop using cached match results (avoids redundant countMatches calls) + // (Optimization: reduces player iteration from 4 passes to 2 passes + eliminates duplicate countMatches) + locals.i = 0; + while (locals.i < state.numberOfPlayers) + { + locals.matches = locals.cachedMatches.get(locals.i); // Use cached result + + // k2 payout + if (locals.matches == 2 && locals.countK2 > 0 && locals.k2PerWinner > 0) + { + qpi.transfer(state.players.get(locals.i).player, locals.k2PerWinner); + fillWinnerData(state, state.players.get(locals.i), locals.winningValues, locals.currentEpoch); + } + // k3 payout + if (locals.matches == 3 && locals.countK3 > 0 && locals.k3PerWinner > 0) + { + qpi.transfer(state.players.get(locals.i).player, locals.k3PerWinner); + fillWinnerData(state, state.players.get(locals.i), locals.winningValues, locals.currentEpoch); + } + // k4 payout (jackpot) + if (locals.matches == 4 && locals.countK4 > 0) + { + if (locals.jackpotPerK4Winner > 0) + { + qpi.transfer(state.players.get(locals.i).player, locals.jackpotPerK4Winner); + } + fillWinnerData(state, state.players.get(locals.i), locals.winningValues, locals.currentEpoch); + } + + ++locals.i; + } + + // Always save winning values and epoch, even if no winners + state.lastWinnerData.winnerValues = locals.winningValues; + state.lastWinnerData.epoch = locals.currentEpoch; + + // Post-jackpot (k4) logic: reset counters and reseed if jackpot was hit + if (locals.countK4 > 0) + { + // Jackpot was paid out in combined loop above, now deplete it + state.jackpot = 0; + + // Reset FR counters after jackpot hit + state.frRoundsSinceK4 = 0; + state.frRoundsAtOrAboveTarget = 0; + + // Reseed jackpot from QReservePool (up to targetJackpot or available reserve) + // Re-query available reserve because k2/k3 top-ups may have reduced it. + CALL_OTHER_CONTRACT_FUNCTION(QRP, GetAvailableReserve, locals.qrpGetAvailableInput, locals.qrpGetAvailableOutput); + locals.qrpRequested = RL::min(locals.qrpGetAvailableOutput.availableReserve, state.targetJackpot); + if (locals.qrpRequested > 0) + { + locals.qrpGetReserveInput.revenue = locals.qrpRequested; + INVOKE_OTHER_CONTRACT_PROCEDURE(QRP, WithdrawReserve, locals.qrpGetReserveInput, locals.qrpGetReserveOutput, 0ll); + + if (locals.qrpGetReserveOutput.returnCode == QRP::toReturnCode(QRP::EReturnCode::SUCCESS)) + { + locals.qrpReceived = locals.qrpGetReserveOutput.allocatedRevenue; + state.jackpot = sadd(state.jackpot, locals.qrpReceived); + } + } + } + else + { + // No jackpot hit: increment rounds counter for FR post-k4 window tracking + ++state.frRoundsSinceK4; + } + + // Overflow split: unawarded tier funds split between reserve and jackpot. + // FR mode: 95% to jackpot, 5% to reserve (alpha=0.05) + // Baseline mode: 50/50 split (alpha=0.50) + if (locals.winnersOverflow > 0) + { + if (state.frActive) + { + locals.reserveAdd = div(smul(locals.winnersOverflow, QTF_FR_ALPHA_BP), 10000); + } + else + { + locals.reserveAdd = div(smul(locals.winnersOverflow, state.overflowAlphaBP), 10000); + } + locals.carryAdd = sadd(locals.carryAdd, (locals.winnersOverflow - locals.reserveAdd)); + } + + // Add all jackpot contributions: overflow carryAdd + FR redirects (if active) + locals.totalJackpotContribution = sadd(locals.carryAdd, sadd(locals.devRedirect, locals.distRedirect)); + state.jackpot = sadd(state.jackpot, locals.totalJackpotContribution); + + // Transfer reserve overflow to QReservePool + if (locals.reserveAdd > 0) + { + qpi.transfer(QTF_RESERVE_POOL_CONTRACT_ID, locals.reserveAdd); + } + + if (locals.devPayout > 0) + { + qpi.transfer(state.teamAddress, locals.devPayout); + } + // Manual dividend payout to RL shareholders (no extra fee). + if (locals.distPayout > 0) + { + locals.rlAsset.issuer = id::zero(); + locals.rlAsset.assetName = QTF_RANDOM_LOTTERY_ASSET_NAME; + locals.rlTotalShares = NUMBER_OF_COMPUTORS; + + if (locals.rlTotalShares > 0) + { + locals.dividendPerShare = div(locals.distPayout, locals.rlTotalShares); + if (locals.dividendPerShare > 0) + { + locals.rlIter.begin(locals.rlAsset); + while (!locals.rlIter.reachedEnd()) + { + locals.rlShares = static_cast(locals.rlIter.numberOfPossessedShares()); + if (locals.rlShares > 0) + { + qpi.transfer(locals.rlIter.possessor(), smul(locals.rlShares, locals.dividendPerShare)); + } + locals.rlIter.next(); + } + + locals.rlPayback = locals.distPayout - smul(locals.dividendPerShare, locals.rlTotalShares); + if (locals.rlPayback > 0) + { + qpi.transfer(QTF_RANDOM_LOTTERY_CONTRACT_ID, locals.rlPayback); + } + } + } + } + + if (locals.burnAmount > 0) + { + qpi.burn(locals.burnAmount); + } + } + + /** + * @brief Refunds ticket price to all players who bought tickets in the current epoch. + * + * This procedure is used to return funds to all participants, typically in cases where: + * - Revenue calculation resulted in 0 (overflow or invalid state) + * - Contract balance is insufficient for settlement + * + * Performs one transfer per player entry. After refund, caller should reset numberOfPlayers. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ReturnAllTickets) + { + // Refund ticket price to each player + for (locals.i = 0; locals.i < state.numberOfPlayers; ++locals.i) + { + qpi.transfer(state.players.get(locals.i).player, state.ticketPrice); + } + } + + PRIVATE_FUNCTION_WITH_LOCALS(CountMatches) + { + locals.maskA = 0; + locals.maskB = 0; + for (locals.i = 0; locals.i < input.playerValues.capacity(); ++locals.i) + { + locals.randomValue = input.playerValues.get(locals.i); + ASSERT(locals.randomValue > 0 && locals.randomValue <= QTF_MAX_RANDOM_VALUE); + + locals.maskA |= 1u << locals.randomValue; + } + + for (locals.i = 0; locals.i < input.winningValues.capacity(); ++locals.i) + { + locals.randomValue = input.winningValues.get(locals.i); + ASSERT(locals.randomValue > 0 && locals.randomValue <= QTF_MAX_RANDOM_VALUE); + + locals.maskB |= 1u << locals.randomValue; + } + output.matches = bitcount32(locals.maskA & locals.maskB); + } + + /** + * @brief Checks if the contract has sufficient balance to cover expected revenue. + * + * This function verifies that the on-chain balance (incoming - outgoing) of the contract + * is at least equal to the expected revenue. Used as a safety check before settlement + * to prevent underflow or incomplete payouts. + * + * @param input.expectedRevenue - The amount of revenue expected to be available + * @param output.hasEnough - true if actualBalance >= expectedRevenue + * @param output.actualBalance - Current net balance of the contract + */ + PRIVATE_FUNCTION_WITH_LOCALS(CheckContractBalance) + { + qpi.getEntity(SELF, locals.entity); + output.actualBalance = RL::max(locals.entity.incomingAmount - locals.entity.outgoingAmount, 0i64); + output.hasEnough = (output.actualBalance >= input.expectedRevenue); + } + + /** + * @brief Computes (base^exp) in fixed-point arithmetic using fast exponentiation. + * + * @param input.base - Base value in fixed-point (scaled by QTF_FIXED_POINT_SCALE) + * @param input.exp - Exponent (integer) + * @param output.result - (base^exp) in fixed-point + */ + PRIVATE_FUNCTION_WITH_LOCALS(PowerFixedPoint) + { + if (input.exp == 0) + { + output.result = QTF_FIXED_POINT_SCALE; // base^0 = 1.0 + return; + } + + locals.tmpBase = input.base; + locals.expCopy = input.exp; + output.result = QTF_FIXED_POINT_SCALE; + + while (locals.expCopy > 0) + { + if (locals.expCopy & 1) + { + // result *= tmpBase (both in fixed-point, so divide by SCALE) + output.result = div(smul(output.result, locals.tmpBase), QTF_FIXED_POINT_SCALE); + } + // tmpBase *= tmpBase + locals.tmpBase = div(smul(locals.tmpBase, locals.tmpBase), QTF_FIXED_POINT_SCALE); + locals.expCopy >>= 1; + } + } + + /** + * @brief Calculates expected rounds until at least one k=4 win: E_k4(N) = 1 / (1 - (1-p4)^N) + * + * Uses exact p4 = 1/27405 (combinatorics for 4-of-30). + * Returns result in fixed-point scaled by QTF_FIXED_POINT_SCALE. + * + * @param input.N - Number of tickets sold in round + * @param output.expectedRounds - E_k4(N) in fixed-point + */ + PRIVATE_FUNCTION_WITH_LOCALS(CalculateExpectedRoundsToK4) + { + if (input.N == 0) + { + // No tickets, infinite expected rounds + output.expectedRounds = UINT64_MAX; + return; + } + + // p4 = 1/27405, so (1 - p4) = 27404/27405 + // In fixed-point: (1 - p4) = (27404 * SCALE) / 27405 + locals.oneMinusP4 = div(smul(27404ULL, QTF_FIXED_POINT_SCALE), QTF_P4_DENOMINATOR); + + // Compute (1-p4)^N in fixed-point using CALL + locals.pfInput.base = locals.oneMinusP4; + locals.pfInput.exp = input.N; + CALL(PowerFixedPoint, locals.pfInput, locals.pfOutput); + locals.pow1mP4N = locals.pfOutput.result; + + // Compute 1 - (1-p4)^N + if (locals.pow1mP4N < QTF_FIXED_POINT_SCALE) + { + locals.denomFP = QTF_FIXED_POINT_SCALE - locals.pow1mP4N; + } + else + { + // Fallback: should not happen, but protect against underflow + locals.denomFP = 1; + } + + // E_k4 = 1 / (1 - (1-p4)^N) = SCALE / denomFP + if (locals.denomFP > 0) + { + output.expectedRounds = div(QTF_FIXED_POINT_SCALE, locals.denomFP); + } + else + { + // Extremely unlikely, fallback to large value + output.expectedRounds = UINT64_MAX; + } + + // Additional safety: if result unreasonably large, cap it + if (output.expectedRounds > smul(QTF_FR_GOAL_ROUNDS_CAP, QTF_FIXED_POINT_SCALE)) + { + output.expectedRounds = smul(QTF_FR_GOAL_ROUNDS_CAP, QTF_FIXED_POINT_SCALE); + } + } + + /** + * @brief Generate 4 unique random values from [1..30] using seed. + * + * Protection against infinite loop: if unable to find unique value after max attempts, + * fallback to sequential selection of first available unused value. + */ + PRIVATE_FUNCTION_WITH_LOCALS(GetRandomValues) + { + for (locals.index = 0; locals.index < output.values.capacity(); ++locals.index) + { + deriveOne(input.seed, locals.index, locals.tempValue); + locals.candidate = static_cast(sadd(mod(locals.tempValue, QTF_MAX_RANDOM_VALUE), 1ull)); + + locals.attempts = 0; + while (locals.used.contains(locals.candidate) && locals.attempts < QTF_MAX_RANDOM_GENERATION_ATTEMPTS) + { + ++locals.attempts; + // Regenerate a fresh candidate from the evolving tempValue until it is unique + locals.tempValue ^= locals.tempValue >> 12; + locals.tempValue ^= locals.tempValue << 25; + locals.tempValue ^= locals.tempValue >> 27; + locals.tempValue *= 2685821657736338717ULL; + locals.candidate = static_cast(sadd(mod(locals.tempValue, QTF_MAX_RANDOM_VALUE), 1ull)); + } + + // Fallback: if still duplicate after max attempts, find first unused value + if (locals.used.contains(locals.candidate)) + { + for (locals.fallback = 1; locals.fallback <= QTF_MAX_RANDOM_VALUE; ++locals.fallback) + { + if (!locals.used.contains(locals.fallback)) + { + locals.candidate = locals.fallback; + break; + } + } + } + + locals.used.add(locals.candidate); + output.values.set(locals.index, locals.candidate); + } + } + + /** + * @brief Validates that all numbers in the array are unique and in range [1..30]. + * + * @param input.numbers - Array of numbers to validate + * @param output.isValid - true if all numbers are valid and unique + */ + PRIVATE_FUNCTION_WITH_LOCALS(ValidateNumbers) + { + output.isValid = true; + for (locals.idx = 0; locals.idx < input.numbers.capacity(); ++locals.idx) + { + locals.value = input.numbers.get(locals.idx); + if (locals.value == 0 || locals.value > QTF_MAX_RANDOM_VALUE) + { + output.isValid = false; + return; + } + if (locals.seen.contains(locals.value)) + { + output.isValid = false; + return; + } + locals.seen.add(locals.value); + } + } + + /** + * @brief Calculate safe reserve top-up amount respecting safety limits. + * + * Safety limits per spec: + * - Max 10% of total QRP reserve per round + * - Soft floor: keep at least 20*P in QRP reserve + * - Per-winner cap: max 25*P per winner + * + * @param input.totalQRPBalance - Actual QRP contract balance (for 10% limit and soft floor) + * @param input.needed - Amount needed for top-up + * @param input.perWinnerCapTotal - Per-winner cap multiplied by winner count + * @param input.ticketPrice - Current ticket price + * @param output.topUpAmount - Safe amount to top up from reserve + */ + PRIVATE_FUNCTION_WITH_LOCALS(CalcReserveTopUp) + { + if (input.totalQRPBalance == 0) + { + output.topUpAmount = 0; + return; + } + + // Calculate soft floor: keep at least 20 * P in total QRP reserve + locals.softFloor = smul(input.ticketPrice, QTF_RESERVE_SOFT_FLOOR_MULT); + + // Calculate available reserve from QRP (above soft floor) + if (input.totalQRPBalance > locals.softFloor) + { + locals.availableAboveFloor = input.totalQRPBalance - locals.softFloor; + } + else + { + locals.availableAboveFloor = 0; + } + + // Calculate max per round (10% of total QRP reserve per spec) + locals.maxPerRound = div(smul(input.totalQRPBalance, QTF_TOPUP_RESERVE_PCT_BP), 10000); + // Cap by available above floor + locals.maxPerRound = RL::min(locals.maxPerRound, locals.availableAboveFloor); + // Cap by per-winner cap + locals.maxPerRound = RL::min(locals.maxPerRound, input.perWinnerCapTotal); + + // Return min of needed and max allowed + if (input.needed < locals.maxPerRound) + { + output.topUpAmount = input.needed; + } + else + { + output.topUpAmount = locals.maxPerRound; + } + } + + /** + * @brief Unified tier payout processing for k2/k3 winners. + * + * Handles floor guarantee with QRP top-up if pool is insufficient. + * Calculates per-winner payout (capped at perWinnerCap) and overflow. + * + * @param input.floorPerWinner - Floor payout per winner (0.5*P for k2, 5*P for k3) + * @param input.winnerCount - Number of winners in this tier + * @param input.payoutPool - Initial payout pool for this tier + * @param input.perWinnerCap - Per-winner cap (25*P) + * @param input.totalQRPBalance - QRP balance for safety limits + * @param input.ticketPrice - Current ticket price + * @param output.perWinnerPayout - Calculated per-winner payout + * @param output.overflow - Overflow amount (unused funds) + * @param output.topUpReceived - Amount received from QRP top-up + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessTierPayout) + { + output.topUpReceived = 0; + output.overflow = 0; + output.perWinnerPayout = 0; + + if (input.winnerCount == 0) + { + // No winners: entire pool becomes overflow + output.overflow = input.payoutPool; + return; + } + + locals.floorTotalNeeded = smul(input.floorPerWinner, input.winnerCount); + locals.finalPool = input.payoutPool; + + // Top-up from QRP if pool insufficient for floor guarantee + if (input.payoutPool < locals.floorTotalNeeded) + { + locals.calcTopUpInput.totalQRPBalance = input.totalQRPBalance; + locals.calcTopUpInput.needed = locals.floorTotalNeeded - input.payoutPool; + locals.calcTopUpInput.perWinnerCapTotal = smul(input.perWinnerCap, input.winnerCount); + locals.calcTopUpInput.ticketPrice = input.ticketPrice; + CALL(CalcReserveTopUp, locals.calcTopUpInput, locals.calcTopUpOutput); + + locals.qrpRequested = RL::min(locals.calcTopUpOutput.topUpAmount, locals.floorTotalNeeded - input.payoutPool); + if (locals.qrpRequested > 0) + { + locals.qrpGetReserveInput.revenue = locals.qrpRequested; + INVOKE_OTHER_CONTRACT_PROCEDURE(QRP, WithdrawReserve, locals.qrpGetReserveInput, locals.qrpGetReserveOutput, 0ll); + + if (locals.qrpGetReserveOutput.returnCode == QRP::toReturnCode(QRP::EReturnCode::SUCCESS)) + { + output.topUpReceived = locals.qrpGetReserveOutput.allocatedRevenue; + locals.finalPool = sadd(input.payoutPool, output.topUpReceived); + } + } + } + + // Calculate per-winner payout (capped at perWinnerCap) + output.perWinnerPayout = RL::min(input.perWinnerCap, div(locals.finalPool, input.winnerCount)); + output.overflow = locals.finalPool - smul(output.perWinnerPayout, input.winnerCount); + } + + /** + * @brief Calculate k2/k3 prize pools from revenue (reusable for settlement and estimation). + * + * This function encapsulates the common logic for calculating prize pools: + * 1. Get fee percentages from RL contract + * 2. Calculate winners block (typically 68% of revenue) + * 3. Apply FR rake if active (5% of winners block) + * 4. Split remaining into k2 (28%) and k3 (40%) pools + * + * @param input.revenue - Total revenue from ticket sales + * @param input.applyFRRake - Whether to apply 5% FR rake + * @param output.winnersBlock - Winners block after rake + * @param output.winnersRake - Amount taken as FR rake (0 if not applied) + * @param output.k2Pool - Prize pool for k=2 tier + * @param output.k3Pool - Prize pool for k=3 tier + */ + PRIVATE_FUNCTION_WITH_LOCALS(CalculatePrizePools) + { + if (input.revenue == 0) + { + output.winnersBlock = 0; + output.winnersRake = 0; + output.k2Pool = 0; + output.k3Pool = 0; + return; + } + + // Get fee percentages from RL contract + CALL(GetFees, locals.feesInput, locals.feesOutput); + + // Calculate winners block (typically 68% of revenue) + locals.winnersBlockBeforeRake = div(smul(input.revenue, static_cast(locals.feesOutput.winnerFeePercent)), 100); + + // Apply FR rake if requested + if (input.applyFRRake) + { + output.winnersRake = div(smul(locals.winnersBlockBeforeRake, QTF_FR_WINNERS_RAKE_BP), 10000); + output.winnersBlock = locals.winnersBlockBeforeRake - output.winnersRake; + } + else + { + output.winnersRake = 0; + output.winnersBlock = locals.winnersBlockBeforeRake; + } + + // Calculate k2 and k3 pools using shared static function + calcK2K3Pool(output.winnersBlock, output.k2Pool, output.k3Pool); + } + + /** + * @brief Estimates base carry gain per round from FR mechanisms (without extra redirect). + * + * Includes: + * - Base Dev redirect: QTF_FR_DEV_REDIRECT_BP of R + * - Base Dist redirect: QTF_FR_DIST_REDIRECT_BP of R + * - Winners rake: QTF_FR_WINNERS_RAKE_BP of winners block + * - Overflow bias: (1 - alpha_fr) of overflow to carry + * + * This is a simplified estimate; actual gain depends on winners count and overflow. + * We use conservative approximation: assume moderate overflow and typical winner distribution. + */ + PRIVATE_FUNCTION_WITH_LOCALS(CalculateBaseGain) + { + // Base redirects from Dev and Dist + locals.devRedirect = div(smul(input.revenue, QTF_FR_DEV_REDIRECT_BP), 10000); + locals.distRedirect = div(smul(input.revenue, QTF_FR_DIST_REDIRECT_BP), 10000); + + // Winners rake: 5% of winners block + locals.winnersRake = div(smul(input.winnersBlock, QTF_FR_WINNERS_RAKE_BP), 10000); + + // Overflow estimate: assume ~10% of winnersBlock not paid out (conservative heuristic) + // With alpha_fr = 0.05, 95% of overflow goes to carry + locals.estimatedOverflow = div(input.winnersBlock, 10); + locals.overflowToCarry = div(smul(locals.estimatedOverflow, 10000 - QTF_FR_ALPHA_BP), 10000); + + // Total base gain + output.baseGain = sadd(sadd(locals.devRedirect, locals.distRedirect), sadd(locals.winnersRake, locals.overflowToCarry)); + } + + /** + * @brief Calculates deficit-driven extra redirect percentage in basis points. + * + * Formula: + * - delta = max(0, targetJackpot - currentJackpot) + * - E_k4(N) = expected rounds to k=4 + * - H = min(E_k4(N), cap) + * - g_need = max(0, delta/H - baseGain) + * - extra_pp = clamp(g_need / revenue, 0, extra_max) + */ + PRIVATE_FUNCTION_WITH_LOCALS(CalculateExtraRedirectBP) + { + if (input.delta == 0 || input.revenue == 0 || input.N == 0) + { + output.extraBP = 0; + return; + } + + // Calculate E_k4(N) in fixed-point using CALL + locals.calcE4Input.N = input.N; + CALL(CalculateExpectedRoundsToK4, locals.calcE4Input, locals.calcE4Output); + + // Apply cap: H = min(E_k4, cap) + locals.horizonFP = RL::min(locals.calcE4Output.expectedRounds, smul(QTF_FR_GOAL_ROUNDS_CAP, QTF_FIXED_POINT_SCALE)); + + // Convert horizon back to integer rounds (divide by SCALE) + locals.horizon = RL::max(div(locals.horizonFP, QTF_FIXED_POINT_SCALE), 1ULL); + + // Calculate required gain per round: delta / H + locals.requiredGainPerRound = div(input.delta, locals.horizon); + + // Calculate needed additional gain: g_need = max(0, requiredGainPerRound - baseGain) + if (locals.requiredGainPerRound > input.baseGain) + { + locals.gNeed = locals.requiredGainPerRound - input.baseGain; + } + else + { + output.extraBP = 0; + return; + } + + // Convert g_need to percentage of revenue in basis points: (g_need / revenue) * 10000 + locals.extraBPTemp = div(smul(locals.gNeed, 10000ULL), input.revenue); + + // Clamp to maximum + output.extraBP = RL::min(locals.extraBPTemp, QTF_FR_EXTRA_MAX_BP); + } + + static void calcK2K3Pool(uint64 winnersBlock, uint64& outK2Pool, uint64& outK3Pool) + { + outK3Pool = div(smul(winnersBlock, QTF_BASE_K3_SHARE_BP), 10000); + outK2Pool = div(smul(winnersBlock, QTF_BASE_K2_SHARE_BP), 10000); + } +}; diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h index 7de4979c4..e871714c8 100644 --- a/src/contracts/RandomLottery.h +++ b/src/contracts/RandomLottery.h @@ -778,9 +778,6 @@ struct RL : public ContractBase output.returnCode = toReturnCode(EReturnCode::SUCCESS); } - // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. - static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } - private: /** * @brief Internal: records a winner into the cyclic winners array. @@ -815,6 +812,13 @@ struct RL : public ContractBase } } +public: + // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } + + template static constexpr T min(const T& a, const T& b) { return (a < b) ? a : b; } + template static constexpr T max(const T& a, const T& b) { return a > b ? a : b; } + protected: /** * @brief Circular buffer storing the history of winners. @@ -957,8 +961,4 @@ struct RL : public ContractBase // Reads current net on-chain balance of SELF (incoming - outgoing). static void getSCRevenue(const Entity& entity, uint64& revenue) { revenue = entity.incomingAmount - entity.outgoingAmount; } - - template static constexpr const T& min(const T& a, const T& b) { return (a < b) ? a : b; } - - template static constexpr const T& max(const T& a, const T& b) { return (a > b) ? a : b; } }; diff --git a/test/contract_qduel.cpp b/test/contract_qduel.cpp index d36856662..161949366 100644 --- a/test/contract_qduel.cpp +++ b/test/contract_qduel.cpp @@ -39,7 +39,7 @@ class QDuelChecker : public QDUEL uint8 devFee() const { return devFeePercentBps; } uint8 burnFee() const { return burnFeePercentBps; } uint8 shareholdersFee() const { return shareholdersFeePercentBps; } - uint64 minDuelAmount() const { return minimumDuelAmount; } + sint64 minDuelAmount() const { return minimumDuelAmount; } void setState(EState newState) { currentState = newState; } EState getState() const { return currentState; } // Helper to fetch user record without exposing contract internals. @@ -109,7 +109,7 @@ class ContractTestingQDuel : protected ContractTesting // Access helper for the underlying contract state. QDuelChecker* state() { return reinterpret_cast(contractStates[QDUEL_CONTRACT_INDEX]); } - QDUEL::CreateRoom_output createRoom(const id& user, const id& allowedPlayer, uint64 stake, uint64 raiseStep, uint64 maxStake, sint64 reward) + QDUEL::CreateRoom_output createRoom(const id& user, const id& allowedPlayer, sint64 stake, sint64 raiseStep, sint64 maxStake, sint64 reward) { QDUEL::CreateRoom_input input{allowedPlayer, stake, raiseStep, maxStake}; QDUEL::CreateRoom_output output; @@ -205,7 +205,7 @@ class ContractTestingQDuel : protected ContractTesting return output; } - QDUEL::Withdraw_output withdraw(const id& user, uint64 amount, sint64 reward = 0) + QDUEL::Withdraw_output withdraw(const id& user, sint64 amount, sint64 reward = 0) { QDUEL::Withdraw_input input{amount}; QDUEL::Withdraw_output output; @@ -294,7 +294,7 @@ namespace QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); // Setup: give both players enough balance to cover the duel. - constexpr uint64 duelAmount = 100000ULL; + constexpr sint64 duelAmount = 100000ULL; increaseEnergy(player1, duelAmount); increaseEnergy(player2, duelAmount); const uint64 player1Before = getBalance(player1); @@ -350,7 +350,7 @@ TEST(ContractQDuel, EndEpochKeepsDepositWhileRoomsRecreatedEachEpoch) qduel.setDeterministicTime(2025, 1, 1, 0); const id owner(40, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); const uint64 epochs = 3; const uint64 reward = stake + (stake * epochs); increaseEnergy(owner, reward); @@ -396,7 +396,7 @@ TEST(ContractQDuel, BeginEpochKeepsRoomsAndUsers) qduel.setDeterministicTime(2022, 4, 13, 0); const id owner(1, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); // Give the owner enough balance to create a room. increaseEnergy(owner, stake); @@ -425,7 +425,7 @@ TEST(ContractQDuel, FirstTickAfterUnlockResetsTimerStart) qduel.setDeterministicTime(2022, 4, 13, 0); const id owner(2, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); // Fund owner so the room creation succeeds. increaseEnergy(owner, stake); @@ -464,7 +464,7 @@ TEST(ContractQDuel, EndTickExpiresRoomCreatesNewWhenDepositAvailable) qduel.setDeterministicTime(2025, 1, 1, 0); const id owner(3, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); // Fund owner with enough to re-create room after finalize. increaseEnergy(owner, stake); @@ -497,7 +497,7 @@ TEST(ContractQDuel, EndTickExpiresRoomWithoutAvailableDepositRemovesUser) qduel.setDeterministicTime(2025, 1, 1, 0); const id owner(4, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); // Fund owner just enough to create the initial room. increaseEnergy(owner, stake); @@ -528,7 +528,7 @@ TEST(ContractQDuel, EndTickSkipsNonPeriodTicks) qduel.setDeterministicTime(2025, 1, 1, 0); const id owner(5, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); // Fund owner to create a room. increaseEnergy(owner, stake); @@ -564,7 +564,7 @@ TEST(ContractQDuel, LockedStateBlocksCreateAndConnect) const id owner(6, 0, 0, 0); const id other(7, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); // Fund owner to create the baseline room. increaseEnergy(owner, stake); @@ -590,7 +590,7 @@ TEST(ContractQDuel, EndTickRecreatesRoomWithUpdatedStake) qduel.setDeterministicTime(2025, 1, 1, 0); const id owner(8, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); // Fund owner so next stake can be doubled. increaseEnergy(owner, stake * 2); @@ -623,7 +623,7 @@ TEST(ContractQDuel, ConnectFinalizeIgnoresLockedAmount) const id owner(9, 0, 0, 0); const id opponent(10, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); // Fund both players so creation and join can proceed. increaseEnergy(owner, stake); increaseEnergy(opponent, stake); @@ -645,7 +645,7 @@ TEST(ContractQDuel, InitializeDefaults) ContractTestingQDuel qduel; EXPECT_EQ(qduel.state()->team(), QDUEL_TEAM_ADDRESS); - EXPECT_EQ(qduel.state()->minDuelAmount(), QDUEL_MINIMUM_DUEL_AMOUNT); + EXPECT_EQ(qduel.state()->minDuelAmount(), static_cast(QDUEL_MINIMUM_DUEL_AMOUNT)); EXPECT_EQ(qduel.state()->devFee(), QDUEL_DEV_FEE_PERCENT_BPS); EXPECT_EQ(qduel.state()->burnFee(), QDUEL_BURN_FEE_PERCENT_BPS); EXPECT_EQ(qduel.state()->shareholdersFee(), QDUEL_SHAREHOLDERS_FEE_PERCENT_BPS); @@ -662,7 +662,7 @@ TEST(ContractQDuel, CreateRoomStoresRoomAndUser) const id owner(11, 0, 0, 0); const id allowed(12, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); const uint64 reward = stake + 5000; increaseEnergy(owner, reward); @@ -710,7 +710,7 @@ TEST(ContractQDuel, CreateRoomRejectsMaxStakeBelowStake) qduel.state()->setState(QDUEL::EState::NONE); const id owner(14, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); const uint64 reward = stake; increaseEnergy(owner, reward); @@ -724,7 +724,7 @@ TEST(ContractQDuel, CreateRoomRejectsRewardBelowMinimum) qduel.state()->setState(QDUEL::EState::NONE); const id owner(15, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); const uint64 reward = qduel.state()->minDuelAmount() - 1; increaseEnergy(owner, reward); @@ -754,7 +754,7 @@ TEST(ContractQDuel, CreateRoomRejectsDuplicateUser) qduel.state()->setState(QDUEL::EState::NONE); const id owner(17, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(owner, stake * 2); EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); @@ -767,7 +767,7 @@ TEST(ContractQDuel, CreateRoomRejectsWhenRoomsFull) ContractTestingQDuel qduel; qduel.state()->setState(QDUEL::EState::NONE); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); for (uint32 i = 0; i < QDUEL_MAX_NUMBER_OF_ROOMS; ++i) { const id owner(100 + i, 0, 0, 0); @@ -790,7 +790,7 @@ TEST(ContractQDuel, ConnectToRoomRejectsMissingRoom) qduel.state()->setState(QDUEL::EState::NONE); const id player(18, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(player, stake); EXPECT_EQ(qduel.connectToRoom(player, id(999, 0, 0, 0), stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::ROOM_NOT_FOUND)); @@ -804,7 +804,7 @@ TEST(ContractQDuel, ConnectToRoomRejectsNotAllowedPlayer) const id owner(19, 0, 0, 0); const id allowed(20, 0, 0, 0); const id other(21, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(owner, stake); increaseEnergy(other, stake); @@ -821,7 +821,7 @@ TEST(ContractQDuel, ConnectToRoomRejectsInsufficientReward) const id owner(22, 0, 0, 0); const id opponent(23, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(owner, stake); increaseEnergy(opponent, stake - 1); @@ -841,7 +841,7 @@ TEST(ContractQDuel, ConnectToRoomRefundsExcessRewardForLoser) id opponent; ASSERT_TRUE(findPlayersForWinner(qduel, true, owner, opponent)); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); const uint64 reward = stake + 5000; increaseEnergy(owner, stake); increaseEnergy(opponent, reward); @@ -860,7 +860,7 @@ TEST(ContractQDuel, ConnectFinalizeCreatesRoomFromDeposit) const id owner(24, 0, 0, 0); const id opponent(25, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(owner, stake * 2); increaseEnergy(opponent, stake); @@ -933,7 +933,7 @@ TEST(ContractQDuel, SetPercentFeesAccessDeniedAndGetPercentFees) EXPECT_EQ(after.devFeePercentBps, before.devFeePercentBps); EXPECT_EQ(after.burnFeePercentBps, before.burnFeePercentBps); EXPECT_EQ(after.shareholdersFeePercentBps, before.shareholdersFeePercentBps); - EXPECT_EQ(after.percentScale, before.percentScale); + EXPECT_EQ(static_cast(after.percentScale), static_cast(before.percentScale)); } TEST(ContractQDuel, SetPercentFeesUpdatesState) @@ -947,7 +947,7 @@ TEST(ContractQDuel, SetPercentFeesUpdatesState) EXPECT_EQ(output.devFeePercentBps, 1); EXPECT_EQ(output.burnFeePercentBps, 2); EXPECT_EQ(output.shareholdersFeePercentBps, 3); - EXPECT_EQ(output.percentScale, QDUEL_PERCENT_SCALE); + EXPECT_EQ(static_cast(output.percentScale), static_cast(QDUEL_PERCENT_SCALE)); EXPECT_EQ(output.returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); } @@ -987,7 +987,7 @@ TEST(ContractQDuel, GetRoomsReturnsActiveRooms) const id owner1(30, 0, 0, 0); const id owner2(31, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(owner1, stake); increaseEnergy(owner2, stake); @@ -1031,7 +1031,7 @@ TEST(ContractQDuel, GetUserProfileReportsUserData) EXPECT_EQ(missing.returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::USER_NOT_FOUND)); const id owner(32, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(owner, stake + 200); EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 2, stake * 2, stake + 200).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); @@ -1061,7 +1061,7 @@ TEST(ContractQDuel, DepositValidationsAndUpdatesBalance) EXPECT_EQ(getBalance(missingUser), missingBefore); const id owner(34, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(owner, stake); EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); @@ -1085,7 +1085,7 @@ TEST(ContractQDuel, WithdrawValidationsAndTransfers) EXPECT_EQ(qduel.withdraw(missingUser, 1).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::USER_NOT_FOUND)); const id owner(36, 0, 0, 0); - const uint64 stake = qduel.state()->minDuelAmount(); + const sint64 stake = qduel.state()->minDuelAmount(); increaseEnergy(owner, stake + 1000); EXPECT_EQ(qduel.createRoom(owner, NULL_ID, stake, 1, stake, stake + 1000).returnCode, QDUEL::toReturnCode(QDUEL::EReturnCode::SUCCESS)); diff --git a/test/contract_qrp.cpp b/test/contract_qrp.cpp new file mode 100644 index 000000000..9881d4dca --- /dev/null +++ b/test/contract_qrp.cpp @@ -0,0 +1,207 @@ +#define NO_UEFI + +#include "contract_testing.h" + +// Procedure/function indices (must match REGISTER_USER_FUNCTIONS_AND_PROCEDURES in `src/contracts/QReservePool.h`). +constexpr uint16 QRP_PROC_GET_RESERVE = 1; +constexpr uint16 QRP_PROC_ADD_ALLOWED_SC = 2; +constexpr uint16 QRP_PROC_REMOVE_ALLOWED_SC = 3; + +constexpr uint16 QRP_FUNC_GET_AVAILABLE_RESERVE = 1; +constexpr uint16 QRP_FUNC_GET_ALLOWED_SC = 2; + +static const id QRP_CONTRACT_ID(QRP_CONTRACT_INDEX, 0, 0, 0); +static const id QRP_DEFAULT_SC_ID(QRP_QTF_INDEX, 0, 0, 0); +static const id QRP_OWNER_TEAM_ADDRESS = + ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, _W, _E, _T, _H, _N, _G, + _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + +class QRPChecker : public QRP +{ +public: + const id& team() const { return teamAddress; } + const id& owner() const { return ownerAddress; } + bool hasAllowedSC(const id& sc) const { return allowedSmartContracts.contains(sc); } + uint64 allowedCount() const { return allowedSmartContracts.population(); } +}; + +class ContractTestingQRP : protected ContractTesting +{ +public: + ContractTestingQRP() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QRP); + callSystemProcedure(QRP_CONTRACT_INDEX, INITIALIZE); + } + + QRPChecker* state() { return reinterpret_cast(contractStates[QRP_CONTRACT_INDEX]); } + + uint64 balanceOf(const id& account) const { return static_cast(getBalance(account)); } + uint64 balanceQrp() const { return balanceOf(QRP_CONTRACT_ID); } + void fund(const id& account, uint64 amount) { increaseEnergy(account, amount); } + void fundQrp(uint64 amount) { fund(QRP_CONTRACT_ID, amount); } + + QRP::WithdrawReserve_output withdrawReserveReserve(const id& invocator, uint64 revenue, sint64 attachedAmount = 0) + { + QRP::WithdrawReserve_input input{revenue}; + QRP::WithdrawReserve_output output{}; + invokeUserProcedure(QRP_CONTRACT_INDEX, QRP_PROC_GET_RESERVE, input, output, invocator, attachedAmount); + return output; + } + + QRP::AddAllowedSC_output addAllowedSC(const id& invocator, uint64 scIndex) + { + QRP::AddAllowedSC_input input{scIndex}; + QRP::AddAllowedSC_output output{}; + invokeUserProcedure(QRP_CONTRACT_INDEX, QRP_PROC_ADD_ALLOWED_SC, input, output, invocator, 0); + return output; + } + + QRP::RemoveAllowedSC_output removeAllowedSC(const id& invocator, uint64 scIndex) + { + QRP::RemoveAllowedSC_input input{scIndex}; + QRP::RemoveAllowedSC_output output{}; + invokeUserProcedure(QRP_CONTRACT_INDEX, QRP_PROC_REMOVE_ALLOWED_SC, input, output, invocator, 0); + return output; + } + + QRP::GetAvailableReserve_output getAvailableReserve() const + { + QRP::GetAvailableReserve_input input{}; + QRP::GetAvailableReserve_output output{}; + callFunction(QRP_CONTRACT_INDEX, QRP_FUNC_GET_AVAILABLE_RESERVE, input, output); + return output; + } + + QRP::GetAllowedSC_output getAllowedSC() const + { + QRP::GetAllowedSC_input input{}; + QRP::GetAllowedSC_output output{}; + callFunction(QRP_CONTRACT_INDEX, QRP_FUNC_GET_ALLOWED_SC, input, output); + return output; + } +}; + +static bool containsAllowedSC(const QRP::GetAllowedSC_output& allowed, const id& sc) +{ + for (uint64 i = 0; i < QRP_ALLOWED_SC_NUM; ++i) + { + if (allowed.allowedSC.get(i) == sc) + { + return true; + } + } + return false; +} + +TEST(ContractQReservePool, WithdrawReserveEnforcesAuthorizationAndBalance) +{ + ContractTestingQRP qrp; + const id unauthorized = id::randomValue(); + qrp.fund(unauthorized, 0); + qrp.fund(QRP_DEFAULT_SC_ID, 0); + + QRP::WithdrawReserve_output denied = qrp.withdrawReserveReserve(unauthorized, 100); + EXPECT_EQ(denied.returnCode, QRP::toReturnCode(QRP::EReturnCode::ACCESS_DENIED)); + EXPECT_EQ(denied.allocatedRevenue, 0ull); + + qrp.fundQrp(1000); + EXPECT_EQ(qrp.balanceQrp(), 1000); + + QRP::WithdrawReserve_output granted = qrp.withdrawReserveReserve(QRP_DEFAULT_SC_ID, 600); + EXPECT_EQ(granted.returnCode, QRP::toReturnCode(QRP::EReturnCode::SUCCESS)); + EXPECT_EQ(granted.allocatedRevenue, 600ull); + EXPECT_EQ(qrp.balanceQrp(), 400); + EXPECT_EQ(qrp.balanceOf(QRP_DEFAULT_SC_ID), 600); + + QRP::WithdrawReserve_output insufficient = qrp.withdrawReserveReserve(QRP_DEFAULT_SC_ID, 500); + EXPECT_EQ(insufficient.returnCode, QRP::toReturnCode(QRP::EReturnCode::INSUFFICIENT_RESERVE)); + EXPECT_EQ(insufficient.allocatedRevenue, 0ull); + EXPECT_EQ(qrp.balanceQrp(), 400); + EXPECT_EQ(qrp.balanceOf(QRP_DEFAULT_SC_ID), 600); +} + +TEST(ContractQReservePool, WithdrawReserve_ZeroAndExactRemaining) +{ + ContractTestingQRP qrp; + qrp.fund(QRP_DEFAULT_SC_ID, 0); + + qrp.fundQrp(1000); + EXPECT_EQ(qrp.balanceQrp(), 1000); + + // Zero request should not move funds. + const QRP::WithdrawReserve_output zero = qrp.withdrawReserveReserve(QRP_DEFAULT_SC_ID, 0); + EXPECT_EQ(zero.returnCode, QRP::toReturnCode(QRP::EReturnCode::SUCCESS)); + EXPECT_EQ(zero.allocatedRevenue, 0ull); + EXPECT_EQ(qrp.balanceQrp(), 1000); + + // Exact remaining should succeed and drain the reserve. + const QRP::WithdrawReserve_output exact = qrp.withdrawReserveReserve(QRP_DEFAULT_SC_ID, 1000); + EXPECT_EQ(exact.returnCode, QRP::toReturnCode(QRP::EReturnCode::SUCCESS)); + EXPECT_EQ(exact.allocatedRevenue, 1000ull); + EXPECT_EQ(qrp.balanceQrp(), 0); + EXPECT_EQ(qrp.balanceOf(QRP_DEFAULT_SC_ID), 1000); +} + +TEST(ContractQReservePool, OwnerAddsAndRemovesSmartContracts) +{ + ContractTestingQRP qrp; + QRPChecker* state = qrp.state(); + constexpr uint64 newScIndex = 77; + const id newScId(newScIndex, 0, 0, 0); + const id outsider(200, 0, 0, 0); + qrp.fund(newScId, 0); + qrp.fund(outsider, 0); + qrp.fund(state->owner(), 0); + + QRP::AddAllowedSC_output deniedAdd = qrp.addAllowedSC(outsider, newScIndex); + EXPECT_EQ(deniedAdd.returnCode, QRP::toReturnCode(QRP::EReturnCode::ACCESS_DENIED)); + EXPECT_FALSE(state->hasAllowedSC(newScId)); + + QRP::AddAllowedSC_output approvedAdd = qrp.addAllowedSC(state->owner(), newScIndex); + EXPECT_EQ(approvedAdd.returnCode, QRP::toReturnCode(QRP::EReturnCode::SUCCESS)); + EXPECT_TRUE(state->hasAllowedSC(newScId)); + + QRP::GetAllowedSC_output allowed = qrp.getAllowedSC(); + EXPECT_TRUE(containsAllowedSC(allowed, newScId)); + + QRP::RemoveAllowedSC_output deniedRemove = qrp.removeAllowedSC(outsider, newScIndex); + EXPECT_EQ(deniedRemove.returnCode, QRP::toReturnCode(QRP::EReturnCode::ACCESS_DENIED)); + EXPECT_TRUE(state->hasAllowedSC(newScId)); + + QRP::RemoveAllowedSC_output approvedRemove = qrp.removeAllowedSC(state->owner(), newScIndex); + EXPECT_EQ(approvedRemove.returnCode, QRP::toReturnCode(QRP::EReturnCode::SUCCESS)); + EXPECT_FALSE(state->hasAllowedSC(newScId)); +} + +TEST(ContractQReservePool, OwnerAddRemove_IdempotencyAndBounds) +{ + ContractTestingQRP qrp; + QRPChecker* state = qrp.state(); + qrp.fund(state->owner(), 0); + + constexpr uint64 newScIndex = 88; + const id newScId(newScIndex, 0, 0, 0); + qrp.fund(newScId, 0); + + EXPECT_FALSE(state->hasAllowedSC(newScId)); + + // This test focuses on idempotency (repeat add/remove) while keeping authorization valid. + // Add twice: first should succeed, second should not change membership (return code may be SUCCESS or specific). + const auto add1 = qrp.addAllowedSC(state->owner(), newScIndex); + EXPECT_EQ(add1.returnCode, QRP::toReturnCode(QRP::EReturnCode::SUCCESS)); + EXPECT_TRUE(state->hasAllowedSC(newScId)); + + const auto add2 = qrp.addAllowedSC(state->owner(), newScIndex); + EXPECT_TRUE(state->hasAllowedSC(newScId)); + + // Remove twice: first should succeed, second should keep it removed (return code may be SUCCESS or specific). + const auto rem1 = qrp.removeAllowedSC(state->owner(), newScIndex); + EXPECT_EQ(rem1.returnCode, QRP::toReturnCode(QRP::EReturnCode::SUCCESS)); + EXPECT_FALSE(state->hasAllowedSC(newScId)); + + const auto rem2 = qrp.removeAllowedSC(state->owner(), newScIndex); + EXPECT_FALSE(state->hasAllowedSC(newScId)); +} diff --git a/test/contract_qtf.cpp b/test/contract_qtf.cpp new file mode 100644 index 000000000..9ecd05747 --- /dev/null +++ b/test/contract_qtf.cpp @@ -0,0 +1,3121 @@ +#define NO_UEFI + +#define _ALLOW_KEYWORD_MACROS 1 + +#define private protected +#include "contract_testing.h" +#undef private +#undef _ALLOW_KEYWORD_MACROS + +#include +#include +#include + +// Procedure/function indices (must match REGISTER_USER_FUNCTIONS_AND_PROCEDURES in `src/contracts/QThirtyFour.h`). +constexpr uint16 QTF_PROCEDURE_BUY_TICKET = 1; +constexpr uint16 QTF_PROCEDURE_SET_PRICE = 2; +constexpr uint16 QTF_PROCEDURE_SET_SCHEDULE = 3; +constexpr uint16 QTF_PROCEDURE_SET_TARGET_JACKPOT = 4; +constexpr uint16 QTF_PROCEDURE_SET_DRAW_HOUR = 5; + +constexpr uint16 QTF_FUNCTION_GET_TICKET_PRICE = 1; +constexpr uint16 QTF_FUNCTION_GET_NEXT_EPOCH_DATA = 2; +constexpr uint16 QTF_FUNCTION_GET_WINNER_DATA = 3; +constexpr uint16 QTF_FUNCTION_GET_POOLS = 4; +constexpr uint16 QTF_FUNCTION_GET_SCHEDULE = 5; +constexpr uint16 QTF_FUNCTION_GET_DRAW_HOUR = 6; +constexpr uint16 QTF_FUNCTION_GET_STATE = 7; +constexpr uint16 QTF_FUNCTION_GET_FEES = 8; +constexpr uint16 QTF_FUNCTION_ESTIMATE_PRIZE_PAYOUTS = 9; + +using QTFRandomValues = Array; + +namespace +{ + static void issueRlSharesTo(std::vector>& initialOwnerShares) + { + issueContractShares(RL_CONTRACT_INDEX, initialOwnerShares, false); + } + + static void primeQpiFunctionContext(QpiContextUserFunctionCall& qpi) + { + QTF::GetTicketPrice_input input{}; + qpi.call(QTF_FUNCTION_GET_TICKET_PRICE, &input, sizeof(input)); + } + + static void primeQpiProcedureContext(QpiContextUserProcedureCall& qpi, uint8 drawHour) + { + QTF::SetDrawHour_input input{}; + input.newDrawHour = drawHour; + qpi.call(QTF_PROCEDURE_SET_DRAW_HOUR, &input, sizeof(input)); + ASSERT_EQ(contractError[QTF_CONTRACT_INDEX], 0); + } + + static bool valuesEqual(const QTFRandomValues& a, const QTFRandomValues& b) + { + return memcmp(&a, &b, sizeof(a)) == 0; + } + + static void expectWinnerValuesValidAndUnique(const QTF::GetWinnerData_output& winnerData) + { + std::set unique; + for (uint64 i = 0; i < QTF_RANDOM_VALUES_COUNT; ++i) + { + const uint8 v = winnerData.winnerData.winnerValues.get(i); + EXPECT_GE(v, 1u) << "Winning value " << i << " should be >= 1"; + EXPECT_LE(v, QTF_MAX_RANDOM_VALUE) << "Winning value " << i << " should be <= 30"; + unique.insert(v); + } + EXPECT_EQ(unique.size(), static_cast(QTF_RANDOM_VALUES_COUNT)) << "All 4 winning numbers should be unique"; + EXPECT_GT(static_cast(winnerData.winnerData.epoch), 0u) << "Epoch should be recorded after draw"; + } + + static void computeBaselinePrizePools(uint64 revenue, const QTF::GetFees_output& fees, uint64& winnersBlock, uint64& k2Pool, uint64& k3Pool) + { + winnersBlock = (revenue * static_cast(fees.winnerFeePercent)) / 100ULL; + k2Pool = (winnersBlock * QTF_BASE_K2_SHARE_BP) / 10000ULL; + k3Pool = (winnersBlock * QTF_BASE_K3_SHARE_BP) / 10000ULL; + } +} // namespace + +static const id QTF_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, + _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + +constexpr uint8 QTF_ANY_DAY_SCHEDULE = 0xFF; + +// Test helper class exposing internal state +class QTFChecker : public QTF +{ +public: + uint64 getNumberOfPlayers() const { return numberOfPlayers; } + uint64 getTicketPriceInternal() const { return ticketPrice; } + uint64 getJackpot() const { return jackpot; } + uint64 getTargetJackpotInternal() const { return targetJackpot; } + uint32 getDrawHourInternal() const { return drawHour; } + bool getFrActive() const { return frActive; } + uint32 getFrRoundsSinceK4() const { return frRoundsSinceK4; } + uint32 getFrRoundsAtOrAboveTarget() const { return frRoundsAtOrAboveTarget; } + + void setScheduleMask(uint8 newMask) { schedule = newMask; } + void setJackpot(uint64 value) { jackpot = value; } + void setTargetJackpotInternal(uint64 value) { targetJackpot = value; } + void setTicketPriceInternal(uint64 value) { ticketPrice = value; } + void setFrActive(bit value) { frActive = value; } + void setFrRoundsSinceK4(uint16 value) { frRoundsSinceK4 = value; } + void setFrRoundsAtOrAboveTarget(uint16 value) { frRoundsAtOrAboveTarget = value; } + void setOverflowAlphaBP(uint64 value) { overflowAlphaBP = value; } + + const PlayerData& getPlayer(uint64 index) const { return players.get(index); } + void addPlayerDirect(const id& playerId, const QTFRandomValues& randomValues) { players.set(numberOfPlayers++, {playerId, randomValues}); } + + // ---- Private method wrappers (private->protected in this TU) -------------- + ValidateNumbers_output callValidateNumbers(const QPI::QpiContextFunctionCall& qpi, const QTFRandomValues& numbers) const + { + ValidateNumbers_input input{}; + ValidateNumbers_output output{}; + ValidateNumbers_locals locals{}; + + input.numbers = numbers; + ValidateNumbers(qpi, *this, input, output, locals); + return output; + } + + GetRandomValues_output callGetRandomValues(const QPI::QpiContextFunctionCall& qpi, uint64 seed) const + { + GetRandomValues_input input{}; + GetRandomValues_output output{}; + GetRandomValues_locals locals{}; + + input.seed = seed; + GetRandomValues(qpi, *this, input, output, locals); + return output; + } + + CountMatches_output callCountMatches(const QPI::QpiContextFunctionCall& qpi, const QTFRandomValues& playerValues, + const QTFRandomValues& winningValues) const + { + CountMatches_input input{}; + CountMatches_output output{}; + CountMatches_locals locals{}; + + input.playerValues = playerValues; + input.winningValues = winningValues; + CountMatches(qpi, *this, input, output, locals); + return output; + } + + CheckContractBalance_output callCheckContractBalance(const QPI::QpiContextFunctionCall& qpi, uint64 expectedRevenue) const + { + CheckContractBalance_input input{}; + CheckContractBalance_output output{}; + CheckContractBalance_locals locals{}; + + input.expectedRevenue = expectedRevenue; + CheckContractBalance(qpi, *this, input, output, locals); + return output; + } + + PowerFixedPoint_output callPowerFixedPoint(const QPI::QpiContextFunctionCall& qpi, uint64 base, uint64 exp) const + { + PowerFixedPoint_input input{}; + PowerFixedPoint_output output{}; + PowerFixedPoint_locals locals{}; + + input.base = base; + input.exp = exp; + PowerFixedPoint(qpi, *this, input, output, locals); + return output; + } + + CalculateExpectedRoundsToK4_output callCalculateExpectedRoundsToK4(const QPI::QpiContextFunctionCall& qpi, uint64 N) const + { + CalculateExpectedRoundsToK4_input input{}; + CalculateExpectedRoundsToK4_output output{}; + CalculateExpectedRoundsToK4_locals locals{}; + + input.N = N; + CalculateExpectedRoundsToK4(qpi, *this, input, output, locals); + return output; + } + + CalcReserveTopUp_output callCalcReserveTopUp(const QPI::QpiContextFunctionCall& qpi, uint64 totalQRPBalance, uint64 needed, + uint64 perWinnerCapTotal, uint64 ticketPrice) const + { + CalcReserveTopUp_input input{}; + CalcReserveTopUp_output output{}; + CalcReserveTopUp_locals locals{}; + + input.totalQRPBalance = totalQRPBalance; + input.needed = needed; + input.perWinnerCapTotal = perWinnerCapTotal; + input.ticketPrice = ticketPrice; + CalcReserveTopUp(qpi, *this, input, output, locals); + return output; + } + + CalculatePrizePools_output callCalculatePrizePools(const QPI::QpiContextFunctionCall& qpi, uint64 revenue, bit applyFRRake) const + { + CalculatePrizePools_input input{}; + CalculatePrizePools_output output{}; + CalculatePrizePools_locals locals{}; + + input.revenue = revenue; + input.applyFRRake = applyFRRake; + CalculatePrizePools(qpi, *this, input, output, locals); + return output; + } + + CalculateBaseGain_output callCalculateBaseGain(const QPI::QpiContextFunctionCall& qpi, uint64 revenue, uint64 winnersBlock) const + { + CalculateBaseGain_input input{}; + CalculateBaseGain_output output{}; + CalculateBaseGain_locals locals{}; + + input.revenue = revenue; + input.winnersBlock = winnersBlock; + CalculateBaseGain(qpi, *this, input, output, locals); + return output; + } + + CalculateExtraRedirectBP_output callCalculateExtraRedirectBP(const QPI::QpiContextFunctionCall& qpi, uint64 N, uint64 delta, uint64 revenue, + uint64 baseGain) const + { + CalculateExtraRedirectBP_input input{}; + CalculateExtraRedirectBP_output output{}; + CalculateExtraRedirectBP_locals locals{}; + + input.N = N; + input.delta = delta; + input.revenue = revenue; + input.baseGain = baseGain; + CalculateExtraRedirectBP(qpi, *this, input, output, locals); + return output; + } + + void callReturnAllTickets(const QPI::QpiContextProcedureCall& qpi) + { + ReturnAllTickets_input input{}; + ReturnAllTickets_output output{}; + ReturnAllTickets_locals locals{}; + + ReturnAllTickets(qpi, *this, input, output, locals); + } + + ProcessTierPayout_output callProcessTierPayout(const QPI::QpiContextProcedureCall& qpi, uint64 floorPerWinner, uint64 winnerCount, + uint64 payoutPool, uint64 perWinnerCap, uint64 totalQRPBalance, uint64 ticketPrice) + { + ProcessTierPayout_input input{}; + ProcessTierPayout_output output{}; + ProcessTierPayout_locals locals{}; + + input.floorPerWinner = floorPerWinner; + input.winnerCount = winnerCount; + input.payoutPool = payoutPool; + input.perWinnerCap = perWinnerCap; + input.totalQRPBalance = totalQRPBalance; + input.ticketPrice = ticketPrice; + ProcessTierPayout(qpi, *this, input, output, locals); + return output; + } +}; + +class ContractTestingQTF : protected ContractTesting +{ +public: + ContractTestingQTF() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QRP); + INIT_CONTRACT(RL); + INIT_CONTRACT(QTF); + + // Initialize QRP first (QTF depends on it for reserve operations) + callSystemProcedure(QRP_CONTRACT_INDEX, INITIALIZE); + // Initialize RL (RandomLottery contract) + callSystemProcedure(RL_CONTRACT_INDEX, INITIALIZE); + // Initialize QTF + system.epoch = contractDescriptions[QTF_CONTRACT_INDEX].constructionEpoch; + callSystemProcedure(QTF_CONTRACT_INDEX, INITIALIZE); + } + + // Access internal contract state + QTFChecker* state() { return reinterpret_cast(contractStates[QTF_CONTRACT_INDEX]); } + + id qtfSelf() { return id(QTF_CONTRACT_INDEX, 0, 0, 0); } + id qrpSelf() { return id(QRP_CONTRACT_INDEX, 0, 0, 0); } + void addPlayerDirect(const id& playerId, const QTFRandomValues& randomValues) { state()->addPlayerDirect(playerId, randomValues); } + + // Public function wrappers + QTF::GetTicketPrice_output getTicketPrice() + { + QTF::GetTicketPrice_input input{}; + QTF::GetTicketPrice_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_GET_TICKET_PRICE, input, output); + return output; + } + + QTF::GetNextEpochData_output getNextEpochData() + { + QTF::GetNextEpochData_input input{}; + QTF::GetNextEpochData_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_GET_NEXT_EPOCH_DATA, input, output); + return output; + } + + QTF::GetWinnerData_output getWinnerData() + { + QTF::GetWinnerData_input input{}; + QTF::GetWinnerData_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_GET_WINNER_DATA, input, output); + return output; + } + + QTF::GetPools_output getPools() + { + QTF::GetPools_input input{}; + QTF::GetPools_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_GET_POOLS, input, output); + return output; + } + + QTF::GetSchedule_output getSchedule() + { + QTF::GetSchedule_input input{}; + QTF::GetSchedule_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_GET_SCHEDULE, input, output); + return output; + } + + QTF::GetDrawHour_output getDrawHour() + { + QTF::GetDrawHour_input input{}; + QTF::GetDrawHour_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_GET_DRAW_HOUR, input, output); + return output; + } + + QTF::GetState_output getStateInfo() + { + QTF::GetState_input input{}; + QTF::GetState_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_GET_STATE, input, output); + return output; + } + + QTF::GetFees_output getFees() + { + QTF::GetFees_input input{}; + QTF::GetFees_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_GET_FEES, input, output); + return output; + } + + QTF::EstimatePrizePayouts_output estimatePrizePayouts(uint64 k2WinnerCount, uint64 k3WinnerCount) + { + QTF::EstimatePrizePayouts_input input{}; + input.k2WinnerCount = k2WinnerCount; + input.k3WinnerCount = k3WinnerCount; + QTF::EstimatePrizePayouts_output output{}; + callFunction(QTF_CONTRACT_INDEX, QTF_FUNCTION_ESTIMATE_PRIZE_PAYOUTS, input, output); + return output; + } + + // Procedure wrappers + QTF::BuyTicket_output buyTicket(const id& user, uint64 reward, const QTFRandomValues& numbers) + { + QTF::BuyTicket_input input{}; + input.randomValues = numbers; + QTF::BuyTicket_output output{}; + if (!invokeUserProcedure(QTF_CONTRACT_INDEX, QTF_PROCEDURE_BUY_TICKET, input, output, user, reward)) + { + output.returnCode = static_cast(QTF::EReturnCode::MAX_VALUE); + } + return output; + } + + QTF::SetPrice_output setPrice(const id& invocator, uint64 newPrice) + { + QTF::SetPrice_input input{}; + input.newPrice = newPrice; + QTF::SetPrice_output output{}; + if (!invokeUserProcedure(QTF_CONTRACT_INDEX, QTF_PROCEDURE_SET_PRICE, input, output, invocator, 0)) + { + output.returnCode = static_cast(QTF::EReturnCode::MAX_VALUE); + } + return output; + } + + QTF::SetSchedule_output setSchedule(const id& invocator, uint8 newSchedule) + { + QTF::SetSchedule_input input{}; + input.newSchedule = newSchedule; + QTF::SetSchedule_output output{}; + if (!invokeUserProcedure(QTF_CONTRACT_INDEX, QTF_PROCEDURE_SET_SCHEDULE, input, output, invocator, 0)) + { + output.returnCode = static_cast(QTF::EReturnCode::MAX_VALUE); + } + return output; + } + + QTF::SetTargetJackpot_output setTargetJackpot(const id& invocator, uint64 newTarget) + { + QTF::SetTargetJackpot_input input{}; + input.newTargetJackpot = newTarget; + QTF::SetTargetJackpot_output output{}; + if (!invokeUserProcedure(QTF_CONTRACT_INDEX, QTF_PROCEDURE_SET_TARGET_JACKPOT, input, output, invocator, 0)) + { + output.returnCode = static_cast(QTF::EReturnCode::MAX_VALUE); + } + return output; + } + + QTF::SetDrawHour_output setDrawHour(const id& invocator, uint8 newHour) + { + QTF::SetDrawHour_input input{}; + input.newDrawHour = newHour; + QTF::SetDrawHour_output output{}; + if (!invokeUserProcedure(QTF_CONTRACT_INDEX, QTF_PROCEDURE_SET_DRAW_HOUR, input, output, invocator, 0)) + { + output.returnCode = static_cast(QTF::EReturnCode::MAX_VALUE); + } + return output; + } + + // System procedure wrappers + void beginEpoch() { callSystemProcedure(QTF_CONTRACT_INDEX, BEGIN_EPOCH); } + void endEpoch() { callSystemProcedure(QTF_CONTRACT_INDEX, END_EPOCH); } + void beginTick() { callSystemProcedure(QTF_CONTRACT_INDEX, BEGIN_TICK); } + + void setDateTime(uint16 year, uint8 month, uint8 day, uint8 hour) + { + updateTime(); + utcTime.Year = year; + utcTime.Month = month; + utcTime.Day = day; + utcTime.Hour = hour; + utcTime.Minute = 0; + utcTime.Second = 0; + utcTime.Nanosecond = 0; + updateQpiTime(); + } + + void forceBeginTick() + { + system.tick = system.tick + (RL_TICK_UPDATE_PERIOD - (system.tick % RL_TICK_UPDATE_PERIOD)); + beginTick(); + } + + void beginEpochWithDate(uint16 year, uint8 month, uint8 day, uint8 hour = 12) + { + setDateTime(year, month, day, hour); + beginEpoch(); + } + + void beginEpochWithValidTime() { beginEpochWithDate(2025, 1, 20); } + + // Force schedule mask directly in state + void forceSchedule(uint8 scheduleMask) { state()->setScheduleMask(scheduleMask); } + + void forceFRDisabledForBaseline() + { + state()->setFrActive(false); + state()->setFrRoundsSinceK4(QTF_FR_POST_K4_WINDOW_ROUNDS); + state()->setOverflowAlphaBP(QTF_BASELINE_OVERFLOW_ALPHA_BP); + } + + void forceFREnabledWithinWindow(uint16 roundsSinceK4 = 1) + { + state()->setFrActive(true); + state()->setFrRoundsSinceK4(roundsSinceK4); + } + + void startAnyDayEpoch() + { + forceSchedule(QTF_ANY_DAY_SCHEDULE); + beginEpochWithValidTime(); + } + + // Trigger a tick that performs the draw (time is set to a scheduled day and hour). + void triggerDrawTick() + { + constexpr uint16 y = 2025; + constexpr uint8 m = 1; + constexpr uint8 d = 10; + setDateTime(y, m, d, 12); + __pauseLogMessage(); + forceBeginTick(); + } + + // Helper to create valid random values + QTFRandomValues makeValidNumbers(uint8 n1, uint8 n2, uint8 n3, uint8 n4) + { + QTFRandomValues values; + values.set(0, n1); + values.set(1, n2); + values.set(2, n3); + values.set(3, n4); + return values; + } + + // Fund user and buy a ticket + void fundAndBuyTicket(const id& user, uint64 ticketPrice, const QTFRandomValues& numbers) + { + increaseEnergy(user, ticketPrice * 2); + const QTF::BuyTicket_output out = buyTicket(user, ticketPrice, numbers); + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + } + + // Set prevSpectrumDigest for deterministic random number generation + // This allows tests to predict winning numbers by fixing the RNG seed + void setPrevSpectrumDigest(const m256i& digest) { etalonTick.prevSpectrumDigest = digest; } + + void drawWithDigest(const m256i& digest) + { + setPrevSpectrumDigest(digest); + triggerDrawTick(); + } + + // Compute the winning numbers that would be generated for a given prevSpectrumDigest. + // Uses the contract GetRandomValues() implementation (so tests don't duplicate RNG logic). + // Returns values in generation order (not sorted). + QTFRandomValues computeWinningNumbersForDigest(const m256i& digest) + { + m256i hashResult; + KangarooTwelve((const uint8*)&digest, sizeof(m256i), (uint8*)&hashResult, sizeof(m256i)); + const uint64 seed = hashResult.m256i_u64[0]; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + const auto out = state()->callGetRandomValues(qpi, seed); + return out.values; + } + + struct WinningAndLosing + { + QTFRandomValues winning; + QTFRandomValues losing; + }; + + QTFRandomValues makeLosingNumbers(const QTFRandomValues& winningNumbers) + { + bool isWinning[31] = {}; + for (uint64 i = 0; i < QTF_RANDOM_VALUES_COUNT; ++i) + { + isWinning[winningNumbers.get(i)] = true; + } + + QTFRandomValues losingNumbers; + uint64 outIndex = 0; + for (uint8 candidate = 1; candidate <= QTF_MAX_RANDOM_VALUE && outIndex < QTF_RANDOM_VALUES_COUNT; ++candidate) + { + if (!isWinning[candidate]) + { + losingNumbers.set(outIndex++, candidate); + } + } + EXPECT_EQ(outIndex, static_cast(QTF_RANDOM_VALUES_COUNT)); + return losingNumbers; + } + + WinningAndLosing computeWinningAndLosing(const m256i& digest) + { + WinningAndLosing out; + out.winning = computeWinningNumbersForDigest(digest); + out.losing = makeLosingNumbers(out.winning); + return out; + } + + void buyRandomTickets(uint64 count, uint64 ticketPrice, const QTFRandomValues& numbers) + { + for (uint64 i = 0; i < count; ++i) + { + const id user = id::randomValue(); + fundAndBuyTicket(user, ticketPrice, numbers); + } + } + + // Create a ticket that matches exactly `matchCount` numbers with `winningNumbers`. + // `variant` makes it deterministic to generate multiple distinct tickets for the same winning set. + // Guarantees values are unique and in [1..30]. + QTFRandomValues makeNumbersWithExactMatches(const QTFRandomValues& winningNumbers, uint8 matchCount, uint8 variant = 0) + { + EXPECT_LE(matchCount, static_cast(QTF_RANDOM_VALUES_COUNT)); + + bool isWinning[31] = {}; + bool used[31] = {}; + for (uint64 i = 0; i < QTF_RANDOM_VALUES_COUNT; ++i) + { + const uint8 v = winningNumbers.get(i); + EXPECT_GE(v, 1u); + EXPECT_LE(v, QTF_MAX_RANDOM_VALUE); + EXPECT_FALSE(isWinning[v]) << "winningNumbers must be unique"; + isWinning[v] = true; + } + + QTFRandomValues ticket; + uint64 outIndex = 0; + + // Take `matchCount` winning numbers as the matches (variant-dependent, wrap around 4). + for (uint8 i = 0; i < matchCount; ++i) + { + const uint8 v = winningNumbers.get((variant + i) % QTF_RANDOM_VALUES_COUNT); + used[v] = true; + ticket.set(outIndex++, v); + } + + // Fill the remaining positions with non-winning numbers. + const uint8 start = static_cast((variant * 7) % QTF_MAX_RANDOM_VALUE + 1); + for (uint8 step = 0; step < QTF_MAX_RANDOM_VALUE && outIndex < QTF_RANDOM_VALUES_COUNT; ++step) + { + const uint8 candidate = static_cast(((start - 1 + step) % QTF_MAX_RANDOM_VALUE) + 1); + if (!isWinning[candidate] && !used[candidate]) + { + used[candidate] = true; + ticket.set(outIndex++, candidate); + } + } + + EXPECT_EQ(outIndex, static_cast(QTF_RANDOM_VALUES_COUNT)); + + // Verify exact overlap count and uniqueness (debug safety for tests). + uint64 overlap = 0; + std::set uniqueValues; + for (uint64 i = 0; i < QTF_RANDOM_VALUES_COUNT; ++i) + { + const uint8 v = ticket.get(i); + EXPECT_GE(v, 1u); + EXPECT_LE(v, QTF_MAX_RANDOM_VALUE); + uniqueValues.insert(v); + if (isWinning[v]) + { + ++overlap; + } + } + EXPECT_EQ(uniqueValues.size(), static_cast(QTF_RANDOM_VALUES_COUNT)); + EXPECT_EQ(overlap, static_cast(matchCount)); + + return ticket; + } + + QTFRandomValues makeK2Numbers(const QTFRandomValues& winningNumbers, uint8 variant = 0) + { + return makeNumbersWithExactMatches(winningNumbers, 2, variant); + } + QTFRandomValues makeK3Numbers(const QTFRandomValues& winningNumbers, uint8 variant = 0) + { + return makeNumbersWithExactMatches(winningNumbers, 3, variant); + } +}; + +// ============================================================================ +// PRIVATE METHOD TESTS +// ============================================================================ + +TEST(ContractQThirtyFour_Private, CountMatches_CountsOverlappingNumbers) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + // Include values > 8 to cover the full [1..30] bitmask range. + const QTFRandomValues player = ctl.makeValidNumbers(1, 16, 29, 30); + const QTFRandomValues winning = ctl.makeValidNumbers(16, 29, 2, 3); + const auto out = ctl.state()->callCountMatches(qpi, player, winning); + EXPECT_EQ(out.matches, 2); +} + +TEST(ContractQThirtyFour_Private, ValidateNumbers_WorksForValidDuplicateAndRangeErrors) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + const QTFRandomValues ok = ctl.makeValidNumbers(1, 2, 3, 4); + EXPECT_TRUE(ctl.state()->callValidateNumbers(qpi, ok).isValid); + + QTFRandomValues dup = ctl.makeValidNumbers(1, 2, 3, 4); + dup.set(3, 2); + EXPECT_FALSE(ctl.state()->callValidateNumbers(qpi, dup).isValid); + + QTFRandomValues outOfRange = ctl.makeValidNumbers(1, 2, 3, 4); + outOfRange.set(2, 31); + EXPECT_FALSE(ctl.state()->callValidateNumbers(qpi, outOfRange).isValid); +} + +TEST(ContractQThirtyFour_Private, GetRandomValues_IsDeterministicAndUniqueInRange) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + const uint64 seed = 0x123456789ABCDEF0ULL; + const auto out1 = ctl.state()->callGetRandomValues(qpi, seed); + const auto out2 = ctl.state()->callGetRandomValues(qpi, seed); + EXPECT_TRUE(valuesEqual(out1.values, out2.values)); + + std::set seen; + for (uint64 i = 0; i < QTF_RANDOM_VALUES_COUNT; ++i) + { + const uint8 v = out1.values.get(i); + EXPECT_GE(v, 1); + EXPECT_LE(v, QTF_MAX_RANDOM_VALUE); + seen.insert(v); + EXPECT_EQ(out1.values.get(i), out2.values.get(i)); + } + EXPECT_EQ(seen.size(), static_cast(QTF_RANDOM_VALUES_COUNT)); +} + +TEST(ContractQThirtyFour_Private, CheckContractBalance_UsesIncomingMinusOutgoing) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + const uint64 balance = 123456; + increaseEnergy(ctl.qtfSelf(), balance); + + const auto outExact = ctl.state()->callCheckContractBalance(qpi, balance); + EXPECT_TRUE(outExact.hasEnough); + EXPECT_EQ(outExact.actualBalance, balance); + + const auto outTooHigh = ctl.state()->callCheckContractBalance(qpi, balance + 1); + EXPECT_FALSE(outTooHigh.hasEnough); + EXPECT_EQ(outTooHigh.actualBalance, balance); +} + +TEST(ContractQThirtyFour_Private, PowerFixedPoint_ComputesFastExponentiationInFixedPoint) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + // 0.5^2 = 0.25 + const auto out025 = ctl.state()->callPowerFixedPoint(qpi, QTF_FIXED_POINT_SCALE / 2, 2); + EXPECT_EQ(out025.result, QTF_FIXED_POINT_SCALE / 4); + + // 2.0^3 = 8.0 + const auto out8 = ctl.state()->callPowerFixedPoint(qpi, 2 * QTF_FIXED_POINT_SCALE, 3); + EXPECT_EQ(out8.result, 8 * QTF_FIXED_POINT_SCALE); +} + +TEST(ContractQThirtyFour_Private, CalculateExpectedRoundsToK4_HandlesEdgeCaseAndMonotonicity) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + const auto out0 = ctl.state()->callCalculateExpectedRoundsToK4(qpi, 0); + EXPECT_EQ(out0.expectedRounds, UINT64_MAX); + + const auto out1 = ctl.state()->callCalculateExpectedRoundsToK4(qpi, 1); + const auto out100 = ctl.state()->callCalculateExpectedRoundsToK4(qpi, 100); + EXPECT_GT(out1.expectedRounds, 0ULL); + EXPECT_GT(out100.expectedRounds, 0ULL); + EXPECT_LE(out1.expectedRounds, QTF_FIXED_POINT_SCALE); + EXPECT_LE(out100.expectedRounds, QTF_FIXED_POINT_SCALE); + EXPECT_GT(out1.expectedRounds, out100.expectedRounds); +} + +TEST(ContractQThirtyFour_Private, CalcReserveTopUp_RespectsSoftFloorPerRoundAndPerWinnerCaps) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + const uint64 P = 1000000ULL; + + // Below soft floor => nothing can be topped up. + { + const uint64 softFloor = smul(P, QTF_RESERVE_SOFT_FLOOR_MULT); + const auto out = ctl.state()->callCalcReserveTopUp(qpi, softFloor - 1, 1000ULL, 1000000000ULL, P); + EXPECT_EQ(out.topUpAmount, 0ULL); + } + + // Soft floor binds availableAboveFloor and per-round is 10% of total. + { + const auto out = ctl.state()->callCalcReserveTopUp(qpi, 25000000ULL, 10000000ULL, 1000000000ULL, P); + EXPECT_EQ(out.topUpAmount, 2500000ULL); + } + + // Per-winner cap binds. + { + const auto out = ctl.state()->callCalcReserveTopUp(qpi, 1000000000ULL, 50000000ULL, 1000000ULL, P); + EXPECT_EQ(out.topUpAmount, 1000000ULL); + } + + // Needed is below all caps. + { + const auto out = ctl.state()->callCalcReserveTopUp(qpi, 1000000000ULL, 12345ULL, 1000000000ULL, P); + EXPECT_EQ(out.topUpAmount, 12345ULL); + } +} + +TEST(ContractQThirtyFour_Private, CalculatePrizePools_MatchesFeeAndRakeMath) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + const auto fees = ctl.getFees(); + ASSERT_NE(fees.winnerFeePercent, 0); + + const uint64 revenue = 1000000ULL; + const uint64 winnersBlockBeforeRake = (revenue * static_cast(fees.winnerFeePercent)) / 100ULL; + + { + const auto out = ctl.state()->callCalculatePrizePools(qpi, revenue, false); + EXPECT_EQ(out.winnersRake, 0ULL); + EXPECT_EQ(out.winnersBlock, winnersBlockBeforeRake); + EXPECT_EQ(out.k3Pool, (out.winnersBlock * QTF_BASE_K3_SHARE_BP) / 10000ULL); + EXPECT_EQ(out.k2Pool, (out.winnersBlock * QTF_BASE_K2_SHARE_BP) / 10000ULL); + } + + { + const auto out = ctl.state()->callCalculatePrizePools(qpi, revenue, true); + const uint64 expectedRake = (winnersBlockBeforeRake * QTF_FR_WINNERS_RAKE_BP) / 10000ULL; + EXPECT_EQ(out.winnersRake, expectedRake); + EXPECT_EQ(out.winnersBlock, winnersBlockBeforeRake - expectedRake); + EXPECT_EQ(out.k3Pool, (out.winnersBlock * QTF_BASE_K3_SHARE_BP) / 10000ULL); + EXPECT_EQ(out.k2Pool, (out.winnersBlock * QTF_BASE_K2_SHARE_BP) / 10000ULL); + } +} + +TEST(ContractQThirtyFour_Private, CalculateBaseGain_FollowsConfiguredRedirectsAndOverflowBias) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + const uint64 revenue = 1000000ULL; + const uint64 winnersBlock = 680000ULL; + + const auto out = ctl.state()->callCalculateBaseGain(qpi, revenue, winnersBlock); + EXPECT_EQ(out.baseGain, 118600ULL); +} + +TEST(ContractQThirtyFour_Private, CalculateExtraRedirectBP_ReturnsZeroOrClampsToMax) +{ + ContractTestingQTF ctl; + + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + + // Early exits + EXPECT_EQ(ctl.state()->callCalculateExtraRedirectBP(qpi, 0, 1, 1, 0).extraBP, 0ULL); + EXPECT_EQ(ctl.state()->callCalculateExtraRedirectBP(qpi, 1, 0, 1, 0).extraBP, 0ULL); + EXPECT_EQ(ctl.state()->callCalculateExtraRedirectBP(qpi, 1, 1, 0, 0).extraBP, 0ULL); + + // Clamp to max under large deficit. + { + const uint64 revenue = 1000000ULL; + const uint64 delta = revenue * 1000ULL; + const auto out = ctl.state()->callCalculateExtraRedirectBP(qpi, 100, delta, revenue, 0); + EXPECT_EQ(out.extraBP, QTF_FR_EXTRA_MAX_BP); + } + + // Base gain already covers required gain -> zero. + { + const auto out = ctl.state()->callCalculateExtraRedirectBP(qpi, 100, 1000ULL, 1000000ULL, 2000ULL); + EXPECT_EQ(out.extraBP, 0ULL); + } +} + +TEST(ContractQThirtyFour_Private, ProcessTierPayout_ComputesPayoutAndOptionalTopUp) +{ + ContractTestingQTF ctl; + + const id originator = id::randomValue(); + QpiContextUserProcedureCall qpi(QTF_CONTRACT_INDEX, originator, 0); + primeQpiProcedureContext(qpi, static_cast(ctl.state()->getDrawHourInternal())); + + // No winners -> all overflow. + { + const auto out = ctl.state()->callProcessTierPayout(qpi, 50, 0, 123, 100, 0, 1000000ULL); + EXPECT_EQ(out.perWinnerPayout, 0ULL); + EXPECT_EQ(out.overflow, 123ULL); + EXPECT_EQ(out.topUpReceived, 0ULL); + } + + // Top-up from QRP to meet floor. + { + const uint64 qrpBalanceBefore = 1000000000ULL; + increaseEnergy(ctl.qrpSelf(), qrpBalanceBefore); + + const uint64 qtfBalanceBefore = getBalance(ctl.qtfSelf()); + const uint64 qrpBalanceBeforeActual = getBalance(ctl.qrpSelf()); + + const auto out = ctl.state()->callProcessTierPayout(qpi, 50, 2, 10, 100, qrpBalanceBeforeActual, 1000000ULL); + EXPECT_EQ(out.perWinnerPayout, 50ULL); + EXPECT_EQ(out.overflow, 0ULL); + EXPECT_EQ(out.topUpReceived, 90ULL); + + EXPECT_EQ(getBalance(ctl.qtfSelf()), qtfBalanceBefore + 90); + EXPECT_EQ(getBalance(ctl.qrpSelf()), qrpBalanceBeforeActual - 90); + } + + // Per-winner cap applies and leaves overflow. + { + const uint64 P = 1000000ULL; + const uint64 cap = smul(P, QTF_TOPUP_PER_WINNER_CAP_MULT); + const auto out = ctl.state()->callProcessTierPayout(qpi, div(P, 2), 1, sadd(cap, 1234ULL), cap, 0, P); + EXPECT_EQ(out.perWinnerPayout, cap); + EXPECT_EQ(out.topUpReceived, 0ULL); + EXPECT_EQ(out.overflow, 1234ULL); + } +} + +TEST(ContractQThirtyFour_Private, ReturnAllTickets_RefundsEachPlayerAndClearsViaSettleEpochRevenueZeroBranch) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + const id originator = id::randomValue(); + QpiContextUserProcedureCall qpi(QTF_CONTRACT_INDEX, originator, 0); + primeQpiProcedureContext(qpi, static_cast(ctl.state()->getDrawHourInternal())); + + // Setup a few players and refund them. + const uint64 ticketPrice = 10; + ctl.state()->setTicketPriceInternal(ticketPrice); + + const id p1 = id::randomValue(); + const id p2 = id::randomValue(); + const QTFRandomValues n1 = ctl.makeValidNumbers(1, 2, 3, 4); + const QTFRandomValues n2 = ctl.makeValidNumbers(5, 6, 7, 8); + ctl.addPlayerDirect(p1, n1); + ctl.addPlayerDirect(p2, n2); + + increaseEnergy(ctl.qtfSelf(), ticketPrice * 2); + const uint64 balBeforeContract = getBalance(ctl.qtfSelf()); + const uint64 balBeforeP1 = getBalance(p1); + const uint64 balBeforeP2 = getBalance(p2); + + ctl.state()->callReturnAllTickets(qpi); + + EXPECT_EQ(getBalance(p1), balBeforeP1 + ticketPrice); + EXPECT_EQ(getBalance(p2), balBeforeP2 + ticketPrice); + EXPECT_EQ(getBalance(ctl.qtfSelf()), balBeforeContract - (ticketPrice * 2)); + + // Now exercise SettleEpoch revenue==0 branch, which must clear players. + ctl.state()->setTicketPriceInternal(0); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 2ULL); + ctl.triggerDrawTick(); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0ULL); +} + +// ============================================================================ +// BUY TICKET TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, BuyTicket_WhenSellingClosed_RefundsAndFails) +{ + ContractTestingQTF ctl; + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Selling is closed initially (before beginEpoch with valid time) + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 2); + const uint64 balBefore = getBalance(user); + + QTFRandomValues nums = ctl.makeValidNumbers(1, 2, 3, 4); + const QTF::BuyTicket_output out = ctl.buyTicket(user, ticketPrice, nums); + + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(getBalance(user), balBefore); // Refunded + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, BuyTicket_TooLowPrice_RefundsAndFails) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 5); + const uint64 balBefore = getBalance(user); + + QTFRandomValues nums = ctl.makeValidNumbers(1, 2, 3, 4); + + // Test with price too low - should fail and refund + const QTF::BuyTicket_output outLow = ctl.buyTicket(user, ticketPrice - 1, nums); + EXPECT_EQ(outLow.returnCode, static_cast(QTF::EReturnCode::INVALID_TICKET_PRICE)); + EXPECT_EQ(getBalance(user), balBefore); // Fully refunded + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, BuyTicket_ZeroPrice_RefundsAndFails) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 2); + const uint64 balBefore = getBalance(user); + + const QTFRandomValues nums = ctl.makeValidNumbers(1, 2, 3, 4); + + const QTF::BuyTicket_output out = ctl.buyTicket(user, 0, nums); + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::INVALID_TICKET_PRICE)); + EXPECT_EQ(getBalance(user), balBefore); // Fully refunded (0) + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, BuyTicket_OverpaidPrice_AcceptsAndReturnsExcess) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id user = id::randomValue(); + const uint64 overpayment = ticketPrice * 2; // Pay double + increaseEnergy(user, overpayment * 2); + const uint64 balBefore = getBalance(user); + + QTFRandomValues nums = ctl.makeValidNumbers(1, 2, 3, 4); + + // Test with overpayment - should accept ticket and return excess + const QTF::BuyTicket_output outHigh = ctl.buyTicket(user, overpayment, nums); + EXPECT_EQ(outHigh.returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + + // Should have paid exactly ticketPrice, excess returned + const uint64 excess = overpayment - ticketPrice; + EXPECT_EQ(getBalance(user), balBefore - ticketPrice) << "User should pay exactly ticket price, excess returned"; + + // Ticket should be registered + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 1u); +} + +TEST(ContractQThirtyFour, BuyTicket_OverpaidInvalidNumbers_RefundsFull_NoLeak) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id user = id::randomValue(); + const uint64 overpayment = ticketPrice * 2; + increaseEnergy(user, overpayment * 2); + const uint64 balBefore = getBalance(user); + + // Invalid: out of range + const QTFRandomValues invalidNums = ctl.makeValidNumbers(1, 2, 3, 31); + const QTF::BuyTicket_output out = ctl.buyTicket(user, overpayment, invalidNums); + + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::INVALID_NUMBERS)); + EXPECT_EQ(getBalance(user), balBefore) << "Full invocationReward must be refunded once"; + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, BuyTicket_InvalidNumbers_OutOfRange_Fails) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 10); + const uint64 balBefore = getBalance(user); + + // Number 0 is invalid (valid range is 1-30) + QTFRandomValues numsWithZero; + numsWithZero.set(0, 0); + numsWithZero.set(1, 2); + numsWithZero.set(2, 3); + numsWithZero.set(3, 4); + + const QTF::BuyTicket_output out1 = ctl.buyTicket(user, ticketPrice, numsWithZero); + EXPECT_EQ(out1.returnCode, static_cast(QTF::EReturnCode::INVALID_NUMBERS)); + EXPECT_EQ(getBalance(user), balBefore); + + // Number 31 is invalid (valid range is 1-30) + QTFRandomValues numsOver30; + numsOver30.set(0, 1); + numsOver30.set(1, 2); + numsOver30.set(2, 3); + numsOver30.set(3, 31); + + const QTF::BuyTicket_output out2 = ctl.buyTicket(user, ticketPrice, numsOver30); + EXPECT_EQ(out2.returnCode, static_cast(QTF::EReturnCode::INVALID_NUMBERS)); + EXPECT_EQ(getBalance(user), balBefore); + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, BuyTicket_DuplicateNumbers_Fails) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 5); + const uint64 balBefore = getBalance(user); + + // Duplicate number 5 + QTFRandomValues dupNums; + dupNums.set(0, 5); + dupNums.set(1, 5); + dupNums.set(2, 10); + dupNums.set(3, 15); + + const QTF::BuyTicket_output out = ctl.buyTicket(user, ticketPrice, dupNums); + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::INVALID_NUMBERS)); + EXPECT_EQ(getBalance(user), balBefore); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, BuyTicket_ValidPurchase_Success) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 2); + const uint64 balBefore = getBalance(user); + + QTFRandomValues nums = ctl.makeValidNumbers(5, 10, 15, 20); + + const QTF::BuyTicket_output out = ctl.buyTicket(user, ticketPrice, nums); + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + EXPECT_EQ(getBalance(user), balBefore - ticketPrice); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 1u); + + // Verify player data stored correctly + const QTF::PlayerData& player = ctl.state()->getPlayer(0); + EXPECT_EQ(player.player, user); + EXPECT_EQ(player.randomValues.get(0), 5); + EXPECT_EQ(player.randomValues.get(1), 10); + EXPECT_EQ(player.randomValues.get(2), 15); + EXPECT_EQ(player.randomValues.get(3), 20); +} + +TEST(ContractQThirtyFour, BuyTicket_MultiplePlayers_Success) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Add 10 different players + for (int i = 0; i < 10; ++i) + { + const id user = id::randomValue(); + QTFRandomValues nums = ctl.makeValidNumbers(static_cast(1 + i), static_cast(11 + i), 21, 30); + + ctl.fundAndBuyTicket(user, ticketPrice, nums); + } + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 10u); +} + +TEST(ContractQThirtyFour, BuyTicket_MaxPlayersReached_Fails) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Fill up to max players (1024) + for (uint64 i = 0; i < QTF_MAX_NUMBER_OF_PLAYERS; ++i) + { + const id user = id::randomValue(); + QTFRandomValues nums = ctl.makeValidNumbers(static_cast((i % 27) + 1), static_cast(((i + 1) % 27) + 1), + static_cast(((i + 2) % 27) + 1), static_cast(((i + 3) % 27) + 1)); + + // Only fund and buy; we expect all to succeed until max + increaseEnergy(user, ticketPrice * 2); + const QTF::BuyTicket_output out = ctl.buyTicket(user, ticketPrice, nums); + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + } + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), QTF_MAX_NUMBER_OF_PLAYERS); + + // Try one more - should fail + const id extraUser = id::randomValue(); + increaseEnergy(extraUser, ticketPrice * 2); + const uint64 balBefore = getBalance(extraUser); + QTFRandomValues nums = ctl.makeValidNumbers(1, 2, 3, 4); + + const QTF::BuyTicket_output out = ctl.buyTicket(extraUser, ticketPrice, nums); + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::MAX_PLAYERS_REACHED)); + EXPECT_EQ(getBalance(extraUser), balBefore); // Refunded +} + +TEST(ContractQThirtyFour, BuyTicket_SamePlayerMultipleTickets_Allowed) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 10); + + // Same player buys multiple tickets with different numbers + QTFRandomValues nums1 = ctl.makeValidNumbers(1, 2, 3, 4); + QTFRandomValues nums2 = ctl.makeValidNumbers(5, 6, 7, 8); + QTFRandomValues nums3 = ctl.makeValidNumbers(9, 10, 11, 12); + + EXPECT_EQ(ctl.buyTicket(user, ticketPrice, nums1).returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.buyTicket(user, ticketPrice, nums2).returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.buyTicket(user, ticketPrice, nums3).returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 3u); +} + +// ============================================================================ +// CONFIGURATION CHANGE TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, SetPrice_AccessControl) +{ + ContractTestingQTF ctl; + + const uint64 oldPrice = ctl.state()->getTicketPriceInternal(); + const uint64 newPrice = oldPrice * 2; + + // Random user should be denied + const id randomUser = id::randomValue(); + increaseEnergy(randomUser, 1); + const QTF::SetPrice_output outDenied = ctl.setPrice(randomUser, newPrice); + EXPECT_EQ(outDenied.returnCode, static_cast(QTF::EReturnCode::ACCESS_DENIED)); + + // Price unchanged + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); +} + +TEST(ContractQThirtyFour, SetPrice_ZeroNotAllowed) +{ + ContractTestingQTF ctl; + increaseEnergy(QTF_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPriceInternal(); + const QTF::SetPrice_output outInvalid = ctl.setPrice(QTF_DEV_ADDRESS, 0); + EXPECT_EQ(outInvalid.returnCode, static_cast(QTF::EReturnCode::INVALID_TICKET_PRICE)); + + // Price unchanged + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); +} + +TEST(ContractQThirtyFour, SetPrice_AppliesAfterEndEpoch) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + increaseEnergy(QTF_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPriceInternal(); + const uint64 newPrice = oldPrice * 3; + + const QTF::SetPrice_output outOk = ctl.setPrice(QTF_DEV_ADDRESS, newPrice); + EXPECT_EQ(outOk.returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + + // Queued in NextEpochData + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newTicketPrice, newPrice); + + // Old price still active + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + + // Apply after END_EPOCH + ctl.endEpoch(); + ctl.beginEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); + + // NextEpochData cleared + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newTicketPrice, 0u); +} + +TEST(ContractQThirtyFour, SetSchedule_AccessControl) +{ + ContractTestingQTF ctl; + + const id randomUser = id::randomValue(); + increaseEnergy(randomUser, 1); + const QTF::SetSchedule_output outDenied = ctl.setSchedule(randomUser, QTF_ANY_DAY_SCHEDULE); + EXPECT_EQ(outDenied.returnCode, static_cast(QTF::EReturnCode::ACCESS_DENIED)); +} + +TEST(ContractQThirtyFour, SetSchedule_ZeroNotAllowed) +{ + ContractTestingQTF ctl; + increaseEnergy(QTF_DEV_ADDRESS, 1); + + const QTF::SetSchedule_output outInvalid = ctl.setSchedule(QTF_DEV_ADDRESS, 0); + EXPECT_EQ(outInvalid.returnCode, static_cast(QTF::EReturnCode::INVALID_VALUE)); +} + +TEST(ContractQThirtyFour, SetSchedule_AppliesAfterEndEpoch) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + increaseEnergy(QTF_DEV_ADDRESS, 1); + + const uint8 newSchedule = 0x7F; // All days + + const QTF::SetSchedule_output outOk = ctl.setSchedule(QTF_DEV_ADDRESS, newSchedule); + EXPECT_EQ(outOk.returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + + // Queued + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newSchedule, newSchedule); + + // Apply + ctl.endEpoch(); + ctl.beginEpoch(); + EXPECT_EQ(ctl.getSchedule().schedule, newSchedule); +} + +TEST(ContractQThirtyFour, SetTargetJackpot_AccessControl) +{ + ContractTestingQTF ctl; + + const id randomUser = id::randomValue(); + increaseEnergy(randomUser, 1); + const QTF::SetTargetJackpot_output outDenied = ctl.setTargetJackpot(randomUser, 2000000000ULL); + EXPECT_EQ(outDenied.returnCode, static_cast(QTF::EReturnCode::ACCESS_DENIED)); +} + +TEST(ContractQThirtyFour, SetTargetJackpot_ZeroNotAllowed) +{ + ContractTestingQTF ctl; + increaseEnergy(QTF_DEV_ADDRESS, 1); + + const QTF::SetTargetJackpot_output outInvalid = ctl.setTargetJackpot(QTF_DEV_ADDRESS, 0); + EXPECT_EQ(outInvalid.returnCode, static_cast(QTF::EReturnCode::INVALID_VALUE)); +} + +TEST(ContractQThirtyFour, SetTargetJackpot_AppliesAfterEndEpoch) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + increaseEnergy(QTF_DEV_ADDRESS, 1); + + const uint64 newTarget = 5000000000ULL; + + const QTF::SetTargetJackpot_output outOk = ctl.setTargetJackpot(QTF_DEV_ADDRESS, newTarget); + EXPECT_EQ(outOk.returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + + // Queued + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newTargetJackpot, newTarget); + + // Apply + ctl.endEpoch(); + ctl.beginEpoch(); + EXPECT_EQ(ctl.state()->getTargetJackpotInternal(), newTarget); +} + +TEST(ContractQThirtyFour, SetDrawHour_AccessControl) +{ + ContractTestingQTF ctl; + + const id randomUser = id::randomValue(); + increaseEnergy(randomUser, 1); + const QTF::SetDrawHour_output outDenied = ctl.setDrawHour(randomUser, 15); + EXPECT_EQ(outDenied.returnCode, static_cast(QTF::EReturnCode::ACCESS_DENIED)); +} + +TEST(ContractQThirtyFour, SetDrawHour_InvalidValues) +{ + ContractTestingQTF ctl; + increaseEnergy(QTF_DEV_ADDRESS, 2); + + // 0 is invalid + const QTF::SetDrawHour_output out0 = ctl.setDrawHour(QTF_DEV_ADDRESS, 0); + EXPECT_EQ(out0.returnCode, static_cast(QTF::EReturnCode::INVALID_VALUE)); + + // 24+ is invalid + const QTF::SetDrawHour_output out24 = ctl.setDrawHour(QTF_DEV_ADDRESS, 24); + EXPECT_EQ(out24.returnCode, static_cast(QTF::EReturnCode::INVALID_VALUE)); +} + +TEST(ContractQThirtyFour, SetDrawHour_AppliesAfterEndEpoch) +{ + ContractTestingQTF ctl; + ctl.beginEpochWithValidTime(); + increaseEnergy(QTF_DEV_ADDRESS, 1); + + const uint8 newHour = 18; + + const QTF::SetDrawHour_output outOk = ctl.setDrawHour(QTF_DEV_ADDRESS, newHour); + EXPECT_EQ(outOk.returnCode, static_cast(QTF::EReturnCode::SUCCESS)); + + // Queued + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newDrawHour, newHour); + + // Apply + ctl.endEpoch(); + ctl.beginEpoch(); + EXPECT_EQ(ctl.getDrawHour().drawHour, newHour); +} + +// ============================================================================ +// STATE AND POOLS TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, GetState_NoneThenSelling) +{ + ContractTestingQTF ctl; + + // Initially not selling + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(QTF::EState::STATE_NONE)); + + // After epoch start with valid time it should sell + ctl.beginEpochWithValidTime(); + EXPECT_EQ(ctl.getStateInfo().currentState, static_cast(QTF::EState::STATE_SELLING)); +} + +TEST(ContractQThirtyFour, GetPools_ReserveReflectsQRPAvailable) +{ + ContractTestingQTF ctl; + + const QTF::GetPools_output poolsBefore = ctl.getPools(); + const uint64 before = poolsBefore.pools.reserve; + + constexpr uint64 qrpFunding = 10'000'000'000ULL; + increaseEnergy(ctl.qrpSelf(), qrpFunding); + + const QTF::GetPools_output poolsAfter = ctl.getPools(); + const uint64 after = poolsAfter.pools.reserve; + + EXPECT_GE(after, before); + EXPECT_GT(after, 0u); + EXPECT_LE(after, before + qrpFunding); +} + +// ============================================================================ +// SETTLEMENT AND PAYOUT TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, Settlement_WithPlayers_FeesDistributed) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + ctl.forceFRDisabledForBaseline(); + + // Fix RNG so we can deterministically avoid winners (and especially k=4). + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x1010101010101010ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const QTF::GetFees_output fees = ctl.getFees(); + constexpr uint64 numPlayers = 10; + + // Ensure RL shares exist so distribution path is exercised deterministically. + const id shareholder1 = id::randomValue(); + const id shareholder2 = id::randomValue(); + constexpr uint32 shares1 = NUMBER_OF_COMPUTORS / 3; + constexpr uint32 shares2 = NUMBER_OF_COMPUTORS - shares1; + std::vector> rlShares{{shareholder1, shares1}, {shareholder2, shares2}}; + issueRlSharesTo(rlShares); + + // Verify FR is not active initially (baseline mode) + EXPECT_EQ(ctl.state()->getFrActive(), false); + + // Add players + ctl.buyRandomTickets(numPlayers, ticketPrice, nums.losing); + + const uint64 totalRevenue = ticketPrice * numPlayers; + const uint64 devBalBefore = getBalance(QTF_DEV_ADDRESS); + const uint64 sh1Before = getBalance(shareholder1); + const uint64 sh2Before = getBalance(shareholder2); + const uint64 rlBefore = getBalance(id(RL_CONTRACT_INDEX, 0, 0, 0)); + const uint64 contractBalBefore = getBalance(ctl.qtfSelf()); + + EXPECT_EQ(contractBalBefore, totalRevenue); + + ctl.drawWithDigest(testDigest); + + EXPECT_EQ(ctl.state()->getFrActive(), false); + + // In baseline mode (FR not active), dev receives full 10% of revenue + // No redirects are applied + const uint64 expectedDevFee = (totalRevenue * fees.teamFeePercent) / 100; + EXPECT_EQ(getBalance(QTF_DEV_ADDRESS), devBalBefore + expectedDevFee) + << "In baseline mode, dev should receive full " << static_cast(fees.teamFeePercent) << "% of revenue"; + + // Distribution is paid to RL shareholders with flooring to dividendPerShare and payback remainder to RL contract. + const uint64 expectedDistFee = (totalRevenue * fees.distributionFeePercent) / 100; + const uint64 dividendPerShare = expectedDistFee / NUMBER_OF_COMPUTORS; + const uint64 expectedSh1Gain = static_cast(shares1) * dividendPerShare; + const uint64 expectedSh2Gain = static_cast(shares2) * dividendPerShare; + const uint64 expectedPayback = expectedDistFee - (dividendPerShare * NUMBER_OF_COMPUTORS); + EXPECT_EQ(getBalance(shareholder1), sh1Before + expectedSh1Gain); + EXPECT_EQ(getBalance(shareholder2), sh2Before + expectedSh2Gain); + EXPECT_EQ(getBalance(id(RL_CONTRACT_INDEX, 0, 0, 0)), rlBefore + expectedPayback); + + // No winners -> winnersOverflow == winnersBlock. In baseline: 50/50 split reserve/jackpot. + const uint64 winnersBlock = (totalRevenue * fees.winnerFeePercent) / 100; + const uint64 reserveAdd = (winnersBlock * QTF_BASELINE_OVERFLOW_ALPHA_BP) / 10000; + const uint64 expectedJackpotAdd = winnersBlock - reserveAdd; + EXPECT_EQ(ctl.state()->getJackpot(), expectedJackpotAdd); + EXPECT_EQ(static_cast(getBalance(ctl.qtfSelf())), expectedJackpotAdd) << "Contract balance should match carry (jackpot) after settlement"; + + // Players cleared + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, Settlement_NoPlayers_NoChanges) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + const uint64 jackpotBefore = ctl.state()->getJackpot(); + const QTF::GetWinnerData_output winnersBefore = ctl.getWinnerData(); + + ctl.triggerDrawTick(); + + // No changes when no players + EXPECT_EQ(ctl.state()->getJackpot(), jackpotBefore); + const QTF::GetWinnerData_output winnersAfter = ctl.getWinnerData(); + EXPECT_EQ(winnersAfter.winnerData.winnerCounter, winnersBefore.winnerData.winnerCounter); +} + +TEST(ContractQThirtyFour, Settlement_InsufficientBalance_ClearsPlayersAndAbortsSettlement) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x3030303030303030ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + constexpr uint64 numPlayers = 2; + + ctl.buyRandomTickets(numPlayers, ticketPrice, nums.losing); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), numPlayers); + + // Drain the contract so CheckContractBalance() fails in SettleEpoch. + const uint64 totalRevenue = ticketPrice * numPlayers; + const int qtfIndex = spectrumIndex(ctl.qtfSelf()); + ASSERT_GE(qtfIndex, 0); + ASSERT_TRUE(decreaseEnergy(qtfIndex, totalRevenue)); + EXPECT_EQ(getBalance(ctl.qtfSelf()), 0); + + ctl.drawWithDigest(testDigest); + + // Even if refunds can't be paid (because we drained balance), the contract must clear the epoch state. + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0ULL); +} + +TEST(ContractQThirtyFour, Settlement_WithPlayers_FeesDistributed_FRMode) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + // Fix RNG so we can deterministically avoid winners (and especially k=4). + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x2020202020202020ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + // Activate FR mode + ctl.state()->setJackpot(100000000ULL); // Below target + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); + ctl.forceFREnabledWithinWindow(5); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const QTF::GetFees_output fees = ctl.getFees(); + constexpr uint64 numPlayers = 10; + + // Verify FR is active + EXPECT_EQ(ctl.state()->getFrActive(), true); + + // Add players + ctl.buyRandomTickets(numPlayers, ticketPrice, nums.losing); + + const uint64 totalRevenue = ticketPrice * numPlayers; + const uint64 devBalBefore = getBalance(QTF_DEV_ADDRESS); + const uint64 contractBalBefore = getBalance(ctl.qtfSelf()); + const uint64 jackpotBefore = ctl.state()->getJackpot(); + const uint64 roundsSinceK4Before = ctl.state()->getFrRoundsSinceK4(); + + EXPECT_EQ(contractBalBefore, totalRevenue); + + ctl.drawWithDigest(testDigest); + + // In FR mode, dev receives less than full 10% of revenue + // Base redirect: 1% of revenue (QTF_FR_DEV_REDIRECT_BP = 100 basis points) + // Possible extra redirect depending on deficit + const uint64 baseDevRedirect = (totalRevenue * QTF_FR_DEV_REDIRECT_BP) / 10000; + + // Full dev fee from revenue split (10%) + const uint64 fullDevFee = (totalRevenue * fees.teamFeePercent) / 100; + + // Actual dev payout = fullDevFee - redirects + // Expected: fullDevFee - at least baseDevRedirect + const uint64 maxExpectedDevPayout = fullDevFee - baseDevRedirect; + + const uint64 actualDevPayout = getBalance(QTF_DEV_ADDRESS) - devBalBefore; + + // Dev should receive less than full fee (due to redirects to jackpot) + EXPECT_LT(actualDevPayout, fullDevFee) << "In FR mode, dev payout should be reduced by redirects"; + + // Dev should receive at most fullDevFee - baseDevRedirect + EXPECT_LE(actualDevPayout, maxExpectedDevPayout) << "Dev payout should be reduced by at least base redirect (1%)"; + + // Jackpot should have grown (receives redirects) + EXPECT_GT(ctl.state()->getJackpot(), jackpotBefore) << "Jackpot should grow from dev/dist redirects in FR mode"; + + // No k=4 can happen (we buy losing tickets), so counter increments. + EXPECT_EQ(ctl.state()->getFrRoundsSinceK4(), roundsSinceK4Before + 1); + + // Players cleared + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, Settlement_JackpotGrowsFromOverflow) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + ctl.forceFRDisabledForBaseline(); + + // Fix RNG so we can deterministically create "no winners" tickets. + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0xBADC0FFEE0DDF00DULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const uint64 jackpotBefore = ctl.state()->getJackpot(); + constexpr uint64 numPlayers = 20; + + // Add players + for (uint64 i = 0; i < numPlayers; ++i) + { + const id user = id::randomValue(); + ctl.fundAndBuyTicket(user, ticketPrice, nums.losing); + } + + // Calculate expected jackpot growth in baseline mode (FR not active) + const uint64 revenue = ticketPrice * numPlayers; + const QTF::GetFees_output fees = ctl.getFees(); + + // winnersBlock = revenue * winnerFeePercent / 100 (68%) + const uint64 winnersBlock = (revenue * fees.winnerFeePercent) / 100; + + // With no winners, the entire winners block becomes overflow (k2+k3 pools also roll into overflow). + const uint64 winnersOverflow = winnersBlock; + + // In baseline mode: 50% of overflow goes to jackpot, 50% to reserve. + const uint64 reserveAdd = (winnersOverflow * QTF_BASELINE_OVERFLOW_ALPHA_BP) / 10000; + const uint64 overflowToJackpot = winnersOverflow - reserveAdd; + + // Minimum expected jackpot growth (assuming no k2/k3 winners, all overflow goes to jackpot) + const uint64 minExpectedGrowth = overflowToJackpot; + + ctl.drawWithDigest(testDigest); + + // Verify jackpot growth + const uint64 jackpotAfter = ctl.state()->getJackpot(); + const uint64 actualGrowth = jackpotAfter - jackpotBefore; + + // Deterministic: losing tickets guarantee no winners, so growth should match exactly. + EXPECT_EQ(actualGrowth, minExpectedGrowth) << "Actual growth: " << actualGrowth << ", Expected: " << minExpectedGrowth + << ", Overflow to jackpot (50%): " << overflowToJackpot; + + // Verify the 50% overflow split is working correctly + const uint64 expected50Percent = winnersOverflow / 2; + EXPECT_GE(overflowToJackpot, expected50Percent - 1) << "50% overflow split verification"; + EXPECT_LE(overflowToJackpot, winnersOverflow) << "Overflow to jackpot should not exceed total overflow"; +} + +TEST(ContractQThirtyFour, Settlement_RoundsSinceK4_Increments) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x1111222233334444ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Run several rounds without k=4 win + for (int round = 0; round < 3; ++round) + { + ctl.beginEpochWithValidTime(); + + ctl.buyRandomTickets(5, ticketPrice, nums.losing); + + const uint64 roundsBefore = ctl.state()->getFrRoundsSinceK4(); + ctl.drawWithDigest(testDigest); + + // Deterministic: no ticket matches any winning number, so k=4 cannot occur. + EXPECT_EQ(ctl.state()->getFrRoundsSinceK4(), roundsBefore + 1); + } +} + +// ============================================================================ +// FAST-RECOVERY (FR) TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, FR_Activation_WhenBelowTarget) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + // Set jackpot below target to trigger FR + ctl.state()->setJackpot(100000000ULL); // 100M + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); // 1B target + ctl.state()->setFrRoundsSinceK4(5); // Within post-k4 window + + EXPECT_EQ(ctl.state()->getFrActive(), false); + + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Add players and settle + for (int i = 0; i < 10; ++i) + { + const id user = id::randomValue(); + QTFRandomValues nums = ctl.makeValidNumbers(static_cast((i % 25) + 1), static_cast((i % 25) + 2), + static_cast((i % 25) + 3), static_cast((i % 25) + 4)); + ctl.fundAndBuyTicket(user, ticketPrice, nums); + } + + ctl.triggerDrawTick(); + + // FR should be active since jackpot < target and within window + EXPECT_EQ(ctl.state()->getFrActive(), true); +} + +TEST(ContractQThirtyFour, FR_Deactivation_AfterHysteresis) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x3030303030303030ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + // Set jackpot at target + ctl.state()->setJackpot(QTF_DEFAULT_TARGET_JACKPOT); + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); + ctl.state()->setFrActive(true); + ctl.state()->setFrRoundsAtOrAboveTarget(0); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Run rounds at or above target (hysteresis requirement) + for (int round = 0; round < QTF_FR_HYSTERESIS_ROUNDS; ++round) + { + ctl.beginEpochWithValidTime(); + + ctl.buyRandomTickets(5, ticketPrice, nums.losing); + + // Keep jackpot at target (add back what might be paid out) + ctl.state()->setJackpot(QTF_DEFAULT_TARGET_JACKPOT); + ctl.drawWithDigest(testDigest); + } + + // After 3 rounds at target, FR should deactivate + EXPECT_GE(ctl.state()->getFrRoundsAtOrAboveTarget(), QTF_FR_HYSTERESIS_ROUNDS); + EXPECT_EQ(ctl.state()->getFrActive(), false); +} + +TEST(ContractQThirtyFour, FR_OverflowBias_95PercentToJackpot) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + // Fix RNG so we can deterministically create "no winners" tickets. + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0xCAFEBABEDEADBEEFULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + // Activate FR + ctl.state()->setJackpot(100000000ULL); // Below target + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); + ctl.state()->setFrActive(true); + ctl.state()->setFrRoundsSinceK4(5); + + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const uint64 jackpotBefore = ctl.state()->getJackpot(); + constexpr uint64 numPlayers = 50; + + // Add many players to generate significant overflow + ctl.buyRandomTickets(numPlayers, ticketPrice, nums.losing); + + // Calculate expected jackpot growth + const uint64 revenue = ticketPrice * numPlayers; + const QTF::GetFees_output fees = ctl.getFees(); + + // winnersBlock = revenue * winnerFeePercent / 100 (68%) + const uint64 winnersBlock = (revenue * fees.winnerFeePercent) / 100; + + // In FR mode: 5% rake from winnersBlock goes to jackpot + const uint64 winnersRake = (winnersBlock * QTF_FR_WINNERS_RAKE_BP) / 10000; + const uint64 winnersBlockAfterRake = winnersBlock - winnersRake; + + // With no winners, the entire winners block after rake becomes overflow (k2+k3 pools also roll into overflow). + const uint64 winnersOverflow = winnersBlockAfterRake; + + // In FR mode: 95% of overflow goes to jackpot, 5% to reserve + const uint64 reserveAdd = (winnersOverflow * QTF_FR_ALPHA_BP) / 10000; + const uint64 overflowToJackpot = winnersOverflow - reserveAdd; + + // Dev and Dist redirects in FR mode: base (1% each) + extra (deficit-driven) + // First calculate base gain to pass to extra redirect calculation + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + const auto baseGainOut = ctl.state()->callCalculateBaseGain(qpi, revenue, winnersBlock); + + // Calculate extra redirect based on deficit + const uint64 delta = ctl.state()->getTargetJackpotInternal() - jackpotBefore; + const auto extraOut = ctl.state()->callCalculateExtraRedirectBP(qpi, numPlayers, delta, revenue, baseGainOut.baseGain); + + // Total redirect BP = base + extra (split 50/50 between dev and dist) + const uint64 devExtraBP = extraOut.extraBP / 2; + const uint64 distExtraBP = extraOut.extraBP - devExtraBP; + const uint64 totalDevRedirectBP = QTF_FR_DEV_REDIRECT_BP + devExtraBP; + const uint64 totalDistRedirectBP = QTF_FR_DIST_REDIRECT_BP + distExtraBP; + + const uint64 devRedirect = (revenue * totalDevRedirectBP) / 10000; + const uint64 distRedirect = (revenue * totalDistRedirectBP) / 10000; + + // Expected jackpot growth (with both base and extra redirects, assuming no k2/k3 winners) + // totalJackpotContribution = overflowToJackpot + winnersRake + devRedirect + distRedirect + const uint64 expectedGrowth = overflowToJackpot + winnersRake + devRedirect + distRedirect; + + ctl.drawWithDigest(testDigest); + + // Verify that jackpot grew by the expected amount + const uint64 actualGrowth = ctl.state()->getJackpot() - jackpotBefore; + + // Deterministic: losing tickets guarantee no winners, so growth should match exactly. + EXPECT_EQ(actualGrowth, expectedGrowth) << "Actual growth: " << actualGrowth << ", Expected: " << expectedGrowth + << ", Overflow to jackpot (95%): " << overflowToJackpot << ", Winners rake: " << winnersRake + << ", Extra redirect BP: " << extraOut.extraBP; + + // Verify the 95% overflow bias is working correctly + // overflowToJackpot should be ~95% of winnersOverflow + const uint64 expected95Percent = (winnersOverflow * 95) / 100; + EXPECT_GE(overflowToJackpot, expected95Percent - 1) << "95% overflow bias verification"; + EXPECT_LE(overflowToJackpot, winnersOverflow) << "Overflow to jackpot should not exceed total overflow"; +} + +// ============================================================================ +// WINNER COUNTING AND TIER TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, WinnerData_RecordsWinners) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // At least one ticket is required, otherwise END_EPOCH returns early and winner values are not generated. + const id user = id::randomValue(); + ctl.fundAndBuyTicket(user, ticketPrice, ctl.makeValidNumbers(1, 2, 3, 4)); + + ctl.triggerDrawTick(); + + const QTF::GetWinnerData_output winnerData = ctl.getWinnerData(); + expectWinnerValuesValidAndUnique(winnerData); +} + +TEST(ContractQThirtyFour, WinnerData_ResetEachRound) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + // Round 1: force a deterministic k=2 winner so winnerCounter becomes > 0. + m256i digest1 = {}; + digest1.m256i_u64[0] = 0x13579BDF2468ACE0ULL; + const auto nums1 = ctl.computeWinningAndLosing(digest1); + + ctl.beginEpochWithValidTime(); + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + QTFRandomValues k2Numbers = ctl.makeK2Numbers(nums1.winning); + const id k2Winner = id::randomValue(); + ctl.fundAndBuyTicket(k2Winner, ticketPrice, k2Numbers); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 1u); + + ctl.drawWithDigest(digest1); + + const QTF::GetWinnerData_output afterRound1 = ctl.getWinnerData(); + EXPECT_GT(afterRound1.winnerData.winnerCounter, 0u); + + // Round 2: force a deterministic "no winners" round, winnerCounter must reset to 0. + m256i digest2 = {}; + digest2.m256i_u64[0] = 0x0F0E0D0C0B0A0908ULL; + const auto nums2 = ctl.computeWinningAndLosing(digest2); + + ctl.beginEpochWithValidTime(); + ctl.buyRandomTickets(5, ticketPrice, nums2.losing); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 5u); + + ctl.drawWithDigest(digest2); + + const QTF::GetWinnerData_output afterRound2 = ctl.getWinnerData(); + EXPECT_EQ(afterRound2.winnerData.winnerCounter, 0u) << "Winner snapshot must reset each round"; +} + +// ============================================================================ +// EDGE CASE TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, BuyTicket_ValidNumberSelections_EdgeCases_Success) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + static constexpr uint8 cases[][4] = { + {1, 2, 29, 30}, // boundary + {15, 16, 17, 18}, // consecutive + {27, 28, 29, 30}, // highest + {1, 2, 3, 4}, // lowest + }; + + for (uint64 i = 0; i < (sizeof(cases) / sizeof(cases[0])); ++i) + { + const id user = id::randomValue(); + const QTFRandomValues nums = ctl.makeValidNumbers(cases[i][0], cases[i][1], cases[i][2], cases[i][3]); + ctl.fundAndBuyTicket(user, ticketPrice, nums); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), i + 1); + } +} + +// ============================================================================ +// MULTIPLE ROUNDS TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, MultipleRounds_JackpotAccumulates) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x0DDC0FFEE0DDF00DULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + uint64 prevJackpot = 0; + + // Run multiple rounds + for (int round = 0; round < 5; ++round) + { + ctl.beginEpochWithValidTime(); + + ctl.buyRandomTickets(10, ticketPrice, nums.losing); + ctl.drawWithDigest(testDigest); + + // Jackpot should increase each round (no k=4 winners in this test) + const uint64 currentJackpot = ctl.state()->getJackpot(); + EXPECT_GT(currentJackpot, prevJackpot) << "Round " << round << ": jackpot should grow"; + + // Track for next iteration + prevJackpot = currentJackpot; + } +} + +TEST(ContractQThirtyFour, MultipleRounds_StateResetsCorrectly) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + for (int round = 0; round < 3; ++round) + { + ctl.beginEpochWithValidTime(); + + // Add different number of players each round + const int playersThisRound = 5 + round * 3; + for (int i = 0; i < playersThisRound; ++i) + { + const id user = id::randomValue(); + QTFRandomValues nums = ctl.makeValidNumbers(static_cast((i + round) % 27 + 1), static_cast((i + round + 5) % 27 + 1), + static_cast((i + round + 10) % 27 + 1), static_cast((i + round + 15) % 27 + 1)); + ctl.fundAndBuyTicket(user, ticketPrice, nums); + } + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), static_cast(playersThisRound)); + + ctl.triggerDrawTick(); + + // Players should be cleared after each round + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); + } +} + +// ============================================================================ +// POST_INCOMING_TRANSFER TEST +// ============================================================================ + +TEST(ContractQThirtyFour, PostIncomingTransfer_StandardTransaction_Refunded) +{ + ContractTestingQTF ctl; + constexpr uint64 transferAmount = 123456789; + + const id sender = id::randomValue(); + increaseEnergy(sender, transferAmount); + EXPECT_EQ(getBalance(sender), transferAmount); + + const id contractAddress = ctl.qtfSelf(); + EXPECT_EQ(getBalance(contractAddress), 0); + + // Standard transaction should be refunded + notifyContractOfIncomingTransfer(sender, contractAddress, transferAmount, QPI::TransferType::standardTransaction); + + // Amount should be refunded to sender + EXPECT_EQ(getBalance(sender), transferAmount); + EXPECT_EQ(getBalance(contractAddress), 0); +} + +// ============================================================================ +// SCHEDULE AND TIME TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, Schedule_WednesdayAlwaysDraws_IgnoresScheduleMask) +{ + ContractTestingQTF ctl; + + // Exclude Wednesday from schedule mask (e.g., Monday only). + constexpr uint8 mondayOnly = 1 << MONDAY; + ctl.forceSchedule(mondayOnly); + + ctl.beginEpochWithValidTime(); + + const m256i testDigest = {}; + ctl.setPrevSpectrumDigest(testDigest); + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + for (int i = 0; i < 5; ++i) + { + const id user = id::randomValue(); + ctl.fundAndBuyTicket(user, ticketPrice, nums.losing); + } + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 5u); + + // Wednesday should always trigger a draw at/after draw hour, even if schedule mask does not include it. + const uint8 drawHour = ctl.state()->getDrawHourInternal(); + ctl.setDateTime(2025, 1, 15, drawHour); + ctl.forceBeginTick(); + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, Schedule_DrawOnlyOnScheduledDays) +{ + ContractTestingQTF ctl; + + // Set schedule to Wednesday only (default) + constexpr uint8 wednesdayOnly = 1 << WEDNESDAY; + ctl.forceSchedule(wednesdayOnly); + + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Add players + for (int i = 0; i < 5; ++i) + { + const id user = id::randomValue(); + QTFRandomValues nums = + ctl.makeValidNumbers(static_cast(i + 1), static_cast(i + 5), static_cast(i + 10), static_cast(i + 15)); + ctl.fundAndBuyTicket(user, ticketPrice, nums); + } + + const uint64 playersBefore = ctl.state()->getNumberOfPlayers(); + EXPECT_EQ(playersBefore, 5u); + + // Tuesday 2025-01-14 is not scheduled - should NOT trigger draw + ctl.setDateTime(2025, 1, 14, 12); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), playersBefore); // Unchanged + + // Wednesday 2025-01-15 IS scheduled - should trigger draw + ctl.setDateTime(2025, 1, 15, 12); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); // Cleared after draw +} + +TEST(ContractQThirtyFour, Schedule_DrawAtMostOncePerDay_LastDrawDateStampGuards) +{ + ContractTestingQTF ctl; + + // Use a non-Wednesday scheduled day so selling is re-enabled after the draw. + constexpr uint8 thursdayOnly = 1 << THURSDAY; + ctl.forceSchedule(thursdayOnly); + + ctl.beginEpochWithValidTime(); + + const m256i testDigest = {}; + ctl.setPrevSpectrumDigest(testDigest); + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + { + const id user = id::randomValue(); + ctl.fundAndBuyTicket(user, ticketPrice, nums.losing); + } + + const uint8 drawHour = ctl.state()->getDrawHourInternal(); + + // First draw on Thursday. + ctl.setDateTime(2025, 1, 16, drawHour); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); + + const uint64 jackpotAfterFirst = ctl.state()->getJackpot(); + const QTF::GetWinnerData_output winnersAfterFirst = ctl.getWinnerData(); + + // Buy another ticket on the same date (selling should be open on non-Wednesday). + { + const id user2 = id::randomValue(); + ctl.fundAndBuyTicket(user2, ticketPrice, nums.losing); + } + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 1u); + + // Second tick on the same date must NOT trigger another draw. + ctl.setDateTime(2025, 1, 16, drawHour); + ctl.forceBeginTick(); + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 1u); + EXPECT_EQ(ctl.state()->getJackpot(), jackpotAfterFirst); + const QTF::GetWinnerData_output winnersAfterSecondAttempt = ctl.getWinnerData(); + for (uint64 i = 0; i < QTF_RANDOM_VALUES_COUNT; ++i) + { + EXPECT_EQ(winnersAfterSecondAttempt.winnerData.winnerValues.get(i), winnersAfterFirst.winnerData.winnerValues.get(i)); + } + EXPECT_EQ((uint64)winnersAfterSecondAttempt.winnerData.epoch, (uint64)winnersAfterFirst.winnerData.epoch); +} + +TEST(ContractQThirtyFour, DrawHour_NoDrawBeforeScheduledHour) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Add players + for (int i = 0; i < 5; ++i) + { + const id user = id::randomValue(); + QTFRandomValues nums = + ctl.makeValidNumbers(static_cast(i + 1), static_cast(i + 5), static_cast(i + 10), static_cast(i + 15)); + ctl.fundAndBuyTicket(user, ticketPrice, nums); + } + + const uint8 drawHour = ctl.state()->getDrawHourInternal(); + const uint64 playersBefore = ctl.state()->getNumberOfPlayers(); + + // Before draw hour - should NOT trigger draw + ctl.setDateTime(2025, 1, 15, drawHour - 1); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), playersBefore); + + // At or after draw hour - should trigger draw + ctl.setDateTime(2025, 1, 15, drawHour); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); +} + +TEST(ContractQThirtyFour, DrawHour_WednesdayDrawClosesTicketSelling) +{ + ContractTestingQTF ctl; + + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + ctl.beginEpochWithValidTime(); + + const m256i testDigest = {}; + ctl.setPrevSpectrumDigest(testDigest); + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + { + const id user = id::randomValue(); + ctl.fundAndBuyTicket(user, ticketPrice, nums.losing); + } + + const uint8 drawHour = ctl.state()->getDrawHourInternal(); + ctl.setDateTime(2025, 1, 15, drawHour); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 0u); + + // After a Wednesday draw, selling must remain closed until next epoch. + const id lateBuyer = id::randomValue(); + increaseEnergy(lateBuyer, ticketPrice * 2); + const uint64 before = getBalance(lateBuyer); + const QTF::BuyTicket_output out = ctl.buyTicket(lateBuyer, ticketPrice, nums.losing); + EXPECT_EQ(out.returnCode, static_cast(QTF::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(getBalance(lateBuyer), before); +} + +// ============================================================================ +// PROBABILITY AND COMBINATORICS VERIFICATION +// ============================================================================ + +TEST(ContractQThirtyFour, Combinatorics_P4Denominator) +{ + // Verify the P4 denominator constant matches combinatorics + // C(30,4) = 30! / (4! * 26!) = 27405 + constexpr uint64 numerator = QTF_MAX_RANDOM_VALUE * 29 * 28 * 27; + constexpr uint64 denominator = QTF_RANDOM_VALUES_COUNT * 3 * 2 * 1; + constexpr uint64 expected = numerator / denominator; + + EXPECT_EQ(expected, QTF_P4_DENOMINATOR); + EXPECT_EQ(QTF_P4_DENOMINATOR, 27405u); +} + +// ============================================================================ +// FEE CALCULATION VERIFICATION +// ============================================================================ + +TEST(ContractQThirtyFour, FeeCalculation_TotalEquals100Percent) +{ + ContractTestingQTF ctl; + const QTF::GetFees_output fees = ctl.getFees(); + + const uint32 total = fees.teamFeePercent + fees.distributionFeePercent + fees.winnerFeePercent + fees.burnPercent; + + EXPECT_EQ(total, 100u); +} + +// ============================================================================ +// PRIZE PAYOUT ESTIMATION +// ============================================================================ + +TEST(ContractQThirtyFour, EstimatePrizePayouts_NoTickets) +{ + ContractTestingQTF ctl; + + // No tickets sold, should return zero payouts + QTF::EstimatePrizePayouts_output estimate = ctl.estimatePrizePayouts(1, 1); + + EXPECT_EQ(estimate.k2PayoutPerWinner, 0ull); + EXPECT_EQ(estimate.k3PayoutPerWinner, 0ull); + EXPECT_EQ(estimate.k2Pool, 0ull); + EXPECT_EQ(estimate.k3Pool, 0ull); + EXPECT_EQ(estimate.totalRevenue, 0ull); +} + +TEST(ContractQThirtyFour, EstimatePrizePayouts_WithTicketsSingleWinner) +{ + ContractTestingQTF ctl; + + ctl.startAnyDayEpoch(); + + // Buy 100 tickets + constexpr uint64 ticketPrice = 1000000ull; // 1M QU + constexpr uint64 numTickets = 100; + + const QTFRandomValues numbers = ctl.makeValidNumbers(1, 2, 3, 4); + ctl.buyRandomTickets(numTickets, ticketPrice, numbers); + + // Verify tickets were purchased + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), numTickets); + + // Estimate for 1 k2 winner and 1 k3 winner + QTF::EstimatePrizePayouts_output estimate = ctl.estimatePrizePayouts(1, 1); + + const uint64 expectedRevenue = ticketPrice * numTickets; + EXPECT_EQ(estimate.totalRevenue, expectedRevenue); + + // Check minimum floors and cap using constants from contract + constexpr uint64 expectedK2Floor = ticketPrice * QTF_K2_FLOOR_MULT / QTF_K2_FLOOR_DIV; + constexpr uint64 expectedK3Floor = ticketPrice * QTF_K3_FLOOR_MULT; + constexpr uint64 expectedCap = ticketPrice * QTF_TOPUP_PER_WINNER_CAP_MULT; + EXPECT_EQ(estimate.k2MinFloor, expectedK2Floor); + EXPECT_EQ(estimate.k3MinFloor, expectedK3Floor); + EXPECT_EQ(estimate.perWinnerCap, expectedCap); + + // Winners block using contract constants + const QTF::GetFees_output fees = ctl.getFees(); + uint64 winnersBlock = 0, k2PoolExpected = 0, k3PoolExpected = 0; + computeBaselinePrizePools(expectedRevenue, fees, winnersBlock, k2PoolExpected, k3PoolExpected); + + EXPECT_EQ(estimate.k2Pool, k2PoolExpected); + EXPECT_EQ(estimate.k3Pool, k3PoolExpected); + + // With 1 winner each: k2 payout equals pool (below cap), k3 payout is capped at 25*P + EXPECT_EQ(estimate.k2PayoutPerWinner, k2PoolExpected); // 19.04M < 25M cap + EXPECT_EQ(estimate.k3PayoutPerWinner, expectedCap); // 27.2M capped to 25M +} + +TEST(ContractQThirtyFour, EstimatePrizePayouts_WithMultipleWinners) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + // Buy 1000 tickets + const uint64 ticketPrice = 1000000ull; + const uint64 numTickets = 1000; + + const QTFRandomValues numbers = ctl.makeValidNumbers(5, 10, 15, 20); + ctl.buyRandomTickets(numTickets, ticketPrice, numbers); + + // Verify tickets were purchased + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), numTickets); + + // Estimate for 10 k2 winners and 5 k3 winners + QTF::EstimatePrizePayouts_output estimate = ctl.estimatePrizePayouts(10, 5); + + const uint64 expectedRevenue = ticketPrice * numTickets; + const QTF::GetFees_output fees = ctl.getFees(); + uint64 winnersBlock = 0, k2Pool = 0, k3Pool = 0; + computeBaselinePrizePools(expectedRevenue, fees, winnersBlock, k2Pool, k3Pool); + + // Verify pools + EXPECT_EQ(estimate.k2Pool, k2Pool); + EXPECT_EQ(estimate.k3Pool, k3Pool); + + // Verify per-winner payouts (should be pool / winner count, capped) + const uint64 k2ExpectedPerWinner = k2Pool / 10; + const uint64 k3ExpectedPerWinner = k3Pool / 5; + + EXPECT_EQ(estimate.k2PayoutPerWinner, std::min(k2ExpectedPerWinner, estimate.perWinnerCap)); + EXPECT_EQ(estimate.k3PayoutPerWinner, std::min(k3ExpectedPerWinner, estimate.perWinnerCap)); + + // Both should be above minimum floors + EXPECT_GE(estimate.k2PayoutPerWinner, estimate.k2MinFloor); + EXPECT_GE(estimate.k3PayoutPerWinner, estimate.k3MinFloor); +} + +TEST(ContractQThirtyFour, EstimatePrizePayouts_NoWinnersShowsPotential) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + // Buy 50 tickets + const uint64 ticketPrice = 1000000ull; + const uint64 numTickets = 50; + + const QTFRandomValues numbers = ctl.makeValidNumbers(7, 14, 21, 28); + ctl.buyRandomTickets(numTickets, ticketPrice, numbers); + + // Verify tickets were purchased + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), numTickets); + + // Estimate with 0 winners (shows what a single winner would get) + QTF::EstimatePrizePayouts_output estimate = ctl.estimatePrizePayouts(0, 0); + + const uint64 expectedRevenue = ticketPrice * numTickets; + const QTF::GetFees_output fees = ctl.getFees(); + uint64 winnersBlock = 0, k2Pool = 0, k3Pool = 0; + computeBaselinePrizePools(expectedRevenue, fees, winnersBlock, k2Pool, k3Pool); + + // When no winners specified, should show full pool (capped) + EXPECT_EQ(estimate.k2PayoutPerWinner, std::min(k2Pool, estimate.perWinnerCap)); + EXPECT_EQ(estimate.k3PayoutPerWinner, std::min(k3Pool, estimate.perWinnerCap)); +} + +// ============================================================================ +// DETERMINISTIC WINNER TESTING +// ============================================================================ +// Solution: By fixing prevSpectrumDigest, we can deterministically control winning numbers +// +// Background: +// Settlement generates winning numbers using: seed = K12(prevSpectrumDigest).u64._0 +// This seed is then used in GetRandomValues (QThirtyFour.h:1663-1698) to derive 4 numbers. +// +// Approach: +// 1. Create a fixed test prevSpectrumDigest (e.g., testDigest) +// 2. Compute expected winning numbers for that digest +// 3. Buy tickets with exact winning numbers (for k=4), partial matches (for k=2/k=3), etc. +// 4. Trigger settlement with drawWithDigest(testDigest) +// 5. Settlement will use our fixed digest, generating the pre-computed winning numbers +// 6. Verify actual payouts, jackpot depletion, FR resets, etc. +// +// This enables deterministic testing of: +// - Actual k=4 jackpot win payouts and jackpot depletion +// - Actual k=2/k=3 winner payouts with real matching logic +// - Actual FR reset behavior after k=4 win (frRoundsSinceK4 = 0) +// - Pool splitting among multiple winners +// - Revenue distribution and fee calculations with real winners + +TEST(ContractQThirtyFour, DeterministicWinner_K4JackpotWin_DepletesAndReseeds) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + // Ensure QRP has enough reserve to reseed to target. + increaseEnergy(ctl.qrpSelf(), QTF_DEFAULT_TARGET_JACKPOT + 1000000ULL); + const uint64 qrpBalanceBefore = static_cast(getBalance(ctl.qrpSelf())); + + // Create a deterministic prevSpectrumDigest + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x123456789ABCDEF0ULL; // Arbitrary seed + + const auto nums = ctl.computeWinningAndLosing(testDigest); + + // Setup: FR active with jackpot below target + const uint64 initialJackpot = 800000000ULL; // 800M QU + ctl.state()->setJackpot(initialJackpot); + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); // 1B target + ctl.forceFREnabledWithinWindow(10); + // IMPORTANT: internal `state.jackpot` must be backed by actual contract balance, otherwise transfers will fail. + increaseEnergy(ctl.qtfSelf(), initialJackpot); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // User1: Buy ticket with EXACT winning numbers (k=4 winner) + const id k4Winner = id::randomValue(); + ctl.fundAndBuyTicket(k4Winner, ticketPrice, nums.winning); + + // User2: Buy ticket with 3 matching numbers (k=3 winner) + QTFRandomValues k3Numbers = ctl.makeK3Numbers(nums.winning); + const id k3Winner = id::randomValue(); + ctl.fundAndBuyTicket(k3Winner, ticketPrice, k3Numbers); + + // User3: Buy ticket with 2 matching numbers (k=2 winner) + QTFRandomValues k2Numbers = ctl.makeK2Numbers(nums.winning); + const id k2Winner = id::randomValue(); + ctl.fundAndBuyTicket(k2Winner, ticketPrice, k2Numbers); + + // User4: No match + const id loser = id::randomValue(); + QTFRandomValues loserNumbers = ctl.makeValidNumbers(1, 2, 3, 4); + ctl.fundAndBuyTicket(loser, ticketPrice, loserNumbers); + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 4ULL); + + // Verify state before settlement + const uint64 jackpotBefore = ctl.state()->getJackpot(); + const uint64 roundsSinceK4Before = ctl.state()->getFrRoundsSinceK4(); + EXPECT_EQ(jackpotBefore, initialJackpot); + EXPECT_EQ(roundsSinceK4Before, 10u); + + // Trigger settlement using our fixed prevSpectrumDigest + const uint64 k4WinnerBefore = getBalance(k4Winner); + ctl.drawWithDigest(testDigest); + const uint64 k4WinnerAfter = getBalance(k4Winner); + + // Verify k=4 jackpot win behavior: + const uint64 jackpotAfter = ctl.state()->getJackpot(); + EXPECT_GE(jackpotAfter, QTF_DEFAULT_TARGET_JACKPOT) << "Jackpot should be reseeded from QRP after k=4 win"; + EXPECT_LT(static_cast(getBalance(ctl.qrpSelf())), qrpBalanceBefore) << "QRP reserve should decrease due to reseed"; + + // FR counters reset + const uint64 roundsSinceK4After = ctl.state()->getFrRoundsSinceK4(); + EXPECT_EQ(roundsSinceK4After, 0u) << "frRoundsSinceK4 should reset to 0 after k=4 win"; + + const uint64 roundsAtTargetAfter = ctl.state()->getFrRoundsAtOrAboveTarget(); + EXPECT_EQ(roundsAtTargetAfter, 0u) << "frRoundsAtOrAboveTarget should reset to 0 after k=4 win"; + + // 3. Verify winner data contains our winning numbers + QTF::GetWinnerData_output winnerData = ctl.getWinnerData(); + EXPECT_EQ(winnerData.winnerData.winnerValues.get(0), nums.winning.get(0)); + EXPECT_EQ(winnerData.winnerData.winnerValues.get(1), nums.winning.get(1)); + EXPECT_EQ(winnerData.winnerData.winnerValues.get(2), nums.winning.get(2)); + EXPECT_EQ(winnerData.winnerData.winnerValues.get(3), nums.winning.get(3)); + + // Verify k=4 winner received exact payout (jackpotBefore / countK4). + EXPECT_EQ(static_cast(k4WinnerAfter - k4WinnerBefore), initialJackpot); +} + +TEST(ContractQThirtyFour, DeterministicWinner_K4JackpotWin_MultipleWinners_SplitsEvenly) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + // Ensure QRP has enough reserve to reseed (so settlement completes without relying on carry math). + increaseEnergy(ctl.qrpSelf(), QTF_DEFAULT_TARGET_JACKPOT + 1000000ULL); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0xA5A5A5A5A5A5A5A5ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 initialJackpot = 900000000ULL; + ctl.state()->setJackpot(initialJackpot); + ctl.forceFREnabledWithinWindow(1); + increaseEnergy(ctl.qtfSelf(), initialJackpot); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + const id w1 = id::randomValue(); + const id w2 = id::randomValue(); + ctl.fundAndBuyTicket(w1, ticketPrice, nums.winning); + ctl.fundAndBuyTicket(w2, ticketPrice, nums.winning); + + const uint64 w1Before = getBalance(w1); + const uint64 w2Before = getBalance(w2); + + ctl.drawWithDigest(testDigest); + + const uint64 expectedPerWinner = initialJackpot / 2; + EXPECT_EQ(static_cast(getBalance(w1) - w1Before), expectedPerWinner); + EXPECT_EQ(static_cast(getBalance(w2) - w2Before), expectedPerWinner); +} + +TEST(ContractQThirtyFour, DeterministicWinner_K4JackpotWin_ReseedLimitedByQRP) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + ctl.forceFRDisabledForBaseline(); + + // Fund QRP below target so reseed amount is limited by available reserve. + const uint64 qrpFunded = 200000000ULL; + increaseEnergy(ctl.qrpSelf(), qrpFunded); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x0A0B0C0D0E0F1011ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 initialJackpot = 800000000ULL; + ctl.state()->setJackpot(initialJackpot); + increaseEnergy(ctl.qtfSelf(), initialJackpot); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const id w1 = id::randomValue(); + ctl.fundAndBuyTicket(w1, ticketPrice, nums.winning); + + const uint64 qrpBefore = static_cast(getBalance(ctl.qrpSelf())); + const uint64 w1Before = getBalance(w1); + + ctl.drawWithDigest(testDigest); + + EXPECT_EQ(static_cast(getBalance(w1) - w1Before), initialJackpot); + + // With a single winning ticket and baseline overflow split, winnersOverflow == winnersBlock, reserveAdd == winnersBlock/2, carryAdd == + // winnersBlock/2. + const QTF::GetFees_output fees = ctl.getFees(); + const uint64 revenue = ticketPrice; + const uint64 winnersBlock = (revenue * fees.winnerFeePercent) / 100; + const uint64 reserveAdd = (winnersBlock * QTF_BASELINE_OVERFLOW_ALPHA_BP) / 10000; + const uint64 carryAdd = winnersBlock - reserveAdd; + + EXPECT_EQ(ctl.state()->getJackpot(), qrpFunded + carryAdd); + EXPECT_EQ(static_cast(getBalance(ctl.qrpSelf())), qrpBefore - qrpFunded + reserveAdd); +} + +// Test k=2 and k=3 payouts with deterministic winning numbers +TEST(ContractQThirtyFour, DeterministicWinner_K2K3Payouts_VerifyRevenueSplit) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + // This test validates baseline k2/k3 pool splitting (no FR rake). + // Force FR activation window to be expired so SettleEpoch cannot auto-enable FR. + ctl.forceFRDisabledForBaseline(); + + // Create deterministic prevSpectrumDigest + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0xFEDCBA9876543210ULL; // Different seed + + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Create multiple k=2 and k=3 winners to test pool splitting + // 2 k=3 winners + QTFRandomValues k3Numbers1 = ctl.makeK3Numbers(nums.winning, 0); + const id k3Winner1 = id::randomValue(); + ctl.fundAndBuyTicket(k3Winner1, ticketPrice, k3Numbers1); + + QTFRandomValues k3Numbers2 = ctl.makeK3Numbers(nums.winning, 1); + const id k3Winner2 = id::randomValue(); + ctl.fundAndBuyTicket(k3Winner2, ticketPrice, k3Numbers2); + + // 3 k=2 winners + QTFRandomValues k2Numbers1 = ctl.makeK2Numbers(nums.winning, 0); + const id k2Winner1 = id::randomValue(); + ctl.fundAndBuyTicket(k2Winner1, ticketPrice, k2Numbers1); + + QTFRandomValues k2Numbers2 = ctl.makeK2Numbers(nums.winning, 1); + const id k2Winner2 = id::randomValue(); + ctl.fundAndBuyTicket(k2Winner2, ticketPrice, k2Numbers2); + + QTFRandomValues k2Numbers3 = ctl.makeK2Numbers(nums.winning, 2); + const id k2Winner3 = id::randomValue(); + ctl.fundAndBuyTicket(k2Winner3, ticketPrice, k2Numbers3); + + // 5 losers (no matches) + for (int i = 0; i < 5; ++i) + { + const id loser = id::randomValue(); + QTFRandomValues loserNumbers = ctl.makeValidNumbers(1, 2, 3, 4); + ctl.fundAndBuyTicket(loser, ticketPrice, loserNumbers); + } + + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 10ULL); + + // Calculate expected pools + const uint64 revenue = ticketPrice * 10; + const QTF::GetFees_output fees = ctl.getFees(); + const uint64 winnersBlock = (revenue * fees.winnerFeePercent) / 100; // 68% + const uint64 expectedK2Pool = (winnersBlock * QTF_BASE_K2_SHARE_BP) / 10000; // 28% of winners block + const uint64 expectedK3Pool = (winnersBlock * QTF_BASE_K3_SHARE_BP) / 10000; // 40% of winners block + + // Get balances before settlement + const uint64 k3Winner1Before = getBalance(k3Winner1); + const uint64 k2Winner1Before = getBalance(k2Winner1); + + // Trigger settlement + ctl.drawWithDigest(testDigest); + + // Verify winner payouts + // k=3 pool split between 2 winners + const uint64 expectedK3PayoutPerWinner = expectedK3Pool / 2; + const uint64 k3Winner1After = getBalance(k3Winner1); + const uint64 k3Winner1Gained = k3Winner1After - k3Winner1Before; + EXPECT_EQ(static_cast(k3Winner1Gained), expectedK3PayoutPerWinner) << "k=3 winner should receive half of k3 pool"; + + // k=2 pool split between 3 winners + const uint64 expectedK2PayoutPerWinner = expectedK2Pool / 3; + const uint64 k2Winner1After = getBalance(k2Winner1); + const uint64 k2Winner1Gained = k2Winner1After - k2Winner1Before; + EXPECT_EQ(static_cast(k2Winner1Gained), expectedK2PayoutPerWinner) << "k=2 winner should receive one-third of k2 pool"; + + // Verify winning numbers in winner data + QTF::GetWinnerData_output winnerData = ctl.getWinnerData(); + EXPECT_EQ(winnerData.winnerData.winnerValues.get(0), nums.winning.get(0)); + EXPECT_EQ(winnerData.winnerData.winnerValues.get(1), nums.winning.get(1)); + EXPECT_EQ(winnerData.winnerData.winnerValues.get(2), nums.winning.get(2)); + EXPECT_EQ(winnerData.winnerData.winnerValues.get(3), nums.winning.get(3)); + + // Jackpot should have grown (no k=4 winner) + EXPECT_GT(ctl.state()->getJackpot(), 0ULL); +} + +TEST(ContractQThirtyFour, EstimatePrizePayouts_FRMode_AppliesRakeToPools) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Enable FR so EstimatePrizePayouts applies the 5% winners rake. + ctl.state()->setJackpot(QTF_DEFAULT_TARGET_JACKPOT / 2); + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); + ctl.forceFREnabledWithinWindow(1); + + constexpr uint64 numPlayers = 100; + for (uint64 i = 0; i < numPlayers; ++i) + { + const id user = id::randomValue(); + QTFRandomValues nums = ctl.makeValidNumbers(static_cast((i % 26) + 1), static_cast((i % 26) + 2), + static_cast((i % 26) + 3), static_cast((i % 26) + 4)); + ctl.fundAndBuyTicket(user, ticketPrice, nums); + } + + const QTF::EstimatePrizePayouts_output estimate = ctl.estimatePrizePayouts(0, 0); + + const uint64 revenue = ticketPrice * numPlayers; + const QTF::GetFees_output fees = ctl.getFees(); + const uint64 winnersBlock = (revenue * fees.winnerFeePercent) / 100; + const uint64 winnersRake = (winnersBlock * QTF_FR_WINNERS_RAKE_BP) / 10000; + const uint64 winnersBlockAfterRake = winnersBlock - winnersRake; + + const uint64 expectedK2Pool = (winnersBlockAfterRake * QTF_BASE_K2_SHARE_BP) / 10000; + const uint64 expectedK3Pool = (winnersBlockAfterRake * QTF_BASE_K3_SHARE_BP) / 10000; + + EXPECT_EQ(estimate.totalRevenue, revenue); + EXPECT_EQ(estimate.k2Pool, expectedK2Pool); + EXPECT_EQ(estimate.k3Pool, expectedK3Pool); +} + +// ============================================================================ +// RESERVE TOP-UP AND FLOOR GUARANTEE TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, Settlement_PerWinnerCap_AppliesToK3Winner_OverflowAccountsForRemainder) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + ctl.forceFRDisabledForBaseline(); + + // Ensure RL shares exist so distribution payouts leave the contract (otherwise most of distPayout can remain in QTF balance). + const id shareholder1 = id::randomValue(); + const id shareholder2 = id::randomValue(); + constexpr uint32 shares1 = NUMBER_OF_COMPUTORS / 3; + constexpr uint32 shares2 = NUMBER_OF_COMPUTORS - shares1; + std::vector> rlShares{{shareholder1, shares1}, {shareholder2, shares2}}; + issueRlSharesTo(rlShares); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0xD1CEB00BD1CEB00BULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 P = ctl.state()->getTicketPriceInternal(); + const uint64 perWinnerCap = smul(P, QTF_TOPUP_PER_WINNER_CAP_MULT); + + const id k3Winner = id::randomValue(); + ctl.fundAndBuyTicket(k3Winner, P, ctl.makeK3Numbers(nums.winning, 0)); + + constexpr uint64 numLosers = 100; + ctl.buyRandomTickets(numLosers, P, nums.losing); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), numLosers + 1); + + const uint64 qrpBefore = static_cast(getBalance(ctl.qrpSelf())); + const uint64 k3Before = getBalance(k3Winner); + + ctl.drawWithDigest(testDigest); + + EXPECT_EQ(static_cast(getBalance(k3Winner) - k3Before), perWinnerCap); + + // Baseline settlement: with no k2 winners and exactly one k3 winner capped at 25*P, + // winnersOverflow ends up being winnersBlock - perWinnerCap. + const QTF::GetFees_output fees = ctl.getFees(); + const uint64 revenue = smul(P, numLosers + 1); + const uint64 winnersBlock = div(smul(revenue, static_cast(fees.winnerFeePercent)), 100); + const uint64 winnersOverflow = winnersBlock - perWinnerCap; + const uint64 reserveAdd = (winnersOverflow * QTF_BASELINE_OVERFLOW_ALPHA_BP) / 10000; + const uint64 carryAdd = winnersOverflow - reserveAdd; + + EXPECT_EQ(ctl.state()->getJackpot(), carryAdd); + EXPECT_EQ(static_cast(getBalance(ctl.qtfSelf())), carryAdd); + EXPECT_EQ(static_cast(getBalance(ctl.qrpSelf())), qrpBefore + reserveAdd); +} + +TEST(ContractQThirtyFour, Settlement_FloorTopUp_LimitedBySafetyCaps_PayoutBelowFloor) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + ctl.forceFRDisabledForBaseline(); + + // Ensure RL shares exist so distribution payouts leave the contract (otherwise most of distPayout can remain in QTF balance). + const id shareholder1 = id::randomValue(); + const id shareholder2 = id::randomValue(); + constexpr uint32 shares1 = NUMBER_OF_COMPUTORS / 2; + constexpr uint32 shares2 = NUMBER_OF_COMPUTORS - shares1; + std::vector> rlShares{{shareholder1, shares1}, {shareholder2, shares2}}; + issueRlSharesTo(rlShares); + + // Fund QRP just above soft floor so top-up is limited by both 10% cap and soft floor. + const uint64 P = ctl.state()->getTicketPriceInternal(); + const uint64 softFloor = smul(P, QTF_RESERVE_SOFT_FLOOR_MULT); // 20*P + const uint64 qrpFunding = softFloor + 5 * P; // 25*P + increaseEnergy(ctl.qrpSelf(), qrpFunding); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x0DDC0FFEE0DDF00DULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const id k3Winner = id::randomValue(); + ctl.fundAndBuyTicket(k3Winner, P, ctl.makeK3Numbers(nums.winning, 0)); + EXPECT_EQ(ctl.state()->getNumberOfPlayers(), 1u); + + const uint64 qrpBefore = static_cast(getBalance(ctl.qrpSelf())); + const uint64 k3Before = getBalance(k3Winner); + + ctl.drawWithDigest(testDigest); + + const QTF::GetFees_output fees = ctl.getFees(); + const uint64 revenue = P; + const uint64 winnersBlock = div(smul(revenue, static_cast(fees.winnerFeePercent)), 100); + const uint64 k3Pool = (winnersBlock * QTF_BASE_K3_SHARE_BP) / 10000; + const uint64 k3Floor = smul(P, QTF_K3_FLOOR_MULT); + const uint64 needed = k3Floor - k3Pool; + const uint64 availableAboveFloor = qrpBefore - softFloor; // 5*P + const uint64 maxPerRound = (qrpBefore * QTF_TOPUP_RESERVE_PCT_BP) / 10000; // 10% of total + const uint64 perWinnerCapTotal = smul(P, QTF_TOPUP_PER_WINNER_CAP_MULT); // 25*P + const uint64 maxAllowed = std::min(std::min(maxPerRound, availableAboveFloor), perWinnerCapTotal); // 2.5*P + const uint64 expectedTopUp = std::min(needed, maxAllowed); + const uint64 expectedPayout = k3Pool + expectedTopUp; + + EXPECT_LT(expectedPayout, k3Floor); + EXPECT_EQ(static_cast(getBalance(k3Winner) - k3Before), expectedPayout); + + // With no k2 winners and k3 pool fully paid (top-ups only increase payouts), + // winnersOverflow equals winnersBlock - k3Pool. + const uint64 winnersOverflow = winnersBlock - k3Pool; + const uint64 reserveAdd = (winnersOverflow * QTF_BASELINE_OVERFLOW_ALPHA_BP) / 10000; + const uint64 carryAdd = winnersOverflow - reserveAdd; + + EXPECT_EQ(ctl.state()->getJackpot(), carryAdd); + EXPECT_EQ(static_cast(getBalance(ctl.qtfSelf())), carryAdd); + EXPECT_EQ(static_cast(getBalance(ctl.qrpSelf())), qrpBefore - expectedTopUp + reserveAdd); + EXPECT_GE(static_cast(getBalance(ctl.qrpSelf())), softFloor); +} + +TEST(ContractQThirtyFour, Settlement_FloorTopUp_Integration_K2K3FloorsMetWhenReserveSufficient) +{ + ContractTestingQTF ctl; + ctl.startAnyDayEpoch(); + ctl.forceFRDisabledForBaseline(); + + // Ensure RL shares exist so distribution path is exercised (and rounding/payback is deterministic). + const id shareholder1 = id::randomValue(); + const id shareholder2 = id::randomValue(); + constexpr uint32 shares1 = NUMBER_OF_COMPUTORS / 4; + constexpr uint32 shares2 = NUMBER_OF_COMPUTORS - shares1; + std::vector> rlShares{{shareholder1, shares1}, {shareholder2, shares2}}; + issueRlSharesTo(rlShares); + + // Fund QRP enough so both tiers can be topped up to floors under all caps. + const uint64 qrpFunding = 100000000ULL; // 100M, 10% cap = 10M, soft floor = 20M. + increaseEnergy(ctl.qrpSelf(), qrpFunding); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x5566778899AABBCCULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + const uint64 P = ctl.state()->getTicketPriceInternal(); + + // Create deterministic winners: 2x k2 winners and 1x k3 winner => pools are small and must be topped up. + const id k2w1 = id::randomValue(); + const id k2w2 = id::randomValue(); + const id k3w1 = id::randomValue(); + ctl.fundAndBuyTicket(k2w1, P, ctl.makeK2Numbers(nums.winning, 0)); + ctl.fundAndBuyTicket(k2w2, P, ctl.makeK2Numbers(nums.winning, 1)); + ctl.fundAndBuyTicket(k3w1, P, ctl.makeK3Numbers(nums.winning, 2)); + + const uint64 qrpBefore = static_cast(getBalance(ctl.qrpSelf())); + const uint64 qtfBefore = static_cast(getBalance(ctl.qtfSelf())); + const uint64 k2w1Before = getBalance(k2w1); + const uint64 k3w1Before = getBalance(k3w1); + const uint64 sh1Before = getBalance(shareholder1); + const uint64 sh2Before = getBalance(shareholder2); + const uint64 rlBefore = getBalance(id(RL_CONTRACT_INDEX, 0, 0, 0)); + + EXPECT_EQ(qtfBefore, 3 * P); + + ctl.drawWithDigest(testDigest); + + // Expected pools and top-ups. + const QTF::GetFees_output fees = ctl.getFees(); + const uint64 revenue = 3 * P; + const uint64 winnersBlock = (revenue * fees.winnerFeePercent) / 100; + const uint64 k2Pool = (winnersBlock * QTF_BASE_K2_SHARE_BP) / 10000; + const uint64 k3Pool = (winnersBlock * QTF_BASE_K3_SHARE_BP) / 10000; + + const uint64 k2Floor = P / 2; + const uint64 k3Floor = 5 * P; + const uint64 k2TopUp = (k2Floor * 2 > k2Pool) ? (k2Floor * 2 - k2Pool) : 0; + const uint64 k3TopUp = (k3Floor > k3Pool) ? (k3Floor - k3Pool) : 0; + + // Winners must receive the floors (no per-winner cap binding in this scenario). + EXPECT_EQ(static_cast(getBalance(k2w1) - k2w1Before), k2Floor); + EXPECT_EQ(static_cast(getBalance(k3w1) - k3w1Before), k3Floor); + + // Baseline overflow is the unallocated 32% of winnersBlock (tier pools are fully paid out with floor top-ups, so no extra overflow). + const uint64 winnersOverflow = winnersBlock - k2Pool - k3Pool; + const uint64 reserveAdd = (winnersOverflow * QTF_BASELINE_OVERFLOW_ALPHA_BP) / 10000; + const uint64 carryAdd = winnersOverflow - reserveAdd; + + // Contract balance should match carry (jackpot) after settlement. + EXPECT_EQ(ctl.state()->getJackpot(), carryAdd); + EXPECT_EQ(static_cast(getBalance(ctl.qtfSelf())), carryAdd); + + // QRP: receives reserveAdd, pays out top-ups. + EXPECT_EQ(static_cast(getBalance(ctl.qrpSelf())), qrpBefore - k2TopUp - k3TopUp + reserveAdd); + + // Distribution: verify two holders and RL payback remainder. + const uint64 expectedDistFee = (revenue * fees.distributionFeePercent) / 100; + const uint64 dividendPerShare = expectedDistFee / NUMBER_OF_COMPUTORS; + const uint64 expectedSh1Gain = static_cast(shares1) * dividendPerShare; + const uint64 expectedSh2Gain = static_cast(shares2) * dividendPerShare; + const uint64 expectedPayback = expectedDistFee - (dividendPerShare * NUMBER_OF_COMPUTORS); + EXPECT_EQ(getBalance(shareholder1), sh1Before + expectedSh1Gain); + EXPECT_EQ(getBalance(shareholder2), sh2Before + expectedSh2Gain); + EXPECT_EQ(getBalance(id(RL_CONTRACT_INDEX, 0, 0, 0)), rlBefore + expectedPayback); +} + +// ============================================================================ +// HIGH-DEFICIT FR EXTRA REDIRECTS TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, FR_HighDeficit_ExtraRedirectsCalculated) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + // Fix RNG so we can deterministically avoid winners (and especially k=4). + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x4040404040404040ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + // Setup: High deficit scenario + // Jackpot = 0, Target = 1B, FR active + ctl.state()->setJackpot(0ULL); // Empty jackpot + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); // 1B target + ctl.state()->setFrActive(true); + ctl.state()->setFrRoundsSinceK4(5); + + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + const QTF::GetFees_output fees = ctl.getFees(); + + // Add many players to generate high revenue + constexpr int numPlayers = 500; + ctl.buyRandomTickets(numPlayers, ticketPrice, nums.losing); + + const uint64 revenue = ticketPrice * numPlayers; // 500M QU + const uint64 deficit = QTF_DEFAULT_TARGET_JACKPOT - 0; // 1B deficit + + // With high deficit (1B) and significant revenue (500M), extra redirects should be calculated + // Formula (from spec and QThirtyFour.h:1928-1965): + // - deficit Δ = 1B + // - E_k4(500) ≈ 55 rounds (expected rounds to k=4 with 500 tickets) + // - horizon H = min(55, 50) = 50 (capped) + // - required gain per round = Δ/H = 1B/50 = 20M + // - base gain (without extra) ≈ 1% dev + 1% dist + 5% rake + 95% overflow + // ≈ 5M + 5M + 17M + ~98M = ~125M (rough estimate) + // - Since base gain (125M) > required (20M), extra might be 0 or small + // But let's verify the mechanism is working + + const uint64 devBalBefore = getBalance(QTF_DEV_ADDRESS); + const uint64 jackpotBefore = ctl.state()->getJackpot(); + EXPECT_EQ(jackpotBefore, 0ULL); + + ctl.drawWithDigest(testDigest); + + // After settlement with FR active and high deficit: + const uint64 devBalAfter = getBalance(QTF_DEV_ADDRESS); + const uint64 jackpotAfter = ctl.state()->getJackpot(); + + // Verify FR is still active + EXPECT_EQ(ctl.state()->getFrActive(), true); + + // Dev should receive less than full 10% of revenue due to FR redirects + const uint64 fullDevPayout = (revenue * fees.teamFeePercent) / 100; // 50M (10% of 500M) + const uint64 actualDevPayout = devBalAfter - devBalBefore; + + // Base redirect alone is 1% of revenue = 5M + const uint64 baseDevRedirect = (revenue * QTF_FR_DEV_REDIRECT_BP) / 10000; // 5M + EXPECT_LT(actualDevPayout, fullDevPayout) << "Dev should receive less than full 10% in FR mode"; + EXPECT_LE(actualDevPayout, fullDevPayout - baseDevRedirect) << "Dev redirect should be at least base 1%"; + + // Jackpot should have grown significantly from: + // - Winners rake (5% of 340M winners block = 17M) + // - Dev/Dist redirects (base 1% each + possible extra) + // - Overflow bias (95% of overflow) + EXPECT_GT(jackpotAfter, 100000000ULL) << "Jackpot should grow by at least 100M from FR mechanisms"; + + // Verify extra redirect cap: dev redirect should not exceed base (1%) + extra max (0.35%) = 1.35% total + const uint64 maxDevRedirectTotal = (revenue * (QTF_FR_DEV_REDIRECT_BP + QTF_FR_EXTRA_MAX_BP / 2)) / 10000; // 1.35% + const uint64 actualDevRedirect = fullDevPayout - actualDevPayout; + EXPECT_LE(actualDevRedirect, maxDevRedirectTotal) << "Dev redirect should not exceed 1.35% of revenue"; + + // Note: The exact extra redirect amount depends on complex calculation in CalculateExtraRedirectBP + // (QThirtyFour.h:1928-1965), which uses fixed-point arithmetic, power calculations, and horizon capping. + // This test verifies the mechanism is active and within bounds. +} + +TEST(ContractQThirtyFour, Settlement_FRMode_ExtraRedirect_ClampsToMax_AndAffectsDevAndDist) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + // Ensure RL shares exist so distribution can be asserted. + const id shareholder1 = id::randomValue(); + const id shareholder2 = id::randomValue(); + constexpr uint32 shares1 = NUMBER_OF_COMPUTORS / 2; + constexpr uint32 shares2 = NUMBER_OF_COMPUTORS - shares1; + std::vector> rlShares{{shareholder1, shares1}, {shareholder2, shares2}}; + issueRlSharesTo(rlShares); + + // Deterministic no-winner tickets. + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0x7777777777777777ULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + // Force FR on and create an extreme deficit to guarantee extra redirect clamps to max. + ctl.state()->setJackpot(0ULL); + ctl.state()->setTargetJackpotInternal(1000000000000000ULL); // 1e15 + ctl.state()->setFrActive(true); + ctl.state()->setFrRoundsSinceK4(1); + + ctl.beginEpochWithValidTime(); + + const uint64 P = ctl.state()->getTicketPriceInternal(); + constexpr uint64 numPlayers = 10; + ctl.buyRandomTickets(numPlayers, P, nums.losing); + + const QTF::GetFees_output fees = ctl.getFees(); + const uint64 revenue = P * numPlayers; + + const uint64 devBefore = getBalance(QTF_DEV_ADDRESS); + const uint64 sh1Before = getBalance(shareholder1); + const uint64 sh2Before = getBalance(shareholder2); + const uint64 rlBefore = getBalance(id(RL_CONTRACT_INDEX, 0, 0, 0)); + + // Pre-compute expected extra BP using the same private helpers as the contract. + QpiContextUserFunctionCall qpi(QTF_CONTRACT_INDEX); + primeQpiFunctionContext(qpi); + const auto pools = ctl.state()->callCalculatePrizePools(qpi, revenue, true); + const auto baseGainOut = ctl.state()->callCalculateBaseGain(qpi, revenue, pools.winnersBlock); + const uint64 delta = ctl.state()->getTargetJackpotInternal() - ctl.state()->getJackpot(); + const auto extraOut = ctl.state()->callCalculateExtraRedirectBP(qpi, numPlayers, delta, revenue, baseGainOut.baseGain); + ASSERT_EQ(extraOut.extraBP, QTF_FR_EXTRA_MAX_BP); + + const uint64 devExtraBP = extraOut.extraBP / 2; + const uint64 distExtraBP = extraOut.extraBP - devExtraBP; + const uint64 totalDevRedirectBP = QTF_FR_DEV_REDIRECT_BP + devExtraBP; + const uint64 totalDistRedirectBP = QTF_FR_DIST_REDIRECT_BP + distExtraBP; + + const uint64 fullDevFee = (revenue * fees.teamFeePercent) / 100; + const uint64 fullDistFee = (revenue * fees.distributionFeePercent) / 100; + + const uint64 expectedDevRedirect = (revenue * totalDevRedirectBP) / 10000; + const uint64 expectedDistRedirect = (revenue * totalDistRedirectBP) / 10000; + const uint64 expectedDevPayout = fullDevFee - expectedDevRedirect; + const uint64 expectedDistPayout = fullDistFee - expectedDistRedirect; + + ctl.drawWithDigest(testDigest); + + // Dev payout must match exact base+extra redirect math (no caps expected in this scenario). + EXPECT_EQ(static_cast(getBalance(QTF_DEV_ADDRESS) - devBefore), expectedDevPayout); + + // Distribution must match expectedDistPayout (dividendPerShare flooring + payback). + const uint64 dividendPerShare = expectedDistPayout / NUMBER_OF_COMPUTORS; + const uint64 expectedSh1Gain = static_cast(shares1) * dividendPerShare; + const uint64 expectedSh2Gain = static_cast(shares2) * dividendPerShare; + const uint64 expectedPayback = expectedDistPayout - (dividendPerShare * NUMBER_OF_COMPUTORS); + EXPECT_EQ(getBalance(shareholder1), sh1Before + expectedSh1Gain); + EXPECT_EQ(getBalance(shareholder2), sh2Before + expectedSh2Gain); + EXPECT_EQ(getBalance(id(RL_CONTRACT_INDEX, 0, 0, 0)), rlBefore + expectedPayback); +} + +// ============================================================================ +// POST-K4 WINDOW EXPIRY TESTS +// ============================================================================ + +TEST(ContractQThirtyFour, FR_PostK4WindowExpiry_DoesNotActivateWhenInactive) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0xABCDABCDABCDABCDULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + // Setup: Jackpot below target, but window expired and FR inactive. + ctl.state()->setJackpot(QTF_DEFAULT_TARGET_JACKPOT / 2); + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); + ctl.state()->setFrActive(false); + ctl.state()->setFrRoundsSinceK4(QTF_FR_POST_K4_WINDOW_ROUNDS); + + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + constexpr int numPlayers = 10; + ctl.buyRandomTickets(numPlayers, ticketPrice, nums.losing); + + ctl.drawWithDigest(testDigest); + + EXPECT_EQ(ctl.state()->getFrActive(), false); + EXPECT_EQ(ctl.state()->getFrRoundsSinceK4(), QTF_FR_POST_K4_WINDOW_ROUNDS + 1); +} + +TEST(ContractQThirtyFour, FR_PostK4WindowExpiry_DoesNotReactivateWhenWindowExpired) +{ + ContractTestingQTF ctl; + ctl.forceSchedule(QTF_ANY_DAY_SCHEDULE); + + m256i testDigest = {}; + testDigest.m256i_u64[0] = 0xFACEFEEDFACEFEEDULL; + const auto nums = ctl.computeWinningAndLosing(testDigest); + + // Setup: FR active, jackpot below target, but approaching window expiry + ctl.state()->setJackpot(QTF_DEFAULT_TARGET_JACKPOT / 2); // 500M (below target) + ctl.state()->setTargetJackpotInternal(QTF_DEFAULT_TARGET_JACKPOT); // 1B target + ctl.state()->setFrActive(true); + ctl.state()->setFrRoundsSinceK4(QTF_FR_POST_K4_WINDOW_ROUNDS - 1); // One round before window expiry (50 = QTF_FR_POST_K4_WINDOW_ROUNDS) + + ctl.beginEpochWithValidTime(); + + const uint64 ticketPrice = ctl.state()->getTicketPriceInternal(); + + // Add players + constexpr int numPlayers = 10; + for (int i = 0; i < numPlayers; ++i) + { + const id user = id::randomValue(); + ctl.fundAndBuyTicket(user, ticketPrice, nums.losing); + } + + // Verify FR is active before settlement + EXPECT_EQ(ctl.state()->getFrActive(), true); + EXPECT_EQ(ctl.state()->getFrRoundsSinceK4(), QTF_FR_POST_K4_WINDOW_ROUNDS - 1); + EXPECT_LT(ctl.state()->getJackpot(), ctl.state()->getTargetJackpotInternal()); + + ctl.drawWithDigest(testDigest); + + // After settlement (deterministic: no k=4 win is possible): + // - roundsSinceK4 should increment to 50 + // - Next round starts outside the FR post-k4 window. + + const uint64 roundsSinceK4After = ctl.state()->getFrRoundsSinceK4(); + EXPECT_EQ(roundsSinceK4After, QTF_FR_POST_K4_WINDOW_ROUNDS) << "Counter should increment to 50 after draw"; + + // Run one more round: FR must be OFF because roundsSinceK4 >= 50. + ctl.beginEpochWithValidTime(); + + for (int i = 0; i < numPlayers; ++i) + { + const id user = id::randomValue(); + ctl.fundAndBuyTicket(user, ticketPrice, nums.losing); + } + + ctl.drawWithDigest(testDigest); + + // After second round: + // - Jackpot still below target + // - roundsSinceK4 = 51 (>= 50) + // - FR is forced OFF outside the window. + EXPECT_EQ(ctl.state()->getFrRoundsSinceK4(), QTF_FR_POST_K4_WINDOW_ROUNDS + 1); + EXPECT_EQ(ctl.state()->getFrActive(), false); + + // Run a third round to ensure FR stays OFF while still outside the window. + ctl.beginEpochWithValidTime(); + for (int i = 0; i < numPlayers; ++i) + { + const id user = id::randomValue(); + ctl.fundAndBuyTicket(user, ticketPrice, nums.losing); + } + ctl.drawWithDigest(testDigest); + + EXPECT_EQ(ctl.state()->getFrRoundsSinceK4(), QTF_FR_POST_K4_WINDOW_ROUNDS + 2); + EXPECT_EQ(ctl.state()->getFrActive(), false); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 145d717bc..2581a0c63 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -145,6 +145,8 @@ + + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index c58434225..43a188b75 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -45,6 +45,8 @@ + + @@ -73,4 +75,4 @@ core - \ No newline at end of file + From fa20bdfaeafe6aa66871c9b458150809ae8510de Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:58:02 +0100 Subject: [PATCH 58/90] add NO_QRP and NO_QTF toggles --- src/contract_core/contract_def.h | 34 +++++++++++++++++++++++++++----- src/qubic.cpp | 8 ++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index e3b877276..91bc2267f 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -215,36 +215,52 @@ #define CONTRACT_STATE2_TYPE QRWA2 #include "contracts/qRWA.h" +#ifndef NO_QRP + +constexpr unsigned short qrpContractIndex = CONTRACT_INDEX + 1; + #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QRP_CONTRACT_INDEX 21 +#define QRP_CONTRACT_INDEX qrpContractIndex #define CONTRACT_INDEX QRP_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QRP #define CONTRACT_STATE2_TYPE QRP2 #include "contracts/QReservePool.h" +#endif // NO_QRP + +#ifndef NO_QTF + +constexpr unsigned short qtfContractIndex = CONTRACT_INDEX + 1; + #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QTF_CONTRACT_INDEX 22 +#define QTF_CONTRACT_INDEX qtfContractIndex #define CONTRACT_INDEX QTF_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QTF #define CONTRACT_STATE2_TYPE QTF2 #include "contracts/QThirtyFour.h" +#endif // NO_QTF + #ifndef NO_QDUEL + +constexpr unsigned short qduelContractIndex = CONTRACT_INDEX + 1; + #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QDUEL_CONTRACT_INDEX 23 +#define QDUEL_CONTRACT_INDEX qduelContractIndex #define CONTRACT_INDEX QDUEL_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QDUEL #define CONTRACT_STATE2_TYPE QDUEL2 #include "contracts/QDuel.h" + #endif // NO_QDUEL // new contracts should be added above this line @@ -355,8 +371,12 @@ constexpr struct ContractDescription {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 +#ifndef NO_QRP {"QRP", 199, 10000, sizeof(IPO)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 +#endif +#ifndef NO_QTF {"QTF", 199, 10000, sizeof(QTF)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 +#endif #ifndef NO_QDUEL {"QDUEL", 199, 10000, sizeof(QDUEL)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 #endif @@ -476,11 +496,15 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRWA); +#ifndef NO_QRP + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRP); +#endif +#ifndef NO_QTF + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QTF); +#endif #ifndef NO_QDUEL REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL); #endif - REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRP); - REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QTF); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/qubic.cpp b/src/qubic.cpp index 7821ff493..2cdb697d2 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,8 +1,16 @@ #define SINGLE_COMPILE_UNIT // #define OLD_SWATCH +// #define NO_QRP +// #define NO_QTF // #define NO_QDUEL +// QTF in its current state is only usable with QRP. +// If the QRP proposal is rejected, disable QTF as well. +#if defined NO_QRP && !defined NO_QTF +#define NO_QTF +#endif + // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" From 9b044a95833579bdbc7d7d20139a7df482eb7586 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:22:23 +0100 Subject: [PATCH 59/90] Fix reorgBuffer race condition (#724) * Fix race condition in use of reorgBuffer This fixes a problem that led to random freezes because of the concurrent use of the reorgBuffer (aka scratchpad). So far, only the tick processor or the contract processor were allowed to use it (or the main processor if the tick and contract processors were not running). However, due to the use of the scratchpad in QPI::Collection and the use of Collection::add() -> Collection::_rebuild() in pendingTxsPool.add(), the reorgBuffer recently has also been used by the request processor without any locking. In consequence, concurrent execution of pendingTxsPool.add() and other functions using the reorgBuffer in the tick/contract processor led to undefined behaviour, usually freezing the request processor before leaving pendingTxsPool.add() and before unlocking pendingTxsPool.lock. As a clean solution, reorgBuffer has been replaced by commonBuffers, a class that supports concurrent use of multiple buffers with proper locking. In qubic.cpp, it is now configured to provide 2 buffers, in order to support concurrent execution of pendingTxsPool.add() and one other commonBuffers / scratchpad use case without blocking. When other use cases of commonBuffers / scratchpad will be added in the future, the console log output "Common buffers: [...] max waiting processors" should be monitored in order to decide about adding more buffers by changing COMMON_BUFFERS_COUNT. * Add ASSERT nullptr of commonBuffer + fix typo --- src/assets/assets.h | 5 +- src/common_buffers.h | 199 +++++++++++++++++++++--- src/contract_core/pre_qpi_def.h | 37 ++++- src/contract_core/qpi_collection_impl.h | 7 +- src/contract_core/qpi_hash_map_impl.h | 8 +- src/contract_core/qpi_proposal_voting.h | 4 +- src/logging/logging.h | 20 ++- src/qubic.cpp | 16 +- src/spectrum/spectrum.h | 71 +++++---- test/assets.cpp | 4 +- test/contract_testing.h | 4 +- test/pending_txs_pool.cpp | 2 + test/platform.cpp | 175 +++++++++++++++++++++ test/qpi_collection.cpp | 26 ++-- test/qpi_hash_map.cpp | 30 ++-- test/spectrum.cpp | 4 +- 16 files changed, 508 insertions(+), 104 deletions(-) diff --git a/src/assets/assets.h b/src/assets/assets.h index 2bee80603..5a5adff22 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -793,8 +793,8 @@ static void assetsEndEpoch() ACQUIRE(universeLock); // rebuild asset hash map, getting rid of all elements with zero shares - AssetRecord* reorgAssets = (AssetRecord*)reorgBuffer; - setMem(reorgAssets, ASSETS_CAPACITY * sizeof(AssetRecord), 0); + AssetRecord* reorgAssets = (AssetRecord*)commonBuffers.acquireBuffer(universeSizeInBytes); + setMem(reorgAssets, universeSizeInBytes, 0); for (unsigned int i = 0; i < ASSETS_CAPACITY; i++) { if (assets[i].varStruct.possession.type == POSSESSION @@ -874,6 +874,7 @@ static void assetsEndEpoch() } } copyMem(assets, reorgAssets, ASSETS_CAPACITY * sizeof(AssetRecord)); + commonBuffers.releaseBuffer(reorgAssets); setMem(assetChangeFlags, ASSETS_CAPACITY / 8, 0xFF); diff --git a/src/common_buffers.h b/src/common_buffers.h index 18dcd4478..0a7925672 100644 --- a/src/common_buffers.h +++ b/src/common_buffers.h @@ -3,43 +3,200 @@ #include "platform/global_var.h" #include "platform/memory_util.h" #include "platform/assert.h" +#include "platform/concurrency.h" #include "network_messages/entity.h" #include "network_messages/assets.h" +#include "contract_core/pre_qpi_def.h" +#include "contracts/math_lib.h" constexpr unsigned long long spectrumSizeInBytes = SPECTRUM_CAPACITY * sizeof(EntityRecord); constexpr unsigned long long universeSizeInBytes = ASSETS_CAPACITY * sizeof(AssetRecord); -// TODO: check that max contract state size does not exceed size of spectrum or universe -constexpr unsigned long long reorgBufferSize = (spectrumSizeInBytes >= universeSizeInBytes) ? spectrumSizeInBytes : universeSizeInBytes; +constexpr unsigned long long defaultCommonBuffersSize = math_lib::max(MAX_CONTRACT_STATE_SIZE, math_lib::max(spectrumSizeInBytes, universeSizeInBytes)); -// Buffer used for reorganizing spectrum and universe hash maps, currently also used as scratchpad buffer for contracts +// Buffer(s) used for: +// - reorganizing spectrum and universe hash maps (tick processor) +// - scratchpad buffer used internally in QPI::Collection, QPI::HashMap, QPI::HashSet, +// QPI::ProposalAndVotingByShareholders +// (often used in contract processor which does not run concurrently with tick processor, but now also used outside +// of contracts, e.g. pendingTxsPool.add() running in request processor may trigger Collection::_rebuild() which +// uses scratchpad) +// - building oracle transactions in processTick() in tick processor +// - calculateStableComputorIndex() in tick processor +// - saving and loading of logging state +// - DustBurnLogger used in increaseEnergy() in tick / contract processor // Must be large enough to fit any contract, full spectrum, and full universe! -GLOBAL_VAR_DECL void* reorgBuffer GLOBAL_VAR_INIT(nullptr); +class CommonBuffers +{ +public: + // Allocate common buffers. With count > 1, multiple buffers may be used concurrently. The maximum buffer size + // that can be acquired is given by size. + bool init(unsigned int count, unsigned long long size = defaultCommonBuffersSize) + { + if (!count || !size) + return false; + + // soft limit, just to detect mistakes in usage like init(sizeof(Object)) + ASSERT(count < 16); + + // memory layout of buffer: sub buffer pointers | sub buffer locks | sub buffer 1 | sub buffer 2 | ... + unsigned char* buffer = nullptr; + const unsigned long long ptrSize = count * sizeof(unsigned char*); + const unsigned long long lockSize = (count + 7) / 8; + const unsigned long long bufSize = count * size; + + if (!allocPoolWithErrorLog(L"commonBuffers", ptrSize + lockSize + bufSize, (void**)&buffer, __LINE__)) + { + return false; + } -static bool initCommonBuffers() -{ - if (!allocPoolWithErrorLog(L"reorgBuffer", reorgBufferSize, (void**)&reorgBuffer, __LINE__)) + bufferCount = count; + subBufferSize = size; + subBufferPtr = (unsigned char**)buffer; + subBufferLock = (volatile char*)(buffer + ptrSize); + unsigned char* subBuf = buffer + ptrSize + lockSize; + for (unsigned int i = 0; i < count; ++i) + { + subBufferPtr[i] = subBuf; + subBuf += size; + } + + return true; + } + + // Free common buffers. + void deinit() { - return false; + if (subBufferPtr) + { + freePool(subBufferPtr); + subBufferPtr = nullptr; + subBufferLock = nullptr; + subBufferSize = 0; + bufferCount = 0; + waitingCount = 0; + maxWaitingCount = 0; + invalidReleaseCount = 0; + } } - return true; -} - -static void deinitCommonBuffers() -{ - if (reorgBuffer) + // Get buffer of given size. + // Returns nullptr if size is too big. Otherwise may block until buffer is available. + // Does not init buffer! Buffer needs to be released with releaseBuffer() after use. + void* acquireBuffer(unsigned long long size) + { + ASSERT(subBufferLock && subBufferPtr); +#if !defined(NO_UEFI) + ASSERT(size <= subBufferSize); +#endif + if (size > subBufferSize) + return nullptr; + + // shortcut for default case + if (TRY_ACQUIRE(subBufferLock[0])) + { + return subBufferPtr[0]; + } + + long cnt = _InterlockedIncrement(&waitingCount); + if (maxWaitingCount < cnt) + maxWaitingCount = cnt; + + unsigned int i = 0; + BEGIN_WAIT_WHILE(TRY_ACQUIRE(subBufferLock[i]) == false) + { + ++i; + if (i >= bufferCount) + i = 0; + } + END_WAIT_WHILE(); + + _InterlockedDecrement(&waitingCount); + + return subBufferPtr[i]; + } + + // Release buffer that was acquired with acquireBuffer() before. + void releaseBuffer(void* buffer) + { + ASSERT(subBufferLock && subBufferPtr && buffer); + + // shortcut for default case + if (subBufferPtr[0] == buffer) + { + if (subBufferLock[0]) + RELEASE(subBufferLock[0]); + else + ++invalidReleaseCount; + return; + } + + // find buffer + unsigned int bufferIdx = 1; + while (bufferIdx < bufferCount && subBufferPtr[bufferIdx] != buffer) + ++bufferIdx; + + // invalid pointer passed? +#if !defined(NO_UEFI) + ASSERT(bufferIdx < bufferCount); + ASSERT(subBufferLock[bufferIdx]); +#endif + if (bufferIdx >= bufferCount || !subBufferLock[bufferIdx]) + { + ++invalidReleaseCount; + return; + } + + // release buffer + RELEASE(subBufferLock[bufferIdx]); + } + + // Heuristics how many processors were waiting for a buffer in parallel (for deciding the count of buffers) + long getMaxWaitingProcessorCount() const + { + return maxWaitingCount; + } + + // Counter of invalid release calls as an indicator if debugging is needed + long getInvalidReleaseCount() const { - freePool(reorgBuffer); - reorgBuffer = nullptr; + return invalidReleaseCount; } + + // Returns number of buffers currently acquired + unsigned int acquiredBuffers() const + { + unsigned int count = 0; + for (unsigned int i = 0; i < bufferCount; ++i) + if (subBufferLock[i]) + ++count; + return count; + } + +protected: + unsigned char** subBufferPtr = nullptr; + volatile char* subBufferLock = nullptr; + unsigned long long subBufferSize = 0; + unsigned int bufferCount = 0; + volatile long waitingCount = 0; + long maxWaitingCount = 0; + long invalidReleaseCount = 0; +}; + + +GLOBAL_VAR_DECL CommonBuffers commonBuffers; + + +static void* __acquireScratchpad(unsigned long long size, bool initZero = true) +{ + void* ptr = commonBuffers.acquireBuffer(size); + if (ptr && initZero) + setMem(ptr, size, 0); + return ptr; } -static void* __scratchpad(unsigned long long sizeToMemsetZero) +static void __releaseScratchpad(void* ptr) { - ASSERT(sizeToMemsetZero <= reorgBufferSize); - if (sizeToMemsetZero) - setMem(reorgBuffer, sizeToMemsetZero, 0); - return reorgBuffer; + commonBuffers.releaseBuffer(ptr); } diff --git a/src/contract_core/pre_qpi_def.h b/src/contract_core/pre_qpi_def.h index 7a5aabd49..51af0f2a8 100644 --- a/src/contract_core/pre_qpi_def.h +++ b/src/contract_core/pre_qpi_def.h @@ -38,12 +38,39 @@ template static void __logContractWarningMessage(unsigned int, T&); static void __pauseLogMessage(); static void __resumeLogMessage(); -// Get buffer for temporary use. Can only be used in contract procedures / tick processor / contract processor! -// Always returns the same one buffer, no concurrent access! -static void* __scratchpad(unsigned long long sizeToMemsetZero = 0); +// Implemented in common_buffers.h +static void* __acquireScratchpad(unsigned long long size, bool initZero); +static void __releaseScratchpad(void* ptr); + +// Create an instance on the stack to acquire a scratchpad buffer. +// When the scope is left, the scratchpad is released automatically. +struct __ScopedScratchpad +{ + // Construction of object, trying to acquire buffer. May init ptr == nullptr if size is bigger than supported or + // block if no buffer is available at the moment. + inline __ScopedScratchpad(unsigned long long size, bool initZero = true) + { + ptr = __acquireScratchpad(size, initZero); + } + + // Destructor, releasing buffer at object's end of life. + inline ~__ScopedScratchpad() + { + release(); + } + + // Release buffer before end of life + inline void release() + { + if (!ptr) + return; + __releaseScratchpad(ptr); + ptr = nullptr; + } + + void* ptr; +}; -// static void* __tryAcquireScratchpad(unsigned int size); // Thread-safe, may return nullptr if no appropriate buffer is available -// static void __ReleaseScratchpad(void*); template struct __FunctionOrProcedureBeginEndGuard diff --git a/src/contract_core/qpi_collection_impl.h b/src/contract_core/qpi_collection_impl.h index 2fbf9cedf..e5281a703 100644 --- a/src/contract_core/qpi_collection_impl.h +++ b/src/contract_core/qpi_collection_impl.h @@ -280,7 +280,8 @@ namespace QPI template sint64 Collection::_rebuild(sint64 rootIdx) { - auto* sortedElementIndices = reinterpret_cast(::__scratchpad()); + __ScopedScratchpad scratchpad(sizeof(*this), /*initZero=*/false); + auto* sortedElementIndices = reinterpret_cast(scratchpad.ptr); if (sortedElementIndices == NULL) { return rootIdx; @@ -615,7 +616,9 @@ namespace QPI } // Init buffers - auto* _povsBuffer = reinterpret_cast(::__scratchpad(sizeof(_povs) + sizeof(_povOccupationFlags))); + __ScopedScratchpad scratchpad(sizeof(_povs) + sizeof(_povOccupationFlags), /*initZero=*/true); + ASSERT(scratchpad.ptr); + auto* _povsBuffer = reinterpret_cast(scratchpad.ptr); auto* _povOccupationFlagsBuffer = reinterpret_cast(_povsBuffer + L); auto* _stackBuffer = reinterpret_cast( _povOccupationFlagsBuffer + sizeof(_povOccupationFlags) / sizeof(_povOccupationFlags[0])); diff --git a/src/contract_core/qpi_hash_map_impl.h b/src/contract_core/qpi_hash_map_impl.h index 6266cfdb9..5834f690c 100644 --- a/src/contract_core/qpi_hash_map_impl.h +++ b/src/contract_core/qpi_hash_map_impl.h @@ -288,7 +288,9 @@ namespace QPI } // Init buffers - auto* _elementsBuffer = reinterpret_cast(::__scratchpad(sizeof(_elements) + sizeof(_occupationFlags))); + __ScopedScratchpad scratchpad(sizeof(_elements) + sizeof(_occupationFlags), /*initZero=*/true); + ASSERT(scratchpad.ptr); + auto* _elementsBuffer = reinterpret_cast(scratchpad.ptr); auto* _occupationFlagsBuffer = reinterpret_cast(_elementsBuffer + L); auto* _stackBuffer = reinterpret_cast( _occupationFlagsBuffer + sizeof(_occupationFlags) / sizeof(_occupationFlags[0])); @@ -614,7 +616,9 @@ namespace QPI } // Init buffers - auto* _keyBuffer = reinterpret_cast(::__scratchpad(sizeof(_keys) + sizeof(_occupationFlags))); + __ScopedScratchpad scratchpad(sizeof(_keys) + sizeof(_occupationFlags), /*initZero=*/true); + ASSERT(scratchpad.ptr); + auto* _keyBuffer = reinterpret_cast(scratchpad.ptr); auto* _occupationFlagsBuffer = reinterpret_cast(_keyBuffer + L); auto* _stackBuffer = reinterpret_cast( _occupationFlagsBuffer + sizeof(_occupationFlags) / sizeof(_occupationFlags[0])); diff --git a/src/contract_core/qpi_proposal_voting.h b/src/contract_core/qpi_proposal_voting.h index e1ccb00d3..85dc444a5 100644 --- a/src/contract_core/qpi_proposal_voting.h +++ b/src/contract_core/qpi_proposal_voting.h @@ -156,7 +156,9 @@ namespace QPI id possessor; sint64 shares; }; - Shareholder* shareholders = reinterpret_cast(__scratchpad(sizeof(Shareholder) * maxVotes)); + __ScopedScratchpad scratchpad(sizeof(Shareholder)* maxVotes, /*initZero=*/true); + ASSERT(scratchpad.ptr); + Shareholder* shareholders = reinterpret_cast(scratchpad.ptr); int lastShareholderIdx = -1; // gather shareholder info in sorted array diff --git a/src/logging/logging.h b/src/logging/logging.h index c0ed6f6d7..8fb4ee41d 100644 --- a/src/logging/logging.h +++ b/src/logging/logging.h @@ -617,9 +617,12 @@ class qLogger bool saveCurrentLoggingStates(CHAR16* dir) { #if ENABLED_LOGGING - unsigned char* buffer = (unsigned char*)__scratchpad(); - static_assert(reorgBufferSize >= LOG_BUFFER_PAGE_SIZE + PMAP_LOG_PAGE_SIZE * sizeof(BlobInfo) + IMAP_LOG_PAGE_SIZE * sizeof(TickBlobInfo) - + sizeof(digests) + 600, "scratchpad is too small"); + constexpr auto bufferSize = LOG_BUFFER_PAGE_SIZE + PMAP_LOG_PAGE_SIZE * sizeof(BlobInfo) + IMAP_LOG_PAGE_SIZE * sizeof(TickBlobInfo) + + sizeof(digests) + 600; + static_assert(defaultCommonBuffersSize >= bufferSize, "commonBuffer size is too small"); + __ScopedScratchpad scratchpad(bufferSize, /*initZero=*/false); + ASSERT(scratchpad.ptr); + unsigned char* buffer = (unsigned char*)scratchpad.ptr; unsigned long long writeSz = 0; // copy currentPage of log buffer ~ 100MiB unsigned long long sz = logBuffer.dumpVMState(buffer); @@ -654,7 +657,7 @@ class qLogger *((unsigned int*)buffer) = currentTxId; buffer += 4; *((unsigned int*)buffer) = currentTick; buffer += 4; writeSz += 8 + 8 + 4 + 4 + 4 + 4; - buffer = (unsigned char*)__scratchpad(); // reset back to original pos + buffer = (unsigned char*)scratchpad.ptr; // reset back to original pos sz = save(L"logEventState.db", writeSz, buffer, dir); if (sz != writeSz) { @@ -669,9 +672,12 @@ class qLogger void loadLastLoggingStates(CHAR16* dir) { #if ENABLED_LOGGING - unsigned char* buffer = (unsigned char*)__scratchpad(); - static_assert(reorgBufferSize >= LOG_BUFFER_PAGE_SIZE + PMAP_LOG_PAGE_SIZE * sizeof(BlobInfo) + IMAP_LOG_PAGE_SIZE * sizeof(TickBlobInfo) - + sizeof(digests) + 600, "scratchpad is too small"); + constexpr auto bufferSize = LOG_BUFFER_PAGE_SIZE + PMAP_LOG_PAGE_SIZE * sizeof(BlobInfo) + IMAP_LOG_PAGE_SIZE * sizeof(TickBlobInfo) + + sizeof(digests) + 600; + static_assert(defaultCommonBuffersSize >= bufferSize, "commonBuffer size is too small"); + __ScopedScratchpad scratchpad(bufferSize, /*initZero=*/false); + unsigned char* buffer = (unsigned char*)scratchpad.ptr; + ASSERT(scratchpad.ptr); CHAR16 fileName[] = L"logEventState.db"; const long long fileSz = getFileSize(fileName, dir); if (fileSz == -1) diff --git a/src/qubic.cpp b/src/qubic.cpp index 2cdb697d2..cbc2020c0 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -101,6 +101,7 @@ #define MIN_MINING_SOLUTIONS_PUBLICATION_OFFSET 3 // Must be 3+ #define TIME_ACCURACY 5000 constexpr unsigned long long TARGET_MAINTHREAD_LOOP_DURATION = 30; // mcs, it is the target duration of the main thread loop +constexpr unsigned int COMMON_BUFFERS_COUNT = 2; struct Processor : public CustomStack @@ -5308,8 +5309,11 @@ static void tickProcessor(void*) // Reorder futureComputors so requalifying computors keep their index // This is needed for correct execution fee reporting across epoch boundaries - static_assert(reorgBufferSize >= stableComputorIndexBufferSize(), "reorgBuffer too small for stable computor index"); + static_assert(defaultCommonBuffersSize >= stableComputorIndexBufferSize(), "commonBuffers too small for stable computor index"); + void* reorgBuffer = commonBuffers.acquireBuffer(stableComputorIndexBufferSize()); + ASSERT(reorgBuffer); calculateStableComputorIndex(system.futureComputors, broadcastedComputors.computors.publicKeys, reorgBuffer); + commonBuffers.releaseBuffer(reorgBuffer); // instruct main loop to save system and wait until it is done systemMustBeSaved = true; @@ -5624,7 +5628,7 @@ static bool initialize() if (!initSpectrum()) return false; - if (!initCommonBuffers()) + if (!commonBuffers.init(COMMON_BUFFERS_COUNT)) return false; if (!initAssets()) @@ -5950,7 +5954,7 @@ static void deinitialize() deinitAssets(); deinitSpectrum(); - deinitCommonBuffers(); + commonBuffers.deinit(); logger.deinitLogging(); @@ -6411,6 +6415,12 @@ static void logHealthStatus() appendNumber(message, contractLocalsStackLockWaitingCountMax, TRUE); logToConsole(message); + setText(message, L"Common buffers: invalid release "); + appendNumber(message, commonBuffers.getInvalidReleaseCount(), FALSE); + appendText(message, L", max waiting processors "); + appendNumber(message, commonBuffers.getMaxWaitingProcessorCount(), FALSE); + logToConsole(message); + setText(message, L"Connections:"); for (int i = 0; i < NUMBER_OF_OUTGOING_CONNECTIONS + NUMBER_OF_INCOMING_CONNECTIONS; ++i) { diff --git a/src/spectrum/spectrum.h b/src/spectrum/spectrum.h index e1a57dd2a..66cc13409 100644 --- a/src/spectrum/spectrum.h +++ b/src/spectrum/spectrum.h @@ -110,15 +110,23 @@ static void logSpectrumStats() } // Build and log variable-size DustBurning log message. -// Assumes to be used tick processor or contract processor only, so can use reorgBuffer. struct DustBurnLogger { + // TODO: better use 0xffff (when proved to be stable with logging) + static constexpr unsigned short maxEntries = 1000; + DustBurnLogger() { - buf = (DustBurning*)reorgBuffer; + buf = (DustBurning*)commonBuffers.acquireBuffer(2 + maxEntries * sizeof(DustBurning::Entity)); + ASSERT(buf); buf->numberOfBurns = 0; } + ~DustBurnLogger() + { + commonBuffers.releaseBuffer(buf); + } + // Add burned amount of of entity, may send buffered message to logging. void addDustBurn(const m256i& publicKey, unsigned long long amount) { @@ -126,7 +134,7 @@ struct DustBurnLogger e.publicKey = publicKey; e.amount = amount; - if (buf->numberOfBurns == 1000) // TODO: better use 0xffff (when proved to be stable with logging) + if (buf->numberOfBurns == maxEntries) finished(); } @@ -149,8 +157,9 @@ static void reorganizeSpectrum() unsigned long long spectrumReorgStartTick = __rdtsc(); - EntityRecord* reorgSpectrum = (EntityRecord*)reorgBuffer; - setMem(reorgSpectrum, SPECTRUM_CAPACITY * sizeof(EntityRecord), 0); + EntityRecord* reorgSpectrum = (EntityRecord*)commonBuffers.acquireBuffer(spectrumSizeInBytes); + ASSERT(reorgSpectrum); + setMem(reorgSpectrum, spectrumSizeInBytes, 0); for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) { if (spectrum[i].incomingAmount - spectrum[i].outgoingAmount) @@ -171,6 +180,7 @@ static void reorganizeSpectrum() } } copyMem(spectrum, reorgSpectrum, SPECTRUM_CAPACITY * sizeof(EntityRecord)); + commonBuffers.releaseBuffer(reorgSpectrum); unsigned int digestIndex; for (digestIndex = 0; digestIndex < SPECTRUM_CAPACITY; digestIndex++) @@ -252,50 +262,55 @@ static void increaseEnergy(const m256i& publicKey, long long amount) #if LOG_SPECTRUM logSpectrumStats(); #endif + + // Burn the dust and log the burns. The extra scope is needed to make sure that DurstBurnLogger is + // destructed (and its common buffer is released) before reorganizeSpectrum() is called. + { #if LOG_SPECTRUM - DustBurnLogger dbl; + DustBurnLogger dbl; #endif - if (dustThresholdBurnAll > 0) - { - // Burn every balance with balance < dustThresholdBurnAll - for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) + if (dustThresholdBurnAll > 0) { - const unsigned long long balance = spectrum[i].incomingAmount - spectrum[i].outgoingAmount; - if (balance <= dustThresholdBurnAll && balance) + // Burn every balance with balance < dustThresholdBurnAll + for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) { - spectrum[i].outgoingAmount = spectrum[i].incomingAmount; + const unsigned long long balance = spectrum[i].incomingAmount - spectrum[i].outgoingAmount; + if (balance <= dustThresholdBurnAll && balance) + { + spectrum[i].outgoingAmount = spectrum[i].incomingAmount; #if LOG_SPECTRUM - dbl.addDustBurn(spectrum[i].publicKey, balance); + dbl.addDustBurn(spectrum[i].publicKey, balance); #endif + } } } - } - if (dustThresholdBurnHalf > 0) - { - // Burn every second balance with balance < dustThresholdBurnHalf - unsigned int countBurnCanadiates = 0; - for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) + if (dustThresholdBurnHalf > 0) { - const unsigned long long balance = spectrum[i].incomingAmount - spectrum[i].outgoingAmount; - if (balance <= dustThresholdBurnHalf && balance) + // Burn every second balance with balance < dustThresholdBurnHalf + unsigned int countBurnCanadiates = 0; + for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) { - if (++countBurnCanadiates & 1) + const unsigned long long balance = spectrum[i].incomingAmount - spectrum[i].outgoingAmount; + if (balance <= dustThresholdBurnHalf && balance) { - spectrum[i].outgoingAmount = spectrum[i].incomingAmount; + if (++countBurnCanadiates & 1) + { + spectrum[i].outgoingAmount = spectrum[i].incomingAmount; #if LOG_SPECTRUM - dbl.addDustBurn(spectrum[i].publicKey, balance); + dbl.addDustBurn(spectrum[i].publicKey, balance); #endif + } } } } - } #if LOG_SPECTRUM - // Finished dust burning (pass message to log) - dbl.finished(); + // Finished dust burning (pass message to log) + dbl.finished(); #endif + } // Remove entries with balance zero from hash map reorganizeSpectrum(); diff --git a/test/assets.cpp b/test/assets.cpp index 7c2bafd71..fc15c4e50 100644 --- a/test/assets.cpp +++ b/test/assets.cpp @@ -21,12 +21,12 @@ class AssetsTest : public AssetStorage, LoggingTest AssetsTest() { initAssets(); - initCommonBuffers(); + commonBuffers.init(1, universeSizeInBytes); } ~AssetsTest() { - deinitCommonBuffers(); + commonBuffers.deinit(); deinitAssets(); } diff --git a/test/contract_testing.h b/test/contract_testing.h index 61d3a71dc..1f1c47fc8 100644 --- a/test/contract_testing.h +++ b/test/contract_testing.h @@ -33,7 +33,7 @@ class ContractTesting : public LoggingTest #ifdef __AVX512F__ initAVX512FourQConstants(); #endif - initCommonBuffers(); + commonBuffers.init(1); initContractExec(); initSpecialEntities(); @@ -46,7 +46,7 @@ class ContractTesting : public LoggingTest deinitSpecialEntities(); deinitAssets(); deinitSpectrum(); - deinitCommonBuffers(); + commonBuffers.deinit(); deinitContractExec(); for (unsigned int i = 0; i < contractCount; ++i) { diff --git a/test/pending_txs_pool.cpp b/test/pending_txs_pool.cpp index b0c6338fc..b191a5994 100644 --- a/test/pending_txs_pool.cpp +++ b/test/pending_txs_pool.cpp @@ -43,11 +43,13 @@ class TestPendingTxsPool : public PendingTxsPool spectrum[NUM_INITIALIZED_ENTITIES + i].publicKey = m256i{ 0, 0, 0, NUM_INITIALIZED_ENTITIES + i + 1 }; } updateSpectrumInfo(); + commonBuffers.init(1, sizeof(*txsPriorities)); } ~TestPendingTxsPool() { deinitSpectrum(); + commonBuffers.deinit(); } static constexpr unsigned int getMaxNumTxsPerTick() diff --git a/test/platform.cpp b/test/platform.cpp index 371bce9d4..5bc997dd3 100644 --- a/test/platform.cpp +++ b/test/platform.cpp @@ -6,6 +6,9 @@ #include "../src/platform/custom_stack.h" #include "../src/platform/profiling.h" +#include "common_buffers.h" +#include + TEST(TestCoreReadWriteLock, SimpleSingleThread) { ReadWriteLock l; @@ -324,3 +327,175 @@ TEST(TestCoreProfiling, CheckTicksToMicroseconds) checkTicksToMicroseconds(2, 0xffffffffffffffffllu, 12345); checkTicksToMicroseconds(2, 0xffffffffffffffffllu, 123456); } + +static volatile bool waitingForAcquire = false; + +static void acquireAndReleaseCommonBuffer() +{ + waitingForAcquire = true; + void* buf = commonBuffers.acquireBuffer(64); + waitingForAcquire = false; + EXPECT_NE(buf, nullptr); + sleepMilliseconds(100); + commonBuffers.releaseBuffer(buf); +} + +TEST(TestCoreCommonBuffers, OneBuffer) +{ + if (!frequency) + initTimeStampCounter(); + + // init fail: 0 buffers / size 0 + EXPECT_FALSE(commonBuffers.init(0, 1024)); + EXPECT_FALSE(commonBuffers.init(1, 0)); + + // init success + EXPECT_TRUE(commonBuffers.init(1, 1024)); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // acquire fail: requested buffer size too big + void* buf = commonBuffers.acquireBuffer(2000); + EXPECT_EQ(buf, nullptr); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // acquire success + buf = commonBuffers.acquireBuffer(1024); + EXPECT_NE(buf, nullptr); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 1); + + // release success + commonBuffers.releaseBuffer(buf); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // check stats + EXPECT_EQ(commonBuffers.getInvalidReleaseCount(), 0); + EXPECT_EQ(commonBuffers.getMaxWaitingProcessorCount(), 0); + + // invalid release (double free) + commonBuffers.releaseBuffer(buf); + EXPECT_EQ(commonBuffers.getInvalidReleaseCount(), 1); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // invalid release (invalid pointer) + commonBuffers.releaseBuffer((void*)0x123456); + EXPECT_EQ(commonBuffers.getInvalidReleaseCount(), 2); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // acquire success + buf = commonBuffers.acquireBuffer(512); + EXPECT_NE(buf, nullptr); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 1); + + // acquire in other thread has to wait + auto thread = std::thread(acquireAndReleaseCommonBuffer); + sleepMilliseconds(100); + EXPECT_TRUE(waitingForAcquire); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 1); + + // release buffer -> other thread can acquire it + commonBuffers.releaseBuffer(buf); + sleepMilliseconds(50); // after 50 ms the other thread should have acquired the buffer + EXPECT_FALSE(waitingForAcquire); + thread.join(); // after ending the thread, the buffer is release again + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + EXPECT_EQ(commonBuffers.getMaxWaitingProcessorCount(), 1); + + // free all buffers, reset state + commonBuffers.deinit(); + EXPECT_EQ(commonBuffers.getInvalidReleaseCount(), 0); + EXPECT_EQ(commonBuffers.getMaxWaitingProcessorCount(), 0); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); +} + +TEST(TestCoreCommonBuffers, ThreeBuffers) +{ + if (!frequency) + initTimeStampCounter(); + + // init success + EXPECT_TRUE(commonBuffers.init(3, 1024)); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // acquire fail: requested buffer size too big + void* buf = commonBuffers.acquireBuffer(2000); + EXPECT_EQ(buf, nullptr); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // acquire success + buf = commonBuffers.acquireBuffer(1024); + EXPECT_NE(buf, nullptr); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 1); + + // release + commonBuffers.releaseBuffer(buf); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // acquire three buffer success + void* buf2 = commonBuffers.acquireBuffer(1024); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 1); + void* buf3 = commonBuffers.acquireBuffer(42); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 2); + buf = commonBuffers.acquireBuffer(123); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 3); + EXPECT_NE(buf, nullptr); + EXPECT_NE(buf2, nullptr); + EXPECT_NE(buf3, nullptr); + + // release three buffers + commonBuffers.releaseBuffer(buf); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 2); + commonBuffers.releaseBuffer(buf2); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 1); + commonBuffers.releaseBuffer(buf3); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + // check stats + EXPECT_EQ(commonBuffers.getInvalidReleaseCount(), 0); + EXPECT_EQ(commonBuffers.getMaxWaitingProcessorCount(), 1); // heuristic is off by one (in facto no waiting needed) + + // invalid release (double free) + commonBuffers.releaseBuffer(buf); + EXPECT_EQ(commonBuffers.getInvalidReleaseCount(), 1); + + // invalid release (invalid pointer) + commonBuffers.releaseBuffer((void*)0x123456); + EXPECT_EQ(commonBuffers.getInvalidReleaseCount(), 2); + + // acquire three buffers + buf = commonBuffers.acquireBuffer(512); + buf2 = commonBuffers.acquireBuffer(1024); + buf3 = commonBuffers.acquireBuffer(987); + EXPECT_NE(buf, nullptr); + EXPECT_NE(buf2, nullptr); + EXPECT_NE(buf3, nullptr); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 3); + + // acquire in two other thread has to wait + auto thread1 = std::thread(acquireAndReleaseCommonBuffer); + auto thread2 = std::thread(acquireAndReleaseCommonBuffer); + sleepMilliseconds(100); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 3); + EXPECT_TRUE(waitingForAcquire); + + // release 2 buffers -> other threads can acquire them + commonBuffers.releaseBuffer(buf); + commonBuffers.releaseBuffer(buf2); + sleepMilliseconds(50); + EXPECT_FALSE(waitingForAcquire); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 3); + thread1.join(); + thread2.join(); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 1); + + commonBuffers.releaseBuffer(buf3); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); + + EXPECT_EQ(commonBuffers.getMaxWaitingProcessorCount(), 2); + + // free all buffers, reset state + commonBuffers.deinit(); + EXPECT_EQ(commonBuffers.getInvalidReleaseCount(), 0); + EXPECT_EQ(commonBuffers.getMaxWaitingProcessorCount(), 0); + EXPECT_EQ(commonBuffers.acquiredBuffers(), 0); +} diff --git a/test/qpi_collection.cpp b/test/qpi_collection.cpp index b2b9a6ce5..5988bfd6c 100644 --- a/test/qpi_collection.cpp +++ b/test/qpi_collection.cpp @@ -680,6 +680,9 @@ void testCollectionOnePovMultiElements(int prioAmpFactor, int prioFreqDiv) QPI::Collection coll; coll.reset(); + // scratchpad may be needed if Collection::_rebuild() is called + EXPECT_TRUE(commonBuffers.init(1, sizeof(coll))); + // check that behavior of collection and reference implementation matches CollectionReferenceImpl collReference; @@ -888,6 +891,9 @@ void testCollectionOnePovMultiElements(int prioAmpFactor, int prioFreqDiv) EXPECT_FALSE(isCompletelySame(resetColl, coll)); coll.cleanup(); EXPECT_TRUE(isCompletelySame(resetColl, coll)); + + // cleanup + commonBuffers.deinit(); } TEST(TestCoreQPI, CollectionOnePovMultiElements) @@ -1267,6 +1273,9 @@ TEST(TestCoreQPI, CollectionSubCollectionsRandom) QPI::Collection coll; coll.reset(); + // scratchpad may be needed if Collection::_rebuild() is called + EXPECT_TRUE(commonBuffers.init(1, sizeof(coll))); + const int seed = 246357; std::mt19937_64 gen64(seed); @@ -1355,6 +1364,8 @@ TEST(TestCoreQPI, CollectionSubCollectionsRandom) } } } + + commonBuffers.deinit(); } TEST(TestCoreQPI, CollectionReplaceElements) @@ -1511,7 +1522,7 @@ void testCollectionPseudoRandom(int povs, int seed, bool povCollisions, int clea TEST(TestCoreQPI, CollectionInsertRemoveCleanupRandom) { - reorgBuffer = new char[10 * 1024 * 1024]; + commonBuffers.init(1, 10 * 1024 * 1024); constexpr unsigned int numCleanups = 30; for (int i = 0; i < 10; ++i) { @@ -1529,21 +1540,19 @@ TEST(TestCoreQPI, CollectionInsertRemoveCleanupRandom) testCollectionPseudoRandom<16>(10, 12 + i, povCollisions, numCleanups, 55, 45); testCollectionPseudoRandom<4>(4, 42 + i, povCollisions, numCleanups, 52, 48); } - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); } TEST(TestCoreQPI, CollectionCleanupWithPovCollisions) { // Shows bugs in cleanup() that occur in case of massive pov hash map collisions and in case of capacity < 32 - reorgBuffer = new char[10 * 1024 * 1024]; + commonBuffers.init(1, 10 * 1024 * 1024); bool cleanupAfterEachRemove = true; testCollectionMultiPovOneElement<16>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<32>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<64>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<128>(cleanupAfterEachRemove); - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); } @@ -1662,7 +1671,7 @@ QPI::uint64 testCollectionPerformance( TEST(TestCoreQPI, CollectionPerformance) { - reorgBuffer = new char[16 * 1024 * 1024]; + commonBuffers.init(1, 16 * 1024 * 1024); std::vector durations; std::vector descriptions; @@ -1691,8 +1700,7 @@ TEST(TestCoreQPI, CollectionPerformance) durations.push_back(testCollectionPerformance<512>(16, 333)); descriptions.push_back("[CollectionPerformance] Collection<512>(16, 333)"); - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); bool verbose = true; if (verbose) diff --git a/test/qpi_hash_map.cpp b/test/qpi_hash_map.cpp index 0f9b7acbc..b28dc2e7d 100644 --- a/test/qpi_hash_map.cpp +++ b/test/qpi_hash_map.cpp @@ -363,7 +363,7 @@ TYPED_TEST_P(QPIHashMapTest, TestCleanup) constexpr QPI::uint64 capacity = 4; QPI::HashMap hashMap; - reorgBuffer = new char[2 * sizeof(hashMap)]; + commonBuffers.init(1, 2 * sizeof(hashMap)); std::array keyValuePairs = HashMapTestData::CreateKeyValueTestPairs(); auto ids = std::views::keys(keyValuePairs); @@ -402,8 +402,7 @@ TYPED_TEST_P(QPIHashMapTest, TestCleanup) EXPECT_NE(returnedIndex, QPI::NULL_INDEX); EXPECT_EQ(hashMap.population(), 4); - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); } TYPED_TEST_P(QPIHashMapTest, TestCleanupPerformanceShortcuts) @@ -439,7 +438,7 @@ TEST(NonTypedQPIHashMapTest, TestCleanupLargeMapSameHashes) constexpr QPI::uint64 capacity = 64; QPI::HashMap hashMap; - reorgBuffer = new char[2 * sizeof(hashMap)]; + commonBuffers.init(1, 2 * sizeof(hashMap)); for (QPI::uint64 i = 0; i < 64; ++i) { @@ -452,8 +451,7 @@ TEST(NonTypedQPIHashMapTest, TestCleanupLargeMapSameHashes) // Cleanup will have to iterate through the whole map to find an empty slot for the last element. hashMap.cleanup(); - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); } TYPED_TEST_P(QPIHashMapTest, TestReplace) @@ -612,7 +610,7 @@ void testHashMapPseudoRandom(int seed, int cleanups, int percentAdd, int percent std::map referenceMap; QPI::HashMap map; - reorgBuffer = new char[2 * sizeof(map)]; + commonBuffers.init(1, 2 * sizeof(map)); map.reset(); @@ -674,8 +672,7 @@ void testHashMapPseudoRandom(int seed, int cleanups, int percentAdd, int percent // std::cout << "capacity: " << set.capacity() << ", pupulation:" << set.population() << std::endl; } - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); } TEST(QPIHashMapTest, HashMapPseudoRandom) @@ -707,7 +704,7 @@ TEST(QPIHashMapTest, HashSet) { constexpr QPI::uint64 capacity = 128; QPI::HashSet hashSet; - reorgBuffer = new char[2 * sizeof(hashSet)]; + commonBuffers.init(1, 2 * sizeof(hashSet)); EXPECT_EQ(hashSet.capacity(), capacity); // Test add() and contains() @@ -805,8 +802,7 @@ TEST(QPIHashMapTest, HashSet) hashSet.reset(); EXPECT_EQ(hashSet.population(), 0); - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); } template @@ -875,7 +871,7 @@ void testHashSetPseudoRandom(int seed, int cleanups, int percentAdd, int percent std::set referenceSet; QPI::HashSet set; - reorgBuffer = new char[2 * sizeof(set)]; + commonBuffers.init(1, 2 * sizeof(set)); set.reset(); @@ -935,8 +931,7 @@ void testHashSetPseudoRandom(int seed, int cleanups, int percentAdd, int percent // std::cout << "capacity: " << set.capacity() << ", pupulation:" << set.population() << std::endl; } - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); } TEST(QPIHashMapTest, HashSetPseudoRandom) @@ -972,7 +967,7 @@ static void perfTestCleanup(int seed) std::mt19937_64 gen64(seed); auto* set = new QPI::HashSet(); - reorgBuffer = new char[sizeof(*set)]; + commonBuffers.init(1, sizeof(*set)); for (QPI::uint64 i = 1; i <= 100; ++i) { @@ -996,8 +991,7 @@ static void perfTestCleanup(int seed) } delete set; - delete[] reorgBuffer; - reorgBuffer = nullptr; + commonBuffers.deinit(); } TEST(QPIHashMapTest, HashSetPerfTest) diff --git a/test/spectrum.cpp b/test/spectrum.cpp index 6520eb8e9..dca62d896 100644 --- a/test/spectrum.cpp +++ b/test/spectrum.cpp @@ -132,7 +132,7 @@ struct SpectrumTest : public LoggingTest _rdrand64_step(&seed); rnd64.seed(seed); EXPECT_TRUE(initSpectrum()); - EXPECT_TRUE(initCommonBuffers()); + EXPECT_TRUE(commonBuffers.init(1)); system.tick = 15700000; clearSpectrum(); antiDustCornerCase = false; @@ -141,7 +141,7 @@ struct SpectrumTest : public LoggingTest ~SpectrumTest() { deinitSpectrum(); - deinitCommonBuffers(); + commonBuffers.deinit(); } void clearSpectrum() From b08fadf0beef68b49a321289b6fd448846c9a8ae Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:30:39 +0100 Subject: [PATCH 60/90] update params for epoch 198 / v1.276.0 --- src/public_settings.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/public_settings.h b/src/public_settings.h index ab18d3850..3df5ff134 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -66,12 +66,12 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // Config options that should NOT be changed by operators #define VERSION_A 1 -#define VERSION_B 275 +#define VERSION_B 276 #define VERSION_C 0 // Epoch and initial tick for node startup -#define EPOCH 197 -#define TICK 42715000 +#define EPOCH 198 +#define TICK 43101000 #define TICK_IS_FIRST_TICK_OF_EPOCH 1 // Set to 0 if the network is restarted during the EPOCH with a new initial TICK #define ARBITRATOR "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" From 038d5e30023d4de5741d5a8e3160b9a015f3c146 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:41:56 +0100 Subject: [PATCH 61/90] Fix oracle-related use cases of reorgBuffer Don't use commonBuffers in oracle engine for less locking in commonBuffers. A static buffer is sufficient for the the oracle engine use case, because it only requires a small buffer. However, in commonBuffers is used in the tick processor for creating oracle reply transactions for simplicity. --- src/oracle_core/oracle_engine.h | 16 +++++++++++----- src/qubic.cpp | 9 ++++++--- test/oracle_engine.cpp | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 5a97c8c13..adfffdd35 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -5,7 +5,6 @@ #include "oracle_core/oracle_interfaces_def.h" #include "system.h" -#include "common_buffers.h" #include "spectrum/special_entities.h" #include "ticking/tick_storage.h" #include "logging/logging.h" @@ -309,6 +308,9 @@ class OracleEngine /// buffer used to store output of getNotification() OracleNotificationData notificationOutputBuffer; + /// buffer used by enqueueOracleQuery() + uint8_t enqueueOracleQueryBuffer[sizeof(OracleMachineQuery) + MAX_ORACLE_QUERY_SIZE]; + /// lock for preventing race conditions in concurrent execution mutable volatile char lock; @@ -515,18 +517,22 @@ class OracleEngine } protected: - // Enqueue oracle machine query message. May be called from tick processor or contract processor only (uses reorgBuffer). - static void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint32_t timeoutMillisec, const void* queryData, uint16_t querySize) + // Enqueue oracle machine query message. Cannot be run concurrently. Caller must acquire engine lock! + void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint32_t timeoutMillisec, const void* queryData, uint16_t querySize) { + // Check input size and compute total payload size + ASSERT(querySize <= MAX_ORACLE_QUERY_SIZE); + const unsigned int payloadSize = sizeof(OracleMachineQuery) + querySize; + // Prepare message payload - OracleMachineQuery* omq = reinterpret_cast(reorgBuffer); + OracleMachineQuery* omq = reinterpret_cast(enqueueOracleQueryBuffer); omq->oracleQueryId = queryId; omq->oracleInterfaceIndex = interfaceIdx; omq->timeoutInMilliseconds = timeoutMillisec; copyMem(omq + 1, queryData, querySize); // Enqueue for sending to all oracle machine peers (peer pointer address 0x1 is reserved for that) - enqueueResponse((Peer*)0x1, sizeof(*omq) + querySize, OracleMachineQuery::type(), 0, omq); + enqueueResponse((Peer*)0x1, payloadSize, OracleMachineQuery::type(), 0, omq); } void logQueryStatusChange(const OracleQueryMetadata& oqm) const diff --git a/src/qubic.cpp b/src/qubic.cpp index 4081027ba..043c07399 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -3623,14 +3623,15 @@ static void processTick(unsigned long long processorNumber) } } - // Publish oracle reply commit and reveal transactions (uses reorgBuffer for constructing packets) + // Publish oracle reply commit and reveal transactions if (isMainMode()) { unsigned char digest[32]; + void* txBuffer = commonBuffers.acquireBuffer(MAX_TRANSACTION_SIZE); { PROFILE_NAMED_SCOPE("processTick(): broadcast oracle reply transactions"); const auto txTick = system.tick + ORACLE_REPLY_COMMIT_PUBLICATION_OFFSET; - auto* tx = (OracleReplyCommitTransactionPrefix*)reorgBuffer; + auto* tx = (OracleReplyCommitTransactionPrefix*)txBuffer; unsigned int txCount = 0; for (unsigned int i = 0; i < numberOfOwnComputorIndices; i++) { @@ -3688,7 +3689,7 @@ static void processTick(unsigned long long processorNumber) { PROFILE_NAMED_SCOPE("processTick(): broadcast oracle reveal transactions"); - auto* tx = (OracleReplyRevealTransactionPrefix*)reorgBuffer; + auto* tx = (OracleReplyRevealTransactionPrefix*)txBuffer; const auto txTick = system.tick + ORACLE_REPLY_REVEAL_PUBLICATION_OFFSET; // create reply reveal transaction in tx (without signature), returning: // - 0 if no tx was created (no need to send reply commits) @@ -3702,6 +3703,8 @@ static void processTick(unsigned long long processorNumber) enqueueResponse(NULL, tx->totalSize(), BROADCAST_TRANSACTION, 0, tx); } } + + commonBuffers.releaseBuffer(txBuffer); } if (isMainMode()) diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index e3406402b..ae792a48e 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -7,7 +7,7 @@ struct OracleEngineTest : public LoggingTest { OracleEngineTest() { - EXPECT_TRUE(initCommonBuffers()); + EXPECT_TRUE(commonBuffers.init(1, sizeof(OracleMachineQuery) + MAX_ORACLE_QUERY_SIZE)); EXPECT_TRUE(initSpecialEntities()); EXPECT_TRUE(initContractExec()); EXPECT_TRUE(ts.init()); @@ -31,7 +31,7 @@ struct OracleEngineTest : public LoggingTest ~OracleEngineTest() { - deinitCommonBuffers(); + commonBuffers.deinit(); deinitContractExec(); ts.deinit(); } From 014641ad04faa3ebcde64554e9357d6e1d3483e0 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 19 Jan 2026 08:48:34 +0100 Subject: [PATCH 62/90] Improve debug output --- src/qubic.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qubic.cpp b/src/qubic.cpp index 043c07399..5b6f09839 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -3450,7 +3450,7 @@ static void processTick(unsigned long long processorNumber) { CHAR16 dbgMsg[500]; setText(dbgMsg, L"pending tx: tick/count"); - for (unsigned int i = system.tick; i < system.tick + 10; ++i) + for (unsigned int i = system.tick; i < system.tick + ORACLE_REPLY_COMMIT_PUBLICATION_OFFSET; ++i) { appendText(dbgMsg, " "); appendNumber(dbgMsg, i, FALSE); From a37f5c58237dd3b67b276cde058a06d9ad0b21a2 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:47:50 +0100 Subject: [PATCH 63/90] Fix oracle stats network message --- src/oracle_core/net_msg_impl.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index b11ef47ad..74866d4d2 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -160,7 +160,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R p->pendingRevealCount = pendingRevealReplyStateIndices.numValues; p->successfulCount = stats.successCount; p->unresolvableCount = stats.unresolvableCount; - const uint64_t totalTimeouts = stats.timeoutNoReplyCount + stats.timeoutNoCommitCount + stats.timeoutNoReplyCount; + const uint64_t totalTimeouts = stats.timeoutNoReplyCount + stats.timeoutNoCommitCount + stats.timeoutNoRevealCount; p->timeoutCount = totalTimeouts; p->timeoutNoReplyCount = stats.timeoutNoReplyCount; p->timeoutNoCommitCount = stats.timeoutNoCommitCount; @@ -170,6 +170,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R p->commitAvgMilliTicksPerQuery = (stats.commitCount) ? stats.commitTicksSum * 1000 / stats.commitCount : 0; p->successAvgMilliTicksPerQuery = (stats.successCount) ? stats.successTicksSum * 1000 / stats.successCount : 0; p->timeoutAvgMilliTicksPerQuery = (totalTimeouts) ? stats.timeoutTicksSum * 1000 / totalTimeouts : 0; + p->revealTxCount = stats.revealTxCount; // send response enqueueResponse(peer, sizeof(RespondOracleData) + sizeof(RespondOracleDataQueryStatistics), From 4043127505ca3b174ba691d3f1de3bb8058ee12b Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:50:33 +0100 Subject: [PATCH 64/90] Set oracle reply commit publication offset to 4 --- src/qubic.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qubic.cpp b/src/qubic.cpp index 5b6f09839..1ad515afe 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -105,7 +105,7 @@ #define SYSTEM_DATA_SAVING_PERIOD 300000ULL #define TICK_TRANSACTIONS_PUBLICATION_OFFSET 2 // Must be only 2 #define MIN_MINING_SOLUTIONS_PUBLICATION_OFFSET 3 // Must be 3+ -#define ORACLE_REPLY_COMMIT_PUBLICATION_OFFSET 5 +#define ORACLE_REPLY_COMMIT_PUBLICATION_OFFSET 4 #define ORACLE_REPLY_REVEAL_PUBLICATION_OFFSET 3 #define TIME_ACCURACY 5000 constexpr unsigned long long TARGET_MAINTHREAD_LOOP_DURATION = 30; // mcs, it is the target duration of the main thread loop From 18da20938ae06fe712a4e49e3a7ab636a90798aa Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:24:30 +0100 Subject: [PATCH 65/90] Save/load snapshot of all oracle engine data --- src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 3 + src/oracle_core/oracle_engine.h | 98 +++++++++-------- src/oracle_core/snapshot_files.h | 176 +++++++++++++++++++++++++++++++ src/qubic.cpp | 6 ++ 5 files changed, 233 insertions(+), 51 deletions(-) create mode 100644 src/oracle_core/snapshot_files.h diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 3d173b98e..c9afee4dc 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -105,6 +105,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 859bfa02f..c16c54007 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -362,6 +362,9 @@ contracts + + oracle_core + diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index adfffdd35..77993bf25 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -200,6 +200,48 @@ struct UnsortedMultiset } }; +struct OracleEngineStatistics +{ + /// total number of successful oracle queries + unsigned long long successCount; + + /// sum of ticks that were required to reach the success state + unsigned long long successTicksSum; + + /// total number of timeout oracle queries without reply from oracle machine + unsigned long long timeoutNoReplyCount; + + /// total number of timeout oracle queries without commit quorum + unsigned long long timeoutNoCommitCount; + + /// total number of timeout oracle queries without reveal + unsigned long long timeoutNoRevealCount; + + /// sum of ticks until timeout of all timeout cases + unsigned long long timeoutTicksSum; + + /// total number of unresolvable oracle queries + unsigned long long unresolvableCount; + + /// total number of oracle queries that got oracle machine reply locally + unsigned long long oracleMachineReplyCount; + + /// sum of ticks that were required to get oracle machine reply locally + unsigned long long oracleMachineReplyTicksSum; + + /// total number of oracle queries that reached commit state + unsigned long long commitCount; + + /// sum of ticks that were required to reach the commit state + unsigned long long commitTicksSum; + + /// total number of oracle machin replies that disagree with the first reply received for a query + unsigned long long oracleMachineRepliesDisagreeCount; + + /// total number of reply reveal transactions + unsigned long long revealTxCount; +}; + template class OracleEngine @@ -245,46 +287,7 @@ class OracleEngine // fast lookup of query indices for which the contract should be notified UnsortedMultiset notificationQueryIndicies; - struct { - /// total number of successful oracle queries - unsigned long long successCount; - - /// sum of ticks that were required to reach the success state - unsigned long long successTicksSum; - - /// total number of timeout oracle queries without reply from oracle machine - unsigned long long timeoutNoReplyCount; - - /// total number of timeout oracle queries without commit quorum - unsigned long long timeoutNoCommitCount; - - /// total number of timeout oracle queries without reveal - unsigned long long timeoutNoRevealCount; - - /// sum of ticks until timeout of all timeout cases - unsigned long long timeoutTicksSum; - - /// total number of unresolvable oracle queries - unsigned long long unresolvableCount; - - /// total number of oracle queries that got oracle machine reply locally - unsigned long long oracleMachineReplyCount; - - /// sum of ticks that were required to get oracle machine reply locally - unsigned long long oracleMachineReplyTicksSum; - - /// total number of oracle queries that reached commit state - unsigned long long commitCount; - - /// sum of ticks that were required to reach the commit state - unsigned long long commitTicksSum; - - /// total number of oracle machin replies that disagree with the first reply received for a query - unsigned long long oracleMachineRepliesDisagreeCount; - - /// total number of reply reveal transactions - unsigned long long revealTxCount; - } stats; + OracleEngineStatistics stats; #if ENABLE_ORACLE_STATS_RECORD struct @@ -380,18 +383,11 @@ class OracleEngine freePool(replyStates); } - void save() const - { - LockGuard lockGuard(lock); - // save state (excluding queryIdToIndex and unused parts of large buffers) - } + /// Save current state to snapshot files. Can only be called from main processor! + bool saveSnapshot(unsigned short epoch, CHAR16* directory) const; - void load() - { - LockGuard lockGuard(lock); - // load state (excluding queryIdToIndex and unused parts of large buffers) - // init queryIdToIndex - } + /// Load state from snapshot files. Can only be called from main processor! + bool loadSnapshot(unsigned short epoch, CHAR16* directory); /** * Check and start user query based on transaction (should be called from tick processor). diff --git a/src/oracle_core/snapshot_files.h b/src/oracle_core/snapshot_files.h new file mode 100644 index 000000000..f7cd2c091 --- /dev/null +++ b/src/oracle_core/snapshot_files.h @@ -0,0 +1,176 @@ +#pragma once + +#include "oracle_core/oracle_engine.h" +#include "network_messages/oracles.h" + +#if TICK_STORAGE_AUTOSAVE_MODE + +#include "platform/file_io.h" + +static unsigned short ORACLE_SNAPSHOT_ENGINE_FILENAME[] = L"snapshotOracleEngine.???"; +static unsigned short ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME[] = L"snapshotOracleQueryMetadata.???"; +static unsigned short ORACLE_SNAPSHOT_QUERY_DATA_FILENAME[] = L"snapshotOracleQueryData.???"; +static unsigned short ORACLE_SNAPSHOT_REPLY_STATES_FILENAME[] = L"snapshotOracleReplyStates.???"; + + +struct OracleEngineSnapshotData +{ + uint64_t queryStorageBytesUsed; + uint32_t oracleQueryCount; + uint32_t contractQueryIdStateTick; + uint32_t contractQueryIdStateQueryIndexInTick; + int32_t replyStatesIndex; + UnsortedMultiset pendingQueryIndices; + UnsortedMultiset pendingCommitReplyStateIndices; + UnsortedMultiset pendingRevealReplyStateIndices; + UnsortedMultiset notificationQueryIndicies; + OracleEngineStatistics stats; +}; + + +// save state (excluding queryIdToIndex and unused parts of large buffers) +template +bool OracleEngine::saveSnapshot(unsigned short epoch, CHAR16* directory) const +{ + addEpochToFileName(ORACLE_SNAPSHOT_ENGINE_FILENAME, sizeof(ORACLE_SNAPSHOT_ENGINE_FILENAME) / sizeof(ORACLE_SNAPSHOT_ENGINE_FILENAME[0]), epoch); + addEpochToFileName(ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME, sizeof(ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME) / sizeof(ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME[0]), epoch); + addEpochToFileName(ORACLE_SNAPSHOT_QUERY_DATA_FILENAME, sizeof(ORACLE_SNAPSHOT_QUERY_DATA_FILENAME) / sizeof(ORACLE_SNAPSHOT_QUERY_DATA_FILENAME[0]), epoch); + addEpochToFileName(ORACLE_SNAPSHOT_REPLY_STATES_FILENAME, sizeof(ORACLE_SNAPSHOT_REPLY_STATES_FILENAME) / sizeof(ORACLE_SNAPSHOT_REPLY_STATES_FILENAME[0]), epoch); + + LockGuard lockGuard(lock); + + OracleEngineSnapshotData engineData; + engineData.queryStorageBytesUsed = queryStorageBytesUsed; + engineData.oracleQueryCount = oracleQueryCount; + engineData.contractQueryIdStateTick = contractQueryIdState.tick; + engineData.contractQueryIdStateQueryIndexInTick = contractQueryIdState.queryIndexInTick; + engineData.replyStatesIndex = replyStatesIndex; + copyMemory(engineData.pendingQueryIndices, pendingQueryIndices); + copyMemory(engineData.pendingCommitReplyStateIndices, pendingCommitReplyStateIndices); + copyMemory(engineData.pendingRevealReplyStateIndices, pendingRevealReplyStateIndices); + copyMemory(engineData.notificationQueryIndicies, notificationQueryIndicies); + copyMemory(engineData.stats, stats); + + logToConsole(L"Saving oracle engine data ..."); + long long sz = saveLargeFile(ORACLE_SNAPSHOT_ENGINE_FILENAME, sizeof(engineData), (unsigned char*)&engineData, directory); + if (sz != sizeof(engineData)) + { + logToConsole(L"Failed to save oracle engine data!"); + return false; + } + + logToConsole(L"Saving oracle query metadata ..."); + unsigned long long sizeToSave = sizeof(*queries) * oracleQueryCount; + // TODO: only save parts that were added after previous snapshot + sz = saveLargeFile(ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME, sizeToSave, (unsigned char*)queries, directory); + if (sz != sizeToSave) + { + logToConsole(L"Failed to save oracle query metadata!"); + return false; + } + + logToConsole(L"Saving oracle query data storage ..."); + sizeToSave = queryStorageBytesUsed; + // TODO: only save parts that were added after previous snapshot + sz = saveLargeFile(ORACLE_SNAPSHOT_QUERY_DATA_FILENAME, sizeToSave, queryStorage, directory); + if (sz != sizeToSave) + { + logToConsole(L"Failed to save oracle query data storage!"); + return false; + } + + logToConsole(L"Saving oracle reply states ..."); + sizeToSave = sizeof(ReplyState) * MAX_SIMULTANEOUS_ORACLE_QUERIES; + sz = saveLargeFile(ORACLE_SNAPSHOT_REPLY_STATES_FILENAME, sizeToSave, (unsigned char*)replyStates, directory); + if (sz != sizeToSave) + { + logToConsole(L"Failed to save oracle reply states!"); + return false; + } + + logToConsole(L"Successfully saved all oracle engine data to snapshot!"); + + return true; +} + +template +bool OracleEngine::loadSnapshot(unsigned short epoch, CHAR16* directory) +{ + addEpochToFileName(ORACLE_SNAPSHOT_ENGINE_FILENAME, sizeof(ORACLE_SNAPSHOT_ENGINE_FILENAME) / sizeof(ORACLE_SNAPSHOT_ENGINE_FILENAME[0]), epoch); + addEpochToFileName(ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME, sizeof(ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME) / sizeof(ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME[0]), epoch); + addEpochToFileName(ORACLE_SNAPSHOT_QUERY_DATA_FILENAME, sizeof(ORACLE_SNAPSHOT_QUERY_DATA_FILENAME) / sizeof(ORACLE_SNAPSHOT_QUERY_DATA_FILENAME[0]), epoch); + addEpochToFileName(ORACLE_SNAPSHOT_REPLY_STATES_FILENAME, sizeof(ORACLE_SNAPSHOT_REPLY_STATES_FILENAME) / sizeof(ORACLE_SNAPSHOT_REPLY_STATES_FILENAME[0]), epoch); + + LockGuard lockGuard(lock); + + OracleEngineSnapshotData engineData; + + logToConsole(L"Loading oracle engine data ..."); + long long sz = loadLargeFile(ORACLE_SNAPSHOT_ENGINE_FILENAME, sizeof(engineData), (unsigned char*)&engineData, directory); + if (sz != sizeof(engineData)) + { + logToConsole(L"Failed to load oracle engine data!"); + return false; + } + + queryStorageBytesUsed = engineData.queryStorageBytesUsed; + oracleQueryCount = engineData.oracleQueryCount; + contractQueryIdState.tick = engineData.contractQueryIdStateTick; + contractQueryIdState.queryIndexInTick = engineData.contractQueryIdStateQueryIndexInTick; + replyStatesIndex = engineData.replyStatesIndex; + copyMemory(pendingQueryIndices, engineData.pendingQueryIndices); + copyMemory(pendingCommitReplyStateIndices, engineData.pendingCommitReplyStateIndices); + copyMemory(pendingRevealReplyStateIndices, engineData.pendingRevealReplyStateIndices); + copyMemory(notificationQueryIndicies, engineData.notificationQueryIndicies); + copyMemory(stats, engineData.stats); + + + logToConsole(L"Loading oracle query metadata ..."); + unsigned long long sizeToLoad = sizeof(*queries) * oracleQueryCount; + sz = loadLargeFile(ORACLE_SNAPSHOT_QUERY_METADATA_FILENAME, sizeToLoad, (unsigned char*)queries, directory); + if (sz != sizeToLoad) + { + logToConsole(L"Failed to load oracle query metadata!"); + return false; + } + + logToConsole(L"Loading oracle query data storage ..."); + sizeToLoad = queryStorageBytesUsed; + // TODO: only save parts that were added after previous snapshot + sz = loadLargeFile(ORACLE_SNAPSHOT_QUERY_DATA_FILENAME, sizeToLoad, queryStorage, directory); + if (sz != sizeToLoad) + { + logToConsole(L"Failed to load oracle query data storage!"); + return false; + } + + logToConsole(L"Loading oracle reply states ..."); + sizeToLoad = sizeof(ReplyState) * MAX_SIMULTANEOUS_ORACLE_QUERIES; + sz = loadLargeFile(ORACLE_SNAPSHOT_REPLY_STATES_FILENAME, sizeToLoad, (unsigned char*)replyStates, directory); + if (sz != sizeToLoad) + { + logToConsole(L"Failed to load oracle reply states!"); + return false; + } + + // init queryIdToIndex (not saved to file) + queryIdToIndex->reset(); + for (uint32_t queryIndex = 0; queryIndex < oracleQueryCount; ++queryIndex) + queryIdToIndex->set(queries[queryIndex].queryId, queryIndex); + + logToConsole(L"Successfully loaded all oracle engine data from snapshot!"); +} + +#else + +template +bool OracleEngine::saveSnapshot(unsigned short epoch, CHAR16* directory) const +{ +} + +template +bool OracleEngine::loadSnapshot(unsigned short epoch, CHAR16* directory) +{ +} + +#endif diff --git a/src/qubic.cpp b/src/qubic.cpp index 1ad515afe..49deb1acb 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -86,6 +86,7 @@ #include "oracle_core/oracle_engine.h" #include "oracle_core/net_msg_impl.h" +#include "oracle_core/snapshot_files.h" #include "contract_core/qpi_oracle_impl.h" #include "contract_core/qpi_mining_impl.h" @@ -4352,6 +4353,11 @@ static bool saveAllNodeStates() return false; } + if (oracleEngine.saveSnapshot(system.epoch, directory) != 0) + { + return false; + } + #if ADDON_TX_STATUS_REQUEST if (!saveStateTxStatus(numberOfTransactions, directory)) { From 0ad79181bfd58264a4ea5877f03e45e292920366 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:48:52 +0100 Subject: [PATCH 66/90] Disable pendingTxsPool.add() debug output... ... for out-of-storage tick numbers (too distant future or past) --- src/ticking/pending_txs_pool.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ticking/pending_txs_pool.h b/src/ticking/pending_txs_pool.h index f5d8bff5f..b419dd2f0 100644 --- a/src/ticking/pending_txs_pool.h +++ b/src/ticking/pending_txs_pool.h @@ -351,7 +351,7 @@ class PendingTxsPool } #endif } -#if !defined(NDEBUG) && !defined(NO_UEFI) +#if !defined(NDEBUG) && !defined(NO_UEFI) && 0 else { CHAR16 dbgMsgBuf[250]; From dc2d8393024cf18e0a39e3d51ff0156fc86442d0 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:30:36 +0100 Subject: [PATCH 67/90] Improve oracle status output (to console) --- src/oracle_core/oracle_engine.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 77993bf25..bc57d8977 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -1466,13 +1466,12 @@ class OracleEngine appendNumber(message, totalTimeouts, FALSE); appendText(message, " (OM "); appendNumber(message, stats.timeoutNoReplyCount, FALSE); - appendText(message, "; commit "); + appendText(message, ", commit "); appendNumber(message, stats.timeoutNoCommitCount, FALSE); - appendText(message, "; reveal "); + appendText(message, ", reveal "); appendNumber(message, stats.timeoutNoRevealCount, FALSE); - appendText(message, "; ticks "); appendQuotientWithOneDecimal(message, stats.timeoutTicksSum, totalTimeouts); - appendText(message, "), conflicting OM replies "); + appendText(message, " ticks), conflicting OM replies "); appendNumber(message, stats.oracleMachineRepliesDisagreeCount, FALSE); appendText(message, "), query slots occupied "); appendNumber(message, oracleQueryCount * 100 / MAX_ORACLE_QUERIES, FALSE); From 7ff1a1ebda22dd641b43e7093db1e0fbc40a3a1f Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 23 Jan 2026 09:24:45 +0100 Subject: [PATCH 68/90] Fix typos --- src/oracle_core/oracle_engine.h | 16 ++++++++-------- src/oracle_interfaces/Mock.h | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index bc57d8977..c6874538f 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -533,14 +533,14 @@ class OracleEngine void logQueryStatusChange(const OracleQueryMetadata& oqm) const { - m256i queryingEntitiy = m256i::zero(); + m256i queryingEntity = m256i::zero(); if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) - queryingEntitiy.u64._0 = oqm.typeVar.contract.queryingContract; + queryingEntity.u64._0 = oqm.typeVar.contract.queryingContract; else if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_SUBSCRIPTION) - queryingEntitiy.u64._0 = oqm.typeVar.subscription.subscriptionId; + queryingEntity.u64._0 = oqm.typeVar.subscription.subscriptionId; else if (oqm.type == ORACLE_QUERY_TYPE_USER_QUERY) - queryingEntitiy = oqm.typeVar.user.queryingEntity; - OracleQueryStatusChange logEvent{ queryingEntitiy, oqm.queryId, oqm.interfaceIndex, oqm.type, oqm.status }; + queryingEntity = oqm.typeVar.user.queryingEntity; + OracleQueryStatusChange logEvent{ queryingEntity, oqm.queryId, oqm.interfaceIndex, oqm.type, oqm.status }; logger.logOracleQueryStatusChange(logEvent); } @@ -618,7 +618,7 @@ class OracleEngine // Update the stats for each type of oracles const uint32_t ifaceIdx = oqm.interfaceIndex; oracleStats[ifaceIdx].replyCount++; - // Now only record contract querry + // Now only record contract query if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) { const void* queryData = queryStorage + oqm.typeVar.contract.queryStorageOffset; @@ -1215,7 +1215,7 @@ class OracleEngine // lock for accessing engine data LockGuard lockGuard(lock); - // consider peinding queries + // consider pending queries const uint32_t queryIdxCount = pendingQueryIndices.numValues; const uint32_t* queryIndices = pendingQueryIndices.values; const QPI::DateAndTime now = QPI::DateAndTime::now(); @@ -1283,7 +1283,7 @@ class OracleEngine } /** - * @brief Get info for notfying contracts. Call until nullptr is returned. + * @brief Get info for notifying contracts. Call until nullptr is returned. * @return Pointer to notification info or nullptr if no notifications are needed. * * Only to be used in tick processor! No concurrent use supported. Uses one internal buffer for returned data. diff --git a/src/oracle_interfaces/Mock.h b/src/oracle_interfaces/Mock.h index 131bdfab0..6993c34ad 100644 --- a/src/oracle_interfaces/Mock.h +++ b/src/oracle_interfaces/Mock.h @@ -43,8 +43,8 @@ struct Mock } /// Check if the passed oracle reply is valid - static bool replyIsValid(const OracleQuery& querry, const OracleReply& reply) + static bool replyIsValid(const OracleQuery& query, const OracleReply& reply) { - return (reply.echoedValue == querry.value) && (reply.doubledValue == 2 * querry.value); + return (reply.echoedValue == query.value) && (reply.doubledValue == 2 * query.value); } }; From bfc3acd371a7c0ed6f3e3e56256fd7a03922463e Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:30:24 +0100 Subject: [PATCH 69/90] Delete all oracle queries in beginEpoch() --- src/oracle_core/oracle_engine.h | 31 +++++++++++++++++++++++++++---- src/qubic.cpp | 1 + 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index c6874538f..81e0d3511 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -349,12 +349,29 @@ class OracleEngine // alloc arrays and set to 0 if (!allocPoolWithErrorLog(L"OracleEngine::queries", MAX_ORACLE_QUERIES * sizeof(*queries), (void**)&queries, __LINE__) || !allocPoolWithErrorLog(L"OracleEngine::queryStorage", ORACLE_QUERY_STORAGE_SIZE, (void**)&queryStorage, __LINE__) - || !allocPoolWithErrorLog(L"OracleEngine::replyCommitState", MAX_SIMULTANEOUS_ORACLE_QUERIES * sizeof(*replyStates), (void**)&replyStates, __LINE__) + || !allocPoolWithErrorLog(L"OracleEngine::replyStates", MAX_SIMULTANEOUS_ORACLE_QUERIES * sizeof(*replyStates), (void**)&replyStates, __LINE__) || !allocPoolWithErrorLog(L"OracleEngine::queryIdToIndex", sizeof(*queryIdToIndex), (void**)&queryIdToIndex, __LINE__)) { return false; } + reset(); + + return true; + } + + /// Delete all queries, reply states, statistics etc. + void reset() + { + ASSERT(queries && queryStorage && replyStates && queryIdToIndex); + if (oracleQueryCount || queryStorageBytesUsed > 8 || queryIdToIndex->population()) + { + setMem(queries, MAX_ORACLE_QUERIES * sizeof(*queries), 0); + setMem(queryStorage, ORACLE_QUERY_STORAGE_SIZE, 0); + setMem(replyStates, MAX_SIMULTANEOUS_ORACLE_QUERIES * sizeof(*replyStates), 0); + queryIdToIndex->reset(); + } + oracleQueryCount = 0; queryStorageBytesUsed = 8; // reserve offset 0 for "no data" setMem(&contractQueryIdState, sizeof(contractQueryIdState), 0); @@ -368,8 +385,6 @@ class OracleEngine #if ENABLE_ORACLE_STATS_RECORD setMem(&oracleStats, sizeof(oracleStats), 0); #endif - - return true; } void deinit() @@ -1330,14 +1345,22 @@ class OracleEngine return ¬ificationOutputBuffer; } + // Drop all queries of the previous epoch. void beginEpoch() { // lock for accessing engine data LockGuard lockGuard(lock); + reset(); + // TODO // clean all subscriptions - // clean all queries (except for last n ticks in case of seamless transition) + // Issue: For some queries at the end of the epoch, the notification is never called, possible solutions: + // - notify timeout or another error at end of epoch + // - save state of oracle engine also for start from scratch + // in the future we may: + // - keep all non-pending queries of the last n ticks in case of seamless transition + // - keep info of all pending queries as well } protected: diff --git a/src/qubic.cpp b/src/qubic.cpp index 49deb1acb..4475a5eea 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -3852,6 +3852,7 @@ static void beginEpoch() #endif ts.beginEpoch(system.initialTick); pendingTxsPool.beginEpoch(system.initialTick); + oracleEngine.beginEpoch(); voteCounter.init(); #ifndef NDEBUG ts.checkStateConsistencyWithAssert(); From 59691d0b5e13a0ce705f23fa62e256ed3f0b34c3 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:04:30 +0100 Subject: [PATCH 70/90] Oracle engine snapshot loading and consistency check --- src/oracle_core/oracle_engine.h | 193 +++++++++++++++++++++++++++++++ src/oracle_core/snapshot_files.h | 16 +++ src/qubic.cpp | 20 +++- 3 files changed, 224 insertions(+), 5 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 81e0d3511..e34bbd334 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -1450,6 +1450,199 @@ class OracleEngine return queryMetadata.status; } + + // Useful for debugging, but expensive: check that everything is as expected. + void checkStateConsistencyWithAssert() const + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Begin oracleEngine.checkStateConsistencyWithAssert()"); + __ScopedScratchpad scratchpad(1000, /*initZero=*/true); + CHAR16* dbgMsgBuf = (CHAR16*)scratchpad.ptr; + logStatus(dbgMsgBuf); + addDebugMessage(dbgMsgBuf); +#endif + + ASSERT(queries); + ASSERT(queryStorage); + ASSERT(queryIdToIndex); + ASSERT(replyStates); + ASSERT(oracleQueryCount <= MAX_ORACLE_QUERIES); + ASSERT(queryStorageBytesUsed <= ORACLE_QUERY_STORAGE_SIZE); + ASSERT(oracleQueryCount == queryIdToIndex->population()); + ASSERT(contractQueryIdState.tick <= system.tick); + ASSERT(contractQueryIdState.queryIndexInTick < 0x7FFFFFFF); + + uint64_t successCount = 0; + uint64_t timeoutCount = 0; + uint64_t unresolvableCount = 0; + uint64_t pendingCount = 0; + uint64_t committedCount = 0; + uint64_t storageBytesUsed = 8; + for (uint32_t queryIndex = 0; queryIndex < oracleQueryCount; ++queryIndex) + { + const OracleQueryMetadata& oqm = queries[queryIndex]; + ASSERT(oqm.interfaceIndex < OI::oracleInterfacesCount); + ASSERT(oqm.queryId); + uint32_t idx = 0; + ASSERT(queryIdToIndex->get(oqm.queryId, idx) && idx == queryIndex); + ASSERT(oqm.queryTick <= system.tick); + ASSERT(oqm.queryId >> 31 == oqm.queryTick); + ASSERT(oqm.timeout.isValid()); + switch (oqm.type) + { + case ORACLE_QUERY_TYPE_CONTRACT_QUERY: + ASSERT(oqm.typeVar.contract.queryingContract > 0 && oqm.typeVar.contract.queryingContract < MAX_NUMBER_OF_CONTRACTS); + ASSERT(oqm.typeVar.contract.queryStorageOffset < queryStorageBytesUsed && oqm.typeVar.contract.queryStorageOffset > 0); + storageBytesUsed += OI::oracleInterfaces[oqm.interfaceIndex].querySize; + break; + case ORACLE_QUERY_TYPE_CONTRACT_SUBSCRIPTION: + // TODO + break; + case ORACLE_QUERY_TYPE_USER_QUERY: + ASSERT(!isZero(oqm.typeVar.user.queryingEntity)); + ASSERT(oqm.typeVar.user.queryTxIndex < NUMBER_OF_TRANSACTIONS_PER_TICK); + // TODO: check vs tick storage? + break; + default: +#if !defined(NDEBUG) && !defined(NO_UEFI) + CHAR16* dbgMsgBuf = (CHAR16*)scratchpad.ptr; + setText(dbgMsgBuf, L"Unexpected oracle query type "); + appendNumber(dbgMsgBuf, oqm.status, FALSE); + addDebugMessage(dbgMsgBuf); +#endif + break; + } + + // shared status checks + const ReplyState* replyState = nullptr; + uint16_t agreeingCommits = 0; + switch (oqm.status) + { + case ORACLE_QUERY_STATUS_PENDING: + case ORACLE_QUERY_STATUS_COMMITTED: + ASSERT(oqm.statusVar.pending.replyStateIndex < MAX_SIMULTANEOUS_ORACLE_QUERIES); + replyState = replyStates + oqm.statusVar.pending.replyStateIndex; + ASSERT(replyState->queryId == oqm.queryId); + ASSERT(replyState->ownReplySize == 0 || replyState->ownReplySize == OI::oracleInterfaces[oqm.interfaceIndex].replySize); + ASSERT(replyState->mostCommitsHistIdx < NUMBER_OF_COMPUTORS); + agreeingCommits = replyState->replyCommitHistogramCount[replyState->mostCommitsHistIdx]; + ASSERT(agreeingCommits <= replyState->totalCommits); + ASSERT(replyState->totalCommits <= NUMBER_OF_COMPUTORS); + break; + case ORACLE_QUERY_STATUS_UNRESOLVABLE: + case ORACLE_QUERY_STATUS_TIMEOUT: + ASSERT(oqm.statusVar.failure.agreeingCommits < QUORUM); + ASSERT(oqm.statusVar.failure.totalCommits <= NUMBER_OF_COMPUTORS); + ASSERT(oqm.statusVar.failure.agreeingCommits <= oqm.statusVar.failure.totalCommits); + break; + } + + // specific status checks + switch (oqm.status) + { + case ORACLE_QUERY_STATUS_PENDING: + ++pendingCount; + ASSERT(agreeingCommits < QUORUM); + break; + case ORACLE_QUERY_STATUS_COMMITTED: + ++committedCount; + ASSERT(agreeingCommits >= QUORUM); + break; + case ORACLE_QUERY_STATUS_SUCCESS: + ++successCount; + ASSERT(oqm.statusVar.success.revealTick <= system.tick); + ASSERT(oqm.statusVar.success.revealTxIndex < NUMBER_OF_TRANSACTIONS_PER_TICK); + break; + case ORACLE_QUERY_STATUS_UNRESOLVABLE: + ++unresolvableCount; + ASSERT(oqm.statusVar.failure.totalCommits - oqm.statusVar.failure.agreeingCommits > NUMBER_OF_COMPUTORS - QUORUM); + break; + case ORACLE_QUERY_STATUS_TIMEOUT: + ++timeoutCount; + break; + default: +#if !defined(NDEBUG) && !defined(NO_UEFI) + CHAR16* dbgMsgBuf = (CHAR16*)scratchpad.ptr; + setText(dbgMsgBuf, L"Unexpected oracle query state "); + appendNumber(dbgMsgBuf, oqm.status, FALSE); + addDebugMessage(dbgMsgBuf); +#endif + break; + } + } + for (uint32_t queryIndex = oracleQueryCount; queryIndex < MAX_ORACLE_QUERIES; ++queryIndex) + { + const OracleQueryMetadata& oqm = queries[queryIndex]; + ASSERT(!oqm.queryId); + ASSERT(!oqm.status); + } + + ASSERT(queryStorageBytesUsed == storageBytesUsed); + ASSERT(oracleQueryCount == pendingCount + committedCount + successCount + timeoutCount + unresolvableCount); + ASSERT(committedCount == stats.commitCount); + ASSERT(committedCount == pendingRevealReplyStateIndices.numValues); + ASSERT(pendingCount + committedCount == pendingQueryIndices.numValues); + ASSERT(successCount == stats.successCount); + ASSERT(timeoutCount == stats.timeoutNoCommitCount + stats.timeoutNoReplyCount + stats.timeoutNoReplyCount); + ASSERT(unresolvableCount == stats.unresolvableCount); + + // check pending queries index + uint32_t queryIdxCount = pendingQueryIndices.numValues; + const uint32_t* queryIndices = pendingQueryIndices.values; + for (uint32_t i = 0; i < queryIdxCount; ++i) + { + const uint32_t queryIndex = queryIndices[i]; + ASSERT(queryIndex < oracleQueryCount); + const OracleQueryMetadata& oqm = queries[queryIndex]; + ASSERT(oqm.status == ORACLE_QUERY_STATUS_PENDING || oqm.status == ORACLE_QUERY_STATUS_COMMITTED); + } + + // check index of reply states with pending reply commit quroum + uint32_t replyIdxCount = pendingCommitReplyStateIndices.numValues; + const uint32_t* replyIndices = pendingCommitReplyStateIndices.values; + for (uint32_t idx = 0; idx < replyIdxCount; ++idx) + { + const uint32_t replyIdx = replyIndices[idx]; + ASSERT(replyIdx < MAX_SIMULTANEOUS_ORACLE_QUERIES); + const ReplyState& replyState = replyStates[replyIdx]; + ASSERT(replyState.queryId); + uint32_t queryIndex = 0; + ASSERT(queryIdToIndex->get(replyState.queryId, queryIndex) && queryIndex < oracleQueryCount); + const OracleQueryMetadata& oqm = queries[queryIndex]; + ASSERT(oqm.status == ORACLE_QUERY_STATUS_PENDING); + } + + // check index of reply states with pending reply reveal tx + replyIdxCount = pendingRevealReplyStateIndices.numValues; + replyIndices = pendingRevealReplyStateIndices.values; + for (uint32_t idx = 0; idx < replyIdxCount; ++idx) + { + const uint32_t replyIdx = replyIndices[idx]; + ASSERT(replyIdx < MAX_SIMULTANEOUS_ORACLE_QUERIES); + const ReplyState& replyState = replyStates[replyIdx]; + ASSERT(replyState.queryId); + uint32_t queryIndex = 0; + ASSERT(queryIdToIndex->get(replyState.queryId, queryIndex) && queryIndex < oracleQueryCount); + const OracleQueryMetadata& oqm = queries[queryIndex]; + ASSERT(oqm.status == ORACLE_QUERY_STATUS_COMMITTED); + } + + // check index of pending notifications + queryIdxCount = notificationQueryIndicies.numValues; + queryIndices = notificationQueryIndicies.values; + for (uint32_t i = 0; i < queryIdxCount; ++i) + { + const uint32_t queryIndex = queryIndices[i]; + ASSERT(queryIndex < oracleQueryCount); + const OracleQueryMetadata& oqm = queries[queryIndex]; + ASSERT(oqm.status != ORACLE_QUERY_STATUS_PENDING); + } + +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"End oracleEngine.checkStateConsistencyWithAssert()"); +#endif + } + void logStatus(CHAR16* message) const { auto appendQuotientWithOneDecimal = [](CHAR16* message, uint64_t dividend, uint64_t divisor) diff --git a/src/oracle_core/snapshot_files.h b/src/oracle_core/snapshot_files.h index f7cd2c091..30e17d9d6 100644 --- a/src/oracle_core/snapshot_files.h +++ b/src/oracle_core/snapshot_files.h @@ -123,6 +123,11 @@ bool OracleEngine::loadSnapshot(unsigned short epoch, CHA copyMemory(pendingRevealReplyStateIndices, engineData.pendingRevealReplyStateIndices); copyMemory(notificationQueryIndicies, engineData.notificationQueryIndicies); copyMemory(stats, engineData.stats); + if (oracleQueryCount > MAX_ORACLE_QUERIES || queryStorageBytesUsed > ORACLE_QUERY_STORAGE_SIZE) + { + logToConsole(L"Oracle engine data is invalid!"); + return false; + } logToConsole(L"Loading oracle query metadata ..."); @@ -133,6 +138,11 @@ bool OracleEngine::loadSnapshot(unsigned short epoch, CHA logToConsole(L"Failed to load oracle query metadata!"); return false; } + if (oracleQueryCount < MAX_ORACLE_QUERIES) + { + unsigned long long sizeToZero = (MAX_ORACLE_QUERIES - oracleQueryCount) * sizeof(*queries); + setMem(queries + oracleQueryCount, sizeToZero, 0); + } logToConsole(L"Loading oracle query data storage ..."); sizeToLoad = queryStorageBytesUsed; @@ -143,6 +153,11 @@ bool OracleEngine::loadSnapshot(unsigned short epoch, CHA logToConsole(L"Failed to load oracle query data storage!"); return false; } + if (queryStorageBytesUsed < ORACLE_QUERY_STORAGE_SIZE) + { + unsigned long long sizeToZero = ORACLE_QUERY_STORAGE_SIZE - queryStorageBytesUsed; + setMem(queryStorage + queryStorageBytesUsed, sizeToZero, 0); + } logToConsole(L"Loading oracle reply states ..."); sizeToLoad = sizeof(ReplyState) * MAX_SIMULTANEOUS_ORACLE_QUERIES; @@ -159,6 +174,7 @@ bool OracleEngine::loadSnapshot(unsigned short epoch, CHA queryIdToIndex->set(queries[queryIndex].queryId, queryIndex); logToConsole(L"Successfully loaded all oracle engine data from snapshot!"); + return true; } #else diff --git a/src/qubic.cpp b/src/qubic.cpp index 4475a5eea..8bb0e9cf2 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -4354,7 +4354,10 @@ static bool saveAllNodeStates() return false; } - if (oracleEngine.saveSnapshot(system.epoch, directory) != 0) +#if !defined(NDEBUG) + oracleEngine.checkStateConsistencyWithAssert(); +#endif + if (!oracleEngine.saveSnapshot(system.epoch, directory)) { return false; } @@ -4522,6 +4525,14 @@ static bool loadAllNodeStates() return false; } + if (!oracleEngine.loadSnapshot(system.epoch, directory)) + { + return false; + } +#if !defined(NDEBUG) + oracleEngine.checkStateConsistencyWithAssert(); +#endif + #if ADDON_TX_STATUS_REQUEST if (!loadStateTxStatus(numberOfTransactions, directory)) { @@ -5875,7 +5886,9 @@ static bool initialize() { return false; } - + + if (!oracleEngine.init(computorPublicKeys)) + return false; #if ADDON_TX_STATUS_REQUEST if (!initTxStatusRequestAddOn()) @@ -6018,9 +6031,6 @@ static bool initialize() } } - if (!oracleEngine.init(computorPublicKeys)) - return false; - #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES increaseEnergy(id(TESTEXC_CONTRACT_INDEX, 0, 0, 0), 100000000llu); #endif From fbfcf6983c7d5a9acd61fd1bc9fc0b001b753e1f Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:59:11 +0100 Subject: [PATCH 71/90] Improve output + checks (mainly snapshots) --- src/oracle_core/oracle_engine.h | 17 ++++++++--------- src/qubic.cpp | 23 +++++++++++++++++++---- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index e34bbd334..c4ab943b8 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -1456,10 +1456,11 @@ class OracleEngine { #if !defined(NDEBUG) && !defined(NO_UEFI) addDebugMessage(L"Begin oracleEngine.checkStateConsistencyWithAssert()"); + forceLogToConsoleAsAddDebugMessage = true; + logStatus(); + forceLogToConsoleAsAddDebugMessage = false; __ScopedScratchpad scratchpad(1000, /*initZero=*/true); CHAR16* dbgMsgBuf = (CHAR16*)scratchpad.ptr; - logStatus(dbgMsgBuf); - addDebugMessage(dbgMsgBuf); #endif ASSERT(queries); @@ -1505,7 +1506,6 @@ class OracleEngine break; default: #if !defined(NDEBUG) && !defined(NO_UEFI) - CHAR16* dbgMsgBuf = (CHAR16*)scratchpad.ptr; setText(dbgMsgBuf, L"Unexpected oracle query type "); appendNumber(dbgMsgBuf, oqm.status, FALSE); addDebugMessage(dbgMsgBuf); @@ -1562,7 +1562,6 @@ class OracleEngine break; default: #if !defined(NDEBUG) && !defined(NO_UEFI) - CHAR16* dbgMsgBuf = (CHAR16*)scratchpad.ptr; setText(dbgMsgBuf, L"Unexpected oracle query state "); appendNumber(dbgMsgBuf, oqm.status, FALSE); addDebugMessage(dbgMsgBuf); @@ -1579,12 +1578,12 @@ class OracleEngine ASSERT(queryStorageBytesUsed == storageBytesUsed); ASSERT(oracleQueryCount == pendingCount + committedCount + successCount + timeoutCount + unresolvableCount); - ASSERT(committedCount == stats.commitCount); - ASSERT(committedCount == pendingRevealReplyStateIndices.numValues); - ASSERT(pendingCount + committedCount == pendingQueryIndices.numValues); + ASSERT(committedCount == pendingRevealReplyStateIndices.numValues); // currently in committed state + ASSERT(pendingCount + committedCount == pendingQueryIndices.numValues); // not finished (no success / failure) ASSERT(successCount == stats.successCount); - ASSERT(timeoutCount == stats.timeoutNoCommitCount + stats.timeoutNoReplyCount + stats.timeoutNoReplyCount); + ASSERT(timeoutCount == stats.timeoutNoReplyCount + stats.timeoutNoCommitCount + stats.timeoutNoRevealCount); ASSERT(unresolvableCount == stats.unresolvableCount); + ASSERT(committedCount + successCount + stats.timeoutNoRevealCount == stats.commitCount); // check pending queries index uint32_t queryIdxCount = pendingQueryIndices.numValues; @@ -1643,7 +1642,7 @@ class OracleEngine #endif } - void logStatus(CHAR16* message) const + void logStatus() const { auto appendQuotientWithOneDecimal = [](CHAR16* message, uint64_t dividend, uint64_t divisor) { diff --git a/src/qubic.cpp b/src/qubic.cpp index 8bb0e9cf2..62ccb128b 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -4206,6 +4206,10 @@ static bool saveAllNodeStates() { PROFILE_SCOPE(); +#if !defined(NDEBUG) + forceLogToConsoleAsAddDebugMessage = true; +#endif + CHAR16 directory[16]; setText(directory, L"ep"); appendNumber(directory, system.epoch, false); @@ -4372,11 +4376,20 @@ static bool saveAllNodeStates() #if ENABLED_LOGGING logger.saveCurrentLoggingStates(directory); #endif + +#if !defined(NDEBUG) + forceLogToConsoleAsAddDebugMessage = false; +#endif + return true; } static bool loadAllNodeStates() { +#if !defined(NDEBUG) + forceLogToConsoleAsAddDebugMessage = true; +#endif + CHAR16 directory[16]; setText(directory, L"ep"); appendNumber(directory, system.epoch, false); @@ -4545,6 +4558,11 @@ static bool loadAllNodeStates() logToConsole(L"Loading old logger..."); logger.loadLastLoggingStates(directory); #endif + +#if !defined(NDEBUG) + forceLogToConsoleAsAddDebugMessage = false; +#endif + return true; } @@ -6510,10 +6528,7 @@ static void logInfo() logToConsole(message); -#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES - oracleEngine.logStatus(message); - //logToConsole(message); -#endif + oracleEngine.logStatus(); } static void logHealthStatus() From 7ee406e23d17841430bb2135d22e30d19be085b4 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:00:31 +0100 Subject: [PATCH 72/90] Disable some debug output of pendingTxsPool --- src/ticking/pending_txs_pool.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ticking/pending_txs_pool.h b/src/ticking/pending_txs_pool.h index b419dd2f0..06ca2c06a 100644 --- a/src/ticking/pending_txs_pool.h +++ b/src/ticking/pending_txs_pool.h @@ -310,7 +310,7 @@ class PendingTxsPool txAdded = true; } -#if !defined(NDEBUG) && !defined(NO_UEFI) +#if !defined(NDEBUG) && !defined(NO_UEFI) && 0 else { CHAR16 dbgMsgBuf[300]; From 2f0a2e59d8129c783032cc4c4613f1558474c658 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:53:33 +0100 Subject: [PATCH 73/90] Fix name clash in FourQ --- src/four_q.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/four_q.h b/src/four_q.h index 3e65680ac..9b23a15ba 100644 --- a/src/four_q.h +++ b/src/four_q.h @@ -42,11 +42,14 @@ #define C4 0x6BC57DEF56CE8877 #ifdef __AVX512F__ +namespace FourQ +{ static __m256i B1, B2, B3, B4, C; +} static void initAVX512FourQConstants() { - + using namespace FourQ; B1 = _mm256_set_epi64x(B14, B13, B12, B11); B2 = _mm256_set_epi64x(B24, B23, B22, B21); B3 = _mm256_set_epi64x(B34, B33, B32, B31); @@ -1232,6 +1235,7 @@ static void decompose(unsigned long long* k, unsigned long long* scalars) const unsigned long long a4 = mul_truncate(k, (unsigned long long*)ell4); #ifdef __AVX512F__ + using namespace FourQ; * ((__m256i*)scalars) = _mm256_add_epi64(_mm256_add_epi64(_mm256_add_epi64(_mm256_add_epi64(_mm256_mullo_epi64(_mm256_set1_epi64x(a1), B1), _mm256_mullo_epi64(_mm256_set1_epi64x(a2), B2)), _mm256_mullo_epi64(_mm256_set1_epi64x(a3), B3)), _mm256_mullo_epi64(_mm256_set1_epi64x(a4), B4)), C); if (!((scalars[0] += k[0]) & 1)) { From faac34c76aa56245643de7a6ba5f0d28bca46388 Mon Sep 17 00:00:00 2001 From: cyber-pc <165458555+cyber-pc@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:32:19 +0700 Subject: [PATCH 74/90] Improve connection between node and OM. (#725) * Improve connection between node and OM. * Fix missing debug log. --- src/network_core/peers.h | 219 +++++++++++++++++++++++++++++++-------- src/network_core/tcp4.h | 1 + src/qubic.cpp | 28 ++++- 3 files changed, 205 insertions(+), 43 deletions(-) diff --git a/src/network_core/peers.h b/src/network_core/peers.h index 78d89267b..a7bdaf43f 100644 --- a/src/network_core/peers.h +++ b/src/network_core/peers.h @@ -34,10 +34,20 @@ #define NUMBER_OF_PUBLIC_PEERS_TO_KEEP 10 #define NUMBER_OF_WHITE_LIST_PEERS sizeof(whiteListPeers) / sizeof(whiteListPeers[0]) #define NUMBER_OF_INCOMING_CONNECTIONS_RESERVED_FOR_WHITELIST_IPS 16 + +// OM related setting +static constexpr unsigned int ORACLE_MACHINE_CONNECTION_TIMEOUT_SECS = 15; // Config timout for connecting attemp to OM +static constexpr unsigned int ORACLE_MACHINE_TRANSMITING_TIMEOUT_SECS = 30; // Transmitting timeout to OM +static constexpr unsigned int ORACLE_MACHINE_GRACEFULL_CLOSE_RETIRES = 3; // Gracefull close retries for connecting attemp to OM +static constexpr unsigned long long OM_RECONNECT_COOLDOWN_SECS = 5; + static_assert((NUMBER_OF_INCOMING_CONNECTIONS / NUMBER_OF_REGULAR_OUTGOING_CONNECTIONS) >= 11, "Number of incoming connections must be x11+ number of outgoing connections to keep healthy network"); static volatile bool listOfPeersIsStatic = false; +#define OM_RETRY_COUNT(dejavu) ((dejavu) >> 24) +#define OM_SET_RETRY_COUNT(dejavu, count) (((dejavu) & 0x00FFFFFF) | ((count) << 24)) +#define OM_MAX_RETRIES 3 struct Peer { @@ -61,6 +71,10 @@ struct Peer // Indicate the peer is OM connection type which is a subtype of outgoing connection BOOLEAN isOMNode; + unsigned long long connectionStartTime; + unsigned long long lastOMActivityTime; + unsigned long long omTransmitStartTime; + unsigned long long lastOMCloseTime; // Extra data to determine if this peer is a fullnode // Note: an **active fullnode** is a peer that is able to reply valid tick data, tick vote to this node after getting requested @@ -118,7 +132,13 @@ struct Peer exchangedPublicPeers = FALSE; isClosing = FALSE; isIncommingConnection = FALSE; + isOMNode = FALSE; + lastOMActivityTime = 0; + connectionStartTime = 0; + omTransmitStartTime = 0; + lastOMCloseTime = 0; + dataToTransmitSize = 0; lastActiveTick = 0; trackRequestedCounter = 0; @@ -215,7 +235,7 @@ static bool isPrivateIp(const unsigned char address[4]) return false; } -static void closePeer(Peer* peer) +static void closePeer(Peer* peer, int closeGracefullyRetries = 0) { PROFILE_SCOPE(); ASSERT(isMainProcessor()); @@ -223,21 +243,45 @@ static void closePeer(Peer* peer) { if (!peer->isClosing) { - EFI_STATUS status; - if (status = peer->tcp4Protocol->Configure(peer->tcp4Protocol, NULL)) + peer->isClosing = TRUE; + if (peer->isOracleMachineNode()) { - logStatusToConsole(L"EFI_TCP4_PROTOCOL.Configure() fails", status, __LINE__); + // Track close time for OM nodes to enable reconnection cooldown + peer->lastOMCloseTime = __rdtsc(); } - peer->isClosing = TRUE; - } + EFI_STATUS status = EFI_SUCCESS; + // Decide to close gracefully with Close() + if (closeGracefullyRetries > 0) + { + EFI_TCP4_CLOSE_TOKEN closeToken; + bs->SetMem(&closeToken, sizeof(EFI_TCP4_CLOSE_TOKEN), 0); + status = bs->CreateEvent(0, TPL_CALLBACK, NULL, NULL, &closeToken.CompletionToken.Event); + closeToken.AbortOnClose = TRUE; // quickly close by send RST then don't care anymore + status = peer->tcp4Protocol->Close(peer->tcp4Protocol, &closeToken); + if (status == EFI_SUCCESS) + { + // Poll a few times (non-blocking) - hopefully RST will arrive the + for (int i = 0; i < closeGracefullyRetries; i++) + { + peer->tcp4Protocol->Poll(peer->tcp4Protocol); + if (bs->CheckEvent(closeToken.CompletionToken.Event) != EFI_NOT_READY) + { + break; + } + } + bs->CloseEvent(closeToken.CompletionToken.Event); + } + else + { + logStatusToConsole(L"EFI_TCP4_PROTOCOL.Close() fails", status, __LINE__); + } + } - // For Oracle machine, forces closing so that can reconnect faster - if (peer->isOracleMachineNode() && peer->isClosing) - { - peer->isConnectingAccepting = FALSE; - peer->isReceiving = FALSE; - peer->isTransmitting = FALSE; + if (status = peer->tcp4Protocol->Configure(peer->tcp4Protocol, NULL)) + { + logStatusToConsole(L"EFI_TCP4_PROTOCOL.Configure() fails", status, __LINE__); + } } if (!peer->isConnectingAccepting && !peer->isReceiving && !peer->isTransmitting) @@ -255,7 +299,20 @@ static void closePeer(Peer* peer) numberOfAcceptedIncommingConnection--; ASSERT(numberOfAcceptedIncommingConnection >= 0); } + + // Save OM close time before reset + unsigned long long savedOMCloseTime = peer->lastOMCloseTime; + bool wasOMNode = peer->isOracleMachineNode(); + unsigned int peerIndex = (unsigned int)(peer - peers); + peer->reset(); + + if (wasOMNode) + { + peer->lastOMCloseTime = savedOMCloseTime; + peer->isOMNode = TRUE; + peer->address = omIPv4Address[peerIndex - NUMBER_OF_REGULAR_OUTGOING_CONNECTIONS]; + } } } } @@ -367,35 +424,6 @@ static void pushToFullNodes(RequestResponseHeader* requestResponseHeader, int nu } } -static void pushToOracleMachineNodes(RequestResponseHeader* requestResponseHeader) -{ -#if !defined(NDEBUG) - // TODO: cleanup this debug code when OM connection is fully stable - setText(::message, L"pushToOracleMachineNodes(): "); -#endif - if (NUMBER_OF_OM_NODE_CONNECTIONS > 0) - { - unsigned short numberOfSuitablePeers = 0; - for (unsigned int i = 0; i < NUMBER_OF_OUTGOING_CONNECTIONS + NUMBER_OF_INCOMING_CONNECTIONS && numberOfSuitablePeers < NUMBER_OF_OM_NODE_CONNECTIONS; i++) - { - if (peers[i].isOracleMachineNode() - && peers[i].tcp4Protocol - && peers[i].isConnectedAccepted - && !peers[i].isClosing) - { -#if !defined(NDEBUG) - appendIPv4Address(::message, peers[i].address); -#endif - push(&peers[i], requestResponseHeader); - numberOfSuitablePeers++; - } - } - } -#if !defined(NDEBUG) - addDebugMessage(::message); -#endif -} - // Add message to response queue of specific peer. If peer is NULL, it will be sent to random peers. Can be called from any thread. static void enqueueResponse(Peer* peer, RequestResponseHeader* responseHeader) { @@ -424,6 +452,65 @@ static void enqueueResponse(Peer* peer, RequestResponseHeader* responseHeader) RELEASE(responseQueueHeadLock); } + +static void pushToOracleMachineNodes(RequestResponseHeader* requestResponseHeader) +{ +#if !defined(NDEBUG) + // TODO: cleanup this debug code when OM connection is fully stable + setText(::message, L"pushToOracleMachineNodes(): "); +#endif + if (NUMBER_OF_OM_NODE_CONNECTIONS > 0) + { + bool pushedToAny = false; + unsigned short numberOfSuitablePeers = 0; + + // Heuristic pick the first byte of dejavu for tracking the retries + // Clear the dejavu retry if first bytes is greater than OM_MAX_RETRIES + // Incase of first bytes is 1 .. OM_MAX_RETRIES, we retrie fewer + // Currently is 0 when creating a querry, so it is not a problem + unsigned int currentDejavu = requestResponseHeader->dejavu(); + if (OM_RETRY_COUNT(currentDejavu) > OM_MAX_RETRIES) + { + requestResponseHeader->setDejavu(currentDejavu & 0x00FFFFFF); + } + + for (unsigned int i = 0; i < NUMBER_OF_OUTGOING_CONNECTIONS + NUMBER_OF_INCOMING_CONNECTIONS && numberOfSuitablePeers < NUMBER_OF_OM_NODE_CONNECTIONS; i++) + { + if (peers[i].isOracleMachineNode()) + { + if (peers[i].tcp4Protocol + && peers[i].isConnectedAccepted + && !peers[i].isClosing) + { +#if !defined(NDEBUG) + appendIPv4Address(::message, peers[i].address); +#endif + push(&peers[i], requestResponseHeader); + numberOfSuitablePeers++; + pushedToAny = true; + } + } + } + + // Re-enqueue if no OM peer was ready. + if (!pushedToAny) + { + unsigned int retryCount = OM_RETRY_COUNT(requestResponseHeader->dejavu()); + if (retryCount < OM_MAX_RETRIES) + { + requestResponseHeader->setDejavu( + OM_SET_RETRY_COUNT(requestResponseHeader->dejavu(), retryCount + 1)); + enqueueResponse((Peer*)1, requestResponseHeader); + } + } + } +#if !defined(NDEBUG) + addDebugMessage(::message); +#endif + +} + + // Add message to response queue of specific peer. If peer is NULL, it will be sent to random peers. Can be called from any thread. static void enqueueResponse(Peer* peer, unsigned int dataSize, unsigned char type, unsigned int dejavu, const void* data) { @@ -618,7 +705,9 @@ static bool peerConnectionNewlyEstablished(unsigned int i) // connection rejected peers[i].connectAcceptToken.CompletionToken.Status = -1; penalizePublicPeerRejectedConnection(peers[i].address); + closePeer(&peers[i]); + } else { @@ -630,6 +719,10 @@ static bool peerConnectionNewlyEstablished(unsigned int i) else { peers[i].isConnectedAccepted = TRUE; + if (peers[i].isOracleMachineNode()) + { + peers[i].lastOMActivityTime = __rdtsc(); + } } } } @@ -882,6 +975,19 @@ static void processTransmittedData(unsigned int i, unsigned int salt) if (((unsigned long long)peers[i].tcp4Protocol) > 1) { + // Special treatment for OM, make sure we close peer when the transmitting is stucked + if (peers[i].isOracleMachineNode() && peers[i].isTransmitting && peers[i].omTransmitStartTime > 0) + { + unsigned long long elapsedSecs = (__rdtsc() - peers[i].omTransmitStartTime) / frequency; + if (elapsedSecs > ORACLE_MACHINE_TRANSMITING_TIMEOUT_SECS) + { + peers[i].isTransmitting = FALSE; + peers[i].omTransmitStartTime = 0; // mark as invalid + closePeer(&peers[i]); + return; // Exit early + } + } + // check if transmission is completed if (peers[i].transmitToken.CompletionToken.Status != -1) { @@ -933,12 +1039,15 @@ static void transmitData(unsigned int i, unsigned int salt) if (status = peers[i].tcp4Protocol->Transmit(peers[i].tcp4Protocol, &peers[i].transmitToken)) { logStatusToConsole(L"EFI_TCP4_PROTOCOL.Transmit() fails", status, __LINE__); - closePeer(&peers[i]); } else { peers[i].isTransmitting = TRUE; + if (peers[i].isOracleMachineNode()) + { + peers[i].omTransmitStartTime = __rdtsc(); + } } } } @@ -967,10 +1076,15 @@ static void peerReconnectIfInactive(unsigned int i, unsigned short port) EFI_STATUS status; if (!peers[i].tcp4Protocol) { + // Save OM close time before reset (for cooldown to work) + unsigned long long savedOMCloseTime = peers[i].lastOMCloseTime; + peers[i].reset(); // peer slot without active connection if (i < NUMBER_OF_OUTGOING_CONNECTIONS) { + unsigned int connectionTimeout = 0; + bool experimentSetting = false; // outgoing connection: peers[i].isIncommingConnection = FALSE; // Check if this slot is for OM node @@ -978,6 +1092,23 @@ static void peerReconnectIfInactive(unsigned int i, unsigned short port) { peers[i].isOMNode = TRUE; peers[i].address = omIPv4Address[i - NUMBER_OF_REGULAR_OUTGOING_CONNECTIONS]; + + // Restore OM close time for cooldown check + peers[i].lastOMCloseTime = savedOMCloseTime; + + // Mark it as failure + peers[i].connectAcceptToken.CompletionToken.Status = -1; + + // Reconnection cooldown: prevent rapid reconnect cycles of the OM + if (peers[i].lastOMCloseTime > 0) + { + unsigned long long elapsedSecs = (__rdtsc() - peers[i].lastOMCloseTime) / frequency; + if (elapsedSecs < OM_RECONNECT_COOLDOWN_SECS) + { + return; // Still in cooldown, don't attempt reconnection yet + } + } + } else { @@ -1014,6 +1145,10 @@ static void peerReconnectIfInactive(unsigned int i, unsigned short port) else { peers[i].isConnectingAccepting = TRUE; + if (peers[i].isOracleMachineNode()) + { + peers[i].connectionStartTime = __rdtsc(); + } } } else diff --git a/src/network_core/tcp4.h b/src/network_core/tcp4.h index bdfacfc48..292f50261 100644 --- a/src/network_core/tcp4.h +++ b/src/network_core/tcp4.h @@ -60,6 +60,7 @@ static EFI_HANDLE getTcp4Protocol(const unsigned char* remoteAddress, const unsi option.SendBufferSize = BUFFER_SIZE; option.KeepAliveProbes = 1; option.EnableWindowScaling = TRUE; + configData.ControlOption = &option; if ((status = (*tcp4Protocol)->Configure(*tcp4Protocol, &configData)) diff --git a/src/qubic.cpp b/src/qubic.cpp index 62ccb128b..8de0742d9 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -378,6 +378,7 @@ static void logToConsole(const CHAR16* message) #endif } + static inline bool isMainMode() { return (mainAuxStatus & 1) == 1; @@ -1818,6 +1819,7 @@ static void processOracleMachineReply(Peer* peer, RequestResponseHeader* header) if (header->size() >= sizeof(RequestResponseHeader) + sizeof(OracleMachineReply)) { oracleEngine.processOracleMachineReply(msg, header->getPayloadSize()); + peer->lastOMActivityTime = __rdtsc(); } } @@ -7386,6 +7388,27 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) // reconnect if this peer slot has no active connection peerReconnectIfInactive(i, PORT); + + if (peers[i].isOracleMachineNode()) + { + if (ORACLE_MACHINE_CONNECTION_TIMEOUT_SECS > 0 + && peers[i].connectionStartTime > 0 + && peers[i].isConnectingAccepting + && ((__rdtsc() - peers[i].connectionStartTime) / frequency > ORACLE_MACHINE_CONNECTION_TIMEOUT_SECS)) + { + closePeer(&peers[i], ORACLE_MACHINE_GRACEFULL_CLOSE_RETIRES); + } + + constexpr unsigned long long OM_INACTIVITY_TIMEOUT_SECS = 300; // 5 minutes + if (peers[i].isConnectedAccepted && + !peers[i].isClosing && + peers[i].lastOMActivityTime > 0 && + ((__rdtsc() - peers[i].lastOMActivityTime) / frequency > OM_INACTIVITY_TIMEOUT_SECS)) + { + closePeer(&peers[i], ORACLE_MACHINE_GRACEFULL_CLOSE_RETIRES); + } + } + } #if !TICK_STORAGE_AUTOSAVE_MODE @@ -7569,7 +7592,10 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) forceRefreshPeerList = false; for (unsigned int i = 0; i < NUMBER_OF_OUTGOING_CONNECTIONS + NUMBER_OF_INCOMING_CONNECTIONS; i++) { - closePeer(&peers[i]); + if (!peers[i].isOracleMachineNode()) + { + closePeer(&peers[i]); + } } } From 78c526d7de482a81a3297bcdd5f8ce1ae16a8466 Mon Sep 17 00:00:00 2001 From: Franziska Mueller <11660876+Franziska-Mueller@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:03:15 +0100 Subject: [PATCH 75/90] remove all toggles for code that depended on proposals --- src/Qubic.vcxproj | 1 - src/Qubic.vcxproj.filters | 3 --- src/contract_core/contract_def.h | 40 +++---------------------------- src/contracts/SupplyWatcher_old.h | 30 ----------------------- src/qubic.cpp | 11 --------- 5 files changed, 3 insertions(+), 82 deletions(-) delete mode 100644 src/contracts/SupplyWatcher_old.h diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index f4cfb3115..db159d520 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -46,7 +46,6 @@ - diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index e80136b24..7925cb33b 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -336,9 +336,6 @@ contracts - - contracts - diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 91bc2267f..29988c27a 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -79,11 +79,7 @@ #define CONTRACT_INDEX SWATCH_CONTRACT_INDEX #define CONTRACT_STATE_TYPE SWATCH #define CONTRACT_STATE2_TYPE SWATCH2 -#ifdef OLD_SWATCH -#include "contracts/SupplyWatcher_old.h" -#else #include "contracts/SupplyWatcher.h" -#endif #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE @@ -215,54 +211,36 @@ #define CONTRACT_STATE2_TYPE QRWA2 #include "contracts/qRWA.h" -#ifndef NO_QRP - -constexpr unsigned short qrpContractIndex = CONTRACT_INDEX + 1; - #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QRP_CONTRACT_INDEX qrpContractIndex +#define QRP_CONTRACT_INDEX 21 #define CONTRACT_INDEX QRP_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QRP #define CONTRACT_STATE2_TYPE QRP2 #include "contracts/QReservePool.h" -#endif // NO_QRP - -#ifndef NO_QTF - -constexpr unsigned short qtfContractIndex = CONTRACT_INDEX + 1; - #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QTF_CONTRACT_INDEX qtfContractIndex +#define QTF_CONTRACT_INDEX 22 #define CONTRACT_INDEX QTF_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QTF #define CONTRACT_STATE2_TYPE QTF2 #include "contracts/QThirtyFour.h" -#endif // NO_QTF - -#ifndef NO_QDUEL - -constexpr unsigned short qduelContractIndex = CONTRACT_INDEX + 1; - #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QDUEL_CONTRACT_INDEX qduelContractIndex +#define QDUEL_CONTRACT_INDEX 23 #define CONTRACT_INDEX QDUEL_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QDUEL #define CONTRACT_STATE2_TYPE QDUEL2 #include "contracts/QDuel.h" -#endif // NO_QDUEL - // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -371,15 +349,9 @@ constexpr struct ContractDescription {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 -#ifndef NO_QRP {"QRP", 199, 10000, sizeof(IPO)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 -#endif -#ifndef NO_QTF {"QTF", 199, 10000, sizeof(QTF)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 -#endif -#ifndef NO_QDUEL {"QDUEL", 199, 10000, sizeof(QDUEL)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 -#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA)}, @@ -496,15 +468,9 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRWA); -#ifndef NO_QRP REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRP); -#endif -#ifndef NO_QTF REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QTF); -#endif -#ifndef NO_QDUEL REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL); -#endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/contracts/SupplyWatcher_old.h b/src/contracts/SupplyWatcher_old.h deleted file mode 100644 index 087291c9e..000000000 --- a/src/contracts/SupplyWatcher_old.h +++ /dev/null @@ -1,30 +0,0 @@ -using namespace QPI; - -struct SWATCH2 -{ -}; - -struct SWATCH : public ContractBase -{ - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() - { - } - - struct BEGIN_EPOCH_locals - { - Entity ownEntity; - sint64 ownBalance; - }; - - BEGIN_EPOCH_WITH_LOCALS() - { - // Burn all coins of this contract. According to agreement of the quorum, a part of the - // computor revenue is donated to this contract for burning. - if (qpi.getEntity(SELF, locals.ownEntity)) - { - locals.ownBalance = locals.ownEntity.incomingAmount - locals.ownEntity.outgoingAmount; - if (locals.ownBalance > 0) - qpi.burn(locals.ownBalance); - } - } -}; diff --git a/src/qubic.cpp b/src/qubic.cpp index cbc2020c0..66722baa8 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,16 +1,5 @@ #define SINGLE_COMPILE_UNIT -// #define OLD_SWATCH -// #define NO_QRP -// #define NO_QTF -// #define NO_QDUEL - -// QTF in its current state is only usable with QRP. -// If the QRP proposal is rejected, disable QTF as well. -#if defined NO_QRP && !defined NO_QTF -#define NO_QTF -#endif - // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" From bf033093612befd24855718e791f8aafe8b9fcd4 Mon Sep 17 00:00:00 2001 From: cyber-pc <165458555+cyber-pc@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:34:01 +0700 Subject: [PATCH 76/90] Add ability to adjust multipler of each mining algo's solution. (#721) * Add ability to adjust multipler of each mining algo's solution. * Set ADDITION multiplier equal to HYPERIDENTITY. --- src/public_settings.h | 4 ++++ src/qubic.cpp | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/public_settings.h b/src/public_settings.h index 3df5ff134..d504c1bb4 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -104,6 +104,10 @@ static constexpr unsigned long long ADDITION_NUMBER_OF_MUTATIONS = 150; static constexpr unsigned long long ADDITION_POPULATION_THRESHOLD = ADDITION_NUMBER_OF_INPUT_NEURONS + ADDITION_NUMBER_OF_OUTPUT_NEURONS + ADDITION_NUMBER_OF_MUTATIONS; // P static constexpr unsigned int ADDITION_SOLUTION_THRESHOLD_DEFAULT = 74200; +// Multipler of score +static constexpr unsigned int HYPERIDENTITY_SOLUTION_MULTIPLER = 1; +static constexpr unsigned int ADDITION_SOLUTION_MULTIPLER = 1; + static constexpr long long NEURON_VALUE_LIMIT = 1LL; diff --git a/src/qubic.cpp b/src/qubic.cpp index 66722baa8..1e496cdc9 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -200,6 +200,11 @@ static unsigned long long K12MeasurementsCount = 0; static unsigned long long K12MeasurementsSum = 0; static volatile char minerScoreArrayLock = 0; static SpecialCommandGetMiningScoreRanking requestMiningScoreRanking; +static constexpr unsigned int gScoreMultiplier[score_engine::AlgoType::MaxAlgoCount] = +{ + HYPERIDENTITY_SOLUTION_MULTIPLER, // HyperIdentity + ADDITION_SOLUTION_MULTIPLER // Addition +}; // Custom mining related variables and constants static unsigned int gCustomMiningSharesCount[NUMBER_OF_COMPUTORS] = { 0 }; @@ -2634,7 +2639,7 @@ static void processTickTransactionSolution(const MiningSolutionTransaction* tran { if (transaction->sourcePublicKey == minerPublicKeys[minerIndex]) { - minerScores[minerIndex]++; + minerScores[minerIndex] += gScoreMultiplier[selectedAlgo]; break; } @@ -2643,7 +2648,7 @@ static void processTickTransactionSolution(const MiningSolutionTransaction* tran && numberOfMiners < MAX_NUMBER_OF_MINERS) { minerPublicKeys[numberOfMiners] = transaction->sourcePublicKey; - minerScores[numberOfMiners++] = 1; + minerScores[numberOfMiners++] = gScoreMultiplier[selectedAlgo]; } const m256i tmpPublicKey = minerPublicKeys[minerIndex]; From d87310722cd747fd2958ce2813195dfbf8b5beff Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:22:42 +0100 Subject: [PATCH 77/90] Implement RequestOracleData::requestAllQueryIdsByTick --- src/oracle_core/net_msg_impl.h | 25 +++++++++++- src/oracle_core/oracle_engine.h | 35 ++++++++++++++++ test/oracle_engine.cpp | 71 ++++++++++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index 74866d4d2..eec7d8747 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -14,7 +14,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R return; // prepare buffer - constexpr int maxQueryIdCount = 2; + constexpr int maxQueryIdCount = 2; // TODO: increase value after testing constexpr int payloadBufferSize = math_lib::max((int)math_lib::max(MAX_ORACLE_QUERY_SIZE, MAX_ORACLE_REPLY_SIZE), maxQueryIdCount * 8); static_assert(payloadBufferSize >= sizeof(RespondOracleDataQueryMetadata), "Buffer too small."); static_assert(payloadBufferSize < 32 * 1024, "Large alloc in stack may need reconsideration."); @@ -33,7 +33,27 @@ void OracleEngine::processRequestOracleData(Peer* peer, R case RequestOracleData::requestAllQueryIdsByTick: { - // TODO + // send query IDs of queries of a given tick, splitting the array in multiple messages if needed + if (request->reqTickOrId >= UINT32_MAX) + break; + const unsigned int tick = (unsigned int)request->reqTickOrId; + unsigned int queryIndex = findFirstQueryIndexOfTick(tick); + if (queryIndex == UINT32_MAX) + break; + response->resType = RespondOracleData::respondQueryIds; + bool moreQueries = true; + do + { + unsigned int idxInMsg = 0; + for (; idxInMsg < maxQueryIdCount && moreQueries; ++idxInMsg) + { + payloadQueryIds[idxInMsg] = queries[queryIndex].queryId; + ++queryIndex; + moreQueries = queryIndex < oracleQueryCount && queries[queryIndex].queryTick == tick; + } + enqueueResponse(peer, sizeof(RespondOracleData) + idxInMsg * 8, + RespondOracleData::type(), header->dejavu(), response); + } while (moreQueries); break; } @@ -51,6 +71,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R case RequestOracleData::requestPendingQueryIds: { + // send query IDs of pending queries, splitting the array in multiple messages if needed response->resType = RespondOracleData::respondQueryIds; const unsigned int numMessages = (pendingQueryIndices.numValues + maxQueryIdCount - 1) / maxQueryIdCount; unsigned int idIdx = 0; diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index c4ab943b8..86b41d36e 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -339,6 +339,41 @@ class OracleEngine setMem(&replyStates[replyStateIdx], sizeof(*replyStates), 0); } + uint32_t findFirstQueryIndexOfTick(uint32_t tick) const + { + // precondition: queries is sorted by tick +#if !defined(NDEBUG) + for (uint32_t t = 1; t < oracleQueryCount; ++t) + ASSERT(queries[t].queryTick >= queries[t - 1].queryTick); +#endif + + if (!oracleQueryCount || tick < queries[0].queryTick || tick > queries[oracleQueryCount - 1].queryTick) + return UINT32_MAX; + + uint32_t lower = 0; + uint32_t upper = oracleQueryCount - 1; + + // invariants: + // 1. lower <= upper + // 2. queries[lower].queryTick <= tick + // 2. tick <= queries[upper].queryTick + while (upper - lower > 1) + { + uint32_t mid = (lower + upper) / 2; + if (queries[mid].queryTick < tick) + lower = mid; + else + upper = mid; + } + + if (queries[lower].queryTick == tick) + return lower; + else if (queries[upper].queryTick == tick) + return upper; + else + return UINT32_MAX; + } + public: /// Initialize object, passing array of own computor public keys (with number of elements given by template param ownComputorSeedsCount). bool init(const m256i* ownComputorPublicKeys) diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index ae792a48e..0fd601c06 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -2,6 +2,8 @@ #include "oracle_testing.h" +#include "platform/random.h" + struct OracleEngineTest : public LoggingTest { @@ -71,6 +73,27 @@ struct OracleEngineWithInitAndDeinit : public OracleEnginequeries[queryIndex]; EXPECT_EQ(oqm.status, expectedStatus); } + + // Test findFirstQueryIndexOfTick(). Ticks must be sorted! + void testFindFirstQueryIndexOfTick(const std::vector& ticks) + { + // setup pseudo-queries + this->oracleQueryCount = 0; + for (auto tick : ticks) + this->queries[this->oracleQueryCount++].queryTick = tick; + + // test function with all values in range + uint32_t minValue = (ticks.empty()) ? 0 : ticks.front() - 1; + uint32_t maxValue = (ticks.empty()) ? 2 : ticks.back() + 1; + for (uint32 value = minValue; value <= maxValue; ++value) + { + auto it = std::find(ticks.begin(), ticks.end(), value); + uint32_t expectedIndex = (it == ticks.end()) ? UINT32_MAX : uint32(it - ticks.begin()); + EXPECT_EQ(this->findFirstQueryIndexOfTick(value), expectedIndex); + } + + this->reset(); + } }; static void dummyNotificationProc(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals) @@ -464,4 +487,50 @@ TEST(OracleEngine, ContractQueryTimeout) - oracleEngine.getReplyCommitTransaction() with more than 1 commit / tx - processOracleReplyCommitTransaction wihtout get getReplyCommitTransaction - trigger failure -*/ \ No newline at end of file +*/ + +TEST(OracleEngine, FindFirstQueryIndexOfTick) +{ + OracleEngineTest test; + OracleEngineWithInitAndDeinit<676> oracleEngine(broadcastedComputors.computors.publicKeys); + oracleEngine.testFindFirstQueryIndexOfTick({ 1 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 2 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 3 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1, 1 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1, 2 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1, 3 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 2, 2 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 2, 3 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 3, 3 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1, 1, 1 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1, 1, 2 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1, 1, 3 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1, 1, 4 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 1, 2, 4 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 2, 3, 4 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 2, 4, 8 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 1, 8, 8, 8 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 100, 105, 108, 109 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 100, 100, 105, 108, 109 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 100, 105, 105, 108, 109 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 100, 105, 108, 108, 109 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 100, 105, 108, 109, 109 }); + oracleEngine.testFindFirstQueryIndexOfTick({ 100, 100, 100, 100, 100, 105, 105, 105, 108, 109, 109 }); + + // random test + std::vector ticks; + uint32_t ticksWithQueries = random(100) + 1; + uint32_t prevTick = 100; + for (uint32_t i = 0; i < ticksWithQueries; ++i) + { + const uint32_t tick = prevTick + random(10); + const uint32_t queriesInTick = random(100) + 1; + for (uint32_t j = 0; j < queriesInTick; ++j) + { + ticks.push_back(tick); + } + prevTick = tick; + } + oracleEngine.testFindFirstQueryIndexOfTick(ticks); +} From 2f82271336ec291278633e990f7e6da73f0ed73c Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:50:19 +0100 Subject: [PATCH 78/90] Remove oracle status flags that don't add to status --- src/network_messages/common_def.h | 4 +--- src/oracle_core/oracle_engine.h | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/network_messages/common_def.h b/src/network_messages/common_def.h index e8580e0e7..d317dfc90 100644 --- a/src/network_messages/common_def.h +++ b/src/network_messages/common_def.h @@ -72,9 +72,7 @@ constexpr uint16_t ORACLE_FLAG_OM_ERROR_FLAGS = 0xff; ///< Mask of all error fl constexpr uint16_t ORACLE_FLAG_REPLY_RECEIVED = 0x100; ///< Oracle engine got valid reply from the oracle machine. constexpr uint16_t ORACLE_FLAG_BAD_SIZE_REPLY = 0x200; ///< Oracle engine got reply of wrong size from the oracle machine. constexpr uint16_t ORACLE_FLAG_OM_DISAGREE = 0x400; ///< Oracle engine got different replies from oracle machines. -constexpr uint16_t ORACLE_FLAG_COMP_DISAGREE = 0x800; ///< The reply commits differ too much and no quorum can be reached. -constexpr uint16_t ORACLE_FLAG_TIMEOUT = 0x1000; ///< The weren't enough reply commit tx with the same digest before timeout (< 451). -constexpr uint16_t ORACLE_FLAG_BAD_SIZE_REVEAL = 0x200; ///< Reply in a reveal tx had wrong size. +constexpr uint16_t ORACLE_FLAG_BAD_SIZE_REVEAL = 0x800; ///< Reply in a reveal tx had wrong size. typedef union IPv4Address { diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 86b41d36e..b9b261db6 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -961,7 +961,6 @@ class OracleEngine // more than 1/3 of commits don't vote for most voted digest -> getting quorum isn't possible // -> switch to status UNRESOLVABLE oqm.status = ORACLE_QUERY_STATUS_UNRESOLVABLE; - oqm.statusFlags |= ORACLE_FLAG_COMP_DISAGREE; oqm.statusVar.failure.agreeingCommits = mostCommitsCount; oqm.statusVar.failure.totalCommits = replyState.totalCommits; pendingQueryIndices.removeByValue(queryIndex); @@ -1299,7 +1298,6 @@ class OracleEngine // update state to TIMEOUT oqm.status = ORACLE_QUERY_STATUS_TIMEOUT; - oqm.statusFlags |= ORACLE_FLAG_TIMEOUT; oqm.statusVar.failure.agreeingCommits = mostCommitsCount; oqm.statusVar.failure.totalCommits = replyState.totalCommits; pendingQueryIndices.removeByValue(queryIndex); From 2664d565b299f369f1966f3a8c2506559caf5ba5 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:49:16 +0100 Subject: [PATCH 79/90] Solve issues with anonymous unions in non-MSVC Some compilers, such as gcc, are less permissive with anonymous unions. Such cases are fixed here in order to get rid of warnings when the core is included as a submodule in other repos. --- src/contract_core/qpi_proposal_voting.h | 6 +- src/contracts/ComputorControlledFund.h | 6 +- src/contracts/GeneralQuorumProposal.h | 18 +-- src/contracts/QUtil.h | 2 +- src/contracts/TestExampleA.h | 22 +-- src/contracts/TestExampleB.h | 18 +-- src/contracts/qpi.h | 36 ++--- test/contract_ccf.cpp | 30 ++-- test/contract_gqmprop.cpp | 10 +- test/contract_testex.cpp | 44 ++--- test/qpi.cpp | 204 ++++++++++++------------ 11 files changed, 198 insertions(+), 198 deletions(-) diff --git a/src/contract_core/qpi_proposal_voting.h b/src/contract_core/qpi_proposal_voting.h index 85dc444a5..b4aa26e45 100644 --- a/src/contract_core/qpi_proposal_voting.h +++ b/src/contract_core/qpi_proposal_voting.h @@ -429,7 +429,7 @@ namespace QPI if (supportScalarVotes) { ASSERT(sizeof(votes[0]) == 8); - if ((voteValue >= this->variableScalar.minValue && voteValue <= this->variableScalar.maxValue)) + if ((voteValue >= this->data.variableScalar.minValue && voteValue <= this->data.variableScalar.maxValue)) { // (cast should not be needed but is to get rid of warning) votes[voteIndex] = static_cast(voteValue); @@ -893,8 +893,8 @@ namespace QPI // scalar voting -> compute mean value of votes sint64 value; sint64 accumulation = 0; - if (p.variableScalar.maxValue > p.variableScalar.maxSupportedValue / maxVotes - || p.variableScalar.minValue < p.variableScalar.minSupportedValue / maxVotes) + if (p.data.variableScalar.maxValue > p.data.variableScalar.maxSupportedValue / maxVotes + || p.data.variableScalar.minValue < p.data.variableScalar.minSupportedValue / maxVotes) { // calculating mean in a way that avoids overflow of sint64 // algorithm based on https://stackoverflow.com/questions/56663116/how-to-calculate-average-of-int64-t diff --git a/src/contracts/ComputorControlledFund.h b/src/contracts/ComputorControlledFund.h index 25d0029b3..709ac8440 100644 --- a/src/contracts/ComputorControlledFund.h +++ b/src/contracts/ComputorControlledFund.h @@ -228,7 +228,7 @@ struct CCF : public ContractBase // Store subscription proposal data in the array indexed by proposalIndex locals.subscriptionProposal.proposerId = qpi.originator(); - locals.subscriptionProposal.destination = input.proposal.transfer.destination; + locals.subscriptionProposal.destination = input.proposal.data.transfer.destination; copyMemory(locals.subscriptionProposal.url, input.proposal.url); locals.subscriptionProposal.weeksPerPeriod = input.weeksPerPeriod; locals.subscriptionProposal.numberOfPeriods = input.numberOfPeriods; @@ -577,8 +577,8 @@ struct CCF : public ContractBase else { // Regular one-time transfer (no subscription data) - locals.transfer.destination = locals.proposal.transfer.destination; - locals.transfer.amount = locals.proposal.transfer.amount; + locals.transfer.destination = locals.proposal.data.transfer.destination; + locals.transfer.amount = locals.proposal.data.transfer.amount; locals.transfer.tick = qpi.tick(); copyMemory(locals.transfer.url, locals.proposal.url); locals.transfer.success = (qpi.transfer(locals.transfer.destination, locals.transfer.amount) >= 0); diff --git a/src/contracts/GeneralQuorumProposal.h b/src/contracts/GeneralQuorumProposal.h index 08debc592..28002320c 100644 --- a/src/contracts/GeneralQuorumProposal.h +++ b/src/contracts/GeneralQuorumProposal.h @@ -202,7 +202,7 @@ struct GQMPROP : public ContractBase // Check that amounts, which are in millionth, are in range of 0 (= 0%) to 1000000 (= 100%) for (locals.i = 0; locals.i < 4; ++locals.i) { - locals.millionthAmount = input.transfer.amounts.get(locals.i); + locals.millionthAmount = input.data.transfer.amounts.get(locals.i); if (locals.millionthAmount < 0 || locals.millionthAmount > 1000000) { return; @@ -212,8 +212,8 @@ struct GQMPROP : public ContractBase case ProposalTypes::Class::TransferInEpoch: // Check amount and epoch - locals.millionthAmount = input.transferInEpoch.amount; - if (locals.millionthAmount < 0 || locals.millionthAmount > 1000000 || input.transferInEpoch.targetEpoch <= qpi.epoch()) + locals.millionthAmount = input.data.transferInEpoch.amount; + if (locals.millionthAmount < 0 || locals.millionthAmount > 1000000 || input.data.transferInEpoch.targetEpoch <= qpi.epoch()) { return; } @@ -419,15 +419,15 @@ struct GQMPROP : public ContractBase if (locals.propClass == ProposalTypes::Class::TransferInEpoch) { ASSERT(locals.mostVotedOptionIndex == 1); - ASSERT(locals.proposal.transferInEpoch.targetEpoch >= qpi.epoch()); - locals.revenueDonationEntry.destinationPublicKey = locals.proposal.transferInEpoch.destination; - locals.revenueDonationEntry.millionthAmount = locals.proposal.transferInEpoch.amount; - locals.revenueDonationEntry.firstEpoch = locals.proposal.transferInEpoch.targetEpoch; + ASSERT(locals.proposal.data.transferInEpoch.targetEpoch >= qpi.epoch()); + locals.revenueDonationEntry.destinationPublicKey = locals.proposal.data.transferInEpoch.destination; + locals.revenueDonationEntry.millionthAmount = locals.proposal.data.transferInEpoch.amount; + locals.revenueDonationEntry.firstEpoch = locals.proposal.data.transferInEpoch.targetEpoch; } else { - locals.revenueDonationEntry.destinationPublicKey = locals.proposal.transfer.destination; - locals.revenueDonationEntry.millionthAmount = locals.proposal.transfer.amounts.get(locals.mostVotedOptionIndex - 1); + locals.revenueDonationEntry.destinationPublicKey = locals.proposal.data.transfer.destination; + locals.revenueDonationEntry.millionthAmount = locals.proposal.data.transfer.amounts.get(locals.mostVotedOptionIndex - 1); locals.revenueDonationEntry.firstEpoch = qpi.epoch(); } CALL(_SetRevenueDonationEntry, locals.revenueDonationEntry, locals.success); diff --git a/src/contracts/QUtil.h b/src/contracts/QUtil.h index fe39068a5..39fd651a4 100644 --- a/src/contracts/QUtil.h +++ b/src/contracts/QUtil.h @@ -1489,7 +1489,7 @@ struct QUTIL : public ContractBase IMPLEMENT_FinalizeShareholderStateVarProposals() { - switch (input.proposal.variableOptions.variable) + switch (input.proposal.data.variableOptions.variable) { case 0: state.smt1InvocationFee = input.acceptedValue; diff --git a/src/contracts/TestExampleA.h b/src/contracts/TestExampleA.h index f8eedc102..70eaa3798 100644 --- a/src/contracts/TestExampleA.h +++ b/src/contracts/TestExampleA.h @@ -491,15 +491,15 @@ struct TESTEXA : public ContractBase case ProposalTypes::Class::Variable: // check that variable index is in valid range - if (input.proposalData.variableOptions.variable >= 3) + if (input.proposalData.data.variableOptions.variable >= 3) return; // check that proposed value is in valid range - if (input.proposalData.variableOptions.variable == 0 && input.proposalData.variableOptions.value > 1000000000000llu) + if (input.proposalData.data.variableOptions.variable == 0 && input.proposalData.data.variableOptions.value > 1000000000000llu) return; - if (input.proposalData.variableOptions.variable == 1 && input.proposalData.variableOptions.value > 1000000llu) + if (input.proposalData.data.variableOptions.variable == 1 && input.proposalData.data.variableOptions.value > 1000000llu) return; - if (input.proposalData.variableOptions.variable == 2 && (input.proposalData.variableOptions.value > 100 || input.proposalData.variableOptions.value < -100)) + if (input.proposalData.data.variableOptions.variable == 2 && (input.proposalData.data.variableOptions.value > 100 || input.proposalData.data.variableOptions.value < -100)) return; break; @@ -509,7 +509,7 @@ struct TESTEXA : public ContractBase break; default: - // this forbids other proposals including transfers and all future propsasl classes not implemented yet + // this forbids other proposals including transfers and all future proposal classes not implemented yet return; } } @@ -567,12 +567,12 @@ struct TESTEXA : public ContractBase // Check if the yes option (1) has been accepted if (locals.results.getAcceptedOption() == 1) { - if (locals.proposal.variableOptions.variable == 0) - state.dummyStateVariable1 = uint64(locals.proposal.variableOptions.value); - if (locals.proposal.variableOptions.variable == 1) - state.dummyStateVariable2 = uint32(locals.proposal.variableOptions.value); - if (locals.proposal.variableOptions.variable == 2) - state.dummyStateVariable3 = sint8(locals.proposal.variableOptions.value); + if (locals.proposal.data.variableOptions.variable == 0) + state.dummyStateVariable1 = uint64(locals.proposal.data.variableOptions.value); + if (locals.proposal.data.variableOptions.variable == 1) + state.dummyStateVariable2 = uint32(locals.proposal.data.variableOptions.value); + if (locals.proposal.data.variableOptions.variable == 2) + state.dummyStateVariable3 = sint8(locals.proposal.data.variableOptions.value); } } diff --git a/src/contracts/TestExampleB.h b/src/contracts/TestExampleB.h index 3cd6420d2..c729faaf5 100644 --- a/src/contracts/TestExampleB.h +++ b/src/contracts/TestExampleB.h @@ -398,7 +398,7 @@ struct TESTEXB : public ContractBase { case ProposalTypes::Class::Variable: // check that variable index is in valid range - if (input.proposalData.variableOptions.variable >= 3) + if (input.proposalData.data.variableOptions.variable >= 3) return; // check that proposed value is in valid range @@ -407,16 +407,16 @@ struct TESTEXB : public ContractBase if (locals.optionCount == 0) { // votes are scalar values - if (input.proposalData.variableScalar.minValue < 0 - || input.proposalData.variableScalar.maxValue < 0 - || input.proposalData.variableScalar.proposedValue < 0) + if (input.proposalData.data.variableScalar.minValue < 0 + || input.proposalData.data.variableScalar.maxValue < 0 + || input.proposalData.data.variableScalar.proposedValue < 0) return; } else { // votes are option indices (option 0 is no change, value i is option i + 1) for (locals.i = 0; locals.i < locals.optionCount - 1; ++locals.i) - if (input.proposalData.variableOptions.values.get(locals.i) < 0) + if (input.proposalData.data.variableOptions.values.get(locals.i) < 0) return; } @@ -447,11 +447,11 @@ struct TESTEXB : public ContractBase PRIVATE_PROCEDURE(FinalizeShareholderProposalSetStateVar) { - if (input.proposal.variableOptions.variable == 0) + if (input.proposal.data.variableOptions.variable == 0) state.fee1 = input.acceptedValue; - else if (input.proposal.variableOptions.variable == 1) + else if (input.proposal.data.variableOptions.variable == 1) state.fee2 = input.acceptedValue; - else if (input.proposal.variableOptions.variable == 2) + else if (input.proposal.data.variableOptions.variable == 2) state.fee3 = input.acceptedValue; } @@ -496,7 +496,7 @@ struct TESTEXB : public ContractBase continue; // option 0 is "no change", option 1 has index 0 in variableOptions - locals.p.acceptedValue = locals.p.proposal.variableOptions.values.get(locals.p.acceptedOption - 1); + locals.p.acceptedValue = locals.p.proposal.data.variableOptions.values.get(locals.p.acceptedOption - 1); } CALL(FinalizeShareholderProposalSetStateVar, locals.p, output); diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index cfd2e697a..a272551c6 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -1853,7 +1853,7 @@ namespace QPI uint32 tick; // Proposal payload data (for all except types with class GeneralProposal) - union + union Data { // Used if type class is Transfer struct Transfer @@ -1888,7 +1888,7 @@ namespace QPI static constexpr sint64 minSupportedValue = 0x8000000000000001; static constexpr sint64 maxSupportedValue = 0x7fffffffffffffff; } variableScalar; - }; + } data; // Check if content of instance are valid. Epoch is not checked. // Also useful to show requirements of valid proposal. @@ -1905,42 +1905,42 @@ namespace QPI okay = options >= 2 && options <= 8; break; case ProposalTypes::Class::Transfer: - if (!isZero(transfer.destination) && options >= 2 && options <= 5) + if (!isZero(data.transfer.destination) && options >= 2 && options <= 5) { uint16 proposedAmounts = options - 1; okay = true; for (uint16 i = 0; i < proposedAmounts; ++i) { // no negative amounts - if (transfer.amounts.get(i) < 0) + if (data.transfer.amounts.get(i) < 0) { okay = false; break; } } okay = okay - && isArraySortedWithoutDuplicates(transfer.amounts, 0, proposedAmounts) - && transfer.amounts.rangeEquals(proposedAmounts, transfer.amounts.capacity(), 0); + && isArraySortedWithoutDuplicates(data.transfer.amounts, 0, proposedAmounts) + && data.transfer.amounts.rangeEquals(proposedAmounts, data.transfer.amounts.capacity(), 0); } break; case ProposalTypes::Class::TransferInEpoch: - okay = options == 2 && !isZero(transferInEpoch.destination) && transferInEpoch.amount >= 0; + okay = options == 2 && !isZero(data.transferInEpoch.destination) && data.transferInEpoch.amount >= 0; break; case ProposalTypes::Class::Variable: if (options >= 2 && options <= 5) { // option voting uint16 proposedValues = options - 1; - okay = isArraySortedWithoutDuplicates(variableOptions.values, 0, proposedValues) - && variableOptions.values.rangeEquals(proposedValues, variableOptions.values.capacity(), 0); + okay = isArraySortedWithoutDuplicates(data.variableOptions.values, 0, proposedValues) + && data.variableOptions.values.rangeEquals(proposedValues, data.variableOptions.values.capacity(), 0); } else if (options == 0) { // scalar voting if (supportScalarVotes) - okay = variableScalar.minValue <= variableScalar.proposedValue - && variableScalar.proposedValue <= variableScalar.maxValue - && variableScalar.minValue > NO_VOTE_VALUE; + okay = data.variableScalar.minValue <= data.variableScalar.proposedValue + && data.variableScalar.proposedValue <= data.variableScalar.maxValue + && data.variableScalar.minValue > NO_VOTE_VALUE; } break; } @@ -1980,7 +1980,7 @@ namespace QPI uint32 tick; // Proposal payload data (for all except types with class GeneralProposal) - union + union Data { // Used if type class is Transfer struct Transfer @@ -1995,7 +1995,7 @@ namespace QPI uint64 variable; // For identifying variable (interpreted by contract only) sint64 value; // Value of proposed option, rest zero } variableOptions; - }; + } data; // Check if content of instance are valid. Epoch is not checked. // Also useful to show requirements of valid proposal. @@ -2012,7 +2012,7 @@ namespace QPI okay = options >= 2 && options <= 3; // 3 options can be encoded in the yes/no type of storage as well break; case ProposalTypes::Class::Transfer: - okay = (options == 2 && !isZero(transfer.destination) && transfer.amount >= 0); + okay = (options == 2 && !isZero(data.transfer.destination) && data.transfer.amount >= 0); break; case ProposalTypes::Class::Variable: okay = (options == 2); @@ -2950,8 +2950,8 @@ namespace QPI typedef uint16 SetShareholderProposal_output; \ PUBLIC_PROCEDURE(SetShareholderProposal) { \ if (qpi.invocationReward() < setProposalFeeVarOrValue || (input.epoch \ - && (input.type != ProposalTypes::VariableYesNo || input.variableOptions.variable >= numFeeStateVariables \ - || input.variableOptions.value < 0))) { \ + && (input.type != ProposalTypes::VariableYesNo || input.data.variableOptions.variable >= numFeeStateVariables \ + || input.data.variableOptions.value < 0))) { \ qpi.transfer(qpi.invocator(), qpi.invocationReward()); \ output = INVALID_PROPOSAL_INDEX; \ return; } \ @@ -3042,7 +3042,7 @@ namespace QPI locals.p.acceptedOption = locals.p.results.getAcceptedOption(); \ if (locals.p.acceptedOption <= 0) \ continue; \ - locals.p.acceptedValue = locals.p.proposal.variableOptions.value; \ + locals.p.acceptedValue = locals.p.proposal.data.variableOptions.value; \ CALL(FinalizeShareholderProposalSetStateVar, locals.p, output); } } } \ PRIVATE_PROCEDURE(FinalizeShareholderProposalSetStateVar) diff --git a/test/contract_ccf.cpp b/test/contract_ccf.cpp index 8aacb777d..655e613a7 100644 --- a/test/contract_ccf.cpp +++ b/test/contract_ccf.cpp @@ -352,8 +352,8 @@ class ContractTestingCCF : protected ContractTesting setMemory(input, 0); input.proposal.epoch = system.epoch; input.proposal.type = ProposalTypes::TransferYesNo; - input.proposal.transfer.destination = destination; - input.proposal.transfer.amount = amount; + input.proposal.data.transfer.destination = destination; + input.proposal.data.transfer.amount = amount; input.isSubscription = false; auto output = setProposal(proposer, input); @@ -371,8 +371,8 @@ class ContractTestingCCF : protected ContractTesting setMemory(input, 0); input.proposal.epoch = system.epoch; input.proposal.type = ProposalTypes::TransferYesNo; - input.proposal.transfer.destination = destination; - input.proposal.transfer.amount = amountPerPeriod; + input.proposal.data.transfer.destination = destination; + input.proposal.data.transfer.amount = amountPerPeriod; input.isSubscription = true; input.weeksPerPeriod = weeksPerPeriod; input.numberOfPeriods = numberOfPeriods; @@ -448,7 +448,7 @@ TEST(ContractCCF, RegularProposalAndVoting) // Get proposal auto proposal = test.getProposal(proposalIndex); EXPECT_TRUE(proposal.okay); - EXPECT_EQ(proposal.proposal.transfer.destination, ENTITY1); + EXPECT_EQ(proposal.proposal.data.transfer.destination, ENTITY1); // Vote on proposal test.voteMultipleComputors(proposalIndex, 200, 350); @@ -691,8 +691,8 @@ TEST(ContractCCF, SubscriptionValidation) setMemory(input, 0); input.proposal.epoch = system.epoch; input.proposal.type = ProposalTypes::TransferYesNo; - input.proposal.transfer.destination = ENTITY1; - input.proposal.transfer.amount = 1000; + input.proposal.data.transfer.destination = ENTITY1; + input.proposal.data.transfer.amount = 1000; input.isSubscription = true; input.weeksPerPeriod = 0; input.numberOfPeriods = 4; @@ -841,8 +841,8 @@ TEST(ContractCCF, SubscriptionMaxEpochsValidation) setMemory(input, 0); input.proposal.epoch = system.epoch; input.proposal.type = ProposalTypes::TransferYesNo; - input.proposal.transfer.destination = ENTITY1; - input.proposal.transfer.amount = 1000; + input.proposal.data.transfer.destination = ENTITY1; + input.proposal.data.transfer.amount = 1000; input.isSubscription = true; input.weeksPerPeriod = 4; // 4 weeks per period (monthly) input.numberOfPeriods = 14; // 14 * 4 = 56 epochs > 52 max @@ -957,8 +957,8 @@ TEST(ContractCCF, SubscriptionSlotReuse) setMemory(cancelInput, 0); cancelInput.proposal.epoch = 0; cancelInput.proposal.type = ProposalTypes::TransferYesNo; - cancelInput.proposal.transfer.destination = ENTITY1; - cancelInput.proposal.transfer.amount = 1000; + cancelInput.proposal.data.transfer.destination = ENTITY1; + cancelInput.proposal.data.transfer.amount = 1000; cancelInput.weeksPerPeriod = 1; // 1 week per period (weekly) cancelInput.numberOfPeriods = 4; cancelInput.startEpoch = 189; @@ -1022,8 +1022,8 @@ TEST(ContractCCF, CancelSubscriptionByZeroAmount) setMemory(cancelInput, 0); cancelInput.proposal.epoch = system.epoch; cancelInput.proposal.type = ProposalTypes::TransferYesNo; - cancelInput.proposal.transfer.destination = ENTITY1; - cancelInput.proposal.transfer.amount = 0; + cancelInput.proposal.data.transfer.destination = ENTITY1; + cancelInput.proposal.data.transfer.amount = 0; cancelInput.isSubscription = true; cancelInput.weeksPerPeriod = 1; cancelInput.numberOfPeriods = 4; @@ -1080,8 +1080,8 @@ TEST(ContractCCF, CancelSubscriptionByZeroPeriods) setMemory(cancelInput, 0); cancelInput.proposal.epoch = system.epoch; cancelInput.proposal.type = ProposalTypes::TransferYesNo; - cancelInput.proposal.transfer.destination = ENTITY1; - cancelInput.proposal.transfer.amount = 0; + cancelInput.proposal.data.transfer.destination = ENTITY1; + cancelInput.proposal.data.transfer.amount = 0; cancelInput.isSubscription = true; cancelInput.weeksPerPeriod = 1; cancelInput.numberOfPeriods = 0; // Zero periods will cancel subscription diff --git a/test/contract_gqmprop.cpp b/test/contract_gqmprop.cpp index d108ae261..bf0854b3b 100644 --- a/test/contract_gqmprop.cpp +++ b/test/contract_gqmprop.cpp @@ -186,14 +186,14 @@ class ContractTestingGQmProp : protected ContractTesting { const auto amountCount = ProposalTypes::optionCount(type) - 1; for (int i = 0; i < amountCount; ++i) - proposal.transfer.amounts.set(i, amount + i); - proposal.transfer.destination = dest; + proposal.data.transfer.amounts.set(i, amount + i); + proposal.data.transfer.destination = dest; break; } case ProposalTypes::Class::TransferInEpoch: - proposal.transferInEpoch.amount = amount; - proposal.transferInEpoch.destination = dest; - proposal.transferInEpoch.targetEpoch = targetEpoch; + proposal.data.transferInEpoch.amount = amount; + proposal.data.transferInEpoch.destination = dest; + proposal.data.transferInEpoch.targetEpoch = targetEpoch; break; } uint16 proposalIdx = setProposal(id(proposer, 1, 2, 3), proposal); diff --git a/test/contract_testex.cpp b/test/contract_testex.cpp index 9b4c7490a..b3c7046fb 100644 --- a/test/contract_testex.cpp +++ b/test/contract_testex.cpp @@ -468,22 +468,22 @@ class ContractTestingTestEx : protected ContractTesting { EXPECT_FALSE(setVar2); EXPECT_FALSE(setVar3); - input.proposalData.variableOptions.variable = 0; - input.proposalData.variableOptions.value = valueVar1; + input.proposalData.data.variableOptions.variable = 0; + input.proposalData.data.variableOptions.value = valueVar1; } else if (setVar2) { EXPECT_FALSE(setVar1); EXPECT_FALSE(setVar3); - input.proposalData.variableOptions.variable = 1; - input.proposalData.variableOptions.value = valueVar2; + input.proposalData.data.variableOptions.variable = 1; + input.proposalData.data.variableOptions.value = valueVar2; } else if (setVar3) { EXPECT_FALSE(setVar1); EXPECT_FALSE(setVar2); - input.proposalData.variableOptions.variable = 2; - input.proposalData.variableOptions.value = valueVar3; + input.proposalData.data.variableOptions.variable = 2; + input.proposalData.data.variableOptions.value = valueVar3; } break; } @@ -1878,39 +1878,39 @@ TEST(ContractTestEx, ShareholderProposals) TESTEXB::ProposalDataT proposalB1; proposalB1.epoch = system.epoch; proposalB1.type = ProposalTypes::VariableScalarMean; - proposalB1.variableScalar.variable = 0; - proposalB1.variableScalar.minValue = 0; - proposalB1.variableScalar.maxValue = MAX_AMOUNT; - proposalB1.variableScalar.proposedValue = 1000; + proposalB1.data.variableScalar.variable = 0; + proposalB1.data.variableScalar.minValue = 0; + proposalB1.data.variableScalar.maxValue = MAX_AMOUNT; + proposalB1.data.variableScalar.proposedValue = 1000; uint16 proposalIdxB1 = test.setShareholderProposal(USER2, { proposalB1 }); EXPECT_NE((int)proposalIdxB1, (int)INVALID_PROPOSAL_INDEX); auto proposalDataB1 = test.getShareholderProposal(proposalIdxB1); proposalB1 = proposalDataB1.proposal; // needed to set tick EXPECT_EQ((int)proposalDataB1.proposal.type, (int)ProposalTypes::VariableScalarMean); EXPECT_EQ(proposalDataB1.proposerPubicKey, USER2); - EXPECT_EQ(proposalDataB1.proposal.variableScalar.maxValue, MAX_AMOUNT); - EXPECT_EQ(proposalDataB1.proposal.variableScalar.proposedValue, 1000); + EXPECT_EQ(proposalDataB1.proposal.data.variableScalar.maxValue, MAX_AMOUNT); + EXPECT_EQ(proposalDataB1.proposal.data.variableScalar.proposedValue, 1000); // Create multi-option variable proposal as shareholder TESTEXA TESTEXB::ProposalDataT proposalB2; proposalB2.epoch = system.epoch; proposalB2.type = ProposalTypes::VariableFourValues; - proposalB2.variableOptions.variable = 1; - proposalB2.variableOptions.values.set(0, 100); - proposalB2.variableOptions.values.set(1, 1000); - proposalB2.variableOptions.values.set(2, 10000); - proposalB2.variableOptions.values.set(3, 100000); + proposalB2.data.variableOptions.variable = 1; + proposalB2.data.variableOptions.values.set(0, 100); + proposalB2.data.variableOptions.values.set(1, 1000); + proposalB2.data.variableOptions.values.set(2, 10000); + proposalB2.data.variableOptions.values.set(3, 100000); uint16 proposalIdxB2 = test.setProposalInOtherContractAsShareholder(USER1, TESTEXB_CONTRACT_INDEX, TESTEXB::SetShareholderProposal_input{ proposalB2 }); EXPECT_NE((int)proposalIdxB2, (int)INVALID_PROPOSAL_INDEX); auto proposalDataB2 = test.getShareholderProposal(proposalIdxB2); proposalB2 = proposalDataB2.proposal; // needed to set tick EXPECT_EQ((int)proposalDataB2.proposal.type, (int)ProposalTypes::VariableFourValues); EXPECT_EQ(proposalDataB2.proposerPubicKey, TESTEXA_CONTRACT_ID); - EXPECT_EQ(proposalDataB2.proposal.variableOptions.variable, 1); - EXPECT_EQ(proposalDataB2.proposal.variableOptions.values.get(0), 100); - EXPECT_EQ(proposalDataB2.proposal.variableOptions.values.get(1), 1000); - EXPECT_EQ(proposalDataB2.proposal.variableOptions.values.get(2), 10000); - EXPECT_EQ(proposalDataB2.proposal.variableOptions.values.get(3), 100000); + EXPECT_EQ(proposalDataB2.proposal.data.variableOptions.variable, 1); + EXPECT_EQ(proposalDataB2.proposal.data.variableOptions.values.get(0), 100); + EXPECT_EQ(proposalDataB2.proposal.data.variableOptions.values.get(1), 1000); + EXPECT_EQ(proposalDataB2.proposal.data.variableOptions.values.get(2), 10000); + EXPECT_EQ(proposalDataB2.proposal.data.variableOptions.values.get(3), 100000); // cast votes in A1 EXPECT_TRUE(test.setShareholderVotes(USER1, proposalIdxA1, proposalA1, { {0, 60}, {1, 270} })); diff --git a/test/qpi.cpp b/test/qpi.cpp index 9ea0b67fe..4c5e0b780 100644 --- a/test/qpi.cpp +++ b/test/qpi.cpp @@ -747,31 +747,31 @@ void testProposalWithAllVoteData() // TransferYesNo proposal proposal.type = QPI::ProposalTypes::TransferYesNo; - proposal.transfer.destination = QPI::id(1, 2, 3, 4); - proposal.transfer.amounts.setAll(0); - proposal.transfer.amounts.set(0, 1234); + proposal.data.transfer.destination = QPI::id(1, 2, 3, 4); + proposal.data.transfer.amounts.setAll(0); + proposal.data.transfer.amounts.set(0, 1234); testProposalWithAllVoteDataOptionVotes(pwav, proposal, 2); // TransferTwoAmounts proposal.type = QPI::ProposalTypes::TransferTwoAmounts; - proposal.transfer.amounts.set(1, 12345); + proposal.data.transfer.amounts.set(1, 12345); testProposalWithAllVoteDataOptionVotes(pwav, proposal, 3); // TransferThreeAmounts proposal.type = QPI::ProposalTypes::TransferThreeAmounts; - proposal.transfer.amounts.set(2, 123456); + proposal.data.transfer.amounts.set(2, 123456); testProposalWithAllVoteDataOptionVotes(pwav, proposal, 4); // TransferFourAmounts proposal.type = QPI::ProposalTypes::TransferFourAmounts; - proposal.transfer.amounts.set(3, 1234567); + proposal.data.transfer.amounts.set(3, 1234567); testProposalWithAllVoteDataOptionVotes(pwav, proposal, 5); // TransferInEpochYesNo proposal proposal.type = QPI::ProposalTypes::TransferInEpochYesNo; - proposal.transferInEpoch.destination = QPI::id(1, 2, 3, 4); - proposal.transferInEpoch.amount = 10; - proposal.transferInEpoch.targetEpoch = 123; + proposal.data.transferInEpoch.destination = QPI::id(1, 2, 3, 4); + proposal.data.transferInEpoch.amount = 10; + proposal.data.transferInEpoch.targetEpoch = 123; testProposalWithAllVoteDataOptionVotes(pwav, proposal, 2); // fail: test TransferInEpoch proposal with too many or too few options @@ -784,24 +784,24 @@ void testProposalWithAllVoteData() // VariableYesNo proposal proposal.type = QPI::ProposalTypes::VariableYesNo; - proposal.variableOptions.variable = 42; - proposal.variableOptions.values.set(0, 987); - proposal.variableOptions.values.setRange(1, proposal.variableOptions.values.capacity(), 0); + proposal.data.variableOptions.variable = 42; + proposal.data.variableOptions.values.set(0, 987); + proposal.data.variableOptions.values.setRange(1, proposal.data.variableOptions.values.capacity(), 0); testProposalWithAllVoteDataOptionVotes(pwav, proposal, 2); // VariableTwoValues proposal proposal.type = QPI::ProposalTypes::VariableTwoValues; - proposal.variableOptions.values.set(1, 9876); + proposal.data.variableOptions.values.set(1, 9876); testProposalWithAllVoteDataOptionVotes(pwav, proposal, 3); // VariableThreeValues proposal proposal.type = QPI::ProposalTypes::VariableThreeValues; - proposal.variableOptions.values.set(2, 98765); + proposal.data.variableOptions.values.set(2, 98765); testProposalWithAllVoteDataOptionVotes(pwav, proposal, 4); // VariableFourValues proposal proposal.type = QPI::ProposalTypes::VariableFourValues; - proposal.variableOptions.values.set(3, 987654); + proposal.data.variableOptions.values.set(3, 987654); testProposalWithAllVoteDataOptionVotes(pwav, proposal, 5); // fail: test variable proposal with too many or too few options (0 options means scalar) @@ -815,15 +815,15 @@ void testProposalWithAllVoteData() // fail: wrong sorting with class Variable proposal.type = QPI::ProposalTypes::VariableFourValues; for (int i = 0; i < 4; ++i) - proposal.variableOptions.values.set(i, 20 - i); + proposal.data.variableOptions.values.set(i, 20 - i); EXPECT_FALSE(proposal.checkValidity()); // VariableScalarMean proposal proposal.type = QPI::ProposalTypes::VariableScalarMean; - proposal.variableScalar.variable = 42; - proposal.variableScalar.minValue = 0; - proposal.variableScalar.maxValue = 25; - proposal.variableScalar.proposedValue = 1; + proposal.data.variableScalar.variable = 42; + proposal.data.variableScalar.minValue = 0; + proposal.data.variableScalar.maxValue = 25; + proposal.data.variableScalar.proposedValue = 1; if (supportScalarVotes) testProposalWithAllVoteDataOptionVotes(pwav, proposal, 26); else @@ -891,8 +891,8 @@ TEST(TestCoreQPI, ProposalWithAllVoteDataYesNoProposals) // TransferYesNo proposal proposal.type = QPI::ProposalTypes::TransferYesNo; - proposal.transfer.destination = QPI::id(1, 2, 3, 4); - proposal.transfer.amount = 1234; + proposal.data.transfer.destination = QPI::id(1, 2, 3, 4); + proposal.data.transfer.amount = 1234; testProposalWithAllVoteDataOptionVotes(pwav, proposal, 2); // TransferTwoAmounts @@ -909,8 +909,8 @@ TEST(TestCoreQPI, ProposalWithAllVoteDataYesNoProposals) // VariableYesNo proposal proposal.type = QPI::ProposalTypes::VariableYesNo; - proposal.variableOptions.variable = 42; - proposal.variableOptions.value = 987; + proposal.data.variableOptions.variable = 42; + proposal.data.variableOptions.value = 987; testProposalWithAllVoteDataOptionVotes(pwav, proposal, 2); // VariableTwoValues proposal @@ -977,9 +977,9 @@ bool isReturnedProposalAsExpected( && proposalReturnedByGet.epoch == qpi.epoch() && proposalReturnedByGet.type == proposalSet.type && proposalReturnedByGet.supportScalarVotes == proposalSet.supportScalarVotes - && (memcmp(&proposalReturnedByGet.transfer, &proposalSet.transfer, sizeof(proposalSet.transfer)) == 0) - && (memcmp(&proposalReturnedByGet.variableOptions, &proposalSet.variableOptions, sizeof(proposalSet.variableOptions)) == 0) - && (memcmp(&proposalReturnedByGet.variableScalar, &proposalSet.variableScalar, sizeof(proposalSet.variableScalar)) == 0) + && (memcmp(&proposalReturnedByGet.data.transfer, &proposalSet.data.transfer, sizeof(proposalSet.data.transfer)) == 0) + && (memcmp(&proposalReturnedByGet.data.variableOptions, &proposalSet.data.variableOptions, sizeof(proposalSet.data.variableOptions)) == 0) + && (memcmp(&proposalReturnedByGet.data.variableScalar, &proposalSet.data.variableScalar, sizeof(proposalSet.data.variableScalar)) == 0) && (memcmp(&proposalReturnedByGet.url, &proposalSet.url, sizeof(proposalSet.url)) == 0); return expected; } @@ -1436,8 +1436,8 @@ void testProposalVotingComputorsV1() // fail: proposal of transfer with wrong address proposal.type = QPI::ProposalTypes::TransferYesNo; - proposal.transfer.destination = QPI::NULL_ID; - proposal.transfer.amounts.setAll(0); + proposal.data.transfer.destination = QPI::NULL_ID; + proposal.data.transfer.amounts.setAll(0); setProposalExpectFailure(qpi, pv, secondProposer, proposal); // check that overwrite did not work EXPECT_TRUE(qpi(*pv).getProposal(qpi(*pv).proposalIndex(secondProposer), proposalReturned)); @@ -1456,13 +1456,13 @@ void testProposalVotingComputorsV1() // fail: proposal of revenue distribution with invalid amount proposal.type = QPI::ProposalTypes::TransferYesNo; - proposal.transfer.destination = qpi.originator(); - proposal.transfer.amounts.set(0, -123456); + proposal.data.transfer.destination = qpi.originator(); + proposal.data.transfer.amounts.set(0, -123456); setProposalExpectFailure(qpi, pv, secondProposer, proposal); // okay: revenue distribution, overwrite existing proposal of comp 2 (proposal index 1, reused) - proposal.transfer.destination = qpi.originator(); - proposal.transfer.amounts.set(0, 1005); + proposal.data.transfer.destination = qpi.originator(); + proposal.data.transfer.amounts.set(0, 1005); setProposalWithSuccessCheck(qpi, pv, secondProposer, proposal); EXPECT_EQ((int)qpi(*pv).proposalIndex(secondProposer), 1); EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); @@ -1508,23 +1508,23 @@ void testProposalVotingComputorsV1() { // fail: scalar proposal with wrong min/max proposal.type = QPI::ProposalTypes::VariableScalarMean; - proposal.variableScalar.proposedValue = 10; - proposal.variableScalar.minValue = 11; - proposal.variableScalar.maxValue = 20; - proposal.variableScalar.variable = 123; // not checked, full range usable + proposal.data.variableScalar.proposedValue = 10; + proposal.data.variableScalar.minValue = 11; + proposal.data.variableScalar.maxValue = 20; + proposal.data.variableScalar.variable = 123; // not checked, full range usable setProposalExpectFailure(qpi, pv, qpi.computor(1), proposal); - proposal.variableScalar.minValue = 0; - proposal.variableScalar.maxValue = 9; + proposal.data.variableScalar.minValue = 0; + proposal.data.variableScalar.maxValue = 9; setProposalExpectFailure(qpi, pv, qpi.computor(1), proposal); // fail: scalar proposal with full range is invalid, because NO_VOTE_VALUE is reserved for no vote - proposal.variableScalar.minValue = proposal.variableScalar.minSupportedValue - 1; - proposal.variableScalar.maxValue = proposal.variableScalar.maxSupportedValue; + proposal.data.variableScalar.minValue = proposal.data.variableScalar.minSupportedValue - 1; + proposal.data.variableScalar.maxValue = proposal.data.variableScalar.maxSupportedValue; setProposalExpectFailure(qpi, pv, qpi.computor(1), proposal); // okay: scalar proposal with nearly full range - proposal.variableScalar.minValue = proposal.variableScalar.minSupportedValue; - proposal.variableScalar.maxValue = proposal.variableScalar.maxSupportedValue; + proposal.data.variableScalar.minValue = proposal.data.variableScalar.minSupportedValue; + proposal.data.variableScalar.maxValue = proposal.data.variableScalar.maxSupportedValue; setProposalWithSuccessCheck(qpi, pv, qpi.computor(1), proposal); EXPECT_EQ((int)qpi(*pv).proposalIndex(qpi.computor(1)), 2); EXPECT_EQ((int)qpi(*pv).proposalIndex(secondProposer), (int)secondProposalIdx); @@ -1538,8 +1538,8 @@ void testProposalVotingComputorsV1() expectNoVotes(qpi, pv, qpi(*pv).proposalIndex(qpi.computor(1))); for (int i = 0; i < 99; ++i) { - voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.variableScalar.maxSupportedValue - 2 + i % 3); - voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.variableScalar.maxSupportedValue - 2 + i % 3); + voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.data.variableScalar.maxSupportedValue - 2 + i % 3); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.data.variableScalar.maxSupportedValue - 2 + i % 3); } EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(qpi.computor(1)), votingSummaryReturned)); EXPECT_EQ((int)votingSummaryReturned.proposalIndex, (int)qpi(*pv).proposalIndex(qpi.computor(1))); @@ -1548,20 +1548,20 @@ void testProposalVotingComputorsV1() EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), -1); EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), -1); - EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.variableScalar.maxSupportedValue - 1); + EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.data.variableScalar.maxSupportedValue - 1); for (int i = 0; i < 555; ++i) { - voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.variableScalar.minSupportedValue + 10 - i % 5); - voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.variableScalar.minSupportedValue + 10 - i % 5); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.data.variableScalar.minSupportedValue + 10 - i % 5); + voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.data.variableScalar.minSupportedValue + 10 - i % 5); } EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(qpi.computor(1)), votingSummaryReturned)); EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 555); EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); - EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.variableScalar.minSupportedValue + 8); + EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.data.variableScalar.minSupportedValue + 8); // okay: scalar proposal with limited range - proposal.variableScalar.minValue = -1000; - proposal.variableScalar.maxValue = 1000; + proposal.data.variableScalar.minValue = -1000; + proposal.data.variableScalar.maxValue = 1000; setProposalWithSuccessCheck(qpi, pv, qpi.computor(10), proposal); EXPECT_EQ((int)qpi(*pv).proposalIndex(qpi.computor(10)), 3); EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); @@ -1614,36 +1614,36 @@ void testProposalVotingComputorsV1() // fail: test multi-option transfer proposal with invalid amounts proposal.type = QPI::ProposalTypes::TransferThreeAmounts; - proposal.transfer.destination = qpi.originator(); + proposal.data.transfer.destination = qpi.originator(); for (int i = 0; i < 4; ++i) { - proposal.transfer.amounts.setAll(0); - proposal.transfer.amounts.set(i, -100 * i - 1); + proposal.data.transfer.amounts.setAll(0); + proposal.data.transfer.amounts.set(i, -100 * i - 1); setProposalExpectFailure(qpi, pv, qpi.computor(1), proposal); } - proposal.transfer.amounts.set(0, 0); - proposal.transfer.amounts.set(1, 10); - proposal.transfer.amounts.set(2, 20); - proposal.transfer.amounts.set(3, 100); // for ProposalTypes::TransferThreeAmounts, fourth must be 0 + proposal.data.transfer.amounts.set(0, 0); + proposal.data.transfer.amounts.set(1, 10); + proposal.data.transfer.amounts.set(2, 20); + proposal.data.transfer.amounts.set(3, 100); // for ProposalTypes::TransferThreeAmounts, fourth must be 0 setProposalExpectFailure(qpi, pv, qpi.computor(1), proposal); // fail: duplicate options - proposal.transfer.amounts.setAll(0); + proposal.data.transfer.amounts.setAll(0); setProposalExpectFailure(qpi, pv, qpi.computor(1), proposal); // fail: options not sorted for (int i = 0; i < 3; ++i) - proposal.transfer.amounts.set(i, 100 - i); + proposal.data.transfer.amounts.set(i, 100 - i); setProposalExpectFailure(qpi, pv, qpi.computor(1), proposal); // okay: fill proposal storage - proposal.transfer.amounts.setAll(0); + proposal.data.transfer.amounts.setAll(0); constexpr QPI::uint16 computorProposalToFillAll = (proposalByComputorsOnly) ? pv->maxProposals : pv->maxProposals - 1; for (int i = 0; i < computorProposalToFillAll; ++i) { - proposal.transfer.amounts.set(0, i); - proposal.transfer.amounts.set(1, i * 2 + 1); - proposal.transfer.amounts.set(2, i * 3 + 2); + proposal.data.transfer.amounts.set(0, i); + proposal.data.transfer.amounts.set(1, i * 2 + 1); + proposal.data.transfer.amounts.set(2, i * 3 + 2); setProposalWithSuccessCheck(qpi, pv, qpi.computor(i), proposal); } EXPECT_EQ(countActiveProposals(qpi, pv), (int)pv->maxProposals); @@ -1958,8 +1958,8 @@ void testProposalVotingShareholdersV1() // fail: proposal of transfer with wrong address proposal.type = QPI::ProposalTypes::TransferYesNo; - proposal.transfer.destination = QPI::NULL_ID; - proposal.transfer.amounts.setAll(0); + proposal.data.transfer.destination = QPI::NULL_ID; + proposal.data.transfer.amounts.setAll(0); setProposalExpectFailure(qpi, pv, secondProposer, proposal); // check that overwrite did not work EXPECT_TRUE(qpi(*pv).getProposal(qpi(*pv).proposalIndex(secondProposer), proposalReturned)); @@ -1978,13 +1978,13 @@ void testProposalVotingShareholdersV1() // fail: proposal of revenue distribution with invalid amount proposal.type = QPI::ProposalTypes::TransferYesNo; - proposal.transfer.destination = qpi.originator(); - proposal.transfer.amounts.set(0, -123456); + proposal.data.transfer.destination = qpi.originator(); + proposal.data.transfer.amounts.set(0, -123456); setProposalExpectFailure(qpi, pv, secondProposer, proposal); // okay: revenue distribution, overwrite existing proposal of comp 2 (proposal index 1, reused) - proposal.transfer.destination = qpi.originator(); - proposal.transfer.amounts.set(0, 1005); + proposal.data.transfer.destination = qpi.originator(); + proposal.data.transfer.amounts.set(0, 1005); setProposalWithSuccessCheck(qpi, pv, secondProposer, proposal); EXPECT_EQ((int)qpi(*pv).proposalIndex(secondProposer), 1); EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); @@ -2036,23 +2036,23 @@ void testProposalVotingShareholdersV1() { // fail: scalar proposal with wrong min/max proposal.type = QPI::ProposalTypes::VariableScalarMean; - proposal.variableScalar.proposedValue = 10; - proposal.variableScalar.minValue = 11; - proposal.variableScalar.maxValue = 20; - proposal.variableScalar.variable = 123; // not checked, full range usable + proposal.data.variableScalar.proposedValue = 10; + proposal.data.variableScalar.minValue = 11; + proposal.data.variableScalar.maxValue = 20; + proposal.data.variableScalar.variable = 123; // not checked, full range usable setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); - proposal.variableScalar.minValue = 0; - proposal.variableScalar.maxValue = 9; + proposal.data.variableScalar.minValue = 0; + proposal.data.variableScalar.maxValue = 9; setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); // fail: scalar proposal with full range is invalid, because NO_VOTE_VALUE is reserved for no vote - proposal.variableScalar.minValue = proposal.variableScalar.minSupportedValue - 1; - proposal.variableScalar.maxValue = proposal.variableScalar.maxSupportedValue; + proposal.data.variableScalar.minValue = proposal.data.variableScalar.minSupportedValue - 1; + proposal.data.variableScalar.maxValue = proposal.data.variableScalar.maxSupportedValue; setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); // okay: scalar proposal with nearly full range - proposal.variableScalar.minValue = proposal.variableScalar.minSupportedValue; - proposal.variableScalar.maxValue = proposal.variableScalar.maxSupportedValue; + proposal.data.variableScalar.minValue = proposal.data.variableScalar.minSupportedValue; + proposal.data.variableScalar.maxValue = proposal.data.variableScalar.maxSupportedValue; setProposalWithSuccessCheck(qpi, pv, shareholderShares[1].first, proposal); EXPECT_EQ((int)qpi(*pv).proposalIndex(shareholderShares[1].first), 2); EXPECT_EQ((int)qpi(*pv).proposalIndex(secondProposer), (int)secondProposalIdx); @@ -2068,16 +2068,16 @@ void testProposalVotingShareholdersV1() { voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, qpi(*pv).proposalIndex(shareholderShares[1].first), proposal.type, qpi.tick(), - proposal.variableScalar.maxSupportedValue - 2 + i % 3, 5, - proposal.variableScalar.maxSupportedValue - 2 + (i + 1) % 3, 3, - proposal.variableScalar.maxSupportedValue - 2 + (i + 2) % 3, 2); + proposal.data.variableScalar.maxSupportedValue - 2 + i % 3, 5, + proposal.data.variableScalar.maxSupportedValue - 2 + (i + 1) % 3, 3, + proposal.data.variableScalar.maxSupportedValue - 2 + (i + 2) % 3, 2); } EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(shareholderShares[1].first), votingSummaryReturned)); EXPECT_EQ((int)votingSummaryReturned.proposalIndex, (int)qpi(*pv).proposalIndex(shareholderShares[1].first)); EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 30); EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); - EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.variableScalar.maxSupportedValue - 1); + EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.data.variableScalar.maxSupportedValue - 1); EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), -1); EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), -1); @@ -2085,17 +2085,17 @@ void testProposalVotingShareholdersV1() { voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, qpi(*pv).proposalIndex(shareholderShares[1].first), proposal.type, qpi.tick(), - proposal.variableScalar.minSupportedValue + 4 - i % 5, 4, - proposal.variableScalar.minSupportedValue + 12 - i % 5, 2); + proposal.data.variableScalar.minSupportedValue + 4 - i % 5, 4, + proposal.data.variableScalar.minSupportedValue + 12 - i % 5, 2); } EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(shareholderShares[1].first), votingSummaryReturned)); EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 30); EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); - EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.variableScalar.minSupportedValue + 2 + 3); + EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.data.variableScalar.minSupportedValue + 2 + 3); // okay: scalar proposal with limited range - proposal.variableScalar.minValue = -1000; - proposal.variableScalar.maxValue = 1000; + proposal.data.variableScalar.minValue = -1000; + proposal.data.variableScalar.maxValue = 1000; setProposalWithSuccessCheck(qpi, pv, shareholderShares[5].first, proposal); EXPECT_EQ((int)qpi(*pv).proposalIndex(shareholderShares[5].first), 3); EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); @@ -2150,36 +2150,36 @@ void testProposalVotingShareholdersV1() // fail: test multi-option transfer proposal with invalid amounts proposal.type = QPI::ProposalTypes::TransferThreeAmounts; - proposal.transfer.destination = qpi.originator(); + proposal.data.transfer.destination = qpi.originator(); for (int i = 0; i < 4; ++i) { - proposal.transfer.amounts.setAll(0); - proposal.transfer.amounts.set(i, -100 * i - 1); + proposal.data.transfer.amounts.setAll(0); + proposal.data.transfer.amounts.set(i, -100 * i - 1); setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); } - proposal.transfer.amounts.set(0, 0); - proposal.transfer.amounts.set(1, 10); - proposal.transfer.amounts.set(2, 20); - proposal.transfer.amounts.set(3, 100); // for ProposalTypes::TransferThreeAmounts, fourth must be 0 + proposal.data.transfer.amounts.set(0, 0); + proposal.data.transfer.amounts.set(1, 10); + proposal.data.transfer.amounts.set(2, 20); + proposal.data.transfer.amounts.set(3, 100); // for ProposalTypes::TransferThreeAmounts, fourth must be 0 setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); // fail: duplicate options - proposal.transfer.amounts.setAll(0); + proposal.data.transfer.amounts.setAll(0); setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); // fail: options not sorted for (int i = 0; i < 3; ++i) - proposal.transfer.amounts.set(i, 100 - i); + proposal.data.transfer.amounts.set(i, 100 - i); setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); // okay: fill proposal storage - proposal.transfer.amounts.setAll(0); + proposal.data.transfer.amounts.setAll(0); ASSERT_EQ((int)pv->maxProposals, (int)shareholderShares.size() - 1); for (int i = 0; i < pv->maxProposals; ++i) { - proposal.transfer.amounts.set(0, i); - proposal.transfer.amounts.set(1, i * 2 + 1); - proposal.transfer.amounts.set(2, i * 3 + 2); + proposal.data.transfer.amounts.set(0, i); + proposal.data.transfer.amounts.set(1, i * 2 + 1); + proposal.data.transfer.amounts.set(2, i * 3 + 2); setProposalWithSuccessCheck(qpi, pv, shareholderShares[i].first, proposal); } EXPECT_EQ(countActiveProposals(qpi, pv), (int)pv->maxProposals); From cdf2f99aa9e49662450d3182bbdb244c859f0547 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:22:29 +0100 Subject: [PATCH 80/90] Update docs --- doc/contracts_proposals.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/contracts_proposals.md b/doc/contracts_proposals.md index 258c0002c..3e2edf590 100644 --- a/doc/contracts_proposals.md +++ b/doc/contracts_proposals.md @@ -120,7 +120,7 @@ struct QUTIL // Your code should set the state variable that the proposal is about to the accepted value. // This can be done as in this example taken from QUTIL: - switch (input.proposal.variableOptions.variable) + switch (input.proposal.data.variableOptions.variable) { case 0: state.smt1InvocationFee = input.acceptedValue; @@ -348,7 +348,7 @@ PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeShareholderStateVarProposals) if (locals.p.acceptedOption <= 0) continue; - locals.p.acceptedValue = locals.p.proposal.variableOptions.value; + locals.p.acceptedValue = locals.p.proposal.data.variableOptions.value; CALL(FinalizeShareholderProposalSetStateVar, locals.p, output); } @@ -376,8 +376,8 @@ PUBLIC_PROCEDURE(SetShareholderProposal) { // check proposal input and fees if (qpi.invocationReward() < setProposalFeeVarOrValue || (input.epoch - && (input.type != ProposalTypes::VariableYesNo || input.variableOptions.variable >= numFeeStateVariables - || input.variableOptions.value < 0))) + && (input.type != ProposalTypes::VariableYesNo || input.data.variableOptions.variable >= numFeeStateVariables + || input.data.variableOptions.value < 0))) { // error -> reimburse invocation reward qpi.transfer(qpi.invocator(), qpi.invocationReward()); From 1082e713bc7ef5a02498ea12bad13c4b8d10d0dd Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:38:45 +0100 Subject: [PATCH 81/90] Fix gcc/clang compatibility issue --- src/platform/uint128.h | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/platform/uint128.h b/src/platform/uint128.h index a55950d23..f3a81656d 100644 --- a/src/platform/uint128.h +++ b/src/platform/uint128.h @@ -21,17 +21,19 @@ #pragma once +#include + class uint128_t{ public: - unsigned __int64 low; - unsigned __int64 high; + uint64_t low; + uint64_t high; - uint128_t(unsigned __int64 n){ + uint128_t(uint64_t n){ low = n; high = 0; }; - uint128_t(unsigned __int64 i_high, unsigned __int64 i_low){ + uint128_t(uint64_t i_high, uint64_t i_low){ high = i_high; low = i_low; } @@ -71,7 +73,7 @@ class uint128_t{ } uint128_t operator<<(const uint128_t & rhs) const{ - const unsigned __int64 shift = rhs.low; + const uint64_t shift = rhs.low; if (((bool) rhs.high) || (shift >= 128)){ return uint128_t(0, 0); } @@ -98,7 +100,7 @@ class uint128_t{ } uint128_t operator>>(const uint128_t & rhs) const{ - const unsigned __int64 shift = rhs.low; + const uint64_t shift = rhs.low; if (((bool) rhs.high) || (shift >= 128)){ return uint128_t(0, 0); } @@ -143,18 +145,18 @@ class uint128_t{ } // bits - unsigned __int8 bits() const{ - unsigned __int8 out = 0; + uint8_t bits() const{ + uint8_t out = 0; if (high){ out = 64; - unsigned __int64 up = high; + uint64_t up = high; while (up){ up >>= 1; out++; } } else{ - unsigned __int64 inner_low = low; + uint64_t inner_low = low; while (inner_low){ inner_low >>= 1; out++; @@ -235,10 +237,10 @@ class uint128_t{ uint128_t operator*(const uint128_t& rhs) const{ // split values into 4 32-bit parts - unsigned __int64 top[4] = {high >> 32, high & 0xffffffff, low >> 32, low & 0xffffffff}; + uint64_t top[4] = {high >> 32, high & 0xffffffff, low >> 32, low & 0xffffffff}; - unsigned __int64 bottom[4] = {rhs.high >> 32, rhs.high & 0xffffffff, rhs.low >> 32, rhs.low & 0xffffffff}; - unsigned __int64 products[4][4]; + uint64_t bottom[4] = {rhs.high >> 32, rhs.high & 0xffffffff, rhs.low >> 32, rhs.low & 0xffffffff}; + uint64_t products[4][4]; // multiply each component of the values for(int y = 3; y > -1; y--){ @@ -248,10 +250,10 @@ class uint128_t{ } // first row - unsigned __int64 fourth32 = (products[0][3] & 0xffffffff); - unsigned __int64 third32 = (products[0][2] & 0xffffffff) + (products[0][3] >> 32); - unsigned __int64 second32 = (products[0][1] & 0xffffffff) + (products[0][2] >> 32); - unsigned __int64 first32 = (products[0][0] & 0xffffffff) + (products[0][1] >> 32); + uint64_t fourth32 = (products[0][3] & 0xffffffff); + uint64_t third32 = (products[0][2] & 0xffffffff) + (products[0][3] >> 32); + uint64_t second32 = (products[0][1] & 0xffffffff) + (products[0][2] >> 32); + uint64_t first32 = (products[0][0] & 0xffffffff) + (products[0][1] >> 32); // second row third32 += (products[1][3] & 0xffffffff); From 6c82d68dc6cf4dd8a073099f891f700a5b419c83 Mon Sep 17 00:00:00 2001 From: cyber-pc Date: Mon, 2 Feb 2026 21:34:35 +0700 Subject: [PATCH 82/90] Allow excluding Oracle Machine nodes from peers cleanup. --- src/network_core/peers.h | 12 ++++++++++++ src/qubic.cpp | 18 +++--------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/network_core/peers.h b/src/network_core/peers.h index a7bdaf43f..92d415234 100644 --- a/src/network_core/peers.h +++ b/src/network_core/peers.h @@ -317,6 +317,18 @@ static void closePeer(Peer* peer, int closeGracefullyRetries = 0) } } +// Closes all peer connections; optionally includes Oracle Machine nodes. +static void closeAllPeers(bool closeOM = false) +{ + for (unsigned int i = 0; i < NUMBER_OF_OUTGOING_CONNECTIONS + NUMBER_OF_INCOMING_CONNECTIONS; i++) + { + if (closeOM || !peers[i].isOracleMachineNode()) + { + closePeer(&peers[i]); + } + } +} + // Add message to sending buffer of specific peer, can only called from main thread (not thread-safe). static void push(Peer* peer, RequestResponseHeader* requestResponseHeader) { diff --git a/src/qubic.cpp b/src/qubic.cpp index aa11d2ecd..4848e1610 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -6935,10 +6935,7 @@ static void processKeyPresses() case 0x0E: { logToConsole(L"Pressed F4 key"); - for (unsigned int i = 0; i < NUMBER_OF_OUTGOING_CONNECTIONS + NUMBER_OF_INCOMING_CONNECTIONS; i++) - { - closePeer(&peers[i]); - } + closeAllPeers(); } break; @@ -7585,13 +7582,7 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) if (forceRefreshPeerList) { forceRefreshPeerList = false; - for (unsigned int i = 0; i < NUMBER_OF_OUTGOING_CONNECTIONS + NUMBER_OF_INCOMING_CONNECTIONS; i++) - { - if (!peers[i].isOracleMachineNode()) - { - closePeer(&peers[i]); - } - } + closeAllPeers(); } processKeyPresses(); @@ -7630,10 +7621,7 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) { // Saving node state takes a lot of time -> Close peer connections before to signal that // the peers should connect to another node. - for (unsigned int i = 0; i < NUMBER_OF_OUTGOING_CONNECTIONS + NUMBER_OF_INCOMING_CONNECTIONS; i++) - { - closePeer(&peers[i]); - } + closeAllPeers(true); logToConsole(L"Saving node state..."); saveAllNodeStates(); From 6d7d52cecc27706c9017f4e5644b4a7ce3ad015b Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:19:47 +0100 Subject: [PATCH 83/90] Implement other RequestOracleData::request*QueryIdsByTick --- src/oracle_core/net_msg_impl.h | 37 +++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index eec7d8747..373c4e872 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -32,7 +32,24 @@ void OracleEngine::processRequestOracleData(Peer* peer, R { case RequestOracleData::requestAllQueryIdsByTick: + case RequestOracleData::requestUserQueryIdsByTick: + case RequestOracleData::requestContractDirectQueryIdsByTick: + case RequestOracleData::requestContractSubscriptionQueryIdsByTick: { + // select filter function for the request type + static_assert(RequestOracleData::requestAllQueryIdsByTick == 0); + static_assert(RequestOracleData::requestUserQueryIdsByTick == 1); + static_assert(RequestOracleData::requestContractDirectQueryIdsByTick == 2); + static_assert(RequestOracleData::requestContractSubscriptionQueryIdsByTick == 3); + typedef bool (*FilterFunc)(const OracleQueryMetadata& oqm); + const FilterFunc allFilterFunc[] = { + nullptr, + [](const OracleQueryMetadata& oqm) { return oqm.type == ORACLE_QUERY_TYPE_USER_QUERY; }, + [](const OracleQueryMetadata& oqm) { return oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY; }, + [](const OracleQueryMetadata& oqm) { return oqm.type == ORACLE_QUERY_TYPE_CONTRACT_SUBSCRIPTION; }, + }; + const FilterFunc filterFunc = allFilterFunc[request->reqType]; + // send query IDs of queries of a given tick, splitting the array in multiple messages if needed if (request->reqTickOrId >= UINT32_MAX) break; @@ -45,9 +62,13 @@ void OracleEngine::processRequestOracleData(Peer* peer, R do { unsigned int idxInMsg = 0; - for (; idxInMsg < maxQueryIdCount && moreQueries; ++idxInMsg) + while (idxInMsg < maxQueryIdCount && moreQueries) { - payloadQueryIds[idxInMsg] = queries[queryIndex].queryId; + if (!filterFunc || filterFunc(queries[queryIndex])) + { + payloadQueryIds[idxInMsg] = queries[queryIndex].queryId; + ++idxInMsg; + } ++queryIndex; moreQueries = queryIndex < oracleQueryCount && queries[queryIndex].queryTick == tick; } @@ -57,18 +78,6 @@ void OracleEngine::processRequestOracleData(Peer* peer, R break; } - case RequestOracleData::requestUserQueryIdsByTick: - // TODO - break; - - case RequestOracleData::requestContractDirectQueryIdsByTick: - // TODO - break; - - case RequestOracleData::requestContractSubscriptionQueryIdsByTick: - // TODO - break; - case RequestOracleData::requestPendingQueryIds: { // send query IDs of pending queries, splitting the array in multiple messages if needed From 1f4a4997ae0bb87ce3d005a61750751acd9b991e Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:03:30 +0100 Subject: [PATCH 84/90] Discard oracle reply if OM node sets error flag --- src/oracle_core/oracle_engine.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index b9b261db6..7bdebd734 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -616,8 +616,13 @@ class OracleEngine if (oqm.status != ORACLE_QUERY_STATUS_PENDING) return; - // update query status flags - oqm.statusFlags |= (replyMessage->oracleMachineErrorFlags & ORACLE_FLAG_OM_ERROR_FLAGS); + // check error flags + uint16_t errorFlags = (replyMessage->oracleMachineErrorFlags & ORACLE_FLAG_OM_ERROR_FLAGS); + if (errorFlags != 0) + { + oqm.statusFlags |= errorFlags; + return; + } // check reply size vs size expected by interface ASSERT(oqm.interfaceIndex < OI::oracleInterfacesCount); From 87bd0b6a5bc7f2e6ca56100e598b0ea1fbd42174 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:36:05 +0100 Subject: [PATCH 85/90] Add debug output to failure conditions of QUERY_ORACLE --- src/contract_core/qpi_oracle_impl.h | 6 ++++++ src/oracle_core/oracle_engine.h | 30 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/contract_core/qpi_oracle_impl.h b/src/contract_core/qpi_oracle_impl.h index a9244d938..35eaccd4e 100644 --- a/src/contract_core/qpi_oracle_impl.h +++ b/src/contract_core/qpi_oracle_impl.h @@ -54,6 +54,12 @@ QPI::sint64 QPI::QpiContextProcedureCall::__qpiQueryOracle( return queryId; } } +#if !defined(NDEBUG) && !defined(NO_UEFI) + else + { + addDebugMessage(L"Cannot start contract oracle query due to fee issue!"); + } +#endif // notify about error (status and queryId are 0, indicating that an error happened before sending query) auto* state = (ContractStateType*)contractStates[contractIndex]; diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 7bdebd734..411dd91f7 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -464,24 +464,44 @@ class OracleEngine { // check inputs if (contractIndex >= MAX_NUMBER_OF_CONTRACTS || interfaceIndex >= OI::oracleInterfacesCount || querySize != OI::oracleInterfaces[interfaceIndex].querySize) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start contract oracle query due to contractIndex / oracleInterface issue!"); +#endif return -1; + } // lock for accessing engine data LockGuard lockGuard(lock); // check that still have free capacity for the query if (oracleQueryCount >= MAX_ORACLE_QUERIES || pendingQueryIndices.numValues >= MAX_SIMULTANEOUS_ORACLE_QUERIES || queryStorageBytesUsed + querySize > ORACLE_QUERY_STORAGE_SIZE) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start contract oracle query due to lack of space!"); +#endif return -1; + } // find slot storing temporary reply state uint32_t replyStateSlotIdx = getEmptyReplyStateSlot(); if (replyStateSlotIdx >= MAX_SIMULTANEOUS_ORACLE_QUERIES) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start contract oracle query due to lack of free reply slot!"); +#endif return -1; + } // compute timeout as absolute point in time auto timeout = QPI::DateAndTime::now(); if (!timeout.addMillisec(timeoutMillisec)) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start contract oracle query due to timeout timestamp issue!"); +#endif return -1; + } // get sequential query index of contract in tick auto& cs = contractQueryIdState; @@ -493,7 +513,12 @@ class OracleEngine else { if (cs.queryIndexInTick >= 0x7FFFFFFF) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start contract oracle query due to queryId issue!"); +#endif return -1; + } ++cs.queryIndexInTick; } @@ -504,7 +529,12 @@ class OracleEngine // map ID to index ASSERT(!queryIdToIndex->contains(queryId)); if (queryIdToIndex->set(queryId, oracleQueryCount) == QPI::NULL_INDEX) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start contract oracle query due to queryIdToIndex issue!"); +#endif return -1; + } // register index of pending query pendingQueryIndices.add(oracleQueryCount); From 95a9dda92c8da6c285cbb32c124b30038e23ce6d Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:39:49 +0100 Subject: [PATCH 86/90] TESTEXC: Improve oracle testing - test multiple queries in one tick - increase timeouts - emit log events in END_TICK - replace half of coingecko queries by mock price queries --- src/contracts/TestExampleC.h | 48 +++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index c2e434cd5..b591f27b7 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -332,30 +332,56 @@ struct TESTEXC : public ContractBase OI::Price::OracleQuery priceOracleQuery; OI::Mock::OracleQuery mockOracleQuery; sint64 oracleQueryId; + uint32 c; + NotificationLog notificationLog; }; END_TICK_WITH_LOCALS() { // Query oracles - if (qpi.tick() % 10 == 0) + if (qpi.tick() % 11 == 1) { - // Setup query (in extra scope limit scope of using namespace Ch + for (locals.c = (qpi.tick() % 5) + 1; locals.c > 0; --locals.c) { - using namespace Ch; - locals.priceOracleQuery.oracle = OI::Price::getMockOracleId(); - locals.priceOracleQuery.currency1 = id(B, T, C, null, null); - locals.priceOracleQuery.currency2 = id(U, S, D, null, null); - locals.priceOracleQuery.timestamp = qpi.now(); + // Setup query (in extra scope limit scope of using namespace Ch + if (locals.c % 3 == 0) + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getMockOracleId(); + locals.priceOracleQuery.currency1 = id(B, T, C, null, null); + locals.priceOracleQuery.currency2 = id(U, S, D, null, null); + locals.priceOracleQuery.timestamp = qpi.now(); + } + else if (locals.c % 3 == 1) + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getMockOracleId(); + locals.priceOracleQuery.currency1 = id(B, T, C, null, null); + locals.priceOracleQuery.currency2 = id(E, T, H, null, null); + locals.priceOracleQuery.timestamp = qpi.now(); + } + else + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getCoingeckoOracleId(); + locals.priceOracleQuery.currency1 = id(B, T, C, null, null); + locals.priceOracleQuery.currency2 = id(U, S, D, T, null); + locals.priceOracleQuery.timestamp = qpi.now(); + } + + locals.oracleQueryId = QUERY_ORACLE(OI::Price, locals.priceOracleQuery, NotifyPriceOracleReply, 60000); + ASSERT(qpi.getOracleQueryStatus(locals.oracleQueryId) == ORACLE_QUERY_STATUS_PENDING); + + locals.notificationLog = NotificationLog{ CONTRACT_INDEX, OI::Price::oracleInterfaceIndex, ORACLE_QUERY_STATUS_PENDING, 0, 0, locals.oracleQueryId }; } - - locals.oracleQueryId = QUERY_ORACLE(OI::Price, locals.priceOracleQuery, NotifyPriceOracleReply, 20000); - ASSERT(qpi.getOracleQueryStatus(locals.oracleQueryId) == ORACLE_QUERY_STATUS_PENDING); } if (qpi.tick() % 2 == 1) { locals.mockOracleQuery.value = qpi.tick(); - QUERY_ORACLE(OI::Mock, locals.mockOracleQuery, NotifyMockOracleReply, 8000); + QUERY_ORACLE(OI::Mock, locals.mockOracleQuery, NotifyMockOracleReply, 60000); + locals.notificationLog = NotificationLog{ CONTRACT_INDEX, OI::Mock::oracleInterfaceIndex, ORACLE_QUERY_STATUS_PENDING, 0, qpi.tick(), locals.oracleQueryId}; } + LOG_INFO(locals.notificationLog); } //--------------------------------------------------------------- From 5f239299beb8ca6d5524f3129a2d35b050e9d4f1 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:30:39 +0100 Subject: [PATCH 87/90] Prevent that test contracts' fee reserve runs empty --- src/contract_core/contract_def.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 681348687..3b97da099 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -482,10 +482,10 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXD); // fill execution fee reserves for test contracts - setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 10000); - setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 10000); - setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 10000); - setContractFeeReserve(TESTEXD_CONTRACT_INDEX, 10000); + setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 100000000000); + setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 100000000000); + setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 100000000000); + setContractFeeReserve(TESTEXD_CONTRACT_INDEX, 100000000000); #endif } From 2f7ae30c96d92925b4fb8b587f48de12ebab2472 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:32:13 +0100 Subject: [PATCH 88/90] Improve contract status error output to console --- src/qubic.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/qubic.cpp b/src/qubic.cpp index 4848e1610..0e808da19 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -6623,7 +6623,7 @@ static void logHealthStatus() appendText(message, L"Contract #"); appendNumber(message, i, FALSE); appendText(message, L": "); - const CHAR16* errorMsg = L"Unknown error"; + const CHAR16* errorMsg = nullptr; switch (contractError[i]) { // The alloc failures can be fixed by increasing the size of ContractLocalsStack @@ -6635,8 +6635,12 @@ static void logHealthStatus() case ContractErrorTooManyActions: errorMsg = L"TooManyActions"; break; // Timeout requires to remove endless loop, speed-up code, or change the timeout case ContractErrorTimeout: errorMsg = L"Timeout"; break; + case ContractErrorIPOFailed: errorMsg = L"IPO failed"; break; } - appendText(message, errorMsg); + if (errorMsg) + appendText(message, errorMsg); + else + appendNumber(message, contractError[i], FALSE); } } if (!anyContractError) From add444b383375b00446aeb2c867805288748e07a Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:00:48 +0100 Subject: [PATCH 89/90] Increase maxQueryIdCount in RespondOracleData --- src/oracle_core/net_msg_impl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index 373c4e872..7435a73ab 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -14,7 +14,7 @@ void OracleEngine::processRequestOracleData(Peer* peer, R return; // prepare buffer - constexpr int maxQueryIdCount = 2; // TODO: increase value after testing + constexpr int maxQueryIdCount = 128; constexpr int payloadBufferSize = math_lib::max((int)math_lib::max(MAX_ORACLE_QUERY_SIZE, MAX_ORACLE_REPLY_SIZE), maxQueryIdCount * 8); static_assert(payloadBufferSize >= sizeof(RespondOracleDataQueryMetadata), "Buffer too small."); static_assert(payloadBufferSize < 32 * 1024, "Large alloc in stack may need reconsideration."); From 29493b25f8121a7f676ea9d0956bce6de85308f0 Mon Sep 17 00:00:00 2001 From: Philipp Werner <22914157+philippwerner@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:26:33 +0100 Subject: [PATCH 90/90] Refund oracle query fees if query cannot be started --- src/contract_core/qpi_oracle_impl.h | 4 ++++ src/oracle_core/oracle_engine.h | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/contract_core/qpi_oracle_impl.h b/src/contract_core/qpi_oracle_impl.h index 35eaccd4e..f9140d4fa 100644 --- a/src/contract_core/qpi_oracle_impl.h +++ b/src/contract_core/qpi_oracle_impl.h @@ -53,6 +53,10 @@ QPI::sint64 QPI::QpiContextProcedureCall::__qpiQueryOracle( // success return queryId; } + else + { + oracleEngine.refundFees(_currentContractId, fee); + } } #if !defined(NDEBUG) && !defined(NO_UEFI) else diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index 411dd91f7..b738dc9e8 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -592,6 +592,14 @@ class OracleEngine return queryId; } + // Refund fees if an error occured while trying to start an oracle query + static void refundFees(const m256i& sourcePublicKey, int64_t refundAmount) + { + increaseEnergy(sourcePublicKey, refundAmount); + const QuTransfer quTransfer = { m256i::zero(), sourcePublicKey, refundAmount }; + logger.logQuTransfer(quTransfer); + } + protected: // Enqueue oracle machine query message. Cannot be run concurrently. Caller must acquire engine lock! void enqueueOracleQuery(int64_t queryId, uint32_t interfaceIdx, uint32_t timeoutMillisec, const void* queryData, uint16_t querySize)