Commit 220c92ce authored by 贾浩@五瓣科技's avatar 贾浩@五瓣科技

Initial commit

parents
Pipeline #885 failed with stages
.vscode
.idea
faucet
# 区块链水龙头性能测试工具
这是一个用于测试区块链水龙头性能的命令行工具,使用 Golang 开发。该工具可以:
1. 随机生成以太坊地址以获取测试币
2. 指定总的测试时间
3. 指定每分钟的请求数
4. 跟踪交易回执状态,以确认请求成功率
## 功能特点
- 随机生成以太坊地址
- 可配置的测试持续时间
- 可配置的请求速率(支持最小/最大请求范围内的随机调整)
- 自动查询交易回执以确认交易状态
- 详细的性能统计(成功率、延迟等)
- 优雅的中断处理(支持 Ctrl+C 提前结束测试)
## 安装
确保已安装 Go 环境(推荐 Go 1.21 或更高版本),然后执行:
```bash
git clone <repository-url>
cd faucet
go mod tidy
```
## 使用方法
### 基本用法
```bash
go run main.go
```
这将使用默认参数运行测试:
- 测试时间:1分钟
- 每分钟请求数范围:60-60(固定值)
- 水龙头URL:https://faucet.mars.movachain.com/api/faucet/v1/transfer
- 令牌:5cd5c5c18f27acae
### 自定义参数
```bash
go run main.go -duration=5 -min-rpm=30 -max-rpm=100 -url="https://faucet.example.com/api" -token="your-token" -tx-url="https://example.com/api/tx"
```
### 参数说明
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `-duration` | 测试持续时间(分钟) | 1 |
| `-min-rpm` | 最小每分钟请求数 | 60 |
| `-max-rpm` | 最大每分钟请求数 | 60 |
| `-url` | 水龙头API URL | https://faucet.mars.movachain.com/api/faucet/v1/transfer |
| `-token` | 水龙头API令牌 | 5cd5c5c18f27acae |
| `-tx-url` | 区块链RPC端点URL | https://mars.rpc.movachain.com |
## 输出示例
```
开始测试水龙头性能:
- 测试时间: 1 分钟
- 每分钟请求数范围: 60-60
- 水龙头URL: https://faucet.mars.movachain.com/api/faucet/v1/transfer
- 交易回执URL: https://mars.movachain.com/api/v1/jsonrpc
测试开始,按Ctrl+C可以提前结束测试
已完成 60 个请求
测试完成,结果统计:
- 总请求数: 60
- 成功请求数: 58 (96.67%)
- 失败请求数: 1 (1.67%)
- 待处理请求数: 1 (1.67%)
- 平均延迟: 2.5s
- 最大延迟: 4.2s
- 最小延迟: 1.8s
```
## 构建可执行文件
```bash
go build -o faucet-tester
```
然后可以直接运行可执行文件:
```bash
./faucet-tester -duration=5 -min-rpm=30 -max-rpm=100
```
curl --location 'https://faucet.mars.movachain.com/api/faucet/v1/transfer' \
--header 'Content-Type: application/json' \
--data '{"to":"0x7d58b6a716d3391831aa095e0375c3b553fa5eb5", "token":"5cd5c5c18f27acae" }'
\ No newline at end of file
module faucet
go 1.21
require (
github.com/ethereum/go-ethereum v1.13.10
golang.org/x/time v0.5.0
)
require (
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)
github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/ethereum/go-ethereum v1.13.10 h1:Ppdil79nN+Vc+mXfge0AuUgmKWuVv4eMqzoIVSdqZek=
github.com/ethereum/go-ethereum v1.13.10/go.mod h1:sc48XYQxCzH3fG9BcrXCOOgQk2JfZzNAmIKnceogzsA=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
package main
import (
"bytes"
"context"
"crypto/ecdsa"
crypto_rand "crypto/rand"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"os/signal"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/crypto"
"golang.org/x/time/rate"
)
const (
// 默认值
defaultDuration = 1 * time.Minute
defaultRequestsPerMin = 60
defaultFaucetURL = "https://faucet.mars.movachain.com/api/faucet/v1/transfer"
defaultToken = "5cd5c5c18f27acae"
defaultTxReceiptURL = "https://mars.rpc.movachain.com" // 更新默认的TxReceiptURL为正确的RPC端点
// 状态常量
StatusSuccess = "success"
StatusFailed = "failed"
StatusPending = "pending"
)
// 请求和响应结构体
type FaucetRequest struct {
To string `json:"to"`
Token string `json:"token"`
}
type FaucetResponse struct {
Data string `json:"data"` // 交易哈希
ErrMsg string `json:"err_msg"`
Error string `json:"error"` // 状态码
}
// EVM区块链API交易回执响应结构体
type TxReceiptResponse struct {
Jsonrpc string `json:"jsonrpc"`
Id int `json:"id"`
Result struct {
BlockHash string `json:"blockHash"`
BlockNumber string `json:"blockNumber"`
ContractAddress interface{} `json:"contractAddress"`
CumulativeGasUsed string `json:"cumulativeGasUsed"`
EffectiveGasPrice string `json:"effectiveGasPrice"`
From string `json:"from"`
GasUsed string `json:"gasUsed"`
Logs []interface{} `json:"logs"`
LogsBloom string `json:"logsBloom"`
Status string `json:"status"`
To string `json:"to"`
TransactionHash string `json:"transactionHash"`
TransactionIndex string `json:"transactionIndex"`
Type string `json:"type"`
} `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
// 测试结果统计
type TestStats struct {
TotalRequests int64
SuccessfulRequests int64
FailedRequests int64
PendingRequests int64
TotalLatency time.Duration
MaxLatency time.Duration
MinLatency time.Duration
}
// 配置结构体
type Config struct {
Duration time.Duration
MinRequestsPerMin int
MaxRequestsPerMin int
FaucetURL string
Token string
TxReceiptURL string
}
func main() {
// 解析命令行参数
durationMinutes := flag.Int("duration", int(defaultDuration.Minutes()), "测试持续时间(分钟)")
minRequestsPerMin := flag.Int("min-rpm", defaultRequestsPerMin, "最小每分钟请求数")
maxRequestsPerMin := flag.Int("max-rpm", defaultRequestsPerMin, "最大每分钟请求数")
faucetURL := flag.String("url", defaultFaucetURL, "水龙头API URL")
token := flag.String("token", defaultToken, "水龙头API令牌")
txReceiptURL := flag.String("tx-url", defaultTxReceiptURL, "交易回执查询API URL")
flag.Parse()
// 确保最大请求数不小于最小请求数
if *maxRequestsPerMin < *minRequestsPerMin {
*maxRequestsPerMin = *minRequestsPerMin
}
// 创建配置
config := Config{
Duration: time.Duration(*durationMinutes) * time.Minute,
MinRequestsPerMin: *minRequestsPerMin,
MaxRequestsPerMin: *maxRequestsPerMin,
FaucetURL: *faucetURL,
Token: *token,
TxReceiptURL: *txReceiptURL,
}
// 打印测试配置
fmt.Printf("开始测试水龙头性能:\n")
fmt.Printf("- 测试时间: %v 分钟\n", *durationMinutes)
fmt.Printf("- 每分钟请求数范围: %v-%v\n", *minRequestsPerMin, *maxRequestsPerMin)
fmt.Printf("- 水龙头URL: %v\n", *faucetURL)
fmt.Printf("- RPC URL: %v\n\n", *txReceiptURL)
// 运行测试
stats := runTest(config)
// 打印测试结果
fmt.Printf("\n测试完成,结果统计:\n")
fmt.Printf("- 总请求数: %v\n", stats.TotalRequests)
fmt.Printf("- 成功请求数: %v (%.2f%%)\n", stats.SuccessfulRequests, float64(stats.SuccessfulRequests)/float64(stats.TotalRequests)*100)
fmt.Printf("- 失败请求数: %v (%.2f%%)\n", stats.FailedRequests, float64(stats.FailedRequests)/float64(stats.TotalRequests)*100)
fmt.Printf("- 待处理请求数: %v (%.2f%%)\n", stats.PendingRequests, float64(stats.PendingRequests)/float64(stats.TotalRequests)*100)
// 计算平均延迟
var avgLatency time.Duration
if stats.TotalRequests > 0 {
avgLatency = stats.TotalLatency / time.Duration(stats.TotalRequests)
}
fmt.Printf("- 平均延迟: %v\n", avgLatency)
fmt.Printf("- 最大延迟: %v\n", stats.MaxLatency)
fmt.Printf("- 最小延迟: %v\n", stats.MinLatency)
}
// 运行测试
func runTest(config Config) TestStats {
// 初始化统计数据
stats := TestStats{
MinLatency: time.Hour, // 初始化为一个较大的值
}
// 创建限速器,先用最小速率初始化
limiter := rate.NewLimiter(rate.Limit(config.MinRequestsPerMin)/60, 1) // 转换为每秒速率
// 创建等待组和上下文
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(context.Background(), config.Duration)
defer cancel()
// 处理中断信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
go func() {
<-sigCh
fmt.Println("\n收到中断信号,正在优雅退出...")
cancel()
}()
fmt.Println("测试开始,按Ctrl+C可以提前结束测试")
// 创建结果通道,使用最大请求率计算容量
resultCh := make(chan struct {
status string
latency time.Duration
}, config.MaxRequestsPerMin*int(config.Duration.Minutes()))
// 启动测试协程
go func() {
// 定义一个计时器,用于定期调整请求速率
adjustTicker := time.NewTicker(5 * time.Second)
defer adjustTicker.Stop()
for {
select {
case <-adjustTicker.C:
// 每隔5秒随机调整请求速率
if config.MaxRequestsPerMin > config.MinRequestsPerMin {
// 在最小和最大请求率之间随机选择一个值
randomRange := config.MaxRequestsPerMin - config.MinRequestsPerMin + 1
randomRPM := config.MinRequestsPerMin + int(rand.Int31n(int32(randomRange)))
// 更新限速器
limiter.SetLimit(rate.Limit(randomRPM) / 60)
fmt.Printf("调整速率为: %d 请求/分钟\r", randomRPM)
}
case <-ctx.Done():
return
default:
// 等待限速器允许
err := limiter.Wait(ctx)
if err != nil {
// 上下文已取消
return
}
// 增加等待组计数
wg.Add(1)
// 启动一个协程处理请求
go func() {
defer wg.Done()
// 生成随机地址
address := generateRandomAddress()
// 发送请求并测量延迟
startTime := time.Now()
txHash, err := sendFaucetRequest(address, config)
if err != nil {
fmt.Println("获取回执失败:", err)
resultCh <- struct {
status string
latency time.Duration
}{StatusFailed, time.Since(startTime)}
return
}
// 查询交易回执
status := checkTxReceipt(txHash, config)
latency := time.Since(startTime)
// 发送结果到通道
resultCh <- struct {
status string
latency time.Duration
}{status, latency}
}()
}
}
}()
// 等待测试完成或上下文取消
// 创建一个完成信号通道
done := make(chan struct{})
go func() {
// 等待上下文超时或取消
<-ctx.Done()
// 等待所有请求完成
wg.Wait()
// 关闭结果通道
close(resultCh)
// 发送完成信号
close(done)
}()
// 收集结果
for result := range resultCh {
atomic.AddInt64(&stats.TotalRequests, 1)
switch result.status {
case StatusSuccess:
atomic.AddInt64(&stats.SuccessfulRequests, 1)
case StatusFailed:
atomic.AddInt64(&stats.FailedRequests, 1)
case StatusPending:
atomic.AddInt64(&stats.PendingRequests, 1)
}
// 更新延迟统计
atomic.AddInt64((*int64)(&stats.TotalLatency), int64(result.latency))
// 更新最大延迟
for {
current := stats.MaxLatency
if result.latency <= current {
break
}
if atomic.CompareAndSwapInt64((*int64)(&stats.MaxLatency), int64(current), int64(result.latency)) {
break
}
}
// 更新最小延迟
for {
current := stats.MinLatency
if result.latency >= current {
break
}
if atomic.CompareAndSwapInt64((*int64)(&stats.MinLatency), int64(current), int64(result.latency)) {
break
}
}
// 打印进度
if atomic.LoadInt64(&stats.TotalRequests)%10 == 0 {
fmt.Printf("已完成 %d 个请求\r", atomic.LoadInt64(&stats.TotalRequests))
}
}
// 等待测试完成
<-done
return stats
}
// 生成随机以太坊地址
func generateRandomAddress() string {
// 生成随机私钥
privateKey, err := ecdsa.GenerateKey(crypto.S256(), crypto_rand.Reader)
if err != nil {
log.Fatalf("生成私钥失败: %v", err)
}
// 从私钥获取公钥
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("无法获取公钥")
}
// 从公钥获取地址
address := crypto.PubkeyToAddress(*publicKeyECDSA)
return address.Hex()
}
// 发送水龙头请求
func sendFaucetRequest(address string, config Config) (string, error) {
// 创建请求体
request := FaucetRequest{
To: address,
Token: config.Token,
}
// 转换为JSON
requestBody, err := json.Marshal(request)
if err != nil {
return "", fmt.Errorf("JSON编码失败: %v", err)
}
// 创建HTTP请求
req, err := http.NewRequest("POST", config.FaucetURL, bytes.NewBuffer(requestBody))
if err != nil {
return "", fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
// 发送请求
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var response FaucetResponse
if err := json.Unmarshal(body, &response); err != nil {
return "", fmt.Errorf("解析响应失败: %v", err)
}
// 检查响应状态
if response.Error != "200" {
return "", fmt.Errorf("请求失败: %s", response.ErrMsg)
}
return response.Data, nil
}
// 检查交易回执
func checkTxReceipt(txHash string, config Config) string {
// 构建JSON-RPC请求体
rpcRequest := map[string]interface{}{
"jsonrpc": "2.0",
"method": "eth_getTransactionReceipt",
"params": []string{txHash},
"id": 1,
}
// 将请求体转换为JSON
requestBody, err := json.Marshal(rpcRequest)
if err != nil {
return StatusFailed
}
// 设置重试次数和间隔
maxRetries := 10
retryInterval := 2 * time.Second
// 重试查询交易回执
for i := 0; i < maxRetries; i++ {
// 创建HTTP请求
req, err := http.NewRequest("POST", config.TxReceiptURL, bytes.NewBuffer(requestBody))
if err != nil {
time.Sleep(retryInterval)
continue
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
// 发送请求
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
time.Sleep(retryInterval)
continue
}
// 读取响应
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
time.Sleep(retryInterval)
continue
}
// 解析响应
var response TxReceiptResponse
if err := json.Unmarshal(body, &response); err != nil {
time.Sleep(retryInterval)
continue
}
// 检查是否有错误
if response.Error != nil {
// 如果是“交易未找到”类型的错误,可能是交易还未被挖矿,等待重试
time.Sleep(retryInterval)
continue
}
// 检查是否有结果
if response.Result.Status == "" {
// 交易还未被挖矿,等待重试
time.Sleep(retryInterval)
continue
}
// 检查交易状态
// 在以太坊中,状态 "0x1" 表示成功,"0x0" 表示失败
if response.Result.Status == "0x1" {
return StatusSuccess
} else if response.Result.Status == "0x0" {
return StatusFailed
}
// 如果状态不是成功或失败,等待重试
time.Sleep(retryInterval)
}
// 达到最大重试次数,仍未确认
return StatusPending
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment