diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index ba0b76866..e9a9a108f 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -61,6 +61,8 @@ import ( "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/params" "github.com/gorilla/websocket" + + lru "github.com/hashicorp/golang-lru" ) var ( @@ -235,12 +237,23 @@ func main() { // request represents an accepted funding request. type request struct { + Id string `json:"-"` Avatar string `json:"avatar"` // Avatar URL to make the UI nicer Account common.Address `json:"account"` // Ethereum address being funded Time time.Time `json:"time"` // Timestamp when the request was accepted Tx *types.Transaction `json:"tx"` // Transaction funding the account } +type fundReq struct { + Id string + Username string + Avatar string + Symbol string + Tier int64 + Address common.Address + Wsconn *wsConn +} + type bep2eInfo struct { Contract common.Address Amount big.Int @@ -261,15 +274,18 @@ type faucet struct { nonce uint64 // Current pending nonce of the faucet price *big.Int // Current gas price to issue funds with - conns []*wsConn // Currently live websocket connections - timeouts map[string]time.Time // History of users and their funding timeouts - reqs []*request // Currently pending funding requests - update chan struct{} // Channel to signal request updates + conns []*wsConn // Currently live websocket connections + timeouts map[string]time.Time // History of users and their funding timeouts + reqs []*request // Currently pending funding requests + update chan struct{} // Channel to signal request updates + fundQueue chan *fundReq // Channel to signal funded requests lock sync.RWMutex // Lock protecting the faucet's internals bep2eInfos map[string]bep2eInfo bep2eAbi abi.ABI + + fundedCache *lru.Cache // LRU cache of recently funded users } // wsConn wraps a websocket connection with a write mutex as the underlying @@ -338,17 +354,24 @@ func newFaucet(genesis *core.Genesis, port int, enodes []*enode.Node, network ui } client := ethclient.NewClient(api) + lru, err := lru.New(20000) + if err != nil { + return nil, err + } + return &faucet{ - config: genesis.Config, - stack: stack, - client: client, - index: index, - keystore: ks, - account: ks.Accounts()[0], - timeouts: make(map[string]time.Time), - update: make(chan struct{}, 1), - bep2eInfos: bep2eInfos, - bep2eAbi: bep2eAbi, + config: genesis.Config, + stack: stack, + client: client, + index: index, + keystore: ks, + account: ks.Accounts()[0], + timeouts: make(map[string]time.Time), + update: make(chan struct{}, 1), + fundQueue: make(chan *fundReq, 1024), + bep2eInfos: bep2eInfos, + bep2eAbi: bep2eAbi, + fundedCache: lru, }, nil } @@ -357,20 +380,29 @@ func newHttpFaucet(genesis *core.Genesis, rpcApi string, ks *keystore.KeyStore, if err != nil { return nil, err } + client, err := ethclient.Dial(rpcApi) if err != nil { return nil, err } + + lru, err := lru.New(20000) + if err != nil { + return nil, err + } + return &faucet{ - config: genesis.Config, - client: client, - index: index, - keystore: ks, - account: ks.Accounts()[0], - timeouts: make(map[string]time.Time), - update: make(chan struct{}, 1), - bep2eInfos: bep2eInfos, - bep2eAbi: bep2eAbi, + config: genesis.Config, + client: client, + index: index, + keystore: ks, + account: ks.Accounts()[0], + timeouts: make(map[string]time.Time), + update: make(chan struct{}, 1), + fundQueue: make(chan *fundReq, 1024), + bep2eInfos: bep2eInfos, + bep2eAbi: bep2eAbi, + fundedCache: lru, }, nil } @@ -474,6 +506,10 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { log.Warn("Failed to send initial header to client", "err", err) return } + + sendCount := 0 + requestTimestamp := time.Now().Add(time.Duration(-100) * time.Millisecond) + // Keep reading requests from the websocket until the connection breaks for { // Fetch the next funding request and validate against github @@ -486,6 +522,26 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { if err = conn.ReadJSON(&msg); err != nil { return } + + // if send count > 100, return and close connection + if sendCount > 100 { + if err = sendError(wsconn, errors.New("too many requests from client")); err != nil { + log.Warn("Failed to send busy error to client", "err", err) + } + + return + } + sendCount++ + + // if request timestamp < 100ms, return and close connection + if time.Since(requestTimestamp) < time.Duration(100)*time.Millisecond { + if err = sendError(wsconn, errors.New("too many requests from client")); err != nil { + log.Warn("Failed to send busy error to client", "err", err) + } + + return + } + if !*noauthFlag && !strings.HasPrefix(msg.URL, "https://twitter.com/") && !strings.HasPrefix(msg.URL, "https://www.facebook.com/") { if err = sendError(wsconn, errors.New("URL doesn't link to supported services")); err != nil { log.Warn("Failed to send URL error to client", "err", err) @@ -571,81 +627,48 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { // Ensure the user didn't request funds too recently f.lock.Lock() - var ( - fund bool - timeout time.Time - ) - if timeout = f.timeouts[id]; time.Now().After(timeout) { - var tx *types.Transaction - if msg.Symbol == "NativeToken" { - // User wasn't funded recently, create the funding transaction - amount := new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether) - amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil)) - amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil)) - tx = types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil) + // check if the user has already requested funds + if qosT, exist := f.fundedCache.Get(address); exist { + if time.Now().After(qosT.(time.Time)) { + f.fundedCache.Remove(address) } else { - tokenInfo, ok := f.bep2eInfos[msg.Symbol] - if !ok { - f.lock.Unlock() - log.Warn("Failed to find symbol", "symbol", msg.Symbol) - continue - } - input, err := f.bep2eAbi.Pack("transfer", address, &tokenInfo.Amount) - if err != nil { - f.lock.Unlock() - log.Warn("Failed to pack transfer transaction", "err", err) - continue - } - tx = types.NewTransaction(f.nonce+uint64(len(f.reqs)), tokenInfo.Contract, nil, 420000, f.price, input) - } - signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID) - if err != nil { + f.fundedCache.Add(address, time.Now().Add(time.Duration(*minutesFlag)*time.Minute)) f.lock.Unlock() - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send transaction creation error to client", "err", err) - return - } - continue - } - // Submit the transaction and mark as funded if successful - if err := f.client.SendTransaction(context.Background(), signed); err != nil { - f.lock.Unlock() - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send transaction transmission error to client", "err", err) + + if err = sendError(wsconn, errors.New("you have already requested funds recently, please try again later")); err != nil { + log.Warn("Failed to send request error to client", "err", err) return } continue } - f.reqs = append(f.reqs, &request{ - Avatar: avatar, - Account: address, - Time: time.Now(), - Tx: signed, - }) - timeout := time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute - grace := timeout / 288 // 24h timeout => 5m grace - - f.timeouts[id] = time.Now().Add(timeout - grace) - fund = true } + f.lock.Unlock() - // Send an error if too frequent funding, othewise a success - if !fund { - if err = sendError(wsconn, fmt.Errorf("%s left until next allowance", common.PrettyDuration(time.Until(timeout)))); err != nil { // nolint: gosimple - log.Warn("Failed to send funding error to client", "err", err) - return - } - continue - } - if err = sendSuccess(wsconn, fmt.Sprintf("Funding request accepted for %s into %s", username, address.Hex())); err != nil { - log.Warn("Failed to send funding success to client", "err", err) - return + req := &fundReq{ + Id: id, + Username: username, + Avatar: avatar, + Symbol: msg.Symbol, + Tier: int64(msg.Tier), + Address: address, + Wsconn: wsconn, } + select { - case f.update <- struct{}{}: + case f.fundQueue <- req: + requestTimestamp = time.Now() + + if err := sendSuccess(wsconn, "Funding request sent into queue, please waiting"); err != nil { + log.Warn("Failed to send funding success to client", "err", err) + return + } default: + if err = sendError(wsconn, errors.New("faucet is busy, please try again later")); err != nil { + log.Warn("Failed to send busy error to client", "err", err) + return + } } } } @@ -653,6 +676,9 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { // refresh attempts to retrieve the latest header from the chain and extract the // associated faucet balance and nonce for connectivity caching. func (f *faucet) refresh(head *types.Header) error { + f.lock.Lock() + defer f.lock.Unlock() + // Ensure a state update does not run for too long ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -684,17 +710,158 @@ func (f *faucet) refresh(head *types.Header) error { } } // Everything succeeded, update the cached stats and eject old requests - f.lock.Lock() f.head, f.balance = head, balance f.price, f.nonce = price, nonce + for len(f.reqs) > 0 && f.reqs[0].Tx.Nonce() < f.nonce { f.reqs = f.reqs[1:] } - f.lock.Unlock() + + // check transaction status + // if transaction timestamp is more than 1 min, send cancel transaction and remove it + for len(f.reqs) > 0 && time.Now().After(f.reqs[0].Time.Add(time.Minute)) { + txNonce := f.reqs[0].Tx.Nonce() + txToAddress := f.reqs[0].Account + id := f.reqs[0].Id + + // send cancel transaction + resetPrice := new(big.Int).Mul(price, big.NewInt(2)) + + tx := types.NewTransaction(txNonce, f.account.Address, big.NewInt(0), 21000, resetPrice, nil) + signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID) + if err != nil { + return err + } + + err = func(signed *types.Transaction) error { + ctxRT, cancelRT := context.WithTimeout(context.Background(), 30*time.Second) + defer cancelRT() + + if err := f.client.SendTransaction(ctxRT, signed); err != nil { + return err + } + + return nil + }(signed) + + if err != nil { + return err + } + + // creanup the transaction + f.reqs = f.reqs[1:] + + f.fundedCache.Remove(txToAddress) + delete(f.timeouts, id) + } return nil } +func (f *faucet) fundHandle() { + f.lock.Lock() + defer f.lock.Unlock() + + defer func() { + select { + case f.update <- struct{}{}: + default: + } + }() + + for { + var req *fundReq + ok := false + + select { + case req, ok = <-f.fundQueue: + if !ok { + return + } + default: + return + } + + id := req.Id + username := req.Username + avatar := req.Avatar + address := req.Address + symbol := req.Symbol + wsconn := req.Wsconn + tier := req.Tier + + var ( + fund bool + timeout time.Time + ) + if timeout = f.timeouts[id]; time.Now().After(timeout) { + f.fundedCache.Add(address, time.Now().Add(time.Duration(*minutesFlag)*time.Minute)) + + var tx *types.Transaction + if symbol == "NativeToken" { + // User wasn't funded recently, create the funding transaction + amount := new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether) + amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(tier), nil)) + amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(tier), nil)) + + tx = types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil) + } else { + tokenInfo, ok := f.bep2eInfos[symbol] + if !ok { + log.Warn("Failed to find symbol", "symbol", symbol) + continue + } + input, err := f.bep2eAbi.Pack("transfer", address, &tokenInfo.Amount) + if err != nil { + log.Warn("Failed to pack transfer transaction", "err", err) + continue + } + tx = types.NewTransaction(f.nonce+uint64(len(f.reqs)), tokenInfo.Contract, nil, 420000, f.price, input) + } + signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID) + if err != nil { + if err = sendError(wsconn, err); err != nil { + log.Warn("Failed to send transaction creation error to client", "err", err) + } + continue + } + // Submit the transaction and mark as funded if successful + if err := f.client.SendTransaction(context.Background(), signed); err != nil { + if err = sendError(wsconn, err); err != nil { + log.Warn("Failed to send transaction transmission error to client", "err", err) + } + continue + } + f.reqs = append(f.reqs, &request{ + Id: id, + Avatar: avatar, + Account: address, + Time: time.Now(), + Tx: signed, + }) + timeout := time.Duration(*minutesFlag*int(math.Pow(3, float64(tier)))) * time.Minute + grace := timeout / 288 // 24h timeout => 5m grace + + f.timeouts[id] = time.Now().Add(timeout - grace) + f.fundedCache.Add(address, f.timeouts[id]) + fund = true + } + + // Send an error if too frequent funding, othewise a success + if !fund { + if err := sendError(wsconn, fmt.Errorf("%s left until next allowance", common.PrettyDuration(time.Until(timeout)))); err != nil { // nolint: gosimple + log.Warn("Failed to send funding error to client", "err", err) + } + continue + } + + if err := sendSuccess(wsconn, fmt.Sprintf("Funding request accepted for %s into %s", username, address.Hex())); err != nil { + log.Warn("Failed to send funding success to client", "err", err) + return + } + } +} + // loop keeps waiting for interesting events and pushes them out to connected // websockets. func (f *faucet) loop() { @@ -759,6 +926,8 @@ func (f *faucet) loop() { } } f.lock.RUnlock() + + f.fundHandle() } }()