Skip to content

Commit 7527603

Browse files
Merge pull request #787 from qubic/feature/2026-02-20-oracle-subscriptions
Implement oracle subscriptions
2 parents 88d1805 + 4b36ea2 commit 7527603

File tree

24 files changed

+2651
-334
lines changed

24 files changed

+2651
-334
lines changed

.github/workflows/contract-verify.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- name: Find all contract files to verify
3636
id: filepaths
3737
run: |
38-
files=$(find src/contracts/ -maxdepth 1 -type f -name "*.h" ! -name "*TestExample*" ! -name "*math_lib*" ! -name "*qpi*" -printf "%p\n" | paste -sd, -)
38+
files=$(find src/contracts/ -maxdepth 1 -type f -name "*.h" ! -name "*QUtil*" ! -name "*TestExample*" ! -name "*math_lib*" ! -name "*qpi*" -printf "%p\n" | paste -sd, -)
3939
echo "contract-filepaths=$files" >> "$GITHUB_OUTPUT"
4040
- name: Contract verify action step
4141
id: verify

lib/platform_common/sorting.h

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,176 @@ void quickSort(T* range, int first, int last, SortingOrder order)
5555

5656
return;
5757
}
58+
59+
template <typename T>
60+
struct CompareLess
61+
{
62+
constexpr bool operator()(const T& lhs, const T& rhs) const
63+
{
64+
return lhs < rhs;
65+
}
66+
};
67+
68+
template <typename T>
69+
struct CompareGreater
70+
{
71+
constexpr bool operator()(const T& lhs, const T& rhs) const
72+
{
73+
return lhs > rhs;
74+
}
75+
};
76+
77+
// Data structure that provides efficient access to the min element (useful for priority queues).
78+
template <typename T, unsigned int Capacity, class Compare = CompareLess<T>>
79+
class MinHeap
80+
{
81+
public:
82+
void init(Compare compare = Compare())
83+
{
84+
_size = 0;
85+
_compare = compare;
86+
}
87+
88+
// Insert new element. Returns true on success.
89+
bool insert(const T& newElement)
90+
{
91+
if (_size >= capacity())
92+
return false;
93+
const unsigned int newIndex = _size++;
94+
_data[newIndex] = newElement;
95+
upHeap(newIndex);
96+
return true;
97+
}
98+
99+
// Extract minimum element (copy to output reference and remove it from the heap). Returns true on success.
100+
inline bool extract(T& minElement)
101+
{
102+
return peek(minElement) && drop();
103+
}
104+
105+
// Remove minimum element. Returns true on success.
106+
bool drop()
107+
{
108+
if (_size == 0)
109+
return false;
110+
_data[0] = _data[--_size];
111+
downHeap(0);
112+
return true;
113+
}
114+
115+
// Extract minimum element and add a new element in one operation (more efficient than extract followed by insert).
116+
// Returns true on success.
117+
bool replace(T& minElement, const T& newElement)
118+
{
119+
if (_size == 0)
120+
return false;
121+
minElement = _data[0];
122+
_data[0] = newElement;
123+
downHeap(0);
124+
return true;
125+
}
126+
127+
// Remove first elementToRemove that is found. Requires T::operator==(). Least efficient operation of MinHeap with O(N).
128+
bool removeFirstMatch(const T& elementToRemove)
129+
{
130+
// find element
131+
unsigned int i = 0;
132+
for (; i < _size; ++i)
133+
{
134+
if (_data[i] == elementToRemove)
135+
break;
136+
}
137+
if (i == _size)
138+
return false;
139+
140+
// replace it by the last element and heapify
141+
--_size;
142+
if (i < _size)
143+
{
144+
_data[i] = _data[_size];
145+
i = upHeap(i);
146+
downHeap(i);
147+
}
148+
return true;
149+
}
150+
151+
// Get minimum element (copy to output reference and WITHOUT removing it from the heap). Returns true on success.
152+
bool peek(T& minElement) const
153+
{
154+
if (_size == 0)
155+
return false;
156+
minElement = _data[0];
157+
return true;
158+
}
159+
160+
// Return current number of elements in heap.
161+
unsigned int size() const
162+
{
163+
return _size;
164+
}
165+
166+
// Return pointer to elements in heap.
167+
const T* data() const
168+
{
169+
return _data;
170+
}
171+
172+
// Return maximum number of elements that can be stored in the heap.
173+
constexpr unsigned int capacity() const
174+
{
175+
return Capacity;
176+
}
177+
178+
protected:
179+
void swap(unsigned int idx1, unsigned int idx2)
180+
{
181+
const T tmp = _data[idx1];
182+
_data[idx1] = _data[idx2];
183+
_data[idx2] = tmp;
184+
}
185+
186+
unsigned int upHeap(unsigned int idx)
187+
{
188+
while (idx != 0)
189+
{
190+
const unsigned int parentIdx = (idx - 1) / 2;
191+
if (_compare(_data[idx], _data[parentIdx]))
192+
{
193+
swap(idx, parentIdx);
194+
idx = parentIdx;
195+
}
196+
else
197+
{
198+
break;
199+
}
200+
}
201+
return idx;
202+
}
203+
204+
void downHeap(unsigned int idx)
205+
{
206+
while (1)
207+
{
208+
const unsigned int childIdx1 = idx * 2 + 1;
209+
const unsigned int childIdx2 = childIdx1 + 1;
210+
if (childIdx1 >= _size)
211+
return;
212+
unsigned int minChildIdx = childIdx1;
213+
if (childIdx2 < _size && _compare(_data[childIdx2], _data[childIdx1]))
214+
minChildIdx = childIdx2;
215+
if (_compare(_data[minChildIdx], _data[idx]))
216+
{
217+
swap(idx, minChildIdx);
218+
idx = minChildIdx;
219+
}
220+
else
221+
{
222+
return;
223+
}
224+
}
225+
}
226+
227+
T _data[Capacity];
228+
unsigned int _size;
229+
Compare _compare;
230+
};

src/contract_core/contract_def.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,6 @@
257257
// new contracts should be added above this line
258258

259259
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
260-
// forward declaration, defined in qpi_spectrum_impl.h
261-
static void setContractFeeReserve(unsigned int contractIndex, long long newValue);
262260

263261
constexpr unsigned short TESTEXA_CONTRACT_INDEX = (CONTRACT_INDEX + 1);
264262
#undef CONTRACT_INDEX
@@ -492,12 +490,6 @@ static void initializeContracts()
492490
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXB);
493491
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXC);
494492
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXD);
495-
496-
// fill execution fee reserves for test contracts
497-
setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 100000000000);
498-
setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 100000000000);
499-
setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 100000000000);
500-
setContractFeeReserve(TESTEXD_CONTRACT_INDEX, 100000000000);
501493
#endif
502494
}
503495

src/contract_core/qpi_oracle_impl.h

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,12 @@ QPI::sint64 QPI::QpiContextProcedureCall::__qpiQueryOracle(
7070
}
7171
#endif
7272

73-
// notify about error (status and queryId are 0, indicating that an error happened before sending query)
73+
// notify about error (status is 0 and queryId is -1, indicating that an error happened before sending query)
7474
auto* state = (ContractStateType*)contractStates[contractIndex];
7575
auto* input = (QPI::OracleNotificationInput<OracleInterface>*)__qpiAllocLocals(sizeof(QPI::OracleNotificationInput<OracleInterface>));
7676
input->status = ORACLE_QUERY_STATUS_UNKNOWN;
7777
input->queryId = -1;
78+
input->subscriptionId = -1;
7879
QPI::NoData output;
7980
auto* locals = (LocalsType*)__qpiAllocLocals(sizeof(LocalsType));
8081
notificationProcPtr(*this, *state, *input, output, *locals);
@@ -87,22 +88,114 @@ template <typename OracleInterface, typename ContractStateType, typename LocalsT
8788
inline QPI::sint32 QPI::QpiContextProcedureCall::__qpiSubscribeOracle(
8889
const OracleInterface::OracleQuery& query,
8990
void (*notificationProcPtr)(const QPI::QpiContextProcedureCall& qpi, ContractStateType& state, OracleNotificationInput<OracleInterface>& input, NoData& output, LocalsType& locals),
90-
QPI::uint32 notificationIntervalInMilliseconds,
9191
unsigned int notificationProcId,
92+
QPI::uint32 notificationPeriodInMilliseconds,
9293
bool notifyWithPreviousReply
9394
) const
9495
{
96+
// check that query has timestamp member
9597
static_assert(sizeof(query.timestamp) == sizeof(DateAndTime));
96-
// TODO
98+
99+
// check that size of oracle query, oracle reply, notification procedure locals are valid
100+
static_assert(sizeof(OracleInterface::OracleQuery) <= MAX_ORACLE_QUERY_SIZE);
101+
static_assert(sizeof(OracleInterface::OracleReply) <= MAX_ORACLE_REPLY_SIZE);
102+
static_assert(sizeof(LocalsType) <= MAX_SIZE_OF_CONTRACT_LOCALS);
103+
104+
// check that oracle interface and notification input type are as expected
105+
static_assert(sizeof(QPI::OracleNotificationInput<typename OracleInterface::OracleReply>) == 16 + sizeof(OracleInterface::OracleReply));
106+
static_assert(OracleInterface::oracleInterfaceIndex < OI::oracleInterfacesCount);
107+
static_assert(OI::oracleInterfaces[OracleInterface::oracleInterfaceIndex].replySize == sizeof(OracleInterface::OracleReply));
108+
static_assert(OI::oracleInterfaces[OracleInterface::oracleInterfaceIndex].querySize == sizeof(OracleInterface::OracleQuery));
109+
110+
// check contract index
111+
ASSERT(this->_currentContractIndex < 0xffff);
112+
const QPI::uint16 contractIndex = static_cast<QPI::uint16>(this->_currentContractIndex);
113+
114+
// check callback
115+
if (!notificationProcPtr || ContractStateType::__contract_index != contractIndex)
116+
return -1;
117+
118+
// check vs registry of user procedures for notification
119+
const UserProcedureRegistry::UserProcedureData* procData;
120+
if (!userProcedureRegistry || !(procData = userProcedureRegistry->get(notificationProcId)) || procData->procedure != (USER_PROCEDURE)notificationProcPtr)
121+
return -1;
122+
ASSERT(procData->inputSize == sizeof(OracleNotificationInput<OracleInterface>));
123+
ASSERT(procData->localsSize == sizeof(LocalsType));
124+
125+
// get and destroy fee (not adding to contracts execution fee reserve)
126+
const sint64 fee = OracleInterface::getSubscriptionFee(query, notificationPeriodInMilliseconds);
127+
const int contractSpectrumIdx = ::spectrumIndex(this->_currentContractId);
128+
if (fee >= 0 && contractSpectrumIdx >= 0 && decreaseEnergy(contractSpectrumIdx, fee))
129+
{
130+
// log burning of QU
131+
const QuTransfer quTransfer = { this->_currentContractId, m256i::zero(), fee };
132+
logger.logQuTransfer(quTransfer);
133+
134+
// try to subscribe
135+
QPI::sint32 subscriptionId = oracleEngine.startContractSubscription(
136+
contractIndex, OracleInterface::oracleInterfaceIndex,
137+
&query, sizeof(query), notificationPeriodInMilliseconds,
138+
notificationProcId, offsetof(OracleInterface::OracleQuery, timestamp));
139+
if (subscriptionId >= 0)
140+
{
141+
// success
142+
if (notifyWithPreviousReply)
143+
{
144+
// notify contract with previous reply if any is available
145+
const OracleSubscription* subscription = oracleEngine.getOracleSubscription(subscriptionId);
146+
if (subscription && subscription->lastRevealedQueryId >= 0)
147+
{
148+
const int64_t queryId = subscription->lastRevealedQueryId;
149+
auto* state = (ContractStateType*)contractStates[contractIndex];
150+
auto* input = (QPI::OracleNotificationInput<OracleInterface>*)__qpiAllocLocals(sizeof(QPI::OracleNotificationInput<OracleInterface>));
151+
input->status = ORACLE_QUERY_STATUS_SUCCESS;
152+
input->queryId = queryId;
153+
input->subscriptionId = subscriptionId;
154+
oracleEngine.getOracleReply(queryId, &input->reply, sizeof(input->reply));
155+
QPI::NoData output;
156+
auto* locals = (LocalsType*)__qpiAllocLocals(sizeof(LocalsType));
157+
notificationProcPtr(*this, *state, *input, output, *locals);
158+
__qpiFreeLocals();
159+
__qpiFreeLocals();
160+
}
161+
}
162+
163+
return subscriptionId;
164+
}
165+
else if (fee > 0)
166+
{
167+
// failure -> refund fee
168+
oracleEngine.refundFees(_currentContractId, fee);
169+
}
170+
}
171+
#if !defined(NDEBUG) && !defined(NO_UEFI)
172+
else
173+
{
174+
addDebugMessage(L"Cannot start contract oracle subscription due to fee issue!");
175+
}
176+
#endif
177+
178+
// notify about error (status is 0 and subscriptionId is -1, indicating that subscribing failed)
179+
auto* state = (ContractStateType*)contractStates[contractIndex];
180+
auto* input = (QPI::OracleNotificationInput<OracleInterface>*)__qpiAllocLocals(sizeof(QPI::OracleNotificationInput<OracleInterface>));
181+
input->status = ORACLE_QUERY_STATUS_UNKNOWN;
182+
input->queryId = -1;
183+
input->subscriptionId = -1;
184+
QPI::NoData output;
185+
auto* locals = (LocalsType*)__qpiAllocLocals(sizeof(LocalsType));
186+
notificationProcPtr(*this, *state, *input, output, *locals);
187+
__qpiFreeLocals();
188+
__qpiFreeLocals();
97189
return -1;
98190
}
99191

100192
inline bool QPI::QpiContextProcedureCall::unsubscribeOracle(
101193
QPI::sint32 oracleSubscriptionId
102194
) const
103195
{
104-
// TODO
105-
return false;
196+
ASSERT(this->_currentContractIndex < 0xffff);
197+
const QPI::uint16 contractIndex = static_cast<QPI::uint16>(this->_currentContractIndex);
198+
return oracleEngine.stopContractSubscription(oracleSubscriptionId, contractIndex);
106199
}
107200

108201
template <typename OracleInterface>

0 commit comments

Comments
 (0)