Skip to content

Commit 5e7fe63

Browse files
feat(prover-api): production-ready claim migration prover with security hardening (#68)
* refactor(prover-api): modularize codebase with job queue and worker pool Split monolithic main.rs into separate modules for better maintainability: - config.rs: Configuration loading and validation - types.rs: Shared types (AppState, JobEntry, CachedProof, etc.) - handlers.rs: HTTP request handlers (submit_job, job_status, health) - prover.rs: ZK proof generation logic - queue.rs: JobQueue and WorkerPool for concurrent proof generation - cache.rs: Proof caching with TTL cleanup - rate_limit.rs: Per-pubkey rate limiting - jobs.rs: Job entry cleanup - validation.rs: Request validation utilities Added production features: - Configurable worker pool size and queue capacity - Background cleanup tasks for cache, rate limits, and stale jobs - Max body size limiting - Proof timeout configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(prover-api): add eligibility verification, signature validation, and IP rate limiting Adds production hardening features: - Load eligibility data from merkle-tree.json at startup with pubkey-based O(1) lookup - Verify sr25519 signatures before proof generation using schnorrkel - IP-based rate limiting (separate from pubkey rate limiting) to catch bots/scanners - Add proof metrics tracking (completions, timeouts, background tasks) - Update Dockerfile to bundle eligibility data with configurable path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore(prover-api): tune default config values for production use - Increase cache TTL from 10 minutes to 1 hour since proofs are deterministic and expensive to compute - Reduce queue capacity from 100 to 50 for better wait time estimation (~1 hour max with 4 workers) - Increase jobs TTL from 10 minutes to 1 hour to let users return for their proof 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(prover-api): use explicit Mainnet mode for SP1 network prover ProverClient::from_env() defaults to Reserved mode which has an invalid domain for the mainnet network. Explicitly use NetworkMode::Mainnet via ProverClient::builder().network_for(NetworkMode::Mainnet) to ensure correct mainnet RPC URL is used. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(prover-api): improve security and fix concurrency control - Add trust_proxy_headers config for secure IP extraction behind reverse proxies - When false (default), only socket address is used for rate limiting - When true, respects X-Forwarded-For and X-Real-IP headers - Strip port from socket addresses for consistent rate limiting - Add configurable RPC timeout for on-chain verification - timeout_seconds field in VerifyOnchainConfig - Uses RPC_TIMEOUT_SECONDS env var (default: 10s) - Fix unbounded concurrency when proof generation times out - Acquire semaphore permit before spawning task, not inside - Hold permit until blocking task actually completes (not just timeout) - Use tokio::select! instead of tokio::time::timeout to keep handle valid - Add decrement_timed_out_still_running metric tracking - Fix JobQueue::from_sender to use shared queue_size counter - Prevents queue size tracking inconsistencies - Add error handling for misconfigured state Co-Authored-By: Claude Opus 4.5 <[email protected]> * test(prover-api): add httpmock tests for on-chain verification - Add httpmock dev dependency for mocking RPC responses - Add tests for verify_onchain_proof success and revert cases - Add test for check_already_claimed with claimed/unclaimed scenarios - Minor code cleanup and formatting fixes Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(prover-api): use consistent timestamp in rate limiter Capture timestamp once at the start of check_and_update() to avoid potential race condition where multiple calls to now_ts() could return different values. Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent ea1a7b4 commit 5e7fe63

File tree

18 files changed

+4483
-461
lines changed

18 files changed

+4483
-461
lines changed

packages/migration-claim/sp1/Cargo.lock

Lines changed: 574 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../merkle-tree.json

packages/migration-claim/sp1/prover-api/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ alloy-sol-types = { workspace = true }
1010
anyhow = { workspace = true }
1111
axum = "0.7"
1212
hex = { workspace = true }
13+
merlin = { workspace = true }
1314
primitive-types = "0.12"
1415
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
16+
schnorrkel = { workspace = true }
1517
serde = { workspace = true }
1618
serde_json = "1.0"
1719
sp1-sdk = { workspace = true }
@@ -22,3 +24,6 @@ tracing = "0.1"
2224
tracing-subscriber = { version = "0.3", features = ["fmt"] }
2325
uuid = { version = "1", features = ["v4"] }
2426
rustls = { version = "0.23", features = ["ring"] }
27+
28+
[dev-dependencies]
29+
httpmock = "0.7"

packages/migration-claim/sp1/prover-api/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,17 @@ RUN apt-get update \
1616
&& apt-get install -y --no-install-recommends ca-certificates libssl3 \
1717
&& rm -rf /var/lib/apt/lists/*
1818

19+
# Create app directory structure
20+
WORKDIR /app
21+
22+
# Copy eligibility data (can be overridden via volume mount at runtime)
23+
COPY packages/migration-claim/sp1/merkle-tree.json /app/data/merkle-tree.json
24+
25+
# Copy binary
1926
COPY --from=builder /repo/packages/migration-claim/sp1/target/release/tnt-claim-prover-api /usr/local/bin/tnt-claim-prover-api
2027

28+
# Set default eligibility path (overridable via ELIGIBILITY_FILE env var)
29+
ENV ELIGIBILITY_FILE=/app/data/merkle-tree.json
2130
ENV PORT=8080
2231
EXPOSE 8080
2332

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/bin/bash
2+
# Integration test script for SP1 Prover API
3+
# Run this script after starting the API server
4+
5+
set -e
6+
7+
API_URL="${API_URL:-http://localhost:8080}"
8+
9+
# Colors for output
10+
RED='\033[0;31m'
11+
GREEN='\033[0;32m'
12+
YELLOW='\033[1;33m'
13+
NC='\033[0m' # No Color
14+
15+
pass() {
16+
echo -e "${GREEN}PASS${NC}: $1"
17+
}
18+
19+
fail() {
20+
echo -e "${RED}FAIL${NC}: $1"
21+
exit 1
22+
}
23+
24+
warn() {
25+
echo -e "${YELLOW}WARN${NC}: $1"
26+
}
27+
28+
# Test data
29+
VALID_SS58="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
30+
VALID_SIGNATURE="0x$(printf 'ab%.0s' {1..64})"
31+
VALID_EVM_ADDRESS="0x742d35Cc6634C0532925a3b844Bc9e7595f4a3b2"
32+
VALID_CHALLENGE="0x$(printf '12%.0s' {1..32})"
33+
VALID_AMOUNT="1000000000000000000"
34+
35+
echo "======================================"
36+
echo "SP1 Prover API Integration Tests"
37+
echo "API URL: $API_URL"
38+
echo "======================================"
39+
echo
40+
41+
# Test 1: Health endpoint
42+
echo "Test 1: Health endpoint"
43+
HEALTH=$(curl -s "$API_URL/health")
44+
if echo "$HEALTH" | grep -q '"status":"ok"'; then
45+
pass "Health endpoint returns ok"
46+
echo " Response: $HEALTH"
47+
else
48+
fail "Health endpoint failed: $HEALTH"
49+
fi
50+
echo
51+
52+
# Test 2: Missing required fields
53+
echo "Test 2: Missing required fields -> 400"
54+
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
55+
-H "Content-Type: application/json" \
56+
-d '{"ss58Address": ""}')
57+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
58+
BODY=$(echo "$RESPONSE" | head -n-1)
59+
60+
if [ "$HTTP_CODE" = "400" ]; then
61+
pass "Missing fields returns 400"
62+
echo " Response: $BODY"
63+
else
64+
fail "Expected 400, got $HTTP_CODE: $BODY"
65+
fi
66+
echo
67+
68+
# Test 3: Invalid signature length
69+
echo "Test 3: Invalid signature length -> 400"
70+
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
71+
-H "Content-Type: application/json" \
72+
-d "{
73+
\"ss58Address\": \"$VALID_SS58\",
74+
\"signature\": \"0x1234\",
75+
\"evmAddress\": \"$VALID_EVM_ADDRESS\",
76+
\"challenge\": \"$VALID_CHALLENGE\",
77+
\"amount\": \"$VALID_AMOUNT\"
78+
}")
79+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
80+
BODY=$(echo "$RESPONSE" | head -n-1)
81+
82+
if [ "$HTTP_CODE" = "400" ]; then
83+
pass "Invalid signature length returns 400"
84+
echo " Response: $BODY"
85+
else
86+
fail "Expected 400, got $HTTP_CODE: $BODY"
87+
fi
88+
echo
89+
90+
# Test 4: Invalid amount (hex instead of decimal)
91+
echo "Test 4: Invalid amount format -> 400"
92+
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
93+
-H "Content-Type: application/json" \
94+
-d "{
95+
\"ss58Address\": \"$VALID_SS58\",
96+
\"signature\": \"$VALID_SIGNATURE\",
97+
\"evmAddress\": \"$VALID_EVM_ADDRESS\",
98+
\"challenge\": \"$VALID_CHALLENGE\",
99+
\"amount\": \"0x1234\"
100+
}")
101+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
102+
BODY=$(echo "$RESPONSE" | head -n-1)
103+
104+
if [ "$HTTP_CODE" = "400" ]; then
105+
pass "Hex amount returns 400"
106+
echo " Response: $BODY"
107+
else
108+
fail "Expected 400, got $HTTP_CODE: $BODY"
109+
fi
110+
echo
111+
112+
# Test 5: Invalid SS58 address
113+
echo "Test 5: Invalid SS58 address -> 400"
114+
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
115+
-H "Content-Type: application/json" \
116+
-d "{
117+
\"ss58Address\": \"invalid_address_here\",
118+
\"signature\": \"$VALID_SIGNATURE\",
119+
\"evmAddress\": \"$VALID_EVM_ADDRESS\",
120+
\"challenge\": \"$VALID_CHALLENGE\",
121+
\"amount\": \"$VALID_AMOUNT\"
122+
}")
123+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
124+
BODY=$(echo "$RESPONSE" | head -n-1)
125+
126+
if [ "$HTTP_CODE" = "400" ]; then
127+
pass "Invalid SS58 address returns 400"
128+
echo " Response: $BODY"
129+
else
130+
fail "Expected 400, got $HTTP_CODE: $BODY"
131+
fi
132+
echo
133+
134+
# Test 6: Unknown job ID -> 404
135+
echo "Test 6: Unknown job ID -> 404"
136+
RESPONSE=$(curl -s -w "\n%{http_code}" "$API_URL/status/non-existent-job-id")
137+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
138+
BODY=$(echo "$RESPONSE" | head -n-1)
139+
140+
if [ "$HTTP_CODE" = "404" ]; then
141+
pass "Unknown job ID returns 404"
142+
echo " Response: $BODY"
143+
else
144+
fail "Expected 404, got $HTTP_CODE: $BODY"
145+
fi
146+
echo
147+
148+
# Test 7: Valid request (only if in mock mode)
149+
echo "Test 7: Valid request submission"
150+
if echo "$HEALTH" | grep -q '"prover_mode":"mock"'; then
151+
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
152+
-H "Content-Type: application/json" \
153+
-d "{
154+
\"ss58Address\": \"$VALID_SS58\",
155+
\"signature\": \"$VALID_SIGNATURE\",
156+
\"evmAddress\": \"$VALID_EVM_ADDRESS\",
157+
\"challenge\": \"$VALID_CHALLENGE\",
158+
\"amount\": \"$VALID_AMOUNT\"
159+
}")
160+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
161+
BODY=$(echo "$RESPONSE" | head -n-1)
162+
163+
if [ "$HTTP_CODE" = "200" ]; then
164+
pass "Valid request accepted"
165+
echo " Response: $BODY"
166+
167+
# Extract job ID and poll status
168+
JOB_ID=$(echo "$BODY" | grep -o '"jobId":"[^"]*"' | cut -d'"' -f4)
169+
if [ -n "$JOB_ID" ]; then
170+
echo " Polling status for job: $JOB_ID"
171+
for i in {1..10}; do
172+
STATUS=$(curl -s "$API_URL/status/$JOB_ID")
173+
echo " Status: $STATUS"
174+
if echo "$STATUS" | grep -q '"status":"completed"'; then
175+
pass "Job completed successfully"
176+
break
177+
elif echo "$STATUS" | grep -q '"status":"failed"'; then
178+
warn "Job failed (expected in mock mode without proper setup)"
179+
break
180+
fi
181+
sleep 1
182+
done
183+
fi
184+
else
185+
fail "Expected 200, got $HTTP_CODE: $BODY"
186+
fi
187+
else
188+
warn "Skipping - API is not in mock mode (prover_mode != mock)"
189+
fi
190+
echo
191+
192+
# Test 8: Rate limiting test
193+
echo "Test 8: Rate limiting"
194+
if echo "$HEALTH" | grep -q '"prover_mode":"mock"'; then
195+
echo " Sending multiple rapid requests..."
196+
for i in {1..5}; do
197+
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
198+
-H "Content-Type: application/json" \
199+
-d "{
200+
\"ss58Address\": \"$VALID_SS58\",
201+
\"signature\": \"$VALID_SIGNATURE\",
202+
\"evmAddress\": \"$VALID_EVM_ADDRESS\",
203+
\"challenge\": \"$VALID_CHALLENGE\",
204+
\"amount\": \"$VALID_AMOUNT\"
205+
}")
206+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
207+
BODY=$(echo "$RESPONSE" | head -n-1)
208+
echo " Request $i: HTTP $HTTP_CODE"
209+
210+
if [ "$HTTP_CODE" = "429" ]; then
211+
pass "Rate limiting kicked in on request $i"
212+
echo " Response: $BODY"
213+
break
214+
fi
215+
done
216+
else
217+
warn "Skipping - API is not in mock mode"
218+
fi
219+
echo
220+
221+
echo "======================================"
222+
echo "Integration tests completed"
223+
echo "======================================"

0 commit comments

Comments
 (0)