code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/node.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package sqlstore 17 18 import ( 19 "context" 20 "fmt" 21 "time" 22 23 "code.vegaprotocol.io/vega/datanode/entities" 24 "code.vegaprotocol.io/vega/datanode/metrics" 25 "code.vegaprotocol.io/vega/logging" 26 v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2" 27 28 "github.com/georgysavva/scany/pgxscan" 29 ) 30 31 var nodeOrdering = TableOrdering{ 32 ColumnOrdering{Name: "id", Sorting: ASC}, 33 } 34 35 type Node struct { 36 *ConnectionSource 37 } 38 39 func NewNode(connectionSource *ConnectionSource) *Node { 40 return &Node{ 41 ConnectionSource: connectionSource, 42 } 43 } 44 45 // this query requires a epoch_id as a first argument: WHERE epoch_id = $1. 46 func selectNodeQuery() string { 47 return `WITH 48 current_delegations AS ( 49 SELECT * FROM delegations_current 50 WHERE epoch_id = $1 51 ), 52 pending_delegations AS ( 53 SELECT * FROM delegations_current 54 WHERE epoch_id = $1 + 1 55 ), 56 57 /* partitioned by node_id find the join/leave announcement with the biggest epoch that is also less or equal to the target epoch */ 58 join_event AS ( 59 SELECT 60 node_id, added 61 FROM ( 62 SELECT 63 node_id, added, epoch_seq, Row_Number() 64 OVER(PARTITION BY node_id order BY epoch_seq desc) 65 AS 66 row_number 67 FROM 68 nodes_announced 69 WHERE epoch_seq <= $1) AS a 70 WHERE row_number = 1 AND added = true 71 ), 72 this_epoch AS ( 73 SELECT nodes.id AS node_id, 74 COALESCE(SUM(current_delegations.amount) FILTER (WHERE current_delegations.party_id = nodes.vega_pub_key), 0) AS staked_by_operator, 75 COALESCE(SUM(current_delegations.amount) FILTER (WHERE current_delegations.party_id != nodes.vega_pub_key), 0) AS staked_by_delegates, 76 COALESCE(SUM(current_delegations.amount), 0) AS staked_total, 77 COALESCE(JSON_AGG(JSON_BUILD_OBJECT( 78 'party_id', ENCODE(current_delegations.party_id, 'hex'), 79 'node_id', ENCODE(current_delegations.node_id, 'hex'), 80 'epoch_id', current_delegations.epoch_id, 81 'amount', current_delegations.amount) 82 ) FILTER (WHERE current_delegations.party_id IS NOT NULL), json_build_array()) AS "delegations" 83 FROM nodes LEFT JOIN current_delegations on current_delegations.node_id = nodes.id 84 GROUP BY nodes.id), 85 next_epoch AS ( 86 SELECT nodes.id as node_id, 87 COALESCE(SUM(pending_delegations.amount), 0) AS staked_total 88 FROM nodes LEFT JOIN pending_delegations on pending_delegations.node_id = nodes.id 89 GROUP BY nodes.id 90 ) 91 92 SELECT 93 nodes.id, 94 nodes.vega_pub_key, 95 nodes.tendermint_pub_key, 96 nodes.ethereum_address, 97 nodes.name, 98 nodes.location, 99 nodes.info_url, 100 nodes.avatar_url, 101 nodes.status, 102 ROW_TO_JSON(reward_scores.*)::JSONB AS "reward_score", 103 ROW_TO_JSON(ranking_scores.*)::JSONB AS "ranking_score", 104 this_epoch.delegations, 105 this_epoch.staked_by_operator, 106 this_epoch.staked_by_delegates, 107 this_epoch.staked_total, 108 next_epoch.staked_total - this_epoch.staked_total as pending_stake 109 FROM nodes 110 JOIN this_epoch on nodes.id = this_epoch.node_id 111 JOIN next_epoch on nodes.id = next_epoch.node_id 112 LEFT JOIN ranking_scores ON ranking_scores.node_id = nodes.id AND ranking_scores.epoch_seq = $1 113 LEFT JOIN reward_scores ON reward_scores.node_id = nodes.id AND reward_scores.epoch_seq = $1 114 WHERE EXISTS ( 115 SELECT * 116 FROM join_event WHERE node_id = nodes.id 117 )` 118 } 119 120 func (store *Node) UpsertNode(ctx context.Context, node *entities.Node) error { 121 defer metrics.StartSQLQuery("Node", "UpsertNode")() 122 123 _, err := store.Exec(ctx, ` 124 INSERT INTO nodes ( 125 id, 126 vega_pub_key, 127 tendermint_pub_key, 128 ethereum_address, 129 info_url, 130 location, 131 status, 132 name, 133 avatar_url, 134 tx_hash, 135 vega_time) 136 VALUES 137 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) 138 ON CONFLICT (id) DO UPDATE 139 SET 140 vega_pub_key = EXCLUDED.vega_pub_key, 141 tendermint_pub_key = EXCLUDED.tendermint_pub_key, 142 ethereum_address = EXCLUDED.ethereum_address, 143 info_url = EXCLUDED.info_url, 144 location = EXCLUDED.location, 145 status = EXCLUDED.status, 146 name = EXCLUDED.name, 147 avatar_url = EXCLUDED.avatar_url, 148 tx_hash = EXCLUDED.tx_hash, 149 vega_time = EXCLUDED.vega_time`, 150 node.ID, 151 node.PubKey, 152 node.TmPubKey, 153 node.EthereumAddress, 154 node.InfoURL, 155 node.Location, 156 node.Status, 157 node.Name, 158 node.AvatarURL, 159 node.TxHash, 160 node.VegaTime, 161 ) 162 163 return err 164 } 165 166 // AddNodeAnnouncedEvent store data about which epoch a particular node was added or removed from the roster of validators. 167 func (store *Node) AddNodeAnnouncedEvent(ctx context.Context, nodeID string, vegatime time.Time, aux *entities.ValidatorUpdateAux) error { 168 defer metrics.StartSQLQuery("Node", "AddNodeAnnouncedEvent")() 169 _, err := store.Exec(ctx, ` 170 INSERT INTO nodes_announced ( 171 node_id, 172 epoch_seq, 173 added, 174 tx_hash, 175 vega_time) 176 VALUES 177 ($1, $2, $3, $4, $5) 178 ON CONFLICT (node_id, epoch_seq, vega_time) DO UPDATE SET 179 added=EXCLUDED.added`, 180 entities.NodeID(nodeID), 181 aux.EpochSeq, 182 aux.Added, 183 aux.TxHash, 184 vegatime, 185 ) 186 187 return err 188 } 189 190 func (store *Node) UpsertRanking(ctx context.Context, rs *entities.RankingScore, aux *entities.RankingScoreAux) error { 191 defer metrics.StartSQLQuery("Node", "UpsertRanking")() 192 193 _, err := store.Exec(ctx, ` 194 INSERT INTO ranking_scores ( 195 node_id, 196 epoch_seq, 197 stake_score, 198 performance_score, 199 ranking_score, 200 voting_power, 201 previous_status, 202 status, 203 tx_hash, 204 vega_time) 205 VALUES 206 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) 207 ON CONFLICT (node_id, epoch_seq) DO UPDATE 208 SET 209 stake_score = EXCLUDED.stake_score, 210 performance_score = EXCLUDED.performance_score, 211 ranking_score = EXCLUDED.ranking_score, 212 voting_power = EXCLUDED.voting_power, 213 previous_status = EXCLUDED.previous_status, 214 status = EXCLUDED.status, 215 tx_hash = EXCLUDED.tx_hash, 216 vega_time = EXCLUDED.vega_time`, 217 aux.NodeID, 218 rs.EpochSeq, 219 rs.StakeScore, 220 rs.PerformanceScore, 221 rs.RankingScore, 222 rs.VotingPower, 223 rs.PreviousStatus, 224 rs.Status, 225 rs.TxHash, 226 rs.VegaTime, 227 ) 228 229 return err 230 } 231 232 func (store *Node) UpsertScore(ctx context.Context, rs *entities.RewardScore, aux *entities.RewardScoreAux) error { 233 defer metrics.StartSQLQuery("Node", "UpsertScore")() 234 235 _, err := store.Exec(ctx, ` 236 INSERT INTO reward_scores ( 237 node_id, 238 epoch_seq, 239 validator_node_status, 240 raw_validator_score, 241 performance_score, 242 multisig_score, 243 validator_score, 244 normalised_score, 245 tx_hash, 246 vega_time) 247 VALUES 248 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, 249 aux.NodeID, 250 rs.EpochSeq, 251 rs.ValidatorNodeStatus, 252 rs.RawValidatorScore, 253 rs.PerformanceScore, 254 rs.MultisigScore, 255 rs.ValidatorScore, 256 rs.NormalisedScore, 257 rs.TxHash, 258 rs.VegaTime, 259 ) 260 261 return err 262 } 263 264 func (store *Node) UpdatePublicKey(ctx context.Context, kr *entities.KeyRotation) error { 265 defer metrics.StartSQLQuery("Node", "UpdatePublicKey")() 266 267 _, err := store.Exec(ctx, `UPDATE nodes SET vega_pub_key = $1 WHERE id = $2`, kr.NewPubKey, kr.NodeID) 268 269 return err 270 } 271 272 func (store *Node) UpdateEthereumAddress(ctx context.Context, kr entities.EthereumKeyRotation) error { 273 defer metrics.StartSQLQuery("Node", "UpdateEthereumPublicKey")() 274 275 _, err := store.Exec(ctx, `UPDATE nodes SET ethereum_address = $1 WHERE id = $2`, kr.NewAddress, kr.NodeID) 276 277 return err 278 } 279 280 func (store *Node) GetNodeData(ctx context.Context, epochSeq uint64) (entities.NodeData, error) { 281 defer metrics.StartSQLQuery("Node", "GetNetworkData")() 282 query := ` 283 WITH 284 uptime AS ( 285 SELECT 286 -- Uptime denominated in minutes, hence division by 60 seconds 287 EXTRACT(EPOCH FROM SUM(end_time - start_time)) / 60.0 AS total, 288 ROW_NUMBER() OVER () AS id 289 FROM 290 epochs 291 WHERE 292 id <= $1 293 ), 294 295 staked AS ( 296 SELECT 297 COALESCE(SUM(amount),0) AS total, 298 ROW_NUMBER() OVER () AS id 299 FROM 300 delegations 301 WHERE 302 -- Select the current epoch 303 epoch_id = $1 304 ) 305 SELECT 306 staked.total AS staked_total, 307 coalesce(uptime.total, 0) AS uptime 308 309 FROM staked 310 -- This join is "fake" as to extract all the individual values as one row 311 JOIN uptime ON uptime.id = staked.id; 312 ` 313 314 nodeData := entities.NodeData{} 315 err := pgxscan.Get(ctx, store.ConnectionSource, &nodeData, query, epochSeq) 316 if err != nil { 317 return nodeData, store.wrapE(err) 318 } 319 320 // now we fill in the more complicated bits about node sets 321 nodes, _, err := store.GetNodes(ctx, epochSeq, entities.DefaultCursorPagination(true)) 322 if err != nil { 323 return nodeData, err 324 } 325 326 nodeSets := map[entities.ValidatorNodeStatus]*entities.NodeSet{ 327 entities.ValidatorNodeStatusTendermint: &nodeData.TendermintNodes, 328 entities.ValidatorNodeStatusErsatz: &nodeData.ErsatzNodes, 329 entities.ValidatorNodeStatusPending: &nodeData.PendingNodes, 330 } 331 for _, n := range nodes { 332 if n.RankingScore == nil { 333 store.log.Warn("ignoring node with empty ranking score", logging.String("id", n.ID.String())) 334 continue 335 } 336 status := n.RankingScore.Status 337 previousStatus := n.RankingScore.PreviousStatus 338 if status == entities.ValidatorNodeStatusUnspecified { 339 continue 340 } 341 ns := nodeSets[status] 342 nodeData.TotalNodes++ 343 ns.Total++ 344 345 // but was it active 346 if n.RankingScore.PerformanceScore.IsZero() { 347 ns.Inactive++ 348 nodeData.InactiveNodes++ 349 } 350 351 // check if the node was promoted or demoted into its set this epoch 352 switch { 353 case uint32(status) < uint32(previousStatus): 354 ns.Promoted = append(ns.Promoted, n.ID.String()) 355 case uint32(status) > uint32(previousStatus): 356 ns.Demoted = append(ns.Promoted, n.ID.String()) 357 default: 358 // node stayed in the same set, thats cool 359 } 360 } 361 return nodeData, err 362 } 363 364 func (store *Node) GetNodes(ctx context.Context, epochSeq uint64, pagination entities.CursorPagination) ([]entities.Node, entities.PageInfo, error) { 365 defer metrics.StartSQLQuery("Node", "GetNodes")() 366 var ( 367 nodes []entities.Node 368 pageInfo entities.PageInfo 369 err error 370 ) 371 372 args := []interface{}{ 373 epochSeq, 374 } 375 376 query, args, err := PaginateQuery[entities.NodeCursor](selectNodeQuery(), args, nodeOrdering, pagination) 377 if err != nil { 378 return nil, pageInfo, err 379 } 380 381 if err = pgxscan.Select(ctx, store.ConnectionSource, &nodes, query, args...); err != nil { 382 return nil, pageInfo, fmt.Errorf("could not get nodes: %w", err) 383 } 384 385 nodes, pageInfo = entities.PageEntities[*v2.NodeEdge](nodes, pagination) 386 387 return nodes, pageInfo, nil 388 } 389 390 func (store *Node) GetNodeByID(ctx context.Context, nodeID string, epochSeq uint64) (entities.Node, error) { 391 defer metrics.StartSQLQuery("Node", "GetNodeById")() 392 393 var node entities.Node 394 id := entities.NodeID(nodeID) 395 396 query := fmt.Sprintf("%s AND nodes.id=$2", selectNodeQuery()) 397 return node, store.wrapE(pgxscan.Get(ctx, store.ConnectionSource, &node, query, epochSeq, id)) 398 } 399 400 func (store *Node) GetNodeTxHash(ctx context.Context, nodeID string, epochSeq uint64) (entities.Node, error) { 401 defer metrics.StartSQLQuery("Node", "GetNodeById")() 402 403 var node entities.Node 404 id := entities.NodeID(nodeID) 405 406 query := fmt.Sprintf("%s AND nodes.id=$2", selectNodeQuery()) 407 return node, store.wrapE(pgxscan.Get(ctx, store.ConnectionSource, &node, query, epochSeq, id)) 408 } 409 410 func (store *Node) GetByTxHash(ctx context.Context, txHash entities.TxHash) ([]entities.NodeBasic, error) { 411 defer metrics.StartSQLQuery("Node", "GeByTxHash")() 412 413 var nodes []entities.NodeBasic 414 query := `SELECT id, vega_pub_key, tendermint_pub_key, ethereum_address, name, location, 415 info_url, avatar_url, status FROM nodes WHERE tx_hash = $1` 416 417 if err := pgxscan.Select(ctx, store.ConnectionSource, &nodes, query, txHash); err != nil { 418 return nil, store.wrapE(err) 419 } 420 421 return nodes, nil 422 }