github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/services/oracle/request.go (about)

     1  package oracle
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"mime"
     7  	"net/http"
     8  	"net/url"
     9  	"time"
    10  
    11  	"github.com/nspcc-dev/neo-go/pkg/core/state"
    12  	"github.com/nspcc-dev/neo-go/pkg/core/storage"
    13  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    14  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    15  	"github.com/nspcc-dev/neo-go/pkg/services/oracle/neofs"
    16  	"go.uber.org/zap"
    17  )
    18  
    19  const defaultMaxConcurrentRequests = 10
    20  
    21  type request struct {
    22  	ID  uint64
    23  	Req *state.OracleRequest
    24  }
    25  
    26  func (o *Oracle) runRequestWorker() {
    27  	for {
    28  		select {
    29  		case <-o.close:
    30  			return
    31  		case req := <-o.requestCh:
    32  			acc := o.getAccount()
    33  			if acc == nil {
    34  				continue
    35  			}
    36  			err := o.processRequest(acc.PrivateKey(), req)
    37  			if err != nil {
    38  				o.Log.Debug("can't process request", zap.Uint64("id", req.ID), zap.Error(err))
    39  			}
    40  		}
    41  	}
    42  }
    43  
    44  // RemoveRequests removes all data associated with requests
    45  // which have been processed by oracle contract.
    46  func (o *Oracle) RemoveRequests(ids []uint64) {
    47  	o.respMtx.Lock()
    48  	defer o.respMtx.Unlock()
    49  	if !o.running {
    50  		for _, id := range ids {
    51  			delete(o.pending, id)
    52  		}
    53  	} else {
    54  		for _, id := range ids {
    55  			delete(o.responses, id)
    56  		}
    57  	}
    58  }
    59  
    60  // AddRequests saves all requests in-fly for further processing.
    61  func (o *Oracle) AddRequests(reqs map[uint64]*state.OracleRequest) {
    62  	if len(reqs) == 0 {
    63  		return
    64  	}
    65  
    66  	o.respMtx.Lock()
    67  	if !o.running {
    68  		for id, r := range reqs {
    69  			o.pending[id] = r
    70  		}
    71  		o.respMtx.Unlock()
    72  		return
    73  	}
    74  	o.respMtx.Unlock()
    75  
    76  	select {
    77  	case o.requestMap <- reqs:
    78  	default:
    79  		select {
    80  		case old := <-o.requestMap:
    81  			for id, r := range old {
    82  				reqs[id] = r
    83  			}
    84  		default:
    85  		}
    86  		o.requestMap <- reqs
    87  	}
    88  }
    89  
    90  // ProcessRequestsInternal processes the provided requests synchronously.
    91  func (o *Oracle) ProcessRequestsInternal(reqs map[uint64]*state.OracleRequest) {
    92  	acc := o.getAccount()
    93  	if acc == nil {
    94  		return
    95  	}
    96  
    97  	// Process actual requests.
    98  	for id, req := range reqs {
    99  		if err := o.processRequest(acc.PrivateKey(), request{ID: id, Req: req}); err != nil {
   100  			o.Log.Debug("can't process request", zap.Error(err))
   101  		}
   102  	}
   103  }
   104  
   105  func (o *Oracle) processRequest(priv *keys.PrivateKey, req request) error {
   106  	if req.Req == nil {
   107  		o.processFailedRequest(priv, req)
   108  		return nil
   109  	}
   110  
   111  	incTx := o.getResponse(req.ID, true)
   112  	if incTx == nil {
   113  		return nil
   114  	}
   115  	resp := &transaction.OracleResponse{ID: req.ID, Code: transaction.Success}
   116  	u, err := url.ParseRequestURI(req.Req.URL)
   117  	if err != nil {
   118  		o.Log.Warn("malformed oracle request", zap.String("url", req.Req.URL), zap.Error(err))
   119  		resp.Code = transaction.ProtocolNotSupported
   120  	} else {
   121  		switch u.Scheme {
   122  		case "https":
   123  			httpReq, err := http.NewRequest("GET", req.Req.URL, nil)
   124  			if err != nil {
   125  				o.Log.Warn("failed to create http request", zap.String("url", req.Req.URL), zap.Error(err))
   126  				resp.Code = transaction.Error
   127  				break
   128  			}
   129  			httpReq.Header.Set("User-Agent", "NeoOracleService/3.0")
   130  			httpReq.Header.Set("Content-Type", "application/json")
   131  			r, err := o.Client.Do(httpReq)
   132  			if err != nil {
   133  				if errors.Is(err, ErrRestrictedRedirect) {
   134  					resp.Code = transaction.Forbidden
   135  				} else {
   136  					resp.Code = transaction.Error
   137  				}
   138  				o.Log.Warn("oracle request failed", zap.String("url", req.Req.URL), zap.Error(err), zap.Stringer("code", resp.Code))
   139  				break
   140  			}
   141  			defer r.Body.Close()
   142  			switch r.StatusCode {
   143  			case http.StatusOK:
   144  				if !checkMediaType(r.Header.Get("Content-Type"), o.MainCfg.AllowedContentTypes) {
   145  					resp.Code = transaction.ContentTypeNotSupported
   146  					break
   147  				}
   148  
   149  				resp.Result, resp.Code = o.readResponse(r.Body, req.Req.URL)
   150  			case http.StatusForbidden:
   151  				resp.Code = transaction.Forbidden
   152  			case http.StatusNotFound:
   153  				resp.Code = transaction.NotFound
   154  			case http.StatusRequestTimeout:
   155  				resp.Code = transaction.Timeout
   156  			default:
   157  				resp.Code = transaction.Error
   158  			}
   159  		case neofs.URIScheme:
   160  			ctx, cancel := context.WithTimeout(context.Background(), o.MainCfg.NeoFS.Timeout)
   161  			defer cancel()
   162  			index := (int(req.ID) + incTx.attempts) % len(o.MainCfg.NeoFS.Nodes)
   163  			rc, err := neofs.Get(ctx, priv, u, o.MainCfg.NeoFS.Nodes[index])
   164  			if err != nil {
   165  				resp.Code = transaction.Error
   166  				o.Log.Warn("failed to perform oracle request", zap.String("url", req.Req.URL), zap.Error(err))
   167  				if rc != nil {
   168  					rc.Close() // intentionally skip the closing error, make it unified with Oracle `https` protocol.
   169  				}
   170  				break
   171  			}
   172  			resp.Result, resp.Code = o.readResponse(rc, req.Req.URL)
   173  			rc.Close() // intentionally skip the closing error, make it unified with Oracle `https` protocol.
   174  		default:
   175  			resp.Code = transaction.ProtocolNotSupported
   176  			o.Log.Warn("unknown oracle request scheme", zap.String("url", req.Req.URL))
   177  		}
   178  	}
   179  	if resp.Code == transaction.Success {
   180  		resp.Result, err = filterRequest(resp.Result, req.Req)
   181  		if err != nil {
   182  			o.Log.Warn("oracle filter failed", zap.Uint64("request", req.ID), zap.Error(err))
   183  			resp.Code = transaction.Error
   184  		}
   185  	}
   186  	o.Log.Debug("oracle request processed", zap.String("url", req.Req.URL), zap.Int("code", int(resp.Code)), zap.String("result", string(resp.Result)))
   187  
   188  	currentHeight := o.Chain.BlockHeight()
   189  	vubInc := o.Chain.GetConfig().MaxValidUntilBlockIncrement
   190  	_, h, err := o.Chain.GetTransaction(req.Req.OriginalTxID)
   191  	if err != nil {
   192  		if !errors.Is(err, storage.ErrKeyNotFound) {
   193  			return err
   194  		}
   195  		// The only reason tx can be not found is that it hasn't been persisted from DAO yet.
   196  		h = currentHeight
   197  	}
   198  	h += vubInc // Main tx is only valid for RequestHeight + ValidUntilBlock.
   199  	tx, err := o.CreateResponseTx(int64(req.Req.GasForResponse), h, resp)
   200  	if err != nil {
   201  		return err
   202  	}
   203  	for h <= currentHeight { // Backup tx must be valid in any event.
   204  		h += vubInc
   205  	}
   206  	backupTx, err := o.CreateResponseTx(int64(req.Req.GasForResponse), h, &transaction.OracleResponse{
   207  		ID:   req.ID,
   208  		Code: transaction.ConsensusUnreachable,
   209  	})
   210  	if err != nil {
   211  		return err
   212  	}
   213  
   214  	incTx.Lock()
   215  	incTx.request = req.Req
   216  	incTx.tx = tx
   217  	incTx.backupTx = backupTx
   218  	incTx.reverifyTx(o.Network)
   219  
   220  	txSig := priv.SignHashable(uint32(o.Network), tx)
   221  	incTx.addResponse(priv.PublicKey(), txSig, false)
   222  
   223  	backupSig := priv.SignHashable(uint32(o.Network), backupTx)
   224  	incTx.addResponse(priv.PublicKey(), backupSig, true)
   225  
   226  	readyTx, ready := incTx.finalize(o.getOracleNodes(), false)
   227  	if ready {
   228  		ready = !incTx.isSent
   229  		incTx.isSent = true
   230  	}
   231  	incTx.time = time.Now()
   232  	incTx.attempts++
   233  	incTx.Unlock()
   234  
   235  	o.ResponseHandler.SendResponse(priv, resp, txSig)
   236  	if ready {
   237  		o.sendTx(readyTx)
   238  	}
   239  	return nil
   240  }
   241  
   242  func (o *Oracle) processFailedRequest(priv *keys.PrivateKey, req request) {
   243  	// Request is being processed again.
   244  	incTx := o.getResponse(req.ID, false)
   245  	if incTx == nil {
   246  		// Request was processed by other oracle nodes.
   247  		return
   248  	} else if incTx.isSent {
   249  		// Tx was sent but not yet persisted. Try to pool it again.
   250  		o.sendTx(incTx.tx)
   251  		return
   252  	}
   253  
   254  	// Don't process request again, fallback to backup tx.
   255  	incTx.Lock()
   256  	readyTx, ready := incTx.finalize(o.getOracleNodes(), true)
   257  	if ready {
   258  		ready = !incTx.isSent
   259  		incTx.isSent = true
   260  	}
   261  	incTx.time = time.Now()
   262  	incTx.attempts++
   263  	txSig := incTx.backupSigs[string(priv.PublicKey().Bytes())].sig
   264  	incTx.Unlock()
   265  
   266  	o.ResponseHandler.SendResponse(priv, getFailedResponse(req.ID), txSig)
   267  	if ready {
   268  		o.sendTx(readyTx)
   269  	}
   270  }
   271  
   272  func checkMediaType(hdr string, allowed []string) bool {
   273  	if len(allowed) == 0 {
   274  		return true
   275  	}
   276  
   277  	typ, _, err := mime.ParseMediaType(hdr)
   278  	if err != nil {
   279  		return false
   280  	}
   281  
   282  	for _, ct := range allowed {
   283  		if ct == typ {
   284  			return true
   285  		}
   286  	}
   287  	return false
   288  }