diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2a0370..e8049f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Sign stats request [\#143](https://github.com/NodeFactoryIo/vedran/pull/143) ([MakMuftic](https://github.com/MakMuftic)) ### Fix +- Fix payout [\#148](https://github.com/NodeFactoryIo/vedran/pull/148) ([MakMuftic](https://github.com/MakMuftic)) ### Changed - Write byte response directly with io.Write [\#113](https://github.com/NodeFactoryIo/vedran/pull/113) ([mpetrun5](https://github.com/mpetrun5)) diff --git a/README.md b/README.md index e8509be5..afd74082 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ For Westend's WND tokens, see the faucet [instructions on the Wiki](https://wiki When starting _vedran loadbalancer_ it is possible to configure automatic payout by providing these flags: -`--private-key` - loadbalancers wallet private key, used for sending rewards on payout +`--private-key` - loadbalancers wallet private key (string representation of hex value prefixed with 0x), used for sending rewards on payout `--payout-interval` - automatic payout interval specified as number of days @@ -130,7 +130,7 @@ If all flags have been provided, then each {_payout-interval_} days automatic pa It is possible to run payout script at any time by invoking `vedran payout` command trough console. This command has two required flags: -`--private-key` - loadbalancers wallet private key, used for sending founds on payout +`--private-key` - loadbalancers wallet private key (string representation of hex value prefixed with 0x), used for sending founds on payout `--payout-reward` - defined total reward amount that will be distributed on the payout (amount in Planck) @@ -138,6 +138,19 @@ Additionally, it is possible to change url on which payout script will connect w `--load-balancer-url` - loadbalancer url +### Get private key +You can use [subkey](https://substrate.dev/docs/en/knowledgebase/integrate/subkey) tool to get private key for your wallet. + +After installing subkey tool call `subkey inspect "insert your mnemonic here"`. +You can find private key as _Secreet seed_. See example output of subkey command: + +``` + Secret seed: 0x1a84771145cdcee05e49142aaff2e5d669ce4b29344a09b973b751ae661acabf + Public key (hex): 0xa4548fa9b3b15dc4d1c59789952f0ccf6138dd63faf802637895c941f0522d35 + Account ID: 0xa4548fa9b3b15dc4d1c59789952f0ccf6138dd63faf802637895c941f0522d35 + SS58 Address: 5FnAq6wrMzri5V6jLfKgBkbR2rSAMkVAHVYWa3eU7TAV5rv9 +``` + ## Vedran loadbalancer API `POST api/v1/nodes` diff --git a/cmd/start.go b/cmd/start.go index 1f5599a4..9549d222 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -300,6 +300,6 @@ func startCommand(_ *cobra.Command, _ []string) { WhitelistEnabled: whitelistEnabled, }, payoutConfiguration, - privateKey, + payoutPrivateKey, ) } diff --git a/go.mod b/go.mod index d2d11f04..20e3fe8c 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,13 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/decred/base58 v1.0.2 github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/ethereum/go-ethereum v1.9.24 // indirect + github.com/ethereum/go-ethereum v1.9.24 github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.1 github.com/gosuri/uitable v0.0.4 github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b + github.com/pkg/errors v0.8.1 github.com/sirupsen/logrus v1.6.0 github.com/spf13/cobra v1.0.0 github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum index 142d810d..478efd7c 100644 --- a/go.sum +++ b/go.sum @@ -235,6 +235,7 @@ github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssy github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo= github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/controllers/stats.go b/internal/controllers/stats.go index 8daa4215..6fc581a3 100644 --- a/internal/controllers/stats.go +++ b/internal/controllers/stats.go @@ -2,6 +2,7 @@ package controllers import ( "encoding/json" + "github.com/ethereum/go-ethereum/common/hexutil" "net/http" "time" @@ -43,11 +44,15 @@ type LoadbalancerStatsResponse struct { func (c *ApiController) StatisticsHandlerAllStatsForLoadbalancer(w http.ResponseWriter, r *http.Request) { sig := r.Header.Get("X-Signature") if sig == "" { - log.Errorf("Missing signature header") + log.Error("Missing signature header") http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } - - verified, err := signature.Verify([]byte(StatsSignedData), []byte(sig), c.privateKey) + sigInBytes, err := hexutil.Decode(sig) + if err != nil { + log.Errorf("Unable to decode signature, because of: %v", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + verified, err := signature.Verify([]byte(StatsSignedData), sigInBytes, c.privateKey) if err != nil { log.Errorf("Failed to verify signature, because %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/internal/controllers/stats_test.go b/internal/controllers/stats_test.go index 05d0fe6c..5b3360ff 100644 --- a/internal/controllers/stats_test.go +++ b/internal/controllers/stats_test.go @@ -10,6 +10,7 @@ import ( "github.com/NodeFactoryIo/vedran/internal/models" "github.com/NodeFactoryIo/vedran/internal/repositories" mocks "github.com/NodeFactoryIo/vedran/mocks/repositories" + "github.com/ethereum/go-ethereum/common/hexutil" muxhelpper "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -353,7 +354,7 @@ func TestApiController_StatisticsHandlerAllStatsForLoadbalancer(t *testing.T) { if test.secret != "" { sig, _ := signature.Sign([]byte(test.signatureData), test.secret) - req.Header.Set("X-Signature", string(sig)) + req.Header.Set("X-Signature", hexutil.Encode(sig)) } rr := httptest.NewRecorder() diff --git a/internal/loadbalancer/server.go b/internal/loadbalancer/server.go index 742fc84c..23ff0530 100644 --- a/internal/loadbalancer/server.go +++ b/internal/loadbalancer/server.go @@ -59,7 +59,6 @@ func StartLoadBalancerServer( log.Fatalf("Failed creating initial payout because of: %v", err) } else if len(*p) == 0 { err := repos.PayoutRepo.Save(&models.Payout{ - ID: "1", Timestamp: time.Now(), PaymentDetails: nil, }) diff --git a/internal/models/payout.go b/internal/models/payout.go index 89efdc35..8086b5bd 100644 --- a/internal/models/payout.go +++ b/internal/models/payout.go @@ -3,7 +3,7 @@ package models import "time" type Payout struct { - ID string `storm:"id"` + ID int `storm:"id,increment"` Timestamp time.Time `json:"timestamp"` PaymentDetails map[string]NodeStatsDetails } diff --git a/internal/payout/tx.go b/internal/payout/tx.go index 8410259c..5b503700 100644 --- a/internal/payout/tx.go +++ b/internal/payout/tx.go @@ -15,15 +15,12 @@ func ExecuteTransaction( amount big.Int, keyringPair signature.KeyringPair, mux *sync.Mutex, + metadataLatest *types.Metadata, + nonce uint32, ) (*TransactionDetails, error) { // lock segment so goroutines don't access api at the same time mux.Lock() - metadataLatest, err := api.RPC.State.GetMetadataLatest() - if err != nil { - return nil, err - } - decoded := base58.Decode(to) // remove the 1st byte (network identifier) & last 2 bytes (blake2b hash) pubKey := decoded[1 : len(decoded)-2] @@ -51,25 +48,6 @@ func ExecuteTransaction( return nil, err } - storageKey, err := types.CreateStorageKey( - metadataLatest, - "System", - "Account", - keyringPair.PublicKey, - nil, - ) - if err != nil { - return nil, err - } - - var accountInfo types.AccountInfo - ok, err := api.RPC.State.GetStorageLatest(storageKey, &accountInfo) - if err != nil || !ok { - return nil, err - } - - nonce := uint32(accountInfo.Nonce) - signatureOptions := types.SignatureOptions{ Era: types.ExtrinsicEra{IsMortalEra: false}, Nonce: types.NewUCompactFromUInt(uint64(nonce)), diff --git a/internal/payout/txpayout.go b/internal/payout/txpayout.go index 5cb3c078..6bd11b8e 100644 --- a/internal/payout/txpayout.go +++ b/internal/payout/txpayout.go @@ -4,6 +4,7 @@ import ( gsrpc "github.com/NodeFactoryIo/go-substrate-rpc-client" "github.com/NodeFactoryIo/go-substrate-rpc-client/signature" "github.com/NodeFactoryIo/go-substrate-rpc-client/types" + "github.com/pkg/errors" "math/big" "sync" ) @@ -44,17 +45,28 @@ func executeAllTransactions( // define number of goroutines wg.Add(len(payoutDistribution)) + metadataLatest, err := api.RPC.State.GetMetadataLatest() + if err != nil { + return nil, errors.Wrap(err, "unable to get latest metadat") + } + + nonce, err := getNonce(metadataLatest, keyringPair, api) + if err != nil { + return nil, errors.Wrap(err, "unable to get nonce") + } + for nodePayoutAddress, amount := range payoutDistribution { // execute transaction in separate goroutine and collect results in channels - go func(to string, amount big.Int, wg *sync.WaitGroup, mux *sync.Mutex) { + go func(to string, amount big.Int, wg *sync.WaitGroup, mux *sync.Mutex, nonce uint32) { defer wg.Done() - transactionDetails, err := ExecuteTransaction(api, to, amount, keyringPair, mux) + transactionDetails, err := ExecuteTransaction(api, to, amount, keyringPair, mux, metadataLatest, nonce) if err != nil { fatalErrorsChannel <- err } else { resultsChannel <- transactionDetails } - }(nodePayoutAddress, amount, &wg, &mux) + }(nodePayoutAddress, amount, &wg, &mux, nonce) + nonce += 1 } go func() { @@ -67,6 +79,28 @@ func executeAllTransactions( return waitForTransactionDetails(waitGroupDoneChannel, fatalErrorsChannel, resultsChannel) } +func getNonce(metadataLatest *types.Metadata, keyringPair signature.KeyringPair, api *gsrpc.SubstrateAPI) (uint32, error) { + storageKey, err := types.CreateStorageKey( + metadataLatest, + "System", + "Account", + keyringPair.PublicKey, + nil, + ) + if err != nil { + return 0, err + } + + var accountInfo types.AccountInfo + ok, err := api.RPC.State.GetStorageLatest(storageKey, &accountInfo) + if err != nil || !ok { + return 0, err + } + + nonce := uint32(accountInfo.Nonce) + return nonce, err +} + func waitForTransactionDetails( waitGroupDoneChannel chan bool, fatalErrorsChannel chan error, diff --git a/internal/repositories/downtime.go b/internal/repositories/downtime.go index 1b24aa11..2e1a14b5 100644 --- a/internal/repositories/downtime.go +++ b/internal/repositories/downtime.go @@ -34,12 +34,12 @@ func (r *DowntimeRepo) FindDowntimesInsideInterval(nodeID string, from time.Time q.Eq("NodeId", nodeID), q.Or( q.And( // start inside interval - q.Gte("start", from), - q.Lte("start", to), + q.Gte("Start", from), + q.Lte("Start", to), ), q.And( // end inside interval - q.Gte("end", from), - q.Lte("end", to), + q.Gte("End", from), + q.Lte("End", to), ), ), )).Find(&downtimes) diff --git a/internal/repositories/payout.go b/internal/repositories/payout.go index 4815f72a..d301841c 100644 --- a/internal/repositories/payout.go +++ b/internal/repositories/payout.go @@ -33,6 +33,6 @@ func (p *payoutRepo) GetAll() (*[]models.Payout, error) { func (p *payoutRepo) FindLatestPayout() (*models.Payout, error) { var payout models.Payout - err := p.db.Select().OrderBy("Timestamp").First(&payout) + err := p.db.Select().OrderBy("Timestamp").Reverse().First(&payout) return &payout, err } diff --git a/internal/repositories/ping.go b/internal/repositories/ping.go index b271fa46..426de96c 100644 --- a/internal/repositories/ping.go +++ b/internal/repositories/ping.go @@ -67,6 +67,5 @@ func (r *pingRepo) CalculateDowntime(nodeId string, pingTime time.Time) (time.Ti return pingTime, time.Duration(0), err } - - return lastPing.Timestamp, lastPing.Timestamp.Sub(pingTime), nil + return lastPing.Timestamp, pingTime.Sub(lastPing.Timestamp), nil } diff --git a/internal/schedule/payout/payout_test.go b/internal/schedule/payout/payout_test.go index 2166bc0e..87503f92 100644 --- a/internal/schedule/payout/payout_test.go +++ b/internal/schedule/payout/payout_test.go @@ -28,7 +28,7 @@ func Test_numOfDaysSinceLastPayout(t *testing.T) { { name: "last payout before 20 days", latestPayout: &models.Payout{ - ID: "", + ID: 1, Timestamp: timestampSince20days, PaymentDetails: nil, }, @@ -40,7 +40,7 @@ func Test_numOfDaysSinceLastPayout(t *testing.T) { { name: "last payout before 2 days", latestPayout: &models.Payout{ - ID: "", + ID: 1, Timestamp: timestampSince2days, PaymentDetails: nil, }, @@ -52,7 +52,7 @@ func Test_numOfDaysSinceLastPayout(t *testing.T) { { name: "last payout before 2 hours", latestPayout: &models.Payout{ - ID: "", + ID: 1, Timestamp: timestampSince2hours, PaymentDetails: nil, }, diff --git a/internal/script/payout.go b/internal/script/payout.go index e8ffc92f..49a6cfa5 100644 --- a/internal/script/payout.go +++ b/internal/script/payout.go @@ -3,6 +3,7 @@ package script import ( "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/common/hexutil" "net/http" "net/url" "strconv" @@ -41,7 +42,7 @@ func fetchStatsFromEndpoint(endpoint *url.URL, secret string) (*controllers.Load } request, _ := http.NewRequest("POST", endpoint.String(), nil) - request.Header.Set("X-Signature", string(sig)) + request.Header.Set("X-Signature", hexutil.Encode(sig)) c := &http.Client{} resp, err := c.Do(request)