code.vegaprotocol.io/vega@v0.79.0/core/protocolupgrade/engine.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 protocolupgrade
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"sort"
    23  	"sync"
    24  
    25  	"code.vegaprotocol.io/vega/core/events"
    26  	"code.vegaprotocol.io/vega/core/txn"
    27  	"code.vegaprotocol.io/vega/core/types"
    28  	"code.vegaprotocol.io/vega/libs/num"
    29  	"code.vegaprotocol.io/vega/logging"
    30  	eventspb "code.vegaprotocol.io/vega/protos/vega/events/v1"
    31  
    32  	"github.com/blang/semver"
    33  	"github.com/cenkalti/backoff"
    34  	"github.com/golang/protobuf/proto"
    35  )
    36  
    37  type protocolUpgradeProposal struct {
    38  	blockHeight    uint64
    39  	vegaReleaseTag string
    40  	accepted       map[string]struct{}
    41  }
    42  
    43  func protocolUpgradeProposalID(upgradeBlockHeight uint64, vegaReleaseTag string) string {
    44  	return fmt.Sprintf("%v@%v", vegaReleaseTag, upgradeBlockHeight)
    45  }
    46  
    47  // TrimReleaseTag removes 'v' or 'V' at the beginning of the tag if present.
    48  func TrimReleaseTag(tag string) string {
    49  	if len(tag) == 0 {
    50  		return tag
    51  	}
    52  
    53  	switch tag[0] {
    54  	case 'v', 'V':
    55  		return tag[1:]
    56  	default:
    57  		return tag
    58  	}
    59  }
    60  
    61  func (p *protocolUpgradeProposal) approvers() []string {
    62  	accepted := make([]string, 0, len(p.accepted))
    63  	for k := range p.accepted {
    64  		accepted = append(accepted, k)
    65  	}
    66  	sort.Strings(accepted)
    67  	return accepted
    68  }
    69  
    70  type ValidatorTopology interface {
    71  	IsTendermintValidator(pubkey string) bool
    72  	IsSelfTendermintValidator() bool
    73  	GetVotingPower(pubkey string) int64
    74  	GetTotalVotingPower() int64
    75  }
    76  
    77  type Commander interface {
    78  	CommandSync(ctx context.Context, cmd txn.Command, payload proto.Message, f func(error), bo *backoff.ExponentialBackOff)
    79  }
    80  
    81  type Broker interface {
    82  	Send(event events.Event)
    83  }
    84  
    85  type Engine struct {
    86  	log            *logging.Logger
    87  	config         Config
    88  	broker         Broker
    89  	topology       ValidatorTopology
    90  	hashKeys       []string
    91  	currentVersion string
    92  
    93  	currentBlockHeight uint64
    94  	activeProposals    map[string]*protocolUpgradeProposal
    95  	events             map[string]*eventspb.ProtocolUpgradeEvent
    96  	lock               sync.RWMutex
    97  
    98  	requiredMajority   num.Decimal
    99  	upgradeStatus      *types.UpgradeStatus
   100  	coreReadyToUpgrade bool
   101  }
   102  
   103  func New(log *logging.Logger, config Config, broker Broker, topology ValidatorTopology, version string) *Engine {
   104  	log = log.Named(namedLogger)
   105  	log.SetLevel(config.Level.Get())
   106  	return &Engine{
   107  		activeProposals: map[string]*protocolUpgradeProposal{},
   108  		events:          map[string]*eventspb.ProtocolUpgradeEvent{},
   109  		log:             log,
   110  		config:          config,
   111  		broker:          broker,
   112  		topology:        topology,
   113  		hashKeys:        []string{(&types.PayloadProtocolUpgradeProposals{}).Key()},
   114  		upgradeStatus:   &types.UpgradeStatus{},
   115  		currentVersion:  version,
   116  	}
   117  }
   118  
   119  func (e *Engine) OnRequiredMajorityChanged(_ context.Context, requiredMajority num.Decimal) error {
   120  	e.requiredMajority = requiredMajority
   121  	return nil
   122  }
   123  
   124  func (e *Engine) IsValidProposal(_ context.Context, pk string, upgradeBlockHeight uint64, vegaReleaseTag string) error {
   125  	if !e.topology.IsTendermintValidator(pk) {
   126  		// not a tendermint validator, so we don't care about their intention
   127  		return errors.New("only tendermint validator can propose a protocol upgrade")
   128  	}
   129  
   130  	if upgradeBlockHeight == 0 {
   131  		return errors.New("upgrade block out of range")
   132  	}
   133  
   134  	if upgradeBlockHeight <= e.currentBlockHeight {
   135  		return errors.New("upgrade block earlier than current block height")
   136  	}
   137  
   138  	newv, err := semver.Parse(TrimReleaseTag(vegaReleaseTag))
   139  	if err != nil {
   140  		err = fmt.Errorf("invalid protocol version for upgrade received: version (%s), %w", vegaReleaseTag, err)
   141  		e.log.Error("", logging.Error(err))
   142  		return err
   143  	}
   144  
   145  	if semver.MustParse(TrimReleaseTag(e.currentVersion)).GT(newv) {
   146  		return fmt.Errorf("upgrade version is too old (%s > %s)", e.currentVersion, newv)
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // UpgradeProposal records the intention of a validator to upgrade the protocol to a release tag at block height.
   153  func (e *Engine) UpgradeProposal(ctx context.Context, pk string, upgradeBlockHeight uint64, vegaReleaseTag string) error {
   154  	e.lock.RLock()
   155  	defer e.lock.RUnlock()
   156  
   157  	e.log.Debug("Adding protocol upgrade proposal",
   158  		logging.String("validatorPubKey", pk),
   159  		logging.Uint64("upgradeBlockHeight", upgradeBlockHeight),
   160  		logging.String("vegaReleaseTag", vegaReleaseTag),
   161  		logging.String("currentVersion", e.currentVersion),
   162  	)
   163  
   164  	if err := e.IsValidProposal(ctx, pk, upgradeBlockHeight, vegaReleaseTag); err != nil {
   165  		return err
   166  	}
   167  
   168  	ID := protocolUpgradeProposalID(upgradeBlockHeight, vegaReleaseTag)
   169  
   170  	// if the proposed upgrade version is different from the current version we create a new proposal and keep it
   171  	// if it is the same as the current version, this is taken as a signal to withdraw previous vote for another proposal - in this case the validator will have no vote for no proposal.
   172  	if vegaReleaseTag != e.currentVersion {
   173  		// if it's the first time we see this ID, generate an active proposal entry
   174  		if _, ok := e.activeProposals[ID]; !ok {
   175  			e.activeProposals[ID] = &protocolUpgradeProposal{
   176  				blockHeight:    upgradeBlockHeight,
   177  				vegaReleaseTag: vegaReleaseTag,
   178  				accepted:       map[string]struct{}{},
   179  			}
   180  		}
   181  
   182  		active := e.activeProposals[ID]
   183  		active.accepted[pk] = struct{}{}
   184  		e.sendAndKeepEvent(ctx, ID, active)
   185  
   186  		e.log.Debug("Successfully added protocol upgrade proposal",
   187  			logging.String("validatorPubKey", pk),
   188  			logging.Uint64("upgradeBlockHeight", upgradeBlockHeight),
   189  			logging.String("vegaReleaseTag", vegaReleaseTag),
   190  		)
   191  	}
   192  
   193  	activeIDs := make([]string, 0, len(e.activeProposals))
   194  	for k := range e.activeProposals {
   195  		activeIDs = append(activeIDs, k)
   196  	}
   197  	sort.Strings(activeIDs)
   198  
   199  	// each validator can only have one vote so if we got a vote for a different release than they voted for before, we remove that vote
   200  	for _, activeID := range activeIDs {
   201  		if activeID == ID {
   202  			continue
   203  		}
   204  		activeProposal := e.activeProposals[activeID]
   205  		// if there is a vote for another proposal from the pk, remove it and send an update
   206  		if _, ok := activeProposal.accepted[pk]; ok {
   207  			delete(activeProposal.accepted, pk)
   208  			e.sendAndKeepEvent(ctx, activeID, activeProposal)
   209  
   210  			e.log.Debug("Removed validator vote from previous proposal",
   211  				logging.String("validatorPubKey", pk),
   212  				logging.Uint64("upgradeBlockHeight", activeProposal.blockHeight),
   213  				logging.String("vegaReleaseTag", activeProposal.vegaReleaseTag),
   214  			)
   215  		}
   216  		if len(activeProposal.accepted) == 0 {
   217  			delete(e.activeProposals, activeID)
   218  			delete(e.events, activeID)
   219  
   220  			e.log.Debug("Removed previous upgrade proposal",
   221  				logging.String("validatorPubKey", pk),
   222  				logging.Uint64("upgradeBlockHeight", activeProposal.blockHeight),
   223  				logging.String("vegaReleaseTag", activeProposal.vegaReleaseTag),
   224  			)
   225  		}
   226  	}
   227  
   228  	return nil
   229  }
   230  
   231  func (e *Engine) sendAndKeepEvent(ctx context.Context, ID string, activeProposal *protocolUpgradeProposal) {
   232  	status := eventspb.ProtocolUpgradeProposalStatus_PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING
   233  	if len(activeProposal.approvers()) == 0 {
   234  		status = eventspb.ProtocolUpgradeProposalStatus_PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED
   235  	}
   236  	evt := events.NewProtocolUpgradeProposalEvent(ctx, activeProposal.blockHeight, activeProposal.vegaReleaseTag, activeProposal.approvers(), status)
   237  	evtProto := evt.Proto()
   238  	e.events[ID] = &evtProto
   239  	e.broker.Send(evt)
   240  }
   241  
   242  // TimeForUpgrade is called by abci at the beginning of the block (before calling begin block of the engine) - if a block height for upgrade is set and is equal
   243  // to the last block's height then return true.
   244  func (e *Engine) TimeForUpgrade() bool {
   245  	e.lock.RLock()
   246  	defer e.lock.RUnlock()
   247  	return e.upgradeStatus.AcceptedReleaseInfo != nil && e.currentBlockHeight-e.upgradeStatus.AcceptedReleaseInfo.UpgradeBlockHeight == 0
   248  }
   249  
   250  func (e *Engine) isAccepted(p *protocolUpgradeProposal) bool {
   251  	// if the block is already behind us or we've already accepted a proposal return false
   252  	if p.blockHeight < e.currentBlockHeight {
   253  		return false
   254  	}
   255  	totalVotingPower := e.topology.GetTotalVotingPower()
   256  	if totalVotingPower <= 0 {
   257  		return false
   258  	}
   259  	totalD := num.DecimalFromInt64(totalVotingPower)
   260  	ratio := num.DecimalZero()
   261  	for k := range p.accepted {
   262  		ratio = ratio.Add(num.DecimalFromInt64(e.topology.GetVotingPower(k)).Div(totalD))
   263  	}
   264  	return ratio.GreaterThanOrEqual(e.requiredMajority)
   265  }
   266  
   267  func (e *Engine) getProposalIDs() []string {
   268  	proposalIDs := make([]string, 0, len(e.activeProposals))
   269  	for k := range e.activeProposals {
   270  		proposalIDs = append(proposalIDs, k)
   271  	}
   272  	sort.Strings(proposalIDs)
   273  	return proposalIDs
   274  }
   275  
   276  // BeginBlock is called at the beginning of the block, to mark the current block height and check if there are proposals that are accepted/rejected.
   277  // If there is more than one active proposal that is accepted (unlikely) we choose the one with the earliest upgrade block.
   278  func (e *Engine) BeginBlock(ctx context.Context, blockHeight uint64) {
   279  	e.lock.Lock()
   280  	e.currentBlockHeight = blockHeight
   281  	e.lock.Unlock()
   282  
   283  	var accepted *protocolUpgradeProposal
   284  	for _, ID := range e.getProposalIDs() {
   285  		pup := e.activeProposals[ID]
   286  		if e.isAccepted(pup) {
   287  			if accepted == nil || accepted.blockHeight > pup.blockHeight {
   288  				accepted = pup
   289  			}
   290  		} else {
   291  			if blockHeight >= pup.blockHeight {
   292  				delete(e.activeProposals, ID)
   293  				delete(e.events, ID)
   294  				e.log.Info("protocol upgrade rejected", logging.String("vega-release-tag", pup.vegaReleaseTag), logging.Uint64("upgrade-block-height", pup.blockHeight))
   295  				e.broker.Send(events.NewProtocolUpgradeProposalEvent(ctx, pup.blockHeight, pup.vegaReleaseTag, pup.approvers(), eventspb.ProtocolUpgradeProposalStatus_PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED))
   296  			}
   297  		}
   298  	}
   299  	e.lock.Lock()
   300  
   301  	if accepted != nil {
   302  		e.upgradeStatus.AcceptedReleaseInfo = &types.ReleaseInfo{
   303  			VegaReleaseTag:     accepted.vegaReleaseTag,
   304  			UpgradeBlockHeight: accepted.blockHeight,
   305  		}
   306  	} else {
   307  		e.upgradeStatus.AcceptedReleaseInfo = &types.ReleaseInfo{}
   308  	}
   309  
   310  	e.lock.Unlock()
   311  }
   312  
   313  // Cleanup is called by the abci before the final snapshot is taken to clear remaining state. It emits events for the accepted and rejected proposals.
   314  func (e *Engine) Cleanup(ctx context.Context) {
   315  	e.lock.Lock()
   316  	defer e.lock.Unlock()
   317  	for _, ID := range e.getProposalIDs() {
   318  		pup := e.activeProposals[ID]
   319  		status := eventspb.ProtocolUpgradeProposalStatus_PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED
   320  
   321  		if !e.isAccepted(pup) {
   322  			e.log.Info("protocol upgrade rejected", logging.String("vega-release-tag", pup.vegaReleaseTag), logging.Uint64("upgrade-block-height", pup.blockHeight))
   323  			status = eventspb.ProtocolUpgradeProposalStatus_PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED
   324  		}
   325  
   326  		e.broker.Send(events.NewProtocolUpgradeProposalEvent(ctx, pup.blockHeight, pup.vegaReleaseTag, pup.approvers(), status))
   327  		delete(e.activeProposals, ID)
   328  		delete(e.events, ID)
   329  	}
   330  }
   331  
   332  // SetCoreReadyForUpgrade is called by abci after a snapshot has been taken and the core process is ready to
   333  // wait for data node to process if connected or to be shutdown.
   334  func (e *Engine) SetCoreReadyForUpgrade() {
   335  	e.lock.Lock()
   336  	defer e.lock.Unlock()
   337  	if int(e.currentBlockHeight)-int(e.upgradeStatus.AcceptedReleaseInfo.UpgradeBlockHeight) != 0 {
   338  		e.log.Panic("can only call SetCoreReadyForUpgrade at the block of the block height for upgrade", logging.Uint64("block-height", e.currentBlockHeight), logging.Int("block-height-for-upgrade", int(e.upgradeStatus.AcceptedReleaseInfo.UpgradeBlockHeight)))
   339  	}
   340  	e.log.Info("marking vega core as ready to shut down")
   341  
   342  	e.coreReadyToUpgrade = true
   343  }
   344  
   345  func (e *Engine) CoreReadyForUpgrade() bool {
   346  	e.lock.RLock()
   347  	defer e.lock.RUnlock()
   348  	return e.coreReadyToUpgrade
   349  }
   350  
   351  // SetReadyForUpgrade is called by abci after both core and data node has processed all required events before the update.
   352  // This will modify the RPC API.
   353  func (e *Engine) SetReadyForUpgrade() {
   354  	e.lock.Lock()
   355  	defer e.lock.Unlock()
   356  	if !e.coreReadyToUpgrade {
   357  		e.log.Panic("can only call SetReadyForUpgrade when core node is ready up upgrade")
   358  	}
   359  	e.log.Info("marking vega core and data node as ready to shut down")
   360  
   361  	e.upgradeStatus.ReadyToUpgrade = true
   362  }
   363  
   364  // GetUpgradeStatus is an RPC call that returns the status of an upgrade.
   365  func (e *Engine) GetUpgradeStatus() types.UpgradeStatus {
   366  	e.lock.RLock()
   367  	defer e.lock.RUnlock()
   368  	return *e.upgradeStatus
   369  }