code.vegaprotocol.io/vega@v0.79.0/core/governance/node_validation.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 governance
    17  
    18  import (
    19  	"context"
    20  	"encoding/binary"
    21  	"errors"
    22  	"fmt"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	vgcrypto "code.vegaprotocol.io/vega/libs/crypto"
    28  	vgerrors "code.vegaprotocol.io/vega/libs/errors"
    29  	"code.vegaprotocol.io/vega/logging"
    30  	snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1"
    31  )
    32  
    33  const (
    34  	minValidationPeriod = 1         // 1 sec
    35  	maxValidationPeriod = 48 * 3600 // 2 days
    36  )
    37  
    38  var (
    39  	ErrNoNodeValidationRequired                = errors.New("no node validation required")
    40  	ErrProposalReferenceDuplicate              = errors.New("proposal duplicate")
    41  	ErrProposalValidationTimestampTooLate      = errors.New("proposal validation timestamp must be earlier than closing time")
    42  	ErrProposalValidationTimestampOutsideRange = fmt.Errorf("proposal validation timestamp must be within %d-%d seconds from submission time", minValidationPeriod, maxValidationPeriod)
    43  )
    44  
    45  const (
    46  	pendingValidationProposal uint32 = iota
    47  	okProposal
    48  	rejectedProposal
    49  )
    50  
    51  type NodeValidation struct {
    52  	log                *logging.Logger
    53  	assets             Assets
    54  	currentTimestamp   time.Time
    55  	nodeProposals      []*nodeProposal
    56  	nodeBatchProposals []*nodeBatchProposal
    57  	witness            Witness
    58  }
    59  
    60  type nodeProposal struct {
    61  	*proposal
    62  	state   atomic.Uint32
    63  	checker func() error
    64  }
    65  
    66  type nodeBatchProposal struct {
    67  	*batchProposal
    68  	nodeProposals []*nodeProposal
    69  	state         atomic.Uint32
    70  }
    71  
    72  func (n *nodeBatchProposal) UpdateState() {
    73  	pending, failed := 0, 0
    74  
    75  	for _, v := range n.nodeProposals {
    76  		switch v.state.Load() {
    77  		case okProposal:
    78  			continue
    79  		case pendingValidationProposal:
    80  			pending++
    81  		case rejectedProposal:
    82  			failed++
    83  		}
    84  	}
    85  
    86  	// nothing to do
    87  	if pending > 0 {
    88  		return
    89  	}
    90  
    91  	// if at least 1 failure and no pending, then the whole batch is failed.
    92  	if failed > 0 {
    93  		n.state.Store(rejectedProposal)
    94  		return
    95  	}
    96  
    97  	n.state.Store(okProposal)
    98  }
    99  
   100  func (n *nodeProposal) GetID() string {
   101  	return n.ID
   102  }
   103  
   104  func (n *nodeProposal) GetChainID() string {
   105  	switch na := n.Terms.Change.(type) {
   106  	case *types.ProposalTermsNewAsset:
   107  		if erc20 := na.NewAsset.Changes.GetERC20(); erc20 != nil {
   108  			return erc20.ChainID
   109  		}
   110  	}
   111  	return ""
   112  }
   113  
   114  func (n *nodeProposal) GetType() types.NodeVoteType {
   115  	return types.NodeVoteTypeGovernanceValidateAsset
   116  }
   117  
   118  func (n *nodeProposal) Check(_ context.Context) error {
   119  	if err := n.checker(); err != nil {
   120  		return err
   121  	}
   122  
   123  	return nil
   124  }
   125  
   126  func NewNodeValidation(
   127  	log *logging.Logger,
   128  	assets Assets,
   129  	now time.Time,
   130  	witness Witness,
   131  ) *NodeValidation {
   132  	return &NodeValidation{
   133  		log:                log,
   134  		nodeProposals:      []*nodeProposal{},
   135  		nodeBatchProposals: []*nodeBatchProposal{},
   136  		assets:             assets,
   137  		currentTimestamp:   now,
   138  		witness:            witness,
   139  	}
   140  }
   141  
   142  func (n *NodeValidation) Hash() []byte {
   143  	// 32 -> len(proposal.ID) = 32 bytes pubkey
   144  	// vote counts = 3*uint64
   145  	output := make([]byte, len(n.nodeProposals)*(32+8*3))
   146  	var i int
   147  	for _, k := range n.nodeProposals {
   148  		idbytes := []byte(k.ID)
   149  		copy(output[i:], idbytes[:])
   150  		i += 32
   151  		binary.BigEndian.PutUint64(output[i:], uint64(len(k.yes)))
   152  		i += 8
   153  		binary.BigEndian.PutUint64(output[i:], uint64(len(k.no)))
   154  		i += 8
   155  		binary.BigEndian.PutUint64(output[i:], uint64(len(k.invalidVotes)))
   156  		i += 8
   157  	}
   158  
   159  	return vgcrypto.Hash(output)
   160  }
   161  
   162  func (n *NodeValidation) onResChecked(i interface{}, valid bool) {
   163  	np, ok := i.(*nodeProposal)
   164  	if !ok {
   165  		n.log.Error("not an node proposal received from ext check")
   166  		return
   167  	}
   168  
   169  	newState := rejectedProposal
   170  	if valid {
   171  		newState = okProposal
   172  	}
   173  	np.state.Store(newState)
   174  }
   175  
   176  func (n *NodeValidation) getProposal(id string) (*nodeProposal, bool) {
   177  	for _, v := range n.nodeProposals {
   178  		if v.ID == id {
   179  			return v, true
   180  		}
   181  	}
   182  	return nil, false
   183  }
   184  
   185  func (n *NodeValidation) getBatchProposal(id string) (*nodeBatchProposal, bool) {
   186  	for _, v := range n.nodeBatchProposals {
   187  		if v.ID == id {
   188  			return v, true
   189  		}
   190  	}
   191  	return nil, false
   192  }
   193  
   194  func (n *NodeValidation) getProposals() []*nodeProposal {
   195  	return n.nodeProposals
   196  }
   197  
   198  func (n *NodeValidation) getBatchProposals() []*nodeBatchProposal {
   199  	return n.nodeBatchProposals
   200  }
   201  
   202  func (n *NodeValidation) removeProposal(id string) {
   203  	for i, p := range n.nodeProposals {
   204  		if p.ID == id {
   205  			copy(n.nodeProposals[i:], n.nodeProposals[i+1:])
   206  			n.nodeProposals[len(n.nodeProposals)-1] = nil
   207  			n.nodeProposals = n.nodeProposals[:len(n.nodeProposals)-1]
   208  			return
   209  		}
   210  	}
   211  }
   212  
   213  func (n *NodeValidation) removeBatchProposal(id string) {
   214  	for i, p := range n.nodeBatchProposals {
   215  		if p.ID == id {
   216  			copy(n.nodeBatchProposals[i:], n.nodeBatchProposals[i+1:])
   217  			n.nodeBatchProposals[len(n.nodeBatchProposals)-1] = nil
   218  			n.nodeBatchProposals = n.nodeBatchProposals[:len(n.nodeBatchProposals)-1]
   219  			return
   220  		}
   221  	}
   222  }
   223  
   224  // OnTick returns validated proposal by all nodes.
   225  func (n *NodeValidation) OnTick(t time.Time) (accepted []*proposal, rejected []*proposal) { //revive:disable:unexported-return
   226  	n.currentTimestamp = t
   227  
   228  	toRemove := []string{} // id of proposals to remove
   229  
   230  	// check that any proposal is ready
   231  	for _, prop := range n.nodeProposals {
   232  		// this proposal has passed the node-voting period, or all nodes have voted/approved
   233  		// time expired, or all vote aggregated, and own vote sent
   234  		switch prop.state.Load() {
   235  		case pendingValidationProposal:
   236  			continue
   237  		case okProposal:
   238  			accepted = append(accepted, prop.proposal)
   239  		case rejectedProposal:
   240  			rejected = append(rejected, prop.proposal)
   241  		}
   242  		toRemove = append(toRemove, prop.ID)
   243  	}
   244  
   245  	// now we iterate over all proposal ids to remove them from the list
   246  	for _, id := range toRemove {
   247  		n.removeProposal(id)
   248  	}
   249  
   250  	return accepted, rejected
   251  }
   252  
   253  // OnTickBatch returns validated proposal by all nodes.
   254  func (n *NodeValidation) OnTickBatch(t time.Time) (accepted []*batchProposal, rejected []*batchProposal) { //revive:disable:unexported-return
   255  	n.currentTimestamp = t
   256  
   257  	toRemove := []string{} // id of proposals to remove
   258  
   259  	// check that any proposal is ready
   260  	for _, prop := range n.nodeBatchProposals {
   261  		// update the top level batch proposal
   262  		prop.UpdateState()
   263  		// this proposal has passed the node-voting period, or all nodes have voted/approved
   264  		// time expired, or all vote aggregated, and own vote sent
   265  		switch prop.state.Load() {
   266  		case pendingValidationProposal:
   267  			continue
   268  		case okProposal:
   269  			accepted = append(accepted, prop.batchProposal)
   270  		case rejectedProposal:
   271  			rejected = append(rejected, prop.batchProposal)
   272  		}
   273  		toRemove = append(toRemove, prop.ID)
   274  	}
   275  
   276  	// now we iterate over all proposal ids to remove them from the list
   277  	for _, id := range toRemove {
   278  		n.removeBatchProposal(id)
   279  	}
   280  
   281  	return accepted, rejected
   282  }
   283  
   284  // IsNodeValidationRequired returns true if the given proposal require validation from a node.
   285  func (n *NodeValidation) IsNodeValidationRequired(p *types.Proposal) bool {
   286  	switch p.Terms.Change.(type) {
   287  	case *types.ProposalTermsNewAsset:
   288  		return true
   289  	default:
   290  		return false
   291  	}
   292  }
   293  
   294  func (n *NodeValidation) IsNodeValidationRequiredBatch(p *types.BatchProposal) (is bool) {
   295  	for _, v := range p.Proposals {
   296  		is = is || n.IsNodeValidationRequired(v)
   297  	}
   298  
   299  	return is
   300  }
   301  
   302  // Start the node validation of a proposal.
   303  func (n *NodeValidation) StartBatch(ctx context.Context, p *types.BatchProposal) error {
   304  	if !n.IsNodeValidationRequiredBatch(p) {
   305  		n.log.Error("no node validation required", logging.String("ref", p.ID))
   306  		return ErrNoNodeValidationRequired
   307  	}
   308  
   309  	if _, ok := n.getBatchProposal(p.ID); ok {
   310  		return ErrProposalReferenceDuplicate
   311  	}
   312  
   313  	if err := n.checkBatchProposal(p); err != nil {
   314  		return err
   315  	}
   316  
   317  	nodeProposals := []*nodeProposal{}
   318  	for _, v := range p.Proposals {
   319  		if !n.IsNodeValidationRequired(v) {
   320  			// nothing to do here
   321  			continue
   322  		}
   323  		checker, err := n.getChecker(ctx, v)
   324  		if err != nil {
   325  			return err
   326  		}
   327  
   328  		np := &nodeProposal{
   329  			proposal: &proposal{
   330  				Proposal:     v,
   331  				yes:          map[string]*types.Vote{},
   332  				no:           map[string]*types.Vote{},
   333  				invalidVotes: map[string]*types.Vote{},
   334  			},
   335  			state:   atomic.Uint32{},
   336  			checker: checker,
   337  		}
   338  
   339  		np.state.Store(pendingValidationProposal)
   340  		nodeProposals = append(nodeProposals, np)
   341  	}
   342  
   343  	nbp := &nodeBatchProposal{
   344  		batchProposal: &batchProposal{
   345  			BatchProposal: p,
   346  			yes:           map[string]*types.Vote{},
   347  			no:            map[string]*types.Vote{},
   348  			invalidVotes:  map[string]*types.Vote{},
   349  		},
   350  		nodeProposals: nodeProposals,
   351  		state:         atomic.Uint32{},
   352  	}
   353  	nbp.state.Store(pendingValidationProposal)
   354  	n.nodeBatchProposals = append(n.nodeBatchProposals, nbp)
   355  
   356  	errs := vgerrors.NewCumulatedErrors()
   357  	for _, v := range nbp.nodeProposals {
   358  		err := n.witness.StartCheck(v, n.onResChecked, time.Unix(v.Terms.ValidationTimestamp, 0))
   359  		if err != nil {
   360  			errs.Add(err)
   361  		}
   362  	}
   363  
   364  	if errs.HasAny() {
   365  		return errs
   366  	}
   367  
   368  	return nil
   369  }
   370  
   371  // Start the node validation of a proposal.
   372  func (n *NodeValidation) Start(ctx context.Context, p *types.Proposal) error {
   373  	if !n.IsNodeValidationRequired(p) {
   374  		n.log.Error("no node validation required", logging.String("ref", p.ID))
   375  		return ErrNoNodeValidationRequired
   376  	}
   377  
   378  	if _, ok := n.getProposal(p.ID); ok {
   379  		return ErrProposalReferenceDuplicate
   380  	}
   381  
   382  	if err := n.checkProposal(p); err != nil {
   383  		return err
   384  	}
   385  
   386  	checker, err := n.getChecker(ctx, p)
   387  	if err != nil {
   388  		return err
   389  	}
   390  
   391  	np := &nodeProposal{
   392  		proposal: &proposal{
   393  			Proposal:     p,
   394  			yes:          map[string]*types.Vote{},
   395  			no:           map[string]*types.Vote{},
   396  			invalidVotes: map[string]*types.Vote{},
   397  		},
   398  		state:   atomic.Uint32{},
   399  		checker: checker,
   400  	}
   401  	np.state.Store(pendingValidationProposal)
   402  	n.nodeProposals = append(n.nodeProposals, np)
   403  
   404  	return n.witness.StartCheck(np, n.onResChecked, time.Unix(p.Terms.ValidationTimestamp, 0))
   405  }
   406  
   407  func (n *NodeValidation) restoreBatch(ctx context.Context, pProto *snapshotpb.BatchProposalData) (*types.BatchProposal, error) {
   408  	p := types.BatchProposalFromSnapshotProto(pProto.BatchProposal.Proposal, pProto.Proposals)
   409  	nodeProposals := []*nodeProposal{}
   410  	for _, v := range p.Proposals {
   411  		if !n.IsNodeValidationRequired(v) {
   412  			// nothing to do here
   413  			continue
   414  		}
   415  		checker, err := n.getChecker(ctx, v)
   416  		if err != nil {
   417  			return nil, err
   418  		}
   419  
   420  		np := &nodeProposal{
   421  			proposal: &proposal{
   422  				Proposal:     v,
   423  				yes:          map[string]*types.Vote{},
   424  				no:           map[string]*types.Vote{},
   425  				invalidVotes: map[string]*types.Vote{},
   426  			},
   427  			state:   atomic.Uint32{},
   428  			checker: checker,
   429  		}
   430  
   431  		np.state.Store(pendingValidationProposal)
   432  		nodeProposals = append(nodeProposals, np)
   433  	}
   434  
   435  	nbp := &nodeBatchProposal{
   436  		batchProposal: &batchProposal{
   437  			BatchProposal: p,
   438  			yes:           votesAsMapFromProto(pProto.BatchProposal.Yes),
   439  			no:            votesAsMapFromProto(pProto.BatchProposal.No),
   440  			invalidVotes:  votesAsMapFromProto(pProto.BatchProposal.Invalid),
   441  		},
   442  		nodeProposals: nodeProposals,
   443  		state:         atomic.Uint32{},
   444  	}
   445  
   446  	nbp.state.Store(pendingValidationProposal)
   447  	n.nodeBatchProposals = append(n.nodeBatchProposals, nbp)
   448  
   449  	for _, v := range nbp.nodeProposals {
   450  		if err := n.witness.RestoreResource(v, n.onResChecked); err != nil {
   451  			n.log.Panic("unable to restore witness resource", logging.String("id", v.ID), logging.Error(err))
   452  		}
   453  	}
   454  
   455  	return p, nil
   456  }
   457  
   458  func (n *NodeValidation) restore(ctx context.Context, p *types.ProposalData) error {
   459  	checker, err := n.getChecker(ctx, p.Proposal)
   460  	if err != nil {
   461  		return err
   462  	}
   463  	np := &nodeProposal{
   464  		proposal: &proposal{
   465  			Proposal:     p.Proposal,
   466  			yes:          votesAsMap(p.Yes),
   467  			no:           votesAsMap(p.No),
   468  			invalidVotes: votesAsMap(p.Invalid),
   469  		},
   470  		state:   atomic.Uint32{},
   471  		checker: checker,
   472  	}
   473  	np.state.Store(pendingValidationProposal)
   474  	n.nodeProposals = append(n.nodeProposals, np)
   475  	if err := n.witness.RestoreResource(np, n.onResChecked); err != nil {
   476  		n.log.Panic("unable to restore witness resource", logging.String("id", np.ID), logging.Error(err))
   477  	}
   478  	return nil
   479  }
   480  
   481  func (n *NodeValidation) getChecker(ctx context.Context, p *types.Proposal) (func() error, error) {
   482  	switch change := p.Terms.Change.(type) {
   483  	case *types.ProposalTermsNewAsset:
   484  		assetID, err := n.assets.NewAsset(ctx, p.ID, change.NewAsset.GetChanges())
   485  		if err != nil {
   486  			n.log.Error("unable to instantiate asset",
   487  				logging.AssetID(assetID),
   488  				logging.Error(err))
   489  			return nil, err
   490  		}
   491  		return func() error {
   492  			return n.checkAsset(p.ID)
   493  		}, nil
   494  	default: // this should have been checked earlier but in case of.
   495  		return nil, ErrNoNodeValidationRequired
   496  	}
   497  }
   498  
   499  func (n *NodeValidation) checkAsset(assetID string) error {
   500  	err := n.assets.ValidateAsset(assetID)
   501  	if err != nil {
   502  		// we just log the error, but these are not critical, as it may be
   503  		// things unrelated to the current node, and would recover later on.
   504  		// it's just informative
   505  		n.log.Warn("error validating asset", logging.Error(err))
   506  	}
   507  	return err
   508  }
   509  
   510  func (n *NodeValidation) checkProposal(prop *types.Proposal) error {
   511  	if prop.Terms.ClosingTimestamp < prop.Terms.ValidationTimestamp {
   512  		return ErrProposalValidationTimestampTooLate
   513  	}
   514  	minValid, maxValid := n.currentTimestamp.Add(minValidationPeriod*time.Second), n.currentTimestamp.Add(maxValidationPeriod*time.Second)
   515  	if prop.Terms.ValidationTimestamp < minValid.Unix() || prop.Terms.ValidationTimestamp > maxValid.Unix() {
   516  		return ErrProposalValidationTimestampOutsideRange
   517  	}
   518  	return nil
   519  }
   520  
   521  func (n *NodeValidation) checkBatchProposal(prop *types.BatchProposal) error {
   522  	for _, v := range prop.Proposals {
   523  		if !n.IsNodeValidationRequired(v) {
   524  			continue
   525  		}
   526  
   527  		if err := n.checkProposal(v); err != nil {
   528  			return err
   529  		}
   530  	}
   531  
   532  	return nil
   533  }