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
// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.
package blacklist
import (
"errors"
"math/big"
"strings"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
)
// Reference imports to suppress errors if they are not otherwise used.
var (
_ = errors.New
_ = big.NewInt
_ = strings.NewReader
_ = ethereum.NotFound
_ = bind.Bind
_ = common.Big1
_ = types.BloomLookup
_ = event.NewSubscription
)
// BlacklistMetaData contains all meta data concerning the Blacklist contract.
var BlacklistMetaData = &bind.MetaData{
ABI: "[{\"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\"}]",
}
// BlacklistABI is the input ABI used to generate the binding from.
// Deprecated: Use BlacklistMetaData.ABI instead.
var BlacklistABI = BlacklistMetaData.ABI
// Blacklist is an auto generated Go binding around an Ethereum contract.
type Blacklist struct {
BlacklistCaller // Read-only binding to the contract
BlacklistTransactor // Write-only binding to the contract
BlacklistFilterer // Log filterer for contract events
}
// BlacklistCaller is an auto generated read-only Go binding around an Ethereum contract.
type BlacklistCaller struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// BlacklistTransactor is an auto generated write-only Go binding around an Ethereum contract.
type BlacklistTransactor struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// BlacklistFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
type BlacklistFilterer struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// BlacklistSession is an auto generated Go binding around an Ethereum contract,
// with pre-set call and transact options.
type BlacklistSession struct {
Contract *Blacklist // Generic contract binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// BlacklistCallerSession is an auto generated read-only Go binding around an Ethereum contract,
// with pre-set call options.
type BlacklistCallerSession struct {
Contract *BlacklistCaller // Generic contract caller binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
}
// BlacklistTransactorSession is an auto generated write-only Go binding around an Ethereum contract,
// with pre-set transact options.
type BlacklistTransactorSession struct {
Contract *BlacklistTransactor // Generic contract transactor binding to set the session for
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// BlacklistRaw is an auto generated low-level Go binding around an Ethereum contract.
type BlacklistRaw struct {
Contract *Blacklist // Generic contract binding to access the raw methods on
}
// BlacklistCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract.
type BlacklistCallerRaw struct {
Contract *BlacklistCaller // Generic read-only contract binding to access the raw methods on
}
// BlacklistTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
type BlacklistTransactorRaw struct {
Contract *BlacklistTransactor // Generic write-only contract binding to access the raw methods on
}
// NewBlacklist creates a new instance of Blacklist, bound to a specific deployed contract.
func NewBlacklist(address common.Address, backend bind.ContractBackend) (*Blacklist, error) {
contract, err := bindBlacklist(address, backend, backend, backend)
if err != nil {
return nil, err
}
return &Blacklist{BlacklistCaller: BlacklistCaller{contract: contract}, BlacklistTransactor: BlacklistTransactor{contract: contract}, BlacklistFilterer: BlacklistFilterer{contract: contract}}, nil
}
// NewBlacklistCaller creates a new read-only instance of Blacklist, bound to a specific deployed contract.
func NewBlacklistCaller(address common.Address, caller bind.ContractCaller) (*BlacklistCaller, error) {
contract, err := bindBlacklist(address, caller, nil, nil)
if err != nil {
return nil, err
}
return &BlacklistCaller{contract: contract}, nil
}
// NewBlacklistTransactor creates a new write-only instance of Blacklist, bound to a specific deployed contract.
func NewBlacklistTransactor(address common.Address, transactor bind.ContractTransactor) (*BlacklistTransactor, error) {
contract, err := bindBlacklist(address, nil, transactor, nil)
if err != nil {
return nil, err
}
return &BlacklistTransactor{contract: contract}, nil
}
// NewBlacklistFilterer creates a new log filterer instance of Blacklist, bound to a specific deployed contract.
func NewBlacklistFilterer(address common.Address, filterer bind.ContractFilterer) (*BlacklistFilterer, error) {
contract, err := bindBlacklist(address, nil, nil, filterer)
if err != nil {
return nil, err
}
return &BlacklistFilterer{contract: contract}, nil
}
// bindBlacklist binds a generic wrapper to an already deployed contract.
func bindBlacklist(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) {
parsed, err := abi.JSON(strings.NewReader(BlacklistABI))
if err != nil {
return nil, err
}
return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_Blacklist *BlacklistRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
return _Blacklist.Contract.BlacklistCaller.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_Blacklist *BlacklistRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _Blacklist.Contract.BlacklistTransactor.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_Blacklist *BlacklistRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _Blacklist.Contract.BlacklistTransactor.contract.Transact(opts, method, params...)
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_Blacklist *BlacklistCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
return _Blacklist.Contract.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_Blacklist *BlacklistTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _Blacklist.Contract.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_Blacklist *BlacklistTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _Blacklist.Contract.contract.Transact(opts, method, params...)
}
// InBlackList is a free data retrieval call binding the contract method 0x5dae584f.
//
// Solidity: function inBlackList(address[] addrList) view returns(bool[] isBlack)
func (_Blacklist *BlacklistCaller) InBlackList(opts *bind.CallOpts, addrList []common.Address) ([]bool, error) {
var out []interface{}
err := _Blacklist.contract.Call(opts, &out, "inBlackList", addrList)
if err != nil {
return *new([]bool), err
}
out0 := *abi.ConvertType(out[0], new([]bool)).(*[]bool)
return out0, err
}
// InBlackList is a free data retrieval call binding the contract method 0x5dae584f.
//
// Solidity: function inBlackList(address[] addrList) view returns(bool[] isBlack)
func (_Blacklist *BlacklistSession) InBlackList(addrList []common.Address) ([]bool, error) {
return _Blacklist.Contract.InBlackList(&_Blacklist.CallOpts, addrList)
}
// InBlackList is a free data retrieval call binding the contract method 0x5dae584f.
//
// Solidity: function inBlackList(address[] addrList) view returns(bool[] isBlack)
func (_Blacklist *BlacklistCallerSession) InBlackList(addrList []common.Address) ([]bool, error) {
return _Blacklist.Contract.InBlackList(&_Blacklist.CallOpts, addrList)
}
// SetBlackList is a paid mutator transaction binding the contract method 0x8f85a043.
//
// Solidity: function setBlackList(address[] addrList, bool isBlack) returns(bool)
func (_Blacklist *BlacklistTransactor) SetBlackList(opts *bind.TransactOpts, addrList []common.Address, isBlack bool) (*types.Transaction, error) {
return _Blacklist.contract.Transact(opts, "setBlackList", addrList, isBlack)
}
// SetBlackList is a paid mutator transaction binding the contract method 0x8f85a043.
//
// Solidity: function setBlackList(address[] addrList, bool isBlack) returns(bool)
func (_Blacklist *BlacklistSession) SetBlackList(addrList []common.Address, isBlack bool) (*types.Transaction, error) {
return _Blacklist.Contract.SetBlackList(&_Blacklist.TransactOpts, addrList, isBlack)
}
// SetBlackList is a paid mutator transaction binding the contract method 0x8f85a043.
//
// Solidity: function setBlackList(address[] addrList, bool isBlack) returns(bool)
func (_Blacklist *BlacklistTransactorSession) SetBlackList(addrList []common.Address, isBlack bool) (*types.Transaction, error) {
return _Blacklist.Contract.SetBlackList(&_Blacklist.TransactOpts, addrList, isBlack)
}
......@@ -12,6 +12,7 @@ services:
max-file: "5"
environment:
- 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
command:
- "/bin/sh"
......
......@@ -4,4 +4,27 @@ go 1.24.4
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/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/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 (
"database/sql"
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"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"
)
......@@ -56,6 +59,8 @@ type RPCResponse struct {
var (
db *sql.DB
rpcBackend = os.Getenv("ETH_RPC_BACKEND") // Real Ethereum RPC address, recommend using environment variable
ethClient *ethclient.Client
blacklistContract common.Address
)
func main() {
......@@ -66,7 +71,41 @@ func main() {
if err != nil {
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)
log.Println("RPC proxy service started, listening on port: 8545")
......@@ -83,12 +122,16 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
return
}
body, err := ioutil.ReadAll(r.Body)
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request", http.StatusBadRequest)
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
// Try to parse as batch request first
......@@ -104,7 +147,9 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
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)
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
}
......@@ -115,6 +160,7 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Handle eth_getBalance (existing behavior)
if req.Method == "eth_getBalance" && len(req.Params) > 0 {
// get remote ip from header
xForwardedFor := r.Header.Get("X-Forwarded-For")
......@@ -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])
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
}
}
// 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
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) {
origin := r.Header.Get("Origin")
if origin != "" {
......@@ -155,6 +297,31 @@ func setCORSHeaders(w http.ResponseWriter, r *http.Request) {
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 {
var count int
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