// Copyright 2020 The go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // The go-ethereum library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . package utils import ( "math/rand" "testing" "github.com/ethereum/go-ethereum/p2p/enode" ) const ( ltTolerance = 0.03 ltRounds = 7 ) type ( ltNode struct { addr, id int value, exp float64 cost uint reqRate float64 reqMax, runCount int lastTotalCost uint served, dropped int } ltResult struct { node *ltNode ch chan struct{} } limTest struct { limiter *Limiter results chan ltResult runCount int expCost, totalCost uint } ) func (lt *limTest) request(n *ltNode) { var ( address string id enode.ID ) if n.addr >= 0 { address = string([]byte{byte(n.addr)}) } else { var b [32]byte rand.Read(b[:]) address = string(b[:]) } if n.id >= 0 { id = enode.ID{byte(n.id)} } else { rand.Read(id[:]) } lt.runCount++ n.runCount++ cch := lt.limiter.Add(id, address, n.value, n.cost) go func() { lt.results <- ltResult{n, <-cch} }() } func (lt *limTest) moreRequests(n *ltNode) { maxStart := int(float64(lt.totalCost-n.lastTotalCost) * n.reqRate) if maxStart != 0 { n.lastTotalCost = lt.totalCost } for n.reqMax > n.runCount && maxStart > 0 { lt.request(n) maxStart-- } } func (lt *limTest) process() { res := <-lt.results lt.runCount-- res.node.runCount-- if res.ch != nil { res.node.served++ if res.node.exp != 0 { lt.expCost += res.node.cost } lt.totalCost += res.node.cost close(res.ch) } else { res.node.dropped++ } } func TestLimiter(t *testing.T) { limTests := [][]*ltNode{ { // one id from an individual address and two ids from a shared address {addr: 0, id: 0, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.5}, {addr: 1, id: 1, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25}, {addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25}, }, { // varying request costs {addr: 0, id: 0, value: 0, cost: 10, reqRate: 0.2, reqMax: 1, exp: 0.5}, {addr: 1, id: 1, value: 0, cost: 3, reqRate: 0.5, reqMax: 1, exp: 0.25}, {addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25}, }, { // different request rate {addr: 0, id: 0, value: 0, cost: 1, reqRate: 2, reqMax: 2, exp: 0.5}, {addr: 1, id: 1, value: 0, cost: 1, reqRate: 10, reqMax: 10, exp: 0.25}, {addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25}, }, { // adding value {addr: 0, id: 0, value: 3, cost: 1, reqRate: 1, reqMax: 1, exp: (0.5 + 0.3) / 2}, {addr: 1, id: 1, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25 / 2}, {addr: 1, id: 2, value: 7, cost: 1, reqRate: 1, reqMax: 1, exp: (0.25 + 0.7) / 2}, }, { // DoS attack from a single address with a single id {addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 3, id: 3, value: 0, cost: 1, reqRate: 10, reqMax: 1000000000, exp: 0}, }, { // DoS attack from a single address with different ids {addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 3, id: -1, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0}, }, { // DDoS attack from different addresses with a single id {addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: -1, id: 3, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0}, }, { // DDoS attack from different addresses with different ids {addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, {addr: -1, id: -1, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0}, }, } lt := &limTest{ limiter: NewLimiter(100), results: make(chan ltResult), } for _, test := range limTests { lt.expCost, lt.totalCost = 0, 0 iterCount := 10000 for j := 0; j < ltRounds; j++ { // try to reach expected target range in multiple rounds with increasing iteration counts last := j == ltRounds-1 for _, n := range test { lt.request(n) } for i := 0; i < iterCount; i++ { lt.process() for _, n := range test { lt.moreRequests(n) } } for lt.runCount > 0 { lt.process() } if spamRatio := 1 - float64(lt.expCost)/float64(lt.totalCost); spamRatio > 0.5*(1+ltTolerance) { t.Errorf("Spam ratio too high (%f)", spamRatio) } fail, success := false, true for _, n := range test { if n.exp != 0 { if n.dropped > 0 { t.Errorf("Dropped %d requests of non-spam node", n.dropped) fail = true } r := float64(n.served) * float64(n.cost) / float64(lt.expCost) if r < n.exp*(1-ltTolerance) || r > n.exp*(1+ltTolerance) { if last { // print error only if the target is still not reached in the last round t.Errorf("Request ratio (%f) does not match expected value (%f)", r, n.exp) } success = false } } } if fail || success { break } // neither failed nor succeeded; try more iterations to reach probability targets iterCount *= 2 } } lt.limiter.Stop() }