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 }