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

primitives: Add initial proof-of-work funcs. #2788

Merged
merged 9 commits into from
Dec 4, 2021
63 changes: 63 additions & 0 deletions internal/staging/primitives/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
primitives
==========

[![Build Status](https://github.com/decred/dcrd/workflows/Build%20and%20Test/badge.svg)](https://github.com/decred/dcrd/actions)
[![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org)
[![Doc](https://img.shields.io/badge/doc-reference-blue.svg)](https://pkg.go.dev/github.com/decred/dcrd/internal/staging/primitives)

## Package and Module Status

This package is currently a work in progress in the context of a larger refactor
and thus does not yet provide most of the things that are ultimately planned.
See https://github.com/decred/dcrd/issues/2786 for further details.

The intention is to create a containing `primitives` module that will be kept at
an experimental module version ("v0") until everything is stabilized to avoid
major module version churn in the mean time.

## Overview

This package ultimately aims to provide core data structures and functions for
working with several aspects of Decred consensus.

The provided functions fall into the following categories:

- Proof-of-work
- Converting to and from the target difficulty bits representation
- Calculating work values based on the target difficulty bits
- Checking that a block hash satisfies a target difficulty and that the target
difficulty is within a valid range

## Maintainer Note

Since the `primitives` module is heavily relied upon by consensus code, there
are some important aspects that must be kept in mind when working on this code:

- It must provide correctness guarantees and full test coverage
- Be extremely careful when making any changes to avoid breaking consensus
- This often means existing code can't be changed without adding an internal
flag to control behavior and introducing a new method with the new behavior
- Minimize the number of allocations as much as possible
- Opt for data structures that improve cache locality
- Keep a strong focus on providing efficient code
- Avoid external dependencies
- Consensus code requires much stronger guarantees than typical code and
consequently code that is not specifically designed with such rigid
constraints will almost always eventually break things in subtle ways over
time
- Do not change the API in a way that requires a new major semantic version
- This is not entirely prohibited, but major module version bumps have severe
ramifications on every consumer, and thus they should be an absolute last
resort
- Take care when adding new methods to avoid method signatures that would
require a major version bump due to a new major dependency version

## Installation and Updating

This package is internal and therefore is neither directly installed nor needs
to be manually updated.

## License

Package primitives is licensed under the [copyfree](http://copyfree.org) ISC
License.
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
}
}
}
Loading