github.com/hardtosaygoodbye/go-ethereum@v1.10.16-0.20220122011429-97003b9e6c15/les/utils/limiter_test.go (about)

     1  // Copyright 2020 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package utils
    18  
    19  import (
    20  	"math/rand"
    21  	"testing"
    22  
    23  	"github.com/hardtosaygoodbye/go-ethereum/p2p/enode"
    24  )
    25  
    26  const (
    27  	ltTolerance = 0.03
    28  	ltRounds    = 7
    29  )
    30  
    31  type (
    32  	ltNode struct {
    33  		addr, id         int
    34  		value, exp       float64
    35  		cost             uint
    36  		reqRate          float64
    37  		reqMax, runCount int
    38  		lastTotalCost    uint
    39  
    40  		served, dropped int
    41  	}
    42  
    43  	ltResult struct {
    44  		node *ltNode
    45  		ch   chan struct{}
    46  	}
    47  
    48  	limTest struct {
    49  		limiter            *Limiter
    50  		results            chan ltResult
    51  		runCount           int
    52  		expCost, totalCost uint
    53  	}
    54  )
    55  
    56  func (lt *limTest) request(n *ltNode) {
    57  	var (
    58  		address string
    59  		id      enode.ID
    60  	)
    61  	if n.addr >= 0 {
    62  		address = string([]byte{byte(n.addr)})
    63  	} else {
    64  		var b [32]byte
    65  		rand.Read(b[:])
    66  		address = string(b[:])
    67  	}
    68  	if n.id >= 0 {
    69  		id = enode.ID{byte(n.id)}
    70  	} else {
    71  		rand.Read(id[:])
    72  	}
    73  	lt.runCount++
    74  	n.runCount++
    75  	cch := lt.limiter.Add(id, address, n.value, n.cost)
    76  	go func() {
    77  		lt.results <- ltResult{n, <-cch}
    78  	}()
    79  }
    80  
    81  func (lt *limTest) moreRequests(n *ltNode) {
    82  	maxStart := int(float64(lt.totalCost-n.lastTotalCost) * n.reqRate)
    83  	if maxStart != 0 {
    84  		n.lastTotalCost = lt.totalCost
    85  	}
    86  	for n.reqMax > n.runCount && maxStart > 0 {
    87  		lt.request(n)
    88  		maxStart--
    89  	}
    90  }
    91  
    92  func (lt *limTest) process() {
    93  	res := <-lt.results
    94  	lt.runCount--
    95  	res.node.runCount--
    96  	if res.ch != nil {
    97  		res.node.served++
    98  		if res.node.exp != 0 {
    99  			lt.expCost += res.node.cost
   100  		}
   101  		lt.totalCost += res.node.cost
   102  		close(res.ch)
   103  	} else {
   104  		res.node.dropped++
   105  	}
   106  }
   107  
   108  func TestLimiter(t *testing.T) {
   109  	limTests := [][]*ltNode{
   110  		{ // one id from an individual address and two ids from a shared address
   111  			{addr: 0, id: 0, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.5},
   112  			{addr: 1, id: 1, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25},
   113  			{addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25},
   114  		},
   115  		{ // varying request costs
   116  			{addr: 0, id: 0, value: 0, cost: 10, reqRate: 0.2, reqMax: 1, exp: 0.5},
   117  			{addr: 1, id: 1, value: 0, cost: 3, reqRate: 0.5, reqMax: 1, exp: 0.25},
   118  			{addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25},
   119  		},
   120  		{ // different request rate
   121  			{addr: 0, id: 0, value: 0, cost: 1, reqRate: 2, reqMax: 2, exp: 0.5},
   122  			{addr: 1, id: 1, value: 0, cost: 1, reqRate: 10, reqMax: 10, exp: 0.25},
   123  			{addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25},
   124  		},
   125  		{ // adding value
   126  			{addr: 0, id: 0, value: 3, cost: 1, reqRate: 1, reqMax: 1, exp: (0.5 + 0.3) / 2},
   127  			{addr: 1, id: 1, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25 / 2},
   128  			{addr: 1, id: 2, value: 7, cost: 1, reqRate: 1, reqMax: 1, exp: (0.25 + 0.7) / 2},
   129  		},
   130  		{ // DoS attack from a single address with a single id
   131  			{addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   132  			{addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   133  			{addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   134  			{addr: 3, id: 3, value: 0, cost: 1, reqRate: 10, reqMax: 1000000000, exp: 0},
   135  		},
   136  		{ // DoS attack from a single address with different ids
   137  			{addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   138  			{addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   139  			{addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   140  			{addr: 3, id: -1, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0},
   141  		},
   142  		{ // DDoS attack from different addresses with a single id
   143  			{addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   144  			{addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   145  			{addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   146  			{addr: -1, id: 3, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0},
   147  		},
   148  		{ // DDoS attack from different addresses with different ids
   149  			{addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   150  			{addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   151  			{addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333},
   152  			{addr: -1, id: -1, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0},
   153  		},
   154  	}
   155  
   156  	lt := &limTest{
   157  		limiter: NewLimiter(100),
   158  		results: make(chan ltResult),
   159  	}
   160  	for _, test := range limTests {
   161  		lt.expCost, lt.totalCost = 0, 0
   162  		iterCount := 10000
   163  		for j := 0; j < ltRounds; j++ {
   164  			// try to reach expected target range in multiple rounds with increasing iteration counts
   165  			last := j == ltRounds-1
   166  			for _, n := range test {
   167  				lt.request(n)
   168  			}
   169  			for i := 0; i < iterCount; i++ {
   170  				lt.process()
   171  				for _, n := range test {
   172  					lt.moreRequests(n)
   173  				}
   174  			}
   175  			for lt.runCount > 0 {
   176  				lt.process()
   177  			}
   178  			if spamRatio := 1 - float64(lt.expCost)/float64(lt.totalCost); spamRatio > 0.5*(1+ltTolerance) {
   179  				t.Errorf("Spam ratio too high (%f)", spamRatio)
   180  			}
   181  			fail, success := false, true
   182  			for _, n := range test {
   183  				if n.exp != 0 {
   184  					if n.dropped > 0 {
   185  						t.Errorf("Dropped %d requests of non-spam node", n.dropped)
   186  						fail = true
   187  					}
   188  					r := float64(n.served) * float64(n.cost) / float64(lt.expCost)
   189  					if r < n.exp*(1-ltTolerance) || r > n.exp*(1+ltTolerance) {
   190  						if last {
   191  							// print error only if the target is still not reached in the last round
   192  							t.Errorf("Request ratio (%f) does not match expected value (%f)", r, n.exp)
   193  						}
   194  						success = false
   195  					}
   196  				}
   197  			}
   198  			if fail || success {
   199  				break
   200  			}
   201  			// neither failed nor succeeded; try more iterations to reach probability targets
   202  			iterCount *= 2
   203  		}
   204  	}
   205  	lt.limiter.Stop()
   206  }