Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion harnesses/gas-estimation/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ func chains() []Chain {
Slug: "arbitrum",
ChainID: 42161,
RealizedRPC: envDefault("GAS_REALIZED_RPC_ARBITRUM", "https://arbitrum-one-rpc.publicnode.com"),
VerifyRPC: envDefault("GAS_REALIZED_RPC_VERIFY_ARBITRUM", "https://arbitrum.drpc.org"),
// dRPC's arbitrum public routes to 1rpc.io which rate-limits within 30s.
// arb1.arbitrum.io is Offchain Labs' own hosted RPC — genuinely different
// upstream from PublicNode (Node Fleet), sustains 5+ req/s in tests.
VerifyRPC: envDefault("GAS_REALIZED_RPC_VERIFY_ARBITRUM", "https://arb1.arbitrum.io/rpc"),
OwlracleSlug: "arb",
BlockTimeSec: 1, // Arb One has sub-second nominal block time; realizer catch-up loop handles the burst.
SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan, OracleMetaMask},
Expand Down
63 changes: 62 additions & 1 deletion harnesses/gas-estimation/cmd/script/oracles.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
Expand All @@ -21,6 +23,39 @@ const (
httpTimeout = 8 * time.Second
)

// proxyClient is a rotating-IP HTTP client built at process start
// from HTTPS_PROXY / HTTP_PROXY. Used only by pollers that need to
// evade per-IP throttles (currently: Owlracle, whose 10 req/h guest
// ceiling per IP made every 60 s poll from a single VPS collapse
// into HTTP 403 within an hour). Keep-alives are disabled so every
// request opens a fresh TCP conn and the rotating proxy (webshare)
// assigns a new upstream IP per request. If no proxy env is set,
// proxyClient stays nil and pollers fall back to the default client.
var proxyClient *http.Client

func init() {
raw := os.Getenv("HTTPS_PROXY")
if raw == "" {
raw = os.Getenv("HTTP_PROXY")
}
if raw == "" {
return
}
parsed, err := url.Parse(raw)
if err != nil {
fmt.Printf("[proxy] parse %q: %v\n", raw, err)
return
}
proxyClient = &http.Client{
Timeout: httpTimeout,
Transport: &http.Transport{
Proxy: http.ProxyURL(parsed),
DisableKeepAlives: true,
},
}
fmt.Printf("[proxy] wired via %s\n", parsed.Host)
}

// pollResult is what every oracle client returns. TargetBlock is the
// block these predictions apply to (oracle-specific: Blocknative
// gives an explicit next-block number, feeHistory returns the
Expand Down Expand Up @@ -228,7 +263,12 @@ type owlResp struct {

func pollOwlracle(ctx context.Context, ep OracleEndpoint) pollResult {
req, _ := http.NewRequestWithContext(ctx, "GET", ep.URL, nil)
body, status, err := httpDo(ctx, req)
// Owlracle's guest tier is 10 req/h per IP. Our 4 chains × 60s
// poll = 240 req/h from a single VPS IP, which collapses to a
// permanent HTTP 403 within an hour. Route through the rotating
// proxy pool so each request gets a fresh upstream IP and stays
// under the per-IP guest ceiling.
body, status, err := httpDoProxied(ctx, req)
if err != nil {
return pollResult{Err: err}
}
Expand Down Expand Up @@ -491,6 +531,27 @@ func httpDo(ctx context.Context, req *http.Request) ([]byte, int, error) {
return raw, resp.StatusCode, nil
}

// httpDoProxied routes the request through proxyClient if a proxy is
// configured (webshare rotating pool via HTTPS_PROXY env). Falls
// back to httpDo when no proxy is set — useful for local runs
// without proxy access.
func httpDoProxied(ctx context.Context, req *http.Request) ([]byte, int, error) {
if proxyClient == nil {
return httpDo(ctx, req)
}
req.Header.Set("User-Agent", "OpenChainBench/1.0 (+https://openchainbench.com)")
resp, err := proxyClient.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, err
}
return raw, resp.StatusCode, nil
}

func parseHexU64(s string) (uint64, error) {
s = strings.TrimPrefix(s, "0x")
if s == "" {
Expand Down
28 changes: 20 additions & 8 deletions harnesses/gas-estimation/cmd/script/realized.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,26 @@ func runRealizer(ctx context.Context, buf *Buffer, chain Chain) {
lastObserved = head
return
}
// Catch up at most 5 blocks per tick to bound the per-tick
// work even if the realizer lagged behind. On Polygon /
// Avalanche (2s block time, 12s realizer cadence) we expect
// to see 6 new blocks per tick on average, so the catch-up
// loop intentionally trails reality by a block or two —
// that's fine because the buffer holds predictions for
// pendingTTLBlocks worth of blocks anyway.
for n := lastObserved + 1; n <= head && n <= lastObserved+5; n++ {
// Per-chain catch-up cap. ETH (12s blocks) needs 5, Polygon
// and Avalanche (2s blocks) need ~10, Arbitrum L2 (sub-second
// blocks) sees ~48 new blocks per 12s realizer tick and falls
// hopelessly behind at cap=5 (buffers spike to 99 predictions
// per oracle, most get evicted before grading). Cap scales as
// `max(5, ceil(realizedPollInterval / BlockTimeSec) + 5)` —
// one tick of blocks plus a small slack. Hard ceiling of 60
// so a stalled realizer can never fill the tick window with
// fetches on the busiest chain.
catchupCap := uint64(5)
if chain.BlockTimeSec > 0 {
c := uint64(int(realizedPollInterval.Seconds())/chain.BlockTimeSec + 5)
if c > catchupCap {
catchupCap = c
}
}
if catchupCap > 60 {
catchupCap = 60
}
for n := lastObserved + 1; n <= head && n <= lastObserved+catchupCap; n++ {
processBlock(ctx, buf, n, chain)
lastObserved = n
}
Expand Down
Loading