Skip to content

Commit 4bed1d6

Browse files
committed
Initial
1 parent 466313d commit 4bed1d6

File tree

3 files changed

+160
-38
lines changed

3 files changed

+160
-38
lines changed

.env.example

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
PRIVATE_KEY=todo
2-
TO_ADDRESS=0x7d5A90C2cd5567B9966E92c710b5E9936F400d74
1+
PRIVATE_KEY=your_private_key_here
2+
TO_ADDRESS=0xc2F695613de0885dA3bdd18E8c317B9fAf7d4eba
3+
4+
# Polling interval when using asynchronous method
35
POLLING_INTERVAL_MS=100
4-
FLASHBLOCKS_URL=https://sepolia-preconf.base.org
5-
BASE_URL=https://sepolia.base.org
6-
REGION=texas
6+
7+
BASE_NODE_ENDPOINT_1=https://your-node-endpoint-1.com
8+
BASE_NODE_ENDPOINT_2=https://your-node-endpoint-2.com
9+
10+
# Simple label for resulting filename
11+
REGION=singapore
12+
NUMBER_OF_TRANSACTIONS=10
13+
14+
# Use eth_sendRawTransactionSync (true) or standard async method (false)
15+
SEND_TXN_SYNC=true
16+
17+
RUN_ENDPOINT2_TESTING=true

README.md

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,115 @@
1-
# transaction-latency
1+
# Transaction latency testing tool
22

3+
A Go-based tool for testing and comparing transaction latency performance between different [Flashblocks-enabled Base node](https://blog.base.dev/accelerating-base-with-flashblocks) endpoints.
4+
5+
Flashblocks are sub-blocks issued by the block builder every 200ms, allowing for early confirmation times and making Base 10x faster for real-time applications.
6+
7+
## Requirements
8+
9+
### Node compatibility
10+
11+
For **synchronous** transaction sending testing, the node endpoints must support the `eth_sendRawTransactionSync` RPC method. See [EIP-7966: eth_sendRawTransactionSync Method](https://eips.ethereum.org/EIPS/eip-7966).
12+
13+
For **asynchronous** transaction sending testing, the node endpoints do not have to support the `eth_sendRawTransactionSync` RPC method and will rely on separate polling for confirmed transactions.
14+
15+
[Chainstack Base nodes](https://chainstack.com/build-better-with-base/) for Mainnet & Testnet support both the synchronous one with `eth_sendRawTransactionSync` and the asynchronous one.
16+
17+
## Configuration
18+
19+
Create a `.env` file based on `.env.example`:
20+
21+
```bash
22+
PRIVATE_KEY=your_private_key_here
23+
TO_ADDRESS=0xc2F695613de0885dA3bdd18E8c317B9fAf7d4eba
24+
25+
# If you are going with the asynchronous method and poll for the transaction receipts separately
26+
POLLING_INTERVAL_MS=100
27+
28+
BASE_NODE_ENDPOINT_1=https://your-node-endpoint-1.com
29+
BASE_NODE_ENDPOINT_2=https://your-node-endpoint-2.com
30+
31+
# This a simple label for the resulting filename to remember where you sent the test transactions from. Not used in any node routing.
32+
REGION=singapore
33+
NUMBER_OF_TRANSACTIONS=10
34+
35+
# With `true`, the synchronous eth_sendRawTransactionSync method is used. With `false`, the asynchronous method is used with POLLING_INTERVAL_MS=100
36+
SEND_TXN_SYNC=true
37+
38+
RUN_ENDPOINT2_TESTING=true
39+
```
40+
41+
### Configuration options
42+
43+
#### Transaction sending testing modes
44+
45+
**`SEND_TXN_SYNC=true`**:
46+
- Uses `eth_sendRawTransactionSync` method
47+
- Provides instant confirmation receipt when transaction is included in a Flashblock
48+
- Transactions wait for immediate inclusion confirmation
49+
50+
**`SEND_TXN_SYNC=false`**:
51+
- Uses standard `eth_sendTransaction` method
52+
- Polls for transaction receipts using `POLLING_INTERVAL_MS`
53+
- Traditional async transaction sending
54+
55+
#### Polling configuration
56+
57+
**`POLLING_INTERVAL_MS`**:
58+
- Used only when `SEND_TXN_SYNC=false`
59+
- Defines how frequently (in milliseconds) to check for transaction receipts
60+
- Default: 100ms
61+
- Lower values = more frequent polling = faster detection but higher load
62+
- Not used when `SEND_TXN_SYNC=true` since confirmations are immediate
63+
64+
#### Endpoint testing
65+
66+
**`BASE_NODE_ENDPOINT_1`**: Primary endpoint (e.g., a Flashblocks-enabled)
67+
**`BASE_NODE_ENDPOINT_2`**: Secondary endpoint (e.g., a standard non-Flashblocks endpoint)
68+
69+
The tool will:
70+
1. Send transactions to `BASE_NODE_ENDPOINT_1` first
71+
2. Then send transactions to `BASE_NODE_ENDPOINT_2` (if `RUN_ENDPOINT2_TESTING=true`)
72+
3. Compare performance between both endpoints
73+
74+
## Usage
75+
76+
```bash
77+
# Build the container
378
docker build -t transaction-latency .
4-
docker run -v (pwd)/data:/data --env-file .env --rm -it transaction-latency
79+
80+
# Run the test
81+
docker run -v $(pwd)/data:/data --env-file .env --rm -it transaction-latency
82+
```
83+
84+
## Results
85+
86+
After completion, results are saved to the `./data/` directory:
87+
88+
- `endpoint1-{region}.csv`: Results from `BASE_NODE_ENDPOINT_1`
89+
- `endpoint2-{region}.csv`: Results from `BASE_NODE_ENDPOINT_2` (if enabled)
90+
91+
### CSV Format
92+
93+
Each CSV contains:
94+
- `sent_at`: Timestamp when transaction was sent
95+
- `txn_hash`: Transaction hash
96+
- `included_in_block`: Block number where transaction was included
97+
- `inclusion_delay_ms`: Time from sending to confirmation (milliseconds)
98+
99+
## Performance expectations
100+
101+
Flashblocks are produced at 200ms intervals, but actual confirmation times will be higher due to network latency:
102+
103+
- **Standard endpoint**: ~2000ms (2-second block time)
104+
- **Flashblocks endpoint**: ~300-500ms (200ms Flashblock interval + network travel time)
105+
106+
The actual confirmation time depends on:
107+
- Network latency to/from the Base node
108+
- Transaction processing time
109+
- Time until next Flashblock (up to 200ms)
110+
- Network travel time for confirmation response
111+
112+
## Learn more
113+
114+
- [Base Flashblocks documentation](https://docs.base.org/base-chain/flashblocks/apps)
115+
- [We’re making Base 10x faster with Flashblocks](https://blog.base.dev/accelerating-base-with-flashblocks)

main.go

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,18 @@ func main() {
5353
log.Fatal("TO_ADDRESS environment variable not set")
5454
}
5555

56-
flashblocksUrl := os.Getenv("FLASHBLOCKS_URL")
57-
if flashblocksUrl == "" {
58-
log.Fatal("FLASHBLOCKS_URL environment variable not set")
56+
endpoint1 := os.Getenv("BASE_NODE_ENDPOINT_1")
57+
if endpoint1 == "" {
58+
log.Fatal("BASE_NODE_ENDPOINT_1 environment variable not set")
5959
}
6060

61-
baseUrl := os.Getenv("BASE_URL")
62-
if baseUrl == "" {
63-
log.Fatal("BASE_URL environment variable not set")
61+
endpoint2 := os.Getenv("BASE_NODE_ENDPOINT_2")
62+
if endpoint2 == "" {
63+
log.Fatal("BASE_NODE_ENDPOINT_2 environment variable not set")
6464
}
6565

6666
sendTxnSync := os.Getenv("SEND_TXN_SYNC") == "true"
67-
runStandardTransactionSending := os.Getenv("RUN_STANDARD_TRANSACTION_SENDING") != "false"
67+
runEndpoint2Testing := os.Getenv("RUN_ENDPOINT2_TESTING") != "false"
6868

6969
pollingIntervalMs := 100
7070
if pollingEnv := os.Getenv("POLLING_INTERVAL_MS"); pollingEnv != "" {
@@ -82,12 +82,12 @@ func main() {
8282
}
8383
}
8484

85-
flashblocksClient, err := ethclient.Dial(flashblocksUrl)
85+
endpoint1Client, err := ethclient.Dial(endpoint1)
8686
if err != nil {
8787
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
8888
}
8989

90-
baseClient, err := ethclient.Dial(baseUrl)
90+
endpoint2Client, err := ethclient.Dial(endpoint2)
9191
if err != nil {
9292
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
9393
}
@@ -104,26 +104,26 @@ func main() {
104104
}
105105
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
106106

107-
var flashblockTimings []stats
108-
var baseTimings []stats
107+
var endpoint1Timings []stats
108+
var endpoint2Timings []stats
109109

110-
chainId, err := baseClient.NetworkID(context.Background())
110+
chainId, err := endpoint2Client.NetworkID(context.Background())
111111
if err != nil {
112112
log.Fatalf("Failed to get network ID: %v", err)
113113
}
114114

115-
flashblockErrors := 0
116-
baseErrors := 0
115+
endpoint1Errors := 0
116+
endpoint2Errors := 0
117117

118-
log.Printf("Starting flashblock transactions, syncMode=%v", sendTxnSync)
118+
log.Printf("Starting endpoint1 transactions, syncMode=%v", sendTxnSync)
119119
for i := 0; i < numberOfTransactions; i++ {
120-
timing, err := timeTransaction(chainId, privateKey, fromAddress, toAddress, flashblocksClient, sendTxnSync, pollingIntervalMs)
120+
timing, err := timeTransaction(chainId, privateKey, fromAddress, toAddress, endpoint1Client, sendTxnSync, pollingIntervalMs)
121121
if err != nil {
122-
flashblockErrors += 1
122+
endpoint1Errors += 1
123123
log.Printf("Failed to send transaction: %v", err)
124124
}
125125

126-
flashblockTimings = append(flashblockTimings, timing)
126+
endpoint1Timings = append(endpoint1Timings, timing)
127127

128128
if !sendTxnSync {
129129
// wait for it to be mined -- sleep a random amount between 600ms and 1s
@@ -133,41 +133,41 @@ func main() {
133133
}
134134
}
135135

136-
// wait for the final fb transaction to land
136+
// wait for the final endpoint1 transaction to land
137137
time.Sleep(5 * time.Second)
138138

139-
if runStandardTransactionSending {
140-
log.Printf("Starting regular transactions")
139+
if runEndpoint2Testing {
140+
log.Printf("Starting endpoint2 transactions")
141141
for i := 0; i < numberOfTransactions; i++ {
142-
// Currently not supported on non-flashblock endpoints
143-
timing, err := timeTransaction(chainId, privateKey, fromAddress, toAddress, baseClient, false, pollingIntervalMs)
142+
// Use async mode for endpoint2 testing
143+
timing, err := timeTransaction(chainId, privateKey, fromAddress, toAddress, endpoint2Client, false, pollingIntervalMs)
144144
if err != nil {
145-
baseErrors += 1
145+
endpoint2Errors += 1
146146
log.Printf("Failed to send transaction: %v", err)
147147
}
148148

149-
baseTimings = append(baseTimings, timing)
149+
endpoint2Timings = append(endpoint2Timings, timing)
150150

151151
// wait for it to be mined -- sleep a random amount between 4s and 3s
152152
time.Sleep(time.Duration(rand.Int63n(1000)+4000) * time.Millisecond)
153153
}
154154
} else {
155-
log.Printf("Skipping regular transactions (RUN_STANDARD_TRANSACTION_SENDING=false)")
155+
log.Printf("Skipping endpoint2 transactions (RUN_ENDPOINT2_TESTING=false)")
156156
}
157157

158-
if err := writeToFile(fmt.Sprintf("./data/flashblocks-%s.csv", region), flashblockTimings); err != nil {
158+
if err := writeToFile(fmt.Sprintf("./data/endpoint1-%s.csv", region), endpoint1Timings); err != nil {
159159
log.Fatalf("Failed to write to file: %v", err)
160160
}
161161

162-
if runStandardTransactionSending {
163-
if err := writeToFile(fmt.Sprintf("./data/base-%s.csv", region), baseTimings); err != nil {
162+
if runEndpoint2Testing {
163+
if err := writeToFile(fmt.Sprintf("./data/endpoint2-%s.csv", region), endpoint2Timings); err != nil {
164164
log.Fatalf("Failed to write to file: %v", err)
165165
}
166166
}
167167

168168
log.Printf("Completed test with %d transactions", numberOfTransactions)
169-
log.Printf("Flashblock errors: %v", flashblockErrors)
170-
log.Printf("BaseErrors: %v", baseErrors)
169+
log.Printf("Endpoint1 errors: %v", endpoint1Errors)
170+
log.Printf("Endpoint2 errors: %v", endpoint2Errors)
171171
}
172172

173173
func writeToFile(filename string, data []stats) error {

0 commit comments

Comments
 (0)