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 }