neo-go/pkg/services/oracle/oracle.go

294 lines
7.8 KiB
Go
Raw Normal View History

2020-09-25 14:39:11 +00:00
package oracle
import (
"bytes"
"errors"
2020-09-25 14:39:11 +00:00
"net/http"
"sync"
"time"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/state"
2020-09-25 14:39:11 +00:00
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/services/oracle/broadcaster"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
2020-09-25 14:39:11 +00:00
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"go.uber.org/zap"
)
type (
// Ledger is an interface to Blockchain sufficient for Oracle.
Ledger interface {
BlockHeight() uint32
FeePerByte() int64
GetBaseExecFee() int64
GetConfig() config.Blockchain
GetMaxVerificationGAS() int64
GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error)
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
}
// Oracle represents an oracle module capable of talking
2020-09-25 14:39:11 +00:00
// with the external world.
Oracle struct {
Config
// This fields are readonly thus not protected by mutex.
oracleHash util.Uint160
oracleResponse []byte
oracleScript []byte
verifyOffset int
2020-09-25 14:39:11 +00:00
// accMtx protects account and oracle nodes.
accMtx sync.RWMutex
currAccount *wallet.Account
oracleNodes keys.PublicKeys
oracleSignContract []byte
close chan struct{}
done chan struct{}
2020-10-07 07:48:41 +00:00
requestCh chan request
requestMap chan map[uint64]*state.OracleRequest
// respMtx protects responses and pending maps.
respMtx sync.RWMutex
// running is false until Run() is invoked.
running bool
// pending contains requests for not yet started service.
pending map[uint64]*state.OracleRequest
// responses contains active not completely processed requests.
2020-09-25 14:39:11 +00:00
responses map[uint64]*incompleteTx
2020-10-09 07:44:31 +00:00
// removed contains ids of requests which won't be processed further due to expiration.
removed map[uint64]bool
2020-09-25 14:39:11 +00:00
wallet *wallet.Wallet
}
// Config contains oracle module parameters.
Config struct {
Log *zap.Logger
Network netmode.Magic
MainCfg config.OracleConfiguration
2020-09-25 14:39:11 +00:00
Client HTTPClient
Chain Ledger
2020-09-25 14:39:11 +00:00
ResponseHandler Broadcaster
OnTransaction TxCallback
}
// HTTPClient is an interface capable of doing oracle requests.
HTTPClient interface {
Do(*http.Request) (*http.Response, error)
2020-09-25 14:39:11 +00:00
}
// Broadcaster broadcasts oracle responses.
Broadcaster interface {
SendResponse(priv *keys.PrivateKey, resp *transaction.OracleResponse, txSig []byte)
2020-10-08 11:50:10 +00:00
Run()
Shutdown()
2020-09-25 14:39:11 +00:00
}
// TxCallback executes on new transactions when they are ready to be pooled.
TxCallback = func(tx *transaction.Transaction) error
2020-09-25 14:39:11 +00:00
)
const (
// defaultRequestTimeout is the default request timeout.
2020-09-25 14:39:11 +00:00
defaultRequestTimeout = time.Second * 5
2020-10-09 07:44:31 +00:00
// defaultMaxTaskTimeout is the default timeout for the request to be dropped if it can't be processed.
2020-10-09 07:44:31 +00:00
defaultMaxTaskTimeout = time.Hour
// defaultRefreshInterval is the default timeout for the failed request to be reprocessed.
2020-10-09 07:44:31 +00:00
defaultRefreshInterval = time.Minute * 3
// maxRedirections is the number of allowed redirections for Oracle HTTPS request.
maxRedirections = 2
2020-09-25 14:39:11 +00:00
)
// ErrRestrictedRedirect is returned when redirection to forbidden address occurs
// during Oracle response creation.
var ErrRestrictedRedirect = errors.New("oracle request redirection error")
2020-09-25 14:39:11 +00:00
// NewOracle returns new oracle instance.
func NewOracle(cfg Config) (*Oracle, error) {
o := &Oracle{
Config: cfg,
close: make(chan struct{}),
done: make(chan struct{}),
requestMap: make(chan map[uint64]*state.OracleRequest, 1),
pending: make(map[uint64]*state.OracleRequest),
responses: make(map[uint64]*incompleteTx),
2020-10-09 07:44:31 +00:00
removed: make(map[uint64]bool),
}
if o.MainCfg.RequestTimeout == 0 {
o.MainCfg.RequestTimeout = defaultRequestTimeout
2020-09-25 14:39:11 +00:00
}
if o.MainCfg.NeoFS.Timeout == 0 {
o.MainCfg.NeoFS.Timeout = defaultRequestTimeout
}
2020-10-07 07:48:41 +00:00
if o.MainCfg.MaxConcurrentRequests == 0 {
o.MainCfg.MaxConcurrentRequests = defaultMaxConcurrentRequests
}
o.requestCh = make(chan request, o.MainCfg.MaxConcurrentRequests)
2020-10-09 07:44:31 +00:00
if o.MainCfg.MaxTaskTimeout == 0 {
o.MainCfg.MaxTaskTimeout = defaultMaxTaskTimeout
}
if o.MainCfg.RefreshInterval == 0 {
o.MainCfg.RefreshInterval = defaultRefreshInterval
}
2020-09-25 14:39:11 +00:00
var err error
w := cfg.MainCfg.UnlockWallet
2020-09-25 14:39:11 +00:00
if o.wallet, err = wallet.NewWalletFromFile(w.Path); err != nil {
return nil, err
}
haveAccount := false
for _, acc := range o.wallet.Accounts {
if err := acc.Decrypt(w.Password, o.wallet.Scrypt); err == nil {
haveAccount = true
break
}
}
if !haveAccount {
return nil, errors.New("no wallet account could be unlocked")
}
2020-09-25 14:39:11 +00:00
if o.ResponseHandler == nil {
o.ResponseHandler = broadcaster.New(cfg.MainCfg, cfg.Log)
2020-09-25 14:39:11 +00:00
}
if o.OnTransaction == nil {
o.OnTransaction = func(*transaction.Transaction) error { return nil }
2020-09-25 14:39:11 +00:00
}
if o.Client == nil {
o.Client = getDefaultClient(o.MainCfg)
2020-09-25 14:39:11 +00:00
}
return o, nil
}
// Name returns service name.
func (o *Oracle) Name() string {
return "oracle"
}
// Shutdown shutdowns Oracle. It can only be called once, subsequent calls
// to Shutdown on the same instance are no-op. The instance that was stopped can
// not be started again by calling Start (use a new instance if needed).
func (o *Oracle) Shutdown() {
2022-07-01 20:31:25 +00:00
o.respMtx.Lock()
defer o.respMtx.Unlock()
if !o.running {
return
}
o.Log.Info("stopping oracle service")
2022-07-01 20:31:25 +00:00
o.running = false
close(o.close)
o.ResponseHandler.Shutdown()
<-o.done
o.wallet.Close()
_ = o.Log.Sync()
}
// Start runs the oracle service in a separate goroutine.
// The Oracle only starts once, subsequent calls to Start are no-op.
func (o *Oracle) Start() {
o.respMtx.Lock()
if o.running {
o.respMtx.Unlock()
return
}
o.Log.Info("starting oracle service")
go o.start()
}
// IsAuthorized returns whether Oracle service currently is authorized to collect
// signatures. It returns true iff designated Oracle node's account provided to
// the Oracle service in decrypted state.
func (o *Oracle) IsAuthorized() bool {
return o.getAccount() != nil
}
func (o *Oracle) start() {
o.requestMap <- o.pending // Guaranteed to not block, only AddRequests sends to it.
o.pending = nil
o.running = true
o.respMtx.Unlock()
2020-10-07 07:48:41 +00:00
for i := 0; i < o.MainCfg.MaxConcurrentRequests; i++ {
go o.runRequestWorker()
}
go o.ResponseHandler.Run()
2020-10-09 07:44:31 +00:00
tick := time.NewTicker(o.MainCfg.RefreshInterval)
main:
for {
select {
case <-o.close:
break main
2020-10-09 07:44:31 +00:00
case <-tick.C:
var reprocess []uint64
services: fix Oracle responces mutex Solves the following problem: === RUN TestOracleFull logger.go:130: 2021-02-04T09:25:16.305Z INFO P2PNotaryRequestPayloadPool size is not set or wrong, setting default value {"P2PNotaryRequestPayloadPoolSize": 1000} logger.go:130: 2021-02-04T09:25:16.306Z INFO no storage version found! creating genesis block logger.go:130: 2021-02-04T09:25:27.687Z DEBUG done processing headers {"headerIndex": 1, "blockHeight": 0, "took": "2.413398ms"} logger.go:130: 2021-02-04T09:25:27.696Z DEBUG done processing headers {"headerIndex": 2, "blockHeight": 1, "took": "1.138196ms"} logger.go:130: 2021-02-04T09:25:28.680Z INFO blockchain persist completed {"persistedBlocks": 2, "persistedKeys": 173, "headerHeight": 2, "blockHeight": 2, "took": "166.793µs"} fatal error: sync: Unlock of unlocked RWMutex goroutine 6157 [running]: runtime.throw(0x115dfdb, 0x20) /usr/local/go/src/runtime/panic.go:1116 +0x72 fp=0xc000297ca0 sp=0xc000297c70 pc=0x44f432 sync.throw(0x115dfdb, 0x20) /usr/local/go/src/runtime/panic.go:1102 +0x35 fp=0xc000297cc0 sp=0xc000297ca0 pc=0x44f3b5 sync.(*RWMutex).Unlock(0xc000135300) /usr/local/go/src/sync/rwmutex.go:129 +0xf3 fp=0xc000297d00 sp=0xc000297cc0 pc=0x4a1ac3 github.com/nspcc-dev/neo-go/pkg/services/oracle.(*Oracle).Run(0xc000135180) /go/src/github.com/nspcc-dev/neo-go/pkg/services/oracle/oracle.go:189 +0x82b fp=0xc000297fd8 sp=0xc000297d00 pc=0xe13b0b runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:1373 +0x1 fp=0xc000297fe0 sp=0xc000297fd8 pc=0x4834d1 created by github.com/nspcc-dev/neo-go/pkg/core.TestOracleFull /go/src/github.com/nspcc-dev/neo-go/pkg/core/oracle_test.go:276 +0x3f1
2021-02-04 09:42:48 +00:00
o.respMtx.Lock()
2020-10-09 07:44:31 +00:00
o.removed = make(map[uint64]bool)
for id, incTx := range o.responses {
incTx.RLock()
since := time.Since(incTx.time)
if since > o.MainCfg.MaxTaskTimeout {
o.removed[id] = true
} else if since > o.MainCfg.RefreshInterval {
reprocess = append(reprocess, id)
}
incTx.RUnlock()
}
for id := range o.removed {
delete(o.responses, id)
}
o.respMtx.Unlock()
for _, id := range reprocess {
o.requestCh <- request{ID: id}
}
case reqs := <-o.requestMap:
2020-10-07 07:48:41 +00:00
for id, req := range reqs {
o.requestCh <- request{
ID: id,
Req: req,
}
}
}
}
tick.Stop()
drain:
for {
select {
case <-o.requestMap:
default:
break drain
}
}
close(o.requestMap)
close(o.done)
}
// UpdateNativeContract updates native oracle contract info for tx verification.
func (o *Oracle) UpdateNativeContract(script, resp []byte, h util.Uint160, verifyOffset int) {
o.oracleScript = bytes.Clone(script)
o.oracleResponse = bytes.Clone(resp)
o.oracleHash = h
o.verifyOffset = verifyOffset
}
func (o *Oracle) sendTx(tx *transaction.Transaction) {
if err := o.OnTransaction(tx); err != nil {
o.Log.Error("can't pool oracle tx",
zap.String("hash", tx.Hash().StringLE()),
zap.Error(err))
}
}