github.com/prysmaticlabs/prysm@v1.4.4/tools/faucet/server.go (about) 1 package main 2 3 import ( 4 "context" 5 "crypto/ecdsa" 6 "errors" 7 "fmt" 8 "log" 9 "math/big" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/ethereum/go-ethereum/common" 15 "github.com/ethereum/go-ethereum/core/types" 16 "github.com/ethereum/go-ethereum/crypto" 17 "github.com/ethereum/go-ethereum/ethclient" 18 "github.com/ethereum/go-ethereum/params" 19 "github.com/prestonvanloon/go-recaptcha" 20 faucetpb "github.com/prysmaticlabs/prysm/proto/faucet" 21 "github.com/prysmaticlabs/prysm/shared/timeutils" 22 "google.golang.org/grpc/metadata" 23 ) 24 25 const ipLimit = 5 26 27 var fundingAmount *big.Int 28 var funded = make(map[string]bool) 29 var ipCounter = make(map[string]int) 30 var fundingLock sync.Mutex 31 var pruneDuration = time.Hour * 4 32 33 const txGasLimit = 40000 34 const fundingAmountWei = "32500000000000000000" // 32.5 ETH in Wei. 35 36 type faucetServer struct { 37 r recaptcha.Recaptcha 38 client *ethclient.Client 39 funder common.Address 40 pk *ecdsa.PrivateKey 41 minScore float64 42 } 43 44 func init() { 45 var ok bool 46 fundingAmount, ok = new(big.Int).SetString(fundingAmountWei, 10) 47 if !ok { 48 log.Fatal("could not set funding amount") 49 } 50 } 51 52 func newFaucetServer( 53 r recaptcha.Recaptcha, 54 rpcPath, 55 funderPrivateKey string, 56 minScore float64, 57 ) *faucetServer { 58 client, err := ethclient.DialContext(context.Background(), rpcPath) 59 if err != nil { 60 panic(err) 61 } 62 63 pk, err := crypto.HexToECDSA(funderPrivateKey) 64 if err != nil { 65 panic(err) 66 } 67 68 funder := crypto.PubkeyToAddress(pk.PublicKey) 69 70 bal, err := client.BalanceAt(context.Background(), funder, nil) 71 if err != nil { 72 panic(err) 73 } 74 75 fmt.Printf("Funder is %s\n", funder.Hex()) 76 fmt.Printf("Funder has %d\n", bal) 77 78 return &faucetServer{ 79 r: r, 80 client: client, 81 funder: funder, 82 pk: pk, 83 minScore: minScore, 84 } 85 } 86 87 func (s *faucetServer) verifyRecaptcha(peer string, req *faucetpb.FundingRequest) error { 88 fmt.Printf("Sending captcha request for peer %s\n", peer) 89 90 rr, err := s.r.Check(peer, req.RecaptchaResponse) 91 if err != nil { 92 return err 93 } 94 if !rr.Success { 95 fmt.Printf("Unsuccessful recaptcha request. Error codes: %+v\n", rr.ErrorCodes) 96 return errors.New("failed") 97 } 98 if rr.Score < s.minScore { 99 return fmt.Errorf("recaptcha score too low (%f)", rr.Score) 100 } 101 if timeutils.Now().After(rr.ChallengeTS.Add(2 * time.Minute)) { 102 return errors.New("captcha challenge too old") 103 } 104 if rr.Action != req.WalletAddress { 105 return fmt.Errorf("action was %s, wanted %s", rr.Action, req.WalletAddress) 106 } 107 if !strings.HasSuffix(rr.Hostname, "prylabs.net") && !strings.HasSuffix(rr.Hostname, "prylabs.network") { 108 return fmt.Errorf("expected hostname (%s) to end in prylabs.net", rr.Hostname) 109 } 110 111 return nil 112 } 113 114 // RequestFunds from the ethereum 1.x faucet. Requires a valid captcha 115 // response. 116 func (s *faucetServer) RequestFunds(ctx context.Context, req *faucetpb.FundingRequest) (*faucetpb.FundingResponse, error) { 117 peer, err := s.getPeer(ctx) 118 if err != nil { 119 fmt.Printf("peer failure %v\n", err) 120 return &faucetpb.FundingResponse{Error: "peer error"}, nil 121 } 122 123 if err := s.verifyRecaptcha(peer, req); err != nil { 124 fmt.Printf("Recaptcha failure %v\n", err) 125 return &faucetpb.FundingResponse{Error: "recaptcha error"}, nil 126 } 127 128 fundingLock.Lock() 129 exceedPeerLimit := ipCounter[peer] >= ipLimit 130 if funded[req.WalletAddress] || exceedPeerLimit { 131 if exceedPeerLimit { 132 fmt.Printf("peer %s trying to get funded despite being over peer limit\n", peer) 133 } 134 fundingLock.Unlock() 135 return &faucetpb.FundingResponse{Error: "funded too recently"}, nil 136 } 137 funded[req.WalletAddress] = true 138 fundingLock.Unlock() 139 140 txHash, err := s.fundAndWait(common.HexToAddress(req.WalletAddress)) 141 if err != nil { 142 return &faucetpb.FundingResponse{Error: fmt.Sprintf("Failed to send transaction %v", err)}, nil 143 } 144 fundingLock.Lock() 145 ipCounter[peer]++ 146 fundingLock.Unlock() 147 148 fmt.Printf("Funded with TX %s\n", txHash) 149 150 return &faucetpb.FundingResponse{ 151 Amount: fundingAmount.String(), 152 TransactionHash: txHash, 153 }, nil 154 } 155 156 func (s *faucetServer) fundAndWait(to common.Address) (string, error) { 157 nonce := uint64(0) 158 nonce, err := s.client.PendingNonceAt(context.Background(), s.funder) 159 if err != nil { 160 return "", err 161 } 162 163 tx := types.NewTransaction(nonce, to, fundingAmount, txGasLimit, big.NewInt(1*params.GWei), nil /*data*/) 164 165 tx, err = types.SignTx(tx, types.NewEIP155Signer(big.NewInt(5)), s.pk) 166 if err != nil { 167 return "", err 168 } 169 170 if err := s.client.SendTransaction(context.Background(), tx); err != nil { 171 return "", err 172 } 173 174 // Wait for contract to mine 175 for pending := true; pending; _, pending, err = s.client.TransactionByHash(context.Background(), tx.Hash()) { 176 if err != nil { 177 log.Fatal(err) 178 } 179 time.Sleep(1 * time.Second) 180 } 181 182 return tx.Hash().Hex(), nil 183 } 184 185 func (s *faucetServer) getPeer(ctx context.Context) (string, error) { 186 md, ok := metadata.FromIncomingContext(ctx) 187 if !ok || len(md.Get("x-forwarded-for")) < 1 { 188 return "", errors.New("metadata not ok") 189 } 190 peer := md.Get("x-forwarded-for")[0] 191 return peer, nil 192 } 193 194 // reduce the counter for each ip every few hours. 195 func counterWatcher() { 196 ticker := time.NewTicker(pruneDuration) 197 for { 198 <-ticker.C 199 fundingLock.Lock() 200 for ip, ctr := range ipCounter { 201 if ctr == 0 { 202 continue 203 } 204 ipCounter[ip] = ctr - 1 205 } 206 fundingLock.Unlock() 207 } 208 }