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  }