diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index 6191e509c..ed8054357 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -341,10 +341,10 @@ struct TESTEXC : public ContractBase // Query oracles if (qpi.tick() % 11 == 1) { - for (locals.c = (qpi.tick() % 5) + 1; locals.c > 0; --locals.c) + locals.c = (qpi.tick() / 11) % 8; { // Setup query (in extra scope limit scope of using namespace Ch - if (locals.c % 3 == 0) + if (locals.c == 0) { using namespace Ch; locals.priceOracleQuery.oracle = OI::Price::getMockOracleId(); @@ -352,15 +352,15 @@ struct TESTEXC : public ContractBase locals.priceOracleQuery.currency2 = id(U, S, D, null, null); locals.priceOracleQuery.timestamp = qpi.now(); } - else if (locals.c % 3 == 1) + else if (locals.c == 1) { using namespace Ch; - locals.priceOracleQuery.oracle = OI::Price::getMockOracleId(); + locals.priceOracleQuery.oracle = OI::Price::getBinanceOracleId(); locals.priceOracleQuery.currency1 = id(B, T, C, null, null); locals.priceOracleQuery.currency2 = id(E, T, H, null, null); locals.priceOracleQuery.timestamp = qpi.now(); } - else + else if (locals.c == 2) { using namespace Ch; locals.priceOracleQuery.oracle = OI::Price::getCoingeckoOracleId(); @@ -368,20 +368,64 @@ struct TESTEXC : public ContractBase locals.priceOracleQuery.currency2 = id(U, S, D, T, null); locals.priceOracleQuery.timestamp = qpi.now(); } + else if (locals.c == 3) + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getGateOracleId(); + locals.priceOracleQuery.currency1 = id(B, T, C, null, null); + locals.priceOracleQuery.currency2 = id(U, S, D, T, null); + locals.priceOracleQuery.timestamp = qpi.now(); + } + else if (locals.c == 4) + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getMexcOracleId(); + locals.priceOracleQuery.currency1 = id(B, T, C, null, null); + locals.priceOracleQuery.currency2 = id(U, S, D, T, null); + locals.priceOracleQuery.timestamp = qpi.now(); + } + else if (locals.c == 5) + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getBinanceGateOracleId(); + locals.priceOracleQuery.currency1 = id(B, T, C, null, null); + locals.priceOracleQuery.currency2 = id(U, S, D, T, null); + locals.priceOracleQuery.timestamp = qpi.now(); + } + else if (locals.c == 6) + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getBinanceMexcOracleId(); + locals.priceOracleQuery.currency1 = id(B, T, C, null, null); + locals.priceOracleQuery.currency2 = id(U, S, D, T, null); + locals.priceOracleQuery.timestamp = qpi.now(); + } + else if (locals.c == 7) + { + using namespace Ch; + locals.priceOracleQuery.oracle = OI::Price::getGateMexcOracleId(); + 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 }; + LOG_INFO(locals.notificationLog); } } if (qpi.tick() % 2 == 1) { - locals.mockOracleQuery.value = qpi.tick(); - QUERY_ORACLE(OI::Mock, locals.mockOracleQuery, NotifyMockOracleReply, 20000); - locals.notificationLog = NotificationLog{ CONTRACT_INDEX, OI::Mock::oracleInterfaceIndex, ORACLE_QUERY_STATUS_PENDING, 0, qpi.tick(), locals.oracleQueryId}; + for (locals.c = 0; locals.c < (qpi.tick() / 2) % 15; ++locals.c) + { + locals.mockOracleQuery.value = uint64(qpi.tick()) * (locals.c + 1); + QUERY_ORACLE(OI::Mock, locals.mockOracleQuery, NotifyMockOracleReply, 20000); + locals.notificationLog = NotificationLog{ CONTRACT_INDEX, OI::Mock::oracleInterfaceIndex, ORACLE_QUERY_STATUS_PENDING, 0, (sint64)locals.mockOracleQuery.value, locals.oracleQueryId }; + LOG_INFO(locals.notificationLog); + } } - LOG_INFO(locals.notificationLog); } //--------------------------------------------------------------- diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index d3c029e69..74236cfbd 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -77,6 +77,7 @@ namespace QPI { null = 0, space = ' ', slash = '/', backslash = '\\', dot = '.', comma = ',', colon = ':', semicolon = ';', + underscore = '_', minus = '-', plus = '+', star = '*', dollar = '$', question_mark = '?', exclamation_mark = '!', 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', diff --git a/src/network_messages/oracles.h b/src/network_messages/oracles.h index 90a224d14..c390b584b 100644 --- a/src/network_messages/oracles.h +++ b/src/network_messages/oracles.h @@ -76,6 +76,9 @@ struct RespondOracleData // The payload is an array of 676 revenue points (one per computer, each 8 bytes). static constexpr unsigned int respondOracleRevenuePoints = 8; + // The payload is RespondOracleDataValidTickRange. + static constexpr unsigned int respondTickRange = 9; + // type of oracle response unsigned int resType; }; @@ -152,3 +155,10 @@ struct RespondOracleDataQueryStatistics /// Total number of commit with correct digest but wrong knowledge proof uint64_t wrongKnowledgeProofCount; }; + +// Core node responds with this if the requested query tick is outside the range of ticks with available query info. +struct RespondOracleDataValidTickRange +{ + uint32_t firstTick; + uint32_t currentTick; +}; diff --git a/src/oracle_core/net_msg_impl.h b/src/oracle_core/net_msg_impl.h index 76e798827..f29a2ddff 100644 --- a/src/oracle_core/net_msg_impl.h +++ b/src/oracle_core/net_msg_impl.h @@ -38,6 +38,19 @@ void OracleEngine::processRequestOracleData(Peer* peer, R case RequestOracleData::requestContractDirectQueryIdsByTick: case RequestOracleData::requestContractSubscriptionQueryIdsByTick: { + // check if data for tick is available + if (request->reqTickOrId < system.initialTick || request->reqTickOrId >= system.tick) // TODO: > or >= + { + // data isn't available -> send RespondOracleDataValidTickRange message + response->resType = RespondOracleData::respondTickRange; + auto* payloadTickRange = (RespondOracleDataValidTickRange*)payload; + payloadTickRange->firstTick = system.initialTick; + payloadTickRange->currentTick = system.tick; + enqueueResponse(peer, sizeof(RespondOracleData) + sizeof(RespondOracleDataValidTickRange), + RespondOracleData::type(), header->dejavu(), response); + break; + } + // select filter function for the request type static_assert(RequestOracleData::requestAllQueryIdsByTick == 0); static_assert(RequestOracleData::requestUserQueryIdsByTick == 1); diff --git a/src/oracle_core/oracle_engine.h b/src/oracle_core/oracle_engine.h index ba0fc583e..f4e5ebef9 100644 --- a/src/oracle_core/oracle_engine.h +++ b/src/oracle_core/oracle_engine.h @@ -335,7 +335,7 @@ class OracleEngine for (uint32_t i = 0; i < MAX_SIMULTANEOUS_ORACLE_QUERIES; ++i) { if (replyStates[replyStatesIndex].queryId <= 0) - return replyStatesIndex++; + return replyStatesIndex; ++replyStatesIndex; if (replyStatesIndex >= MAX_SIMULTANEOUS_ORACLE_QUERIES) @@ -455,19 +455,149 @@ class OracleEngine * Check and start user query based on transaction (should be called from tick processor). * @param tx Transaction, whose validity and signature has been checked before. * @param txIndex Index of tx in tick data, for referencing in tick storage. - * @return Query ID or zero on error. + * @return Query ID or -1 on error. */ int64_t startUserQuery(const OracleUserQueryTransactionPrefix* tx, uint32_t txIndex) { - // ASSERT that tx is in tick storage at tx->tick, txIndex. - // check interface index - // check size of payload vs expected query of given interface + // check preconditions (that function is used correctly) + ASSERT(tx); + ASSERT(tx->checkValidity()); + ASSERT(isZero(tx->destinationPublicKey)); + ASSERT(tx->tick == system.tick); + ASSERT(txIndex < NUMBER_OF_TRANSACTIONS_PER_TICK); + { + // check that tick storage contains tx at expected position + ASSERT(ts.tickInCurrentEpochStorage(tx->tick)); + const uint64_t* tsTickTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(tx->tick); + const auto* tsTx = ts.tickTransactions.ptr(tsTickTransactionOffsets[txIndex]); + ASSERT(compareMem(tx, tsTx, tx->totalSize()) == 0); + } + + // check size of tx / query data and validity of interface index + const uint16_t querySize = tx->inputSize - OracleUserQueryTransactionPrefix::minInputSize(); + if (tx->inputSize < OracleUserQueryTransactionPrefix::minInputSize() + || tx->oracleInterfaceIndex >= OI::oracleInterfacesCount + || querySize != OI::oracleInterfaces[tx->oracleInterfaceIndex].querySize) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start user oracle query due to inputSize / oracleInterface issue!"); +#endif + return -1; + } + + // check fee + const void* queryData = (tx + 1); + const int64_t fee = OI::getOracleQueryFeeFunc[tx->oracleInterfaceIndex](queryData); + if (tx->amount < fee) + { + // tx amount insufficient for fee -> return error (caller should refund in all error cases) +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start user oracle query due to insufficient invocation reward!"); +#endif + return -1; + } // lock for accessing engine data LockGuard lockGuard(lock); - // add to query storage - // send query to oracle machine node + // check that we still have free capacity for the query + if (oracleQueryCount >= MAX_ORACLE_QUERIES || pendingQueryIndices.numValues >= MAX_SIMULTANEOUS_ORACLE_QUERIES) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start user 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 user 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 (tx->timeoutMilliseconds > MAX_ORACLE_TIMEOUT_MILLISEC || !timeout.addMillisec(tx->timeoutMilliseconds)) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Cannot start user oracle query due to timeout timestamp issue!"); +#endif + return -1; + } + + // compose query ID + int64_t queryId = ((int64_t)system.tick << 31) | txIndex; + ASSERT(txIndex < (1ull << 31)); + + // 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 user oracle query due to queryIdToIndex issue!"); +#endif + return -1; + } + + // register index of pending query + pendingQueryIndices.add(oracleQueryCount); + + // init query metadata (persistent) + auto& queryMetadata = queries[oracleQueryCount++]; + queryMetadata.queryId = queryId; + queryMetadata.type = ORACLE_QUERY_TYPE_USER_QUERY; + queryMetadata.status = ORACLE_QUERY_STATUS_PENDING; + queryMetadata.statusFlags = ORACLE_FLAG_REPLY_PENDING; + queryMetadata.interfaceIndex = tx->oracleInterfaceIndex; + queryMetadata.queryTick = system.tick; + queryMetadata.timeout = timeout; + queryMetadata.typeVar.user.queryingEntity = tx->sourcePublicKey; + queryMetadata.typeVar.user.queryTxIndex = txIndex; + 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; + + // enqueue query message to oracle machine node + enqueueOracleQuery(queryId, tx->oracleInterfaceIndex, tx->timeoutMilliseconds, queryData, querySize); + + // log status change + OracleQueryStatusChange logEvent{ tx->sourcePublicKey, queryId, tx->oracleInterfaceIndex, queryMetadata.type, queryMetadata.status }; + logger.logOracleQueryStatusChange(logEvent); + + // Debug logging +#if ENABLE_ORACLE_STATS_RECORD + oracleStats[queryMetadata.interfaceIndex].queryCount++; +#endif + +#if !defined(NDEBUG) && !defined(NO_UEFI) + CHAR16 dbgMsg[200]; + setText(dbgMsg, L"oracleEngine.startUserQuery(), tick "); + appendNumber(dbgMsg, system.tick, FALSE); + appendText(dbgMsg, ", queryId "); + appendNumber(dbgMsg, queryId, FALSE); + appendText(dbgMsg, ", interfaceIndex "); + appendNumber(dbgMsg, tx->oracleInterfaceIndex, FALSE); + appendText(dbgMsg, ", timeout "); + appendDateAndTime(dbgMsg, timeout); + appendText(dbgMsg, ", now "); + appendDateAndTime(dbgMsg, QPI::DateAndTime::now()); + addDebugMessage(dbgMsg); +#endif + + if (tx->amount > fee) + { + // success case: refund the amount that has been paid too much (error case refund is handled by caller) + refundFees(tx->sourcePublicKey, tx->amount - fee); + } + + return queryId; } int64_t startContractQuery(uint16_t contractIndex, uint32_t interfaceIndex, @@ -724,10 +854,8 @@ class OracleEngine // Update the stats for each type of oracles const uint32_t ifaceIdx = oqm.interfaceIndex; oracleStats[ifaceIdx].replyCount++; - // Now only record contract query - if (oqm.type == ORACLE_QUERY_TYPE_CONTRACT_QUERY) { - const void* queryData = queryStorage + oqm.typeVar.contract.queryStorageOffset; + const void* queryData = getOracleQueryPointerFromMetadata(oqm, (uint16_t)OI::oracleInterfaces[ifaceIdx].querySize); bool valid = false; switch (ifaceIdx) @@ -1120,7 +1248,8 @@ class OracleEngine continue; // check if local view is the quorum view - const m256i quorumCommitDigest = replyState.replyCommitDigests[replyState.replyCommitHistogramIdx[replyState.mostCommitsHistIdx]]; + const uint16_t mostCommitsDigestIdx = replyState.replyCommitHistogramIdx[replyState.mostCommitsHistIdx]; + const m256i quorumCommitDigest = replyState.replyCommitDigests[mostCommitsDigestIdx]; if (quorumCommitDigest != replyState.ownReplyDigest) continue; @@ -1208,7 +1337,8 @@ class OracleEngine KangarooTwelve(replyData, replySize, revealDigest.m256i_u8, 32); // check that revealed reply matches the quorum digest - const m256i quorumCommitDigest = replyState.replyCommitDigests[replyState.replyCommitHistogramIdx[replyState.mostCommitsHistIdx]]; + const uint16_t mostCommitsDigestIdx = replyState.replyCommitHistogramIdx[replyState.mostCommitsHistIdx]; + const m256i quorumCommitDigest = replyState.replyCommitDigests[mostCommitsDigestIdx]; ASSERT(!isZero(quorumCommitDigest)); if (revealDigest != quorumCommitDigest) return nullptr; @@ -1317,8 +1447,10 @@ class OracleEngine copyMem(replyData, transaction + 1, replySize); uint16_t* replyDataCompIdx = (uint16_t*)(replyData + replySize); - const auto quorumHistIdx = replyState->replyCommitHistogramIdx[replyState->mostCommitsHistIdx]; - const m256i quorumCommitDigest = replyState->replyCommitDigests[quorumHistIdx]; + // check digests and knowledge proofs + const uint16_t quorumHistIdx = replyState->mostCommitsHistIdx; + const uint16_t quorumCommitDigestIdx = replyState->replyCommitHistogramIdx[quorumHistIdx]; + const m256i quorumCommitDigest = replyState->replyCommitDigests[quorumCommitDigestIdx]; for (int computorIdx = 0; computorIdx < NUMBER_OF_COMPUTORS; ++computorIdx) { // check that commit digest matches quorum @@ -1573,6 +1705,36 @@ class OracleEngine } protected: + // Caller is responsible for locking. + const void* getOracleQueryPointerFromMetadata(const OracleQueryMetadata& queryMetadata, uint16_t querySize) const + { + 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); + return queryStorage + offset; + } + // TODO: support subscription + case ORACLE_QUERY_TYPE_USER_QUERY: + { + const uint32_t txSlotInTickData = queryMetadata.typeVar.user.queryTxIndex; + ASSERT(txSlotInTickData < NUMBER_OF_TRANSACTIONS_PER_TICK); + ASSERT(ts.tickInCurrentEpochStorage(queryMetadata.queryTick)); + const uint64_t* tsTickTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(queryMetadata.queryTick); + const auto* tx = (OracleUserQueryTransactionPrefix*)ts.tickTransactions.ptr(tsTickTransactionOffsets[txSlotInTickData]); + ASSERT(queryMetadata.interfaceIndex == tx->oracleInterfaceIndex); + ASSERT(tx->inputSize - OracleUserQueryTransactionPrefix::minInputSize() == querySize); + if (tx->inputSize - OracleUserQueryTransactionPrefix::minInputSize() != querySize) + return nullptr; + return tx + 1; + } + default: + return nullptr; + } + } + // Caller is responsible for locking. bool getOracleQueryWithoutLocking(int64_t queryId, void* queryData, uint16_t querySize) const { @@ -1587,22 +1749,14 @@ class OracleEngine if (querySize != OI::oracleInterfaces[queryMetadata.interfaceIndex].querySize) return false; - void* querySrcPtr = nullptr; - switch (queryMetadata.type) + // get raw pointer to query data + const void* querySrcPtr = getOracleQueryPointerFromMetadata(queryMetadata, querySize); + if (!querySrcPtr) { - 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 false; } - // Return query data + // return query data (by copying to provided buffer) copyMem(queryData, querySrcPtr, querySize); return true; } @@ -1711,10 +1865,20 @@ class OracleEngine // 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? + ASSERT(ts.tickInCurrentEpochStorage(oqm.queryTick)); + const uint64_t* tsTickTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(oqm.queryTick); + const auto* tx = (OracleUserQueryTransactionPrefix*)ts.tickTransactions.ptr(tsTickTransactionOffsets[oqm.typeVar.user.queryTxIndex]); + ASSERT(tx->oracleInterfaceIndex == oqm.interfaceIndex); + ASSERT(tx->sourcePublicKey == oqm.typeVar.user.queryingEntity); + ASSERT(isZero(tx->destinationPublicKey)); + ASSERT(tx->tick == oqm.queryTick); + ASSERT(tx->inputType == OracleUserQueryTransactionPrefix::transactionType()); + ASSERT(tx->inputSize == OI::oracleInterfaces[oqm.interfaceIndex].querySize + OracleUserQueryTransactionPrefix::minInputSize()); break; + } default: #if !defined(NDEBUG) && !defined(NO_UEFI) setText(dbgMsgBuf, L"Unexpected oracle query type "); @@ -1741,8 +1905,9 @@ class OracleEngine ASSERT(replyState->totalCommits <= NUMBER_OF_COMPUTORS); break; case ORACLE_QUERY_STATUS_UNRESOLVABLE: - case ORACLE_QUERY_STATUS_TIMEOUT: ASSERT(oqm.statusVar.failure.agreeingCommits < QUORUM); + // no break by intention + case ORACLE_QUERY_STATUS_TIMEOUT: ASSERT(oqm.statusVar.failure.totalCommits <= NUMBER_OF_COMPUTORS); ASSERT(oqm.statusVar.failure.agreeingCommits <= oqm.statusVar.failure.totalCommits); break; @@ -1766,7 +1931,8 @@ class OracleEngine break; case ORACLE_QUERY_STATUS_UNRESOLVABLE: ++unresolvableCount; - ASSERT(oqm.statusVar.failure.totalCommits - oqm.statusVar.failure.agreeingCommits > NUMBER_OF_COMPUTORS - QUORUM); + if (!(oqm.statusFlags & ORACLE_FLAG_FAKE_COMMITS)) + ASSERT(oqm.statusVar.failure.totalCommits - oqm.statusVar.failure.agreeingCommits > NUMBER_OF_COMPUTORS - QUORUM); break; case ORACLE_QUERY_STATUS_TIMEOUT: ++timeoutCount; @@ -1911,7 +2077,7 @@ class OracleEngine appendNumber(message, queryStorageBytesUsed * 100 / ORACLE_QUERY_STORAGE_SIZE, FALSE); appendText(message, "%"); appendQuotientWithOneDecimal(message, stats.revealTxCount, stats.successCount); - appendText(message, " reveal tx per success, wrong knowledge proofs"); + appendText(message, " reveal tx per success, wrong knowledge proofs "); appendNumber(message, stats.wrongKnowledgeProofCount, FALSE); logToConsole(message); diff --git a/src/oracle_core/oracle_interfaces_def.h b/src/oracle_core/oracle_interfaces_def.h index 1e6c72793..e1db23125 100644 --- a/src/oracle_core/oracle_interfaces_def.h +++ b/src/oracle_core/oracle_interfaces_def.h @@ -13,19 +13,51 @@ namespace OI #include "oracle_interfaces/Mock.h" #undef ORACLE_INTERFACE_INDEX -#define REGISTER_ORACLE_INTERFACE(Interface) {sizeof(Interface::OracleQuery), sizeof(Interface::OracleReply)} +#define DEFINE_ORACLE_INTERFACE(Interface) {sizeof(Interface::OracleQuery), sizeof(Interface::OracleReply)} constexpr struct { unsigned long long querySize; unsigned long long replySize; } oracleInterfaces[] = { - REGISTER_ORACLE_INTERFACE(Price), - REGISTER_ORACLE_INTERFACE(Mock), + DEFINE_ORACLE_INTERFACE(Price), + DEFINE_ORACLE_INTERFACE(Mock), }; static constexpr uint32_t oracleInterfacesCount = sizeof(oracleInterfaces) / sizeof(oracleInterfaces[0]); -#undef REGISTER_ORACLE_INTERFACE +#undef DEFINE_ORACLE_INTERFACE + + typedef sint64(*__GetQueryFeeFunc)(const void* query); + + static __GetQueryFeeFunc getOracleQueryFeeFunc[oracleInterfacesCount]; + +#define REGISTER_ORACLE_INTERFACE(Interface) { \ + getOracleQueryFeeFunc[Interface::oracleInterfaceIndex] = (__GetQueryFeeFunc)Interface::getQueryFee; \ + if (oracleInterfaces[Interface::oracleInterfaceIndex].querySize != sizeof(Interface::OracleQuery)) \ + return false; \ + if (oracleInterfaces[Interface::oracleInterfaceIndex].replySize != sizeof(Interface::OracleReply)) \ + return false; \ + } + + + static bool initOracleInterfaces() + { + for (uint32_t idx = 0; idx < oracleInterfacesCount; ++idx) + { + getOracleQueryFeeFunc[idx] = nullptr; + } + + REGISTER_ORACLE_INTERFACE(Price); + REGISTER_ORACLE_INTERFACE(Mock); + + for (uint32_t idx = 0; idx < oracleInterfacesCount; ++idx) + { + if (!getOracleQueryFeeFunc[idx]) + return false; + } + return true; + } + #define ENABLE_ORACLE_STATS_RECORD 1 diff --git a/src/oracle_interfaces/Price.h b/src/oracle_interfaces/Price.h index 5c8f12071..53ab0abcb 100644 --- a/src/oracle_interfaces/Price.h +++ b/src/oracle_interfaces/Price.h @@ -95,7 +95,7 @@ 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() @@ -104,10 +104,52 @@ struct Price return id(m, o, c, k, null); } + /// Get oracle ID of binance oracle + static id getBinanceOracleId() + { + using namespace Ch; + return id(b, i, n, a, n, c, e); + } + + /// Get oracle ID of mexc oracle + static id getMexcOracleId() + { + using namespace Ch; + return id(m, e, x, c, 0); + } + + /// Get oracle ID of gate.io oracle + static id getGateOracleId() + { + using namespace Ch; + return id(g, a, t, e, 0); + } + /// Get oracle ID of coingecko oracle static id getCoingeckoOracleId() { using namespace Ch; return id(c, o, i, n, g, e, c, k, o); } + + /// Get oracle ID of combined binance + mexc oracle (mean of prices of both sources) + static id getBinanceMexcOracleId() + { + using namespace Ch; + return id(b, i, n, a, n, c, e, underscore, m, e, x, c); + } + + /// Get oracle ID of combined binance + gate oracle (mean of prices of both sources) + static id getBinanceGateOracleId() + { + using namespace Ch; + return id(b, i, n, a, n, c, e, underscore, g, a, t, e); + } + + /// Get oracle ID of combined gate + mexc oracle (mean of prices of both sources) + static id getGateMexcOracleId() + { + using namespace Ch; + return id(g, a, t, e, underscore, m, e, x, c); + } }; diff --git a/src/qubic.cpp b/src/qubic.cpp index fb64ae6b3..bd85e1019 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -2978,7 +2978,11 @@ static void processTickTransaction(const Transaction* transaction, unsigned int case OracleUserQueryTransactionPrefix::transactionType(): { - // TODO + const bool error = (oracleEngine.startUserQuery((OracleUserQueryTransactionPrefix*)transaction, transactionIndex) < 0); + if (error && transaction->amount) + { + oracleEngine.refundFees(transaction->sourcePublicKey, transaction->amount); + } } break; @@ -5901,6 +5905,11 @@ static bool initialize() return false; } + if (!OI::initOracleInterfaces()) + { + logToConsole(L"initOracleInterfaces() failed! Not all interfaces are properly defined!"); + return false; + } if (!oracleEngine.init(computorPublicKeys)) return false; @@ -7397,7 +7406,9 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) closePeer(&peers[i], ORACLE_MACHINE_GRACEFULL_CLOSE_RETIRES); } - constexpr unsigned long long OM_INACTIVITY_TIMEOUT_SECS = 300; // 5 minutes + // inactivity timeout between 1 and 2 minutes (depending on peer index to reduce risk of + // all OM node connections being closed simultaneously) + const unsigned long long OM_INACTIVITY_TIMEOUT_SECS = 120 - (i % 5) * 15; if (peers[i].isConnectedAccepted && !peers[i].isClosing && peers[i].lastOMActivityTime > 0 && diff --git a/test/contract_testex.cpp b/test/contract_testex.cpp index 9330b3b4e..d763fad38 100644 --- a/test/contract_testex.cpp +++ b/test/contract_testex.cpp @@ -146,6 +146,7 @@ class ContractTestingTestEx : protected ContractTesting callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); EXPECT_TRUE(oracleEngine.init(computorPublicKeys)); + EXPECT_TRUE(OI::initOracleInterfaces()); checkContractExecCleanup(); @@ -2106,7 +2107,7 @@ TEST(ContractTestEx, OracleQuery) { ContractTestingTestEx test; system.epoch = 200; - system.tick = 123456789; + system.tick = 123456783; //------------------------------------------------------------------------- // Test qpi.queryOracle() and generating message to oracle machine node diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index 17687bce2..a44c84e70 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -9,10 +9,12 @@ struct OracleEngineTest : public LoggingTest { OracleEngineTest() { + EXPECT_TRUE(initSpectrum()); EXPECT_TRUE(commonBuffers.init(1, 1024 * 1024)); EXPECT_TRUE(initSpecialEntities()); EXPECT_TRUE(initContractExec()); EXPECT_TRUE(ts.init()); + EXPECT_TRUE(OI::initOracleInterfaces()); // init computors for (int computorIndex = 0; computorIndex < NUMBER_OF_COMPUTORS; computorIndex++) @@ -33,6 +35,7 @@ struct OracleEngineTest : public LoggingTest ~OracleEngineTest() { + deinitSpectrum(); commonBuffers.deinit(); deinitContractExec(); ts.deinit(); @@ -314,6 +317,11 @@ TEST(OracleEngine, ContractQuerySuccess) EXPECT_EQ(rev2.computorRevPoints[i], 0); EXPECT_EQ(rev3.computorRevPoints[i], 0); } + + // check that oracle engine is in consistent state + oracleEngine1.checkStateConsistencyWithAssert(); + oracleEngine2.checkStateConsistencyWithAssert(); + oracleEngine3.checkStateConsistencyWithAssert(); } TEST(OracleEngine, ContractQueryUnresolvable) @@ -477,6 +485,11 @@ TEST(OracleEngine, ContractQueryUnresolvable) EXPECT_EQ(rev2.computorRevPoints[i], 0); EXPECT_EQ(rev3.computorRevPoints[i], 0); } + + // check that oracle engine is in consistent state + oracleEngine1.checkStateConsistencyWithAssert(); + oracleEngine2.checkStateConsistencyWithAssert(); + oracleEngine3.checkStateConsistencyWithAssert(); } TEST(OracleEngine, ContractQueryWrongKnowledgeProof) @@ -652,6 +665,11 @@ TEST(OracleEngine, ContractQueryWrongKnowledgeProof) EXPECT_EQ(rev2.computorRevPoints[i], 0); EXPECT_EQ(rev3.computorRevPoints[i], 0); } + + // check that oracle engine is in consistent state + oracleEngine1.checkStateConsistencyWithAssert(); + oracleEngine2.checkStateConsistencyWithAssert(); + oracleEngine3.checkStateConsistencyWithAssert(); } TEST(OracleEngine, ContractQueryTimeout) @@ -721,6 +739,9 @@ TEST(OracleEngine, ContractQueryTimeout) // no reveal EXPECT_EQ(rev1.computorRevPoints[i], 0); } + + // check that oracle engine is in consistent state + oracleEngine1.checkStateConsistencyWithAssert(); } template @@ -878,8 +899,8 @@ TEST(OracleEngine, MultiContractQuerySuccess) for (int tick = 0; tick < 3; ++tick) { const unsigned long long* tsTickTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(system.tick); - const unsigned long long* tsTickTransactionOffsets2 = ts.tickTransactionOffsets.getByTickInCurrentEpoch(system.tick+1); - const unsigned long long* tsTickTransactionOffsets3 = ts.tickTransactionOffsets.getByTickInCurrentEpoch(system.tick+2); + const unsigned long long* tsTickTransactionOffsets2 = ts.tickTransactionOffsets.getByTickInCurrentEpoch(system.tick + 1); + const unsigned long long* tsTickTransactionOffsets3 = ts.tickTransactionOffsets.getByTickInCurrentEpoch(system.tick + 2); for (txIndexInTickData = 0; txIndexInTickData < NUMBER_OF_TRANSACTIONS_PER_TICK; ++txIndexInTickData) { const unsigned long long offset = tsTickTransactionOffsets[txIndexInTickData]; @@ -1014,6 +1035,11 @@ TEST(OracleEngine, MultiContractQuerySuccess) EXPECT_EQ(rev2.computorRevPoints[i], expectedRevPoints); EXPECT_EQ(rev3.computorRevPoints[i], expectedRevPoints); } + + // check that oracle engine is in consistent state + oracleEngine1.checkStateConsistencyWithAssert(); + oracleEngine2.checkStateConsistencyWithAssert(); + oracleEngine3.checkStateConsistencyWithAssert(); } /* @@ -1021,6 +1047,218 @@ TEST(OracleEngine, MultiContractQuerySuccess) - error conditions */ +struct PriceQueryTransaction : public OracleUserQueryTransactionPrefix +{ + OI::Price::OracleQuery query; + uint8_t signature[SIGNATURE_SIZE]; +}; + +static PriceQueryTransaction getPriceQueryTransaction(const OI::Price::OracleQuery& query, const m256i& sourcePublicKey, int64_t fee, uint32_t timeout) +{ + PriceQueryTransaction tx; + tx.amount = fee; + tx.destinationPublicKey = m256i::zero(); + tx.inputSize = OracleUserQueryTransactionPrefix::minInputSize() + sizeof(query); + tx.inputType = OracleUserQueryTransactionPrefix::transactionType(); + tx.oracleInterfaceIndex = OI::Price::oracleInterfaceIndex; + tx.sourcePublicKey = sourcePublicKey; + tx.tick = system.tick; + tx.timeoutMilliseconds = timeout; + tx.query = query; + setMem(tx.signature, SIGNATURE_SIZE, 0); // keep signature uninitialized + return tx; +} + +TEST(OracleEngine, UserQuerySuccess) +{ + OracleEngineTest test; + + const id USER1(123, 456, 789, 876); + increaseEnergy(USER1, 10000000000); + + // 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(42, 13, 100, 1000); + priceQuery.currency1 = m256i(20, 30, 40, 50); + priceQuery.currency2 = m256i(300, 400, 500, 600); + priceQuery.timestamp = QPI::DateAndTime::now(); + QPI::uint32 interfaceIndex = 0; + QPI::uint32 timeout = 40000; + QPI::sint64 fee = OI::Price::getQueryFee(priceQuery); + PriceQueryTransaction priceQueryTx = getPriceQueryTransaction(priceQuery, USER1, fee, timeout); + unsigned int priceQueryTxIndex = 15; + addOracleTransactionToTickStorage(&priceQueryTx, priceQueryTxIndex); + + const QPI::uint32 notificationProcId = 12345; + EXPECT_TRUE(userProcedureRegistry->add(notificationProcId, { dummyNotificationProc, 1, 128, 128, 1 })); + + //------------------------------------------------------------------------- + // start user query / check message to OM node + QPI::sint64 queryId = oracleEngine1.startUserQuery(&priceQueryTx, priceQueryTxIndex); + EXPECT_EQ(queryId, getUserOracleQueryId(system.tick, priceQueryTxIndex)); + checkNetworkMessageOracleMachineQuery(queryId, timeout, priceQuery); + EXPECT_EQ(queryId, oracleEngine2.startUserQuery(&priceQueryTx, priceQueryTxIndex)); + EXPECT_EQ(queryId, oracleEngine3.startUserQuery(&priceQueryTx, priceQueryTxIndex)); + + //------------------------------------------------------------------------- + // get query contract data + OI::Price::OracleQuery priceQueryReturned; + EXPECT_TRUE(oracleEngine1.getOracleQuery(queryId, &priceQueryReturned, sizeof(priceQueryReturned))); + EXPECT_EQ(compareMem(&priceQueryReturned, &priceQuery, sizeof(priceQuery)), 0); + + //------------------------------------------------------------------------- + // process simulated reply from OM node + struct + { + OracleMachineReply metadata; + OI::Price::OracleReply data; + } priceOracleMachineReply; + + priceOracleMachineReply.metadata.oracleMachineErrorFlags = 0; + priceOracleMachineReply.metadata.oracleQueryId = queryId; + priceOracleMachineReply.data.numerator = 1234; + priceOracleMachineReply.data.denominator = 1; + + oracleEngine1.processOracleMachineReply(&priceOracleMachineReply.metadata, sizeof(priceOracleMachineReply)); + oracleEngine2.processOracleMachineReply(&priceOracleMachineReply.metadata, sizeof(priceOracleMachineReply)); + // test: no reply to oracle engine 3! + + // duplicate from other node + oracleEngine1.processOracleMachineReply(&priceOracleMachineReply.metadata, sizeof(priceOracleMachineReply)); + + // other value from other node + priceOracleMachineReply.data.numerator = 1233; + oracleEngine1.processOracleMachineReply(&priceOracleMachineReply.metadata, sizeof(priceOracleMachineReply)); + priceOracleMachineReply.data.numerator = 1234; + + //------------------------------------------------------------------------- + // 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)); + + // 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 reveal tx + + // create tx of node 3 computers? -> no commit tx because reply data is not available in node 3 (no OM reply) + for (int i = 600; i < 676; ++i) + { + EXPECT_EQ(oracleEngine3.getReplyCommitTransaction(txBuffer, i, i - 600, system.tick + 3, 0), 0); + } + + // 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 + 1, 1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine1.checkPendingState(queryId, txFromNode2 + 2, 1, ORACLE_QUERY_STATUS_PENDING); + oracleEngine2.checkPendingState(queryId, txFromNode2 + 1, txFromNode2 + 0, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine2.checkPendingState(queryId, txFromNode2 + 2, txFromNode2 + 1, ORACLE_QUERY_STATUS_PENDING); + oracleEngine3.checkPendingState(queryId, txFromNode2 + 1, 0, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine3.checkPendingState(queryId, txFromNode2 + 2, 0, 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 + 200) >= 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 + 200 < 450) ? ORACLE_QUERY_STATUS_PENDING : ORACLE_QUERY_STATUS_COMMITTED; + oracleEngine1.checkPendingState(queryId, txFromNode1 + 200, txFromNode1, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine1.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine1.checkPendingState(queryId, txFromNode1 + 201, txFromNode1 + 1, newStatus); + oracleEngine2.checkPendingState(queryId, txFromNode1 + 200, 200, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine2.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine2.checkPendingState(queryId, txFromNode1 + 201, 200, newStatus); + oracleEngine3.checkPendingState(queryId, txFromNode1 + 200, 0, ORACLE_QUERY_STATUS_PENDING); + EXPECT_TRUE(oracleEngine3.processOracleReplyCommitTransaction(replyCommitTx)); + oracleEngine3.checkPendingState(queryId, txFromNode1 + 201, 0, newStatus); + } + else + { + oracleEngine1.checkPendingState(queryId, 451, 251, ORACLE_QUERY_STATUS_COMMITTED); + oracleEngine2.checkPendingState(queryId, 451, 200, ORACLE_QUERY_STATUS_COMMITTED); + oracleEngine3.checkPendingState(queryId, 451, 0, ORACLE_QUERY_STATUS_COMMITTED); + } + } + EXPECT_EQ(oracleEngine1.getOracleQueryStatus(queryId), 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); + + // node 3 is in committed state but cannot generate reveal tx, because it did not receive OM reply + EXPECT_EQ(oracleEngine3.getReplyRevealTransaction(txBuffer, 0, system.tick + 3, 0), 0); + + system.tick += 3; + auto* replyRevealTx = (OracleReplyRevealTransactionPrefix*)txBuffer; + const unsigned int txIndex = 10; + addOracleTransactionToTickStorage(replyRevealTx, txIndex); + oracleEngine1.processOracleReplyRevealTransaction(replyRevealTx, txIndex); + oracleEngine2.processOracleReplyRevealTransaction(replyRevealTx, txIndex); + oracleEngine3.processOracleReplyRevealTransaction(replyRevealTx, txIndex); + + //------------------------------------------------------------------------- + // status + EXPECT_EQ(oracleEngine1.getOracleQueryStatus(queryId), ORACLE_QUERY_STATUS_SUCCESS); + EXPECT_EQ(oracleEngine2.getOracleQueryStatus(queryId), ORACLE_QUERY_STATUS_SUCCESS); + EXPECT_EQ(oracleEngine3.getOracleQueryStatus(queryId), ORACLE_QUERY_STATUS_SUCCESS); + + OI::Price::OracleReply reply; + EXPECT_TRUE(oracleEngine1.getOracleReply(queryId, &reply, sizeof(reply))); + EXPECT_TRUE(compareMem(&reply, &priceOracleMachineReply.data, sizeof(reply)) == 0); + EXPECT_TRUE(oracleEngine2.getOracleReply(queryId, &reply, sizeof(reply))); + EXPECT_TRUE(compareMem(&reply, &priceOracleMachineReply.data, sizeof(reply)) == 0); + EXPECT_TRUE(oracleEngine3.getOracleReply(queryId, &reply, sizeof(reply))); + EXPECT_TRUE(compareMem(&reply, &priceOracleMachineReply.data, sizeof(reply)) == 0); + + // check that oracle engine is in consistent state + oracleEngine1.checkStateConsistencyWithAssert(); + oracleEngine2.checkStateConsistencyWithAssert(); + oracleEngine3.checkStateConsistencyWithAssert(); +} + TEST(OracleEngine, FindFirstQueryIndexOfTick) { OracleEngineTest test; diff --git a/test/oracle_testing.h b/test/oracle_testing.h index da538c107..fd278abe1 100644 --- a/test/oracle_testing.h +++ b/test/oracle_testing.h @@ -60,6 +60,12 @@ static inline QPI::uint64 getContractOracleQueryId(QPI::uint32 tick, QPI::uint32 return ((QPI::uint64)tick << 31) | (indexInTick + NUMBER_OF_TRANSACTIONS_PER_TICK); } +static inline QPI::uint64 getUserOracleQueryId(QPI::uint32 tick, QPI::uint32 indexInTick) +{ + ASSERT(indexInTick < NUMBER_OF_TRANSACTIONS_PER_TICK); + return ((QPI::uint64)tick << 31) | indexInTick; +} + static const Transaction* addOracleTransactionToTickStorage(const Transaction* tx, unsigned int txIndex) { ASSERT(txIndex < NUMBER_OF_TRANSACTIONS_PER_TICK);