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 }