github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/cache/node_blocklist_wrapper.go (about)

     1  package cache
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/dgraph-io/badger/v2"
     9  	"github.com/libp2p/go-libp2p/core/peer"
    10  
    11  	"github.com/onflow/flow-go/model/flow"
    12  	"github.com/onflow/flow-go/module"
    13  	"github.com/onflow/flow-go/network"
    14  	"github.com/onflow/flow-go/storage"
    15  	"github.com/onflow/flow-go/storage/badger/operation"
    16  )
    17  
    18  // IdentifierSet represents a set of node IDs (operator-defined) whose communication should be blocked.
    19  type IdentifierSet map[flow.Identifier]struct{}
    20  
    21  // Contains returns true iff id ∈ s
    22  func (s IdentifierSet) Contains(id flow.Identifier) bool {
    23  	_, found := s[id]
    24  	return found
    25  }
    26  
    27  // NodeDisallowListingWrapper is a wrapper for an `module.IdentityProvider` instance, where the
    28  // wrapper overrides the `Ejected` flag to true for all NodeIDs in a `disallowList`.
    29  // To avoid modifying the source of the identities, the wrapper creates shallow copies
    30  // of the identities (whenever necessary) and modifies the `Ejected` flag only in
    31  // the copy.
    32  // The `NodeDisallowListingWrapper` internally represents the `disallowList` as a map, to enable
    33  // performant lookup. However, the exported API works with `flow.IdentifierList` for
    34  // disallowList, as this is a broadly supported data structure which lends itself better
    35  // to config or command-line inputs.
    36  // When a node is disallow-listed, the networking layer connection to that node is closed and no
    37  // incoming or outgoing connections are established with that node.
    38  // TODO: terminology change - rename `blocklist` to `disallowList` everywhere to be consistent with the code.
    39  type NodeDisallowListingWrapper struct {
    40  	m  sync.RWMutex
    41  	db *badger.DB
    42  
    43  	identityProvider module.IdentityProvider
    44  	disallowList     IdentifierSet // `IdentifierSet` is a map, hence efficient O(1) lookup
    45  
    46  	// updateConsumerOracle is called whenever the disallow-list is updated.
    47  	// Note that we do not use the `updateConsumer` directly due to the circular dependency between the
    48  	// networking layer Underlay interface (i.e., updateConsumer), and the wrapper (i.e., NodeDisallowListingWrapper).
    49  	// Underlay needs identity provider to be initialized, and identity provider needs this wrapper to be initialized.
    50  	// Hence, if we pass the updateConsumer by the interface value, it will be nil at the time of initialization.
    51  	// Instead, we use the oracle function to get the updateConsumer whenever we need it.
    52  	updateConsumerOracle func() network.DisallowListNotificationConsumer
    53  }
    54  
    55  var _ module.IdentityProvider = (*NodeDisallowListingWrapper)(nil)
    56  
    57  // NewNodeDisallowListWrapper wraps the given `IdentityProvider`. The disallow-list is
    58  // loaded from the database (or assumed to be empty if no database entry is present).
    59  func NewNodeDisallowListWrapper(
    60  	identityProvider module.IdentityProvider,
    61  	db *badger.DB,
    62  	updateConsumerOracle func() network.DisallowListNotificationConsumer) (*NodeDisallowListingWrapper, error) {
    63  
    64  	disallowList, err := retrieveDisallowList(db)
    65  	if err != nil {
    66  		return nil, fmt.Errorf("failed to read set of disallowed node IDs from data base: %w", err)
    67  	}
    68  
    69  	return &NodeDisallowListingWrapper{
    70  		db:                   db,
    71  		identityProvider:     identityProvider,
    72  		disallowList:         disallowList,
    73  		updateConsumerOracle: updateConsumerOracle,
    74  	}, nil
    75  }
    76  
    77  // Update sets the wrapper's internal set of blocked nodes to `disallowList`. Empty list and `nil`
    78  // (equivalent to empty list) are accepted inputs. To avoid legacy entries in the database, this
    79  // function purges the entire data base entry if `disallowList` is empty.
    80  // This implementation is _eventually consistent_, where changes are written to the database first
    81  // and then (non-atomically!) the in-memory set of blocked nodes is updated. This strongly
    82  // benefits performance and modularity. No errors are expected during normal operations.
    83  //
    84  // Args:
    85  // - disallowList: list of node IDs to be disallow-listed from the networking layer, i.e., the existing connections
    86  // to these nodes will be closed and no new connections will be established (neither incoming nor outgoing).
    87  //
    88  // Returns:
    89  // - error: if the update fails, e.g., due to a database error. Any returned error is irrecoverable and the caller
    90  // should abort the process.
    91  func (w *NodeDisallowListingWrapper) Update(disallowList flow.IdentifierList) error {
    92  	b := disallowList.Lookup() // converts slice to map
    93  
    94  	w.m.Lock()
    95  	defer w.m.Unlock()
    96  	err := persistDisallowList(b, w.db)
    97  	if err != nil {
    98  		return fmt.Errorf("failed to persist set of blocked nodes to the data base: %w", err)
    99  	}
   100  	w.disallowList = b
   101  	w.updateConsumerOracle().OnDisallowListNotification(&network.DisallowListingUpdate{
   102  		FlowIds: disallowList,
   103  		Cause:   network.DisallowListedCauseAdmin,
   104  	})
   105  
   106  	return nil
   107  }
   108  
   109  // ClearDisallowList purges the set of blocked node IDs. Convenience function
   110  // equivalent to w.Update(nil). No errors are expected during normal operations.
   111  func (w *NodeDisallowListingWrapper) ClearDisallowList() error {
   112  	return w.Update(nil)
   113  }
   114  
   115  // GetDisallowList returns the set of blocked node IDs.
   116  func (w *NodeDisallowListingWrapper) GetDisallowList() flow.IdentifierList {
   117  	w.m.RLock()
   118  	defer w.m.RUnlock()
   119  
   120  	identifiers := make(flow.IdentifierList, 0, len(w.disallowList))
   121  	for i := range w.disallowList {
   122  		identifiers = append(identifiers, i)
   123  	}
   124  	return identifiers
   125  }
   126  
   127  // Identities returns the full identities of _all_ nodes currently known to the
   128  // protocol that pass the provided filter. Caution, this includes ejected nodes.
   129  // Please check the `Ejected` flag in the returned identities (or provide a
   130  // filter for removing ejected nodes).
   131  func (w *NodeDisallowListingWrapper) Identities(filter flow.IdentityFilter[flow.Identity]) flow.IdentityList {
   132  	identities := w.identityProvider.Identities(filter)
   133  	if len(identities) == 0 {
   134  		return identities
   135  	}
   136  
   137  	// Iterate over all returned identities and set the `EpochParticipationStatus` to `flow.EpochParticipationStatusEjected`.
   138  	// We copy both the return slice and identities of blocked nodes to avoid
   139  	// any possibility of accidentally modifying the wrapped IdentityProvider
   140  	idtx := make(flow.IdentityList, 0, len(identities))
   141  	w.m.RLock()
   142  	for _, identity := range identities {
   143  		if w.disallowList.Contains(identity.NodeID) {
   144  			var i = *identity // shallow copy is sufficient, because `EpochParticipationStatus` is a value type in DynamicIdentity which is also a value type.
   145  			i.EpochParticipationStatus = flow.EpochParticipationStatusEjected
   146  			if filter(&i) { // we need to check the filter here again, because the filter might drop ejected nodes and we are modifying the ejected status here
   147  				idtx = append(idtx, &i)
   148  			}
   149  		} else {
   150  			idtx = append(idtx, identity)
   151  		}
   152  	}
   153  	w.m.RUnlock()
   154  	return idtx
   155  }
   156  
   157  // ByNodeID returns the full identity for the node with the given Identifier,
   158  // where Identifier is the way the protocol refers to the node. The function
   159  // has the same semantics as a map lookup, where the boolean return value is
   160  // true if and only if Identity has been found, i.e. `Identity` is not nil.
   161  // Caution: function returns include ejected nodes. Please check the `Ejected`
   162  // flag in the identity.
   163  func (w *NodeDisallowListingWrapper) ByNodeID(identifier flow.Identifier) (*flow.Identity, bool) {
   164  	identity, b := w.identityProvider.ByNodeID(identifier)
   165  	return w.setEjectedIfBlocked(identity), b
   166  }
   167  
   168  // setEjectedIfBlocked checks whether the node with the given identity is on the `disallowList`.
   169  // Shortcuts:
   170  //   - If the node's identity is nil, there is nothing to do because we don't generate identities here.
   171  //   - If the node is already ejected, we don't have to check the disallowList.
   172  func (w *NodeDisallowListingWrapper) setEjectedIfBlocked(identity *flow.Identity) *flow.Identity {
   173  	if identity == nil || identity.IsEjected() {
   174  		return identity
   175  	}
   176  
   177  	w.m.RLock()
   178  	isBlocked := w.disallowList.Contains(identity.NodeID)
   179  	w.m.RUnlock()
   180  	if !isBlocked {
   181  		return identity
   182  	}
   183  
   184  	// For blocked nodes, we want to return their `Identity` with the `EpochParticipationStatus`
   185  	// set to `flow.EpochParticipationStatusEjected`.
   186  	// Caution: we need to copy the `Identity` before we override `EpochParticipationStatus`, as we
   187  	// would otherwise potentially change the wrapped IdentityProvider.
   188  	var i = *identity // shallow copy is sufficient, because `EpochParticipationStatus` is a value type in DynamicIdentity which is also a value type.
   189  	i.EpochParticipationStatus = flow.EpochParticipationStatusEjected
   190  	return &i
   191  }
   192  
   193  // ByPeerID returns the full identity for the node with the given peer ID,
   194  // peer.ID is the libp2p-level identifier of a Flow node. The function
   195  // has the same semantics as a map lookup, where the boolean return value is
   196  // true if and only if Identity has been found, i.e. `Identity` is not nil.
   197  // Caution: function returns include ejected nodes. Please check the `Ejected`
   198  // flag in the identity.
   199  func (w *NodeDisallowListingWrapper) ByPeerID(p peer.ID) (*flow.Identity, bool) {
   200  	identity, b := w.identityProvider.ByPeerID(p)
   201  	return w.setEjectedIfBlocked(identity), b
   202  }
   203  
   204  // persistDisallowList writes the given disallowList to the database. To avoid legacy
   205  // entries in the database, we prune the entire data base entry if `disallowList` is
   206  // empty. No errors are expected during normal operations.
   207  func persistDisallowList(disallowList IdentifierSet, db *badger.DB) error {
   208  	if len(disallowList) == 0 {
   209  		return db.Update(operation.PurgeBlocklist())
   210  	}
   211  	return db.Update(operation.PersistBlocklist(disallowList))
   212  }
   213  
   214  // retrieveDisallowList reads the set of blocked nodes from the data base.
   215  // In case no database entry exists, an empty set (nil map) is returned.
   216  // No errors are expected during normal operations.
   217  func retrieveDisallowList(db *badger.DB) (IdentifierSet, error) {
   218  	var blocklist map[flow.Identifier]struct{}
   219  	err := db.View(operation.RetrieveBlocklist(&blocklist))
   220  	if err != nil && !errors.Is(err, storage.ErrNotFound) {
   221  		return nil, fmt.Errorf("unexpected error reading set of blocked nodes from data base: %w", err)
   222  	}
   223  	return blocklist, nil
   224  }