github.com/Evanesco-Labs/go-evanesco@v1.0.1/zkpminer/miner.go (about)

     1  package zkpminer
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"github.com/Evanesco-Labs/go-evanesco/accounts/abi/bind"
     7  	"github.com/Evanesco-Labs/go-evanesco/common"
     8  	"github.com/Evanesco-Labs/go-evanesco/core"
     9  	"github.com/Evanesco-Labs/go-evanesco/core/types"
    10  	"github.com/Evanesco-Labs/go-evanesco/evaclient"
    11  	"github.com/Evanesco-Labs/go-evanesco/event"
    12  	"github.com/Evanesco-Labs/go-evanesco/log"
    13  	"github.com/Evanesco-Labs/go-evanesco/rpc"
    14  	"github.com/Evanesco-Labs/go-evanesco/zkpminer/keypair"
    15  	"github.com/Evanesco-Labs/go-evanesco/zkpminer/problem"
    16  	"math/rand"
    17  	"os"
    18  	"runtime"
    19  	"runtime/debug"
    20  	"sync"
    21  	"sync/atomic"
    22  	"time"
    23  )
    24  
    25  var (
    26  	ErrorMinerWorkerOutOfRange    = errors.New("miner's workers reach MaxWorkerCnt, can not add more workers")
    27  	ErrorLocalMinerWithoutBackend = errors.New("new local miner with nil backend")
    28  	ErrorBlockHeaderSubscribe     = errors.New("block header subscribe error")
    29  )
    30  
    31  type TaskStep int
    32  
    33  const (
    34  	TASKSTART TaskStep = iota
    35  	TASKWAITCHALLENGEBLOCK
    36  	TASKGETCHALLENGEBLOCK
    37  	TASKPROBLEMSOLVED
    38  	TASKSUBMITTED
    39  )
    40  
    41  const (
    42  	COINBASEINTERVAL = types.CoinBaseInterval
    43  	SUBMITADVANCE    = types.SubmitAdvance
    44  	RPCTIMEOUT       = time.Minute
    45  )
    46  
    47  var WSUrlTryRound = 5
    48  var RetryWSRPCWaitDuration = time.Second * 5
    49  var RetryJitterMaxDuration = 1000 //millisecond
    50  
    51  type Backend interface {
    52  	BlockChain() *core.BlockChain
    53  	EventMux() *event.TypeMux
    54  }
    55  
    56  type Task struct {
    57  	CoinbaseAddr     common.Address
    58  	minerAddr        common.Address
    59  	Step             TaskStep
    60  	lastCoinBaseHash [32]byte
    61  	challengeHeader  types.HeaderShort
    62  	challengeIndex   Height
    63  	lottery          *types.Lottery
    64  	signature        [65]byte
    65  }
    66  
    67  func (t *Task) SetHeader(h types.HeaderShort) {
    68  	t.challengeHeader = h
    69  	t.lottery.ChallengeHeaderHash = h.Hash()
    70  	t.Step = TASKGETCHALLENGEBLOCK
    71  }
    72  
    73  func (t *Task) SetCoinbaseAddr(coinbaseAddr common.Address) {
    74  	t.CoinbaseAddr = coinbaseAddr
    75  }
    76  
    77  //SetTaskMinerAddr only use in TASKSTART step
    78  func SetTaskMinerAddr(template *Task, minerAddr common.Address) Task {
    79  	if template.Step != TASKSTART {
    80  		panic("only use it to update task in step TASKSTART")
    81  	}
    82  	//Deep Copy task
    83  	return Task{
    84  		minerAddr:        minerAddr,
    85  		CoinbaseAddr:     template.CoinbaseAddr,
    86  		Step:             TASKSTART,
    87  		lastCoinBaseHash: template.lastCoinBaseHash,
    88  		challengeIndex:   Height(uint64(0)),
    89  		lottery: &types.Lottery{
    90  			MinerAddr:    minerAddr,
    91  			CoinbaseAddr: template.CoinbaseAddr,
    92  		},
    93  	}
    94  }
    95  
    96  type Config struct {
    97  	MinerList        []keypair.Key
    98  	MaxWorkerCnt     int32
    99  	MaxTaskCnt       int32
   100  	CoinbaseInterval uint64
   101  	SubmitAdvance    uint64
   102  	CoinbaseAddr     common.Address
   103  	WsUrl            []string
   104  	RpcTimeout       time.Duration
   105  	PkPath           string
   106  }
   107  
   108  func DefaultConfig() Config {
   109  	return Config{
   110  		MinerList:        make([]keypair.Key, 0),
   111  		MaxWorkerCnt:     1,
   112  		MaxTaskCnt:       1,
   113  		CoinbaseInterval: COINBASEINTERVAL,
   114  		SubmitAdvance:    SUBMITADVANCE,
   115  		CoinbaseAddr:     common.Address{},
   116  		WsUrl:            []string{},
   117  		RpcTimeout:       RPCTIMEOUT,
   118  		PkPath:           "./QmQL4k1hKYiW3SDtMREjnrah1PBsak1VE3VgEqTyoDckz9",
   119  	}
   120  }
   121  
   122  func (config *Config) Customize(minerList []keypair.Key, coinbase common.Address, url []string, pkPath string) {
   123  	config.MinerList = minerList
   124  
   125  	config.CoinbaseAddr = coinbase
   126  
   127  	if len(url) != 0 {
   128  		config.WsUrl = append(config.WsUrl, url...)
   129  	}
   130  
   131  	if pkPath != "" {
   132  		config.PkPath = pkPath
   133  	}
   134  }
   135  
   136  type Miner struct {
   137  	mu               sync.RWMutex
   138  	isEffective      sync.Once
   139  	config           Config
   140  	zkpProver        *problem.Prover
   141  	MaxWorkerCnt     int32
   142  	MaxTaskCnt       int32
   143  	CoinbaseAddr     common.Address
   144  	Workers          map[common.Address]*Worker
   145  	scanner          *Scanner
   146  	coinbaseInterval Height
   147  	submitAdvance    Height
   148  	urlList          []string
   149  	exitCh           chan struct{}
   150  }
   151  
   152  func NewLocalMiner(config Config, backend Backend) (*Miner, error) {
   153  	runtime.GOMAXPROCS(1)
   154  	if backend == nil {
   155  		return nil, ErrorLocalMinerWithoutBackend
   156  	}
   157  	zkpProver, err := problem.NewProblemProver(config.PkPath)
   158  	if err != nil {
   159  		log.Error(err.Error())
   160  		return nil, err
   161  	}
   162  	log.Info("Init ZKP Problem worker success!")
   163  
   164  	miner := Miner{
   165  		mu:               sync.RWMutex{},
   166  		config:           config,
   167  		zkpProver:        zkpProver,
   168  		MaxWorkerCnt:     config.MaxWorkerCnt,
   169  		MaxTaskCnt:       config.MaxTaskCnt,
   170  		CoinbaseAddr:     config.CoinbaseAddr,
   171  		Workers:          make(map[common.Address]*Worker),
   172  		coinbaseInterval: Height(config.CoinbaseInterval),
   173  		submitAdvance:    Height(config.SubmitAdvance),
   174  		exitCh:           make(chan struct{}),
   175  		urlList:          config.WsUrl,
   176  		isEffective:      sync.Once{},
   177  	}
   178  
   179  	checkEffective := func() {
   180  		//check effective
   181  		minerAddress := config.MinerList[0].Address
   182  		ok, coinbasePledge := Iseffective(minerAddress, backend.BlockChain().InprocHandler)
   183  		if !ok {
   184  			log.Error("Miner address not staked", "address", minerAddress.String())
   185  		}
   186  		emptyAddr := common.Address{}
   187  		//coinbase address is not set on pledge, use miner address by default
   188  		if coinbasePledge == emptyAddr {
   189  			if miner.CoinbaseAddr == emptyAddr {
   190  				miner.CoinbaseAddr = minerAddress
   191  				return
   192  			}
   193  			return
   194  		}
   195  		//coinbase address is set, use pledge coinbase address by default
   196  		if coinbasePledge != emptyAddr {
   197  			if miner.CoinbaseAddr == emptyAddr {
   198  				miner.CoinbaseAddr = coinbasePledge
   199  				return
   200  			}
   201  			if miner.CoinbaseAddr != coinbasePledge {
   202  				log.Error(NotPledgeCoinbaseError.Error())
   203  				log.Info("miner coinbase address:" + miner.CoinbaseAddr.String() + ", fortress coinbase address:" + coinbasePledge.String())
   204  				return
   205  			}
   206  			return
   207  		}
   208  	}
   209  
   210  	miner.isEffective.Do(checkEffective)
   211  
   212  	explorer := LocalExplorer{
   213  		Backend:  backend,
   214  		headerCh: make(chan types.HeaderShort),
   215  	}
   216  	blockEventCh := make(chan core.ChainHeadEvent)
   217  	sub := backend.BlockChain().SubscribeChainHeadEvent(blockEventCh)
   218  	go func() {
   219  		for {
   220  			select {
   221  			case blockEvent := <-blockEventCh:
   222  				short := blockEvent.Block.Header().Short()
   223  				explorer.headerCh <- short
   224  			case <-sub.Err():
   225  				log.Error(ErrorBlockHeaderSubscribe.Error())
   226  				miner.Close()
   227  			}
   228  		}
   229  	}()
   230  	miner.NewScanner(&explorer)
   231  	miner.StartScanner()
   232  
   233  	go miner.Loop()
   234  	//add new workers
   235  	for _, key := range config.MinerList {
   236  		miner.NewWorker(key)
   237  	}
   238  	log.Info("miner start")
   239  	log.Info("waiting for next mining epoch")
   240  	return &miner, nil
   241  }
   242  
   243  func NewMiner(config Config) (*Miner, error) {
   244  	runtime.GOMAXPROCS(1)
   245  	zkpProver, err := problem.NewProblemProver(config.PkPath)
   246  	debug.FreeOSMemory()
   247  	if err != nil {
   248  		log.Error(err.Error())
   249  		return nil, err
   250  	}
   251  	log.Info("Init ZKP Problem worker success!")
   252  	if len(config.WsUrl) == 0 {
   253  		Fatalf("Evanesco websocket url unset")
   254  	}
   255  	miner := Miner{
   256  		mu:               sync.RWMutex{},
   257  		config:           config,
   258  		zkpProver:        zkpProver,
   259  		MaxWorkerCnt:     config.MaxWorkerCnt,
   260  		MaxTaskCnt:       config.MaxTaskCnt,
   261  		CoinbaseAddr:     config.CoinbaseAddr,
   262  		Workers:          make(map[common.Address]*Worker),
   263  		coinbaseInterval: Height(config.CoinbaseInterval),
   264  		submitAdvance:    Height(config.SubmitAdvance),
   265  		exitCh:           make(chan struct{}),
   266  		urlList:          config.WsUrl,
   267  		isEffective:      sync.Once{},
   268  	}
   269  
   270  	explorer := RpcExplorer{
   271  		Client:     new(rpc.Client),
   272  		Sub:        new(rpc.ClientSubscription),
   273  		HeaderCh:   make(chan types.HeaderShort),
   274  		rpcTimeOut: config.RpcTimeout,
   275  		WsUrl:      "",
   276  	}
   277  
   278  	miner.NewScanner(&explorer)
   279  	miner.updateWS()
   280  
   281  	go func() {
   282  		for {
   283  			err := <-explorer.Sub.Err()
   284  			log.Warn(ErrorBlockHeaderSubscribe.Error(), "err", err)
   285  			log.Info("try to connect another node")
   286  			miner.updateWS()
   287  		}
   288  	}()
   289  
   290  	go miner.Loop()
   291  	//add new workers
   292  	for _, key := range config.MinerList {
   293  		miner.NewWorker(key)
   294  	}
   295  	log.Info("miner start")
   296  	log.Info("waiting for next mining epoch")
   297  	return &miner, nil
   298  }
   299  
   300  func (m *Miner) updateWS() {
   301  	if m.scanner.IsUpdating() {
   302  		return
   303  	}
   304  	m.scanner.close()
   305  	exp, ok := m.scanner.explorer.(*RpcExplorer)
   306  	if !ok {
   307  		Fatalf("Full node miner disconnected from Avis Network more than %v", NewHeaderTimeoutDuration.String())
   308  		return
   309  	}
   310  	//clean old rpc explorer and new
   311  	oldURL := exp.WsUrl
   312  	go func() {
   313  		if exp.Sub == nil || exp.Client == nil {
   314  			return
   315  		}
   316  		exp.Sub.Unsubscribe()
   317  		exp.Client.Close()
   318  	}()
   319  	remoteExp := RpcExplorer{
   320  		Client:     new(rpc.Client),
   321  		Sub:        new(rpc.ClientSubscription),
   322  		HeaderCh:   make(chan types.HeaderShort),
   323  		rpcTimeOut: RPCTIMEOUT,
   324  		WsUrl:      oldURL,
   325  	}
   326  	m.scanner.explorer = &remoteExp
   327  
   328  	//set scanner status updating
   329  	atomic.StoreInt32(&m.scanner.updating, int32(1))
   330  	defer func() {
   331  		atomic.StoreInt32(&m.scanner.updating, int32(0))
   332  	}()
   333  
   334  	res := false
   335  	var err error
   336  	for i := 0; i < WSUrlTryRound; i++ {
   337  		for _, url := range m.urlList {
   338  			jitter := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(RetryJitterMaxDuration)
   339  			time.Sleep(RetryWSRPCWaitDuration + time.Millisecond*time.Duration(jitter))
   340  			if url == remoteExp.WsUrl {
   341  				continue
   342  			}
   343  			remoteExp.Client, err = rpc.Dial(url)
   344  			if err != nil {
   345  				log.Warn("Websocket dial Evanesco node err", "err", err)
   346  				continue
   347  			}
   348  
   349  			remoteExp.Sub, err = remoteExp.Client.EthSubscribe(context.Background(), remoteExp.HeaderCh, "newHeadShort")
   350  			if err != nil {
   351  				log.Warn("Subscribe block err", "err", err)
   352  				continue
   353  			}
   354  
   355  			res = true
   356  			remoteExp.WsUrl = url
   357  			log.Info("Connected node WebSocket URL", "url", url)
   358  			break
   359  		}
   360  		if res == true {
   361  			//check miner address effective
   362  			checkEffective := func() {
   363  				defer func() {
   364  					m.scanner.CoinbaseAddr = m.CoinbaseAddr
   365  				}()
   366  				evaClient := evaclient.NewClient(remoteExp.Client)
   367  				caller, err := NewPledgeCaller(PledgeContract, evaClient)
   368  				if err != nil {
   369  					Fatalf("New Pledge caller error %v", err)
   370  				}
   371  				minerKey := m.config.MinerList[0]
   372  				ok, coinbasePledge, err := caller.IseffectiveNew(&bind.CallOpts{Pending: false}, minerKey.Address)
   373  				if err != nil {
   374  					Fatalf("call pledge contract abi IseffectiveNew error %v", err)
   375  				}
   376  				if !ok {
   377  					log.Error(NotEffectiveAddrError.Error(), "address", minerKey.Address.String())
   378  				}
   379  				emptyAddr := common.Address{}
   380  				//coinbase address is not set on pledge, use miner address by default
   381  				if coinbasePledge == emptyAddr {
   382  					if m.CoinbaseAddr == emptyAddr {
   383  						m.CoinbaseAddr = minerKey.Address
   384  						return
   385  					}
   386  					return
   387  				}
   388  				//coinbase address is set, use pledge coinbase address by default
   389  				if coinbasePledge != emptyAddr {
   390  					if m.CoinbaseAddr == emptyAddr {
   391  						m.CoinbaseAddr = coinbasePledge
   392  						return
   393  					}
   394  					if m.CoinbaseAddr != coinbasePledge {
   395  						log.Error(NotPledgeCoinbaseError.Error())
   396  						log.Info("miner coinbase address:" + m.CoinbaseAddr.String() + ", fortress coinbase address:" + coinbasePledge.String())
   397  						return
   398  					}
   399  					return
   400  				}
   401  			}
   402  
   403  			m.isEffective.Do(checkEffective)
   404  			break
   405  		} else {
   406  			//reset url to try this url again
   407  			remoteExp.WsUrl = ""
   408  		}
   409  	}
   410  
   411  	if res == false {
   412  		log.Error("Dial all websocket urls failed")
   413  		os.Exit(1)
   414  	}
   415  
   416  	m.StartScanner()
   417  	return
   418  }
   419  
   420  func (m *Miner) Close() {
   421  	defer func() {
   422  		if recover() != nil {
   423  		}
   424  	}()
   425  	//close workers
   426  	for _, worker := range m.Workers {
   427  		worker.close()
   428  	}
   429  	//close scanner
   430  	m.scanner.close()
   431  	close(m.exitCh)
   432  	os.Exit(1)
   433  }
   434  
   435  func (m *Miner) NewWorker(minerKey keypair.Key) {
   436  	m.mu.Lock()
   437  	defer m.mu.Unlock()
   438  	if len(m.Workers) == int(m.MaxWorkerCnt) {
   439  		log.Error(ErrorMinerWorkerOutOfRange.Error())
   440  		return
   441  	}
   442  	worker := Worker{
   443  		mu:               sync.RWMutex{},
   444  		running:          0,
   445  		MaxTaskCnt:       m.MaxTaskCnt,
   446  		CoinbaseAddr:     m.CoinbaseAddr,
   447  		minerAddr:        minerKey.Address,
   448  		pk:               minerKey.PrivateKey.Public(),
   449  		sk:               &minerKey.PrivateKey,
   450  		workingTaskCnt:   0,
   451  		coinbaseInterval: m.coinbaseInterval,
   452  		inboundTaskCh:    make(chan *Task),
   453  		submitAdvance:    m.submitAdvance,
   454  		scanner:          m.scanner,
   455  		zkpProver:        m.zkpProver,
   456  		exitCh:           make(chan struct{}),
   457  	}
   458  
   459  	m.Workers[minerKey.Address] = &worker
   460  	go worker.Loop()
   461  	worker.start()
   462  	log.Debug("worker start")
   463  }
   464  
   465  func (m *Miner) CloseWorker(addr common.Address) {
   466  	if worker, ok := m.Workers[addr]; ok {
   467  		worker.close()
   468  		delete(m.Workers, addr)
   469  	}
   470  }
   471  
   472  func (m *Miner) StopWorker(addr common.Address) {
   473  	if worker, ok := m.Workers[addr]; ok {
   474  		worker.stop()
   475  	}
   476  }
   477  
   478  func (m *Miner) StartWorker(addr common.Address) {
   479  	if worker, ok := m.Workers[addr]; ok {
   480  		worker.start()
   481  	}
   482  }
   483  
   484  func (m *Miner) Loop() {
   485  	for {
   486  		select {
   487  		case <-m.exitCh:
   488  			return
   489  		case taskTem := <-m.scanner.outboundTaskCh:
   490  			if taskTem.Step == TASKSTART {
   491  				for _, worker := range m.Workers {
   492  					task := SetTaskMinerAddr(taskTem, worker.minerAddr)
   493  					worker.inboundTaskCh <- &task
   494  				}
   495  				continue
   496  			}
   497  			if taskTem.Step == TASKGETCHALLENGEBLOCK {
   498  				if worker, ok := m.Workers[taskTem.minerAddr]; ok {
   499  					worker.inboundTaskCh <- taskTem
   500  					continue
   501  				}
   502  				log.Warn("worker for this task not exist")
   503  			}
   504  			if taskTem.Step == TASKSUBMITTED {
   505  				//todo: store submitted lotteries for later queries
   506  			}
   507  		}
   508  	}
   509  }
   510  
   511  func (m *Miner) NewScanner(explorer Explorer) {
   512  	m.scanner = &Scanner{
   513  		miner:              m,
   514  		mu:                 sync.RWMutex{},
   515  		CoinbaseAddr:       m.CoinbaseAddr,
   516  		BestScore:          zero,
   517  		LastBlockHeight:    0,
   518  		CoinbaseInterval:   m.coinbaseInterval,
   519  		LastCoinbaseHeight: 0,
   520  		taskWait:           make(map[Height][]*Task),
   521  		inboundTaskCh:      make(chan *Task),
   522  		outboundTaskCh:     make(chan *Task),
   523  		explorer:           explorer,
   524  		exitCh:             make(chan struct{}),
   525  		running:            int32(0),
   526  		updating:           int32(0),
   527  	}
   528  }
   529  
   530  func (m *Miner) StartScanner() {
   531  	i := 0
   532  	for {
   533  		i++
   534  		if m.scanner.IsClosed() {
   535  			break
   536  		}
   537  		time.Sleep(time.Millisecond * 100)
   538  		m.scanner.close()
   539  		if i == 10 {
   540  			Fatalf("start carrier scanner failed")
   541  		}
   542  	}
   543  	m.scanner.exitCh = make(chan struct{})
   544  	go m.scanner.Loop()
   545  }