From 9ecd8c44da1eb0a8014602053188eeddbf85957f Mon Sep 17 00:00:00 2001 From: iczc Date: Tue, 19 Sep 2023 01:13:00 +0800 Subject: [PATCH] refactor: maintain nonce locally (#22) --- README.md | 1 - cmd/server.go | 3 +- go.mod | 1 - go.sum | 2 -- internal/chain/transaction.go | 42 +++++++++++++++++++++------ internal/server/config.go | 4 +-- internal/server/dto.go | 2 +- internal/server/server.go | 53 ++--------------------------------- 8 files changed, 38 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 9d85d4c2..4246b2e8 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,6 @@ The following are the available command-line flags(excluding above wallet flags) |-------------------|--------------------------------------------------|---------------| | -httpport | Listener port to serve HTTP connection | 8080 | | -proxycount | Count of reverse proxies in front of the server | 0 | -| -queuecap | Maximum transactions waiting to be sent | 100 | | -faucet.amount | Number of Ethers to transfer per user request | 1 | | -faucet.minutes | Number of minutes to wait between funding rounds | 1440 | | -faucet.name | Network name to display on the frontend | testnet | diff --git a/cmd/server.go b/cmd/server.go index 91a00c88..abaaedc6 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -22,7 +22,6 @@ var ( httpPortFlag = flag.Int("httpport", 8080, "Listener port to serve HTTP connection") proxyCntFlag = flag.Int("proxycount", 0, "Count of reverse proxies in front of the server") - queueCapFlag = flag.Int("queuecap", 100, "Maximum transactions waiting to be sent") versionFlag = flag.Bool("version", false, "Print version number") payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to transfer per user request") @@ -61,7 +60,7 @@ func Execute() { if err != nil { panic(fmt.Errorf("cannot connect to web3 provider: %w", err)) } - config := server.NewConfig(*netnameFlag, *symbolFlag, *httpPortFlag, *intervalFlag, *payoutFlag, *proxyCntFlag, *queueCapFlag, *hcaptchaSiteKeyFlag, *hcaptchaSecretFlag) + config := server.NewConfig(*netnameFlag, *symbolFlag, *httpPortFlag, *intervalFlag, *payoutFlag, *proxyCntFlag, *hcaptchaSiteKeyFlag, *hcaptchaSecretFlag) go server.NewServer(txBuilder, config).Run() c := make(chan os.Signal, 1) diff --git a/go.mod b/go.mod index ae218795..e964e556 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/chainflag/eth-faucet go 1.17 require ( - github.com/LK4D4/trylock v0.0.0-20191027065348-ff7e133a5c54 github.com/agiledragon/gomonkey/v2 v2.10.1 github.com/ethereum/go-ethereum v1.10.26 github.com/jellydator/ttlcache/v2 v2.11.1 diff --git a/go.sum b/go.sum index e7d1c6e6..7b017ab0 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/LK4D4/trylock v0.0.0-20191027065348-ff7e133a5c54 h1:sg9CWNOhr58hMGmJ0q7x7jQ/B1RK/GyHNmeaYCJos9M= -github.com/LK4D4/trylock v0.0.0-20191027065348-ff7e133a5c54/go.mod h1:uHbOgfPowb74TKlV4AR5Az2haG6evxzM8Lmj1Xil25E= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= diff --git a/internal/chain/transaction.go b/internal/chain/transaction.go index 32f04270..b6092096 100644 --- a/internal/chain/transaction.go +++ b/internal/chain/transaction.go @@ -4,12 +4,15 @@ import ( "context" "crypto/ecdsa" "math/big" + "strings" + "sync/atomic" "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/crypto" "github.com/ethereum/go-ethereum/ethclient" + log "github.com/sirupsen/logrus" ) type TxBuilder interface { @@ -22,6 +25,7 @@ type TxBuild struct { privateKey *ecdsa.PrivateKey signer types.Signer fromAddress common.Address + nonce uint64 } func NewTxBuilder(provider string, privateKey *ecdsa.PrivateKey, chainID *big.Int) (TxBuilder, error) { @@ -37,12 +41,15 @@ func NewTxBuilder(provider string, privateKey *ecdsa.PrivateKey, chainID *big.In } } - return &TxBuild{ + txBuilder := &TxBuild{ client: client, privateKey: privateKey, signer: types.NewEIP155Signer(chainID), fromAddress: crypto.PubkeyToAddress(privateKey.PublicKey), - }, nil + } + txBuilder.refreshNonce(context.Background()) + + return txBuilder, nil } func (b *TxBuild) Sender() common.Address { @@ -50,11 +57,6 @@ func (b *TxBuild) Sender() common.Address { } func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (common.Hash, error) { - nonce, err := b.client.PendingNonceAt(ctx, b.Sender()) - if err != nil { - return common.Hash{}, err - } - gasLimit := uint64(21000) gasPrice, err := b.client.SuggestGasPrice(ctx) if err != nil { @@ -63,7 +65,7 @@ func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (comm toAddress := common.HexToAddress(to) unsignedTx := types.NewTx(&types.LegacyTx{ - Nonce: nonce, + Nonce: b.getAndIncrementNonce(), To: &toAddress, Value: value, Gas: gasLimit, @@ -75,5 +77,27 @@ func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (comm return common.Hash{}, err } - return signedTx.Hash(), b.client.SendTransaction(ctx, signedTx) + if err = b.client.SendTransaction(ctx, signedTx); err != nil { + log.Error("failed to send tx", "tx hash", signedTx.Hash().String(), "err", err) + if strings.Contains(err.Error(), "nonce") { + b.refreshNonce(context.Background()) + } + return common.Hash{}, err + } + + return signedTx.Hash(), nil +} + +func (b *TxBuild) getAndIncrementNonce() uint64 { + return atomic.AddUint64(&b.nonce, 1) - 1 +} + +func (b *TxBuild) refreshNonce(ctx context.Context) { + nonce, err := b.client.PendingNonceAt(ctx, b.Sender()) + if err != nil { + log.Error("failed to refresh nonce", "address", b.Sender(), "err", err) + return + } + + b.nonce = nonce } diff --git a/internal/server/config.go b/internal/server/config.go index 9fbb6e20..514bcfd3 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -7,12 +7,11 @@ type Config struct { interval int payout int proxyCount int - queueCap int hcaptchaSiteKey string hcaptchaSecret string } -func NewConfig(network, symbol string, httpPort, interval, payout, proxyCount, queueCap int, hcaptchaSiteKey, hcaptchaSecret string) *Config { +func NewConfig(network, symbol string, httpPort, interval, payout, proxyCount int, hcaptchaSiteKey, hcaptchaSecret string) *Config { return &Config{ network: network, symbol: symbol, @@ -20,7 +19,6 @@ func NewConfig(network, symbol string, httpPort, interval, payout, proxyCount, q interval: interval, payout: payout, proxyCount: proxyCount, - queueCap: queueCap, hcaptchaSiteKey: hcaptchaSiteKey, hcaptchaSecret: hcaptchaSecret, } diff --git a/internal/server/dto.go b/internal/server/dto.go index 4699839b..ac75ff55 100644 --- a/internal/server/dto.go +++ b/internal/server/dto.go @@ -56,7 +56,7 @@ func decodeJSONBody(r *http.Request, dst interface{}) error { msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset) return &malformedRequest{status: http.StatusBadRequest, message: msg} case errors.Is(err, io.ErrUnexpectedEOF): - msg := fmt.Sprintf("Request body contains badly-formed JSON") + msg := "Request body contains badly-formed JSON" return &malformedRequest{status: http.StatusBadRequest, message: msg} case errors.As(err, &unmarshalTypeError): msg := fmt.Sprintf("Request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset) diff --git a/internal/server/server.go b/internal/server/server.go index 49fde9f6..656f63cd 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,7 +7,6 @@ import ( "strconv" "time" - "github.com/LK4D4/trylock" log "github.com/sirupsen/logrus" "github.com/urfave/negroni" @@ -17,16 +16,13 @@ import ( type Server struct { chain.TxBuilder - mutex trylock.Mutex - cfg *Config - queue chan string + cfg *Config } func NewServer(builder chain.TxBuilder, cfg *Config) *Server { return &Server{ TxBuilder: builder, cfg: cfg, - queue: make(chan string, cfg.queueCap), } } @@ -42,40 +38,12 @@ func (s *Server) setupRouter() *http.ServeMux { } func (s *Server) Run() { - go func() { - ticker := time.NewTicker(time.Second) - for range ticker.C { - s.consumeQueue() - } - }() - n := negroni.New(negroni.NewRecovery(), negroni.NewLogger()) n.UseHandler(s.setupRouter()) log.Infof("Starting http server %d", s.cfg.httpPort) log.Fatal(http.ListenAndServe(":"+strconv.Itoa(s.cfg.httpPort), n)) } -func (s *Server) consumeQueue() { - if len(s.queue) == 0 { - return - } - - s.mutex.Lock() - defer s.mutex.Unlock() - for len(s.queue) != 0 { - address := <-s.queue - txHash, err := s.Transfer(context.Background(), address, chain.EtherToWei(int64(s.cfg.payout))) - if err != nil { - log.WithError(err).Error("Failed to handle transaction in the queue") - } else { - log.WithFields(log.Fields{ - "txHash": txHash, - "address": address, - }).Info("Consume from queue successfully") - } - } -} - func (s *Server) handleClaim() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { @@ -85,26 +53,9 @@ func (s *Server) handleClaim() http.HandlerFunc { // The error always be nil since it has already been handled in limiter address, _ := readAddress(r) - // Try to lock mutex if the work queue is empty - if len(s.queue) != 0 || !s.mutex.TryLock() { - select { - case s.queue <- address: - log.WithFields(log.Fields{ - "address": address, - }).Info("Added to queue successfully") - resp := claimResponse{Message: fmt.Sprintf("Added %s to the queue", address)} - renderJSON(w, resp, http.StatusOK) - default: - log.Warn("Max queue capacity reached") - renderJSON(w, claimResponse{Message: "Faucet queue is too long, please try again later"}, http.StatusServiceUnavailable) - } - return - } - ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() txHash, err := s.Transfer(ctx, address, chain.EtherToWei(int64(s.cfg.payout))) - s.mutex.Unlock() if err != nil { log.WithError(err).Error("Failed to send transaction") renderJSON(w, claimResponse{Message: err.Error()}, http.StatusInternalServerError) @@ -114,7 +65,7 @@ func (s *Server) handleClaim() http.HandlerFunc { log.WithFields(log.Fields{ "txHash": txHash, "address": address, - }).Info("Funded directly successfully") + }).Info("Transaction sent successfully") resp := claimResponse{Message: fmt.Sprintf("Txhash: %s", txHash)} renderJSON(w, resp, http.StatusOK) }