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
+
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..59b432d4f
--- /dev/null
+++ b/src/oracle_interfaces/EvmCommon.h
@@ -0,0 +1,59 @@
+#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
+{
+ // Array and uint8, uint64 inherit from QPI
+ /// 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..cae67b72c
--- /dev/null
+++ b/src/oracle_interfaces/EvmLogRead.h
@@ -0,0 +1,98 @@
+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.
+ // 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
+ {
+ /// 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..a37d71a73
--- /dev/null
+++ b/src/oracle_interfaces/QubicLogRead.h
@@ -0,0 +1,98 @@
+using namespace QPI;
+
+/**
+* Oracle interface "QubicLogRead" (see Price.h for general documentation about oracle interfaces).
+*
+* 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 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.
+*
+* 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, 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
+ 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)
+
+ /// Global log id of the requested log event (counted from epoch's first log).
+ uint64 logId; // [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..989fb580a 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, 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;
+
+ // 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, logId), 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_NOT_FOUND, 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.logId = 1;
+ };
+
+ Q::OracleQuery a, b;
+ build(a);
+ build(b);
+ EXPECT_EQ(compareMem(&a, &b, sizeof(Q::OracleQuery)), 0);
+}