Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(SPV-1106): introduce new bhs errors #294

Merged
merged 12 commits into from
Oct 25, 2024
105 changes: 105 additions & 0 deletions bhserrors/definitions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package bhserrors

// ////////////////////////////////// GENERIC ERRORS

// ErrGeneric is a generic error that something went wrong
var ErrGeneric = BHSError{Message: "internal server error", StatusCode: 500, Code: "ErrGeneric"}

// ErrBindBody is an error when it fails to bind JSON body
var ErrBindBody = BHSError{Message: "error during bind JSON body", StatusCode: 400, Code: "ErrBindBody"}

// ////////////////////////////////// AUTH ERRORS

// ErrMissingAuthHeader is when request does not have auth header
var ErrMissingAuthHeader = BHSError{Message: "empty auth header", StatusCode: 401, Code: "ErrMissingAuthHeader"}

// ErrInvalidAuthHeader is when request does not have a valid auth header
var ErrInvalidAuthHeader = BHSError{Message: "invalid auth header", StatusCode: 401, Code: "ErrInvalidAuthHeader"}

// ErrInvalidAccessToken is when access token is invalid
var ErrInvalidAccessToken = BHSError{Message: "invalid access token", StatusCode: 401, Code: "ErrInvalidAccessToken"}

// ErrUnauthorized is a generic error when user is unauthorized to make a request
var ErrUnauthorized = BHSError{Message: "not authorized", StatusCode: 401, Code: "ErrUnauthorized"}

// ErrAdminTokenNotFound is when admin token was not found in Block Header Service
var ErrAdminTokenNotFound = BHSError{Message: "admin token not found", StatusCode: 401, Code: "ErrAdminTokenNotFound"}

// ////////////////////////////////// MERKLE ROOTS ERRORS

// ErrMerklerootNotFound is when provided merkleroot from user was not found in Block Header Service's database
var ErrMerklerootNotFound = BHSError{Message: "no block with provided merkleroot was found", Code: "ErrMerkleRootNotFound", StatusCode: 404}

// ErrMerklerootNotInLongestChain is when provided merkleroot from user was found in Block Header Service's database but is not in Longest Chain state
var ErrMerklerootNotInLongestChain = BHSError{Message: "provided merkleroot is not part of the longest chain", Code: "ErrMerkleRootNotInLongestChain", StatusCode: 409}

// ErrInvalidBatchSize is when user provided incorrect batchSize
var ErrInvalidBatchSize = BHSError{Message: "batchSize must be 0 or a positive integer", Code: "ErrInvalidBatchSize", StatusCode: 400}

// ErrGetChainTipHeight is when it fails to get a chain tip height
var ErrGetChainTipHeight = BHSError{Message: "failed to get chain tip height", Code: "ErrGetChainTipHeight", StatusCode: 400}

// ErrVerifyMerklerootsBadBody is when request for verify merkleroots has wrong body
var ErrVerifyMerklerootsBadBody = BHSError{Message: "at least one merkleroot is required", Code: "ErrVerifyMerklerootsBadBody", StatusCode: 400}

// ////////////////////////////////// ACCESS ERRORS

// ErrTokenNotFound is when token was not found in Block Header Service
var ErrTokenNotFound = BHSError{Message: "token not found", StatusCode: 404, Code: "ErrTokenNotFound"}

// ErrCreateToken is when create token fails
var ErrCreateToken = BHSError{Message: "failed to create new token", StatusCode: 400, Code: "ErrCreateToken"}

// ErrDeleteToken is when delete token fails
var ErrDeleteToken = BHSError{Message: "failed to delete token", StatusCode: 400, Code: "ErrDeleteToken"}

// ////////////////////////////////// HEADERS ERRORS

// ErrAncestorHashHigher is when ancestor hash height is higher than requested header
var ErrAncestorHashHigher = BHSError{Message: "ancestor header height can not be higher than requested header height", StatusCode: 400, Code: "ErrAncestorHashHigher"}

// ErrAncestorNotFound is when ancestor for a given hash was not found
var ErrAncestorNotFound = BHSError{Message: "failed to get ancestor with given hash ", StatusCode: 400, Code: "ErrAncestorNotFound"}

// ErrHeadersNotPartOfTheSameChain is when provided headers are not part of the same chain
var ErrHeadersNotPartOfTheSameChain = BHSError{Message: "the headers provided are not part of the same chain", StatusCode: 400, Code: "ErrHeadersNotPartOfTheSameChain"}

// ErrHeaderWithGivenHashes is when getting header with given hashes fails
var ErrHeaderWithGivenHashes = BHSError{Message: "error during getting headers with given hashes", StatusCode: 400, Code: "ErrHeaderWithGivenHashes"}

// ErrHeaderNotFound is when hash could not be found
var ErrHeaderNotFound = BHSError{Message: "header not found", StatusCode: 404, Code: "ErrHeaderNotFound"}

// ErrHeadersForGivenRangeNotFound is when hash could not be found for given range
var ErrHeadersForGivenRangeNotFound = BHSError{Message: "could not find headers in given range", StatusCode: 404, Code: "ErrHeadersForGivenRangeNotFound"}

// ErrHeaderStopHeightNotFound is when stop height for given heade was not found
var ErrHeaderStopHeightNotFound = BHSError{Message: "could not find stop height for given header", StatusCode: 404, Code: "ErrHeaderStopHeightNotFound"}

// ////////////////////////////////// TIPS ERRORS

// ErrGetTips is when it fails to get tips
var ErrGetTips = BHSError{Message: "failed to get tips", StatusCode: 400, Code: "ErrGetTips"}

// ////////////////////////////////// WEBHOOK ERRORS

// ErrURLBodyRequired is when url is not provided in body
var ErrURLBodyRequired = BHSError{Message: "url is required", StatusCode: 400, Code: "ErrURLBodyRequired"}

// ErrURLParamRequired is when url is not provided in param
var ErrURLParamRequired = BHSError{Message: "url is required", StatusCode: 400, Code: "ErrURLParamRequired"}

// ErrCreateWebhook is when it fails to create a webhook
var ErrCreateWebhook = BHSError{Message: "failed to create a webhook", StatusCode: 400, Code: "ErrCreateWebhook"}

// ErrWebhookNotFound is when webhook was not found
var ErrWebhookNotFound = BHSError{Message: "webhook not found", StatusCode: 404, Code: "ErrWebhookNotFound"}

// ErrRefreshWebhook is when a webhook already exists and is active and we tried to refresh it
var ErrRefreshWebhook = BHSError{Message: "webhook already exists and is active", StatusCode: 400, Code: "ErrRefreshWebhook"}

// ErrGetAllWebhooks is when it failed to get all webhooks
var ErrGetAllWebhooks = BHSError{Message: "failed to get all webhooks", StatusCode: 400, Code: "ErrGetAllWebhooks"}

// ErrDeleteWebhook is when it failed to delete a webhook
var ErrDeleteWebhook = BHSError{Message: "failed to delete webhook", StatusCode: 400, Code: "ErrDeleteWebhook"}
90 changes: 90 additions & 0 deletions bhserrors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package bhserrors

import (
"github.com/pkg/errors"
)

type stackTracer interface {
StackTrace() errors.StackTrace
}

// ExtendedError is an interface for errors that hold information about http status and code that should be returned
type ExtendedError interface {
error
GetCode() string
GetMessage() string
GetStatusCode() int
StackTrace() (trace errors.StackTrace)
}

// BHSError is extended error which holds information about http status and code that should be returned from Block Header Service
type BHSError struct {
Code string
Message string
StatusCode int
cause error
}

// ResponseError is an error which will be returned in HTTP response
type ResponseError struct {
Code string `json:"code"`
Message string `json:"message"`
}

// Error returns the error message string for BHSError, satisfying the error interface
func (e BHSError) Error() string {
return e.Message
}

// GetCode returns the error code string for BHSError
func (e BHSError) GetCode() string {
return e.Code
}

// GetMessage returns the error message string for BHSError
func (e BHSError) GetMessage() string {
return e.Message
}

// GetStatusCode returns the error status code for BHSError
func (e BHSError) GetStatusCode() int {
return e.StatusCode
}

// StackTrace returns the error's stack trace.
func (e BHSError) StackTrace() errors.StackTrace {
err, ok := e.cause.(stackTracer)
if !ok {
return nil
}

return err.StackTrace()
}

// Unwrap returns the "cause" error
func (e BHSError) Unwrap() error {
return e.cause
}

// Wrap sets the "cause" error
func (e BHSError) Wrap(err error) BHSError {
e.cause = err
return e
}

// WithTrace save the stack trace of the error
func (e BHSError) WithTrace(err error) BHSError {
if st := stackTracer(nil); !errors.As(e.cause, &st) {
return e.Wrap(errors.WithStack(err))
}
return e.Wrap(err)
}

// Is checks if the target error is the same as the current error
func (e BHSError) Is(target error) bool {
t, ok := target.(BHSError)
if !ok {
return false
}
return e.Code == t.Code
}
53 changes: 53 additions & 0 deletions bhserrors/http_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package bhserrors

import (
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)

// UnknownErrorCode is a constant for unknown error code
const UnknownErrorCode = "error-unknown"

// ErrorResponse is searching for error and setting it up in gin context
func ErrorResponse(c *gin.Context, err error, log *zerolog.Logger) {
response, statusCode := mapAndLog(err, log)
c.JSON(statusCode, response)
}

// AbortWithErrorResponse is searching for error and abort with error set
func AbortWithErrorResponse(c *gin.Context, err error, log *zerolog.Logger) {
response, statusCode := mapAndLog(err, log)
c.AbortWithStatusJSON(statusCode, response)
}

func mapAndLog(err error, log *zerolog.Logger) (model ResponseError, statusCode int) {
model.Code = UnknownErrorCode
model.Message = "Internal server error"
statusCode = 500

logLevel := zerolog.WarnLevel
exposedInternalError := false
var extendedErr ExtendedError
if errors.As(err, &extendedErr) {
model.Code = extendedErr.GetCode()
model.Message = extendedErr.GetMessage()
statusCode = extendedErr.GetStatusCode()
if statusCode >= 500 {
logLevel = zerolog.ErrorLevel
}
} else {
// we should wrap all internal errors into BHSError (with proper code, message and status code)
// if you find out that some endpoint produces this warning, feel free to fix it
exposedInternalError = true
}

if log != nil {
logInstance := log.WithLevel(logLevel).Str("module", "block-header-error")
if exposedInternalError {
logInstance.Str("warning", "internal error returned as HTTP response")
}
logInstance.Err(err).Msgf("Error HTTP response, returning %d", statusCode)
}
return
}
32 changes: 12 additions & 20 deletions database/sql/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"

"github.com/bitcoin-sv/block-headers-service/bhserrors"
"github.com/bitcoin-sv/block-headers-service/domains"
"github.com/bitcoin-sv/block-headers-service/repository/dto"
"github.com/jmoiron/sqlx"
Expand Down Expand Up @@ -278,10 +279,7 @@ func (h *HeadersDb) Count(ctx context.Context) (int, error) {
func (h *HeadersDb) GetHeaderByHash(ctx context.Context, hash string) (*dto.DbBlockHeader, error) {
var bh dto.DbBlockHeader
if err := h.db.GetContext(ctx, &bh, h.db.Rebind(sqlHeader), hash); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.New("could not find hash")
}
return nil, errors.Wrapf(err, "failed to get blockhash using hash %s", hash)
return nil, bhserrors.ErrHeaderNotFound.Wrap(err)
}
return &bh, nil
}
Expand All @@ -302,10 +300,7 @@ func (h *HeadersDb) GetHeaderByHeight(ctx context.Context, height int32, state s
func (h *HeadersDb) GetHeaderByHeightRange(from int, to int) ([]*dto.DbBlockHeader, error) {
var bh []*dto.DbBlockHeader
if err := h.db.Select(&bh, h.db.Rebind(sqlHeaderByHeightRange), from, to); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.New("could not find headers in given range")
}
return nil, errors.Wrapf(err, "failed to get headers using given range from: %d to: %d", from, to)
return nil, bhserrors.ErrHeadersForGivenRangeNotFound.Wrap(err)
}
return bh, nil
}
Expand Down Expand Up @@ -344,10 +339,7 @@ func (h *HeadersDb) GenesisExists(_ context.Context) bool {
func (h *HeadersDb) GetPreviousHeader(ctx context.Context, hash string) (*dto.DbBlockHeader, error) {
var bh dto.DbBlockHeader
if err := h.db.GetContext(ctx, &bh, h.db.Rebind(sqlSelectPreviousBlock), hash); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.New("could not find header")
}
return nil, errors.Wrapf(err, "failed to get prev header using hash %s", hash)
return nil, bhserrors.ErrHeaderNotFound.Wrap(err)
}
return &bh, nil
}
Expand All @@ -370,10 +362,10 @@ func (h *HeadersDb) GetTip(_ context.Context) (*dto.DbBlockHeader, error) {
func (h *HeadersDb) GetAncestorOnHeight(hash string, height int32) (*dto.DbBlockHeader, error) {
var bh []*dto.DbBlockHeader
if err := h.db.Select(&bh, h.db.Rebind(sqlSelectAncestorOnHeight), hash, int(height), int(height)); err != nil {
return nil, errors.Wrapf(err, "failed to get ancestors using given hash: %s ", hash)
return nil, bhserrors.ErrAncestorNotFound.Wrap(err)
}
if len(bh) == 0 {
return nil, errors.New("could not find ancestors for a providen hash")
return nil, bhserrors.ErrAncestorNotFound
}
return bh[0], nil
}
Expand All @@ -382,7 +374,7 @@ func (h *HeadersDb) GetAncestorOnHeight(hash string, height int32) (*dto.DbBlock
func (h *HeadersDb) GetAllTips() ([]*dto.DbBlockHeader, error) {
var bh []*dto.DbBlockHeader
if err := h.db.Select(&bh, sqlSelectTips); err != nil {
return nil, errors.Wrapf(err, "failed to get tips")
return nil, bhserrors.ErrGetTips.Wrap(err)
}
return bh, nil
}
Expand All @@ -391,10 +383,10 @@ func (h *HeadersDb) GetAllTips() ([]*dto.DbBlockHeader, error) {
func (h *HeadersDb) GetChainBetweenTwoHashes(low string, high string) ([]*dto.DbBlockHeader, error) {
var bh []*dto.DbBlockHeader
if err := h.db.Select(&bh, h.db.Rebind(sqlChainBetweenTwoHashes), high, low, low); err != nil {
return nil, errors.Wrapf(err, "failed to get headers using given range from: %s to: %s", low, high)
return nil, bhserrors.ErrHeadersForGivenRangeNotFound.Wrap(err)
}
if len(bh) == 0 {
return nil, errors.New("could not find headers in given range")
return nil, bhserrors.ErrHeadersForGivenRangeNotFound
}
return bh, nil
}
Expand All @@ -406,7 +398,7 @@ func (h *HeadersDb) GetMerkleRootsConfirmations(
confirmations := make([]*dto.DbMerkleRootConfirmation, 0)
tipHeight, err := h.getChainTipHeight()
if err != nil {
return nil, errors.Wrap(err, "failed to get chain tip height")
return nil, bhserrors.ErrGetChainTipHeight.Wrap(err)
}

for _, item := range request {
Expand Down Expand Up @@ -511,15 +503,15 @@ func (h *HeadersDb) getLastEvaluatedMerklerootHeight(lastEvaluatedKey string) (i
err := h.db.Get(&lastEvaluatedMerkleroot, h.db.Rebind(sqlGetSingleMerkleroot), lastEvaluatedKey)

if errors.Is(err, sql.ErrNoRows) {
return 0, domains.ErrMerklerootNotFound
return 0, bhserrors.ErrMerklerootNotFound
}

if err != nil {
return 0, err
}

if lastEvaluatedMerkleroot.ToBlockHeader().State != domains.LongestChain {
return 0, domains.ErrMerklerootNotInLongestChain
return 0, bhserrors.ErrMerklerootNotInLongestChain
}

lastEvaluatedHeight := lastEvaluatedMerkleroot.Height
Expand Down
Loading
Loading