Skip to content

Commit

Permalink
Add faucet for Web3Q (ethereum#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
ping-ke committed Mar 15, 2022
1 parent 0381b48 commit 8800def
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 169 deletions.
20 changes: 8 additions & 12 deletions cmd/faucet/README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
# Faucet

The `faucet` is a simplistic web application with the goal of distributing small amounts of Ether in private and test networks.
The `faucet` is a simplistic web application with the goal of distributing small amounts of W3Q in private and test networks.

Users need to post their Ethereum addresses to fund in a Twitter status update or public Facebook post and share the link to the faucet. The faucet will in turn deduplicate user requests and send the Ether. After a funding round, the faucet prevents the same user requesting again for a pre-configured amount of time, proportional to the amount of Ether requested.
Users need to post their Web3Q addresses to fund in a Twitter status update or public Facebook post and share the link to the faucet. The faucet will in turn deduplicate user requests and send the W3Q. After a funding round, the faucet prevents the same user requesting again for a pre-configured amount of time, proportional to the amount of W3Q requested.

## Operation

The `faucet` is a single binary app (everything included) with all configurations set via command line flags and a few files.

First thing's first, the `faucet` needs to connect to an Ethereum network, for which it needs the necessary genesis and network infos. Each of the following flags must be set:
First thing's first, the `faucet` needs to connect to an Web3Q network, for which it needs the wsrpc to connect network. Each of the following flags must be set:

- `--genesis` is a path to a file containin the network `genesis.json`
- `--network` is the devp2p network id used during connection
- `--bootnodes` is a list of `enode://` ids to join the network through

The `faucet` will use the `les` protocol to join the configured Ethereum network and will store its data in `$HOME/.faucet` (currently not configurable).
- `--wsrpc` is a websocket rpc URL for ethclient to get data and submit tx from network

## Funding

To be able to distribute funds, the `faucet` needs access to an already funded Ethereum account. This can be configured via:
To be able to distribute funds, the `faucet` needs access to an already funded Web3Q account. This can be configured via:

- `--account.json` is a path to the Ethereum account's JSON key file
- `--account.json` is a path to the Web3Q account's JSON key file
- `--account.pass` is a path to a text file with the decryption passphrase

The faucet is able to distribute various amounts of Ether in exchange for various timeouts. These can be configured via:
The faucet is able to distribute various amounts of W3Q in exchange for various timeouts. These can be configured via:

- `--faucet.amount` is the number of Ethers to send by default
- `--faucet.amount` is the number of W3Qs to send by default
- `--faucet.minutes` is the time to wait before allowing a rerequest
- `--faucet.tiers` is the funding tiers to support (x3 time, x2.5 funds)

Expand Down
150 changes: 23 additions & 127 deletions cmd/faucet/faucet.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,16 @@ import (

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/ethstats"
"github.com/ethereum/go-ethereum/les"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/nat"
"github.com/ethereum/go-ethereum/params"
"github.com/gorilla/websocket"
)

var (
genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with")
apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection")
ethPortFlag = flag.Int("ethport", 30303, "Listener port for the devp2p connection")
bootFlag = flag.String("bootnodes", "", "Comma separated bootnode enode URLs to seed with")
netFlag = flag.Uint64("network", 0, "Network ID to use for the Ethereum protocol")
statsFlag = flag.String("ethstats", "", "Ethstats network monitoring auth string")
wsRpcFlag = flag.String("wsrpc", "", "websocket rpc URL for ethclient to get data and submit tx")
apiPortFlag = flag.Int("apiport", 81, "Listener port for the HTTP API connection")

netnameFlag = flag.String("faucet.name", "", "Network name to assign to the faucet")
payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to pay out per user request")
Expand All @@ -85,20 +70,12 @@ var (

twitterTokenFlag = flag.String("twitter.token", "", "Bearer token to authenticate with the v2 Twitter API")
twitterTokenV1Flag = flag.String("twitter.token.v1", "", "Bearer token to authenticate with the v1.1 Twitter API")

goerliFlag = flag.Bool("goerli", false, "Initializes the faucet with Görli network config")
rinkebyFlag = flag.Bool("rinkeby", false, "Initializes the faucet with Rinkeby network config")
)

var (
ether = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)
)

var (
gitCommit = "" // Git SHA1 commit hash of the release (set via linker flags)
gitDate = "" // Git commit date YYYYMMDD of the release (set via linker flags)
)

func main() {
// Parse the flags and set up the logger to print everything requested
flag.Parse()
Expand All @@ -110,7 +87,7 @@ func main() {
for i := 0; i < *tiersFlag; i++ {
// Calculate the amount for the next tier and format it
amount := float64(*payoutFlag) * math.Pow(2.5, float64(i))
amounts[i] = fmt.Sprintf("%s Ethers", strconv.FormatFloat(amount, 'f', -1, 64))
amounts[i] = fmt.Sprintf("%s W3Qs", strconv.FormatFloat(amount, 'f', -1, 64))
if amount == 1 {
amounts[i] = strings.TrimSuffix(amounts[i], "s")
}
Expand Down Expand Up @@ -146,20 +123,7 @@ func main() {
if err != nil {
log.Crit("Failed to render the faucet template", "err", err)
}
// Load and parse the genesis block requested by the user
genesis, err := getGenesis(genesisFlag, *goerliFlag, *rinkebyFlag)
if err != nil {
log.Crit("Failed to parse genesis config", "err", err)
}
// Convert the bootnodes to internal enode representations
var enodes []*enode.Node
for _, boot := range strings.Split(*bootFlag, ",") {
if url, err := enode.Parse(enode.ValidSchemes, boot); err == nil {
enodes = append(enodes, url)
} else {
log.Error("Failed to parse bootnode URL", "url", boot, "err", err)
}
}

// Load up the account key and decrypt its password
blob, err := ioutil.ReadFile(*accPassFlag)
if err != nil {
Expand All @@ -179,11 +143,10 @@ func main() {
log.Crit("Failed to unlock faucet signer account", "err", err)
}
// Assemble and start the faucet light service
faucet, err := newFaucet(genesis, *ethPortFlag, enodes, *netFlag, *statsFlag, ks, website.Bytes())
faucet, err := newFaucet(*wsRpcFlag, ks, website.Bytes())
if err != nil {
log.Crit("Failed to start faucet", "err", err)
}
defer faucet.close()

if err := faucet.listenAndServe(*apiPortFlag); err != nil {
log.Crit("Failed to launch faucet API", "err", err)
Expand All @@ -200,16 +163,15 @@ type request struct {

// faucet represents a crypto faucet backed by an Ethereum light client.
type faucet struct {
config *params.ChainConfig // Chain configurations for signing
stack *node.Node // Ethereum protocol stack
client *ethclient.Client // Client connection to the Ethereum chain
index []byte // Index page to serve up on the web
client *ethclient.Client // Client connection to the Ethereum chain
index []byte // Index page to serve up on the web

keystore *keystore.KeyStore // Keystore containing the single signer
account accounts.Account // Account funding user faucet requests
head *types.Header // Current head header of the faucet
balance *big.Int // Current balance of the faucet
nonce uint64 // Current pending nonce of the faucet
chainId *big.Int // Current chainId use to generate faucet tx
price *big.Int // Current gas price to issue funds with

conns []*wsConn // Currently live websocket connections
Expand All @@ -227,78 +189,27 @@ type wsConn struct {
wlock sync.Mutex
}

func newFaucet(genesis *core.Genesis, port int, enodes []*enode.Node, network uint64, stats string, ks *keystore.KeyStore, index []byte) (*faucet, error) {
// Assemble the raw devp2p protocol stack
stack, err := node.New(&node.Config{
Name: "geth",
Version: params.VersionWithCommit(gitCommit, gitDate),
DataDir: filepath.Join(os.Getenv("HOME"), ".faucet"),
P2P: p2p.Config{
NAT: nat.Any(),
NoDiscovery: true,
DiscoveryV5: true,
ListenAddr: fmt.Sprintf(":%d", port),
MaxPeers: 25,
BootstrapNodesV5: enodes,
},
})
if err != nil {
return nil, err
}

// Assemble the Ethereum light client protocol
cfg := ethconfig.Defaults
cfg.SyncMode = downloader.LightSync
cfg.NetworkId = network
cfg.Genesis = genesis
utils.SetDNSDiscoveryDefaults(&cfg, genesis.ToBlock(nil).Hash())

lesBackend, err := les.New(stack, &cfg)
func newFaucet(rpcFlag string, ks *keystore.KeyStore, index []byte) (*faucet, error) {
client, err := ethclient.Dial(rpcFlag)
if err != nil {
return nil, fmt.Errorf("Failed to register the Ethereum service: %w", err)
}

// Assemble the ethstats monitoring and reporting service'
if stats != "" {
if err := ethstats.New(stack, lesBackend.ApiBackend, lesBackend.Engine(), stats); err != nil {
return nil, err
}
}
// Boot up the client and ensure it connects to bootnodes
if err := stack.Start(); err != nil {
return nil, err
}
for _, boot := range enodes {
old, err := enode.Parse(enode.ValidSchemes, boot.String())
if err == nil {
stack.Server().AddPeer(old)
}
}
// Attach to the client and retrieve and interesting metadatas
api, err := stack.Attach()
chainId, err := client.ChainID(context.Background())
if err != nil {
stack.Close()
return nil, err
}
client := ethclient.NewClient(api)

return &faucet{
config: genesis.Config,
stack: stack,
client: client,
index: index,
keystore: ks,
chainId: chainId,
account: ks.Accounts()[0],
timeouts: make(map[string]time.Time),
update: make(chan struct{}, 1),
}, nil
}

// close terminates the Ethereum connection and tears down the faucet.
func (f *faucet) close() error {
return f.stack.Close()
}

// listenAndServe registers the HTTP handlers for the faucet and boots it up
// for service user funding requests.
func (f *faucet) listenAndServe(port int) error {
Expand All @@ -317,7 +228,11 @@ func (f *faucet) webHandler(w http.ResponseWriter, r *http.Request) {

// apiHandler handles requests for Ether grants and transaction statuses.
func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{}
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
Expand Down Expand Up @@ -376,7 +291,6 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) {
if err = send(wsconn, map[string]interface{}{
"funds": new(big.Int).Div(balance, ether),
"funded": nonce,
"peers": f.stack.Server().PeerCount(),
"requests": reqs,
}, 3*time.Second); err != nil {
log.Warn("Failed to send initial stats to client", "err", err)
Expand Down Expand Up @@ -493,7 +407,7 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) {
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)
signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID)
signed, err := f.keystore.SignTx(f.account, tx, f.chainId)
if err != nil {
f.lock.Unlock()
if err = sendError(wsconn, err); err != nil {
Expand Down Expand Up @@ -616,13 +530,11 @@ func (f *faucet) loop() {
log.Info("Updated faucet state", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp), "balance", f.balance, "nonce", f.nonce, "price", f.price)

balance := new(big.Int).Div(f.balance, ether)
peers := f.stack.Server().PeerCount()

for _, conn := range f.conns {
if err := send(conn, map[string]interface{}{
"funds": balance,
"funded": f.nonce,
"peers": peers,
"requests": f.reqs,
}, time.Second); err != nil {
log.Warn("Failed to send stats to client", "err", err)
Expand Down Expand Up @@ -738,10 +650,10 @@ func authTwitter(url string, tokenV1, tokenV2 string) (string, string, string, c
address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body)))
if address == (common.Address{}) {
//lint:ignore ST1005 This error is to be displayed in the browser
return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund")
return "", "", "", common.Address{}, errors.New("No Web3Q Chain address found to fund")
}
var avatar string
if parts = regexp.MustCompile(`src="([^"]+twimg\.com/profile_images[^"]+)"`).FindStringSubmatch(string(body)); len(parts) == 2 {
if parts = regexp.MustCompile("src=\"([^\"]+twimg.com/profile_images[^\"]+)\"").FindStringSubmatch(string(body)); len(parts) == 2 {
avatar = parts[1]
}
return username + "@twitter", username, avatar, address, nil
Expand Down Expand Up @@ -864,10 +776,10 @@ func authFacebook(url string) (string, string, common.Address, error) {
address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body)))
if address == (common.Address{}) {
//lint:ignore ST1005 This error is to be displayed in the browser
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
return "", "", common.Address{}, errors.New("No Web3Q Chain address found to fund")
}
var avatar string
if parts = regexp.MustCompile(`src="([^"]+fbcdn\.net[^"]+)"`).FindStringSubmatch(string(body)); len(parts) == 2 {
if parts = regexp.MustCompile("src=\"([^\"]+fbcdn.net[^\"]+)\"").FindStringSubmatch(string(body)); len(parts) == 2 {
avatar = parts[1]
}
return username + "@facebook", avatar, address, nil
Expand All @@ -880,23 +792,7 @@ func authNoAuth(url string) (string, string, common.Address, error) {
address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(url))
if address == (common.Address{}) {
//lint:ignore ST1005 This error is to be displayed in the browser
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
return "", "", common.Address{}, errors.New("No Web3Q Chain address found to fund")
}
return address.Hex() + "@noauth", "", address, nil
}

// getGenesis returns a genesis based on input args
func getGenesis(genesisFlag *string, goerliFlag bool, rinkebyFlag bool) (*core.Genesis, error) {
switch {
case genesisFlag != nil:
var genesis core.Genesis
err := common.LoadJSON(*genesisFlag, &genesis)
return &genesis, err
case goerliFlag:
return core.DefaultGoerliGenesisBlock(), nil
case rinkebyFlag:
return core.DefaultRinkebyGenesisBlock(), nil
default:
return nil, fmt.Errorf("no genesis flag provided")
}
}
Loading

0 comments on commit 8800def

Please sign in to comment.