Skip to content

Commit 8a28408

Browse files
committed
cmd/faucet, cmd/puppeth: support multi-tiered faucet
1 parent e1dc7ec commit 8a28408

File tree

5 files changed

+92
-30
lines changed

5 files changed

+92
-30
lines changed

cmd/faucet/faucet.go

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ import (
2727
"fmt"
2828
"html/template"
2929
"io/ioutil"
30+
"math"
3031
"math/big"
3132
"net/http"
3233
"net/url"
3334
"os"
3435
"path/filepath"
36+
"strconv"
3537
"strings"
3638
"sync"
3739
"time"
@@ -67,6 +69,7 @@ var (
6769
netnameFlag = flag.String("faucet.name", "", "Network name to assign to the faucet")
6870
payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to pay out per user request")
6971
minutesFlag = flag.Int("faucet.minutes", 1440, "Number of minutes to wait between funding rounds")
72+
tiersFlag = flag.Int("faucet.tiers", 3, "Number of funding tiers to enable (x3 time, x2.5 funds)")
7073

7174
accJSONFlag = flag.String("account.json", "", "Key json file to fund user requests with")
7275
accPassFlag = flag.String("account.pass", "", "Decryption password to access faucet funds")
@@ -89,22 +92,46 @@ func main() {
8992
flag.Parse()
9093
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*logFlag), log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
9194

95+
// Construct the payout tiers
96+
amounts := make([]string, *tiersFlag)
97+
periods := make([]string, *tiersFlag)
98+
for i := 0; i < *tiersFlag; i++ {
99+
// Calculate the amount for the next tier and format it
100+
amount := float64(*payoutFlag) * math.Pow(2.5, float64(i))
101+
amounts[i] = fmt.Sprintf("%s Ethers", strconv.FormatFloat(amount, 'f', -1, 64))
102+
if amount == 1 {
103+
amounts[i] = strings.TrimSuffix(amounts[i], "s")
104+
}
105+
// Calcualte the period for th enext tier and format it
106+
period := *minutesFlag * int(math.Pow(3, float64(i)))
107+
periods[i] = fmt.Sprintf("%d mins", period)
108+
if period%60 == 0 {
109+
period /= 60
110+
periods[i] = fmt.Sprintf("%d hours", period)
111+
}
112+
if period%24 == 0 {
113+
period /= 24
114+
periods[i] = fmt.Sprintf("%d days", period)
115+
}
116+
if period == 1 {
117+
periods[i] = strings.TrimSuffix(periods[i], "s")
118+
}
119+
}
92120
// Load up and render the faucet website
93121
tmpl, err := Asset("faucet.html")
94122
if err != nil {
95123
log.Crit("Failed to load the faucet template", "err", err)
96124
}
97-
period := fmt.Sprintf("%d minute(s)", *minutesFlag)
98-
if *minutesFlag%60 == 0 {
99-
period = fmt.Sprintf("%d hour(s)", *minutesFlag/60)
100-
}
101125
website := new(bytes.Buffer)
102-
template.Must(template.New("").Parse(string(tmpl))).Execute(website, map[string]interface{}{
126+
err = template.Must(template.New("").Parse(string(tmpl))).Execute(website, map[string]interface{}{
103127
"Network": *netnameFlag,
104-
"Amount": *payoutFlag,
105-
"Period": period,
128+
"Amounts": amounts,
129+
"Periods": periods,
106130
"Recaptcha": *captchaToken,
107131
})
132+
if err != nil {
133+
log.Crit("Failed to render the faucet template", "err", err)
134+
}
108135
// Load and parse the genesis block requested by the user
109136
blob, err := ioutil.ReadFile(*genesisFlag)
110137
if err != nil {
@@ -171,10 +198,10 @@ type faucet struct {
171198
nonce uint64 // Current pending nonce of the faucet
172199
price *big.Int // Current gas price to issue funds with
173200

174-
conns []*websocket.Conn // Currently live websocket connections
175-
history map[string]time.Time // History of users and their funding requests
176-
reqs []*request // Currently pending funding requests
177-
update chan struct{} // Channel to signal request updates
201+
conns []*websocket.Conn // Currently live websocket connections
202+
timeouts map[string]time.Time // History of users and their funding timeouts
203+
reqs []*request // Currently pending funding requests
204+
update chan struct{} // Channel to signal request updates
178205

179206
lock sync.RWMutex // Lock protecting the faucet's internals
180207
}
@@ -241,7 +268,7 @@ func newFaucet(genesis *core.Genesis, port int, enodes []*discv5.Node, network u
241268
index: index,
242269
keystore: ks,
243270
account: ks.Accounts()[0],
244-
history: make(map[string]time.Time),
271+
timeouts: make(map[string]time.Time),
245272
update: make(chan struct{}, 1),
246273
}, nil
247274
}
@@ -295,14 +322,22 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
295322
"peers": f.stack.Server().PeerCount(),
296323
"requests": f.reqs,
297324
})
298-
header, _ := f.client.HeaderByNumber(context.Background(), nil)
299-
websocket.JSON.Send(conn, header)
325+
// Send the initial block to the client
326+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
327+
header, err := f.client.HeaderByNumber(ctx, nil)
328+
cancel()
300329

330+
if err != nil {
331+
log.Error("Failed to retrieve latest header", "err", err)
332+
} else {
333+
websocket.JSON.Send(conn, header)
334+
}
301335
// Keep reading requests from the websocket until the connection breaks
302336
for {
303337
// Fetch the next funding request and validate against github
304338
var msg struct {
305339
URL string `json:"url"`
340+
Tier uint `json:"tier"`
306341
Captcha string `json:"captcha"`
307342
}
308343
if err := websocket.JSON.Receive(conn, &msg); err != nil {
@@ -312,7 +347,11 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
312347
websocket.JSON.Send(conn, map[string]string{"error": "URL doesn't link to GitHub Gists"})
313348
continue
314349
}
315-
log.Info("Faucet funds requested", "gist", msg.URL)
350+
if msg.Tier >= uint(*tiersFlag) {
351+
websocket.JSON.Send(conn, map[string]string{"error": "Invalid funding tier requested"})
352+
continue
353+
}
354+
log.Info("Faucet funds requested", "gist", msg.URL, "tier", msg.Tier)
316355

317356
// If captcha verifications are enabled, make sure we're not dealing with a robot
318357
if *captchaToken != "" {
@@ -337,7 +376,7 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
337376
}
338377
if !result.Success {
339378
log.Warn("Captcha verification failed", "err", string(result.Errors))
340-
websocket.JSON.Send(conn, map[string]string{"error": "Beep-boop, you're a robot!"})
379+
websocket.JSON.Send(conn, map[string]string{"error": "Beep-bop, you're a robot!"})
341380
continue
342381
}
343382
}
@@ -396,11 +435,15 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
396435
f.lock.Lock()
397436
var (
398437
fund bool
399-
elapsed time.Duration
438+
timeout time.Time
400439
)
401-
if elapsed = time.Since(f.history[gist.Owner.Login]); elapsed > time.Duration(*minutesFlag)*time.Minute {
440+
if timeout = f.timeouts[gist.Owner.Login]; time.Now().After(timeout) {
402441
// User wasn't funded recently, create the funding transaction
403-
tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether), big.NewInt(21000), f.price, nil)
442+
amount := new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether)
443+
amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil))
444+
amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil))
445+
446+
tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, big.NewInt(21000), f.price, nil)
404447
signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainId)
405448
if err != nil {
406449
websocket.JSON.Send(conn, map[string]string{"error": err.Error()})
@@ -419,14 +462,14 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
419462
Time: time.Now(),
420463
Tx: signed,
421464
})
422-
f.history[gist.Owner.Login] = time.Now()
465+
f.timeouts[gist.Owner.Login] = time.Now().Add(time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute)
423466
fund = true
424467
}
425468
f.lock.Unlock()
426469

427470
// Send an error if too frequent funding, othewise a success
428471
if !fund {
429-
websocket.JSON.Send(conn, map[string]string{"error": fmt.Sprintf("User already funded %s ago", common.PrettyDuration(elapsed))})
472+
websocket.JSON.Send(conn, map[string]string{"error": fmt.Sprintf("%s left until next allowance", common.PrettyDuration(timeout.Sub(time.Now())))})
430473
continue
431474
}
432475
websocket.JSON.Send(conn, map[string]string{"success": fmt.Sprintf("Funding request accepted for %s into %s", gist.Owner.Login, address.Hex())})

cmd/faucet/faucet.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ <h1 style="text-align: center;"><i class="fa fa-bath" aria-hidden="true"></i> {{
5151
<div class="input-group">
5252
<input id="gist" type="text" class="form-control" placeholder="GitHub Gist URL containing your Ethereum address...">
5353
<span class="input-group-btn">
54-
<button class="btn btn-default" type="button" onclick="{{if .Recaptcha}}grecaptcha.execute(){{else}}submit(){{end}}">Give me Ether!</button>
54+
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Give me Ether <i class="fa fa-caret-down" aria-hidden="true"></i></button>
55+
<ul class="dropdown-menu dropdown-menu-right">{{range $idx, $amount := .Amounts}}
56+
<li><a style="text-align: center;" onclick="tier={{$idx}}; {{if $.Recaptcha}}grecaptcha.execute(){{else}}submit({{$idx}}){{end}}">{{$amount}} / {{index $.Periods $idx}}</a></li>{{end}}
57+
</ul>
5558
</span>
5659
</div>{{if .Recaptcha}}
5760
<div class="g-recaptcha" data-sitekey="{{.Recaptcha}}" data-callback="submit" data-size="invisible"></div>{{end}}
@@ -77,8 +80,9 @@ <h1 style="text-align: center;"><i class="fa fa-bath" aria-hidden="true"></i> {{
7780
<div class="row" style="margin-top: 32px;">
7881
<div class="col-lg-12">
7982
<h3>How does this work?</h3>
80-
<p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to GitHub accounts. Anyone having a GitHub account may request funds within the permitted limit of <strong>{{.Amount}} Ether(s) / {{.Period}}</strong>.{{if .Recaptcha}} The faucet is running invisible reCaptcha protection against bots.{{end}}</p>
83+
<p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to GitHub accounts. Anyone having a GitHub account may request funds within the permitted limits.</p>
8184
<p>To request funds, simply create a <a href="https://gist.github.com/" target="_about:blank">GitHub Gist</a> with your Ethereum address pasted into the contents (the file name doesn't matter), copy paste the gists URL into the above input box and fire away! You can track the current pending requests below the input field to see how much you have to wait until your turn comes.</p>
85+
{{if .Recaptcha}}<em>The faucet is running invisible reCaptcha protection against bots.</em>{{end}}
8286
</div>
8387
</div>
8488
</div>
@@ -88,10 +92,11 @@ <h3>How does this work?</h3>
8892
// Global variables to hold the current status of the faucet
8993
var attempt = 0;
9094
var server;
95+
var tier = 0;
9196

9297
// Define the function that submits a gist url to the server
9398
var submit = function({{if .Recaptcha}}captcha{{end}}) {
94-
server.send(JSON.stringify({url: $("#gist")[0].value{{if .Recaptcha}}, captcha: captcha{{end}}}));{{if .Recaptcha}}
99+
server.send(JSON.stringify({url: $("#gist")[0].value, tier: tier{{if .Recaptcha}}, captcha: captcha{{end}}}));{{if .Recaptcha}}
95100
grecaptcha.reset();{{end}}
96101
};
97102
// Define a method to reconnect upon server loss

0 commit comments

Comments
 (0)