diff --git a/api/mock/facade.go b/api/mock/facade.go index a511f910..037085a4 100644 --- a/api/mock/facade.go +++ b/api/mock/facade.go @@ -9,7 +9,7 @@ import ( // Facade is the mock implementation of a node router handler type Facade struct { GetAccountHandler func(address string) (*data.Account, error) - SendTransactionHandler func(nonce uint64, sender string, receiver string, value *big.Int, code string, signature []byte) (*data.Transaction, error) + SendTransactionHandler func(nonce uint64, sender string, receiver string, value *big.Int, code string, signature []byte) (string, error) } // GetAccount is the mock implementation of a handler's GetAccount method @@ -18,7 +18,7 @@ func (f *Facade) GetAccount(address string) (*data.Account, error) { } // SendTransaction is the mock implementation of a handler's SendTransaction method -func (f *Facade) SendTransaction(nonce uint64, sender string, receiver string, value *big.Int, code string, signature []byte) (*data.Transaction, error) { +func (f *Facade) SendTransaction(nonce uint64, sender string, receiver string, value *big.Int, code string, signature []byte) (string, error) { return f.SendTransactionHandler(nonce, sender, receiver, value, code, signature) } diff --git a/api/transaction/interface.go b/api/transaction/interface.go index a86cd510..475852e9 100644 --- a/api/transaction/interface.go +++ b/api/transaction/interface.go @@ -2,11 +2,9 @@ package transaction import ( "math/big" - - "github.com/ElrondNetwork/elrond-proxy-go/data" ) // FacadeHandler interface defines methods that can be used from `elrondProxyFacade` context variable type FacadeHandler interface { - SendTransaction(nonce uint64, sender string, receiver string, value *big.Int, code string, signature []byte) (*data.Transaction, error) + SendTransaction(nonce uint64, sender string, receiver string, value *big.Int, code string, signature []byte) (string, error) } diff --git a/api/transaction/routes.go b/api/transaction/routes.go index 642fd0ff..707a1e30 100644 --- a/api/transaction/routes.go +++ b/api/transaction/routes.go @@ -36,11 +36,11 @@ func SendTransaction(c *gin.Context) { return } - tx, err := ef.SendTransaction(gtx.Nonce, gtx.Sender, gtx.Receiver, gtx.Value, gtx.Data, signature) + txHash, err := ef.SendTransaction(gtx.Nonce, gtx.Sender, gtx.Receiver, gtx.Value, gtx.Data, signature) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("%s: %s", errors.ErrTxGenerationFailed.Error(), err.Error())}) return } - c.JSON(http.StatusOK, gin.H{"transaction": tx}) + c.JSON(http.StatusOK, gin.H{"txHash": txHash}) } diff --git a/api/transaction/routes_test.go b/api/transaction/routes_test.go index ab2f63cb..3000cc00 100644 --- a/api/transaction/routes_test.go +++ b/api/transaction/routes_test.go @@ -15,7 +15,6 @@ import ( apiErrors "github.com/ElrondNetwork/elrond-proxy-go/api/errors" "github.com/ElrondNetwork/elrond-proxy-go/api/mock" "github.com/ElrondNetwork/elrond-proxy-go/api/transaction" - "github.com/ElrondNetwork/elrond-proxy-go/data" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" @@ -26,6 +25,12 @@ type GeneralResponse struct { Error string `json:"error"` } +// TxHashResponse structure +type TxHashResponse struct { + Error string `json:"error"` + TxHash string `json:"txHash"` +} + func startNodeServerWrongFacade() *gin.Engine { ws := gin.New() ws.Use(cors.Default()) @@ -147,8 +152,8 @@ func TestSendTransaction_ErrorWhenFacadeSendTransactionError(t *testing.T) { facade := mock.Facade{ SendTransactionHandler: func(nonce uint64, sender string, receiver string, value *big.Int, - code string, signature []byte) (transaction *data.Transaction, e error) { - return nil, errors.New(errorString) + code string, signature []byte) (string, error) { + return "", errors.New(errorString) }, } ws := startNodeServer(&facade) @@ -181,18 +186,12 @@ func TestSendTransaction_ReturnsSuccessfully(t *testing.T) { value := big.NewInt(10) dataField := "data" signature := "aabbccdd" + txHash := "tx hash" facade := mock.Facade{ SendTransactionHandler: func(nonce uint64, sender string, receiver string, value *big.Int, - code string, signature []byte) (transaction *data.Transaction, e error) { - return &data.Transaction{ - Nonce: nonce, - Sender: sender, - Receiver: receiver, - Value: value, - Data: code, - Signature: string(signature), - }, nil + code string, signature []byte) (string, error) { + return txHash, nil }, } ws := startNodeServer(&facade) @@ -211,9 +210,10 @@ func TestSendTransaction_ReturnsSuccessfully(t *testing.T) { resp := httptest.NewRecorder() ws.ServeHTTP(resp, req) - response := GeneralResponse{} + response := TxHashResponse{} loadResponse(resp.Body, &response) assert.Equal(t, http.StatusOK, resp.Code) assert.Empty(t, response.Error) + assert.Equal(t, txHash, response.TxHash) } diff --git a/cmd/proxy/config/config.toml b/cmd/proxy/config/config.toml new file mode 100644 index 00000000..856ecf87 --- /dev/null +++ b/cmd/proxy/config/config.toml @@ -0,0 +1,12 @@ +# GeneralSettings section of the proxy server +[GeneralSettings] + # ServerPort is the port used for the web server. The frontend will connect to this port + ServerPort = 8079 + +[[Observers]] + ShardId = 0 + Address = "127.0.0.1:8080" + +[[Observers]] + ShardId = 1 + Address = "127.0.0.1:8081" diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index c16c08ba..247eb329 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -1,14 +1,20 @@ package main import ( + "fmt" "os" "os/signal" "syscall" + "github.com/ElrondNetwork/elrond-go-sandbox/core" "github.com/ElrondNetwork/elrond-go-sandbox/core/logger" + "github.com/ElrondNetwork/elrond-go-sandbox/data/state/addressConverters" "github.com/ElrondNetwork/elrond-proxy-go/api" + "github.com/ElrondNetwork/elrond-proxy-go/config" + "github.com/ElrondNetwork/elrond-proxy-go/data" "github.com/ElrondNetwork/elrond-proxy-go/facade" "github.com/ElrondNetwork/elrond-proxy-go/process" + "github.com/ElrondNetwork/elrond-proxy-go/testing" "github.com/pkg/profile" "github.com/urfave/cli" ) @@ -44,6 +50,13 @@ VERSION: Usage: "The main configuration file to load", Value: "./config/config.toml", } + // testHttpServerEn used to enable a test (mock) http server that will handle all requests + testHttpServerEn = cli.BoolFlag{ + Name: "test-http-server-enable", + Usage: "Enables a test http server that will handle all requests", + } + + testServer *testing.TestHttpServer ) func main() { @@ -53,9 +66,13 @@ func main() { app := cli.NewApp() cli.AppHelpTemplate = proxyHelpTemplate app.Name = "Elrond Node Proxy CLI App" - app.Version = "v0.0.1" + app.Version = "v1.0.0" app.Usage = "This is the entry point for starting a new Elrond node proxy" - app.Flags = []cli.Flag{configurationFile, profileMode} + app.Flags = []cli.Flag{ + configurationFile, + profileMode, + testHttpServerEn, + } app.Authors = []cli.Author{ { Name: "The Elrond Team", @@ -67,6 +84,12 @@ func main() { return startProxy(c) } + defer func() { + if testServer != nil { + testServer.Close() + } + }() + err := app.Run(os.Args) if err != nil { log.Error(err.Error()) @@ -93,19 +116,23 @@ func startProxy(ctx *cli.Context) error { log.Info("Starting proxy...") + configurationFileName := ctx.GlobalString(configurationFile.Name) + generalConfig, err := loadMainConfig(configurationFileName, log) + if err != nil { + return err + } + log.Info(fmt.Sprintf("Initialized with config from: %s", configurationFileName)) + stop := make(chan bool, 1) sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - epf, err := createElrondProxyFacade() + epf, err := createElrondProxyFacade(ctx, generalConfig) if err != nil { return err } - err = startWebServer(epf, 8079) - if err != nil { - return err - } + startWebServer(epf, generalConfig.GeneralSettings.ServerPort) go func() { <-sigs @@ -119,12 +146,77 @@ func startProxy(ctx *cli.Context) error { return nil } -func createElrondProxyFacade() (*facade.ElrondProxyFacade, error) { - accntProc := &process.GetAccountProcessor{} +func loadMainConfig(filepath string, log *logger.Logger) (*config.Config, error) { + cfg := &config.Config{} + err := core.LoadTomlFile(cfg, filepath, log) + if err != nil { + return nil, err + } + return cfg, nil +} + +func createElrondProxyFacade( + ctx *cli.Context, + cfg *config.Config, +) (*facade.ElrondProxyFacade, error) { + + var testHttpServerEnabled bool + if ctx.IsSet(testHttpServerEn.Name) { + testHttpServerEnabled = ctx.GlobalBool(testHttpServerEn.Name) + } + + if testHttpServerEnabled { + log.Info("Starting test HTTP server handling the requests...") + testServer = testing.NewTestHttpServer() + log.Info("Test HTTP server running at " + testServer.URL()) + + testCfg := &config.Config{ + Observers: []*data.Observer{ + { + ShardId: 0, + Address: testServer.URL(), + }, + }, + } + + return createFacade(testCfg) + } - return facade.NewElrondProxyFacade(accntProc, nil) + return createFacade(cfg) } -func startWebServer(proxyHandler api.ElrondProxyHandler, port int) error { - return api.Start(proxyHandler, port) +func createFacade(cfg *config.Config) (*facade.ElrondProxyFacade, error) { + addrConv, err := addressConverters.NewPlainAddressConverter(32, "") + if err != nil { + return nil, err + } + + bp, err := process.NewBaseProcessor(addrConv) + if err != nil { + return nil, err + } + + err = bp.ApplyConfig(cfg) + if err != nil { + return nil, err + } + + accntProc, err := process.NewAccountProcessor(bp) + if err != nil { + return nil, err + } + + txProc, err := process.NewTransactionProcessor(bp) + if err != nil { + return nil, err + } + + return facade.NewElrondProxyFacade(accntProc, txProc) +} + +func startWebServer(proxyHandler api.ElrondProxyHandler, port int) { + go func() { + err := api.Start(proxyHandler, port) + log.LogIfError(err) + }() } diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..a4f087a8 --- /dev/null +++ b/config/config.go @@ -0,0 +1,15 @@ +package config + +import "github.com/ElrondNetwork/elrond-proxy-go/data" + +// GeneralSettingsConfig will hold the general settings for a node +type GeneralSettingsConfig struct { + ServerPort int + CfgFileReadInterval int +} + +// Config will hold the whole config file's data +type Config struct { + GeneralSettings GeneralSettingsConfig + Observers []*data.Observer +} diff --git a/data/account.go b/data/account.go index b8543409..6dec04fd 100644 --- a/data/account.go +++ b/data/account.go @@ -8,3 +8,8 @@ type Account struct { CodeHash []byte `json:"codeHash"` RootHash []byte `json:"rootHash"` } + +// ResponseAccount defines a wrapped account that the node respond with +type ResponseAccount struct { + AccountData Account `json:"account"` +} diff --git a/data/observer.go b/data/observer.go new file mode 100644 index 00000000..6fe3d3dc --- /dev/null +++ b/data/observer.go @@ -0,0 +1,7 @@ +package data + +// Observer holds an observer data +type Observer struct { + ShardId uint32 + Address string +} diff --git a/data/transaction.go b/data/transaction.go index adad9af2..2b8f1b20 100644 --- a/data/transaction.go +++ b/data/transaction.go @@ -4,13 +4,18 @@ import "math/big" // Transaction represents the structure that maps and validates user input for publishing a new transaction type Transaction struct { - Sender string `form:"sender" json:"sender"` - Receiver string `form:"receiver" json:"receiver"` - Value *big.Int `form:"value" json:"value"` - Data string `form:"data" json:"data"` Nonce uint64 `form:"nonce" json:"nonce"` - GasPrice *big.Int `form:"gasPrice" json:"gasPrice"` - GasLimit *big.Int `form:"gasLimit" json:"gasLimit"` - Signature string `form:"signature" json:"signature"` - Challenge string `form:"challenge" json:"challenge"` + Value *big.Int `form:"value" json:"value"` + Receiver string `form:"receiver" json:"receiver"` + Sender string `form:"sender" json:"sender"` + GasPrice *big.Int `form:"gasPrice" json:"gasPrice,omitempty"` + GasLimit *big.Int `form:"gasLimit" json:"gasLimit,omitempty"` + Data string `form:"data" json:"data,omitempty"` + Signature string `form:"signature" json:"signature,omitempty"` + Challenge string `form:"challenge" json:"challenge,omitempty"` +} + +// ResponseTransaction defines a response tx holding the resulting hash +type ResponseTransaction struct { + TxHash string `json:"txHash"` } diff --git a/facade/elrondProxyFacade.go b/facade/elrondProxyFacade.go index 9fa06721..29b7307f 100644 --- a/facade/elrondProxyFacade.go +++ b/facade/elrondProxyFacade.go @@ -1,6 +1,10 @@ package facade -import "github.com/ElrondNetwork/elrond-proxy-go/data" +import ( + "math/big" + + "github.com/ElrondNetwork/elrond-proxy-go/data" +) // ElrondProxyFacade implements the facade used in api calls type ElrondProxyFacade struct { @@ -15,9 +19,11 @@ func NewElrondProxyFacade( ) (*ElrondProxyFacade, error) { if accountProc == nil { - return nil, ErrNilAccountProccessor + return nil, ErrNilAccountProcessor + } + if txProc == nil { + return nil, ErrNilTransactionProcessor } - //TODO check txProc when implemented return &ElrondProxyFacade{ accountProc: accountProc, @@ -29,3 +35,16 @@ func NewElrondProxyFacade( func (epf *ElrondProxyFacade) GetAccount(address string) (*data.Account, error) { return epf.accountProc.GetAccount(address) } + +// SendTransaction should sends the transaction to the correct observer +func (epf *ElrondProxyFacade) SendTransaction( + nonce uint64, + sender string, + receiver string, + value *big.Int, + code string, + signature []byte, +) (string, error) { + + return epf.txProc.SendTransaction(nonce, sender, receiver, value, code, signature) +} diff --git a/facade/errors.go b/facade/errors.go index 628cd822..ca65c08c 100644 --- a/facade/errors.go +++ b/facade/errors.go @@ -2,5 +2,8 @@ package facade import "github.com/pkg/errors" -// ErrNilAccountProccessor signals that a nil account processor has been provided -var ErrNilAccountProccessor = errors.New("nil account processor provided") +// ErrNilAccountProcessor signals that a nil account processor has been provided +var ErrNilAccountProcessor = errors.New("nil account processor provided") + +// ErrNilTransactionProcessor signals that a nil transaction processor has been provided +var ErrNilTransactionProcessor = errors.New("nil transaction processor provided") diff --git a/facade/interface.go b/facade/interface.go index 3b8254f6..958e6359 100644 --- a/facade/interface.go +++ b/facade/interface.go @@ -1,6 +1,10 @@ package facade -import "github.com/ElrondNetwork/elrond-proxy-go/data" +import ( + "math/big" + + "github.com/ElrondNetwork/elrond-proxy-go/data" +) // AccountProcessor defines what an account request processor should do type AccountProcessor interface { @@ -9,4 +13,5 @@ type AccountProcessor interface { // TransactionProcessor defines what a transaction request processor should do type TransactionProcessor interface { + SendTransaction(nonce uint64, sender string, receiver string, value *big.Int, code string, signature []byte) (string, error) } diff --git a/process/accountProcessor.go b/process/accountProcessor.go new file mode 100644 index 00000000..109e9357 --- /dev/null +++ b/process/accountProcessor.go @@ -0,0 +1,59 @@ +package process + +import ( + "encoding/hex" + "fmt" + + "github.com/ElrondNetwork/elrond-proxy-go/data" +) + +// AddressPath defines the address path at which the nodes answer +const AddressPath = "/address/" + +// AccountProcessor is able to process account requests +type AccountProcessor struct { + proc Processor +} + +// NewAccountProcessor creates a new instance of AccountProcessor +func NewAccountProcessor(proc Processor) (*AccountProcessor, error) { + if proc == nil { + return nil, ErrNilCoreProcessor + } + + return &AccountProcessor{ + proc: proc, + }, nil +} + +// GetAccount resolves the request by sending the request to the right observer and replies back the answer +func (ap *AccountProcessor) GetAccount(address string) (*data.Account, error) { + addressBytes, err := hex.DecodeString(address) + if err != nil { + return nil, err + } + + shardId, err := ap.proc.ComputeShardId(addressBytes) + if err != nil { + return nil, err + } + + observers, err := ap.proc.GetObservers(shardId) + if err != nil { + return nil, err + } + + for _, observer := range observers { + responseAccount := &data.ResponseAccount{} + + err = ap.proc.CallGetRestEndPoint(observer.Address, AddressPath+address, responseAccount) + if err == nil { + log.Info(fmt.Sprintf("Got account request from observer %v from shard %v", observer.Address, shardId)) + return &responseAccount.AccountData, nil + } + + log.LogIfError(err) + } + + return nil, ErrSendingRequest +} diff --git a/process/accountProcessor_test.go b/process/accountProcessor_test.go new file mode 100644 index 00000000..014e2f3a --- /dev/null +++ b/process/accountProcessor_test.go @@ -0,0 +1,139 @@ +package process_test + +import ( + "errors" + "testing" + + "github.com/ElrondNetwork/elrond-proxy-go/data" + "github.com/ElrondNetwork/elrond-proxy-go/process" + "github.com/ElrondNetwork/elrond-proxy-go/process/mock" + "github.com/stretchr/testify/assert" +) + +func TestNewAccountProcessor_NilCoreProcessorShouldErr(t *testing.T) { + t.Parallel() + + ap, err := process.NewAccountProcessor(nil) + + assert.Nil(t, ap) + assert.Equal(t, process.ErrNilCoreProcessor, err) +} + +func TestNewAccountProcessor_WithCoreProcessorShouldWork(t *testing.T) { + t.Parallel() + + ap, err := process.NewAccountProcessor(&mock.ProcessorStub{}) + + assert.NotNil(t, ap) + assert.Nil(t, err) +} + +//------- GetAccount + +func TestAccountProcessor_GetAccountInvalidHexAdressShouldErr(t *testing.T) { + t.Parallel() + + ap, _ := process.NewAccountProcessor(&mock.ProcessorStub{}) + accnt, err := ap.GetAccount("invalid hex number") + + assert.Nil(t, accnt) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid byte") +} + +func TestAccountProcessor_GetAccountComputeShardIdFailsShouldErr(t *testing.T) { + t.Parallel() + + errExpected := errors.New("expected error") + ap, _ := process.NewAccountProcessor(&mock.ProcessorStub{ + ComputeShardIdCalled: func(addressBuff []byte) (u uint32, e error) { + return 0, errExpected + }, + }) + address := "DEADBEEF" + accnt, err := ap.GetAccount(address) + + assert.Nil(t, accnt) + assert.Equal(t, errExpected, err) +} + +func TestAccountProcessor_GetAccountGetObserversFailsShouldErr(t *testing.T) { + t.Parallel() + + errExpected := errors.New("expected error") + ap, _ := process.NewAccountProcessor(&mock.ProcessorStub{ + ComputeShardIdCalled: func(addressBuff []byte) (u uint32, e error) { + return 0, nil + }, + GetObserversCalled: func(shardId uint32) (observers []*data.Observer, e error) { + return nil, errExpected + }, + }) + address := "DEADBEEF" + accnt, err := ap.GetAccount(address) + + assert.Nil(t, accnt) + assert.Equal(t, errExpected, err) +} + +func TestAccountProcessor_GetAccountSendingFailsOnAllObserversShouldErr(t *testing.T) { + t.Parallel() + + errExpected := errors.New("expected error") + ap, _ := process.NewAccountProcessor(&mock.ProcessorStub{ + ComputeShardIdCalled: func(addressBuff []byte) (u uint32, e error) { + return 0, nil + }, + GetObserversCalled: func(shardId uint32) (observers []*data.Observer, e error) { + return []*data.Observer{ + {Address: "adress1", ShardId: 0}, + {Address: "adress2", ShardId: 0}, + }, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) error { + return errExpected + }, + }) + address := "DEADBEEF" + accnt, err := ap.GetAccount(address) + + assert.Nil(t, accnt) + assert.Equal(t, process.ErrSendingRequest, err) +} + +func TestAccountProcessor_GetAccountSendingFailsOnFirstObserverShouldStillSend(t *testing.T) { + t.Parallel() + + addressFail := "address1" + errExpected := errors.New("expected error") + respondedAccount := &data.ResponseAccount{ + AccountData: data.Account{ + Address: "an address", + }, + } + ap, _ := process.NewAccountProcessor(&mock.ProcessorStub{ + ComputeShardIdCalled: func(addressBuff []byte) (u uint32, e error) { + return 0, nil + }, + GetObserversCalled: func(shardId uint32) (observers []*data.Observer, e error) { + return []*data.Observer{ + {Address: addressFail, ShardId: 0}, + {Address: "adress2", ShardId: 0}, + }, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) error { + if address == addressFail { + return errExpected + } + + valRespond := value.(*data.ResponseAccount) + valRespond.AccountData = respondedAccount.AccountData + return nil + }, + }) + address := "DEADBEEF" + accnt, err := ap.GetAccount(address) + + assert.Equal(t, &respondedAccount.AccountData, accnt) + assert.Nil(t, err) +} diff --git a/process/baseProcessor.go b/process/baseProcessor.go new file mode 100644 index 00000000..151cab06 --- /dev/null +++ b/process/baseProcessor.go @@ -0,0 +1,165 @@ +package process + +import ( + "bytes" + "net/http" + "sync" + + "github.com/ElrondNetwork/elrond-go-sandbox/core/logger" + "github.com/ElrondNetwork/elrond-go-sandbox/data/state" + "github.com/ElrondNetwork/elrond-go-sandbox/sharding" + "github.com/ElrondNetwork/elrond-proxy-go/config" + "github.com/ElrondNetwork/elrond-proxy-go/data" + "github.com/gin-gonic/gin/json" +) + +var log = logger.DefaultLogger() + +// BaseProcessor represents an implementation of CoreProcessor that helps +// processing requests +type BaseProcessor struct { + addressConverter state.AddressConverter + lastConfig *config.Config + mutState sync.RWMutex + shardCoordinator sharding.Coordinator + observers map[uint32][]*data.Observer + + httpClient *http.Client +} + +// NewBaseProcessor creates a new instance of BaseProcessor struct +func NewBaseProcessor(addressConverter state.AddressConverter) (*BaseProcessor, error) { + if addressConverter == nil { + return nil, ErrNilAddressConverter + } + + return &BaseProcessor{ + observers: make(map[uint32][]*data.Observer), + httpClient: http.DefaultClient, + addressConverter: addressConverter, + }, nil +} + +// ApplyConfig applies a config on a base processor +func (bp *BaseProcessor) ApplyConfig(cfg *config.Config) error { + if cfg == nil { + return ErrNilConfig + } + if len(cfg.Observers) == 0 { + return ErrEmptyObserversList + } + + newObservers := make(map[uint32][]*data.Observer) + maxShardId := uint32(0) + for _, observer := range cfg.Observers { + shardId := observer.ShardId + if maxShardId < shardId { + maxShardId = shardId + } + + newObservers[shardId] = append(newObservers[shardId], observer) + } + + newShardCoordinator, err := sharding.NewMultiShardCoordinator(maxShardId+1, 0) + if err != nil { + return err + } + + bp.mutState.Lock() + bp.shardCoordinator = newShardCoordinator + bp.observers = newObservers + bp.mutState.Unlock() + + return nil +} + +// GetObservers returns the registered observers on a shard +func (bp *BaseProcessor) GetObservers(shardId uint32) ([]*data.Observer, error) { + bp.mutState.RLock() + defer bp.mutState.RUnlock() + + observers := bp.observers[shardId] + if len(observers) == 0 { + return nil, ErrMissingObserver + } + + return observers, nil +} + +// ComputeShardId computes the shard id in which the account resides +func (bp *BaseProcessor) ComputeShardId(addressBuff []byte) (uint32, error) { + bp.mutState.RLock() + defer bp.mutState.RUnlock() + + address, err := bp.addressConverter.CreateAddressFromPublicKeyBytes(addressBuff) + if err != nil { + return 0, err + } + + return bp.shardCoordinator.ComputeId(address), nil +} + +// CallGetRestEndPoint calls an external end point (sends a request on a node) +func (bp *BaseProcessor) CallGetRestEndPoint( + address string, + path string, + value interface{}, +) error { + + req, err := http.NewRequest("GET", address+path, nil) + if err != nil { + return err + } + + userAgent := "Elrond Proxy / 1.0.0 " + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", userAgent) + + resp, err := bp.httpClient.Do(req) + if err != nil { + return err + } + + defer func() { + errNotCritical := resp.Body.Close() + log.LogIfError(errNotCritical) + }() + + return json.NewDecoder(resp.Body).Decode(value) +} + +// CallPostRestEndPoint calls an external end point (sends a request on a node) +func (bp *BaseProcessor) CallPostRestEndPoint( + address string, + path string, + data interface{}, + response interface{}, +) error { + + buff, err := json.Marshal(data) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", address+path, bytes.NewReader(buff)) + if err != nil { + return err + } + + userAgent := "Elrond Proxy / 1.0.0 " + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", userAgent) + + resp, err := bp.httpClient.Do(req) + if err != nil { + return err + } + + defer func() { + errNotCritical := resp.Body.Close() + log.LogIfError(errNotCritical) + }() + + return json.NewDecoder(resp.Body).Decode(response) +} diff --git a/process/baseProcessor_test.go b/process/baseProcessor_test.go new file mode 100644 index 00000000..3879d2a4 --- /dev/null +++ b/process/baseProcessor_test.go @@ -0,0 +1,205 @@ +package process_test + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ElrondNetwork/elrond-go-sandbox/data/state" + "github.com/ElrondNetwork/elrond-proxy-go/config" + "github.com/ElrondNetwork/elrond-proxy-go/data" + "github.com/ElrondNetwork/elrond-proxy-go/process" + "github.com/ElrondNetwork/elrond-proxy-go/process/mock" + "github.com/gin-gonic/gin/json" + "github.com/stretchr/testify/assert" +) + +type testStruct struct { + Nonce int + Name string +} + +func createTestHttpServer( + matchingPath string, + response []byte, +) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.Method == "GET" { + if req.URL.String() == matchingPath { + _, _ = rw.Write(response) + } + } + + if req.Method == "POST" { + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(req.Body) + _, _ = rw.Write(buf.Bytes()) + } + })) +} + +func TestNewBaseProcessor_WithNilAddressConverterShouldErr(t *testing.T) { + t.Parallel() + + bp, err := process.NewBaseProcessor(nil) + + assert.Nil(t, bp) + assert.Equal(t, process.ErrNilAddressConverter, err) +} + +func TestNewBaseProcessor_WithValidAddressConverterShouldWork(t *testing.T) { + t.Parallel() + + bp, err := process.NewBaseProcessor(&mock.AddressConverterStub{}) + + assert.NotNil(t, bp) + assert.Nil(t, err) +} + +//------- ApplyConfig + +func TestBaseProcessor_ApplyConfigNilCfgShouldErr(t *testing.T) { + t.Parallel() + + bp, _ := process.NewBaseProcessor(&mock.AddressConverterStub{}) + err := bp.ApplyConfig(nil) + + assert.Equal(t, process.ErrNilConfig, err) +} + +func TestBaseProcessor_ApplyConfigNoObserversShouldErr(t *testing.T) { + t.Parallel() + + bp, _ := process.NewBaseProcessor(&mock.AddressConverterStub{}) + err := bp.ApplyConfig(&config.Config{}) + + assert.Equal(t, process.ErrEmptyObserversList, err) +} + +func TestBaseProcessor_ApplyConfigShouldProcessConfigAndGetShouldWork(t *testing.T) { + t.Parallel() + + observersList := []*data.Observer{ + { + Address: "address1", + ShardId: 0, + }, + { + Address: "address2", + ShardId: 0, + }, + { + Address: "address3", + ShardId: 1, + }, + } + + bp, _ := process.NewBaseProcessor(&mock.AddressConverterStub{}) + err := bp.ApplyConfig(&config.Config{ + Observers: observersList, + }) + + assert.Nil(t, err) + observers, err := bp.GetObservers(0) + assert.Nil(t, err) + assert.Equal(t, 2, len(observers)) + assert.Equal(t, observers[0], observersList[0]) + assert.Equal(t, observers[1], observersList[1]) + + observers, err = bp.GetObservers(1) + assert.Nil(t, err) + assert.Equal(t, 1, len(observers)) + assert.Equal(t, observers[0], observersList[2]) +} + +//------- GetObservers + +func TestBaseProcessor_GetObserversEmptyListShouldErr(t *testing.T) { + t.Parallel() + + bp, _ := process.NewBaseProcessor(&mock.AddressConverterStub{}) + observers, err := bp.GetObservers(0) + + assert.Nil(t, observers) + assert.Equal(t, process.ErrMissingObserver, err) +} + +//------- ComputeShardId + +func TestBaseProcessor_ComputeShardId(t *testing.T) { + t.Parallel() + + observersList := []*data.Observer{ + { + Address: "address1", + ShardId: 0, + }, + { + Address: "address2", + ShardId: 1, + }, + } + + bp, _ := process.NewBaseProcessor(&mock.AddressConverterStub{ + CreateAddressFromPublicKeyBytesCalled: func(pubKey []byte) (container state.AddressContainer, e error) { + return &mock.AddressContainerMock{ + BytesField: pubKey, + }, nil + }, + }) + _ = bp.ApplyConfig(&config.Config{ + Observers: observersList, + }) + + //there are 2 shards, compute ID should correctly process + addressInShard0 := []byte{0} + shardId, err := bp.ComputeShardId(addressInShard0) + assert.Nil(t, err) + assert.Equal(t, uint32(0), shardId) + + addressInShard1 := []byte{1} + shardId, err = bp.ComputeShardId(addressInShard1) + assert.Nil(t, err) + assert.Equal(t, uint32(1), shardId) +} + +//------- Calls + +func TestBaseProcessor_CallGetRestEndPoint(t *testing.T) { + ts := &testStruct{ + Nonce: 10000, + Name: "a test struct to be send and received", + } + response, _ := json.Marshal(ts) + + server := createTestHttpServer("/some/path", response) + fmt.Printf("Server: %s\n", server.URL) + defer server.Close() + + tsRecovered := &testStruct{} + bp, _ := process.NewBaseProcessor(&mock.AddressConverterStub{}) + err := bp.CallGetRestEndPoint(server.URL, "/some/path", tsRecovered) + + assert.Nil(t, err) + assert.Equal(t, ts, tsRecovered) +} + +func TestBaseProcessor_CallPostRestEndPoint(t *testing.T) { + ts := &testStruct{ + Nonce: 10000, + Name: "a test struct to be send", + } + tsRecv := &testStruct{} + + server := createTestHttpServer("/some/path", nil) + fmt.Printf("Server: %s\n", server.URL) + defer server.Close() + + bp, _ := process.NewBaseProcessor(&mock.AddressConverterStub{}) + err := bp.CallPostRestEndPoint(server.URL, "/some/path", ts, tsRecv) + + assert.Nil(t, err) + assert.Equal(t, ts, tsRecv) +} diff --git a/process/errors.go b/process/errors.go new file mode 100644 index 00000000..e647d2ec --- /dev/null +++ b/process/errors.go @@ -0,0 +1,21 @@ +package process + +import "errors" + +// ErrNilConfig signals that a nil config has been provided +var ErrNilConfig = errors.New("nil configuration provided") + +// ErrEmptyObserversList signals that an empty list of observers has been provided +var ErrEmptyObserversList = errors.New("empty observers list provided") + +// ErrMissingObserver signals that no observers have been provided for provided shard ID +var ErrMissingObserver = errors.New("missing observer") + +// ErrSendingRequest signals that sending the request failed on all observers +var ErrSendingRequest = errors.New("sending request error") + +// ErrNilAddressConverter signals that a nil address converter has been provided +var ErrNilAddressConverter = errors.New("nil address converter") + +// ErrNilCoreProcessor signals that a nil core processor has been provided +var ErrNilCoreProcessor = errors.New("nil core processor") diff --git a/process/getAccountProcessor.go b/process/getAccountProcessor.go deleted file mode 100644 index 31d19e32..00000000 --- a/process/getAccountProcessor.go +++ /dev/null @@ -1,20 +0,0 @@ -package process - -import "github.com/ElrondNetwork/elrond-proxy-go/data" - -// GetAccountProcessor is able to process account requests -type GetAccountProcessor struct { -} - -// GetAccount resolves the request by sending the request to the right observer and replies back the answer -func (gap *GetAccountProcessor) GetAccount(address string) (*data.Account, error) { - //TODO, fix this mock with a real impl - - return &data.Account{ - Nonce: 1, - Balance: "2", - Address: address, - RootHash: []byte("ROOT_HASH"), - CodeHash: []byte("CODE_HASH"), - }, nil -} diff --git a/process/interface.go b/process/interface.go new file mode 100644 index 00000000..d32d76e7 --- /dev/null +++ b/process/interface.go @@ -0,0 +1,15 @@ +package process + +import ( + "github.com/ElrondNetwork/elrond-proxy-go/config" + "github.com/ElrondNetwork/elrond-proxy-go/data" +) + +// Processor defines what a processor should be able to do +type Processor interface { + ApplyConfig(cfg *config.Config) error + GetObservers(shardId uint32) ([]*data.Observer, error) + ComputeShardId(addressBuff []byte) (uint32, error) + CallGetRestEndPoint(address string, path string, value interface{}) error + CallPostRestEndPoint(address string, path string, data interface{}, response interface{}) error +} diff --git a/process/mock/addressContainerMock.go b/process/mock/addressContainerMock.go new file mode 100644 index 00000000..dbe6dc48 --- /dev/null +++ b/process/mock/addressContainerMock.go @@ -0,0 +1,9 @@ +package mock + +type AddressContainerMock struct { + BytesField []byte +} + +func (adr *AddressContainerMock) Bytes() []byte { + return adr.BytesField +} diff --git a/process/mock/addressConverterStub.go b/process/mock/addressConverterStub.go new file mode 100644 index 00000000..5f5d65fe --- /dev/null +++ b/process/mock/addressConverterStub.go @@ -0,0 +1,42 @@ +package mock + +import "github.com/ElrondNetwork/elrond-go-sandbox/data/state" + +type AddressConverterStub struct { + CreateAddressFromPublicKeyBytesCalled func(pubKey []byte) (state.AddressContainer, error) + ConvertToHexCalled func(addressContainer state.AddressContainer) (string, error) + CreateAddressFromHexCalled func(hexAddress string) (state.AddressContainer, error) + PrepareAddressBytesCalled func(addressBytes []byte) ([]byte, error) +} + +func (acs *AddressConverterStub) CreateAddressFromPublicKeyBytes(pubKey []byte) (state.AddressContainer, error) { + if acs.CreateAddressFromPublicKeyBytesCalled != nil { + return acs.CreateAddressFromPublicKeyBytesCalled(pubKey) + } + + return nil, errNotImplemented +} + +func (acs *AddressConverterStub) ConvertToHex(addressContainer state.AddressContainer) (string, error) { + if acs.ConvertToHexCalled != nil { + return acs.ConvertToHexCalled(addressContainer) + } + + return "", errNotImplemented +} + +func (acs *AddressConverterStub) CreateAddressFromHex(hexAddress string) (state.AddressContainer, error) { + if acs.CreateAddressFromHexCalled != nil { + return acs.CreateAddressFromHexCalled(hexAddress) + } + + return nil, errNotImplemented +} + +func (acs *AddressConverterStub) PrepareAddressBytes(addressBytes []byte) ([]byte, error) { + if acs.PrepareAddressBytesCalled != nil { + return acs.PrepareAddressBytesCalled(addressBytes) + } + + return nil, errNotImplemented +} diff --git a/process/mock/processorStub.go b/process/mock/processorStub.go new file mode 100644 index 00000000..b14d6ad6 --- /dev/null +++ b/process/mock/processorStub.go @@ -0,0 +1,57 @@ +package mock + +import ( + "github.com/ElrondNetwork/elrond-proxy-go/config" + "github.com/ElrondNetwork/elrond-proxy-go/data" + "github.com/pkg/errors" +) + +var errNotImplemented = errors.New("not implemented") + +type ProcessorStub struct { + ApplyConfigCalled func(cfg *config.Config) error + GetObserversCalled func(shardId uint32) ([]*data.Observer, error) + ComputeShardIdCalled func(addressBuff []byte) (uint32, error) + CallGetRestEndPointCalled func(address string, path string, value interface{}) error + CallPostRestEndPointCalled func(address string, path string, data interface{}, response interface{}) error +} + +func (ps *ProcessorStub) ApplyConfig(cfg *config.Config) error { + if ps.ApplyConfigCalled != nil { + return ps.ApplyConfigCalled(cfg) + } + + return errNotImplemented +} + +func (ps *ProcessorStub) GetObservers(shardId uint32) ([]*data.Observer, error) { + if ps.GetObserversCalled != nil { + return ps.GetObserversCalled(shardId) + } + + return nil, errNotImplemented +} + +func (ps *ProcessorStub) ComputeShardId(addressBuff []byte) (uint32, error) { + if ps.ComputeShardIdCalled != nil { + return ps.ComputeShardIdCalled(addressBuff) + } + + return 0, errNotImplemented +} + +func (ps *ProcessorStub) CallGetRestEndPoint(address string, path string, value interface{}) error { + if ps.CallGetRestEndPointCalled != nil { + return ps.CallGetRestEndPointCalled(address, path, value) + } + + return errNotImplemented +} + +func (ps *ProcessorStub) CallPostRestEndPoint(address string, path string, data interface{}, response interface{}) error { + if ps.CallPostRestEndPointCalled != nil { + return ps.CallPostRestEndPointCalled(address, path, data, response) + } + + return errNotImplemented +} diff --git a/process/transactionProcessor.go b/process/transactionProcessor.go new file mode 100644 index 00000000..2cbd8159 --- /dev/null +++ b/process/transactionProcessor.go @@ -0,0 +1,72 @@ +package process + +import ( + "encoding/hex" + "fmt" + "math/big" + + "github.com/ElrondNetwork/elrond-proxy-go/data" +) + +// TransactionPath defines the address path at which the nodes answer +const TransactionPath = "/transaction/send" + +// TransactionProcessor is able to process transaction requests +type TransactionProcessor struct { + proc Processor +} + +// NewTransactionProcessor creates a new instance of TransactionProcessor +func NewTransactionProcessor(proc Processor) (*TransactionProcessor, error) { + if proc == nil { + return nil, ErrNilCoreProcessor + } + + return &TransactionProcessor{ + proc: proc, + }, nil +} + +// SendTransaction relay the post request by sending the request to the right observer and replies back the answer +func (ap *TransactionProcessor) SendTransaction(nonce uint64, sender string, receiver string, value *big.Int, code string, signature []byte) (string, error) { + senderBuff, err := hex.DecodeString(sender) + if err != nil { + return "", err + } + + shardId, err := ap.proc.ComputeShardId(senderBuff) + if err != nil { + return "", err + } + + observers, err := ap.proc.GetObservers(shardId) + if err != nil { + return "", err + } + + for _, observer := range observers { + tx := &data.Transaction{ + Nonce: nonce, + Sender: sender, + Receiver: receiver, + Value: value, + Data: code, + Signature: hex.EncodeToString(signature), + } + txResponse := &data.ResponseTransaction{} + + err = ap.proc.CallPostRestEndPoint(observer.Address, TransactionPath, tx, txResponse) + if err == nil { + log.Info(fmt.Sprintf("Transaction sent successfully to observer %v from shard %v, received tx hash %s", + observer.Address, + shardId, + txResponse.TxHash, + )) + return txResponse.TxHash, nil + } + + log.LogIfError(err) + } + + return "", ErrSendingRequest +} diff --git a/process/transactionProcessor_test.go b/process/transactionProcessor_test.go new file mode 100644 index 00000000..0a07621d --- /dev/null +++ b/process/transactionProcessor_test.go @@ -0,0 +1,136 @@ +package process_test + +import ( + "errors" + "math/big" + "testing" + + "github.com/ElrondNetwork/elrond-proxy-go/data" + "github.com/ElrondNetwork/elrond-proxy-go/process" + "github.com/ElrondNetwork/elrond-proxy-go/process/mock" + "github.com/stretchr/testify/assert" +) + +func TestNewTransaction_NilCoreProcessorShouldErr(t *testing.T) { + t.Parallel() + + tp, err := process.NewTransactionProcessor(nil) + + assert.Nil(t, tp) + assert.Equal(t, process.ErrNilCoreProcessor, err) +} + +func TestNewTransactionProcessor_WithCoreProcessorShouldWork(t *testing.T) { + t.Parallel() + + tp, err := process.NewTransactionProcessor(&mock.ProcessorStub{}) + + assert.NotNil(t, tp) + assert.Nil(t, err) +} + +//------- SendTransaction + +func TestNewTransactionProcessor_SendTransactionInvalidHexAdressShouldErr(t *testing.T) { + t.Parallel() + + tp, _ := process.NewTransactionProcessor(&mock.ProcessorStub{}) + sig := make([]byte, 0) + txHash, err := tp.SendTransaction(0, "invalid hex number", "FF", big.NewInt(0), "", sig) + + assert.Empty(t, txHash) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid byte") +} + +func TestNewTransactionProcessor_SendTransactionComputeShardIdFailsShouldErr(t *testing.T) { + t.Parallel() + + errExpected := errors.New("expected error") + tp, _ := process.NewTransactionProcessor(&mock.ProcessorStub{ + ComputeShardIdCalled: func(addressBuff []byte) (u uint32, e error) { + return 0, errExpected + }, + }) + address := "DEADBEEF" + sig := make([]byte, 0) + txHash, err := tp.SendTransaction(0, address, address, big.NewInt(0), "", sig) + + assert.Empty(t, txHash) + assert.Equal(t, errExpected, err) +} + +func TestNewTransactionProcessor_SendTransactionGetObserversFailsShouldErr(t *testing.T) { + t.Parallel() + + errExpected := errors.New("expected error") + tp, _ := process.NewTransactionProcessor(&mock.ProcessorStub{ + ComputeShardIdCalled: func(addressBuff []byte) (u uint32, e error) { + return 0, nil + }, + GetObserversCalled: func(shardId uint32) (observers []*data.Observer, e error) { + return nil, errExpected + }, + }) + address := "DEADBEEF" + sig := make([]byte, 0) + txHash, err := tp.SendTransaction(0, address, address, big.NewInt(0), "", sig) + + assert.Empty(t, txHash) + assert.Equal(t, errExpected, err) +} + +func TestNewTransactionProcessor_SendTransactionSendingFailsOnAllObserversShouldErr(t *testing.T) { + t.Parallel() + + errExpected := errors.New("expected error") + tp, _ := process.NewTransactionProcessor(&mock.ProcessorStub{ + ComputeShardIdCalled: func(addressBuff []byte) (u uint32, e error) { + return 0, nil + }, + GetObserversCalled: func(shardId uint32) (observers []*data.Observer, e error) { + return []*data.Observer{ + {Address: "adress1", ShardId: 0}, + {Address: "adress2", ShardId: 0}, + }, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) error { + return errExpected + }, + }) + address := "DEADBEEF" + sig := make([]byte, 0) + txHash, err := tp.SendTransaction(0, address, address, big.NewInt(0), "", sig) + + assert.Empty(t, txHash) + assert.Equal(t, process.ErrSendingRequest, err) +} + +func TestNewTransactionProcessor_SendTransactionSendingFailsOnFirstObserverShouldStillSend(t *testing.T) { + t.Parallel() + + addressFail := "address1" + txHash := "DEADBEEF01234567890" + tp, _ := process.NewTransactionProcessor(&mock.ProcessorStub{ + ComputeShardIdCalled: func(addressBuff []byte) (u uint32, e error) { + return 0, nil + }, + GetObserversCalled: func(shardId uint32) (observers []*data.Observer, e error) { + return []*data.Observer{ + {Address: addressFail, ShardId: 0}, + {Address: "adress2", ShardId: 0}, + }, nil + }, + CallPostRestEndPointCalled: func(address string, path string, value interface{}, response interface{}) error { + txResponse := response.(*data.ResponseTransaction) + txResponse.TxHash = txHash + return nil + }, + }) + address := "DEADBEEF" + sig := make([]byte, 0) + resultedTxHash, err := tp.SendTransaction(0, address, address, big.NewInt(0), "", sig) + + assert.Equal(t, resultedTxHash, txHash) + assert.Nil(t, err) +} diff --git a/testing/testHttpServer.go b/testing/testHttpServer.go new file mode 100644 index 00000000..d2cffc60 --- /dev/null +++ b/testing/testHttpServer.go @@ -0,0 +1,93 @@ +package testing + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "path" + "strings" + + "github.com/ElrondNetwork/elrond-go-sandbox/core/logger" + "github.com/ElrondNetwork/elrond-proxy-go/data" +) + +var log = logger.DefaultLogger() + +// TestHttpServer is a test http server used for testing the whole binary +type TestHttpServer struct { + httpServer *httptest.Server +} + +// NewTestHttpServer creates a new TestHttpServer instance +func NewTestHttpServer() *TestHttpServer { + ths := &TestHttpServer{} + ths.httpServer = httptest.NewServer( + http.HandlerFunc(ths.processRequest), + ) + + return ths +} + +func (ths *TestHttpServer) processRequest(rw http.ResponseWriter, req *http.Request) { + if strings.Contains(req.URL.Path, "address") { + ths.processRequestAddress(rw, req) + return + } + + if strings.Contains(req.URL.Path, "transaction") { + ths.processRequestTransaction(rw, req) + return + } + + fmt.Printf("Can not serve request: %v\n", req.URL) +} + +func (ths *TestHttpServer) processRequestAddress(rw http.ResponseWriter, req *http.Request) { + _, address := path.Split(req.URL.String()) + + responseAccount := &data.ResponseAccount{ + AccountData: data.Account{ + Address: address, + Nonce: 45, + Balance: "1234", + CodeHash: []byte(address), + RootHash: []byte(address), + }, + } + + responseBuff, _ := json.Marshal(responseAccount) + _, err := rw.Write(responseBuff) + log.LogIfError(err) +} + +func (ths *TestHttpServer) processRequestTransaction(rw http.ResponseWriter, req *http.Request) { + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(req.Body) + newStr := buf.String() + + txHash := sha256.Sum256([]byte(newStr)) + txHexHash := hex.EncodeToString(txHash[:]) + + fmt.Printf("Got new request: %s, replying with %s\n", newStr, txHexHash) + response := data.ResponseTransaction{ + TxHash: txHexHash, + } + responseBuff, _ := json.Marshal(response) + + _, err := rw.Write(responseBuff) + log.LogIfError(err) +} + +// Close closes the test http server +func (ths *TestHttpServer) Close() { + ths.httpServer.Close() +} + +// URL returns the connecting url to the http test server +func (ths *TestHttpServer) URL() string { + return ths.httpServer.URL +}