Skip to content

Commit b7b2968

Browse files
aditya520claude
andcommitted
fix(fortuna): confirm reveal via on-chain event for already-revealed requests
When a reveal errors after retries but the request is gone from contract storage, confirm a reveal actually happened by querying the on-chain v2 Revealed event for (provider, sequence) before recording a terminal state: record Completed if found, otherwise leave Pending. This is reorg-safe -- get_request_v2 == None is ambiguous (revealed vs reorged out), so we no longer infer a reveal from absence. Reverts the earlier Resolved-state approach (and its entropy-explorer changes) in this PR; recording the existing Completed state needs no explorer change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8935b10 commit b7b2968

10 files changed

Lines changed: 112 additions & 206 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/entropy-explorer/src/components/Home/search-controls.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ const useStatusSelect = () => {
136136
{ id: Status.Pending },
137137
{ id: Status.Failed },
138138
{ id: Status.CallbackError },
139-
{ id: Status.Resolved },
140139
],
141140
},
142141
],
@@ -169,9 +168,6 @@ const parseStatus = (value: string) => {
169168
case StatusParams[Status.Failed]: {
170169
return Status.Failed;
171170
}
172-
case StatusParams[Status.Resolved]: {
173-
return Status.Resolved;
174-
}
175171
default: {
176172
return "all";
177173
}

apps/entropy-explorer/src/components/Status/index.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,5 @@ export const Status = ({ status, ...props }: Props) => {
3737
</StatusImpl>
3838
);
3939
}
40-
case StatusType.Resolved: {
41-
return (
42-
<StatusImpl variant="info" {...props}>
43-
REVEALED
44-
</StatusImpl>
45-
);
46-
}
4740
}
4841
};

apps/entropy-explorer/src/requests.ts

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,6 @@ export const getRequests = async ({
9494
case "pending": {
9595
return Request.Pending(common);
9696
}
97-
case "resolved": {
98-
return Request.Resolved({
99-
...common,
100-
providerContribution:
101-
request.state.provider_random_number,
102-
});
103-
}
10497
case "failed": {
10598
return Request.Failed({
10699
...common,
@@ -201,11 +194,6 @@ const failedSchema = z.strictObject({
201194
state: z.literal("failed"),
202195
});
203196

204-
const resolvedSchema = z.strictObject({
205-
provider_random_number: z.string(),
206-
state: z.literal("resolved"),
207-
});
208-
209197
const fortunaSchema = z.strictObject({
210198
requests: z.array(
211199
z.strictObject({
@@ -219,12 +207,7 @@ const fortunaSchema = z.strictObject({
219207
request_tx_hash: hexStringSchema,
220208
sender: hexStringSchema,
221209
sequence: z.number(),
222-
state: z.union([
223-
completedStateSchema,
224-
pendingStateSchema,
225-
failedSchema,
226-
resolvedSchema,
227-
]),
210+
state: z.union([completedStateSchema, pendingStateSchema, failedSchema]),
228211
user_random_number: z.string(),
229212
}),
230213
),
@@ -236,7 +219,6 @@ export enum Status {
236219
Failed,
237220
CallbackError,
238221
Complete,
239-
Resolved,
240222
}
241223

242224
type BaseArgs = {
@@ -264,9 +246,6 @@ type CompletedArgs = BaseArgs & {
264246
type CallbackErrorArgs = CompletedArgs & {
265247
reason: string;
266248
};
267-
type ResolvedArgs = BaseArgs & {
268-
providerContribution: string;
269-
};
270249

271250
const Request = {
272251
CallbackErrored: (args: CallbackErrorArgs) => ({
@@ -285,24 +264,18 @@ const Request = {
285264
status: Status.Pending as const,
286265
...args,
287266
}),
288-
Resolved: (args: ResolvedArgs) => ({
289-
status: Status.Resolved as const,
290-
...args,
291-
}),
292267
};
293268
export type Request = ReturnType<(typeof Request)[keyof typeof Request]>;
294269
export type PendingRequest = ReturnType<typeof Request.Pending>;
295270
export type FailedRequest = ReturnType<typeof Request.Failed>;
296271
export type CallbackErrorRequest = ReturnType<typeof Request.CallbackErrored>;
297272
export type CompleteRequest = ReturnType<typeof Request.Complete>;
298-
export type ResolvedRequest = ReturnType<typeof Request.Resolved>;
299273

300274
export const StatusParams = {
301275
[Status.Pending]: "pending",
302276
[Status.Failed]: "failed",
303277
[Status.Complete]: "complete",
304278
[Status.CallbackError]: "callback-error",
305-
[Status.Resolved]: "resolved",
306279
} as const;
307280

308281
const toFortunaStatus = (status: string) => {
@@ -319,9 +292,6 @@ const toFortunaStatus = (status: string) => {
319292
case StatusParams[Status.CallbackError]: {
320293
return "CallbackErrored";
321294
}
322-
case StatusParams[Status.Resolved]: {
323-
return "Resolved";
324-
}
325295
default: {
326296
return;
327297
}

apps/fortuna/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fortuna"
3-
version = "9.5.0"
3+
version = "9.4.1"
44
edition = "2021"
55

66
[lib]

apps/fortuna/src/api.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ pub enum StateTag {
4545
Failed,
4646
Completed,
4747
CallbackErrored,
48-
Resolved,
4948
}
5049

5150
#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]

apps/fortuna/src/chain/ethereum.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
use {
44
crate::{
55
api::ChainId,
6-
chain::reader::{self, BlockNumber, BlockStatus, EntropyReader, RequestedV2Event},
6+
chain::reader::{
7+
self, BlockNumber, BlockStatus, EntropyReader, RequestedV2Event, RevealedV2Event,
8+
},
79
config::EthereumConfig,
810
eth_utils::{
911
eth_gas_oracle::EthProviderOracle,
@@ -22,7 +24,7 @@ use {
2224
prelude::JsonRpcClient,
2325
providers::{Http, Middleware, Provider},
2426
signers::{LocalWallet, Signer},
25-
types::{BlockNumber as EthersBlockNumber, U256},
27+
types::{BlockNumber as EthersBlockNumber, H256, U256},
2628
},
2729
sha3::{Digest, Keccak256},
2830
std::sync::Arc,
@@ -339,6 +341,49 @@ impl<T: JsonRpcClient + 'static> EntropyReader for PythRandom<Provider<T>> {
339341
.collect())
340342
}
341343

344+
async fn get_revealed_event(
345+
&self,
346+
provider: Address,
347+
sequence_number: u64,
348+
from_block: BlockNumber,
349+
) -> Result<Option<RevealedV2Event>> {
350+
let mut event = self.revealed_2_filter();
351+
// provider and sequence_number are indexed (topic1, topic3), so filter server-side to this
352+
// single request rather than scanning the whole block range.
353+
event.filter = event
354+
.filter
355+
.address(self.address())
356+
.from_block(from_block)
357+
.topic1(provider)
358+
.topic3(H256::from_low_u64_be(sequence_number));
359+
360+
let res: Vec<(Revealed2Filter, LogMeta)> = event.query_with_meta().await?;
361+
// The callback-failed branch emits a Revealed event without clearing the request, so only a
362+
// `callback_failed == false` event is a clearing reveal.
363+
let Some((revealed, log_meta)) = res.into_iter().find(|(r, _)| !r.callback_failed) else {
364+
return Ok(None);
365+
};
366+
367+
let gas_used = self
368+
.client()
369+
.get_transaction_receipt(log_meta.transaction_hash)
370+
.await
371+
.ok()
372+
.flatten()
373+
.and_then(|receipt| receipt.gas_used)
374+
.unwrap_or_default();
375+
376+
Ok(Some(RevealedV2Event {
377+
provider_revelation: revealed.provider_contribution,
378+
random_number: revealed.random_number,
379+
callback_failed: revealed.callback_failed,
380+
callback_return_value: revealed.callback_return_value,
381+
callback_gas_used: revealed.callback_gas_used,
382+
gas_used,
383+
log_meta,
384+
}))
385+
}
386+
342387
async fn estimate_reveal_with_callback_gas(
343388
&self,
344389
sender: Address,

apps/fortuna/src/chain/reader.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use {
33
axum::async_trait,
44
ethers::{
55
prelude::LogMeta,
6-
types::{Address, BlockNumber as EthersBlockNumber, U256},
6+
types::{Address, BlockNumber as EthersBlockNumber, Bytes, U256},
77
},
88
};
99

@@ -54,6 +54,18 @@ pub struct RequestedV2Event {
5454
pub log_meta: LogMeta,
5555
}
5656

57+
/// A v2 `Revealed` event that cleared a request on chain.
58+
#[derive(Clone)]
59+
pub struct RevealedV2Event {
60+
pub provider_revelation: [u8; 32],
61+
pub random_number: [u8; 32],
62+
pub callback_failed: bool,
63+
pub callback_return_value: Bytes,
64+
pub callback_gas_used: u32,
65+
pub gas_used: U256,
66+
pub log_meta: LogMeta,
67+
}
68+
5769
/// EntropyReader is the read-only interface of the Entropy contract.
5870
#[async_trait]
5971
pub trait EntropyReader: Send + Sync {
@@ -75,6 +87,15 @@ pub trait EntropyReader: Send + Sync {
7587
provider: Address,
7688
) -> Result<Vec<RequestedV2Event>>;
7789

90+
/// Find the v2 `Revealed` event that cleared `(provider, sequence_number)`, searching
91+
/// `[from_block, latest]`. Returns `None` if no such reveal is on chain (e.g. reorged out).
92+
async fn get_revealed_event(
93+
&self,
94+
provider: Address,
95+
sequence_number: u64,
96+
from_block: BlockNumber,
97+
) -> Result<Option<RevealedV2Event>>;
98+
7899
/// Estimate the gas required to reveal a random number with a callback.
79100
async fn estimate_reveal_with_callback_gas(
80101
&self,
@@ -235,6 +256,15 @@ pub mod mock {
235256
Ok(vec![])
236257
}
237258

259+
async fn get_revealed_event(
260+
&self,
261+
_provider: Address,
262+
_sequence_number: u64,
263+
_from_block: BlockNumber,
264+
) -> Result<Option<super::RevealedV2Event>> {
265+
Ok(None)
266+
}
267+
238268
async fn estimate_reveal_with_callback_gas(
239269
&self,
240270
_sender: Address,

0 commit comments

Comments
 (0)