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

     1  package oracle
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	gio "io"
     7  	"unicode/utf8"
     8  
     9  	"github.com/nspcc-dev/neo-go/pkg/core/fee"
    10  	"github.com/nspcc-dev/neo-go/pkg/core/interop"
    11  	"github.com/nspcc-dev/neo-go/pkg/core/native/nativehashes"
    12  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    13  	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
    14  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    15  	"github.com/nspcc-dev/neo-go/pkg/io"
    16  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
    17  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
    18  	"go.uber.org/zap"
    19  )
    20  
    21  func (o *Oracle) getResponse(reqID uint64, create bool) *incompleteTx {
    22  	o.respMtx.Lock()
    23  	defer o.respMtx.Unlock()
    24  	incTx, ok := o.responses[reqID]
    25  	if !ok && create && !o.removed[reqID] {
    26  		incTx = newIncompleteTx()
    27  		o.responses[reqID] = incTx
    28  	}
    29  	return incTx
    30  }
    31  
    32  // AddResponse handles an oracle response (transaction signature for some identified request) signed by the given key.
    33  // sig is a response transaction signature.
    34  func (o *Oracle) AddResponse(pub *keys.PublicKey, reqID uint64, txSig []byte) {
    35  	incTx := o.getResponse(reqID, true)
    36  	if incTx == nil {
    37  		return
    38  	}
    39  
    40  	incTx.Lock()
    41  	isBackup := false
    42  	if incTx.tx != nil {
    43  		ok := pub.VerifyHashable(txSig, uint32(o.Network), incTx.tx)
    44  		if !ok {
    45  			ok = pub.VerifyHashable(txSig, uint32(o.Network), incTx.backupTx)
    46  			if !ok {
    47  				o.Log.Debug("invalid response signature",
    48  					zap.String("pub", pub.StringCompressed()))
    49  				incTx.Unlock()
    50  				return
    51  			}
    52  			isBackup = true
    53  		}
    54  	}
    55  	incTx.addResponse(pub, txSig, isBackup)
    56  	readyTx, ready := incTx.finalize(o.getOracleNodes(), false)
    57  	if ready {
    58  		ready = !incTx.isSent
    59  		incTx.isSent = true
    60  	}
    61  	incTx.Unlock()
    62  
    63  	if ready {
    64  		o.sendTx(readyTx)
    65  	}
    66  }
    67  
    68  // ErrResponseTooLarge is returned when a response exceeds the max allowed size.
    69  var ErrResponseTooLarge = errors.New("too big response")
    70  
    71  func (o *Oracle) readResponse(rc gio.Reader, url string) ([]byte, transaction.OracleResponseCode) {
    72  	const limit = transaction.MaxOracleResultSize
    73  	buf := make([]byte, limit+1)
    74  	n, err := gio.ReadFull(rc, buf)
    75  	if errors.Is(err, gio.ErrUnexpectedEOF) && n <= limit {
    76  		res, err := checkUTF8(buf[:n])
    77  		return o.handleResponseError(res, err, url)
    78  	}
    79  	if err == nil || n > limit {
    80  		return o.handleResponseError(nil, ErrResponseTooLarge, url)
    81  	}
    82  
    83  	return o.handleResponseError(nil, err, url)
    84  }
    85  
    86  func (o *Oracle) handleResponseError(data []byte, err error, url string) ([]byte, transaction.OracleResponseCode) {
    87  	if err != nil {
    88  		o.Log.Warn("failed to read data for oracle request", zap.String("url", url), zap.Error(err))
    89  		if errors.Is(err, ErrResponseTooLarge) {
    90  			return nil, transaction.ResponseTooLarge
    91  		}
    92  		return nil, transaction.Error
    93  	}
    94  	return data, transaction.Success
    95  }
    96  
    97  func checkUTF8(v []byte) ([]byte, error) {
    98  	if !utf8.Valid(v) {
    99  		return nil, errors.New("invalid UTF-8")
   100  	}
   101  	return v, nil
   102  }
   103  
   104  // CreateResponseTx creates an unsigned oracle response transaction.
   105  func (o *Oracle) CreateResponseTx(gasForResponse int64, vub uint32, resp *transaction.OracleResponse) (*transaction.Transaction, error) {
   106  	var respScript []byte
   107  	o.oracleInfoLock.RLock()
   108  	respScript = o.oracleResponse
   109  	o.oracleInfoLock.RUnlock()
   110  
   111  	tx := transaction.New(respScript, 0)
   112  	tx.Nonce = uint32(resp.ID)
   113  	tx.ValidUntilBlock = vub
   114  	tx.Attributes = []transaction.Attribute{{
   115  		Type:  transaction.OracleResponseT,
   116  		Value: resp,
   117  	}}
   118  
   119  	oracleSignContract := o.getOracleSignContract()
   120  	tx.Signers = []transaction.Signer{
   121  		{
   122  			Account: nativehashes.OracleContract,
   123  			Scopes:  transaction.None,
   124  		},
   125  		{
   126  			Account: hash.Hash160(oracleSignContract),
   127  			Scopes:  transaction.None,
   128  		},
   129  	}
   130  	tx.Scripts = []transaction.Witness{
   131  		{}, // native contract witness is fixed, second witness is set later.
   132  	}
   133  
   134  	// Calculate network fee.
   135  	size := io.GetVarSize(tx)
   136  	tx.Scripts = append(tx.Scripts, transaction.Witness{VerificationScript: oracleSignContract})
   137  
   138  	gasConsumed, ok, err := o.testVerify(tx)
   139  	if err != nil {
   140  		return nil, fmt.Errorf("failed to prepare `verify` invocation: %w", err)
   141  	}
   142  	if !ok {
   143  		return nil, errors.New("can't verify transaction")
   144  	}
   145  	tx.NetworkFee += gasConsumed
   146  
   147  	netFee, sizeDelta := fee.Calculate(o.Chain.GetBaseExecFee(), tx.Scripts[1].VerificationScript)
   148  	tx.NetworkFee += netFee
   149  	size += sizeDelta
   150  
   151  	currNetFee := tx.NetworkFee + int64(size)*o.Chain.FeePerByte()
   152  	if currNetFee > gasForResponse {
   153  		attrSize := io.GetVarSize(tx.Attributes)
   154  		resp.Code = transaction.InsufficientFunds
   155  		resp.Result = nil
   156  		size = size - attrSize + io.GetVarSize(tx.Attributes)
   157  	}
   158  	tx.NetworkFee += int64(size) * o.Chain.FeePerByte() // 233
   159  
   160  	// Calculate system fee.
   161  	tx.SystemFee = gasForResponse - tx.NetworkFee
   162  	return tx, nil
   163  }
   164  
   165  func (o *Oracle) testVerify(tx *transaction.Transaction) (int64, bool, error) {
   166  	// (*Blockchain).GetTestVM calls Hash() method of the provided transaction; once being called, this
   167  	// method caches transaction hash, but tx building is not yet completed and hash will be changed.
   168  	// So, make a copy of the tx to avoid wrong hash caching.
   169  	cp := *tx
   170  	ic, err := o.Chain.GetTestVM(trigger.Verification, &cp, nil)
   171  	if err != nil {
   172  		return 0, false, fmt.Errorf("failed to create test VM: %w", err)
   173  	}
   174  	ic.VM.GasLimit = o.Chain.GetMaxVerificationGAS()
   175  
   176  	o.oracleInfoLock.RLock()
   177  	ic.VM.LoadScriptWithHash(o.oracleScript, nativehashes.OracleContract, callflag.ReadOnly)
   178  	ic.VM.Context().Jump(o.verifyOffset)
   179  	o.oracleInfoLock.RUnlock()
   180  
   181  	ok := isVerifyOk(ic)
   182  	return ic.VM.GasConsumed(), ok, nil
   183  }
   184  
   185  func isVerifyOk(ic *interop.Context) bool {
   186  	defer ic.Finalize()
   187  	if err := ic.VM.Run(); err != nil {
   188  		return false
   189  	}
   190  	if ic.VM.Estack().Len() != 1 {
   191  		return false
   192  	}
   193  	ok, err := ic.VM.Estack().Pop().Item().TryBool()
   194  	return err == nil && ok
   195  }
   196  
   197  func getFailedResponse(id uint64) *transaction.OracleResponse {
   198  	return &transaction.OracleResponse{
   199  		ID:   id,
   200  		Code: transaction.Error,
   201  	}
   202  }