|
2 | 2 |
|
3 | 3 | ## Overview |
4 | 4 |
|
5 | | -Commit-reveal prevents oracle front-running by requiring the oracle to commit to randomness before the raffle ends, then reveal after draw is triggered. |
| 5 | +Commit-reveal prevents oracle front-running by requiring the oracle to commit to randomness before the raffle ends, then reveal after the draw is triggered. The contract verifies `SHA-256(secret || nonce) == stored_commitment`. |
6 | 6 |
|
7 | | -## How It Works |
| 7 | +## Event Flow |
8 | 8 |
|
9 | | -### Commit Phase (Before end_time) |
10 | | -1. Oracle generates random `secret` and `nonce` |
11 | | -2. Computes `commitment = SHA-256(secret || nonce)` |
12 | | -3. Submits commitment to contract via `commit_randomness(raffleId, commitment)` |
13 | | -4. Stores secret/nonce locally for later reveal |
14 | | - |
15 | | -### Reveal Phase (After draw triggered) |
16 | | -1. Oracle retrieves stored secret/nonce |
17 | | -2. Submits to contract via `reveal_randomness(raffleId, secret, nonce)` |
18 | | -3. Contract verifies: `SHA-256(secret || nonce) == stored_commitment` |
19 | | -4. If valid, uses secret as randomness seed for winner selection |
20 | | - |
21 | | -## Security Properties |
22 | | - |
23 | | -- **Front-running prevention**: Oracle cannot observe ticket purchases after committing |
24 | | -- **Unpredictability**: Secret is cryptographically random |
25 | | -- **Verifiability**: Anyone can verify the reveal matches the commitment |
26 | | -- **Non-manipulability**: Oracle cannot change commitment after submission |
27 | | - |
28 | | -## Usage |
29 | | - |
30 | | -```typescript |
31 | | -// Commit phase (before raffle ends) |
32 | | -await commitRevealWorker.processCommit({ |
33 | | - raffleId: 1, |
34 | | - endTime: Date.now() + 86400000, |
35 | | -}); |
36 | | - |
37 | | -// Reveal phase (after draw triggered) |
38 | | -await commitRevealWorker.processReveal({ |
39 | | - raffleId: 1, |
40 | | - requestId: 'req-123', |
41 | | -}); |
| 9 | +``` |
| 10 | +RaffleCreated event |
| 11 | + ↓ |
| 12 | +CommitRevealWorker.processCommit() |
| 13 | + → CommitmentService.commit(raffleId) — generates secret + nonce, stores locally |
| 14 | + → TxSubmitterService.submitCommitment() — calls commit_randomness(raffle_id, commitment) |
| 15 | +
|
| 16 | +DrawTriggered event |
| 17 | + ↓ |
| 18 | +CommitRevealWorker.processReveal() |
| 19 | + → CommitmentService.reveal(raffleId) — retrieves stored secret + nonce |
| 20 | + → TxSubmitterService.submitReveal() — calls reveal_randomness(raffle_id, secret, nonce) |
| 21 | + → CommitmentService.clearCommitment() — removes local state |
42 | 22 | ``` |
43 | 23 |
|
44 | | -## Contract Interface |
| 24 | +## Contract Interface (Required) |
45 | 25 |
|
46 | | -The contract must implement: |
| 26 | +The Soroban contract must implement both functions: |
47 | 27 |
|
48 | 28 | ```rust |
49 | | -// Commit phase |
| 29 | +/// Commit phase — oracle submits hash before raffle ends |
50 | 30 | pub fn commit_randomness(env: Env, raffle_id: u32, commitment: BytesN<32>) |
51 | 31 |
|
52 | | -// Reveal phase |
| 32 | +/// Reveal phase — oracle reveals preimage after draw is triggered |
53 | 33 | pub fn reveal_randomness(env: Env, raffle_id: u32, secret: BytesN<32>, nonce: BytesN<16>) |
54 | 34 | ``` |
55 | 35 |
|
56 | | -Contract verification: |
| 36 | +Contract verification logic: |
57 | 37 | ```rust |
58 | | -let computed = sha256(secret || nonce); |
59 | | -assert_eq!(computed, stored_commitment); |
| 38 | +let computed: BytesN<32> = env.crypto().sha256(&Bytes::from_slice(&env, &[secret.as_ref(), nonce.as_ref()].concat())); |
| 39 | +assert_eq!(computed, stored_commitment, "commitment mismatch"); |
60 | 40 | ``` |
61 | 41 |
|
62 | | -## Implementation Status |
| 42 | +For low-stakes raffles that use single-shot randomness, the contract must also implement: |
| 43 | +```rust |
| 44 | +pub fn receive_randomness(env: Env, raffle_id: u32, seed: BytesN<32>, proof: BytesN<64>) |
| 45 | +``` |
63 | 46 |
|
64 | | -✅ CommitmentService - Generate and store commitments |
65 | | -✅ CommitRevealWorker - Process commit and reveal phases |
66 | | -✅ Unit tests for commitment verification |
67 | | -⏳ Contract integration (waiting for contract support) |
68 | | -⏳ TxSubmitter methods for commit/reveal transactions |
| 47 | +## Oracle Events Consumed |
69 | 48 |
|
70 | | -## Configuration |
| 49 | +| Event name | Trigger | Oracle action | |
| 50 | +|---|---|---| |
| 51 | +| `RaffleCreated` | New raffle created | `commit_randomness` | |
| 52 | +| `DrawTriggered` | Draw initiated | `reveal_randomness` | |
| 53 | +| `RandomnessRequested` | Low-stakes draw | `receive_randomness` (direct) | |
71 | 54 |
|
72 | | -Set threshold for commit-reveal vs direct randomness: |
| 55 | +Event payload (XDR map): |
| 56 | +- `RaffleCreated`: `{ raffle_id: u32, end_time: u64 }` |
| 57 | +- `DrawTriggered`: `{ raffle_id: u32, request_id: string|u64|bytes }` |
| 58 | +- `RandomnessRequested`: `{ raffle_id: u32, request_id: string|u64|bytes }` |
73 | 59 |
|
74 | | -```typescript |
75 | | -// In worker configuration |
76 | | -const USE_COMMIT_REVEAL = prizeAmount >= 500; // XLM |
| 60 | +## Security Properties |
77 | 61 |
|
78 | | -if (USE_COMMIT_REVEAL) { |
79 | | - await commitRevealWorker.processCommit(...); |
80 | | - // Later... |
81 | | - await commitRevealWorker.processReveal(...); |
82 | | -} else { |
83 | | - await randomnessWorker.processRequest(...); |
84 | | -} |
85 | | -``` |
| 62 | +- **Front-running prevention**: Oracle commits before ticket sales close; cannot observe final ticket set before choosing randomness |
| 63 | +- **Unpredictability**: Secret is 32 cryptographically random bytes |
| 64 | +- **Verifiability**: Anyone can verify `SHA-256(secret || nonce) == commitment` |
| 65 | +- **Non-manipulability**: Commitment is on-chain before the draw; oracle cannot change it |
86 | 66 |
|
87 | | -## Testing |
| 67 | +## Configuration |
88 | 68 |
|
89 | | -```bash |
90 | | -npm test commitment.service.spec.ts |
| 69 | +```env |
| 70 | +# Enable commit-reveal for high-stakes raffles (prize >= threshold) |
| 71 | +COMMIT_REVEAL_ENABLED=true # default: true when contract supports it |
| 72 | +HIGH_STAKES_THRESHOLD_XLM=500 # raffles above this use commit-reveal + VRF |
91 | 73 | ``` |
92 | 74 |
|
| 75 | +Low-stakes raffles (prize < threshold) skip commit-reveal and call `receive_randomness` directly via `RandomnessWorker`. |
| 76 | + |
| 77 | +## Implementation Status |
| 78 | + |
| 79 | +| Component | Status | |
| 80 | +|---|---| |
| 81 | +| `CommitmentService` — generate/store/verify commitments | ✅ | |
| 82 | +| `CommitRevealWorker` — processCommit / processReveal | ✅ | |
| 83 | +| `TxSubmitterService.submitCommitment` | ✅ | |
| 84 | +| `TxSubmitterService.submitReveal` | ✅ | |
| 85 | +| `EventListenerService` — `RaffleCreated` → commit | ✅ | |
| 86 | +| `EventListenerService` — `DrawTriggered` → reveal | ✅ | |
| 87 | +| Contract `commit_randomness` / `reveal_randomness` | ⏳ Pending contract deployment | |
| 88 | +| Persistent commitment storage (Redis/DB) | ⏳ Currently in-memory | |
| 89 | + |
93 | 90 | ## Notes |
94 | 91 |
|
95 | | -- Commitment storage is in-memory; consider persistent storage for production |
96 | | -- Ensure commit happens before end_time to prevent front-running |
97 | | -- Monitor for failed reveals and implement retry logic |
| 92 | +- Commitment storage is in-memory. A process restart before reveal will lose pending commitments. For production, persist to Redis or a database keyed by `raffle_id`. |
| 93 | +- Ensure `commit_randomness` is called before `end_time` to prevent front-running. |
| 94 | +- If a reveal fails after max retries, the job is retained in the Bull dead-letter queue and an `[ALERT]` log is emitted. |
0 commit comments