diff --git a/cmd/node/node.go b/cmd/node/node.go index e8a5388eb5..3a431e279c 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -468,7 +468,7 @@ func runNode(ctx context.Context, nc *config) (_ io.Closer, retErr error) { return nil, errors.Wrap(err, "failed to create services") } - app, err := api.NewApp(nc.apiKey, minerScheduler, svs) + app, err := api.NewApp(nc.apiKey, minerScheduler, svs, cfg) if err != nil { return nil, errors.Wrap(err, "failed to initialize application") } diff --git a/cmd/wallet/main.go b/cmd/wallet/main.go index 8bee25f8ba..3565777c90 100644 --- a/cmd/wallet/main.go +++ b/cmd/wallet/main.go @@ -449,11 +449,11 @@ func createWallet( return errors.Wrap(err, "failed to write the wallet's data to the wallet") } - fmt.Printf("New account has been added to wallet successfully %s\n", walletPath) //nolint:forbidigo // As intended - fmt.Printf("Account Seed: %s\n", walletCredentials.accountSeed.String()) - fmt.Printf("Public Key: %s\n", walletCredentials.pk.String()) - fmt.Printf("Secret Key: %s\n", walletCredentials.sk.String()) - fmt.Printf("Address: %s\n", walletCredentials.address.String()) + fmt.Fprintf(os.Stdout, "New account has been added to wallet successfully %s\n", walletPath) + fmt.Fprintf(os.Stdout, "Account Seed: %s\n", walletCredentials.accountSeed.String()) + fmt.Fprintf(os.Stdout, "Public Key: %s\n", walletCredentials.pk.String()) + fmt.Fprintf(os.Stdout, "Secret Key: %s\n", walletCredentials.sk.String()) + fmt.Fprintf(os.Stdout, "Address: %s\n", walletCredentials.address.String()) return nil } diff --git a/itests/clients/http_client.go b/itests/clients/http_client.go index 48fa70a159..8f592b04f8 100644 --- a/itests/clients/http_client.go +++ b/itests/clients/http_client.go @@ -153,3 +153,48 @@ func (c *HTTPClient) RollbackToHeight(t testing.TB, height uint64, returnTxToUtx require.NoErrorf(t, err, "failed to rollback to height on %s node", c.impl.String()) return blockID } + +func (c *HTTPClient) HeightFinalized(t testing.TB) proto.Height { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + h, _, err := c.cli.Blocks.HeightFinalized(ctx) + require.NoErrorf(t, err, "failed to get finalized height from %s node", c.impl.String()) + + return h +} + +func (c *HTTPClient) BlockFinalized(t testing.TB) *proto.BlockHeader { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + header, _, err := c.cli.Blocks.BlockFinalized(ctx) + require.NoErrorf(t, err, "failed to get finalized header from %s node", c.impl.String()) + require.NotNil(t, header, "finalized header is nil from %s node", c.impl.String()) + + return header +} + +func (c *HTTPClient) CommitmentGeneratorsAt(t testing.TB, height proto.Height) []client.GeneratorInfoResponse { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + gens, _, err := c.cli.Generators.CommitmentGeneratorsAt(ctx, height) + require.NoErrorf(t, err, "failed to get generators at height %d from %s node", height, c.impl.String()) + + return gens +} + +func (c *HTTPClient) SignCommit( + t testing.TB, + req *client.SignCommitRequest, +) *proto.CommitToGenerationWithProofs { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + out, _, err := c.cli.Transactions.SignCommit(ctx, req) + require.NoErrorf(t, err, "failed to sign commit transaction on %s node", c.impl.String()) + + require.NotNil(t, out, "empty response from /transactions/sign on %s node", c.impl.String()) + return out +} diff --git a/pkg/api/app.go b/pkg/api/app.go index af88798821..0502ec2341 100644 --- a/pkg/api/app.go +++ b/pkg/api/app.go @@ -7,12 +7,14 @@ import ( "github.com/pkg/errors" + apiErr "github.com/wavesplatform/gowaves/pkg/api/errors" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/miner/scheduler" "github.com/wavesplatform/gowaves/pkg/node/messages" "github.com/wavesplatform/gowaves/pkg/node/peers" "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" "github.com/wavesplatform/gowaves/pkg/state" "github.com/wavesplatform/gowaves/pkg/types" ) @@ -35,6 +37,7 @@ const ( type appSettings struct { BlockRequestLimit uint64 AssetDetailsLimit int + GenerationPeriod uint64 } func defaultAppSettings() *appSettings { @@ -56,19 +59,23 @@ type App struct { settings *appSettings } -func NewApp(apiKey string, scheduler SchedulerEmits, services services.Services) (*App, error) { - return newApp(apiKey, scheduler, services, nil) +func NewApp(apiKey string, scheduler SchedulerEmits, services services.Services, + cfg *settings.BlockchainSettings) (*App, error) { + return newApp(apiKey, scheduler, services, nil, cfg) } -func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, settings *appSettings) (*App, error) { - if settings == nil { - settings = defaultAppSettings() +func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, appSettings *appSettings, + cfg *settings.BlockchainSettings) (*App, error) { + if appSettings == nil { + appSettings = defaultAppSettings() } digest, err := crypto.SecureHash([]byte(apiKey)) if err != nil { return nil, err } - + if cfg != nil { + appSettings.GenerationPeriod = cfg.GenerationPeriod + } return &App{ hashedApiKey: digest, apiKeyEnabled: len(apiKey) > 0, @@ -77,7 +84,7 @@ func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, utx: services.UtxPool, peers: services.Peers, services: services, - settings: settings, + settings: appSettings, }, nil } @@ -85,17 +92,17 @@ func (a *App) TransactionsBroadcast(ctx context.Context, b []byte) (proto.Transa tt := proto.TransactionTypeVersion{} err := json.Unmarshal(b, &tt) if err != nil { - return nil, wrapToBadRequestError(err) + return nil, apiErr.NewBadRequestError(err) } realType, err := proto.GuessTransactionType(&tt) if err != nil { - return nil, wrapToBadRequestError(err) + return nil, apiErr.NewBadRequestError(err) } err = proto.UnmarshalTransactionFromJSON(b, a.services.Scheme, realType) if err != nil { - return nil, wrapToBadRequestError(err) + return nil, apiErr.NewBadRequestError(err) } respCh := make(chan error, 1) diff --git a/pkg/api/app_test.go b/pkg/api/app_test.go index fdec4901b8..54fc58b1b1 100644 --- a/pkg/api/app_test.go +++ b/pkg/api/app_test.go @@ -5,10 +5,16 @@ import ( "github.com/stretchr/testify/require" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" ) func TestAppAuth(t *testing.T) { - app, _ := NewApp("apiKey", nil, services.Services{}) + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + app, _ := NewApp("apiKey", nil, services.Services{}, cfg) require.Error(t, app.checkAuth("bla")) require.NoError(t, app.checkAuth("apiKey")) } diff --git a/pkg/api/blocks_test.go b/pkg/api/blocks_test.go index badb0212aa..865ec87691 100644 --- a/pkg/api/blocks_test.go +++ b/pkg/api/blocks_test.go @@ -11,6 +11,7 @@ import ( "github.com/wavesplatform/gowaves/pkg/mock" "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" ) func TestApp_BlocksFirst(t *testing.T) { @@ -26,7 +27,12 @@ func TestApp_BlocksFirst(t *testing.T) { s := mock.NewMockState(ctrl) s.EXPECT().BlockByHeight(proto.Height(1)).Return(g, nil) - app, err := NewApp("api-key", nil, services.Services{State: s}) + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + app, err := NewApp("api-key", nil, services.Services{State: s}, cfg) require.NoError(t, err) first, err := app.BlocksFirst() require.NoError(t, err) @@ -42,7 +48,12 @@ func TestApp_BlocksLast(t *testing.T) { s.EXPECT().Height().Return(proto.Height(1), nil) s.EXPECT().BlockByHeight(proto.Height(1)).Return(g, nil) - app, err := NewApp("api-key", nil, services.Services{State: s}) + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + app, err := NewApp("api-key", nil, services.Services{State: s}, cfg) require.NoError(t, err) first, err := app.BlocksLast() require.NoError(t, err) diff --git a/pkg/api/errors.go b/pkg/api/errors.go index bd654dc1e7..1e16746038 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors.go @@ -18,20 +18,6 @@ var ( notFound = errors.New("not found") ) -// BadRequestError represents a bad request error. -// Deprecated: don't use this error type in new code. Create a new error type or value in 'pkg/api/errors' package. -type BadRequestError struct { - inner error -} - -func wrapToBadRequestError(err error) *BadRequestError { - return &BadRequestError{inner: err} -} - -func (e *BadRequestError) Error() string { - return e.inner.Error() -} - // AuthError represents an authentication error or problem. // Deprecated: don't use this error type in new code. Create a new error type or value in 'pkg/api/errors' package. type AuthError struct { @@ -62,17 +48,18 @@ func (eh *ErrorHandler) Handle(w http.ResponseWriter, r *http.Request, err error } // target errors var ( - badRequestError *BadRequestError - authError *AuthError - unknownError *apiErrs.UnknownError - apiError apiErrs.ApiError + badRequestError *apiErrs.BadRequestError + authError *AuthError + unknownError *apiErrs.UnknownError + apiError apiErrs.ApiError + unavailableError *apiErrs.UnavailableError // check that all targets implement the error interface - _, _, _, _ = error(badRequestError), error(authError), error(unknownError), error(apiError) + _, _, _, _, _ = error(badRequestError), error(authError), error(unknownError), + error(apiError), error(unavailableError) ) switch { case errors.As(err, &badRequestError): - // nickeskov: this error type will be removed in future - http.Error(w, fmt.Sprintf("Failed to complete request: %s", badRequestError.Error()), http.StatusBadRequest) + eh.sendApiErrJSON(w, r, badRequestError) case errors.As(err, &authError): // nickeskov: this error type will be removed in future http.Error(w, fmt.Sprintf("Failed to complete request: %s", authError.Error()), http.StatusForbidden) @@ -86,6 +73,8 @@ func (eh *ErrorHandler) Handle(w http.ResponseWriter, r *http.Request, err error eh.sendApiErrJSON(w, r, unknownError) case errors.As(err, &apiError): eh.sendApiErrJSON(w, r, apiError) + case errors.As(err, &unavailableError): + eh.sendApiErrJSON(w, r, unavailableError) default: eh.logger.Error("InternalServerError", slog.String("proto", r.Proto), diff --git a/pkg/api/errors/auth.go b/pkg/api/errors/auth.go index f2ccad9393..2c3d271324 100644 --- a/pkg/api/errors/auth.go +++ b/pkg/api/errors/auth.go @@ -18,7 +18,7 @@ type ( var ( ApiKeyNotValid = &ApiKeyNotValidError{ genericError: genericError{ - ID: ApiKeyNotValidErrorID, + ID: APIKeyNotValidErrorID, HttpCode: http.StatusBadRequest, Message: "Provided API key is not correct", }, diff --git a/pkg/api/errors/basics.go b/pkg/api/errors/basics.go index 20a02d549f..f0701bea5d 100644 --- a/pkg/api/errors/basics.go +++ b/pkg/api/errors/basics.go @@ -114,7 +114,7 @@ type WrongJsonError struct { func NewWrongJsonError(cause string, validationErrors []error) *WrongJsonError { return &WrongJsonError{ genericError: genericError{ - ID: WrongJsonErrorID, + ID: WrongJSONErrorID, HttpCode: http.StatusBadRequest, Message: "failed to parse json message", }, @@ -122,3 +122,111 @@ func NewWrongJsonError(cause string, validationErrors []error) *WrongJsonError { ValidationErrors: validationErrors, } } + +// UnavailableError UnknownError is a wrapper for any error related to service unavailability. +type UnavailableError struct { + genericError + inner error +} + +func (u *UnavailableError) Unwrap() error { + return u.inner +} + +func (u *UnavailableError) Error() string { + if u.Unwrap() != nil { + return fmt.Sprintf( + "%s; inner error (%T): %s", + u.genericError.Error(), + u.Unwrap(), u.Unwrap().Error(), + ) + } + return u.genericError.Error() +} + +func NewUnavailableError(inner error) *UnavailableError { + return NewUnavailableErrorWithMsg("Service is unavailable", inner) +} + +func NewUnavailableErrorWithMsg(message string, inner error) *UnavailableError { + return &UnavailableError{ + genericError: genericError{ + ID: ServiceUnavailableErrorID, + HttpCode: http.StatusServiceUnavailable, + Message: message, + }, + inner: inner, + } +} + +// BadRequestError is a wrapper for any bad request error. +type BadRequestError struct { + genericError + inner error +} + +func (u *BadRequestError) Unwrap() error { + return u.inner +} + +func (u *BadRequestError) Error() string { + if u.Unwrap() != nil { + return fmt.Sprintf( + "%s; inner error (%T): %s", + u.genericError.Error(), + u.Unwrap(), u.Unwrap().Error(), + ) + } + return u.genericError.Error() +} + +func NewBadRequestError(inner error) *BadRequestError { + return NewBadRequestErrorWithMsg("Bad request", inner) +} + +func NewBadRequestErrorWithMsg(message string, inner error) *BadRequestError { + return &BadRequestError{ + genericError: genericError{ + ID: BadRequestErrorID, + HttpCode: http.StatusBadRequest, + Message: message, + }, + inner: inner, + } +} + +// NotImplementedError is a wrapper for any not implemented error. +type NotImplementedError struct { + genericError + inner error +} + +func (u *NotImplementedError) Unwrap() error { + return u.inner +} + +func (u *NotImplementedError) Error() string { + if u.Unwrap() != nil { + return fmt.Sprintf( + "%s; inner error (%T): %s", + u.genericError.Error(), + u.Unwrap(), u.Unwrap().Error(), + ) + } + return u.genericError.Error() +} + +func NewNotImplementedError(inner error) *NotImplementedError { + return NewNotImplementedErrorWithMsg("Not implemented", inner) +} + +func NewNotImplementedErrorWithMsg(message string, inner error) *NotImplementedError { + return &NotImplementedError{ + genericError: genericError{ + ID: NotImplementedErrorID, + HttpCode: http.StatusNotImplemented, + Message: message, + }, + inner: inner, + } +} diff --git a/pkg/api/errors/consts.go b/pkg/api/errors/consts.go index 0a4b829462..57a80461a8 100644 --- a/pkg/api/errors/consts.go +++ b/pkg/api/errors/consts.go @@ -1,13 +1,16 @@ package errors const ( - UnknownErrorID ErrorID = 0 - WrongJsonErrorID ErrorID = 1 + UnknownErrorID ErrorID = 0 + WrongJSONErrorID ErrorID = 1 + BadRequestErrorID ErrorID = 3 + ServiceUnavailableErrorID ErrorID = 4 + NotImplementedErrorID ErrorID = 5 ) // API Auth const ( - ApiKeyNotValidErrorID ApiAuthErrorID = 2 + APIKeyNotValidErrorID ApiAuthErrorID = 2 TooBigArrayAllocationErrorID ApiAuthErrorID = 10 ) @@ -56,10 +59,13 @@ const ( ) var errorNames = map[Identifier]string{ - UnknownErrorID: "UnknownError", - WrongJsonErrorID: "WrongJsonError", + UnknownErrorID: "UnknownError", + WrongJSONErrorID: "WrongJsonError", + BadRequestErrorID: "BadRequestError", + ServiceUnavailableErrorID: "ServiceUnavailableError", + NotImplementedErrorID: "NotImplementedError", - ApiKeyNotValidErrorID: "ApiKeyNotValidError", + APIKeyNotValidErrorID: "ApiKeyNotValidError", TooBigArrayAllocationErrorID: "TooBigArrayAllocationError", InvalidSignatureErrorID: "InvalidSignatureError", InvalidAddressErrorID: "InvalidAddressError", diff --git a/pkg/api/errors_test.go b/pkg/api/errors_test.go index a17f274d72..d5b434dfff 100644 --- a/pkg/api/errors_test.go +++ b/pkg/api/errors_test.go @@ -21,7 +21,7 @@ func TestErrorHandler_Handle(t *testing.T) { require.NoError(t, err) return string(data) } - badReqErr = &BadRequestError{errors.New("bad-request")} + badReqErr = apiErrs.NewBadRequestError(errors.New("bad-request")) unknownErr = apiErrs.NewUnknownError(errors.New("unknown")) defaultErr = errors.New("default") ) @@ -35,13 +35,13 @@ func TestErrorHandler_Handle(t *testing.T) { name: "BadRequestErrorCase", err: errors.WithStack(errors.WithStack(badReqErr)), expectedCode: http.StatusBadRequest, - expectedBody: "Failed to complete request: bad-request\n", + expectedBody: mustJSON(badReqErr) + "\n", }, { name: "ErrorWithMultipleWraps", err: errors.Wrap(errors.Wrap(badReqErr, "wrap1"), "wrap2"), expectedCode: http.StatusBadRequest, - expectedBody: "Failed to complete request: bad-request\n", + expectedBody: mustJSON(badReqErr) + "\n", }, { name: "AuthErrorCase", diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index 4d80e244a9..24024708bb 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -16,8 +16,10 @@ import ( "github.com/go-chi/chi/v5" "github.com/pkg/errors" + "github.com/ccoveille/go-safecast/v2" apiErrs "github.com/wavesplatform/gowaves/pkg/api/errors" "github.com/wavesplatform/gowaves/pkg/crypto" + "github.com/wavesplatform/gowaves/pkg/crypto/bls" "github.com/wavesplatform/gowaves/pkg/errs" "github.com/wavesplatform/gowaves/pkg/logging" "github.com/wavesplatform/gowaves/pkg/proto" @@ -393,7 +395,7 @@ func (a *NodeApi) BlockScoreAt(w http.ResponseWriter, r *http.Request) error { id, err := strconv.ParseUint(s, 10, 64) if err != nil { // TODO(nickeskov): which error it should send? - return wrapToBadRequestError(err) + return apiErrs.NewBadRequestError(err) } rs, err := a.app.BlocksScoreAt(id) if err != nil { @@ -836,7 +838,7 @@ func (a *NodeApi) stateHash(w http.ResponseWriter, r *http.Request) error { height, err := strconv.ParseUint(s, 10, 64) if err != nil { // TODO(nickeskov): which error it should send? - return wrapToBadRequestError(err) + return apiErrs.NewBadRequestError(err) } if height < 1 { return apiErrs.BlockDoesNotExist @@ -882,7 +884,7 @@ func (a *NodeApi) snapshotStateHash(w http.ResponseWriter, r *http.Request) erro height, err := strconv.ParseUint(s, 10, 64) if err != nil { // TODO(nickeskov): which error it should send? - return wrapToBadRequestError(err) + return apiErrs.NewBadRequestError(err) } if height < 1 { return apiErrs.BlockDoesNotExist @@ -903,6 +905,195 @@ func (a *NodeApi) snapshotStateHash(w http.ResponseWriter, r *http.Request) erro return nil } +type generatorInfo struct { + Address string `json:"address"` + Balance uint64 `json:"balance"` + TransactionID string `json:"transactionID"` +} + +func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { + heightStr := chi.URLParam(r, "height") + height, err := strconv.ParseUint(heightStr, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid height") + } + isActivated, actErr := a.state.IsActivated(int16(settings.DeterministicFinality)) + if actErr != nil { + return errors.Wrap(actErr, "failed to check DeterministicFinality activation") + } + if !isActivated { + return apiErrs.NewUnavailableError(errors.New("deterministic finality is not activated")) + } + activationHeight, err := a.state.ActivationHeight(int16(settings.DeterministicFinality)) + if err != nil { + return fmt.Errorf("failed to get DeterministicFinality activation height: %w", err) + } + + periodStart, err := state.CurrentGenerationPeriodStart(activationHeight, height, + a.app.settings.GenerationPeriod) + if err != nil { + return fmt.Errorf("failed to calculate generationPeriodStart: %w", err) + } + generatorAddresses, err := a.state.CommittedGenerators(periodStart) + if err != nil { + return err + } + + generatorsInfo := make([]generatorInfo, 0, len(generatorAddresses)) + for _, generatorAddress := range generatorAddresses { + endorserRecipient := proto.NewRecipientFromAddress(generatorAddress) + balance, pullErr := a.state.GeneratingBalance(endorserRecipient, height) + if pullErr != nil { + return fmt.Errorf("failed to get generating balance for address %s at height %d: %w", + endorserRecipient.String(), height, pullErr) + } + generatorsInfo = append(generatorsInfo, generatorInfo{ + Address: generatorAddress.String(), + Balance: balance, + TransactionID: "", // It was decided to leave it empty. + }) + } + return trySendJSON(w, generatorsInfo) +} + +func (a *NodeApi) FinalizedHeight(w http.ResponseWriter, _ *http.Request) error { + h, err := a.state.LastFinalizedHeight() + if err != nil { + return err + } + return trySendJSON(w, map[string]uint64{"height": h}) +} + +func (a *NodeApi) FinalizedHeader(w http.ResponseWriter, _ *http.Request) error { + blockHeader, err := a.app.state.LastFinalizedBlock() + if err != nil { + return err + } + return trySendJSON(w, blockHeader) +} + +type signTxEnvelope struct { + Type proto.TransactionType `json:"type"` + Version byte `json:"version,omitempty"` +} + +func (a *NodeApi) transactionSign(w http.ResponseWriter, r *http.Request) error { + var signTx signTxEnvelope + if err := json.NewDecoder(r.Body).Decode(&signTx); err != nil { + return apiErrs.NewBadRequestError(errors.Wrap(err, "failed to decode tx envelope")) + } + + tx, err := proto.GuessTransactionType(&proto.TransactionTypeVersion{ + Type: signTx.Type, + Version: signTx.Version, + }) + if err != nil { + return apiErrs.NewBadRequestError(err) + } + + switch tx.GetType() { + case proto.CommitToGenerationTransaction: + var req signCommit + if decodeErr := json.NewDecoder(r.Body).Decode(&req); decodeErr != nil { + return apiErrs.NewBadRequestError(errors.Wrap(decodeErr, "failed to decode JSON")) + } + signedTx, signErr := a.transactionsSignCommitToGeneration(req) + if signErr != nil { + return signErr + } + return trySendJSON(w, signedTx) + case proto.GenesisTransaction, proto.PaymentTransaction, proto.IssueTransaction, proto.TransferTransaction, + proto.ReissueTransaction, proto.BurnTransaction, proto.ExchangeTransaction, proto.LeaseTransaction, + proto.LeaseCancelTransaction, proto.CreateAliasTransaction, proto.MassTransferTransaction, proto.DataTransaction, + proto.SetScriptTransaction, proto.SponsorshipTransaction, proto.SetAssetScriptTransaction, + proto.InvokeScriptTransaction, proto.UpdateAssetInfoTransaction, proto.EthereumMetamaskTransaction, + proto.InvokeExpressionTransaction: + return apiErrs.NewNotImplementedError(errors.Errorf("transaction signing not implemented for type %d", tx.GetType())) + default: + return apiErrs.NewBadRequestError(errors.Errorf("unknown transaction type %d", tx.GetType())) + } +} + +type signCommit struct { + Sender string `json:"sender"` + GenerationPeriodStart *uint32 `json:"generationPeriodStart,omitempty"` + Timestamp *int64 `json:"timestamp,omitempty"` + ChainID *byte `json:"chainId,omitempty"` + Type byte `json:"type,omitempty"` + Version byte `json:"version,omitempty"` +} + +func (a *NodeApi) transactionsSignCommitToGeneration(req signCommit) (*proto.CommitToGenerationWithProofs, error) { + isActivated, activationErr := a.state.IsActivated(int16(settings.DeterministicFinality)) + if activationErr != nil { + return nil, errors.Wrap(activationErr, "failed to check DeterministicFinality activation") + } + if !isActivated { + return nil, apiErrs.NewUnavailableError(errors.New("deterministic finality is not activated")) + } + addr, err := proto.NewAddressFromString(req.Sender) + if err != nil { + return nil, apiErrs.NewBadRequestError(errors.Wrap(err, "invalid sender address")) + } + scheme := a.app.services.Scheme + if req.ChainID != nil { + scheme = *req.ChainID + } + now := time.Now().UnixMilli() + + timestamp := now + if req.Timestamp != nil { + timestamp = *req.Timestamp + } + timestampUint, err := safecast.Convert[uint64](timestamp) + if err != nil { + return nil, apiErrs.NewBadRequestError(errors.Wrap(err, "invalid timestamp")) + } + var periodStart uint32 + if req.GenerationPeriodStart != nil { + periodStart = *req.GenerationPeriodStart + } else { + height, retrieveErr := a.state.Height() + if retrieveErr != nil { + return nil, errors.Wrap(retrieveErr, "failed to get height") + } + activationH, actErr := a.state.ActivationHeight(int16(settings.DeterministicFinality)) + if actErr != nil { + return nil, errors.Wrap(actErr, "failed to get DF activation height") + } + + periodStart, err = state.CurrentGenerationPeriodStart(activationH, height, a.app.settings.GenerationPeriod) + if err != nil { + return nil, errors.Wrap(err, "failed to calculate generationPeriodStart") + } + } + senderPK, err := a.app.services.Wallet.FindPublicKeyByAddress(addr, scheme) + if err != nil { + return nil, errors.Wrap(err, "failed to find key pair by address") + } + + blsSecretKey, blsPublicKey, err := a.app.services.Wallet.BLSPairByWavesPK(senderPK) + if err != nil { + return nil, errors.Wrap(err, "failed to find endorser public key by generator public key") + } + _, commitmentSignature, popErr := bls.ProvePoP(blsSecretKey, blsPublicKey, periodStart) + if popErr != nil { + return nil, errors.Wrap(popErr, "failed to create proof of possession for commitment") + } + tx := proto.NewUnsignedCommitToGenerationWithProofs(req.Version, + senderPK, + periodStart, + blsPublicKey, + commitmentSignature, + state.FeeUnit, + timestampUint) + err = a.app.services.Wallet.SignTransactionWith(senderPK, tx) + if err != nil { + return nil, err + } + return tx, nil +} + func wavesAddressInvalidCharErr(invalidChar rune, id string) *apiErrs.CustomValidationError { return apiErrs.NewCustomValidationError( fmt.Sprintf( diff --git a/pkg/api/node_api_test.go b/pkg/api/node_api_test.go index ce9b55c2c3..998318730f 100644 --- a/pkg/api/node_api_test.go +++ b/pkg/api/node_api_test.go @@ -17,6 +17,7 @@ import ( "github.com/wavesplatform/gowaves/pkg/mock" "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" ) const apiKey = "X-API-Key" @@ -75,6 +76,12 @@ func TestNodeApi_WavesRegularBalanceByAddress(t *testing.T) { return req } + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + t.Run("success", func(t *testing.T) { const ( addrStr = "3Myqjf1D44wR8Vko4Tr5CwSzRNo2Vg9S7u7" @@ -93,7 +100,7 @@ func TestNodeApi_WavesRegularBalanceByAddress(t *testing.T) { a, err := NewApp("", nil, services.Services{ State: st, Scheme: proto.TestNetScheme, - }) + }, cfg) require.NoError(t, err) aErr := NewNodeAPI(a, nil).WavesRegularBalanceByAddress(resp, req) @@ -120,7 +127,7 @@ func TestNodeApi_WavesRegularBalanceByAddress(t *testing.T) { a, err := NewApp("", nil, services.Services{ State: mock.NewMockState(ctrl), Scheme: proto.TestNetScheme, - }) + }, cfg) require.NoError(t, err) aErr := NewNodeAPI(a, nil).WavesRegularBalanceByAddress(resp, req) diff --git a/pkg/api/peers.go b/pkg/api/peers.go index 2623b97a48..ac7f47ac5d 100644 --- a/pkg/api/peers.go +++ b/pkg/api/peers.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" + apiErrs "github.com/wavesplatform/gowaves/pkg/api/errors" "github.com/wavesplatform/gowaves/pkg/p2p/peer" "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/util/common" @@ -81,12 +82,12 @@ func (a *App) PeersConnect(ctx context.Context, apiKey string, addr string) (*Pe d := proto.NewTCPAddrFromString(addr) if d.Empty() { slog.Error("Invalid peer's address to connect", "address", addr) - return nil, wrapToBadRequestError(errors.New("invalid address")) + return nil, apiErrs.NewBadRequestError(errors.New("invalid address")) } err = a.peers.Connect(ctx, d) if err != nil { - return nil, wrapToBadRequestError(err) + return nil, apiErrs.NewBadRequestError(err) } return &PeersConnectResponse{ diff --git a/pkg/api/peers_test.go b/pkg/api/peers_test.go index b8a775f0ff..38496c4bd5 100644 --- a/pkg/api/peers_test.go +++ b/pkg/api/peers_test.go @@ -15,9 +15,16 @@ import ( "github.com/wavesplatform/gowaves/pkg/mock" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" ) func TestApp_PeersKnown(t *testing.T) { + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -25,7 +32,7 @@ func TestApp_PeersKnown(t *testing.T) { addr := proto.NewTCPAddr(net.ParseIP("127.0.0.1"), 6868).ToIpPort() peerManager.EXPECT().KnownPeers().Return([]storage.KnownPeer{storage.KnownPeer(addr)}) - app, err := NewApp("key", nil, services.Services{Peers: peerManager}) + app, err := NewApp("key", nil, services.Services{Peers: peerManager}, cfg) require.NoError(t, err) rs2, err := app.PeersKnown() @@ -40,6 +47,11 @@ func TestApp_PeersSuspended(t *testing.T) { peerManager := mock.NewMockPeerManager(ctrl) now := time.Now() + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } ips := []string{"13.3.4.1", "5.3.6.7"} testData := []storage.SuspendedPeer{ @@ -59,7 +71,7 @@ func TestApp_PeersSuspended(t *testing.T) { peerManager.EXPECT().Suspended().Return(testData) - app, err := NewApp("key", nil, services.Services{Peers: peerManager}) + app, err := NewApp("key", nil, services.Services{Peers: peerManager}, cfg) require.NoError(t, err) suspended := app.PeersSuspended() @@ -100,8 +112,12 @@ func TestApp_PeersBlackList(t *testing.T) { } peerManager.EXPECT().BlackList().Return(testData) - - app, err := NewApp("key", nil, services.Services{Peers: peerManager}) + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + app, err := NewApp("key", nil, services.Services{Peers: peerManager}, cfg) require.NoError(t, err) blackList := app.PeersBlackListed() diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 218c7e2db1..452440fddb 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -107,6 +107,9 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { r.Get("/height/{id}", wrapper(a.BlockHeightByID)) r.Get("/at/{height}", wrapper(a.BlockAt)) r.Get("/{id}", wrapper(a.BlockIDAt)) + // Finalization. + r.Get("/height/finalized", wrapper(a.FinalizedHeight)) + r.Get("/headers/finalized", wrapper(a.FinalizedHeader)) r.Route("/headers", func(r chi.Router) { r.Get("/last", wrapper(a.BlocksHeadersLast)) @@ -116,6 +119,11 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { }) }) + // Finalization generators. + r.Route("/generators", func(r chi.Router) { + r.Get("/at/{height:\\d+}", wrapper(a.GeneratorsAt)) + }) + r.Route("/assets", func(r chi.Router) { r.Get("/details/{id}", wrapper(a.AssetsDetailsByID)) r.Get("/details", wrapper(a.AssetsDetailsByIDsGet)) @@ -136,6 +144,9 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { r.Get("/unconfirmed/size", wrapper(a.unconfirmedSize)) r.Get("/info/{id}", wrapper(a.TransactionInfo)) r.Post("/broadcast", wrapper(a.TransactionsBroadcast)) + + rAuth := r.With(checkAuthMiddleware) + rAuth.Post("/sign", wrapper(a.transactionSign)) }) r.Route("/peers", func(r chi.Router) { diff --git a/pkg/client/blocks.go b/pkg/client/blocks.go index 2a4f096872..d7dde03ae5 100644 --- a/pkg/client/blocks.go +++ b/pkg/client/blocks.go @@ -310,3 +310,44 @@ func (a *Blocks) Address(ctx context.Context, addr proto.WavesAddress, from, to return out, response, nil } + +func (a *Blocks) HeightFinalized(ctx context.Context) (uint64, *Response, error) { + url, err := joinUrl(a.options.BaseUrl, "/blocks/height/finalized") + if err != nil { + return 0, nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + if err != nil { + return 0, nil, err + } + + var out struct { + Height uint64 `json:"height"` + } + + resp, err := doHTTP(ctx, a.options, req, &out) + if err != nil { + return 0, resp, err + } + + return out.Height, resp, nil +} + +func (a *Blocks) BlockFinalized(ctx context.Context) (*proto.BlockHeader, *Response, error) { + url, err := joinUrl(a.options.BaseUrl, "/blocks/headers/finalized") + if err != nil { + return nil, nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + if err != nil { + return nil, nil, err + } + var out proto.BlockHeader + resp, err := doHTTP(ctx, a.options, req, &out) + if err != nil { + return nil, resp, err + } + return &out, resp, nil +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 67ce3d5de3..50c406fd77 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -48,6 +48,7 @@ type Client struct { Leasing *Leasing Debug *Debug Blockchain *Blockchain + Generators *Generators } type Response struct { @@ -93,6 +94,7 @@ func NewClient(options ...Options) (*Client, error) { Leasing: NewLeasing(opts), Debug: NewDebug(opts), Blockchain: NewBlockchain(opts), + Generators: NewGenerators(opts), } return c, nil diff --git a/pkg/client/generators.go b/pkg/client/generators.go new file mode 100644 index 0000000000..60b85555c8 --- /dev/null +++ b/pkg/client/generators.go @@ -0,0 +1,43 @@ +package client + +import ( + "context" + "fmt" + "net/http" +) + +// Generators is a client wrapper for generator-related API endpoints. +type Generators struct { + options Options +} + +func NewGenerators(options Options) *Generators { + return &Generators{ + options: options, + } +} + +type GeneratorInfoResponse struct { + Address string `json:"address"` + Balance uint64 `json:"balance"` +} + +// CommitmentGeneratorsAt returns the list of committed generators for the given height. +func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, + height uint64) ([]GeneratorInfoResponse, *Response, error) { + url, err := joinUrl(a.options.BaseUrl, fmt.Sprintf("/generators/at/%d", height)) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + if err != nil { + return nil, nil, err + } + var out []GeneratorInfoResponse + resp, err := doHTTP(ctx, a.options, req, &out) + if err != nil { + return nil, resp, err + } + + return out, resp, nil +} diff --git a/pkg/client/transactions.go b/pkg/client/transactions.go index 290eb8f5e1..041e1b908a 100644 --- a/pkg/client/transactions.go +++ b/pkg/client/transactions.go @@ -184,3 +184,36 @@ func (a *Transactions) Broadcast(ctx context.Context, transaction proto.Transact } return doHTTP(ctx, a.options, req, nil) } + +type SignCommitRequest struct { + Sender string `json:"sender"` + GenerationPeriodStart *uint32 `json:"generationPeriodStart,omitempty"` + Timestamp *int64 `json:"timestamp,omitempty"` + ChainID *byte `json:"chainId,omitempty"` +} + +// SignCommit calls POST /transactions/sign and returns the signed CommitToGenerationWithProofs tx. +func (a *Transactions) SignCommit(ctx context.Context, + reqBody *SignCommitRequest) (*proto.CommitToGenerationWithProofs, *Response, error) { + if reqBody == nil { + return nil, nil, fmt.Errorf("empty request body") + } + url, err := joinUrl(a.options.BaseUrl, "/transactions/sign") + if err != nil { + return nil, nil, err + } + bts, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewReader(bts)) + if err != nil { + return nil, nil, err + } + out := new(proto.CommitToGenerationWithProofs) + resp, err := doHTTP(ctx, a.options, req, out) + if err != nil { + return nil, resp, err + } + return out, resp, nil +} diff --git a/pkg/node/fsm/ng_state.go b/pkg/node/fsm/ng_state.go index 026d743f90..972db71e76 100644 --- a/pkg/node/fsm/ng_state.go +++ b/pkg/node/fsm/ng_state.go @@ -345,7 +345,6 @@ func (a *NGState) tryFinalize(height proto.Height) (*proto.FinalizationVoting, e } return &finalization, nil } - return nil, errNoFinalization } diff --git a/pkg/types/types.go b/pkg/types/types.go index 558e4d2ba0..bf4b759683 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -222,6 +222,8 @@ type MinerConsensus interface { type EmbeddedWallet interface { SignTransactionWith(pk crypto.PublicKey, tx proto.Transaction) error + FindPublicKeyByAddress(address proto.WavesAddress, scheme proto.Scheme) (crypto.PublicKey, error) + BLSPairByWavesPK(publicKey crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) Load(password []byte) error AccountSeeds() [][]byte } diff --git a/pkg/wallet/embedded_wallet.go b/pkg/wallet/embedded_wallet.go index b388608253..3caa094a27 100644 --- a/pkg/wallet/embedded_wallet.go +++ b/pkg/wallet/embedded_wallet.go @@ -4,6 +4,7 @@ import ( "sync" "github.com/wavesplatform/gowaves/pkg/crypto" + "github.com/wavesplatform/gowaves/pkg/crypto/bls" "github.com/wavesplatform/gowaves/pkg/proto" ) @@ -32,6 +33,47 @@ func (a *EmbeddedWalletImpl) SignTransactionWith(pk crypto.PublicKey, tx proto.T return ErrPublicKeyNotFound } +func (a *EmbeddedWalletImpl) FindPublicKeyByAddress(address proto.WavesAddress, + scheme proto.Scheme) (crypto.PublicKey, error) { + seeds := a.seeder.AccountSeeds() + for _, s := range seeds { + _, public, err := crypto.GenerateKeyPair(s) + if err != nil { + return crypto.PublicKey{}, err + } + retrievedAddress, err := proto.NewAddressFromPublicKey(scheme, public) + if err != nil { + return crypto.PublicKey{}, err + } + if retrievedAddress == address { + return public, nil + } + } + return crypto.PublicKey{}, ErrPublicKeyNotFound +} + +func (a *EmbeddedWalletImpl) BLSPairByWavesPK(publicKey crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) { + seeds := a.seeder.AccountSeeds() + for _, s := range seeds { + _, publicKeyRetrieved, err := crypto.GenerateKeyPair(s) + if err != nil { + return bls.SecretKey{}, bls.PublicKey{}, err + } + if publicKeyRetrieved == publicKey { + secretKeyBls, genErr := bls.GenerateSecretKey(s) + if genErr != nil { + return bls.SecretKey{}, bls.PublicKey{}, genErr + } + publicKeyBls, retrieveErr := secretKeyBls.PublicKey() + if retrieveErr != nil { + return bls.SecretKey{}, bls.PublicKey{}, retrieveErr + } + return secretKeyBls, publicKeyBls, nil + } + } + return bls.SecretKey{}, bls.PublicKey{}, ErrPublicKeyNotFound +} + func (a *EmbeddedWalletImpl) Load(password []byte) error { bts, err := a.loader.Load() if err != nil { diff --git a/pkg/wallet/stub.go b/pkg/wallet/stub.go deleted file mode 100644 index 032e09b789..0000000000 --- a/pkg/wallet/stub.go +++ /dev/null @@ -1,22 +0,0 @@ -package wallet - -import ( - "github.com/wavesplatform/gowaves/pkg/crypto" - "github.com/wavesplatform/gowaves/pkg/proto" -) - -type Stub struct { - S [][]byte -} - -func (s Stub) SignTransactionWith(pk crypto.PublicKey, tx proto.Transaction) error { - panic("Stub.SignTransactionWith: Unsupported operation") -} - -func (s Stub) Load(password []byte) error { - panic("Stub.Load: Unsupported operation") -} - -func (s Stub) AccountSeeds() [][]byte { - return s.S -}