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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ corpus-gen: ## Build the corpus generator
@mkdir -p build
@go build -o build/corpus-gen ./bench/rpc/cmd/corpus-gen

feeder-sim: ## Build the feeder gateway simulator
@mkdir -p build
@go build -o build/feeder-sim ./bench/sync/cmd/feeder-sim

MINIMUM_RUST_VERSION = 1.94.1
CURR_RUST_VERSION = $(shell rustc --version | grep -o '[0-9.]\+' | head -n1)
check-rust: ## Ensure rust version is greater than minimum
Expand Down
69 changes: 69 additions & 0 deletions bench/sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Sync benchmark: feeder-sim

Captures feeder gateway (FGW) responses for a block range, then serves them from memory behind a
simulated chain tip. Removes FGW latency, rate limits and chain activity from sync benchmarks.

## Use

```sh
make feeder-sim # -> build/feeder-sim
build/feeder-sim --data ./data/mainnet --network mainnet --from 1500000 --to 1501000 --listen off # capture
build/feeder-sim --data ./data/mainnet --from 1500000 --to 1501000 # serve, 2s cadence
build/feeder-sim --data ./data/mainnet --from 1500000 --to 1501000 --speed 1 # serve, captured block times
build/feeder-sim --data ./data/mainnet --from 1500000 --to 1501000 --tip 1501000 # serve everything at once
```

Capture is resumable. A 400 from the FGW usually means `--to` is above the tip. One `--data`
directory per network, checked via `get_contract_addresses`. Without `--network` the sim is offline and fails on a missing file; with
`--network` it fills gaps before serving.

## Flags

| Flag | Meaning | Default |
| ------------------- | -------------------------------------------------------------------------- | ---------------- |
| `--data` | dataset directory (required) | |
| `--from`, `--to` | block range, inclusive (required) | |
| `--network` | capture source: `mainnet`, `sepolia`, `sepolia-integration` | offline |
| `--listen` | `host:port`; `:7070` binds all interfaces; `off` = capture only | `127.0.0.1:7070` |
| `--tip` | initial tip, in `[from, to]` | `from` |
| `--interval` | advance the tip by one block every interval | `2s` |
| `--speed` | replay captured block timestamps at this multiplier; excludes `--interval` | unset |
| `--latency` | fixed delay added to every response, no jitter | `0` |
| `--api-key` | `X-Throttling-Bypass` header during capture | |
| `--concurrency` | parallel capture requests | `8` |
| `--capture-timeout` | per-request timeout during capture | `30s` |
| `--capture-retries` | retries per request during capture | `5` |
| `--log-level` | `debug`, `info`, `warn`, `error` | `info` |

`blockNumber=latest` resolves to the tip. Blocks above the tip get 400 (debug log). Classes are not tip-gated.

## Juno

```sh
juno --db-path <copy of a DB synced to from-1> \
--cn-name mainnet --cn-feeder-url http://127.0.0.1:7070/feeder_gateway/ \
--cn-gateway-url http://127.0.0.1:7070/gateway/ \
--cn-l2-chain-id SN_MAIN --cn-l1-chain-id 1 \
--cn-core-contract-address 0xc662c410c0ecf747543f5ba90660f6abebd9c8c4 \
--cn-unverifiable-range 0,0 --preconfirmed-poll-interval 0 --metrics
```

- Restore the DB copy before every run. To prepare a DB at `X-1`, run against the sim with `--tip X-1`.
- Keep the range at Starknet v0.13.2 or later; older blocks fail hash verification on custom networks.
- Juno retries a 400 ten times with backoff, so wall clock between blocks measures the retry
schedule. Measure `sync_step_duration` instead, or use `--tip <to>` for throughput.

## Dataset layout

One gzipped file per FGW response:

```
<data>/
get_contract_addresses.json.gz
get_block/<N>.json.gz # headerOnly=true
get_state_update/<N>.json.gz # includeBlock=true&includeSignature=true
get_class_by_hash/<hash>.json.gz # blockNumber=latest
get_compiled_class_by_class_hash/<hash>.json.gz # blockNumber=latest
```

Timestamps, class lists and completeness are derived from the state updates at startup.
70 changes: 70 additions & 0 deletions bench/sync/cmd/feeder-sim/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"context"
"sync/atomic"
"time"

"github.com/NethermindEth/juno/utils/log"
"go.uber.org/zap"
)

type clock struct {
current atomic.Uint64
from uint64
to uint64
interval time.Duration
speed float64
blocks []blockInfo
logger *log.ZapLogger
}

func newClock(blocks []blockInfo, config *config, logger *log.ZapLogger) *clock {
clock := &clock{
from: config.from,
to: config.to,
interval: config.interval,
speed: config.speed,
blocks: blocks,
logger: logger,

Check warning on line 29 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L23-L29

Added lines #L23 - L29 were not covered by tests
}
clock.current.Store(config.tip)
return clock

Check warning on line 32 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L31-L32

Added lines #L31 - L32 were not covered by tests
}

func (clock *clock) tip() uint64 {
return clock.current.Load()

Check warning on line 36 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L36

Added line #L36 was not covered by tests
}

func (clock *clock) run(ctx context.Context) {
if clock.speed > 0 {
clock.logger.Info("replaying captured block times", zap.Float64("speed", clock.speed))

Check warning on line 41 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L40-L41

Added lines #L40 - L41 were not covered by tests
} else {
clock.logger.Info("advancing tip on a fixed interval", zap.Duration("interval", clock.interval))

Check warning on line 43 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L43

Added line #L43 was not covered by tests
}

for clock.tip() < clock.to {
select {

Check warning on line 47 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L46-L47

Added lines #L46 - L47 were not covered by tests
case <-ctx.Done():
return

Check warning on line 49 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L49

Added line #L49 was not covered by tests
case <-time.After(clock.nextDelay()):
clock.logger.Info("tip advanced", zap.Uint64("tip", clock.current.Add(1)))

Check warning on line 51 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L51

Added line #L51 was not covered by tests
}
}

clock.logger.Info("tip reached --to; everything is served", zap.Uint64("tip", clock.to))

Check warning on line 55 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L55

Added line #L55 was not covered by tests
}

func (clock *clock) nextDelay() time.Duration {
if clock.speed == 0 {
return clock.interval

Check warning on line 60 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L59-L60

Added lines #L59 - L60 were not covered by tests
}
tip := clock.tip()
current := clock.blocks[tip-clock.from].timestamp
next := clock.blocks[tip+1-clock.from].timestamp

Check warning on line 64 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L62-L64

Added lines #L62 - L64 were not covered by tests

if next <= current {
return 0

Check warning on line 67 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L66-L67

Added lines #L66 - L67 were not covered by tests
}
return time.Duration(float64(next-current) * float64(time.Second) / clock.speed)

Check warning on line 69 in bench/sync/cmd/feeder-sim/clock.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/clock.go#L69

Added line #L69 was not covered by tests
}
98 changes: 98 additions & 0 deletions bench/sync/cmd/feeder-sim/dataset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package main

import (
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
)

const directoryMode = 0o750

type dataset struct {
root string
}

func (dataset dataset) path(file string) string {
return filepath.Join(dataset.root, file)

Check warning on line 20 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L20

Added line #L20 was not covered by tests
}

func (dataset dataset) read(file string) ([]byte, error) {
return os.ReadFile(dataset.path(file))

Check warning on line 24 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L24

Added line #L24 was not covered by tests
}

func (dataset dataset) write(file string, gzipped []byte) error {
target := dataset.path(file)
if err := os.MkdirAll(filepath.Dir(target), directoryMode); err != nil {
return err

Check warning on line 30 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L28-L30

Added lines #L28 - L30 were not covered by tests
}

temporary, err := writeTemporary(target, gzipped)
if err != nil {
return err

Check warning on line 35 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L33-L35

Added lines #L33 - L35 were not covered by tests
}
return os.Rename(temporary, target)

Check warning on line 37 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L37

Added line #L37 was not covered by tests
}

func writeTemporary(target string, data []byte) (name string, err error) {
file, err := os.CreateTemp(filepath.Dir(target), filepath.Base(target)+".*.tmp")
if err != nil {
return "", err

Check warning on line 43 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L41-L43

Added lines #L41 - L43 were not covered by tests
}
defer func() {
err = errors.Join(err, file.Close())
if err != nil {
err = errors.Join(err, os.Remove(file.Name()))

Check warning on line 48 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L45-L48

Added lines #L45 - L48 were not covered by tests
}
}()

if err = writeAndSync(file, data); err != nil {
return "", err

Check warning on line 53 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L52-L53

Added lines #L52 - L53 were not covered by tests
}
return file.Name(), nil

Check warning on line 55 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L55

Added line #L55 was not covered by tests
}

func writeAndSync(file *os.File, data []byte) error {
if _, err := file.Write(data); err != nil {
return err

Check warning on line 60 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L59-L60

Added lines #L59 - L60 were not covered by tests
}
return file.Sync()

Check warning on line 62 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L62

Added line #L62 was not covered by tests
}
Comment thread
rodrodros marked this conversation as resolved.

func gzipBytes(body []byte) ([]byte, error) {
var buffer bytes.Buffer
writer := gzip.NewWriter(&buffer)
if _, err := writer.Write(body); err != nil {
return nil, err

Check warning on line 69 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L66-L69

Added lines #L66 - L69 were not covered by tests
}
if err := writer.Close(); err != nil {
return nil, err

Check warning on line 72 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L71-L72

Added lines #L71 - L72 were not covered by tests
}
return buffer.Bytes(), nil

Check warning on line 74 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L74

Added line #L74 was not covered by tests
}

func gunzip(gzipped []byte) ([]byte, error) {
reader, err := gzip.NewReader(bytes.NewReader(gzipped))
if err != nil {
return nil, err

Check warning on line 80 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L78-L80

Added lines #L78 - L80 were not covered by tests
}

body, err := io.ReadAll(reader)
if err != nil {
return nil, err

Check warning on line 85 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L83-L85

Added lines #L83 - L85 were not covered by tests
}
return body, reader.Close()

Check warning on line 87 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L87

Added line #L87 was not covered by tests
}

func unmarshalGzipped[T any](gzipped []byte) (T, error) {
var value T
body, err := gunzip(gzipped)
if err != nil {
return value, err

Check warning on line 94 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L91-L94

Added lines #L91 - L94 were not covered by tests
}
err = json.Unmarshal(body, &value)
return value, err

Check warning on line 97 in bench/sync/cmd/feeder-sim/dataset.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/dataset.go#L96-L97

Added lines #L96 - L97 were not covered by tests
}
90 changes: 90 additions & 0 deletions bench/sync/cmd/feeder-sim/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"errors"
"fmt"
"maps"
"slices"

"github.com/NethermindEth/juno/l1/eth"
"github.com/NethermindEth/juno/starknet"
)

type contractAddressesResponse struct {
Starknet eth.Address `json:"Starknet"`
}

func coreContractAddress(gzipped []byte) (eth.Address, error) {
response, err := unmarshalGzipped[contractAddressesResponse](gzipped)
if err != nil {
return eth.Address{}, fmt.Errorf("%s: %w", contractAddresses.name, err)

Check warning on line 20 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L18-L20

Added lines #L18 - L20 were not covered by tests
}
return response.Starknet, nil

Check warning on line 22 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L22

Added line #L22 was not covered by tests
}

func hexAddress(address eth.Address) string {
return fmt.Sprintf("0x%x", address.Bytes())

Check warning on line 26 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L26

Added line #L26 was not covered by tests
}

type blockInfo struct {
number uint64
timestamp uint64
classHashes []string
}

func newBlockInfo(gzipped []byte, number uint64) (blockInfo, error) {
response, err := decodeStateUpdateResponse(gzipped)
if err != nil {
return blockInfo{}, fmt.Errorf("%s %d: %w", stateUpdate.name, number, err)

Check warning on line 38 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L36-L38

Added lines #L36 - L38 were not covered by tests
}
return blockInfo{
number: response.Block.Number,
timestamp: response.Block.Timestamp,
classHashes: classHashes(&response.StateUpdate.StateDiff),
}, nil

Check warning on line 44 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L40-L44

Added lines #L40 - L44 were not covered by tests
}

func decodeStateUpdateResponse(gzipped []byte) (*starknet.StateUpdateWithBlockAndSignature, error) {
response, err := unmarshalGzipped[starknet.StateUpdateWithBlockAndSignature](gzipped)
if err != nil {
return nil, err

Check warning on line 50 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L48-L50

Added lines #L48 - L50 were not covered by tests
}
if response.Block == nil || response.StateUpdate == nil {
return nil, errors.New("state update response lacks block or state_update")

Check warning on line 53 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L52-L53

Added lines #L52 - L53 were not covered by tests
}
return &response, nil

Check warning on line 55 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L55

Added line #L55 was not covered by tests
}

func classHashes(diff *starknet.StateDiff) []string {
size := len(diff.DeployedContracts) + len(diff.OldDeclaredContracts) + len(diff.DeclaredClasses)
hashes := make([]string, 0, size)

Check warning on line 60 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L59-L60

Added lines #L59 - L60 were not covered by tests

for _, deployed := range diff.DeployedContracts {
hashes = append(hashes, deployed.ClassHash.String())

Check warning on line 63 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L62-L63

Added lines #L62 - L63 were not covered by tests
}
for _, hash := range diff.OldDeclaredContracts {
hashes = append(hashes, hash.String())

Check warning on line 66 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L65-L66

Added lines #L65 - L66 were not covered by tests
}
for _, declared := range diff.DeclaredClasses {
hashes = append(hashes, declared.ClassHash.String())

Check warning on line 69 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L68-L69

Added lines #L68 - L69 were not covered by tests
}
return hashes

Check warning on line 71 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L71

Added line #L71 was not covered by tests
}

func uniqueClassHashes(blocks []blockInfo) []string {
set := make(map[string]struct{})
for _, info := range blocks {
for _, hash := range info.classHashes {
set[hash] = struct{}{}

Check warning on line 78 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L75-L78

Added lines #L75 - L78 were not covered by tests
}
}
return slices.Sorted(maps.Keys(set))

Check warning on line 81 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L81

Added line #L81 was not covered by tests
}

func isSierra(classGzipped []byte, hash string) (bool, error) {
class, err := unmarshalGzipped[starknet.ClassDefinition](classGzipped)
if err != nil {
return false, fmt.Errorf("%s %s: %w", classByHash.name, hash, err)

Check warning on line 87 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L85-L87

Added lines #L85 - L87 were not covered by tests
}
return class.Sierra != nil, nil

Check warning on line 89 in bench/sync/cmd/feeder-sim/decode.go

View check run for this annotation

Codecov / codecov/patch

bench/sync/cmd/feeder-sim/decode.go#L89

Added line #L89 was not covered by tests
}
Loading
Loading