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  }