From 34f5d439eea373da141426f41ee1f32cbffb8549 Mon Sep 17 00:00:00 2001 From: Eugene Chung Date: Wed, 28 Mar 2018 12:58:53 +0900 Subject: [PATCH 01/59] remove Heap.Update() call when setting Proposer field In for loop of IncrementAccum(), Heap.Update() call is unnecessary when i == times - 1. --- types/validator_set.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/types/validator_set.go b/types/validator_set.go index 83d066ec1..3885369d3 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -60,13 +60,14 @@ func (valSet *ValidatorSet) IncrementAccum(times int) { // Decrement the validator with most accum times times for i := 0; i < times; i++ { mostest := validatorsHeap.Peek().(*Validator) - if i == times-1 { - valSet.Proposer = mostest - } - // mind underflow mostest.Accum = safeSubClip(mostest.Accum, valSet.TotalVotingPower()) - validatorsHeap.Update(mostest, accumComparable{mostest}) + + if i == times-1 { + valSet.Proposer = mostest + } else { + validatorsHeap.Update(mostest, accumComparable{mostest}) + } } } From 1fe41be9294662d08641ae2f7048c689a9f800cb Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Sat, 28 Apr 2018 16:45:08 -0400 Subject: [PATCH 02/59] p2p: prevent connections from same ip --- p2p/errors.go | 28 +++++++++++++++++++++++----- p2p/peer.go | 10 ++++++++++ p2p/peer_set.go | 29 +++++++++++++++++++++++------ p2p/switch.go | 24 +++++++++++++++--------- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/p2p/errors.go b/p2p/errors.go index f4a09e6c0..314507785 100644 --- a/p2p/errors.go +++ b/p2p/errors.go @@ -1,14 +1,32 @@ package p2p import ( - "errors" "fmt" ) -var ( - ErrSwitchDuplicatePeer = errors.New("Duplicate peer") - ErrSwitchConnectToSelf = errors.New("Connect to self") -) +type ErrSwitchDuplicatePeerID struct { + ID ID +} + +func (e ErrSwitchDuplicatePeerID) Error() string { + return fmt.Errorf("Duplicate peer ID %v", e.ID) +} + +type ErrSwitchDuplicatePeerIP struct { + Addr string +} + +func (e ErrSwitchDuplicatePeerIP) Error() string { + return fmt.Errorf("Duplicate peer IP %v", e.Addr) +} + +type ErrSwitchConnectToSelf struct { + Addr *NetAddress +} + +func (e ErrSwitchConnectToSelf) Error() string { + return fmt.Errorf("Connect to self: %v", e.Addr) +} type ErrSwitchAuthenticationFailure struct { Dialed *NetAddress diff --git a/p2p/peer.go b/p2p/peer.go index b9c8f8b41..1b5ec0ae4 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -17,6 +17,7 @@ type Peer interface { cmn.Service ID() ID // peer's cryptographic ID + RemoteIP() string // remote IP of the connection IsOutbound() bool // did we dial the peer IsPersistent() bool // do we redial this peer when we disconnect NodeInfo() NodeInfo // peer's info @@ -45,6 +46,15 @@ func (pc peerConn) ID() ID { return PubKeyToID(pc.conn.(*tmconn.SecretConnection).RemotePubKey()) } +// Return the IP from the connection RemoteAddr +func (pc peerConn) RemoteIP() string { + host, _, err := net.SplitHostPort(pc.conn.RemoteAddr().String()) + if err != nil { + panic(err) + } + return host +} + // peer implements Peer. // // Before using a peer, you will need to perform a handshake on connection. diff --git a/p2p/peer_set.go b/p2p/peer_set.go index a4565ea1d..3ac48ea5e 100644 --- a/p2p/peer_set.go +++ b/p2p/peer_set.go @@ -7,6 +7,7 @@ import ( // IPeerSet has a (immutable) subset of the methods of PeerSet. type IPeerSet interface { Has(key ID) bool + HasIP(ip string) bool Get(key ID) Peer List() []Peer Size() int @@ -17,9 +18,10 @@ type IPeerSet interface { // PeerSet is a special structure for keeping a table of peers. // Iteration over the peers is super fast and thread-safe. type PeerSet struct { - mtx sync.Mutex - lookup map[ID]*peerSetItem - list []Peer + mtx sync.Mutex + lookup map[ID]*peerSetItem + lookupIP map[string]struct{} + list []Peer } type peerSetItem struct { @@ -30,8 +32,9 @@ type peerSetItem struct { // NewPeerSet creates a new peerSet with a list of initial capacity of 256 items. func NewPeerSet() *PeerSet { return &PeerSet{ - lookup: make(map[ID]*peerSetItem), - list: make([]Peer, 0, 256), + lookup: make(map[ID]*peerSetItem), + lookupIP: make(map[string]struct{}), + list: make([]Peer, 0, 256), } } @@ -41,7 +44,10 @@ func (ps *PeerSet) Add(peer Peer) error { ps.mtx.Lock() defer ps.mtx.Unlock() if ps.lookup[peer.ID()] != nil { - return ErrSwitchDuplicatePeer + return ErrSwitchDuplicatePeerID{peer.ID()} + } + if _, ok := ps.lookupIP[peer.RemoteIP()]; ok { + return ErrSwitchDuplicatePeerIP{peer.RemoteIP()} } index := len(ps.list) @@ -49,6 +55,7 @@ func (ps *PeerSet) Add(peer Peer) error { // iterating over the ps.list slice. ps.list = append(ps.list, peer) ps.lookup[peer.ID()] = &peerSetItem{peer, index} + ps.lookupIP[peer.RemoteIP()] = struct{}{} return nil } @@ -61,6 +68,15 @@ func (ps *PeerSet) Has(peerKey ID) bool { return ok } +// HasIP returns true iff the PeerSet contains +// the peer referred to by this IP address. +func (ps *PeerSet) HasIP(peerIP string) bool { + ps.mtx.Lock() + _, ok := ps.lookupIP[peerIP] + ps.mtx.Unlock() + return ok +} + // Get looks up a peer by the provided peerKey. func (ps *PeerSet) Get(peerKey ID) Peer { ps.mtx.Lock() @@ -76,6 +92,7 @@ func (ps *PeerSet) Get(peerKey ID) Peer { func (ps *PeerSet) Remove(peer Peer) { ps.mtx.Lock() defer ps.mtx.Unlock() + delete(ps.lookupIP[peer.RemoteIP()]) item := ps.lookup[peer.ID()] if item == nil { return diff --git a/p2p/switch.go b/p2p/switch.go index f62e5f992..61c9ce969 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -403,7 +403,7 @@ func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent b sw.randomSleep(0) err := sw.DialPeerWithAddress(addr, persistent) if err != nil { - switch err { + switch err.(type) { case ErrSwitchConnectToSelf, ErrSwitchDuplicatePeer: sw.Logger.Debug("Error dialing peer", "err", err) default: @@ -534,6 +534,8 @@ func (sw *Switch) addPeer(pc peerConn) error { return err } + // dont connect to multiple peers on the same IP + // NOTE: if AuthEnc==false, we don't have a peerID until after the handshake. // If AuthEnc==true then we already know the ID and could do the checks first before the handshake, // but it's simple to just deal with both cases the same after the handshake. @@ -564,20 +566,24 @@ func (sw *Switch) addPeer(pc peerConn) error { // Avoid self if sw.nodeKey.ID() == peerID { addr := peerNodeInfo.NetAddress() - - // remove the given address from the address book if we added it earlier + // remove the given address from the address book + // and add to our addresses to avoid dialing again sw.addrBook.RemoveAddress(addr) - - // add the given address to the address book to avoid dialing ourselves - // again this is our public address sw.addrBook.AddOurAddress(addr) - - return ErrSwitchConnectToSelf + return ErrSwitchConnectToSelf{addr} } // Avoid duplicate if sw.peers.Has(peerID) { - return ErrSwitchDuplicatePeer + return ErrSwitchDuplicatePeerID{peerID} + } + + // check ips for both the connection addr and the self reported addr + if sw.peers.HasIP(addr) { + return ErrSwitchDuplicatePeerIP{addr} + } + if sw.peers.HasIP(peerNodeInfo.ListenAddr) { + return ErrSwitchDuplicatePeerIP{peerNodeInfo.ListenAddr} } // Filter peer against ID white list From 77f09f5b5e898694b16ae9744466e843a5b77df4 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Mon, 7 May 2018 17:15:12 +0200 Subject: [PATCH 03/59] Move to ne.IP --- p2p/dummy/peer.go | 8 ++++++++ p2p/errors.go | 20 ++++++++++++++----- p2p/peer.go | 19 +++++++++++++++--- p2p/peer_set.go | 39 +++++++++++++++++++++---------------- p2p/peer_set_test.go | 14 +++++++++---- p2p/pex/pex_reactor_test.go | 2 ++ p2p/switch.go | 9 +++------ p2p/switch_test.go | 2 +- 8 files changed, 77 insertions(+), 36 deletions(-) diff --git a/p2p/dummy/peer.go b/p2p/dummy/peer.go index 97fb7e2ef..fc2242366 100644 --- a/p2p/dummy/peer.go +++ b/p2p/dummy/peer.go @@ -1,6 +1,8 @@ package dummy import ( + "net" + p2p "github.com/tendermint/tendermint/p2p" tmconn "github.com/tendermint/tendermint/p2p/conn" cmn "github.com/tendermint/tmlibs/common" @@ -19,6 +21,7 @@ func NewPeer() *peer { kv: make(map[string]interface{}), } p.BaseService = *cmn.NewBaseService(nil, "peer", p) + return p } @@ -42,6 +45,11 @@ func (p *peer) NodeInfo() p2p.NodeInfo { return p2p.NodeInfo{} } +// RemoteIP always returns localhost. +func (p *peer) RemoteIP() net.IP { + return net.ParseIP("127.0.0.1") +} + // Status always returns empry connection status. func (p *peer) Status() tmconn.ConnectionStatus { return tmconn.ConnectionStatus{} diff --git a/p2p/errors.go b/p2p/errors.go index 314507785..fc477d1c2 100644 --- a/p2p/errors.go +++ b/p2p/errors.go @@ -2,30 +2,36 @@ package p2p import ( "fmt" + "net" ) +// ErrSwitchDuplicatePeerID to be raised when a peer is connecting with a known +// ID. type ErrSwitchDuplicatePeerID struct { ID ID } func (e ErrSwitchDuplicatePeerID) Error() string { - return fmt.Errorf("Duplicate peer ID %v", e.ID) + return fmt.Sprintf("Duplicate peer ID %v", e.ID) } +// ErrSwitchDuplicatePeerIP to be raised whena a peer is connecting with a known +// IP. type ErrSwitchDuplicatePeerIP struct { - Addr string + IP net.IP } func (e ErrSwitchDuplicatePeerIP) Error() string { - return fmt.Errorf("Duplicate peer IP %v", e.Addr) + return fmt.Sprintf("Duplicate peer IP %v", e.IP.String()) } +// ErrSwitchConnectToSelf to be raised when trying to connect to itself. type ErrSwitchConnectToSelf struct { Addr *NetAddress } func (e ErrSwitchConnectToSelf) Error() string { - return fmt.Errorf("Connect to self: %v", e.Addr) + return fmt.Sprintf("Connect to self: %v", e.Addr) } type ErrSwitchAuthenticationFailure struct { @@ -34,7 +40,11 @@ type ErrSwitchAuthenticationFailure struct { } func (e ErrSwitchAuthenticationFailure) Error() string { - return fmt.Sprintf("Failed to authenticate peer. Dialed %v, but got peer with ID %s", e.Dialed, e.Got) + return fmt.Sprintf( + "Failed to authenticate peer. Dialed %v, but got peer with ID %s", + e.Dialed, + e.Got, + ) } //------------------------------------------------------------------- diff --git a/p2p/peer.go b/p2p/peer.go index 1b5ec0ae4..2cb3d9f9f 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -17,7 +17,7 @@ type Peer interface { cmn.Service ID() ID // peer's cryptographic ID - RemoteIP() string // remote IP of the connection + RemoteIP() net.IP // remote IP of the connection IsOutbound() bool // did we dial the peer IsPersistent() bool // do we redial this peer when we disconnect NodeInfo() NodeInfo // peer's info @@ -38,6 +38,7 @@ type peerConn struct { persistent bool config *PeerConfig conn net.Conn // source connection + ips []net.IP } // ID only exists for SecretConnection. @@ -47,12 +48,24 @@ func (pc peerConn) ID() ID { } // Return the IP from the connection RemoteAddr -func (pc peerConn) RemoteIP() string { +func (pc peerConn) RemoteIP() net.IP { + if len(pc.ips) > 0 { + return pc.ips[0] + } + host, _, err := net.SplitHostPort(pc.conn.RemoteAddr().String()) if err != nil { panic(err) } - return host + + ips, err := net.LookupIP(host) + if err != nil { + panic(err) + } + + pc.ips = ips + + return ips[0] } // peer implements Peer. diff --git a/p2p/peer_set.go b/p2p/peer_set.go index 3ac48ea5e..a46d18b17 100644 --- a/p2p/peer_set.go +++ b/p2p/peer_set.go @@ -1,13 +1,14 @@ package p2p import ( + "net" "sync" ) // IPeerSet has a (immutable) subset of the methods of PeerSet. type IPeerSet interface { Has(key ID) bool - HasIP(ip string) bool + HasIP(ip net.IP) bool Get(key ID) Peer List() []Peer Size() int @@ -18,10 +19,9 @@ type IPeerSet interface { // PeerSet is a special structure for keeping a table of peers. // Iteration over the peers is super fast and thread-safe. type PeerSet struct { - mtx sync.Mutex - lookup map[ID]*peerSetItem - lookupIP map[string]struct{} - list []Peer + mtx sync.Mutex + lookup map[ID]*peerSetItem + list []Peer } type peerSetItem struct { @@ -32,21 +32,21 @@ type peerSetItem struct { // NewPeerSet creates a new peerSet with a list of initial capacity of 256 items. func NewPeerSet() *PeerSet { return &PeerSet{ - lookup: make(map[ID]*peerSetItem), - lookupIP: make(map[string]struct{}), - list: make([]Peer, 0, 256), + lookup: make(map[ID]*peerSetItem), + list: make([]Peer, 0, 256), } } // Add adds the peer to the PeerSet. -// It returns ErrSwitchDuplicatePeer if the peer is already present. +// It returns an error carrying the reason, if the peer is already present. func (ps *PeerSet) Add(peer Peer) error { ps.mtx.Lock() defer ps.mtx.Unlock() if ps.lookup[peer.ID()] != nil { return ErrSwitchDuplicatePeerID{peer.ID()} } - if _, ok := ps.lookupIP[peer.RemoteIP()]; ok { + + if ps.HasIP(peer.RemoteIP()) { return ErrSwitchDuplicatePeerIP{peer.RemoteIP()} } @@ -55,7 +55,6 @@ func (ps *PeerSet) Add(peer Peer) error { // iterating over the ps.list slice. ps.list = append(ps.list, peer) ps.lookup[peer.ID()] = &peerSetItem{peer, index} - ps.lookupIP[peer.RemoteIP()] = struct{}{} return nil } @@ -68,13 +67,19 @@ func (ps *PeerSet) Has(peerKey ID) bool { return ok } -// HasIP returns true iff the PeerSet contains -// the peer referred to by this IP address. -func (ps *PeerSet) HasIP(peerIP string) bool { +// HasIP returns true if the PeerSet contains the peer referred to by this IP +// address. +func (ps *PeerSet) HasIP(peerIP net.IP) bool { ps.mtx.Lock() - _, ok := ps.lookupIP[peerIP] ps.mtx.Unlock() - return ok + + for _, item := range ps.lookup { + if item.peer.RemoteIP().Equal(peerIP) { + return true + } + } + + return false } // Get looks up a peer by the provided peerKey. @@ -92,7 +97,7 @@ func (ps *PeerSet) Get(peerKey ID) Peer { func (ps *PeerSet) Remove(peer Peer) { ps.mtx.Lock() defer ps.mtx.Unlock() - delete(ps.lookupIP[peer.RemoteIP()]) + item := ps.lookup[peer.ID()] if item == nil { return diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go index 872758355..fafe1e262 100644 --- a/p2p/peer_set_test.go +++ b/p2p/peer_set_test.go @@ -112,18 +112,24 @@ func TestPeerSetAddDuplicate(t *testing.T) { } // Now collect and tally the results - errsTally := make(map[error]int) + errsTally := make(map[string]int) for i := 0; i < n; i++ { err := <-errsChan - errsTally[err]++ + + switch err.(type) { + case ErrSwitchDuplicatePeerID: + errsTally["duplicateID"]++ + default: + errsTally["other"]++ + } } // Our next procedure is to ensure that only one addition // succeeded and that the rest are each ErrSwitchDuplicatePeer. - wantErrCount, gotErrCount := n-1, errsTally[ErrSwitchDuplicatePeer] + wantErrCount, gotErrCount := n-1, errsTally["duplicateID"] assert.Equal(t, wantErrCount, gotErrCount, "invalid ErrSwitchDuplicatePeer count") - wantNilErrCount, gotNilErrCount := 1, errsTally[nil] + wantNilErrCount, gotNilErrCount := 1, errsTally["other"] assert.Equal(t, wantNilErrCount, gotNilErrCount, "invalid nil errCount") } diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index f7297a343..38aa84059 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -3,6 +3,7 @@ package pex import ( "fmt" "io/ioutil" + "net" "os" "path/filepath" "testing" @@ -365,6 +366,7 @@ func (mp mockPeer) NodeInfo() p2p.NodeInfo { ListenAddr: mp.addr.DialString(), } } +func (mp mockPeer) RemoteIP() net.IP { return net.ParseIP("127.0.0.1") } func (mp mockPeer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} } func (mp mockPeer) Send(byte, []byte) bool { return false } func (mp mockPeer) TrySend(byte, []byte) bool { return false } diff --git a/p2p/switch.go b/p2p/switch.go index 61c9ce969..22307bd91 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -404,7 +404,7 @@ func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent b err := sw.DialPeerWithAddress(addr, persistent) if err != nil { switch err.(type) { - case ErrSwitchConnectToSelf, ErrSwitchDuplicatePeer: + case ErrSwitchConnectToSelf, ErrSwitchDuplicatePeerID: sw.Logger.Debug("Error dialing peer", "err", err) default: sw.Logger.Error("Error dialing peer", "err", err) @@ -579,11 +579,8 @@ func (sw *Switch) addPeer(pc peerConn) error { } // check ips for both the connection addr and the self reported addr - if sw.peers.HasIP(addr) { - return ErrSwitchDuplicatePeerIP{addr} - } - if sw.peers.HasIP(peerNodeInfo.ListenAddr) { - return ErrSwitchDuplicatePeerIP{peerNodeInfo.ListenAddr} + if sw.peers.HasIP(pc.RemoteIP()) { + return ErrSwitchDuplicatePeerIP{pc.RemoteIP()} } // Filter peer against ID white list diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 25ed73bce..373378d73 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -193,7 +193,7 @@ func TestSwitchFiltersOutItself(t *testing.T) { // addr should be rejected in addPeer based on the same ID err := s1.DialPeerWithAddress(rp.Addr(), false) if assert.Error(t, err) { - assert.Equal(t, ErrSwitchConnectToSelf, err) + assert.EqualValues(t, ErrSwitchConnectToSelf{}, err) } assert.True(t, s1.addrBook.OurAddress(rp.Addr())) From c5f45275ec972936ac7f448f0436f08c727b320a Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Mon, 7 May 2018 17:54:55 +0200 Subject: [PATCH 04/59] Use remotePeer for test switch --- p2p/peer_test.go | 2 +- p2p/test_util.go | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 24d750a9f..0e21dcaf2 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -154,7 +154,7 @@ func (p *remotePeer) accept(l net.Listener) { _, err = pc.HandshakeTimeout(NodeInfo{ ID: p.Addr().ID, Moniker: "remote_peer", - Network: "testing", + Network: "localhost", Version: "123.123.123", ListenAddr: l.Addr().String(), Channels: []byte{testCh}, diff --git a/p2p/test_util.go b/p2p/test_util.go index 2c90bf516..e48e426dd 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -1,7 +1,9 @@ package p2p import ( + "fmt" "net" + "time" crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" @@ -80,7 +82,37 @@ func MakeConnectedSwitches(cfg *cfg.P2PConfig, n int, initSwitch func(int, *Swit func Connect2Switches(switches []*Switch, i, j int) { switchI := switches[i] switchJ := switches[j] - c1, c2 := conn.NetPipe() + + p1 := &remotePeer{ + Config: switchJ.peerConfig, + PrivKey: switchJ.nodeKey.PrivKey, + } + p1.Start() + + c1, err := net.DialTimeout( + "tcp", + fmt.Sprintf("%s:%d", p1.addr.IP.String(), p1.addr.Port), + 100*time.Millisecond, + ) + if err != nil { + panic(err) + } + + p2 := &remotePeer{ + Config: switchI.peerConfig, + PrivKey: switchI.nodeKey.PrivKey, + } + p2.Start() + + c2, err := net.DialTimeout( + "tcp", + fmt.Sprintf("%s:%d", p2.addr.IP.String(), p2.addr.Port), + 100*time.Millisecond, + ) + if err != nil { + panic(err) + } + doneCh := make(chan struct{}) go func() { err := switchI.addPeerWithConnection(c1) From b698a9febcd41795bd94a824178c42b1742d0855 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Wed, 16 May 2018 19:13:45 +0200 Subject: [PATCH 05/59] Remove double locking in HasIP --- consensus/byzantine_test.go | 2 +- p2p/peer_set.go | 11 ++++- p2p/peer_set_test.go | 37 ++++++++++++---- p2p/peer_test.go | 72 ------------------------------ p2p/test_util.go | 87 +++++++++++++++++++++++++++++++++++-- 5 files changed, 121 insertions(+), 88 deletions(-) diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index 5f04a3308..f18f16230 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -27,7 +27,7 @@ func init() { // Heal partition and ensure A sees the commit func TestByzantine(t *testing.T) { N := 4 - logger := consensusLogger() + logger := consensusLogger().With("test", "byzantine") css := randConsensusNet(N, "consensus_byzantine_test", newMockTickerFunc(false), newCounter) // give the byzantine validator a normal ticker diff --git a/p2p/peer_set.go b/p2p/peer_set.go index a46d18b17..66a7fdadb 100644 --- a/p2p/peer_set.go +++ b/p2p/peer_set.go @@ -42,11 +42,12 @@ func NewPeerSet() *PeerSet { func (ps *PeerSet) Add(peer Peer) error { ps.mtx.Lock() defer ps.mtx.Unlock() + if ps.lookup[peer.ID()] != nil { return ErrSwitchDuplicatePeerID{peer.ID()} } - if ps.HasIP(peer.RemoteIP()) { + if ps.hasIP(peer.RemoteIP()) { return ErrSwitchDuplicatePeerIP{peer.RemoteIP()} } @@ -71,8 +72,14 @@ func (ps *PeerSet) Has(peerKey ID) bool { // address. func (ps *PeerSet) HasIP(peerIP net.IP) bool { ps.mtx.Lock() - ps.mtx.Unlock() + defer ps.mtx.Unlock() + return ps.hasIP(peerIP) +} + +// hasIP does not acquire a lock so it can be used in public methods which +// already lock. +func (ps *PeerSet) hasIP(peerIP net.IP) bool { for _, item := range ps.lookup { if item.peer.RemoteIP().Equal(peerIP) { return true diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go index fafe1e262..ff2bdbad5 100644 --- a/p2p/peer_set_test.go +++ b/p2p/peer_set_test.go @@ -2,6 +2,7 @@ package p2p import ( "math/rand" + "net" "sync" "testing" @@ -12,23 +13,34 @@ import ( ) // Returns an empty kvstore peer -func randPeer() *peer { +func randPeer(ip net.IP) *peer { + if ip == nil { + ip = net.IP{127, 0, 0, 1} + } + nodeKey := NodeKey{PrivKey: crypto.GenPrivKeyEd25519()} - return &peer{ + p := &peer{ nodeInfo: NodeInfo{ ID: nodeKey.ID(), ListenAddr: cmn.Fmt("%v.%v.%v.%v:46656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256), }, } + + p.ips = []net.IP{ + ip, + } + + return p } func TestPeerSetAddRemoveOne(t *testing.T) { t.Parallel() + peerSet := NewPeerSet() var peerList []Peer for i := 0; i < 5; i++ { - p := randPeer() + p := randPeer(net.IP{127, 0, 0, byte(i)}) if err := peerSet.Add(p); err != nil { t.Error(err) } @@ -72,7 +84,7 @@ func TestPeerSetAddRemoveMany(t *testing.T) { peers := []Peer{} N := 100 for i := 0; i < N; i++ { - peer := randPeer() + peer := randPeer(net.IP{127, 0, 0, byte(i)}) if err := peerSet.Add(peer); err != nil { t.Errorf("Failed to add new peer") } @@ -96,7 +108,7 @@ func TestPeerSetAddRemoveMany(t *testing.T) { func TestPeerSetAddDuplicate(t *testing.T) { t.Parallel() peerSet := NewPeerSet() - peer := randPeer() + peer := randPeer(nil) n := 20 errsChan := make(chan error) @@ -133,10 +145,17 @@ func TestPeerSetAddDuplicate(t *testing.T) { assert.Equal(t, wantNilErrCount, gotNilErrCount, "invalid nil errCount") } +func TestPeerSetAddDuplicateIP(t *testing.T) { +} + func TestPeerSetGet(t *testing.T) { t.Parallel() - peerSet := NewPeerSet() - peer := randPeer() + + var ( + peerSet = NewPeerSet() + peer = randPeer(nil) + ) + assert.Nil(t, peerSet.Get(peer.ID()), "expecting a nil lookup, before .Add") if err := peerSet.Add(peer); err != nil { @@ -150,8 +169,8 @@ func TestPeerSetGet(t *testing.T) { wg.Add(1) go func(i int) { defer wg.Done() - got, want := peerSet.Get(peer.ID()), peer - assert.Equal(t, got, want, "#%d: got=%v want=%v", i, got, want) + have, want := peerSet.Get(peer.ID()), peer + assert.Equal(t, have, want, "%d: have %v, want %v", i, have, want) }(i) } wg.Wait() diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 0e21dcaf2..a93b2ceea 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -1,8 +1,6 @@ package p2p import ( - golog "log" - "net" "testing" "time" @@ -14,8 +12,6 @@ import ( "github.com/tendermint/tmlibs/log" ) -const testCh = 0x01 - func TestPeerBasic(t *testing.T) { assert, require := assert.New(t), require.New(t) @@ -109,71 +105,3 @@ func createOutboundPeerAndPerformHandshake(addr *NetAddress, config *PeerConfig) p.SetLogger(log.TestingLogger().With("peer", addr)) return p, nil } - -type remotePeer struct { - PrivKey crypto.PrivKey - Config *PeerConfig - addr *NetAddress - quit chan struct{} -} - -func (p *remotePeer) Addr() *NetAddress { - return p.addr -} - -func (p *remotePeer) ID() ID { - return PubKeyToID(p.PrivKey.PubKey()) -} - -func (p *remotePeer) Start() { - l, e := net.Listen("tcp", "127.0.0.1:0") // any available address - if e != nil { - golog.Fatalf("net.Listen tcp :0: %+v", e) - } - p.addr = NewNetAddress(PubKeyToID(p.PrivKey.PubKey()), l.Addr()) - p.quit = make(chan struct{}) - go p.accept(l) -} - -func (p *remotePeer) Stop() { - close(p.quit) -} - -func (p *remotePeer) accept(l net.Listener) { - conns := []net.Conn{} - - for { - conn, err := l.Accept() - if err != nil { - golog.Fatalf("Failed to accept conn: %+v", err) - } - pc, err := newInboundPeerConn(conn, p.Config, p.PrivKey) - if err != nil { - golog.Fatalf("Failed to create a peer: %+v", err) - } - _, err = pc.HandshakeTimeout(NodeInfo{ - ID: p.Addr().ID, - Moniker: "remote_peer", - Network: "localhost", - Version: "123.123.123", - ListenAddr: l.Addr().String(), - Channels: []byte{testCh}, - }, 1*time.Second) - if err != nil { - golog.Fatalf("Failed to perform handshake: %+v", err) - } - - conns = append(conns, conn) - - select { - case <-p.quit: - for _, conn := range conns { - if err := conn.Close(); err != nil { - golog.Fatal(err) - } - } - return - default: - } - } -} diff --git a/p2p/test_util.go b/p2p/test_util.go index e48e426dd..de9243499 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -2,6 +2,7 @@ package p2p import ( "fmt" + golog "log" "net" "time" @@ -13,6 +14,8 @@ import ( "github.com/tendermint/tendermint/p2p/conn" ) +const testCh = 0x01 + func AddPeerToSwitch(sw *Switch, peer Peer) { sw.peers.Add(peer) } @@ -84,8 +87,9 @@ func Connect2Switches(switches []*Switch, i, j int) { switchJ := switches[j] p1 := &remotePeer{ - Config: switchJ.peerConfig, - PrivKey: switchJ.nodeKey.PrivKey, + Config: switchJ.peerConfig, + PrivKey: switchJ.nodeKey.PrivKey, + channels: switchJ.NodeInfo().Channels, } p1.Start() @@ -99,8 +103,9 @@ func Connect2Switches(switches []*Switch, i, j int) { } p2 := &remotePeer{ - Config: switchI.peerConfig, - PrivKey: switchI.nodeKey.PrivKey, + Config: switchI.peerConfig, + PrivKey: switchI.nodeKey.PrivKey, + channels: switchI.NodeInfo().Channels, } p2.Start() @@ -183,3 +188,77 @@ func MakeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch f sw.SetNodeKey(nodeKey) return sw } + +type remotePeer struct { + PrivKey crypto.PrivKey + Config *PeerConfig + addr *NetAddress + quit chan struct{} + channels cmn.HexBytes +} + +func (rp *remotePeer) Addr() *NetAddress { + return rp.addr +} + +func (rp *remotePeer) ID() ID { + return PubKeyToID(rp.PrivKey.PubKey()) +} + +func (rp *remotePeer) Start() { + l, e := net.Listen("tcp", "127.0.0.1:0") // any available address + if e != nil { + golog.Fatalf("net.Listen tcp :0: %+v", e) + } + rp.addr = NewNetAddress(PubKeyToID(rp.PrivKey.PubKey()), l.Addr()) + rp.quit = make(chan struct{}) + if rp.channels == nil { + rp.channels = []byte{testCh} + } + go rp.accept(l) +} + +func (rp *remotePeer) Stop() { + close(rp.quit) +} + +func (rp *remotePeer) accept(l net.Listener) { + conns := []net.Conn{} + + for { + conn, err := l.Accept() + if err != nil { + golog.Fatalf("Failed to accept conn: %+v", err) + } + + pc, err := newInboundPeerConn(conn, rp.Config, rp.PrivKey) + if err != nil { + golog.Fatalf("Failed to create a peer: %+v", err) + } + + _, err = pc.HandshakeTimeout(NodeInfo{ + ID: rp.Addr().ID, + Moniker: "remote_peer", + Network: "localhost", + Version: "123.123.123", + ListenAddr: l.Addr().String(), + Channels: rp.channels, + }, 1*time.Second) + if err != nil { + golog.Fatalf("Failed to perform handshake: %+v", err) + } + + conns = append(conns, conn) + + select { + case <-rp.quit: + for _, conn := range conns { + if err := conn.Close(); err != nil { + golog.Fatal(err) + } + } + return + default: + } + } +} From d596ed1bc2ac9f0260b67b87c7d8a36216aba649 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Fri, 18 May 2018 16:27:57 +0200 Subject: [PATCH 06/59] Let peerConn handle IPs in for tests --- p2p/peer.go | 12 +++++ p2p/peer_test.go | 79 +++++++++++++++++++++++++++++++++ p2p/test_util.go | 111 +---------------------------------------------- 3 files changed, 92 insertions(+), 110 deletions(-) diff --git a/p2p/peer.go b/p2p/peer.go index 2cb3d9f9f..93d05410e 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -12,6 +12,8 @@ import ( tmconn "github.com/tendermint/tendermint/p2p/conn" ) +var testIPSuffix = 0 + // Peer is an interface representing a peer connected on a reactor. type Peer interface { cmn.Service @@ -53,6 +55,16 @@ func (pc peerConn) RemoteIP() net.IP { return pc.ips[0] } + if pc.conn.RemoteAddr().String() == "pipe" { + pc.ips = []net.IP{ + net.IP{172, 16, 0, byte(testIPSuffix)}, + } + + testIPSuffix++ + + return pc.ips[0] + } + host, _, err := net.SplitHostPort(pc.conn.RemoteAddr().String()) if err != nil { panic(err) diff --git a/p2p/peer_test.go b/p2p/peer_test.go index a93b2ceea..db4f63ed7 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -1,6 +1,8 @@ package p2p import ( + golog "log" + "net" "testing" "time" @@ -9,9 +11,12 @@ import ( crypto "github.com/tendermint/go-crypto" tmconn "github.com/tendermint/tendermint/p2p/conn" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" ) +const testCh = 0x01 + func TestPeerBasic(t *testing.T) { assert, require := assert.New(t), require.New(t) @@ -105,3 +110,77 @@ func createOutboundPeerAndPerformHandshake(addr *NetAddress, config *PeerConfig) p.SetLogger(log.TestingLogger().With("peer", addr)) return p, nil } + +type remotePeer struct { + PrivKey crypto.PrivKey + Config *PeerConfig + addr *NetAddress + quit chan struct{} + channels cmn.HexBytes +} + +func (rp *remotePeer) Addr() *NetAddress { + return rp.addr +} + +func (rp *remotePeer) ID() ID { + return PubKeyToID(rp.PrivKey.PubKey()) +} + +func (rp *remotePeer) Start() { + l, e := net.Listen("tcp", "127.0.0.1:0") // any available address + if e != nil { + golog.Fatalf("net.Listen tcp :0: %+v", e) + } + rp.addr = NewNetAddress(PubKeyToID(rp.PrivKey.PubKey()), l.Addr()) + rp.quit = make(chan struct{}) + if rp.channels == nil { + rp.channels = []byte{testCh} + } + go rp.accept(l) +} + +func (rp *remotePeer) Stop() { + close(rp.quit) +} + +func (rp *remotePeer) accept(l net.Listener) { + conns := []net.Conn{} + + for { + conn, err := l.Accept() + if err != nil { + golog.Fatalf("Failed to accept conn: %+v", err) + } + + pc, err := newInboundPeerConn(conn, rp.Config, rp.PrivKey) + if err != nil { + golog.Fatalf("Failed to create a peer: %+v", err) + } + + _, err = pc.HandshakeTimeout(NodeInfo{ + ID: rp.Addr().ID, + Moniker: "remote_peer", + Network: "testing", + Version: "123.123.123", + ListenAddr: l.Addr().String(), + Channels: rp.channels, + }, 1*time.Second) + if err != nil { + golog.Fatalf("Failed to perform handshake: %+v", err) + } + + conns = append(conns, conn) + + select { + case <-rp.quit: + for _, conn := range conns { + if err := conn.Close(); err != nil { + golog.Fatal(err) + } + } + return + default: + } + } +} diff --git a/p2p/test_util.go b/p2p/test_util.go index de9243499..a0b3a5b88 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -1,10 +1,7 @@ package p2p import ( - "fmt" - golog "log" "net" - "time" crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" @@ -14,8 +11,6 @@ import ( "github.com/tendermint/tendermint/p2p/conn" ) -const testCh = 0x01 - func AddPeerToSwitch(sw *Switch, peer Peer) { sw.peers.Add(peer) } @@ -86,37 +81,7 @@ func Connect2Switches(switches []*Switch, i, j int) { switchI := switches[i] switchJ := switches[j] - p1 := &remotePeer{ - Config: switchJ.peerConfig, - PrivKey: switchJ.nodeKey.PrivKey, - channels: switchJ.NodeInfo().Channels, - } - p1.Start() - - c1, err := net.DialTimeout( - "tcp", - fmt.Sprintf("%s:%d", p1.addr.IP.String(), p1.addr.Port), - 100*time.Millisecond, - ) - if err != nil { - panic(err) - } - - p2 := &remotePeer{ - Config: switchI.peerConfig, - PrivKey: switchI.nodeKey.PrivKey, - channels: switchI.NodeInfo().Channels, - } - p2.Start() - - c2, err := net.DialTimeout( - "tcp", - fmt.Sprintf("%s:%d", p2.addr.IP.String(), p2.addr.Port), - 100*time.Millisecond, - ) - if err != nil { - panic(err) - } + c1, c2 := conn.NetPipe() doneCh := make(chan struct{}) go func() { @@ -188,77 +153,3 @@ func MakeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch f sw.SetNodeKey(nodeKey) return sw } - -type remotePeer struct { - PrivKey crypto.PrivKey - Config *PeerConfig - addr *NetAddress - quit chan struct{} - channels cmn.HexBytes -} - -func (rp *remotePeer) Addr() *NetAddress { - return rp.addr -} - -func (rp *remotePeer) ID() ID { - return PubKeyToID(rp.PrivKey.PubKey()) -} - -func (rp *remotePeer) Start() { - l, e := net.Listen("tcp", "127.0.0.1:0") // any available address - if e != nil { - golog.Fatalf("net.Listen tcp :0: %+v", e) - } - rp.addr = NewNetAddress(PubKeyToID(rp.PrivKey.PubKey()), l.Addr()) - rp.quit = make(chan struct{}) - if rp.channels == nil { - rp.channels = []byte{testCh} - } - go rp.accept(l) -} - -func (rp *remotePeer) Stop() { - close(rp.quit) -} - -func (rp *remotePeer) accept(l net.Listener) { - conns := []net.Conn{} - - for { - conn, err := l.Accept() - if err != nil { - golog.Fatalf("Failed to accept conn: %+v", err) - } - - pc, err := newInboundPeerConn(conn, rp.Config, rp.PrivKey) - if err != nil { - golog.Fatalf("Failed to create a peer: %+v", err) - } - - _, err = pc.HandshakeTimeout(NodeInfo{ - ID: rp.Addr().ID, - Moniker: "remote_peer", - Network: "localhost", - Version: "123.123.123", - ListenAddr: l.Addr().String(), - Channels: rp.channels, - }, 1*time.Second) - if err != nil { - golog.Fatalf("Failed to perform handshake: %+v", err) - } - - conns = append(conns, conn) - - select { - case <-rp.quit: - for _, conn := range conns { - if err := conn.Close(); err != nil { - golog.Fatal(err) - } - } - return - default: - } - } -} From 383c255f355e90c044fbf24d537e6e76ff47fca9 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Sun, 20 May 2018 16:54:21 -0400 Subject: [PATCH 07/59] dev version bump --- CHANGELOG.md | 4 ++++ version/version.go | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d2462c81..b96c995ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.19.6 + +*TBD* + ## 0.19.5 *May 20th, 2018* diff --git a/version/version.go b/version/version.go index f5d77dc48..ceec57656 100644 --- a/version/version.go +++ b/version/version.go @@ -4,13 +4,13 @@ package version const ( Maj = "0" Min = "19" - Fix = "5" + Fix = "6" ) var ( // Version is the current version of Tendermint // Must be a string because scripts like dist.sh read this file. - Version = "0.19.5" + Version = "0.19.6-dev" // GitCommit is the current HEAD set using ldflags. GitCommit string From 21f5f3faa7d5aee7c3778107a40472f757c88fd4 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 15 May 2018 14:32:06 +0400 Subject: [PATCH 08/59] use channels to send votes, ... from consensus state to reactor Refs #847 --- consensus/common_test.go | 2 +- consensus/reactor.go | 72 +++++++++++----------------------------- consensus/state.go | 37 +++++++++++++++++++++ 3 files changed, 57 insertions(+), 54 deletions(-) diff --git a/consensus/common_test.go b/consensus/common_test.go index 4ddd6b8aa..3eaeea700 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -264,7 +264,7 @@ func newConsensusStateWithConfigAndBlockStore(thisConfig *cfg.Config, state sm.S // mock the evidence pool evpool := types.MockEvidencePool{} - // Make ConsensusReactor + // Make ConsensusState stateDB := dbm.NewMemDB() blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyAppConnCon, mempool, evpool) cs := NewConsensusState(thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool) diff --git a/consensus/reactor.go b/consensus/reactor.go index 9535108c7..1193ed722 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -1,7 +1,6 @@ package consensus import ( - "context" "fmt" "reflect" "sync" @@ -49,6 +48,12 @@ func NewConsensusReactor(consensusState *ConsensusState, fastSync bool) *Consens conS: consensusState, fastSync: fastSync, } + // XXX: modifing state to send us new round steps, votes and proposal heartbeats + consensusState.reactorChs = &reactorChs{ + newRoundSteps: make(chan *cstypes.RoundState), + votes: make(chan *types.Vote), + proposalHeartbeats: make(chan *types.Heartbeat), + } conR.BaseReactor = *p2p.NewBaseReactor("ConsensusReactor", conR) return conR } @@ -345,60 +350,22 @@ func (conR *ConsensusReactor) FastSync() bool { //-------------------------------------- -// startBroadcastRoutine subscribes for new round steps, votes and proposal -// heartbeats using the event bus and starts a go routine to broadcasts events -// to peers upon receiving them. +// startBroadcastRoutine subscribes for new round steps, votes and +// proposal heartbeats using the channels created for precisely this +// purpose in consensus state and starts a goroutine to broadcasts +// events to peers upon receiving them. func (conR *ConsensusReactor) startBroadcastRoutine() error { - const subscriber = "consensus-reactor" - ctx := context.Background() - - // new round steps - stepsCh := make(chan interface{}) - err := conR.eventBus.Subscribe(ctx, subscriber, types.EventQueryNewRoundStep, stepsCh) - if err != nil { - return errors.Wrapf(err, "failed to subscribe %s to %s", subscriber, types.EventQueryNewRoundStep) - } - - // votes - votesCh := make(chan interface{}) - err = conR.eventBus.Subscribe(ctx, subscriber, types.EventQueryVote, votesCh) - if err != nil { - return errors.Wrapf(err, "failed to subscribe %s to %s", subscriber, types.EventQueryVote) - } - - // proposal heartbeats - heartbeatsCh := make(chan interface{}) - err = conR.eventBus.Subscribe(ctx, subscriber, types.EventQueryProposalHeartbeat, heartbeatsCh) - if err != nil { - return errors.Wrapf(err, "failed to subscribe %s to %s", subscriber, types.EventQueryProposalHeartbeat) - } - go func() { - var data interface{} - var ok bool + rchs := conR.conS.reactorChs.(*reactorChs) for { select { - case data, ok = <-stepsCh: - if ok { // a receive from a closed channel returns the zero value immediately - edrs := data.(types.EventDataRoundState) - conR.broadcastNewRoundStep(edrs.RoundState.(*cstypes.RoundState)) - } - case data, ok = <-votesCh: - if ok { - edv := data.(types.EventDataVote) - conR.broadcastHasVoteMessage(edv.Vote) - } - case data, ok = <-heartbeatsCh: - if ok { - edph := data.(types.EventDataProposalHeartbeat) - conR.broadcastProposalHeartbeatMessage(edph) - } + case rs := <-rchs.newRoundSteps: + conR.broadcastNewRoundStepMessage(rs) + case vote := <-rchs.votes: + conR.broadcastHasVoteMessage(vote) + case heartbeat := <-rchs.proposalHeartbeats: + conR.broadcastProposalHeartbeat(heartbeat) case <-conR.Quit(): - conR.eventBus.UnsubscribeAll(ctx, subscriber) - return - } - if !ok { - conR.eventBus.UnsubscribeAll(ctx, subscriber) return } } @@ -407,15 +374,14 @@ func (conR *ConsensusReactor) startBroadcastRoutine() error { return nil } -func (conR *ConsensusReactor) broadcastProposalHeartbeatMessage(heartbeat types.EventDataProposalHeartbeat) { - hb := heartbeat.Heartbeat +func (conR *ConsensusReactor) broadcastProposalHeartbeat(hb *types.Heartbeat) { conR.Logger.Debug("Broadcasting proposal heartbeat message", "height", hb.Height, "round", hb.Round, "sequence", hb.Sequence) msg := &ProposalHeartbeatMessage{hb} conR.Switch.Broadcast(StateChannel, cdc.MustMarshalBinaryBare(msg)) } -func (conR *ConsensusReactor) broadcastNewRoundStep(rs *cstypes.RoundState) { +func (conR *ConsensusReactor) broadcastNewRoundStepMessage(rs *cstypes.RoundState) { nrsMsg, csMsg := makeRoundStepMessages(rs) if nrsMsg != nil { conR.Switch.Broadcast(StateChannel, cdc.MustMarshalBinaryBare(nrsMsg)) diff --git a/consensus/state.go b/consensus/state.go index b5b943688..5668ea82d 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -110,8 +110,40 @@ type ConsensusState struct { // closed when we finish shutting down done chan struct{} + + // synchronous pubsub between consensus state and reactor + // only set when there is a reactor + reactorChs reactorChsI } +type reactorChsI interface { + NewRoundStep(*cstypes.RoundState) + Vote(*types.Vote) + ProposalHeartbeat(*types.Heartbeat) +} + +// A list of channels to send new round steps, votes and proposal heartbeats to. +type reactorChs struct { + newRoundSteps chan (*cstypes.RoundState) + votes chan (*types.Vote) + proposalHeartbeats chan (*types.Heartbeat) +} + +var _ reactorChsI = (*reactorChs)(nil) + +// BLOCKING +func (rchs *reactorChs) NewRoundStep(rs *cstypes.RoundState) { rchs.newRoundSteps <- rs } +func (rchs *reactorChs) Vote(vote *types.Vote) { rchs.votes <- vote } +func (rchs *reactorChs) ProposalHeartbeat(hb *types.Heartbeat) { rchs.proposalHeartbeats <- hb } + +type nilReactorChs struct{} + +var _ reactorChsI = nilReactorChs{} + +func (nilReactorChs) NewRoundStep(rs *cstypes.RoundState) {} +func (nilReactorChs) Vote(vote *types.Vote) {} +func (nilReactorChs) ProposalHeartbeat(hb *types.Heartbeat) {} + // NewConsensusState returns a new ConsensusState. func NewConsensusState(config *cfg.ConsensusConfig, state sm.State, blockExec *sm.BlockExecutor, blockStore types.BlockStore, mempool types.Mempool, evpool types.EvidencePool) *ConsensusState { cs := &ConsensusState{ @@ -126,6 +158,7 @@ func NewConsensusState(config *cfg.ConsensusConfig, state sm.State, blockExec *s doWALCatchup: true, wal: nilWAL{}, evpool: evpool, + reactorChs: nilReactorChs{}, } // set function defaults (may be overwritten before calling Start) cs.decideProposal = cs.defaultDecideProposal @@ -509,6 +542,7 @@ func (cs *ConsensusState) newStep() { // newStep is called by updateToStep in NewConsensusState before the eventBus is set! if cs.eventBus != nil { cs.eventBus.PublishEventNewRoundStep(rs) + cs.reactorChs.NewRoundStep(&cs.RoundState) } } @@ -752,6 +786,7 @@ func (cs *ConsensusState) proposalHeartbeat(height int64, round int) { } cs.privValidator.SignHeartbeat(chainID, heartbeat) cs.eventBus.PublishEventProposalHeartbeat(types.EventDataProposalHeartbeat{heartbeat}) + cs.reactorChs.ProposalHeartbeat(heartbeat) counter++ time.Sleep(proposalHeartbeatIntervalSeconds * time.Second) } @@ -1418,6 +1453,7 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, cs.Logger.Info(cmn.Fmt("Added to lastPrecommits: %v", cs.LastCommit.StringShort())) cs.eventBus.PublishEventVote(types.EventDataVote{vote}) + cs.reactorChs.Vote(vote) // if we can skip timeoutCommit and have all the votes now, if cs.config.SkipTimeoutCommit && cs.LastCommit.HasAll() { @@ -1445,6 +1481,7 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, } cs.eventBus.PublishEventVote(types.EventDataVote{vote}) + cs.reactorChs.Vote(vote) switch vote.Type { case types.VoteTypePrevote: From b77d5344fc28bb2679fcac3a60c1da540ce36527 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 15 May 2018 14:38:15 +0400 Subject: [PATCH 09/59] rename methods for clarity --- consensus/reactor.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/consensus/reactor.go b/consensus/reactor.go index 1193ed722..5e7295b55 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -360,11 +360,11 @@ func (conR *ConsensusReactor) startBroadcastRoutine() error { for { select { case rs := <-rchs.newRoundSteps: - conR.broadcastNewRoundStepMessage(rs) + conR.broadcastNewRoundStepMessages(rs) case vote := <-rchs.votes: conR.broadcastHasVoteMessage(vote) case heartbeat := <-rchs.proposalHeartbeats: - conR.broadcastProposalHeartbeat(heartbeat) + conR.broadcastProposalHeartbeatMessage(heartbeat) case <-conR.Quit(): return } @@ -374,14 +374,14 @@ func (conR *ConsensusReactor) startBroadcastRoutine() error { return nil } -func (conR *ConsensusReactor) broadcastProposalHeartbeat(hb *types.Heartbeat) { +func (conR *ConsensusReactor) broadcastProposalHeartbeatMessage(hb *types.Heartbeat) { conR.Logger.Debug("Broadcasting proposal heartbeat message", "height", hb.Height, "round", hb.Round, "sequence", hb.Sequence) msg := &ProposalHeartbeatMessage{hb} conR.Switch.Broadcast(StateChannel, cdc.MustMarshalBinaryBare(msg)) } -func (conR *ConsensusReactor) broadcastNewRoundStepMessage(rs *cstypes.RoundState) { +func (conR *ConsensusReactor) broadcastNewRoundStepMessages(rs *cstypes.RoundState) { nrsMsg, csMsg := makeRoundStepMessages(rs) if nrsMsg != nil { conR.Switch.Broadcast(StateChannel, cdc.MustMarshalBinaryBare(nrsMsg)) From c4fef499b63e05abe3c461d528ea26843979ca8a Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 16 May 2018 10:28:58 +0400 Subject: [PATCH 10/59] switch to events package --- Gopkg.lock | 3 ++- consensus/reactor.go | 59 ++++++++++++++++++++------------------------ consensus/state.go | 54 ++++++++++++---------------------------- 3 files changed, 45 insertions(+), 71 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 8280148c9..df971a47b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -278,6 +278,7 @@ "clist", "common", "db", + "events", "flowrate", "log", "merkle", @@ -384,6 +385,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "52a0dcbebdf8714612444914cfce59a3af8c47c4453a2d43c4ccc5ff1a91d8ea" + inputs-digest = "a88c20b6e36b3529d6fdcffc3603d9eb193fc3809de8afbba07bad990539b256" solver-name = "gps-cdcl" solver-version = 1 diff --git a/consensus/reactor.go b/consensus/reactor.go index 5e7295b55..f171aa8d9 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -10,6 +10,7 @@ import ( amino "github.com/tendermint/go-amino" cmn "github.com/tendermint/tmlibs/common" + tmevents "github.com/tendermint/tmlibs/events" "github.com/tendermint/tmlibs/log" cstypes "github.com/tendermint/tendermint/consensus/types" @@ -48,12 +49,6 @@ func NewConsensusReactor(consensusState *ConsensusState, fastSync bool) *Consens conS: consensusState, fastSync: fastSync, } - // XXX: modifing state to send us new round steps, votes and proposal heartbeats - consensusState.reactorChs = &reactorChs{ - newRoundSteps: make(chan *cstypes.RoundState), - votes: make(chan *types.Vote), - proposalHeartbeats: make(chan *types.Heartbeat), - } conR.BaseReactor = *p2p.NewBaseReactor("ConsensusReactor", conR) return conR } @@ -65,16 +60,12 @@ func (conR *ConsensusReactor) OnStart() error { return err } - err := conR.startBroadcastRoutine() - if err != nil { - return err - } - if !conR.FastSync() { err := conR.conS.Start() if err != nil { return err } + conR.subscribeToBroadcastEvents() } return nil @@ -106,7 +97,9 @@ func (conR *ConsensusReactor) SwitchToConsensus(state sm.State, blocksSynced int err := conR.conS.Start() if err != nil { conR.Logger.Error("Error starting conS", "err", err) + return } + conR.subscribeToBroadcastEvents() } // GetChannels implements Reactor @@ -350,28 +343,30 @@ func (conR *ConsensusReactor) FastSync() bool { //-------------------------------------- -// startBroadcastRoutine subscribes for new round steps, votes and -// proposal heartbeats using the channels created for precisely this -// purpose in consensus state and starts a goroutine to broadcasts -// events to peers upon receiving them. -func (conR *ConsensusReactor) startBroadcastRoutine() error { - go func() { - rchs := conR.conS.reactorChs.(*reactorChs) - for { - select { - case rs := <-rchs.newRoundSteps: - conR.broadcastNewRoundStepMessages(rs) - case vote := <-rchs.votes: - conR.broadcastHasVoteMessage(vote) - case heartbeat := <-rchs.proposalHeartbeats: - conR.broadcastProposalHeartbeatMessage(heartbeat) - case <-conR.Quit(): - return - } - } - }() +// subscribeToBroadcastEvents subscribes for new round steps, votes and +// proposal heartbeats using internal pubsub defined on state to broadcast +// them to peers upon receiving. +func (conR *ConsensusReactor) subscribeToBroadcastEvents() { + // assert consensus state is running + if !conR.conS.IsRunning() { + panic("consensus state must be running at this point") + } - return nil + const subscriber = "consensus-reactor" + conR.conS.evsw.AddListenerForEvent(subscriber, types.EventNewRoundStep, + func(data tmevents.EventData) { + conR.broadcastNewRoundStepMessages(data.(*cstypes.RoundState)) + }) + + conR.conS.evsw.AddListenerForEvent(subscriber, types.EventVote, + func(data tmevents.EventData) { + conR.broadcastHasVoteMessage(data.(*types.Vote)) + }) + + conR.conS.evsw.AddListenerForEvent(subscriber, types.EventProposalHeartbeat, + func(data tmevents.EventData) { + conR.broadcastProposalHeartbeatMessage(data.(*types.Heartbeat)) + }) } func (conR *ConsensusReactor) broadcastProposalHeartbeatMessage(hb *types.Heartbeat) { diff --git a/consensus/state.go b/consensus/state.go index 5668ea82d..0c6f7b487 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -11,6 +11,7 @@ import ( fail "github.com/ebuchman/fail-test" cmn "github.com/tendermint/tmlibs/common" + tmevents "github.com/tendermint/tmlibs/events" "github.com/tendermint/tmlibs/log" cfg "github.com/tendermint/tendermint/config" @@ -111,39 +112,11 @@ type ConsensusState struct { // closed when we finish shutting down done chan struct{} - // synchronous pubsub between consensus state and reactor - // only set when there is a reactor - reactorChs reactorChsI + // synchronous pubsub between consensus state and reactor. + // state only emits EventNewRoundStep, EventVote and EventProposalHeartbeat + evsw tmevents.EventSwitch } -type reactorChsI interface { - NewRoundStep(*cstypes.RoundState) - Vote(*types.Vote) - ProposalHeartbeat(*types.Heartbeat) -} - -// A list of channels to send new round steps, votes and proposal heartbeats to. -type reactorChs struct { - newRoundSteps chan (*cstypes.RoundState) - votes chan (*types.Vote) - proposalHeartbeats chan (*types.Heartbeat) -} - -var _ reactorChsI = (*reactorChs)(nil) - -// BLOCKING -func (rchs *reactorChs) NewRoundStep(rs *cstypes.RoundState) { rchs.newRoundSteps <- rs } -func (rchs *reactorChs) Vote(vote *types.Vote) { rchs.votes <- vote } -func (rchs *reactorChs) ProposalHeartbeat(hb *types.Heartbeat) { rchs.proposalHeartbeats <- hb } - -type nilReactorChs struct{} - -var _ reactorChsI = nilReactorChs{} - -func (nilReactorChs) NewRoundStep(rs *cstypes.RoundState) {} -func (nilReactorChs) Vote(vote *types.Vote) {} -func (nilReactorChs) ProposalHeartbeat(hb *types.Heartbeat) {} - // NewConsensusState returns a new ConsensusState. func NewConsensusState(config *cfg.ConsensusConfig, state sm.State, blockExec *sm.BlockExecutor, blockStore types.BlockStore, mempool types.Mempool, evpool types.EvidencePool) *ConsensusState { cs := &ConsensusState{ @@ -158,7 +131,7 @@ func NewConsensusState(config *cfg.ConsensusConfig, state sm.State, blockExec *s doWALCatchup: true, wal: nilWAL{}, evpool: evpool, - reactorChs: nilReactorChs{}, + evsw: tmevents.NewEventSwitch(), } // set function defaults (may be overwritten before calling Start) cs.decideProposal = cs.defaultDecideProposal @@ -260,6 +233,10 @@ func (cs *ConsensusState) LoadCommit(height int64) *types.Commit { // OnStart implements cmn.Service. // It loads the latest state via the WAL, and starts the timeout and receive routines. func (cs *ConsensusState) OnStart() error { + if err := cs.evsw.Start(); err != nil { + return err + } + // we may set the WAL in testing before calling Start, // so only OpenWAL if its still the nilWAL if _, ok := cs.wal.(nilWAL); ok { @@ -277,8 +254,7 @@ func (cs *ConsensusState) OnStart() error { // NOTE: we will get a build up of garbage go routines // firing on the tockChan until the receiveRoutine is started // to deal with them (by that point, at most one will be valid) - err := cs.timeoutTicker.Start() - if err != nil { + if err := cs.timeoutTicker.Start(); err != nil { return err } @@ -317,6 +293,8 @@ func (cs *ConsensusState) startRoutines(maxSteps int) { func (cs *ConsensusState) OnStop() { cs.BaseService.OnStop() + cs.evsw.Stop() + cs.timeoutTicker.Stop() // Make BaseService.Wait() wait until cs.wal.Wait() @@ -542,7 +520,7 @@ func (cs *ConsensusState) newStep() { // newStep is called by updateToStep in NewConsensusState before the eventBus is set! if cs.eventBus != nil { cs.eventBus.PublishEventNewRoundStep(rs) - cs.reactorChs.NewRoundStep(&cs.RoundState) + cs.evsw.FireEvent(types.EventNewRoundStep, &cs.RoundState) } } @@ -786,7 +764,7 @@ func (cs *ConsensusState) proposalHeartbeat(height int64, round int) { } cs.privValidator.SignHeartbeat(chainID, heartbeat) cs.eventBus.PublishEventProposalHeartbeat(types.EventDataProposalHeartbeat{heartbeat}) - cs.reactorChs.ProposalHeartbeat(heartbeat) + cs.evsw.FireEvent(types.EventProposalHeartbeat, heartbeat) counter++ time.Sleep(proposalHeartbeatIntervalSeconds * time.Second) } @@ -1453,7 +1431,7 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, cs.Logger.Info(cmn.Fmt("Added to lastPrecommits: %v", cs.LastCommit.StringShort())) cs.eventBus.PublishEventVote(types.EventDataVote{vote}) - cs.reactorChs.Vote(vote) + cs.evsw.FireEvent(types.EventVote, vote) // if we can skip timeoutCommit and have all the votes now, if cs.config.SkipTimeoutCommit && cs.LastCommit.HasAll() { @@ -1481,7 +1459,7 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, } cs.eventBus.PublishEventVote(types.EventDataVote{vote}) - cs.reactorChs.Vote(vote) + cs.evsw.FireEvent(types.EventVote, vote) switch vote.Type { case types.VoteTypePrevote: From bb9aa85d2281aaf1705839a3f4a9f947ee018d9d Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 16 May 2018 11:03:11 +0400 Subject: [PATCH 11/59] copy events and pubsub packages from tmlibs Refs #847 --- .gitignore | 2 + Gopkg.lock | 5 +- consensus/reactor.go | 2 +- consensus/state.go | 2 +- consensus/state_test.go | 2 +- libs/events/Makefile | 9 + libs/events/README.md | 175 +++ libs/events/event_cache.go | 37 + libs/events/event_cache_test.go | 35 + libs/events/events.go | 226 ++++ libs/events/events_test.go | 380 +++++++ libs/pubsub/example_test.go | 28 + libs/pubsub/pubsub.go | 342 ++++++ libs/pubsub/pubsub_test.go | 253 +++++ libs/pubsub/query/Makefile | 11 + libs/pubsub/query/empty.go | 16 + libs/pubsub/query/empty_test.go | 18 + libs/pubsub/query/fuzz_test/main.go | 30 + libs/pubsub/query/parser_test.go | 92 ++ libs/pubsub/query/query.go | 345 ++++++ libs/pubsub/query/query.peg | 33 + libs/pubsub/query/query.peg.go | 1553 +++++++++++++++++++++++++++ libs/pubsub/query/query_test.go | 87 ++ rpc/client/httpclient.go | 2 +- rpc/client/localclient.go | 2 +- rpc/core/events.go | 6 +- rpc/core/tx.go | 5 +- rpc/lib/types/types.go | 2 +- state/txindex/indexer.go | 2 +- state/txindex/indexer_service.go | 3 +- state/txindex/kv/kv.go | 2 +- state/txindex/kv/kv_test.go | 2 +- state/txindex/null/null.go | 2 +- types/event_bus.go | 2 +- types/event_bus_test.go | 4 +- types/events.go | 6 +- types/nop_event_bus.go | 2 +- 37 files changed, 3698 insertions(+), 27 deletions(-) create mode 100644 libs/events/Makefile create mode 100644 libs/events/README.md create mode 100644 libs/events/event_cache.go create mode 100644 libs/events/event_cache_test.go create mode 100644 libs/events/events.go create mode 100644 libs/events/events_test.go create mode 100644 libs/pubsub/example_test.go create mode 100644 libs/pubsub/pubsub.go create mode 100644 libs/pubsub/pubsub_test.go create mode 100644 libs/pubsub/query/Makefile create mode 100644 libs/pubsub/query/empty.go create mode 100644 libs/pubsub/query/empty_test.go create mode 100644 libs/pubsub/query/fuzz_test/main.go create mode 100644 libs/pubsub/query/parser_test.go create mode 100644 libs/pubsub/query/query.go create mode 100644 libs/pubsub/query/query.peg create mode 100644 libs/pubsub/query/query.peg.go create mode 100644 libs/pubsub/query/query_test.go diff --git a/.gitignore b/.gitignore index e76fb1fc5..c67019bd9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ scripts/cutWALUntil/cutWALUntil .idea/ *.iml + +libs/pubsub/query/fuzz_test/output diff --git a/Gopkg.lock b/Gopkg.lock index df971a47b..6e34258f4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -278,12 +278,9 @@ "clist", "common", "db", - "events", "flowrate", "log", "merkle", - "pubsub", - "pubsub/query", "test" ] revision = "cc5f287c4798ffe88c04d02df219ecb6932080fd" @@ -385,6 +382,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a88c20b6e36b3529d6fdcffc3603d9eb193fc3809de8afbba07bad990539b256" + inputs-digest = "d85c98dcac32cc1fe05d006aa75e8985f6447a150a041b972a673a65e7681da9" solver-name = "gps-cdcl" solver-version = 1 diff --git a/consensus/reactor.go b/consensus/reactor.go index f171aa8d9..19b0c0fe2 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -10,10 +10,10 @@ import ( amino "github.com/tendermint/go-amino" cmn "github.com/tendermint/tmlibs/common" - tmevents "github.com/tendermint/tmlibs/events" "github.com/tendermint/tmlibs/log" cstypes "github.com/tendermint/tendermint/consensus/types" + tmevents "github.com/tendermint/tendermint/libs/events" "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" diff --git a/consensus/state.go b/consensus/state.go index 0c6f7b487..3b713e2ec 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -11,11 +11,11 @@ import ( fail "github.com/ebuchman/fail-test" cmn "github.com/tendermint/tmlibs/common" - tmevents "github.com/tendermint/tmlibs/events" "github.com/tendermint/tmlibs/log" cfg "github.com/tendermint/tendermint/config" cstypes "github.com/tendermint/tendermint/consensus/types" + tmevents "github.com/tendermint/tendermint/libs/events" "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" diff --git a/consensus/state_test.go b/consensus/state_test.go index 0d7cad484..f4d79ca77 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -11,7 +11,7 @@ import ( "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" - tmpubsub "github.com/tendermint/tmlibs/pubsub" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" ) func init() { diff --git a/libs/events/Makefile b/libs/events/Makefile new file mode 100644 index 000000000..696aafff1 --- /dev/null +++ b/libs/events/Makefile @@ -0,0 +1,9 @@ +.PHONY: docs +REPO:=github.com/tendermint/tendermint/libs/events + +docs: + @go get github.com/davecheney/godoc2md + godoc2md $(REPO) > README.md + +test: + go test -v ./... diff --git a/libs/events/README.md b/libs/events/README.md new file mode 100644 index 000000000..14aa498ff --- /dev/null +++ b/libs/events/README.md @@ -0,0 +1,175 @@ + + +# events +`import "github.com/tendermint/tendermint/libs/events"` + +* [Overview](#pkg-overview) +* [Index](#pkg-index) + +## Overview +Pub-Sub in go with event caching + + + + +## Index +* [type EventCache](#EventCache) + * [func NewEventCache(evsw Fireable) *EventCache](#NewEventCache) + * [func (evc *EventCache) FireEvent(event string, data EventData)](#EventCache.FireEvent) + * [func (evc *EventCache) Flush()](#EventCache.Flush) +* [type EventCallback](#EventCallback) +* [type EventData](#EventData) +* [type EventSwitch](#EventSwitch) + * [func NewEventSwitch() EventSwitch](#NewEventSwitch) +* [type Eventable](#Eventable) +* [type Fireable](#Fireable) + + +#### Package files +[event_cache.go](/src/github.com/tendermint/tendermint/libs/events/event_cache.go) [events.go](/src/github.com/tendermint/tendermint/libs/events/events.go) + + + + + + +## type [EventCache](/src/target/event_cache.go?s=116:179#L5) +``` go +type EventCache struct { + // contains filtered or unexported fields +} +``` +An EventCache buffers events for a Fireable +All events are cached. Filtering happens on Flush + + + + + + + +### func [NewEventCache](/src/target/event_cache.go?s=239:284#L11) +``` go +func NewEventCache(evsw Fireable) *EventCache +``` +Create a new EventCache with an EventSwitch as backend + + + + + +### func (\*EventCache) [FireEvent](/src/target/event_cache.go?s=449:511#L24) +``` go +func (evc *EventCache) FireEvent(event string, data EventData) +``` +Cache an event to be fired upon finality. + + + + +### func (\*EventCache) [Flush](/src/target/event_cache.go?s=735:765#L31) +``` go +func (evc *EventCache) Flush() +``` +Fire events by running evsw.FireEvent on all cached events. Blocks. +Clears cached events + + + + +## type [EventCallback](/src/target/events.go?s=4201:4240#L185) +``` go +type EventCallback func(data EventData) +``` + + + + + + + + + +## type [EventData](/src/target/events.go?s=243:294#L14) +``` go +type EventData interface { +} +``` +Generic event data can be typed and registered with tendermint/go-amino +via concrete implementation of this interface + + + + + + + + + + +## type [EventSwitch](/src/target/events.go?s=560:771#L29) +``` go +type EventSwitch interface { + cmn.Service + Fireable + + AddListenerForEvent(listenerID, event string, cb EventCallback) + RemoveListenerForEvent(event string, listenerID string) + RemoveListener(listenerID string) +} +``` + + + + + + +### func [NewEventSwitch](/src/target/events.go?s=917:950#L46) +``` go +func NewEventSwitch() EventSwitch +``` + + + + +## type [Eventable](/src/target/events.go?s=378:440#L20) +``` go +type Eventable interface { + SetEventSwitch(evsw EventSwitch) +} +``` +reactors and other modules should export +this interface to become eventable + + + + + + + + + + +## type [Fireable](/src/target/events.go?s=490:558#L25) +``` go +type Fireable interface { + FireEvent(event string, data EventData) +} +``` +an event switch or cache implements fireable + + + + + + + + + + + + + + +- - - +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/libs/events/event_cache.go b/libs/events/event_cache.go new file mode 100644 index 000000000..f508e873d --- /dev/null +++ b/libs/events/event_cache.go @@ -0,0 +1,37 @@ +package events + +// An EventCache buffers events for a Fireable +// All events are cached. Filtering happens on Flush +type EventCache struct { + evsw Fireable + events []eventInfo +} + +// Create a new EventCache with an EventSwitch as backend +func NewEventCache(evsw Fireable) *EventCache { + return &EventCache{ + evsw: evsw, + } +} + +// a cached event +type eventInfo struct { + event string + data EventData +} + +// Cache an event to be fired upon finality. +func (evc *EventCache) FireEvent(event string, data EventData) { + // append to list (go will grow our backing array exponentially) + evc.events = append(evc.events, eventInfo{event, data}) +} + +// Fire events by running evsw.FireEvent on all cached events. Blocks. +// Clears cached events +func (evc *EventCache) Flush() { + for _, ei := range evc.events { + evc.evsw.FireEvent(ei.event, ei.data) + } + // Clear the buffer, since we only add to it with append it's safe to just set it to nil and maybe safe an allocation + evc.events = nil +} diff --git a/libs/events/event_cache_test.go b/libs/events/event_cache_test.go new file mode 100644 index 000000000..ab321da3a --- /dev/null +++ b/libs/events/event_cache_test.go @@ -0,0 +1,35 @@ +package events + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEventCache_Flush(t *testing.T) { + evsw := NewEventSwitch() + evsw.Start() + evsw.AddListenerForEvent("nothingness", "", func(data EventData) { + // Check we are not initialising an empty buffer full of zeroed eventInfos in the EventCache + require.FailNow(t, "We should never receive a message on this switch since none are fired") + }) + evc := NewEventCache(evsw) + evc.Flush() + // Check after reset + evc.Flush() + fail := true + pass := false + evsw.AddListenerForEvent("somethingness", "something", func(data EventData) { + if fail { + require.FailNow(t, "Shouldn't see a message until flushed") + } + pass = true + }) + evc.FireEvent("something", struct{ int }{1}) + evc.FireEvent("something", struct{ int }{2}) + evc.FireEvent("something", struct{ int }{3}) + fail = false + evc.Flush() + assert.True(t, pass) +} diff --git a/libs/events/events.go b/libs/events/events.go new file mode 100644 index 000000000..f1b2a754e --- /dev/null +++ b/libs/events/events.go @@ -0,0 +1,226 @@ +/* +Pub-Sub in go with event caching +*/ +package events + +import ( + "sync" + + cmn "github.com/tendermint/tmlibs/common" +) + +// Generic event data can be typed and registered with tendermint/go-amino +// via concrete implementation of this interface +type EventData interface { + //AssertIsEventData() +} + +// reactors and other modules should export +// this interface to become eventable +type Eventable interface { + SetEventSwitch(evsw EventSwitch) +} + +// an event switch or cache implements fireable +type Fireable interface { + FireEvent(event string, data EventData) +} + +type EventSwitch interface { + cmn.Service + Fireable + + AddListenerForEvent(listenerID, event string, cb EventCallback) + RemoveListenerForEvent(event string, listenerID string) + RemoveListener(listenerID string) +} + +type eventSwitch struct { + cmn.BaseService + + mtx sync.RWMutex + eventCells map[string]*eventCell + listeners map[string]*eventListener +} + +func NewEventSwitch() EventSwitch { + evsw := &eventSwitch{} + evsw.BaseService = *cmn.NewBaseService(nil, "EventSwitch", evsw) + return evsw +} + +func (evsw *eventSwitch) OnStart() error { + evsw.BaseService.OnStart() + evsw.eventCells = make(map[string]*eventCell) + evsw.listeners = make(map[string]*eventListener) + return nil +} + +func (evsw *eventSwitch) OnStop() { + evsw.mtx.Lock() + defer evsw.mtx.Unlock() + evsw.BaseService.OnStop() + evsw.eventCells = nil + evsw.listeners = nil +} + +func (evsw *eventSwitch) AddListenerForEvent(listenerID, event string, cb EventCallback) { + // Get/Create eventCell and listener + evsw.mtx.Lock() + eventCell := evsw.eventCells[event] + if eventCell == nil { + eventCell = newEventCell() + evsw.eventCells[event] = eventCell + } + listener := evsw.listeners[listenerID] + if listener == nil { + listener = newEventListener(listenerID) + evsw.listeners[listenerID] = listener + } + evsw.mtx.Unlock() + + // Add event and listener + eventCell.AddListener(listenerID, cb) + listener.AddEvent(event) +} + +func (evsw *eventSwitch) RemoveListener(listenerID string) { + // Get and remove listener + evsw.mtx.RLock() + listener := evsw.listeners[listenerID] + evsw.mtx.RUnlock() + if listener == nil { + return + } + + evsw.mtx.Lock() + delete(evsw.listeners, listenerID) + evsw.mtx.Unlock() + + // Remove callback for each event. + listener.SetRemoved() + for _, event := range listener.GetEvents() { + evsw.RemoveListenerForEvent(event, listenerID) + } +} + +func (evsw *eventSwitch) RemoveListenerForEvent(event string, listenerID string) { + // Get eventCell + evsw.mtx.Lock() + eventCell := evsw.eventCells[event] + evsw.mtx.Unlock() + + if eventCell == nil { + return + } + + // Remove listenerID from eventCell + numListeners := eventCell.RemoveListener(listenerID) + + // Maybe garbage collect eventCell. + if numListeners == 0 { + // Lock again and double check. + evsw.mtx.Lock() // OUTER LOCK + eventCell.mtx.Lock() // INNER LOCK + if len(eventCell.listeners) == 0 { + delete(evsw.eventCells, event) + } + eventCell.mtx.Unlock() // INNER LOCK + evsw.mtx.Unlock() // OUTER LOCK + } +} + +func (evsw *eventSwitch) FireEvent(event string, data EventData) { + // Get the eventCell + evsw.mtx.RLock() + eventCell := evsw.eventCells[event] + evsw.mtx.RUnlock() + + if eventCell == nil { + return + } + + // Fire event for all listeners in eventCell + eventCell.FireEvent(data) +} + +//----------------------------------------------------------------------------- + +// eventCell handles keeping track of listener callbacks for a given event. +type eventCell struct { + mtx sync.RWMutex + listeners map[string]EventCallback +} + +func newEventCell() *eventCell { + return &eventCell{ + listeners: make(map[string]EventCallback), + } +} + +func (cell *eventCell) AddListener(listenerID string, cb EventCallback) { + cell.mtx.Lock() + cell.listeners[listenerID] = cb + cell.mtx.Unlock() +} + +func (cell *eventCell) RemoveListener(listenerID string) int { + cell.mtx.Lock() + delete(cell.listeners, listenerID) + numListeners := len(cell.listeners) + cell.mtx.Unlock() + return numListeners +} + +func (cell *eventCell) FireEvent(data EventData) { + cell.mtx.RLock() + for _, listener := range cell.listeners { + listener(data) + } + cell.mtx.RUnlock() +} + +//----------------------------------------------------------------------------- + +type EventCallback func(data EventData) + +type eventListener struct { + id string + + mtx sync.RWMutex + removed bool + events []string +} + +func newEventListener(id string) *eventListener { + return &eventListener{ + id: id, + removed: false, + events: nil, + } +} + +func (evl *eventListener) AddEvent(event string) { + evl.mtx.Lock() + defer evl.mtx.Unlock() + + if evl.removed { + return + } + evl.events = append(evl.events, event) +} + +func (evl *eventListener) GetEvents() []string { + evl.mtx.RLock() + defer evl.mtx.RUnlock() + + events := make([]string, len(evl.events)) + copy(events, evl.events) + return events +} + +func (evl *eventListener) SetRemoved() { + evl.mtx.Lock() + defer evl.mtx.Unlock() + evl.removed = true +} diff --git a/libs/events/events_test.go b/libs/events/events_test.go new file mode 100644 index 000000000..4995ae730 --- /dev/null +++ b/libs/events/events_test.go @@ -0,0 +1,380 @@ +package events + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// TestAddListenerForEventFireOnce sets up an EventSwitch, subscribes a single +// listener to an event, and sends a string "data". +func TestAddListenerForEventFireOnce(t *testing.T) { + evsw := NewEventSwitch() + err := evsw.Start() + if err != nil { + t.Errorf("Failed to start EventSwitch, error: %v", err) + } + messages := make(chan EventData) + evsw.AddListenerForEvent("listener", "event", + func(data EventData) { + messages <- data + }) + go evsw.FireEvent("event", "data") + received := <-messages + if received != "data" { + t.Errorf("Message received does not match: %v", received) + } +} + +// TestAddListenerForEventFireMany sets up an EventSwitch, subscribes a single +// listener to an event, and sends a thousand integers. +func TestAddListenerForEventFireMany(t *testing.T) { + evsw := NewEventSwitch() + err := evsw.Start() + if err != nil { + t.Errorf("Failed to start EventSwitch, error: %v", err) + } + doneSum := make(chan uint64) + doneSending := make(chan uint64) + numbers := make(chan uint64, 4) + // subscribe one listener for one event + evsw.AddListenerForEvent("listener", "event", + func(data EventData) { + numbers <- data.(uint64) + }) + // collect received events + go sumReceivedNumbers(numbers, doneSum) + // go fire events + go fireEvents(evsw, "event", doneSending, uint64(1)) + checkSum := <-doneSending + close(numbers) + eventSum := <-doneSum + if checkSum != eventSum { + t.Errorf("Not all messages sent were received.\n") + } +} + +// TestAddListenerForDifferentEvents sets up an EventSwitch, subscribes a single +// listener to three different events and sends a thousand integers for each +// of the three events. +func TestAddListenerForDifferentEvents(t *testing.T) { + evsw := NewEventSwitch() + err := evsw.Start() + if err != nil { + t.Errorf("Failed to start EventSwitch, error: %v", err) + } + doneSum := make(chan uint64) + doneSending1 := make(chan uint64) + doneSending2 := make(chan uint64) + doneSending3 := make(chan uint64) + numbers := make(chan uint64, 4) + // subscribe one listener to three events + evsw.AddListenerForEvent("listener", "event1", + func(data EventData) { + numbers <- data.(uint64) + }) + evsw.AddListenerForEvent("listener", "event2", + func(data EventData) { + numbers <- data.(uint64) + }) + evsw.AddListenerForEvent("listener", "event3", + func(data EventData) { + numbers <- data.(uint64) + }) + // collect received events + go sumReceivedNumbers(numbers, doneSum) + // go fire events + go fireEvents(evsw, "event1", doneSending1, uint64(1)) + go fireEvents(evsw, "event2", doneSending2, uint64(1)) + go fireEvents(evsw, "event3", doneSending3, uint64(1)) + var checkSum uint64 = 0 + checkSum += <-doneSending1 + checkSum += <-doneSending2 + checkSum += <-doneSending3 + close(numbers) + eventSum := <-doneSum + if checkSum != eventSum { + t.Errorf("Not all messages sent were received.\n") + } +} + +// TestAddDifferentListenerForDifferentEvents sets up an EventSwitch, +// subscribes a first listener to three events, and subscribes a second +// listener to two of those three events, and then sends a thousand integers +// for each of the three events. +func TestAddDifferentListenerForDifferentEvents(t *testing.T) { + evsw := NewEventSwitch() + err := evsw.Start() + if err != nil { + t.Errorf("Failed to start EventSwitch, error: %v", err) + } + doneSum1 := make(chan uint64) + doneSum2 := make(chan uint64) + doneSending1 := make(chan uint64) + doneSending2 := make(chan uint64) + doneSending3 := make(chan uint64) + numbers1 := make(chan uint64, 4) + numbers2 := make(chan uint64, 4) + // subscribe two listener to three events + evsw.AddListenerForEvent("listener1", "event1", + func(data EventData) { + numbers1 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener1", "event2", + func(data EventData) { + numbers1 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener1", "event3", + func(data EventData) { + numbers1 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener2", "event2", + func(data EventData) { + numbers2 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener2", "event3", + func(data EventData) { + numbers2 <- data.(uint64) + }) + // collect received events for listener1 + go sumReceivedNumbers(numbers1, doneSum1) + // collect received events for listener2 + go sumReceivedNumbers(numbers2, doneSum2) + // go fire events + go fireEvents(evsw, "event1", doneSending1, uint64(1)) + go fireEvents(evsw, "event2", doneSending2, uint64(1001)) + go fireEvents(evsw, "event3", doneSending3, uint64(2001)) + checkSumEvent1 := <-doneSending1 + checkSumEvent2 := <-doneSending2 + checkSumEvent3 := <-doneSending3 + checkSum1 := checkSumEvent1 + checkSumEvent2 + checkSumEvent3 + checkSum2 := checkSumEvent2 + checkSumEvent3 + close(numbers1) + close(numbers2) + eventSum1 := <-doneSum1 + eventSum2 := <-doneSum2 + if checkSum1 != eventSum1 || + checkSum2 != eventSum2 { + t.Errorf("Not all messages sent were received for different listeners to different events.\n") + } +} + +// TestAddAndRemoveListener sets up an EventSwitch, subscribes a listener to +// two events, fires a thousand integers for the first event, then unsubscribes +// the listener and fires a thousand integers for the second event. +func TestAddAndRemoveListener(t *testing.T) { + evsw := NewEventSwitch() + err := evsw.Start() + if err != nil { + t.Errorf("Failed to start EventSwitch, error: %v", err) + } + doneSum1 := make(chan uint64) + doneSum2 := make(chan uint64) + doneSending1 := make(chan uint64) + doneSending2 := make(chan uint64) + numbers1 := make(chan uint64, 4) + numbers2 := make(chan uint64, 4) + // subscribe two listener to three events + evsw.AddListenerForEvent("listener", "event1", + func(data EventData) { + numbers1 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener", "event2", + func(data EventData) { + numbers2 <- data.(uint64) + }) + // collect received events for event1 + go sumReceivedNumbers(numbers1, doneSum1) + // collect received events for event2 + go sumReceivedNumbers(numbers2, doneSum2) + // go fire events + go fireEvents(evsw, "event1", doneSending1, uint64(1)) + checkSumEvent1 := <-doneSending1 + // after sending all event1, unsubscribe for all events + evsw.RemoveListener("listener") + go fireEvents(evsw, "event2", doneSending2, uint64(1001)) + checkSumEvent2 := <-doneSending2 + close(numbers1) + close(numbers2) + eventSum1 := <-doneSum1 + eventSum2 := <-doneSum2 + if checkSumEvent1 != eventSum1 || + // correct value asserted by preceding tests, suffices to be non-zero + checkSumEvent2 == uint64(0) || + eventSum2 != uint64(0) { + t.Errorf("Not all messages sent were received or unsubscription did not register.\n") + } +} + +// TestRemoveListener does basic tests on adding and removing +func TestRemoveListener(t *testing.T) { + evsw := NewEventSwitch() + err := evsw.Start() + if err != nil { + t.Errorf("Failed to start EventSwitch, error: %v", err) + } + count := 10 + sum1, sum2 := 0, 0 + // add some listeners and make sure they work + evsw.AddListenerForEvent("listener", "event1", + func(data EventData) { + sum1++ + }) + evsw.AddListenerForEvent("listener", "event2", + func(data EventData) { + sum2++ + }) + for i := 0; i < count; i++ { + evsw.FireEvent("event1", true) + evsw.FireEvent("event2", true) + } + assert.Equal(t, count, sum1) + assert.Equal(t, count, sum2) + + // remove one by event and make sure it is gone + evsw.RemoveListenerForEvent("event2", "listener") + for i := 0; i < count; i++ { + evsw.FireEvent("event1", true) + evsw.FireEvent("event2", true) + } + assert.Equal(t, count*2, sum1) + assert.Equal(t, count, sum2) + + // remove the listener entirely and make sure both gone + evsw.RemoveListener("listener") + for i := 0; i < count; i++ { + evsw.FireEvent("event1", true) + evsw.FireEvent("event2", true) + } + assert.Equal(t, count*2, sum1) + assert.Equal(t, count, sum2) +} + +// TestAddAndRemoveListenersAsync sets up an EventSwitch, subscribes two +// listeners to three events, and fires a thousand integers for each event. +// These two listeners serve as the baseline validation while other listeners +// are randomly subscribed and unsubscribed. +// More precisely it randomly subscribes new listeners (different from the first +// two listeners) to one of these three events. At the same time it starts +// randomly unsubscribing these additional listeners from all events they are +// at that point subscribed to. +// NOTE: it is important to run this test with race conditions tracking on, +// `go test -race`, to examine for possible race conditions. +func TestRemoveListenersAsync(t *testing.T) { + evsw := NewEventSwitch() + err := evsw.Start() + if err != nil { + t.Errorf("Failed to start EventSwitch, error: %v", err) + } + doneSum1 := make(chan uint64) + doneSum2 := make(chan uint64) + doneSending1 := make(chan uint64) + doneSending2 := make(chan uint64) + doneSending3 := make(chan uint64) + numbers1 := make(chan uint64, 4) + numbers2 := make(chan uint64, 4) + // subscribe two listener to three events + evsw.AddListenerForEvent("listener1", "event1", + func(data EventData) { + numbers1 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener1", "event2", + func(data EventData) { + numbers1 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener1", "event3", + func(data EventData) { + numbers1 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener2", "event1", + func(data EventData) { + numbers2 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener2", "event2", + func(data EventData) { + numbers2 <- data.(uint64) + }) + evsw.AddListenerForEvent("listener2", "event3", + func(data EventData) { + numbers2 <- data.(uint64) + }) + // collect received events for event1 + go sumReceivedNumbers(numbers1, doneSum1) + // collect received events for event2 + go sumReceivedNumbers(numbers2, doneSum2) + addListenersStress := func() { + s1 := rand.NewSource(time.Now().UnixNano()) + r1 := rand.New(s1) + for k := uint16(0); k < 400; k++ { + listenerNumber := r1.Intn(100) + 3 + eventNumber := r1.Intn(3) + 1 + go evsw.AddListenerForEvent(fmt.Sprintf("listener%v", listenerNumber), + fmt.Sprintf("event%v", eventNumber), + func(_ EventData) {}) + } + } + removeListenersStress := func() { + s2 := rand.NewSource(time.Now().UnixNano()) + r2 := rand.New(s2) + for k := uint16(0); k < 80; k++ { + listenerNumber := r2.Intn(100) + 3 + go evsw.RemoveListener(fmt.Sprintf("listener%v", listenerNumber)) + } + } + addListenersStress() + // go fire events + go fireEvents(evsw, "event1", doneSending1, uint64(1)) + removeListenersStress() + go fireEvents(evsw, "event2", doneSending2, uint64(1001)) + go fireEvents(evsw, "event3", doneSending3, uint64(2001)) + checkSumEvent1 := <-doneSending1 + checkSumEvent2 := <-doneSending2 + checkSumEvent3 := <-doneSending3 + checkSum := checkSumEvent1 + checkSumEvent2 + checkSumEvent3 + close(numbers1) + close(numbers2) + eventSum1 := <-doneSum1 + eventSum2 := <-doneSum2 + if checkSum != eventSum1 || + checkSum != eventSum2 { + t.Errorf("Not all messages sent were received.\n") + } +} + +//------------------------------------------------------------------------------ +// Helper functions + +// sumReceivedNumbers takes two channels and adds all numbers received +// until the receiving channel `numbers` is closed; it then sends the sum +// on `doneSum` and closes that channel. Expected to be run in a go-routine. +func sumReceivedNumbers(numbers, doneSum chan uint64) { + var sum uint64 = 0 + for { + j, more := <-numbers + sum += j + if !more { + doneSum <- sum + close(doneSum) + return + } + } +} + +// fireEvents takes an EventSwitch and fires a thousand integers under +// a given `event` with the integers mootonically increasing from `offset` +// to `offset` + 999. It additionally returns the addition of all integers +// sent on `doneChan` for assertion that all events have been sent, and enabling +// the test to assert all events have also been received. +func fireEvents(evsw EventSwitch, event string, doneChan chan uint64, + offset uint64) { + var sentSum uint64 = 0 + for i := offset; i <= offset+uint64(999); i++ { + sentSum += i + evsw.FireEvent(event, i) + } + doneChan <- sentSum + close(doneChan) +} diff --git a/libs/pubsub/example_test.go b/libs/pubsub/example_test.go new file mode 100644 index 000000000..550b4447e --- /dev/null +++ b/libs/pubsub/example_test.go @@ -0,0 +1,28 @@ +package pubsub_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/tendermint/libs/pubsub" + "github.com/tendermint/tendermint/libs/pubsub/query" +) + +func TestExample(t *testing.T) { + s := pubsub.NewServer() + s.SetLogger(log.TestingLogger()) + s.Start() + defer s.Stop() + + ctx := context.Background() + ch := make(chan interface{}, 1) + err := s.Subscribe(ctx, "example-client", query.MustParse("abci.account.name='John'"), ch) + require.NoError(t, err) + err = s.PublishWithTags(ctx, "Tombstone", pubsub.NewTagMap(map[string]interface{}{"abci.account.name": "John"})) + require.NoError(t, err) + assertReceive(t, "Tombstone", ch) +} diff --git a/libs/pubsub/pubsub.go b/libs/pubsub/pubsub.go new file mode 100644 index 000000000..67f264ace --- /dev/null +++ b/libs/pubsub/pubsub.go @@ -0,0 +1,342 @@ +// Package pubsub implements a pub-sub model with a single publisher (Server) +// and multiple subscribers (clients). +// +// Though you can have multiple publishers by sharing a pointer to a server or +// by giving the same channel to each publisher and publishing messages from +// that channel (fan-in). +// +// Clients subscribe for messages, which could be of any type, using a query. +// When some message is published, we match it with all queries. If there is a +// match, this message will be pushed to all clients, subscribed to that query. +// See query subpackage for our implementation. +package pubsub + +import ( + "context" + "errors" + "sync" + + cmn "github.com/tendermint/tmlibs/common" +) + +type operation int + +const ( + sub operation = iota + pub + unsub + shutdown +) + +var ( + // ErrSubscriptionNotFound is returned when a client tries to unsubscribe + // from not existing subscription. + ErrSubscriptionNotFound = errors.New("subscription not found") + + // ErrAlreadySubscribed is returned when a client tries to subscribe twice or + // more using the same query. + ErrAlreadySubscribed = errors.New("already subscribed") +) + +// TagMap is used to associate tags to a message. +// They can be queried by subscribers to choose messages they will received. +type TagMap interface { + // Get returns the value for a key, or nil if no value is present. + // The ok result indicates whether value was found in the tags. + Get(key string) (value interface{}, ok bool) + // Len returns the number of tags. + Len() int +} + +type tagMap map[string]interface{} + +type cmd struct { + op operation + query Query + ch chan<- interface{} + clientID string + msg interface{} + tags TagMap +} + +// Query defines an interface for a query to be used for subscribing. +type Query interface { + Matches(tags TagMap) bool + String() string +} + +// Server allows clients to subscribe/unsubscribe for messages, publishing +// messages with or without tags, and manages internal state. +type Server struct { + cmn.BaseService + + cmds chan cmd + cmdsCap int + + mtx sync.RWMutex + subscriptions map[string]map[string]Query // subscriber -> query (string) -> Query +} + +// Option sets a parameter for the server. +type Option func(*Server) + +// NewTagMap constructs a new immutable tag set from a map. +func NewTagMap(data map[string]interface{}) TagMap { + return tagMap(data) +} + +// Get returns the value for a key, or nil if no value is present. +// The ok result indicates whether value was found in the tags. +func (ts tagMap) Get(key string) (value interface{}, ok bool) { + value, ok = ts[key] + return +} + +// Len returns the number of tags. +func (ts tagMap) Len() int { + return len(ts) +} + +// NewServer returns a new server. See the commentary on the Option functions +// for a detailed description of how to configure buffering. If no options are +// provided, the resulting server's queue is unbuffered. +func NewServer(options ...Option) *Server { + s := &Server{ + subscriptions: make(map[string]map[string]Query), + } + s.BaseService = *cmn.NewBaseService(nil, "PubSub", s) + + for _, option := range options { + option(s) + } + + // if BufferCapacity option was not set, the channel is unbuffered + s.cmds = make(chan cmd, s.cmdsCap) + + return s +} + +// BufferCapacity allows you to specify capacity for the internal server's +// queue. Since the server, given Y subscribers, could only process X messages, +// this option could be used to survive spikes (e.g. high amount of +// transactions during peak hours). +func BufferCapacity(cap int) Option { + return func(s *Server) { + if cap > 0 { + s.cmdsCap = cap + } + } +} + +// BufferCapacity returns capacity of the internal server's queue. +func (s *Server) BufferCapacity() int { + return s.cmdsCap +} + +// Subscribe creates a subscription for the given client. It accepts a channel +// on which messages matching the given query can be received. An error will be +// returned to the caller if the context is canceled or if subscription already +// exist for pair clientID and query. +func (s *Server) Subscribe(ctx context.Context, clientID string, query Query, out chan<- interface{}) error { + s.mtx.RLock() + clientSubscriptions, ok := s.subscriptions[clientID] + if ok { + _, ok = clientSubscriptions[query.String()] + } + s.mtx.RUnlock() + if ok { + return ErrAlreadySubscribed + } + + select { + case s.cmds <- cmd{op: sub, clientID: clientID, query: query, ch: out}: + s.mtx.Lock() + if _, ok = s.subscriptions[clientID]; !ok { + s.subscriptions[clientID] = make(map[string]Query) + } + s.subscriptions[clientID][query.String()] = query + s.mtx.Unlock() + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// Unsubscribe removes the subscription on the given query. An error will be +// returned to the caller if the context is canceled or if subscription does +// not exist. +func (s *Server) Unsubscribe(ctx context.Context, clientID string, query Query) error { + var origQuery Query + s.mtx.RLock() + clientSubscriptions, ok := s.subscriptions[clientID] + if ok { + origQuery, ok = clientSubscriptions[query.String()] + } + s.mtx.RUnlock() + if !ok { + return ErrSubscriptionNotFound + } + + // original query is used here because we're using pointers as map keys + select { + case s.cmds <- cmd{op: unsub, clientID: clientID, query: origQuery}: + s.mtx.Lock() + delete(clientSubscriptions, query.String()) + s.mtx.Unlock() + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// UnsubscribeAll removes all client subscriptions. An error will be returned +// to the caller if the context is canceled or if subscription does not exist. +func (s *Server) UnsubscribeAll(ctx context.Context, clientID string) error { + s.mtx.RLock() + _, ok := s.subscriptions[clientID] + s.mtx.RUnlock() + if !ok { + return ErrSubscriptionNotFound + } + + select { + case s.cmds <- cmd{op: unsub, clientID: clientID}: + s.mtx.Lock() + delete(s.subscriptions, clientID) + s.mtx.Unlock() + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// Publish publishes the given message. An error will be returned to the caller +// if the context is canceled. +func (s *Server) Publish(ctx context.Context, msg interface{}) error { + return s.PublishWithTags(ctx, msg, NewTagMap(make(map[string]interface{}))) +} + +// PublishWithTags publishes the given message with the set of tags. The set is +// matched with clients queries. If there is a match, the message is sent to +// the client. +func (s *Server) PublishWithTags(ctx context.Context, msg interface{}, tags TagMap) error { + select { + case s.cmds <- cmd{op: pub, msg: msg, tags: tags}: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// OnStop implements Service.OnStop by shutting down the server. +func (s *Server) OnStop() { + s.cmds <- cmd{op: shutdown} +} + +// NOTE: not goroutine safe +type state struct { + // query -> client -> ch + queries map[Query]map[string]chan<- interface{} + // client -> query -> struct{} + clients map[string]map[Query]struct{} +} + +// OnStart implements Service.OnStart by starting the server. +func (s *Server) OnStart() error { + go s.loop(state{ + queries: make(map[Query]map[string]chan<- interface{}), + clients: make(map[string]map[Query]struct{}), + }) + return nil +} + +// OnReset implements Service.OnReset +func (s *Server) OnReset() error { + return nil +} + +func (s *Server) loop(state state) { +loop: + for cmd := range s.cmds { + switch cmd.op { + case unsub: + if cmd.query != nil { + state.remove(cmd.clientID, cmd.query) + } else { + state.removeAll(cmd.clientID) + } + case shutdown: + for clientID := range state.clients { + state.removeAll(clientID) + } + break loop + case sub: + state.add(cmd.clientID, cmd.query, cmd.ch) + case pub: + state.send(cmd.msg, cmd.tags) + } + } +} + +func (state *state) add(clientID string, q Query, ch chan<- interface{}) { + // add query if needed + if _, ok := state.queries[q]; !ok { + state.queries[q] = make(map[string]chan<- interface{}) + } + + // create subscription + state.queries[q][clientID] = ch + + // add client if needed + if _, ok := state.clients[clientID]; !ok { + state.clients[clientID] = make(map[Query]struct{}) + } + state.clients[clientID][q] = struct{}{} +} + +func (state *state) remove(clientID string, q Query) { + clientToChannelMap, ok := state.queries[q] + if !ok { + return + } + + ch, ok := clientToChannelMap[clientID] + if ok { + close(ch) + + delete(state.clients[clientID], q) + + // if it not subscribed to anything else, remove the client + if len(state.clients[clientID]) == 0 { + delete(state.clients, clientID) + } + + delete(state.queries[q], clientID) + } +} + +func (state *state) removeAll(clientID string) { + queryMap, ok := state.clients[clientID] + if !ok { + return + } + + for q := range queryMap { + ch := state.queries[q][clientID] + close(ch) + + delete(state.queries[q], clientID) + } + + delete(state.clients, clientID) +} + +func (state *state) send(msg interface{}, tags TagMap) { + for q, clientToChannelMap := range state.queries { + if q.Matches(tags) { + for _, ch := range clientToChannelMap { + ch <- msg + } + } + } +} diff --git a/libs/pubsub/pubsub_test.go b/libs/pubsub/pubsub_test.go new file mode 100644 index 000000000..a39d015ce --- /dev/null +++ b/libs/pubsub/pubsub_test.go @@ -0,0 +1,253 @@ +package pubsub_test + +import ( + "context" + "fmt" + "runtime/debug" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/tendermint/libs/pubsub" + "github.com/tendermint/tendermint/libs/pubsub/query" +) + +const ( + clientID = "test-client" +) + +func TestSubscribe(t *testing.T) { + s := pubsub.NewServer() + s.SetLogger(log.TestingLogger()) + s.Start() + defer s.Stop() + + ctx := context.Background() + ch := make(chan interface{}, 1) + err := s.Subscribe(ctx, clientID, query.Empty{}, ch) + require.NoError(t, err) + err = s.Publish(ctx, "Ka-Zar") + require.NoError(t, err) + assertReceive(t, "Ka-Zar", ch) + + err = s.Publish(ctx, "Quicksilver") + require.NoError(t, err) + assertReceive(t, "Quicksilver", ch) +} + +func TestDifferentClients(t *testing.T) { + s := pubsub.NewServer() + s.SetLogger(log.TestingLogger()) + s.Start() + defer s.Stop() + + ctx := context.Background() + ch1 := make(chan interface{}, 1) + err := s.Subscribe(ctx, "client-1", query.MustParse("tm.events.type='NewBlock'"), ch1) + require.NoError(t, err) + err = s.PublishWithTags(ctx, "Iceman", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewBlock"})) + require.NoError(t, err) + assertReceive(t, "Iceman", ch1) + + ch2 := make(chan interface{}, 1) + err = s.Subscribe(ctx, "client-2", query.MustParse("tm.events.type='NewBlock' AND abci.account.name='Igor'"), ch2) + require.NoError(t, err) + err = s.PublishWithTags(ctx, "Ultimo", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewBlock", "abci.account.name": "Igor"})) + require.NoError(t, err) + assertReceive(t, "Ultimo", ch1) + assertReceive(t, "Ultimo", ch2) + + ch3 := make(chan interface{}, 1) + err = s.Subscribe(ctx, "client-3", query.MustParse("tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10"), ch3) + require.NoError(t, err) + err = s.PublishWithTags(ctx, "Valeria Richards", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewRoundStep"})) + require.NoError(t, err) + assert.Zero(t, len(ch3)) +} + +func TestClientSubscribesTwice(t *testing.T) { + s := pubsub.NewServer() + s.SetLogger(log.TestingLogger()) + s.Start() + defer s.Stop() + + ctx := context.Background() + q := query.MustParse("tm.events.type='NewBlock'") + + ch1 := make(chan interface{}, 1) + err := s.Subscribe(ctx, clientID, q, ch1) + require.NoError(t, err) + err = s.PublishWithTags(ctx, "Goblin Queen", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewBlock"})) + require.NoError(t, err) + assertReceive(t, "Goblin Queen", ch1) + + ch2 := make(chan interface{}, 1) + err = s.Subscribe(ctx, clientID, q, ch2) + require.Error(t, err) + + err = s.PublishWithTags(ctx, "Spider-Man", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewBlock"})) + require.NoError(t, err) + assertReceive(t, "Spider-Man", ch1) +} + +func TestUnsubscribe(t *testing.T) { + s := pubsub.NewServer() + s.SetLogger(log.TestingLogger()) + s.Start() + defer s.Stop() + + ctx := context.Background() + ch := make(chan interface{}) + err := s.Subscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'"), ch) + require.NoError(t, err) + err = s.Unsubscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'")) + require.NoError(t, err) + + err = s.Publish(ctx, "Nick Fury") + require.NoError(t, err) + assert.Zero(t, len(ch), "Should not receive anything after Unsubscribe") + + _, ok := <-ch + assert.False(t, ok) +} + +func TestResubscribe(t *testing.T) { + s := pubsub.NewServer() + s.SetLogger(log.TestingLogger()) + s.Start() + defer s.Stop() + + ctx := context.Background() + ch := make(chan interface{}) + err := s.Subscribe(ctx, clientID, query.Empty{}, ch) + require.NoError(t, err) + err = s.Unsubscribe(ctx, clientID, query.Empty{}) + require.NoError(t, err) + ch = make(chan interface{}) + err = s.Subscribe(ctx, clientID, query.Empty{}, ch) + require.NoError(t, err) + + err = s.Publish(ctx, "Cable") + require.NoError(t, err) + assertReceive(t, "Cable", ch) +} + +func TestUnsubscribeAll(t *testing.T) { + s := pubsub.NewServer() + s.SetLogger(log.TestingLogger()) + s.Start() + defer s.Stop() + + ctx := context.Background() + ch1, ch2 := make(chan interface{}, 1), make(chan interface{}, 1) + err := s.Subscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'"), ch1) + require.NoError(t, err) + err = s.Subscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlockHeader'"), ch2) + require.NoError(t, err) + + err = s.UnsubscribeAll(ctx, clientID) + require.NoError(t, err) + + err = s.Publish(ctx, "Nick Fury") + require.NoError(t, err) + assert.Zero(t, len(ch1), "Should not receive anything after UnsubscribeAll") + assert.Zero(t, len(ch2), "Should not receive anything after UnsubscribeAll") + + _, ok := <-ch1 + assert.False(t, ok) + _, ok = <-ch2 + assert.False(t, ok) +} + +func TestBufferCapacity(t *testing.T) { + s := pubsub.NewServer(pubsub.BufferCapacity(2)) + s.SetLogger(log.TestingLogger()) + + assert.Equal(t, 2, s.BufferCapacity()) + + ctx := context.Background() + err := s.Publish(ctx, "Nighthawk") + require.NoError(t, err) + err = s.Publish(ctx, "Sage") + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond) + defer cancel() + err = s.Publish(ctx, "Ironclad") + if assert.Error(t, err) { + assert.Equal(t, context.DeadlineExceeded, err) + } +} + +func Benchmark10Clients(b *testing.B) { benchmarkNClients(10, b) } +func Benchmark100Clients(b *testing.B) { benchmarkNClients(100, b) } +func Benchmark1000Clients(b *testing.B) { benchmarkNClients(1000, b) } + +func Benchmark10ClientsOneQuery(b *testing.B) { benchmarkNClientsOneQuery(10, b) } +func Benchmark100ClientsOneQuery(b *testing.B) { benchmarkNClientsOneQuery(100, b) } +func Benchmark1000ClientsOneQuery(b *testing.B) { benchmarkNClientsOneQuery(1000, b) } + +func benchmarkNClients(n int, b *testing.B) { + s := pubsub.NewServer() + s.Start() + defer s.Stop() + + ctx := context.Background() + for i := 0; i < n; i++ { + ch := make(chan interface{}) + go func() { + for range ch { + } + }() + s.Subscribe(ctx, clientID, query.MustParse(fmt.Sprintf("abci.Account.Owner = 'Ivan' AND abci.Invoices.Number = %d", i)), ch) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.PublishWithTags(ctx, "Gamora", pubsub.NewTagMap(map[string]interface{}{"abci.Account.Owner": "Ivan", "abci.Invoices.Number": i})) + } +} + +func benchmarkNClientsOneQuery(n int, b *testing.B) { + s := pubsub.NewServer() + s.Start() + defer s.Stop() + + ctx := context.Background() + q := query.MustParse("abci.Account.Owner = 'Ivan' AND abci.Invoices.Number = 1") + for i := 0; i < n; i++ { + ch := make(chan interface{}) + go func() { + for range ch { + } + }() + s.Subscribe(ctx, clientID, q, ch) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.PublishWithTags(ctx, "Gamora", pubsub.NewTagMap(map[string]interface{}{"abci.Account.Owner": "Ivan", "abci.Invoices.Number": 1})) + } +} + +/////////////////////////////////////////////////////////////////////////////// +/// HELPERS +/////////////////////////////////////////////////////////////////////////////// + +func assertReceive(t *testing.T, expected interface{}, ch <-chan interface{}, msgAndArgs ...interface{}) { + select { + case actual := <-ch: + if actual != nil { + assert.Equal(t, expected, actual, msgAndArgs...) + } + case <-time.After(1 * time.Second): + t.Errorf("Expected to receive %v from the channel, got nothing after 1s", expected) + debug.PrintStack() + } +} diff --git a/libs/pubsub/query/Makefile b/libs/pubsub/query/Makefile new file mode 100644 index 000000000..91030ef09 --- /dev/null +++ b/libs/pubsub/query/Makefile @@ -0,0 +1,11 @@ +gen_query_parser: + @go get github.com/pointlander/peg + peg -inline -switch query.peg + +fuzzy_test: + @go get github.com/dvyukov/go-fuzz/go-fuzz + @go get github.com/dvyukov/go-fuzz/go-fuzz-build + go-fuzz-build github.com/tendermint/tendermint/libs/pubsub/query/fuzz_test + go-fuzz -bin=./fuzz_test-fuzz.zip -workdir=./fuzz_test/output + +.PHONY: gen_query_parser fuzzy_test diff --git a/libs/pubsub/query/empty.go b/libs/pubsub/query/empty.go new file mode 100644 index 000000000..17d7acefa --- /dev/null +++ b/libs/pubsub/query/empty.go @@ -0,0 +1,16 @@ +package query + +import "github.com/tendermint/tendermint/libs/pubsub" + +// Empty query matches any set of tags. +type Empty struct { +} + +// Matches always returns true. +func (Empty) Matches(tags pubsub.TagMap) bool { + return true +} + +func (Empty) String() string { + return "empty" +} diff --git a/libs/pubsub/query/empty_test.go b/libs/pubsub/query/empty_test.go new file mode 100644 index 000000000..9c82f73ed --- /dev/null +++ b/libs/pubsub/query/empty_test.go @@ -0,0 +1,18 @@ +package query_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tendermint/tendermint/libs/pubsub" + "github.com/tendermint/tendermint/libs/pubsub/query" +) + +func TestEmptyQueryMatchesAnything(t *testing.T) { + q := query.Empty{} + assert.True(t, q.Matches(pubsub.NewTagMap(map[string]interface{}{}))) + assert.True(t, q.Matches(pubsub.NewTagMap(map[string]interface{}{"Asher": "Roth"}))) + assert.True(t, q.Matches(pubsub.NewTagMap(map[string]interface{}{"Route": 66}))) + assert.True(t, q.Matches(pubsub.NewTagMap(map[string]interface{}{"Route": 66, "Billy": "Blue"}))) +} diff --git a/libs/pubsub/query/fuzz_test/main.go b/libs/pubsub/query/fuzz_test/main.go new file mode 100644 index 000000000..7a46116b5 --- /dev/null +++ b/libs/pubsub/query/fuzz_test/main.go @@ -0,0 +1,30 @@ +package fuzz_test + +import ( + "fmt" + + "github.com/tendermint/tendermint/libs/pubsub/query" +) + +func Fuzz(data []byte) int { + sdata := string(data) + q0, err := query.New(sdata) + if err != nil { + return 0 + } + + sdata1 := q0.String() + q1, err := query.New(sdata1) + if err != nil { + panic(err) + } + + sdata2 := q1.String() + if sdata1 != sdata2 { + fmt.Printf("q0: %q\n", sdata1) + fmt.Printf("q1: %q\n", sdata2) + panic("query changed") + } + + return 1 +} diff --git a/libs/pubsub/query/parser_test.go b/libs/pubsub/query/parser_test.go new file mode 100644 index 000000000..708dee484 --- /dev/null +++ b/libs/pubsub/query/parser_test.go @@ -0,0 +1,92 @@ +package query_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tendermint/tendermint/libs/pubsub/query" +) + +// TODO: fuzzy testing? +func TestParser(t *testing.T) { + cases := []struct { + query string + valid bool + }{ + {"tm.events.type='NewBlock'", true}, + {"tm.events.type = 'NewBlock'", true}, + {"tm.events.name = ''", true}, + {"tm.events.type='TIME'", true}, + {"tm.events.type='DATE'", true}, + {"tm.events.type='='", true}, + {"tm.events.type='TIME", false}, + {"tm.events.type=TIME'", false}, + {"tm.events.type==", false}, + {"tm.events.type=NewBlock", false}, + {">==", false}, + {"tm.events.type 'NewBlock' =", false}, + {"tm.events.type>'NewBlock'", false}, + {"", false}, + {"=", false}, + {"='NewBlock'", false}, + {"tm.events.type=", false}, + + {"tm.events.typeNewBlock", false}, + {"tm.events.type'NewBlock'", false}, + {"'NewBlock'", false}, + {"NewBlock", false}, + {"", false}, + + {"tm.events.type='NewBlock' AND abci.account.name='Igor'", true}, + {"tm.events.type='NewBlock' AND", false}, + {"tm.events.type='NewBlock' AN", false}, + {"tm.events.type='NewBlock' AN tm.events.type='NewBlockHeader'", false}, + {"AND tm.events.type='NewBlock' ", false}, + + {"abci.account.name CONTAINS 'Igor'", true}, + + {"tx.date > DATE 2013-05-03", true}, + {"tx.date < DATE 2013-05-03", true}, + {"tx.date <= DATE 2013-05-03", true}, + {"tx.date >= DATE 2013-05-03", true}, + {"tx.date >= DAT 2013-05-03", false}, + {"tx.date <= DATE2013-05-03", false}, + {"tx.date <= DATE -05-03", false}, + {"tx.date >= DATE 20130503", false}, + {"tx.date >= DATE 2013+01-03", false}, + // incorrect year, month, day + {"tx.date >= DATE 0013-01-03", false}, + {"tx.date >= DATE 2013-31-03", false}, + {"tx.date >= DATE 2013-01-83", false}, + + {"tx.date > TIME 2013-05-03T14:45:00+07:00", true}, + {"tx.date < TIME 2013-05-03T14:45:00-02:00", true}, + {"tx.date <= TIME 2013-05-03T14:45:00Z", true}, + {"tx.date >= TIME 2013-05-03T14:45:00Z", true}, + {"tx.date >= TIME2013-05-03T14:45:00Z", false}, + {"tx.date = IME 2013-05-03T14:45:00Z", false}, + {"tx.date = TIME 2013-05-:45:00Z", false}, + {"tx.date >= TIME 2013-05-03T14:45:00", false}, + {"tx.date >= TIME 0013-00-00T14:45:00Z", false}, + {"tx.date >= TIME 2013+05=03T14:45:00Z", false}, + + {"account.balance=100", true}, + {"account.balance >= 200", true}, + {"account.balance >= -300", false}, + {"account.balance >>= 400", false}, + {"account.balance=33.22.1", false}, + + {"hash='136E18F7E4C348B780CF873A0BF43922E5BAFA63'", true}, + {"hash=136E18F7E4C348B780CF873A0BF43922E5BAFA63", false}, + } + + for _, c := range cases { + _, err := query.New(c.query) + if c.valid { + assert.NoErrorf(t, err, "Query was '%s'", c.query) + } else { + assert.Errorf(t, err, "Query was '%s'", c.query) + } + } +} diff --git a/libs/pubsub/query/query.go b/libs/pubsub/query/query.go new file mode 100644 index 000000000..a900d9838 --- /dev/null +++ b/libs/pubsub/query/query.go @@ -0,0 +1,345 @@ +// Package query provides a parser for a custom query format: +// +// abci.invoice.number=22 AND abci.invoice.owner=Ivan +// +// See query.peg for the grammar, which is a https://en.wikipedia.org/wiki/Parsing_expression_grammar. +// More: https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics +// +// It has a support for numbers (integer and floating point), dates and times. +package query + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "github.com/tendermint/tendermint/libs/pubsub" +) + +// Query holds the query string and the query parser. +type Query struct { + str string + parser *QueryParser +} + +// Condition represents a single condition within a query and consists of tag +// (e.g. "tx.gas"), operator (e.g. "=") and operand (e.g. "7"). +type Condition struct { + Tag string + Op Operator + Operand interface{} +} + +// New parses the given string and returns a query or error if the string is +// invalid. +func New(s string) (*Query, error) { + p := &QueryParser{Buffer: fmt.Sprintf(`"%s"`, s)} + p.Init() + if err := p.Parse(); err != nil { + return nil, err + } + return &Query{str: s, parser: p}, nil +} + +// MustParse turns the given string into a query or panics; for tests or others +// cases where you know the string is valid. +func MustParse(s string) *Query { + q, err := New(s) + if err != nil { + panic(fmt.Sprintf("failed to parse %s: %v", s, err)) + } + return q +} + +// String returns the original string. +func (q *Query) String() string { + return q.str +} + +// Operator is an operator that defines some kind of relation between tag and +// operand (equality, etc.). +type Operator uint8 + +const ( + // "<=" + OpLessEqual Operator = iota + // ">=" + OpGreaterEqual + // "<" + OpLess + // ">" + OpGreater + // "=" + OpEqual + // "CONTAINS"; used to check if a string contains a certain sub string. + OpContains +) + +// Conditions returns a list of conditions. +func (q *Query) Conditions() []Condition { + conditions := make([]Condition, 0) + + buffer, begin, end := q.parser.Buffer, 0, 0 + + var tag string + var op Operator + + // tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7") + for _, token := range q.parser.Tokens() { + switch token.pegRule { + + case rulePegText: + begin, end = int(token.begin), int(token.end) + case ruletag: + tag = buffer[begin:end] + case rulele: + op = OpLessEqual + case rulege: + op = OpGreaterEqual + case rulel: + op = OpLess + case ruleg: + op = OpGreater + case ruleequal: + op = OpEqual + case rulecontains: + op = OpContains + case rulevalue: + // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") + valueWithoutSingleQuotes := buffer[begin+1 : end-1] + conditions = append(conditions, Condition{tag, op, valueWithoutSingleQuotes}) + case rulenumber: + number := buffer[begin:end] + if strings.Contains(number, ".") { // if it looks like a floating-point number + value, err := strconv.ParseFloat(number, 64) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number)) + } + conditions = append(conditions, Condition{tag, op, value}) + } else { + value, err := strconv.ParseInt(number, 10, 64) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", err, number)) + } + conditions = append(conditions, Condition{tag, op, value}) + } + case ruletime: + value, err := time.Parse(time.RFC3339, buffer[begin:end]) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end])) + } + conditions = append(conditions, Condition{tag, op, value}) + case ruledate: + value, err := time.Parse("2006-01-02", buffer[begin:end]) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)", err, buffer[begin:end])) + } + conditions = append(conditions, Condition{tag, op, value}) + } + } + + return conditions +} + +// Matches returns true if the query matches the given set of tags, false otherwise. +// +// For example, query "name=John" matches tags = {"name": "John"}. More +// examples could be found in parser_test.go and query_test.go. +func (q *Query) Matches(tags pubsub.TagMap) bool { + if tags.Len() == 0 { + return false + } + + buffer, begin, end := q.parser.Buffer, 0, 0 + + var tag string + var op Operator + + // tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7") + for _, token := range q.parser.Tokens() { + switch token.pegRule { + + case rulePegText: + begin, end = int(token.begin), int(token.end) + case ruletag: + tag = buffer[begin:end] + case rulele: + op = OpLessEqual + case rulege: + op = OpGreaterEqual + case rulel: + op = OpLess + case ruleg: + op = OpGreater + case ruleequal: + op = OpEqual + case rulecontains: + op = OpContains + case rulevalue: + // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") + valueWithoutSingleQuotes := buffer[begin+1 : end-1] + + // see if the triplet (tag, operator, operand) matches any tag + // "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" } + if !match(tag, op, reflect.ValueOf(valueWithoutSingleQuotes), tags) { + return false + } + case rulenumber: + number := buffer[begin:end] + if strings.Contains(number, ".") { // if it looks like a floating-point number + value, err := strconv.ParseFloat(number, 64) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number)) + } + if !match(tag, op, reflect.ValueOf(value), tags) { + return false + } + } else { + value, err := strconv.ParseInt(number, 10, 64) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", err, number)) + } + if !match(tag, op, reflect.ValueOf(value), tags) { + return false + } + } + case ruletime: + value, err := time.Parse(time.RFC3339, buffer[begin:end]) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end])) + } + if !match(tag, op, reflect.ValueOf(value), tags) { + return false + } + case ruledate: + value, err := time.Parse("2006-01-02", buffer[begin:end]) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)", err, buffer[begin:end])) + } + if !match(tag, op, reflect.ValueOf(value), tags) { + return false + } + } + } + + return true +} + +// match returns true if the given triplet (tag, operator, operand) matches any tag. +// +// First, it looks up the tag in tags and if it finds one, tries to compare the +// value from it to the operand using the operator. +// +// "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" } +func match(tag string, op Operator, operand reflect.Value, tags pubsub.TagMap) bool { + // look up the tag from the query in tags + value, ok := tags.Get(tag) + if !ok { + return false + } + switch operand.Kind() { + case reflect.Struct: // time + operandAsTime := operand.Interface().(time.Time) + v, ok := value.(time.Time) + if !ok { // if value from tags is not time.Time + return false + } + switch op { + case OpLessEqual: + return v.Before(operandAsTime) || v.Equal(operandAsTime) + case OpGreaterEqual: + return v.Equal(operandAsTime) || v.After(operandAsTime) + case OpLess: + return v.Before(operandAsTime) + case OpGreater: + return v.After(operandAsTime) + case OpEqual: + return v.Equal(operandAsTime) + } + case reflect.Float64: + operandFloat64 := operand.Interface().(float64) + var v float64 + // try our best to convert value from tags to float64 + switch vt := value.(type) { + case float64: + v = vt + case float32: + v = float64(vt) + case int: + v = float64(vt) + case int8: + v = float64(vt) + case int16: + v = float64(vt) + case int32: + v = float64(vt) + case int64: + v = float64(vt) + default: // fail for all other types + panic(fmt.Sprintf("Incomparable types: %T (%v) vs float64 (%v)", value, value, operandFloat64)) + } + switch op { + case OpLessEqual: + return v <= operandFloat64 + case OpGreaterEqual: + return v >= operandFloat64 + case OpLess: + return v < operandFloat64 + case OpGreater: + return v > operandFloat64 + case OpEqual: + return v == operandFloat64 + } + case reflect.Int64: + operandInt := operand.Interface().(int64) + var v int64 + // try our best to convert value from tags to int64 + switch vt := value.(type) { + case int64: + v = vt + case int8: + v = int64(vt) + case int16: + v = int64(vt) + case int32: + v = int64(vt) + case int: + v = int64(vt) + case float64: + v = int64(vt) + case float32: + v = int64(vt) + default: // fail for all other types + panic(fmt.Sprintf("Incomparable types: %T (%v) vs int64 (%v)", value, value, operandInt)) + } + switch op { + case OpLessEqual: + return v <= operandInt + case OpGreaterEqual: + return v >= operandInt + case OpLess: + return v < operandInt + case OpGreater: + return v > operandInt + case OpEqual: + return v == operandInt + } + case reflect.String: + v, ok := value.(string) + if !ok { // if value from tags is not string + return false + } + switch op { + case OpEqual: + return v == operand.String() + case OpContains: + return strings.Contains(v, operand.String()) + } + default: + panic(fmt.Sprintf("Unknown kind of operand %v", operand.Kind())) + } + + return false +} diff --git a/libs/pubsub/query/query.peg b/libs/pubsub/query/query.peg new file mode 100644 index 000000000..739892e4f --- /dev/null +++ b/libs/pubsub/query/query.peg @@ -0,0 +1,33 @@ +package query + +type QueryParser Peg { +} + +e <- '\"' condition ( ' '+ and ' '+ condition )* '\"' !. + +condition <- tag ' '* (le ' '* (number / time / date) + / ge ' '* (number / time / date) + / l ' '* (number / time / date) + / g ' '* (number / time / date) + / equal ' '* (number / time / date / value) + / contains ' '* value + ) + +tag <- < (![ \t\n\r\\()"'=><] .)+ > +value <- < '\'' (!["'] .)* '\''> +number <- < ('0' + / [1-9] digit* ('.' digit*)?) > +digit <- [0-9] +time <- "TIME " < year '-' month '-' day 'T' digit digit ':' digit digit ':' digit digit (('-' / '+') digit digit ':' digit digit / 'Z') > +date <- "DATE " < year '-' month '-' day > +year <- ('1' / '2') digit digit digit +month <- ('0' / '1') digit +day <- ('0' / '1' / '2' / '3') digit +and <- "AND" + +equal <- "=" +contains <- "CONTAINS" +le <- "<=" +ge <- ">=" +l <- "<" +g <- ">" diff --git a/libs/pubsub/query/query.peg.go b/libs/pubsub/query/query.peg.go new file mode 100644 index 000000000..c86e4a47f --- /dev/null +++ b/libs/pubsub/query/query.peg.go @@ -0,0 +1,1553 @@ +// nolint +package query + +import ( + "fmt" + "math" + "sort" + "strconv" +) + +const endSymbol rune = 1114112 + +/* The rule types inferred from the grammar are below. */ +type pegRule uint8 + +const ( + ruleUnknown pegRule = iota + rulee + rulecondition + ruletag + rulevalue + rulenumber + ruledigit + ruletime + ruledate + ruleyear + rulemonth + ruleday + ruleand + ruleequal + rulecontains + rulele + rulege + rulel + ruleg + rulePegText +) + +var rul3s = [...]string{ + "Unknown", + "e", + "condition", + "tag", + "value", + "number", + "digit", + "time", + "date", + "year", + "month", + "day", + "and", + "equal", + "contains", + "le", + "ge", + "l", + "g", + "PegText", +} + +type token32 struct { + pegRule + begin, end uint32 +} + +func (t *token32) String() string { + return fmt.Sprintf("\x1B[34m%v\x1B[m %v %v", rul3s[t.pegRule], t.begin, t.end) +} + +type node32 struct { + token32 + up, next *node32 +} + +func (node *node32) print(pretty bool, buffer string) { + var print func(node *node32, depth int) + print = func(node *node32, depth int) { + for node != nil { + for c := 0; c < depth; c++ { + fmt.Printf(" ") + } + rule := rul3s[node.pegRule] + quote := strconv.Quote(string(([]rune(buffer)[node.begin:node.end]))) + if !pretty { + fmt.Printf("%v %v\n", rule, quote) + } else { + fmt.Printf("\x1B[34m%v\x1B[m %v\n", rule, quote) + } + if node.up != nil { + print(node.up, depth+1) + } + node = node.next + } + } + print(node, 0) +} + +func (node *node32) Print(buffer string) { + node.print(false, buffer) +} + +func (node *node32) PrettyPrint(buffer string) { + node.print(true, buffer) +} + +type tokens32 struct { + tree []token32 +} + +func (t *tokens32) Trim(length uint32) { + t.tree = t.tree[:length] +} + +func (t *tokens32) Print() { + for _, token := range t.tree { + fmt.Println(token.String()) + } +} + +func (t *tokens32) AST() *node32 { + type element struct { + node *node32 + down *element + } + tokens := t.Tokens() + var stack *element + for _, token := range tokens { + if token.begin == token.end { + continue + } + node := &node32{token32: token} + for stack != nil && stack.node.begin >= token.begin && stack.node.end <= token.end { + stack.node.next = node.up + node.up = stack.node + stack = stack.down + } + stack = &element{node: node, down: stack} + } + if stack != nil { + return stack.node + } + return nil +} + +func (t *tokens32) PrintSyntaxTree(buffer string) { + t.AST().Print(buffer) +} + +func (t *tokens32) PrettyPrintSyntaxTree(buffer string) { + t.AST().PrettyPrint(buffer) +} + +func (t *tokens32) Add(rule pegRule, begin, end, index uint32) { + if tree := t.tree; int(index) >= len(tree) { + expanded := make([]token32, 2*len(tree)) + copy(expanded, tree) + t.tree = expanded + } + t.tree[index] = token32{ + pegRule: rule, + begin: begin, + end: end, + } +} + +func (t *tokens32) Tokens() []token32 { + return t.tree +} + +type QueryParser struct { + Buffer string + buffer []rune + rules [20]func() bool + parse func(rule ...int) error + reset func() + Pretty bool + tokens32 +} + +func (p *QueryParser) Parse(rule ...int) error { + return p.parse(rule...) +} + +func (p *QueryParser) Reset() { + p.reset() +} + +type textPosition struct { + line, symbol int +} + +type textPositionMap map[int]textPosition + +func translatePositions(buffer []rune, positions []int) textPositionMap { + length, translations, j, line, symbol := len(positions), make(textPositionMap, len(positions)), 0, 1, 0 + sort.Ints(positions) + +search: + for i, c := range buffer { + if c == '\n' { + line, symbol = line+1, 0 + } else { + symbol++ + } + if i == positions[j] { + translations[positions[j]] = textPosition{line, symbol} + for j++; j < length; j++ { + if i != positions[j] { + continue search + } + } + break search + } + } + + return translations +} + +type parseError struct { + p *QueryParser + max token32 +} + +func (e *parseError) Error() string { + tokens, error := []token32{e.max}, "\n" + positions, p := make([]int, 2*len(tokens)), 0 + for _, token := range tokens { + positions[p], p = int(token.begin), p+1 + positions[p], p = int(token.end), p+1 + } + translations := translatePositions(e.p.buffer, positions) + format := "parse error near %v (line %v symbol %v - line %v symbol %v):\n%v\n" + if e.p.Pretty { + format = "parse error near \x1B[34m%v\x1B[m (line %v symbol %v - line %v symbol %v):\n%v\n" + } + for _, token := range tokens { + begin, end := int(token.begin), int(token.end) + error += fmt.Sprintf(format, + rul3s[token.pegRule], + translations[begin].line, translations[begin].symbol, + translations[end].line, translations[end].symbol, + strconv.Quote(string(e.p.buffer[begin:end]))) + } + + return error +} + +func (p *QueryParser) PrintSyntaxTree() { + if p.Pretty { + p.tokens32.PrettyPrintSyntaxTree(p.Buffer) + } else { + p.tokens32.PrintSyntaxTree(p.Buffer) + } +} + +func (p *QueryParser) Init() { + var ( + max token32 + position, tokenIndex uint32 + buffer []rune + ) + p.reset = func() { + max = token32{} + position, tokenIndex = 0, 0 + + p.buffer = []rune(p.Buffer) + if len(p.buffer) == 0 || p.buffer[len(p.buffer)-1] != endSymbol { + p.buffer = append(p.buffer, endSymbol) + } + buffer = p.buffer + } + p.reset() + + _rules := p.rules + tree := tokens32{tree: make([]token32, math.MaxInt16)} + p.parse = func(rule ...int) error { + r := 1 + if len(rule) > 0 { + r = rule[0] + } + matches := p.rules[r]() + p.tokens32 = tree + if matches { + p.Trim(tokenIndex) + return nil + } + return &parseError{p, max} + } + + add := func(rule pegRule, begin uint32) { + tree.Add(rule, begin, position, tokenIndex) + tokenIndex++ + if begin != position && position > max.end { + max = token32{rule, begin, position} + } + } + + matchDot := func() bool { + if buffer[position] != endSymbol { + position++ + return true + } + return false + } + + /*matchChar := func(c byte) bool { + if buffer[position] == c { + position++ + return true + } + return false + }*/ + + /*matchRange := func(lower byte, upper byte) bool { + if c := buffer[position]; c >= lower && c <= upper { + position++ + return true + } + return false + }*/ + + _rules = [...]func() bool{ + nil, + /* 0 e <- <('"' condition (' '+ and ' '+ condition)* '"' !.)> */ + func() bool { + position0, tokenIndex0 := position, tokenIndex + { + position1 := position + if buffer[position] != rune('"') { + goto l0 + } + position++ + if !_rules[rulecondition]() { + goto l0 + } + l2: + { + position3, tokenIndex3 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l3 + } + position++ + l4: + { + position5, tokenIndex5 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l5 + } + position++ + goto l4 + l5: + position, tokenIndex = position5, tokenIndex5 + } + { + position6 := position + { + position7, tokenIndex7 := position, tokenIndex + if buffer[position] != rune('a') { + goto l8 + } + position++ + goto l7 + l8: + position, tokenIndex = position7, tokenIndex7 + if buffer[position] != rune('A') { + goto l3 + } + position++ + } + l7: + { + position9, tokenIndex9 := position, tokenIndex + if buffer[position] != rune('n') { + goto l10 + } + position++ + goto l9 + l10: + position, tokenIndex = position9, tokenIndex9 + if buffer[position] != rune('N') { + goto l3 + } + position++ + } + l9: + { + position11, tokenIndex11 := position, tokenIndex + if buffer[position] != rune('d') { + goto l12 + } + position++ + goto l11 + l12: + position, tokenIndex = position11, tokenIndex11 + if buffer[position] != rune('D') { + goto l3 + } + position++ + } + l11: + add(ruleand, position6) + } + if buffer[position] != rune(' ') { + goto l3 + } + position++ + l13: + { + position14, tokenIndex14 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l14 + } + position++ + goto l13 + l14: + position, tokenIndex = position14, tokenIndex14 + } + if !_rules[rulecondition]() { + goto l3 + } + goto l2 + l3: + position, tokenIndex = position3, tokenIndex3 + } + if buffer[position] != rune('"') { + goto l0 + } + position++ + { + position15, tokenIndex15 := position, tokenIndex + if !matchDot() { + goto l15 + } + goto l0 + l15: + position, tokenIndex = position15, tokenIndex15 + } + add(rulee, position1) + } + return true + l0: + position, tokenIndex = position0, tokenIndex0 + return false + }, + /* 1 condition <- <(tag ' '* ((le ' '* ((&('D' | 'd') date) | (&('T' | 't') time) | (&('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') number))) / (ge ' '* ((&('D' | 'd') date) | (&('T' | 't') time) | (&('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') number))) / ((&('=') (equal ' '* ((&('\'') value) | (&('D' | 'd') date) | (&('T' | 't') time) | (&('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') number)))) | (&('>') (g ' '* ((&('D' | 'd') date) | (&('T' | 't') time) | (&('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') number)))) | (&('<') (l ' '* ((&('D' | 'd') date) | (&('T' | 't') time) | (&('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') number)))) | (&('C' | 'c') (contains ' '* value)))))> */ + func() bool { + position16, tokenIndex16 := position, tokenIndex + { + position17 := position + { + position18 := position + { + position19 := position + { + position22, tokenIndex22 := position, tokenIndex + { + switch buffer[position] { + case '<': + if buffer[position] != rune('<') { + goto l22 + } + position++ + break + case '>': + if buffer[position] != rune('>') { + goto l22 + } + position++ + break + case '=': + if buffer[position] != rune('=') { + goto l22 + } + position++ + break + case '\'': + if buffer[position] != rune('\'') { + goto l22 + } + position++ + break + case '"': + if buffer[position] != rune('"') { + goto l22 + } + position++ + break + case ')': + if buffer[position] != rune(')') { + goto l22 + } + position++ + break + case '(': + if buffer[position] != rune('(') { + goto l22 + } + position++ + break + case '\\': + if buffer[position] != rune('\\') { + goto l22 + } + position++ + break + case '\r': + if buffer[position] != rune('\r') { + goto l22 + } + position++ + break + case '\n': + if buffer[position] != rune('\n') { + goto l22 + } + position++ + break + case '\t': + if buffer[position] != rune('\t') { + goto l22 + } + position++ + break + default: + if buffer[position] != rune(' ') { + goto l22 + } + position++ + break + } + } + + goto l16 + l22: + position, tokenIndex = position22, tokenIndex22 + } + if !matchDot() { + goto l16 + } + l20: + { + position21, tokenIndex21 := position, tokenIndex + { + position24, tokenIndex24 := position, tokenIndex + { + switch buffer[position] { + case '<': + if buffer[position] != rune('<') { + goto l24 + } + position++ + break + case '>': + if buffer[position] != rune('>') { + goto l24 + } + position++ + break + case '=': + if buffer[position] != rune('=') { + goto l24 + } + position++ + break + case '\'': + if buffer[position] != rune('\'') { + goto l24 + } + position++ + break + case '"': + if buffer[position] != rune('"') { + goto l24 + } + position++ + break + case ')': + if buffer[position] != rune(')') { + goto l24 + } + position++ + break + case '(': + if buffer[position] != rune('(') { + goto l24 + } + position++ + break + case '\\': + if buffer[position] != rune('\\') { + goto l24 + } + position++ + break + case '\r': + if buffer[position] != rune('\r') { + goto l24 + } + position++ + break + case '\n': + if buffer[position] != rune('\n') { + goto l24 + } + position++ + break + case '\t': + if buffer[position] != rune('\t') { + goto l24 + } + position++ + break + default: + if buffer[position] != rune(' ') { + goto l24 + } + position++ + break + } + } + + goto l21 + l24: + position, tokenIndex = position24, tokenIndex24 + } + if !matchDot() { + goto l21 + } + goto l20 + l21: + position, tokenIndex = position21, tokenIndex21 + } + add(rulePegText, position19) + } + add(ruletag, position18) + } + l26: + { + position27, tokenIndex27 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l27 + } + position++ + goto l26 + l27: + position, tokenIndex = position27, tokenIndex27 + } + { + position28, tokenIndex28 := position, tokenIndex + { + position30 := position + if buffer[position] != rune('<') { + goto l29 + } + position++ + if buffer[position] != rune('=') { + goto l29 + } + position++ + add(rulele, position30) + } + l31: + { + position32, tokenIndex32 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l32 + } + position++ + goto l31 + l32: + position, tokenIndex = position32, tokenIndex32 + } + { + switch buffer[position] { + case 'D', 'd': + if !_rules[ruledate]() { + goto l29 + } + break + case 'T', 't': + if !_rules[ruletime]() { + goto l29 + } + break + default: + if !_rules[rulenumber]() { + goto l29 + } + break + } + } + + goto l28 + l29: + position, tokenIndex = position28, tokenIndex28 + { + position35 := position + if buffer[position] != rune('>') { + goto l34 + } + position++ + if buffer[position] != rune('=') { + goto l34 + } + position++ + add(rulege, position35) + } + l36: + { + position37, tokenIndex37 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l37 + } + position++ + goto l36 + l37: + position, tokenIndex = position37, tokenIndex37 + } + { + switch buffer[position] { + case 'D', 'd': + if !_rules[ruledate]() { + goto l34 + } + break + case 'T', 't': + if !_rules[ruletime]() { + goto l34 + } + break + default: + if !_rules[rulenumber]() { + goto l34 + } + break + } + } + + goto l28 + l34: + position, tokenIndex = position28, tokenIndex28 + { + switch buffer[position] { + case '=': + { + position40 := position + if buffer[position] != rune('=') { + goto l16 + } + position++ + add(ruleequal, position40) + } + l41: + { + position42, tokenIndex42 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l42 + } + position++ + goto l41 + l42: + position, tokenIndex = position42, tokenIndex42 + } + { + switch buffer[position] { + case '\'': + if !_rules[rulevalue]() { + goto l16 + } + break + case 'D', 'd': + if !_rules[ruledate]() { + goto l16 + } + break + case 'T', 't': + if !_rules[ruletime]() { + goto l16 + } + break + default: + if !_rules[rulenumber]() { + goto l16 + } + break + } + } + + break + case '>': + { + position44 := position + if buffer[position] != rune('>') { + goto l16 + } + position++ + add(ruleg, position44) + } + l45: + { + position46, tokenIndex46 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l46 + } + position++ + goto l45 + l46: + position, tokenIndex = position46, tokenIndex46 + } + { + switch buffer[position] { + case 'D', 'd': + if !_rules[ruledate]() { + goto l16 + } + break + case 'T', 't': + if !_rules[ruletime]() { + goto l16 + } + break + default: + if !_rules[rulenumber]() { + goto l16 + } + break + } + } + + break + case '<': + { + position48 := position + if buffer[position] != rune('<') { + goto l16 + } + position++ + add(rulel, position48) + } + l49: + { + position50, tokenIndex50 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l50 + } + position++ + goto l49 + l50: + position, tokenIndex = position50, tokenIndex50 + } + { + switch buffer[position] { + case 'D', 'd': + if !_rules[ruledate]() { + goto l16 + } + break + case 'T', 't': + if !_rules[ruletime]() { + goto l16 + } + break + default: + if !_rules[rulenumber]() { + goto l16 + } + break + } + } + + break + default: + { + position52 := position + { + position53, tokenIndex53 := position, tokenIndex + if buffer[position] != rune('c') { + goto l54 + } + position++ + goto l53 + l54: + position, tokenIndex = position53, tokenIndex53 + if buffer[position] != rune('C') { + goto l16 + } + position++ + } + l53: + { + position55, tokenIndex55 := position, tokenIndex + if buffer[position] != rune('o') { + goto l56 + } + position++ + goto l55 + l56: + position, tokenIndex = position55, tokenIndex55 + if buffer[position] != rune('O') { + goto l16 + } + position++ + } + l55: + { + position57, tokenIndex57 := position, tokenIndex + if buffer[position] != rune('n') { + goto l58 + } + position++ + goto l57 + l58: + position, tokenIndex = position57, tokenIndex57 + if buffer[position] != rune('N') { + goto l16 + } + position++ + } + l57: + { + position59, tokenIndex59 := position, tokenIndex + if buffer[position] != rune('t') { + goto l60 + } + position++ + goto l59 + l60: + position, tokenIndex = position59, tokenIndex59 + if buffer[position] != rune('T') { + goto l16 + } + position++ + } + l59: + { + position61, tokenIndex61 := position, tokenIndex + if buffer[position] != rune('a') { + goto l62 + } + position++ + goto l61 + l62: + position, tokenIndex = position61, tokenIndex61 + if buffer[position] != rune('A') { + goto l16 + } + position++ + } + l61: + { + position63, tokenIndex63 := position, tokenIndex + if buffer[position] != rune('i') { + goto l64 + } + position++ + goto l63 + l64: + position, tokenIndex = position63, tokenIndex63 + if buffer[position] != rune('I') { + goto l16 + } + position++ + } + l63: + { + position65, tokenIndex65 := position, tokenIndex + if buffer[position] != rune('n') { + goto l66 + } + position++ + goto l65 + l66: + position, tokenIndex = position65, tokenIndex65 + if buffer[position] != rune('N') { + goto l16 + } + position++ + } + l65: + { + position67, tokenIndex67 := position, tokenIndex + if buffer[position] != rune('s') { + goto l68 + } + position++ + goto l67 + l68: + position, tokenIndex = position67, tokenIndex67 + if buffer[position] != rune('S') { + goto l16 + } + position++ + } + l67: + add(rulecontains, position52) + } + l69: + { + position70, tokenIndex70 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l70 + } + position++ + goto l69 + l70: + position, tokenIndex = position70, tokenIndex70 + } + if !_rules[rulevalue]() { + goto l16 + } + break + } + } + + } + l28: + add(rulecondition, position17) + } + return true + l16: + position, tokenIndex = position16, tokenIndex16 + return false + }, + /* 2 tag <- <<(!((&('<') '<') | (&('>') '>') | (&('=') '=') | (&('\'') '\'') | (&('"') '"') | (&(')') ')') | (&('(') '(') | (&('\\') '\\') | (&('\r') '\r') | (&('\n') '\n') | (&('\t') '\t') | (&(' ') ' ')) .)+>> */ + nil, + /* 3 value <- <<('\'' (!('"' / '\'') .)* '\'')>> */ + func() bool { + position72, tokenIndex72 := position, tokenIndex + { + position73 := position + { + position74 := position + if buffer[position] != rune('\'') { + goto l72 + } + position++ + l75: + { + position76, tokenIndex76 := position, tokenIndex + { + position77, tokenIndex77 := position, tokenIndex + { + position78, tokenIndex78 := position, tokenIndex + if buffer[position] != rune('"') { + goto l79 + } + position++ + goto l78 + l79: + position, tokenIndex = position78, tokenIndex78 + if buffer[position] != rune('\'') { + goto l77 + } + position++ + } + l78: + goto l76 + l77: + position, tokenIndex = position77, tokenIndex77 + } + if !matchDot() { + goto l76 + } + goto l75 + l76: + position, tokenIndex = position76, tokenIndex76 + } + if buffer[position] != rune('\'') { + goto l72 + } + position++ + add(rulePegText, position74) + } + add(rulevalue, position73) + } + return true + l72: + position, tokenIndex = position72, tokenIndex72 + return false + }, + /* 4 number <- <<('0' / ([1-9] digit* ('.' digit*)?))>> */ + func() bool { + position80, tokenIndex80 := position, tokenIndex + { + position81 := position + { + position82 := position + { + position83, tokenIndex83 := position, tokenIndex + if buffer[position] != rune('0') { + goto l84 + } + position++ + goto l83 + l84: + position, tokenIndex = position83, tokenIndex83 + if c := buffer[position]; c < rune('1') || c > rune('9') { + goto l80 + } + position++ + l85: + { + position86, tokenIndex86 := position, tokenIndex + if !_rules[ruledigit]() { + goto l86 + } + goto l85 + l86: + position, tokenIndex = position86, tokenIndex86 + } + { + position87, tokenIndex87 := position, tokenIndex + if buffer[position] != rune('.') { + goto l87 + } + position++ + l89: + { + position90, tokenIndex90 := position, tokenIndex + if !_rules[ruledigit]() { + goto l90 + } + goto l89 + l90: + position, tokenIndex = position90, tokenIndex90 + } + goto l88 + l87: + position, tokenIndex = position87, tokenIndex87 + } + l88: + } + l83: + add(rulePegText, position82) + } + add(rulenumber, position81) + } + return true + l80: + position, tokenIndex = position80, tokenIndex80 + return false + }, + /* 5 digit <- <[0-9]> */ + func() bool { + position91, tokenIndex91 := position, tokenIndex + { + position92 := position + if c := buffer[position]; c < rune('0') || c > rune('9') { + goto l91 + } + position++ + add(ruledigit, position92) + } + return true + l91: + position, tokenIndex = position91, tokenIndex91 + return false + }, + /* 6 time <- <(('t' / 'T') ('i' / 'I') ('m' / 'M') ('e' / 'E') ' ' <(year '-' month '-' day 'T' digit digit ':' digit digit ':' digit digit ((('-' / '+') digit digit ':' digit digit) / 'Z'))>)> */ + func() bool { + position93, tokenIndex93 := position, tokenIndex + { + position94 := position + { + position95, tokenIndex95 := position, tokenIndex + if buffer[position] != rune('t') { + goto l96 + } + position++ + goto l95 + l96: + position, tokenIndex = position95, tokenIndex95 + if buffer[position] != rune('T') { + goto l93 + } + position++ + } + l95: + { + position97, tokenIndex97 := position, tokenIndex + if buffer[position] != rune('i') { + goto l98 + } + position++ + goto l97 + l98: + position, tokenIndex = position97, tokenIndex97 + if buffer[position] != rune('I') { + goto l93 + } + position++ + } + l97: + { + position99, tokenIndex99 := position, tokenIndex + if buffer[position] != rune('m') { + goto l100 + } + position++ + goto l99 + l100: + position, tokenIndex = position99, tokenIndex99 + if buffer[position] != rune('M') { + goto l93 + } + position++ + } + l99: + { + position101, tokenIndex101 := position, tokenIndex + if buffer[position] != rune('e') { + goto l102 + } + position++ + goto l101 + l102: + position, tokenIndex = position101, tokenIndex101 + if buffer[position] != rune('E') { + goto l93 + } + position++ + } + l101: + if buffer[position] != rune(' ') { + goto l93 + } + position++ + { + position103 := position + if !_rules[ruleyear]() { + goto l93 + } + if buffer[position] != rune('-') { + goto l93 + } + position++ + if !_rules[rulemonth]() { + goto l93 + } + if buffer[position] != rune('-') { + goto l93 + } + position++ + if !_rules[ruleday]() { + goto l93 + } + if buffer[position] != rune('T') { + goto l93 + } + position++ + if !_rules[ruledigit]() { + goto l93 + } + if !_rules[ruledigit]() { + goto l93 + } + if buffer[position] != rune(':') { + goto l93 + } + position++ + if !_rules[ruledigit]() { + goto l93 + } + if !_rules[ruledigit]() { + goto l93 + } + if buffer[position] != rune(':') { + goto l93 + } + position++ + if !_rules[ruledigit]() { + goto l93 + } + if !_rules[ruledigit]() { + goto l93 + } + { + position104, tokenIndex104 := position, tokenIndex + { + position106, tokenIndex106 := position, tokenIndex + if buffer[position] != rune('-') { + goto l107 + } + position++ + goto l106 + l107: + position, tokenIndex = position106, tokenIndex106 + if buffer[position] != rune('+') { + goto l105 + } + position++ + } + l106: + if !_rules[ruledigit]() { + goto l105 + } + if !_rules[ruledigit]() { + goto l105 + } + if buffer[position] != rune(':') { + goto l105 + } + position++ + if !_rules[ruledigit]() { + goto l105 + } + if !_rules[ruledigit]() { + goto l105 + } + goto l104 + l105: + position, tokenIndex = position104, tokenIndex104 + if buffer[position] != rune('Z') { + goto l93 + } + position++ + } + l104: + add(rulePegText, position103) + } + add(ruletime, position94) + } + return true + l93: + position, tokenIndex = position93, tokenIndex93 + return false + }, + /* 7 date <- <(('d' / 'D') ('a' / 'A') ('t' / 'T') ('e' / 'E') ' ' <(year '-' month '-' day)>)> */ + func() bool { + position108, tokenIndex108 := position, tokenIndex + { + position109 := position + { + position110, tokenIndex110 := position, tokenIndex + if buffer[position] != rune('d') { + goto l111 + } + position++ + goto l110 + l111: + position, tokenIndex = position110, tokenIndex110 + if buffer[position] != rune('D') { + goto l108 + } + position++ + } + l110: + { + position112, tokenIndex112 := position, tokenIndex + if buffer[position] != rune('a') { + goto l113 + } + position++ + goto l112 + l113: + position, tokenIndex = position112, tokenIndex112 + if buffer[position] != rune('A') { + goto l108 + } + position++ + } + l112: + { + position114, tokenIndex114 := position, tokenIndex + if buffer[position] != rune('t') { + goto l115 + } + position++ + goto l114 + l115: + position, tokenIndex = position114, tokenIndex114 + if buffer[position] != rune('T') { + goto l108 + } + position++ + } + l114: + { + position116, tokenIndex116 := position, tokenIndex + if buffer[position] != rune('e') { + goto l117 + } + position++ + goto l116 + l117: + position, tokenIndex = position116, tokenIndex116 + if buffer[position] != rune('E') { + goto l108 + } + position++ + } + l116: + if buffer[position] != rune(' ') { + goto l108 + } + position++ + { + position118 := position + if !_rules[ruleyear]() { + goto l108 + } + if buffer[position] != rune('-') { + goto l108 + } + position++ + if !_rules[rulemonth]() { + goto l108 + } + if buffer[position] != rune('-') { + goto l108 + } + position++ + if !_rules[ruleday]() { + goto l108 + } + add(rulePegText, position118) + } + add(ruledate, position109) + } + return true + l108: + position, tokenIndex = position108, tokenIndex108 + return false + }, + /* 8 year <- <(('1' / '2') digit digit digit)> */ + func() bool { + position119, tokenIndex119 := position, tokenIndex + { + position120 := position + { + position121, tokenIndex121 := position, tokenIndex + if buffer[position] != rune('1') { + goto l122 + } + position++ + goto l121 + l122: + position, tokenIndex = position121, tokenIndex121 + if buffer[position] != rune('2') { + goto l119 + } + position++ + } + l121: + if !_rules[ruledigit]() { + goto l119 + } + if !_rules[ruledigit]() { + goto l119 + } + if !_rules[ruledigit]() { + goto l119 + } + add(ruleyear, position120) + } + return true + l119: + position, tokenIndex = position119, tokenIndex119 + return false + }, + /* 9 month <- <(('0' / '1') digit)> */ + func() bool { + position123, tokenIndex123 := position, tokenIndex + { + position124 := position + { + position125, tokenIndex125 := position, tokenIndex + if buffer[position] != rune('0') { + goto l126 + } + position++ + goto l125 + l126: + position, tokenIndex = position125, tokenIndex125 + if buffer[position] != rune('1') { + goto l123 + } + position++ + } + l125: + if !_rules[ruledigit]() { + goto l123 + } + add(rulemonth, position124) + } + return true + l123: + position, tokenIndex = position123, tokenIndex123 + return false + }, + /* 10 day <- <(((&('3') '3') | (&('2') '2') | (&('1') '1') | (&('0') '0')) digit)> */ + func() bool { + position127, tokenIndex127 := position, tokenIndex + { + position128 := position + { + switch buffer[position] { + case '3': + if buffer[position] != rune('3') { + goto l127 + } + position++ + break + case '2': + if buffer[position] != rune('2') { + goto l127 + } + position++ + break + case '1': + if buffer[position] != rune('1') { + goto l127 + } + position++ + break + default: + if buffer[position] != rune('0') { + goto l127 + } + position++ + break + } + } + + if !_rules[ruledigit]() { + goto l127 + } + add(ruleday, position128) + } + return true + l127: + position, tokenIndex = position127, tokenIndex127 + return false + }, + /* 11 and <- <(('a' / 'A') ('n' / 'N') ('d' / 'D'))> */ + nil, + /* 12 equal <- <'='> */ + nil, + /* 13 contains <- <(('c' / 'C') ('o' / 'O') ('n' / 'N') ('t' / 'T') ('a' / 'A') ('i' / 'I') ('n' / 'N') ('s' / 'S'))> */ + nil, + /* 14 le <- <('<' '=')> */ + nil, + /* 15 ge <- <('>' '=')> */ + nil, + /* 16 l <- <'<'> */ + nil, + /* 17 g <- <'>'> */ + nil, + nil, + } + p.rules = _rules +} diff --git a/libs/pubsub/query/query_test.go b/libs/pubsub/query/query_test.go new file mode 100644 index 000000000..f266b1214 --- /dev/null +++ b/libs/pubsub/query/query_test.go @@ -0,0 +1,87 @@ +package query_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/libs/pubsub" + "github.com/tendermint/tendermint/libs/pubsub/query" +) + +func TestMatches(t *testing.T) { + const shortForm = "2006-Jan-02" + txDate, err := time.Parse(shortForm, "2017-Jan-01") + require.NoError(t, err) + txTime, err := time.Parse(time.RFC3339, "2018-05-03T14:45:00Z") + require.NoError(t, err) + + testCases := []struct { + s string + tags map[string]interface{} + err bool + matches bool + }{ + {"tm.events.type='NewBlock'", map[string]interface{}{"tm.events.type": "NewBlock"}, false, true}, + + {"tx.gas > 7", map[string]interface{}{"tx.gas": 8}, false, true}, + {"tx.gas > 7 AND tx.gas < 9", map[string]interface{}{"tx.gas": 8}, false, true}, + {"body.weight >= 3.5", map[string]interface{}{"body.weight": 3.5}, false, true}, + {"account.balance < 1000.0", map[string]interface{}{"account.balance": 900}, false, true}, + {"apples.kg <= 4", map[string]interface{}{"apples.kg": 4.0}, false, true}, + {"body.weight >= 4.5", map[string]interface{}{"body.weight": float32(4.5)}, false, true}, + {"oranges.kg < 4 AND watermellons.kg > 10", map[string]interface{}{"oranges.kg": 3, "watermellons.kg": 12}, false, true}, + {"peaches.kg < 4", map[string]interface{}{"peaches.kg": 5}, false, false}, + + {"tx.date > DATE 2017-01-01", map[string]interface{}{"tx.date": time.Now()}, false, true}, + {"tx.date = DATE 2017-01-01", map[string]interface{}{"tx.date": txDate}, false, true}, + {"tx.date = DATE 2018-01-01", map[string]interface{}{"tx.date": txDate}, false, false}, + + {"tx.time >= TIME 2013-05-03T14:45:00Z", map[string]interface{}{"tx.time": time.Now()}, false, true}, + {"tx.time = TIME 2013-05-03T14:45:00Z", map[string]interface{}{"tx.time": txTime}, false, false}, + + {"abci.owner.name CONTAINS 'Igor'", map[string]interface{}{"abci.owner.name": "Igor,Ivan"}, false, true}, + {"abci.owner.name CONTAINS 'Igor'", map[string]interface{}{"abci.owner.name": "Pavel,Ivan"}, false, false}, + } + + for _, tc := range testCases { + q, err := query.New(tc.s) + if !tc.err { + require.Nil(t, err) + } + + if tc.matches { + assert.True(t, q.Matches(pubsub.NewTagMap(tc.tags)), "Query '%s' should match %v", tc.s, tc.tags) + } else { + assert.False(t, q.Matches(pubsub.NewTagMap(tc.tags)), "Query '%s' should not match %v", tc.s, tc.tags) + } + } +} + +func TestMustParse(t *testing.T) { + assert.Panics(t, func() { query.MustParse("=") }) + assert.NotPanics(t, func() { query.MustParse("tm.events.type='NewBlock'") }) +} + +func TestConditions(t *testing.T) { + txTime, err := time.Parse(time.RFC3339, "2013-05-03T14:45:00Z") + require.NoError(t, err) + + testCases := []struct { + s string + conditions []query.Condition + }{ + {s: "tm.events.type='NewBlock'", conditions: []query.Condition{query.Condition{Tag: "tm.events.type", Op: query.OpEqual, Operand: "NewBlock"}}}, + {s: "tx.gas > 7 AND tx.gas < 9", conditions: []query.Condition{query.Condition{Tag: "tx.gas", Op: query.OpGreater, Operand: int64(7)}, query.Condition{Tag: "tx.gas", Op: query.OpLess, Operand: int64(9)}}}, + {s: "tx.time >= TIME 2013-05-03T14:45:00Z", conditions: []query.Condition{query.Condition{Tag: "tx.time", Op: query.OpGreaterEqual, Operand: txTime}}}, + } + + for _, tc := range testCases { + q, err := query.New(tc.s) + require.Nil(t, err) + + assert.Equal(t, tc.conditions, q.Conditions()) + } +} diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index ed1a5b32d..e25cac1f5 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -11,7 +11,7 @@ import ( rpcclient "github.com/tendermint/tendermint/rpc/lib/client" "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" - tmpubsub "github.com/tendermint/tmlibs/pubsub" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" ) /* diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index c9bdddf1c..d3eeb4261 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -8,7 +8,7 @@ import ( ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" - tmpubsub "github.com/tendermint/tmlibs/pubsub" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" ) /* diff --git a/rpc/core/events.go b/rpc/core/events.go index a46e0947c..36722fcf9 100644 --- a/rpc/core/events.go +++ b/rpc/core/events.go @@ -5,10 +5,10 @@ import ( "github.com/pkg/errors" + tmquery "github.com/tendermint/tendermint/libs/pubsub/query" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpctypes "github.com/tendermint/tendermint/rpc/lib/types" tmtypes "github.com/tendermint/tendermint/types" - tmquery "github.com/tendermint/tmlibs/pubsub/query" ) // Subscribe for events via WebSocket. @@ -46,10 +46,10 @@ import ( // https://godoc.org/github.com/tendermint/tendermint/types#pkg-constants // // For complete query syntax, check out -// https://godoc.org/github.com/tendermint/tmlibs/pubsub/query. +// https://godoc.org/github.com/tendermint/tendermint/libs/pubsub/query. // // ```go -// import "github.com/tendermint/tmlibs/pubsub/query" +// import "github.com/tendermint/tendermint/libs/pubsub/query" // import "github.com/tendermint/tendermint/types" // // client := client.NewHTTP("tcp://0.0.0.0:46657", "/websocket") diff --git a/rpc/core/tx.go b/rpc/core/tx.go index 5fc01a86d..615136a92 100644 --- a/rpc/core/tx.go +++ b/rpc/core/tx.go @@ -3,11 +3,12 @@ package core import ( "fmt" + cmn "github.com/tendermint/tmlibs/common" + + tmquery "github.com/tendermint/tendermint/libs/pubsub/query" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/state/txindex/null" "github.com/tendermint/tendermint/types" - cmn "github.com/tendermint/tmlibs/common" - tmquery "github.com/tendermint/tmlibs/pubsub/query" ) // Tx allows you to query the transaction results. `nil` could mean the diff --git a/rpc/lib/types/types.go b/rpc/lib/types/types.go index 5fa723bb4..1eeb19ea8 100644 --- a/rpc/lib/types/types.go +++ b/rpc/lib/types/types.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" "github.com/tendermint/go-amino" - tmpubsub "github.com/tendermint/tmlibs/pubsub" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" ) //---------------------------------------- diff --git a/state/txindex/indexer.go b/state/txindex/indexer.go index e23840f14..bf7760fc8 100644 --- a/state/txindex/indexer.go +++ b/state/txindex/indexer.go @@ -4,7 +4,7 @@ import ( "errors" "github.com/tendermint/tendermint/types" - "github.com/tendermint/tmlibs/pubsub/query" + "github.com/tendermint/tendermint/libs/pubsub/query" ) // TxIndexer interface defines methods to index and search transactions. diff --git a/state/txindex/indexer_service.go b/state/txindex/indexer_service.go index 93e6269e8..264be1fd8 100644 --- a/state/txindex/indexer_service.go +++ b/state/txindex/indexer_service.go @@ -3,8 +3,9 @@ package txindex import ( "context" - "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" + + "github.com/tendermint/tendermint/types" ) const ( diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index 87861f050..718a55d15 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -12,8 +12,8 @@ import ( "github.com/pkg/errors" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" - "github.com/tendermint/tmlibs/pubsub/query" + "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" ) diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index a8537219d..af35ec411 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -11,8 +11,8 @@ import ( abci "github.com/tendermint/abci/types" cmn "github.com/tendermint/tmlibs/common" db "github.com/tendermint/tmlibs/db" - "github.com/tendermint/tmlibs/pubsub/query" + "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" ) diff --git a/state/txindex/null/null.go b/state/txindex/null/null.go index 0764faa9e..2d3961e6b 100644 --- a/state/txindex/null/null.go +++ b/state/txindex/null/null.go @@ -5,7 +5,7 @@ import ( "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" - "github.com/tendermint/tmlibs/pubsub/query" + "github.com/tendermint/tendermint/libs/pubsub/query" ) var _ txindex.TxIndexer = (*TxIndex)(nil) diff --git a/types/event_bus.go b/types/event_bus.go index 460a3e294..925907fd0 100644 --- a/types/event_bus.go +++ b/types/event_bus.go @@ -6,7 +6,7 @@ import ( cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" - tmpubsub "github.com/tendermint/tmlibs/pubsub" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" ) const defaultCapacity = 1000 diff --git a/types/event_bus_test.go b/types/event_bus_test.go index 70a537745..95d061f40 100644 --- a/types/event_bus_test.go +++ b/types/event_bus_test.go @@ -12,8 +12,8 @@ import ( abci "github.com/tendermint/abci/types" cmn "github.com/tendermint/tmlibs/common" - tmpubsub "github.com/tendermint/tmlibs/pubsub" - tmquery "github.com/tendermint/tmlibs/pubsub/query" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" + tmquery "github.com/tendermint/tendermint/libs/pubsub/query" ) func TestEventBusPublishEventTx(t *testing.T) { diff --git a/types/events.go b/types/events.go index 342d4bc20..2b87297cd 100644 --- a/types/events.go +++ b/types/events.go @@ -3,9 +3,9 @@ package types import ( "fmt" - "github.com/tendermint/go-amino" - tmpubsub "github.com/tendermint/tmlibs/pubsub" - tmquery "github.com/tendermint/tmlibs/pubsub/query" + amino "github.com/tendermint/go-amino" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" + tmquery "github.com/tendermint/tendermint/libs/pubsub/query" ) // Reserved event types diff --git a/types/nop_event_bus.go b/types/nop_event_bus.go index 06b70987d..cd1eab8cd 100644 --- a/types/nop_event_bus.go +++ b/types/nop_event_bus.go @@ -3,7 +3,7 @@ package types import ( "context" - tmpubsub "github.com/tendermint/tmlibs/pubsub" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" ) type NopEventBus struct{} From c6f612bfc32d78e562e532873df8188911a36ba6 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 16 May 2018 11:25:53 +0400 Subject: [PATCH 12/59] subscribe before state emits NewRoundStep I had to alter events package for that. Hope that's fine. Refs #847 --- consensus/reactor.go | 24 ++++++++++++++---------- libs/events/events.go | 16 +++++----------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/consensus/reactor.go b/consensus/reactor.go index 19b0c0fe2..2034ad344 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -43,7 +43,8 @@ type ConsensusReactor struct { eventBus *types.EventBus } -// NewConsensusReactor returns a new ConsensusReactor with the given consensusState. +// NewConsensusReactor returns a new ConsensusReactor with the given +// consensusState. func NewConsensusReactor(consensusState *ConsensusState, fastSync bool) *ConsensusReactor { conR := &ConsensusReactor{ conS: consensusState, @@ -53,27 +54,31 @@ func NewConsensusReactor(consensusState *ConsensusState, fastSync bool) *Consens return conR } -// OnStart implements BaseService. +// OnStart implements BaseService by subscribing to events, which later will be +// broadcasted to other peers and starting state if we're not in fast sync. func (conR *ConsensusReactor) OnStart() error { conR.Logger.Info("ConsensusReactor ", "fastSync", conR.FastSync()) if err := conR.BaseReactor.OnStart(); err != nil { return err } + conR.subscribeToBroadcastEvents() + if !conR.FastSync() { err := conR.conS.Start() if err != nil { return err } - conR.subscribeToBroadcastEvents() } return nil } -// OnStop implements BaseService +// OnStop implements BaseService by unsubscribing from events and stopping +// state. func (conR *ConsensusReactor) OnStop() { conR.BaseReactor.OnStop() + conR.unsubscribeFromBroadcastEvents() conR.conS.Stop() } @@ -99,7 +104,6 @@ func (conR *ConsensusReactor) SwitchToConsensus(state sm.State, blocksSynced int conR.Logger.Error("Error starting conS", "err", err) return } - conR.subscribeToBroadcastEvents() } // GetChannels implements Reactor @@ -347,11 +351,6 @@ func (conR *ConsensusReactor) FastSync() bool { // proposal heartbeats using internal pubsub defined on state to broadcast // them to peers upon receiving. func (conR *ConsensusReactor) subscribeToBroadcastEvents() { - // assert consensus state is running - if !conR.conS.IsRunning() { - panic("consensus state must be running at this point") - } - const subscriber = "consensus-reactor" conR.conS.evsw.AddListenerForEvent(subscriber, types.EventNewRoundStep, func(data tmevents.EventData) { @@ -369,6 +368,11 @@ func (conR *ConsensusReactor) subscribeToBroadcastEvents() { }) } +func (conR *ConsensusReactor) unsubscribeFromBroadcastEvents() { + const subscriber = "consensus-reactor" + conR.conS.evsw.RemoveListener(subscriber) +} + func (conR *ConsensusReactor) broadcastProposalHeartbeatMessage(hb *types.Heartbeat) { conR.Logger.Debug("Broadcasting proposal heartbeat message", "height", hb.Height, "round", hb.Round, "sequence", hb.Sequence) diff --git a/libs/events/events.go b/libs/events/events.go index f1b2a754e..075f9b42b 100644 --- a/libs/events/events.go +++ b/libs/events/events.go @@ -44,25 +44,19 @@ type eventSwitch struct { } func NewEventSwitch() EventSwitch { - evsw := &eventSwitch{} + evsw := &eventSwitch{ + eventCells: make(map[string]*eventCell), + listeners: make(map[string]*eventListener), + } evsw.BaseService = *cmn.NewBaseService(nil, "EventSwitch", evsw) return evsw } func (evsw *eventSwitch) OnStart() error { - evsw.BaseService.OnStart() - evsw.eventCells = make(map[string]*eventCell) - evsw.listeners = make(map[string]*eventListener) return nil } -func (evsw *eventSwitch) OnStop() { - evsw.mtx.Lock() - defer evsw.mtx.Unlock() - evsw.BaseService.OnStop() - evsw.eventCells = nil - evsw.listeners = nil -} +func (evsw *eventSwitch) OnStop() {} func (evsw *eventSwitch) AddListenerForEvent(listenerID, event string, cb EventCallback) { // Get/Create eventCell and listener From 3485edf4f5f3844bab54473435218f1744bfe1a9 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 16 May 2018 12:24:46 +0400 Subject: [PATCH 13/59] update test docker image Go version to 1.10 --- test/docker/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index f26e60d56..def20bdf3 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.9.4 +FROM golang:1.10 # Add testing deps for curl RUN echo 'deb http://httpredir.debian.org/debian testing main non-free contrib' >> /etc/apt/sources.list @@ -29,7 +29,6 @@ RUN bash scripts/install_abci_apps.sh # NOTE: this will overwrite whatever is in vendor/ COPY . $REPO - RUN go install ./cmd/tendermint # expose the volume for debugging From a9d0adbdef564ffcad7c4c5bcd352e17bded5b10 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 16 May 2018 12:55:28 +0400 Subject: [PATCH 14/59] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b96c995ee..599c66073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ *TBD* +IMPROVEMENTS: + +- [consensus] consensus reactor now receives events from a separate event bus, + which is not dependant on external RPC load + ## 0.19.5 *May 20th, 2018* From 0cd92a494855e02cbd0f0a36c3c4e949a0b5d946 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Mon, 21 May 2018 17:35:49 +0200 Subject: [PATCH 15/59] Fix race in test suffix --- p2p/peer.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/p2p/peer.go b/p2p/peer.go index 93d05410e..e80716268 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -3,6 +3,7 @@ package p2p import ( "fmt" "net" + "sync/atomic" "time" "github.com/tendermint/go-crypto" @@ -12,7 +13,7 @@ import ( tmconn "github.com/tendermint/tendermint/p2p/conn" ) -var testIPSuffix = 0 +var testIPSuffix uint32 = 0 // Peer is an interface representing a peer connected on a reactor. type Peer interface { @@ -57,11 +58,9 @@ func (pc peerConn) RemoteIP() net.IP { if pc.conn.RemoteAddr().String() == "pipe" { pc.ips = []net.IP{ - net.IP{172, 16, 0, byte(testIPSuffix)}, + net.IP{172, 16, 0, byte(atomic.AddUint32(&testIPSuffix, 1))}, } - testIPSuffix++ - return pc.ips[0] } From 7b02b5b66b436505ec680e882163804678ae0906 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Mon, 21 May 2018 17:41:34 +0200 Subject: [PATCH 16/59] Add RemoteIP to test implementation --- blockchain/reactor_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index 63e3c72bb..49913c10e 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -1,6 +1,7 @@ package blockchain import ( + "net" "testing" cmn "github.com/tendermint/tmlibs/common" @@ -204,3 +205,4 @@ func (tp *bcrTestPeer) IsOutbound() bool { return false } func (tp *bcrTestPeer) IsPersistent() bool { return true } func (tp *bcrTestPeer) Get(s string) interface{} { return s } func (tp *bcrTestPeer) Set(string, interface{}) {} +func (tp *bcrTestPeer) RemoteIP() net.IP { return []byte{127, 0, 0, 1} } From 20e9dd07378be8996fb4385ab72741629af56071 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Mon, 21 May 2018 17:55:40 +0200 Subject: [PATCH 17/59] Return fake IP even when there is no conn --- p2p/peer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/peer.go b/p2p/peer.go index e80716268..39656efc5 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -56,7 +56,7 @@ func (pc peerConn) RemoteIP() net.IP { return pc.ips[0] } - if pc.conn.RemoteAddr().String() == "pipe" { + if pc.conn == nil || pc.conn.RemoteAddr().String() == "pipe" { pc.ips = []net.IP{ net.IP{172, 16, 0, byte(atomic.AddUint32(&testIPSuffix, 1))}, } From 91b6d3f18c8614365bedc2d28ad2047cf88e3218 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Mon, 21 May 2018 18:47:14 +0200 Subject: [PATCH 18/59] Do not set address for self error --- p2p/switch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/switch.go b/p2p/switch.go index 22307bd91..eba8679e7 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -570,7 +570,7 @@ func (sw *Switch) addPeer(pc peerConn) error { // and add to our addresses to avoid dialing again sw.addrBook.RemoveAddress(addr) sw.addrBook.AddOurAddress(addr) - return ErrSwitchConnectToSelf{addr} + return ErrSwitchConnectToSelf{} } // Avoid duplicate From 60d7486de27d121a1a86b32e8107de5f50626562 Mon Sep 17 00:00:00 2001 From: Zach Ramsay Date: Tue, 22 May 2018 14:46:56 -0400 Subject: [PATCH 19/59] docs: fix dead links, closes #1608 --- docs/spec/p2p/node.md | 2 +- docs/spec/p2p/peer.md | 2 +- p2p/README.md | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/spec/p2p/node.md b/docs/spec/p2p/node.md index c54cfeb32..5f5b312ce 100644 --- a/docs/spec/p2p/node.md +++ b/docs/spec/p2p/node.md @@ -12,7 +12,7 @@ Seeds should operate full nodes with the PEX reactor in a "crawler" mode that continuously explores to validate the availability of peers. Seeds should only respond with some top percentile of the best peers it knows about. -See [the peer-exchange docs](/docs/specification/new-spec/reactors/pex/pex.md)for details on peer quality. +See [the peer-exchange docs](../reactors/pex/pex.md)for details on peer quality. ## New Full Node diff --git a/docs/spec/p2p/peer.md b/docs/spec/p2p/peer.md index b2808a60b..c9d05d5ac 100644 --- a/docs/spec/p2p/peer.md +++ b/docs/spec/p2p/peer.md @@ -2,7 +2,7 @@ This document explains how Tendermint Peers are identified and how they connect to one another. -For details on peer discovery, see the [peer exchange (PEX) reactor doc](/docs/specification/new-spec/reactors/pex/pex.md). +For details on peer discovery, see the [peer exchange (PEX) reactor doc](../reactors/pex/pex.md). ## Peer Identity diff --git a/p2p/README.md b/p2p/README.md index 9a8ddc6c3..98e9ce2d9 100644 --- a/p2p/README.md +++ b/p2p/README.md @@ -4,8 +4,8 @@ The p2p package provides an abstraction around peer-to-peer communication. Docs: -- [Connection](../docs/specification/new-spec/p2p/connection.md) for details on how connections and multiplexing work -- [Peer](../docs/specification/new-spec/p2p/peer.md) for details on peer ID, handshakes, and peer exchange -- [Node](../docs/specification/new-spec/p2p/node.md) for details about different types of nodes and how they should work -- [Pex](../docs/specification/new-spec/p2p/pex.md) for details on peer discovery and exchange -- [Config](../docs/specification/new-spec/p2p/config.md) for details on some config option \ No newline at end of file +- [Connection](../docs/spec/p2p/connection.md) for details on how connections and multiplexing work +- [Peer](../docs/spec/p2p/peer.md) for details on peer ID, handshakes, and peer exchange +- [Node](../docs/spec/p2p/node.md) for details about different types of nodes and how they should work +- [Pex](../docs/spec/reactors/pex/pex.md) for details on peer discovery and exchange +- [Config](../docs/spec/p2p/config.md) for details on some config option From 4848e88737eecdc31ec88a6c0b00832135b0042d Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Wed, 23 May 2018 00:24:40 +0200 Subject: [PATCH 20/59] Fix persistent peer switch test --- p2p/peer_test.go | 17 +++++++++++------ p2p/switch_test.go | 8 +++++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/p2p/peer_test.go b/p2p/peer_test.go index db4f63ed7..22913f2de 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -112,11 +112,12 @@ func createOutboundPeerAndPerformHandshake(addr *NetAddress, config *PeerConfig) } type remotePeer struct { - PrivKey crypto.PrivKey - Config *PeerConfig - addr *NetAddress - quit chan struct{} - channels cmn.HexBytes + PrivKey crypto.PrivKey + Config *PeerConfig + addr *NetAddress + quit chan struct{} + channels cmn.HexBytes + listenAddr string } func (rp *remotePeer) Addr() *NetAddress { @@ -128,7 +129,11 @@ func (rp *remotePeer) ID() ID { } func (rp *remotePeer) Start() { - l, e := net.Listen("tcp", "127.0.0.1:0") // any available address + if rp.listenAddr == "" { + rp.listenAddr = "127.0.0.1:0" + } + + l, e := net.Listen("tcp", rp.listenAddr) // any available address if e != nil { golog.Fatalf("net.Listen tcp :0: %+v", e) } diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 373378d73..14b018297 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -317,7 +317,13 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { assert.False(peer.IsRunning()) // simulate another remote peer - rp = &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: DefaultPeerConfig()} + rp = &remotePeer{ + PrivKey: crypto.GenPrivKeyEd25519(), + Config: DefaultPeerConfig(), + // Use different interface to prevent duplicate IP filter, this will break + // beyond two peers. + listenAddr: "0.0.0.0:0", + } rp.Start() defer rp.Stop() From 7d98cfd3d6d3788a70df5db7167abc017d45ed6b Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Wed, 23 May 2018 01:24:27 +0200 Subject: [PATCH 21/59] Test duplicate IP guard in peer set --- p2p/peer_set_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go index ff2bdbad5..b043a36b2 100644 --- a/p2p/peer_set_test.go +++ b/p2p/peer_set_test.go @@ -146,6 +146,17 @@ func TestPeerSetAddDuplicate(t *testing.T) { } func TestPeerSetAddDuplicateIP(t *testing.T) { + t.Parallel() + + peerSet := NewPeerSet() + + if err := peerSet.Add(randPeer(net.IP{172, 0, 0, 1})); err != nil { + t.Fatal(err) + } + + // Add peer with same IP. + err := peerSet.Add(randPeer(net.IP{172, 0, 0, 1})) + assert.Equal(t, ErrSwitchDuplicatePeerIP{IP: net.IP{172, 0, 0, 1}}, err) } func TestPeerSetGet(t *testing.T) { From e11f3167ff78f84ae0d07bf4a3b666f59da36d16 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Wed, 23 May 2018 01:35:03 +0200 Subject: [PATCH 22/59] Fix pex reactor test --- p2p/pex/pex_reactor_test.go | 2 +- p2p/switch_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 38aa84059..68443e74b 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -73,7 +73,7 @@ func TestPEXReactorRunning(t *testing.T) { // create switches for i := 0; i < N; i++ { - switches[i] = p2p.MakeSwitch(config, i, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { + switches[i] = p2p.MakeSwitch(config, i, "127.0.0.2", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false) books[i].SetLogger(logger.With("pex", i)) sw.SetAddrBook(books[i]) diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 14b018297..2c59d13e4 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -322,7 +322,7 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { Config: DefaultPeerConfig(), // Use different interface to prevent duplicate IP filter, this will break // beyond two peers. - listenAddr: "0.0.0.0:0", + listenAddr: "127.0.0.2:0", } rp.Start() defer rp.Stop() From 01fd102dba892973f786d589194ae96e01b3f4bb Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Wed, 23 May 2018 01:53:37 +0200 Subject: [PATCH 23/59] Incoporate review feedback --- p2p/peer.go | 18 +++++++++--------- p2p/peer_set_test.go | 4 +--- p2p/switch.go | 7 +++---- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/p2p/peer.go b/p2p/peer.go index 39656efc5..447225bfb 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -41,7 +41,7 @@ type peerConn struct { persistent bool config *PeerConfig conn net.Conn // source connection - ips []net.IP + ip net.IP } // ID only exists for SecretConnection. @@ -52,16 +52,16 @@ func (pc peerConn) ID() ID { // Return the IP from the connection RemoteAddr func (pc peerConn) RemoteIP() net.IP { - if len(pc.ips) > 0 { - return pc.ips[0] + if pc.ip != nil { + return pc.ip } + // In test cases a conn could not be present at all or be an in-memory + // implementation where we want to return a fake ip. if pc.conn == nil || pc.conn.RemoteAddr().String() == "pipe" { - pc.ips = []net.IP{ - net.IP{172, 16, 0, byte(atomic.AddUint32(&testIPSuffix, 1))}, - } + pc.ip = net.IP{172, 16, 0, byte(atomic.AddUint32(&testIPSuffix, 1))} - return pc.ips[0] + return pc.ip } host, _, err := net.SplitHostPort(pc.conn.RemoteAddr().String()) @@ -74,9 +74,9 @@ func (pc peerConn) RemoteIP() net.IP { panic(err) } - pc.ips = ips + pc.ip = ips[0] - return ips[0] + return pc.ip } // peer implements Peer. diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go index b043a36b2..fc3004684 100644 --- a/p2p/peer_set_test.go +++ b/p2p/peer_set_test.go @@ -26,9 +26,7 @@ func randPeer(ip net.IP) *peer { }, } - p.ips = []net.IP{ - ip, - } + p.ip = ip return p } diff --git a/p2p/switch.go b/p2p/switch.go index eba8679e7..6ea7e408f 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -534,8 +534,6 @@ func (sw *Switch) addPeer(pc peerConn) error { return err } - // dont connect to multiple peers on the same IP - // NOTE: if AuthEnc==false, we don't have a peerID until after the handshake. // If AuthEnc==true then we already know the ID and could do the checks first before the handshake, // but it's simple to just deal with both cases the same after the handshake. @@ -578,8 +576,9 @@ func (sw *Switch) addPeer(pc peerConn) error { return ErrSwitchDuplicatePeerID{peerID} } - // check ips for both the connection addr and the self reported addr - if sw.peers.HasIP(pc.RemoteIP()) { + // Check for duplicate connection or peer info IP. + if sw.peers.HasIP(pc.RemoteIP()) || + sw.peers.HasIP(peerNodeInfo.NetAddress().IP) { return ErrSwitchDuplicatePeerIP{pc.RemoteIP()} } From 186d38dd8a1e56afad56b74c4a489c234f646376 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Wed, 23 May 2018 02:36:48 +0200 Subject: [PATCH 24/59] Use different loopback addresses for test switch --- p2p/pex/pex_reactor_test.go | 2 +- p2p/test_util.go | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 68443e74b..fc40f6fa0 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -73,7 +73,7 @@ func TestPEXReactorRunning(t *testing.T) { // create switches for i := 0; i < N; i++ { - switches[i] = p2p.MakeSwitch(config, i, "127.0.0.2", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { + switches[i] = p2p.MakeSwitch(config, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false) books[i].SetLogger(logger.With("pex", i)) sw.SetAddrBook(books[i]) diff --git a/p2p/test_util.go b/p2p/test_util.go index a0b3a5b88..86955f692 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -1,7 +1,9 @@ package p2p import ( + "fmt" "net" + "sync/atomic" crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" @@ -130,6 +132,8 @@ func StartSwitches(switches []*Switch) error { return nil } +var listenAddrSuffix uint32 = 1 + func MakeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch func(int, *Switch) *Switch) *Switch { // new switch, add reactors // TODO: let the config be passed in? @@ -144,7 +148,7 @@ func MakeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch f Moniker: cmn.Fmt("switch%d", i), Network: network, Version: version, - ListenAddr: cmn.Fmt("%v:%v", network, cmn.RandIntn(64512)+1023), + ListenAddr: fmt.Sprintf("127.0.0.%d:%d", atomic.AddUint32(&listenAddrSuffix, 1), cmn.RandIntn(64512)+1023), } for ch := range sw.reactorsByCh { ni.Channels = append(ni.Channels, ch) From 126ddca1a63f12225d2fcb3d9506c2858ba20283 Mon Sep 17 00:00:00 2001 From: Zach Ramsay Date: Wed, 23 May 2018 08:01:07 -0400 Subject: [PATCH 25/59] remove debora scripts (#1610) --- .gitignore | 2 -- scripts/debora/unsafe_debug_net.sh | 13 ------------- scripts/debora/unsafe_reset_net.sh | 11 ----------- scripts/debora/unsafe_start_group.sh | 10 ---------- scripts/debora/unsafe_stop_group.sh | 9 --------- scripts/debora/unsafe_upgrade_barak.sh | 19 ------------------- scripts/debora/unsafe_upgrade_group.sh | 9 --------- 7 files changed, 73 deletions(-) delete mode 100755 scripts/debora/unsafe_debug_net.sh delete mode 100755 scripts/debora/unsafe_reset_net.sh delete mode 100755 scripts/debora/unsafe_start_group.sh delete mode 100755 scripts/debora/unsafe_stop_group.sh delete mode 100755 scripts/debora/unsafe_upgrade_barak.sh delete mode 100755 scripts/debora/unsafe_upgrade_group.sh diff --git a/.gitignore b/.gitignore index e76fb1fc5..616cbc687 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ .DS_Store build/* rpc/test/.tendermint -.debora .tendermint remote_dump .revision @@ -13,7 +12,6 @@ vendor .vagrant test/p2p/data/ test/logs -.glide coverage.txt docs/_build docs/tools diff --git a/scripts/debora/unsafe_debug_net.sh b/scripts/debora/unsafe_debug_net.sh deleted file mode 100755 index b8e9d0b42..000000000 --- a/scripts/debora/unsafe_debug_net.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; killall tendermint" -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; tendermint unsafe_reset_priv_validator; rm -rf ~/.tendermint/data" -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; git pull origin develop; make" -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; mkdir -p ~/.tendermint/logs" -debora run --bg --label tendermint -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; tendermint node 2>&1 | stdinwriter -outpath ~/.tendermint/logs/tendermint.log" -printf "\n\nSleeping for a minute\n" -sleep 60 -debora download tendermint "logs/async$1" -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; killall tendermint" diff --git a/scripts/debora/unsafe_reset_net.sh b/scripts/debora/unsafe_reset_net.sh deleted file mode 100755 index 3698e5ace..000000000 --- a/scripts/debora/unsafe_reset_net.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; killall tendermint; killall logjack" -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; tendermint unsafe_reset_priv_validator; rm -rf ~/.tendermint/data; rm ~/.tendermint/config/genesis.json; rm ~/.tendermint/logs/*" -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; git pull origin develop; make" -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; mkdir -p ~/.tendermint/logs" -debora run --bg --label tendermint -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; tendermint node 2>&1 | stdinwriter -outpath ~/.tendermint/logs/tendermint.log" -debora run --bg --label logjack -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; logjack -chopSize='10M' -limitSize='1G' ~/.tendermint/logs/tendermint.log" -printf "Done\n" diff --git a/scripts/debora/unsafe_start_group.sh b/scripts/debora/unsafe_start_group.sh deleted file mode 100755 index a53669203..000000000 --- a/scripts/debora/unsafe_start_group.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -printf "Starting group $1...\n" -sleep 3 - -debora --group "$1" run --bg --label tendermint -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; tendermint node 2>&1 | stdinwriter -outpath ~/.tendermint/logs/tendermint.log" -debora --group "$1" run --bg --label logjack -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; logjack -chopSize='10M' -limitSize='1G' ~/.tendermint/logs/tendermint.log" -printf "Done\n" diff --git a/scripts/debora/unsafe_stop_group.sh b/scripts/debora/unsafe_stop_group.sh deleted file mode 100755 index cc1c61350..000000000 --- a/scripts/debora/unsafe_stop_group.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -printf "Stopping group $1...\n" -sleep 3 - -debora --group "$1" run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; killall tendermint; killall logjack" -printf "Done\n" diff --git a/scripts/debora/unsafe_upgrade_barak.sh b/scripts/debora/unsafe_upgrade_barak.sh deleted file mode 100755 index f7e9a2042..000000000 --- a/scripts/debora/unsafe_upgrade_barak.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -debora open "[::]:46661" -debora --group default.upgrade status -printf "\n\nShutting down barak default port...\n\n" -sleep 3 -debora --group default.upgrade close "[::]:46660" -debora --group default.upgrade run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; git pull origin develop; make" -debora --group default.upgrade run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; mkdir -p ~/.barak/logs" -debora --group default.upgrade run --bg --label barak -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; barak --config=cmd/barak/seed 2>&1 | stdinwriter -outpath ~/.barak/logs/barak.log" -printf "\n\nTesting new barak...\n\n" -sleep 3 -debora status -printf "\n\nShutting down old barak...\n\n" -sleep 3 -debora --group default.upgrade quit -printf "Done!\n" diff --git a/scripts/debora/unsafe_upgrade_group.sh b/scripts/debora/unsafe_upgrade_group.sh deleted file mode 100755 index 814e6c60b..000000000 --- a/scripts/debora/unsafe_upgrade_group.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -printf "Upgrading group $1...\n" -sleep 3 - -debora --group "$1" run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; git pull origin develop; make" -printf "Done\n" From a0201e78622f9fb1899c97a73142e9087613743d Mon Sep 17 00:00:00 2001 From: Zach Ramsay Date: Tue, 22 May 2018 15:09:47 -0400 Subject: [PATCH 26/59] docker readme: update --- DOCKER/README.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/DOCKER/README.md b/DOCKER/README.md index 2fe3db866..fd05d1b0f 100644 --- a/DOCKER/README.md +++ b/DOCKER/README.md @@ -17,7 +17,7 @@ # Quick reference * **Where to get help:** - https://tendermint.com/community + https://cosmos.network/community * **Where to file issues:** https://github.com/tendermint/tendermint/issues @@ -37,25 +37,29 @@ To get started developing applications, see the [application developers guide](h ## Start one instance of the Tendermint core with the `kvstore` app -A very simple example of a built-in app and Tendermint core in one container. +A quick example of a built-in app and Tendermint core in one container. ``` docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint init docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint node --proxy_app=kvstore ``` -## mintnet-kubernetes +# Local cluster -If you want to see many containers talking to each other, consider using [mintnet-kubernetes](https://github.com/tendermint/tools/tree/master/mintnet-kubernetes), which is a tool for running Tendermint-based applications on a Kubernetes cluster. +To run a 4-node network, see the `Makefile` in the root of [the repo](https://github.com/tendermint/tendermint/master/Makefile) and run: + +``` +make build-linux +make build-docker-localnode +make localnet-start +``` + +Note that this will build and use a different image than the ones provided here. # License -View [license information](https://raw.githubusercontent.com/tendermint/tendermint/master/LICENSE) for the software contained in this image. +- Tendermint's license is [Apache 2.0](https://github.com/tendermint/tendermint/master/LICENSE). -# User Feedback +# Contributing -## Contributing - -You are invited to contribute new features, fixes, or updates, large or small; we are always thrilled to receive pull requests, and do our best to process them as fast as we can. - -Before you start to code, we recommend discussing your plans through a [GitHub](https://github.com/tendermint/tendermint/issues) issue, especially for more ambitious contributions. This gives other contributors a chance to point you in the right direction, give you feedback on your design, and help you find out if someone else is working on the same thing. +Contributions are most welcome! See the [contributing file](https://github.com/tendermint/tendermint/blob/master/CONTRIBUTING.md) for more information. From b4d10b5b91069a8e930b2b135bb7cd5709b4a2ca Mon Sep 17 00:00:00 2001 From: Zach Ramsay Date: Wed, 23 May 2018 09:41:54 -0400 Subject: [PATCH 27/59] consensus: link to spec from readme (#1609) --- consensus/README.md | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/consensus/README.md b/consensus/README.md index 182e30bfa..8afaa0814 100644 --- a/consensus/README.md +++ b/consensus/README.md @@ -1,18 +1 @@ -# The core consensus algorithm. - -* state.go - The state machine as detailed in the whitepaper -* reactor.go - A reactor that connects the state machine to the gossip network - -# Go-routine summary - -The reactor runs 2 go-routines for each added peer: gossipDataRoutine and gossipVotesRoutine. - -The consensus state runs two persistent go-routines: timeoutRoutine and receiveRoutine. -Go-routines are also started to trigger timeouts and to avoid blocking when the internalMsgQueue is really backed up. - -# Replay/WAL - -A write-ahead log is used to record all messages processed by the receiveRoutine, -which amounts to all inputs to the consensus state machine: -messages from peers, messages from ourselves, and timeouts. -They can be played back deterministically at startup or using the replay console. +See the [consensus spec](https://github.com/tendermint/tendermint/tree/master/docs/spec/consensus) From 423fef1416539ebbf322c52f1caeb26fbbd197ad Mon Sep 17 00:00:00 2001 From: Zach Ramsay Date: Wed, 23 May 2018 10:01:32 -0400 Subject: [PATCH 28/59] docs: use absolute links (#1617) --- docs/spec/README.md | 16 ++++++++-------- docs/spec/blockchain/blockchain.md | 2 +- docs/spec/blockchain/encoding.md | 7 ++++--- docs/spec/blockchain/state.md | 3 +-- docs/spec/consensus/abci.md | 4 +--- docs/spec/p2p/node.md | 2 +- docs/spec/p2p/peer.md | 2 +- docs/spec/reactors/consensus/consensus.md | 4 ++-- .../reactors/consensus/proposer-selection.md | 1 - docs/spec/reactors/mempool/README.md | 11 ----------- docs/spec/reactors/mempool/messages.md | 3 ++- docs/spec/reactors/pex/pex.md | 4 ++-- docs/specification/new-spec/README.md | 2 +- p2p/README.md | 10 +++++----- 14 files changed, 29 insertions(+), 42 deletions(-) delete mode 100644 docs/spec/reactors/mempool/README.md diff --git a/docs/spec/README.md b/docs/spec/README.md index e13e65c1f..e2f6d1fa1 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -14,9 +14,9 @@ please submit them to our [bug bounty](https://tendermint.com/security)! ### Data Structures -- [Encoding and Digests](./blockchain/encoding.md) -- [Blockchain](./blockchain/blockchain.md) -- [State](./blockchain/state.md) +- [Encoding and Digests](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/encoding.md) +- [Blockchain](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md) +- [State](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/state.md) ### Consensus Protocol @@ -24,11 +24,11 @@ please submit them to our [bug bounty](https://tendermint.com/security)! ### P2P and Network Protocols -- [The Base P2P Layer](p2p): multiplex the protocols ("reactors") on authenticated and encrypted TCP connections -- [Peer Exchange (PEX)](reactors/pex): gossip known peer addresses so peers can find each other -- [Block Sync](reactors/block_sync): gossip blocks so peers can catch up quickly -- [Consensus](reactors/consensus): gossip votes and block parts so new blocks can be committed -- [Mempool](reactors/mempool): gossip transactions so they get included in blocks +- [The Base P2P Layer](https://github.com/tendermint/tendermint/tree/master/docs/spec/p2p): multiplex the protocols ("reactors") on authenticated and encrypted TCP connections +- [Peer Exchange (PEX)](https://github.com/tendermint/tendermint/tree/master/docs/spec/reactors/pex): gossip known peer addresses so peers can find each other +- [Block Sync](https://github.com/tendermint/tendermint/tree/master/docs/spec/reactors/block_sync): gossip blocks so peers can catch up quickly +- [Consensus](https://github.com/tendermint/tendermint/tree/master/docs/spec/reactors/consensus): gossip votes and block parts so new blocks can be committed +- [Mempool](https://github.com/tendermint/tendermint/tree/master/docs/spec/reactors/mempool): gossip transactions so they get included in blocks - Evidence: TODO ### More diff --git a/docs/spec/blockchain/blockchain.md b/docs/spec/blockchain/blockchain.md index d95d5d330..7ef0d768c 100644 --- a/docs/spec/blockchain/blockchain.md +++ b/docs/spec/blockchain/blockchain.md @@ -162,7 +162,7 @@ We refer to certain globally available objects: and `state` keeps track of the validator set, the consensus parameters and other results from the application. Elements of an object are accessed as expected, -ie. `block.Header`. See [here](state.md) for the definition of `state`. +ie. `block.Header`. See [here](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/state.md) for the definition of `state`. ### Header diff --git a/docs/spec/blockchain/encoding.md b/docs/spec/blockchain/encoding.md index f897bb1dc..9817d4ed5 100644 --- a/docs/spec/blockchain/encoding.md +++ b/docs/spec/blockchain/encoding.md @@ -2,7 +2,7 @@ ## Amino -Tendermint uses the Protobuf3 derrivative [Amino]() for all data structures. +Tendermint uses the Protobuf3 derivative [Amino](https://github.com/tendermint/go-amino for all data structures). Think of Amino as an object-oriented Protobuf3 with native JSON support. The goal of the Amino encoding protocol is to bring parity between application logic objects and persistence objects. @@ -51,8 +51,8 @@ Notice that when encoding byte-arrays, the length of the byte-array is appended to the PrefixBytes. Thus the encoding of a byte array becomes ` ` -(NOTE: the remainder of this section on Public Key Cryptography can be generated -from [this script](./scripts/crypto.go)) +NOTE: the remainder of this section on Public Key Cryptography can be generated +from [this script](https://github.com/tendermint/tendermint/blob/master/docs/spec/scripts/crypto.go) ### PubKeyEd25519 @@ -290,6 +290,7 @@ Amino also supports JSON encoding - registered types are simply encoded as: "type": "", "value": } +``` For instance, an ED25519 PubKey would look like: diff --git a/docs/spec/blockchain/state.md b/docs/spec/blockchain/state.md index c7de007f3..3b374f70a 100644 --- a/docs/spec/blockchain/state.md +++ b/docs/spec/blockchain/state.md @@ -77,5 +77,4 @@ func TotalVotingPower(vals []Validators) int64{ ### ConsensusParams -TODO: - +TODO diff --git a/docs/spec/consensus/abci.md b/docs/spec/consensus/abci.md index a16dd8fce..aa09cf3ad 100644 --- a/docs/spec/consensus/abci.md +++ b/docs/spec/consensus/abci.md @@ -58,7 +58,7 @@ message Validator { ``` The `pub_key` is the Amino encoded public key for the validator. For details on -Amino encoded public keys, see the [section of the encoding spec](./encoding.md#public-key-cryptography). +Amino encoded public keys, see the [section of the encoding spec](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/encoding.md#public-key-cryptography). For Ed25519 pubkeys, the Amino prefix is always "1624DE6220". For example, the 32-byte Ed25519 pubkey `76852933A4686A721442E931A8415F62F5F1AEDF4910F1F252FB393F74C40C85` would be @@ -121,7 +121,6 @@ stateBlockHeight = height of the last block for which Tendermint completed all block processing and saved all ABCI results to disk appBlockHeight = height of the last block for which ABCI app succesfully completely Commit - ``` Note we always have `storeBlockHeight >= stateBlockHeight` and `storeBlockHeight >= appBlockHeight` @@ -165,4 +164,3 @@ If `storeBlockHeight == stateBlockHeight+1` If appBlockHeight == storeBlockHeight { update the state using the saved ABCI responses but dont run the block against the real app. This happens if we crashed after the app finished Commit but before Tendermint saved the state. - diff --git a/docs/spec/p2p/node.md b/docs/spec/p2p/node.md index 5f5b312ce..366b27dd2 100644 --- a/docs/spec/p2p/node.md +++ b/docs/spec/p2p/node.md @@ -12,7 +12,7 @@ Seeds should operate full nodes with the PEX reactor in a "crawler" mode that continuously explores to validate the availability of peers. Seeds should only respond with some top percentile of the best peers it knows about. -See [the peer-exchange docs](../reactors/pex/pex.md)for details on peer quality. +See [the peer-exchange docs](https://github.com/tendermint/tendermint/blob/master/docs/spec/reactors/pex/pex.md)for details on peer quality. ## New Full Node diff --git a/docs/spec/p2p/peer.md b/docs/spec/p2p/peer.md index c9d05d5ac..2b8c48c16 100644 --- a/docs/spec/p2p/peer.md +++ b/docs/spec/p2p/peer.md @@ -2,7 +2,7 @@ This document explains how Tendermint Peers are identified and how they connect to one another. -For details on peer discovery, see the [peer exchange (PEX) reactor doc](../reactors/pex/pex.md). +For details on peer discovery, see the [peer exchange (PEX) reactor doc](https://github.com/tendermint/tendermint/blob/master/docs/spec/reactors/pex/pex.md). ## Peer Identity diff --git a/docs/spec/reactors/consensus/consensus.md b/docs/spec/reactors/consensus/consensus.md index d5655297b..4ea619b51 100644 --- a/docs/spec/reactors/consensus/consensus.md +++ b/docs/spec/reactors/consensus/consensus.md @@ -16,7 +16,7 @@ explained in a forthcoming document. For efficiency reasons, validators in Tendermint consensus protocol do not agree directly on the block as the block size is big, i.e., they don't embed the block inside `Proposal` and `VoteMessage`. Instead, they reach agreement on the `BlockID` (see `BlockID` definition in -[Blockchain](blockchain.md) section) that uniquely identifies each block. The block itself is +[Blockchain](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#blockid) section) that uniquely identifies each block. The block itself is disseminated to validator processes using peer-to-peer gossiping protocol. It starts by having a proposer first splitting a block into a number of block parts, that are then gossiped between processes using `BlockPartMessage`. @@ -69,7 +69,7 @@ BlockID contains PartSetHeader. ## VoteMessage VoteMessage is sent to vote for some block (or to inform others that a process does not vote in the -current round). Vote is defined in [Blockchain](blockchain.md) section and contains validator's +current round). Vote is defined in the [Blockchain](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#blockid) section and contains validator's information (validator address and index), height and round for which the vote is sent, vote type, blockID if process vote for some block (`nil` otherwise) and a timestamp when the vote is sent. The message is signed by the validator private key. diff --git a/docs/spec/reactors/consensus/proposer-selection.md b/docs/spec/reactors/consensus/proposer-selection.md index 01fa95b8a..649d3dd21 100644 --- a/docs/spec/reactors/consensus/proposer-selection.md +++ b/docs/spec/reactors/consensus/proposer-selection.md @@ -44,4 +44,3 @@ p0, p0, p0, p0, p0, p0, p0, p0, p0, p0, p0, p0, p0, p1, p0, p0, p0, p0, p0, etc This basically means that almost all rounds have the same proposer. But in this case, the process p0 has anyway enough voting power to decide whatever he wants, so the fact that he coordinates almost all rounds seems correct. - diff --git a/docs/spec/reactors/mempool/README.md b/docs/spec/reactors/mempool/README.md deleted file mode 100644 index 138b287a5..000000000 --- a/docs/spec/reactors/mempool/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Mempool Specification - -This package contains documents specifying the functionality -of the mempool module. - -Components: - -* [Config](./config.md) - how to configure it -* [External Messages](./messages.md) - The messages we accept over p2p and rpc interfaces -* [Functionality](./functionality.md) - high-level description of the functionality it provides -* [Concurrency Model](./concurrency.md) - What guarantees we provide, what locks we require. diff --git a/docs/spec/reactors/mempool/messages.md b/docs/spec/reactors/mempool/messages.md index 5bd1d1e55..e45ef02e0 100644 --- a/docs/spec/reactors/mempool/messages.md +++ b/docs/spec/reactors/mempool/messages.md @@ -32,6 +32,7 @@ wait before returning (sync makes sure CheckTx passes, commit makes sure it was included in a signed block). Request (`POST http://gaia.zone:46657/`): + ```json { "id": "", @@ -43,8 +44,8 @@ Request (`POST http://gaia.zone:46657/`): } ``` - Response: + ```json { "error": "", diff --git a/docs/spec/reactors/pex/pex.md b/docs/spec/reactors/pex/pex.md index 44a0e3fd8..317803b8e 100644 --- a/docs/spec/reactors/pex/pex.md +++ b/docs/spec/reactors/pex/pex.md @@ -117,7 +117,7 @@ current, past, and rate-of-change data to inform peer quality. While a PID trust metric has been implemented, it remains for future work to use it in the PEX. -See the [trustmetric](../../../architecture/adr-006-trust-metric.md ) -and [trustmetric useage](../../../architecture/adr-007-trust-metric-usage.md ) +See the [trustmetric](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-006-trust-metric.md) +and [trustmetric useage](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-007-trust-metric-usage.md) architecture docs for more details. diff --git a/docs/specification/new-spec/README.md b/docs/specification/new-spec/README.md index f5ebd2714..907ddd945 100644 --- a/docs/specification/new-spec/README.md +++ b/docs/specification/new-spec/README.md @@ -1 +1 @@ -Spec moved to [docs/spec](/docs/spec). +Spec moved to [docs/spec](https://github.com/tendermint/tendermint/tree/master/docs/spec). diff --git a/p2p/README.md b/p2p/README.md index 98e9ce2d9..819a5056b 100644 --- a/p2p/README.md +++ b/p2p/README.md @@ -4,8 +4,8 @@ The p2p package provides an abstraction around peer-to-peer communication. Docs: -- [Connection](../docs/spec/p2p/connection.md) for details on how connections and multiplexing work -- [Peer](../docs/spec/p2p/peer.md) for details on peer ID, handshakes, and peer exchange -- [Node](../docs/spec/p2p/node.md) for details about different types of nodes and how they should work -- [Pex](../docs/spec/reactors/pex/pex.md) for details on peer discovery and exchange -- [Config](../docs/spec/p2p/config.md) for details on some config option +- [Connection](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/p2p/connection.md) for details on how connections and multiplexing work +- [Peer](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/p2p/peer.md) for details on peer ID, handshakes, and peer exchange +- [Node](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/p2p/node.md) for details about different types of nodes and how they should work +- [Pex](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/reactors/pex/pex.md) for details on peer discovery and exchange +- [Config](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/p2p/config.md) for details on some config option From b166831fb50f6d3e8ea5a2d2d02439824f3f646e Mon Sep 17 00:00:00 2001 From: Zach Ramsay Date: Wed, 23 May 2018 10:05:03 -0400 Subject: [PATCH 29/59] link to both consensus specs --- consensus/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/README.md b/consensus/README.md index 8afaa0814..1111317d5 100644 --- a/consensus/README.md +++ b/consensus/README.md @@ -1 +1 @@ -See the [consensus spec](https://github.com/tendermint/tendermint/tree/master/docs/spec/consensus) +See the [consensus spec](https://github.com/tendermint/tendermint/tree/master/docs/spec/consensus) and the [reactor consensus spec](https://github.com/tendermint/tendermint/tree/master/docs/spec/reactors/consensus) for more information. From 2aa5285c6689ccd37b034957d805c5643bab701b Mon Sep 17 00:00:00 2001 From: Zach Ramsay Date: Wed, 23 May 2018 10:08:57 -0400 Subject: [PATCH 30/59] fix from self-review --- docs/spec/blockchain/encoding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/blockchain/encoding.md b/docs/spec/blockchain/encoding.md index 9817d4ed5..4fcd9e7f0 100644 --- a/docs/spec/blockchain/encoding.md +++ b/docs/spec/blockchain/encoding.md @@ -2,7 +2,7 @@ ## Amino -Tendermint uses the Protobuf3 derivative [Amino](https://github.com/tendermint/go-amino for all data structures). +Tendermint uses the Protobuf3 derivative [Amino](https://github.com/tendermint/go-amino) for all data structures. Think of Amino as an object-oriented Protobuf3 with native JSON support. The goal of the Amino encoding protocol is to bring parity between application logic objects and persistence objects. From b9afcbe3a2f49bee4b7dbea4980736c88b6740c4 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 22 May 2018 15:39:27 +0400 Subject: [PATCH 31/59] fix typo --- consensus/wal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/wal.go b/consensus/wal.go index 0db0dc50a..0ddc3138e 100644 --- a/consensus/wal.go +++ b/consensus/wal.go @@ -111,7 +111,7 @@ func (wal *baseWAL) OnStop() { } // Write is called in newStep and for each receive on the -// peerMsgQueue and the timoutTicker. +// peerMsgQueue and the timeoutTicker. // NOTE: does not call fsync() func (wal *baseWAL) Write(msg WALMessage) { if wal == nil { From 118b86b1ef78bd43d0a03dc663eac3b3516cc80c Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 22 May 2018 15:42:37 +0400 Subject: [PATCH 32/59] fix nil panic error msg is nil and if we continue executing, we'll get nil exception at `msg.Msg.(....)` --- consensus/wal.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/consensus/wal.go b/consensus/wal.go index 0ddc3138e..7ae36eae5 100644 --- a/consensus/wal.go +++ b/consensus/wal.go @@ -144,8 +144,8 @@ type WALSearchOptions struct { IgnoreDataCorruptionErrors bool } -// SearchForEndHeight searches for the EndHeightMessage with the height and -// returns an auto.GroupReader, whenever it was found or not and an error. +// SearchForEndHeight searches for the EndHeightMessage with the given height +// and returns an auto.GroupReader, whenever it was found or not and an error. // Group reader will be nil if found equals false. // // CONTRACT: caller must close group reader. @@ -170,7 +170,9 @@ func (wal *baseWAL) SearchForEndHeight(height int64, options *WALSearchOptions) break } if options.IgnoreDataCorruptionErrors && IsDataCorruptionError(err) { + wal.Logger.Debug("Corrupted entry. Skipping...", "err", err) // do nothing + continue } else if err != nil { gr.Close() return nil, false, err From 68f6226bea3b07be15b0a154b9869b2aae1e7234 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 22 May 2018 15:50:23 +0400 Subject: [PATCH 33/59] data is corrupted, but this requires manual intervention i.e., can't be skipped and we should only return DataCorruptionError if we can skip a msg safely --- consensus/wal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/wal.go b/consensus/wal.go index 7ae36eae5..1abf97293 100644 --- a/consensus/wal.go +++ b/consensus/wal.go @@ -282,7 +282,7 @@ func (dec *WALDecoder) Decode() (*TimedWALMessage, error) { length := binary.BigEndian.Uint32(b) if length > maxMsgSizeBytes { - return nil, DataCorruptionError{fmt.Errorf("length %d exceeded maximum possible value of %d bytes", length, maxMsgSizeBytes)} + return nil, fmt.Errorf("length %d exceeded maximum possible value of %d bytes", length, maxMsgSizeBytes) } data := make([]byte, length) From f3f5c7f472a4f87d2e393c4b27d794243589231e Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 22 May 2018 15:53:33 +0400 Subject: [PATCH 34/59] we must only return io.EOF to progress to the next file in auto.Group since we never write msg partially, if we've encountered io.EOF in the middle of the msg, we must abort --- consensus/wal.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/consensus/wal.go b/consensus/wal.go index 1abf97293..c00fec614 100644 --- a/consensus/wal.go +++ b/consensus/wal.go @@ -273,9 +273,6 @@ func (dec *WALDecoder) Decode() (*TimedWALMessage, error) { b = make([]byte, 4) _, err = dec.rd.Read(b) - if err == io.EOF { - return nil, err - } if err != nil { return nil, fmt.Errorf("failed to read length: %v", err) } @@ -287,9 +284,6 @@ func (dec *WALDecoder) Decode() (*TimedWALMessage, error) { data := make([]byte, length) _, err = dec.rd.Read(data) - if err == io.EOF { - return nil, err - } if err != nil { return nil, fmt.Errorf("failed to read data: %v", err) } From 708f35e5c1155f75b3fa2808d460720c44193532 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 22 May 2018 17:05:54 +0400 Subject: [PATCH 35/59] do not look for height in older files if we've seen height - 1 Refs #1600 --- CHANGELOG.md | 1 + consensus/wal.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 599c66073..bf3059374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ IMPROVEMENTS: - [consensus] consensus reactor now receives events from a separate event bus, which is not dependant on external RPC load +- [consensus/wal] do not look for height in older files if we've seen height - 1 ## 0.19.5 diff --git a/consensus/wal.go b/consensus/wal.go index c00fec614..80cb8fc3c 100644 --- a/consensus/wal.go +++ b/consensus/wal.go @@ -151,6 +151,7 @@ type WALSearchOptions struct { // CONTRACT: caller must close group reader. func (wal *baseWAL) SearchForEndHeight(height int64, options *WALSearchOptions) (gr *auto.GroupReader, found bool, err error) { var msg *TimedWALMessage + lastHeightFound := int64(-1) // NOTE: starting from the last file in the group because we're usually // searching for the last height. See replay.go @@ -166,6 +167,11 @@ func (wal *baseWAL) SearchForEndHeight(height int64, options *WALSearchOptions) for { msg, err = dec.Decode() if err == io.EOF { + // OPTIMISATION: no need to look for height in older files if we've seen h < height + if lastHeightFound > 0 && lastHeightFound < height { + gr.Close() + return nil, false, nil + } // check next file break } @@ -179,6 +185,7 @@ func (wal *baseWAL) SearchForEndHeight(height int64, options *WALSearchOptions) } if m, ok := msg.Msg.(EndHeightMessage); ok { + lastHeightFound = m.Height if m.Height == height { // found wal.Logger.Debug("Found", "height", height, "index", index) return gr, true, nil From 2a0e9f93ceb9ed89cda8b17f2dd4ad273351f143 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 24 May 2018 16:29:27 +0400 Subject: [PATCH 36/59] provide arg to error BEFORE: ``` E[05-24|11:55:37.229] Dialing failed pex=0 addr=022ec801d79025caab3afbbf816d92ff8450d040@127.0.0.2:6593 err="Connect to self: " attempts=0 ``` AFTER: ``` E[05-24|11:55:37.229] Dialing failed pex=0 addr=022ec801d79025caab3afbbf816d92ff8450d040@127.0.0.2:6593 err="Connect to self: 022ec801d79025caab3afbbf816d92ff8450d040@127.0.0.2:6593" attempts=0 ``` --- p2p/switch.go | 2 +- p2p/switch_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/switch.go b/p2p/switch.go index 6ea7e408f..dc9b1698a 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -568,7 +568,7 @@ func (sw *Switch) addPeer(pc peerConn) error { // and add to our addresses to avoid dialing again sw.addrBook.RemoveAddress(addr) sw.addrBook.AddOurAddress(addr) - return ErrSwitchConnectToSelf{} + return ErrSwitchConnectToSelf{addr} } // Avoid duplicate diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 2c59d13e4..74e9e9776 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -193,7 +193,7 @@ func TestSwitchFiltersOutItself(t *testing.T) { // addr should be rejected in addPeer based on the same ID err := s1.DialPeerWithAddress(rp.Addr(), false) if assert.Error(t, err) { - assert.EqualValues(t, ErrSwitchConnectToSelf{}, err) + assert.Equal(t, ErrSwitchConnectToSelf{rp.Addr()}.Error(), err.Error()) } assert.True(t, s1.addrBook.OurAddress(rp.Addr())) From 67068a34f294b0a763ef1d3bdfbabe04ffbd4104 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 24 May 2018 17:56:48 +0400 Subject: [PATCH 37/59] log requesting addresses --- p2p/pex/pex_reactor.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index b26e7d3af..457e54278 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/tendermint/go-amino" + amino "github.com/tendermint/go-amino" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/p2p" @@ -281,6 +281,7 @@ func (r *PEXReactor) receiveRequest(src Peer) error { // RequestAddrs asks peer for more addresses if we do not already // have a request out for this peer. func (r *PEXReactor) RequestAddrs(p Peer) { + r.Logger.Debug("Request addrs", "from", p) id := string(p.ID()) if r.requestsSent.Has(id) { return From 4da81aa0b7c239c4e87cd88bc7d0324c14af592d Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 25 May 2018 14:50:42 +0400 Subject: [PATCH 38/59] commented out TestPEXReactorRunning --- p2p/peer.go | 2 +- p2p/pex/pex_reactor_test.go | 92 ++++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/p2p/peer.go b/p2p/peer.go index 447225bfb..742fad656 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -6,7 +6,7 @@ import ( "sync/atomic" "time" - "github.com/tendermint/go-crypto" + crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index fc40f6fa0..55960e6f5 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -59,59 +59,69 @@ func TestPEXReactorAddRemovePeer(t *testing.T) { assert.Equal(t, size+1, book.Size()) } -func TestPEXReactorRunning(t *testing.T) { - N := 3 - switches := make([]*p2p.Switch, N) +// --- FAIL: TestPEXReactorRunning (11.10s) +// pex_reactor_test.go:411: expected all switches to be connected to at +// least one peer (switches: 0 => {outbound: 1, inbound: 0}, 1 => +// {outbound: 0, inbound: 1}, 2 => {outbound: 0, inbound: 0}, ) +// +// EXPLANATION: peers are getting rejected because in switch#addPeer we check +// if any peer (who we already connected to) has the same IP. Even though local +// peers have different IP addresses, they all have the same underlying remote +// IP: 127.0.0.1. +// +// func TestPEXReactorRunning(t *testing.T) { +// N := 3 +// switches := make([]*p2p.Switch, N) - // directory to store address books - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(t, err) - defer os.RemoveAll(dir) // nolint: errcheck +// // directory to store address books +// dir, err := ioutil.TempDir("", "pex_reactor") +// require.Nil(t, err) +// defer os.RemoveAll(dir) // nolint: errcheck - books := make([]*addrBook, N) - logger := log.TestingLogger() +// books := make([]*addrBook, N) +// logger := log.TestingLogger() - // create switches - for i := 0; i < N; i++ { - switches[i] = p2p.MakeSwitch(config, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { - books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false) - books[i].SetLogger(logger.With("pex", i)) - sw.SetAddrBook(books[i]) +// // create switches +// for i := 0; i < N; i++ { +// switches[i] = p2p.MakeSwitch(config, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { +// books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false) +// books[i].SetLogger(logger.With("pex", i)) +// sw.SetAddrBook(books[i]) - sw.SetLogger(logger.With("pex", i)) +// sw.SetLogger(logger.With("pex", i)) - r := NewPEXReactor(books[i], &PEXReactorConfig{}) - r.SetLogger(logger.With("pex", i)) - r.SetEnsurePeersPeriod(250 * time.Millisecond) - sw.AddReactor("pex", r) +// r := NewPEXReactor(books[i], &PEXReactorConfig{}) +// r.SetLogger(logger.With("pex", i)) +// r.SetEnsurePeersPeriod(250 * time.Millisecond) +// sw.AddReactor("pex", r) - return sw - }) - } +// return sw +// }) +// } - addOtherNodeAddrToAddrBook := func(switchIndex, otherSwitchIndex int) { - addr := switches[otherSwitchIndex].NodeInfo().NetAddress() - books[switchIndex].AddAddress(addr, addr) - } +// addOtherNodeAddrToAddrBook := func(switchIndex, otherSwitchIndex int) { +// addr := switches[otherSwitchIndex].NodeInfo().NetAddress() +// books[switchIndex].AddAddress(addr, addr) +// } - addOtherNodeAddrToAddrBook(0, 1) - addOtherNodeAddrToAddrBook(1, 0) - addOtherNodeAddrToAddrBook(2, 1) +// addOtherNodeAddrToAddrBook(0, 1) +// addOtherNodeAddrToAddrBook(1, 0) +// addOtherNodeAddrToAddrBook(2, 1) - for i, sw := range switches { - sw.AddListener(p2p.NewDefaultListener("tcp", sw.NodeInfo().ListenAddr, true, logger.With("pex", i))) +// for i, sw := range switches { +// sw.AddListener(p2p.NewDefaultListener("tcp", sw.NodeInfo().ListenAddr, true, logger.With("pex", i))) - err := sw.Start() // start switch and reactors - require.Nil(t, err) - } +// err := sw.Start() // start switch and reactors +// require.Nil(t, err) +// } - assertPeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second, N-1) +// assertPeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second, N-1) - // stop them - for _, s := range switches { - s.Stop() - } -} +// // stop them +// for _, s := range switches { +// s.Stop() +// } +// } func TestPEXReactorReceive(t *testing.T) { r, book := createReactor(&PEXReactorConfig{}) From 7f20eb5f8ecdb5528d9aee8e3549880229ac73b8 Mon Sep 17 00:00:00 2001 From: Zach Date: Fri, 25 May 2018 07:59:24 -0400 Subject: [PATCH 39/59] generate RPC docs using Slate (#1612) * generate RPC docs using Slate (#691) * update changelog * skip if branch not develop * slate: only build if rpc/core has changes * fetch develop to compare against * slate: build on master only * [rpc/core] use original repo, not fork in README --- .circleci/config.yml | 19 ++++ CHANGELOG.md | 4 +- Makefile | 7 +- docs/specification/rpc.rst | 188 +------------------------------------ rpc/core/README.md | 9 +- rpc/core/doc.go | 2 +- scripts/slate.sh | 77 +++++++++++++++ 7 files changed, 110 insertions(+), 196 deletions(-) create mode 100644 scripts/slate.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 177c1458f..4209d531e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,6 +77,22 @@ jobs: paths: - "bin/abci*" + build_slate: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: slate docs + command: | + set -ex + export PATH="$GOBIN:$PATH" + make build-slate + lint: <<: *defaults steps: @@ -180,6 +196,9 @@ workflows: test-suite: jobs: - setup_dependencies + - build_slate: + requires: + - setup_dependencies - setup_abci: requires: - setup_dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3059374..bde5b2e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## 0.19.6 -*TBD* +FEATURES + +- [rpc] the RPC documentation is now published to https://tendermint.github.io/slate IMPROVEMENTS: diff --git a/Makefile b/Makefile index 991bfb263..079c58f90 100755 --- a/Makefile +++ b/Makefile @@ -226,8 +226,11 @@ sentry-stop: @if [ -z "$(DO_API_TOKEN)" ]; then echo "DO_API_TOKEN environment variable not set." ; false ; fi cd networks/remote/terraform && terraform destroy -var DO_API_TOKEN="$(DO_API_TOKEN)" -var SSH_KEY_FILE="$(HOME)/.ssh/id_rsa.pub" +# meant for the CI, inspect script & adapt accordingly +build-slate: + bash scripts/slate.sh + # To avoid unintended conflicts with file names, always add to .PHONY # unless there is a reason not to. # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: check build build_race dist install check_tools get_tools update_tools get_vendor_deps draw_deps test_cover test_apps test_persistence test_p2p test test_race test_integrations test_release test100 vagrant_test fmt build-linux localnet-start localnet-stop build-docker build-docker-localnode sentry-start sentry-config sentry-stop - +.PHONY: check build build_race dist install check_tools get_tools update_tools get_vendor_deps draw_deps test_cover test_apps test_persistence test_p2p test test_race test_integrations test_release test100 vagrant_test fmt build-linux localnet-start localnet-stop build-docker build-docker-localnode sentry-start sentry-config sentry-stop build-slate diff --git a/docs/specification/rpc.rst b/docs/specification/rpc.rst index 386791aa7..1dd1165b9 100644 --- a/docs/specification/rpc.rst +++ b/docs/specification/rpc.rst @@ -1,190 +1,4 @@ RPC === -Coming soon: RPC docs powered by `slate `__. Until then, read on. - -Tendermint supports the following RPC protocols: - -- URI over HTTP -- JSONRPC over HTTP -- JSONRPC over websockets - -Tendermint RPC is build using `our own RPC -library `__. -Documentation and tests for that library could be found at -``tendermint/rpc/lib`` directory. - -Configuration -~~~~~~~~~~~~~ - -Set the ``laddr`` config parameter under ``[rpc]`` table in the -$TMHOME/config/config.toml file or the ``--rpc.laddr`` command-line flag to the -desired protocol://host:port setting. Default: ``tcp://0.0.0.0:46657``. - -Arguments -~~~~~~~~~ - -Arguments which expect strings or byte arrays may be passed as quoted -strings, like ``"abc"`` or as ``0x``-prefixed strings, like -``0x616263``. - -URI/HTTP -~~~~~~~~ - -Example request: - -.. code:: bash - - curl -s 'http://localhost:46657/broadcast_tx_sync?tx="abc"' | jq . - -Response: - -.. code:: json - - { - "error": "", - "result": { - "hash": "2B8EC32BA2579B3B8606E42C06DE2F7AFA2556EF", - "log": "", - "data": "", - "code": 0 - }, - "id": "", - "jsonrpc": "2.0" - } - -The first entry in the result-array (``96``) is the method this response -correlates with. ``96`` refers to "ResultTypeBroadcastTx", see -`responses.go `__ -for a complete overview. - -JSONRPC/HTTP -~~~~~~~~~~~~ - -JSONRPC requests can be POST'd to the root RPC endpoint via HTTP (e.g. -``http://localhost:46657/``). - -Example request: - -.. code:: json - - { - "method": "broadcast_tx_sync", - "jsonrpc": "2.0", - "params": [ "abc" ], - "id": "dontcare" - } - -JSONRPC/websockets -~~~~~~~~~~~~~~~~~~ - -JSONRPC requests can be made via websocket. The websocket endpoint is at -``/websocket``, e.g. ``http://localhost:46657/websocket``. Asynchronous -RPC functions like event ``subscribe`` and ``unsubscribe`` are only -available via websockets. - -Endpoints -~~~~~~~~~ - -An HTTP Get request to the root RPC endpoint (e.g. -``http://localhost:46657``) shows a list of available endpoints. - -:: - - Available endpoints: - http://localhost:46657/abci_info - http://localhost:46657/dump_consensus_state - http://localhost:46657/genesis - http://localhost:46657/net_info - http://localhost:46657/num_unconfirmed_txs - http://localhost:46657/health - http://localhost:46657/status - http://localhost:46657/unconfirmed_txs - http://localhost:46657/unsafe_flush_mempool - http://localhost:46657/unsafe_stop_cpu_profiler - http://localhost:46657/validators - - Endpoints that require arguments: - http://localhost:46657/abci_query?path=_&data=_&prove=_ - http://localhost:46657/block?height=_ - http://localhost:46657/blockchain?minHeight=_&maxHeight=_ - http://localhost:46657/broadcast_tx_async?tx=_ - http://localhost:46657/broadcast_tx_commit?tx=_ - http://localhost:46657/broadcast_tx_sync?tx=_ - http://localhost:46657/commit?height=_ - http://localhost:46657/dial_seeds?seeds=_ - http://localhost:46657/dial_peers?peers=_&persistent=_ - http://localhost:46657/subscribe?event=_ - http://localhost:46657/tx?hash=_&prove=_ - http://localhost:46657/unsafe_start_cpu_profiler?filename=_ - http://localhost:46657/unsafe_write_heap_profile?filename=_ - http://localhost:46657/unsubscribe?event=_ - -tx -~~ - -Returns a transaction matching the given transaction hash. - -**Parameters** - -1. hash - the transaction hash -2. prove - include a proof of the transaction inclusion in the block in - the result (optional, default: false) - -**Returns** - -- ``proof``: the ``types.TxProof`` object -- ``tx``: ``[]byte`` - the transaction -- ``tx_result``: the ``abci.Result`` object -- ``index``: ``int`` - index of the transaction -- ``height``: ``int`` - height of the block where this transaction was - in - -**Example** - -.. code:: bash - - curl -s 'http://localhost:46657/broadcast_tx_commit?tx="abc"' | jq . - # { - # "error": "", - # "result": { - # "hash": "2B8EC32BA2579B3B8606E42C06DE2F7AFA2556EF", - # "log": "", - # "data": "", - # "code": 0 - # }, - # "id": "", - # "jsonrpc": "2.0" - # } - - curl -s 'http://localhost:46657/tx?hash=0x2B8EC32BA2579B3B8606E42C06DE2F7AFA2556EF' | jq . - # { - # "error": "", - # "result": { - # "proof": { - # "Proof": { - # "aunts": [] - # }, - # "Data": "YWJjZA==", - # "RootHash": "2B8EC32BA2579B3B8606E42C06DE2F7AFA2556EF", - # "Total": 1, - # "Index": 0 - # }, - # "tx": "YWJjZA==", - # "tx_result": { - # "log": "", - # "data": "", - # "code": 0 - # }, - # "index": 0, - # "height": 52 - # }, - # "id": "", - # "jsonrpc": "2.0" - # } - -More Examples -~~~~~~~~~~~~~ - -See the various bash tests using curl in ``test/``, and examples using -the ``Go`` API in ``rpc/client/``. +The RPC documentation is hosted `here `__ and is generated by the CI from our `Slate repo `__. To update the documentation, edit the relevant ``godoc`` comments in the `rpc/core directory `__. diff --git a/rpc/core/README.md b/rpc/core/README.md index df84d6e64..9547079b2 100644 --- a/rpc/core/README.md +++ b/rpc/core/README.md @@ -3,17 +3,16 @@ ## Generate markdown for [Slate](https://github.com/tendermint/slate) We are using [Slate](https://github.com/tendermint/slate) to power our RPC -documentation. If you are changing a comment, make sure to copy the resulting -changes to the slate repo and make a PR -[there](https://github.com/tendermint/slate) as well. For generating markdown -use: +documentation. For generating markdown use: ```shell -go get github.com/melekes/godoc2md +go get github.com/davecheney/godoc2md godoc2md -template rpc/core/doc_template.txt github.com/tendermint/tendermint/rpc/core | grep -v -e "pipe.go" -e "routes.go" -e "dev.go" | sed 's$/src/target$https://github.com/tendermint/tendermint/tree/master/rpc/core$' ``` +For more information see the [CI script for building the Slate docs](/scripts/slate.sh) + ## Pagination Requests that return multiple items will be paginated to 30 items by default. diff --git a/rpc/core/doc.go b/rpc/core/doc.go index b479482c4..d18cda6ac 100644 --- a/rpc/core/doc.go +++ b/rpc/core/doc.go @@ -7,7 +7,7 @@ Tendermint supports the following RPC protocols: * JSONRPC over HTTP * JSONRPC over websockets -Tendermint RPC is built using [our own RPC library](https://github.com/tendermint/tendermint/tree/master/rpc/lib). Documentation and tests for that library could be found at `tendermint/rpc/lib` directory. +Tendermint RPC is built using [our own RPC library](https://github.com/tendermint/tendermint/tree/master/rpc/lib) which contains its own set of documentation and tests. ## Configuration diff --git a/scripts/slate.sh b/scripts/slate.sh new file mode 100644 index 000000000..e18babea7 --- /dev/null +++ b/scripts/slate.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$CIRCLE_BRANCH" == "" ]; then + echo "this script is meant to be run on CircleCI, exiting" + echo 1 +fi + +# check for changes in the `rpc/core` directory +did_rpc_change=$(git diff --name-status $CIRCLE_BRANCH origin/master | grep rpc/core) + +if [ "$did_rpc_change" == "" ]; then + echo "no changes detected in rpc/core, exiting" + exit 0 +else + echo "changes detected in rpc/core, continuing" +fi + +# only run this script on changes to rpc/core committed to develop +if [ "$CIRCLE_BRANCH" != "master" ]; then + echo "the branch being built isn't master, exiting" + exit 0 +else + echo "on master, building the RPC docs" +fi + +# godoc2md used to convert the go documentation from +# `rpc/core` into a markdown file consumed by Slate +go get github.com/davecheney/godoc2md + +# slate works via forks, and we'll be committing to +# master branch, which will trigger our fork to run +# the `./deploy.sh` and publish via the `gh-pages` branch +slate_repo=github.com/tendermint/slate +slate_path="$GOPATH"/src/"$slate_repo" + +if [ ! -d "$slate_path" ]; then + git clone https://"$slate_repo".git $slate_path +fi + +# the main file we need to update if rpc/core changed +destination="$slate_path"/source/index.html.md + +# we remove it then re-create it with the latest changes +rm $destination + +header="--- +title: RPC Reference + +language_tabs: + - shell + - go + +toc_footers: + - Tendermint + - Documentation Powered by Slate + +search: true +---" + +# write header to the main slate file +echo "$header" > "$destination" + +# generate a markdown from the godoc comments, using a template +rpc_docs=$(godoc2md -template rpc/core/doc_template.txt github.com/tendermint/tendermint/rpc/core | grep -v -e "pipe.go" -e "routes.go" -e "dev.go" | sed 's$/src/target$https://github.com/tendermint/tendermint/tree/master/rpc/core$') + +# append core RPC docs +echo "$rpc_docs" >> "$destination" + +# commit the changes +cd $slate_path + +git config --global user.email "github@tendermint.com" +git config --global user.name "tenderbot" + +git commit -a -m "Update tendermint RPC docs via CircleCI" +git push -q https://${GITHUB_ACCESS_TOKEN}@github.com/tendermint/slate.git master From f55725ebfa77a294d3fa5add869ca4f7dfe09c4f Mon Sep 17 00:00:00 2001 From: Jae Kwon Date: Sat, 26 May 2018 22:08:42 -0700 Subject: [PATCH 40/59] Potential fix for blockchain pool halting issue --- blockchain/pool.go | 62 ++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/blockchain/pool.go b/blockchain/pool.go index 603b4bf2a..8b964e81a 100644 --- a/blockchain/pool.go +++ b/blockchain/pool.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "sync" + "sync/atomic" "time" cmn "github.com/tendermint/tmlibs/common" @@ -66,11 +67,13 @@ type BlockPool struct { // block requests requesters map[int64]*bpRequester height int64 // the lowest key in requesters. - numPending int32 // number of requests pending assignment or block response // peers peers map[p2p.ID]*bpPeer maxPeerHeight int64 + // atomic + numPending int32 // number of requests pending assignment or block response + requestsCh chan<- BlockRequest errorsCh chan<- peerError } @@ -151,7 +154,7 @@ func (pool *BlockPool) GetStatus() (height int64, numPending int32, lenRequester pool.mtx.Lock() defer pool.mtx.Unlock() - return pool.height, pool.numPending, len(pool.requesters) + return pool.height, atomic.LoadInt32(&pool.numPending), len(pool.requesters) } // TODO: relax conditions, prevent abuse. @@ -245,7 +248,7 @@ func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int } if requester.setBlock(block, peerID) { - pool.numPending-- + atomic.AddInt32(&pool.numPending, -1) peer := pool.peers[peerID] if peer != nil { peer.decrPending(blockSize) @@ -291,10 +294,7 @@ func (pool *BlockPool) RemovePeer(peerID p2p.ID) { func (pool *BlockPool) removePeer(peerID p2p.ID) { for _, requester := range pool.requesters { if requester.getPeerID() == peerID { - if requester.getBlock() != nil { - pool.numPending++ - } - go requester.redo() // pick another peer and ... + requester.redo() } } delete(pool.peers, peerID) @@ -332,7 +332,7 @@ func (pool *BlockPool) makeNextRequester() { // request.SetLogger(pool.Logger.With("height", nextHeight)) pool.requesters[nextHeight] = request - pool.numPending++ + atomic.AddInt32(&pool.numPending, 1) err := request.Start() if err != nil { @@ -360,7 +360,7 @@ func (pool *BlockPool) sendError(err error, peerID p2p.ID) { // unused by tendermint; left for debugging purposes func (pool *BlockPool) debug() string { - pool.mtx.Lock() // Lock + pool.mtx.Lock() defer pool.mtx.Unlock() str := "" @@ -466,8 +466,8 @@ func newBPRequester(pool *BlockPool, height int64) *bpRequester { bpr := &bpRequester{ pool: pool, height: height, - gotBlockCh: make(chan struct{}), - redoCh: make(chan struct{}), + gotBlockCh: make(chan struct{}, 1), + redoCh: make(chan struct{}, 1), peerID: "", block: nil, @@ -481,7 +481,7 @@ func (bpr *bpRequester) OnStart() error { return nil } -// Returns true if the peer matches +// Returns true if the peer matches and block doesn't already exist. func (bpr *bpRequester) setBlock(block *types.Block, peerID p2p.ID) bool { bpr.mtx.Lock() if bpr.block != nil || bpr.peerID != peerID { @@ -491,7 +491,10 @@ func (bpr *bpRequester) setBlock(block *types.Block, peerID p2p.ID) bool { bpr.block = block bpr.mtx.Unlock() - bpr.gotBlockCh <- struct{}{} + select { + case bpr.gotBlockCh <- struct{}{}: + default: + } return true } @@ -507,17 +510,27 @@ func (bpr *bpRequester) getPeerID() p2p.ID { return bpr.peerID } +// This is called from the requestRoutine, upon redo(). func (bpr *bpRequester) reset() { bpr.mtx.Lock() + defer bpr.mtx.Unlock() + + if bpr.block != nil { + atomic.AddInt32(&bpr.pool.numPending, 1) + } + bpr.peerID = "" bpr.block = nil - bpr.mtx.Unlock() } // Tells bpRequester to pick another peer and try again. -// NOTE: blocking +// NOTE: Nonblocking, and does nothing if another redo +// was already requested. func (bpr *bpRequester) redo() { - bpr.redoCh <- struct{}{} + select { + case bpr.redoCh <- struct{}{}: + default: + } } // Responsible for making more requests as necessary @@ -546,17 +559,8 @@ OUTER_LOOP: // Send request and wait. bpr.pool.sendRequest(bpr.height, peer.id) - select { - case <-bpr.pool.Quit(): - bpr.Stop() - return - case <-bpr.Quit(): - return - case <-bpr.redoCh: - bpr.reset() - continue OUTER_LOOP // When peer is removed - case <-bpr.gotBlockCh: - // We got the block, now see if it's good. + WAIT_LOOP: + for { select { case <-bpr.pool.Quit(): bpr.Stop() @@ -566,6 +570,10 @@ OUTER_LOOP: case <-bpr.redoCh: bpr.reset() continue OUTER_LOOP + case <-bpr.gotBlockCh: + // We got a block! + // Continue the for-loop and wait til Quit. + continue WAIT_LOOP } } } From 600458734788aa499ae2f8ffbe4d7e942262180e Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 28 May 2018 14:37:11 +0400 Subject: [PATCH 41/59] expect all tags to be strings (#1498) * expect all tags to be strings Refs #1369 * port changes from https://github.com/tendermint/tmlibs/pull/204 Refs #1369 --- CHANGELOG.md | 7 +++ libs/pubsub/example_test.go | 2 +- libs/pubsub/pubsub.go | 32 ++++++------ libs/pubsub/pubsub_test.go | 14 ++--- libs/pubsub/query/empty_test.go | 8 +-- libs/pubsub/query/query.go | 90 +++++++++++++++------------------ libs/pubsub/query/query_test.go | 44 ++++++++-------- state/state_test.go | 4 +- types/event_bus.go | 8 +-- types/event_bus_test.go | 4 +- 10 files changed, 108 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bde5b2e40..a288d7404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.20.0 + +BREAKING: + +- [libs/pubsub] TagMap#Get returns a string value +- [libs/pubsub] NewTagMap accepts a map of strings + ## 0.19.6 FEATURES diff --git a/libs/pubsub/example_test.go b/libs/pubsub/example_test.go index 550b4447e..260521cd9 100644 --- a/libs/pubsub/example_test.go +++ b/libs/pubsub/example_test.go @@ -22,7 +22,7 @@ func TestExample(t *testing.T) { ch := make(chan interface{}, 1) err := s.Subscribe(ctx, "example-client", query.MustParse("abci.account.name='John'"), ch) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Tombstone", pubsub.NewTagMap(map[string]interface{}{"abci.account.name": "John"})) + err = s.PublishWithTags(ctx, "Tombstone", pubsub.NewTagMap(map[string]string{"abci.account.name": "John"})) require.NoError(t, err) assertReceive(t, "Tombstone", ch) } diff --git a/libs/pubsub/pubsub.go b/libs/pubsub/pubsub.go index 67f264ace..684ff358a 100644 --- a/libs/pubsub/pubsub.go +++ b/libs/pubsub/pubsub.go @@ -38,18 +38,6 @@ var ( ErrAlreadySubscribed = errors.New("already subscribed") ) -// TagMap is used to associate tags to a message. -// They can be queried by subscribers to choose messages they will received. -type TagMap interface { - // Get returns the value for a key, or nil if no value is present. - // The ok result indicates whether value was found in the tags. - Get(key string) (value interface{}, ok bool) - // Len returns the number of tags. - Len() int -} - -type tagMap map[string]interface{} - type cmd struct { op operation query Query @@ -80,14 +68,28 @@ type Server struct { // Option sets a parameter for the server. type Option func(*Server) +// TagMap is used to associate tags to a message. +// They can be queried by subscribers to choose messages they will received. +type TagMap interface { + // Get returns the value for a key, or nil if no value is present. + // The ok result indicates whether value was found in the tags. + Get(key string) (value string, ok bool) + // Len returns the number of tags. + Len() int +} + +type tagMap map[string]string + +var _ TagMap = (*tagMap)(nil) + // NewTagMap constructs a new immutable tag set from a map. -func NewTagMap(data map[string]interface{}) TagMap { +func NewTagMap(data map[string]string) TagMap { return tagMap(data) } // Get returns the value for a key, or nil if no value is present. // The ok result indicates whether value was found in the tags. -func (ts tagMap) Get(key string) (value interface{}, ok bool) { +func (ts tagMap) Get(key string) (value string, ok bool) { value, ok = ts[key] return } @@ -213,7 +215,7 @@ func (s *Server) UnsubscribeAll(ctx context.Context, clientID string) error { // Publish publishes the given message. An error will be returned to the caller // if the context is canceled. func (s *Server) Publish(ctx context.Context, msg interface{}) error { - return s.PublishWithTags(ctx, msg, NewTagMap(make(map[string]interface{}))) + return s.PublishWithTags(ctx, msg, NewTagMap(make(map[string]string))) } // PublishWithTags publishes the given message with the set of tags. The set is diff --git a/libs/pubsub/pubsub_test.go b/libs/pubsub/pubsub_test.go index a39d015ce..fd6c11cf4 100644 --- a/libs/pubsub/pubsub_test.go +++ b/libs/pubsub/pubsub_test.go @@ -49,14 +49,14 @@ func TestDifferentClients(t *testing.T) { ch1 := make(chan interface{}, 1) err := s.Subscribe(ctx, "client-1", query.MustParse("tm.events.type='NewBlock'"), ch1) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Iceman", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewBlock"})) + err = s.PublishWithTags(ctx, "Iceman", pubsub.NewTagMap(map[string]string{"tm.events.type": "NewBlock"})) require.NoError(t, err) assertReceive(t, "Iceman", ch1) ch2 := make(chan interface{}, 1) err = s.Subscribe(ctx, "client-2", query.MustParse("tm.events.type='NewBlock' AND abci.account.name='Igor'"), ch2) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Ultimo", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewBlock", "abci.account.name": "Igor"})) + err = s.PublishWithTags(ctx, "Ultimo", pubsub.NewTagMap(map[string]string{"tm.events.type": "NewBlock", "abci.account.name": "Igor"})) require.NoError(t, err) assertReceive(t, "Ultimo", ch1) assertReceive(t, "Ultimo", ch2) @@ -64,7 +64,7 @@ func TestDifferentClients(t *testing.T) { ch3 := make(chan interface{}, 1) err = s.Subscribe(ctx, "client-3", query.MustParse("tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10"), ch3) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Valeria Richards", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewRoundStep"})) + err = s.PublishWithTags(ctx, "Valeria Richards", pubsub.NewTagMap(map[string]string{"tm.events.type": "NewRoundStep"})) require.NoError(t, err) assert.Zero(t, len(ch3)) } @@ -81,7 +81,7 @@ func TestClientSubscribesTwice(t *testing.T) { ch1 := make(chan interface{}, 1) err := s.Subscribe(ctx, clientID, q, ch1) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Goblin Queen", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewBlock"})) + err = s.PublishWithTags(ctx, "Goblin Queen", pubsub.NewTagMap(map[string]string{"tm.events.type": "NewBlock"})) require.NoError(t, err) assertReceive(t, "Goblin Queen", ch1) @@ -89,7 +89,7 @@ func TestClientSubscribesTwice(t *testing.T) { err = s.Subscribe(ctx, clientID, q, ch2) require.Error(t, err) - err = s.PublishWithTags(ctx, "Spider-Man", pubsub.NewTagMap(map[string]interface{}{"tm.events.type": "NewBlock"})) + err = s.PublishWithTags(ctx, "Spider-Man", pubsub.NewTagMap(map[string]string{"tm.events.type": "NewBlock"})) require.NoError(t, err) assertReceive(t, "Spider-Man", ch1) } @@ -209,7 +209,7 @@ func benchmarkNClients(n int, b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - s.PublishWithTags(ctx, "Gamora", pubsub.NewTagMap(map[string]interface{}{"abci.Account.Owner": "Ivan", "abci.Invoices.Number": i})) + s.PublishWithTags(ctx, "Gamora", pubsub.NewTagMap(map[string]string{"abci.Account.Owner": "Ivan", "abci.Invoices.Number": string(i)})) } } @@ -232,7 +232,7 @@ func benchmarkNClientsOneQuery(n int, b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - s.PublishWithTags(ctx, "Gamora", pubsub.NewTagMap(map[string]interface{}{"abci.Account.Owner": "Ivan", "abci.Invoices.Number": 1})) + s.PublishWithTags(ctx, "Gamora", pubsub.NewTagMap(map[string]string{"abci.Account.Owner": "Ivan", "abci.Invoices.Number": "1"})) } } diff --git a/libs/pubsub/query/empty_test.go b/libs/pubsub/query/empty_test.go index 9c82f73ed..6183b6bd4 100644 --- a/libs/pubsub/query/empty_test.go +++ b/libs/pubsub/query/empty_test.go @@ -11,8 +11,8 @@ import ( func TestEmptyQueryMatchesAnything(t *testing.T) { q := query.Empty{} - assert.True(t, q.Matches(pubsub.NewTagMap(map[string]interface{}{}))) - assert.True(t, q.Matches(pubsub.NewTagMap(map[string]interface{}{"Asher": "Roth"}))) - assert.True(t, q.Matches(pubsub.NewTagMap(map[string]interface{}{"Route": 66}))) - assert.True(t, q.Matches(pubsub.NewTagMap(map[string]interface{}{"Route": 66, "Billy": "Blue"}))) + assert.True(t, q.Matches(pubsub.NewTagMap(map[string]string{}))) + assert.True(t, q.Matches(pubsub.NewTagMap(map[string]string{"Asher": "Roth"}))) + assert.True(t, q.Matches(pubsub.NewTagMap(map[string]string{"Route": "66"}))) + assert.True(t, q.Matches(pubsub.NewTagMap(map[string]string{"Route": "66", "Billy": "Blue"}))) } diff --git a/libs/pubsub/query/query.go b/libs/pubsub/query/query.go index a900d9838..ec187486e 100644 --- a/libs/pubsub/query/query.go +++ b/libs/pubsub/query/query.go @@ -77,6 +77,13 @@ const ( OpContains ) +const ( + // DateLayout defines a layout for all dates (`DATE date`) + DateLayout = "2006-01-02" + // TimeLayout defines a layout for all times (`TIME time`) + TimeLayout = time.RFC3339 +) + // Conditions returns a list of conditions. func (q *Query) Conditions() []Condition { conditions := make([]Condition, 0) @@ -112,7 +119,7 @@ func (q *Query) Conditions() []Condition { conditions = append(conditions, Condition{tag, op, valueWithoutSingleQuotes}) case rulenumber: number := buffer[begin:end] - if strings.Contains(number, ".") { // if it looks like a floating-point number + if strings.ContainsAny(number, ".") { // if it looks like a floating-point number value, err := strconv.ParseFloat(number, 64) if err != nil { panic(fmt.Sprintf("got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number)) @@ -126,7 +133,7 @@ func (q *Query) Conditions() []Condition { conditions = append(conditions, Condition{tag, op, value}) } case ruletime: - value, err := time.Parse(time.RFC3339, buffer[begin:end]) + value, err := time.Parse(TimeLayout, buffer[begin:end]) if err != nil { panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end])) } @@ -188,7 +195,7 @@ func (q *Query) Matches(tags pubsub.TagMap) bool { } case rulenumber: number := buffer[begin:end] - if strings.Contains(number, ".") { // if it looks like a floating-point number + if strings.ContainsAny(number, ".") { // if it looks like a floating-point number value, err := strconv.ParseFloat(number, 64) if err != nil { panic(fmt.Sprintf("got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number)) @@ -206,7 +213,7 @@ func (q *Query) Matches(tags pubsub.TagMap) bool { } } case ruletime: - value, err := time.Parse(time.RFC3339, buffer[begin:end]) + value, err := time.Parse(TimeLayout, buffer[begin:end]) if err != nil { panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end])) } @@ -242,9 +249,18 @@ func match(tag string, op Operator, operand reflect.Value, tags pubsub.TagMap) b switch operand.Kind() { case reflect.Struct: // time operandAsTime := operand.Interface().(time.Time) - v, ok := value.(time.Time) - if !ok { // if value from tags is not time.Time - return false + // try our best to convert value from tags to time.Time + var ( + v time.Time + err error + ) + if strings.ContainsAny(value, "T") { + v, err = time.Parse(TimeLayout, value) + } else { + v, err = time.Parse(DateLayout, value) + } + if err != nil { + panic(fmt.Sprintf("Failed to convert value %v from tag to time.Time: %v", value, err)) } switch op { case OpLessEqual: @@ -262,23 +278,9 @@ func match(tag string, op Operator, operand reflect.Value, tags pubsub.TagMap) b operandFloat64 := operand.Interface().(float64) var v float64 // try our best to convert value from tags to float64 - switch vt := value.(type) { - case float64: - v = vt - case float32: - v = float64(vt) - case int: - v = float64(vt) - case int8: - v = float64(vt) - case int16: - v = float64(vt) - case int32: - v = float64(vt) - case int64: - v = float64(vt) - default: // fail for all other types - panic(fmt.Sprintf("Incomparable types: %T (%v) vs float64 (%v)", value, value, operandFloat64)) + v, err := strconv.ParseFloat(value, 64) + if err != nil { + panic(fmt.Sprintf("Failed to convert value %v from tag to float64: %v", value, err)) } switch op { case OpLessEqual: @@ -295,24 +297,20 @@ func match(tag string, op Operator, operand reflect.Value, tags pubsub.TagMap) b case reflect.Int64: operandInt := operand.Interface().(int64) var v int64 - // try our best to convert value from tags to int64 - switch vt := value.(type) { - case int64: - v = vt - case int8: - v = int64(vt) - case int16: - v = int64(vt) - case int32: - v = int64(vt) - case int: - v = int64(vt) - case float64: - v = int64(vt) - case float32: - v = int64(vt) - default: // fail for all other types - panic(fmt.Sprintf("Incomparable types: %T (%v) vs int64 (%v)", value, value, operandInt)) + // if value looks like float, we try to parse it as float + if strings.ContainsAny(value, ".") { + v1, err := strconv.ParseFloat(value, 64) + if err != nil { + panic(fmt.Sprintf("Failed to convert value %v from tag to float64: %v", value, err)) + } + v = int64(v1) + } else { + var err error + // try our best to convert value from tags to int64 + v, err = strconv.ParseInt(value, 10, 64) + if err != nil { + panic(fmt.Sprintf("Failed to convert value %v from tag to int64: %v", value, err)) + } } switch op { case OpLessEqual: @@ -327,15 +325,11 @@ func match(tag string, op Operator, operand reflect.Value, tags pubsub.TagMap) b return v == operandInt } case reflect.String: - v, ok := value.(string) - if !ok { // if value from tags is not string - return false - } switch op { case OpEqual: - return v == operand.String() + return value == operand.String() case OpContains: - return strings.Contains(v, operand.String()) + return strings.Contains(value, operand.String()) } default: panic(fmt.Sprintf("Unknown kind of operand %v", operand.Kind())) diff --git a/libs/pubsub/query/query_test.go b/libs/pubsub/query/query_test.go index f266b1214..f0d940992 100644 --- a/libs/pubsub/query/query_test.go +++ b/libs/pubsub/query/query_test.go @@ -1,6 +1,7 @@ package query_test import ( + "fmt" "testing" "time" @@ -12,38 +13,37 @@ import ( ) func TestMatches(t *testing.T) { - const shortForm = "2006-Jan-02" - txDate, err := time.Parse(shortForm, "2017-Jan-01") - require.NoError(t, err) - txTime, err := time.Parse(time.RFC3339, "2018-05-03T14:45:00Z") - require.NoError(t, err) + var ( + txDate = "2017-01-01" + txTime = "2018-05-03T14:45:00Z" + ) testCases := []struct { s string - tags map[string]interface{} + tags map[string]string err bool matches bool }{ - {"tm.events.type='NewBlock'", map[string]interface{}{"tm.events.type": "NewBlock"}, false, true}, + {"tm.events.type='NewBlock'", map[string]string{"tm.events.type": "NewBlock"}, false, true}, - {"tx.gas > 7", map[string]interface{}{"tx.gas": 8}, false, true}, - {"tx.gas > 7 AND tx.gas < 9", map[string]interface{}{"tx.gas": 8}, false, true}, - {"body.weight >= 3.5", map[string]interface{}{"body.weight": 3.5}, false, true}, - {"account.balance < 1000.0", map[string]interface{}{"account.balance": 900}, false, true}, - {"apples.kg <= 4", map[string]interface{}{"apples.kg": 4.0}, false, true}, - {"body.weight >= 4.5", map[string]interface{}{"body.weight": float32(4.5)}, false, true}, - {"oranges.kg < 4 AND watermellons.kg > 10", map[string]interface{}{"oranges.kg": 3, "watermellons.kg": 12}, false, true}, - {"peaches.kg < 4", map[string]interface{}{"peaches.kg": 5}, false, false}, + {"tx.gas > 7", map[string]string{"tx.gas": "8"}, false, true}, + {"tx.gas > 7 AND tx.gas < 9", map[string]string{"tx.gas": "8"}, false, true}, + {"body.weight >= 3.5", map[string]string{"body.weight": "3.5"}, false, true}, + {"account.balance < 1000.0", map[string]string{"account.balance": "900"}, false, true}, + {"apples.kg <= 4", map[string]string{"apples.kg": "4.0"}, false, true}, + {"body.weight >= 4.5", map[string]string{"body.weight": fmt.Sprintf("%v", float32(4.5))}, false, true}, + {"oranges.kg < 4 AND watermellons.kg > 10", map[string]string{"oranges.kg": "3", "watermellons.kg": "12"}, false, true}, + {"peaches.kg < 4", map[string]string{"peaches.kg": "5"}, false, false}, - {"tx.date > DATE 2017-01-01", map[string]interface{}{"tx.date": time.Now()}, false, true}, - {"tx.date = DATE 2017-01-01", map[string]interface{}{"tx.date": txDate}, false, true}, - {"tx.date = DATE 2018-01-01", map[string]interface{}{"tx.date": txDate}, false, false}, + {"tx.date > DATE 2017-01-01", map[string]string{"tx.date": time.Now().Format(query.DateLayout)}, false, true}, + {"tx.date = DATE 2017-01-01", map[string]string{"tx.date": txDate}, false, true}, + {"tx.date = DATE 2018-01-01", map[string]string{"tx.date": txDate}, false, false}, - {"tx.time >= TIME 2013-05-03T14:45:00Z", map[string]interface{}{"tx.time": time.Now()}, false, true}, - {"tx.time = TIME 2013-05-03T14:45:00Z", map[string]interface{}{"tx.time": txTime}, false, false}, + {"tx.time >= TIME 2013-05-03T14:45:00Z", map[string]string{"tx.time": time.Now().Format(query.TimeLayout)}, false, true}, + {"tx.time = TIME 2013-05-03T14:45:00Z", map[string]string{"tx.time": txTime}, false, false}, - {"abci.owner.name CONTAINS 'Igor'", map[string]interface{}{"abci.owner.name": "Igor,Ivan"}, false, true}, - {"abci.owner.name CONTAINS 'Igor'", map[string]interface{}{"abci.owner.name": "Pavel,Ivan"}, false, false}, + {"abci.owner.name CONTAINS 'Igor'", map[string]string{"abci.owner.name": "Igor,Ivan"}, false, true}, + {"abci.owner.name CONTAINS 'Igor'", map[string]string{"abci.owner.name": "Pavel,Ivan"}, false, false}, } for _, tc := range testCases { diff --git a/state/state_test.go b/state/state_test.go index ba995cc00..497695373 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/tendermint/abci/types" - "github.com/tendermint/go-crypto" + crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" @@ -121,7 +121,7 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { {Code: 383}, {Data: []byte("Gotcha!"), Tags: []cmn.KVPair{ - cmn.KVPair{[]byte("a"), []byte{1}}, + cmn.KVPair{[]byte("a"), []byte("1")}, cmn.KVPair{[]byte("build"), []byte("stuff")}, }}, }, diff --git a/types/event_bus.go b/types/event_bus.go index 925907fd0..2bc339da7 100644 --- a/types/event_bus.go +++ b/types/event_bus.go @@ -67,7 +67,7 @@ func (b *EventBus) UnsubscribeAll(ctx context.Context, subscriber string) error func (b *EventBus) Publish(eventType string, eventData TMEventData) error { // no explicit deadline for publishing events ctx := context.Background() - b.pubsub.PublishWithTags(ctx, eventData, tmpubsub.NewTagMap(map[string]interface{}{EventTypeKey: eventType})) + b.pubsub.PublishWithTags(ctx, eventData, tmpubsub.NewTagMap(map[string]string{EventTypeKey: eventType})) return nil } @@ -92,7 +92,7 @@ func (b *EventBus) PublishEventTx(event EventDataTx) error { // no explicit deadline for publishing events ctx := context.Background() - tags := make(map[string]interface{}) + tags := make(map[string]string) // validate and fill tags from tx result for _, tag := range event.Result.Tags { @@ -112,7 +112,7 @@ func (b *EventBus) PublishEventTx(event EventDataTx) error { tags[TxHashKey] = fmt.Sprintf("%X", event.Tx.Hash()) logIfTagExists(TxHeightKey, tags, b.Logger) - tags[TxHeightKey] = event.Height + tags[TxHeightKey] = fmt.Sprintf("%d", event.Height) b.pubsub.PublishWithTags(ctx, event, tmpubsub.NewTagMap(tags)) return nil @@ -160,7 +160,7 @@ func (b *EventBus) PublishEventLock(event EventDataRoundState) error { return b.Publish(EventLock, event) } -func logIfTagExists(tag string, tags map[string]interface{}, logger log.Logger) { +func logIfTagExists(tag string, tags map[string]string, logger log.Logger) { if value, ok := tags[tag]; ok { logger.Error("Found predefined tag (value will be overwritten)", "tag", tag, "value", value) } diff --git a/types/event_bus_test.go b/types/event_bus_test.go index 95d061f40..8358ad261 100644 --- a/types/event_bus_test.go +++ b/types/event_bus_test.go @@ -23,12 +23,12 @@ func TestEventBusPublishEventTx(t *testing.T) { defer eventBus.Stop() tx := Tx("foo") - result := abci.ResponseDeliverTx{Data: []byte("bar"), Tags: []cmn.KVPair{}, Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0}} + result := abci.ResponseDeliverTx{Data: []byte("bar"), Tags: []cmn.KVPair{{[]byte("baz"), []byte("1")}}, Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0}} txEventsCh := make(chan interface{}) // PublishEventTx adds all these 3 tags, so the query below should work - query := fmt.Sprintf("tm.event='Tx' AND tx.height=1 AND tx.hash='%X'", tx.Hash()) + query := fmt.Sprintf("tm.event='Tx' AND tx.height=1 AND tx.hash='%X' AND baz=1", tx.Hash()) err = eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query), txEventsCh) require.NoError(t, err) From ec34c8f9d26cb0d46259f34a67af4385b6e88eaa Mon Sep 17 00:00:00 2001 From: Zach Date: Mon, 28 May 2018 14:06:02 -0400 Subject: [PATCH 42/59] docs: update ABCI output (#1635) --- docs/abci-cli.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/abci-cli.rst b/docs/abci-cli.rst index 4cdd9b234..d4a73723a 100644 --- a/docs/abci-cli.rst +++ b/docs/abci-cli.rst @@ -183,6 +183,7 @@ Try running these commands: > commit -> code: OK + -> data.hex: 0x0000000000000000 > deliver_tx "abc" -> code: OK @@ -194,7 +195,7 @@ Try running these commands: > commit -> code: OK - -> data.hex: 0x49DFD15CCDACDEAE9728CB01FBB5E8688CA58B91 + -> data.hex: 0x0200000000000000 > query "abc" -> code: OK @@ -208,7 +209,7 @@ Try running these commands: > commit -> code: OK - -> data.hex: 0x70102DB32280373FBF3F9F89DA2A20CE2CD62B0B + -> data.hex: 0x0400000000000000 > query "def" -> code: OK @@ -301,6 +302,7 @@ In another window, start the ``abci-cli console``: > set_option serial on -> code: OK + -> log: OK (SetOption doesn't return anything.) > check_tx 0x00 -> code: OK From c8be091d4a41976252e931f2a570c121be235d41 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Mon, 28 May 2018 16:44:14 -0400 Subject: [PATCH 43/59] gut docs/app-arch --- docs/app-architecture.rst | 151 ++++++++------------------------------ 1 file changed, 30 insertions(+), 121 deletions(-) diff --git a/docs/app-architecture.rst b/docs/app-architecture.rst index 4a7c414ec..cbcac4d12 100644 --- a/docs/app-architecture.rst +++ b/docs/app-architecture.rst @@ -1,133 +1,42 @@ Application Architecture Guide ============================== -Overview --------- +Here we provide a brief guide on the recommended architecture of a Tendermint blockchain +application. -A blockchain application is more than the consensus engine and the -transaction logic (eg. smart contracts, business logic) as implemented -in the ABCI app. There are also (mobile, web, desktop) clients that will -need to connect and make use of the app. We will assume for now that you -have a well designed transactions and database model, but maybe this -will be the topic of another article. This article is more interested in -various ways of setting up the "plumbing" and connecting these pieces, -and demonstrating some evolving best practices. +The following diagram provides a superb example: -Security --------- +https://lh3.googleusercontent.com/eOT9KuaBjv7xKBg2xN8u2O1nU2Iw1-6PtFXrBSFWW8LYhfAMu84EwXW4RwyOnuiNqCoOoqXxE8Pkhr4Fyq9f=w2559-h1303-rw -A very important aspect when constructing a blockchain is security. The -consensus model can be DoSed (no consensus possible) by corrupting 1/3 -of the validators and exploited (writing arbitrary blocks) by corrupting -2/3 of the validators. So, while the security is not that of the -"weakest link", you should take care that the "average link" is -sufficiently hardened. +The end-user application here is the Cosmos Voyager, at the bottom left. +Voyager communicates with a REST API exposed by a local Light-Client Daemon. +The Light-Client Daemon is an application specific program that communciates with +Tendermint nodes and verifies Tendermint light-client proofs through the Tendermint Core RPC. +The Tendermint Core process communicates with a local ABCI application, where the +user query or transaction is actually processed. -One big attack surface on the validators is the communication between -the ABCI app and the tendermint core. This should be highly protected. -Ideally, the app and the core are running on the same machine, so no -external agent can target the communication channel. You can use unix -sockets (with permissions preventing access from other users), or even -compile the two apps into one binary if the ABCI app is also writen in -go. If you are unable to do that due to language support, then the ABCI -app should bind a TCP connection to localhost (127.0.0.1), which is less -efficient and secure, but still not reachable from outside. If you must -run the ABCI app and tendermint core on separate machines, make sure you -have a secure communication channel (ssh tunnel?) +The ABCI application must be a deterministic result of the Tendermint consensus - any external influence +on the application state that didn't come through Tendermint could cause a +consensus failure. Thus *nothing* should communicate with the application except Tendermint via ABCI. -Now assuming, you have linked together your app and the core securely, -you must also make sure no one can get on the machine it is hosted on. -At this point it is basic network security. Run on a secure operating -system (SELinux?). Limit who has access to the machine (user accounts, -but also where the physical machine is hosted). Turn off all services -except for ssh, which should only be accessible by some well-guarded -public/private key pairs (no password). And maybe even firewall off -access to the ports used by the validators, so only known validators can -connect. +If the application is written in Go, it can be compiled into the Tendermint binary. +Otherwise, it should use a unix socket to communicate with Tendermint. +If it's necessary to use TCP, extra care must be taken to encrypt and authenticate the connection. -There was also a suggestion on slack from @jhon about compiling -everything together with a unikernel for more security, such as -`Mirage `__ or -`UNIK `__. +All reads from the app happen through the Tendermint `/abci_query` endpoint. +All writes to the app happen through the Tendermint `/broadcast_tx_*` endpoints. -Connecting your client to the blockchain ----------------------------------------- +The Light-Client Daemon is what provides light clients (end users) with nearly all the security of a full node. +It formats and broadcasts transactions, and verifies proofs of queries and transaction results. +Note that it need not be a daemon - the Light-Client logic could instead be implemented in the same process as the end-user application. -Tendermint Core RPC -~~~~~~~~~~~~~~~~~~~ +Note for those ABCI applications with weaker security requirements, the functionality of the Light-Client Daemon can be moved +into the ABCI application process itself. That said, exposing the application process to anything besides Tendermint over ABCI +requires extreme caution, as all transactions, and possibly all queries, should still pass through Tendermint. -The concept is that the ABCI app is completely hidden from the outside -world and only communicated through a tested and secured `interface -exposed by the tendermint core <./specification/rpc.html>`__. This interface -exposes a lot of data on the block header and consensus process, which -is quite useful for externally verifying the system. It also includes -3(!) methods to broadcast a transaction (propose it for the blockchain, -and possibly await a response). And one method to query app-specific -data from the ABCI application. - -Pros: - -- Server code already written -- Access to block headers to validate merkle proofs (nice for light clients) -- Basic read/write functionality is supported - -Cons: - -- Limited interface to app. All queries must be serialized into []byte (less expressive than JSON over HTTP) and there is no way to push data from ABCI app to the client (eg. notify me if account X receives a transaction) - -Custom ABCI server -~~~~~~~~~~~~~~~~~~ - -This was proposed by @wolfposd on slack and demonstrated by -`TMChat `__, a sample app. The -concept is to write a custom server for your app (with typical REST -API/websockets/etc for easy use by a mobile app). This custom server is -in the same binary as the ABCI app and data store, so can easily react -to complex events there that involve understanding the data format (send -a message if my balance drops below 500). All "writes" sent to this -server are proxied via websocket/JSON-RPC to tendermint core. When they -come back as deliver\_tx over ABCI, they will be written to the data -store. For "reads", we can do any queries we wish that are supported by -our architecture, using any web technology that is useful. The general -architecture is shown in the following diagram: - -.. figure:: assets/tm-application-example.png - -Pros: - -- Separates application logic from blockchain logic -- Allows much richer, more flexible client-facing API -- Allows pub-sub, watching certain fields, etc. - -Cons: - -- Access to ABCI app can be dangerous (be VERY careful not to write unless it comes from the validator node) -- No direct access to the blockchain headers to verify tx -- You must write your own API (but maybe that's a pro...) - -Hybrid solutions -~~~~~~~~~~~~~~~~ - -Likely the least secure but most versatile. The client can access both -the tendermint node for all blockchain info, as well as a custom app -server, for complex queries and pub-sub on the abci app. - -Pros: - -- All from both above solutions - -Cons: - -- Even more complexity; even more attack vectors (less -security) - -Scalability ------------ - -Read replica using non-validating nodes? They could forward transactions -to the validators (fewer connections, more security), and locally allow -all queries in any of the above configurations. Thus, while -transaction-processing speed is limited by the speed of the abci app and -the number of validators, one should be able to scale our read -performance to quite an extent (until the replication process drains too -many resources from the validator nodes). +See the following for more extensive documentation: +- [Interchain Standard for the Light-Client REST API](https://github.com/cosmos/cosmos-sdk/pull/1028) +- [Tendermint RPC Docs](https://tendermint.github.io/slate/) +- [Tendermint in Production](https://github.com/tendermint/tendermint/pull/1618) +- [Tendermint Basics](https://tendermint.readthedocs.io/en/master/using-tendermint.html) +- [ABCI spec](https://github.com/tendermint/abci/blob/master/specification.rst) From 1e87ef7f75eb46a989ed33be18d293935a4c889b Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 29 May 2018 09:28:29 -0400 Subject: [PATCH 44/59] circle: add GOCACHE=off and -v to tests --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4209d531e..30b70f77e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -139,7 +139,7 @@ jobs: for pkg in $(go list github.com/tendermint/tendermint/... | grep -v /vendor/ | circleci tests split --split-by=timings); do id=$(basename "$pkg") - go test -timeout 5m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic "$pkg" + GOCACHE=off go test -v -timeout 5m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic "$pkg" done - persist_to_workspace: root: /tmp/workspace From 5796e879b9ca85509218f3d53665c45f94ed4176 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Wed, 30 May 2018 10:37:39 +0200 Subject: [PATCH 45/59] Introduce option to skip duplicate ip check In some scenarios like tests we want to disable the guard which prevents peers connecting from the same ip. Fixes #1632 Closes #1634 --- config/config.go | 4 ++ p2p/peer_set.go | 4 -- p2p/peer_set_test.go | 14 ------- p2p/pex/pex_reactor_test.go | 83 +++++++++++++++++++------------------ p2p/switch.go | 5 ++- p2p/test_util.go | 5 +-- 6 files changed, 50 insertions(+), 65 deletions(-) diff --git a/config/config.go b/config/config.go index 47df46264..932e1ae8e 100644 --- a/config/config.go +++ b/config/config.go @@ -292,6 +292,9 @@ type P2PConfig struct { // Comma separated list of peer IDs to keep private (will not be gossiped to other peers) PrivatePeerIDs string `mapstructure:"private_peer_ids"` + + // Toggle to disable guard against peers connecting from the same ip. + SkipDuplicatePeerIPCheck bool `mapstructure:"skip_duplicate_peer_ip_check"` } // DefaultP2PConfig returns a default configuration for the peer-to-peer layer @@ -317,6 +320,7 @@ func TestP2PConfig() *P2PConfig { cfg.ListenAddress = "tcp://0.0.0.0:36656" cfg.SkipUPNP = true cfg.FlushThrottleTimeout = 10 + cfg.SkipDuplicatePeerIPCheck = true return cfg } diff --git a/p2p/peer_set.go b/p2p/peer_set.go index 66a7fdadb..e048cf4e3 100644 --- a/p2p/peer_set.go +++ b/p2p/peer_set.go @@ -47,10 +47,6 @@ func (ps *PeerSet) Add(peer Peer) error { return ErrSwitchDuplicatePeerID{peer.ID()} } - if ps.hasIP(peer.RemoteIP()) { - return ErrSwitchDuplicatePeerIP{peer.RemoteIP()} - } - index := len(ps.list) // Appending is safe even with other goroutines // iterating over the ps.list slice. diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go index fc3004684..172767781 100644 --- a/p2p/peer_set_test.go +++ b/p2p/peer_set_test.go @@ -143,20 +143,6 @@ func TestPeerSetAddDuplicate(t *testing.T) { assert.Equal(t, wantNilErrCount, gotNilErrCount, "invalid nil errCount") } -func TestPeerSetAddDuplicateIP(t *testing.T) { - t.Parallel() - - peerSet := NewPeerSet() - - if err := peerSet.Add(randPeer(net.IP{172, 0, 0, 1})); err != nil { - t.Fatal(err) - } - - // Add peer with same IP. - err := peerSet.Add(randPeer(net.IP{172, 0, 0, 1})) - assert.Equal(t, ErrSwitchDuplicatePeerIP{IP: net.IP{172, 0, 0, 1}}, err) -} - func TestPeerSetGet(t *testing.T) { t.Parallel() diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 55960e6f5..06fe6174e 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -27,6 +27,7 @@ var ( func init() { config = cfg.DefaultP2PConfig() config.PexReactor = true + config.SkipDuplicatePeerIPCheck = true } func TestPEXReactorBasic(t *testing.T) { @@ -69,59 +70,59 @@ func TestPEXReactorAddRemovePeer(t *testing.T) { // peers have different IP addresses, they all have the same underlying remote // IP: 127.0.0.1. // -// func TestPEXReactorRunning(t *testing.T) { -// N := 3 -// switches := make([]*p2p.Switch, N) +func TestPEXReactorRunning(t *testing.T) { + N := 3 + switches := make([]*p2p.Switch, N) -// // directory to store address books -// dir, err := ioutil.TempDir("", "pex_reactor") -// require.Nil(t, err) -// defer os.RemoveAll(dir) // nolint: errcheck + // directory to store address books + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(t, err) + defer os.RemoveAll(dir) // nolint: errcheck -// books := make([]*addrBook, N) -// logger := log.TestingLogger() + books := make([]*addrBook, N) + logger := log.TestingLogger() -// // create switches -// for i := 0; i < N; i++ { -// switches[i] = p2p.MakeSwitch(config, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { -// books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false) -// books[i].SetLogger(logger.With("pex", i)) -// sw.SetAddrBook(books[i]) + // create switches + for i := 0; i < N; i++ { + switches[i] = p2p.MakeSwitch(config, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { + books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false) + books[i].SetLogger(logger.With("pex", i)) + sw.SetAddrBook(books[i]) -// sw.SetLogger(logger.With("pex", i)) + sw.SetLogger(logger.With("pex", i)) -// r := NewPEXReactor(books[i], &PEXReactorConfig{}) -// r.SetLogger(logger.With("pex", i)) -// r.SetEnsurePeersPeriod(250 * time.Millisecond) -// sw.AddReactor("pex", r) + r := NewPEXReactor(books[i], &PEXReactorConfig{}) + r.SetLogger(logger.With("pex", i)) + r.SetEnsurePeersPeriod(250 * time.Millisecond) + sw.AddReactor("pex", r) -// return sw -// }) -// } + return sw + }) + } -// addOtherNodeAddrToAddrBook := func(switchIndex, otherSwitchIndex int) { -// addr := switches[otherSwitchIndex].NodeInfo().NetAddress() -// books[switchIndex].AddAddress(addr, addr) -// } + addOtherNodeAddrToAddrBook := func(switchIndex, otherSwitchIndex int) { + addr := switches[otherSwitchIndex].NodeInfo().NetAddress() + books[switchIndex].AddAddress(addr, addr) + } -// addOtherNodeAddrToAddrBook(0, 1) -// addOtherNodeAddrToAddrBook(1, 0) -// addOtherNodeAddrToAddrBook(2, 1) + addOtherNodeAddrToAddrBook(0, 1) + addOtherNodeAddrToAddrBook(1, 0) + addOtherNodeAddrToAddrBook(2, 1) -// for i, sw := range switches { -// sw.AddListener(p2p.NewDefaultListener("tcp", sw.NodeInfo().ListenAddr, true, logger.With("pex", i))) + for i, sw := range switches { + sw.AddListener(p2p.NewDefaultListener("tcp", sw.NodeInfo().ListenAddr, true, logger.With("pex", i))) -// err := sw.Start() // start switch and reactors -// require.Nil(t, err) -// } + err := sw.Start() // start switch and reactors + require.Nil(t, err) + } -// assertPeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second, N-1) + assertPeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second, N-1) -// // stop them -// for _, s := range switches { -// s.Stop() -// } -// } + // stop them + for _, s := range switches { + s.Stop() + } +} func TestPEXReactorReceive(t *testing.T) { r, book := createReactor(&PEXReactorConfig{}) diff --git a/p2p/switch.go b/p2p/switch.go index dc9b1698a..7656b9b02 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -577,8 +577,9 @@ func (sw *Switch) addPeer(pc peerConn) error { } // Check for duplicate connection or peer info IP. - if sw.peers.HasIP(pc.RemoteIP()) || - sw.peers.HasIP(peerNodeInfo.NetAddress().IP) { + if !sw.config.SkipDuplicatePeerIPCheck && + (sw.peers.HasIP(pc.RemoteIP()) || + sw.peers.HasIP(peerNodeInfo.NetAddress().IP)) { return ErrSwitchDuplicatePeerIP{pc.RemoteIP()} } diff --git a/p2p/test_util.go b/p2p/test_util.go index 86955f692..b5b739af9 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -3,7 +3,6 @@ package p2p import ( "fmt" "net" - "sync/atomic" crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" @@ -132,8 +131,6 @@ func StartSwitches(switches []*Switch) error { return nil } -var listenAddrSuffix uint32 = 1 - func MakeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch func(int, *Switch) *Switch) *Switch { // new switch, add reactors // TODO: let the config be passed in? @@ -148,7 +145,7 @@ func MakeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch f Moniker: cmn.Fmt("switch%d", i), Network: network, Version: version, - ListenAddr: fmt.Sprintf("127.0.0.%d:%d", atomic.AddUint32(&listenAddrSuffix, 1), cmn.RandIntn(64512)+1023), + ListenAddr: fmt.Sprintf("127.0.0.1:%d", cmn.RandIntn(64512)+1023), } for ch := range sw.reactorsByCh { ni.Channels = append(ni.Channels, ch) From eec9f142b56739bccc94a6ecffb88f0b616b00ff Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 30 May 2018 13:10:04 +0400 Subject: [PATCH 46/59] [docs] indexing transactions and subscribing to events Refs #1643 --- docs/index.rst | 2 + docs/indexing-transactions.rst | 100 +++++++++++++++++++ docs/subscribing-to-events-via-websocket.rst | 28 ++++++ 3 files changed, 130 insertions(+) create mode 100644 docs/indexing-transactions.rst create mode 100644 docs/subscribing-to-events-via-websocket.rst diff --git a/docs/index.rst b/docs/index.rst index 2bed07446..a9b207a12 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,6 +55,8 @@ Tendermint 102 abci-spec.rst app-architecture.rst app-development.rst + subscribing-to-events-via-websocket.rst + indexing-transactions.rst how-to-read-logs.rst Tendermint 201 diff --git a/docs/indexing-transactions.rst b/docs/indexing-transactions.rst new file mode 100644 index 000000000..2487a771c --- /dev/null +++ b/docs/indexing-transactions.rst @@ -0,0 +1,100 @@ +Indexing Transactions +===================== + +Tendermint allows you to index transactions and later query or subscribe to +their results. + +Let's take a look at the ``[tx_index]`` config section: + +:: + + ##### transactions indexer configuration options ##### + [tx_index] + + # What indexer to use for transactions + # + # Options: + # 1) "null" (default) + # 2) "kv" - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). + indexer = "kv" + + # Comma-separated list of tags to index (by default the only tag is tx hash) + # + # It's recommended to index only a subset of tags due to possible memory + # bloat. This is, of course, depends on the indexer's DB and the volume of + # transactions. + index_tags = "" + + # When set to true, tells indexer to index all tags. Note this may be not + # desirable (see the comment above). IndexTags has a precedence over + # IndexAllTags (i.e. when given both, IndexTags will be indexed). + index_all_tags = false + +By default, Tendermint will index all transactions by their respective hashes +using an embedded simple indexer. Note, we are planning to add more options in +the future (e.g., Postgresql indexer). + +Adding tags +----------- + +In your application's ``DeliverTx`` method, add the ``Tags`` field with the +pairs of UTF-8 encoded strings (e.g. "account.owner": "Bob", "balance": +"100.0", "date": "2018-01-02"). + +Example: + +:: + + func (app *KVStoreApplication) DeliverTx(tx []byte) types.Result { + ... + tags := []cmn.KVPair{ + {[]byte("account.name"), []byte("igor")}, + {[]byte("account.address"), []byte("0xdeadbeef")}, + {[]byte("tx.amount"), []byte("7")}, + } + return types.ResponseDeliverTx{Code: code.CodeTypeOK, Tags: tags} + } + +If you want Tendermint to only index transactions by "account.name" tag, in the +config set ``tx_index.index_tags="account.name"``. If you to index all tags, +set ``index_all_tags=true`` + +Note, there are a few predefined tags: + +- ``tm.event`` (event type) +- ``tx.hash`` (transaction's hash) +- ``tx.height`` (height of the block transaction was committed in) + +Tendermint will throw a warning if you try to use any of the above keys. + +Quering transactions +-------------------- + +You can query the transaction results by calling ``/tx_search`` RPC endpoint: + +:: + + curl "localhost:46657/tx_search?query=\"account.name='igor'\"&prove=true" + +Check out `API docs `__ for more +information on query syntax and other options. + +Subscribing to transactions +--------------------------- + +Clients can subscribe to transactions with the given tags via Websocket by +providing a query to ``/subscribe`` RPC endpoint. + +:: + + { + "jsonrpc": "2.0", + "method": "subscribe", + "id": "0", + "params": { + "query": "account.name='igor'" + } + } + +Check out `API docs `__ for more +information on query syntax and other options. diff --git a/docs/subscribing-to-events-via-websocket.rst b/docs/subscribing-to-events-via-websocket.rst new file mode 100644 index 000000000..f99d94b66 --- /dev/null +++ b/docs/subscribing-to-events-via-websocket.rst @@ -0,0 +1,28 @@ +Subscribing to events via Websocket +=================================== + +Tendermint emits different events, to which you can subscribe via `Websocket +`__. This can be useful for +third-party applications (for analysys) or inspecting state. + +`List of events `__ + +You can subscribe to any of the events above by calling ``subscribe`` RPC method via Websocket. + +:: + + { + "jsonrpc": "2.0", + "method": "subscribe", + "id": "0", + "params": { + "query": "tm.event='NewBlock'" + } + } + +Check out `API docs `__ for more +information on query syntax and other options. + +You can also use tags, given you had included them into DeliverTx response, to +query transaction results. See `Indexing transactions +<./indexing-transactions.html>`__ for details. From 33ec8cb6092189e163fd9bae4b7e8439daa4b51f Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 22 May 2018 16:55:29 +0400 Subject: [PATCH 47/59] document logging Refs #1494 --- docs/running-in-production.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/running-in-production.rst diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst new file mode 100644 index 000000000..d9bbe33b9 --- /dev/null +++ b/docs/running-in-production.rst @@ -0,0 +1,20 @@ +Running in production +===================== + +Logging +------- + +Default logging level (``main:info,state:info,*:``) should suffice for normal +operation mode. Read `this post +__` +for details on how to configure ``log_level`` config variable. Some of the +modules can be found `here <./how-to-read-logs.html#list-of-modules>__`. + +If you're trying to debug Tendermint or asked to provide logs with debug +logging level, you can do so by running tendermint with +``--log_level="*:debug"``. + +Consensus WAL +------------- + +Consensus module writes every message to the WAL (write-ahead log). From 83c6f2864df90d118c9626ed5d5f1e9ae9b68538 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 23 May 2018 10:29:44 +0400 Subject: [PATCH 48/59] document the consensus WAL Refs #1494 --- docs/running-in-production.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index d9bbe33b9..2e435f830 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -18,3 +18,33 @@ Consensus WAL ------------- Consensus module writes every message to the WAL (write-ahead log). + +It also issues fsync syscall through `File#Sync +__` for messages signed by this node (to +prevent double signing). + +Under the hood, it uses `autofile.Group +__`, which +rotates files when those get too big (> 10MB). + +The total maximum size is 1GB. We only need the latest block and the block before it, +but if the former is dragging on across many rounds, we want all those rounds. + +Replay +~~~~~~ + +Consensus module will replay all the messages of the last height written to WAL +before a crash (if such occurs). + +The private validator may try to sign messages during replay because it runs +somewhat autonomously and does not know about replay process. + +For example, if we got all the way to precommit in the WAL and then crash, +after we replay the proposal message, the private validator will try to sign a +prevote. But it will fail. That's ok because we’ll see the prevote later in the +WAL. Then it will go to precommit, and that time it will work because the +private validator contains the ``LastSignBytes`` and then we’ll replay the +precommit from the WAL. + +Make sure to read about `WAL corruption +<./specification/corruption.html#wal-corruption>__` and recovery strategies. From e0d4fe2dba27b517d9875224c227b0ac09f3323c Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 23 May 2018 12:41:47 +0400 Subject: [PATCH 49/59] document DOS exposure and mitigation Refs #1494 --- docs/running-in-production.rst | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index 2e435f830..5fe4684b2 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -48,3 +48,87 @@ precommit from the WAL. Make sure to read about `WAL corruption <./specification/corruption.html#wal-corruption>__` and recovery strategies. + +DOS Exposure and Mitigation +--------------------------- + +Validators are supposed to setup `Sentry Node Architecture +__` +to prevent Denial-of-service attacks. You can read more about it `here +__`. + +Blockchain Reactor +~~~~~~~~~~~~~~~~~~ + +Defines ``maxMsgSize`` for the maximum size of incoming messages, +``SendQueueCapacity`` and ``RecvBufferCapacity`` for maximum sending and +receiving buffers respectively. These are supposed to prevent amplification +attacks by setting up the upper limit on how much data we can receive & send to +a peer. + +Sending incorrectly encoded data will result in stopping the peer. + +Consensus Reactor +~~~~~~~~~~~~~~~~~ + +Defines 4 channels: state, data, vote and vote_set_bits. Each channel +has ``SendQueueCapacity`` and ``RecvBufferCapacity`` and +``RecvMessageCapacity`` set to ``maxMsgSize``. + +Sending incorrectly encoded data will result in stopping the peer. + +Evidence Reactor +~~~~~~~~~~~~~~~~ + +`#1503 __` + +Sending invalid evidence will result in stopping the peer. + +Sending incorrectly encoded data or data exceeding ``maxMsgSize`` will result +in stopping the peer. + +PEX Reactor +~~~~~~~~~~~ + +Defines only ``SendQueueCapacity``. `#1503 __` + +Implements rate-limiting by enforcing minimal time between two consecutive +``pexRequestMessage`` requests. If the peer sends us addresses we did not ask, +it is stopped. + +Sending incorrectly encoded data or data exceeding ``maxMsgSize`` will result +in stopping the peer. + +Mempool Reactor +~~~~~~~~~~~~~~~ + +`#1503 __` + +Mempool maintains a cache of the last 10000 transactions to prevent replaying +old transactions (plus transactions coming from other validators, who are +continually exchanging transactions). Read `Replay Protection +<./app-development.html#replay-protection>` for details. + +Sending incorrectly encoded data or data exceeding ``maxMsgSize`` will result +in stopping the peer. + +P2P +~~~ + +The core of the Tendermint peer-to-peer system is ``MConnection``. Each +connection has ``MaxPacketMsgPayloadSize``, which is the maximum packet size +and bounded send & receive queues. One can impose restrictions on send & +receive rate per connection (``SendRate``, ``RecvRate``). + +RPC +~~~ + +Endpoints returning multiple entries are limited by default to return 30 +elements (100 max). + +Rate-limiting and authentication are another key aspects to help protect +against DOS attacks. While in the future we may implement these features, for +now, validators are supposed to use external tools like `NGINX +__` or `traefik +__` to archive +the same things. From 82ded582f2e5e72f38ca9c85cd10e6a4150d95c8 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 28 May 2018 15:51:59 +0400 Subject: [PATCH 50/59] [docs] debugging/monitoring sections, restart handling Refs #1494 --- docs/running-in-production.rst | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index 5fe4684b2..ec3b46db6 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -132,3 +132,58 @@ now, validators are supposed to use external tools like `NGINX __` or `traefik __` to archive the same things. + +Debugging Tendermint +-------------------- + +If you ever have to debug Tendermint, the first thing you should probably do is +to check out the logs. See `"How to read logs" <./how-to-read-logs.html>__`, +where we explain what certain log statements mean. + +If, after skimming through the logs, things are not clear still, the second +TODO is to query the `/status` RPC endpoint. It provides the necessary info: +whenever the node is syncing or not, what height it is on, etc. + +``` +$ curl http(s)://{ip}:{rpcPort}/status +``` + +`/dump_consensus_state` will give you a detailed overview of the consensus +state (proposer, lastest validators, peers states). From it, you should be able +to figure out why, for example, the network had halted. + +``` +$ curl http(s)://{ip}:{rpcPort}/dump_consensus_state +``` + +There is a reduced version of this endpoint - `/consensus_state`, which +returns just the votes seen at the current height. + +- `Github Issues __` +- `StackOverflow questions __` + +Monitoring Tendermint +--------------------- + +Each Tendermint instance has a standard `/health` RPC endpoint, which responds +with 200 (OK) if everything is fine and 500 (or no response) - if something is +wrong. + +Other useful endpoints include mentioned earlier `/status`, `/net_info` and +`/validators`. + +We have a small tool, called tm-monitor, which outputs information from the +endpoints above plus some statistics. The tool can be found `here +__`. + +What happens when my app die? +----------------------------- + +You are supposed to run Tendermint under a `process supervisor +__` (like systemd or runit). +It will ensure Tendermint is always running (despite possible errors). + +Getting back to the original question, if your application dies, Tendermint +will panic. After a process supervisor restarts your application, Tendermint +should be able to reconnect successfully. The order of restart does not matter +for it. From b542dce2e1cd4e0c98a6f88a66e69acb6d9fcedc Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 28 May 2018 16:20:15 +0400 Subject: [PATCH 51/59] [docs] signal handling Refs #1494 --- docs/running-in-production.rst | 49 +++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index ec3b46db6..c2af48cad 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -6,13 +6,11 @@ Logging Default logging level (``main:info,state:info,*:``) should suffice for normal operation mode. Read `this post -__` +`__ for details on how to configure ``log_level`` config variable. Some of the -modules can be found `here <./how-to-read-logs.html#list-of-modules>__`. - -If you're trying to debug Tendermint or asked to provide logs with debug -logging level, you can do so by running tendermint with -``--log_level="*:debug"``. +modules can be found `here <./how-to-read-logs.html#list-of-modules>`__. If +you're trying to debug Tendermint or asked to provide logs with debug logging +level, you can do so by running tendermint with ``--log_level="*:debug"``. Consensus WAL ------------- @@ -20,11 +18,11 @@ Consensus WAL Consensus module writes every message to the WAL (write-ahead log). It also issues fsync syscall through `File#Sync -__` for messages signed by this node (to +`__ for messages signed by this node (to prevent double signing). Under the hood, it uses `autofile.Group -__`, which +`__, which rotates files when those get too big (> 10MB). The total maximum size is 1GB. We only need the latest block and the block before it, @@ -47,15 +45,15 @@ private validator contains the ``LastSignBytes`` and then we’ll replay the precommit from the WAL. Make sure to read about `WAL corruption -<./specification/corruption.html#wal-corruption>__` and recovery strategies. +<./specification/corruption.html#wal-corruption>`__ and recovery strategies. DOS Exposure and Mitigation --------------------------- Validators are supposed to setup `Sentry Node Architecture -__` +`__ to prevent Denial-of-service attacks. You can read more about it `here -__`. +`__. Blockchain Reactor ~~~~~~~~~~~~~~~~~~ @@ -80,7 +78,7 @@ Sending incorrectly encoded data will result in stopping the peer. Evidence Reactor ~~~~~~~~~~~~~~~~ -`#1503 __` +`#1503 `__ Sending invalid evidence will result in stopping the peer. @@ -90,7 +88,7 @@ in stopping the peer. PEX Reactor ~~~~~~~~~~~ -Defines only ``SendQueueCapacity``. `#1503 __` +Defines only ``SendQueueCapacity``. `#1503 `__ Implements rate-limiting by enforcing minimal time between two consecutive ``pexRequestMessage`` requests. If the peer sends us addresses we did not ask, @@ -102,12 +100,12 @@ in stopping the peer. Mempool Reactor ~~~~~~~~~~~~~~~ -`#1503 __` +`#1503 `__ Mempool maintains a cache of the last 10000 transactions to prevent replaying old transactions (plus transactions coming from other validators, who are continually exchanging transactions). Read `Replay Protection -<./app-development.html#replay-protection>` for details. +<./app-development.html#replay-protection>`__ for details. Sending incorrectly encoded data or data exceeding ``maxMsgSize`` will result in stopping the peer. @@ -129,15 +127,15 @@ elements (100 max). Rate-limiting and authentication are another key aspects to help protect against DOS attacks. While in the future we may implement these features, for now, validators are supposed to use external tools like `NGINX -__` or `traefik -__` to archive +`__ or `traefik +`__ to archive the same things. Debugging Tendermint -------------------- If you ever have to debug Tendermint, the first thing you should probably do is -to check out the logs. See `"How to read logs" <./how-to-read-logs.html>__`, +to check out the logs. See `"How to read logs" <./how-to-read-logs.html>`__, where we explain what certain log statements mean. If, after skimming through the logs, things are not clear still, the second @@ -159,8 +157,8 @@ $ curl http(s)://{ip}:{rpcPort}/dump_consensus_state There is a reduced version of this endpoint - `/consensus_state`, which returns just the votes seen at the current height. -- `Github Issues __` -- `StackOverflow questions __` +- `Github Issues `__ +- `StackOverflow questions `__ Monitoring Tendermint --------------------- @@ -174,16 +172,23 @@ Other useful endpoints include mentioned earlier `/status`, `/net_info` and We have a small tool, called tm-monitor, which outputs information from the endpoints above plus some statistics. The tool can be found `here -__`. +`__. What happens when my app die? ----------------------------- You are supposed to run Tendermint under a `process supervisor -__` (like systemd or runit). +`__ (like systemd or runit). It will ensure Tendermint is always running (despite possible errors). Getting back to the original question, if your application dies, Tendermint will panic. After a process supervisor restarts your application, Tendermint should be able to reconnect successfully. The order of restart does not matter for it. + +Signal handling +--------------- + +We catch SIGINT and SIGTERM and try to clean up nicely. For other signals we +use the default behaviour in Go: `Default behavior of signals in Go programs +`__. From 2a517ac98c7556498aed5446235b438cfd1f5166 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 29 May 2018 14:31:28 +0400 Subject: [PATCH 52/59] hardware specs and configuration params Refs #1494 --- docs/running-in-production.rst | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index c2af48cad..04a8abffa 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -192,3 +192,52 @@ Signal handling We catch SIGINT and SIGTERM and try to clean up nicely. For other signals we use the default behaviour in Go: `Default behavior of signals in Go programs `__. + +Hardware +-------- + +Processor and Memory +~~~~~~~~~~~~~~~~~~~~ + +While actual specs vary depending on the load and validators count, minimal requirements are: + +- 1GB RAM +- 25GB of disk space +- 1.4 GHz CPU + +SSD disks are preffereble for applications with high transaction throughput. + +Recommended: + +- 2GB RAM +- 100GB SSD +- x64 2.0 GHz 2v CPU + +Operating Systems +~~~~~~~~~~~~~~~~~ + +Tendermint can be compiled for a wide range of operating systems thanks to Go +language (the list of $OS/$ARCH pairs can be found `here +`__). + +While we do not favor any operation system, more secure and stable Linux server +distributions (like Centos) should be preferred over desktop operation systems +(like Mac OS). + +Misc. +~~~~~ + +NOTE: if you are going to use Tendermint in a public domain, make sure you read +`hardware recommendations (see "4. Hardware") +`__ for a validator in the Cosmos network. + +Configuration parameters +------------------------ + +- ``skip_timeout_commit`` + +We want skip_timeout_commit=false when there is economics on the line because +proposers should wait to hear for more votes. But if you don't care about that +and want the fastest consensus, you can skip it. So we will keep it false for +the hub and as default, but for enterprise applications, no problem to set to +true. From f7106bfb3915514b09439e0911ae90bb873c0dcc Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 29 May 2018 16:24:52 +0400 Subject: [PATCH 53/59] more config variables Refs #1494 --- docs/running-in-production.rst | 46 +++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index 04a8abffa..432710831 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -234,10 +234,54 @@ NOTE: if you are going to use Tendermint in a public domain, make sure you read Configuration parameters ------------------------ -- ``skip_timeout_commit`` +- ``p2p.flush_throttle_timeout`` + ``p2p.max_packet_msg_payload_size`` + ``p2p.send_rate`` + ``p2p.recv_rate`` + +If you are going to use Tendermint in a private domain and you have a private +high-speed network among your peers, it makes sense to lower flush throttle +timeout and increase other params. + +:: + + [p2p] + + send_rate=20000000 # 2MB/s + recv_rate=20000000 # 2MB/s + flush_throttle_timeout=10 + max_packet_msg_payload_size=10240 # 10KB + +- ``mempool.recheck`` + +After every block, Tendermint rechecks every transaction left in the mempool to +see if transactions committed in that block affected the application state, so +some of the transactions left may become invalid. If that does not apply to +your application, you can disable it by setting ``mempool.recheck=false``. + +- ``mempool.broadcast`` + +Setting this to false will stop the mempool from relaying transactions to other +peers until they are included in a block. It means only the peer you send the +tx to will see it until it is included in a block. + +- ``consensus.skip_timeout_commit`` We want skip_timeout_commit=false when there is economics on the line because proposers should wait to hear for more votes. But if you don't care about that and want the fastest consensus, you can skip it. So we will keep it false for the hub and as default, but for enterprise applications, no problem to set to true. + +- ``consensus.peer_gossip_sleep_duration`` + +You can try to reduce the time node sleeps before checking if theres something to send its peers. + +- ``consensus.timeout_commit`` + +You can also try lowering ``timeout_commit`` (time we sleep before proposing the next block). + +- ``consensus.max_block_size_txs`` + +By default, the maximum number of transactions per a block is 10_000. Feel free +to change it to suit your needs. From 252a0a392b52e43d88d3bc53be888b97c6881d7c Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 29 May 2018 16:49:02 +0400 Subject: [PATCH 54/59] move reactor descriptions to relevant specs --- docs/running-in-production.rst | 90 ------------------- docs/spec/consensus/wal.md | 33 +++++++ docs/spec/reactors/block_sync/reactor.md | 10 +++ .../reactors/consensus/consensus-reactor.md | 8 ++ docs/spec/reactors/evidence/reactor.md | 10 +++ docs/spec/reactors/mempool/reactor.md | 14 +++ docs/spec/reactors/pex/reactor.md | 12 +++ 7 files changed, 87 insertions(+), 90 deletions(-) create mode 100644 docs/spec/consensus/wal.md create mode 100644 docs/spec/reactors/evidence/reactor.md create mode 100644 docs/spec/reactors/mempool/reactor.md create mode 100644 docs/spec/reactors/pex/reactor.md diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index 432710831..dedbd56d0 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -12,41 +12,6 @@ modules can be found `here <./how-to-read-logs.html#list-of-modules>`__. If you're trying to debug Tendermint or asked to provide logs with debug logging level, you can do so by running tendermint with ``--log_level="*:debug"``. -Consensus WAL -------------- - -Consensus module writes every message to the WAL (write-ahead log). - -It also issues fsync syscall through `File#Sync -`__ for messages signed by this node (to -prevent double signing). - -Under the hood, it uses `autofile.Group -`__, which -rotates files when those get too big (> 10MB). - -The total maximum size is 1GB. We only need the latest block and the block before it, -but if the former is dragging on across many rounds, we want all those rounds. - -Replay -~~~~~~ - -Consensus module will replay all the messages of the last height written to WAL -before a crash (if such occurs). - -The private validator may try to sign messages during replay because it runs -somewhat autonomously and does not know about replay process. - -For example, if we got all the way to precommit in the WAL and then crash, -after we replay the proposal message, the private validator will try to sign a -prevote. But it will fail. That's ok because we’ll see the prevote later in the -WAL. Then it will go to precommit, and that time it will work because the -private validator contains the ``LastSignBytes`` and then we’ll replay the -precommit from the WAL. - -Make sure to read about `WAL corruption -<./specification/corruption.html#wal-corruption>`__ and recovery strategies. - DOS Exposure and Mitigation --------------------------- @@ -55,61 +20,6 @@ Validators are supposed to setup `Sentry Node Architecture to prevent Denial-of-service attacks. You can read more about it `here `__. -Blockchain Reactor -~~~~~~~~~~~~~~~~~~ - -Defines ``maxMsgSize`` for the maximum size of incoming messages, -``SendQueueCapacity`` and ``RecvBufferCapacity`` for maximum sending and -receiving buffers respectively. These are supposed to prevent amplification -attacks by setting up the upper limit on how much data we can receive & send to -a peer. - -Sending incorrectly encoded data will result in stopping the peer. - -Consensus Reactor -~~~~~~~~~~~~~~~~~ - -Defines 4 channels: state, data, vote and vote_set_bits. Each channel -has ``SendQueueCapacity`` and ``RecvBufferCapacity`` and -``RecvMessageCapacity`` set to ``maxMsgSize``. - -Sending incorrectly encoded data will result in stopping the peer. - -Evidence Reactor -~~~~~~~~~~~~~~~~ - -`#1503 `__ - -Sending invalid evidence will result in stopping the peer. - -Sending incorrectly encoded data or data exceeding ``maxMsgSize`` will result -in stopping the peer. - -PEX Reactor -~~~~~~~~~~~ - -Defines only ``SendQueueCapacity``. `#1503 `__ - -Implements rate-limiting by enforcing minimal time between two consecutive -``pexRequestMessage`` requests. If the peer sends us addresses we did not ask, -it is stopped. - -Sending incorrectly encoded data or data exceeding ``maxMsgSize`` will result -in stopping the peer. - -Mempool Reactor -~~~~~~~~~~~~~~~ - -`#1503 `__ - -Mempool maintains a cache of the last 10000 transactions to prevent replaying -old transactions (plus transactions coming from other validators, who are -continually exchanging transactions). Read `Replay Protection -<./app-development.html#replay-protection>`__ for details. - -Sending incorrectly encoded data or data exceeding ``maxMsgSize`` will result -in stopping the peer. - P2P ~~~ diff --git a/docs/spec/consensus/wal.md b/docs/spec/consensus/wal.md new file mode 100644 index 000000000..a2e03137d --- /dev/null +++ b/docs/spec/consensus/wal.md @@ -0,0 +1,33 @@ +# WAL + +Consensus module writes every message to the WAL (write-ahead log). + +It also issues fsync syscall through +[File#Sync](https://golang.org/pkg/os/#File.Sync) for messages signed by this +node (to prevent double signing). + +Under the hood, it uses +[autofile.Group](https://godoc.org/github.com/tendermint/tmlibs/autofile#Group), +which rotates files when those get too big (> 10MB). + +The total maximum size is 1GB. We only need the latest block and the block before it, +but if the former is dragging on across many rounds, we want all those rounds. + +## Replay + +Consensus module will replay all the messages of the last height written to WAL +before a crash (if such occurs). + +The private validator may try to sign messages during replay because it runs +somewhat autonomously and does not know about replay process. + +For example, if we got all the way to precommit in the WAL and then crash, +after we replay the proposal message, the private validator will try to sign a +prevote. But it will fail. That's ok because we’ll see the prevote later in the +WAL. Then it will go to precommit, and that time it will work because the +private validator contains the `LastSignBytes` and then we’ll replay the +precommit from the WAL. + +Make sure to read about [WAL +corruption](https://tendermint.readthedocs.io/projects/tools/en/master/specification/corruption.html#wal-corruption) +and recovery strategies. diff --git a/docs/spec/reactors/block_sync/reactor.md b/docs/spec/reactors/block_sync/reactor.md index c00ea96f3..9a814bead 100644 --- a/docs/spec/reactors/block_sync/reactor.md +++ b/docs/spec/reactors/block_sync/reactor.md @@ -47,3 +47,13 @@ type bcStatusResponseMessage struct { ## Protocol TODO + +## Channels + +Defines `maxMsgSize` for the maximum size of incoming messages, +`SendQueueCapacity` and `RecvBufferCapacity` for maximum sending and +receiving buffers respectively. These are supposed to prevent amplification +attacks by setting up the upper limit on how much data we can receive & send to +a peer. + +Sending incorrectly encoded data will result in stopping the peer. diff --git a/docs/spec/reactors/consensus/consensus-reactor.md b/docs/spec/reactors/consensus/consensus-reactor.md index 21098dcac..0f03b44b7 100644 --- a/docs/spec/reactors/consensus/consensus-reactor.md +++ b/docs/spec/reactors/consensus/consensus-reactor.md @@ -342,3 +342,11 @@ It broadcasts `NewRoundStepMessage` or `CommitStepMessage` upon new round state broadcasting these messages does not depend on the PeerRoundState; it is sent on the StateChannel. Upon receiving VoteMessage it broadcasts `HasVoteMessage` message to its peers on the StateChannel. `ProposalHeartbeatMessage` is sent the same way on the StateChannel. + +## Channels + +Defines 4 channels: state, data, vote and vote_set_bits. Each channel +has `SendQueueCapacity` and `RecvBufferCapacity` and +`RecvMessageCapacity` set to `maxMsgSize`. + +Sending incorrectly encoded data will result in stopping the peer. diff --git a/docs/spec/reactors/evidence/reactor.md b/docs/spec/reactors/evidence/reactor.md new file mode 100644 index 000000000..efa63aa4c --- /dev/null +++ b/docs/spec/reactors/evidence/reactor.md @@ -0,0 +1,10 @@ +# Evidence Reactor + +## Channels + +[#1503](https://github.com/tendermint/tendermint/issues/1503) + +Sending invalid evidence will result in stopping the peer. + +Sending incorrectly encoded data or data exceeding `maxMsgSize` will result +in stopping the peer. diff --git a/docs/spec/reactors/mempool/reactor.md b/docs/spec/reactors/mempool/reactor.md new file mode 100644 index 000000000..2bdbd8951 --- /dev/null +++ b/docs/spec/reactors/mempool/reactor.md @@ -0,0 +1,14 @@ +# Mempool Reactor + +## Channels + +[#1503](https://github.com/tendermint/tendermint/issues/1503) + +Mempool maintains a cache of the last 10000 transactions to prevent +replaying old transactions (plus transactions coming from other +validators, who are continually exchanging transactions). Read [Replay +Protection](https://tendermint.readthedocs.io/projects/tools/en/master/app-development.html?#replay-protection) +for details. + +Sending incorrectly encoded data or data exceeding `maxMsgSize` will result +in stopping the peer. diff --git a/docs/spec/reactors/pex/reactor.md b/docs/spec/reactors/pex/reactor.md new file mode 100644 index 000000000..468f182cc --- /dev/null +++ b/docs/spec/reactors/pex/reactor.md @@ -0,0 +1,12 @@ +# PEX Reactor + +## Channels + +Defines only `SendQueueCapacity`. [#1503](https://github.com/tendermint/tendermint/issues/1503) + +Implements rate-limiting by enforcing minimal time between two consecutive +`pexRequestMessage` requests. If the peer sends us addresses we did not ask, +it is stopped. + +Sending incorrectly encoded data or data exceeding `maxMsgSize` will result +in stopping the peer. From 3da5198631702eb7afac037c3a6b221cb76899bb Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 30 May 2018 09:48:07 +0400 Subject: [PATCH 55/59] fixes after @zramsay's review --- docs/index.rst | 1 + docs/running-in-production.rst | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index a9b207a12..f9d714296 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,6 +58,7 @@ Tendermint 102 subscribing-to-events-via-websocket.rst indexing-transactions.rst how-to-read-logs.rst + running-in-production.rst Tendermint 201 -------------- diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index dedbd56d0..d0b577681 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -38,7 +38,7 @@ Rate-limiting and authentication are another key aspects to help protect against DOS attacks. While in the future we may implement these features, for now, validators are supposed to use external tools like `NGINX `__ or `traefik -`__ to archive +`__ to achieve the same things. Debugging Tendermint @@ -84,8 +84,8 @@ We have a small tool, called tm-monitor, which outputs information from the endpoints above plus some statistics. The tool can be found `here `__. -What happens when my app die? ------------------------------ +What happens when my app dies? +------------------------------ You are supposed to run Tendermint under a `process supervisor `__ (like systemd or runit). @@ -115,7 +115,7 @@ While actual specs vary depending on the load and validators count, minimal requ - 25GB of disk space - 1.4 GHz CPU -SSD disks are preffereble for applications with high transaction throughput. +SSD disks are preferable for applications with high transaction throughput. Recommended: @@ -179,13 +179,14 @@ tx to will see it until it is included in a block. We want skip_timeout_commit=false when there is economics on the line because proposers should wait to hear for more votes. But if you don't care about that -and want the fastest consensus, you can skip it. So we will keep it false for -the hub and as default, but for enterprise applications, no problem to set to -true. +and want the fastest consensus, you can skip it. It will be kept false by +default for public deployments (e.g. `Cosmos Hub +`__) while for enterprise applications, +setting it to true is not a problem. - ``consensus.peer_gossip_sleep_duration`` -You can try to reduce the time node sleeps before checking if theres something to send its peers. +You can try to reduce the time your node sleeps before checking if theres something to send its peers. - ``consensus.timeout_commit`` From f0ce8b3883d8b9d6bb76d8984019eea193e59434 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 30 May 2018 16:55:39 +0400 Subject: [PATCH 56/59] note about state syncing --- docs/running-in-production.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst index d0b577681..162dfdd86 100644 --- a/docs/running-in-production.rst +++ b/docs/running-in-production.rst @@ -123,6 +123,11 @@ Recommended: - 100GB SSD - x64 2.0 GHz 2v CPU +While for now, Tendermint stores all the history and it may require significant +disk space over time, we are planning to implement state syncing (See `#828 +`__). So, storing all the +past blocks will not be necessary. + Operating Systems ~~~~~~~~~~~~~~~~~ From d454b1b25f99518e5123df6de3a17c0797cb494f Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 30 May 2018 21:44:39 -0400 Subject: [PATCH 57/59] SkipDuplicate -> AllowDuplicate; fix p2p test on mac --- CHANGELOG.md | 4 ++++ config/config.go | 5 +++-- p2p/pex/pex_reactor_test.go | 2 +- p2p/switch.go | 2 +- p2p/switch_test.go | 5 +++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f19c543d1..dd5cad4aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,16 @@ BREAKING: FEATURES - [rpc] the RPC documentation is now published to https://tendermint.github.io/slate +- [p2p] AllowDuplicateIP config option to refuse connections from same IP. + - true by default for now, false by default in next breaking release +- [docs] Add docs for query, tx indexing, events, pubsub IMPROVEMENTS: - [consensus] consensus reactor now receives events from a separate event bus, which is not dependant on external RPC load - [consensus/wal] do not look for height in older files if we've seen height - 1 +- [docs] Various cleanup and link fixes ## 0.19.6 diff --git a/config/config.go b/config/config.go index 932e1ae8e..aabc3d05a 100644 --- a/config/config.go +++ b/config/config.go @@ -294,7 +294,7 @@ type P2PConfig struct { PrivatePeerIDs string `mapstructure:"private_peer_ids"` // Toggle to disable guard against peers connecting from the same ip. - SkipDuplicatePeerIPCheck bool `mapstructure:"skip_duplicate_peer_ip_check"` + AllowDuplicateIP bool `mapstructure:"allow_duplicate_ip"` } // DefaultP2PConfig returns a default configuration for the peer-to-peer layer @@ -311,6 +311,7 @@ func DefaultP2PConfig() *P2PConfig { PexReactor: true, SeedMode: false, AuthEnc: true, + AllowDuplicateIP: true, // so non-breaking yet } } @@ -320,7 +321,7 @@ func TestP2PConfig() *P2PConfig { cfg.ListenAddress = "tcp://0.0.0.0:36656" cfg.SkipUPNP = true cfg.FlushThrottleTimeout = 10 - cfg.SkipDuplicatePeerIPCheck = true + cfg.AllowDuplicateIP = true return cfg } diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 06fe6174e..307427b5a 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -27,7 +27,7 @@ var ( func init() { config = cfg.DefaultP2PConfig() config.PexReactor = true - config.SkipDuplicatePeerIPCheck = true + config.AllowDuplicateIP = true } func TestPEXReactorBasic(t *testing.T) { diff --git a/p2p/switch.go b/p2p/switch.go index 7656b9b02..69a7badbd 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -577,7 +577,7 @@ func (sw *Switch) addPeer(pc peerConn) error { } // Check for duplicate connection or peer info IP. - if !sw.config.SkipDuplicatePeerIPCheck && + if !sw.config.AllowDuplicateIP && (sw.peers.HasIP(pc.RemoteIP()) || sw.peers.HasIP(peerNodeInfo.NetAddress().IP)) { return ErrSwitchDuplicatePeerIP{pc.RemoteIP()} diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 74e9e9776..d33797a2b 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -25,6 +25,7 @@ var ( func init() { config = cfg.DefaultP2PConfig() config.PexReactor = true + config.AllowDuplicateIP = true } type PeerMessage struct { @@ -180,7 +181,7 @@ func TestConnAddrFilter(t *testing.T) { } func TestSwitchFiltersOutItself(t *testing.T) { - s1 := MakeSwitch(config, 1, "127.0.0.2", "123.123.123", initSwitchFunc) + s1 := MakeSwitch(config, 1, "127.0.0.1", "123.123.123", initSwitchFunc) // addr := s1.NodeInfo().NetAddress() // // add ourselves like we do in node.go#427 @@ -322,7 +323,7 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { Config: DefaultPeerConfig(), // Use different interface to prevent duplicate IP filter, this will break // beyond two peers. - listenAddr: "127.0.0.2:0", + listenAddr: "127.0.0.1:0", } rp.Start() defer rp.Stop() From d584e034272dff8c088304c8f00a2f970e4e30fd Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 30 May 2018 23:23:39 -0400 Subject: [PATCH 58/59] fix link and typo --- docs/app-architecture.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/app-architecture.rst b/docs/app-architecture.rst index cbcac4d12..c303ba4a5 100644 --- a/docs/app-architecture.rst +++ b/docs/app-architecture.rst @@ -6,11 +6,11 @@ application. The following diagram provides a superb example: -https://lh3.googleusercontent.com/eOT9KuaBjv7xKBg2xN8u2O1nU2Iw1-6PtFXrBSFWW8LYhfAMu84EwXW4RwyOnuiNqCoOoqXxE8Pkhr4Fyq9f=w2559-h1303-rw +https://drive.google.com/open?id=1yR2XpRi9YCY9H9uMfcw8-RMJpvDyvjz9 The end-user application here is the Cosmos Voyager, at the bottom left. Voyager communicates with a REST API exposed by a local Light-Client Daemon. -The Light-Client Daemon is an application specific program that communciates with +The Light-Client Daemon is an application specific program that communicates with Tendermint nodes and verifies Tendermint light-client proofs through the Tendermint Core RPC. The Tendermint Core process communicates with a local ABCI application, where the user query or transaction is actually processed. From aaaa5f23e2ce31dbd974f47bd8c205ee25950f47 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 30 May 2018 23:36:36 -0400 Subject: [PATCH 59/59] changelog and version --- CHANGELOG.md | 3 +++ version/version.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd5cad4aa..81264df80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 0.19.7 +*May 31st, 2018* + BREAKING: - [libs/pubsub] TagMap#Get returns a string value @@ -13,6 +15,7 @@ FEATURES - [p2p] AllowDuplicateIP config option to refuse connections from same IP. - true by default for now, false by default in next breaking release - [docs] Add docs for query, tx indexing, events, pubsub +- [docs] Add some notes about running Tendermint in production IMPROVEMENTS: diff --git a/version/version.go b/version/version.go index 32f003bb5..c235d6a72 100644 --- a/version/version.go +++ b/version/version.go @@ -10,7 +10,7 @@ const ( var ( // Version is the current version of Tendermint // Must be a string because scripts like dist.sh read this file. - Version = "0.19.7-dev" + Version = "0.19.7" // GitCommit is the current HEAD set using ldflags. GitCommit string