diff --git a/peer/addrbook.go b/peer/addrbook.go index f3b2db185..5e6d0f68e 100644 --- a/peer/addrbook.go +++ b/peer/addrbook.go @@ -9,7 +9,6 @@ import ( "encoding/binary" "encoding/json" "fmt" - . "github.com/tendermint/tendermint/binary" "io" "math" "math/rand" @@ -18,24 +17,26 @@ import ( "sync" "sync/atomic" "time" + + . "github.com/tendermint/tendermint/binary" ) /* AddrBook - concurrency safe peer address manager */ type AddrBook struct { filePath string - mtx sync.Mutex - rand *rand.Rand - key [32]byte - addrIndex map[string]*KnownAddress // addr.String() -> KnownAddress - addrNew [newBucketCount]map[string]*KnownAddress - addrOld [oldBucketCount][]*KnownAddress - started int32 - shutdown int32 - wg sync.WaitGroup - quit chan struct{} - nOld int - nNew int + mtx sync.Mutex + rand *rand.Rand + key [32]byte + addrNewIndex map[string]*knownAddress // addr.String() -> knownAddress + addrNew [newBucketCount]map[string]*knownAddress + addrOld [oldBucketCount][]*knownAddress + started int32 + shutdown int32 + wg sync.WaitGroup + quit chan struct{} + nOld int + nNew int } const ( @@ -87,7 +88,7 @@ const ( getAddrPercent = 23 // current version of the on-disk format. - serialisationVersion = 1 + serializationVersion = 1 ) // Use Start to begin processing asynchronous address updates. @@ -103,13 +104,13 @@ func NewAddrBook(filePath string) *AddrBook { // When modifying this, don't forget to update loadFromFile() func (a *AddrBook) init() { - a.addrIndex = make(map[string]*KnownAddress) + a.addrNewIndex = make(map[string]*knownAddress) io.ReadFull(crand.Reader, a.key[:]) for i := range a.addrNew { - a.addrNew[i] = make(map[string]*KnownAddress) + a.addrNew[i] = make(map[string]*knownAddress) } for i := range a.addrOld { - a.addrOld[i] = make([]*KnownAddress, 0, oldBucketSize) + a.addrOld[i] = make([]*knownAddress, 0, oldBucketSize) } } @@ -139,17 +140,17 @@ func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) { } func (a *AddrBook) NeedMoreAddresses() bool { - return a.NumAddresses() < needAddressThreshold + return a.Size() < needAddressThreshold } -func (a *AddrBook) NumAddresses() int { +func (a *AddrBook) Size() int { a.mtx.Lock() defer a.mtx.Unlock() return a.nOld + a.nNew } // Pick a new address to connect to. -func (a *AddrBook) PickAddress(class string, newBias int) *KnownAddress { +func (a *AddrBook) PickAddress(class string, newBias int) *knownAddress { a.mtx.Lock() defer a.mtx.Unlock() @@ -169,7 +170,7 @@ func (a *AddrBook) PickAddress(class string, newBias int) *KnownAddress { if (newCorrelation+oldCorrelation)*a.rand.Float64() < oldCorrelation { // pick random Old bucket. - var bucket []*KnownAddress = nil + var bucket []*knownAddress = nil for len(bucket) == 0 { bucket = a.addrOld[a.rand.Intn(len(a.addrOld))] } @@ -177,7 +178,7 @@ func (a *AddrBook) PickAddress(class string, newBias int) *KnownAddress { return bucket[a.rand.Intn(len(bucket))] } else { // pick random New bucket. - var bucket map[string]*KnownAddress = nil + var bucket map[string]*knownAddress = nil for len(bucket) == 0 { bucket = a.addrNew[a.rand.Intn(len(a.addrNew))] } @@ -197,11 +198,11 @@ func (a *AddrBook) PickAddress(class string, newBias int) *KnownAddress { func (a *AddrBook) MarkGood(addr *NetAddress) { a.mtx.Lock() defer a.mtx.Unlock() - ka := a.addrIndex[addr.String()] + ka := a.addrNewIndex[addr.String()] if ka == nil { return } - ka.MarkAttempt(true) + ka.MarkGood() if ka.OldBucket == -1 { a.moveToOld(ka) } @@ -210,30 +211,40 @@ func (a *AddrBook) MarkGood(addr *NetAddress) { func (a *AddrBook) MarkAttempt(addr *NetAddress) { a.mtx.Lock() defer a.mtx.Unlock() - ka := a.addrIndex[addr.String()] + ka := a.addrNewIndex[addr.String()] if ka == nil { return } - ka.MarkAttempt(false) + ka.MarkAttempt() } /* Loading & Saving */ type addrBookJSON struct { Key [32]byte - AddrNew [newBucketCount]map[string]*KnownAddress - AddrOld [oldBucketCount][]*KnownAddress - NOld int - NNew int + AddrNew [newBucketCount][]*knownAddress + AddrOld [oldBucketCount][]*knownAddress + NumOld int + NumNew int } func (a *AddrBook) saveToFile(filePath string) { + // turn a.addrNew into an array like a.addrOld + __addrNew := [newBucketCount][]*knownAddress{} + for i, newBucket := range a.addrNew { + var array []*knownAddress = make([]*knownAddress, 0) + for _, ka := range newBucket { + array = append(array, ka) + } + __addrNew[i] = array + } + aJSON := &addrBookJSON{ Key: a.key, - AddrNew: a.addrNew, + AddrNew: __addrNew, AddrOld: a.addrOld, - NOld: a.nOld, - NNew: a.nNew, + NumOld: a.nOld, + NumNew: a.nNew, } w, err := os.Create(filePath) @@ -271,20 +282,28 @@ func (a *AddrBook) loadFromFile(filePath string) { panic(fmt.Errorf("error reading %s: %v", filePath, err)) } - // Now we need to initialize self. + // Now we need to restore the fields + // Restore the key copy(a.key[:], aJSON.Key[:]) - a.addrNew = aJSON.AddrNew + // Restore .addrNew + for i, newBucket := range aJSON.AddrNew { + for _, ka := range newBucket { + a.addrNew[i][ka.Addr.String()] = ka + } + } + // Restore .addrOld for i, oldBucket := range aJSON.AddrOld { copy(a.addrOld[i], oldBucket) } - a.nNew = aJSON.NNew - a.nOld = aJSON.NOld - - a.addrIndex = make(map[string]*KnownAddress) + // Restore simple fields + a.nNew = aJSON.NumNew + a.nOld = aJSON.NumOld + // Restore addrNewIndex + a.addrNewIndex = make(map[string]*knownAddress) for _, newBucket := range a.addrNew { for key, ka := range newBucket { - a.addrIndex[key] = ka + a.addrNewIndex[key] = ka } } } @@ -314,7 +333,7 @@ func (a *AddrBook) addAddress(addr, src *NetAddress) { } key := addr.String() - ka := a.addrIndex[key] + ka := a.addrNewIndex[key] if ka != nil { // Already added @@ -331,8 +350,8 @@ func (a *AddrBook) addAddress(addr, src *NetAddress) { return } } else { - ka = NewKnownAddress(addr, src) - a.addrIndex[key] = ka + ka = NewknownAddress(addr, src) + a.addrNewIndex[key] = ka a.nNew++ } @@ -359,16 +378,16 @@ func (a *AddrBook) addAddress(addr, src *NetAddress) { // Make space in the new buckets by expiring the really bad entries. // If no bad entries are available we look at a few and remove the oldest. func (a *AddrBook) expireNew(bucket int) { - var oldest *KnownAddress + var oldest *knownAddress for k, v := range a.addrNew[bucket] { // If an entry is bad, throw it away - if v.Bad() { + if v.IsBad() { log.Tracef("expiring bad address %v", k) delete(a.addrNew[bucket], k) v.NewRefs-- if v.NewRefs == 0 { a.nNew-- - delete(a.addrIndex, k) + delete(a.addrNewIndex, k) } return } @@ -388,12 +407,12 @@ func (a *AddrBook) expireNew(bucket int) { oldest.NewRefs-- if oldest.NewRefs == 0 { a.nNew-- - delete(a.addrIndex, key) + delete(a.addrNewIndex, key) } } } -func (a *AddrBook) moveToOld(ka *KnownAddress) { +func (a *AddrBook) moveToOld(ka *knownAddress) { // Remove from all new buckets. // Remember one of those new buckets. addrKey := ka.Addr.String() @@ -448,7 +467,7 @@ func (a *AddrBook) moveToOld(ka *KnownAddress) { // Returns the index in old bucket of oldest entry. func (a *AddrBook) pickOld(bucket int) int { - var oldest *KnownAddress + var oldest *knownAddress var oldestIndex int for i, ka := range a.addrOld[bucket] { if oldest == nil || ka.LastAttempt.Before(oldest.LastAttempt.Time) { @@ -547,3 +566,102 @@ func GroupKey(na *NetAddress) string { return (&net.IPNet{IP: na.IP, Mask: net.CIDRMask(bits, 128)}).String() } + +/* + knownAddress + + tracks information about a known network address that is used + to determine how viable an address is. +*/ +type knownAddress struct { + Addr *NetAddress + Src *NetAddress + Attempts UInt32 + LastAttempt Time + LastSuccess Time + NewRefs UInt16 + OldBucket Int16 +} + +func NewknownAddress(addr *NetAddress, src *NetAddress) *knownAddress { + return &knownAddress{ + Addr: addr, + Src: src, + OldBucket: -1, + LastAttempt: Time{time.Now()}, + Attempts: 0, + } +} + +func ReadknownAddress(r io.Reader) *knownAddress { + return &knownAddress{ + Addr: ReadNetAddress(r), + Src: ReadNetAddress(r), + Attempts: ReadUInt32(r), + LastAttempt: ReadTime(r), + LastSuccess: ReadTime(r), + NewRefs: ReadUInt16(r), + OldBucket: ReadInt16(r), + } +} + +func (ka *knownAddress) WriteTo(w io.Writer) (n int64, err error) { + n, err = WriteOnto(ka.Addr, w, n, err) + n, err = WriteOnto(ka.Src, w, n, err) + n, err = WriteOnto(ka.Attempts, w, n, err) + n, err = WriteOnto(ka.LastAttempt, w, n, err) + n, err = WriteOnto(ka.LastSuccess, w, n, err) + n, err = WriteOnto(ka.NewRefs, w, n, err) + n, err = WriteOnto(ka.OldBucket, w, n, err) + return +} + +func (ka *knownAddress) MarkAttempt() { + now := Time{time.Now()} + ka.LastAttempt = now + ka.Attempts += 1 +} + +func (ka *knownAddress) MarkGood() { + now := Time{time.Now()} + ka.LastAttempt = now + ka.Attempts = 0 + ka.LastSuccess = now +} + +/* + An address is bad if the address in question has not been tried in the last + minute and meets one of the following criteria: + + 1) It claims to be from the future + 2) It hasn't been seen in over a month + 3) It has failed at least three times and never succeeded + 4) It has failed ten times in the last week + + All addresses that meet these criteria are assumed to be worthless and not + worth keeping hold of. +*/ +func (ka *knownAddress) IsBad() bool { + // Has been attempted in the last minute --> good + if ka.LastAttempt.Before(time.Now().Add(-1 * time.Minute)) { + return false + } + + // Over a month old? + if ka.LastAttempt.After(time.Now().Add(-1 * numMissingDays * time.Hour * 24)) { + return true + } + + // Never succeeded? + if ka.LastSuccess.IsZero() && ka.Attempts >= numRetries { + return true + } + + // Hasn't succeeded in too long? + if ka.LastSuccess.Before(time.Now().Add(-1*minBadDays*time.Hour*24)) && + ka.Attempts >= maxFailures { + return true + } + + return false +} diff --git a/peer/addrbook_test.go b/peer/addrbook_test.go new file mode 100644 index 000000000..d03d53366 --- /dev/null +++ b/peer/addrbook_test.go @@ -0,0 +1,159 @@ +package peer + +import ( + "fmt" + "io/ioutil" + "math/rand" + "testing" +) + +func createTempFileName(prefix string) string { + f, err := ioutil.TempFile("", prefix) + if err != nil { + panic(err) + } + fname := f.Name() + err = f.Close() + if err != nil { + panic(err) + } + return fname +} + +func TestEmpty(t *testing.T) { + fname := createTempFileName("addrbook_test") + // t.Logf("New tempfile name: %v", fname) + + // Save an empty book & load it + book := NewAddrBook(fname) + book.saveToFile(fname) + + book = NewAddrBook(fname) + book.loadFromFile(fname) + + if book.Size() != 0 { + t.Errorf("Expected 0 addresses, found %v", book.Size()) + } +} + +func randIPv4Address() *NetAddress { + for { + ip := fmt.Sprintf("%v.%v.%v.%v", + rand.Intn(254)+1, + rand.Intn(255), + rand.Intn(255), + rand.Intn(255), + ) + port := rand.Intn(65535-1) + 1 + addr := NewNetAddressString(fmt.Sprintf("%v:%v", ip, port)) + if addr.Routable() { + return addr + } + } +} + +func TestSaveAddresses(t *testing.T) { + fname := createTempFileName("addrbook_test") + //t.Logf("New tempfile name: %v", fname) + + // Create some random addresses + randAddrs := []struct { + addr *NetAddress + src *NetAddress + }{} + for i := 0; i < 100; i++ { + addr := randIPv4Address() + src := randIPv4Address() + randAddrs = append(randAddrs, struct { + addr *NetAddress + src *NetAddress + }{ + addr: addr, + src: src, + }) + } + + // Create the book & populate & save + book := NewAddrBook(fname) + for _, addrSrc := range randAddrs { + book.AddAddress(addrSrc.addr, addrSrc.src) + } + if book.Size() != 100 { + t.Errorf("Expected 100 addresses, found %v", book.Size()) + } + book.saveToFile(fname) + + // Reload the book + book = NewAddrBook(fname) + book.loadFromFile(fname) + + // Test ... + + if book.Size() != 100 { + t.Errorf("Expected 100 addresses, found %v", book.Size()) + } + + for _, addrSrc := range randAddrs { + addr := addrSrc.addr + src := addrSrc.src + ka := book.addrNewIndex[addr.String()] + if ka == nil { + t.Fatalf("Expected to find KnownAddress %v but wasn't there.", addr) + } + if !(ka.Addr.Equals(addr) && ka.Src.Equals(src)) { + t.Fatalf("KnownAddress doesn't match addr & src") + } + } +} + +func TestPromoteToOld(t *testing.T) { + fname := createTempFileName("addrbook_test") + t.Logf("New tempfile name: %v", fname) + + // Create some random addresses + randAddrs := []struct { + addr *NetAddress + src *NetAddress + }{} + for i := 0; i < 100; i++ { + addr := randIPv4Address() + src := randIPv4Address() + randAddrs = append(randAddrs, struct { + addr *NetAddress + src *NetAddress + }{ + addr: addr, + src: src, + }) + } + + // Create the book & populate & save + book := NewAddrBook(fname) + for _, addrSrc := range randAddrs { + book.AddAddress(addrSrc.addr, addrSrc.src) + } + // Attempt all addresses. + for _, addrSrc := range randAddrs { + book.MarkAttempt(addrSrc.addr) + } + // Promote half of them + for i, addrSrc := range randAddrs { + if i%2 == 0 { + book.MarkGood(addrSrc.addr) + } + } + book.saveToFile(fname) + + // Reload the book + book = NewAddrBook(fname) + book.loadFromFile(fname) + + // Test ... + + if book.Size() != 100 { + t.Errorf("Expected 100 addresses, found %v", book.Size()) + } + + // TODO: do more testing :) + +} diff --git a/peer/knownaddress.go b/peer/knownaddress.go deleted file mode 100644 index 0db057fe3..000000000 --- a/peer/knownaddress.go +++ /dev/null @@ -1,105 +0,0 @@ -package peer - -import ( - "io" - "time" - - . "github.com/tendermint/tendermint/binary" -) - -/* - KnownAddress - - tracks information about a known network address that is used - to determine how viable an address is. -*/ -type KnownAddress struct { - Addr *NetAddress - Src *NetAddress - Attempts UInt32 - LastAttempt Time - LastSuccess Time - NewRefs UInt16 - OldBucket Int16 -} - -func NewKnownAddress(addr *NetAddress, src *NetAddress) *KnownAddress { - return &KnownAddress{ - Addr: addr, - Src: src, - OldBucket: -1, - LastAttempt: Time{time.Now()}, - Attempts: 0, - } -} - -func ReadKnownAddress(r io.Reader) *KnownAddress { - return &KnownAddress{ - Addr: ReadNetAddress(r), - Src: ReadNetAddress(r), - Attempts: ReadUInt32(r), - LastAttempt: ReadTime(r), - LastSuccess: ReadTime(r), - NewRefs: ReadUInt16(r), - OldBucket: ReadInt16(r), - } -} - -func (ka *KnownAddress) WriteTo(w io.Writer) (n int64, err error) { - n, err = WriteOnto(ka.Addr, w, n, err) - n, err = WriteOnto(ka.Src, w, n, err) - n, err = WriteOnto(ka.Attempts, w, n, err) - n, err = WriteOnto(ka.LastAttempt, w, n, err) - n, err = WriteOnto(ka.LastSuccess, w, n, err) - n, err = WriteOnto(ka.NewRefs, w, n, err) - n, err = WriteOnto(ka.OldBucket, w, n, err) - return -} - -func (ka *KnownAddress) MarkAttempt(success bool) { - now := Time{time.Now()} - ka.LastAttempt = now - if success { - ka.LastSuccess = now - ka.Attempts = 0 - } else { - ka.Attempts += 1 - } -} - -/* - An address is bad if the address in question has not been tried in the last - minute and meets one of the following criteria: - - 1) It claims to be from the future - 2) It hasn't been seen in over a month - 3) It has failed at least three times and never succeeded - 4) It has failed ten times in the last week - - All addresses that meet these criteria are assumed to be worthless and not - worth keeping hold of. -*/ -func (ka *KnownAddress) Bad() bool { - // Has been attempted in the last minute --> good - if ka.LastAttempt.Before(time.Now().Add(-1 * time.Minute)) { - return false - } - - // Over a month old? - if ka.LastAttempt.After(time.Now().Add(-1 * numMissingDays * time.Hour * 24)) { - return true - } - - // Never succeeded? - if ka.LastSuccess.IsZero() && ka.Attempts >= numRetries { - return true - } - - // Hasn't succeeded in too long? - if ka.LastSuccess.Before(time.Now().Add(-1*minBadDays*time.Hour*24)) && - ka.Attempts >= maxFailures { - return true - } - - return false -}