code.vegaprotocol.io/vega@v0.79.0/core/pow/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 pow 17 18 import ( 19 "context" 20 "encoding/hex" 21 "errors" 22 "sync" 23 24 "code.vegaprotocol.io/vega/core/blockchain/abci" 25 "code.vegaprotocol.io/vega/core/types" 26 "code.vegaprotocol.io/vega/libs/crypto" 27 "code.vegaprotocol.io/vega/libs/num" 28 "code.vegaprotocol.io/vega/libs/ptr" 29 "code.vegaprotocol.io/vega/logging" 30 protoapi "code.vegaprotocol.io/vega/protos/vega/api/v1" 31 ) 32 33 const ( 34 ringSize = 500 35 ) 36 37 type ValidationEntry struct { 38 ValResult ValidationResult 39 Difficulty *uint 40 Tx abci.Tx 41 } 42 43 var ErrNonceAlreadyUsedByParty = errors.New("nonce already used by party") 44 45 type ValidationResult int64 46 47 const ( 48 ValidationResultVerificationPowError ValidationResult = iota 49 ValidationResultValidatorCommand 50 ValidationResultTooManyTx 51 ValidationResultSuccess 52 ) 53 54 // params defines the modifiable set of parameters to be applied at the from block and valid for transactions generated for the untilBlock. 55 type params struct { 56 spamPoWNumberOfPastBlocks uint64 57 spamPoWDifficulty uint 58 spamPoWHashFunction string 59 spamPoWNumberOfTxPerBlock uint64 60 spamPoWIncreasingDifficulty bool 61 fromBlock uint64 62 untilBlock *uint64 63 } 64 65 type nonceRef struct { 66 party string 67 nonce uint64 68 } 69 70 // isActive for a given block height returns true if: 71 // 1. there is no expiration for the param set (i.e. untilBlock is nil) or 72 // 2. the block is within the lookback from the until block. 73 func (p *params) isActive(blockHeight uint64) bool { 74 return p.untilBlock == nil || *p.untilBlock+p.spamPoWNumberOfPastBlocks > blockHeight 75 } 76 77 // represents the number of transactions seen from a party and the total observed difficulty 78 // of transactions generated with a given block height. 79 type partyStateForBlock struct { 80 observedDifficulty uint 81 seenCount uint 82 } 83 84 type state struct { 85 blockToPartyState map[uint64]map[string]*partyStateForBlock 86 } 87 type Engine struct { 88 activeParams []*params // active sets of parameters 89 activeStates []*state // active states corresponding to the sets of parameters 90 91 currentBlock uint64 // the current block height 92 blockHeight [ringSize]uint64 // block heights in scope ring buffer - this has a fixed size which is equal to the maximum value of the network parameter 93 blockHash [ringSize]string // block hashes in scope ring buffer - this has a fixed size which is equal to the maximum value of the network parameter 94 seenTx map[string]struct{} // seen transactions in scope set 95 heightToTx map[uint64][]string // height to slice of seen transaction in scope ring buffer 96 seenTid map[string]struct{} // seen tid in scope set 97 heightToTid map[uint64][]string // height to slice of seen tid in scope ring buffer 98 lastPruningBlock uint64 99 100 // tracking the nonces used in the input data to make sure they are not reused 101 seenNonceRef map[nonceRef]struct{} 102 heightToNonceRef map[uint64][]nonceRef 103 104 mempoolSeenTid map[string]struct{} // tids seen already in this node's mempool, cleared at the end of the block 105 106 noVerify bool // disables verification of PoW in scenario, where we use the null chain and we do not want to send transaction w/o verification 107 108 // snapshot key 109 hashKeys []string 110 log *logging.Logger 111 lock sync.RWMutex 112 } 113 114 // New instantiates the proof of work engine. 115 func New(log *logging.Logger, config Config) *Engine { 116 log = log.Named(namedLogger) 117 log.SetLevel(config.Level.Get()) 118 e := &Engine{ 119 log: log, 120 hashKeys: []string{(&types.PayloadProofOfWork{}).Key()}, 121 activeParams: []*params{}, 122 activeStates: []*state{}, 123 seenTx: map[string]struct{}{}, 124 heightToTx: map[uint64][]string{}, 125 heightToNonceRef: map[uint64][]nonceRef{}, 126 seenTid: map[string]struct{}{}, 127 seenNonceRef: map[nonceRef]struct{}{}, 128 mempoolSeenTid: map[string]struct{}{}, 129 heightToTid: map[uint64][]string{}, 130 } 131 e.log.Info("PoW spam protection started") 132 return e 133 } 134 135 // OnBeginBlock updates the block height and block hash and clears any out of scope parameters set and states. 136 // It also records all of the block's transactions. 137 func (e *Engine) BeginBlock(blockHeight uint64, blockHash string, txs []abci.Tx) { 138 e.lock.Lock() 139 defer e.lock.Unlock() 140 e.currentBlock = blockHeight 141 idx := blockHeight % ringSize 142 e.blockHeight[idx] = blockHeight 143 e.blockHash[idx] = blockHash 144 e.updatePowState(txs) 145 } 146 147 // CheckTx is called by checkTx in the abci and verifies the proof of work, it doesn't update any state. 148 func (e *Engine) CheckTx(tx abci.Tx) error { 149 if e.log.IsDebug() { 150 e.lock.RLock() 151 e.log.Debug("checktx got tx", logging.String("command", tx.Command().String()), logging.Uint64("height", tx.BlockHeight()), logging.String("tid", tx.GetPoWTID()), logging.Uint64("current-block", e.currentBlock)) 152 e.lock.RUnlock() 153 } 154 if !tx.Command().IsValidatorCommand() { 155 e.lock.Lock() 156 if _, ok := e.mempoolSeenTid[tx.GetPoWTID()]; ok { 157 e.log.Error("tid already seen", logging.String("tid", tx.GetPoWTID()), logging.String("party", tx.Party())) 158 e.lock.Unlock() 159 return errors.New("proof of work tid already seen by the node") 160 } 161 e.mempoolSeenTid[tx.GetPoWTID()] = struct{}{} 162 e.lock.Unlock() 163 } 164 165 _, err := e.verify(tx) 166 if err != nil { 167 e.log.Debug("checktx error", logging.String("command", tx.Command().String()), logging.Uint64("height", tx.BlockHeight()), logging.String("tid", tx.GetPoWTID()), logging.Uint64("current-block", e.currentBlock)) 168 } 169 return err 170 } 171 172 // EndPrepareProposal is a callback called at the end of prepareBlock to revert to the state 173 // before prepare block. 174 func (e *Engine) EndPrepareProposal(txs []ValidationEntry) { 175 e.log.Debug("EndPrepareBlock called with", logging.Int("txs", len(txs))) 176 e.rollback(txs) 177 } 178 179 // updatePowState updates the pow state given the block transaction and cleans up out of scope states and param sets. 180 func (e *Engine) updatePowState(txs []abci.Tx) { 181 // run this once for migration cleanup 182 // this is going to clean up blocks that belong to inactive states which are unreachable by the latter loop. 183 if e.lastPruningBlock == 0 { 184 if len(e.activeParams) > 0 && e.activeParams[0].fromBlock > 0 { 185 end := e.activeParams[0].fromBlock 186 for block := uint64(0); block <= end; block++ { 187 if b, ok := e.heightToTx[block]; ok { 188 for _, v := range b { 189 delete(e.seenTx, v) 190 } 191 } 192 for _, v := range e.heightToTid[block] { 193 delete(e.seenTid, v) 194 } 195 for _, v := range e.heightToNonceRef[block] { 196 delete(e.seenNonceRef, v) 197 } 198 delete(e.heightToTx, block) 199 delete(e.heightToTid, block) 200 delete(e.heightToNonceRef, block) 201 } 202 } 203 } 204 205 for _, tx := range txs { 206 d, _ := e.verifyWithLock(tx) 207 dUint := uint(d) 208 txHash := hex.EncodeToString(tx.Hash()) 209 txBlock := tx.BlockHeight() 210 stateInd := 0 211 for i, p := range e.activeParams { 212 if txBlock >= p.fromBlock && (p.untilBlock == nil || *p.untilBlock >= txBlock) { 213 stateInd = i 214 break 215 } 216 } 217 state := e.activeStates[stateInd] 218 e.seenTx[txHash] = struct{}{} 219 e.heightToTx[tx.BlockHeight()] = append(e.heightToTx[tx.BlockHeight()], txHash) 220 if tx.Command().IsValidatorCommand() { 221 continue 222 } 223 224 e.heightToTid[tx.BlockHeight()] = append(e.heightToTid[tx.BlockHeight()], tx.GetPoWTID()) 225 e.heightToNonceRef[tx.BlockHeight()] = append(e.heightToNonceRef[tx.BlockHeight()], nonceRef{tx.Party(), tx.GetNonce()}) 226 e.seenTid[tx.GetPoWTID()] = struct{}{} 227 e.seenNonceRef[nonceRef{tx.Party(), tx.GetNonce()}] = struct{}{} 228 if _, ok := state.blockToPartyState[txBlock]; !ok { 229 state.blockToPartyState[txBlock] = map[string]*partyStateForBlock{tx.Party(): {observedDifficulty: dUint, seenCount: uint(1)}} 230 continue 231 } 232 if _, ok := state.blockToPartyState[txBlock][tx.Party()]; !ok { 233 state.blockToPartyState[txBlock][tx.Party()] = &partyStateForBlock{observedDifficulty: dUint, seenCount: uint(1)} 234 continue 235 } 236 partyState := state.blockToPartyState[txBlock][tx.Party()] 237 partyState.observedDifficulty += dUint 238 partyState.seenCount++ 239 } 240 241 // update out of scope states/params 242 toDelete := []int{} 243 // iterate over parameters set and clear them out if they're not relevant anymore. 244 for i, p := range e.activeParams { 245 // is active means if we're still accepting transactions from it i.e. if the untilBlock + spamPoWNumberOfPastBlocks <= blockHeight 246 if !p.isActive(e.currentBlock) { 247 toDelete = append(toDelete, i) 248 continue 249 } 250 } 251 252 for i, p := range e.activeParams { 253 outOfScopeBlock := int64(e.currentBlock) - int64(p.spamPoWNumberOfPastBlocks) 254 if outOfScopeBlock < 0 { 255 continue 256 } 257 258 start := uint64(outOfScopeBlock) 259 end := uint64(outOfScopeBlock) 260 if e.currentBlock%1000 == 0 { 261 start = e.lastPruningBlock 262 e.lastPruningBlock = e.currentBlock 263 } 264 265 for block := start; block <= end; block++ { 266 if b, ok := e.heightToTx[block]; ok { 267 for _, v := range b { 268 delete(e.seenTx, v) 269 } 270 } 271 for _, v := range e.heightToTid[block] { 272 delete(e.seenTid, v) 273 } 274 for _, v := range e.heightToNonceRef[block] { 275 delete(e.seenNonceRef, v) 276 } 277 delete(e.heightToTx, block) 278 delete(e.heightToTid, block) 279 delete(e.heightToNonceRef, block) 280 delete(e.activeStates[i].blockToPartyState, block) 281 } 282 } 283 284 // delete all out of scope configurations and states 285 for i := len(toDelete) - 1; i >= 0; i-- { 286 e.activeParams = append(e.activeParams[:toDelete[i]], e.activeParams[toDelete[i]+1:]...) 287 e.activeStates = append(e.activeStates[:toDelete[i]], e.activeStates[toDelete[i]+1:]...) 288 } 289 } 290 291 // OnCommit is called when the finalizeBlock is completed to clenup the mempool cache. 292 func (e *Engine) OnCommit() { 293 e.log.Debug("OnCommit") 294 e.log.Debug("mempool seen cleared") 295 e.lock.Lock() 296 e.mempoolSeenTid = map[string]struct{}{} 297 e.lock.Unlock() 298 } 299 300 func (e *Engine) DisableVerification() { 301 e.log.Info("Disabling PoW verification") 302 e.noVerify = true 303 } 304 305 // rollback is called without the lock. For each input validation entry depending on its status it reverts any changes made to the interim block state. 306 func (e *Engine) rollback(txs []ValidationEntry) { 307 e.lock.Lock() 308 defer e.lock.Unlock() 309 for _, ve := range txs { 310 e.log.Debug("rollback", logging.String("party", ve.Tx.Party()), logging.String("tx-hash", hex.EncodeToString(ve.Tx.Hash())), logging.Int64("ve-result", int64(ve.ValResult))) 311 // pow error does not change state, we can skip 312 if ve.ValResult == ValidationResultVerificationPowError { 313 continue 314 } 315 txHash := hex.EncodeToString(ve.Tx.Hash()) 316 // remove the transaction from seenTx - need to acquire lock! 317 318 delete(e.seenTx, txHash) 319 320 // if it's a validator command, we're done 321 if ve.ValResult == ValidationResultValidatorCommand { 322 continue 323 } 324 325 // otherwise need to remove the seenTid from the block state - need to acquire lock! 326 delete(e.seenTid, ve.Tx.GetPoWTID()) 327 delete(e.seenNonceRef, nonceRef{ve.Tx.Party(), ve.Tx.GetNonce()}) 328 329 // if the validation result is too many transactions or the difficulty is nil, nothing to revert 330 if ve.ValResult == ValidationResultTooManyTx || ve.Difficulty == nil { 331 continue 332 } 333 stateInd := 0 334 txBlock := ve.Tx.BlockHeight() 335 for i, p := range e.activeParams { 336 if txBlock >= p.fromBlock && (p.untilBlock == nil || *p.untilBlock >= txBlock) { 337 stateInd = i 338 break 339 } 340 } 341 state := e.activeStates[stateInd] 342 if _, ok := state.blockToPartyState[txBlock]; !ok { 343 e.log.Error("cannot find state of the block - that should be impossible") 344 } else if _, ok := state.blockToPartyState[txBlock][ve.Tx.Party()]; !ok { 345 e.log.Error("cannot find the party in the block state - that should be impossible") 346 } 347 348 partyState := state.blockToPartyState[txBlock][ve.Tx.Party()] 349 e.log.Debug("found party state for party", logging.Bool("found", partyState != nil), logging.String("party", ve.Tx.Party())) 350 partyState.seenCount-- 351 partyState.observedDifficulty -= *ve.Difficulty 352 if partyState.seenCount == 0 { 353 e.log.Debug("seen count for party is zero, removing party from block state", logging.String("party", ve.Tx.Party())) 354 delete(state.blockToPartyState[txBlock], ve.Tx.Party()) 355 } 356 if len(state.blockToPartyState[txBlock]) == 0 { 357 e.log.Debug("no more transactions for block, removing block height", logging.Uint64("height", txBlock)) 358 delete(state.blockToPartyState, txBlock) 359 } 360 } 361 } 362 363 func (e *Engine) ProcessProposal(txs []abci.Tx) bool { 364 ves := []ValidationEntry{} 365 success := true 366 for _, tx := range txs { 367 vr, d := e.CheckBlockTx(tx) 368 ves = append(ves, ValidationEntry{Tx: tx, Difficulty: d, ValResult: vr}) 369 if vr == ValidationResultVerificationPowError || vr == ValidationResultTooManyTx { 370 success = false 371 break 372 } 373 } 374 e.rollback(ves) 375 return success 376 } 377 378 // CheckBlockTx verifies if a transaction can be included a prepared/verified block. 379 func (e *Engine) CheckBlockTx(tx abci.Tx) (ValidationResult, *uint) { 380 if e.log.IsDebug() { 381 e.lock.RLock() 382 e.log.Debug("CheckBlockTx got tx", logging.String("command", tx.Command().String()), logging.Uint64("height", tx.BlockHeight()), logging.String("tid", tx.GetPoWTID()), logging.Uint64("current-block", e.currentBlock)) 383 e.lock.RUnlock() 384 } 385 386 d, err := e.verify(tx) 387 dUint := uint(d) 388 if err != nil { 389 e.log.Error("pow error", logging.Error(err)) 390 return ValidationResultVerificationPowError, nil 391 } 392 393 e.lock.Lock() 394 defer e.lock.Unlock() 395 396 // keep the transaction hash 397 txHash := hex.EncodeToString(tx.Hash()) 398 txBlock := tx.BlockHeight() 399 stateInd := 0 400 for i, p := range e.activeParams { 401 if txBlock >= p.fromBlock && (p.untilBlock == nil || *p.untilBlock >= txBlock) { 402 stateInd = i 403 break 404 } 405 } 406 state := e.activeStates[stateInd] 407 params := e.activeParams[stateInd] 408 409 e.seenTx[txHash] = struct{}{} 410 411 if tx.Command().IsValidatorCommand() { 412 return ValidationResultValidatorCommand, nil 413 } 414 415 // if version supports pow, save the pow result and the tid 416 e.seenTid[tx.GetPoWTID()] = struct{}{} 417 e.seenNonceRef[nonceRef{tx.Party(), tx.GetNonce()}] = struct{}{} 418 419 // if it's the first transaction we're seeing from any party for this block height, initialise the state 420 if _, ok := state.blockToPartyState[txBlock]; !ok { 421 state.blockToPartyState[txBlock] = map[string]*partyStateForBlock{tx.Party(): {observedDifficulty: dUint, seenCount: uint(1)}} 422 if e.log.IsDebug() { 423 e.log.Debug("transaction accepted", logging.String("tid", tx.GetPoWTID())) 424 } 425 e.log.Debug("updated party block state", logging.Uint64("height", txBlock), logging.String("party", tx.Party()), logging.String("tx-hash", txHash)) 426 return ValidationResultSuccess, &dUint 427 } 428 429 // if it's the first transaction for the party for this block height 430 if _, ok := state.blockToPartyState[txBlock][tx.Party()]; !ok { 431 state.blockToPartyState[txBlock][tx.Party()] = &partyStateForBlock{observedDifficulty: dUint, seenCount: uint(1)} 432 e.log.Debug("updated party block state", logging.Uint64("height", txBlock), logging.String("party", tx.Party()), logging.String("tx-hash", txHash)) 433 return ValidationResultSuccess, &dUint 434 } 435 436 // it's not the first transaction for the party for the given block height 437 // if we've seen less than the allowed number of transactions per block, take a note and let it pass 438 partyState := state.blockToPartyState[txBlock][tx.Party()] 439 if partyState.seenCount < uint(params.spamPoWNumberOfTxPerBlock) { 440 partyState.observedDifficulty += dUint 441 partyState.seenCount++ 442 443 if e.log.IsDebug() { 444 e.log.Debug("transaction accepted", logging.String("tid", tx.GetPoWTID())) 445 } 446 e.log.Debug("updated party block state", logging.Uint64("height", txBlock), logging.String("party", tx.Party()), logging.String("tx-hash", txHash)) 447 return ValidationResultSuccess, &dUint 448 } 449 450 // if we've seen already enough transactions and `spamPoWIncreasingDifficulty` is not enabled then fail the transaction 451 if !params.spamPoWIncreasingDifficulty { 452 return ValidationResultTooManyTx, nil 453 } 454 455 // calculate the expected difficulty - allow spamPoWNumberOfTxPerBlock for every level of increased difficulty 456 totalExpectedDifficulty, _ := calculateExpectedDifficulty(params.spamPoWDifficulty, uint(params.spamPoWNumberOfTxPerBlock), partyState.seenCount+1) 457 458 // if the observed difficulty sum is less than the expected difficulty, reject the tx 459 if partyState.observedDifficulty+dUint < totalExpectedDifficulty { 460 return ValidationResultTooManyTx, nil 461 } 462 463 partyState.observedDifficulty += dUint 464 partyState.seenCount++ 465 e.log.Debug("updated party block state", logging.Uint64("height", txBlock), logging.String("party", tx.Party()), logging.String("tx-hash", txHash)) 466 return ValidationResultSuccess, &dUint 467 } 468 469 // calculateExpectedDifficulty calculates the expected total difficulty given the default difficulty, the max batch size and the number of seen transactions 470 // such that for each difficulty we allow batch size transactions. 471 // e.g. spamPoWDifficulty = 5 472 // 473 // spamPoWNumberOfTxPerBlock = 10 474 // seenTx = 33 475 // 476 // expected difficulty = 10 * 5 + 10 * 6 + 10 * 7 + 3 * 8 = 204. 477 func calculateExpectedDifficulty(spamPoWDifficulty uint, spamPoWNumberOfTxPerBlock uint, seenTx uint) (uint, uint) { 478 if seenTx <= spamPoWNumberOfTxPerBlock { 479 if seenTx == spamPoWNumberOfTxPerBlock { 480 return seenTx * spamPoWDifficulty, spamPoWDifficulty + 1 481 } 482 483 return seenTx * spamPoWDifficulty, spamPoWDifficulty 484 } 485 total := uint(0) 486 diff := spamPoWDifficulty 487 d := seenTx 488 for { 489 if d > spamPoWNumberOfTxPerBlock { 490 total += diff * spamPoWNumberOfTxPerBlock 491 d -= spamPoWNumberOfTxPerBlock 492 } else { 493 total += diff * d 494 break 495 } 496 diff++ 497 } 498 499 if seenTx%spamPoWNumberOfTxPerBlock == 0 { 500 diff++ 501 } 502 503 return total, diff 504 } 505 506 func (e *Engine) findParamsForBlockHeight(height uint64) int { 507 paramIndex := -1 508 for i, p := range e.activeParams { 509 if height >= p.fromBlock && (p.untilBlock == nil || *p.untilBlock >= height) { 510 paramIndex = i 511 } 512 } 513 return paramIndex 514 } 515 516 func (e *Engine) verifyWithLock(tx abci.Tx) (byte, error) { 517 var h byte 518 if e.noVerify { 519 return h, nil 520 } 521 522 // check if the transaction was seen in scope 523 txHash := hex.EncodeToString(tx.Hash()) 524 if _, ok := e.seenTx[txHash]; ok { 525 e.log.Error("replay attack: txHash already used", logging.String("tx-hash", txHash), logging.String("tid", tx.GetPoWTID()), logging.String("party", tx.Party()), logging.String("command", tx.Command().String())) 526 return h, errors.New("transaction hash already used") 527 } 528 529 // check if the block height is in scope and is known 530 531 // we need to find the parameters that is relevant to the block for which the pow was generated 532 // as assume that the number of elements in the slice is small so no point in bothering with binary search 533 paramIndex := e.findParamsForBlockHeight(tx.BlockHeight()) 534 if paramIndex < 0 { 535 return h, errors.New("transaction too old") 536 } 537 538 params := e.activeParams[paramIndex] 539 idx := tx.BlockHeight() % ringSize 540 // if the block height doesn't match out expectation or is older than what's allowed by the parameters used for the transaction then reject 541 if e.blockHeight[idx] != tx.BlockHeight() || tx.BlockHeight()+params.spamPoWNumberOfPastBlocks <= e.currentBlock { 542 if e.log.IsDebug() { 543 e.log.Debug("unknown block height", logging.Uint64("current-block-height", e.currentBlock), logging.String("tx-hash", txHash), logging.String("tid", tx.GetPoWTID()), logging.Uint64("tx-block-height", tx.BlockHeight()), logging.Uint64("index", idx), logging.String("command", tx.Command().String()), logging.String("party", tx.Party())) 544 } 545 return h, errors.New("unknown block height for tx:" + txHash + ", command:" + tx.Command().String() + ", party:" + tx.Party()) 546 } 547 548 // validator commands skip PoW verification 549 if tx.Command().IsValidatorCommand() { 550 return h, nil 551 } 552 553 // check if the tid was seen in scope 554 if _, ok := e.seenTid[tx.GetPoWTID()]; ok { 555 if e.log.IsDebug() { 556 e.log.Debug("tid already used", logging.String("tid", tx.GetPoWTID()), logging.String("party", tx.Party())) 557 } 558 return h, errors.New("proof of work tid already used") 559 } 560 561 // check if the nonce was seen in scope 562 if _, ok := e.seenNonceRef[nonceRef{tx.Party(), tx.GetNonce()}]; ok { 563 if e.log.IsDebug() { 564 e.log.Debug("nonce already used by party", logging.Uint64("nonce", tx.GetNonce()), logging.String("party", tx.Party())) 565 } 566 return h, ErrNonceAlreadyUsedByParty 567 } 568 569 // verify the proof of work 570 hash := e.blockHash[idx] 571 success, diff := crypto.Verify(hash, tx.GetPoWTID(), tx.GetPoWNonce(), params.spamPoWHashFunction, params.spamPoWDifficulty) 572 if !success { 573 if e.log.IsDebug() { 574 e.log.Debug("failed to verify proof of work", logging.String("tid", tx.GetPoWTID()), logging.String("party", tx.Party())) 575 } 576 return diff, errors.New("failed to verify proof of work") 577 } 578 if e.log.IsDebug() { 579 e.log.Debug("transaction passed verify", logging.String("tid", tx.GetPoWTID()), logging.String("party", tx.Party())) 580 } 581 return diff, nil 582 } 583 584 // verify the proof of work 585 // 1. check that the block height is already known to the engine - this is rejected if it's too old or not yet seen as we need to know the block hash 586 // 2. check that we've not seen this transaction ID before (in the previous `spamPoWNumberOfPastBlocks` blocks) 587 // 3. check that the proof of work can be verified with the required difficulty. 588 func (e *Engine) verify(tx abci.Tx) (byte, error) { 589 e.lock.RLock() 590 defer e.lock.RUnlock() 591 return e.verifyWithLock(tx) 592 } 593 594 func (e *Engine) updateParam(netParamName, netParamValue string, p *params) { 595 switch netParamName { 596 case "spamPoWNumberOfPastBlocks": 597 spamPoWNumberOfPastBlock, _ := num.UintFromString(netParamValue, 10) 598 p.spamPoWNumberOfPastBlocks = spamPoWNumberOfPastBlock.Uint64() 599 case "spamPoWDifficulty": 600 spamPoWDifficulty, _ := num.UintFromString(netParamValue, 10) 601 p.spamPoWDifficulty = uint(spamPoWDifficulty.Uint64()) 602 case "spamPoWHashFunction": 603 p.spamPoWHashFunction = netParamValue 604 case "spamPoWNumberOfTxPerBlock": 605 spamPoWNumberOfTxPerBlock, _ := num.UintFromString(netParamValue, 10) 606 p.spamPoWNumberOfTxPerBlock = spamPoWNumberOfTxPerBlock.Uint64() 607 case "spamPoWIncreasingDifficulty": 608 p.spamPoWIncreasingDifficulty = netParamValue != "0" 609 } 610 } 611 612 func (e *Engine) updateWithLock(netParamName, netParamValue string) { 613 // if there are no settings yet 614 if len(e.activeParams) == 0 { 615 p := ¶ms{ 616 fromBlock: e.currentBlock, 617 untilBlock: nil, 618 } 619 e.activeParams = append(e.activeParams, p) 620 newState := &state{ 621 blockToPartyState: map[uint64]map[string]*partyStateForBlock{}, 622 } 623 e.activeStates = append(e.activeStates, newState) 624 e.updateParam(netParamName, netParamValue, p) 625 return 626 } 627 lastActive := e.activeParams[len(e.activeParams)-1] 628 if lastActive.fromBlock == e.currentBlock+1 || (len(e.activeParams) == 1 && e.activeParams[0].fromBlock == e.currentBlock) { 629 e.updateParam(netParamName, netParamValue, lastActive) 630 return 631 } 632 lastActive.untilBlock = new(uint64) 633 *lastActive.untilBlock = e.currentBlock 634 newParams := ¶ms{ 635 fromBlock: e.currentBlock + 1, 636 untilBlock: nil, 637 spamPoWIncreasingDifficulty: lastActive.spamPoWIncreasingDifficulty, 638 spamPoWNumberOfPastBlocks: lastActive.spamPoWNumberOfPastBlocks, 639 spamPoWDifficulty: lastActive.spamPoWDifficulty, 640 spamPoWHashFunction: lastActive.spamPoWHashFunction, 641 spamPoWNumberOfTxPerBlock: lastActive.spamPoWNumberOfTxPerBlock, 642 } 643 e.updateParam(netParamName, netParamValue, newParams) 644 e.activeParams = append(e.activeParams, newParams) 645 646 newState := &state{ 647 blockToPartyState: map[uint64]map[string]*partyStateForBlock{}, 648 } 649 e.activeStates = append(e.activeStates, newState) 650 } 651 652 // UpdateSpamPoWNumberOfPastBlocks updates the network parameter of number of past blocks to look at. This requires extending or shrinking the size of the cache. 653 func (e *Engine) UpdateSpamPoWNumberOfPastBlocks(_ context.Context, spamPoWNumberOfPastBlocks *num.Uint) error { 654 e.log.Info("updating spamPoWNumberOfPastBlocks", logging.Uint64("new-value", spamPoWNumberOfPastBlocks.Uint64())) 655 e.lock.Lock() 656 defer e.lock.Unlock() 657 e.updateWithLock("spamPoWNumberOfPastBlocks", spamPoWNumberOfPastBlocks.String()) 658 return nil 659 } 660 661 // UpdateSpamPoWDifficulty updates the network parameter for difficulty. 662 func (e *Engine) UpdateSpamPoWDifficulty(_ context.Context, spamPoWDifficulty *num.Uint) error { 663 e.log.Info("updating spamPoWDifficulty", logging.Uint64("new-value", spamPoWDifficulty.Uint64())) 664 e.lock.Lock() 665 defer e.lock.Unlock() 666 e.updateWithLock("spamPoWDifficulty", spamPoWDifficulty.String()) 667 return nil 668 } 669 670 // UpdateSpamPoWHashFunction updates the network parameter for hash function. 671 func (e *Engine) UpdateSpamPoWHashFunction(_ context.Context, spamPoWHashFunction string) error { 672 e.log.Info("updating spamPoWHashFunction", logging.String("new-value", spamPoWHashFunction)) 673 e.lock.Lock() 674 defer e.lock.Unlock() 675 e.updateWithLock("spamPoWHashFunction", spamPoWHashFunction) 676 return nil 677 } 678 679 // UpdateSpamPoWNumberOfTxPerBlock updates the number of transactions allowed for a party per block before increased difficulty kicks in if enabled. 680 func (e *Engine) UpdateSpamPoWNumberOfTxPerBlock(_ context.Context, spamPoWNumberOfTxPerBlock *num.Uint) error { 681 e.log.Info("updating spamPoWNumberOfTxPerBlock", logging.Uint64("new-value", spamPoWNumberOfTxPerBlock.Uint64())) 682 e.lock.Lock() 683 defer e.lock.Unlock() 684 e.updateWithLock("spamPoWNumberOfTxPerBlock", spamPoWNumberOfTxPerBlock.String()) 685 return nil 686 } 687 688 // UpdateSpamPoWIncreasingDifficulty enables/disabled increased difficulty. 689 func (e *Engine) UpdateSpamPoWIncreasingDifficulty(_ context.Context, spamPoWIncreasingDifficulty *num.Uint) error { 690 e.log.Info("updating spamPoWIncreasingDifficulty", logging.Bool("new-value", !spamPoWIncreasingDifficulty.IsZero())) 691 e.lock.Lock() 692 defer e.lock.Unlock() 693 e.updateWithLock("spamPoWIncreasingDifficulty", spamPoWIncreasingDifficulty.String()) 694 return nil 695 } 696 697 func (e *Engine) getActiveParams() *params { 698 if len(e.activeParams) == 1 { 699 return e.activeParams[0] 700 } 701 if e.activeParams[len(e.activeParams)-1].fromBlock > e.currentBlock { 702 return e.activeParams[len(e.activeParams)-2] 703 } 704 return e.activeParams[len(e.activeParams)-1] 705 } 706 707 func (e *Engine) IsReady() bool { 708 return len(e.activeParams) > 0 709 } 710 711 func (e *Engine) SpamPoWNumberOfPastBlocks() uint32 { 712 return uint32(e.getActiveParams().spamPoWNumberOfPastBlocks) 713 } 714 715 func (e *Engine) SpamPoWDifficulty() uint32 { 716 return uint32(e.getActiveParams().spamPoWDifficulty) 717 } 718 719 func (e *Engine) SpamPoWHashFunction() string { 720 return e.getActiveParams().spamPoWHashFunction 721 } 722 723 func (e *Engine) SpamPoWNumberOfTxPerBlock() uint32 { 724 return uint32(e.getActiveParams().spamPoWNumberOfTxPerBlock) 725 } 726 727 func (e *Engine) SpamPoWIncreasingDifficulty() bool { 728 return e.getActiveParams().spamPoWIncreasingDifficulty 729 } 730 731 func (e *Engine) BlockData() (uint64, string) { 732 e.lock.RLock() 733 defer e.lock.RUnlock() 734 735 if len(e.activeParams) == 0 { 736 return 0, "" 737 } 738 return e.currentBlock, e.blockHash[e.currentBlock%ringSize] 739 } 740 741 func getParamsForBlock(block uint64, activeParams []*params) *params { 742 stateInd := 0 743 for i, p := range activeParams { 744 if block >= p.fromBlock && (p.untilBlock == nil || *p.untilBlock >= block) { 745 stateInd = i 746 break 747 } 748 } 749 750 params := activeParams[stateInd] 751 return params 752 } 753 754 func (e *Engine) GetSpamStatistics(partyID string) *protoapi.PoWStatistic { 755 e.lock.RLock() 756 defer e.lock.RUnlock() 757 758 stats := make([]*protoapi.PoWBlockState, 0) 759 760 currentBlockStatsExists := false 761 762 for _, state := range e.activeStates { 763 for block, blockToPartyState := range state.blockToPartyState { 764 if partyState, ok := blockToPartyState[partyID]; ok { 765 if block == e.currentBlock { 766 currentBlockStatsExists = true 767 } 768 blockIndex := block % ringSize 769 params := getParamsForBlock(block, e.activeParams) 770 771 stats = append(stats, &protoapi.PoWBlockState{ 772 BlockHeight: block, 773 BlockHash: e.blockHash[blockIndex], 774 TransactionsSeen: uint64(partyState.seenCount), 775 ExpectedDifficulty: getMinDifficultyForNextTx(params.spamPoWDifficulty, 776 uint(params.spamPoWNumberOfTxPerBlock), 777 partyState.seenCount, 778 partyState.observedDifficulty, 779 params.spamPoWIncreasingDifficulty, 780 ), 781 IncreasingDifficulty: params.spamPoWIncreasingDifficulty, 782 TxPerBlock: params.spamPoWNumberOfTxPerBlock, 783 HashFunction: params.spamPoWHashFunction, 784 Difficulty: uint64(params.spamPoWDifficulty), 785 }) 786 } 787 } 788 } 789 790 // If we don't have any spam stats for the current block, add it 791 if !currentBlockStatsExists { 792 params := getParamsForBlock(e.currentBlock, e.activeParams) 793 expected := uint64(params.spamPoWDifficulty) 794 stats = append(stats, &protoapi.PoWBlockState{ 795 BlockHeight: e.currentBlock, 796 BlockHash: e.blockHash[e.currentBlock%ringSize], 797 TransactionsSeen: 0, 798 ExpectedDifficulty: &expected, 799 HashFunction: params.spamPoWHashFunction, 800 IncreasingDifficulty: params.spamPoWIncreasingDifficulty, 801 TxPerBlock: params.spamPoWNumberOfTxPerBlock, 802 Difficulty: uint64(params.spamPoWDifficulty), 803 }) 804 } 805 806 return &protoapi.PoWStatistic{ 807 BlockStates: stats, 808 NumberOfPastBlocks: e.getActiveParams().spamPoWNumberOfPastBlocks, 809 } 810 } 811 812 func getMinDifficultyForNextTx(baseDifficulty, txPerBlock, seenTx, observedDifficulty uint, increaseDifficulty bool) *uint64 { 813 if !increaseDifficulty { 814 if seenTx < txPerBlock { 815 return ptr.From(uint64(baseDifficulty)) 816 } 817 // they cannot submit any more against this block, do not return a next-difficulty 818 return nil 819 } 820 821 // calculate the total expected difficulty based on the number of transactions seen 822 totalDifficulty, powDiff := calculateExpectedDifficulty(baseDifficulty, txPerBlock, seenTx) 823 // add the current PoW difficulty to the current expected difficulty to get the expected total difficulty for the next transaction 824 totalDifficulty += powDiff 825 nextExpectedDifficulty := totalDifficulty - observedDifficulty 826 if nextExpectedDifficulty < baseDifficulty { 827 nextExpectedDifficulty = baseDifficulty 828 } 829 830 minDifficultyForNextTx := uint64(nextExpectedDifficulty) 831 832 return &minDifficultyForNextTx 833 }