From 560cf2b398091c931b55aba7c14e08b1bc40fe0e Mon Sep 17 00:00:00 2001 From: krypdkat <39078779+krypdkat@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:35:39 +0700 Subject: [PATCH 1/5] init files --- src/oracle_core/oracle_interfaces_def.h | 12 ++ src/oracle_interfaces/EvmCommon.h | 58 ++++++ src/oracle_interfaces/EvmLogRead.h | 97 +++++++++++ src/oracle_interfaces/QubicLogRead.h | 97 +++++++++++ test/oracle_engine.cpp | 223 ++++++++++++++++++++++++ 5 files changed, 487 insertions(+) create mode 100644 src/oracle_interfaces/EvmCommon.h create mode 100644 src/oracle_interfaces/EvmLogRead.h create mode 100644 src/oracle_interfaces/QubicLogRead.h diff --git a/src/oracle_core/oracle_interfaces_def.h b/src/oracle_core/oracle_interfaces_def.h index 64ba5977b..db221341f 100644 --- a/src/oracle_core/oracle_interfaces_def.h +++ b/src/oracle_core/oracle_interfaces_def.h @@ -17,6 +17,14 @@ namespace OI #include "oracle_interfaces/DogeShareValidation.h" #undef ORACLE_INTERFACE_INDEX +#define ORACLE_INTERFACE_INDEX 3 +#include "oracle_interfaces/EvmLogRead.h" +#undef ORACLE_INTERFACE_INDEX + +#define ORACLE_INTERFACE_INDEX 4 +#include "oracle_interfaces/QubicLogRead.h" +#undef ORACLE_INTERFACE_INDEX + // add new interface above this line (define ORACLE_INTERFACE_INDEX, include the header file and undef ORACLE_INTERFACE_INDEX) #define DEFINE_ORACLE_INTERFACE(Interface) {sizeof(Interface::OracleQuery), sizeof(Interface::OracleReply)} @@ -28,6 +36,8 @@ namespace OI DEFINE_ORACLE_INTERFACE(Price), DEFINE_ORACLE_INTERFACE(Mock), DEFINE_ORACLE_INTERFACE(DogeShareValidation), + DEFINE_ORACLE_INTERFACE(EvmLogRead), + DEFINE_ORACLE_INTERFACE(QubicLogRead), // add new interface above this line (with DEFINE_ORACLE_INTERFACE; the order must match the interfaces indices) }; @@ -58,6 +68,8 @@ namespace OI REGISTER_ORACLE_INTERFACE(Price); REGISTER_ORACLE_INTERFACE(Mock); REGISTER_ORACLE_INTERFACE(DogeShareValidation); + REGISTER_ORACLE_INTERFACE(EvmLogRead); + REGISTER_ORACLE_INTERFACE(QubicLogRead); // add new interface above this line (with REGISTER_ORACLE_INTERFACE) for (uint32_t idx = 0; idx < oracleInterfacesCount; ++idx) diff --git a/src/oracle_interfaces/EvmCommon.h b/src/oracle_interfaces/EvmCommon.h new file mode 100644 index 000000000..aa1cf707e --- /dev/null +++ b/src/oracle_interfaces/EvmCommon.h @@ -0,0 +1,58 @@ +#pragma once + +using namespace QPI; + +/** +* Shared building blocks for all EVM cross-chain oracle interfaces. +* +* Keep generic types here (chain ids, address/word representations, helpers) so that future EVM +* oracle interfaces (e.g. reading event logs, storage slots, balances) can reuse them instead of +* redefining their own. Each concrete EVM interface (such as EvmLogRead) includes this header. +* +* Representation conventions (chosen for determinism and to mirror the EVM ABI): +* - All hashes are raw 32-byte big-endian values. +* - All EVM addresses are 20 bytes, stored left-zero-padded into a 32-byte word (the same way the +* ABI encodes an `address`). The QPI Array<> capacity must be a power of two, so 20-byte arrays +* are not representable directly anyway. +* - All EVM 256-bit integers (uint256) are stored as 32-byte big-endian values. +*/ +namespace Evm +{ + /// Raw 32-byte big-endian value: tx hash, block hash, ABI word, etc. + typedef Array Bytes32; + + /// EVM address (20 bytes) left-zero-padded into a 32-byte ABI word. + typedef Array Address; + + /// EVM 256-bit unsigned integer, big-endian (e.g. an ERC20 token amount). + typedef Array Uint256; + + /// Chain ids (decimal) of supported EVM networks. + struct ChainId + { + static constexpr uint64 ethereum = 1; // 0x1 + static constexpr uint64 optimism = 10; // 0xa + static constexpr uint64 bsc = 56; // 0x38 + static constexpr uint64 polygon = 137; // 0x89 + static constexpr uint64 fantom = 250; // 0xfa + static constexpr uint64 base = 8453; // 0x2105 + static constexpr uint64 avalanche = 43114; // 0xa86a + static constexpr uint64 arbitrum = 42161; // 0xa4b1 + static constexpr uint64 sepolia = 11155111;// 0xaa36a7 (Ethereum Sepolia testnet) + // add new chain ids above this line + }; + + /// Return true if the chain id is one this oracle is expected to serve. + static bool isSupportedChain(uint64 chainId) + { + return chainId == ChainId::ethereum + || chainId == ChainId::optimism + || chainId == ChainId::bsc + || chainId == ChainId::polygon + || chainId == ChainId::fantom + || chainId == ChainId::base + || chainId == ChainId::avalanche + || chainId == ChainId::arbitrum + || chainId == ChainId::sepolia; + } +} diff --git a/src/oracle_interfaces/EvmLogRead.h b/src/oracle_interfaces/EvmLogRead.h new file mode 100644 index 000000000..7c95f999d --- /dev/null +++ b/src/oracle_interfaces/EvmLogRead.h @@ -0,0 +1,97 @@ +using namespace QPI; + +#include "oracle_interfaces/EvmCommon.h" + +/** +* Oracle interface "EvmLogRead" (see Price.h for general documentation about oracle interfaces). +* +* Generic cross-chain read of a SINGLE event log from a FINALIZED EVM receipt. Given +* (chainId, txHash, logIndex) the oracle machine returns that one log verbatim: the emitter +* address, its topics and its data, as RAW bytes. No event/ABI semantics are baked in here - +* this interface knows nothing about tokens, amounts or any specific event; the querying smart +* contract decodes the raw log itself. +* +* Determinism requirement (see Price.h): the oracle machine MUST only answer from receipts at or +* below the chain's finalized block (RESULT_TX_NOT_FINALIZED otherwise) and MUST zero all reply +* value fields except code on failure, so replies are byte-identical across computors. +* +* This is the generic read_evm_log service contract. Byte layout mirrors oracle-machine +* oracles/read_evm_log (48-byte query, 440-byte reply). +*/ +struct EvmLogRead +{ + //------------------------------------------------------------------------- + // Mandatory oracle interface definitions + + /// Oracle interface index + static constexpr uint32 oracleInterfaceIndex = ORACLE_INTERFACE_INDEX; + + //--- Result codes returned in OracleReply.code. 0 means success; any non-zero value is a failure + // reason and implies all other reply fields are all-zero. + static constexpr uint64 RESULT_SUCCESS = 0; ///< log found and finalized; reply fields valid + static constexpr uint64 RESULT_BAD_QUERY = 1; ///< malformed query (zero tx hash, ...) + static constexpr uint64 RESULT_CHAIN_UNSUPPORTED = 2; ///< chainId not served by this oracle + static constexpr uint64 RESULT_TX_NOT_FOUND = 3; ///< no such transaction on the chain + static constexpr uint64 RESULT_TX_NOT_FINALIZED = 4; ///< tx pending / reverted / not yet past finality + static constexpr uint64 RESULT_LOG_INDEX_OUT_OF_RANGE = 5; ///< logIndex >= number of logs in the receipt + static constexpr uint64 RESULT_LOG_DATA_TOO_LARGE = 6; ///< log data exceeds the 256-byte reply capacity + // add new result codes above this line + // + // Codes 3/4 are time-varying observations: treat as retriable, never proof of permanent + // absence; near the finality boundary computors may split and the query just times out. + + /// Oracle query data / input to the oracle machine. Fixed 48 bytes. + struct OracleQuery + { + /// EVM chain id (decimal), e.g. Evm::ChainId::ethereum (1) or Evm::ChainId::base. + uint64 chainId; // [0..8) + + /// Transaction hash to inspect (32-byte big-endian EVM tx hash). + Evm::Bytes32 txHash; // [8..40) + + /// Index of the log entry within the transaction receipt (receipt-local log index). + uint64 logIndex; // [40..48) + }; + + /// Oracle reply data / output of the oracle machine. Fixed 440 bytes. + /// On failure (code != RESULT_SUCCESS) all other fields MUST be all-zero so the reply is + /// canonical across computors. + struct OracleReply + { + /// One of the RESULT_* codes above. + uint64 code; // [0..8) + + /// Log emitter (20-byte address right-aligned into a 32-byte word, upper 12 bytes zero). + Evm::Address address; // [8..40) + + /// Number of topics present in this log (0..4). + uint64 topicCount; // [40..48) + + /// The log topics, raw 32-byte words, zero-padded beyond topicCount. + Array topics; // [48..176) + + /// Number of valid bytes in data (0..256). + uint64 dataLen; // [176..184) + + /// The log data, raw bytes, zero-padded beyond dataLen. + Array data; // [184..440) + }; + + /// Return query fee. Cross-chain EVM reads are comparatively expensive. + static sint64 getQueryFee(const OracleQuery& query) + { + return 1000; + } + + //------------------------------------------------------------------------- + // Optional: convenience features for contracts using the oracle interface + + /// True if the reply carries a usable raw-log attestation. + static bool replyIsValid(const OracleReply& reply) + { + return reply.code == RESULT_SUCCESS; + } +}; + +static_assert(sizeof(EvmLogRead::OracleQuery) == 48, "EvmLogRead::OracleQuery must be 48 bytes"); +static_assert(sizeof(EvmLogRead::OracleReply) == 440, "EvmLogRead::OracleReply must be 440 bytes"); diff --git a/src/oracle_interfaces/QubicLogRead.h b/src/oracle_interfaces/QubicLogRead.h new file mode 100644 index 000000000..bc97be986 --- /dev/null +++ b/src/oracle_interfaces/QubicLogRead.h @@ -0,0 +1,97 @@ +using namespace QPI; + +/** +* Oracle interface "QubicLogRead" (see Price.h for general documentation about oracle interfaces). +* +* Generic read of a SINGLE Qubic log event, addressed like EvmLogRead: given +* (tick, txHash, logIndex) the oracle machine returns that one log verbatim - its type, emitting +* contract index and raw body bytes. No semantics are baked in; the querying smart contract +* decodes the raw bytes itself. +* +* A CONTRACT_INFORMATION_MESSAGE (logType 6) can only be emitted by code running inside a +* contract, and the core stamps the emitting contractIndex itself, so (logType, contractIndex) +* authenticates the emitter the same way (address, topic0) does for an EVM log. +* +* Determinism (see Price.h): a log is immutable history - any node that executed the tick serves +* byte-identical bytes. The oracle machine must only answer executed transactions and must zero +* all value fields except code on failure. +* +* Served by oracle-machine oracles/read_qubic_log (reads per-operator bob nodes, all configured +* bobs must agree byte-for-byte). +*/ +struct QubicLogRead +{ + //------------------------------------------------------------------------- + // Mandatory oracle interface definitions + + /// Oracle interface index + static constexpr uint32 oracleInterfaceIndex = ORACLE_INTERFACE_INDEX; + + //--- Result codes returned in OracleReply.code. 0 means success; any non-zero value is a failure + // reason and implies all other reply fields are all-zero. + static constexpr uint64 RESULT_SUCCESS = 0; ///< log found; reply fields valid + static constexpr uint64 RESULT_BAD_QUERY = 1; ///< malformed query (zero tx hash or zero tick) + static constexpr uint64 RESULT_TX_NOT_FOUND = 2; ///< no such transaction + static constexpr uint64 RESULT_TX_NOT_EXECUTED = 3; ///< tx pending or failed (no logs exist) + static constexpr uint64 RESULT_TICK_MISMATCH = 4; ///< tx exists but not in the queried tick + static constexpr uint64 RESULT_LOG_INDEX_OUT_OF_RANGE = 5; ///< logIndex >= number of logs of the tx + static constexpr uint64 RESULT_LOG_DATA_TOO_LARGE = 6; ///< log body exceeds the 256-byte reply capacity + // add new result codes above this line + // + // Codes 2/3 are time-varying observations: treat as retriable, never proof of permanent + // absence; around the execution boundary computors may split and the query just times out. + + /// Oracle query data / input to the oracle machine. Fixed 48 bytes (mirrors EvmLogRead). + struct OracleQuery + { + /// Tick the transaction was executed in. + uint64 tick; // [0..8) + + /// Transaction hash (32-byte digest). + Array txHash; // [8..40) + + /// Index of the log within the transaction's own logs (receipt-local). + uint64 logIndex; // [40..48) + }; + + /// Oracle reply data / output of the oracle machine. Fixed 288 bytes. + /// On failure (code != RESULT_SUCCESS) all other fields MUST be all-zero. + struct OracleReply + { + /// One of the RESULT_* codes above. + uint64 code; // [0..8) + + /// Emitting contract index for contract-emitted log types; 0 otherwise. + uint64 contractIndex; // [8..16) + + /// Log event type (QU_TRANSFER 0, ..., CONTRACT_INFORMATION_MESSAGE 6, ...). + uint64 logType; // [16..24) + + /// Number of valid bytes in data (0..256). + uint64 dataLen; // [24..32) + + /// The raw log body, zero-padded beyond dataLen. For contract-emitted log types (4..7) + /// the body BEGINS with the core-stamped 8-byte prefix (contractIndex u32 LE | + /// contract-defined type u32 LE), so the usable contract payload is at most 248 bytes. + /// The consumer contract defines the layout of the rest. + Array data; // [32..288) + }; + + /// Return query fee. + static sint64 getQueryFee(const OracleQuery& query) + { + return 100; + } + + //------------------------------------------------------------------------- + // Optional: convenience features for contracts using the oracle interface + + /// True if the reply carries a usable raw-log attestation. + static bool replyIsValid(const OracleReply& reply) + { + return reply.code == RESULT_SUCCESS; + } +}; + +static_assert(sizeof(QubicLogRead::OracleQuery) == 48, "QubicLogRead::OracleQuery must be 48 bytes"); +static_assert(sizeof(QubicLogRead::OracleReply) == 288, "QubicLogRead::OracleReply must be 288 bytes"); diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index 9a32e654c..6dbff6d81 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -1924,3 +1924,226 @@ TEST(PriceOracle, SubscriptionFee) EXPECT_EQ(Price::getSubscriptionFee(query, 1024 * 60000), 133); EXPECT_EQ(Price::getSubscriptionFee(query, 2047 * 60000), 133); } + +// --- EvmLogRead oracle interface (generic cross-chain single-log read) --- + +// The query/reply structs must fit in a transaction and must agree with the size table registered +// in oracle_interfaces_def.h. Their sizes are also pinned so accidental layout changes (which would +// break the reply digest that computors must agree on) are caught. +TEST(EvmLogReadOracle, DataContract) +{ + using OI::EvmLogRead; + + // registered at index 3 + EXPECT_EQ(EvmLogRead::oracleInterfaceIndex, 3u); + EXPECT_LT(EvmLogRead::oracleInterfaceIndex, OI::oracleInterfacesCount); + + // fits within transaction limits + EXPECT_LE(sizeof(EvmLogRead::OracleQuery), (size_t)MAX_ORACLE_QUERY_SIZE); + EXPECT_LE(sizeof(EvmLogRead::OracleReply), (size_t)MAX_ORACLE_REPLY_SIZE); + + // pinned sizes (no padding holes -> canonical bytes for the reply digest) + EXPECT_EQ(sizeof(EvmLogRead::OracleQuery), 48u); + EXPECT_EQ(sizeof(EvmLogRead::OracleReply), 440u); + + // registry size table matches the actual structs + EXPECT_EQ(OI::oracleInterfaces[EvmLogRead::oracleInterfaceIndex].querySize, sizeof(EvmLogRead::OracleQuery)); + EXPECT_EQ(OI::oracleInterfaces[EvmLogRead::oracleInterfaceIndex].replySize, sizeof(EvmLogRead::OracleReply)); +} + +// The fee must be accepted by the engine (>= MIN_ORACLE_QUERY_FEE) and must be reachable through the +// type-erased function pointer registered by initOracleInterfaces(). +TEST(EvmLogReadOracle, QueryFee) +{ + using OI::EvmLogRead; + + EXPECT_TRUE(OI::initOracleInterfaces()); + + EvmLogRead::OracleQuery query{}; + query.chainId = OI::Evm::ChainId::ethereum; + + EXPECT_EQ(EvmLogRead::getQueryFee(query), 1000); + EXPECT_GE(EvmLogRead::getQueryFee(query), MIN_ORACLE_QUERY_FEE); + + // dispatched through the registry pointer gives the same value + auto* feeFunc = OI::getOracleQueryFeeFunc[EvmLogRead::oracleInterfaceIndex]; + ASSERT_NE(feeFunc, nullptr); + EXPECT_EQ(feeFunc(&query), 1000); +} + +TEST(EvmLogReadOracle, ChainIds) +{ + namespace Evm = OI::Evm; + + // values from the chain-id table + EXPECT_EQ(Evm::ChainId::ethereum, 1u); + EXPECT_EQ(Evm::ChainId::optimism, 10u); + EXPECT_EQ(Evm::ChainId::bsc, 56u); + EXPECT_EQ(Evm::ChainId::polygon, 137u); + EXPECT_EQ(Evm::ChainId::fantom, 250u); + EXPECT_EQ(Evm::ChainId::base, 8453u); + EXPECT_EQ(Evm::ChainId::avalanche, 43114u); + EXPECT_EQ(Evm::ChainId::arbitrum, 42161u); + EXPECT_EQ(Evm::ChainId::sepolia, 11155111u); + + // every listed chain is recognized + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::ethereum)); + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::optimism)); + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::bsc)); + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::polygon)); + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::fantom)); + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::base)); + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::avalanche)); + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::arbitrum)); + EXPECT_TRUE(Evm::isSupportedChain(Evm::ChainId::sepolia)); + + // unknown chain ids are rejected + EXPECT_FALSE(Evm::isSupportedChain(0)); + EXPECT_FALSE(Evm::isSupportedChain(2)); // a real but unsupported chain id + EXPECT_FALSE(Evm::isSupportedChain(999999)); +} + +// Result codes must have success == 0 and all failure reasons distinct and non-zero. +TEST(EvmLogReadOracle, ResultCodes) +{ + using E = OI::EvmLogRead; + + EXPECT_EQ(E::RESULT_SUCCESS, 0u); + const QPI::uint64 failureCodes[] = { + E::RESULT_BAD_QUERY, E::RESULT_CHAIN_UNSUPPORTED, E::RESULT_TX_NOT_FOUND, + E::RESULT_TX_NOT_FINALIZED, E::RESULT_LOG_INDEX_OUT_OF_RANGE, E::RESULT_LOG_DATA_TOO_LARGE, + }; + const unsigned n = sizeof(failureCodes) / sizeof(failureCodes[0]); + for (unsigned i = 0; i < n; ++i) + { + EXPECT_NE(failureCodes[i], E::RESULT_SUCCESS); + for (unsigned j = i + 1; j < n; ++j) + EXPECT_NE(failureCodes[i], failureCodes[j]); + } + + // on success the reply is valid; any non-zero code means no usable log + E::OracleReply reply{}; + reply.code = E::RESULT_SUCCESS; + EXPECT_TRUE(E::replyIsValid(reply)); + reply.code = E::RESULT_TX_NOT_FINALIZED; + EXPECT_FALSE(E::replyIsValid(reply)); +} + +// Two queries built with identical logical values must serialize to identical bytes. This guards the +// determinism requirement: every computor must produce the exact same query bytes (and therefore the +// OM must produce the exact same reply digest), so no padding byte may be left uninitialized. +TEST(EvmLogReadOracle, DeterministicQueryBytes) +{ + using E = OI::EvmLogRead; + + auto build = [](E::OracleQuery& q) + { + setMem(&q, sizeof(q), 0); + q.chainId = OI::Evm::ChainId::bsc; + for (QPI::uint64 i = 0; i < 32; ++i) + q.txHash.set(i, (QPI::uint8)(i + 1)); + q.logIndex = 1; + }; + + E::OracleQuery a, b; + build(a); + build(b); + EXPECT_EQ(compareMem(&a, &b, sizeof(E::OracleQuery)), 0); + + // survives a copy through a raw wire buffer of the maximum query size + unsigned char buffer[MAX_ORACLE_QUERY_SIZE]; + setMem(buffer, sizeof(buffer), 0); + copyMem(buffer, &a, sizeof(a)); + E::OracleQuery roundtrip{}; + copyMem(&roundtrip, buffer, sizeof(roundtrip)); + EXPECT_EQ(compareMem(&a, &roundtrip, sizeof(E::OracleQuery)), 0); +} + +// --- QubicLogRead oracle interface (generic Qubic log event read) --- + +// Mirrors EvmLogRead addressing: (tick, txHash, logIndex) -> one raw log. Sizes and offsets are +// pinned so the query bytes and the reply digest stay canonical. +TEST(QubicLogReadOracle, DataContract) +{ + using OI::QubicLogRead; + + // registered at index 4 + EXPECT_EQ(QubicLogRead::oracleInterfaceIndex, 4u); + EXPECT_LT(QubicLogRead::oracleInterfaceIndex, OI::oracleInterfacesCount); + + // fits within transaction limits + EXPECT_LE(sizeof(QubicLogRead::OracleQuery), (size_t)MAX_ORACLE_QUERY_SIZE); + EXPECT_LE(sizeof(QubicLogRead::OracleReply), (size_t)MAX_ORACLE_REPLY_SIZE); + + // pinned sizes and offsets (no padding holes -> canonical bytes for the reply digest) + EXPECT_EQ(sizeof(QubicLogRead::OracleQuery), 48u); + EXPECT_EQ(sizeof(QubicLogRead::OracleReply), 288u); + EXPECT_EQ(offsetof(QubicLogRead::OracleQuery, tick), 0u); + EXPECT_EQ(offsetof(QubicLogRead::OracleQuery, txHash), 8u); + EXPECT_EQ(offsetof(QubicLogRead::OracleQuery, logIndex), 40u); + EXPECT_EQ(offsetof(QubicLogRead::OracleReply, code), 0u); + EXPECT_EQ(offsetof(QubicLogRead::OracleReply, contractIndex), 8u); + EXPECT_EQ(offsetof(QubicLogRead::OracleReply, logType), 16u); + EXPECT_EQ(offsetof(QubicLogRead::OracleReply, dataLen), 24u); + EXPECT_EQ(offsetof(QubicLogRead::OracleReply, data), 32u); + + // registry size table matches the actual structs + EXPECT_EQ(OI::oracleInterfaces[QubicLogRead::oracleInterfaceIndex].querySize, sizeof(QubicLogRead::OracleQuery)); + EXPECT_EQ(OI::oracleInterfaces[QubicLogRead::oracleInterfaceIndex].replySize, sizeof(QubicLogRead::OracleReply)); +} + +TEST(QubicLogReadOracle, QueryFeeAndResultCodes) +{ + using Q = OI::QubicLogRead; + + EXPECT_TRUE(OI::initOracleInterfaces()); + + Q::OracleQuery query{}; + EXPECT_EQ(Q::getQueryFee(query), 100); + EXPECT_GE(Q::getQueryFee(query), MIN_ORACLE_QUERY_FEE); + + auto* feeFunc = OI::getOracleQueryFeeFunc[Q::oracleInterfaceIndex]; + ASSERT_NE(feeFunc, nullptr); + EXPECT_EQ(feeFunc(&query), 100); + + // success == 0, failure codes distinct and non-zero + EXPECT_EQ(Q::RESULT_SUCCESS, 0u); + const QPI::uint64 failureCodes[] = { + Q::RESULT_BAD_QUERY, Q::RESULT_TX_NOT_FOUND, Q::RESULT_TX_NOT_EXECUTED, + Q::RESULT_TICK_MISMATCH, Q::RESULT_LOG_INDEX_OUT_OF_RANGE, Q::RESULT_LOG_DATA_TOO_LARGE, + }; + const unsigned n = sizeof(failureCodes) / sizeof(failureCodes[0]); + for (unsigned i = 0; i < n; ++i) + { + EXPECT_NE(failureCodes[i], Q::RESULT_SUCCESS); + for (unsigned j = i + 1; j < n; ++j) + EXPECT_NE(failureCodes[i], failureCodes[j]); + } + + Q::OracleReply reply{}; + reply.code = Q::RESULT_SUCCESS; + EXPECT_TRUE(Q::replyIsValid(reply)); + reply.code = Q::RESULT_TX_NOT_EXECUTED; + EXPECT_FALSE(Q::replyIsValid(reply)); +} + +// Two queries built with identical logical values must serialize to identical bytes (determinism: +// no padding byte may be left uninitialized). +TEST(QubicLogReadOracle, DeterministicQueryBytes) +{ + using Q = OI::QubicLogRead; + + auto build = [](Q::OracleQuery& q) + { + setMem(&q, sizeof(q), 0); + q.tick = 12345678; + for (QPI::uint64 i = 0; i < 32; ++i) + q.txHash.set(i, (QPI::uint8)(i + 1)); + q.logIndex = 1; + }; + + Q::OracleQuery a, b; + build(a); + build(b); + EXPECT_EQ(compareMem(&a, &b, sizeof(Q::OracleQuery)), 0); +} From 930b167df31256208d5eaf1273d2272a3debb36e Mon Sep 17 00:00:00 2001 From: krypdkat <39078779+krypdkat@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:36:37 +0700 Subject: [PATCH 2/5] clarify log event on bob --- src/oracle_interfaces/QubicLogRead.h | 15 ++++++++------- test/oracle_engine.cpp | 10 +++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/oracle_interfaces/QubicLogRead.h b/src/oracle_interfaces/QubicLogRead.h index bc97be986..fb19b3fbd 100644 --- a/src/oracle_interfaces/QubicLogRead.h +++ b/src/oracle_interfaces/QubicLogRead.h @@ -3,10 +3,11 @@ using namespace QPI; /** * Oracle interface "QubicLogRead" (see Price.h for general documentation about oracle interfaces). * -* Generic read of a SINGLE Qubic log event, addressed like EvmLogRead: given -* (tick, txHash, logIndex) the oracle machine returns that one log verbatim - its type, emitting -* contract index and raw body bytes. No semantics are baked in; the querying smart contract -* decodes the raw bytes itself. +* Generic read of a SINGLE Qubic log event: given (tick, txHash, logId) the oracle machine +* returns that one log verbatim - its type, emitting contract index and raw body bytes. logId is +* the GLOBAL log id (counted from the chain's first log event onward), matched against the logId +* of each log in the transaction's receipt. No semantics are baked in; the querying smart +* contract decodes the raw bytes itself. * * A CONTRACT_INFORMATION_MESSAGE (logType 6) can only be emitted by code running inside a * contract, and the core stamps the emitting contractIndex itself, so (logType, contractIndex) @@ -34,7 +35,7 @@ struct QubicLogRead static constexpr uint64 RESULT_TX_NOT_FOUND = 2; ///< no such transaction static constexpr uint64 RESULT_TX_NOT_EXECUTED = 3; ///< tx pending or failed (no logs exist) static constexpr uint64 RESULT_TICK_MISMATCH = 4; ///< tx exists but not in the queried tick - static constexpr uint64 RESULT_LOG_INDEX_OUT_OF_RANGE = 5; ///< logIndex >= number of logs of the tx + static constexpr uint64 RESULT_LOG_NOT_FOUND = 5; ///< no log with the queried logId in this tx static constexpr uint64 RESULT_LOG_DATA_TOO_LARGE = 6; ///< log body exceeds the 256-byte reply capacity // add new result codes above this line // @@ -50,8 +51,8 @@ struct QubicLogRead /// Transaction hash (32-byte digest). Array txHash; // [8..40) - /// Index of the log within the transaction's own logs (receipt-local). - uint64 logIndex; // [40..48) + /// Global log id of the requested log event (counted from the chain's first log). + uint64 logId; // [40..48) }; /// Oracle reply data / output of the oracle machine. Fixed 288 bytes. diff --git a/test/oracle_engine.cpp b/test/oracle_engine.cpp index 6dbff6d81..989fb580a 100644 --- a/test/oracle_engine.cpp +++ b/test/oracle_engine.cpp @@ -2061,8 +2061,8 @@ TEST(EvmLogReadOracle, DeterministicQueryBytes) // --- QubicLogRead oracle interface (generic Qubic log event read) --- -// Mirrors EvmLogRead addressing: (tick, txHash, logIndex) -> one raw log. Sizes and offsets are -// pinned so the query bytes and the reply digest stay canonical. +// Mirrors EvmLogRead addressing: (tick, txHash, logId) -> one raw log. logId is the GLOBAL log +// id. Sizes and offsets are pinned so the query bytes and the reply digest stay canonical. TEST(QubicLogReadOracle, DataContract) { using OI::QubicLogRead; @@ -2080,7 +2080,7 @@ TEST(QubicLogReadOracle, DataContract) EXPECT_EQ(sizeof(QubicLogRead::OracleReply), 288u); EXPECT_EQ(offsetof(QubicLogRead::OracleQuery, tick), 0u); EXPECT_EQ(offsetof(QubicLogRead::OracleQuery, txHash), 8u); - EXPECT_EQ(offsetof(QubicLogRead::OracleQuery, logIndex), 40u); + EXPECT_EQ(offsetof(QubicLogRead::OracleQuery, logId), 40u); EXPECT_EQ(offsetof(QubicLogRead::OracleReply, code), 0u); EXPECT_EQ(offsetof(QubicLogRead::OracleReply, contractIndex), 8u); EXPECT_EQ(offsetof(QubicLogRead::OracleReply, logType), 16u); @@ -2110,7 +2110,7 @@ TEST(QubicLogReadOracle, QueryFeeAndResultCodes) EXPECT_EQ(Q::RESULT_SUCCESS, 0u); const QPI::uint64 failureCodes[] = { Q::RESULT_BAD_QUERY, Q::RESULT_TX_NOT_FOUND, Q::RESULT_TX_NOT_EXECUTED, - Q::RESULT_TICK_MISMATCH, Q::RESULT_LOG_INDEX_OUT_OF_RANGE, Q::RESULT_LOG_DATA_TOO_LARGE, + Q::RESULT_TICK_MISMATCH, Q::RESULT_LOG_NOT_FOUND, Q::RESULT_LOG_DATA_TOO_LARGE, }; const unsigned n = sizeof(failureCodes) / sizeof(failureCodes[0]); for (unsigned i = 0; i < n; ++i) @@ -2139,7 +2139,7 @@ TEST(QubicLogReadOracle, DeterministicQueryBytes) q.tick = 12345678; for (QPI::uint64 i = 0; i < 32; ++i) q.txHash.set(i, (QPI::uint8)(i + 1)); - q.logIndex = 1; + q.logId = 1; }; Q::OracleQuery a, b; From 5a5b8a1b1d0bd4519fed462121d6137a5a9754cf Mon Sep 17 00:00:00 2001 From: krypdkat <39078779+krypdkat@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:19:44 +0700 Subject: [PATCH 3/5] update files to msvc --- src/Qubic.vcxproj | 3 +++ src/Qubic.vcxproj.filters | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 942c974b0..fb13a3278 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -100,6 +100,8 @@ + + @@ -108,6 +110,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 160c5365e..fd01aede8 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -430,6 +430,15 @@ oc_interfaces + + oracle_interfaces + + + oracle_interfaces + + + oracle_interfaces + From 6ea04f4a3384693560b36e6960e01b8befb07b1d Mon Sep 17 00:00:00 2001 From: krypdkat <39078779+krypdkat@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:50:04 +0700 Subject: [PATCH 4/5] add more comments --- src/oracle_interfaces/EvmCommon.h | 1 + src/oracle_interfaces/QubicLogRead.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/oracle_interfaces/EvmCommon.h b/src/oracle_interfaces/EvmCommon.h index aa1cf707e..59b432d4f 100644 --- a/src/oracle_interfaces/EvmCommon.h +++ b/src/oracle_interfaces/EvmCommon.h @@ -18,6 +18,7 @@ using namespace QPI; */ namespace Evm { + // Array and uint8, uint64 inherit from QPI /// Raw 32-byte big-endian value: tx hash, block hash, ABI word, etc. typedef Array Bytes32; diff --git a/src/oracle_interfaces/QubicLogRead.h b/src/oracle_interfaces/QubicLogRead.h index fb19b3fbd..81c0039b0 100644 --- a/src/oracle_interfaces/QubicLogRead.h +++ b/src/oracle_interfaces/QubicLogRead.h @@ -32,7 +32,7 @@ struct QubicLogRead // reason and implies all other reply fields are all-zero. static constexpr uint64 RESULT_SUCCESS = 0; ///< log found; reply fields valid static constexpr uint64 RESULT_BAD_QUERY = 1; ///< malformed query (zero tx hash or zero tick) - static constexpr uint64 RESULT_TX_NOT_FOUND = 2; ///< no such transaction + static constexpr uint64 RESULT_TX_NOT_FOUND = 2; ///< no such transaction, also include the too-old transactions (usually 2 month old transactions can't be found efficiently anymore) static constexpr uint64 RESULT_TX_NOT_EXECUTED = 3; ///< tx pending or failed (no logs exist) static constexpr uint64 RESULT_TICK_MISMATCH = 4; ///< tx exists but not in the queried tick static constexpr uint64 RESULT_LOG_NOT_FOUND = 5; ///< no log with the queried logId in this tx From b6d8001af01985b191052b64951e913d502acea2 Mon Sep 17 00:00:00 2001 From: krypdkat <39078779+krypdkat@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:38:42 +0700 Subject: [PATCH 5/5] update comments --- src/oracle_interfaces/EvmLogRead.h | 1 + src/oracle_interfaces/QubicLogRead.h | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/oracle_interfaces/EvmLogRead.h b/src/oracle_interfaces/EvmLogRead.h index 7c95f999d..cae67b72c 100644 --- a/src/oracle_interfaces/EvmLogRead.h +++ b/src/oracle_interfaces/EvmLogRead.h @@ -39,6 +39,7 @@ struct EvmLogRead // // Codes 3/4 are time-varying observations: treat as retriable, never proof of permanent // absence; near the finality boundary computors may split and the query just times out. + // Retrying is the job of querier and not part of the oracle machine. /// Oracle query data / input to the oracle machine. Fixed 48 bytes. struct OracleQuery diff --git a/src/oracle_interfaces/QubicLogRead.h b/src/oracle_interfaces/QubicLogRead.h index 81c0039b0..a37d71a73 100644 --- a/src/oracle_interfaces/QubicLogRead.h +++ b/src/oracle_interfaces/QubicLogRead.h @@ -5,7 +5,7 @@ using namespace QPI; * * Generic read of a SINGLE Qubic log event: given (tick, txHash, logId) the oracle machine * returns that one log verbatim - its type, emitting contract index and raw body bytes. logId is -* the GLOBAL log id (counted from the chain's first log event onward), matched against the logId +* the GLOBAL log id (counted from epoch's first log event onward), matched against the logId * of each log in the transaction's receipt. No semantics are baked in; the querying smart * contract decodes the raw bytes itself. * @@ -51,7 +51,7 @@ struct QubicLogRead /// Transaction hash (32-byte digest). Array txHash; // [8..40) - /// Global log id of the requested log event (counted from the chain's first log). + /// Global log id of the requested log event (counted from epoch's first log). uint64 logId; // [40..48) };