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 }