Skip to content

Commit

Permalink
primitives: Add check proof of work.
Browse files Browse the repository at this point in the history
This implements functions to verify a block hash is less than the target
difficulty represented by given difficulty bits and that said difficulty
is in min/max range per a proof-of-work limit along with associated
tests.

It also introduces error infrastructure consistent with the rest of the
code.

The functions are the semantic equivalent of the functions of the same
names from blockchain/standalone updated to use and accept the new
uint256 type instead of stdlib big integers.
  • Loading branch information
davecgh committed Dec 4, 2021
1 parent ea4ccfe commit 03bacc6
Show file tree
Hide file tree
Showing 4 changed files with 386 additions and 0 deletions.
50 changes: 50 additions & 0 deletions internal/staging/primitives/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package primitives

// 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

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

// 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 = ErrorKind("ErrUnexpectedDifficulty")

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

// 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 {
Description string
Err error
}

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

// Unwrap returns the underlying wrapped error.
func (e RuleError) Unwrap() error {
return e.Err
}

// ruleError creates a RuleError given a set of arguments.
func ruleError(kind ErrorKind, desc string) RuleError {
return RuleError{Err: kind, Description: desc}
}
131 changes: 131 additions & 0 deletions internal/staging/primitives/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) 2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package primitives

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

// TestErrorKindStringer tests the stringized output for the ErrorKind type.
func TestErrorKindStringer(t *testing.T) {
tests := []struct {
in ErrorKind
want string
}{
{ErrUnexpectedDifficulty, "ErrUnexpectedDifficulty"},
{ErrHighHash, "ErrHighHash"},
}

for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("#%d: got: %s want: %s", i, result, test.want)
continue
}
}
}

// TestRuleError tests the error output for the RuleError type.
func TestRuleError(t *testing.T) {
t.Parallel()

tests := []struct {
in RuleError
want string
}{{
RuleError{Description: "some error"},
"some error",
}, {
RuleError{Description: "human-readable error"},
"human-readable error",
}}

for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("#%d: got: %s want: %s", i, result, test.want)
continue
}
}
}

// TestErrorKindIsAs ensures both ErrorKind and RuleError can be identified as
// being a specific error kind via errors.Is and unwrapped via errors.As.
func TestErrorKindIsAs(t *testing.T) {
tests := []struct {
name string
err error
target error
wantMatch bool
wantAs ErrorKind
}{{
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 != ErrUnexpectedDifficulty",
err: ErrHighHash,
target: ErrUnexpectedDifficulty,
wantMatch: false,
wantAs: ErrHighHash,
}, {
name: "RuleError.ErrHighHash != ErrUnexpectedDifficulty",
err: ruleError(ErrHighHash, ""),
target: ErrUnexpectedDifficulty,
wantMatch: false,
wantAs: ErrHighHash,
}, {
name: "ErrHighHash != RuleError.ErrUnexpectedDifficulty",
err: ErrHighHash,
target: ruleError(ErrUnexpectedDifficulty, ""),
wantMatch: false,
wantAs: ErrHighHash,
}, {
name: "RuleError.ErrHighHash != RuleError.ErrUnexpectedDifficulty",
err: ruleError(ErrHighHash, ""),
target: ruleError(ErrUnexpectedDifficulty, ""),
wantMatch: false,
wantAs: ErrHighHash,
}, {
name: "RuleError.ErrHighHash != io.EOF",
err: ruleError(ErrHighHash, ""),
target: io.EOF,
wantMatch: false,
wantAs: ErrHighHash,
}}

for _, test := range tests {
// 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
}
}
}
61 changes: 61 additions & 0 deletions internal/staging/primitives/pow.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package primitives

import (
"fmt"

"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/internal/staging/primitives/uint256"
)
Expand Down Expand Up @@ -198,3 +200,62 @@ func HashToUint256(hash *chainhash.Hash) uint256.Uint256 {
// treating them as a uint256.
return *new(uint256.Uint256).SetBytesLE((*[32]byte)(hash))
}

// checkProofOfWorkRange ensures the provided target difficulty is in min/max
// range per the provided proof-of-work limit.
func checkProofOfWorkRange(diffBits uint32, powLimit *uint256.Uint256) (uint256.Uint256, error) {
// The target difficulty must be larger than zero and not overflow and less
// than the maximum value that can be represented by a uint256.
target, isNegative, overflows := DiffBitsToUint256(diffBits)
if isNegative {
str := fmt.Sprintf("target difficulty bits %08x is a negative value",
diffBits)
return uint256.Uint256{}, ruleError(ErrUnexpectedDifficulty, str)
}
if overflows {
str := fmt.Sprintf("target difficulty bits %08x is higher than the "+
"max limit %x", diffBits, powLimit)
return uint256.Uint256{}, ruleError(ErrUnexpectedDifficulty, str)
}
if target.IsZero() {
str := "target difficulty is zero"
return uint256.Uint256{}, ruleError(ErrUnexpectedDifficulty, str)
}

// The target difficulty must not exceed the maximum allowed.
if target.Gt(powLimit) {
str := fmt.Sprintf("target difficulty of %x is higher than max of %x",
target, powLimit)
return uint256.Uint256{}, ruleError(ErrUnexpectedDifficulty, str)
}

return target, nil
}

// CheckProofOfWorkRange ensures the provided target difficulty represented by
// the given header bits is in min/max range per the provided proof-of-work
// limit.
func CheckProofOfWorkRange(diffBits uint32, powLimit *uint256.Uint256) error {
_, err := checkProofOfWorkRange(diffBits, powLimit)
return err
}

// CheckProofOfWork ensures the provided block hash is less than the target
// difficulty represented by given header bits and that said difficulty is in
// min/max range per the provided proof-of-work limit.
func CheckProofOfWork(blockHash *chainhash.Hash, diffBits uint32, powLimit *uint256.Uint256) error {
target, err := checkProofOfWorkRange(diffBits, powLimit)
if err != nil {
return err
}

// The block hash must be less than the target difficulty.
hashNum := HashToUint256(blockHash)
if hashNum.Gt(&target) {
str := fmt.Sprintf("block hash of %x is higher than expected max of %x",
hashNum, target)
return ruleError(ErrHighHash, str)
}

return nil
}
Loading

0 comments on commit 03bacc6

Please sign in to comment.