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

multi: update blockchain/standalone error types. #2280

Merged
merged 1 commit into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 22 additions & 40 deletions blockchain/standalone/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,55 @@

package standalone

import (
"fmt"
)

// ErrorCode identifies a kind of error.
type ErrorCode int
// ErrorKind identifies a kind of error. It has full support for errors.Is and
// errors.As, so the caller can directly check against an error kind when
// determining the reason for an error.
type ErrorKind string

// These constants are used to identify a specific RuleError.
const (
// ErrUnexpectedDifficulty indicates specified bits do not align with
// the expected value either because it doesn't match the calculated
// value based on difficulty rules or it is out of the valid range.
ErrUnexpectedDifficulty ErrorCode = iota
ErrUnexpectedDifficulty = ErrorKind("ErrUnexpectedDifficulty")

// ErrHighHash indicates the block does not hash to a value which is
// lower than the required target difficultly.
ErrHighHash
ErrHighHash = ErrorKind("ErrHighHash")

// ErrTSpendStartInvalidExpiry indicates that an invalid expiry was
// provided to calculate the start of a treasury spend vote window.
ErrTSpendStartInvalidExpiry
ErrTSpendStartInvalidExpiry = ErrorKind("ErrTSpendStartInvalidExpiry")

// ErrTSpendEndInvalidExpiry indicates that an invalid expiry was
// provided to calculate the end of a treasury spend vote window.
ErrTSpendEndInvalidExpiry

// numErrorCodes is the maximum error code number used in tests.
numErrorCodes
ErrTSpendEndInvalidExpiry = ErrorKind("ErrTSpendEndInvalidExpiry")
)

// Map of ErrorCode values back to their constant names for pretty printing.
var errorCodeStrings = map[ErrorCode]string{
ErrUnexpectedDifficulty: "ErrUnexpectedDifficulty",
ErrHighHash: "ErrHighHash",
ErrTSpendStartInvalidExpiry: "ErrTSpendStartInvalidExpiry",
ErrTSpendEndInvalidExpiry: "ErrTSpendEndInvalidExpiry",
}

// String returns the ErrorCode as a human-readable name.
func (e ErrorCode) String() string {
if s := errorCodeStrings[e]; s != "" {
return s
}
return fmt.Sprintf("Unknown ErrorCode (%d)", int(e))
// Error satisfies the error interface and prints human-readable errors.
func (e ErrorKind) Error() string {
return string(e)
}

// RuleError identifies a rule violation. The caller can use type assertions to
// determine if a failure was specifically due to a rule violation and access
// the ErrorCode field to ascertain the specific reason for the rule violation.
// RuleError identifies a rule violation. It has full support for errors.Is
// and errors.As, so the caller can ascertain the specific reason for the
// error by checking the underlying error.
type RuleError struct {
ErrorCode ErrorCode // Describes the kind of error
Description string // Human readable description of the issue
Description string
Err error
}

// Error satisfies the error interface and prints human-readable errors.
func (e RuleError) Error() string {
return e.Description
}

// ruleError creates an RuleError given a set of arguments.
func ruleError(c ErrorCode, desc string) RuleError {
return RuleError{ErrorCode: c, Description: desc}
// Unwrap returns the underlying wrapped error.
func (e RuleError) Unwrap() error {
return e.Err
}

// IsErrorCode returns whether or not the provided error is a rule error with
// the provided error code.
func IsErrorCode(err error, c ErrorCode) bool {
e, ok := err.(RuleError)
return ok && e.ErrorCode == c
// ruleError creates a RuleError given a set of arguments.
func ruleError(kind ErrorKind, desc string) RuleError {
return RuleError{Err: kind, Description: desc}
}
126 changes: 81 additions & 45 deletions blockchain/standalone/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,28 @@
package standalone

import (
"errors"
"io"
"testing"
)

// TestErrorCodeStringer tests the stringized output for the ErrorCode type.
func TestErrorCodeStringer(t *testing.T) {
// TestErrorKindStringer tests the stringized output for the ErrorKind type.
func TestErrorKindStringer(t *testing.T) {
tests := []struct {
in ErrorCode
in ErrorKind
want string
}{
{ErrUnexpectedDifficulty, "ErrUnexpectedDifficulty"},
{ErrHighHash, "ErrHighHash"},
{ErrTSpendStartInvalidExpiry, "ErrTSpendStartInvalidExpiry"},
{ErrTSpendEndInvalidExpiry, "ErrTSpendEndInvalidExpiry"},
{0xffff, "Unknown ErrorCode (65535)"},
}

// Detect additional error codes that don't have the stringer added.
if len(tests)-1 != int(numErrorCodes) {
t.Errorf("It appears an error code was added without adding an " +
"associated stringer test")
}

t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.String()
result := test.in.Error()
if result != test.want {
t.Errorf("String #%d\n got: %s want: %s", i, result, test.want)
t.Errorf("%d: got: %s want: %s", i, result, test.want)
continue
}
}
Expand All @@ -43,8 +38,8 @@ func TestRuleError(t *testing.T) {
in RuleError
want string
}{{
RuleError{Description: "duplicate block"},
"duplicate block",
RuleError{Description: "unexpected difficulty"},
"unexpected difficulty",
}, {
RuleError{Description: "human-readable error"},
"human-readable error",
Expand All @@ -55,50 +50,91 @@ func TestRuleError(t *testing.T) {
for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("Error #%d\n got: %s want: %s", i, result, test.want)
t.Errorf("%d: got: %s want: %s", i, result, test.want)
continue
}
}
}

// TestIsErrorCode ensures IsErrorCode works as intended.
func TestIsErrorCode(t *testing.T) {
// TestRuleErrorKindIsAs ensures both ErrorKind and RuleError can be
// identified as being a specific error kind via errors.Is and unwrapped
// via errors.As.
func TestRuleErrorKindIsAs(t *testing.T) {
tests := []struct {
name string
err error
code ErrorCode
want bool
name string
err error
target error
wantMatch bool
wantAs ErrorKind
}{{
name: "ErrUnexpectedDifficulty testing for ErrUnexpectedDifficulty",
err: ruleError(ErrUnexpectedDifficulty, ""),
code: ErrUnexpectedDifficulty,
want: true,
name: "ErrUnexpectedDifficulty == ErrUnexpectedDifficulty",
err: ErrUnexpectedDifficulty,
target: ErrUnexpectedDifficulty,
wantMatch: true,
wantAs: ErrUnexpectedDifficulty,
}, {
name: "RuleError.ErrUnexpectedDifficulty == ErrUnexpectedDifficulty",
err: ruleError(ErrUnexpectedDifficulty, ""),
target: ErrUnexpectedDifficulty,
wantMatch: true,
wantAs: ErrUnexpectedDifficulty,
}, {
name: "ErrHighHash testing for ErrHighHash",
err: ruleError(ErrHighHash, ""),
code: ErrHighHash,
want: true,
name: "RuleError.ErrUnexpectedDifficulty == RuleError.ErrUnexpectedDifficulty",
err: ruleError(ErrUnexpectedDifficulty, ""),
target: ruleError(ErrUnexpectedDifficulty, ""),
wantMatch: true,
wantAs: ErrUnexpectedDifficulty,
}, {
name: "ErrHighHash error testing for ErrUnexpectedDifficulty",
err: ruleError(ErrHighHash, ""),
code: ErrUnexpectedDifficulty,
want: false,
name: "ErrUnexpectedDifficulty != ErrHighHash",
err: ErrUnexpectedDifficulty,
target: ErrHighHash,
wantMatch: false,
wantAs: ErrUnexpectedDifficulty,
}, {
name: "ErrHighHash error testing for unknown error code",
err: ruleError(ErrHighHash, ""),
code: 0xffff,
want: false,
name: "RuleError.ErrUnexpectedDifficulty != ErrHighHash",
err: ruleError(ErrUnexpectedDifficulty, ""),
target: ErrHighHash,
wantMatch: false,
wantAs: ErrUnexpectedDifficulty,
}, {
name: "nil error testing for ErrUnexpectedDifficulty",
err: nil,
code: ErrUnexpectedDifficulty,
want: false,
name: "ErrUnexpectedDifficulty != RuleError.ErrHighHash",
err: ErrUnexpectedDifficulty,
target: ruleError(ErrHighHash, ""),
wantMatch: false,
wantAs: ErrUnexpectedDifficulty,
}, {
name: "RuleError.ErrUnexpectedDifficulty != RuleError.ErrHighHash",
err: ruleError(ErrUnexpectedDifficulty, ""),
target: ruleError(ErrHighHash, ""),
wantMatch: false,
wantAs: ErrUnexpectedDifficulty,
}, {
name: "RuleError.ErrUnexpectedDifficulty != io.EOF",
err: ruleError(ErrUnexpectedDifficulty, ""),
target: io.EOF,
wantMatch: false,
wantAs: ErrUnexpectedDifficulty,
}}

for _, test := range tests {
result := IsErrorCode(test.err, test.code)
if result != test.want {
t.Errorf("%s: unexpected result -- got: %v want: %v", test.name,
result, test.want)
// Ensure the error matches or not depending on the expected result.
result := errors.Is(test.err, test.target)
if result != test.wantMatch {
t.Errorf("%s: incorrect error identification -- got %v, want %v",
test.name, result, test.wantMatch)
continue
}

// Ensure the underlying error kind can be unwrapped is and is the
// expected kind.
var kind ErrorKind
if !errors.As(test.err, &kind) {
t.Errorf("%s: unable to unwrap to error kind", test.name)
continue
}
if kind != test.wantAs {
t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v",
test.name, kind, test.wantAs)
continue
}
}
Expand Down
39 changes: 14 additions & 25 deletions blockchain/standalone/pow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package standalone

import (
"errors"
"math/big"
"testing"

Expand Down Expand Up @@ -248,17 +249,17 @@ func TestCheckProofOfWorkRange(t *testing.T) {
name: "zero",
bits: 0,
powLimit: mockMainNetPowLimit(),
err: ruleError(ErrUnexpectedDifficulty, ""),
err: ErrUnexpectedDifficulty,
}, {
name: "negative",
bits: 0x1810000,
powLimit: mockMainNetPowLimit(),
err: ruleError(ErrUnexpectedDifficulty, ""),
err: ErrUnexpectedDifficulty,
}, {
name: "pow limit + 1",
bits: 0x1d010000,
powLimit: mockMainNetPowLimit(),
err: ruleError(ErrUnexpectedDifficulty, ""),
err: ErrUnexpectedDifficulty,
}}

for _, test := range tests {
Expand All @@ -269,15 +270,9 @@ func TestCheckProofOfWorkRange(t *testing.T) {
}

err := CheckProofOfWorkRange(test.bits, powLimit)
if test.err == nil && err != nil {
t.Errorf("%q: unexpected err -- got %v, want nil", test.name, err)
continue
} else if test.err != nil {
if !IsErrorCode(err, test.err.(RuleError).ErrorCode) {
t.Errorf("%q: unexpected err -- got %v, want %v",
test.name, err, test.err.(RuleError).ErrorCode)
continue
}
if !errors.Is(err, test.err) {
t.Errorf("%q: unexpected err -- got %v, want %v", test.name, err,
test.err)
continue
}
}
Expand Down Expand Up @@ -316,25 +311,25 @@ func TestCheckProofOfWork(t *testing.T) {
hash: "000000000001ffff000000000000000000000000000000000000000000000001",
bits: 0x1b01ffff,
powLimit: mockMainNetPowLimit(),
err: ruleError(ErrHighHash, ""),
err: ErrHighHash,
}, {
name: "hash satisfies target, but target too high at pow limit + 1",
hash: "0000000000000000000000000000000000000000000000000000000000000001",
bits: 0x1d010000,
powLimit: mockMainNetPowLimit(),
err: ruleError(ErrUnexpectedDifficulty, ""),
err: ErrUnexpectedDifficulty,
}, {
name: "zero target difficulty",
hash: "0000000000000000000000000000000000000000000000000000000000000001",
bits: 0,
powLimit: mockMainNetPowLimit(),
err: ruleError(ErrUnexpectedDifficulty, ""),
err: ErrUnexpectedDifficulty,
}, {
name: "negative target difficulty",
hash: "0000000000000000000000000000000000000000000000000000000000000001",
bits: 0x1810000,
powLimit: mockMainNetPowLimit(),
err: ruleError(ErrUnexpectedDifficulty, ""),
err: ErrUnexpectedDifficulty,
}}

for _, test := range tests {
Expand All @@ -351,15 +346,9 @@ func TestCheckProofOfWork(t *testing.T) {
}

err = CheckProofOfWork(hash, test.bits, powLimit)
if test.err == nil && err != nil {
t.Errorf("%q: unexpected err -- got %v, want nil", test.name, err)
continue
} else if test.err != nil {
if !IsErrorCode(err, test.err.(RuleError).ErrorCode) {
t.Errorf("%q: unexpected err -- got %v, want %v",
test.name, err, test.err.(RuleError).ErrorCode)
continue
}
if !errors.Is(err, test.err) {
t.Errorf("%q: unexpected err -- got %v, want %v", test.name, err,
test.err)
continue
}
}
Expand Down
8 changes: 4 additions & 4 deletions blockchain/standalone/treasury.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ func IsTreasuryVoteInterval(height, tvi uint64) bool {
// this function is only called with an expiry that *IS* on a TVI.
func CalculateTSpendWindowStart(expiry uint32, tvi, multiplier uint64) (uint32, error) {
if !IsTreasuryVoteInterval(uint64(expiry-2), tvi) {
return 0, RuleError{ErrTSpendStartInvalidExpiry,
fmt.Sprintf("invalid start expiry: %v", expiry)}
return 0, ruleError(ErrTSpendStartInvalidExpiry,
fmt.Sprintf("invalid start expiry: %v", expiry))
}
return expiry - uint32(tvi*multiplier) - 2, nil
}
Expand All @@ -40,8 +40,8 @@ func CalculateTSpendWindowStart(expiry uint32, tvi, multiplier uint64) (uint32,
// this function is only called with an expiry that *IS* on a TVI.
func CalculateTSpendWindowEnd(expiry uint32, tvi uint64) (uint32, error) {
if !IsTreasuryVoteInterval(uint64(expiry-2), tvi) {
return 0, RuleError{ErrTSpendEndInvalidExpiry,
fmt.Sprintf("invalid end expiry: %v", expiry)}
return 0, ruleError(ErrTSpendEndInvalidExpiry,
fmt.Sprintf("invalid end expiry: %v", expiry))
}
return expiry - 2, nil
}
Expand Down
Loading