* les/utils: Limiter * les/utils: dropped prior weight vs variable cost logic, using fixed weights * les/utils: always create node selector in addressGroup * les/utils: renamed request weight to request cost * les/utils: simplified and improved the DoS penalty mechanism * les/utils: minor fixes * les/utils: made selection weight calculation nicer * les/utils: fixed linter warning * les/utils: more precise and reliable probabilistic test * les/utils: fixed linter warning
		
			
				
	
	
		
			207 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			207 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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 <http://www.gnu.org/licenses/>.
 | 
						|
 | 
						|
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()
 | 
						|
}
 |