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 := &params{
   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 := &params{
   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  }