Commit 71b8af45 authored by vicotor's avatar vicotor

add blacklist check

parent c802d497
package main
import (
"context"
"fmt"
"math/big"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
// ABI for the blacklist contract that exposes: function inBlacklist(address) view returns (bool)
const blacklistABI = `[{"constant":true,"inputs":[{"name":"addr","type":"address"}],"name":"inBlacklist","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"}]`
// cacheEntry holds the cached blacklist boolean and expiry time
type cacheEntry struct {
inBlack bool
expiry time.Time
}
var (
blacklistCache = make(map[string]cacheEntry)
blacklistCacheMu sync.RWMutex
)
// IsInBlacklist calls the contract's inBlacklist(address) view function and returns its boolean result.
// Inputs:
// - client: an initialized *ethclient.Client connected to an Ethereum node
// - contract: address of the blacklist contract
// - addr: address to check
// Output:
// - bool indicating whether the address is in the blacklist
// - error in case of RPC/ABI/decoding problems
func IsInBlacklist(client *ethclient.Client, contract common.Address, addr common.Address) (bool, error) {
if client == nil {
return false, fmt.Errorf("nil ethclient")
}
parsed, err := abi.JSON(strings.NewReader(blacklistABI))
if err != nil {
return false, fmt.Errorf("failed to parse ABI: %w", err)
}
data, err := parsed.Pack("inBlacklist", addr)
if err != nil {
return false, fmt.Errorf("failed to pack params: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := client.CallContract(ctx, ethereum.CallMsg{
To: &contract,
Data: data,
}, nil)
if err != nil {
return false, fmt.Errorf("call contract failed: %w", err)
}
// Unpack into a slice of interfaces
var out []interface{}
if err := parsed.UnpackIntoInterface(&out, "inBlacklist", res); err != nil {
return false, fmt.Errorf("failed to unpack result: %w", err)
}
if len(out) == 0 {
return false, fmt.Errorf("empty result from contract")
}
b, ok := out[0].(bool)
if !ok {
// Some ABI versions may return type *big.Int for booleans encoded as uint8; handle that just in case
if bi, ok2 := out[0].(*big.Int); ok2 {
return bi.Cmp(big.NewInt(0)) != 0, nil
}
return false, fmt.Errorf("unexpected result type: %T", out[0])
}
return b, nil
}
// CachedIsInBlacklist checks an in-memory cache before calling the contract.
// Cache policy:
// - if address is in blacklist -> cache for 1 hour
// - if address is not in blacklist -> cache for 1 minute
// On contract errors, the error is returned and nothing is cached.
func CachedIsInBlacklist(client *ethclient.Client, contract common.Address, addr common.Address) (bool, error) {
key := strings.ToLower(addr.Hex())
now := time.Now()
// fast-path: read lock
blacklistCacheMu.RLock()
entry, ok := blacklistCache[key]
blacklistCacheMu.RUnlock()
if ok {
if now.Before(entry.expiry) {
return entry.inBlack, nil
}
// entry expired: proactively remove it to release memory sooner
blacklistCacheMu.Lock()
// re-check in case another goroutine updated it
if cur, still := blacklistCache[key]; still {
if cur.expiry.Equal(entry.expiry) || cur.expiry.Before(now) {
delete(blacklistCache, key)
}
}
blacklistCacheMu.Unlock()
}
// Miss or expired -> query contract
inBlack, err := IsInBlacklist(client, contract, addr)
if err != nil {
return false, err
}
// determine ttl
var ttl time.Duration
if inBlack {
ttl = time.Hour
} else {
ttl = time.Minute
}
blacklistCacheMu.Lock()
blacklistCache[key] = cacheEntry{inBlack: inBlack, expiry: now.Add(ttl)}
blacklistCacheMu.Unlock()
return inBlack, nil
}
// startBlacklistCacheJanitor starts a background goroutine that periodically
// scans and removes expired entries from the in-memory blacklist cache.
// Call this once at startup. The cleanupInterval controls how often the scan runs.
func startBlacklistCacheJanitor(cleanupInterval time.Duration) {
if cleanupInterval <= 0 {
cleanupInterval = time.Minute * 5
}
go func() {
ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
blacklistCacheMu.Lock()
for k, e := range blacklistCache {
if now.After(e.expiry) {
delete(blacklistCache, k)
}
}
blacklistCacheMu.Unlock()
}
}()
}
// ClearBlacklistCache removes all entries from the cache (useful for tests/admin).
func ClearBlacklistCache() {
blacklistCacheMu.Lock()
blacklistCache = make(map[string]cacheEntry)
blacklistCacheMu.Unlock()
}
// CacheStats returns the current number of entries and the nearest expiry time (zero if empty).
func CacheStats() (count int, nextExpiry time.Time) {
blacklistCacheMu.RLock()
defer blacklistCacheMu.RUnlock()
count = len(blacklistCache)
var earliest time.Time
for _, e := range blacklistCache {
if earliest.IsZero() || e.expiry.Before(earliest) {
earliest = e.expiry
}
}
return count, earliest
}
[
{
"inputs": [
{
"internalType": "address[]",
"name": "addrList",
"type": "address[]"
}
],
"name": "inBlackList",
"outputs": [
{
"internalType": "bool[]",
"name": "isBlack",
"type": "bool[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "addrList",
"type": "address[]"
},
{
"internalType": "bool",
"name": "isBlack",
"type": "bool"
}
],
"name": "setBlackList",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
\ No newline at end of file
This diff is collapsed.
...@@ -12,6 +12,7 @@ services: ...@@ -12,6 +12,7 @@ services:
max-file: "5" max-file: "5"
environment: environment:
- ETH_RPC_BACKEND=http://172.17.0.1:26658 - ETH_RPC_BACKEND=http://172.17.0.1:26658
- BLACKLIST_CONTRACT_ADDR=0x339F0Ca78A02062fcD1E2f81F9976b32d9552e82
- MYSQL_DSN=root:fNWYkvHcA6Pr5q0RGa8m@tcp(172.31.45.123:53306)/tidb_block_browser - MYSQL_DSN=root:fNWYkvHcA6Pr5q0RGa8m@tcp(172.31.45.123:53306)/tidb_block_browser
command: command:
- "/bin/sh" - "/bin/sh"
......
...@@ -4,4 +4,27 @@ go 1.24.4 ...@@ -4,4 +4,27 @@ go 1.24.4
require github.com/go-sql-driver/mysql v1.9.3 require github.com/go-sql-driver/mysql v1.9.3
require filippo.io/edwards25519 v1.1.0 // indirect require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/consensys/gnark-crypto v0.18.0 // indirect
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.3 // indirect
github.com/ethereum/go-ethereum v1.16.5 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0=
github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c=
github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
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/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU=
github.com/ethereum/c-kzg-4844/v2 v2.1.3/go.mod h1:fyNcYI/yAuLWJxf4uzVtS8VDKeoAaRM8G/+ADz/pRdA=
github.com/ethereum/go-ethereum v1.16.5 h1:GZI995PZkzP7ySCxEFaOPzS8+bd8NldE//1qvQDQpe0=
github.com/ethereum/go-ethereum v1.16.5/go.mod h1:kId9vOtlYg3PZk9VwKbGlQmSACB5ESPTBGT+M9zjmok=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw=
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
...@@ -5,13 +5,16 @@ import ( ...@@ -5,13 +5,16 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
) )
...@@ -56,6 +59,8 @@ type RPCResponse struct { ...@@ -56,6 +59,8 @@ type RPCResponse struct {
var ( var (
db *sql.DB db *sql.DB
rpcBackend = os.Getenv("ETH_RPC_BACKEND") // Real Ethereum RPC address, recommend using environment variable rpcBackend = os.Getenv("ETH_RPC_BACKEND") // Real Ethereum RPC address, recommend using environment variable
ethClient *ethclient.Client
blacklistContract common.Address
) )
func main() { func main() {
...@@ -66,7 +71,41 @@ func main() { ...@@ -66,7 +71,41 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("Database connection failed: %v", err) log.Fatalf("Database connection failed: %v", err)
} }
defer db.Close() defer func() {
if err := db.Close(); err != nil {
log.Printf("db close error: %v", err)
}
}()
// Initialize eth client for blacklist checks if backend provided
if rpcBackend != "" {
ehtCli, err := ethclient.Dial(rpcBackend)
if err != nil {
log.Fatalf("failed to create eth client: %v", err)
}
ethClient = ehtCli
} else {
log.Printf("ETH_RPC_BACKEND not set, blacklist checks will be disabled")
}
// Setup blacklist contract address from env if provided
if addr := os.Getenv("BLACKLIST_CONTRACT_ADDR"); addr != "" {
blacklistContract = common.HexToAddress(addr)
} else {
// leave zero address; checks will be skipped
log.Printf("BLACKLIST_CONTRACT_ADDR not set, blacklist checks will be disabled")
}
// Start cache janitor for blacklist cache. Interval can be configured via env BLACKLIST_CACHE_CLEANUP_INTERVAL (e.g. "5m").
cleanupInterval := 5 * time.Minute
if s := os.Getenv("BLACKLIST_CACHE_CLEANUP_INTERVAL"); s != "" {
if d, err := time.ParseDuration(s); err == nil {
cleanupInterval = d
} else {
log.Printf("invalid BLACKLIST_CACHE_CLEANUP_INTERVAL '%s', using default %s", s, cleanupInterval)
}
}
startBlacklistCacheJanitor(cleanupInterval)
http.HandleFunc("/", proxyHandler) http.HandleFunc("/", proxyHandler)
log.Println("RPC proxy service started, listening on port: 8545") log.Println("RPC proxy service started, listening on port: 8545")
...@@ -83,12 +122,16 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) { ...@@ -83,12 +122,16 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
body, err := ioutil.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
http.Error(w, "Failed to read request", http.StatusBadRequest) http.Error(w, "Failed to read request", http.StatusBadRequest)
return return
} }
defer r.Body.Close() defer func() {
if err := r.Body.Close(); err != nil {
log.Printf("request body close error: %v", err)
}
}()
var reqs []RPCRequest var reqs []RPCRequest
// Try to parse as batch request first // Try to parse as batch request first
...@@ -104,7 +147,9 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) { ...@@ -104,7 +147,9 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
xForwardedFor := r.Header.Get("X-Forwarded-For") xForwardedFor := r.Header.Get("X-Forwarded-For")
log.Printf("stop forward to rpc on batch request, request from X-Forwarded-For: %s, param[0]: %v", xForwardedFor, req.Params) log.Printf("stop forward to rpc on batch request, request from X-Forwarded-For: %s, param[0]: %v", xForwardedFor, req.Params)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp) if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("encode response error: %v", err)
}
} }
return return
} }
...@@ -115,6 +160,7 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) { ...@@ -115,6 +160,7 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Handle eth_getBalance (existing behavior)
if req.Method == "eth_getBalance" && len(req.Params) > 0 { if req.Method == "eth_getBalance" && len(req.Params) > 0 {
// get remote ip from header // get remote ip from header
xForwardedFor := r.Header.Get("X-Forwarded-For") xForwardedFor := r.Header.Get("X-Forwarded-For")
...@@ -133,14 +179,110 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) { ...@@ -133,14 +179,110 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
} }
log.Printf("stop forward to rpc on eth_getBalance request from %s, X-Real-IP: %s, X-Forwarded-For: %s, address: %v", r.RemoteAddr, realIp, xForwardedFor, req.Params[0]) log.Printf("stop forward to rpc on eth_getBalance request from %s, X-Real-IP: %s, X-Forwarded-For: %s, address: %v", r.RemoteAddr, realIp, xForwardedFor, req.Params[0])
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp) if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("encode response error: %v", err)
}
return return
} }
} }
// First, special-case eth_sendRawTransaction: extract sender from raw tx
if req.Method == "eth_sendRawTransaction" && len(req.Params) > 0 {
if rawHex, ok := req.Params[0].(string); ok {
if fromAddr, err := getSenderFromRawTx(rawHex); err == nil {
// only perform blacklist check if eth client and blacklist contract are configured
if ethClient != nil && blacklistContract != (common.Address{}) {
inBlack, err := CachedIsInBlacklist(ethClient, blacklistContract, common.HexToAddress(strings.ToLower(fromAddr)))
if err != nil {
log.Printf("blacklist check failed for %s: %v", fromAddr, err)
// fail open: forward
forwardToBackend(w, body)
return
}
if inBlack {
errResp := RPCResponse{
Jsonrpc: req.Jsonrpc,
Id: req.Id,
Error: map[string]interface{}{
"code": -32000,
"message": "sender is blacklisted",
},
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(errResp); err != nil {
log.Printf("encode error: %v", err)
}
return
}
}
} else {
// If we couldn't decode the sender, just forward (fail open)
log.Printf("failed to decode raw tx sender: %v", err)
forwardToBackend(w, body)
return
}
}
}
// New: check if request includes a `from` address (common for eth_sendTransaction, eth_call, eth_estimateGas)
if from, ok := extractFromAddress(req); ok {
// only perform blacklist check if eth client and blacklist contract are configured
if ethClient != nil && blacklistContract != (common.Address{}) {
inBlack, err := CachedIsInBlacklist(ethClient, blacklistContract, common.HexToAddress(strings.ToLower(from)))
if err != nil {
// on error, log and forward to backend (fail open)
log.Printf("blacklist check failed for %s: %v", from, err)
forwardToBackend(w, body)
return
}
if inBlack {
// return JSON-RPC error response indicating sender is blacklisted
errResp := RPCResponse{
Jsonrpc: req.Jsonrpc,
Id: req.Id,
Error: map[string]interface{}{
"code": -32000,
"message": "sender is blacklisted",
},
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(errResp); err != nil {
log.Printf("encode error: %v", err)
}
return
}
}
}
// Forward other cases directly // Forward other cases directly
forwardToBackend(w, body) forwardToBackend(w, body)
} }
// getSenderFromRawTx decodes a raw transaction hex (0x...) and returns the sender address as a hex string.
func getSenderFromRawTx(rawHex string) (string, error) {
// strip 0x if present
b, err := hexutil.Decode(rawHex)
if err != nil {
return "", err
}
var tx types.Transaction
if err := tx.UnmarshalBinary(b); err != nil {
return "", err
}
// determine signer
var signer types.Signer
if tx.ChainId() != nil {
signer = types.LatestSignerForChainID(tx.ChainId())
} else {
signer = types.HomesteadSigner{}
}
from, err := types.Sender(signer, &tx)
if err != nil {
return "", err
}
return strings.ToLower(from.Hex()), nil
}
func setCORSHeaders(w http.ResponseWriter, r *http.Request) { func setCORSHeaders(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin") origin := r.Header.Get("Origin")
if origin != "" { if origin != "" {
...@@ -155,6 +297,31 @@ func setCORSHeaders(w http.ResponseWriter, r *http.Request) { ...@@ -155,6 +297,31 @@ func setCORSHeaders(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Max-Age", "86400") w.Header().Set("Access-Control-Max-Age", "86400")
} }
// extractFromAddress inspects JSON-RPC params and returns the `from` address if present.
func extractFromAddress(req RPCRequest) (string, bool) {
// Typical shapes: params[0] is an object with field "from"
if len(req.Params) == 0 {
return "", false
}
// Check first param
if obj, ok := req.Params[0].(map[string]interface{}); ok {
if f, ok2 := obj["from"].(string); ok2 && f != "" {
return strings.ToLower(f), true
}
}
// Fallback: check all params for an object that includes "from"
for _, p := range req.Params {
if obj, ok := p.(map[string]interface{}); ok {
if f, ok2 := obj["from"].(string); ok2 && f != "" {
return strings.ToLower(f), true
}
}
}
return "", false
}
func accountExists(address string) bool { func accountExists(address string) bool {
var count int var count int
query := "SELECT COUNT(1) FROM tb_account_info WHERE account_address = ? AND is_deleted = 0" query := "SELECT COUNT(1) FROM tb_account_info WHERE account_address = ? AND is_deleted = 0"
......
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