ADR 039: Peer Behaviour Interface
Changelog
- 07-03-2019: Initial draft
- 14-03-2019: Updates from feedback
Context
The responsibility for signaling and acting upon peer behaviour lacks a single
owning component and is heavily coupled with the network stack1. Reactors
maintain a reference to the p2p.Switch which they use to call
switch.StopPeerForError(...) when a peer misbehaves and
switch.MarkAsGood(...) when a peer contributes in some meaningful way.
While the switch handles StopPeerForError internally, the MarkAsGood
method delegates to another component, p2p.AddrBook. This scheme of delegation
across Switch obscures the responsibility for handling peer behaviour
and ties up the reactors in a larger dependency graph when testing.
Decision
Introduce a PeerBehaviour interface and concrete implementations which
provide methods for reactors to signal peer behaviour without direct
coupling p2p.Switch. Introduce a ErrorBehaviourPeer to provide
concrete reasons for stopping peers. Introduce GoodBehaviourPeer to provide
concrete ways in which a peer contributes.
Implementation Changes
PeerBehaviour then becomes an interface for signaling peer errors as well
as for marking peers as good.
type PeerBehaviour interface {Behaved(peer Peer, reason GoodBehaviourPeer)Errored(peer Peer, reason ErrorBehaviourPeer)}
Instead of signaling peers to stop with arbitrary reasons:
reason interface{}
We introduce a concrete error type ErrorBehaviourPeer:
type ErrorBehaviourPeer intconst (ErrorBehaviourUnknown = iotaErrorBehaviourBadMessageErrorBehaviourMessageOutofOrder...)
To provide additional information on the ways a peer contributed, we introduce the GoodBehaviourPeer type.
type GoodBehaviourPeer intconst (GoodBehaviourVote = iotaGoodBehaviourBlockPart...)
As a first iteration we provide a concrete implementation which wraps the switch:
type SwitchedPeerBehaviour struct {sw *Switch}func (spb *SwitchedPeerBehaviour) Errored(peer Peer, reason ErrorBehaviourPeer) {spb.sw.StopPeerForError(peer, reason)}func (spb *SwitchedPeerBehaviour) Behaved(peer Peer, reason GoodBehaviourPeer) {spb.sw.MarkPeerAsGood(peer)}func NewSwitchedPeerBehaviour(sw *Switch) *SwitchedPeerBehaviour {return &SwitchedPeerBehaviour{sw: sw,}}
Reactors, which are often difficult to unit test2 could use an implementation which exposes the signals produced by the reactor in manufactured scenarios:
type ErrorBehaviours map[Peer][]ErrorBehaviourPeertype GoodBehaviours map[Peer][]GoodBehaviourPeertype StorePeerBehaviour struct {eb ErrorBehavioursgb GoodBehaviours}func NewStorePeerBehaviour() *StorePeerBehaviour{return &StorePeerBehaviour{eb: make(ErrorBehaviours),gb: make(GoodBehaviours),}}func (spb StorePeerBehaviour) Errored(peer Peer, reason ErrorBehaviourPeer) {if _, ok := spb.eb[peer]; !ok {spb.eb[peer] = []ErrorBehaviours{reason}} else {spb.eb[peer] = append(spb.eb[peer], reason)}}func (mpb *StorePeerBehaviour) GetErrored() ErrorBehaviours {return mpb.eb}func (spb StorePeerBehaviour) Behaved(peer Peer, reason GoodBehaviourPeer) {if _, ok := spb.gb[peer]; !ok {spb.gb[peer] = []GoodBehaviourPeer{reason}} else {spb.gb[peer] = append(spb.gb[peer], reason)}}func (spb *StorePeerBehaviour) GetBehaved() GoodBehaviours {return spb.gb}
Status
Accepted
Consequences
Positive
* De-couple signaling from acting upon peer behaviour.* Reduce the coupling of reactors and the Switch and the networkstack* The responsibility of managing peer behaviour can be migrated toa single component instead of split between the switch and theaddress book.
Negative
* The first iteration will simply wrap the Switch and introduce alevel of indirection.
