Skip to content
Open
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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.git
.github
.idea
.vscode

.env
.env.*

data/
*.csv

*.log
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
go:
name: Go vet/test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Check gofmt
run: |
test -z "$(gofmt -l .)"

- name: Run go vet
run: |
go vet ./...

- name: Run go test
run: |
go test ./...

docker:
name: Docker build (no push)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build
uses: docker/build-push-action@v5
with:
context: .
push: false
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
# transaction-latency

Measures transaction inclusion latency against two RPC endpoints (e.g. Base + Flashblocks) and writes results to CSV.

## Requirements

- Docker
- A funded EOA private key for the target network

## Setup

1. Copy the env template and fill values:

```bash
cp .env.example .env
```

2. Create an output directory (CSV files are written to `./data`):

```bash
mkdir -p data
```

## Run

```bash
docker build -t transaction-latency .
docker run -v $(pwd)/data:/app/data --env-file .env --rm -it transaction-latency
docker run --rm -it -v "$(pwd)/data:/app/data" --env-file .env transaction-latency
```

## Outputs

- `data/flashblocks-<REGION>.csv`
- `data/base-<REGION>.csv` (if `RUN_STANDARD_TRANSACTION_SENDING` is not set to `false`)
29 changes: 24 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import (
"crypto/ecdsa"
"encoding/csv"
"encoding/hex"
"errors"
"fmt"
"log"
"math/big"
"math/rand"
"os"
"path/filepath"
"strconv"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
Expand Down Expand Up @@ -60,9 +63,13 @@ func main() {
log.Fatal("TO_ADDRESS environment variable not set")
}

if !common.IsHexAddress(toAddressRaw) {
log.Fatal("TO_ADDRESS environment variable is not a valid hex address")
}

toAddress := common.HexToAddress(toAddressRaw)
if toAddress == (common.Address{}) {
log.Fatal("TO_ADDRESS environment variable not set")
log.Fatal("TO_ADDRESS environment variable cannot be the zero address")
}

flashblocksUrl := os.Getenv("FLASHBLOCKS_URL")
Expand Down Expand Up @@ -203,9 +210,16 @@ func main() {
}

func writeToFile(filename string, data []stats) error {
dir := filepath.Dir(filename)
if dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("failed to create output directory %s: %w", dir, err)
}
}

file, err := os.Create(filename)
if err != nil {
log.Fatalf("Failed to create file: %v", err)
return fmt.Errorf("failed to create file %s: %w", filename, err)
}
defer file.Close()

Expand All @@ -214,7 +228,7 @@ func writeToFile(filename string, data []stats) error {

header := []string{"sent_at", "txn_hash", "included_in_block", "inclusion_delay_ms"}
if err := writer.Write(header); err != nil {
log.Fatalf("Failed to write to file: %v", err)
return fmt.Errorf("failed to write CSV header: %w", err)
}

for _, d := range data {
Expand All @@ -225,7 +239,7 @@ func writeToFile(filename string, data []stats) error {
strconv.FormatInt(d.InclusionDelay.Milliseconds(), 10),
}
if err := writer.Write(row); err != nil {
log.Fatalf("Failed to write to file: %v", err)
return fmt.Errorf("failed to write CSV row: %w", err)
}
}

Expand Down Expand Up @@ -324,7 +338,12 @@ func sendTransactionAsync(client *ethclient.Client, signedTx *types.Transaction,
for i := 0; i < 1000; i++ {
receipt, err := client.TransactionReceipt(context.Background(), signedTx.Hash())
if err != nil {
time.Sleep(time.Duration(pollingIntervalMs) * time.Millisecond)
if errors.Is(err, ethereum.NotFound) {
time.Sleep(time.Duration(pollingIntervalMs) * time.Millisecond)
continue
}

return stats{}, fmt.Errorf("failed to fetch receipt: %w", err)
} else {
now := time.Now()
return stats{
Expand Down