Skip to content

Commit 1c2f6f5

Browse files
authored
Merge pull request #14402 from karalabe/tiered-faucet
cmd/faucet, cmd/puppeth: support multi-tiered faucet
2 parents e1dc7ec + 464f30d commit 1c2f6f5

File tree

5 files changed

+93
-30
lines changed

5 files changed

+93
-30
lines changed

cmd/faucet/faucet.go

Lines changed: 65 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,47 @@ 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+
}
117+
if period == 1 {
118+
periods[i] = strings.TrimSuffix(periods[i], "s")
119+
}
120+
}
92121
// Load up and render the faucet website
93122
tmpl, err := Asset("faucet.html")
94123
if err != nil {
95124
log.Crit("Failed to load the faucet template", "err", err)
96125
}
97-
period := fmt.Sprintf("%d minute(s)", *minutesFlag)
98-
if *minutesFlag%60 == 0 {
99-
period = fmt.Sprintf("%d hour(s)", *minutesFlag/60)
100-
}
101126
website := new(bytes.Buffer)
102-
template.Must(template.New("").Parse(string(tmpl))).Execute(website, map[string]interface{}{
127+
err = template.Must(template.New("").Parse(string(tmpl))).Execute(website, map[string]interface{}{
103128
"Network": *netnameFlag,
104-
"Amount": *payoutFlag,
105-
"Period": period,
129+
"Amounts": amounts,
130+
"Periods": periods,
106131
"Recaptcha": *captchaToken,
107132
})
133+
if err != nil {
134+
log.Crit("Failed to render the faucet template", "err", err)
135+
}
108136
// Load and parse the genesis block requested by the user
109137
blob, err := ioutil.ReadFile(*genesisFlag)
110138
if err != nil {
@@ -171,10 +199,10 @@ type faucet struct {
171199
nonce uint64 // Current pending nonce of the faucet
172200
price *big.Int // Current gas price to issue funds with
173201

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
202+
conns []*websocket.Conn // Currently live websocket connections
203+
timeouts map[string]time.Time // History of users and their funding timeouts
204+
reqs []*request // Currently pending funding requests
205+
update chan struct{} // Channel to signal request updates
178206

179207
lock sync.RWMutex // Lock protecting the faucet's internals
180208
}
@@ -241,7 +269,7 @@ func newFaucet(genesis *core.Genesis, port int, enodes []*discv5.Node, network u
241269
index: index,
242270
keystore: ks,
243271
account: ks.Accounts()[0],
244-
history: make(map[string]time.Time),
272+
timeouts: make(map[string]time.Time),
245273
update: make(chan struct{}, 1),
246274
}, nil
247275
}
@@ -295,14 +323,22 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
295323
"peers": f.stack.Server().PeerCount(),
296324
"requests": f.reqs,
297325
})
298-
header, _ := f.client.HeaderByNumber(context.Background(), nil)
299-
websocket.JSON.Send(conn, header)
326+
// Send the initial block to the client
327+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
328+
header, err := f.client.HeaderByNumber(ctx, nil)
329+
cancel()
300330

331+
if err != nil {
332+
log.Error("Failed to retrieve latest header", "err", err)
333+
} else {
334+
websocket.JSON.Send(conn, header)
335+
}
301336
// Keep reading requests from the websocket until the connection breaks
302337
for {
303338
// Fetch the next funding request and validate against github
304339
var msg struct {
305340
URL string `json:"url"`
341+
Tier uint `json:"tier"`
306342
Captcha string `json:"captcha"`
307343
}
308344
if err := websocket.JSON.Receive(conn, &msg); err != nil {
@@ -312,7 +348,11 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
312348
websocket.JSON.Send(conn, map[string]string{"error": "URL doesn't link to GitHub Gists"})
313349
continue
314350
}
315-
log.Info("Faucet funds requested", "gist", msg.URL)
351+
if msg.Tier >= uint(*tiersFlag) {
352+
websocket.JSON.Send(conn, map[string]string{"error": "Invalid funding tier requested"})
353+
continue
354+
}
355+
log.Info("Faucet funds requested", "gist", msg.URL, "tier", msg.Tier)
316356

317357
// If captcha verifications are enabled, make sure we're not dealing with a robot
318358
if *captchaToken != "" {
@@ -337,7 +377,7 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
337377
}
338378
if !result.Success {
339379
log.Warn("Captcha verification failed", "err", string(result.Errors))
340-
websocket.JSON.Send(conn, map[string]string{"error": "Beep-boop, you're a robot!"})
380+
websocket.JSON.Send(conn, map[string]string{"error": "Beep-bop, you're a robot!"})
341381
continue
342382
}
343383
}
@@ -396,11 +436,15 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
396436
f.lock.Lock()
397437
var (
398438
fund bool
399-
elapsed time.Duration
439+
timeout time.Time
400440
)
401-
if elapsed = time.Since(f.history[gist.Owner.Login]); elapsed > time.Duration(*minutesFlag)*time.Minute {
441+
if timeout = f.timeouts[gist.Owner.Login]; time.Now().After(timeout) {
402442
// 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)
443+
amount := new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether)
444+
amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil))
445+
amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil))
446+
447+
tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, big.NewInt(21000), f.price, nil)
404448
signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainId)
405449
if err != nil {
406450
websocket.JSON.Send(conn, map[string]string{"error": err.Error()})
@@ -419,14 +463,14 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
419463
Time: time.Now(),
420464
Tx: signed,
421465
})
422-
f.history[gist.Owner.Login] = time.Now()
466+
f.timeouts[gist.Owner.Login] = time.Now().Add(time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute)
423467
fund = true
424468
}
425469
f.lock.Unlock()
426470

427471
// Send an error if too frequent funding, othewise a success
428472
if !fund {
429-
websocket.JSON.Send(conn, map[string]string{"error": fmt.Sprintf("User already funded %s ago", common.PrettyDuration(elapsed))})
473+
websocket.JSON.Send(conn, map[string]string{"error": fmt.Sprintf("%s left until next allowance", common.PrettyDuration(timeout.Sub(time.Now())))})
430474
continue
431475
}
432476
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)